css_inline 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +63 -10
- data/ext/css_inline/Cargo.toml +2 -2
- data/ext/css_inline/src/lib.rs +96 -4
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dcb73434f736f0fbe7a516bdbeb8064091ac0408910ff3af705ca8e3579368f0
|
4
|
+
data.tar.gz: f52c04d176780cca6c3f758cb8fbcd6123b693ef80696e60993e4d846258584e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 33061093a87721b1cc61215c63124ea2e8669a3e4a88304499abee70a4b8443aabc7da5284dcc41cc95d313f8477a84a75624ab86d737dec5ae6d35c3c13067a
|
7
|
+
data.tar.gz: 057d4df4c802feb3cbebb3c69d7e8369a76114d47423cab1b077f1a57c254ae93ce869a99e13f09c22a0df8e661c6e4c9c53ed6d674e92ba9f060eddb6142ae9
|
data/README.md
CHANGED
@@ -37,6 +37,7 @@ into:
|
|
37
37
|
- Inlines CSS from `style` and `link` tags
|
38
38
|
- Removes `style` and `link` tags
|
39
39
|
- Resolves external stylesheets (including local files)
|
40
|
+
- Optionally caches external stylesheets
|
40
41
|
- Can process multiple documents in parallel
|
41
42
|
- Works on Linux, Windows, and macOS
|
42
43
|
- Supports HTML5 & CSS3
|
@@ -68,7 +69,45 @@ puts inlined
|
|
68
69
|
# Outputs: "<html><head></head><body><h1 style=\"color:blue;\">Big Text</h1></body></html>"
|
69
70
|
```
|
70
71
|
|
71
|
-
|
72
|
+
Note that `css-inline` automatically adds missing `html` and `body` tags, so the output is a valid HTML document.
|
73
|
+
|
74
|
+
Alternatively, you can inline CSS into an HTML fragment, preserving the original structure:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
require 'css_inline'
|
78
|
+
|
79
|
+
fragment = """
|
80
|
+
<main>
|
81
|
+
<h1>Hello</h1>
|
82
|
+
<section>
|
83
|
+
<p>who am i</p>
|
84
|
+
</section>
|
85
|
+
</main>
|
86
|
+
"""
|
87
|
+
|
88
|
+
css = """
|
89
|
+
p {
|
90
|
+
color: red;
|
91
|
+
}
|
92
|
+
|
93
|
+
h1 {
|
94
|
+
color: blue;
|
95
|
+
}
|
96
|
+
"""
|
97
|
+
|
98
|
+
inlined = CSSInline.inline_fragment(fragment, css)
|
99
|
+
|
100
|
+
puts inlined
|
101
|
+
# HTML becomes this:
|
102
|
+
# <main>
|
103
|
+
# <h1 style="color: blue;">Hello</h1>
|
104
|
+
# <section>
|
105
|
+
# <p style="color: red;">who am i</p>
|
106
|
+
# </section>
|
107
|
+
# </main>
|
108
|
+
```
|
109
|
+
|
110
|
+
When there is a need to inline multiple HTML documents simultaneously, `css_inline` offers `inline_many` and `inline_many_fragments` functions.
|
72
111
|
This feature allows for concurrent processing of several inputs, significantly improving performance when dealing with a large number of documents.
|
73
112
|
|
74
113
|
```ruby
|
@@ -93,11 +132,12 @@ inliner = CSSInline::CSSInliner.new(keep_style_tags: true)
|
|
93
132
|
inliner.inline("...")
|
94
133
|
```
|
95
134
|
|
96
|
-
- `inline_style_tags`. Specifies whether to inline CSS from "style" tags. Default: `
|
97
|
-
- `keep_style_tags`. Specifies whether to keep "style" tags after inlining. Default: `
|
98
|
-
- `keep_link_tags`. Specifies whether to keep "link" tags after inlining. Default: `
|
135
|
+
- `inline_style_tags`. Specifies whether to inline CSS from "style" tags. Default: `true`
|
136
|
+
- `keep_style_tags`. Specifies whether to keep "style" tags after inlining. Default: `false`
|
137
|
+
- `keep_link_tags`. Specifies whether to keep "link" tags after inlining. Default: `false`
|
99
138
|
- `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: `nil`
|
100
|
-
- `load_remote_stylesheets`. Specifies whether remote stylesheets should be loaded. Default: `
|
139
|
+
- `load_remote_stylesheets`. Specifies whether remote stylesheets should be loaded. Default: `true`
|
140
|
+
- `cache`. Specifies caching options for external stylesheets (for example, `StylesheetCache(size: 5)`). Default: `nil`
|
101
141
|
- `extra_css`. Extra CSS to be inlined. Default: `nil`
|
102
142
|
- `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`
|
103
143
|
|
@@ -150,6 +190,19 @@ inliner = CSSInline::CSSInliner.new(base_url: "file://styles/email/")
|
|
150
190
|
inliner.inline("...")
|
151
191
|
```
|
152
192
|
|
193
|
+
You can also cache external stylesheets to avoid excessive network requests:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
require 'css_inline'
|
197
|
+
|
198
|
+
inliner = CSSInline::CSSInliner.new(
|
199
|
+
cache: CSSInline::StylesheetCache.new(size: 5)
|
200
|
+
)
|
201
|
+
inliner.inline("...")
|
202
|
+
```
|
203
|
+
|
204
|
+
Caching is disabled by default.
|
205
|
+
|
153
206
|
## Performance
|
154
207
|
|
155
208
|
Leveraging efficient tools from Mozilla's Servo project, this library delivers superior performance.
|
@@ -157,15 +210,15 @@ It consistently outperforms `premailer`, offering speed increases often exceedin
|
|
157
210
|
|
158
211
|
The table below provides a detailed comparison between `css_inline` and `premailer` when inlining CSS into an HTML document (like in the Usage section above):
|
159
212
|
|
160
|
-
| | Size | `css_inline 0.
|
213
|
+
| | Size | `css_inline 0.14.0` | `premailer 1.21.0 with Nokogiri 1.15.2` | Difference |
|
161
214
|
|-------------------|---------|---------------------|------------------------------------------------|------------|
|
162
|
-
| Basic usage | 230 B |
|
163
|
-
| Realistic email 1 | 8.58 KB |
|
164
|
-
| Realistic email 2 | 4.3 KB |
|
215
|
+
| Basic usage | 230 B | 8.82 µs | 417.84 µs | **47.37x** |
|
216
|
+
| Realistic email 1 | 8.58 KB | 160.37 µs | 10.15 ms | **63.32x** |
|
217
|
+
| Realistic email 2 | 4.3 KB | 102.88 µs | Error: Cannot parse 0 calc((100% - 500px) / 2) | - |
|
165
218
|
| GitHub Page | 1.81 MB | 237.03 ms | 3.02 s | **12.78x** |
|
166
219
|
|
167
220
|
Please refer to the `test/bench.rb` file to review the benchmark code.
|
168
|
-
The results displayed above were measured using stable `rustc 1.
|
221
|
+
The results displayed above were measured using stable `rustc 1.77.1` on Ruby `3.2.2`.
|
169
222
|
|
170
223
|
## Further reading
|
171
224
|
|
data/ext/css_inline/Cargo.toml
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
[package]
|
2
2
|
name = "css-inline-ruby"
|
3
|
-
version = "0.
|
3
|
+
version = "0.14.0"
|
4
4
|
authors = ["Dmitry Dygalo <dmitry@dygalo.dev>"]
|
5
5
|
edition = "2021"
|
6
6
|
readme = "README.rdoc"
|
@@ -23,4 +23,4 @@ rayon = "1"
|
|
23
23
|
path = "../../../../css-inline"
|
24
24
|
version = "*"
|
25
25
|
default-features = false
|
26
|
-
features = ["http", "file"]
|
26
|
+
features = ["http", "file", "stylesheet-cache"]
|
data/ext/css_inline/src/lib.rs
CHANGED
@@ -30,10 +30,15 @@ use magnus::{
|
|
30
30
|
class, define_module, function, method,
|
31
31
|
prelude::*,
|
32
32
|
scan_args::{get_kwargs, scan_args, Args},
|
33
|
-
|
33
|
+
typed_data::Obj,
|
34
|
+
DataTypeFunctions, RHash, TypedData, Value,
|
34
35
|
};
|
35
36
|
use rayon::prelude::*;
|
36
|
-
use std::{
|
37
|
+
use std::{
|
38
|
+
borrow::Cow,
|
39
|
+
num::NonZeroUsize,
|
40
|
+
sync::{Arc, Mutex},
|
41
|
+
};
|
37
42
|
|
38
43
|
type RubyResult<T> = Result<T, magnus::Error>;
|
39
44
|
|
@@ -49,6 +54,7 @@ fn parse_options<Req>(
|
|
49
54
|
Option<bool>,
|
50
55
|
Option<String>,
|
51
56
|
Option<bool>,
|
57
|
+
Option<Obj<StylesheetCache>>,
|
52
58
|
Option<String>,
|
53
59
|
Option<usize>,
|
54
60
|
),
|
@@ -62,6 +68,7 @@ fn parse_options<Req>(
|
|
62
68
|
"keep_link_tags",
|
63
69
|
"base_url",
|
64
70
|
"load_remote_stylesheets",
|
71
|
+
"cache",
|
65
72
|
"extra_css",
|
66
73
|
"preallocate_node_capacity",
|
67
74
|
],
|
@@ -73,12 +80,38 @@ fn parse_options<Req>(
|
|
73
80
|
keep_link_tags: kwargs.2.unwrap_or(false),
|
74
81
|
base_url: parse_url(kwargs.3)?,
|
75
82
|
load_remote_stylesheets: kwargs.4.unwrap_or(true),
|
76
|
-
|
77
|
-
|
83
|
+
cache: kwargs
|
84
|
+
.5
|
85
|
+
.map(|cache| Mutex::new(rust_inline::StylesheetCache::new(cache.size))),
|
86
|
+
extra_css: kwargs.6.map(Cow::Owned),
|
87
|
+
preallocate_node_capacity: kwargs.7.unwrap_or(32),
|
78
88
|
resolver: Arc::new(rust_inline::DefaultStylesheetResolver),
|
79
89
|
})
|
80
90
|
}
|
81
91
|
|
92
|
+
#[derive(DataTypeFunctions, TypedData)]
|
93
|
+
#[magnus(class = "CSSInline::StylesheetCache")]
|
94
|
+
struct StylesheetCache {
|
95
|
+
size: NonZeroUsize,
|
96
|
+
}
|
97
|
+
|
98
|
+
impl StylesheetCache {
|
99
|
+
fn new(args: &[Value]) -> RubyResult<StylesheetCache> {
|
100
|
+
fn error() -> magnus::Error {
|
101
|
+
magnus::Error::new(
|
102
|
+
magnus::exception::arg_error(),
|
103
|
+
"Cache size must be an integer greater than zero",
|
104
|
+
)
|
105
|
+
}
|
106
|
+
|
107
|
+
let args: Args<(), (), (), (), RHash, ()> = scan_args::<(), _, _, _, RHash, _>(args)?;
|
108
|
+
let kwargs = get_kwargs::<_, (), (Option<usize>,), ()>(args.keywords, &[], &["size"])
|
109
|
+
.map_err(|_| error())?;
|
110
|
+
let size = NonZeroUsize::new(kwargs.optional.0.unwrap_or(8)).ok_or_else(error)?;
|
111
|
+
Ok(StylesheetCache { size })
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
82
115
|
#[magnus::wrap(class = "CSSInline::CSSInliner")]
|
83
116
|
struct CSSInliner {
|
84
117
|
inner: rust_inline::CSSInliner<'static>,
|
@@ -142,10 +175,27 @@ impl CSSInliner {
|
|
142
175
|
Ok(self.inner.inline(&html).map_err(InlineErrorWrapper)?)
|
143
176
|
}
|
144
177
|
|
178
|
+
#[allow(clippy::needless_pass_by_value)]
|
179
|
+
fn inline_fragment(&self, html: String, css: String) -> RubyResult<String> {
|
180
|
+
Ok(self
|
181
|
+
.inner
|
182
|
+
.inline_fragment(&html, &css)
|
183
|
+
.map_err(InlineErrorWrapper)?)
|
184
|
+
}
|
185
|
+
|
145
186
|
#[allow(clippy::needless_pass_by_value)]
|
146
187
|
fn inline_many(&self, html: Vec<String>) -> RubyResult<Vec<String>> {
|
147
188
|
inline_many_impl(&html, &self.inner)
|
148
189
|
}
|
190
|
+
|
191
|
+
#[allow(clippy::needless_pass_by_value)]
|
192
|
+
fn inline_many_fragments(
|
193
|
+
&self,
|
194
|
+
html: Vec<String>,
|
195
|
+
css: Vec<String>,
|
196
|
+
) -> RubyResult<Vec<String>> {
|
197
|
+
inline_many_fragments_impl(&html, &css, &self.inner)
|
198
|
+
}
|
149
199
|
}
|
150
200
|
|
151
201
|
fn inline(args: &[Value]) -> RubyResult<String> {
|
@@ -157,6 +207,16 @@ fn inline(args: &[Value]) -> RubyResult<String> {
|
|
157
207
|
.map_err(InlineErrorWrapper)?)
|
158
208
|
}
|
159
209
|
|
210
|
+
fn inline_fragment(args: &[Value]) -> RubyResult<String> {
|
211
|
+
let args = scan_args::<(String, String), _, _, _, _, _>(args)?;
|
212
|
+
let options = parse_options(&args)?;
|
213
|
+
let html = args.required.0;
|
214
|
+
let css = args.required.1;
|
215
|
+
Ok(rust_inline::CSSInliner::new(options)
|
216
|
+
.inline_fragment(&html, &css)
|
217
|
+
.map_err(InlineErrorWrapper)?)
|
218
|
+
}
|
219
|
+
|
160
220
|
fn inline_many(args: &[Value]) -> RubyResult<Vec<String>> {
|
161
221
|
let args = scan_args::<(Vec<String>,), _, _, _, _, _>(args)?;
|
162
222
|
let options = parse_options(&args)?;
|
@@ -172,17 +232,49 @@ fn inline_many_impl(
|
|
172
232
|
Ok(output.map_err(InlineErrorWrapper)?)
|
173
233
|
}
|
174
234
|
|
235
|
+
fn inline_many_fragments(args: &[Value]) -> RubyResult<Vec<String>> {
|
236
|
+
let args = scan_args::<(Vec<String>, Vec<String>), _, _, _, _, _>(args)?;
|
237
|
+
let options = parse_options(&args)?;
|
238
|
+
let inliner = rust_inline::CSSInliner::new(options);
|
239
|
+
inline_many_fragments_impl(&args.required.0, &args.required.1, &inliner)
|
240
|
+
}
|
241
|
+
|
242
|
+
fn inline_many_fragments_impl(
|
243
|
+
htmls: &[String],
|
244
|
+
css: &[String],
|
245
|
+
inliner: &rust_inline::CSSInliner<'static>,
|
246
|
+
) -> RubyResult<Vec<String>> {
|
247
|
+
let output: Result<Vec<_>, _> = htmls
|
248
|
+
.par_iter()
|
249
|
+
.zip(css)
|
250
|
+
.map(|(html, css)| inliner.inline_fragment(html, css))
|
251
|
+
.collect();
|
252
|
+
Ok(output.map_err(InlineErrorWrapper)?)
|
253
|
+
}
|
254
|
+
|
175
255
|
#[magnus::init(name = "css_inline")]
|
176
256
|
fn init() -> RubyResult<()> {
|
177
257
|
let module = define_module("CSSInline")?;
|
178
258
|
|
179
259
|
module.define_module_function("inline", function!(inline, -1))?;
|
260
|
+
module.define_module_function("inline_fragment", function!(inline_fragment, -1))?;
|
180
261
|
module.define_module_function("inline_many", function!(inline_many, -1))?;
|
262
|
+
module.define_module_function(
|
263
|
+
"inline_many_fragments",
|
264
|
+
function!(inline_many_fragments, -1),
|
265
|
+
)?;
|
181
266
|
|
182
267
|
let class = module.define_class("CSSInliner", class::object())?;
|
183
268
|
class.define_singleton_method("new", function!(CSSInliner::new, -1))?;
|
184
269
|
class.define_method("inline", method!(CSSInliner::inline, 1))?;
|
270
|
+
class.define_method("inline_fragment", method!(CSSInliner::inline_fragment, 2))?;
|
185
271
|
class.define_method("inline_many", method!(CSSInliner::inline_many, 1))?;
|
272
|
+
class.define_method(
|
273
|
+
"inline_many_fragments",
|
274
|
+
method!(CSSInliner::inline_many_fragments, 2),
|
275
|
+
)?;
|
186
276
|
|
277
|
+
let class = module.define_class("StylesheetCache", class::object())?;
|
278
|
+
class.define_singleton_method("new", function!(StylesheetCache::new, -1))?;
|
187
279
|
Ok(())
|
188
280
|
}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: css_inline
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.14.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dmitry Dygalo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-01
|
11
|
+
date: 2024-04-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake-compiler
|