Skip to content

Commit 7f2315c

Browse files
committed
feat: inline fragments
Signed-off-by: Dmitry Dygalo <[email protected]>
1 parent 37b7e8c commit 7f2315c

36 files changed

+1101
-158
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Added
66

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

910
### Changed
1011

README.md

+28
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,34 @@ fn main() -> css_inline::Result<()> {
7878
}
7979
```
8080

81+
Note that `css-inline` automatically adds missing `html` and `body` tags, so the output is a valid HTML document.
82+
83+
Alternatively, you can inline CSS into an HTML fragment, preserving the original structure:
84+
85+
```rust
86+
const FRAGMENT: &str = r#"<main>
87+
<h1>Hello</h1>
88+
<section>
89+
<p>who am i</p>
90+
</section>
91+
</main>"#;
92+
93+
const CSS: &str = r#"
94+
p {
95+
color: red;
96+
}
97+
98+
h1 {
99+
color: blue;
100+
}
101+
"#;
102+
103+
fn main() -> css_inline::Result<()> {
104+
let inlined = css_inline::inline_fragment(FRAGMENT, CSS)?;
105+
Ok(())
106+
}
107+
```
108+
81109
### Configuration
82110

83111
`css-inline` can be configured by using `CSSInliner::options()` that implements the Builder pattern:

bindings/c/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Added
66

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

910
### Changed
1011

bindings/c/README.md

+42
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,48 @@ int main(void) {
8686
8787
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.
8888
89+
Note that `css-inline` automatically adds missing `html` and `body` tags, so the output is a valid HTML document.
90+
91+
Alternatively, you can inline CSS into an HTML fragment, preserving the original structure:
92+
93+
```c
94+
#include "css_inline.h"
95+
#include <stdio.h>
96+
97+
#define OUTPUT_SIZE 1024
98+
99+
int main(void) {
100+
CssInlinerOptions options = css_inliner_default_options();
101+
const char fragment[] =
102+
"<main>"
103+
"<h1>Hello</h1>"
104+
"<section>"
105+
"<p>who am i</p>"
106+
"</section>"
107+
"</main>";
108+
109+
const char css[] =
110+
"p {"
111+
"color: red;"
112+
"}"
113+
"h1 {"
114+
"color: blue;"
115+
"}";
116+
char output[OUTPUT_SIZE];
117+
if (css_inline_fragment_to(&options, fragment, css, output, sizeof(output)) == CSS_RESULT_OK) {
118+
printf("Inlined CSS: %s\n", output);
119+
// HTML becomes this:
120+
// <main>
121+
// <h1 style="color: blue;">Hello</h1>
122+
// <section>
123+
// <p style="color: red;">who am i</p>
124+
// </section>
125+
// </main>
126+
}
127+
return 0;
128+
}
129+
```
130+
89131
### Configuration
90132

91133
You can change the inline behavior by modifying the `CssInlinerOptions` struct parameter that will be passed to `css_inline_to()`:

bindings/c/src/lib.rs

+67-19
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ pub enum CssResult {
3636
InvalidCacheSize,
3737
}
3838

39+
impl From<InlineError> for CssResult {
40+
fn from(value: InlineError) -> Self {
41+
match value {
42+
InlineError::IO(_) => CssResult::IoError,
43+
InlineError::Network { .. } => CssResult::RemoteStylesheetNotAvailable,
44+
InlineError::ParseError(_) => CssResult::InternalSelectorParseError,
45+
InlineError::MissingStyleSheet { .. } => CssResult::MissingStylesheet,
46+
}
47+
}
48+
}
49+
3950
// must be public because the impl From<&CssInlinerOptions> for InlineOptions would leak this type
4051
/// Error to convert to CssResult later
4152
/// cbindgen:ignore
@@ -84,6 +95,29 @@ pub struct CssInlinerOptions {
8495
pub preallocate_node_capacity: size_t,
8596
}
8697

98+
macro_rules! inliner {
99+
($options:expr) => {
100+
CSSInliner::new(
101+
match InlineOptions::try_from(match $options.as_ref() {
102+
Some(ptr) => ptr,
103+
None => return CssResult::NullOptions,
104+
}) {
105+
Ok(inline_options) => inline_options,
106+
Err(e) => return CssResult::from(e),
107+
},
108+
)
109+
};
110+
}
111+
112+
macro_rules! to_str {
113+
($input:expr) => {
114+
match CStr::from_ptr($input).to_str() {
115+
Ok(val) => val,
116+
Err(_) => return CssResult::InvalidInputString,
117+
}
118+
};
119+
}
120+
87121
/// @brief Inline CSS from @p input & write the result to @p output with @p options.
88122
/// @param options configuration for the inliner.
89123
/// @param input html to inline.
@@ -99,27 +133,41 @@ pub unsafe extern "C" fn css_inline_to(
99133
output: *mut c_char,
100134
output_size: size_t,
101135
) -> CssResult {
102-
let inliner = CSSInliner::new(
103-
match InlineOptions::try_from(match options.as_ref() {
104-
Some(ptr) => ptr,
105-
None => return CssResult::NullOptions,
106-
}) {
107-
Ok(inline_options) => inline_options,
108-
Err(e) => return CssResult::from(e),
109-
},
110-
);
111-
let html = match CStr::from_ptr(input).to_str() {
112-
Ok(val) => val,
113-
Err(_) => return CssResult::InvalidInputString,
114-
};
136+
let inliner = inliner!(options);
137+
let html = to_str!(input);
115138
let mut buffer = CBuffer::new(output, output_size);
116139
if let Err(e) = inliner.inline_to(html, &mut buffer) {
117-
return match e {
118-
InlineError::IO(_) => CssResult::IoError,
119-
InlineError::Network { .. } => CssResult::RemoteStylesheetNotAvailable,
120-
InlineError::ParseError(_) => CssResult::InternalSelectorParseError,
121-
InlineError::MissingStyleSheet { .. } => CssResult::MissingStylesheet,
122-
};
140+
return e.into();
141+
};
142+
// Null terminate the pointer
143+
let ptr: *mut c_char = buffer.buffer.add(buffer.pos);
144+
*ptr = 0;
145+
CssResult::Ok
146+
}
147+
148+
/// @brief Inline CSS @p fragment into @p input & write the result to @p output with @p options.
149+
/// @param options configuration for the inliner.
150+
/// @param input html to inline.
151+
/// @param css css to inline.
152+
/// @param output buffer to save the inlined CSS.
153+
/// @param output_size size of @p output in bytes.
154+
/// @return a CSS_RESULT enum variant regarding if the operation was a success or an error occurred
155+
#[allow(clippy::missing_safety_doc)]
156+
#[must_use]
157+
#[no_mangle]
158+
pub unsafe extern "C" fn css_inline_fragment_to(
159+
options: *const CssInlinerOptions,
160+
input: *const c_char,
161+
css: *const c_char,
162+
output: *mut c_char,
163+
output_size: size_t,
164+
) -> CssResult {
165+
let inliner = inliner!(options);
166+
let html = to_str!(input);
167+
let css = to_str!(css);
168+
let mut buffer = CBuffer::new(output, output_size);
169+
if let Err(e) = inliner.inline_fragment_to(html, css, &mut buffer) {
170+
return e.into();
123171
};
124172
// Null terminate the pointer
125173
let ptr: *mut c_char = buffer.buffer.add(buffer.pos);

bindings/c/tests/main.c

+20
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@
1616
"style=\"font-size: 2px;\"><strong style=\"text-decoration: " \
1717
"none;\">Yes!</strong></p><p class=\"footer\" style=\"font-size: " \
1818
"1px;\">Foot notes</p></body></html>"
19+
#define SAMPLE_FRAGMENT \
20+
"<main>" \
21+
"<h1>Hello</h1>" \
22+
"<section>" \
23+
"<p>who am i</p>" \
24+
"</section>" \
25+
"</main>"
26+
#define SAMPLE_FRAGMENT_STYLE \
27+
"p { color: red; } h1 { color: blue; }"
28+
#define SAMPLE_INLINED_FRAGMENT \
29+
"<main><h1 style=\"color: blue;\">Hello</h1><section><p style=\"color: red;\">who am i</p></section></main>"
1930

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

128+
static void test_inline_fragment(void) {
129+
CssInlinerOptions options = css_inliner_default_options();
130+
char output[MAX_SIZE];
131+
assert(css_inline_fragment_to(&options, SAMPLE_FRAGMENT, SAMPLE_FRAGMENT_STYLE, output, sizeof(output)) ==
132+
CSS_RESULT_OK);
133+
assert(strcmp(output, SAMPLE_INLINED_FRAGMENT) == 0);
134+
}
135+
117136
int main(void) {
118137
test_default_options();
119138
test_output_size_too_small();
@@ -122,5 +141,6 @@ int main(void) {
122141
test_file_scheme();
123142
test_cache_valid();
124143
test_cache_invalid();
144+
test_inline_fragment();
125145
return 0;
126146
}

bindings/javascript/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Added
66

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

910
## [0.13.2] - 2024-03-25
1011

bindings/javascript/README.md

+35
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,41 @@ var inlined = inline(
7676
// Do something with the inlined HTML, e.g. send an email
7777
```
7878

