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,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Ruby2html
6
+ module RailsComponents
7
+ # Compile-time rewrite of a .html.rb template.
8
+ #
9
+ # Instead of instance_exec'ing the template on a Render (which forces the
10
+ # renderer to copy every view instance variable on each render and to
11
+ # route every helper call through method_missing), the template runs
12
+ # directly in the view/component. Prism finds the receiverless calls that
13
+ # belong to Ruby2html and gives them an explicit receiver:
14
+ #
15
+ # div class: 'x' do => __r2h_renderer.div class: 'x' do
16
+ # link_to 'Home', '/' => __r2h_helpers.link_to 'Home', '/'
17
+ # render partial: 'nav' => __r2h_helpers.render partial: 'nav'
18
+ #
19
+ # Only text is inserted, never newlines, so line numbers are unchanged.
20
+ class TemplateCompiler
21
+ RENDERER = '__r2h_renderer'
22
+ HELPERS = '__r2h_helpers'
23
+ RENDERER_PREFIX = "#{RENDERER}."
24
+ HELPERS_PREFIX = "#{HELPERS}."
25
+
26
+ def self.helper_methods
27
+ @helper_methods ||= TemplateHelpers.public_instance_methods(false).to_set.freeze
28
+ end
29
+
30
+ Result = Struct.new(:source, :uses_helpers)
31
+
32
+ class << self
33
+ # Prism is only loaded when the first template is compiled. Without it
34
+ # (Ruby < 3.3 and no prism gem) TemplateHandler falls back to the
35
+ # closure strategy (template block instance_exec'd on the renderer).
36
+ def available?
37
+ return @available if defined?(@available)
38
+
39
+ @available = begin
40
+ require 'prism'
41
+ require_relative 'template_collector'
42
+ true
43
+ rescue LoadError
44
+ false
45
+ end
46
+ end
47
+
48
+ # Returns a Result, or nil when the source can't be parsed (the caller
49
+ # then compiles it unchanged so Ruby reports the syntax error).
50
+ # +locals+: the template's local names (partial locals, `locals:`).
51
+ # Prism must know them, or `h2 title` would parse `title` as a call
52
+ # and be rewritten into a <title> tag.
53
+ def compile(source, locals = [])
54
+ return unless available?
55
+
56
+ parsed = locals.empty? ? Prism.parse(source) : Prism.parse(source, scopes: [locals.map(&:to_sym)])
57
+ return unless parsed.success?
58
+
59
+ collector = Collector.new(renderer_methods)
60
+ parsed.value.accept(collector)
61
+ Result.new(apply(source, collector.edits), collector.uses_helpers)
62
+ end
63
+
64
+ # Ruby2html methods a template may call without a receiver: every
65
+ # tag method plus plain/component. Resolved lazily (and memoized) so
66
+ # it reflects Render after it has been fully loaded.
67
+ def renderer_methods
68
+ @renderer_methods ||= begin
69
+ tags = Ruby2html::Render::HTML5_TAGS.map { |tag| tag.tr('-', '_').to_sym }
70
+ (tags + %i[plain component]).select { |name| Ruby2html::Render.public_method_defined?(name) }.to_set.freeze
71
+ end
72
+ end
73
+
74
+ private
75
+ # Edits are [byte_offset, text] insertions; Prism offsets are bytes.
76
+ def apply(source, edits)
77
+ return source if edits.empty?
78
+
79
+ bytes = source.b
80
+ out = String.new(capacity: bytes.bytesize + (edits.size * 16), encoding: Encoding::BINARY)
81
+ cursor = 0
82
+ edits.sort_by!.with_index { |(offset, _), index| [offset, index] }.each do |offset, text|
83
+ out << bytes.byteslice(cursor, offset - cursor) << text
84
+ cursor = offset
85
+ end
86
+ out << bytes.byteslice(cursor, bytes.bytesize - cursor)
87
+ out.force_encoding(source.encoding)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'output_capture'
4
+ require_relative 'base_component'
5
+ require_relative 'link_to'
6
+ require_relative 'button_to'
7
+ require_relative 'image_tag'
8
+ require_relative 'form_with'
9
+
10
+ module Ruby2html
11
+ module RailsComponents
12
+ # Receiver for the Rails helper calls of a compiled .html.rb template
13
+ # (see TemplateCompiler). Routes each helper through its RailsComponents
14
+ # wrapper so the helper's markup lands in the Ruby2html buffer.
15
+ class TemplateHelpers
16
+ include OutputCapture
17
+
18
+ COMPONENTS = {
19
+ link_to: LinkTo,
20
+ button_to: ButtonTo,
21
+ image_tag: ImageTag,
22
+ form_with: FormWith
23
+ }.freeze
24
+
25
+ def initialize(render, context)
26
+ @render = render
27
+ @context = context
28
+ end
29
+
30
+ COMPONENTS.each do |name, component|
31
+ define_method(name) do |*args, **options, &block|
32
+ component.new(@render, @context, name, *args, **options).render(&block)
33
+ end
34
+ end
35
+
36
+ # Ruby2html::Component instances write straight into the template's
37
+ # buffer; anything else (partials, ViewComponents, ...) goes through the
38
+ # view's render and is appended. A block is captured from the Ruby2html
39
+ # buffer so its tags end up inside the rendered output.
40
+ def render(*args, **options, &block)
41
+ first = args.first
42
+ if args.size == 1 && options.empty? && first.is_a?(Ruby2html::Component)
43
+ first.render_into(@render.current_output, @context, block)
44
+ else
45
+ @render.plain(@context.render(*args, **options, &capturing(block)))
46
+ end
47
+ nil
48
+ end
49
+
50
+ # ActionView's capture only sees the view's output buffer, so a block of
51
+ # Ruby2html tags would be written to the page and capture nothing.
52
+ # Redirect the renderer's buffer instead (same return rule as Rails).
53
+ def capture(*args, &block)
54
+ return @context.capture(*args) unless block
55
+
56
+ value = capture_output(*args, &block)
57
+ value.is_a?(String) ? ERB::Util.html_escape(value) : nil
58
+ end
59
+
60
+ def content_for(name, content = nil, **options, &block)
61
+ content = capture(&block) if block
62
+ @context.content_for(name, content, **options)
63
+ end
64
+
65
+ def provide(name, content = nil, &block)
66
+ content = capture(&block) if block
67
+ @context.provide(name, content)
68
+ end
69
+
70
+ # Kept for templates written against the old instance_exec evaluation,
71
+ # where Render's private escape_html was callable. Returns a plain String.
72
+ def escape_html(text)
73
+ ERB::Util.unwrapped_html_escape(text.to_s)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby2html
4
+ # Include in a controller to get +html(self) { ... }+ inside ERB/other views.
5
+ module RailsHelper
6
+ def self.included(base)
7
+ # Only controllers can expose helpers to views; plain modules just get +html+.
8
+ base.helper_method :html if base.respond_to?(:helper_method)
9
+ end
10
+
11
+ def html(context, &block)
12
+ output = Ruby2html::Render.new(context, &block).render
13
+ # Render#render already returns a SafeBuffer under Rails; avoid copying it.
14
+ output.html_safe? ? output : output.html_safe
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The template handler and Railtie only make sense with ActiveSupport/ActionView.
4
+ return unless defined?(ActiveSupport)
5
+
6
+ require 'active_support/core_ext/class/attribute'
7
+ require_relative '../ruby2html' unless defined?(Ruby2html::RailsComponents) # autoloads
8
+
9
+ module Ruby2html
10
+ # ActionView template handler for +*.html.rb+ views, partials and
11
+ # ViewComponent sidecar templates.
12
+ #
13
+ # Templates are compiled once by ActionView; all decisions that don't depend
14
+ # on the request (rewriting, annotations, whether helpers are needed) are
15
+ # made here so the compiled method does as little as possible per render.
16
+ #
17
+ # Two strategies:
18
+ # * compiled (default): TemplateCompiler rewrites the source so it runs in
19
+ # the view itself. No instance variable copying, no instance_exec, helper
20
+ # calls go straight to the view.
21
+ # * closure (fallback when Prism is unavailable or the source doesn't
22
+ # parse): the source becomes a block instance_exec'd on a Render that
23
+ # copies the view's instance variables, as in 1.x.
24
+ class TemplateHandler
25
+ # Render#render runs its root block; compiled templates have already
26
+ # written their output, so they pass this shared no-op.
27
+ NOOP = proc { }
28
+
29
+ # Every generated prologue ends on the template's first line so that
30
+ # backtrace line numbers point at the right line of the .html.rb file.
31
+ # Locals are prefixed because the template code shares their scope.
32
+ #
33
+ # The renderer is held in a local and published on the view/component as
34
+ # +@__r2h+ (the slot ComponentHelper#method_missing reads), restoring the
35
+ # previous value afterwards so nested partials/components on the same
36
+ # object stay correct. No thread/fiber-global state is involved.
37
+ SAVE_PREVIOUS = '__r2h_prev = @__r2h; begin; '
38
+ COMPILED_PROLOGUE = "#{SAVE_PREVIOUS}__r2h_renderer = Ruby2html::Render.new(nil, &Ruby2html::TemplateHandler::NOOP); "
39
+ HELPERS_PROLOGUE = '__r2h_helpers = Ruby2html::RailsComponents::TemplateHelpers.new(__r2h_renderer, self); '
40
+ PUSH_RENDERER = '@__r2h = __r2h_renderer'
41
+ CLOSURE_PROLOGUE = "#{SAVE_PREVIOUS}__r2h_renderer = Ruby2html::Render.new(self) do; "
42
+ EPILOGUE = "\nensure\n@__r2h = __r2h_prev\nend"
43
+
44
+ class_attribute :default_format
45
+ self.default_format = :html
46
+
47
+ class << self
48
+ def call(template, source)
49
+ compiled = RailsComponents::TemplateCompiler.compile(source, template_locals(template))
50
+ compiled ? compiled_code(template, compiled) : closure_code(template, source)
51
+ end
52
+
53
+ # This handler does no encoding work of its own, so let ActionView
54
+ # transcode the source to Encoding.default_external (honouring an
55
+ # `# encoding:` magic comment) before it reaches #call.
56
+ def handles_encoding?
57
+ false
58
+ end
59
+
60
+ private
61
+ def template_locals(template)
62
+ template.respond_to?(:locals) ? Array(template.locals) : []
63
+ end
64
+
65
+ def compiled_code(template, compiled)
66
+ helpers = compiled.uses_helpers ? HELPERS_PROLOGUE : ''
67
+ "#{COMPILED_PROLOGUE}#{helpers}#{PUSH_RENDERER}; #{compiled.source}\n" \
68
+ "#{render_expression(template)}#{EPILOGUE}"
69
+ end
70
+
71
+ def closure_code(template, source)
72
+ "#{CLOSURE_PROLOGUE}#{source}\nend\n#{PUSH_RENDERER}\n#{render_expression(template)}#{EPILOGUE}"
73
+ end
74
+
75
+ # Mirrors the ERB handler: wrap HTML templates in BEGIN/END comments
76
+ # when config.action_view.annotate_rendered_view_with_filenames is on.
77
+ def render_expression(template)
78
+ return '__r2h_renderer.render' unless annotate?(template)
79
+
80
+ path = template.short_identifier
81
+ "ActiveSupport::SafeBuffer.new(#{"<!-- BEGIN #{path} -->".inspect})" \
82
+ '.safe_concat(__r2h_renderer.render)' \
83
+ ".safe_concat(#{"<!-- END #{path} -->".inspect})"
84
+ end
85
+
86
+ def annotate?(template)
87
+ defined?(ActionView::Base) &&
88
+ ActionView::Base.annotate_rendered_view_with_filenames &&
89
+ template.respond_to?(:format) && template.format == :html &&
90
+ template.respond_to?(:short_identifier)
91
+ end
92
+ end
93
+ end
94
+
95
+ if defined?(Rails::Railtie)
96
+ class Railtie < Rails::Railtie
97
+ # Lazy by default (dev, tests, plain `require`). When the app eager-loads
98
+ # (production), load everything up front instead: no first-request
99
+ # latency, and forking servers share these pages copy-on-write.
100
+ config.before_eager_load do
101
+ Ruby2html::RailsComponents::TemplateCompiler.available? # loads Prism
102
+ Ruby2html::RailsComponents.constants.each { |name| Ruby2html::RailsComponents.const_get(name) }
103
+ Ruby2html::Component
104
+ Ruby2html::ComponentHelper
105
+ end
106
+
107
+ initializer 'ruby2html.initializer' do
108
+ # *.html.rb files are templates, not constants: keep Zeitwerk from
109
+ # trying to autoload/eager-load them anywhere under app/.
110
+ Rails.autoloaders.main.ignore(Rails.root.join('app/**/*.html.rb'))
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ # Register lazily so requiring the gem doesn't force ActionView to load.
117
+ ActiveSupport.on_load(:action_view) do
118
+ ActionView::Template.register_template_handler :rb, Ruby2html::TemplateHandler
119
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ # Only ERB's C escaper (erb >= 4), not the template engine. Absent on
5
+ # JRuby/TruffleRuby: PureRuby.escape then uses its regex fallback.
6
+ require 'erb/escape'
7
+ rescue LoadError
8
+ nil
9
+ end
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
+
21
+ module Ruby2html
22
+ # Pure-Ruby twin of the C extension's Ruby2html::Native (ext/ruby2html),
23
+ # used on JRuby, when the extension isn't built, or with
24
+ # RUBY2HTML_DISABLE_EXT=1. The specs render everything with both and
25
+ # compare. The rules are documented once, in the C sources.
26
+ #
27
+ # Written for JITs (YJIT, the JVM): generated tag methods without splats,
28
+ # everything appended with String#<< to one buffer, and escaping that
29
+ # returns the string itself when there is nothing to escape.
30
+ module PureRuby
31
+ INVALID_ATTRIBUTE_NAME = %r{[\x00-\x20\x7f"'<>/=]}
32
+ STRING_BOOLEAN = /\A(?:data-|aria-|(?:draggable|spellcheck|contenteditable)\z)/
33
+ MAX_NESTING = 32 # see R2H_MAX_NESTING in attributes.c
34
+ ESCAPABLE = /[&<>"']/
35
+ ESCAPABLE_BYTES = /[&<>"']/n
36
+ ESCAPES = { '&' => '&amp;', '<' => '&lt;', '>' => '&gt;', '"' => '&quot;', "'" => '&#39;' }.freeze
37
+
38
+ class << self
39
+ # CRuby's ERB::Escape (erb >= 4, in C) is the fastest option and also
40
+ # returns its argument untouched when it is clean.
41
+ if RUBY_ENGINE == 'ruby' && defined?(ERB::Escape)
42
+ # May return str itself: only for appending to the buffer.
43
+ def escape(str)
44
+ ERB::Escape.html_escape(utf8_compatible(str))
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
53
+ else
54
+ def escape(str)
55
+ str = utf8_compatible(str)
56
+ # A UTF-8 regex raises on invalid bytes; Native passes them through.
57
+ return str.b.gsub(ESCAPABLE_BYTES, ESCAPES).force_encoding(Encoding::UTF_8) unless str.valid_encoding?
58
+
59
+ str.match?(ESCAPABLE) ? str.gsub(ESCAPABLE, ESCAPES) : str
60
+ end
61
+ end
62
+
63
+ # Like Native.escape_html: never hands back the caller's mutable string.
64
+ def escape_html(obj)
65
+ str = obj.to_s
66
+ escaped = escape(str)
67
+ escaped.equal?(str) && !str.frozen? ? str.dup : escaped
68
+ end
69
+
70
+ # See r2h_utf8_compatible in html_escape.c.
71
+ def utf8_compatible(str)
72
+ case str.encoding
73
+ when Encoding::UTF_8, Encoding::US_ASCII then str
74
+ when Encoding::BINARY then str.dup.force_encoding(Encoding::UTF_8)
75
+ else str.ascii_only? ? str : str.encode(Encoding::UTF_8)
76
+ end
77
+ end
78
+
79
+ def define_tag(method_name, tag, void)
80
+ source = void ? void_tag_source(method_name, tag) : tag_source(method_name, tag)
81
+ # The magic comment is per eval: without it every tag literal allocates.
82
+ module_eval("# frozen_string_literal: true\n#{source}", __FILE__, __LINE__)
83
+ end
84
+
85
+ private
86
+ def tag_source(method_name, tag)
87
+ <<~RUBY
88
+ def #{method_name}(content = nil, **attributes)
89
+ buffer = @current_output
90
+ if attributes.empty?
91
+ buffer << '<#{tag}>'
92
+ else
93
+ __r2h_open_tag(buffer, '<#{tag}', attributes)
94
+ buffer << '>'
95
+ end
96
+ if block_given?
97
+ size = buffer.bytesize
98
+ result = yield
99
+ __r2h_block_result(buffer, result) if buffer.bytesize == size
100
+ elsif content.instance_of?(String)
101
+ buffer << PureRuby.escape(content)
102
+ else
103
+ __r2h_text(buffer, content)
104
+ end
105
+ buffer << '</#{tag}>'
106
+ nil
107
+ end
108
+ RUBY
109
+ end
110
+
111
+ def void_tag_source(method_name, tag)
112
+ <<~RUBY
113
+ def #{method_name}(_content = nil, **attributes)
114
+ if attributes.empty?
115
+ @current_output << '<#{tag} />'
116
+ else
117
+ buffer = @current_output
118
+ __r2h_open_tag(buffer, '<#{tag}', attributes)
119
+ buffer << ' />'
120
+ end
121
+ nil
122
+ end
123
+ RUBY
124
+ end
125
+ end
126
+
127
+ def plain(text)
128
+ __r2h_text(@current_output, text)
129
+ nil
130
+ end
131
+
132
+ def component(html)
133
+ @current_output << PureRuby.utf8_compatible(html.to_s) unless html.nil?
134
+ nil
135
+ end
136
+
137
+ private
138
+ def __r2h_text(buffer, obj)
139
+ case obj
140
+ when nil then nil
141
+ when String
142
+ safe = !obj.instance_of?(String) && __r2h_html_safe?(obj)
143
+ buffer << (safe ? PureRuby.utf8_compatible(obj) : PureRuby.escape(obj))
144
+ when Symbol, Integer then buffer << PureRuby.escape(obj.to_s)
145
+ else
146
+ buffer << (__r2h_html_safe?(obj) ? PureRuby.utf8_compatible(obj.to_s) : PureRuby.escape(obj.to_s))
147
+ end
148
+ end
149
+
150
+ def __r2h_block_result(buffer, result)
151
+ __r2h_text(buffer, result) if result.is_a?(String) || result.is_a?(Symbol) || result.is_a?(Numeric)
152
+ end
153
+
154
+ def __r2h_html_safe?(obj)
155
+ obj.respond_to?(:html_safe?) && obj.html_safe?
156
+ end
157
+
158
+ # Writes `<tag attr="..."`. If an attribute raises and the caller rescues,
159
+ # a half-written start tag would turn whatever is appended next into
160
+ # markup, so the buffer is rolled back to before the tag.
161
+ def __r2h_open_tag(buffer, open, attributes)
162
+ mark = buffer.bytesize
163
+ buffer << open
164
+ __r2h_attributes(buffer, attributes, nil)
165
+ rescue Exception # rubocop:disable Lint/RescueException -- re-raised; any exception leaves the tag open
166
+ buffer.replace(buffer.byteslice(0, mark)) if !buffer.frozen? && buffer.bytesize >= mark
167
+ raise
168
+ end
169
+
170
+ def __r2h_attributes(buffer, attributes, prefix, depth = 0)
171
+ raise ArgumentError, "attributes nested more than #{MAX_NESTING} levels deep" if depth > MAX_NESTING
172
+
173
+ attributes.each do |key, value|
174
+ next if value.nil?
175
+
176
+ name = key.is_a?(Symbol) ? key.name : key.to_s
177
+ raise ArgumentError, "attribute name can't be empty" if name.empty?
178
+
179
+ name = PureRuby.utf8_compatible(name)
180
+ name = "#{prefix}-#{__r2h_dasherize(name)}" if prefix
181
+ # Match bytes when the name isn't valid UTF-8 (a UTF-8 regex would raise).
182
+ checked = name.valid_encoding? ? name : name.b
183
+ raise ArgumentError, "invalid attribute name: #{name.inspect}" if INVALID_ATTRIBUTE_NAME.match?(checked)
184
+
185
+ case value
186
+ when String then buffer << ' ' << name << '="' << PureRuby.escape(value) << '"'
187
+ when true then buffer << ' ' << name << (STRING_BOOLEAN.match?(name) ? '="true"' : '')
188
+ when false then buffer << ' ' << name << '="false"' if STRING_BOOLEAN.match?(name)
189
+ when Hash then __r2h_attributes(buffer, value, name, depth + 1)
190
+ when Array then buffer << ' ' << name << '="' << __r2h_tokens(value) << '"'
191
+ else buffer << ' ' << name << '="' << PureRuby.escape(value.to_s) << '"'
192
+ end
193
+ end
194
+ end
195
+
196
+ def __r2h_dasherize(name)
197
+ return name.tr('_', '-') if name.valid_encoding?
198
+
199
+ name.b.tr('_', '-').force_encoding(Encoding::UTF_8)
200
+ end
201
+
202
+ def __r2h_tokens(tokens)
203
+ __r2h_collect_tokens(tokens, [], 1).join(' ')
204
+ end
205
+
206
+ def __r2h_collect_tokens(tokens, collected, depth)
207
+ raise ArgumentError, "attributes nested more than #{MAX_NESTING} levels deep" if depth > MAX_NESTING
208
+
209
+ tokens.each do |token|
210
+ if token.is_a?(Array)
211
+ __r2h_collect_tokens(token, collected, depth + 1)
212
+ elsif token
213
+ collected << PureRuby.escape(token.to_s)
214
+ end
215
+ end
216
+ collected
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'render/pure_ruby'
4
+
5
+ module Ruby2html
6
+ # Loads the C extension (Ruby2html::Native). Without it (JRuby, a failed
7
+ # build, or RUBY2HTML_DISABLE_EXT=1) the pure-Ruby twin is used instead.
8
+ # Installed gems find the extension on the load path; this repo builds it
9
+ # into lib/ruby2html.
10
+ def self.load_native_extension
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'
16
+
17
+ begin
18
+ require 'ruby2html/ruby2html'
19
+ rescue LoadError
20
+ require_relative '../../ruby2html/ruby2html'
21
+ end
22
+ defined?(Native) ? true : false
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"
27
+ false
28
+ end
29
+ private_class_method :load_native_extension
30
+
31
+ # The module providing tag methods, #plain and #component: Native or PureRuby.
32
+ IMPLEMENTATION = load_native_extension ? Native : PureRuby
33
+
34
+ def self.native?
35
+ IMPLEMENTATION != PureRuby
36
+ end
37
+
38
+ def self.escape_html(obj)
39
+ IMPLEMENTATION.escape_html(obj)
40
+ end
41
+
42
+ class Render
43
+ HTML5_TAGS = %w[
44
+ a abbr address area article aside audio b base bdi bdo blockquote body br button canvas caption
45
+ cite code col colgroup data datalist dd del details dfn dialog div dl dt em embed fieldset
46
+ figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hr html i iframe img input ins
47
+ kbd label legend li link main map mark meta meter nav noscript object ol optgroup option
48
+ output p param picture pre progress q rp rt ruby s samp script section select small source
49
+ span strong style sub summary sup table tbody td template textarea tfoot th thead time title
50
+ tr track u ul var video wbr turbo-frame turbo-stream
51
+ ].freeze
52
+
53
+ VOID_ELEMENTS = %w[area base br col embed hr img input link meta param source track wbr].freeze
54
+ # Rails helpers that write their markup into the ruby2html buffer, mapped
55
+ # to their RailsComponents wrapper (shared by Render and Component).
56
+ RAILS_HELPER_COMPONENTS = { link_to: :LinkTo, button_to: :ButtonTo, image_tag: :ImageTag, form_with: :FormWith }.freeze
57
+ # Context ivars that would clobber the renderer's own state if copied.
58
+ SKIPPED_IVAR_PREFIXES = %w[@_ @output @current_output @context @root].freeze
59
+
60
+ # Both implementations get the tags so specs can compare them in one process.
61
+ [PureRuby, IMPLEMENTATION].uniq.each do |implementation|
62
+ HTML5_TAGS.each { |tag| implementation.define_tag(tag.tr('-', '_'), tag, VOID_ELEMENTS.include?(tag)) }
63
+ end
64
+
65
+ include IMPLEMENTATION
66
+
67
+ # No `output` reader: it would shadow the <output> tag method.
68
+ attr_accessor :current_output
69
+
70
+ def initialize(context = nil, &root)
71
+ @context = context
72
+ @root = root
73
+ @output = String.new(capacity: 4096, encoding: Encoding::UTF_8)
74
+ @current_output = @output
75
+ end
76
+
77
+ # Without arguments renders the root block and returns the HTML (a
78
+ # SafeBuffer under ActiveSupport). With arguments it is the context's
79
+ # render (partials, components), appended to the output.
80
+ def render(*args, **options, &block)
81
+ copy_instance_variables if @context && !@__vars_copied
82
+
83
+ return plain @context.render(*args, **options, &block) if !args.empty? || !options.empty? || block
84
+
85
+ instance_exec(&@root)
86
+ defined?(ActiveSupport::SafeBuffer) ? ActiveSupport::SafeBuffer.new(@output) : @output
87
+ end
88
+
89
+ private
90
+ def method_missing(method_name, ...)
91
+ if @context.respond_to?(method_name)
92
+ @context.send(method_name, ...)
93
+ else
94
+ super
95
+ end
96
+ end
97
+
98
+ def respond_to_missing?(method_name, include_private = false)
99
+ @context.respond_to?(method_name) || super
100
+ end
101
+
102
+ # Resolved per call (const_get with a Symbol; autoloaded on first use).
103
+ RAILS_HELPER_COMPONENTS.each do |method, component_name|
104
+ define_method(method) do |*args, **options, &block|
105
+ RailsComponents.const_get(component_name).new(self, @context, method, *args, **options).render(&block)
106
+ end
107
+ end if defined?(ActionView)
108
+
109
+ def escape_html(text)
110
+ IMPLEMENTATION.escape_html(text)
111
+ end
112
+
113
+ # Templates read the view's assigns (@users, ...) as their own ivars.
114
+ def copy_instance_variables
115
+ @context.instance_variables.each do |name|
116
+ next if name.start_with?(*SKIPPED_IVAR_PREFIXES)
117
+
118
+ instance_variable_set(name, @context.instance_variable_get(name))
119
+ end
120
+ @__vars_copied = true
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby2html
4
+ VERSION = '2.1.0'
5
+ end