ruby2html 1.6.6 → 2.0.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 +197 -0
- data/LICENSE +21 -0
- data/README.md +155 -225
- 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 +36 -9
- data/ext/ruby2html/html_escape.c +252 -0
- data/ext/ruby2html/ruby2html.c +15 -107
- data/ext/ruby2html/ruby2html.h +32 -0
- data/ext/ruby2html/tag_render.c +179 -0
- data/lib/gem/ruby2html/component.rb +118 -0
- data/lib/gem/ruby2html/component_helper.rb +36 -15
- data/lib/gem/ruby2html/html_beautifier_middleware.rb +55 -9
- data/lib/gem/ruby2html/rails_components/base_component.rb +10 -1
- data/lib/gem/ruby2html/rails_components/button_to.rb +3 -3
- data/lib/gem/ruby2html/rails_components/form_with.rb +41 -67
- data/lib/gem/ruby2html/rails_components/image_tag.rb +3 -3
- data/lib/gem/ruby2html/rails_components/link_to.rb +3 -3
- 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 +8 -4
- data/lib/gem/ruby2html/railtie.rb +104 -25
- data/lib/gem/ruby2html/render/pure_ruby.rb +202 -0
- data/lib/gem/ruby2html/render.rb +60 -98
- data/lib/gem/ruby2html/version.rb +1 -1
- data/lib/gem/ruby2html.rb +28 -7
- metadata +22 -78
- data/lib/ruby2html/ruby2html.so +0 -0
- data/ruby2html.gemspec +0 -43
|
@@ -1,93 +1,67 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'base_component'
|
|
4
|
+
|
|
3
5
|
module Ruby2html
|
|
4
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.
|
|
5
11
|
class FormWith < BaseComponent
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
method = @options[:method]
|
|
12
|
-
local = @options[:local]
|
|
12
|
+
private
|
|
13
|
+
def block_arg(arg)
|
|
14
|
+
FormBuilderProxy.wrap(@render, arg)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
utf8_enforcer_tag
|
|
24
|
-
block.call(self)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
26
|
+
def self.wrap(render, arg)
|
|
27
|
+
return arg unless defined?(ActionView::Helpers::FormBuilder) && arg.is_a?(ActionView::Helpers::FormBuilder)
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
@render.label(**options.merge(for: field_id(method))) do
|
|
30
|
-
text || method.to_s.humanize
|
|
31
|
-
end
|
|
29
|
+
new(render, arg)
|
|
32
30
|
end
|
|
33
31
|
|
|
34
|
-
def
|
|
35
|
-
@render
|
|
32
|
+
def initialize(render, builder)
|
|
33
|
+
@render = render
|
|
34
|
+
@builder = builder
|
|
36
35
|
end
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
# The underlying ActionView::Helpers::FormBuilder.
|
|
38
|
+
def __builder__
|
|
39
|
+
@builder
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def
|
|
43
|
-
@
|
|
44
|
-
end
|
|
42
|
+
def method_missing(name, ...)
|
|
43
|
+
return super unless @builder.respond_to?(name)
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
@render.input(**options.merge(type: 'file', name: field_name(method), id: field_id(method)))
|
|
45
|
+
forward(name, ...)
|
|
48
46
|
end
|
|
49
47
|
|
|
50
|
-
def
|
|
51
|
-
@
|
|
48
|
+
def respond_to_missing?(name, include_private = false)
|
|
49
|
+
@builder.respond_to?(name, include_private)
|
|
52
50
|
end
|
|
53
51
|
|
|
54
52
|
private
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
return
|
|
58
|
-
nil
|
|
59
|
-
end
|
|
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?
|
|
60
56
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return 'post' unless model
|
|
64
|
-
model.respond_to?(:persisted?) && model.persisted? ? 'patch' : 'post'
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def determine_scope(model, scope)
|
|
68
|
-
return scope if scope
|
|
69
|
-
model.model_name.param_key if model.respond_to?(:model_name)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def authenticity_token_tag
|
|
73
|
-
@render.input(type: 'hidden', name: 'authenticity_token', value: @context.form_authenticity_token)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def utf8_enforcer_tag
|
|
77
|
-
@render.input(type: 'hidden', name: 'utf8', value: '✓')
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def field_name(method)
|
|
81
|
-
@scope ? "#{@scope}[#{method}]" : method.to_s
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def field_id(method)
|
|
85
|
-
@scope ? "#{@scope}_#{method}" : method.to_s
|
|
57
|
+
@render.plain(result)
|
|
58
|
+
nil
|
|
86
59
|
end
|
|
87
60
|
|
|
88
|
-
|
|
89
|
-
|
|
61
|
+
# Nested builders (fields_for / fields) are wrapped as well.
|
|
62
|
+
def block_arg(arg)
|
|
63
|
+
FormBuilderProxy.wrap(@render, arg)
|
|
90
64
|
end
|
|
91
65
|
end
|
|
92
66
|
end
|
|
93
|
-
end
|
|
67
|
+
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'base_component'
|
|
4
|
+
|
|
3
5
|
module Ruby2html
|
|
4
6
|
module RailsComponents
|
|
7
|
+
# Delegates to Rails' +image_tag+; see BaseComponent.
|
|
5
8
|
class ImageTag < BaseComponent
|
|
6
|
-
def render(&block)
|
|
7
|
-
@render.plain(@context.image_tag(*@args, **@options, &block))
|
|
8
|
-
end
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'base_component'
|
|
4
|
+
|
|
3
5
|
module Ruby2html
|
|
4
6
|
module RailsComponents
|
|
7
|
+
# Delegates to Rails' +link_to+; see BaseComponent.
|
|
5
8
|
class LinkTo < BaseComponent
|
|
6
|
-
def render(&block)
|
|
7
|
-
@render.plain(@context.button_to(*@args, **@options, &block))
|
|
8
|
-
end
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
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
|
|
@@ -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
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Ruby2html
|
|
4
|
+
# Include in a controller to get +html(self) { ... }+ inside ERB/other views.
|
|
4
5
|
module RailsHelper
|
|
5
|
-
def
|
|
6
|
-
|
|
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)
|
|
7
9
|
end
|
|
8
10
|
|
|
9
|
-
def
|
|
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
|
|
11
15
|
end
|
|
12
16
|
end
|
|
13
17
|
end
|
|
@@ -1,40 +1,119 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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)
|
|
22
51
|
end
|
|
23
52
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
26
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
|
|
27
92
|
end
|
|
93
|
+
end
|
|
28
94
|
|
|
95
|
+
if defined?(Rails::Railtie)
|
|
29
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
|
+
|
|
30
107
|
initializer 'ruby2html.initializer' do
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
)
|
|
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'))
|
|
35
111
|
end
|
|
36
112
|
end
|
|
37
113
|
end
|
|
114
|
+
end
|
|
38
115
|
|
|
116
|
+
# Register lazily so requiring the gem doesn't force ActionView to load.
|
|
117
|
+
ActiveSupport.on_load(:action_view) do
|
|
39
118
|
ActionView::Template.register_template_handler :rb, Ruby2html::TemplateHandler
|
|
40
119
|
end
|