css_inline 0.10.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fcc13ef01a093e818ec9c59f51cb12ba17cbbbe1a79a956886ef6f939e10a62e
4
+ data.tar.gz: 424e2bf2134c3235169724bb8c257e4193945bafe2e647c01471c184ea72c30e
5
+ SHA512:
6
+ metadata.gz: 186dfcbb167e301066f1b05897a3292655b0266de28e98e30cbb3450a25dcf34f9940162abe1cd625c5695000b2871efc98792fdd2722f921bb1ef3dc862809a
7
+ data.tar.gz: 67ae8e3ee7ab80a6b222df1d613f7d549b1ff94e750674928cf3e0310652808213781c19ca18bb87b11f5baef40bf11dc47c89cd12423ab92732deaa52b54454
data/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # css_inline
2
+
3
+ [<img alt="build status" src="https://img.shields.io/github/actions/workflow/status/Stranger6667/css-inline/build.yml?style=flat-square&labelColor=555555&logo=github" height="20">](https://github.com/Stranger6667/css-inline)
4
+ [<img alt="ruby gems" src="https://img.shields.io/gem/v/css_inline?logo=ruby&style=flat-square" height="20">](https://rubygems.org/gems/css_inline)
5
+ [<img alt="codecov.io" src="https://img.shields.io/codecov/c/gh/Stranger6667/css-inline?logo=codecov&style=flat-square&token=tOzvV4kDY0" height="20">](https://app.codecov.io/github/Stranger6667/css-inline)
6
+ [<img alt="gitter" src="https://img.shields.io/gitter/room/Stranger6667/css-inline?style=flat-square" height="20">](https://gitter.im/Stranger6667/css-inline)
7
+
8
+ `css_inline` inlines CSS into HTML documents, using components from Mozilla's Servo project.
9
+
10
+ This process is essential for sending HTML emails as you need to use "style" attributes instead of "style" tags.
11
+
12
+ For instance, the library transforms HTML like this:
13
+
14
+ ```html
15
+ <html>
16
+ <head>
17
+ <title>Test</title>
18
+ <style>h1 { color:blue; }</style>
19
+ </head>
20
+ <body>
21
+ <h1>Big Text</h1>
22
+ </body>
23
+ </html>
24
+ ```
25
+
26
+ into:
27
+
28
+ ```html
29
+ <html>
30
+ <head>
31
+ <title>Test</title>
32
+ </head>
33
+ <body>
34
+ <h1 style="color:blue;">Big Text</h1>
35
+ </body>
36
+ </html>
37
+ ```
38
+
39
+ - Uses reliable components from Mozilla's Servo
40
+ - Inlines CSS from `style` and `link` tags
41
+ - Removes `style` and `link` tags
42
+ - Resolves external stylesheets (including local files)
43
+ - Can process multiple documents in parallel
44
+ - Works on Linux, Windows, and macOS
45
+ - Supports HTML5 & CSS3
46
+
47
+ ## Installation
48
+
49
+ Add this line to your application's `Gemfile`:
50
+
51
+ ```
52
+ gem 'css_inline'
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ To inline CSS in an HTML document:
58
+
59
+ ```ruby
60
+ require 'css_inline'
61
+
62
+ html = "<html><head><style>h1 { color:blue; }</style></head><body><h1>Big Text</h1></body></html>"
63
+ inlined = CSSInline.inline(html)
64
+
65
+ puts inlined
66
+ # Outputs: "<html><head></head><body><h1 style=\"color:blue;\">Big Text</h1></body></html>"
67
+ ```
68
+
69
+ ## Configuration
70
+
71
+ For customization options use the `CSSInliner` class:
72
+
73
+ ```python
74
+ require 'css_inline'
75
+
76
+ inliner = CSSInline::CSSInliner.new(keep_style_tags: true)
77
+ inliner.inline("...")
78
+ ```
79
+
80
+ - `keep_style_tags`. Specifies whether to keep "style" tags after inlining. Default: `False`
81
+ - `keep_link_tags`. Specifies whether to keep "link" tags after inlining. Default: `False`
82
+ - `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`
83
+ - `load_remote_stylesheets`. Specifies whether remote stylesheets should be loaded. Default: `True`
84
+ - `extra_css`. Extra CSS to be inlined. Default: `nil`
85
+ - `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: `8`
86
+
87
+ You can also skip CSS inlining for an HTML tag by adding the `data-css-inline="ignore"` attribute to it:
88
+
89
+ ```html
90
+ <head>
91
+ <title>Test</title>
92
+ <style>h1 { color:blue; }</style>
93
+ </head>
94
+ <body>
95
+ <!-- The tag below won't receive additional styles -->
96
+ <h1 data-css-inline="ignore">Big Text</h1>
97
+ </body>
98
+ ```
99
+
100
+ The `data-css-inline="ignore"` attribute also allows you to skip `link` and `style` tags:
101
+
102
+ ```html
103
+ <head>
104
+ <title>Test</title>
105
+ <!-- Styles below are ignored -->
106
+ <style data-css-inline="ignore">h1 { color:blue; }</style>
107
+ </head>
108
+ <body>
109
+ <h1>Big Text</h1>
110
+ </body>
111
+ ```
112
+
113
+ If you'd like to load stylesheets from your filesystem, use the `file://` scheme:
114
+
115
+ ```ruby
116
+ require 'css_inline'
117
+
118
+ # styles/email is relative to the current directory
119
+ inliner = CSSInline::CSSInliner.new(base_url: "file://styles/email/")
120
+ inliner.inline("...")
121
+ ```
122
+
123
+ ## Performance
124
+
125
+ Leveraging efficient tools from Mozilla's Servo project, this library delivers superior performance.
126
+ It consistently outperforms `premailer`, offering speed increases often exceeding **30 times**.
127
+
128
+ 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):
129
+
130
+ | | `css_inline 0.10.0` | `premailer 1.21.0 with Nokogiri 1.15.2` | Difference |
131
+ |-------------------|---------------------|------------------------------------------------|------------|
132
+ | Basic usage | 11 µs | 448 µs | **40.6x** |
133
+ | Realistic email 1 | 290 µs | 9.72 ms | **33.5x** |
134
+ | Realistic email 2 | 167.50 µs | Error: Cannot parse 0 calc((100% - 500px) / 2) | - |
135
+
136
+ Please refer to the `test/bench.rb` file to review the benchmark code.
137
+ The results displayed above were measured using stable `rustc 1.70` on Ruby `3.2.2`.
138
+
139
+ ## Ruby support
140
+
141
+ `css_inline` supports Ruby 2.7 and 3.2.
142
+
143
+ ## Extra materials
144
+
145
+ If you want to know how this library was created & how it works internally, you could take a look at these articles:
146
+
147
+ - [Rust crate](https://dygalo.dev/blog/rust-for-a-pythonista-2/)
148
+ - [Python bindings](https://dygalo.dev/blog/rust-for-a-pythonista-3/)
149
+
150
+ ## License
151
+
152
+ This project is licensed under the terms of the [MIT license](https://opensource.org/licenses/MIT).
@@ -0,0 +1,26 @@
1
+ [package]
2
+ name = "css-inline-ruby"
3
+ version = "0.10.0"
4
+ authors = ["Dmitry Dygalo <dadygalo@gmail.com>"]
5
+ edition = "2021"
6
+ readme = "README.rdoc"
7
+ description = "High-performance library for inlining CSS into HTML 'style' attributes"
8
+ repository = "https://github.com/Stranger6667/css-inline"
9
+ keywords = ["css", "html", "email", "stylesheet", "inlining"]
10
+ categories = ["web-programming"]
11
+ license = "MIT"
12
+ rust-version = "1.61"
13
+
14
+ [lib]
15
+ name = "css_inline"
16
+ crate-type = ["cdylib"]
17
+
18
+ [dependencies]
19
+ magnus = "0.5"
20
+ rayon = "1"
21
+
22
+ [dependencies.css-inline]
23
+ path = "../../../../css-inline"
24
+ version = "*"
25
+ default-features = false
26
+ features = ["http", "file"]
@@ -0,0 +1,4 @@
1
+ require "mkmf"
2
+ require "rb_sys/mkmf"
3
+
4
+ create_rust_makefile("css_inline/css_inline")
@@ -0,0 +1,175 @@
1
+ //! Ruby bindings for css-inline
2
+ #![warn(
3
+ clippy::pedantic,
4
+ clippy::doc_markdown,
5
+ clippy::redundant_closure,
6
+ clippy::explicit_iter_loop,
7
+ clippy::match_same_arms,
8
+ clippy::needless_borrow,
9
+ clippy::print_stdout,
10
+ clippy::integer_arithmetic,
11
+ clippy::cast_possible_truncation,
12
+ clippy::unwrap_used,
13
+ clippy::map_unwrap_or,
14
+ clippy::trivially_copy_pass_by_ref,
15
+ clippy::needless_pass_by_value,
16
+ missing_debug_implementations,
17
+ trivial_casts,
18
+ trivial_numeric_casts,
19
+ unused_extern_crates,
20
+ unused_import_braces,
21
+ unused_qualifications,
22
+ variant_size_differences,
23
+ rust_2018_idioms,
24
+ rust_2018_compatibility
25
+ )]
26
+ use css_inline as rust_inline;
27
+ use magnus::{
28
+ class, define_module, function, method,
29
+ prelude::*,
30
+ scan_args::{get_kwargs, scan_args, Args},
31
+ RHash, Value,
32
+ };
33
+ use rayon::prelude::*;
34
+ use std::borrow::Cow;
35
+
36
+ type RubyResult<T> = Result<T, magnus::Error>;
37
+
38
+ fn parse_options<Req>(
39
+ args: &Args<Req, (), (), (), RHash, ()>,
40
+ ) -> RubyResult<rust_inline::InlineOptions<'static>> {
41
+ let kwargs = get_kwargs::<
42
+ _,
43
+ (),
44
+ (
45
+ Option<bool>,
46
+ Option<bool>,
47
+ Option<String>,
48
+ Option<bool>,
49
+ Option<String>,
50
+ Option<usize>,
51
+ ),
52
+ (),
53
+ >(
54
+ args.keywords,
55
+ &[],
56
+ &[
57
+ "keep_style_tags",
58
+ "keep_link_tags",
59
+ "base_url",
60
+ "load_remote_stylesheets",
61
+ "extra_css",
62
+ "preallocate_node_capacity",
63
+ ],
64
+ )?;
65
+ let kwargs = kwargs.optional;
66
+ Ok(rust_inline::InlineOptions {
67
+ keep_style_tags: kwargs.0.unwrap_or(false),
68
+ keep_link_tags: kwargs.1.unwrap_or(false),
69
+ base_url: parse_url(kwargs.2)?,
70
+ load_remote_stylesheets: kwargs.3.unwrap_or(true),
71
+ extra_css: kwargs.4.map(Cow::Owned),
72
+ preallocate_node_capacity: kwargs.5.unwrap_or(8),
73
+ })
74
+ }
75
+
76
+ #[magnus::wrap(class = "CSSInline::CSSInliner")]
77
+ struct CSSInliner {
78
+ inner: rust_inline::CSSInliner<'static>,
79
+ }
80
+
81
+ struct InlineErrorWrapper(rust_inline::InlineError);
82
+
83
+ impl From<InlineErrorWrapper> for magnus::Error {
84
+ fn from(error: InlineErrorWrapper) -> Self {
85
+ match error.0 {
86
+ rust_inline::InlineError::IO(error) => {
87
+ magnus::Error::new(magnus::exception::arg_error(), error.to_string())
88
+ }
89
+ rust_inline::InlineError::Network(error) => {
90
+ magnus::Error::new(magnus::exception::arg_error(), error.to_string())
91
+ }
92
+ rust_inline::InlineError::ParseError(message) => {
93
+ magnus::Error::new(magnus::exception::arg_error(), message.to_string())
94
+ }
95
+ rust_inline::InlineError::MissingStyleSheet { .. } => {
96
+ magnus::Error::new(magnus::exception::arg_error(), error.0.to_string())
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ struct UrlError(rust_inline::ParseError);
103
+
104
+ impl From<UrlError> for magnus::Error {
105
+ fn from(error: UrlError) -> magnus::Error {
106
+ magnus::Error::new(magnus::exception::arg_error(), error.0.to_string())
107
+ }
108
+ }
109
+
110
+ fn parse_url(url: Option<String>) -> RubyResult<Option<rust_inline::Url>> {
111
+ Ok(if let Some(url) = url {
112
+ Some(rust_inline::Url::parse(url.as_str()).map_err(UrlError)?)
113
+ } else {
114
+ None
115
+ })
116
+ }
117
+
118
+ impl CSSInliner {
119
+ fn new(args: &[Value]) -> RubyResult<CSSInliner> {
120
+ let args = scan_args::<(), _, _, _, _, _>(args)?;
121
+ let options = parse_options(&args)?;
122
+ Ok(CSSInliner {
123
+ inner: rust_inline::CSSInliner::new(options),
124
+ })
125
+ }
126
+
127
+ #[allow(clippy::needless_pass_by_value)]
128
+ fn inline(&self, html: String) -> RubyResult<String> {
129
+ Ok(self.inner.inline(&html).map_err(InlineErrorWrapper)?)
130
+ }
131
+
132
+ #[allow(clippy::needless_pass_by_value)]
133
+ fn inline_many(&self, html: Vec<String>) -> RubyResult<Vec<String>> {
134
+ inline_many_impl(&html, &self.inner)
135
+ }
136
+ }
137
+
138
+ fn inline(args: &[Value]) -> RubyResult<String> {
139
+ let args = scan_args::<(String,), _, _, _, _, _>(args)?;
140
+ let options = parse_options(&args)?;
141
+ let html = args.required.0;
142
+ Ok(rust_inline::CSSInliner::new(options)
143
+ .inline(&html)
144
+ .map_err(InlineErrorWrapper)?)
145
+ }
146
+
147
+ fn inline_many(args: &[Value]) -> RubyResult<Vec<String>> {
148
+ let args = scan_args::<(Vec<String>,), _, _, _, _, _>(args)?;
149
+ let options = parse_options(&args)?;
150
+ let inliner = rust_inline::CSSInliner::new(options);
151
+ inline_many_impl(&args.required.0, &inliner)
152
+ }
153
+
154
+ fn inline_many_impl(
155
+ htmls: &[String],
156
+ inliner: &rust_inline::CSSInliner<'static>,
157
+ ) -> RubyResult<Vec<String>> {
158
+ let output: Result<Vec<_>, _> = htmls.par_iter().map(|html| inliner.inline(html)).collect();
159
+ Ok(output.map_err(InlineErrorWrapper)?)
160
+ }
161
+
162
+ #[magnus::init(name = "css_inline")]
163
+ fn init() -> RubyResult<()> {
164
+ let module = define_module("CSSInline")?;
165
+
166
+ module.define_module_function("inline", function!(inline, -1))?;
167
+ module.define_module_function("inline_many", function!(inline_many, -1))?;
168
+
169
+ let class = module.define_class("CSSInliner", class::object())?;
170
+ class.define_singleton_method("new", function!(CSSInliner::new, -1))?;
171
+ class.define_method("inline", method!(CSSInliner::inline, 1))?;
172
+ class.define_method("inline_many", method!(CSSInliner::inline_many, 1))?;
173
+
174
+ Ok(())
175
+ }
data/lib/css_inline.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "css_inline/#{RUBY_VERSION.to_f}/css_inline"
5
+ rescue LoadError
6
+ require "css_inline/css_inline"
7
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: css_inline
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.10.0
5
+ platform: ruby
6
+ authors:
7
+ - Dmitry Dygalo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-06-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake-compiler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.2.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rb_sys
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.9'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.9'
41
+ - !ruby/object:Gem::Dependency
42
+ name: benchmark-ips
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.10'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.10'
55
+ - !ruby/object:Gem::Dependency
56
+ name: premailer
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.21'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.21'
69
+ - !ruby/object:Gem::Dependency
70
+ name: nokogiri
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.15'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.15'
83
+ description: |2
84
+ `css_inline` inlines CSS into HTML documents, using components from Mozilla's Servo project.
85
+ This process is essential for sending HTML emails as you need to use "style" attributes instead of "style" tags.
86
+ email:
87
+ - dadygalo@gmail.com
88
+ executables: []
89
+ extensions:
90
+ - ext/css_inline/extconf.rb
91
+ extra_rdoc_files: []
92
+ files:
93
+ - README.md
94
+ - ext/css_inline/Cargo.toml
95
+ - ext/css_inline/extconf.rb
96
+ - ext/css_inline/src/lib.rs
97
+ - lib/css_inline.rb
98
+ homepage: https://github.com/Stranger6667/css-inline
99
+ licenses:
100
+ - MIT
101
+ metadata:
102
+ bug_tracker_uri: https://github.com/Stranger6667/css-inline/issues
103
+ changelog_uri: https://github.com/Stranger6667/css-inline/tree/master/bindings/ruby/CHANGELOG.md
104
+ source_code_uri: https://github.com/Stranger6667/css-inline/tree/master/bindings/ruby
105
+ post_install_message:
106
+ rdoc_options:
107
+ - "--main"
108
+ - README.rdoc
109
+ - "--charset"
110
+ - utf-8
111
+ - "--exclude"
112
+ - ext/
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: 2.7.0
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: 3.3.26
125
+ requirements:
126
+ - Rust >= 1.61.0
127
+ rubygems_version: 3.4.14
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: High-performance library for inlining CSS into HTML 'style' attributes
131
+ test_files: []