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,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,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,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
|