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.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +211 -0
  3. data/LICENSE +21 -0
  4. data/README.md +263 -0
  5. data/ext/java/Ruby2htmlJrubyService.java +95 -0
  6. data/ext/ruby2html/attributes.c +200 -0
  7. data/ext/ruby2html/buffer.h +43 -0
  8. data/ext/ruby2html/content.c +120 -0
  9. data/ext/ruby2html/extconf.rb +47 -0
  10. data/ext/ruby2html/html_escape.c +252 -0
  11. data/ext/ruby2html/ruby2html.c +16 -0
  12. data/ext/ruby2html/ruby2html.h +32 -0
  13. data/ext/ruby2html/tag_render.c +179 -0
  14. data/lib/gem/ruby2html/component.rb +120 -0
  15. data/lib/gem/ruby2html/component_helper.rb +48 -0
  16. data/lib/gem/ruby2html/html_beautifier_middleware.rb +70 -0
  17. data/lib/gem/ruby2html/rails_components/base_component.rb +28 -0
  18. data/lib/gem/ruby2html/rails_components/button_to.rb +11 -0
  19. data/lib/gem/ruby2html/rails_components/form_with.rb +67 -0
  20. data/lib/gem/ruby2html/rails_components/image_tag.rb +11 -0
  21. data/lib/gem/ruby2html/rails_components/link_to.rb +11 -0
  22. data/lib/gem/ruby2html/rails_components/output_capture.rb +41 -0
  23. data/lib/gem/ruby2html/rails_components/template_collector.rb +62 -0
  24. data/lib/gem/ruby2html/rails_components/template_compiler.rb +92 -0
  25. data/lib/gem/ruby2html/rails_components/template_helpers.rb +77 -0
  26. data/lib/gem/ruby2html/rails_helper.rb +17 -0
  27. data/lib/gem/ruby2html/railtie.rb +119 -0
  28. data/lib/gem/ruby2html/render/pure_ruby.rb +219 -0
  29. data/lib/gem/ruby2html/render.rb +123 -0
  30. data/lib/gem/ruby2html/ruby2html_jruby.jar +0 -0
  31. data/lib/gem/ruby2html/version.rb +5 -0
  32. data/lib/gem/ruby2html.rb +34 -0
  33. 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
+ [![CI](https://github.com/sebyx07/ruby2html/actions/workflows/ci.yml/badge.svg)](https://github.com/sebyx07/ruby2html/actions/workflows/ci.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/ruby2html)](https://rubygems.org/gems/ruby2html)
5
+ [![Maintained by developerz.ai](https://img.shields.io/badge/maintained%20by-developerz.ai-6f42c1)](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 &amp; 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("&amp;");
39
+ private static final byte[] E_LT = bytes("&lt;");
40
+ private static final byte[] E_GT = bytes("&gt;");
41
+ private static final byte[] E_QUOT = bytes("&quot;");
42
+ private static final byte[] E_APOS = bytes("&#39;");
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
+ }