css_inline 0.12.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: 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