Skip to content

Commit 770ffe8

Browse files
committed
feat: Ruby bindings
1 parent 7702b8f commit 770ffe8

File tree

16 files changed

+657
-1
lines changed

16 files changed

+657
-1
lines changed

.github/workflows/build.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ jobs:
9898
run: cargo clippy -- -D warnings
9999
working-directory: ./bindings/python
100100

101+
- name: Ruby
102+
run: cargo clippy -- -D warnings
103+
working-directory: ./bindings/ruby
104+
101105
- name: WASM
102106
run: cargo clippy -- -D warnings
103107
working-directory: ./bindings/wasm
@@ -152,6 +156,32 @@ jobs:
152156
run: tox -e py
153157
working-directory: ./bindings/python
154158

159+
test-ruby:
160+
strategy:
161+
fail-fast: false
162+
matrix:
163+
os: [ubuntu-22.04, macos-12, windows-2022]
164+
ruby-version: ['2.7', '3.2']
165+
exclude:
166+
- os: windows-2022
167+
ruby-version: "3.2"
168+
169+
name: Ruby ${{ matrix.ruby-version }} on ${{ matrix.os }}
170+
runs-on: ${{ matrix.os }}
171+
steps:
172+
- uses: actions/checkout@v3
173+
- name: Set up Ruby & Rust
174+
uses: oxidize-rb/actions/setup-ruby-and-rust@main
175+
with:
176+
ruby-version: ${{ matrix.ruby-version }}
177+
rubygems: "latest"
178+
bundler-cache: true
179+
cargo-cache: true
180+
cache-version: v1
181+
working-directory: ./bindings/ruby
182+
- run: bundle exec rake test
183+
working-directory: ./bindings/ruby
184+
155185
test-wasm:
156186
name: Tests for WASM crate
157187
runs-on: ubuntu-22.04

