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 +4 -4
- data/CHANGELOG.md +15 -1
- data/README.md +3 -3
- data/ext/java/Ruby2htmlJrubyService.java +95 -0
- data/ext/ruby2html/extconf.rb +4 -0
- data/lib/gem/ruby2html/component.rb +4 -2
- data/lib/gem/ruby2html/render/pure_ruby.rb +17 -0
- data/lib/gem/ruby2html/render.rb +8 -1
- data/lib/gem/ruby2html/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c3bc5f1d40d12683edafbe453a291165275d9dc1b2552007a0d757954793a6a
|
|
4
|
+
data.tar.gz: 2fd1df901225789e4327576d414dd16be60bea5b9c2f11020c16a6ce0e7ef1cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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),
|
|
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 (
|
|
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
|
|
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("&");
|
|
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
|
+
}
|
data/ext/ruby2html/extconf.rb
CHANGED
|
@@ -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
|
-
|
|
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)
|
data/lib/gem/ruby2html/render.rb
CHANGED
|
@@ -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
|
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.
|
|
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:
|
|
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: []
|