css_inline 0.12.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 +80 -15
- data/ext/css_inline/Cargo.toml +2 -2
- data/ext/css_inline/src/lib.rs +97 -5
- metadata +3 -3
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,9 +37,11 @@ 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
|
44
|
+
- Tested on Ruby 2.7 and 3.2.
|
43
45
|
|
44
46
|
## Playground
|
45
47
|
|
@@ -67,7 +69,45 @@ puts inlined
|
|
67
69
|
# Outputs: "<html><head></head><body><h1 style=\"color:blue;\">Big Text</h1></body></html>"
|
68
70
|
```
|
69
71
|
|
70
|
-
|
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.
|
71
111
|
This feature allows for concurrent processing of several inputs, significantly improving performance when dealing with a large number of documents.
|
72
112
|
|
73
113
|
```ruby
|
@@ -92,11 +132,12 @@ inliner = CSSInline::CSSInliner.new(keep_style_tags: true)
|
|
92
132
|
inliner.inline("...")
|
93
133
|
```
|
94
134
|
|
95
|
-
- `inline_style_tags`. Specifies whether to inline CSS from "style" tags. Default: `
|
96
|
-
- `keep_style_tags`. Specifies whether to keep "style" tags after inlining. Default: `
|
97
|
-
- `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`
|
98
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`
|
99
|
-
- `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`
|
100
141
|
- `extra_css`. Extra CSS to be inlined. Default: `nil`
|
101
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`
|
102
143
|
|
@@ -124,6 +165,21 @@ The `data-css-inline="ignore"` attribute also allows you to skip `link` and `sty
|
|
124
165
|
</body>
|
125
166
|
```
|
126
167
|
|
168
|
+
Alternatively, you may keep `style` from being removed by using the `data-css-inline="keep"` attribute.
|
169
|
+
This is useful if you want to keep `@media` queries for responsive emails in separate `style` tags:
|
170
|
+
|
171
|
+
```html
|
172
|
+
<head>
|
173
|
+
<!-- Styles below are not removed -->
|
174
|
+
<style data-css-inline="keep">h1 { color:blue; }</style>
|
175
|
+
</head>
|
176
|
+
<body>
|
177
|
+
<h1>Big Text</h1>
|
178
|
+
</body>
|
179
|
+
```
|
180
|
+
|
181
|
+
Such tags will be kept in the resulting HTML even if the `keep_style_tags` option is set to `false`.
|
182
|
+
|
127
183
|
If you'd like to load stylesheets from your filesystem, use the `file://` scheme:
|
128
184
|
|
129
185
|
```ruby
|
@@ -134,6 +190,19 @@ inliner = CSSInline::CSSInliner.new(base_url: "file://styles/email/")
|
|
134
190
|
inliner.inline("...")
|
135
191
|
```
|
136
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
|
+
|
137
206
|
## Performance
|
138
207
|
|
139
208
|
Leveraging efficient tools from Mozilla's Servo project, this library delivers superior performance.
|
@@ -141,19 +210,15 @@ It consistently outperforms `premailer`, offering speed increases often exceedin
|
|
141
210
|
|
142
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):
|
143
212
|
|
144
|
-
| | Size | `css_inline 0.
|
213
|
+
| | Size | `css_inline 0.14.0` | `premailer 1.21.0 with Nokogiri 1.15.2` | Difference |
|
145
214
|
|-------------------|---------|---------------------|------------------------------------------------|------------|
|
146
|
-
| Basic usage | 230 B | 8.
|
147
|
-
| Realistic email 1 | 8.58 KB |
|
148
|
-
| Realistic email 2 | 4.3 KB |
|
149
|
-
| GitHub Page | 1.81 MB |
|
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) | - |
|
218
|
+
| GitHub Page | 1.81 MB | 237.03 ms | 3.02 s | **12.78x** |
|
150
219
|
|
151
220
|
Please refer to the `test/bench.rb` file to review the benchmark code.
|
152
|
-
The results displayed above were measured using stable `rustc 1.
|
153
|
-
|
154
|
-
## Ruby support
|
155
|
-
|
156
|
-
`css_inline` supports Ruby 2.7 and 3.2.
|
221
|
+
The results displayed above were measured using stable `rustc 1.77.1` on Ruby `3.2.2`.
|
157
222
|
|
158
223
|
## Further reading
|
159
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
@@ -25,16 +25,20 @@
|
|
25
25
|
rust_2018_compatibility,
|
26
26
|
rust_2021_compatibility
|
27
27
|
)]
|
28
|
-
#[allow(clippy::module_name_repetitions)]
|
29
28
|
use css_inline as rust_inline;
|
30
29
|
use magnus::{
|
31
30
|
class, define_module, function, method,
|
32
31
|
prelude::*,
|
33
32
|
scan_args::{get_kwargs, scan_args, Args},
|
34
|
-
|
33
|
+
typed_data::Obj,
|
34
|
+
DataTypeFunctions, RHash, TypedData, Value,
|
35
35
|
};
|
36
36
|
use rayon::prelude::*;
|
37
|
-
use std::
|
37
|
+
use std::{
|
38
|
+
borrow::Cow,
|
39
|
+
num::NonZeroUsize,
|
40
|
+
sync::{Arc, Mutex},
|
41
|
+
};
|
38
42
|
|
39
43
|
type RubyResult<T> = Result<T, magnus::Error>;
|
40
44
|
|
@@ -50,6 +54,7 @@ fn parse_options<Req>(
|
|
50
54
|
Option<bool>,
|
51
55
|
Option<String>,
|
52
56
|
Option<bool>,
|
57
|
+
Option<Obj<StylesheetCache>>,
|
53
58
|
Option<String>,
|
54
59
|
Option<usize>,
|
55
60
|
),
|
@@ -63,6 +68,7 @@ fn parse_options<Req>(
|
|
63
68
|
"keep_link_tags",
|
64
69
|
"base_url",
|
65
70
|
"load_remote_stylesheets",
|
71
|
+
"cache",
|
66
72
|
"extra_css",
|
67
73
|
"preallocate_node_capacity",
|
68
74
|
],
|
@@ -74,11 +80,38 @@ fn parse_options<Req>(
|
|
74
80
|
keep_link_tags: kwargs.2.unwrap_or(false),
|
75
81
|
base_url: parse_url(kwargs.3)?,
|
76
82
|
load_remote_stylesheets: kwargs.4.unwrap_or(true),
|
77
|
-
|
78
|
-
|
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),
|
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:
|
11
|
+
date: 2024-04-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake-compiler
|
@@ -124,7 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
124
124
|
version: 3.3.26
|
125
125
|
requirements:
|
126
126
|
- Rust >= 1.65
|
127
|
-
rubygems_version: 3.4.
|
127
|
+
rubygems_version: 3.4.19
|
128
128
|
signing_key:
|
129
129
|
specification_version: 4
|
130
130
|
summary: High-performance library for inlining CSS into HTML 'style' attributes
|