css_inline 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
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: []