haparanda 0.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/COPYING.LIB +504 -0
- data/README.md +123 -0
- data/lib/haparanda/compiler.rb +29 -0
- data/lib/haparanda/content_combiner.rb +48 -0
- data/lib/haparanda/handlebars_compiler.rb +18 -0
- data/lib/haparanda/handlebars_lexer.rb +297 -0
- data/lib/haparanda/handlebars_lexer.rex +135 -0
- data/lib/haparanda/handlebars_parser.output +1691 -0
- data/lib/haparanda/handlebars_parser.rb +880 -0
- data/lib/haparanda/handlebars_parser.y +358 -0
- data/lib/haparanda/handlebars_processor.rb +347 -0
- data/lib/haparanda/template.rb +18 -0
- data/lib/haparanda/version.rb +6 -0
- data/lib/haparanda/whitespace_handler.rb +168 -0
- data/lib/haparanda.rb +15 -0
- metadata +94 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sexp_processor"
|
4
|
+
|
5
|
+
module Haparanda
|
6
|
+
# Process the handlebars AST just to combine subsequent :content items
|
7
|
+
class ContentCombiner < SexpProcessor
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
|
11
|
+
self.require_empty = false
|
12
|
+
end
|
13
|
+
|
14
|
+
def process_statements(expr)
|
15
|
+
statements = expr.sexp_body
|
16
|
+
|
17
|
+
statements = combine_contents(statements)
|
18
|
+
statements = statements.map { process(_1) }
|
19
|
+
|
20
|
+
s(:statements, *statements)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def combine_contents(statements)
|
26
|
+
return statements if statements.length < 2
|
27
|
+
|
28
|
+
prev = nil
|
29
|
+
result = []
|
30
|
+
|
31
|
+
statements.each do |item|
|
32
|
+
if prev
|
33
|
+
if item.sexp_type == :content
|
34
|
+
prev[1] += item[1]
|
35
|
+
else
|
36
|
+
result << item
|
37
|
+
prev = nil
|
38
|
+
end
|
39
|
+
else
|
40
|
+
result << item
|
41
|
+
prev = item if item.sexp_type == :content
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
result
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "content_combiner"
|
4
|
+
require_relative "whitespace_handler"
|
5
|
+
|
6
|
+
module Haparanda
|
7
|
+
# Process the handlebars AST just to combine subsequent :content items
|
8
|
+
class HandlebarsCompiler
|
9
|
+
def initialize(ignore_standalone: false, **)
|
10
|
+
@ignore_standalone = ignore_standalone
|
11
|
+
end
|
12
|
+
|
13
|
+
def process(expr)
|
14
|
+
expr = ContentCombiner.new.process(expr)
|
15
|
+
WhitespaceHandler.new(ignore_standalone: @ignore_standalone).process(expr)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,297 @@
|
|
1
|
+
#--
|
2
|
+
# DO NOT MODIFY!!!!
|
3
|
+
# This file is automatically generated by rex 1.0.8
|
4
|
+
# from lexical definition file "lib/haparanda/handlebars_lexer.rex".
|
5
|
+
#++
|
6
|
+
|
7
|
+
# Based on src/handlebars.l in handlebars-parser.
|
8
|
+
#
|
9
|
+
# src/handlebars.l in handlebars-parser is covered by the ICS license. See README.md
|
10
|
+
# for details.
|
11
|
+
|
12
|
+
class Haparanda::HandlebarsLexer
|
13
|
+
require 'strscan'
|
14
|
+
|
15
|
+
class ScanError < StandardError ; end
|
16
|
+
|
17
|
+
attr_reader :lineno
|
18
|
+
attr_reader :filename
|
19
|
+
attr_accessor :state
|
20
|
+
|
21
|
+
def scan_setup(str)
|
22
|
+
@ss = StringScanner.new(str)
|
23
|
+
@lineno = 1
|
24
|
+
@state = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def action
|
28
|
+
yield
|
29
|
+
end
|
30
|
+
|
31
|
+
def scan_str(str)
|
32
|
+
scan_setup(str)
|
33
|
+
do_parse
|
34
|
+
end
|
35
|
+
alias :scan :scan_str
|
36
|
+
|
37
|
+
def load_file( filename )
|
38
|
+
@filename = filename
|
39
|
+
File.open(filename, "r") do |f|
|
40
|
+
scan_setup(f.read)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def scan_file( filename )
|
45
|
+
load_file(filename)
|
46
|
+
do_parse
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def next_token
|
51
|
+
return if @ss.eos?
|
52
|
+
|
53
|
+
# skips empty actions
|
54
|
+
until token = _next_token or @ss.eos?; end
|
55
|
+
token
|
56
|
+
end
|
57
|
+
|
58
|
+
def _next_token
|
59
|
+
text = @ss.peek(1)
|
60
|
+
@lineno += 1 if text == "\n"
|
61
|
+
token = case @state
|
62
|
+
when nil
|
63
|
+
case
|
64
|
+
when (text = @ss.scan(/\n/))
|
65
|
+
action { [:CONTENT, text] }
|
66
|
+
|
67
|
+
when (text = @ss.scan(/[^\x00\n]*?(?={{)/))
|
68
|
+
action {
|
69
|
+
if(text.slice(-2, 2) === "\\\\")
|
70
|
+
text = text[0..-2]
|
71
|
+
@state = :MU
|
72
|
+
elsif(text.slice(-1) === "\\")
|
73
|
+
text = text[0..-2]
|
74
|
+
@state = :EMU
|
75
|
+
else
|
76
|
+
@state = :MU
|
77
|
+
end
|
78
|
+
[:CONTENT, text] unless(text.empty?)
|
79
|
+
}
|
80
|
+
|
81
|
+
|
82
|
+
when (text = @ss.scan(/[^\x00\n]+/))
|
83
|
+
action { [:CONTENT, text] }
|
84
|
+
|
85
|
+
when (text = @ss.scan(/<INITIAL,mu><<EOF>>/))
|
86
|
+
action { [:EOF, text] }
|
87
|
+
|
88
|
+
|
89
|
+
else
|
90
|
+
text = @ss.string[@ss.pos .. -1]
|
91
|
+
raise ScanError, "can not match: '" + text + "'"
|
92
|
+
end # if
|
93
|
+
|
94
|
+
when :EMU
|
95
|
+
case
|
96
|
+
when (text = @ss.scan(/{{/))
|
97
|
+
action { @state = nil; [:CONTENT, text] }
|
98
|
+
|
99
|
+
|
100
|
+
else
|
101
|
+
text = @ss.string[@ss.pos .. -1]
|
102
|
+
raise ScanError, "can not match: '" + text + "'"
|
103
|
+
end # if
|
104
|
+
|
105
|
+
when :RAW
|
106
|
+
case
|
107
|
+
when (text = @ss.scan(/{{{{\/[^\s!"#%-,\.\/;->@\[-\^`\{-~]+}}}}/))
|
108
|
+
action {
|
109
|
+
ending_delimiter = text[5...-4]
|
110
|
+
if ending_delimiter == @raw_delimiter
|
111
|
+
@state = nil
|
112
|
+
[:END_RAW_BLOCK, ending_delimiter]
|
113
|
+
else
|
114
|
+
[:CONTENT, text]
|
115
|
+
end
|
116
|
+
}
|
117
|
+
|
118
|
+
|
119
|
+
when (text = @ss.scan(/[^\x00]+?(?={{{{)/))
|
120
|
+
action { [:CONTENT, text] }
|
121
|
+
|
122
|
+
|
123
|
+
else
|
124
|
+
text = @ss.string[@ss.pos .. -1]
|
125
|
+
raise ScanError, "can not match: '" + text + "'"
|
126
|
+
end # if
|
127
|
+
|
128
|
+
when :COM
|
129
|
+
case
|
130
|
+
when (text = @ss.scan(/[\s\S]*?\-\-~?}}/))
|
131
|
+
action { @state = nil; [:COMMENT, text] }
|
132
|
+
|
133
|
+
|
134
|
+
else
|
135
|
+
text = @ss.string[@ss.pos .. -1]
|
136
|
+
raise ScanError, "can not match: '" + text + "'"
|
137
|
+
end # if
|
138
|
+
|
139
|
+
when :MU
|
140
|
+
case
|
141
|
+
when (text = @ss.scan(/\(/))
|
142
|
+
action { [:OPEN_SEXPR, text] }
|
143
|
+
|
144
|
+
when (text = @ss.scan(/\)/))
|
145
|
+
action { [:CLOSE_SEXPR, text] }
|
146
|
+
|
147
|
+
when (text = @ss.scan(/{{{{/))
|
148
|
+
action {
|
149
|
+
@expect_raw_block_id = true
|
150
|
+
[:OPEN_RAW_BLOCK, text]
|
151
|
+
}
|
152
|
+
|
153
|
+
|
154
|
+
when (text = @ss.scan(/}}}}/))
|
155
|
+
action {
|
156
|
+
@state = :RAW
|
157
|
+
[:CLOSE_RAW_BLOCK, text]
|
158
|
+
}
|
159
|
+
|
160
|
+
|
161
|
+
when (text = @ss.scan(/{{~?>/))
|
162
|
+
action { [:OPEN_PARTIAL, text] }
|
163
|
+
|
164
|
+
when (text = @ss.scan(/{{~?#>/))
|
165
|
+
action { [:OPEN_PARTIAL_BLOCK, text] }
|
166
|
+
|
167
|
+
when (text = @ss.scan(/{{~?#\*?/))
|
168
|
+
action { [:OPEN_BLOCK, text] }
|
169
|
+
|
170
|
+
when (text = @ss.scan(/{{~?\//))
|
171
|
+
action { [:OPEN_ENDBLOCK, text] }
|
172
|
+
|
173
|
+
when (text = @ss.scan(/{{~?\^\s*~?}}/))
|
174
|
+
action { @state = nil; [:INVERSE, text] }
|
175
|
+
|
176
|
+
when (text = @ss.scan(/{{~?\s*else\s*~?}}/))
|
177
|
+
action { @state = nil; [:INVERSE, text] }
|
178
|
+
|
179
|
+
when (text = @ss.scan(/{{~?\^/))
|
180
|
+
action { [:OPEN_INVERSE, text] }
|
181
|
+
|
182
|
+
when (text = @ss.scan(/{{~?\s*else/))
|
183
|
+
action { [:OPEN_INVERSE_CHAIN, text] }
|
184
|
+
|
185
|
+
when (text = @ss.scan(/{{~?{/))
|
186
|
+
action { [:OPEN_UNESCAPED, text] }
|
187
|
+
|
188
|
+
when (text = @ss.scan(/{{~?&/))
|
189
|
+
action { [:OPEN, text] }
|
190
|
+
|
191
|
+
when (text = @ss.scan(/{{~?!--/))
|
192
|
+
action { @state = :COM; [:COMMENT, text] }
|
193
|
+
|
194
|
+
when (text = @ss.scan(/{{~?![\s\S]*?}}/))
|
195
|
+
action { @state = nil; [:COMMENT, text] }
|
196
|
+
|
197
|
+
when (text = @ss.scan(/{{~?\*?/))
|
198
|
+
action { [:OPEN, text] }
|
199
|
+
|
200
|
+
when (text = @ss.scan(/=/))
|
201
|
+
action { [:EQUALS, text] }
|
202
|
+
|
203
|
+
when (text = @ss.scan(/\.\./))
|
204
|
+
action { [:ID, text] }
|
205
|
+
|
206
|
+
when (text = @ss.scan(/\.(?=[=~}\s\/.)|])/))
|
207
|
+
action { [:ID, text] }
|
208
|
+
|
209
|
+
when (text = @ss.scan(/[\/.]/))
|
210
|
+
action { [:SEP, text] }
|
211
|
+
|
212
|
+
when (text = @ss.scan(/\s+/))
|
213
|
+
;
|
214
|
+
|
215
|
+
when (text = @ss.scan(/}~?}}/))
|
216
|
+
action { @state = nil; [:CLOSE_UNESCAPED, text] }
|
217
|
+
|
218
|
+
when (text = @ss.scan(/~?}}/))
|
219
|
+
action { @state = nil; [:CLOSE, text] }
|
220
|
+
|
221
|
+
when (text = @ss.scan(/"(\\"|[^"])*"/))
|
222
|
+
action { text = handle_stringescape(text, '"'); [:STRING, text] }
|
223
|
+
|
224
|
+
when (text = @ss.scan(/'(\\'|[^'])*'/))
|
225
|
+
action { text = handle_stringescape(text, "'"); [:STRING, text] }
|
226
|
+
|
227
|
+
when (text = @ss.scan(/@/))
|
228
|
+
action { [:DATA, text] }
|
229
|
+
|
230
|
+
when (text = @ss.scan(/true(?=[~}\s)])/))
|
231
|
+
action { [:BOOLEAN, text] }
|
232
|
+
|
233
|
+
when (text = @ss.scan(/false(?=[~}\s)])/))
|
234
|
+
action { [:BOOLEAN, text] }
|
235
|
+
|
236
|
+
when (text = @ss.scan(/undefined(?=[~}\s)])/))
|
237
|
+
action { [:UNDEFINED, text] }
|
238
|
+
|
239
|
+
when (text = @ss.scan(/null(?=[~}\s)])/))
|
240
|
+
action { [:NULL, text] }
|
241
|
+
|
242
|
+
when (text = @ss.scan(/\-?[0-9]+(?:\.[0-9]+)?(?=[~}\s)])/))
|
243
|
+
action { [:NUMBER, text] }
|
244
|
+
|
245
|
+
when (text = @ss.scan(/as\s+\|/))
|
246
|
+
action { [:OPEN_BLOCK_PARAMS, text] }
|
247
|
+
|
248
|
+
when (text = @ss.scan(/\|/))
|
249
|
+
action { [:CLOSE_BLOCK_PARAMS, text] }
|
250
|
+
|
251
|
+
when (text = @ss.scan(/[^\s!"\#%-,\.\/;->@\[-\^`\{-~]+(?=[=~}\s\/.)|])=/))
|
252
|
+
action { [:KEY_ASSIGN, text[0..-2]] }
|
253
|
+
|
254
|
+
when (text = @ss.scan(/[^\s!"\#%-,\.\/;->@\[-\^`\{-~]+(?=[=~}\s\/.)|])/))
|
255
|
+
action {
|
256
|
+
if @expect_raw_block_id
|
257
|
+
@raw_delimiter = text
|
258
|
+
@expect_raw_block_id = false
|
259
|
+
end
|
260
|
+
[:ID, text]
|
261
|
+
}
|
262
|
+
|
263
|
+
|
264
|
+
when (text = @ss.scan(/\[(\\\]|[^\]])*\]/))
|
265
|
+
action { text = text.gsub(/\\([\\\]])/, '\1'); [:ID, text] }
|
266
|
+
|
267
|
+
when (text = @ss.scan(/./))
|
268
|
+
action { [:INVALID, text] }
|
269
|
+
|
270
|
+
|
271
|
+
else
|
272
|
+
text = @ss.string[@ss.pos .. -1]
|
273
|
+
raise ScanError, "can not match: '" + text + "'"
|
274
|
+
end # if
|
275
|
+
|
276
|
+
else
|
277
|
+
raise ScanError, "undefined state: '" + state.to_s + "'"
|
278
|
+
end # case state
|
279
|
+
token
|
280
|
+
end # def _next_token
|
281
|
+
|
282
|
+
def do_parse
|
283
|
+
result = []
|
284
|
+
while token = next_token
|
285
|
+
result << token
|
286
|
+
end
|
287
|
+
result
|
288
|
+
end
|
289
|
+
def handle_stringescape(str, delimiter)
|
290
|
+
case delimiter
|
291
|
+
when '"', "'"
|
292
|
+
str[1..-2].gsub(%r{\\(.)}, "\\1")
|
293
|
+
else
|
294
|
+
raise NotImplementedError
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end # class
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# Based on src/handlebars.l in handlebars-parser.
|
2
|
+
#
|
3
|
+
# src/handlebars.l in handlebars-parser is covered by the ICS license. See README.md
|
4
|
+
# for details.
|
5
|
+
|
6
|
+
class Haparanda::HandlebarsLexer
|
7
|
+
%x mu emu com raw
|
8
|
+
|
9
|
+
inner
|
10
|
+
|
11
|
+
def do_parse
|
12
|
+
result = []
|
13
|
+
while token = next_token
|
14
|
+
result << token
|
15
|
+
end
|
16
|
+
result
|
17
|
+
end
|
18
|
+
|
19
|
+
def handle_stringescape(str, delimiter)
|
20
|
+
case delimiter
|
21
|
+
when '"', "'"
|
22
|
+
str[1..-2].gsub(%r{\\(.)}, "\\1")
|
23
|
+
else
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
macro
|
29
|
+
LEFT_STRIP ~
|
30
|
+
RIGHT_STRIP ~
|
31
|
+
|
32
|
+
LOOKAHEAD [=~}\s\/.)|]
|
33
|
+
LIT_LOOKAHEAD [~}\s)]
|
34
|
+
|
35
|
+
# ID is the inverse of control characters.
|
36
|
+
# Control characters ranges:
|
37
|
+
# [\s] Whitespace
|
38
|
+
# [!"#%-,\./] !, ", #, %, &, ', (, ), *, +, ,, ., /, Exceptions in range: $, -
|
39
|
+
# [;->@] ;, <, =, >, @, Exceptions in range: :, ?
|
40
|
+
# [\[-\^`] [, \, ], ^, `, Exceptions in range: _
|
41
|
+
# [\{-~] {, |, }, ~
|
42
|
+
ID [^\s!"\#%-,\.\/;->@\[-\^`\{-~]+(?={LOOKAHEAD})
|
43
|
+
|
44
|
+
rule
|
45
|
+
|
46
|
+
\n { [:CONTENT, text] }
|
47
|
+
[^\x00\n]*?(?={{) {
|
48
|
+
if(text.slice(-2, 2) === "\\\\")
|
49
|
+
text = text[0..-2]
|
50
|
+
@state = :MU
|
51
|
+
elsif(text.slice(-1) === "\\")
|
52
|
+
text = text[0..-2]
|
53
|
+
@state = :EMU
|
54
|
+
else
|
55
|
+
@state = :MU
|
56
|
+
end
|
57
|
+
[:CONTENT, text] unless(text.empty?)
|
58
|
+
}
|
59
|
+
|
60
|
+
[^\x00\n]+ { [:CONTENT, text] }
|
61
|
+
|
62
|
+
# consume escaped mustache
|
63
|
+
:EMU {{ { @state = nil; [:CONTENT, text] }
|
64
|
+
|
65
|
+
# End of raw block if delimiter text matches opening text
|
66
|
+
:RAW {{{{\/[^\s!"#%-,\.\/;->@\[-\^`\{-~]+}}}} {
|
67
|
+
ending_delimiter = text[5...-4]
|
68
|
+
if ending_delimiter == @raw_delimiter
|
69
|
+
@state = nil
|
70
|
+
[:END_RAW_BLOCK, ending_delimiter]
|
71
|
+
else
|
72
|
+
[:CONTENT, text]
|
73
|
+
end
|
74
|
+
}
|
75
|
+
:RAW [^\x00]+?(?={{{{) { [:CONTENT, text] }
|
76
|
+
|
77
|
+
:COM [\s\S]*?\-\-{RIGHT_STRIP}?}} { @state = nil; [:COMMENT, text] }
|
78
|
+
|
79
|
+
:MU \( { [:OPEN_SEXPR, text] }
|
80
|
+
:MU \) { [:CLOSE_SEXPR, text] }
|
81
|
+
|
82
|
+
:MU {{{{ {
|
83
|
+
@expect_raw_block_id = true
|
84
|
+
[:OPEN_RAW_BLOCK, text]
|
85
|
+
}
|
86
|
+
:MU }}}} {
|
87
|
+
@state = :RAW
|
88
|
+
[:CLOSE_RAW_BLOCK, text]
|
89
|
+
}
|
90
|
+
:MU {{{LEFT_STRIP}?> { [:OPEN_PARTIAL, text] }
|
91
|
+
:MU {{{LEFT_STRIP}?#> { [:OPEN_PARTIAL_BLOCK, text] }
|
92
|
+
:MU {{{LEFT_STRIP}?#\*? { [:OPEN_BLOCK, text] }
|
93
|
+
:MU {{{LEFT_STRIP}?\/ { [:OPEN_ENDBLOCK, text] }
|
94
|
+
:MU {{{LEFT_STRIP}?\^\s*{RIGHT_STRIP}?}} { @state = nil; [:INVERSE, text] }
|
95
|
+
:MU {{{LEFT_STRIP}?\s*else\s*{RIGHT_STRIP}?}} { @state = nil; [:INVERSE, text] }
|
96
|
+
:MU {{{LEFT_STRIP}?\^ { [:OPEN_INVERSE, text] }
|
97
|
+
:MU {{{LEFT_STRIP}?\s*else { [:OPEN_INVERSE_CHAIN, text] }
|
98
|
+
:MU {{{LEFT_STRIP}?{ { [:OPEN_UNESCAPED, text] }
|
99
|
+
:MU {{{LEFT_STRIP}?& { [:OPEN, text] }
|
100
|
+
:MU {{{LEFT_STRIP}?!-- { @state = :COM; [:COMMENT, text] }
|
101
|
+
:MU {{{LEFT_STRIP}?![\s\S]*?}} { @state = nil; [:COMMENT, text] }
|
102
|
+
:MU {{{LEFT_STRIP}?\*? { [:OPEN, text] }
|
103
|
+
|
104
|
+
:MU = { [:EQUALS, text] }
|
105
|
+
:MU \.\. { [:ID, text] }
|
106
|
+
:MU \.(?={LOOKAHEAD}) { [:ID, text] }
|
107
|
+
:MU [\/.] { [:SEP, text] }
|
108
|
+
:MU \s+ // ignore whitespace
|
109
|
+
:MU }{RIGHT_STRIP}?}} { @state = nil; [:CLOSE_UNESCAPED, text] }
|
110
|
+
:MU {RIGHT_STRIP}?}} { @state = nil; [:CLOSE, text] }
|
111
|
+
:MU "(\\"|[^"])*" { text = handle_stringescape(text, '"'); [:STRING, text] }
|
112
|
+
:MU '(\\'|[^'])*' { text = handle_stringescape(text, "'"); [:STRING, text] }
|
113
|
+
:MU @ { [:DATA, text] }
|
114
|
+
:MU true(?={LIT_LOOKAHEAD}) { [:BOOLEAN, text] }
|
115
|
+
:MU false(?={LIT_LOOKAHEAD}) { [:BOOLEAN, text] }
|
116
|
+
:MU undefined(?={LIT_LOOKAHEAD}) { [:UNDEFINED, text] }
|
117
|
+
:MU null(?={LIT_LOOKAHEAD}) { [:NULL, text] }
|
118
|
+
:MU \-?[0-9]+(?:\.[0-9]+)?(?={LIT_LOOKAHEAD}) { [:NUMBER, text] }
|
119
|
+
:MU as\s+\| { [:OPEN_BLOCK_PARAMS, text] }
|
120
|
+
:MU \| { [:CLOSE_BLOCK_PARAMS, text] }
|
121
|
+
|
122
|
+
:MU {ID}= { [:KEY_ASSIGN, text[0..-2]] }
|
123
|
+
:MU {ID} {
|
124
|
+
if @expect_raw_block_id
|
125
|
+
@raw_delimiter = text
|
126
|
+
@expect_raw_block_id = false
|
127
|
+
end
|
128
|
+
[:ID, text]
|
129
|
+
}
|
130
|
+
|
131
|
+
:MU \[(\\\]|[^\]])*\] { text = text.gsub(/\\([\\\]])/, '\1'); [:ID, text] }
|
132
|
+
:MU . { [:INVALID, text] }
|
133
|
+
|
134
|
+
<INITIAL,mu><<EOF>> { [:EOF, text] }
|
135
|
+
end
|