Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: External stylesheet caching #350

Merged
merged 1 commit into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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");
});
Loading
Loading