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 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