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,200 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Attribute rendering, written straight into the output buffer.
|
|
3
|
+
*
|
|
4
|
+
* nil -> omitted
|
|
5
|
+
* true / false -> bare attribute / omitted; data-*, aria-* and the
|
|
6
|
+
* enumerated draggable/spellcheck/contenteditable
|
|
7
|
+
* get "true" / "false" because there the value matters
|
|
8
|
+
* Hash -> expanded: data: { user_id: 1 } -> data-user-id="1"
|
|
9
|
+
* Array -> space-separated tokens, nil/false skipped
|
|
10
|
+
* anything else -> to_s, escaped
|
|
11
|
+
*
|
|
12
|
+
* Names are validated rather than escaped: a name containing quotes,
|
|
13
|
+
* whitespace, = < > or / could inject markup, so it raises ArgumentError.
|
|
14
|
+
* Nothing is cached: generating is cheaper than hashing the Hash for a cache
|
|
15
|
+
* lookup, and a cache keyed by #hash can return another hash's attributes.
|
|
16
|
+
*/
|
|
17
|
+
#include "buffer.h"
|
|
18
|
+
|
|
19
|
+
/* Deeper nesting is a bug (or a self-referencing Hash/Array); refuse it
|
|
20
|
+
* instead of recursing until the C stack overflows. */
|
|
21
|
+
#define R2H_MAX_NESTING 32
|
|
22
|
+
|
|
23
|
+
typedef struct {
|
|
24
|
+
VALUE buf;
|
|
25
|
+
VALUE prefix; /* name of the enclosing Hash attribute, or Qnil */
|
|
26
|
+
int depth;
|
|
27
|
+
} attr_ctx;
|
|
28
|
+
|
|
29
|
+
static void check_nesting(int depth)
|
|
30
|
+
{
|
|
31
|
+
if (depth > R2H_MAX_NESTING) rb_raise(rb_eArgError, "attributes nested more than %d levels deep", R2H_MAX_NESTING);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static int append_attribute(VALUE key, VALUE value, VALUE data);
|
|
35
|
+
|
|
36
|
+
static int is_invalid_name_byte(unsigned char c)
|
|
37
|
+
{
|
|
38
|
+
return c <= ' ' || c == 0x7F || c == '"' || c == '\'' || c == '<' || c == '>' || c == '/' || c == '=';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static VALUE key_to_str(VALUE key)
|
|
42
|
+
{
|
|
43
|
+
if (SYMBOL_P(key)) return rb_sym2str(key);
|
|
44
|
+
if (RB_TYPE_P(key, T_STRING)) return key;
|
|
45
|
+
return rb_obj_as_string(key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* Writes ` name` (or ` prefix-name` with _ turned into -) and returns the
|
|
50
|
+
* offset of the name within buf so callers can inspect or roll it back.
|
|
51
|
+
*/
|
|
52
|
+
/* Error path only: report the full name as written (`data-x onclick`), like PureRuby. */
|
|
53
|
+
NORETURN(static void raise_invalid_name(const attr_ctx *ctx, VALUE name));
|
|
54
|
+
static void raise_invalid_name(const attr_ctx *ctx, VALUE name)
|
|
55
|
+
{
|
|
56
|
+
VALUE full = name;
|
|
57
|
+
if (!NIL_P(ctx->prefix)) {
|
|
58
|
+
VALUE dashed = rb_funcall(name, rb_intern("tr"), 2, rb_str_new_cstr("_"), rb_str_new_cstr("-"));
|
|
59
|
+
full = rb_sprintf("%" PRIsVALUE "-%" PRIsVALUE, ctx->prefix, dashed);
|
|
60
|
+
}
|
|
61
|
+
rb_raise(rb_eArgError, "invalid attribute name: %+" PRIsVALUE, full);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static long append_name(const attr_ctx *ctx, VALUE key)
|
|
65
|
+
{
|
|
66
|
+
VALUE name = r2h_utf8_compatible(key_to_str(key));
|
|
67
|
+
if (name == ctx->buf) name = rb_str_dup(name); /* reserve below may move buf's bytes */
|
|
68
|
+
const char *src = RSTRING_PTR(name);
|
|
69
|
+
const long len = RSTRING_LEN(name);
|
|
70
|
+
const int nested = !NIL_P(ctx->prefix);
|
|
71
|
+
const long prefix_len = nested ? RSTRING_LEN(ctx->prefix) + 1 : 0;
|
|
72
|
+
|
|
73
|
+
if (len == 0) rb_raise(rb_eArgError, "attribute name can't be empty");
|
|
74
|
+
|
|
75
|
+
const long start = RSTRING_LEN(ctx->buf) + 1;
|
|
76
|
+
char *out = r2h_buf_reserve(ctx->buf, 1 + prefix_len + len);
|
|
77
|
+
*out++ = ' ';
|
|
78
|
+
if (nested) {
|
|
79
|
+
memcpy(out, RSTRING_PTR(ctx->prefix), (size_t)(prefix_len - 1));
|
|
80
|
+
out += prefix_len - 1;
|
|
81
|
+
*out++ = '-';
|
|
82
|
+
}
|
|
83
|
+
for (long i = 0; i < len; i++) {
|
|
84
|
+
const unsigned char c = (unsigned char)src[i];
|
|
85
|
+
if (is_invalid_name_byte(c)) {
|
|
86
|
+
rb_str_set_len(ctx->buf, start - 1);
|
|
87
|
+
raise_invalid_name(ctx, name);
|
|
88
|
+
}
|
|
89
|
+
*out++ = (nested && c == '_') ? '-' : (char)c;
|
|
90
|
+
}
|
|
91
|
+
r2h_buf_commit(ctx->buf, out);
|
|
92
|
+
RB_GC_GUARD(name);
|
|
93
|
+
return start;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static int has_prefix(const char *name, long len, const char *prefix, long prefix_len)
|
|
97
|
+
{
|
|
98
|
+
return len >= prefix_len && memcmp(name, prefix, (size_t)prefix_len) == 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static int is_named(const char *name, long len, const char *word, long word_len)
|
|
102
|
+
{
|
|
103
|
+
return len == word_len && memcmp(name, word, (size_t)word_len) == 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#define R2H_PREFIX(name, len, lit) has_prefix((name), (len), (lit), (long)(sizeof(lit) - 1))
|
|
107
|
+
#define R2H_NAMED(name, len, lit) is_named((name), (len), (lit), (long)(sizeof(lit) - 1))
|
|
108
|
+
|
|
109
|
+
/* Attributes whose true/false is a string value rather than presence. */
|
|
110
|
+
static int has_string_boolean(VALUE buf, long start)
|
|
111
|
+
{
|
|
112
|
+
const char *name = RSTRING_PTR(buf) + start;
|
|
113
|
+
const long len = RSTRING_LEN(buf) - start;
|
|
114
|
+
|
|
115
|
+
return R2H_PREFIX(name, len, "data-") || R2H_PREFIX(name, len, "aria-") ||
|
|
116
|
+
R2H_NAMED(name, len, "draggable") || R2H_NAMED(name, len, "spellcheck") ||
|
|
117
|
+
R2H_NAMED(name, len, "contenteditable");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
static void append_token_list(VALUE buf, VALUE tokens, int *first, int depth)
|
|
121
|
+
{
|
|
122
|
+
check_nesting(depth);
|
|
123
|
+
for (long i = 0; i < RARRAY_LEN(tokens); i++) {
|
|
124
|
+
VALUE token = RARRAY_AREF(tokens, i);
|
|
125
|
+
|
|
126
|
+
if (!RTEST(token)) continue;
|
|
127
|
+
if (RB_TYPE_P(token, T_ARRAY)) {
|
|
128
|
+
append_token_list(buf, token, first, depth + 1);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (!*first) R2H_BUF_CAT_LIT(buf, " ");
|
|
132
|
+
*first = 0;
|
|
133
|
+
r2h_append_escaped(buf, SYMBOL_P(token) ? rb_sym2str(token) : rb_obj_as_string(token));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static void append_value(VALUE buf, VALUE value)
|
|
138
|
+
{
|
|
139
|
+
R2H_BUF_CAT_LIT(buf, "=\"");
|
|
140
|
+
switch (rb_type(value)) {
|
|
141
|
+
case T_STRING:
|
|
142
|
+
r2h_append_escaped(buf, value);
|
|
143
|
+
break;
|
|
144
|
+
case T_SYMBOL:
|
|
145
|
+
r2h_append_escaped(buf, rb_sym2str(value));
|
|
146
|
+
break;
|
|
147
|
+
case T_FIXNUM:
|
|
148
|
+
r2h_append_long(buf, FIX2LONG(value));
|
|
149
|
+
break;
|
|
150
|
+
case T_ARRAY: {
|
|
151
|
+
int first = 1;
|
|
152
|
+
append_token_list(buf, value, &first, 1);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
default:
|
|
156
|
+
r2h_append_escaped(buf, rb_obj_as_string(value));
|
|
157
|
+
}
|
|
158
|
+
R2H_BUF_CAT_LIT(buf, "\"");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
static int append_attribute(VALUE key, VALUE value, VALUE data)
|
|
162
|
+
{
|
|
163
|
+
const attr_ctx *ctx = (const attr_ctx *)data;
|
|
164
|
+
VALUE buf = ctx->buf;
|
|
165
|
+
|
|
166
|
+
if (NIL_P(value)) return ST_CONTINUE;
|
|
167
|
+
|
|
168
|
+
const long start = append_name(ctx, key);
|
|
169
|
+
|
|
170
|
+
switch (rb_type(value)) {
|
|
171
|
+
case T_TRUE:
|
|
172
|
+
if (has_string_boolean(buf, start)) R2H_BUF_CAT_LIT(buf, "=\"true\"");
|
|
173
|
+
break;
|
|
174
|
+
case T_FALSE:
|
|
175
|
+
if (has_string_boolean(buf, start)) {
|
|
176
|
+
R2H_BUF_CAT_LIT(buf, "=\"false\"");
|
|
177
|
+
} else {
|
|
178
|
+
rb_str_set_len(buf, start - 1);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
case T_HASH: {
|
|
182
|
+
/* The name just written becomes the prefix of the nested keys. */
|
|
183
|
+
attr_ctx nested = { buf, rb_str_new(RSTRING_PTR(buf) + start, RSTRING_LEN(buf) - start), ctx->depth + 1 };
|
|
184
|
+
rb_str_set_len(buf, start - 1);
|
|
185
|
+
check_nesting(nested.depth);
|
|
186
|
+
rb_hash_foreach(value, append_attribute, (VALUE)&nested);
|
|
187
|
+
RB_GC_GUARD(nested.prefix);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
default:
|
|
191
|
+
append_value(buf, value);
|
|
192
|
+
}
|
|
193
|
+
return ST_CONTINUE;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
void r2h_append_attributes(VALUE buf, VALUE attrs)
|
|
197
|
+
{
|
|
198
|
+
attr_ctx ctx = { buf, Qnil, 0 };
|
|
199
|
+
rb_hash_foreach(attrs, append_attribute, (VALUE)&ctx);
|
|
200
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#ifndef RUBY2HTML_BUFFER_H
|
|
2
|
+
#define RUBY2HTML_BUFFER_H
|
|
3
|
+
|
|
4
|
+
#include <string.h>
|
|
5
|
+
#include "ruby2html.h"
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Output buffer primitives. Everything is written straight into the
|
|
9
|
+
* renderer's String: reserve room, memcpy, then commit the new length.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Returns a write pointer with room for `n` more bytes. Grows at least 2x
|
|
14
|
+
* because rb_str_modify_expand alone grows to the exact size, which would
|
|
15
|
+
* make a long render quadratic. Both branches also unshare the string and
|
|
16
|
+
* clear its coderange, which keeps raw byte writes encoding-correct.
|
|
17
|
+
*/
|
|
18
|
+
static inline char *r2h_buf_reserve(VALUE buf, long n)
|
|
19
|
+
{
|
|
20
|
+
long len = RSTRING_LEN(buf);
|
|
21
|
+
|
|
22
|
+
if ((long)rb_str_capacity(buf) - len < n) {
|
|
23
|
+
rb_str_modify_expand(buf, n > len ? n : len);
|
|
24
|
+
} else {
|
|
25
|
+
rb_str_modify(buf);
|
|
26
|
+
}
|
|
27
|
+
return RSTRING_PTR(buf) + len;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static inline void r2h_buf_commit(VALUE buf, const char *end)
|
|
31
|
+
{
|
|
32
|
+
rb_str_set_len(buf, end - RSTRING_PTR(buf));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* One libruby call; also grows geometrically and clears the coderange. */
|
|
36
|
+
static inline void r2h_buf_cat(VALUE buf, const char *ptr, long len)
|
|
37
|
+
{
|
|
38
|
+
rb_str_cat(buf, ptr, len);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#define R2H_BUF_CAT_LIT(buf, lit) r2h_buf_cat((buf), (lit), (long)(sizeof(lit) - 1))
|
|
42
|
+
|
|
43
|
+
#endif
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Text content: tag arguments, block return values, #plain and #component.
|
|
3
|
+
*/
|
|
4
|
+
#include "buffer.h"
|
|
5
|
+
|
|
6
|
+
ID r2h_id_current_output;
|
|
7
|
+
ID r2h_id_html_safe_p;
|
|
8
|
+
static int utf8_index;
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* The buffer tag methods write UTF-8 bytes into. A binary or US-ASCII
|
|
12
|
+
* buffer (e.g. a bare String.new swapped in to capture output) is relabelled
|
|
13
|
+
* UTF-8, as String#<< would do on the first non-ASCII append.
|
|
14
|
+
*/
|
|
15
|
+
VALUE r2h_current_buffer(VALUE self)
|
|
16
|
+
{
|
|
17
|
+
VALUE buf = rb_ivar_get(self, r2h_id_current_output);
|
|
18
|
+
|
|
19
|
+
if (!RB_TYPE_P(buf, T_STRING)) rb_raise(rb_eTypeError, "ruby2html: @current_output must be a String");
|
|
20
|
+
if (RB_UNLIKELY(ENCODING_GET_INLINED(buf) != utf8_index)) {
|
|
21
|
+
rb_encoding *enc = rb_enc_get(buf);
|
|
22
|
+
if (enc != rb_ascii8bit_encoding() && enc != rb_usascii_encoding()) {
|
|
23
|
+
rb_raise(rb_eEncCompatError, "ruby2html: output buffer must be UTF-8, not %s", rb_enc_name(enc));
|
|
24
|
+
}
|
|
25
|
+
rb_enc_associate_index(buf, utf8_index);
|
|
26
|
+
}
|
|
27
|
+
return buf;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Formats without allocating a String (Integer#to_s would). */
|
|
31
|
+
void r2h_append_long(VALUE buf, long value)
|
|
32
|
+
{
|
|
33
|
+
char digits[24];
|
|
34
|
+
char *end = digits + sizeof(digits), *p = end;
|
|
35
|
+
unsigned long n = value < 0 ? 0UL - (unsigned long)value : (unsigned long)value;
|
|
36
|
+
|
|
37
|
+
do {
|
|
38
|
+
*--p = (char)('0' + n % 10);
|
|
39
|
+
n /= 10;
|
|
40
|
+
} while (n);
|
|
41
|
+
if (value < 0) *--p = '-';
|
|
42
|
+
r2h_buf_cat(buf, p, end - p);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ActiveSupport's html_safe?, false when ActiveSupport isn't loaded. */
|
|
46
|
+
static int is_html_safe(VALUE obj)
|
|
47
|
+
{
|
|
48
|
+
return rb_respond_to(obj, r2h_id_html_safe_p) && RTEST(rb_funcall(obj, r2h_id_html_safe_p, 0));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Escapes obj.to_s unless it is html_safe? (e.g. an ActiveSupport::SafeBuffer). */
|
|
52
|
+
void r2h_append_text(VALUE buf, VALUE obj)
|
|
53
|
+
{
|
|
54
|
+
switch (rb_type(obj)) {
|
|
55
|
+
case T_NIL:
|
|
56
|
+
return;
|
|
57
|
+
case T_STRING:
|
|
58
|
+
/*
|
|
59
|
+
* A plain String is never html_safe?; only subclasses need the call.
|
|
60
|
+
* rb_obj_class (not RBASIC_CLASS) so a String with a singleton
|
|
61
|
+
* html_safe? can't spoof safety (matches PureRuby's instance_of?).
|
|
62
|
+
*/
|
|
63
|
+
if (rb_obj_class(obj) != rb_cString && is_html_safe(obj)) {
|
|
64
|
+
r2h_append_raw(buf, obj);
|
|
65
|
+
} else {
|
|
66
|
+
r2h_append_escaped(buf, obj);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
case T_SYMBOL:
|
|
70
|
+
r2h_append_escaped(buf, rb_sym2str(obj));
|
|
71
|
+
return;
|
|
72
|
+
case T_FIXNUM:
|
|
73
|
+
r2h_append_long(buf, FIX2LONG(obj));
|
|
74
|
+
return;
|
|
75
|
+
default:
|
|
76
|
+
if (is_html_safe(obj)) {
|
|
77
|
+
r2h_append_raw(buf, rb_obj_as_string(obj));
|
|
78
|
+
} else {
|
|
79
|
+
r2h_append_escaped(buf, rb_obj_as_string(obj));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
* A block that wrote nothing contributes its return value when that is a
|
|
86
|
+
* String, Symbol or Numeric (Integer, Float, BigDecimal, Rational, ...):
|
|
87
|
+
* `td { user.name }`. Anything else, such as the Array returned by
|
|
88
|
+
* `items.each`, nil or true, is ignored.
|
|
89
|
+
*/
|
|
90
|
+
void r2h_append_block_result(VALUE buf, VALUE result)
|
|
91
|
+
{
|
|
92
|
+
if (RB_TYPE_P(result, T_STRING) || SYMBOL_P(result) || RB_INTEGER_TYPE_P(result) || RB_FLOAT_TYPE_P(result) ||
|
|
93
|
+
RTEST(rb_obj_is_kind_of(result, rb_cNumeric))) {
|
|
94
|
+
r2h_append_text(buf, result);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* plain(text): escaped text, or raw when html_safe?. */
|
|
99
|
+
static VALUE native_plain(VALUE self, VALUE text)
|
|
100
|
+
{
|
|
101
|
+
r2h_append_text(r2h_current_buffer(self), text);
|
|
102
|
+
return Qnil;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* component(html): already-rendered markup, appended as is. */
|
|
106
|
+
static VALUE native_component(VALUE self, VALUE html)
|
|
107
|
+
{
|
|
108
|
+
if (!NIL_P(html)) r2h_append_raw(r2h_current_buffer(self), rb_obj_as_string(html));
|
|
109
|
+
return Qnil;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
void r2h_init_content(VALUE native)
|
|
113
|
+
{
|
|
114
|
+
r2h_id_current_output = rb_intern("@current_output");
|
|
115
|
+
r2h_id_html_safe_p = rb_intern("html_safe?");
|
|
116
|
+
utf8_index = rb_utf8_encindex();
|
|
117
|
+
|
|
118
|
+
rb_define_method(native, "plain", native_plain, 1);
|
|
119
|
+
rb_define_method(native, "component", native_component, 1);
|
|
120
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Builds Ruby2html::Native. When it can't or shouldn't be built, writes a
|
|
4
|
+
# Makefile that does nothing so `gem install` still succeeds; the gem then
|
|
5
|
+
# runs on its pure-Ruby renderer (see Ruby2html.native?).
|
|
6
|
+
|
|
7
|
+
def skip_native_extension(reason)
|
|
8
|
+
warn "ruby2html: #{reason}; the pure-Ruby renderer will be used"
|
|
9
|
+
File.write('Makefile', "all:\n\ninstall:\n\nclean:\n\ndistclean:\n")
|
|
10
|
+
exit
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
skip_native_extension('RUBY2HTML_DISABLE_EXT=1') if ENV['RUBY2HTML_DISABLE_EXT'] == '1'
|
|
14
|
+
skip_native_extension('JRuby does not load C extensions') if RUBY_ENGINE == 'jruby'
|
|
15
|
+
# TruffleRuby runs C extensions through Sulong (an LLVM interpreter) but gives
|
|
16
|
+
# pure Ruby the full Graal JIT, which here renders ~28x faster than the
|
|
17
|
+
# extension. Building it would only slow TruffleRuby down, so don't.
|
|
18
|
+
skip_native_extension('TruffleRuby is faster on the pure-Ruby renderer') if RUBY_ENGINE == 'truffleruby'
|
|
19
|
+
|
|
20
|
+
require 'mkmf'
|
|
21
|
+
|
|
22
|
+
skip_native_extension('no working C compiler') unless try_compile('int main(void) { return 0; }')
|
|
23
|
+
|
|
24
|
+
# append_cflags keeps only flags the compiler accepts, so this works on gcc
|
|
25
|
+
# and clang, Linux and macOS. No -march=native: the built gem may be copied
|
|
26
|
+
# to another machine (Docker images). SIMD needs no flags: SSE2 is baseline
|
|
27
|
+
# on x86_64 and NEON on arm64.
|
|
28
|
+
append_cflags(%w[-O3 -std=gnu99 -fvisibility=hidden -Wall -Wextra -Wno-unused-parameter])
|
|
29
|
+
# Direct calls into libruby instead of PLT stubs (ELF only).
|
|
30
|
+
append_cflags('-fno-plt') if RbConfig::CONFIG['host_os'].include?('linux')
|
|
31
|
+
# Scalar escape path, for benchmarking: RUBY2HTML_NO_SIMD=1 rake compile
|
|
32
|
+
append_cflags('-DR2H_NO_SIMD') if ENV['RUBY2HTML_NO_SIMD'] == '1'
|
|
33
|
+
|
|
34
|
+
skip_native_extension('ruby/encoding.h not found') unless have_header('ruby/encoding.h')
|
|
35
|
+
|
|
36
|
+
# CRuby is the reference platform, so a broken build there should fail
|
|
37
|
+
# loudly. Elsewhere (TruffleRuby, ...) compile every source first and fall
|
|
38
|
+
# back if any of them doesn't build.
|
|
39
|
+
unless RUBY_ENGINE == 'ruby'
|
|
40
|
+
Dir.glob(File.join(__dir__, '*.c')).each do |source|
|
|
41
|
+
next if try_compile(File.read(source), "-I#{__dir__}")
|
|
42
|
+
|
|
43
|
+
skip_native_extension("#{File.basename(source)} does not compile on #{RUBY_ENGINE}")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
create_makefile('ruby2html/ruby2html')
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* HTML escaping of & < > " ' (safe for text and for quoted attribute values).
|
|
3
|
+
*
|
|
4
|
+
* Strategy: find the first escapable byte with a vector scan. Clean strings
|
|
5
|
+
* (the common case) are then a single memcpy. Dirty strings get their exact
|
|
6
|
+
* escaped size computed once, and are written in one pass that bulk-copies
|
|
7
|
+
* the clean runs between escapable bytes.
|
|
8
|
+
*/
|
|
9
|
+
#include "buffer.h"
|
|
10
|
+
|
|
11
|
+
static const char *const ESCAPES[256] = {
|
|
12
|
+
['&'] = "&", ['<'] = "<", ['>'] = ">", ['"'] = """, ['\''] = "'",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/* Bytes added by escaping each character (0 = not escapable). */
|
|
16
|
+
static const unsigned char EXTRA[256] = {
|
|
17
|
+
['&'] = 4, ['<'] = 3, ['>'] = 3, ['"'] = 5, ['\''] = 4,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
static int utf8_index, usascii_index, binary_index;
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* 16-byte vector scan. The escapable bytes pair up so three compares cover
|
|
24
|
+
* all five: 0x26 '&' | 1 == 0x27 '\'' 0x3C '<' | 2 == 0x3E '>' 0x22 '"'.
|
|
25
|
+
* The pairs also share their EXTRA value (4, 3, 5), which lets the counting
|
|
26
|
+
* pass sum a per-byte weight vector instead of looking each byte up.
|
|
27
|
+
*/
|
|
28
|
+
#if defined(__SSE2__) && !defined(R2H_NO_SIMD)
|
|
29
|
+
#include <emmintrin.h>
|
|
30
|
+
#define R2H_SIMD 1
|
|
31
|
+
|
|
32
|
+
typedef __m128i r2h_vec;
|
|
33
|
+
|
|
34
|
+
static inline void classify(const char *p, r2h_vec *amp, r2h_vec *ang, r2h_vec *quot)
|
|
35
|
+
{
|
|
36
|
+
const __m128i v = _mm_loadu_si128((const __m128i *)p);
|
|
37
|
+
*amp = _mm_cmpeq_epi8(_mm_or_si128(v, _mm_set1_epi8(1)), _mm_set1_epi8(0x27));
|
|
38
|
+
*ang = _mm_cmpeq_epi8(_mm_or_si128(v, _mm_set1_epi8(2)), _mm_set1_epi8(0x3E));
|
|
39
|
+
*quot = _mm_cmpeq_epi8(v, _mm_set1_epi8(0x22));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Offset of the first escapable byte in the chunk, or 16. */
|
|
43
|
+
static inline int chunk_first(const char *p)
|
|
44
|
+
{
|
|
45
|
+
r2h_vec amp, ang, quot;
|
|
46
|
+
classify(p, &, &ang, ");
|
|
47
|
+
const int mask = _mm_movemask_epi8(_mm_or_si128(_mm_or_si128(amp, ang), quot));
|
|
48
|
+
return mask ? __builtin_ctz((unsigned)mask) : 16;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static inline long chunk_extra(const char *p)
|
|
52
|
+
{
|
|
53
|
+
r2h_vec amp, ang, quot;
|
|
54
|
+
classify(p, &, &ang, ");
|
|
55
|
+
const __m128i weights = _mm_or_si128(
|
|
56
|
+
_mm_or_si128(_mm_and_si128(amp, _mm_set1_epi8(4)), _mm_and_si128(ang, _mm_set1_epi8(3))),
|
|
57
|
+
_mm_and_si128(quot, _mm_set1_epi8(5)));
|
|
58
|
+
const __m128i sums = _mm_sad_epu8(weights, _mm_setzero_si128());
|
|
59
|
+
return _mm_cvtsi128_si32(sums) + _mm_cvtsi128_si32(_mm_srli_si128(sums, 8));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#elif defined(__aarch64__) && defined(__ARM_NEON) && !defined(R2H_NO_SIMD)
|
|
63
|
+
#include <arm_neon.h>
|
|
64
|
+
#define R2H_SIMD 1
|
|
65
|
+
|
|
66
|
+
typedef uint8x16_t r2h_vec;
|
|
67
|
+
|
|
68
|
+
static inline void classify(const char *p, r2h_vec *amp, r2h_vec *ang, r2h_vec *quot)
|
|
69
|
+
{
|
|
70
|
+
const uint8x16_t v = vld1q_u8((const uint8_t *)p);
|
|
71
|
+
*amp = vceqq_u8(vorrq_u8(v, vdupq_n_u8(1)), vdupq_n_u8(0x27));
|
|
72
|
+
*ang = vceqq_u8(vorrq_u8(v, vdupq_n_u8(2)), vdupq_n_u8(0x3E));
|
|
73
|
+
*quot = vceqq_u8(v, vdupq_n_u8(0x22));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static inline int chunk_first(const char *p)
|
|
77
|
+
{
|
|
78
|
+
r2h_vec amp, ang, quot;
|
|
79
|
+
classify(p, &, &ang, ");
|
|
80
|
+
const uint8x16_t any = vorrq_u8(vorrq_u8(amp, ang), quot);
|
|
81
|
+
/* NEON has no movemask: narrow each byte to a nibble, giving 4 bits per lane. */
|
|
82
|
+
const uint64_t bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(any), 4)), 0);
|
|
83
|
+
return bits ? __builtin_ctzll(bits) >> 2 : 16;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static inline long chunk_extra(const char *p)
|
|
87
|
+
{
|
|
88
|
+
r2h_vec amp, ang, quot;
|
|
89
|
+
classify(p, &, &ang, ");
|
|
90
|
+
const uint8x16_t weights = vorrq_u8(
|
|
91
|
+
vorrq_u8(vandq_u8(amp, vdupq_n_u8(4)), vandq_u8(ang, vdupq_n_u8(3))),
|
|
92
|
+
vandq_u8(quot, vdupq_n_u8(5)));
|
|
93
|
+
return vaddvq_u8(weights); /* at most 16 * 5 = 80, no overflow */
|
|
94
|
+
}
|
|
95
|
+
#endif
|
|
96
|
+
|
|
97
|
+
/* Offset of the first escapable byte, or len. */
|
|
98
|
+
static inline long find_escapable(const char *p, long len)
|
|
99
|
+
{
|
|
100
|
+
long i = 0;
|
|
101
|
+
#ifdef R2H_SIMD
|
|
102
|
+
for (; i + 16 <= len; i += 16) {
|
|
103
|
+
const int at = chunk_first(p + i);
|
|
104
|
+
if (at < 16) return i + at;
|
|
105
|
+
}
|
|
106
|
+
#endif
|
|
107
|
+
for (; i < len; i++) {
|
|
108
|
+
if (EXTRA[(unsigned char)p[i]]) return i;
|
|
109
|
+
}
|
|
110
|
+
return len;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Total bytes escaping adds to p[0, len). */
|
|
114
|
+
static inline long escaped_extra(const char *p, long len)
|
|
115
|
+
{
|
|
116
|
+
long i = 0, extra = 0;
|
|
117
|
+
#ifdef R2H_SIMD
|
|
118
|
+
for (; i + 16 <= len; i += 16) extra += chunk_extra(p + i);
|
|
119
|
+
#endif
|
|
120
|
+
for (; i < len; i++) extra += EXTRA[(unsigned char)p[i]];
|
|
121
|
+
return extra;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/*
|
|
125
|
+
* Escaping grows a string at most 6x ('"' -> """). Refuse sizes whose
|
|
126
|
+
* worst case doesn't fit in a long (only reachable on 32-bit platforms).
|
|
127
|
+
*/
|
|
128
|
+
static inline void check_escaped_size(long len, long fixed)
|
|
129
|
+
{
|
|
130
|
+
if (len > (LONG_MAX - fixed) / 6) rb_raise(rb_eArgError, "string too large to escape");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Writes the escaped form of p[0, len) to out; returns the new end. */
|
|
134
|
+
static char *escape_into(char *out, const char *p, long len)
|
|
135
|
+
{
|
|
136
|
+
const char *const end = p + len;
|
|
137
|
+
|
|
138
|
+
while (p < end) {
|
|
139
|
+
const long run = find_escapable(p, end - p);
|
|
140
|
+
memcpy(out, p, (size_t)run);
|
|
141
|
+
out += run;
|
|
142
|
+
p += run;
|
|
143
|
+
if (p == end) break;
|
|
144
|
+
|
|
145
|
+
const unsigned char c = (unsigned char)*p++;
|
|
146
|
+
const long size = EXTRA[c] + 1;
|
|
147
|
+
memcpy(out, ESCAPES[c], (size_t)size);
|
|
148
|
+
out += size;
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/*
|
|
154
|
+
* The output buffer is UTF-8. UTF-8, US-ASCII and ASCII-only strings are
|
|
155
|
+
* copied byte for byte. Binary strings are assumed to hold UTF-8 bytes
|
|
156
|
+
* (what 1.x did). Anything else is transcoded, which raises if it can't be.
|
|
157
|
+
*/
|
|
158
|
+
VALUE r2h_utf8_compatible(VALUE str)
|
|
159
|
+
{
|
|
160
|
+
const int index = ENCODING_GET_INLINED(str);
|
|
161
|
+
|
|
162
|
+
if (index == utf8_index || index == usascii_index || index == binary_index) return str;
|
|
163
|
+
if (rb_enc_str_asciionly_p(str)) return str;
|
|
164
|
+
return rb_str_encode(str, rb_enc_from_encoding(rb_utf8_encoding()), 0, Qnil);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/*
|
|
168
|
+
* Appends before + escaped(str) + after with a single buffer reservation:
|
|
169
|
+
* a whole `<li>text</li>` costs one capacity check instead of three.
|
|
170
|
+
*/
|
|
171
|
+
void r2h_append_escaped_wrapped(VALUE buf, const char *before, long before_len, VALUE str,
|
|
172
|
+
const char *after, long after_len)
|
|
173
|
+
{
|
|
174
|
+
str = r2h_utf8_compatible(str);
|
|
175
|
+
/* Growing buf would free the bytes we are reading if they are the same string. */
|
|
176
|
+
if (str == buf) str = rb_str_dup(str);
|
|
177
|
+
|
|
178
|
+
const char *p = RSTRING_PTR(str);
|
|
179
|
+
const long len = RSTRING_LEN(str);
|
|
180
|
+
check_escaped_size(len, before_len + after_len + RSTRING_LEN(buf));
|
|
181
|
+
const long first = find_escapable(p, len);
|
|
182
|
+
const long extra = first == len ? 0 : escaped_extra(p + first, len - first);
|
|
183
|
+
|
|
184
|
+
char *out = r2h_buf_reserve(buf, before_len + len + extra + after_len);
|
|
185
|
+
p = RSTRING_PTR(str); /* reserve may run GC; str is guarded below */
|
|
186
|
+
if (before_len) memcpy(out, before, (size_t)before_len);
|
|
187
|
+
out += before_len;
|
|
188
|
+
memcpy(out, p, (size_t)first);
|
|
189
|
+
out = escape_into(out + first, p + first, len - first);
|
|
190
|
+
if (after_len) memcpy(out, after, (size_t)after_len);
|
|
191
|
+
r2h_buf_commit(buf, out + after_len);
|
|
192
|
+
RB_GC_GUARD(str);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
void r2h_append_escaped(VALUE buf, VALUE str)
|
|
196
|
+
{
|
|
197
|
+
r2h_append_escaped_wrapped(buf, NULL, 0, str, NULL, 0);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
void r2h_append_raw(VALUE buf, VALUE str)
|
|
201
|
+
{
|
|
202
|
+
str = r2h_utf8_compatible(str);
|
|
203
|
+
if (str == buf) str = rb_str_dup(str);
|
|
204
|
+
r2h_buf_cat(buf, RSTRING_PTR(str), RSTRING_LEN(str));
|
|
205
|
+
RB_GC_GUARD(str);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/*
|
|
209
|
+
* Ruby2html::Native.escape_html(obj) -> String
|
|
210
|
+
*
|
|
211
|
+
* Never returns the caller's own mutable string: callers may mutate the
|
|
212
|
+
* result. Frozen input with nothing to escape is returned as is.
|
|
213
|
+
*/
|
|
214
|
+
static VALUE native_escape_html(VALUE mod, VALUE obj)
|
|
215
|
+
{
|
|
216
|
+
VALUE str = r2h_utf8_compatible(rb_obj_as_string(obj));
|
|
217
|
+
const char *p = RSTRING_PTR(str);
|
|
218
|
+
const long len = RSTRING_LEN(str);
|
|
219
|
+
const long first = find_escapable(p, len);
|
|
220
|
+
|
|
221
|
+
/* Binary input holds UTF-8 bytes (see r2h_utf8_compatible): label the result UTF-8. */
|
|
222
|
+
const int binary = ENCODING_GET_INLINED(str) == binary_index;
|
|
223
|
+
|
|
224
|
+
if (first == len) {
|
|
225
|
+
if (!binary) return OBJ_FROZEN(str) ? str : rb_str_dup(str);
|
|
226
|
+
VALUE copy = rb_str_dup(str);
|
|
227
|
+
rb_enc_associate(copy, rb_utf8_encoding());
|
|
228
|
+
return copy;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
check_escaped_size(len, 0);
|
|
232
|
+
VALUE result = rb_str_buf_new(len + escaped_extra(p + first, len - first));
|
|
233
|
+
if (binary) {
|
|
234
|
+
rb_enc_associate(result, rb_utf8_encoding());
|
|
235
|
+
} else {
|
|
236
|
+
rb_enc_copy(result, str);
|
|
237
|
+
}
|
|
238
|
+
char *out = RSTRING_PTR(result);
|
|
239
|
+
memcpy(out, p, (size_t)first);
|
|
240
|
+
r2h_buf_commit(result, escape_into(out + first, p + first, len - first));
|
|
241
|
+
RB_GC_GUARD(str);
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
void r2h_init_escape(VALUE native)
|
|
246
|
+
{
|
|
247
|
+
utf8_index = rb_utf8_encindex();
|
|
248
|
+
usascii_index = rb_usascii_encindex();
|
|
249
|
+
binary_index = rb_ascii8bit_encindex();
|
|
250
|
+
|
|
251
|
+
rb_define_singleton_method(native, "escape_html", native_escape_html, 1);
|
|
252
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Native renderer core. Defines Ruby2html::Native, a module of instance
|
|
3
|
+
* methods (tag methods, plain, component) that Ruby2html::Render includes.
|
|
4
|
+
* Ruby2html::PureRuby (lib/gem/ruby2html/render/pure_ruby.rb) mirrors it.
|
|
5
|
+
*/
|
|
6
|
+
#include "ruby2html.h"
|
|
7
|
+
|
|
8
|
+
RUBY_FUNC_EXPORTED void Init_ruby2html(void)
|
|
9
|
+
{
|
|
10
|
+
VALUE ruby2html = rb_define_module("Ruby2html");
|
|
11
|
+
VALUE native = rb_define_module_under(ruby2html, "Native");
|
|
12
|
+
|
|
13
|
+
r2h_init_escape(native);
|
|
14
|
+
r2h_init_content(native);
|
|
15
|
+
r2h_init_tags(native);
|
|
16
|
+
}
|