trenni 1.7.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.travis.yml +7 -3
- data/Gemfile +4 -0
- data/README.md +72 -10
- data/Rakefile +85 -0
- data/benchmark/call_vs_yield.rb +51 -0
- data/benchmark/interpolation_vs_concat.rb +29 -0
- data/benchmark/io_vs_string.rb +14 -4
- data/entities.json +2233 -0
- data/ext/trenni/extconf.rb +13 -0
- data/ext/trenni/markup.c +1981 -0
- data/ext/trenni/markup.h +6 -0
- data/ext/trenni/markup.rl +223 -0
- data/ext/trenni/template.c +1113 -0
- data/ext/trenni/template.h +6 -0
- data/ext/trenni/template.rl +77 -0
- data/ext/trenni/trenni.c +64 -0
- data/ext/trenni/trenni.h +28 -0
- data/lib/trenni.rb +1 -0
- data/lib/trenni/buffer.rb +13 -2
- data/lib/trenni/builder.rb +54 -47
- data/lib/trenni/entities.rb +2154 -0
- data/lib/trenni/entities.trenni +34 -0
- data/lib/trenni/fallback/markup.rb +1648 -0
- data/lib/trenni/fallback/markup.rl +236 -0
- data/lib/trenni/fallback/template.rb +843 -0
- data/lib/trenni/fallback/template.rl +97 -0
- data/lib/trenni/markup.rb +76 -0
- data/lib/trenni/native.rb +28 -0
- data/lib/trenni/parse_delegate.rb +34 -0
- data/lib/trenni/{scanner.rb → parse_error.rb} +9 -54
- data/lib/trenni/parsers.rb +12 -0
- data/lib/trenni/substitutions.rb +45 -0
- data/lib/trenni/template.rb +52 -135
- data/lib/trenni/version.rb +1 -1
- data/parsers/trenni/entities.rl +11 -0
- data/parsers/trenni/markup.rl +43 -0
- data/parsers/trenni/template.rl +60 -0
- data/spec/trenni/builder_spec.rb +37 -62
- data/spec/trenni/corpus/large.rb +4605 -0
- data/spec/trenni/corpus/large.xhtml +726 -0
- data/spec/trenni/markup_parser_spec.rb +233 -0
- data/spec/trenni/markup_spec.rb +48 -0
- data/spec/trenni/parsers_performance_spec.rb +44 -0
- data/spec/trenni/template_error_spec.rb +36 -0
- data/spec/trenni/template_performance_spec.rb +102 -0
- data/spec/trenni/template_spec.rb +90 -64
- data/spec/trenni/template_spec/basic.trenni +1 -0
- data/spec/trenni/template_spec/capture.trenni +2 -2
- data/spec/trenni/template_spec/error.trenni +4 -0
- data/trenni.gemspec +9 -6
- metadata +57 -15
- data/lib/trenni/parser.rb +0 -153
- data/spec/trenni/parser_spec.rb +0 -144
@@ -0,0 +1,97 @@
|
|
1
|
+
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
%%{
|
22
|
+
machine template;
|
23
|
+
|
24
|
+
action instruction_begin {
|
25
|
+
instruction_begin = p
|
26
|
+
}
|
27
|
+
|
28
|
+
action instruction_end {
|
29
|
+
instruction_end = p
|
30
|
+
}
|
31
|
+
|
32
|
+
action emit_instruction {
|
33
|
+
delegate.instruction(data.byteslice(instruction_begin...instruction_end))
|
34
|
+
}
|
35
|
+
|
36
|
+
action emit_instruction_line {
|
37
|
+
delegate.instruction(data.byteslice(instruction_begin...instruction_end), "\n")
|
38
|
+
}
|
39
|
+
|
40
|
+
action instruction_error {
|
41
|
+
raise ParseError.new("failed to parse instruction", buffer, p)
|
42
|
+
}
|
43
|
+
|
44
|
+
action expression_begin {
|
45
|
+
expression_begin = p
|
46
|
+
}
|
47
|
+
|
48
|
+
action expression_end {
|
49
|
+
expression_end = p
|
50
|
+
}
|
51
|
+
|
52
|
+
action emit_expression {
|
53
|
+
delegate.expression(data.byteslice(expression_begin...expression_end))
|
54
|
+
}
|
55
|
+
|
56
|
+
action expression_error {
|
57
|
+
raise ParseError.new("failed to parse expression", buffer, p)
|
58
|
+
}
|
59
|
+
|
60
|
+
action emit_text {
|
61
|
+
delegate.text(data.byteslice(ts...te))
|
62
|
+
}
|
63
|
+
|
64
|
+
# This magic ensures that we process bytes.
|
65
|
+
getkey bytes[p];
|
66
|
+
|
67
|
+
include template "trenni/template.rl";
|
68
|
+
}%%
|
69
|
+
|
70
|
+
require_relative '../parse_error'
|
71
|
+
|
72
|
+
module Trenni
|
73
|
+
module Fallback
|
74
|
+
%% write data;
|
75
|
+
|
76
|
+
def self.parse_template(buffer, delegate)
|
77
|
+
data = buffer.read
|
78
|
+
bytes = data.bytes
|
79
|
+
|
80
|
+
p = 0
|
81
|
+
pe = eof = data.bytesize
|
82
|
+
stack = []
|
83
|
+
|
84
|
+
expression_begin = expression_end = nil
|
85
|
+
instruction_begin = instruction_end = nil
|
86
|
+
|
87
|
+
%% write init;
|
88
|
+
%% write exec;
|
89
|
+
|
90
|
+
if p != eof
|
91
|
+
raise ParseError.new("could not consume all input", buffer, p)
|
92
|
+
end
|
93
|
+
|
94
|
+
return nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'substitutions'
|
22
|
+
|
23
|
+
module Trenni
|
24
|
+
# A wrapper which indicates that `value` can be appended to the output buffer without any changes.
|
25
|
+
module Markup
|
26
|
+
# Generates a string suitable for concatenating with the output buffer.
|
27
|
+
def self.escape(value)
|
28
|
+
if value.is_a? Markup
|
29
|
+
value
|
30
|
+
elsif value
|
31
|
+
MarkupString.new(value.to_s)
|
32
|
+
else
|
33
|
+
# String#<< won't accept nil, so we return an empty string, thus ensuring a fixed point function:
|
34
|
+
EMPTY
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def escape(value)
|
39
|
+
Markup.escape(value)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Initialized from text which is escaped to use HTML entities.
|
44
|
+
class MarkupString < String
|
45
|
+
include Markup
|
46
|
+
|
47
|
+
# This is only casually related to HTML, it's just enough so that it would not be mis-interpreted by `Trenni::Parser`.
|
48
|
+
ESCAPE = Substitutions.new("&" => "&", "<" => "<", ">" => ">", "\"" => """)
|
49
|
+
|
50
|
+
# Convert ESCAPE characters into their corresponding entities.
|
51
|
+
def initialize(string = nil, escape = true)
|
52
|
+
if string
|
53
|
+
super(string)
|
54
|
+
ESCAPE.gsub!(self) if escape
|
55
|
+
else
|
56
|
+
super()
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.raw(string)
|
61
|
+
self.new(string, false)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
module Script
|
66
|
+
def self.json(value)
|
67
|
+
MarkupString.new(JSON.dump(value), false)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.MarkupString(value)
|
72
|
+
Markup.escape(value)
|
73
|
+
end
|
74
|
+
|
75
|
+
Markup::EMPTY = String.new.extend(Markup).freeze
|
76
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'parse_error'
|
22
|
+
|
23
|
+
begin
|
24
|
+
# Load native code:
|
25
|
+
require_relative 'trenni'
|
26
|
+
rescue LoadError
|
27
|
+
warn "Could not load native implementation: #{$!}" if $VERBOSE
|
28
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
module Trenni
|
22
|
+
# This is a sample delegate for capturing all events. It's only use is for testing.
|
23
|
+
class ParseDelegate
|
24
|
+
def initialize
|
25
|
+
@events = []
|
26
|
+
end
|
27
|
+
|
28
|
+
attr :events
|
29
|
+
|
30
|
+
def method_missing(*args)
|
31
|
+
@events << args
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -20,37 +20,24 @@
|
|
20
20
|
|
21
21
|
require_relative 'buffer'
|
22
22
|
|
23
|
-
require 'strscan'
|
24
|
-
|
25
23
|
module Trenni
|
26
24
|
class ParseError < StandardError
|
27
|
-
def initialize(message,
|
25
|
+
def initialize(message, buffer, offset)
|
28
26
|
super(message)
|
29
27
|
|
30
|
-
@
|
31
|
-
|
32
|
-
@locations = []
|
33
|
-
|
34
|
-
if positions
|
35
|
-
positions.each do |position|
|
36
|
-
@locations << Location.new(scanner.string, position)
|
37
|
-
end
|
38
|
-
else
|
39
|
-
@locations = [Location.new(scanner.string, scanner.pos)]
|
40
|
-
end
|
41
|
-
|
42
|
-
@location = @locations.first
|
43
|
-
|
44
|
-
@input_name = nil
|
28
|
+
@buffer = buffer
|
29
|
+
@offset = offset
|
45
30
|
end
|
46
31
|
|
47
|
-
|
48
|
-
|
32
|
+
def location
|
33
|
+
@location ||= Location.new(@buffer.read, @offset)
|
34
|
+
end
|
49
35
|
|
36
|
+
attr :buffer
|
50
37
|
attr :path
|
51
38
|
|
52
39
|
def to_s
|
53
|
-
"#{
|
40
|
+
"#{buffer.path}#{location}: #{super}\n#{location.line_text}"
|
54
41
|
end
|
55
42
|
end
|
56
43
|
|
@@ -82,7 +69,7 @@ module Trenni
|
|
82
69
|
end
|
83
70
|
|
84
71
|
def to_s
|
85
|
-
"[#{self.line_number}]"
|
72
|
+
"[#{self.line_number}:#{self.line_offset}]"
|
86
73
|
end
|
87
74
|
|
88
75
|
# The line that contains the @offset (base 0 indexing).
|
@@ -103,36 +90,4 @@ module Trenni
|
|
103
90
|
|
104
91
|
attr :line_text
|
105
92
|
end
|
106
|
-
|
107
|
-
class StringScanner < ::StringScanner
|
108
|
-
def initialize(buffer)
|
109
|
-
@buffer = buffer
|
110
|
-
|
111
|
-
super(buffer.read)
|
112
|
-
end
|
113
|
-
|
114
|
-
attr :buffer
|
115
|
-
|
116
|
-
def path
|
117
|
-
@buffer.path
|
118
|
-
end
|
119
|
-
|
120
|
-
STUCK_MESSAGE = "Parser is stuck!".freeze
|
121
|
-
|
122
|
-
def stuck?(position)
|
123
|
-
self.pos == position
|
124
|
-
end
|
125
|
-
|
126
|
-
def raise_if_stuck(position, message = STUCK_MESSAGE)
|
127
|
-
if stuck?(position)
|
128
|
-
parse_error!(message)
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
def parse_error!(message, positions = nil)
|
133
|
-
positions ||= [self.pos]
|
134
|
-
|
135
|
-
raise ParseError.new(message, self, positions)
|
136
|
-
end
|
137
|
-
end
|
138
93
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
|
2
|
+
require_relative 'native'
|
3
|
+
require_relative 'parse_delegate'
|
4
|
+
|
5
|
+
if defined? Trenni::Native and !ENV['TRENNI_PREFER_FALLBACK']
|
6
|
+
Trenni::Parsers = Trenni::Native
|
7
|
+
else
|
8
|
+
require_relative 'fallback/markup'
|
9
|
+
require_relative 'fallback/template'
|
10
|
+
|
11
|
+
Trenni::Parsers = Trenni::Fallback
|
12
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
module Trenni
|
22
|
+
class Substitutions
|
23
|
+
def initialize(tokens)
|
24
|
+
@tokens = tokens
|
25
|
+
end
|
26
|
+
|
27
|
+
def patterns
|
28
|
+
@tokens.keys
|
29
|
+
end
|
30
|
+
|
31
|
+
def pattern
|
32
|
+
@pattern ||= Regexp.union(patterns)
|
33
|
+
end
|
34
|
+
|
35
|
+
attr :tokens
|
36
|
+
|
37
|
+
def gsub!(string)
|
38
|
+
string.gsub!(pattern) {|match| @tokens[match]}
|
39
|
+
end
|
40
|
+
|
41
|
+
def gsub(string)
|
42
|
+
string.gsub(pattern) {|match| @tokens[match]}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/trenni/template.rb
CHANGED
@@ -18,197 +18,114 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
require_relative '
|
21
|
+
require_relative 'parsers'
|
22
22
|
|
23
23
|
module Trenni
|
24
24
|
# The output variable that will be used in templates:
|
25
|
-
OUT =
|
25
|
+
OUT = :_out
|
26
26
|
|
27
27
|
class Template
|
28
28
|
# Returns the output produced by calling the given block.
|
29
29
|
def self.capture(*args, &block)
|
30
|
-
|
31
|
-
|
30
|
+
scope = block.binding
|
31
|
+
output_buffer = scope.local_variable_get(OUT)
|
32
32
|
|
33
|
-
|
33
|
+
capture_buffer = String.new.force_encoding(output_buffer.encoding)
|
34
|
+
scope.local_variable_set(OUT, capture_buffer)
|
34
35
|
|
35
|
-
|
36
|
+
begin
|
37
|
+
block.call(*args)
|
38
|
+
ensure
|
39
|
+
scope.local_variable_set(OUT, output_buffer)
|
40
|
+
end
|
41
|
+
|
42
|
+
return capture_buffer
|
36
43
|
end
|
37
44
|
|
38
45
|
# Returns the buffer used for capturing output.
|
39
46
|
def self.buffer(binding)
|
40
|
-
|
47
|
+
binding.local_variable_get(OUT)
|
41
48
|
end
|
42
49
|
|
43
50
|
class Assembler
|
44
|
-
def initialize
|
45
|
-
@
|
51
|
+
def initialize(filter: String, encoding: Encoding::UTF_8)
|
52
|
+
@code = String.new.force_encoding(encoding)
|
53
|
+
@filter = filter
|
46
54
|
end
|
47
55
|
|
48
|
-
attr :
|
56
|
+
attr :code
|
49
57
|
|
50
58
|
# Output raw text to the template.
|
51
59
|
def text(text)
|
52
60
|
text = text.gsub("'", "\\\\'")
|
53
|
-
@
|
61
|
+
@code << "#{OUT}<<'#{text}';"
|
54
62
|
|
55
63
|
# This is an interesting approach, but it doens't preserve newlines or tabs as raw characters, so template line numbers don't match up.
|
56
64
|
# @parts << "#{OUT}<<#{text.dump};"
|
57
65
|
end
|
58
66
|
|
59
67
|
# Output a ruby expression (or part of).
|
60
|
-
def
|
61
|
-
@
|
68
|
+
def instruction(text, postfix = nil)
|
69
|
+
@code << text << (postfix || ';')
|
62
70
|
end
|
63
71
|
|
64
72
|
# Output a string interpolation.
|
65
|
-
def
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
CODE_PREFIX = "#{OUT}=[];".freeze
|
70
|
-
CODE_POSTFIX = "#{OUT}".freeze
|
71
|
-
|
72
|
-
def code
|
73
|
-
return [CODE_PREFIX, *@parts, CODE_POSTFIX].join
|
73
|
+
def expression(text)
|
74
|
+
# Double brackets are required here to handle expressions like #{foo rescue "bar"}.
|
75
|
+
@code << "#{OUT}<<#{@filter}((#{text}));"
|
74
76
|
end
|
75
77
|
end
|
76
78
|
|
77
|
-
|
78
|
-
|
79
|
-
super(buffer)
|
80
|
-
|
81
|
-
@delegate = delegate
|
82
|
-
end
|
83
|
-
|
84
|
-
def parse!
|
85
|
-
until eos?
|
86
|
-
start_pos = self.pos
|
87
|
-
|
88
|
-
scan_text
|
89
|
-
scan_expression or scan_interpolation
|
90
|
-
|
91
|
-
raise_if_stuck(start_pos)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
# This is formulated specifically so that it matches up until the start of a code block.
|
96
|
-
TEXT = /([^<#]|<(?!\?r)|#(?!\{)){1,}/m
|
97
|
-
|
98
|
-
def scan_text
|
99
|
-
if scan(TEXT)
|
100
|
-
@delegate.text(self.matched)
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
def scan_expression
|
105
|
-
start_pos = self.pos
|
106
|
-
|
107
|
-
if scan(/<\?r/)
|
108
|
-
if scan_until(/(.*?)\?>/m)
|
109
|
-
@delegate.expression(self[1])
|
110
|
-
else
|
111
|
-
parse_error!("Could not find end of expression!", [start_pos, self.pos])
|
112
|
-
end
|
113
|
-
|
114
|
-
return true
|
115
|
-
end
|
116
|
-
|
117
|
-
return false
|
118
|
-
end
|
119
|
-
|
120
|
-
def scan_interpolation
|
121
|
-
start_pos = self.pos
|
122
|
-
|
123
|
-
if scan(/\#\{/)
|
124
|
-
level = 1
|
125
|
-
code = String.new
|
126
|
-
|
127
|
-
until eos?
|
128
|
-
current_pos = self.pos
|
129
|
-
|
130
|
-
# Scan anything other than something which causes nesting:
|
131
|
-
if scan(/[^"'\{\}]+/m)
|
132
|
-
code << matched
|
133
|
-
end
|
134
|
-
|
135
|
-
# Scan a quoted string:
|
136
|
-
if scan(/'(\\'|[^'])*'/m) or scan(/"(\\"|[^"])*"/m)
|
137
|
-
code << matched
|
138
|
-
end
|
139
|
-
|
140
|
-
# Scan something which nests:
|
141
|
-
if scan(/\{/)
|
142
|
-
code << matched
|
143
|
-
level += 1
|
144
|
-
end
|
145
|
-
|
146
|
-
if scan(/\}/)
|
147
|
-
level -= 1
|
148
|
-
if level == 0
|
149
|
-
@delegate.interpolation(code)
|
150
|
-
return true
|
151
|
-
else
|
152
|
-
code << matched
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
break if stuck?(current_pos)
|
157
|
-
end
|
158
|
-
|
159
|
-
parse_error!("Could not find end of interpolation!", [start_pos, self.pos])
|
160
|
-
end
|
161
|
-
|
162
|
-
return false
|
163
|
-
end
|
79
|
+
def self.load_file(path, **options)
|
80
|
+
self.new(FileBuffer.new(path), **options)
|
164
81
|
end
|
165
82
|
|
166
|
-
def
|
167
|
-
self.new(FileBuffer.new(path))
|
168
|
-
end
|
169
|
-
|
170
|
-
def initialize(buffer)
|
83
|
+
def initialize(buffer, filter: String)
|
171
84
|
@buffer = buffer
|
85
|
+
@filter = filter
|
172
86
|
end
|
173
87
|
|
174
|
-
def to_string(scope = Object.new)
|
175
|
-
|
88
|
+
def to_string(scope = Object.new, output = output_buffer)
|
89
|
+
output ||= output_buffer
|
90
|
+
|
91
|
+
scope.instance_exec(output, &to_proc)
|
176
92
|
end
|
177
93
|
|
178
94
|
def to_buffer(scope)
|
179
|
-
Buffer.new(
|
95
|
+
Buffer.new(to_string(scope), path: @buffer.path)
|
180
96
|
end
|
181
97
|
|
182
|
-
|
183
|
-
|
184
|
-
alias result to_string
|
185
|
-
|
186
|
-
def to_array(scope)
|
187
|
-
if Binding === scope
|
188
|
-
# Slow code path, evaluate the code string in the given binding (scope).
|
189
|
-
eval(code, scope, @buffer.path)
|
190
|
-
else
|
191
|
-
# Faster code path, use instance_eval on a compiled Proc.
|
192
|
-
scope.instance_eval(&to_proc)
|
193
|
-
end
|
194
|
-
end
|
195
|
-
|
196
|
-
def to_proc
|
197
|
-
@compiled_proc ||= eval("proc{;#{code};}", binding, @buffer.path)
|
98
|
+
def to_proc(scope = nil)
|
99
|
+
@compiled_proc ||= eval("proc{|#{OUT}|;#{code};#{OUT}}", scope, @buffer.path)
|
198
100
|
end
|
199
101
|
|
200
102
|
protected
|
201
103
|
|
104
|
+
def output_buffer
|
105
|
+
String.new.force_encoding(code.encoding)
|
106
|
+
end
|
107
|
+
|
202
108
|
def code
|
203
109
|
@code ||= compile!
|
204
110
|
end
|
205
111
|
|
206
112
|
def compile!
|
207
|
-
assembler = Assembler.new
|
113
|
+
assembler = Assembler.new(filter: @filter)
|
208
114
|
|
209
|
-
|
115
|
+
Parsers.parse_template(@buffer, assembler)
|
210
116
|
|
211
117
|
assembler.code
|
212
118
|
end
|
213
119
|
end
|
120
|
+
|
121
|
+
class MarkupTemplate < Template
|
122
|
+
def initialize(buffer, filter: MarkupString)
|
123
|
+
super
|
124
|
+
end
|
125
|
+
|
126
|
+
# The output of the markup template is encoded markup (e.g. with entities, tags, etc)
|
127
|
+
def output_buffer
|
128
|
+
MarkupString.new.force_encoding(code.encoding)
|
129
|
+
end
|
130
|
+
end
|
214
131
|
end
|