Skip to content

Commit 2609a6e

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

28 files changed

+503
-54
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

+22
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,26 @@ 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+
fn main() -> css_inline::Result<()> {
188+
let inliner = css_inline::CSSInliner::options()
189+
.cache(
190+
// This is an LRU cache
191+
css_inline::StylesheetCache::new(
192+
NonZeroUsize::new(5).expect("Invalid cache size")
193+
)
194+
)
195+
.build();
196+
Ok(())
197+
}
198+
```
199+
200+
Caching is disabled by default.
201+
180202
## Performance
181203

182204
`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/README.md

+2
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` - TODO
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

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+
});

bindings/javascript/js-binding.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
/* auto-generated by NAPI-RS */
55

6+
export interface StylesheetCache {
7+
/** Cache size. */
8+
size: number
9+
}
610
export interface Options {
711
/**
812
* Whether to inline CSS from "style" tags.
@@ -19,6 +23,8 @@ export interface Options {
1923
baseUrl?: string
2024
/** Whether remote stylesheets should be loaded or not. */
2125
loadRemoteStylesheets?: boolean
26+
/** An LRU Cache for external stylesheets. */
27+
cache?: StylesheetCache
2228
/** Additional CSS to inline. */
2329
extraCss?: string
2430
/**

bindings/javascript/src/options.rs

+28
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ fn parse_url(url: Option<String>) -> std::result::Result<Option<css_inline::Url>
1313
})
1414
}
1515

16+
#[cfg(not(target_arch = "wasm32"))]
17+
#[cfg_attr(not(target_arch = "wasm32"), napi(object))]
18+
#[allow(clippy::struct_excessive_bools)]
19+
#[derive(Debug, Default)]
20+
pub struct StylesheetCache {
21+
/// Cache size.
22+
pub size: u32,
23+
}
24+
1625
#[cfg_attr(
1726
target_arch = "wasm32",
1827
derive(serde::Deserialize),
@@ -35,6 +44,9 @@ pub struct Options {
3544
pub base_url: Option<String>,
3645
/// Whether remote stylesheets should be loaded or not.
3746
pub load_remote_stylesheets: Option<bool>,
47+
#[cfg(not(target_arch = "wasm32"))]
48+
/// An LRU Cache for external stylesheets.
49+
pub cache: Option<StylesheetCache>,
3850
/// Additional CSS to inline.
3951
pub extra_css: Option<String>,
4052
/// Pre-allocate capacity for HTML nodes during parsing.
@@ -53,6 +65,22 @@ impl TryFrom<Options> for css_inline::InlineOptions<'_> {
5365
base_url: parse_url(value.base_url)?,
5466
load_remote_stylesheets: value.load_remote_stylesheets.unwrap_or(true),
5567
extra_css: value.extra_css.map(Cow::Owned),
68+
#[cfg(not(target_arch = "wasm32"))]
69+
cache: {
70+
if let Some(cache) = value.cache {
71+
let size =
72+
std::num::NonZeroUsize::new(cache.size as usize).ok_or_else(|| {
73+
let reason =
74+
"Cache size must be an integer greater than zero".to_string();
75+
napi::Error::from_reason(reason)
76+
})?;
77+
Some(std::sync::Mutex::new(css_inline::StylesheetCache::new(
78+
size,
79+
)))
80+
} else {
81+
None
82+
}
83+
},
5684
preallocate_node_capacity: if let Some(capacity) = value.preallocate_node_capacity {
5785
usize::try_from(capacity).map_err(|_| {
5886
let reason = "Invalid capacity".to_string();

bindings/javascript/wasm/index.js

+15-15
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,6 @@ heap.push(void 0, null, true, false);
3333
function getObject(idx) {
3434
return heap[idx];
3535
}
36-
var heap_next = heap.length;
37-
function dropObject(idx) {
38-
if (idx < 132)
39-
return;
40-
heap[idx] = heap_next;
41-
heap_next = idx;
42-
}
43-
function takeObject(idx) {
44-
const ret = getObject(idx);
45-
dropObject(idx);
46-
return ret;
47-
}
4836
var WASM_VECTOR_LEN = 0;
4937
var cachedUint8Memory0 = null;
5038
function getUint8Memory0() {
@@ -116,6 +104,7 @@ function getStringFromWasm0(ptr, len) {
116104
ptr = ptr >>> 0;
117105
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
118106
}
107+
var heap_next = heap.length;
119108
function addHeapObject(obj) {
120109
if (heap_next === heap.length)
121110
heap.push(heap.length + 1);
@@ -124,6 +113,17 @@ function addHeapObject(obj) {
124113
heap[idx] = obj;
125114
return idx;
126115
}
116+
function dropObject(idx) {
117+
if (idx < 132)
118+
return;
119+
heap[idx] = heap_next;
120+
heap_next = idx;
121+
}
122+
function takeObject(idx) {
123+
const ret = getObject(idx);
124+
dropObject(idx);
125+
return ret;
126+
}
127127
var cachedFloat64Memory0 = null;
128128
function getFloat64Memory0() {
129129
if (cachedFloat64Memory0 === null || cachedFloat64Memory0.byteLength === 0) {
@@ -261,9 +261,6 @@ function __wbg_get_imports() {
261261
const ret = getObject(arg0) === void 0;
262262
return ret;
263263
};
264-
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
265-
takeObject(arg0);
266-
};
267264
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
268265
const obj = getObject(arg1);
269266
const ret = typeof obj === "string" ? obj : void 0;
@@ -306,6 +303,9 @@ function __wbg_get_imports() {
306303
const ret = +getObject(arg0);
307304
return ret;
308305
};
306+
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
307+
takeObject(arg0);
308+
};
309309
imports.wbg.__wbg_length_1d25fa9e4ac21ce7 = function(arg0) {
310310
const ret = getObject(arg0).length;
311311
return ret;

0 commit comments

Comments
 (0)