xrb 0.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/bake/xrb/entities.rb +60 -0
- data/bake/xrb/parsers.rb +69 -0
- data/ext/extconf.rb +21 -0
- data/ext/xrb/escape.c +152 -0
- data/ext/xrb/escape.h +15 -0
- data/ext/xrb/markup.c +1949 -0
- data/ext/xrb/markup.h +6 -0
- data/ext/xrb/markup.rl +226 -0
- data/ext/xrb/query.c +619 -0
- data/ext/xrb/query.h +6 -0
- data/ext/xrb/query.rl +82 -0
- data/ext/xrb/tag.c +204 -0
- data/ext/xrb/tag.h +21 -0
- data/ext/xrb/template.c +1114 -0
- data/ext/xrb/template.h +6 -0
- data/ext/xrb/template.rl +77 -0
- data/ext/xrb/xrb.c +72 -0
- data/ext/xrb/xrb.h +132 -0
- data/lib/xrb/buffer.rb +103 -0
- data/lib/xrb/builder.rb +222 -0
- data/lib/xrb/entities.rb +2137 -0
- data/lib/xrb/entities.xrb +30 -0
- data/lib/xrb/error.rb +81 -0
- data/lib/xrb/fallback/markup.rb +1658 -0
- data/lib/xrb/fallback/markup.rl +228 -0
- data/lib/xrb/fallback/query.rb +548 -0
- data/lib/xrb/fallback/query.rl +88 -0
- data/lib/xrb/fallback/template.rb +829 -0
- data/lib/xrb/fallback/template.rl +80 -0
- data/lib/xrb/markup.rb +56 -0
- data/lib/xrb/native.rb +15 -0
- data/lib/xrb/parse_delegate.rb +19 -0
- data/lib/xrb/parsers.rb +17 -0
- data/lib/xrb/query.rb +80 -0
- data/lib/xrb/reference.rb +108 -0
- data/lib/xrb/strings.rb +47 -0
- data/lib/xrb/tag.rb +115 -0
- data/lib/xrb/template.rb +164 -0
- data/lib/xrb/uri.rb +100 -0
- data/lib/xrb/version.rb +8 -0
- data/lib/xrb.rb +11 -0
- data/license.md +23 -0
- data/readme.md +29 -0
- data.tar.gz.sig +0 -0
- metadata +109 -58
- metadata.gz.sig +0 -0
- data/README +0 -60
- data/app/helpers/ui_helper.rb +0 -80
- data/app/models/xrb/element.rb +0 -9
- data/lib/xrb/engine.rb +0 -4
- data/rails/init.rb +0 -1
- data/xrb.gemspec +0 -12
@@ -0,0 +1,80 @@
|
|
1
|
+
# Released under the MIT License.
|
2
|
+
# Copyright, 2016-2024, by Samuel Williams.
|
3
|
+
|
4
|
+
%%{
|
5
|
+
machine template;
|
6
|
+
|
7
|
+
action instruction_begin {
|
8
|
+
instruction_begin = p
|
9
|
+
}
|
10
|
+
|
11
|
+
action instruction_end {
|
12
|
+
instruction_end = p
|
13
|
+
}
|
14
|
+
|
15
|
+
action emit_instruction {
|
16
|
+
delegate.instruction(data.byteslice(instruction_begin...instruction_end))
|
17
|
+
}
|
18
|
+
|
19
|
+
action emit_instruction_line {
|
20
|
+
delegate.instruction(data.byteslice(instruction_begin...instruction_end), "\n")
|
21
|
+
}
|
22
|
+
|
23
|
+
action instruction_error {
|
24
|
+
raise ParseError.new("failed to parse instruction", buffer, p)
|
25
|
+
}
|
26
|
+
|
27
|
+
action expression_begin {
|
28
|
+
expression_begin = p
|
29
|
+
}
|
30
|
+
|
31
|
+
action expression_end {
|
32
|
+
expression_end = p
|
33
|
+
}
|
34
|
+
|
35
|
+
action emit_expression {
|
36
|
+
delegate.expression(data.byteslice(expression_begin...expression_end))
|
37
|
+
}
|
38
|
+
|
39
|
+
action expression_error {
|
40
|
+
raise ParseError.new("failed to parse expression", buffer, p)
|
41
|
+
}
|
42
|
+
|
43
|
+
action emit_text {
|
44
|
+
delegate.text(data.byteslice(ts...te))
|
45
|
+
}
|
46
|
+
|
47
|
+
# This magic ensures that we process bytes.
|
48
|
+
getkey bytes[p];
|
49
|
+
|
50
|
+
include template "xrb/template.rl";
|
51
|
+
}%%
|
52
|
+
|
53
|
+
require_relative '../error'
|
54
|
+
|
55
|
+
module XRB
|
56
|
+
module Fallback
|
57
|
+
%% write data;
|
58
|
+
|
59
|
+
def self.parse_template(buffer, delegate)
|
60
|
+
data = buffer.read
|
61
|
+
bytes = data.bytes
|
62
|
+
|
63
|
+
p = 0
|
64
|
+
pe = eof = data.bytesize
|
65
|
+
stack = []
|
66
|
+
|
67
|
+
expression_begin = expression_end = nil
|
68
|
+
instruction_begin = instruction_end = nil
|
69
|
+
|
70
|
+
%% write init;
|
71
|
+
%% write exec;
|
72
|
+
|
73
|
+
if p != eof
|
74
|
+
raise ParseError.new("could not consume all input", buffer, p)
|
75
|
+
end
|
76
|
+
|
77
|
+
return nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/xrb/markup.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2016-2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require 'cgi'
|
7
|
+
|
8
|
+
module XRB
|
9
|
+
# A wrapper which indicates that `value` can be appended to the output buffer without any changes.
|
10
|
+
module Markup
|
11
|
+
# Converts special characters `<`, `>`, `&`, and `"` into their equivalent entities.
|
12
|
+
# @return [String] May return the original string if no changes were made.
|
13
|
+
def self.escape_string(string)
|
14
|
+
CGI.escape_html(string)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Appends a string to the output buffer, escaping if if necessary.
|
18
|
+
def self.append(buffer, value)
|
19
|
+
if value.is_a? Markup
|
20
|
+
buffer << value
|
21
|
+
elsif value
|
22
|
+
buffer << self.escape_string(value.to_s)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Initialized from text which is escaped to use HTML entities.
|
28
|
+
class MarkupString < String
|
29
|
+
include Markup
|
30
|
+
|
31
|
+
# @param string [String] the string value itself.
|
32
|
+
# @param escape [Boolean] whether or not to escape the string.
|
33
|
+
def initialize(string = nil, escape = true)
|
34
|
+
if string
|
35
|
+
if escape
|
36
|
+
string = Markup.escape_string(string)
|
37
|
+
end
|
38
|
+
|
39
|
+
super(string)
|
40
|
+
else
|
41
|
+
super()
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Generate a valid MarkupString withot any escaping.
|
46
|
+
def self.raw(string)
|
47
|
+
self.new(string, false)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module Script
|
52
|
+
def self.json(value)
|
53
|
+
MarkupString.new(JSON.dump(value), false)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/xrb/native.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2016-2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'error'
|
7
|
+
|
8
|
+
# Methods on the following classes may be replaced by native implementations:
|
9
|
+
require_relative 'tag'
|
10
|
+
|
11
|
+
begin
|
12
|
+
require 'XRB_Extension'
|
13
|
+
rescue LoadError => error
|
14
|
+
warn "Could not load native parsers: #{error}"
|
15
|
+
end unless ENV['XRB_PREFER_FALLBACK']
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2016-2024, by Samuel Williams.
|
5
|
+
|
6
|
+
module XRB
|
7
|
+
# This is a sample delegate for capturing all events. It's only use is for testing.
|
8
|
+
class ParseDelegate
|
9
|
+
def initialize
|
10
|
+
@events = []
|
11
|
+
end
|
12
|
+
|
13
|
+
attr :events
|
14
|
+
|
15
|
+
def method_missing(*args)
|
16
|
+
@events << args
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/xrb/parsers.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2012-2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'native'
|
7
|
+
require_relative 'parse_delegate'
|
8
|
+
|
9
|
+
if defined? XRB::Native
|
10
|
+
XRB::Parsers = XRB::Native
|
11
|
+
else
|
12
|
+
require_relative 'fallback/markup'
|
13
|
+
require_relative 'fallback/template'
|
14
|
+
require_relative 'fallback/query'
|
15
|
+
|
16
|
+
XRB::Parsers = XRB::Fallback
|
17
|
+
end
|
data/lib/xrb/query.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2020-2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'buffer'
|
7
|
+
require_relative 'parsers'
|
8
|
+
|
9
|
+
require 'uri'
|
10
|
+
|
11
|
+
module XRB
|
12
|
+
module Query
|
13
|
+
def self.parse(buffer)
|
14
|
+
Hash.new.tap do |query|
|
15
|
+
Parsers.parse_query(buffer, Delegate.new(query))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Delegate
|
20
|
+
def initialize(top = {})
|
21
|
+
@top = top
|
22
|
+
|
23
|
+
@current = @top
|
24
|
+
@index = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def string(key, encoded)
|
28
|
+
if encoded
|
29
|
+
key = ::URI.decode_www_form_component(key)
|
30
|
+
end
|
31
|
+
|
32
|
+
index(key.to_sym)
|
33
|
+
end
|
34
|
+
|
35
|
+
def integer(key)
|
36
|
+
index(key.to_i)
|
37
|
+
end
|
38
|
+
|
39
|
+
def index(key)
|
40
|
+
if @index
|
41
|
+
@current = @current.fetch(@index) do
|
42
|
+
@current[@index] = {}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
@index = key
|
47
|
+
end
|
48
|
+
|
49
|
+
def append
|
50
|
+
if @index
|
51
|
+
@current = @current.fetch(@index) do
|
52
|
+
@current[@index] = []
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
@index = @current.size
|
57
|
+
end
|
58
|
+
|
59
|
+
def assign(value, encoded)
|
60
|
+
if encoded
|
61
|
+
value = ::URI.decode_www_form_component(value)
|
62
|
+
end
|
63
|
+
|
64
|
+
@current[@index] = value
|
65
|
+
|
66
|
+
@current = @top
|
67
|
+
@index = nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def pair
|
71
|
+
if @index
|
72
|
+
@current[@index] = true
|
73
|
+
end
|
74
|
+
|
75
|
+
@current = @top
|
76
|
+
@index = nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2020-2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'query'
|
7
|
+
|
8
|
+
module XRB
|
9
|
+
class Reference
|
10
|
+
def initialize(path, query = {}, fragment: nil)
|
11
|
+
@path = path.to_s
|
12
|
+
@query = query
|
13
|
+
@fragment = fragment
|
14
|
+
end
|
15
|
+
|
16
|
+
# The path component of the URI, e.g. /foo/bar/index.html
|
17
|
+
attr :path
|
18
|
+
|
19
|
+
# The query parameters.
|
20
|
+
attr :query
|
21
|
+
|
22
|
+
# A fragment identifier, the part after the '#'
|
23
|
+
attr :fragment
|
24
|
+
|
25
|
+
def append(buffer)
|
26
|
+
buffer << escape_path(@path)
|
27
|
+
|
28
|
+
unless @query.empty?
|
29
|
+
buffer << '?' << query_string
|
30
|
+
end
|
31
|
+
|
32
|
+
if @fragment
|
33
|
+
buffer << '#' << escape(@fragment)
|
34
|
+
end
|
35
|
+
|
36
|
+
return buffer
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_str
|
40
|
+
append(String.new)
|
41
|
+
end
|
42
|
+
|
43
|
+
alias to_s to_str
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# According to https://tools.ietf.org/html/rfc3986#section-3.3, we escape non-pchar.
|
48
|
+
NON_PCHAR = /([^
|
49
|
+
a-zA-Z0-9
|
50
|
+
\-\._~
|
51
|
+
!\$&'\(\)\*\+,;=
|
52
|
+
:@\/
|
53
|
+
]+)/x.freeze
|
54
|
+
|
55
|
+
def escape_path(path)
|
56
|
+
encoding = path.encoding
|
57
|
+
path.b.gsub(NON_PCHAR) do |m|
|
58
|
+
'%' + m.unpack('H2' * m.bytesize).join('%').upcase
|
59
|
+
end.force_encoding(encoding)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Escapes a generic string, using percent encoding.
|
63
|
+
def escape(string)
|
64
|
+
encoding = string.encoding
|
65
|
+
string.b.gsub(/([^a-zA-Z0-9_.\-]+)/) do |m|
|
66
|
+
'%' + m.unpack('H2' * m.bytesize).join('%').upcase
|
67
|
+
end.force_encoding(encoding)
|
68
|
+
end
|
69
|
+
|
70
|
+
def query_string
|
71
|
+
build_nested_query(@query)
|
72
|
+
end
|
73
|
+
|
74
|
+
def build_nested_query(value, prefix = nil)
|
75
|
+
case value
|
76
|
+
when Array
|
77
|
+
value.map { |v|
|
78
|
+
build_nested_query(v, "#{prefix}[]")
|
79
|
+
}.join("&")
|
80
|
+
when Hash
|
81
|
+
value.map { |k, v|
|
82
|
+
build_nested_query(v, prefix ? "#{prefix}[#{escape(k.to_s)}]" : escape(k.to_s))
|
83
|
+
}.reject(&:empty?).join('&')
|
84
|
+
when nil
|
85
|
+
prefix
|
86
|
+
else
|
87
|
+
raise ArgumentError, "value must be a Hash" if prefix.nil?
|
88
|
+
"#{prefix}=#{escape(value.to_s)}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Generate a URI from a path and user parameters. The path may contain a `#fragment` or `?query=parameters`.
|
94
|
+
def self.Reference(path = '', **parameters)
|
95
|
+
base, fragment = path.split('#', 2)
|
96
|
+
path, query_string = base.split('?', 2)
|
97
|
+
|
98
|
+
if query_string
|
99
|
+
query = Query.parse(Buffer.new(query_string))
|
100
|
+
else
|
101
|
+
query = {}
|
102
|
+
end
|
103
|
+
|
104
|
+
query.update(parameters)
|
105
|
+
|
106
|
+
Reference.new(path, query, fragment: fragment)
|
107
|
+
end
|
108
|
+
end
|
data/lib/xrb/strings.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2012-2024, by Samuel Williams.
|
5
|
+
|
6
|
+
module XRB
|
7
|
+
module Strings
|
8
|
+
HTML_ESCAPE = {"&" => "&", "<" => "<", ">" => ">", "\"" => """}
|
9
|
+
HTML_ESCAPE_PATTERN = Regexp.new("[" + Regexp.quote(HTML_ESCAPE.keys.join) + "]")
|
10
|
+
|
11
|
+
def self.to_html(string)
|
12
|
+
string.gsub(HTML_ESCAPE_PATTERN){|c| HTML_ESCAPE[c]}
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.to_quoted_string(string)
|
16
|
+
string = string.gsub('"', '\\"')
|
17
|
+
string.gsub!(/\r/, "\\r")
|
18
|
+
string.gsub!(/\n/, "\\n")
|
19
|
+
|
20
|
+
return "\"#{string}\""
|
21
|
+
end
|
22
|
+
|
23
|
+
# `value` must already be escaped.
|
24
|
+
def self.to_attribute(key, value)
|
25
|
+
%Q{#{key}="#{value}"}
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.to_simple_attribute(key, strict)
|
29
|
+
strict ? %Q{#{key}="#{key}"} : key.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.to_title(string)
|
33
|
+
string = string.gsub(/(^|[ \-_])(.)/){" " + $2.upcase}
|
34
|
+
string.strip!
|
35
|
+
|
36
|
+
return string
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.to_snake(string)
|
40
|
+
string = string.gsub("::", "")
|
41
|
+
string.gsub!(/([A-Z]+)/){"_" + $1.downcase}
|
42
|
+
string.sub!(/^_+/, "")
|
43
|
+
|
44
|
+
return string
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/xrb/tag.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2017-2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'markup'
|
7
|
+
|
8
|
+
module XRB
|
9
|
+
# This represents an individual SGML tag, e.g. <a>, </a> or <a />, with attributes. Attribute values must be escaped.
|
10
|
+
Tag = Struct.new(:name, :closed, :attributes) do
|
11
|
+
include XRB::Markup
|
12
|
+
|
13
|
+
def self.split(qualified_name)
|
14
|
+
if i = qualified_name.index(':')
|
15
|
+
return qualified_name.slice(0...i), qualified_name.slice(i+1..-1)
|
16
|
+
else
|
17
|
+
return nil, qualified_name
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.closed(name, attributes = {})
|
22
|
+
self.new(name, true, attributes)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.opened(name, attributes = {})
|
26
|
+
self.new(name, false, attributes)
|
27
|
+
end
|
28
|
+
|
29
|
+
def [] key
|
30
|
+
attributes[key]
|
31
|
+
end
|
32
|
+
|
33
|
+
alias to_hash attributes
|
34
|
+
|
35
|
+
def to_s(content = nil)
|
36
|
+
self.class.format_tag(name, attributes, content || !closed)
|
37
|
+
end
|
38
|
+
|
39
|
+
alias to_str to_s
|
40
|
+
|
41
|
+
def self_closed?
|
42
|
+
closed
|
43
|
+
end
|
44
|
+
|
45
|
+
def write_opening_tag(buffer)
|
46
|
+
buffer << '<' << name
|
47
|
+
|
48
|
+
self.class.append_attributes(buffer, attributes, nil)
|
49
|
+
|
50
|
+
if self_closed?
|
51
|
+
buffer << '/>'
|
52
|
+
else
|
53
|
+
buffer << '>'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def write_closing_tag(buffer)
|
58
|
+
buffer << '</' << name << '>'
|
59
|
+
end
|
60
|
+
|
61
|
+
def write(buffer, content = nil)
|
62
|
+
self.class.append_tag(buffer, name, attributes, content || !closed)
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.format_tag(name, attributes, content)
|
66
|
+
buffer = String.new.force_encoding(name.encoding)
|
67
|
+
|
68
|
+
self.append_tag(buffer, name, attributes, content)
|
69
|
+
|
70
|
+
return buffer
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.append_tag(buffer, name, attributes, content)
|
74
|
+
buffer << '<' << name.to_s
|
75
|
+
|
76
|
+
self.append_attributes(buffer, attributes, nil)
|
77
|
+
|
78
|
+
if !content
|
79
|
+
buffer << '/>'
|
80
|
+
else
|
81
|
+
buffer << '>'
|
82
|
+
unless content == true
|
83
|
+
Markup.append(buffer, content)
|
84
|
+
end
|
85
|
+
buffer << '</' << name.to_s << '>'
|
86
|
+
end
|
87
|
+
|
88
|
+
return nil
|
89
|
+
end
|
90
|
+
|
91
|
+
# Convert a set of attributes into a string suitable for use within a <tag>.
|
92
|
+
def self.append_attributes(buffer, attributes, prefix)
|
93
|
+
attributes.each do |key, value|
|
94
|
+
next unless value
|
95
|
+
|
96
|
+
attribute_key = prefix ? "#{prefix}-#{key}" : key
|
97
|
+
|
98
|
+
case value
|
99
|
+
when Hash
|
100
|
+
self.append_attributes(buffer, value, attribute_key)
|
101
|
+
when Array
|
102
|
+
self.append_attributes(buffer, value, attribute_key)
|
103
|
+
when TrueClass
|
104
|
+
buffer << ' ' << attribute_key.to_s
|
105
|
+
else
|
106
|
+
buffer << ' ' << attribute_key.to_s << '="'
|
107
|
+
Markup.append(buffer, value)
|
108
|
+
buffer << '"'
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
return nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
data/lib/xrb/template.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2012-2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'parsers'
|
7
|
+
require_relative 'markup'
|
8
|
+
require_relative 'buffer'
|
9
|
+
require_relative 'builder'
|
10
|
+
|
11
|
+
module XRB
|
12
|
+
# The output variable that will be used in templates:
|
13
|
+
OUT = :_out
|
14
|
+
BINDING = binding
|
15
|
+
|
16
|
+
class Builder
|
17
|
+
def capture(*arguments, &block)
|
18
|
+
Template.capture(*arguments, output: self, &block)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Template
|
23
|
+
# Returns the output produced by calling the given block.
|
24
|
+
def self.capture(*arguments, output: nil, &block)
|
25
|
+
scope = block.binding
|
26
|
+
previous_output = scope.local_variable_get(OUT)
|
27
|
+
|
28
|
+
output ||= previous_output.class.new(encoding: previous_output.encoding)
|
29
|
+
scope.local_variable_set(OUT, output)
|
30
|
+
|
31
|
+
begin
|
32
|
+
block.call(*arguments)
|
33
|
+
ensure
|
34
|
+
scope.local_variable_set(OUT, previous_output)
|
35
|
+
end
|
36
|
+
|
37
|
+
return output
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the buffer used for capturing output.
|
41
|
+
def self.buffer(binding)
|
42
|
+
binding.local_variable_get(OUT)
|
43
|
+
end
|
44
|
+
|
45
|
+
class Assembler
|
46
|
+
def initialize(encoding: Encoding::UTF_8)
|
47
|
+
@code = String.new.force_encoding(encoding)
|
48
|
+
end
|
49
|
+
|
50
|
+
attr :code
|
51
|
+
|
52
|
+
# Output raw text to the template.
|
53
|
+
def text(text)
|
54
|
+
text = text.gsub("'", "\\\\'")
|
55
|
+
@code << "#{OUT}<<'#{text}';"
|
56
|
+
|
57
|
+
# This is an interesting approach, but it doens't preserve newlines or tabs as raw characters, so template line numbers don't match up.
|
58
|
+
# @parts << "#{OUT}<<#{text.dump};"
|
59
|
+
end
|
60
|
+
|
61
|
+
# Output a ruby expression (or part of).
|
62
|
+
def instruction(text, postfix = nil)
|
63
|
+
@code << text << (postfix || ';')
|
64
|
+
end
|
65
|
+
|
66
|
+
# Output a string interpolation.
|
67
|
+
def expression(text)
|
68
|
+
# Double brackets are required here to handle expressions like #{foo rescue "bar"}.
|
69
|
+
@code << "#{OUT}<<String(#{text});"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.load_file(path, **options)
|
74
|
+
self.new(FileBuffer.new(path), **options).freeze
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.load(string, *arguments, **options)
|
78
|
+
self.new(Buffer.new(string), **options).freeze
|
79
|
+
end
|
80
|
+
|
81
|
+
# @param binding [Binding] The binding in which the template is compiled. e.g. `TOPLEVEL_BINDING`.
|
82
|
+
def initialize(buffer, binding: BINDING)
|
83
|
+
@buffer = buffer
|
84
|
+
@binding = binding
|
85
|
+
end
|
86
|
+
|
87
|
+
def freeze
|
88
|
+
return self if frozen?
|
89
|
+
|
90
|
+
to_proc
|
91
|
+
|
92
|
+
super
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_string(scope = Object.new, output = nil)
|
96
|
+
output ||= output_buffer
|
97
|
+
|
98
|
+
scope.instance_exec(output, &to_proc)
|
99
|
+
|
100
|
+
return output
|
101
|
+
end
|
102
|
+
|
103
|
+
def to_buffer(scope)
|
104
|
+
Buffer.new(to_string(scope), path: @buffer.path)
|
105
|
+
end
|
106
|
+
|
107
|
+
def to_proc(scope = @binding.dup)
|
108
|
+
@compiled_proc ||= eval("\# frozen_string_literal: true\nproc{|#{OUT}|;#{code}}", scope, @buffer.path, 0).freeze
|
109
|
+
end
|
110
|
+
|
111
|
+
protected
|
112
|
+
|
113
|
+
def output_buffer
|
114
|
+
String.new.force_encoding(code.encoding)
|
115
|
+
end
|
116
|
+
|
117
|
+
def code
|
118
|
+
@code ||= compile!
|
119
|
+
end
|
120
|
+
|
121
|
+
def make_assembler
|
122
|
+
Assembler.new
|
123
|
+
end
|
124
|
+
|
125
|
+
def compile!
|
126
|
+
assembler = make_assembler
|
127
|
+
|
128
|
+
Parsers.parse_template(@buffer, assembler)
|
129
|
+
|
130
|
+
assembler.code
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class MarkupTemplate < Template
|
135
|
+
class Assembler < Template::Assembler
|
136
|
+
# Output a string interpolation.
|
137
|
+
def expression(code)
|
138
|
+
@code << "#{OUT}<<(#{code});"
|
139
|
+
end
|
140
|
+
|
141
|
+
# Output raw text to the template.
|
142
|
+
def text(text)
|
143
|
+
text = text.gsub("'", "\\\\'")
|
144
|
+
@code << "#{OUT}.raw('#{text}');"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def to_string(scope = Object.new, output = nil)
|
149
|
+
super.output
|
150
|
+
end
|
151
|
+
|
152
|
+
protected
|
153
|
+
|
154
|
+
# We need an assembler which builds specific `Markup.append` sequences.
|
155
|
+
def make_assembler
|
156
|
+
Assembler.new
|
157
|
+
end
|
158
|
+
|
159
|
+
# The output of the markup template is encoded markup (e.g. with entities, tags, etc).
|
160
|
+
def output_buffer
|
161
|
+
Builder.new(encoding: code.encoding)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|