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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ecc6091cab3a8511eb3e5135f0028488cb11eebcdcc64efaababbe404a154aff
4
- data.tar.gz: e0c4874267b3455c13f04321a56c5ff526b903acdc517adafdb4a73731598339
3
+ metadata.gz: dcb73434f736f0fbe7a516bdbeb8064091ac0408910ff3af705ca8e3579368f0
4
+ data.tar.gz: f52c04d176780cca6c3f758cb8fbcd6123b693ef80696e60993e4d846258584e
5
5
  SHA512:
6
- metadata.gz: 6c9d1fd5e9c7a81680e19ff417bdc57f815b22399887cf84441d4f03bf6ccd6ce7f96da6555054fddecc9a396de22f76a2e0705f3b3f030db6863994f5a3e8b3
7
- data.tar.gz: c16c50735d1f2dc40691a1c74f9688147818a0d78005549df819044ad6beb0fc11339feb2faf7148c69f78b3dd44108e9cfd604a6904d4ec8953df5ead5f6049
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
- 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.
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: `True`
96
- - `keep_style_tags`. Specifies whether to keep "style" tags after inlining. Default: `False`
97
- - `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`
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: `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`
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.11.2` | `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 |
145
214
  |-------------------|---------|---------------------|------------------------------------------------|------------|
146
- | Basic usage | 230 B | 8.27 µs | 433.55 µs | **52.35x** |
147
- | Realistic email 1 | 8.58 KB | 159.20 µs | 9.88 ms | **62.10x** |
148
- | Realistic email 2 | 4.3 KB | 103.41 µs | Error: Cannot parse 0 calc((100% - 500px) / 2) | - |
149
- | GitHub Page | 1.81 MB | 301.66 ms | 3.08 s | **10.24x** |
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.74.1` on Ruby `3.2.2`.
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
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "css-inline-ruby"
3
- version = "0.12.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"]
@@ -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
- RHash, Value,
33
+ typed_data::Obj,
34
+ DataTypeFunctions, RHash, TypedData, Value,
35
35
  };
36
36
  use rayon::prelude::*;
37
- use std::borrow::Cow;
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
- extra_css: kwargs.5.map(Cow::Owned),
78
- 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),
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.12.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: 2023-12-28 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
@@ -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.10
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