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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +211 -0
- data/LICENSE +21 -0
- data/README.md +263 -0
- data/ext/java/Ruby2htmlJrubyService.java +95 -0
- data/ext/ruby2html/attributes.c +200 -0
- data/ext/ruby2html/buffer.h +43 -0
- data/ext/ruby2html/content.c +120 -0
- data/ext/ruby2html/extconf.rb +47 -0
- data/ext/ruby2html/html_escape.c +252 -0
- data/ext/ruby2html/ruby2html.c +16 -0
- data/ext/ruby2html/ruby2html.h +32 -0
- data/ext/ruby2html/tag_render.c +179 -0
- data/lib/gem/ruby2html/component.rb +120 -0
- data/lib/gem/ruby2html/component_helper.rb +48 -0
- data/lib/gem/ruby2html/html_beautifier_middleware.rb +70 -0
- data/lib/gem/ruby2html/rails_components/base_component.rb +28 -0
- data/lib/gem/ruby2html/rails_components/button_to.rb +11 -0
- data/lib/gem/ruby2html/rails_components/form_with.rb +67 -0
- data/lib/gem/ruby2html/rails_components/image_tag.rb +11 -0
- data/lib/gem/ruby2html/rails_components/link_to.rb +11 -0
- data/lib/gem/ruby2html/rails_components/output_capture.rb +41 -0
- data/lib/gem/ruby2html/rails_components/template_collector.rb +62 -0
- data/lib/gem/ruby2html/rails_components/template_compiler.rb +92 -0
- data/lib/gem/ruby2html/rails_components/template_helpers.rb +77 -0
- data/lib/gem/ruby2html/rails_helper.rb +17 -0
- data/lib/gem/ruby2html/railtie.rb +119 -0
- data/lib/gem/ruby2html/render/pure_ruby.rb +219 -0
- data/lib/gem/ruby2html/render.rb +123 -0
- data/lib/gem/ruby2html/ruby2html_jruby.jar +0 -0
- data/lib/gem/ruby2html/version.rb +5 -0
- data/lib/gem/ruby2html.rb +34 -0
- 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 = { '&' => '&', '<' => '<', '>' => '>', '"' => '"', "'" => ''' }.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
|
|
Binary file
|