css_inline
is a high-performance library for inlining CSS into HTML 'style' attributes.
This library is designed for scenarios such as preparing HTML emails or embedding HTML into third-party web pages.
For instance, the crate transforms HTML like this:
<html>
<head>
<style>h1 { color:blue; }</style>
</head>
<body>
<h1>Big Text</h1>
</body>
</html>
into:
<html>
<head></head>
<body>
<h1 style="color:blue;">Big Text</h1>
</body>
</html>
- Uses reliable components from Mozilla's Servo project
- Inlines CSS from
style
andlink
tags - Removes
style
andlink
tags - Resolves external stylesheets (including local files)
- Optionally caches external stylesheets
- Works on Linux, Windows, and macOS
- Supports HTML5 & CSS3
- Bindings for Python, Ruby, JavaScript, C, and a WebAssembly module to run in browsers.
- Command Line Interface
If you'd like to try css-inline
, you can check the WebAssembly-powered playground to see the results instantly.
To include it in your project, add the following line to the dependencies section in your project's Cargo.toml
file:
[dependencies]
css-inline = "0.14"
The Minimum Supported Rust Version is 1.71.1.
const HTML: &str = r#"<html>
<head>
<style>h1 { color:blue; }</style>
</head>
<body>
<h1>Big Text</h1>
</body>
</html>"#;
fn main() -> css_inline::Result<()> {
let inlined = css_inline::inline(HTML)?;
// Do something with inlined HTML, e.g. send an email
Ok(())
}
Note that css-inline
automatically adds missing html
and body
tags, so the output is a valid HTML document.
Alternatively, you can inline CSS into an HTML fragment, preserving the original structure:
const FRAGMENT: &str = r#"<main>
<h1>Hello</h1>
<section>
<p>who am i</p>
</section>
</main>"#;
const CSS: &str = r#"
p {
color: red;
}
h1 {
color: blue;
}
"#;
fn main() -> css_inline::Result<()> {
let inlined = css_inline::inline_fragment(FRAGMENT, CSS)?;
Ok(())
}
css-inline
can be configured by using CSSInliner::options()
that implements the Builder pattern:
const HTML: &str = "...";
fn main() -> css_inline::Result<()> {
let inliner = css_inline::CSSInliner::options()
.load_remote_stylesheets(false)
.build();
let inlined = inliner.inline(HTML)?;
// Do something with inlined HTML, e.g. send an email
Ok(())
}
inline_style_tags
. Specifies whether to inline CSS from "style" tags. Default:true
keep_style_tags
. Specifies whether to keep "style" tags after inlining. Default:false
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 thefile://
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
You can also skip CSS inlining for an HTML tag by adding the data-css-inline="ignore"
attribute to it:
<head>
<style>h1 { color:blue; }</style>
</head>
<body>
<!-- The tag below won't receive additional styles -->
<h1 data-css-inline="ignore">Big Text</h1>
</body>
The data-css-inline="ignore"
attribute also allows you to skip link
and style
tags:
<head>
<!-- Styles below are ignored -->
<style data-css-inline="ignore">h1 { color:blue; }</style>
</head>
<body>
<h1>Big Text</h1>
</body>
Alternatively, you may keep style
from being removed by using the data-css-inline="keep"
attribute.
This is useful if you want to keep @media
queries for responsive emails in separate style
tags:
<head>
<!-- Styles below are not removed -->
<style data-css-inline="keep">h1 { color:blue; }</style>
</head>
<body>
<h1>Big Text</h1>
</body>
Such tags will be kept in the resulting HTML even if the keep_style_tags
option is set to false
.
If you'd like to load stylesheets from your filesystem, use the file://
scheme:
const HTML: &str = "...";
fn main() -> css_inline::Result<()> {
let base_url = css_inline::Url::parse("file://styles/email/").expect("Invalid URL");
let inliner = css_inline::CSSInliner::options()
.base_url(Some(base_url))
.build();
let inlined = inliner.inline(HTML);
// Do something with inlined HTML, e.g. send an email
Ok(())
}
For resolving remote stylesheets it is possible to implement a custom resolver:
#[derive(Debug, Default)]
pub struct CustomStylesheetResolver;
impl css_inline::StylesheetResolver for CustomStylesheetResolver {
fn retrieve(&self, location: &str) -> css_inline::Result<String> {
Err(self.unsupported("External stylesheets are not supported"))
}
}
fn main() -> css_inline::Result<()> {
let inliner = css_inline::CSSInliner::options()
.resolver(std::sync::Arc::new(CustomStylesheetResolver))
.build();
Ok(())
}
You can also cache external stylesheets to avoid excessive network requests:
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.
css-inline
typically inlines HTML emails within hundreds of microseconds, though results may vary with input complexity.
Benchmarks for css-inline==0.14.1
:
- Basic: 6.44 µs, 230 bytes
- Realistic-1: 128.59 µs, 8.58 KB
- Realistic-2: 81.44 µs, 4.3 KB
- GitHub page: 224.89 ms, 1.81 MB
These benchmarks, conducted using rustc 1.78
on M1 Max, can be found in css-inline/benches/inliner.rs
.
Install with cargo
:
cargo install css-inline
The following command inlines CSS in multiple documents in parallel. The resulting files will be saved
as inlined.email1.html
and inlined.email2.html
:
css-inline email1.html email2.html
For full details of the options available, you can use the --help
flag:
css-inline --help
If you're interested in learning how this library was created and how it works internally, check out these articles:
If you have any questions or discussions related to this library, please join our gitter!
This project is licensed under the terms of the MIT license.