Skip to content

Commit cc55213

Browse files
committed
feat: External stylesheet caching
Ref: #314 Signed-off-by: Dmitry Dygalo <[email protected]>
1 parent ee538f2 commit cc55213

33 files changed

+638
-60
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- External stylesheet caching. [#314](https://github.com/Stranger6667/css-inline/issues/314)
8+
59
### Changed
610

711
- Update `html5ever` to `0.27`.

README.md

+29
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ into:
3838
- Inlines CSS from `style` and `link` tags
3939
- Removes `style` and `link` tags
4040
- Resolves external stylesheets (including local files)
41+
- Optionally caches external stylesheets
4142
- Works on Linux, Windows, and macOS
4243
- Supports HTML5 & CSS3
4344
- Bindings for [Python](https://github.com/Stranger6667/css-inline/tree/master/bindings/python), [Ruby](https://github.com/Stranger6667/css-inline/tree/master/bindings/ruby), [JavaScript](https://github.com/Stranger6667/css-inline/tree/master/bindings/javascript), [C](https://github.com/Stranger6667/css-inline/tree/master/bindings/c), and a [WebAssembly](https://github.com/Stranger6667/css-inline/tree/master/bindings/javascript/wasm) module to run in browsers.
@@ -99,6 +100,7 @@ fn main() -> css_inline::Result<()> {
99100
- `keep_link_tags`. Specifies whether to keep "link" tags after inlining. Default: `false`
100101
- `base_url`. The base URL used to resolve relative URLs. If you'd like to load stylesheets from your filesystem, use the `file://` scheme. Default: `None`
101102
- `load_remote_stylesheets`. Specifies whether remote stylesheets should be loaded. Default: `true`
103+
- `cache`. Specifies cache for external stylesheets. Default: `None`
102104
- `extra_css`. Extra CSS to be inlined. Default: `None`
103105
- `preallocate_node_capacity`. **Advanced**. Preallocates capacity for HTML nodes during parsing. This can improve performance when you have an estimate of the number of nodes in your HTML document. Default: `32`
104106

@@ -177,6 +179,33 @@ fn main() -> css_inline::Result<()> {
177179
}
178180
```
179181

182+
You can also cache external stylesheets to avoid excessive network requests:
183+
184+
```rust
185+
use std::num::NonZeroUsize;
186+
187+
#[cfg(feature = "stylesheet-cache")]
188+
fn main() -> css_inline::Result<()> {
189+
let inliner = css_inline::CSSInliner::options()
190+
.cache(
191+
// This is an LRU cache
192+
css_inline::StylesheetCache::new(
193+
NonZeroUsize::new(5).expect("Invalid cache size")
194+
)
195+
)
196+
.build();
197+
Ok(())
198+
}
199+
200+
// This block is here for testing purposes
201+
#[cfg(not(feature = "stylesheet-cache"))]
202+
fn main() -> css_inline::Result<()> {
203+
Ok(())
204+
}
205+
```
206+
207+
Caching is disabled by default.
208+
180209
## Performance
181210

182211
`css-inline` typically inlines HTML emails within hundreds of microseconds, though results may vary with input complexity.

bindings/c/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- External stylesheet caching. [#314](https://github.com/Stranger6667/css-inline/issues/314)
8+
59
### Changed
610

711
- Update `html5ever` to `0.27`.

bindings/c/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ cbindgen = "0.26"
1818
path = "../../css-inline"
1919
version = "*"
2020
default-features = false
21-
features = ["http", "file"]
21+
features = ["http", "file", "stylesheet-cache"]

bindings/c/README.md

+17
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ into:
3636
- Inlines CSS from `style` and `link` tags
3737
- Removes `style` and `link` tags
3838
- Resolves external stylesheets (including local files)
39+
- Optionally caches external stylesheets
3940
- Works on Linux, Windows, and macOS
4041
- Supports HTML5 & CSS3
4142

@@ -112,6 +113,7 @@ Possible configurations:
112113
- `keep_link_tags`. Specifies whether to keep "link" tags after inlining. Default: `false`
113114
- `base_url`. The base URL used to resolve relative URLs. If you'd like to load stylesheets from your filesystem, use the `file://` scheme. Default: `NULL`
114115
- `load_remote_stylesheets`. Specifies whether remote stylesheets should be loaded. Default: `true`
116+
- `cache`. Specifies caching options for external stylesheets. Default: `NULL`
115117
- `extra_css`. Extra CSS to be inlined. Default: `NULL`
116118
- `preallocate_node_capacity`. **Advanced**. Preallocates capacity for HTML nodes during parsing. This can improve performance when you have an estimate of the number of nodes in your HTML document. Default: `32`
117119

@@ -156,6 +158,21 @@ This is useful if you want to keep `@media` queries for responsive emails in sep
156158

157159
Such tags will be kept in the resulting HTML even if the `keep_style_tags` option is set to `false`.
158160

161+
You can also cache external stylesheets to avoid excessive network requests:
162+
163+
```c
164+
int main(void) {
165+
// Configure cache
166+
StylesheetCache cache = css_inliner_stylesheet_cache(8);
167+
CssInlinerOptions options = css_inliner_default_options();
168+
options.cache = &cache;
169+
// ... Inline CSS
170+
return 0;
171+
}
172+
```
173+
174+
Caching is disabled by default.
175+
159176
## License
160177
161178
This project is licensed under the terms of the [MIT license](https://opensource.org/licenses/MIT).

bindings/c/src/lib.rs

+46-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
use css_inline::{CSSInliner, DefaultStylesheetResolver, InlineError, InlineOptions, Url};
22
use libc::{c_char, size_t};
3-
use std::{borrow::Cow, cmp, ffi::CStr, io::Write, ptr, sync::Arc};
3+
use std::{
4+
borrow::Cow,
5+
cmp,
6+
ffi::CStr,
7+
io::Write,
8+
num::NonZeroUsize,
9+
ptr,
10+
sync::{Arc, Mutex},
11+
};
412

513
/// Result of CSS inlining operations
614
#[repr(C)]
@@ -16,22 +24,42 @@ pub enum CssResult {
1624
IoError,
1725
/// Error while parsing the CSS.
1826
InternalSelectorParseError,
19-
/// options pointer is null.
27+
/// Options pointer is null.
2028
NullOptions,
2129
/// Invalid base_url parameter.
2230
InvalidUrl,
2331
/// Invalid extra_css parameter.
2432
InvalidExtraCss,
25-
/// input string not in UTF-8.
33+
/// Input string not in UTF-8.
2634
InvalidInputString,
35+
/// Invalid cache size.
36+
InvalidCacheSize,
2737
}
2838

2939
// must be public because the impl From<&CssInlinerOptions> for InlineOptions would leak this type
3040
/// Error to convert to CssResult later
3141
/// cbindgen:ignore
3242
pub enum InlineOptionsError {
43+
/// Invalid base_url parameter.
3344
InvalidUrl,
45+
/// Invalid extra_css parameter.
3446
InvalidExtraCss,
47+
/// Invalid cache size.
48+
InvalidCacheSize,
49+
}
50+
51+
/// An LRU Cache for external stylesheets.
52+
#[repr(C)]
53+
pub struct StylesheetCache {
54+
/// Cache size.
55+
size: size_t,
56+
}
57+
58+
/// @brief Creates an instance of StylesheetCache.
59+
/// @return a StylesheetCache struct
60+
#[no_mangle]
61+
pub extern "C" fn css_inliner_stylesheet_cache(size: size_t) -> StylesheetCache {
62+
StylesheetCache { size }
3563
}
3664

3765
/// Configuration options for CSS inlining process.
@@ -45,6 +73,8 @@ pub struct CssInlinerOptions {
4573
pub keep_link_tags: bool,
4674
/// Whether remote stylesheets should be loaded or not.
4775
pub load_remote_stylesheets: bool,
76+
/// Cache for external stylesheets.
77+
pub cache: *const StylesheetCache,
4878
/// Used for loading external stylesheets via relative URLs.
4979
pub base_url: *const c_char,
5080
/// Additional CSS to inline.
@@ -69,7 +99,7 @@ pub unsafe extern "C" fn css_inline_to(
6999
output: *mut c_char,
70100
output_size: size_t,
71101
) -> CssResult {
72-
let options = CSSInliner::new(
102+
let inliner = CSSInliner::new(
73103
match InlineOptions::try_from(match options.as_ref() {
74104
Some(ptr) => ptr,
75105
None => return CssResult::NullOptions,
@@ -83,7 +113,7 @@ pub unsafe extern "C" fn css_inline_to(
83113
Err(_) => return CssResult::InvalidInputString,
84114
};
85115
let mut buffer = CBuffer::new(output, output_size);
86-
if let Err(e) = options.inline_to(html, &mut buffer) {
116+
if let Err(e) = inliner.inline_to(html, &mut buffer) {
87117
return match e {
88118
InlineError::IO(_) => CssResult::IoError,
89119
InlineError::Network { .. } => CssResult::RemoteStylesheetNotAvailable,
@@ -107,6 +137,7 @@ pub extern "C" fn css_inliner_default_options() -> CssInlinerOptions {
107137
keep_link_tags: false,
108138
base_url: ptr::null(),
109139
load_remote_stylesheets: true,
140+
cache: std::ptr::null(),
110141
extra_css: ptr::null(),
111142
preallocate_node_capacity: 32,
112143
}
@@ -151,6 +182,15 @@ impl TryFrom<&CssInlinerOptions> for InlineOptions<'_> {
151182
None => None,
152183
},
153184
load_remote_stylesheets: value.load_remote_stylesheets,
185+
cache: {
186+
if value.cache.is_null() {
187+
None
188+
} else if let Some(size) = NonZeroUsize::new(unsafe { (*value.cache).size }) {
189+
Some(Mutex::new(css_inline::StylesheetCache::new(size)))
190+
} else {
191+
return Err(InlineOptionsError::InvalidCacheSize);
192+
}
193+
},
154194
extra_css: extra_css.map(Cow::Borrowed),
155195
preallocate_node_capacity: value.preallocate_node_capacity,
156196
resolver: Arc::new(DefaultStylesheetResolver),
@@ -163,6 +203,7 @@ impl From<InlineOptionsError> for CssResult {
163203
match value {
164204
InlineOptionsError::InvalidUrl => CssResult::InvalidUrl,
165205
InlineOptionsError::InvalidExtraCss => CssResult::InvalidExtraCss,
206+
InlineOptionsError::InvalidCacheSize => CssResult::InvalidCacheSize,
166207
}
167208
}
168209
}

bindings/c/tests/main.c

+31
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,42 @@ static void test_file_scheme(void) {
8585
CSS_RESULT_OK);
8686
}
8787

88+
static void test_cache_valid(void) {
89+
char html[MAX_SIZE];
90+
assert(make_html(html, SAMPLE_STYLE, SAMPLE_BODY));
91+
92+
StylesheetCache cache = css_inliner_stylesheet_cache(8);
93+
CssInlinerOptions options = css_inliner_default_options();
94+
options.cache = &cache;
95+
96+
char first_output[MAX_SIZE];
97+
char second_output[MAX_SIZE];
98+
99+
assert(css_inline_to(&options, html, first_output, sizeof(first_output)) == CSS_RESULT_OK);
100+
assert(strcmp(first_output, SAMPLE_INLINED) == 0);
101+
}
102+
103+
static void test_cache_invalid(void) {
104+
char html[MAX_SIZE];
105+
assert(make_html(html, SAMPLE_STYLE, SAMPLE_BODY));
106+
107+
StylesheetCache cache = css_inliner_stylesheet_cache(0);
108+
CssInlinerOptions options = css_inliner_default_options();
109+
options.cache = &cache;
110+
111+
char first_output[MAX_SIZE];
112+
char second_output[MAX_SIZE];
113+
114+
assert(css_inline_to(&options, html, first_output, sizeof(first_output)) == CSS_RESULT_INVALID_CACHE_SIZE);
115+
}
116+
88117
int main(void) {
89118
test_default_options();
90119
test_output_size_too_small();
91120
test_missing_stylesheet();
92121
test_invalid_base_url();
93122
test_file_scheme();
123+
test_cache_valid();
124+
test_cache_invalid();
94125
return 0;
95126
}

bindings/javascript/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- External stylesheet caching. [#314](https://github.com/Stranger6667/css-inline/issues/314)
8+
59
## [0.13.2] - 2024-03-25
610

711
### Changed

bindings/javascript/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ serde = { version = "1", features = ["derive"], default-features = false }
3535
path = "../../css-inline"
3636
version = "*"
3737
default-features = false
38-
features = ["http", "file"]
38+
features = ["http", "file", "stylesheet-cache"]
3939

4040
[target.'cfg(target_arch = "wasm32")'.dependencies.css-inline]
4141
path = "../../css-inline"

bindings/javascript/README.md

+26-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ into:
3737
- Inlines CSS from `style` and `link` tags
3838
- Removes `style` and `link` tags
3939
- Resolves external stylesheets (including local files)
40+
- Optionally caches external stylesheets
4041
- Works on Linux, Windows, and macOS
4142
- Supports HTML5 & CSS3
4243
- Tested on Node.js 18 & 20.
@@ -82,6 +83,7 @@ var inlined = inline(
8283
- `keepLinkTags`. Specifies whether to keep "link" tags after inlining. Default: `false`
8384
- `baseUrl`. The base URL used to resolve relative URLs. If you'd like to load stylesheets from your filesystem, use the `file://` scheme. Default: `null`
8485
- `loadRemoteStylesheets`. Specifies whether remote stylesheets should be loaded. Default: `true`
86+
- `cache`. Specifies caching options for external stylesheets (for example, `{size: 5}`). Default: `null`
8587
- `extraCss`. Extra CSS to be inlined. Default: `null`
8688
- `preallocateNodeCapacity`. **Advanced**. Preallocates capacity for HTML nodes during parsing. This can improve performance when you have an estimate of the number of nodes in your HTML document. Default: `32`
8789

@@ -125,6 +127,29 @@ This is useful if you want to keep `@media` queries for responsive emails in sep
125127

126128
Such tags will be kept in the resulting HTML even if the `keep_style_tags` option is set to `false`.
127129

130+
You can also cache external stylesheets to avoid excessive network requests:
131+
132+
```typescript
133+
import { inline } from "@css-inline/css-inline";
134+
135+
var inlined = inline(
136+
`
137+
<html>
138+
<head>
139+
<link href="http://127.0.0.1:1234/external.css" rel="stylesheet">
140+
<style>h1 { color:red }</style>
141+
</head>
142+
<body>
143+
<h1>Test</h1>
144+
</body>
145+
</html>
146+
`,
147+
{ cache: { size: 5 } },
148+
);
149+
```
150+
151+
Caching is disabled by default.
152+
128153
## WebAssembly
129154

130155
`css-inline` also ships a WebAssembly module built with `wasm-bindgen` to run in browsers.
@@ -148,7 +173,7 @@ Such tags will be kept in the resulting HTML even if the `keep_style_tags` optio
148173
</script>
149174
```
150175

151-
**NOTE**: WASM module currently lacks support for fetching stylesheets from network or filesystem.
176+
**NOTE**: WASM module currently lacks support for fetching stylesheets from network or filesystem and caching.
152177

153178
## Performance
154179

bindings/javascript/__test__/index.spec.ts

+30
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,33 @@ h2 { color: red; }
132132
inlinedHtml,
133133
);
134134
});
135+
136+
test("cache external stylesheets", (t) => {
137+
t.is(
138+
inline(
139+
`<html>
140+
<head>
141+
<link href="http://127.0.0.1:1234/external.css" rel="stylesheet">
142+
<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml">
143+
<style>
144+
h2 { color: red; }
145+
</style>
146+
</head>
147+
<body>
148+
<h1>Big Text</h1>
149+
<h2>Smaller Text</h2>
150+
</body>
151+
</html>`,
152+
{ cache: { size: 5 } },
153+
),
154+
inlinedHtml,
155+
);
156+
});
157+
158+
test("invalid cache size", (t) => {
159+
const error = t.throws(() => {
160+
inline("", { cache: { size: 0 } });
161+
});
162+
t.is(error.code, "GenericFailure");
163+
t.is(error.message, "Cache size must be an integer greater than zero");
164+
});

0 commit comments

Comments
 (0)