ruby2html 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ba119a7769924fec25bf5a7b80128f2086dd82fc3c81bd60b0d817e19a8232f
4
- data.tar.gz: b4decdfd8c01b34813e0d092de84d90abc64bbf78417ab38f4ff3e44719e3ac5
3
+ metadata.gz: 7c3bc5f1d40d12683edafbe453a291165275d9dc1b2552007a0d757954793a6a
4
+ data.tar.gz: 2fd1df901225789e4327576d414dd16be60bea5b9c2f11020c16a6ce0e7ef1cb
5
5
  SHA512:
6
- metadata.gz: 3e93a318476275d137fab0f8149df9ac26a9bdf78e6ddd23b63b630ee662d1e4f87aaa039b22f7f8dd3ebef1a38c334423714ebc3544e2d8cd02869574da5d69
7
- data.tar.gz: 5b57bb1eda59d05452e12c7b43161d3b210fd063c597f7ad42c3ccb1320b48fc5378ea2771ebc9a1185384e4ed29eb97e2e7372c697d235d33cbb1c6e93a979a
6
+ metadata.gz: bc63552f617578a8d98938490e564a7c483e7f6179faefa5800452b8ad90d6cc1dfa921f0626bfa2c1a4046e79f298facdef9c7fb337a1e3a481f46049c0fd7a
7
+ data.tar.gz: 2e25baa9f3ecce618f5eac307767e66ee9592216d441444e4fb1da39def3a7a454ac97f6a1b3fc007a3f402ac50eb5e2cdfee42bdb81719b44fe60376205c17c
data/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ Entries before 1.6.6 were reconstructed from `git log` and RubyGems release date
6
6
 
7
7
  ## [Unreleased]
8
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
+
9
22
  ## [2.0.0] - 2026-10-31
10
23
 
11
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.**
@@ -170,7 +183,8 @@ Published to RubyGems; no separate commits in the repository history.
170
183
  ### Added
171
184
  - Initial release: `Ruby2html::Render` DSL, `Ruby2html::RailsHelper#html` for ERB.
172
185
 
173
- [Unreleased]: https://github.com/sebyx07/ruby2html/compare/v2.0.0...HEAD
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
174
188
  [2.0.0]: https://github.com/sebyx07/ruby2html/releases/tag/v2.0.0
175
189
  [1.6.6]: https://rubygems.org/gems/ruby2html/versions/1.6.6
176
190
  [1.6.4]: https://rubygems.org/gems/ruby2html/versions/1.6.4
data/README.md CHANGED
@@ -23,12 +23,12 @@ end
23
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
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
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), and JRuby/TruffleRuby through a byte-identical pure-Ruby implementation. Rails 7.2+.
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
27
  - **AI-ready.** One-file reference for coding agents: [`llms.txt`](llms.txt).
28
28
 
29
29
  ## Installation
30
30
 
31
- Supports all maintained CRuby versions (>= 3.1), JRuby (via the pure-Ruby implementation), TruffleRuby, and Rails 7.2+; the CI matrix is generated automatically.
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
32
 
