trenni 1.7.0 → 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|