ruby2html 2.1.0-java
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 +7 -0
- data/CHANGELOG.md +211 -0
- data/LICENSE +21 -0
- data/README.md +263 -0
- data/ext/java/Ruby2htmlJrubyService.java +95 -0
- data/ext/ruby2html/attributes.c +200 -0
- data/ext/ruby2html/buffer.h +43 -0
- data/ext/ruby2html/content.c +120 -0
- data/ext/ruby2html/extconf.rb +47 -0
- data/ext/ruby2html/html_escape.c +252 -0
- data/ext/ruby2html/ruby2html.c +16 -0
- data/ext/ruby2html/ruby2html.h +32 -0
- data/ext/ruby2html/tag_render.c +179 -0
- data/lib/gem/ruby2html/component.rb +120 -0
- data/lib/gem/ruby2html/component_helper.rb +48 -0
- data/lib/gem/ruby2html/html_beautifier_middleware.rb +70 -0
- data/lib/gem/ruby2html/rails_components/base_component.rb +28 -0
- data/lib/gem/ruby2html/rails_components/button_to.rb +11 -0
- data/lib/gem/ruby2html/rails_components/form_with.rb +67 -0
- data/lib/gem/ruby2html/rails_components/image_tag.rb +11 -0
- data/lib/gem/ruby2html/rails_components/link_to.rb +11 -0
- data/lib/gem/ruby2html/rails_components/output_capture.rb +41 -0
- data/lib/gem/ruby2html/rails_components/template_collector.rb +62 -0
- data/lib/gem/ruby2html/rails_components/template_compiler.rb +92 -0
- data/lib/gem/ruby2html/rails_components/template_helpers.rb +77 -0
- data/lib/gem/ruby2html/rails_helper.rb +17 -0
- data/lib/gem/ruby2html/railtie.rb +119 -0
- data/lib/gem/ruby2html/render/pure_ruby.rb +219 -0
- data/lib/gem/ruby2html/render.rb +123 -0
- data/lib/gem/ruby2html/ruby2html_jruby.jar +0 -0
- data/lib/gem/ruby2html/version.rb +5 -0
- data/lib/gem/ruby2html.rb +34 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 678f6ff9d5e9fe3533aa2cb06c1534ae03dbbe59ef05e7d8ea5b30b6d289ea4c
|
|
4
|
+
data.tar.gz: 5010cb4cf951235866dacf21613f745315ae084c198476c050bab6616899c10e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d71d3a65e68d5bb6771c5d988290641969c0580f589b95b25a64926c898753403b685ef452bbb5b18db818fd33bf75015fcbfacd7fa849540184f0ac8f46f68d
|
|
7
|
+
data.tar.gz: 9bb3546ffe322f98f374ae3fb01622d45485266d2269e74a786957883683a1145a0923cfe1ddaac2e4197a2277d54698c5d827b0cdb1e64ce7db8fc044be7516
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning: [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
Entries before 1.6.6 were reconstructed from `git log` and RubyGems release dates (the repo has no git tags for them); they are best-effort summaries.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [2.1.0] - 2026-10-31
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- JRuby now ships a small Java extension (`Ruby2html::JavaNative`) that accelerates HTML escaping — the renderer's hot path — over the pure-Ruby implementation. About 1.9x faster end-to-end rendering on JRuby; output is byte-identical. Distributed as a `ruby2html-<version>-java` platform gem with the jar prebuilt (no JDK needed to install); everything else stays pure Ruby.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `Ruby2html::Component#render_in` accepts Rails' keyword options (`locals:`), as Rails main (8.2) passes them and deprecates `render_in` without options.
|
|
18
|
+
- TruffleRuby now uses the pure-Ruby implementation instead of the C extension. TruffleRuby runs C extensions through Sulong but JITs pure Ruby with Graal, which here renders ~28x faster; `Ruby2html.native?` is now `false` on TruffleRuby.
|
|
19
|
+
- On CRuby, a C extension that fails to load now prints one warning with the reason (`ruby2html: C extension not loaded (...); using the pure-Ruby renderer`) instead of silently rendering about 3x slower. JRuby/TruffleRuby and `RUBY2HTML_DISABLE_EXT=1` stay silent.
|
|
20
|
+
- Development and single-version CI jobs now run on the latest stable Ruby (4.0). The CRuby test matrix already tracks the official maintained-versions list automatically (currently 3.3, 3.4, 4.0).
|
|
21
|
+
|
|
22
|
+
## [2.0.0] - 2026-10-31
|
|
23
|
+
|
|
24
|
+
A rewrite of the renderer for speed, memory and correctness, plus a new component class, Rails 8.1 / ViewComponent 4 support, JRuby/TruffleRuby support and full CI/release automation. **Contains breaking changes: read "Upgrading to 2.0" below.**
|
|
25
|
+
|
|
26
|
+
### Performance
|
|
27
|
+
|
|
28
|
+
Measured on Ruby 3.4.7 x86_64 (`bench/render_bench.rb`, a 100-item page with attributes and escaped text):
|
|
29
|
+
|
|
30
|
+
- About 4.7x faster than 1.6.6 (≈620 → ≈2,950 renders/s) with about 8x fewer allocations per render (3,531 → 419).
|
|
31
|
+
- Faster than Erubi (Rails' ERB engine) and Phlex on the same page; see the README snapshot and the CI benchmark workflow for current numbers.
|
|
32
|
+
- HTML escaping uses SSE2 (x86_64) / NEON (arm64): up to 5.9x faster on large clean text, about 2x on typical strings.
|
|
33
|
+
- `require 'ruby2html'` costs about 0.4 MB and 19 ms (was about 9.5 MB and 116 ms): Prism, the Rails glue and htmlbeautifier load only when used; in eager-loading (production) Rails apps they load at boot instead.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `Ruby2html::Component`: a lightweight component base class (no ViewComponent needed) with `view_template`/`call`, `render_in`, `helpers` and `content`; nested components write straight into the parent's buffer.
|
|
38
|
+
- Pure-Ruby implementation with byte-identical output (`Ruby2html::PureRuby`), used on JRuby, when the extension can't be built, or with `RUBY2HTML_DISABLE_EXT=1`. `Ruby2html.native?` and `Ruby2html::IMPLEMENTATION` tell which one is loaded.
|
|
39
|
+
- `Ruby2html.escape_html(obj)`.
|
|
40
|
+
- Attributes: `true` renders a bare attribute, `false` omits it; nested `data:`/`aria:` Hashes expand to `data-user-id="1"`; Arrays render as space-separated tokens (`class: ['btn', active && 'on']`).
|
|
41
|
+
- `.html.rb` templates are compiled with Prism to run directly in the view/component (no instance-variable copying, no `method_missing`).
|
|
42
|
+
- `capture`, `content_for` and `provide` with blocks of ruby2html tags work in `.html.rb` templates.
|
|
43
|
+
- Support for Rails 8.1, ViewComponent 4, JRuby and TruffleRuby; fiber-safe rendering (no thread/fiber-global state).
|
|
44
|
+
- `llms.txt`: complete reference for AI coding tools; `AGENTS.md` (with `CLAUDE.md` symlinked to it) and Claude Code skills/agents for maintainers.
|
|
45
|
+
- CI: generated CRuby matrix (all maintained versions), Rails matrix, Linux x86_64/arm64 and macOS builds, `-Werror` gcc/clang builds, sanitizers, JRuby/TruffleRuby, gem install smoke test, benchmarks in every PR; Dependabot; releases published automatically when a version bump merges (RubyGems trusted publishing).
|
|
46
|
+
- Maintained by [developerz.ai](https://developerz.ai): its maintainer agent triages issues, reviews PRs, merges on green, runs daily health and release checkups, and opens release PRs (`.dz/`).
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- Attribute cache keyed by `Object#hash`: a hash collision could render another element's attributes; the cache also grew without bound. Removed.
|
|
51
|
+
- `link_to` rendered a `button_to` form.
|
|
52
|
+
- `form_with` emitted its fields as escaped text, ignored `patch`/`delete` (`_method`), crashed on `submit` without a label, and ignored Rails' CSRF/`data-remote` settings. It now delegates to Rails' own `form_with` and `FormBuilder`.
|
|
53
|
+
- Tags inside `link_to`/`button_to` blocks were emitted before the link instead of inside it.
|
|
54
|
+
- `html_safe` content was escaped twice; non-String content (`p 42`) was silently dropped.
|
|
55
|
+
- A block's String return value replaced markup the block had already written.
|
|
56
|
+
- The `<output>` tag was unusable (shadowed by `Render#output`).
|
|
57
|
+
- Template backtraces were off by 3 lines; the renderer wasn't restored when a template raised; template locals could clash with internal variables or be rewritten into tag calls (`h2 title` in a partial with a `title` local).
|
|
58
|
+
- A rescued exception while writing attributes left a half-written start tag in the output.
|
|
59
|
+
- `HtmlBeautifierMiddleware` beautified each streamed chunk separately, didn't close the body, and touched gzip/204/304 responses.
|
|
60
|
+
- The packaged gem shipped a locally compiled `.so`; `rake build` didn't build a gem.
|
|
61
|
+
|
|
62
|
+
### Upgrading to 2.0
|
|
63
|
+
|
|
64
|
+
Templates and components:
|
|
65
|
+
1. `self` in `.html.rb` templates is now the view (or component), not a `Ruby2html::Render`. Replace `self.div` / `self.plain` with `div` / `plain`. Without Prism (Ruby < 3.3 and no `prism` gem) templates still use the 1.x path.
|
|
66
|
+
2. `Thread.current[:__ruby2html_renderer__]` is gone. The active renderer lives on the component/view; use `component.__ruby2html_renderer__`.
|
|
67
|
+
3. `form_with` output now matches Rails (`_method`, `accept-charset`, no forced `data-remote`, no `utf8` field); its builder is a proxy around Rails' `FormBuilder`.
|
|
68
|
+
4. `Ruby2html::ComponentHelper#method_missing` is now private.
|
|
69
|
+
|
|
70
|
+
HTML output:
|
|
71
|
+
5. Non-String content renders: `p 42` → `<p>42</p>` (1.x: `<p></p>`); `nil` renders nothing.
|
|
72
|
+
6. `html_safe` strings passed as content or returned from a block are inserted raw (1.x escaped them).
|
|
73
|
+
7. A block's return value is used only if the block wrote nothing, and only for String/Symbol/Numeric (1.x: a returned String replaced everything the block wrote).
|
|
74
|
+
8. Attributes: `true` → bare attribute and `false` omitted (except `data-*`, `aria-*`, `draggable`, `spellcheck`, `contenteditable`, which keep `"true"`/`"false"`); Hash values expand; Arrays join with spaces; invalid attribute names raise `ArgumentError`; Hashes/Arrays nested deeper than 32 levels raise `ArgumentError`.
|
|
75
|
+
9. Tag methods accept at most one positional argument (extra arguments raise `ArgumentError`).
|
|
76
|
+
10. The output buffer is UTF-8; strings in other encodings are transcoded (raising `EncodingError` if impossible).
|
|
77
|
+
|
|
78
|
+
Removed / packaging:
|
|
79
|
+
11. `Ruby2html::ATTRIBUTE_CACHE`, `Render::METHOD_DEFINITIONS`, `Render#output` and the `Render#fast_escape_html` / `#fast_attributes_to_s` / `#fast_buffer_append` / `#fast_render_tag` methods are removed. Use `Ruby2html.escape_html`, the value returned by `render`, or `current_output`.
|
|
80
|
+
12. Ruby >= 3.1 and (for Rails integration) Rails >= 7.2 are required.
|
|
81
|
+
13. `htmlbeautifier` is no longer a dependency: add `gem 'htmlbeautifier'` if you use `Ruby2html::HtmlBeautifierMiddleware`.
|
|
82
|
+
14. The gem's require path is `lib/gem` only.
|
|
83
|
+
|
|
84
|
+
## [1.6.6] - 2025-02-10
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
- Stop shipping a prebuilt `lib/ruby2html/ruby2html.so`; the extension is compiled on install (`*.so` gitignored).
|
|
88
|
+
|
|
89
|
+
## [1.6.4] - 2025-02-09
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
- Renderer fixes following the C extension introduction.
|
|
93
|
+
|
|
94
|
+
## [1.6.3] - 2025-02-09
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
- Template handler, `form_with` and gemspec fixes for the C extension build.
|
|
98
|
+
|
|
99
|
+
## [1.6.2] - 2025-02-09
|
|
100
|
+
## [1.6.1] - 2025-02-09
|
|
101
|
+
|
|
102
|
+
Published to RubyGems; no separate commits in the repository history.
|
|
103
|
+
|
|
104
|
+
## [1.6.0] - 2025-02-09
|
|
105
|
+
|
|
106
|
+
### Added
|
|
107
|
+
- C extension (`ext/ruby2html`) for HTML escaping, attribute serialization and buffer appends; `rake compile` via rake-compiler.
|
|
108
|
+
|
|
109
|
+
## [1.5.8] - 2024-11-01
|
|
110
|
+
## [1.5.7] - 2024-11-01
|
|
111
|
+
|
|
112
|
+
### Changed
|
|
113
|
+
- Load `htmlbeautifier` from the main entry file; renderer cleanup. Dependency updates.
|
|
114
|
+
|
|
115
|
+
## [1.5.6] - 2024-08-17
|
|
116
|
+
## [1.5.5] - 2024-08-17
|
|
117
|
+
|
|
118
|
+
### Changed
|
|
119
|
+
- Template handler declares `handles_encoding?`; Railtie cleanup.
|
|
120
|
+
|
|
121
|
+
## [1.5.4] - 2024-08-12
|
|
122
|
+
|
|
123
|
+
### Changed
|
|
124
|
+
- Gemspec cleanup.
|
|
125
|
+
|
|
126
|
+
## [1.5.3] - 2024-08-12
|
|
127
|
+
|
|
128
|
+
### Added
|
|
129
|
+
- `turbo_stream` tag (`<turbo-stream>`).
|
|
130
|
+
|
|
131
|
+
## [1.5.2] - 2024-08-12
|
|
132
|
+
|
|
133
|
+
### Added
|
|
134
|
+
- `turbo_frame` tag (`<turbo-frame>`).
|
|
135
|
+
|
|
136
|
+
## [1.5.1] - 2024-08-12
|
|
137
|
+
|
|
138
|
+
### Fixed
|
|
139
|
+
- `link_to` wrapper simplified.
|
|
140
|
+
|
|
141
|
+
## [1.5.0] - 2024-08-12
|
|
142
|
+
|
|
143
|
+
### Added
|
|
144
|
+
- Rails helper wrappers: `form_with` (with `label`, `text_field`, `hidden_field`, `password_field`, `file_field`, `submit`), `button_to`, `link_to`, `image_tag`.
|
|
145
|
+
|
|
146
|
+
## [1.4.0] - 2024-08-10
|
|
147
|
+
|
|
148
|
+
### Added
|
|
149
|
+
- `Ruby2html::HtmlBeautifierMiddleware`; runtime dependency on `htmlbeautifier`.
|
|
150
|
+
|
|
151
|
+
## [1.3.3] - 2024-08-10
|
|
152
|
+
|
|
153
|
+
### Added
|
|
154
|
+
- Support for `config.action_view.annotate_rendered_view_with_filenames`.
|
|
155
|
+
|
|
156
|
+
## [1.3.2] - 2024-08-09
|
|
157
|
+
|
|
158
|
+
### Fixed
|
|
159
|
+
- Keyword option handling in tag methods.
|
|
160
|
+
|
|
161
|
+
## [1.3.1] - 2024-08-09
|
|
162
|
+
|
|
163
|
+
### Fixed
|
|
164
|
+
- `ComponentHelper` renderer bug.
|
|
165
|
+
|
|
166
|
+
## [1.3.0] - 2024-08-09
|
|
167
|
+
|
|
168
|
+
### Added
|
|
169
|
+
- Calling component methods from component templates; nested renderer tracking.
|
|
170
|
+
|
|
171
|
+
## [1.2.0] - 2024-08-09
|
|
172
|
+
|
|
173
|
+
### Added
|
|
174
|
+
- ViewComponent support: `.html.rb` component templates, `Ruby2html::ComponentHelper`.
|
|
175
|
+
|
|
176
|
+
## [1.1.0] - 2024-08-08
|
|
177
|
+
|
|
178
|
+
### Added
|
|
179
|
+
- `.rb` ActionView template handler (`*.html.rb` views and partials).
|
|
180
|
+
|
|
181
|
+
## [1.0.0] - 2024-08-08
|
|
182
|
+
|
|
183
|
+
### Added
|
|
184
|
+
- Initial release: `Ruby2html::Render` DSL, `Ruby2html::RailsHelper#html` for ERB.
|
|
185
|
+
|
|
186
|
+
[Unreleased]: https://github.com/sebyx07/ruby2html/compare/v2.1.0...HEAD
|
|
187
|
+
[2.1.0]: https://github.com/sebyx07/ruby2html/compare/v2.0.0...v2.1.0
|
|
188
|
+
[2.0.0]: https://github.com/sebyx07/ruby2html/releases/tag/v2.0.0
|
|
189
|
+
[1.6.6]: https://rubygems.org/gems/ruby2html/versions/1.6.6
|
|
190
|
+
[1.6.4]: https://rubygems.org/gems/ruby2html/versions/1.6.4
|
|
191
|
+
[1.6.3]: https://rubygems.org/gems/ruby2html/versions/1.6.3
|
|
192
|
+
[1.6.2]: https://rubygems.org/gems/ruby2html/versions/1.6.2
|
|
193
|
+
[1.6.1]: https://rubygems.org/gems/ruby2html/versions/1.6.1
|
|
194
|
+
[1.6.0]: https://rubygems.org/gems/ruby2html/versions/1.6.0
|
|
195
|
+
[1.5.8]: https://rubygems.org/gems/ruby2html/versions/1.5.8
|
|
196
|
+
[1.5.7]: https://rubygems.org/gems/ruby2html/versions/1.5.7
|
|
197
|
+
[1.5.6]: https://rubygems.org/gems/ruby2html/versions/1.5.6
|
|
198
|
+
[1.5.5]: https://rubygems.org/gems/ruby2html/versions/1.5.5
|
|
199
|
+
[1.5.4]: https://rubygems.org/gems/ruby2html/versions/1.5.4
|
|
200
|
+
[1.5.3]: https://rubygems.org/gems/ruby2html/versions/1.5.3
|
|
201
|
+
[1.5.2]: https://rubygems.org/gems/ruby2html/versions/1.5.2
|
|
202
|
+
[1.5.1]: https://rubygems.org/gems/ruby2html/versions/1.5.1
|
|
203
|
+
[1.5.0]: https://rubygems.org/gems/ruby2html/versions/1.5.0
|
|
204
|
+
[1.4.0]: https://rubygems.org/gems/ruby2html/versions/1.4.0
|
|
205
|
+
[1.3.3]: https://rubygems.org/gems/ruby2html/versions/1.3.3
|
|
206
|
+
[1.3.2]: https://rubygems.org/gems/ruby2html/versions/1.3.2
|
|
207
|
+
[1.3.1]: https://rubygems.org/gems/ruby2html/versions/1.3.1
|
|
208
|
+
[1.3.0]: https://rubygems.org/gems/ruby2html/versions/1.3.0
|
|
209
|
+
[1.2.0]: https://rubygems.org/gems/ruby2html/versions/1.2.0
|
|
210
|
+
[1.1.0]: https://rubygems.org/gems/ruby2html/versions/1.1.0
|
|
211
|
+
[1.0.0]: https://rubygems.org/gems/ruby2html/versions/1.0.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 sebi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Ruby2html
|
|
2
|
+
|
|
3
|
+
[](https://github.com/sebyx07/ruby2html/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/ruby2html)
|
|
5
|
+
[](https://developerz.ai)
|
|
6
|
+
|
|
7
|
+
Write views in pure Ruby; get escaped, compact HTML. A C extension handles escaping, attribute serialization and tag rendering. Works standalone, as Rails `.html.rb` templates, inside ERB, and in ViewComponent.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
div class: 'card', 'data-controller': 'card' do
|
|
11
|
+
h2 @product.title
|
|
12
|
+
p @product.description
|
|
13
|
+
a 'Details', href: product_path(@product), class: 'btn'
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<div class="card" data-controller="card"><h2>Lamp</h2><p>Warm & bright</p><a href="/products/1" class="btn">Details</a></div>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Why ruby2html
|
|
22
|
+
|
|
23
|
+
- **Fastest Ruby HTML renderer we've measured.** The page benchmark renders 1.35x faster than Erubi (Rails' ERB engine) and 2.4–16x faster than Phlex 2, with 40–90% fewer allocations; in a full Rails request it leads ERB, Slim and Phlex ([numbers](#performance)).
|
|
24
|
+
- **Light.** No runtime dependencies, a 32 KB gem, and `require 'ruby2html'` costs about 0.4 MB and 19 ms. Prism, the Rails glue and the optional beautifier load only when used, or at boot in eager-loading production apps.
|
|
25
|
+
- **Plain Ruby views.** `.html.rb` templates, ERB interop, ViewComponent, or the built-in `Ruby2html::Component`. Escaped by default.
|
|
26
|
+
- **Runs everywhere.** CRuby ≥ 3.1 with a C extension (SIMD escaping on x86_64 and arm64), JRuby with a Java escaper over the pure-Ruby implementation, and TruffleRuby on pure Ruby (faster there than the C extension) — all byte-identical. Rails 7.2+.
|
|
27
|
+
- **AI-ready.** One-file reference for coding agents: [`llms.txt`](llms.txt).
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Supports all maintained CRuby versions (>= 3.1; CI currently covers 3.3, 3.4 and 4.0), JRuby (pure Ruby plus a Java escaper), TruffleRuby (pure Ruby), and Rails 7.2+; the CI matrix is generated automatically, so new Ruby releases are picked up without edits.
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# Gemfile
|
|
35
|
+
gem 'ruby2html'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bundle install # compiles the native extension (needs a C compiler); pure-Ruby fallback otherwise
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## AI agents
|
|
43
|
+
|
|
44
|
+
**This project is maintained by [developerz.ai](https://developerz.ai).** Its maintainer agent triages issues, reviews every pull request, merges every pull request once its author marks it ready and CI passes, watches project health daily, and opens release PRs. The policy is in [`.dz/maintainer/maintainer.yml`](.dz/maintainer/maintainer.yml). Breaking changes and major releases stay with the owner.
|
|
45
|
+
|
|
46
|
+
- **Using ruby2html in your app?** Point your agent at [`llms.txt`](llms.txt), the complete reference in one file (DSL rules, escaping, Rails, components, forms, pitfalls). Paste this into your app's `CLAUDE.md` / `AGENTS.md`:
|
|
47
|
+
```markdown
|
|
48
|
+
## ruby2html views
|
|
49
|
+
`*.html.rb` templates and `html do ... end` blocks use the ruby2html gem. Before editing them read
|
|
50
|
+
https://github.com/sebyx07/ruby2html/blob/main/llms.txt
|
|
51
|
+
```
|
|
52
|
+
- **Working on this repo?** [`AGENTS.md`](AGENTS.md) is the only agent-instructions file (`CLAUDE.md` is a symlink to it): commands, code map, invariants, how to verify a change, CI/release automation.
|
|
53
|
+
- **Claude Code**: project skills in `.claude/skills/` (`release`, `benchmark`, `add-html-tag`, `review`, `debug-render`, `upgrade-deps`), read-only reviewer subagents in `.claude/agents/` (`perf-reviewer`, `xss-auditor`), and `.claude/settings.json` with a safe permission allowlist and a non-blocking recompile-reminder hook.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
### DSL basics
|
|
58
|
+
|
|
59
|
+
- Every HTML5 tag is a method (`div`, `p`, `ul`, `li`, `img`, `input`, ...), plus `turbo_frame` / `turbo_stream`.
|
|
60
|
+
- `tag content, **attrs` or `tag(**attrs) { nested tags }`. Text and attribute values are HTML-escaped; numbers and symbols are converted (`td 42`).
|
|
61
|
+
- Raw HTML only from `html_safe` strings (Rails helpers, `'<hr>'.html_safe`), as content or via `plain`. `plain 'text'` escapes.
|
|
62
|
+
- Attributes: `'data-id': 1` or `data: { id: 1 }`, `class: ['btn', active && 'on']`, `disabled: true` → bare `disabled`, `nil`/`false` omitted.
|
|
63
|
+
|
|
64
|
+
Full rules and pitfalls: [`llms.txt`](llms.txt).
|
|
65
|
+
|
|
66
|
+
### Rails views (`.html.rb`)
|
|
67
|
+
|
|
68
|
+
File: `app/views/products/index.html.rb`
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
div class: 'container' do
|
|
72
|
+
h1 'Products', class: 'main-title'
|
|
73
|
+
link_to 'Home', root_path, class: 'btn btn-primary', 'data-turbo': false
|
|
74
|
+
|
|
75
|
+
@products.each do |product|
|
|
76
|
+
h2 product.title, class: 'item-title', id: "product-#{product.id}"
|
|
77
|
+
p product.description, class: 'item-description'
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
plain '<div>Inline html</div>'.html_safe
|
|
82
|
+
render partial: 'shared/navbar'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Controller instance variables are available. Partials can be `.html.rb` too. `link_to`, `image_tag`, `button_to`, `form_with` and `render` write their output directly; other helpers return a value you must output (`h1 t('.title')`, `plain some_safe_html_helper`).
|
|
86
|
+
|
|
87
|
+
### Inside ERB
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
class ApplicationController < ActionController::Base
|
|
91
|
+
include Ruby2html::RailsHelper # exposes the html(context) helper to views
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```erb
|
|
96
|
+
<h1>Legacy ERB header</h1>
|
|
97
|
+
<%= html(self) do
|
|
98
|
+
div class: 'ruby2html-section' do
|
|
99
|
+
h2 'This section is powered by Ruby2html'
|
|
100
|
+
@items.each { |item| p item[:title] }
|
|
101
|
+
end
|
|
102
|
+
end %>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
ERB, Slim and Ruby2html views, partials and components can be mixed freely, so adoption can be gradual.
|
|
106
|
+
|
|
107
|
+
### ViewComponent
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
class ApplicationComponent < ViewComponent::Base
|
|
111
|
+
include Ruby2html::ComponentHelper
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Inline `call`:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
class GreetingComponent < ApplicationComponent
|
|
119
|
+
def initialize(name)
|
|
120
|
+
@name = name
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def call
|
|
124
|
+
html do
|
|
125
|
+
h1 "Hello, #{@name}!", class: 'greeting', 'data-user': @name
|
|
126
|
+
p 'Welcome to Ruby2html!', class: 'welcome-message'
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Or a sidecar template, `app/components/farewell_component.html.rb`:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
div class: 'farewell' do
|
|
136
|
+
h1 "Goodbye, #{@name}!", class: 'farewell-message'
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Public component methods can be called from the template and may emit tags themselves (see `app/components/first_component.rb`).
|
|
141
|
+
|
|
142
|
+
### `Ruby2html::Component` (no ViewComponent required)
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
class CardComponent < Ruby2html::Component
|
|
146
|
+
def initialize(title:)
|
|
147
|
+
@title = title
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def view_template
|
|
151
|
+
div class: 'card' do
|
|
152
|
+
h2 @title
|
|
153
|
+
content # block passed to render
|
|
154
|
+
link_to 'More', helpers.root_path
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# any view or controller
|
|
160
|
+
render CardComponent.new(title: 'Hi')
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Tag methods run on the component itself (no instance-variable copying); nested ruby2html components write into the parent's buffer.
|
|
164
|
+
|
|
165
|
+
### Forms
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
form_with model: @user do |f|
|
|
169
|
+
f.label :email
|
|
170
|
+
f.text_field :email
|
|
171
|
+
f.submit 'Save'
|
|
172
|
+
end
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Without Rails
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
require 'ruby2html'
|
|
179
|
+
|
|
180
|
+
Ruby2html::Render.new do # optional context object: its public methods and ivars become available
|
|
181
|
+
html do
|
|
182
|
+
head { title 'Ruby2html Example' }
|
|
183
|
+
body { h1 'Hello, World!' }
|
|
184
|
+
end
|
|
185
|
+
end.render
|
|
186
|
+
# => "<html><head><title>Ruby2html Example</title></head><body><h1>Hello, World!</h1></body></html>"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Pretty HTML in development
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
# Gemfile (development group): gem 'htmlbeautifier'
|
|
193
|
+
# config/environments/development.rb
|
|
194
|
+
config.middleware.use Ruby2html::HtmlBeautifierMiddleware
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Performance
|
|
198
|
+
|
|
199
|
+
Benchmarks run in CI on every push to `main` and every pull request ([`.github/workflows/benchmark.yml`](.github/workflows/benchmark.yml)): the latest run's summary page has the current table, and PRs get a sticky comment with the delta against `main`. Run them locally with `bundle exec rake bench` (standalone scripts in `bench/`) or `bundle exec rspec spec/benchmarks/` (request-level through the demo app, vs ERB, Slim, Phlex and ViewComponent).
|
|
200
|
+
|
|
201
|
+
### Snapshot at 2.0.0 release (2026-10-31)
|
|
202
|
+
|
|
203
|
+
This is a dated snapshot. CI has the live numbers. Measured on Ruby 3.4.7 x86_64 on a busy machine (load average about 7), so read the ratios rather than the absolute figures.
|
|
204
|
+
|
|
205
|
+
**Page render** (`bench/render_bench.rb`, no Rails): a header with nav, 100 list items with links, images, attributes and escaped text, about 34 KB of HTML.
|
|
206
|
+
|
|
207
|
+
| Engine | Renders/s | Time per render | Allocations per render |
|
|
208
|
+
|---|---|---|---|
|
|
209
|
+
| **ruby2html 2.0 (C extension)** | **2,676** | **0.37 ms** | **419** |
|
|
210
|
+
| Erubi 1.x (Rails' ERB engine) | 1,988 (1.35x slower) | 0.50 ms | 713 |
|
|
211
|
+
| ruby2html 2.0 (pure Ruby, no C extension; CRuby) | ~820 | ~1.2 ms | 1,029 |
|
|
212
|
+
| Phlex 2.4, enlarged attribute cache¹ | ~1,250 (≈2.4x slower) | ~0.8 ms | 831 |
|
|
213
|
+
| Phlex 2.4, defaults | 164 (16x slower) | 6.1 ms | 6,410 |
|
|
214
|
+
| ruby2html 1.6.6 | ~620 | ~1.6 ms | 3,531 |
|
|
215
|
+
|
|
216
|
+
¹ Phlex 2's default attribute cache (2 KB, FIFO) misses on per-row values such as ids and hrefs. `Phlex::ATTRIBUTE_CACHE.expand(1_000_000)` is its best case. Phlex 2 also checks URLs for `javascript:`; ruby2html leaves URL validation to you.
|
|
217
|
+
|
|
218
|
+
**Full Rails request** (`spec/benchmarks/requests_spec.rb`; Rails 8.1, a 7 ms request where most of the time is Rails itself; differences are within ±15% error):
|
|
219
|
+
|
|
220
|
+
| Engine | Requests/s |
|
|
221
|
+
|---|---|
|
|
222
|
+
| **`Ruby2html::Component`** | **144.5** |
|
|
223
|
+
| **ViewComponent 4 + ruby2html** | **142.2** |
|
|
224
|
+
| **`.html.rb` template** | **139.6** |
|
|
225
|
+
| Slim | 128.3 |
|
|
226
|
+
| ERB | 122.6 |
|
|
227
|
+
| Phlex 2.4 | 116.6 |
|
|
228
|
+
|
|
229
|
+
**HTML escaping** (SIMD vs scalar): up to 5.9x faster on 10 KB clean text, about 2x on 200-byte strings; short strings take the scalar path.
|
|
230
|
+
|
|
231
|
+
**Footprint:**
|
|
232
|
+
|
|
233
|
+
| | Ruby/C lines | Runtime dependencies | `require` cost |
|
|
234
|
+
|---|---|---|---|
|
|
235
|
+
| **ruby2html** | 1.1k Ruby + 0.8k C | none | ~0.4 MB, ~19 ms |
|
|
236
|
+
| phlex + phlex-rails | 6.2k | refract, zeitwerk, railties | ~1.4 MB, ~30 ms (phlex alone) |
|
|
237
|
+
| view_component | 5.3k | actionview, activesupport, prism | |
|
|
238
|
+
| slim | 7.3k | temple, tilt | |
|
|
239
|
+
|
|
240
|
+
How it is fast:
|
|
241
|
+
|
|
242
|
+
- **Tag methods in C**: each tag writes open tag, content/block output and close tag straight into one shared buffer (no nested buffers, no `*args`/`**kwargs` allocation without keywords).
|
|
243
|
+
- **HTML escaping**: 16-byte SSE2/NEON scan for `& < > " '`; clean strings are one `memcpy`, dirty strings get their exact size computed once.
|
|
244
|
+
- **Attributes**: written directly into the buffer, no intermediate strings, no cache to thrash or leak.
|
|
245
|
+
- **Lazy loading**: `require 'ruby2html'` loads only the renderer. Production Rails apps load everything at boot, so forked workers share it copy-on-write.
|
|
246
|
+
- **Templates and components**: `.html.rb` templates are rewritten at compile time (Prism) to run in the view itself; `Ruby2html::Component` runs tag methods on the component. No instance-variable copying or `method_missing` per render.
|
|
247
|
+
- A pure-Ruby implementation with identical output is used when the extension is unavailable.
|
|
248
|
+
|
|
249
|
+
## Development
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
bundle install
|
|
253
|
+
bundle exec rake compile # build the C extension
|
|
254
|
+
CI=1 bundle exec rspec # specs (CI=1 skips the slow benchmark spec)
|
|
255
|
+
bundle exec rubocop
|
|
256
|
+
bin/rails server # demo app
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The repository is both the gem (`lib/gem/`, `ext/ruby2html/`) and a Rails demo app (`app/`, `config/`). See [CONTRIBUTING.md](CONTRIBUTING.md), [docs/architecture.md](docs/architecture.md), [docs/release.md](docs/release.md) and [CHANGELOG.md](CHANGELOG.md).
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// JRuby's only native code: a byte-level HTML escaper, the renderer's hottest
|
|
2
|
+
// primitive. Everything else (tag methods, attributes, content rules) stays in
|
|
3
|
+
// the pure-Ruby twin (lib/gem/ruby2html/render/pure_ruby.rb), which JRuby's JIT
|
|
4
|
+
// optimizes well; on TruffleRuby even the C extension loses to pure Ruby, so
|
|
5
|
+
// only JRuby gets this. The C extension (ext/ruby2html/html_escape.c) is the
|
|
6
|
+
// reference; this must stay byte-identical with it and with PureRuby.escape.
|
|
7
|
+
//
|
|
8
|
+
// require 'ruby2html/ruby2html_jruby' loads this: JRuby maps the base name
|
|
9
|
+
// ruby2html_jruby to the class Ruby2htmlJrubyService (CamelCase + "Service") in
|
|
10
|
+
// the default package, so the name and package here are load-bearing.
|
|
11
|
+
//
|
|
12
|
+
// Built by rake-compiler's Rake::JavaExtensionTask (see Rakefile) into
|
|
13
|
+
// lib/gem/ruby2html/ruby2html_jruby.jar and shipped in the -java platform gem.
|
|
14
|
+
|
|
15
|
+
import org.jruby.Ruby;
|
|
16
|
+
import org.jruby.RubyModule;
|
|
17
|
+
import org.jruby.RubyString;
|
|
18
|
+
import org.jruby.anno.JRubyMethod;
|
|
19
|
+
import org.jruby.runtime.ThreadContext;
|
|
20
|
+
import org.jruby.runtime.builtin.IRubyObject;
|
|
21
|
+
import org.jruby.runtime.load.BasicLibraryService;
|
|
22
|
+
import org.jruby.util.ByteList;
|
|
23
|
+
|
|
24
|
+
public class Ruby2htmlJrubyService implements BasicLibraryService {
|
|
25
|
+
// getOrCreateModule/defineModuleUnder/defineAnnotatedMethods are marked
|
|
26
|
+
// deprecated on JRuby 10 but remain the load-service API and still work; the
|
|
27
|
+
// context-taking replacements are not available on the JRuby versions in CI.
|
|
28
|
+
@SuppressWarnings("deprecation")
|
|
29
|
+
public boolean basicLoad(final Ruby runtime) {
|
|
30
|
+
final RubyModule javaNative =
|
|
31
|
+
runtime.getOrCreateModule("Ruby2html").defineModuleUnder("JavaNative");
|
|
32
|
+
javaNative.defineAnnotatedMethods(Escaper.class);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public static final class Escaper {
|
|
37
|
+
private static final byte AMP = '&', LT = '<', GT = '>', QUOT = '"', APOS = '\'';
|
|
38
|
+
private static final byte[] E_AMP = bytes("&");
|
|
39
|
+
private static final byte[] E_LT = bytes("<");
|
|
40
|
+
private static final byte[] E_GT = bytes(">");
|
|
41
|
+
private static final byte[] E_QUOT = bytes(""");
|
|
42
|
+
private static final byte[] E_APOS = bytes("'");
|
|
43
|
+
|
|
44
|
+
private static byte[] bytes(final String s) {
|
|
45
|
+
final byte[] b = new byte[s.length()];
|
|
46
|
+
for (int i = 0; i < s.length(); i++) b[i] = (byte) s.charAt(i);
|
|
47
|
+
return b;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private static boolean escapable(final byte b) {
|
|
51
|
+
return b == AMP || b == LT || b == GT || b == QUOT || b == APOS;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Ruby2html::JavaNative.escape(str) -> String
|
|
55
|
+
//
|
|
56
|
+
// str has already been run through PureRuby.utf8_compatible, so its
|
|
57
|
+
// encoding is UTF-8, US-ASCII or BINARY. All five escapable characters
|
|
58
|
+
// are ASCII, so escaping byte by byte is identical across those
|
|
59
|
+
// encodings and for bytes that are invalid UTF-8 (the C extension and
|
|
60
|
+
// PureRuby both pass such bytes through). The result keeps the input's
|
|
61
|
+
// encoding. A clean string is returned unchanged (same object), which
|
|
62
|
+
// PureRuby.escape_html relies on to avoid a copy.
|
|
63
|
+
@JRubyMethod(name = "escape", module = true)
|
|
64
|
+
public static IRubyObject escape(final ThreadContext ctx, final IRubyObject self,
|
|
65
|
+
final IRubyObject arg) {
|
|
66
|
+
final RubyString str = (RubyString) arg;
|
|
67
|
+
final ByteList in = str.getByteList();
|
|
68
|
+
final byte[] src = in.getUnsafeBytes();
|
|
69
|
+
final int begin = in.getBegin();
|
|
70
|
+
final int len = in.getRealSize();
|
|
71
|
+
|
|
72
|
+
int first = -1;
|
|
73
|
+
for (int i = 0; i < len; i++) {
|
|
74
|
+
if (escapable(src[begin + i])) { first = i; break; }
|
|
75
|
+
}
|
|
76
|
+
if (first < 0) return str; // nothing to escape: hand back the same string
|
|
77
|
+
|
|
78
|
+
final ByteList out = new ByteList(len + 16);
|
|
79
|
+
out.setEncoding(in.getEncoding());
|
|
80
|
+
out.append(src, begin, first);
|
|
81
|
+
for (int i = first; i < len; i++) {
|
|
82
|
+
final byte b = src[begin + i];
|
|
83
|
+
switch (b) {
|
|
84
|
+
case AMP: out.append(E_AMP); break;
|
|
85
|
+
case LT: out.append(E_LT); break;
|
|
86
|
+
case GT: out.append(E_GT); break;
|
|
87
|
+
case QUOT: out.append(E_QUOT); break;
|
|
88
|
+
case APOS: out.append(E_APOS); break;
|
|
89
|
+
default: out.append(b);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return RubyString.newString(ctx.runtime, out);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|