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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 341c8140008fc97311b7c05e23becedbfde8a2bb212cab1602f4fcf4b12d90aa
4
- data.tar.gz: cbcb902ad4446aac95f369258cced29a9729fd19772f001e1faa1dd72641d324
3
+ metadata.gz: dcb73434f736f0fbe7a516bdbeb8064091ac0408910ff3af705ca8e3579368f0
4
+ data.tar.gz: f52c04d176780cca6c3f758cb8fbcd6123b693ef80696e60993e4d846258584e
5
5
  SHA512:
6
- metadata.gz: c6ea53abccd62a28badf56f9298f54307fbff3609a46e6d79bdbf7e5a65171b92ae5d38363e426cda70e1e932117d983c6a302735d41fc82ad40ffb84948f15b
7
- data.tar.gz: 7ada5bffbc4961928a6fe48e70e8bb3f73419cd86bc46315bca86ae55efb3c04ec046888b4eb9c98f4a20f80158d5d9387f28cf25fc20a916dea54c7e83c04e9
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
- When there is a need to inline multiple HTML documents simultaneously, `css_inline` offers the `inline_many` function.
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: `True`
97
- - `keep_style_tags`. Specifies whether to keep "style" tags after inlining. Default: `False`
98
- - `keep_link_tags`. Specifies whether to keep "link" tags after inlining. Default: `False`
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: `True`
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.13.0` | `premailer 1.21.0 with Nokogiri 1.15.2` | Difference |
213
+ | | Size | `css_inline 0.14.0` | `premailer 1.21.0 with Nokogiri 1.15.2` | Difference |
161
214
  |-------------------|---------|---------------------|------------------------------------------------|------------|
162
- | Basic usage | 230 B | 7.79 µs | 417.56 µs | **53.57x** |
163
- | Realistic email 1 | 8.58 KB | 149.56 µs | 9.73 ms | **65.07x** |
164
- | Realistic email 2 | 4.3 KB | 93.19 µs | Error: Cannot parse 0 calc((100% - 500px) / 2) | - |
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.75.0` on Ruby `3.2.2`.
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
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "css-inline-ruby"
3
- version = "0.13.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"]
@@ -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
- RHash, Value,
33
+ typed_data::Obj,
34
+ DataTypeFunctions, RHash, TypedData, Value,
34
35
  };
35
36
  use rayon::prelude::*;
36
- use std::{borrow::Cow, sync::Arc};
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
- extra_css: kwargs.5.map(Cow::Owned),
77
- preallocate_node_capacity: kwargs.6.unwrap_or(32),
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.13.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-19 00:00:00.000000000 Z
11
+ date: 2024-04-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake-compiler