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
@@ -0,0 +1,32 @@
1
+ #ifndef RUBY2HTML_H
2
+ #define RUBY2HTML_H
3
+
4
+ #include <ruby.h>
5
+ #include <ruby/encoding.h>
6
+
7
+ /* Interned at Init so hot paths never call rb_intern. */
8
+ extern ID r2h_id_current_output;
9
+ extern ID r2h_id_html_safe_p;
10
+
11
+ /* html_escape.c */
12
+ void r2h_init_escape(VALUE native);
13
+ VALUE r2h_utf8_compatible(VALUE str);
14
+ void r2h_append_escaped(VALUE buf, VALUE str);
15
+ void r2h_append_escaped_wrapped(VALUE buf, const char *before, long before_len, VALUE str,
16
+ const char *after, long after_len);
17
+ void r2h_append_raw(VALUE buf, VALUE str);
18
+
19
+ /* attributes.c */
20
+ void r2h_append_attributes(VALUE buf, VALUE attrs);
21
+
22
+ /* content.c */
23
+ void r2h_init_content(VALUE native);
24
+ VALUE r2h_current_buffer(VALUE self);
25
+ void r2h_append_long(VALUE buf, long value);
26
+ void r2h_append_text(VALUE buf, VALUE obj);
27
+ void r2h_append_block_result(VALUE buf, VALUE result);
28
+
29
+ /* tag_render.c */
30
+ void r2h_init_tags(VALUE native);
31
+
32
+ #endif
@@ -0,0 +1,179 @@
1
+ /*
2
+ * Tag methods (div, p, img, ...) implemented in C.
3
+ *
4
+ * Each tag is a C method, so a call costs no Ruby frame, no *args or **kwargs
5
+ * allocation when called without keywords, and block content is rendered
6
+ * straight into the shared buffer (open tag, yield, close tag) instead of
7
+ * into a nested buffer that is copied afterwards.
8
+ *
9
+ * The tag list itself lives in Ruby (Render::HTML5_TAGS). A C method can't
10
+ * carry data, so define_tag binds each tag to a slot of a fixed pool of
11
+ * one-line trampolines, each of which passes its slot to render_tag.
12
+ */
13
+ #include "buffer.h"
14
+
15
+ #define R2H_MAX_TAGS 200
16
+ #define R2H_MAX_TAG_NAME 32
17
+
18
+ typedef struct {
19
+ char open[R2H_MAX_TAG_NAME + 5]; /* "<div>", or "<br />" for void tags, + NUL */
20
+ char close[R2H_MAX_TAG_NAME + 4]; /* "</div>" + NUL */
21
+ long open_len;
22
+ long name_len; /* "<div" is open[0, name_len + 1) */
23
+ long close_len;
24
+ int is_void;
25
+ ID method;
26
+ } r2h_tag;
27
+
28
+ static r2h_tag tags[R2H_MAX_TAGS];
29
+ static int tag_count;
30
+
31
+ typedef struct {
32
+ VALUE buf;
33
+ VALUE attrs;
34
+ } attrs_args;
35
+
36
+ static VALUE append_attributes_protected(VALUE data)
37
+ {
38
+ const attrs_args *args = (const attrs_args *)data;
39
+ r2h_append_attributes(args->buf, args->attrs);
40
+ return Qnil;
41
+ }
42
+
43
+ static void open_tag(VALUE buf, const r2h_tag *tag, VALUE attrs)
44
+ {
45
+ if (NIL_P(attrs)) {
46
+ r2h_buf_cat(buf, tag->open, tag->open_len);
47
+ return;
48
+ }
49
+ /*
50
+ * If an attribute raises (invalid name, a value's to_s raising) and the
51
+ * caller rescues, a half-written `<div class="x` would turn whatever is
52
+ * appended next into markup. Roll the buffer back to before the tag.
53
+ */
54
+ const long mark = RSTRING_LEN(buf);
55
+ r2h_buf_cat(buf, tag->open, tag->name_len + 1);
56
+ attrs_args args = { buf, attrs };
57
+ int state = 0;
58
+ rb_protect(append_attributes_protected, (VALUE)&args, &state);
59
+ if (state) {
60
+ /*
61
+ * rb_str_resize (not set_len): the failing to_s may have replaced,
62
+ * shared or shrunk the buffer. Leave a frozen buffer alone so the
63
+ * original exception propagates.
64
+ */
65
+ if (!OBJ_FROZEN(buf) && RSTRING_LEN(buf) >= mark) rb_str_resize(buf, mark);
66
+ rb_jump_tag(state);
67
+ }
68
+ if (tag->is_void) {
69
+ R2H_BUF_CAT_LIT(buf, " />");
70
+ } else {
71
+ R2H_BUF_CAT_LIT(buf, ">");
72
+ }
73
+ }
74
+
75
+ /*
76
+ * tag(content = nil, **attributes) { ... }
77
+ *
78
+ * With a block, the block's output is the content (content is ignored);
79
+ * void tags ignore content and block.
80
+ */
81
+ static VALUE render_tag(int argc, VALUE *argv, VALUE self, const r2h_tag *tag)
82
+ {
83
+ VALUE attrs = Qnil, content = Qnil;
84
+
85
+ if (argc > 0 && rb_keyword_given_p()) {
86
+ attrs = argv[--argc];
87
+ if (RHASH_EMPTY_P(attrs)) attrs = Qnil;
88
+ }
89
+ if (argc > 1) rb_error_arity(argc, 0, 1);
90
+ if (argc == 1) content = argv[0];
91
+
92
+ VALUE buf = r2h_current_buffer(self);
93
+ const int block_given = rb_block_given_p();
94
+
95
+ /* Fast path for the most common shape, `li "text"`: one buffer write. */
96
+ if (NIL_P(attrs) && !block_given && !tag->is_void && RB_TYPE_P(content, T_STRING) &&
97
+ RBASIC_CLASS(content) == rb_cString) {
98
+ r2h_append_escaped_wrapped(buf, tag->open, tag->open_len, content, tag->close, tag->close_len);
99
+ return Qnil;
100
+ }
101
+
102
+ open_tag(buf, tag, attrs);
103
+ if (tag->is_void) return Qnil;
104
+
105
+ if (block_given) {
106
+ const long before = RSTRING_LEN(buf);
107
+ VALUE result = rb_yield_values2(0, NULL);
108
+ if (RSTRING_LEN(buf) == before) r2h_append_block_result(buf, result);
109
+ } else {
110
+ r2h_append_text(buf, content);
111
+ }
112
+ r2h_buf_cat(buf, tag->close, tag->close_len);
113
+ RB_GC_GUARD(buf);
114
+ return Qnil;
115
+ }
116
+
117
+ #define R2H_TRAMPOLINE(n) \
118
+ static VALUE tag_##n(int argc, VALUE *argv, VALUE self) { return render_tag(argc, argv, self, &tags[n]); }
119
+ #define R2H_TRAMPOLINE_REF(n) tag_##n,
120
+ #define R2H_TEN(M, d) M(d##0) M(d##1) M(d##2) M(d##3) M(d##4) M(d##5) M(d##6) M(d##7) M(d##8) M(d##9)
121
+ #define R2H_TWO_HUNDRED(M) \
122
+ R2H_TEN(M, ) R2H_TEN(M, 1) R2H_TEN(M, 2) R2H_TEN(M, 3) R2H_TEN(M, 4) R2H_TEN(M, 5) R2H_TEN(M, 6) \
123
+ R2H_TEN(M, 7) R2H_TEN(M, 8) R2H_TEN(M, 9) R2H_TEN(M, 10) R2H_TEN(M, 11) R2H_TEN(M, 12) R2H_TEN(M, 13) \
124
+ R2H_TEN(M, 14) R2H_TEN(M, 15) R2H_TEN(M, 16) R2H_TEN(M, 17) R2H_TEN(M, 18) R2H_TEN(M, 19)
125
+
126
+ R2H_TWO_HUNDRED(R2H_TRAMPOLINE)
127
+
128
+ typedef VALUE (*r2h_tag_fn)(int, VALUE *, VALUE);
129
+ static const r2h_tag_fn trampolines[R2H_MAX_TAGS] = { R2H_TWO_HUNDRED(R2H_TRAMPOLINE_REF) };
130
+
131
+ /* Tag names are written unescaped, so only allow [a-z0-9-]. */
132
+ static void validate_tag_name(VALUE name)
133
+ {
134
+ const char *p = RSTRING_PTR(name);
135
+ const long len = RSTRING_LEN(name);
136
+
137
+ if (len == 0 || len > R2H_MAX_TAG_NAME) rb_raise(rb_eArgError, "invalid tag name: %+" PRIsVALUE, name);
138
+ for (long i = 0; i < len; i++) {
139
+ const char c = p[i];
140
+ if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-')) {
141
+ rb_raise(rb_eArgError, "invalid tag name: %+" PRIsVALUE, name);
142
+ }
143
+ }
144
+ }
145
+
146
+ static r2h_tag *slot_for(ID method)
147
+ {
148
+ for (int i = 0; i < tag_count; i++) {
149
+ if (tags[i].method == method) return &tags[i];
150
+ }
151
+ if (tag_count == R2H_MAX_TAGS) rb_raise(rb_eRuntimeError, "ruby2html: more than %d tags", R2H_MAX_TAGS);
152
+ return &tags[tag_count++];
153
+ }
154
+
155
+ /* Ruby2html::Native.define_tag(method_name, tag_name, void) */
156
+ static VALUE native_define_tag(VALUE native, VALUE method_name, VALUE tag_name, VALUE is_void)
157
+ {
158
+ const ID method = rb_to_id(method_name);
159
+ StringValue(tag_name);
160
+ validate_tag_name(tag_name);
161
+
162
+ r2h_tag *tag = slot_for(method);
163
+ const long len = RSTRING_LEN(tag_name);
164
+ tag->method = method;
165
+ tag->is_void = RTEST(is_void);
166
+ tag->name_len = len;
167
+ tag->open_len = snprintf(tag->open, sizeof(tag->open), tag->is_void ? "<%.*s />" : "<%.*s>", (int)len,
168
+ RSTRING_PTR(tag_name));
169
+ tag->close_len = snprintf(tag->close, sizeof(tag->close), "</%.*s>", (int)len, RSTRING_PTR(tag_name));
170
+
171
+ /* rb_define_method (not _id): TruffleRuby doesn't implement rb_define_method_id. */
172
+ rb_define_method(native, rb_id2name(method), trampolines[tag - tags], -1);
173
+ return Qnil;
174
+ }
175
+
176
+ void r2h_init_tags(VALUE native)
177
+ {
178
+ rb_define_singleton_method(native, "define_tag", native_define_tag, 3);
179
+ }
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'render'
4
+
5
+ module Ruby2html
6
+ # Lightweight component without ViewComponent:
7
+ #
8
+ # class CardComponent < Ruby2html::Component
9
+ # def initialize(title:) = @title = title
10
+ #
11
+ # def view_template
12
+ # div class: 'card' do
13
+ # h2 @title
14
+ # content # block given to render, if any
15
+ # link_to 'More', helpers.root_path
16
+ # end
17
+ # end
18
+ # end
19
+ #
20
+ # render CardComponent.new(title: 'Hi') # ERB, controllers, .html.rb
21
+ #
22
+ # Why it is fast: the tag methods are mixed straight into the component and
23
+ # write into a buffer it is handed, so templates run with +self+ = the
24
+ # component (own ivars, no copying, no instance_exec, no method_missing).
25
+ # Nested in another Ruby2html component or .html.rb template it writes into
26
+ # the parent's buffer directly; only the outermost render allocates one.
27
+ #
28
+ # Define +view_template+ (or +call+). +template+ is accepted when a subclass
29
+ # overrides it, but shadows the <template> tag for that component.
30
+ class Component
31
+ include Ruby2html::IMPLEMENTATION
32
+
33
+ INITIAL_CAPACITY = 1024
34
+
35
+ # Rails helpers whose markup must land in the component's buffer. The
36
+ # component itself is the RailsComponents "renderer" (it has #plain and
37
+ # #current_output). Constants are resolved lazily: they need ActionView.
38
+ Render::RAILS_HELPER_COMPONENTS.each do |name, klass|
39
+ define_method(name) do |*args, **options, &block|
40
+ RailsComponents.const_get(klass).new(self, helpers, name, *args, **options).render(&block)
41
+ end
42
+ end
43
+
44
+ class << self
45
+ # Entry method, resolved once per class.
46
+ def __r2h_entry
47
+ @__r2h_entry ||=
48
+ if method_defined?(:view_template) then :view_template
49
+ elsif method_defined?(:call) then :call
50
+ elsif instance_method(:template).owner != Ruby2html::IMPLEMENTATION then :template
51
+ else raise NotImplementedError, "#{name} must define #view_template (or #call)"
52
+ end
53
+ end
54
+
55
+ # Last output size, so the next top-level render pre-sizes its buffer.
56
+ # A benign race between threads only affects the capacity hint.
57
+ attr_writer :__r2h_capacity
58
+
59
+ def __r2h_capacity
60
+ @__r2h_capacity || INITIAL_CAPACITY
61
+ end
62
+ end
63
+
64
+ # The buffer currently written to (RailsComponents swap it to capture blocks).
65
+ attr_accessor :current_output
66
+
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)
71
+ buffer = String.new(capacity: self.class.__r2h_capacity, encoding: Encoding::UTF_8)
72
+ content = block && -> { plain(view_context.capture(self, &block)) }
73
+ render_into(buffer, view_context, content)
74
+ self.class.__r2h_capacity = buffer.bytesize if buffer.bytesize > self.class.__r2h_capacity
75
+ defined?(ActiveSupport::SafeBuffer) ? ActiveSupport::SafeBuffer.new(buffer) : buffer
76
+ end
77
+
78
+ # Writes the component into +buffer+ (a parent's output) and returns nil.
79
+ # +content+ is a callable run by #content. Re-entrant: state is restored.
80
+ def render_into(buffer, view_context, content = nil)
81
+ previous_output = @current_output
82
+ previous_helpers = @__r2h_helpers
83
+ previous_content = @__r2h_content
84
+ @current_output = buffer
85
+ @__r2h_helpers = view_context
86
+ @__r2h_content = content
87
+ __send__(self.class.__r2h_entry)
88
+ nil
89
+ ensure
90
+ @current_output = previous_output
91
+ @__r2h_helpers = previous_helpers
92
+ @__r2h_content = previous_content
93
+ end
94
+
95
+ # The view context (Rails helpers, routes, request, ...).
96
+ def helpers
97
+ return @__r2h_helpers if @__r2h_helpers
98
+
99
+ raise Ruby2html::Error, "#{self.class.name}#helpers is only available while rendering"
100
+ end
101
+
102
+ # Emits the block given to render (see #render_in / #render).
103
+ def content
104
+ @__r2h_content&.call
105
+ nil
106
+ end
107
+
108
+ # Renders another component into this buffer, or anything Rails can
109
+ # render (partials, ViewComponents) via the view context.
110
+ def render(*args, **options, &block)
111
+ first = args.first
112
+ if args.size == 1 && options.empty? && first.is_a?(Component)
113
+ first.render_into(@current_output, helpers, block)
114
+ else
115
+ plain(helpers.render(*args, **options, &block))
116
+ end
117
+ nil
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby2html
4
+ # Mixed into ViewComponents (or any object) to render with Ruby2html.
5
+ #
6
+ # The active renderer is stored on the component itself (+@__r2h+), never in
7
+ # thread/fiber-global state, so interleaved renders (fiber schedulers,
8
+ # Falcon, async) can't see each other's renderer. Component methods called
9
+ # while rendering (e.g. a helper that emits +div+) reach it through
10
+ # method_missing. +.html.rb+ templates set the same ivar on their view or
11
+ # component (see TemplateHandler).
12
+ #
13
+ # Each render saves the previous value and restores it in +ensure+, so
14
+ # re-entrant +html+ calls and exceptions leave no stale renderer behind.
15
+ # The ivar starts with '@_' so Render does not copy it into itself.
16
+ module ComponentHelper
17
+ def html(&block)
18
+ previous_renderer = @__r2h
19
+ renderer = Ruby2html::Render.new(self, &block)
20
+ @__r2h = renderer
21
+ output = renderer.render
22
+ # Render#render already returns a SafeBuffer under Rails; avoid copying it.
23
+ output.html_safe? ? output : output.html_safe
24
+ ensure
25
+ @__r2h = previous_renderer
26
+ end
27
+
28
+ # The renderer currently rendering this object, or nil.
29
+ def __ruby2html_renderer__
30
+ @__r2h
31
+ end
32
+
33
+ private
34
+ def method_missing(name, ...)
35
+ renderer = @__r2h
36
+ return super if renderer.nil? || !renderer.respond_to?(name)
37
+
38
+ renderer.__send__(name, ...)
39
+ end
40
+
41
+ # Only claims the renderer's own methods (tags, plain, component, ...).
42
+ # Asking the renderer's respond_to? here could recurse back into this
43
+ # object, because the renderer delegates respond_to? to its context.
44
+ def respond_to_missing?(name, include_private = false)
45
+ (@__r2h && Ruby2html::Render.method_defined?(name)) || super
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby2html
4
+ # Development/test middleware that re-indents HTML responses with
5
+ # HtmlBeautifier. Responses are left untouched unless they are buffered,
6
+ # uncompressed text/html with a body.
7
+ class HtmlBeautifierMiddleware
8
+ CONTENT_TYPE = 'content-type'
9
+ CONTENT_LENGTH = 'content-length'
10
+ CONTENT_ENCODING = 'content-encoding'
11
+ TRANSFER_ENCODING = 'transfer-encoding'
12
+
13
+ def initialize(app)
14
+ self.class.load_beautifier
15
+ @app = app
16
+ end
17
+
18
+ # htmlbeautifier is an optional dependency, only needed by this middleware.
19
+ def self.load_beautifier
20
+ require 'htmlbeautifier'
21
+ rescue LoadError => e
22
+ raise LoadError, "Ruby2html::HtmlBeautifierMiddleware needs the htmlbeautifier gem. Add `gem 'htmlbeautifier'` " \
23
+ "to your Gemfile (e.g. in the :development group) or remove the middleware. (#{e.message})"
24
+ end
25
+
26
+ def call(env)
27
+ status, headers, body = @app.call(env)
28
+ return [status, headers, body] unless beautify?(status, headers, body)
29
+
30
+ html = HtmlBeautifier.beautify(read_body(body))
31
+ set_header(headers, CONTENT_LENGTH, html.bytesize.to_s)
32
+ [status, headers, [html]]
33
+ end
34
+
35
+ private
36
+ def beautify?(status, headers, body)
37
+ status >= 200 && status != 204 && status != 304 &&
38
+ header(headers, CONTENT_TYPE)&.include?('text/html') &&
39
+ header(headers, CONTENT_ENCODING).nil? && # gzip etc.: bytes aren't HTML
40
+ header(headers, TRANSFER_ENCODING).nil? && # streamed by the app
41
+ body.respond_to?(:to_ary) # Rack 3: only buffered bodies; never drain a stream
42
+ end
43
+
44
+ # Beautify the document as a whole: chunk boundaries can fall inside tags.
45
+ # Per Rack spec the original body must be closed once consumed.
46
+ def read_body(body)
47
+ chunks = body.to_ary
48
+ chunks.size == 1 ? chunks.first.to_s : chunks.join
49
+ ensure
50
+ body.close if body.respond_to?(:close)
51
+ end
52
+
53
+ # Rack 3 headers are lowercase (and Rails uses case-insensitive
54
+ # Rack::Headers); fall back to a scan for plain hashes from older apps.
55
+ def header(headers, name)
56
+ key = header_key(headers, name)
57
+ key && headers[key]
58
+ end
59
+
60
+ def set_header(headers, name, value)
61
+ headers[header_key(headers, name) || name] = value
62
+ end
63
+
64
+ def header_key(headers, name)
65
+ return name if headers.key?(name)
66
+
67
+ headers.each_key.find { |key| key.casecmp?(name) }
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'output_capture'
4
+
5
+ module Ruby2html
6
+ module RailsComponents
7
+ # Calls the Rails helper named +method+ on the view context and appends its
8
+ # (html_safe) result to the renderer. A block given to the helper is
9
+ # captured from the renderer's buffer, so nested Ruby2html tags end up
10
+ # inside the helper's markup.
11
+ class BaseComponent
12
+ include OutputCapture
13
+
14
+ def initialize(render, context, method, *args, **options)
15
+ @render = render
16
+ @context = context
17
+ @method = method
18
+ @args = args
19
+ @options = options
20
+ end
21
+
22
+ def render(&block)
23
+ @render.plain(@context.public_send(@method, *@args, **@options, &capturing(block)))
24
+ nil
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_component'
4
+
5
+ module Ruby2html
6
+ module RailsComponents
7
+ # Delegates to Rails' +button_to+; see BaseComponent.
8
+ class ButtonTo < BaseComponent
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_component'
4
+
5
+ module Ruby2html
6
+ module RailsComponents
7
+ # Delegates to Rails' +form_with+ so the form tag, CSRF token, method
8
+ # spoofing (_method), multipart detection and URL resolution match Rails
9
+ # exactly. The yielded builder is wrapped in a FormBuilderProxy so that
10
+ # calls like +f.text_field :name+ write straight into the Ruby2html output.
11
+ class FormWith < BaseComponent
12
+ private
13
+ def block_arg(arg)
14
+ FormBuilderProxy.wrap(@render, arg)
15
+ end
16
+ end
17
+
18
+ # Forwards every call to the wrapped ActionView FormBuilder. Markup results
19
+ # (html_safe strings) are appended to the renderer and +nil+ is returned so
20
+ # a block's return value can't re-emit them; anything else (f.object,
21
+ # f.field_id, ...) is returned unchanged. Builder methods that clash with
22
+ # private Kernel methods (select, format, ...) still reach method_missing.
23
+ class FormBuilderProxy
24
+ include OutputCapture
25
+
26
+ def self.wrap(render, arg)
27
+ return arg unless defined?(ActionView::Helpers::FormBuilder) && arg.is_a?(ActionView::Helpers::FormBuilder)
28
+
29
+ new(render, arg)
30
+ end
31
+
32
+ def initialize(render, builder)
33
+ @render = render
34
+ @builder = builder
35
+ end
36
+
37
+ # The underlying ActionView::Helpers::FormBuilder.
38
+ def __builder__
39
+ @builder
40
+ end
41
+
42
+ def method_missing(name, ...)
43
+ return super unless @builder.respond_to?(name)
44
+
45
+ forward(name, ...)
46
+ end
47
+
48
+ def respond_to_missing?(name, include_private = false)
49
+ @builder.respond_to?(name, include_private)
50
+ end
51
+
52
+ private
53
+ def forward(name, *args, **options, &block)
54
+ result = @builder.public_send(name, *args, **options, &capturing(block))
55
+ return result unless result.is_a?(String) && result.html_safe?
56
+
57
+ @render.plain(result)
58
+ nil
59
+ end
60
+
61
+ # Nested builders (fields_for / fields) are wrapped as well.
62
+ def block_arg(arg)
63
+ FormBuilderProxy.wrap(@render, arg)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_component'
4
+
5
+ module Ruby2html
6
+ module RailsComponents
7
+ # Delegates to Rails' +image_tag+; see BaseComponent.
8
+ class ImageTag < BaseComponent
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_component'
4
+
5
+ module Ruby2html
6
+ module RailsComponents
7
+ # Delegates to Rails' +link_to+; see BaseComponent.
8
+ class LinkTo < BaseComponent
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby2html
4
+ module RailsComponents
5
+ # Bridges Ruby2html blocks and Rails helpers that take a block.
6
+ #
7
+ # Rails helpers (link_to, button_to, form_with, FormBuilder#label, ...) run
8
+ # their block through ActionView's +capture+, which only sees the view's
9
+ # output buffer. Ruby2html tag methods write into the renderer's buffer
10
+ # instead, so without this bridge nested tags would leak out *before* the
11
+ # helper's markup. Including classes must set +@render+.
12
+ module OutputCapture
13
+ private
14
+ # Wraps +block+ in a proc for a Rails helper. The proc redirects the
15
+ # renderer's output into a scratch buffer while the block runs and
16
+ # returns that markup as html_safe. +block_arg+ lets subclasses swap
17
+ # yielded objects (e.g. wrap a FormBuilder) before the user sees them.
18
+ def capturing(block)
19
+ return unless block
20
+
21
+ proc { |*yielded| capture_output(*yielded.map! { |arg| block_arg(arg) }, &block) }
22
+ end
23
+
24
+ def capture_output(*args)
25
+ previous = @render.current_output
26
+ buffer = String.new(capacity: 256)
27
+ @render.current_output = buffer
28
+ value = yield(*args)
29
+ # Same rule as ActionView's capture: markup written to the buffer wins,
30
+ # otherwise the block's return value is used (Rails escapes it if unsafe).
31
+ buffer.empty? ? value : buffer.html_safe
32
+ ensure
33
+ @render.current_output = previous
34
+ end
35
+
36
+ def block_arg(arg)
37
+ arg
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Loaded by TemplateCompiler.available? after Prism (Prism::Visitor must exist).
4
+ module Ruby2html
5
+ module RailsComponents
6
+ class TemplateCompiler
7
+ # Walks the AST collecting insertions. Method/class bodies are skipped:
8
+ # the renderer local isn't visible there.
9
+ class Collector < Prism::Visitor
10
+ attr_reader :edits, :uses_helpers
11
+
12
+ def initialize(renderer_methods)
13
+ super()
14
+ @renderer_methods = renderer_methods
15
+ @edits = []
16
+ @uses_helpers = false
17
+ end
18
+
19
+ def visit_call_node(node)
20
+ rewrite(node) if node.receiver.nil?
21
+ return super unless own_renderer_block?(node)
22
+
23
+ # The block belongs to a fresh renderer (e.g. Ruby2html::Render.new
24
+ # { p 'x' }) and is instance_exec'd on it, so its receiverless calls
25
+ # must stay receiverless. Visit everything except the block body.
26
+ node.receiver&.accept(self)
27
+ node.arguments&.accept(self)
28
+ end
29
+
30
+ def visit_def_node(_node); end
31
+ def visit_class_node(_node); end
32
+ def visit_module_node(_node); end
33
+ def visit_singleton_class_node(_node); end
34
+
35
+ private
36
+ # `Render.new { ... }` / `Ruby2html::Render.new { ... }`
37
+ def own_renderer_block?(node)
38
+ return false unless node.name == :new && node.block.is_a?(Prism::BlockNode)
39
+
40
+ receiver = node.receiver
41
+ case receiver
42
+ when Prism::ConstantReadNode then receiver.name == :Render
43
+ when Prism::ConstantPathNode then receiver.name == :Render
44
+ else false
45
+ end
46
+ end
47
+
48
+ def rewrite(node)
49
+ name = node.name
50
+ return unless node.message_loc
51
+
52
+ if @renderer_methods.include?(name)
53
+ @edits << [node.message_loc.start_offset, TemplateCompiler::RENDERER_PREFIX]
54
+ elsif TemplateCompiler.helper_methods.include?(name)
55
+ @edits << [node.message_loc.start_offset, TemplateCompiler::HELPERS_PREFIX]
56
+ @uses_helpers = true
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end