Skip to content

Commit

Permalink
feat: inline fragments
Browse files Browse the repository at this point in the history
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
  • Loading branch information
Stranger6667 committed Mar 31, 2024
1 parent 37b7e8c commit 7f2315c
Show file tree
Hide file tree
Showing 36 changed files with 1,101 additions and 158 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added

- External stylesheet caching. [#314](https://github.com/Stranger6667/css-inline/issues/314)
- Inlining to HTML fragments. [#335](https://github.com/Stranger6667/css-inline/issues/335)

### Changed

Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,34 @@ fn main() -> css_inline::Result<()> {
}
```

Note that `css-inline` automatically adds missing `html` and `body` tags, so the output is a valid HTML document.

Alternatively, you can inline CSS into an HTML fragment, preserving the original structure:

```rust
const FRAGMENT: &str = r#"<main>
<h1>Hello</h1>
<section>
<p>who am i</p>
</section>
</main>"#;

const CSS: &str = r#"
p {
color: red;
}
h1 {
color: blue;
}
"#;

fn main() -> css_inline::Result<()> {
let inlined = css_inline::inline_fragment(FRAGMENT, CSS)?;
Ok(())
}
```

### Configuration

`css-inline` can be configured by using `CSSInliner::options()` that implements the Builder pattern:
Expand Down
1 change: 1 addition & 0 deletions bindings/c/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added

- External stylesheet caching. [#314](https://github.com/Stranger6667/css-inline/issues/314)
- Inlining to HTML fragments. [#335](https://github.com/Stranger6667/css-inline/issues/335)

### Changed

Expand Down
42 changes: 42 additions & 0 deletions bindings/c/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,48 @@ int main(void) {
The inline function, `css_inline_to()`, doesn't allocate, so you must provide an array big enough to fit the result. If the size is not sufficient, the enum `CSS_RESULT_IO_ERROR` will be returned.
Note that `css-inline` automatically adds missing `html` and `body` tags, so the output is a valid HTML document.
Alternatively, you can inline CSS into an HTML fragment, preserving the original structure:
```c
#include "css_inline.h"
#include <stdio.h>
#define OUTPUT_SIZE 1024
int main(void) {
CssInlinerOptions options = css_inliner_default_options();
const char fragment[] =
"<main>"
"<h1>Hello</h1>"
"<section>"
"<p>who am i</p>"
"</section>"
"</main>";
const char css[] =
"p {"
"color: red;"
"}"
"h1 {"
"color: blue;"
"}";
char output[OUTPUT_SIZE];
if (css_inline_fragment_to(&options, fragment, css, output, sizeof(output)) == CSS_RESULT_OK) {
printf("Inlined CSS: %s\n", output);
// HTML becomes this:
// <main>
// <h1 style="color: blue;">Hello</h1>
// <section>
// <p style="color: red;">who am i</p>
// </section>
// </main>
}
return 0;
}
```

### Configuration

You can change the inline behavior by modifying the `CssInlinerOptions` struct parameter that will be passed to `css_inline_to()`:
Expand Down
86 changes: 67 additions & 19 deletions bindings/c/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ pub enum CssResult {
InvalidCacheSize,
}

impl From<InlineError> for CssResult {
fn from(value: InlineError) -> Self {
match value {
InlineError::IO(_) => CssResult::IoError,
InlineError::Network { .. } => CssResult::RemoteStylesheetNotAvailable,
InlineError::ParseError(_) => CssResult::InternalSelectorParseError,
InlineError::MissingStyleSheet { .. } => CssResult::MissingStylesheet,
}
}
}

// must be public because the impl From<&CssInlinerOptions> for InlineOptions would leak this type
/// Error to convert to CssResult later
/// cbindgen:ignore
Expand Down Expand Up @@ -84,6 +95,29 @@ pub struct CssInlinerOptions {
pub preallocate_node_capacity: size_t,
}

macro_rules! inliner {
($options:expr) => {
CSSInliner::new(
match InlineOptions::try_from(match $options.as_ref() {
Some(ptr) => ptr,
None => return CssResult::NullOptions,
}) {
Ok(inline_options) => inline_options,
Err(e) => return CssResult::from(e),
},
)
};
}

macro_rules! to_str {
($input:expr) => {
match CStr::from_ptr($input).to_str() {
Ok(val) => val,
Err(_) => return CssResult::InvalidInputString,
}
};
}

/// @brief Inline CSS from @p input & write the result to @p output with @p options.
/// @param options configuration for the inliner.
/// @param input html to inline.
Expand All @@ -99,27 +133,41 @@ pub unsafe extern "C" fn css_inline_to(
output: *mut c_char,
output_size: size_t,
) -> CssResult {
let inliner = CSSInliner::new(
match InlineOptions::try_from(match options.as_ref() {
Some(ptr) => ptr,
None => return CssResult::NullOptions,
}) {
Ok(inline_options) => inline_options,
Err(e) => return CssResult::from(e),
},
);
let html = match CStr::from_ptr(input).to_str() {
Ok(val) => val,
Err(_) => return CssResult::InvalidInputString,
};
let inliner = inliner!(options);
let html = to_str!(input);
let mut buffer = CBuffer::new(output, output_size);
if let Err(e) = inliner.inline_to(html, &mut buffer) {
return match e {
InlineError::IO(_) => CssResult::IoError,
InlineError::Network { .. } => CssResult::RemoteStylesheetNotAvailable,
InlineError::ParseError(_) => CssResult::InternalSelectorParseError,
InlineError::MissingStyleSheet { .. } => CssResult::MissingStylesheet,
};
return e.into();
};
// Null terminate the pointer
let ptr: *mut c_char = buffer.buffer.add(buffer.pos);
*ptr = 0;
CssResult::Ok
}

/// @brief Inline CSS @p fragment into @p input & write the result to @p output with @p options.
/// @param options configuration for the inliner.
/// @param input html to inline.
/// @param css css to inline.
/// @param output buffer to save the inlined CSS.
/// @param output_size size of @p output in bytes.
/// @return a CSS_RESULT enum variant regarding if the operation was a success or an error occurred
#[allow(clippy::missing_safety_doc)]
#[must_use]
#[no_mangle]
pub unsafe extern "C" fn css_inline_fragment_to(
options: *const CssInlinerOptions,
input: *const c_char,
css: *const c_char,
output: *mut c_char,
output_size: size_t,
) -> CssResult {
let inliner = inliner!(options);
let html = to_str!(input);
let css = to_str!(css);
let mut buffer = CBuffer::new(output, output_size);
if let Err(e) = inliner.inline_fragment_to(html, css, &mut buffer) {
return e.into();
};
// Null terminate the pointer
let ptr: *mut c_char = buffer.buffer.add(buffer.pos);
Expand Down
20 changes: 20 additions & 0 deletions bindings/c/tests/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@
"style=\"font-size: 2px;\"><strong style=\"text-decoration: " \
"none;\">Yes!</strong></p><p class=\"footer\" style=\"font-size: " \
"1px;\">Foot notes</p></body></html>"
#define SAMPLE_FRAGMENT \
"<main>" \
"<h1>Hello</h1>" \
"<section>" \
"<p>who am i</p>" \
"</section>" \
"</main>"
#define SAMPLE_FRAGMENT_STYLE \
"p { color: red; } h1 { color: blue; }"
#define SAMPLE_INLINED_FRAGMENT \
"<main><h1 style=\"color: blue;\">Hello</h1><section><p style=\"color: red;\">who am i</p></section></main>"

/**
* @brief Makes a html-like string in @p html given a @p style and a @p body.
Expand Down Expand Up @@ -114,6 +125,14 @@ static void test_cache_invalid(void) {
assert(css_inline_to(&options, html, first_output, sizeof(first_output)) == CSS_RESULT_INVALID_CACHE_SIZE);
}

static void test_inline_fragment(void) {
CssInlinerOptions options = css_inliner_default_options();
char output[MAX_SIZE];
assert(css_inline_fragment_to(&options, SAMPLE_FRAGMENT, SAMPLE_FRAGMENT_STYLE, output, sizeof(output)) ==
CSS_RESULT_OK);
assert(strcmp(output, SAMPLE_INLINED_FRAGMENT) == 0);
}

int main(void) {
test_default_options();
test_output_size_too_small();
Expand All @@ -122,5 +141,6 @@ int main(void) {
test_file_scheme();
test_cache_valid();
test_cache_invalid();
test_inline_fragment();
return 0;
}
1 change: 1 addition & 0 deletions bindings/javascript/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added

- External stylesheet caching. [#314](https://github.com/Stranger6667/css-inline/issues/314)
- Inlining to HTML fragments. [#335](https://github.com/Stranger6667/css-inline/issues/335)

## [0.13.2] - 2024-03-25

Expand Down
35 changes: 35 additions & 0 deletions bindings/javascript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,41 @@ var inlined = inline(
// Do something with the inlined HTML, e.g. send an email
```

Note that `css-inline` automatically adds missing `html` and `body` tags, so the output is a valid HTML document.

Alternatively, you can inline CSS into an HTML fragment, preserving the original structure:

```javascript
import { inlineFragment } from "@css-inline/css-inline";

var inlined = inlineFragment(
`
<main>
<h1>Hello</h1>
<section>
<p>who am i</p>
</section>
</main>
`,
`
p {
color: red;
}
h1 {
color: blue;
}
`
);
// HTML becomes this:
// <main>
// <h1 style="color: blue;">Hello</h1>
// <section>
// <p style="color: red;">who am i</p>
// </section>
// </main>
```

### Configuration

- `inlineStyleTags`. Specifies whether to inline CSS from "style" tags. Default: `true`
Expand Down
24 changes: 23 additions & 1 deletion bindings/javascript/__test__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test from "ava";

import { inline } from "../index.js";
import { inline, inlineFragment } from "../index.js";

test("default inlining", (t) => {
t.is(
Expand Down Expand Up @@ -162,3 +162,25 @@ test("invalid cache size", (t) => {
t.is(error.code, "GenericFailure");
t.is(error.message, "Cache size must be an integer greater than zero");
});

test("inline fragment", (t) => {
t.is(
inlineFragment(
`<main>
<h1>Hello</h1>
<section>
<p>who am i</p>
</section>
</main>
`,
`p {
color: red;
}
h1 {
color: blue;
}`,
),
`<main>\n<h1 style="color: blue;">Hello</h1>\n<section>\n<p style="color: red;">who am i</p>\n</section>\n</main>`,
);
});
24 changes: 23 additions & 1 deletion bindings/javascript/__test__/wasm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { join } from "path";

import test from "ava";

import { inline, initWasm } from "../wasm";
import { inline, inlineFragment, initWasm } from "../wasm";

test.before(async () => {
await initWasm(fs.readFile(join(__dirname, "../wasm/index_bg.wasm")));
Expand Down Expand Up @@ -104,3 +104,25 @@ test("unsupported filesystem operation", (t) => {
"Loading local files is not supported on WASM: tests/external.css",
);
});

test("inline fragment", (t) => {
t.is(
inlineFragment(
`<main>
<h1>Hello</h1>
<section>
<p>who am i</p>
</section>
</main>
`,
`p {
color: red;
}
h1 {
color: blue;
}`,
),
`<main>\n<h1 style="color: blue;">Hello</h1>\n<section>\n<p style="color: red;">who am i</p>\n</section>\n</main>`,
);
});
2 changes: 2 additions & 0 deletions bindings/javascript/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ export interface Options {
}
/** Inline CSS styles from <style> tags to matching elements in the HTML tree and return a string. */
export function inline(html: string, options?: Options | undefined | null): string
/** Inline CSS styles into an HTML fragment. */
export function inlineFragment(html: string, css: string, options?: Options | undefined | null): string
3 changes: 2 additions & 1 deletion bindings/javascript/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}

const { inline } = nativeBinding
const { inline, inlineFragment } = nativeBinding

module.exports.inline = inline
module.exports.inlineFragment = inlineFragment
2 changes: 2 additions & 0 deletions bindings/javascript/js-binding.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,7 @@ export interface Options {
}
/** Inline CSS styles from <style> tags to matching elements in the HTML tree and return a string. */
export function inline(html: string, options?: Options | undefined | null): string
/** Inline CSS styles into an HTML fragment. */
export function inlineFragment(html: string, css: string, options?: Options | undefined | null): string
/** Get the package version. */
export function version(): string

0 comments on commit 7f2315c

Please sign in to comment.