33
33
  ```ruby
34
34
  # Gemfile
@@ -208,7 +208,7 @@ This is a dated snapshot. CI has the live numbers. Measured on Ruby 3.4.7 x86_64
208
208
  |---|---|---|---|
209
209
  | **ruby2html 2.0 (C extension)** | **2,676** | **0.37 ms** | **419** |
210
210
  | Erubi 1.x (Rails' ERB engine) | 1,988 (1.35x slower) | 0.50 ms | 713 |
211
- | ruby2html 2.0 (pure Ruby: JRuby, no compiler) | ~820 | ~1.2 ms | 1,029 |
211
+ | ruby2html 2.0 (pure Ruby, no C extension; CRuby) | ~820 | ~1.2 ms | 1,029 |
212
212
  | Phlex 2.4, enlarged attribute cache¹ | ~1,250 (≈2.4x slower) | ~0.8 ms | 831 |
213
213
  | Phlex 2.4, defaults | 164 (16x slower) | 6.1 ms | 6,410 |
214
214
  | ruby2html 1.6.6 | ~620 | ~1.6 ms | 3,531 |
@@ -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
+ }
@@ -12,6 +12,10 @@ end
12
12
 
13
13
  skip_native_extension('RUBY2HTML_DISABLE_EXT=1') if ENV['RUBY2HTML_DISABLE_EXT'] == '1'
14
14
  skip_native_extension('JRuby does not load C extensions') if RUBY_ENGINE == 'jruby'
15
+ # TruffleRuby runs C extensions through Sulong (an LLVM interpreter) but gives
16
+ # pure Ruby the full Graal JIT, which here renders ~28x faster than the
17
+ # extension. Building it would only slow TruffleRuby down, so don't.
18
+ skip_native_extension('TruffleRuby is faster on the pure-Ruby renderer') if RUBY_ENGINE == 'truffleruby'
15
19
 
16
20
  require 'mkmf'
17
21
 
@@ -64,8 +64,10 @@ module Ruby2html
64
64
  # The buffer currently written to (RailsComponents swap it to capture blocks).
65
65
  attr_accessor :current_output
66
66
 
67
- # Rails' entry point: `render component` in views and controllers.
68
- def render_in(view_context, &block)
67
+ # Rails' entry point: `render component` in views and controllers. Rails 8.2+ passes
68
+ # `locals:` (render_in without options is deprecated there); a component takes its data
69
+ # through #initialize, so options are accepted and ignored.
70
+ def render_in(view_context, **_options, &block)
69
71
  buffer = String.new(capacity: self.class.__r2h_capacity, encoding: Encoding::UTF_8)
70
72
  content = block && -> { plain(view_context.capture(self, &block)) }
71
73
  render_into(buffer, view_context, content)
@@ -8,6 +8,16 @@ rescue LoadError
8
8
  nil
9
9
  end
10
10
 
11
+ if RUBY_ENGINE == 'jruby' && ENV['RUBY2HTML_DISABLE_EXT'] != '1'
12
+ begin
13
+ # Defines Ruby2html::JavaNative (a byte-level HTML escaper, ext/java).
14
+ # JRuby's only native code; PureRuby.escape uses it when present.
15
+ require 'ruby2html/ruby2html_jruby'
16
+ rescue LoadError
17
+ nil
18
+ end
19
+ end
20
+
11
21
  module Ruby2html
12
22
  # Pure-Ruby twin of the C extension's Ruby2html::Native (ext/ruby2html),
13
23
  # used on JRuby, when the extension isn't built, or with
@@ -33,6 +43,13 @@ module Ruby2html
33
43
  def escape(str)
34
44
  ERB::Escape.html_escape(utf8_compatible(str))
35
45
  end
46
+ elsif defined?(Ruby2html::JavaNative)
47
+ # JRuby: the Java escaper (ext/java). Byte-identical with the regex
48
+ # branch below (and with the C extension); also returns str itself
49
+ # when clean. utf8_compatible first, so it sees UTF-8/US-ASCII/BINARY.
50
+ def escape(str)
51
+ Ruby2html::JavaNative.escape(utf8_compatible(str))
52
+ end
36
53
  else
37
54
  def escape(str)
38
55
  str = utf8_compatible(str)
@@ -9,6 +9,10 @@ module Ruby2html
9
9
  # into lib/ruby2html.
10
10
  def self.load_native_extension
11
11
  return false if ENV['RUBY2HTML_DISABLE_EXT'] == '1'
12
+ # JRuby has no C extension (it uses a Java escaper over the pure-Ruby twin,
13
+ # see load_java_escaper). TruffleRuby *can* load it via Sulong, but renders
14
+ # ~28x slower that way than on pure Ruby under the Graal JIT, so never do.
15
+ return false if RUBY_ENGINE == 'jruby' || RUBY_ENGINE == 'truffleruby'
12
16
 
13
17
  begin
14
18
  require 'ruby2html/ruby2html'
@@ -16,7 +20,10 @@ module Ruby2html
16
20
  require_relative '../../ruby2html/ruby2html'
17
21
  end
18
22
  defined?(Native) ? true : false
19
- rescue LoadError
23
+ rescue LoadError => e
24
+ # On CRuby a missing extension means a broken build: say so instead of
25
+ # silently rendering about 3x slower.
26
+ warn "ruby2html: C extension not loaded (#{e.message}); using the pure-Ruby renderer"
20
27
  false
21
28
  end
22
29
  private_class_method :load_native_extension
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruby2html
4
- VERSION = '2.0.0'
4
+ VERSION = '2.1.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby2html
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - sebi
@@ -24,6 +24,7 @@ files:
24
24
  - CHANGELOG.md
25
25
  - LICENSE
26
26
  - README.md
27
+ - ext/java/Ruby2htmlJrubyService.java
27
28
  - ext/ruby2html/attributes.c
28
29
  - ext/ruby2html/buffer.h
29
30
  - ext/ruby2html/content.c
@@ -73,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
73
74
  - !ruby/object:Gem::Version
74
75
  version: '0'
75
76
  requirements: []
76
- rubygems_version: 3.6.9
77
+ rubygems_version: 4.0.20
77
78
  specification_version: 4
78
79
  summary: Transform Ruby code into beautiful, structured HTML with C-optimized performance
79
80
  test_files: []