xrb 0.1 → 0.3.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 +7 -0
- checksums.yaml.gz.sig +1 -0
- data/bake/xrb/entities.rb +60 -0
- data/bake/xrb/parsers.rb +66 -0
- data/ext/Makefile +270 -0
- data/ext/XRB_Extension.bundle +0 -0
- data/ext/escape.o +0 -0
- data/ext/extconf.h +5 -0
- data/ext/extconf.rb +21 -0
- data/ext/markup.o +0 -0
- data/ext/mkmf.log +122 -0
- data/ext/query.o +0 -0
- data/ext/tag.o +0 -0
- data/ext/template.o +0 -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/ext/xrb.o +0 -0
- data/lib/xrb/buffer.rb +103 -0
- data/lib/xrb/builder.rb +229 -0
- data/lib/xrb/entities.rb +2137 -0
- data/lib/xrb/entities.xrb +15 -0
- data/lib/xrb/error.rb +81 -0
- data/lib/xrb/fallback/markup.rb +1657 -0
- data/lib/xrb/fallback/markup.rl +227 -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/parsers.rb +16 -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 +128 -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 +34 -0
- data.tar.gz.sig +0 -0
- metadata +118 -58
- metadata.gz.sig +2 -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']
|
data/lib/xrb/parsers.rb
ADDED
@@ -0,0 +1,16 @@
|
|
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
|
+
|
8
|
+
if defined? XRB::Native
|
9
|
+
XRB::Parsers = XRB::Native
|
10
|
+
else
|
11
|
+
require_relative 'fallback/markup'
|
12
|
+
require_relative 'fallback/template'
|
13
|
+
require_relative 'fallback/query'
|
14
|
+
|
15
|
+
XRB::Parsers = XRB::Fallback
|
16
|
+
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,128 @@
|
|
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}.raw('#{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(code)
|
68
|
+
@code << "#{OUT}<<(#{code});"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.load_file(path, **options)
|
73
|
+
self.new(FileBuffer.new(path), **options).freeze
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.load(string, *arguments, **options)
|
77
|
+
self.new(Buffer.new(string), **options).freeze
|
78
|
+
end
|
79
|
+
|
80
|
+
# @param binding [Binding] The binding in which the template is compiled. e.g. `TOPLEVEL_BINDING`.
|
81
|
+
def initialize(buffer, binding: BINDING)
|
82
|
+
@buffer = buffer
|
83
|
+
@binding = binding
|
84
|
+
end
|
85
|
+
|
86
|
+
def freeze
|
87
|
+
return self if frozen?
|
88
|
+
|
89
|
+
to_proc
|
90
|
+
|
91
|
+
super
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_string(scope = Object.new, output = nil)
|
95
|
+
builder = Builder.new(output, encoding: code.encoding)
|
96
|
+
|
97
|
+
scope.instance_exec(builder, &to_proc)
|
98
|
+
|
99
|
+
return builder.output
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_buffer(scope)
|
103
|
+
Buffer.new(to_string(scope), path: @buffer.path)
|
104
|
+
end
|
105
|
+
|
106
|
+
def to_proc(scope = @binding.dup)
|
107
|
+
@compiled_proc ||= eval("\# frozen_string_literal: true\nproc{|#{OUT}|;#{code}}", scope, @buffer.path, 0).freeze
|
108
|
+
end
|
109
|
+
|
110
|
+
protected
|
111
|
+
|
112
|
+
def code
|
113
|
+
@code ||= compile!
|
114
|
+
end
|
115
|
+
|
116
|
+
def make_assembler
|
117
|
+
Assembler.new
|
118
|
+
end
|
119
|
+
|
120
|
+
def compile!
|
121
|
+
assembler = make_assembler
|
122
|
+
|
123
|
+
Parsers.parse_template(@buffer, assembler)
|
124
|
+
|
125
|
+
assembler.code
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|