.github/workflows/ruby-release.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: "[Ruby] Release"
2+
3+
on:
4+
push:
5+
tags:
6+
- ruby-v*
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-22.04
11+
12+
steps:
13+
- uses: actions/checkout@v3
14+
15+
- name: Release Gem
16+
if: contains(github.ref, 'refs/tags/v')
17+
uses: cadwallion/publish-rubygems-action@1
18+
env:
19+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20+
RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
21+
RELEASE_COMMAND: gem build *.gemspec && gem push *.gem

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,11 @@ fn main() -> Result<(), css_inline::InlineError> {
151151

152152
## Bindings
153153

154-
We provide bindings for Python and WebAssembly. Check the `bindings` directory for more information.
154+
`css-inline` is primarily a Rust library, but we also provide bindings for several other languages:
155+
156+
- [Python](https://github.com/Stranger6667/css-inline/tree/master/bindings/python)
157+
- [Ruby](https://github.com/Stranger6667/css-inline/tree/master/bindings/ruby)
158+
- [WebAssembly](https://github.com/Stranger6667/css-inline/tree/master/bindings/wasm)
155159

156160
## Command Line Interface
157161

bindings/ruby/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Changelog
2+
3+
## [Unreleased]
4+
5+
## 0.9.0 - 2023-06-??
6+
7+
- Initial public release
8+
9+
[Unreleased]: https://github.com/Stranger6667/css-inline/compare/ruby-v0.9.0...HEAD

bindings/ruby/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[workspace]
2+
members = ["ext/css_inline"]

bindings/ruby/Gemfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
source "https://rubygems.org"
4+
5+
gemspec
6+
7+
gem 'rspec'

bindings/ruby/Gemfile.lock

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
PATH
2+
remote: .
3+
specs:
4+
css_inline (0.10.0)
5+
6+
GEM
7+
remote: https://rubygems.org/
8+
specs:
9+
addressable (2.8.4)
10+
public_suffix (>= 2.0.2, < 6.0)
11+
benchmark-ips (2.12.0)
12+
css_parser (1.14.0)
13+
addressable
14+
diff-lcs (1.5.0)
15+
htmlentities (4.3.4)
16+
mini_portile2 (2.8.2)
17+
nokogiri (1.15.2)
18+
mini_portile2 (~> 2.8.2)
19+
racc (~> 1.4)
20+
premailer (1.21.0)
21+
addressable
22+
css_parser (>= 1.12.0)
23+
htmlentities (>= 4.0.0)
24+
public_suffix (5.0.1)
25+
racc (1.7.1)
26+
rake (13.0.6)
27+
rake-compiler (1.2.3)
28+
rake
29+
rb_sys (0.9.79)
30+
rspec (3.12.0)
31+
rspec-core (~> 3.12.0)
32+
rspec-expectations (~> 3.12.0)
33+
rspec-mocks (~> 3.12.0)
34+
rspec-core (3.12.2)
35+
rspec-support (~> 3.12.0)
36+
rspec-expectations (3.12.3)
37+
diff-lcs (>= 1.2.0, < 2.0)
38+
rspec-support (~> 3.12.0)
39+
rspec-mocks (3.12.5)
40+
diff-lcs (>= 1.2.0, < 2.0)
41+
rspec-support (~> 3.12.0)
42+
rspec-support (3.12.0)
43+
44+
PLATFORMS
45+
ruby
46+
47+
DEPENDENCIES
48+
benchmark-ips (~> 2.10)
49+
css_inline!
50+
nokogiri (~> 1.15)
51+
premailer (~> 1.21)
52+
rake-compiler (~> 1.2.0)
53+
rb_sys (~> 0.9)
54+
rspec
55+
56+
BUNDLED WITH
57+
2.4.10

bindings/ruby/README.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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).

bindings/ruby/Rakefile

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
require "rake/extensiontask"
4+
require 'rspec/core/rake_task'
5+
6+
task default: :spec
7+
8+
spec = Bundler.load_gemspec("css_inline.gemspec")
9+
spec.requirements.clear
10+
spec.required_ruby_version = nil
11+
spec.required_rubygems_version = nil
12+
spec.extensions.clear
13+
spec.files -= Dir["ext/**/*"]
14+
15+
Rake::ExtensionTask.new("css_inline", spec) do |c|
16+
c.lib_dir = "lib/css_inline"
17+
c.cross_compile = true
18+
c.cross_platform = [
19+
"aarch64-linux",
20+
"arm64-darwin",
21+
"x64-mingw-ucrt",
22+
"x64-mingw32",
23+
"x86_64-darwin",
24+
"x86_64-linux",
25+
"x86_64-linux-musl"]
26+
end
27+
28+
task :dev do
29+
ENV["RB_SYS_CARGO_PROFILE"] = "dev"
30+
end
31+
32+
RSpec::Core::RakeTask.new(:test) do |task|
33+
task.rspec_opts = [ "-f documentation" ]
34+
end
35+
36+
task test: :compile
37+
38+
task bench: :compile do
39+
ruby "test/bench.rb"
40+
end

bindings/ruby/css_inline.gemspec

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
Gem::Specification.new do |spec|
4+
spec.name = "css_inline"
5+
spec.version = "0.10.0"
6+
spec.summary = "High-performance library for inlining CSS into HTML 'style' attributes"
7+
spec.description = <<-EOF
8+
`css_inline` inlines CSS into HTML documents, using components from Mozilla's Servo project.
9+
This process is essential for sending HTML emails as you need to use "style" attributes instead of "style" tags.
10+
EOF
11+
spec.files = Dir["lib/**/*.rb"].concat(Dir["ext/css_inline/src/**/*.rs"]) << "ext/css_inline/Cargo.toml" << "README.md"
12+
spec.extensions = ["ext/css_inline/extconf.rb"]
13+
spec.rdoc_options = ["--main", "README.rdoc", "--charset", "utf-8", "--exclude", "ext/"]
14+
spec.authors = ["Dmitry Dygalo"]
15+
spec.email = ["[email protected]"]
16+
spec.homepage = "https://github.com/Stranger6667/css-inline"
17+
spec.license = "MIT"
18+
spec.metadata = {
19+
"bug_tracker_uri" => "https://github.com/Stranger6667/css-inline/issues",
20+
"changelog_uri" => "https://github.com/Stranger6667/css-inline/tree/master/bindings/ruby/CHANGELOG.md",
21+
"source_code_uri" => "https://github.com/Stranger6667/css-inline/tree/master/bindings/ruby",
22+
}
23+
24+
spec.requirements = ["Rust >= 1.61.0"]
25+
spec.required_ruby_version = ">= 2.7.0"
26+
spec.required_rubygems_version = ">= 3.3.26"
27+
28+
spec.add_development_dependency "rake-compiler", "~> 1.2.0"
29+
spec.add_development_dependency "rb_sys", "~> 0.9"
30+
spec.add_development_dependency "benchmark-ips", "~> 2.10"
31+
spec.add_development_dependency "premailer", "~> 1.21"
32+
spec.add_development_dependency "nokogiri", "~> 1.15"
33+
end

0 commit comments

Comments
 (0)