css_inline 0.13.0 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|