79+
Note that `css-inline` automatically adds missing `html` and `body` tags, so the output is a valid HTML document.
80+
81+
Alternatively, you can inline CSS into an HTML fragment, preserving the original structure:
82+
83+
```javascript
84+
import { inlineFragment } from "@css-inline/css-inline";
85+
86+
var inlined = inlineFragment(
87+
`
88+
<main>
89+
<h1>Hello</h1>
90+
<section>
91+
<p>who am i</p>
92+
</section>
93+
</main>
94+
`,
95+
`
96+
p {
97+
color: red;
98+
}
99+
100+
h1 {
101+
color: blue;
102+
}
103+
`
104+
);
105+
// HTML becomes this:
106+
// <main>
107+
// <h1 style="color: blue;">Hello</h1>
108+
// <section>
109+
// <p style="color: red;">who am i</p>
110+
// </section>
111+
// </main>
112+
```
113+
79114
### Configuration
80115

81116
- `inlineStyleTags`. Specifies whether to inline CSS from "style" tags. Default: `true`

bindings/javascript/__test__/index.spec.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import test from "ava";
22

3-
import { inline } from "../index.js";
3+
import { inline, inlineFragment } from "../index.js";
44

55
test("default inlining", (t) => {
66
t.is(
@@ -162,3 +162,25 @@ test("invalid cache size", (t) => {
162162
t.is(error.code, "GenericFailure");
163163
t.is(error.message, "Cache size must be an integer greater than zero");
164164
});
165+
166+
test("inline fragment", (t) => {
167+
t.is(
168+
inlineFragment(
169+
`<main>
170+
<h1>Hello</h1>
171+
<section>
172+
<p>who am i</p>
173+
</section>
174+
</main>
175+
`,
176+
`p {
177+
color: red;
178+
}
179+
180+
h1 {
181+
color: blue;
182+
}`,
183+
),
184+
`<main>\n<h1 style="color: blue;">Hello</h1>\n<section>\n<p style="color: red;">who am i</p>\n</section>\n</main>`,
185+
);
186+
});

bindings/javascript/__test__/wasm.spec.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { join } from "path";
33

44
import test from "ava";
55

6-
import { inline, initWasm } from "../wasm";
6+
import { inline, inlineFragment, initWasm } from "../wasm";
77

88
test.before(async () => {
99
await initWasm(fs.readFile(join(__dirname, "../wasm/index_bg.wasm")));
@@ -104,3 +104,25 @@ test("unsupported filesystem operation", (t) => {
104104
"Loading local files is not supported on WASM: tests/external.css",
105105
);
106106
});
107+
108+
test("inline fragment", (t) => {
109+
t.is(
110+
inlineFragment(
111+
`<main>
112+
<h1>Hello</h1>
113+
<section>
114+
<p>who am i</p>
115+
</section>
116+
</main>
117+
`,
118+
`p {
119+
color: red;
120+
}
121+
122+
h1 {
123+
color: blue;
124+
}`,
125+
),
126+
`<main>\n<h1 style="color: blue;">Hello</h1>\n<section>\n<p style="color: red;">who am i</p>\n</section>\n</main>`,
127+
);
128+
});

bindings/javascript/index.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@ export interface Options {
2929
}
3030
/** Inline CSS styles from <style> tags to matching elements in the HTML tree and return a string. */
3131
export function inline(html: string, options?: Options | undefined | null): string
32+
/** Inline CSS styles into an HTML fragment. */
33+
export function inlineFragment(html: string, css: string, options?: Options | undefined | null): string

bindings/javascript/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ if (!nativeBinding) {
252252
throw new Error(`Failed to load native binding`)
253253
}
254254

255-
const { inline } = nativeBinding
255+
const { inline, inlineFragment } = nativeBinding
256256

257257
module.exports.inline = inline
258+
module.exports.inlineFragment = inlineFragment

bindings/javascript/js-binding.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,7 @@ export interface Options {
3535
}
3636
/** Inline CSS styles from <style> tags to matching elements in the HTML tree and return a string. */
3737
export function inline(html: string, options?: Options | undefined | null): string
38+
/** Inline CSS styles into an HTML fragment. */
39+
export function inlineFragment(html: string, css: string, options?: Options | undefined | null): string
3840
/** Get the package version. */
3941
export function version(): string

0 commit comments

Comments
 (0)