Skip to content

Commit

Permalink
feat: External stylesheet caching
Browse files Browse the repository at this point in the history
Ref: #314
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
  • Loading branch information
Stranger6667 committed Mar 29, 2024
1 parent ee538f2 commit cc55213
Show file tree
Hide file tree
Showing 33 changed files with 638 additions and 60 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

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

### Changed

- Update `html5ever` to `0.27`.
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ into:
- Inlines CSS from `style` and `link` tags
- Removes `style` and `link` tags
- Resolves external stylesheets (including local files)
- Optionally caches external stylesheets
- Works on Linux, Windows, and macOS
- Supports HTML5 & CSS3
- 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.
Expand Down Expand Up @@ -99,6 +100,7 @@ fn main() -> css_inline::Result<()> {
- `keep_link_tags`. Specifies whether to keep "link" tags after inlining. Default: `false`
- `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`
- `load_remote_stylesheets`. Specifies whether remote stylesheets should be loaded. Default: `true`
- `cache`. Specifies cache for external stylesheets. Default: `None`
- `extra_css`. Extra CSS to be inlined. Default: `None`
- `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`

Expand Down Expand Up @@ -177,6 +179,33 @@ fn main() -> css_inline::Result<()> {
}
```

You can also cache external stylesheets to avoid excessive network requests:

```rust
use std::num::NonZeroUsize;

#[cfg(feature = "stylesheet-cache")]
fn main() -> css_inline::Result<()> {
let inliner = css_inline::CSSInliner::options()
.cache(
// This is an LRU cache
css_inline::StylesheetCache::new(
NonZeroUsize::new(5).expect("Invalid cache size")
)
)
.build();
Ok(())
}

// This block is here for testing purposes
#[cfg(not(feature = "stylesheet-cache"))]
fn main() -> css_inline::Result<()> {
Ok(())
}
```

Caching is disabled by default.

## Performance

`css-inline` typically inlines HTML emails within hundreds of microseconds, though results may vary with input complexity.
Expand Down
4 changes: 4 additions & 0 deletions bindings/c/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

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

### Changed

- Update `html5ever` to `0.27`.
Expand Down
2 changes: 1 addition & 1 deletion bindings/c/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ cbindgen = "0.26"
path = "../../css-inline"
version = "*"
default-features = false
features = ["http", "file"]
features = ["http", "file", "stylesheet-cache"]
17 changes: 17 additions & 0 deletions bindings/c/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ into:
- Inlines CSS from `style` and `link` tags
- Removes `style` and `link` tags
- Resolves external stylesheets (including local files)
- Optionally caches external stylesheets
- Works on Linux, Windows, and macOS
- Supports HTML5 & CSS3

Expand Down Expand Up @@ -112,6 +113,7 @@ Possible configurations:
- `keep_link_tags`. Specifies whether to keep "link" tags after inlining. Default: `false`
- `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`
- `load_remote_stylesheets`. Specifies whether remote stylesheets should be loaded. Default: `true`
- `cache`. Specifies caching options for external stylesheets. Default: `NULL`
- `extra_css`. Extra CSS to be inlined. Default: `NULL`
- `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`

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

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

You can also cache external stylesheets to avoid excessive network requests:

```c
int main(void) {
// Configure cache
StylesheetCache cache = css_inliner_stylesheet_cache(8);
CssInlinerOptions options = css_inliner_default_options();
options.cache = &cache;
// ... Inline CSS
return 0;
}
```
Caching is disabled by default.
## License
This project is licensed under the terms of the [MIT license](https://opensource.org/licenses/MIT).
51 changes: 46 additions & 5 deletions bindings/c/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
use css_inline::{CSSInliner, DefaultStylesheetResolver, InlineError, InlineOptions, Url};
use libc::{c_char, size_t};
use std::{borrow::Cow, cmp, ffi::CStr, io::Write, ptr, sync::Arc};
use std::{
borrow::Cow,
cmp,
ffi::CStr,
io::Write,
num::NonZeroUsize,
ptr,
sync::{Arc, Mutex},
};

/// Result of CSS inlining operations
#[repr(C)]
Expand All @@ -16,22 +24,42 @@ pub enum CssResult {
IoError,
/// Error while parsing the CSS.
InternalSelectorParseError,
/// options pointer is null.
/// Options pointer is null.
NullOptions,
/// Invalid base_url parameter.
InvalidUrl,
/// Invalid extra_css parameter.
InvalidExtraCss,
/// input string not in UTF-8.
/// Input string not in UTF-8.
InvalidInputString,
/// Invalid cache size.
InvalidCacheSize,
}

// must be public because the impl From<&CssInlinerOptions> for InlineOptions would leak this type
/// Error to convert to CssResult later
/// cbindgen:ignore
pub enum InlineOptionsError {
/// Invalid base_url parameter.
InvalidUrl,
/// Invalid extra_css parameter.
InvalidExtraCss,
/// Invalid cache size.
InvalidCacheSize,
}

/// An LRU Cache for external stylesheets.
#[repr(C)]
pub struct StylesheetCache {
/// Cache size.
size: size_t,
}

/// @brief Creates an instance of StylesheetCache.
/// @return a StylesheetCache struct
#[no_mangle]
pub extern "C" fn css_inliner_stylesheet_cache(size: size_t) -> StylesheetCache {
StylesheetCache { size }
}

/// Configuration options for CSS inlining process.
Expand All @@ -45,6 +73,8 @@ pub struct CssInlinerOptions {
pub keep_link_tags: bool,
/// Whether remote stylesheets should be loaded or not.
pub load_remote_stylesheets: bool,
/// Cache for external stylesheets.
pub cache: *const StylesheetCache,
/// Used for loading external stylesheets via relative URLs.
pub base_url: *const c_char,
/// Additional CSS to inline.
Expand All @@ -69,7 +99,7 @@ pub unsafe extern "C" fn css_inline_to(
output: *mut c_char,
output_size: size_t,
) -> CssResult {
let options = CSSInliner::new(
let inliner = CSSInliner::new(
match InlineOptions::try_from(match options.as_ref() {
Some(ptr) => ptr,
None => return CssResult::NullOptions,
Expand All @@ -83,7 +113,7 @@ pub unsafe extern "C" fn css_inline_to(
Err(_) => return CssResult::InvalidInputString,
};
let mut buffer = CBuffer::new(output, output_size);
if let Err(e) = options.inline_to(html, &mut buffer) {
if let Err(e) = inliner.inline_to(html, &mut buffer) {
return match e {
InlineError::IO(_) => CssResult::IoError,
InlineError::Network { .. } => CssResult::RemoteStylesheetNotAvailable,
Expand All @@ -107,6 +137,7 @@ pub extern "C" fn css_inliner_default_options() -> CssInlinerOptions {
keep_link_tags: false,
base_url: ptr::null(),
load_remote_stylesheets: true,
cache: std::ptr::null(),
extra_css: ptr::null(),
preallocate_node_capacity: 32,
}
Expand Down Expand Up @@ -151,6 +182,15 @@ impl TryFrom<&CssInlinerOptions> for InlineOptions<'_> {
None => None,
},
load_remote_stylesheets: value.load_remote_stylesheets,
cache: {
if value.cache.is_null() {
None
} else if let Some(size) = NonZeroUsize::new(unsafe { (*value.cache).size }) {
Some(Mutex::new(css_inline::StylesheetCache::new(size)))
} else {
return Err(InlineOptionsError::InvalidCacheSize);
}
},
extra_css: extra_css.map(Cow::Borrowed),
preallocate_node_capacity: value.preallocate_node_capacity,
resolver: Arc::new(DefaultStylesheetResolver),
Expand All @@ -163,6 +203,7 @@ impl From<InlineOptionsError> for CssResult {
match value {
InlineOptionsError::InvalidUrl => CssResult::InvalidUrl,
InlineOptionsError::InvalidExtraCss => CssResult::InvalidExtraCss,
InlineOptionsError::InvalidCacheSize => CssResult::InvalidCacheSize,
}
}
}
Expand Down
31 changes: 31 additions & 0 deletions bindings/c/tests/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,42 @@ static void test_file_scheme(void) {
CSS_RESULT_OK);
}

static void test_cache_valid(void) {
char html[MAX_SIZE];
assert(make_html(html, SAMPLE_STYLE, SAMPLE_BODY));

StylesheetCache cache = css_inliner_stylesheet_cache(8);
CssInlinerOptions options = css_inliner_default_options();
options.cache = &cache;

char first_output[MAX_SIZE];
char second_output[MAX_SIZE];

assert(css_inline_to(&options, html, first_output, sizeof(first_output)) == CSS_RESULT_OK);
assert(strcmp(first_output, SAMPLE_INLINED) == 0);
}

static void test_cache_invalid(void) {
char html[MAX_SIZE];
assert(make_html(html, SAMPLE_STYLE, SAMPLE_BODY));

StylesheetCache cache = css_inliner_stylesheet_cache(0);
CssInlinerOptions options = css_inliner_default_options();
options.cache = &cache;

char first_output[MAX_SIZE];
char second_output[MAX_SIZE];

assert(css_inline_to(&options, html, first_output, sizeof(first_output)) == CSS_RESULT_INVALID_CACHE_SIZE);
}

int main(void) {
test_default_options();
test_output_size_too_small();
test_missing_stylesheet();
test_invalid_base_url();
test_file_scheme();
test_cache_valid();
test_cache_invalid();
return 0;
}
4 changes: 4 additions & 0 deletions bindings/javascript/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

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

## [0.13.2] - 2024-03-25

### Changed
Expand Down
2 changes: 1 addition & 1 deletion bindings/javascript/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ serde = { version = "1", features = ["derive"], default-features = false }
path = "../../css-inline"
version = "*"
default-features = false
features = ["http", "file"]
features = ["http", "file", "stylesheet-cache"]

[target.'cfg(target_arch = "wasm32")'.dependencies.css-inline]
path = "../../css-inline"
Expand Down
27 changes: 26 additions & 1 deletion bindings/javascript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ into:
- Inlines CSS from `style` and `link` tags
- Removes `style` and `link` tags
- Resolves external stylesheets (including local files)
- Optionally caches external stylesheets
- Works on Linux, Windows, and macOS
- Supports HTML5 & CSS3
- Tested on Node.js 18 & 20.
Expand Down Expand Up @@ -82,6 +83,7 @@ var inlined = inline(
- `keepLinkTags`. Specifies whether to keep "link" tags after inlining. Default: `false`
- `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`
- `loadRemoteStylesheets`. Specifies whether remote stylesheets should be loaded. Default: `true`
- `cache`. Specifies caching options for external stylesheets (for example, `{size: 5}`). Default: `null`
- `extraCss`. Extra CSS to be inlined. Default: `null`
- `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`

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

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

You can also cache external stylesheets to avoid excessive network requests:

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

var inlined = inline(
`
<html>
<head>
<link href="http://127.0.0.1:1234/external.css" rel="stylesheet">
<style>h1 { color:red }</style>
</head>
<body>
<h1>Test</h1>
</body>
</html>
`,
{ cache: { size: 5 } },
);
```

Caching is disabled by default.

## WebAssembly

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

**NOTE**: WASM module currently lacks support for fetching stylesheets from network or filesystem.
**NOTE**: WASM module currently lacks support for fetching stylesheets from network or filesystem and caching.

## Performance

Expand Down
30 changes: 30 additions & 0 deletions bindings/javascript/__test__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,33 @@ h2 { color: red; }
inlinedHtml,
);
});

test("cache external stylesheets", (t) => {
t.is(
inline(
`<html>
<head>
<link href="http://127.0.0.1:1234/external.css" rel="stylesheet">
<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml">
<style>
h2 { color: red; }
</style>
</head>
<body>
<h1>Big Text</h1>
<h2>Smaller Text</h2>
</body>
</html>`,
{ cache: { size: 5 } },
),
inlinedHtml,
);
});

test("invalid cache size", (t) => {
const error = t.throws(() => {
inline("", { cache: { size: 0 } });
});
t.is(error.code, "GenericFailure");
t.is(error.message, "Cache size must be an integer greater than zero");
});

0 comments on commit cc55213

Please sign in to comment.