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