trenni 1.5.1 → 1.6.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 +4 -4
- data/README.md +3 -2
- data/lib/trenni/buffer.rb +79 -0
- data/lib/trenni/parser.rb +60 -123
- data/lib/trenni/scanner.rb +138 -0
- data/lib/trenni/template.rb +68 -53
- data/lib/trenni/version.rb +1 -1
- data/spec/trenni/parser_spec.rb +20 -15
- data/spec/trenni/template_spec.rb +11 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c17ddcecd402c971c098d70f430380b664cd581e
|
4
|
+
data.tar.gz: b1270c75070860bb91008a08c4be7c58f1eaa5e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c7387728d78797608f8369bd617b86edcaa23bd2663ef1b854917515a09bc0f9888f357267378620af854f469007e5fea9928374477a57025e34c0950fc3efc
|
7
|
+
data.tar.gz: 46684a95b5e07c3f522cba2f04db9ca1320c2edcb205d361156014fa9789b6332a014694e8dabb4fe2ce7a32ee56795d4b0be8b1f7dc4a2d7f52f974530bd818
|
data/README.md
CHANGED
@@ -41,13 +41,14 @@ Or install it yourself as:
|
|
41
41
|
|
42
42
|
Trenni templates work essentially the same way as all other templating systems:
|
43
43
|
|
44
|
-
|
44
|
+
buffer = Trenni::Buffer.new('<?r items.each do |item| ?>#{item}<?r end ?>')
|
45
|
+
template = Trenni::Template.new(buffer)
|
45
46
|
|
46
47
|
items = 1..4
|
47
48
|
|
48
49
|
template.to_string(binding) # => "1234"
|
49
50
|
|
50
|
-
The code above demonstrate
|
51
|
+
The code above demonstrate the only two constructs, `<?r expression ?>` and `#{output}`.
|
51
52
|
|
52
53
|
Trenni provides a slightly higher performance API using objects rather than bindings. If you provide an object instance, `instance_eval` would be used instead.
|
53
54
|
|
@@ -0,0 +1,79 @@
|
|
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 Buffer
|
23
|
+
def initialize(string, path: '<string>')
|
24
|
+
@string = string
|
25
|
+
@path = path
|
26
|
+
end
|
27
|
+
|
28
|
+
attr :path
|
29
|
+
|
30
|
+
def read
|
31
|
+
@string
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.load_file(path)
|
35
|
+
FileBuffer.new(path)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.load(string)
|
39
|
+
Buffer.new(string)
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_buffer
|
43
|
+
self
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class FileBuffer
|
48
|
+
def initialize(path)
|
49
|
+
@path = path
|
50
|
+
end
|
51
|
+
|
52
|
+
attr :path
|
53
|
+
|
54
|
+
def read
|
55
|
+
@buffer ||= File.read(@path)
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_buffer
|
59
|
+
Buffer.new(self.read, @path)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class IOBuffer
|
64
|
+
def initialize(io, path: io.inspect)
|
65
|
+
@io = io
|
66
|
+
@path = path
|
67
|
+
end
|
68
|
+
|
69
|
+
attr :path
|
70
|
+
|
71
|
+
def read
|
72
|
+
@io.read
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_buffer
|
76
|
+
Buffer.new(self.read, path: @path)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/trenni/parser.rb
CHANGED
@@ -18,81 +18,21 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
|
21
|
+
require_relative 'scanner'
|
22
22
|
|
23
23
|
module Trenni
|
24
24
|
# This parser processes general markup into a sequence of events which are passed to a delegate.
|
25
|
-
class Parser
|
25
|
+
class Parser < StringScanner
|
26
26
|
OPENED_TAG = :opened
|
27
27
|
CLOSED_TAG = :closed
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
raise ArgumentError.new("Offset #{index} is past end of input #{input.bytesize}") if offset > input.bytesize
|
32
|
-
|
33
|
-
@offset = offset
|
34
|
-
@line_index = 0
|
35
|
-
line_offset = next_line_offset = 0
|
36
|
-
|
37
|
-
input.each_line do |line|
|
38
|
-
line_offset = next_line_offset
|
39
|
-
next_line_offset += line.bytesize
|
40
|
-
|
41
|
-
# Is our input offset within this line?
|
42
|
-
if next_line_offset >= offset
|
43
|
-
@line_text = line.chomp
|
44
|
-
@line_range = line_offset...next_line_offset
|
45
|
-
break
|
46
|
-
else
|
47
|
-
@line_index += 1
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def to_i
|
53
|
-
@offset
|
54
|
-
end
|
55
|
-
|
56
|
-
def to_s
|
57
|
-
":#{self.line_number}"
|
58
|
-
end
|
59
|
-
|
60
|
-
# The line that contains the @offset (base 0 indexing).
|
61
|
-
attr :line_index
|
62
|
-
|
63
|
-
# The line index, but base-1.
|
64
|
-
def line_number
|
65
|
-
@line_index + 1
|
66
|
-
end
|
29
|
+
def initialize(buffer, delegate)
|
30
|
+
super(buffer)
|
67
31
|
|
68
|
-
# The byte offset to the start of that line.
|
69
|
-
attr :line_range
|
70
|
-
|
71
|
-
# The number of bytes from the start of the line to the given offset in the input.
|
72
|
-
def line_offset
|
73
|
-
@offset - @line_range.min
|
74
|
-
end
|
75
|
-
|
76
|
-
attr :line_text
|
77
|
-
end
|
78
|
-
|
79
|
-
class ParseError < StandardError
|
80
|
-
def initialize(message, scanner)
|
81
|
-
@message = message
|
82
|
-
@location = Location.new(scanner.string, scanner.pos)
|
83
|
-
end
|
84
|
-
|
85
|
-
attr :location
|
86
|
-
|
87
|
-
def to_s
|
88
|
-
"#{@message} at #{@location}"
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def initialize(delegate)
|
93
32
|
@delegate = delegate
|
33
|
+
|
94
34
|
# The delegate must respond to:
|
95
|
-
# .begin_parse(
|
35
|
+
# .begin_parse(self)
|
96
36
|
# .text(escaped_data)
|
97
37
|
# .cdata(unescaped_data)
|
98
38
|
# .attribute(name, value_or_true)
|
@@ -103,55 +43,52 @@ module Trenni
|
|
103
43
|
# .instruction(instruction_text)
|
104
44
|
end
|
105
45
|
|
106
|
-
def parse
|
107
|
-
|
108
|
-
@delegate.begin_parse(scanner)
|
46
|
+
def parse!
|
47
|
+
@delegate.begin_parse(self)
|
109
48
|
|
110
|
-
until
|
111
|
-
start_pos =
|
49
|
+
until eos?
|
50
|
+
start_pos = self.pos
|
112
51
|
|
113
|
-
scan_text
|
114
|
-
scan_tag
|
52
|
+
scan_text
|
53
|
+
scan_tag
|
115
54
|
|
116
|
-
|
117
|
-
raise ParseError.new("Scanner didn't move", scanner)
|
118
|
-
end
|
55
|
+
raise_if_stuck(start_pos)
|
119
56
|
end
|
120
57
|
end
|
121
58
|
|
122
59
|
protected
|
123
60
|
|
124
|
-
def scan_text
|
61
|
+
def scan_text
|
125
62
|
# Match any character data except the open tag character.
|
126
|
-
if
|
127
|
-
@delegate.text(
|
63
|
+
if self.scan(/[^<]+/m)
|
64
|
+
@delegate.text(self.matched)
|
128
65
|
end
|
129
66
|
end
|
130
67
|
|
131
|
-
def scan_tag
|
132
|
-
if
|
133
|
-
if
|
134
|
-
scan_tag_normal(
|
135
|
-
elsif
|
136
|
-
scan_tag_cdata
|
137
|
-
elsif
|
138
|
-
scan_tag_comment
|
139
|
-
elsif
|
140
|
-
scan_doctype
|
141
|
-
elsif
|
142
|
-
scan_tag_instruction
|
68
|
+
def scan_tag
|
69
|
+
if self.scan(/</)
|
70
|
+
if self.scan(/\//)
|
71
|
+
scan_tag_normal(CLOSED_TAG)
|
72
|
+
elsif self.scan(/!\[CDATA\[/)
|
73
|
+
scan_tag_cdata
|
74
|
+
elsif self.scan(/!--/)
|
75
|
+
scan_tag_comment
|
76
|
+
elsif self.scan(/!DOCTYPE/)
|
77
|
+
scan_doctype
|
78
|
+
elsif self.scan(/\?/)
|
79
|
+
scan_tag_instruction
|
143
80
|
else
|
144
|
-
scan_tag_normal
|
81
|
+
scan_tag_normal
|
145
82
|
end
|
146
83
|
end
|
147
84
|
end
|
148
85
|
|
149
|
-
def scan_attributes
|
86
|
+
def scan_attributes
|
150
87
|
# Parse an attribute in the form of key="value" or key.
|
151
|
-
while
|
152
|
-
name =
|
153
|
-
if
|
154
|
-
value =
|
88
|
+
while self.scan(/\s*([^\s=\/>]+)/um)
|
89
|
+
name = self[1].freeze
|
90
|
+
if self.scan(/=((['"])(.*?)\2)/um)
|
91
|
+
value = self[3].freeze
|
155
92
|
@delegate.attribute(name, value)
|
156
93
|
else
|
157
94
|
@delegate.attribute(name, true)
|
@@ -159,57 +96,57 @@ module Trenni
|
|
159
96
|
end
|
160
97
|
end
|
161
98
|
|
162
|
-
def scan_tag_normal(
|
163
|
-
if
|
164
|
-
@delegate.begin_tag(
|
99
|
+
def scan_tag_normal(begin_tag_type = OPENED_TAG)
|
100
|
+
if self.scan(/[^\s\/>]+/)
|
101
|
+
@delegate.begin_tag(self.matched.freeze, begin_tag_type)
|
165
102
|
|
166
|
-
|
167
|
-
self.scan_attributes
|
168
|
-
|
103
|
+
self.scan(/\s*/)
|
104
|
+
self.scan_attributes
|
105
|
+
self.scan(/\s*/)
|
169
106
|
|
170
|
-
if
|
107
|
+
if self.scan(/\/>/)
|
171
108
|
if begin_tag_type == CLOSED_TAG
|
172
|
-
|
109
|
+
parse_error!("Tag cannot be closed at both ends!")
|
173
110
|
else
|
174
111
|
@delegate.finish_tag(begin_tag_type, CLOSED_TAG)
|
175
112
|
end
|
176
|
-
elsif
|
113
|
+
elsif self.scan(/>/)
|
177
114
|
@delegate.finish_tag(begin_tag_type, OPENED_TAG)
|
178
115
|
else
|
179
|
-
|
116
|
+
parse_error!("Invalid characters in tag!")
|
180
117
|
end
|
181
118
|
else
|
182
|
-
|
119
|
+
parse_error!("Invalid tag!")
|
183
120
|
end
|
184
121
|
end
|
185
122
|
|
186
|
-
def scan_doctype
|
187
|
-
if
|
188
|
-
@delegate.doctype(
|
123
|
+
def scan_doctype
|
124
|
+
if self.scan_until(/(.*?)>/)
|
125
|
+
@delegate.doctype(self[1].strip.freeze)
|
189
126
|
else
|
190
|
-
|
127
|
+
parse_error!("DOCTYPE is not closed!")
|
191
128
|
end
|
192
129
|
end
|
193
130
|
|
194
|
-
def scan_tag_cdata
|
195
|
-
if
|
196
|
-
@delegate.cdata(
|
131
|
+
def scan_tag_cdata
|
132
|
+
if self.scan_until(/(.*?)\]\]>/m)
|
133
|
+
@delegate.cdata(self[1].freeze)
|
197
134
|
else
|
198
|
-
|
135
|
+
parse_error!("CDATA segment is not closed!")
|
199
136
|
end
|
200
137
|
end
|
201
138
|
|
202
|
-
def scan_tag_comment
|
203
|
-
if
|
204
|
-
@delegate.comment(
|
139
|
+
def scan_tag_comment
|
140
|
+
if self.scan_until(/(.*?)-->/m)
|
141
|
+
@delegate.comment(self[1].freeze)
|
205
142
|
else
|
206
|
-
|
143
|
+
parse_error!("Comment is not closed!")
|
207
144
|
end
|
208
145
|
end
|
209
146
|
|
210
|
-
def scan_tag_instruction
|
211
|
-
if
|
212
|
-
@delegate.instruction(
|
147
|
+
def scan_tag_instruction
|
148
|
+
if self.scan_until(/(.*)\?>/)
|
149
|
+
@delegate.instruction(self[1].freeze)
|
213
150
|
end
|
214
151
|
end
|
215
152
|
end
|
@@ -0,0 +1,138 @@
|
|
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
|
+
require_relative 'buffer'
|
22
|
+
|
23
|
+
require 'strscan'
|
24
|
+
|
25
|
+
module Trenni
|
26
|
+
class ParseError < StandardError
|
27
|
+
def initialize(message, scanner, positions = nil)
|
28
|
+
super(message)
|
29
|
+
|
30
|
+
@path = scanner.path
|
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
|
45
|
+
end
|
46
|
+
|
47
|
+
attr :locations
|
48
|
+
attr :location
|
49
|
+
|
50
|
+
attr :path
|
51
|
+
|
52
|
+
def to_s
|
53
|
+
"#{@path}#{@location}: #{super}\n#{location.line_text}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class Location
|
58
|
+
def initialize(input, offset)
|
59
|
+
raise ArgumentError.new("Offset #{index} is past end of input #{input.bytesize}") if offset > input.bytesize
|
60
|
+
|
61
|
+
@offset = offset
|
62
|
+
@line_index = 0
|
63
|
+
line_offset = next_line_offset = 0
|
64
|
+
|
65
|
+
input.each_line do |line|
|
66
|
+
line_offset = next_line_offset
|
67
|
+
next_line_offset += line.bytesize
|
68
|
+
|
69
|
+
# Is our input offset within this line?
|
70
|
+
if next_line_offset >= offset
|
71
|
+
@line_text = line.chomp
|
72
|
+
@line_range = line_offset...next_line_offset
|
73
|
+
break
|
74
|
+
else
|
75
|
+
@line_index += 1
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_i
|
81
|
+
@offset
|
82
|
+
end
|
83
|
+
|
84
|
+
def to_s
|
85
|
+
"[#{self.line_number}]"
|
86
|
+
end
|
87
|
+
|
88
|
+
# The line that contains the @offset (base 0 indexing).
|
89
|
+
attr :line_index
|
90
|
+
|
91
|
+
# The line index, but base-1.
|
92
|
+
def line_number
|
93
|
+
@line_index + 1
|
94
|
+
end
|
95
|
+
|
96
|
+
# The byte offset to the start of that line.
|
97
|
+
attr :line_range
|
98
|
+
|
99
|
+
# The number of bytes from the start of the line to the given offset in the input.
|
100
|
+
def line_offset
|
101
|
+
@offset - @line_range.min
|
102
|
+
end
|
103
|
+
|
104
|
+
attr :line_text
|
105
|
+
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
|
+
end
|
data/lib/trenni/template.rb
CHANGED
@@ -18,8 +18,7 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
|
22
|
-
require 'stringio'
|
21
|
+
require_relative 'scanner'
|
23
22
|
|
24
23
|
module Trenni
|
25
24
|
# The output variable that will be used in templates:
|
@@ -41,7 +40,7 @@ module Trenni
|
|
41
40
|
eval(OUT, binding)
|
42
41
|
end
|
43
42
|
|
44
|
-
class
|
43
|
+
class Assembler
|
45
44
|
def initialize
|
46
45
|
@parts = []
|
47
46
|
end
|
@@ -76,92 +75,109 @@ module Trenni
|
|
76
75
|
end
|
77
76
|
|
78
77
|
class Scanner < StringScanner
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
@
|
83
|
-
super(string)
|
78
|
+
def initialize(buffer, delegate)
|
79
|
+
super(buffer)
|
80
|
+
|
81
|
+
@delegate = delegate
|
84
82
|
end
|
85
|
-
|
86
|
-
def parse
|
83
|
+
|
84
|
+
def parse!
|
87
85
|
until eos?
|
88
|
-
|
86
|
+
start_pos = self.pos
|
89
87
|
|
90
88
|
scan_text
|
91
|
-
scan_expression
|
92
|
-
|
93
|
-
|
94
|
-
raise StandardError.new "Could not scan current input #{self.pos} #{eos?}!"
|
95
|
-
end
|
89
|
+
scan_expression or scan_interpolation
|
90
|
+
|
91
|
+
raise_if_stuck(start_pos)
|
96
92
|
end
|
97
93
|
end
|
98
94
|
|
95
|
+
# This is formulated specifically so that it matches up until the start of a code block.
|
96
|
+
TEXT = /([^<#]|<(?!\?r)|#(?!\{)){1,}/m
|
97
|
+
|
99
98
|
def scan_text
|
100
99
|
if scan(TEXT)
|
101
|
-
@
|
100
|
+
@delegate.text(self.matched)
|
102
101
|
end
|
103
102
|
end
|
104
103
|
|
105
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
|
+
|
106
123
|
if scan(/\#\{/)
|
107
124
|
level = 1
|
108
|
-
code =
|
125
|
+
code = String.new
|
109
126
|
|
110
|
-
until eos?
|
127
|
+
until eos?
|
128
|
+
current_pos = self.pos
|
129
|
+
|
130
|
+
# Scan anything other than something which causes nesting:
|
111
131
|
if scan(/[^"'\{\}]+/m)
|
112
132
|
code << matched
|
113
133
|
end
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
end
|
118
|
-
|
119
|
-
if scan(/'(\\'|[^'])*'/m)
|
134
|
+
|
135
|
+
# Scan a quoted string:
|
136
|
+
if scan(/'(\\'|[^'])*'/m) or scan(/"(\\"|[^"])*"/m)
|
120
137
|
code << matched
|
121
138
|
end
|
122
|
-
|
139
|
+
|
140
|
+
# Scan something which nests:
|
123
141
|
if scan(/\{/)
|
124
142
|
code << matched
|
125
143
|
level += 1
|
126
144
|
end
|
127
145
|
|
128
146
|
if scan(/\}/)
|
129
|
-
code << matched if level > 1
|
130
147
|
level -= 1
|
148
|
+
if level == 0
|
149
|
+
@delegate.interpolation(code)
|
150
|
+
return true
|
151
|
+
else
|
152
|
+
code << matched
|
153
|
+
end
|
131
154
|
end
|
155
|
+
|
156
|
+
break if stuck?(current_pos)
|
132
157
|
end
|
133
|
-
|
134
|
-
|
135
|
-
@callback.interpolation(code)
|
136
|
-
else
|
137
|
-
raise StandardError.new "Could not find end of expression #{self}!"
|
138
|
-
end
|
139
|
-
elsif scan(/<\?r/)
|
140
|
-
if scan_until(/(.*?)\?>/m)
|
141
|
-
@callback.expression(self[1])
|
142
|
-
else
|
143
|
-
raise StandardError.new "Could not find end of expression #{self}!"
|
144
|
-
end
|
158
|
+
|
159
|
+
parse_error!("Could not find end of interpolation!", [start_pos, self.pos])
|
145
160
|
end
|
161
|
+
|
162
|
+
return false
|
146
163
|
end
|
147
164
|
end
|
148
165
|
|
149
166
|
def self.load_file(path)
|
150
|
-
self.new(
|
167
|
+
self.new(FileBuffer.new(path))
|
151
168
|
end
|
152
169
|
|
153
|
-
def
|
154
|
-
|
155
|
-
end
|
156
|
-
|
157
|
-
def initialize(text, path = '<Trenni>')
|
158
|
-
@text = text
|
159
|
-
@path = path
|
170
|
+
def initialize(buffer)
|
171
|
+
@buffer = buffer
|
160
172
|
end
|
161
173
|
|
162
174
|
def to_string(scope = Object.new)
|
163
175
|
to_array(scope).join
|
164
176
|
end
|
177
|
+
|
178
|
+
def to_buffer(scope)
|
179
|
+
Buffer.new(to_array(scope).join, path: @buffer.path)
|
180
|
+
end
|
165
181
|
|
166
182
|
# Legacy functions:
|
167
183
|
alias evaluate to_string
|
@@ -170,7 +186,7 @@ module Trenni
|
|
170
186
|
def to_array(scope)
|
171
187
|
if Binding === scope
|
172
188
|
# Slow code path, evaluate the code string in the given binding (scope).
|
173
|
-
eval(code, scope, @path)
|
189
|
+
eval(code, scope, @buffer.path)
|
174
190
|
else
|
175
191
|
# Faster code path, use instance_eval on a compiled Proc.
|
176
192
|
scope.instance_eval(&to_proc)
|
@@ -178,7 +194,7 @@ module Trenni
|
|
178
194
|
end
|
179
195
|
|
180
196
|
def to_proc
|
181
|
-
@compiled_proc ||= eval("proc{
|
197
|
+
@compiled_proc ||= eval("proc{;#{code};}", binding, @buffer.path)
|
182
198
|
end
|
183
199
|
|
184
200
|
protected
|
@@ -188,12 +204,11 @@ module Trenni
|
|
188
204
|
end
|
189
205
|
|
190
206
|
def compile!
|
191
|
-
|
192
|
-
scanner = Scanner.new(buffer, @text)
|
207
|
+
assembler = Assembler.new
|
193
208
|
|
194
|
-
|
209
|
+
Scanner.new(@buffer, assembler).parse!
|
195
210
|
|
196
|
-
|
211
|
+
assembler.code
|
197
212
|
end
|
198
213
|
end
|
199
214
|
end
|
data/lib/trenni/version.rb
CHANGED
data/spec/trenni/parser_spec.rb
CHANGED
@@ -40,11 +40,16 @@ module Trenni::ParserSpec
|
|
40
40
|
end
|
41
41
|
|
42
42
|
describe Trenni::Parser do
|
43
|
-
|
44
|
-
|
43
|
+
def parse(input)
|
44
|
+
delegate = ParserDelegate.new
|
45
|
+
buffer = Trenni::Buffer.new(input)
|
46
|
+
Trenni::Parser.new(buffer, delegate).parse!
|
47
|
+
|
48
|
+
return delegate
|
49
|
+
end
|
45
50
|
|
46
51
|
it "should parse self-closing tags correctly" do
|
47
|
-
|
52
|
+
delegate = parse("<br/>")
|
48
53
|
|
49
54
|
expect(delegate.events).to be == [
|
50
55
|
[:begin_tag, "br", :opened],
|
@@ -53,7 +58,7 @@ module Trenni::ParserSpec
|
|
53
58
|
end
|
54
59
|
|
55
60
|
it "should parse doctype correctly" do
|
56
|
-
|
61
|
+
delegate = parse("<!DOCTYPE html>")
|
57
62
|
|
58
63
|
expect(delegate.events).to be == [
|
59
64
|
[:doctype, "html"]
|
@@ -61,7 +66,7 @@ module Trenni::ParserSpec
|
|
61
66
|
end
|
62
67
|
|
63
68
|
it "Should parse instruction correctly" do
|
64
|
-
|
69
|
+
delegate = parse("<?foo=bar?>")
|
65
70
|
|
66
71
|
expect(delegate.events).to be == [
|
67
72
|
[:instruction, "foo=bar"]
|
@@ -69,7 +74,7 @@ module Trenni::ParserSpec
|
|
69
74
|
end
|
70
75
|
|
71
76
|
it "should parse comment correctly" do
|
72
|
-
|
77
|
+
delegate = parse(%Q{<!--comment-->})
|
73
78
|
|
74
79
|
expect(delegate.events).to be == [
|
75
80
|
[:comment, "comment"]
|
@@ -77,7 +82,7 @@ module Trenni::ParserSpec
|
|
77
82
|
end
|
78
83
|
|
79
84
|
it "should parse markup correctly" do
|
80
|
-
|
85
|
+
delegate = parse(%Q{<foo bar="20" baz>Hello World</foo>})
|
81
86
|
|
82
87
|
expected_events = [
|
83
88
|
[:begin_tag, "foo", :opened],
|
@@ -93,7 +98,7 @@ module Trenni::ParserSpec
|
|
93
98
|
end
|
94
99
|
|
95
100
|
it "should parse CDATA correctly" do
|
96
|
-
|
101
|
+
delegate = parse(%Q{<test><![CDATA[Hello World]]></test>})
|
97
102
|
|
98
103
|
expected_events = [
|
99
104
|
[:begin_tag, "test", :opened],
|
@@ -107,20 +112,20 @@ module Trenni::ParserSpec
|
|
107
112
|
end
|
108
113
|
|
109
114
|
it "should generate errors on incorrect input" do
|
110
|
-
expect{
|
115
|
+
expect{parse(%Q{<foo})}.to raise_error Trenni::ParseError
|
111
116
|
|
112
|
-
expect{
|
117
|
+
expect{parse(%Q{<foo bar=>})}.to raise_error Trenni::ParseError
|
113
118
|
|
114
|
-
expect{
|
119
|
+
expect{parse(%Q{<foo bar="" baz>})}.to_not raise_error
|
115
120
|
end
|
116
121
|
|
117
122
|
it "should know about line numbers" do
|
118
123
|
data = %Q{Hello\nWorld\nFoo\nBar!}
|
119
124
|
|
120
|
-
location = Trenni::
|
125
|
+
location = Trenni::Location.new(data, 7)
|
121
126
|
|
122
127
|
expect(location.to_i).to be == 7
|
123
|
-
expect(location.to_s).to be == "
|
128
|
+
expect(location.to_s).to be == "[2]"
|
124
129
|
expect(location.line_text).to be == "World"
|
125
130
|
|
126
131
|
expect(location.line_number).to be == 2
|
@@ -130,9 +135,9 @@ module Trenni::ParserSpec
|
|
130
135
|
|
131
136
|
it "should know about line numbers when input contains multi-byte characters" do
|
132
137
|
data = %Q{<p>\nこんにちは\nWorld\n<p}
|
133
|
-
error =
|
138
|
+
error = parse(data) rescue $!
|
134
139
|
|
135
|
-
expect(error).to be_kind_of Trenni::
|
140
|
+
expect(error).to be_kind_of Trenni::ParseError
|
136
141
|
expect(error.location.line_number).to be == 4
|
137
142
|
end
|
138
143
|
end
|
@@ -46,7 +46,8 @@ module Trenni::TemplateSpec
|
|
46
46
|
end
|
47
47
|
|
48
48
|
it "should process list of items" do
|
49
|
-
|
49
|
+
buffer = Trenni::Buffer.new('<?r items.each do |item| ?>#{item}<?r end ?>')
|
50
|
+
template = Trenni::Template.new(buffer)
|
50
51
|
|
51
52
|
items = 1..4
|
52
53
|
|
@@ -83,5 +84,14 @@ module Trenni::TemplateSpec
|
|
83
84
|
"This\\nisn't one line.\n" +
|
84
85
|
"\\tIndentation is the best."
|
85
86
|
end
|
87
|
+
|
88
|
+
it "should fail to parse" do
|
89
|
+
buffer = Trenni::Buffer.new('<img src="#{poi_product.photo.thumbnail_url" />')
|
90
|
+
broken_template = Trenni::Template.new(buffer)
|
91
|
+
|
92
|
+
expect{broken_template.to_proc}.to raise_error(Trenni::ParseError) do |error|
|
93
|
+
expect(error.to_s).to include("<string>[1]: Could not find end of interpolation!")
|
94
|
+
end
|
95
|
+
end
|
86
96
|
end
|
87
97
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: trenni
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-03-
|
11
|
+
date: 2016-03-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -71,8 +71,10 @@ files:
|
|
71
71
|
- Rakefile
|
72
72
|
- benchmark/io_vs_string.rb
|
73
73
|
- lib/trenni.rb
|
74
|
+
- lib/trenni/buffer.rb
|
74
75
|
- lib/trenni/builder.rb
|
75
76
|
- lib/trenni/parser.rb
|
77
|
+
- lib/trenni/scanner.rb
|
76
78
|
- lib/trenni/strings.rb
|
77
79
|
- lib/trenni/template.rb
|
78
80
|
- lib/trenni/version.rb
|