bade 0.1.3
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/Bade.gemspec +26 -0
- data/Gemfile +4 -0
- data/README.md +49 -0
- data/lib/bade/document.rb +33 -0
- data/lib/bade/generator/html_generator.rb +80 -0
- data/lib/bade/generator/ruby_generator.rb +336 -0
- data/lib/bade/generator.rb +16 -0
- data/lib/bade/node/doctype_node.rb +21 -0
- data/lib/bade/node/key_value_node.rb +10 -0
- data/lib/bade/node/mixin_node.rb +69 -0
- data/lib/bade/node/tag_node.rb +29 -0
- data/lib/bade/node.rb +116 -0
- data/lib/bade/parser.rb +652 -0
- data/lib/bade/renderer.rb +148 -0
- data/lib/bade/ruby_extensions/object.rb +11 -0
- data/lib/bade/ruby_extensions/string.rb +74 -0
- data/lib/bade/runtime/block.rb +40 -0
- data/lib/bade/runtime/render_binding.rb +53 -0
- data/lib/bade/runtime.rb +7 -0
- data/lib/bade/version.rb +4 -0
- data/lib/bade.rb +8 -0
- metadata +108 -0
data/lib/bade/parser.rb
ADDED
@@ -0,0 +1,652 @@
|
|
1
|
+
|
2
|
+
require_relative 'node'
|
3
|
+
require_relative 'document'
|
4
|
+
require_relative 'ruby_extensions/string'
|
5
|
+
|
6
|
+
module Bade
|
7
|
+
class Parser
|
8
|
+
class SyntaxError < StandardError
|
9
|
+
attr_reader :error, :file, :line, :lineno, :column
|
10
|
+
|
11
|
+
def initialize(error, file, line, lineno, column)
|
12
|
+
@error = error
|
13
|
+
@file = file || '(__TEMPLATE__)'
|
14
|
+
@line = line.to_s
|
15
|
+
@lineno = lineno
|
16
|
+
@column = column
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
line = @line.lstrip
|
21
|
+
column = @column + line.size - @line.size
|
22
|
+
%{#{error}
|
23
|
+
#{file}, Line #{lineno}, Column #{@column}
|
24
|
+
#{line}
|
25
|
+
#{' ' * column}^
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class ParserInternalError < StandardError; end
|
31
|
+
|
32
|
+
# @return [Array<String>]
|
33
|
+
#
|
34
|
+
attr_reader :dependency_paths
|
35
|
+
|
36
|
+
# Initialize
|
37
|
+
#
|
38
|
+
# Available options:
|
39
|
+
# :tabsize [Int] default 4
|
40
|
+
# :file [String] default nil
|
41
|
+
#
|
42
|
+
def initialize(options = {})
|
43
|
+
@line = ''
|
44
|
+
|
45
|
+
tabsize = options.delete(:tabsize) { 4 }
|
46
|
+
@tabsize = tabsize
|
47
|
+
|
48
|
+
@tab_re = /\G((?: {#{tabsize}})*) {0,#{tabsize-1}}\t/
|
49
|
+
@tab = '\1' + ' ' * tabsize
|
50
|
+
|
51
|
+
@options = options
|
52
|
+
|
53
|
+
reset
|
54
|
+
end
|
55
|
+
|
56
|
+
# @param [String, Array<String>] str
|
57
|
+
# @return [Bade::Document] root node
|
58
|
+
#
|
59
|
+
def parse(str)
|
60
|
+
@document = Document.new(file_path: @options[:file_path])
|
61
|
+
@root = @document.root
|
62
|
+
|
63
|
+
@dependency_paths = []
|
64
|
+
|
65
|
+
if str.kind_of? Array
|
66
|
+
reset(str, [[@root]])
|
67
|
+
else
|
68
|
+
reset(str.split(/\r?\n/), [[@root]])
|
69
|
+
end
|
70
|
+
|
71
|
+
parse_line while next_line
|
72
|
+
|
73
|
+
reset
|
74
|
+
|
75
|
+
@document
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
|
80
|
+
WORD_RE = ''.respond_to?(:encoding) ? '\p{Word}' : '\w'
|
81
|
+
NAME_RE_STRING = "(#{WORD_RE}(?:#{WORD_RE}|:|-|_)*)"
|
82
|
+
|
83
|
+
ATTR_NAME_RE_STRING = "\\A\\s*#{NAME_RE_STRING}"
|
84
|
+
CODE_ATTR_RE = /#{ATTR_NAME_RE_STRING}\s*(&?):\s*/
|
85
|
+
|
86
|
+
TAG_RE = /\A#{NAME_RE_STRING}/
|
87
|
+
CLASS_TAG_RE = /\A\.#{NAME_RE_STRING}/
|
88
|
+
ID_TAG_RE = /\A##{NAME_RE_STRING}/
|
89
|
+
|
90
|
+
def reset(lines = nil, stacks = nil)
|
91
|
+
# Since you can indent however you like in Slim, we need to keep a list
|
92
|
+
# of how deeply indented you are. For instance, in a template like this:
|
93
|
+
#
|
94
|
+
# doctype # 0 spaces
|
95
|
+
# html # 0 spaces
|
96
|
+
# head # 1 space
|
97
|
+
# title # 4 spaces
|
98
|
+
#
|
99
|
+
# indents will then contain [0, 1, 4] (when it's processing the last line.)
|
100
|
+
#
|
101
|
+
# We uses this information to figure out how many steps we must "jump"
|
102
|
+
# out when we see an de-indented line.
|
103
|
+
@indents = [0]
|
104
|
+
|
105
|
+
# Whenever we want to output something, we'll *always* output it to the
|
106
|
+
# last stack in this array. So when there's a line that expects
|
107
|
+
# indentation, we simply push a new stack onto this array. When it
|
108
|
+
# processes the next line, the content will then be outputted into that
|
109
|
+
# stack.
|
110
|
+
@stacks = stacks
|
111
|
+
|
112
|
+
@lineno = 0
|
113
|
+
@lines = lines
|
114
|
+
|
115
|
+
# @return [String]
|
116
|
+
@line = @orig_line = nil
|
117
|
+
end
|
118
|
+
|
119
|
+
def next_line
|
120
|
+
if @lines.empty?
|
121
|
+
@orig_line = @line = nil
|
122
|
+
else
|
123
|
+
@orig_line = @lines.shift
|
124
|
+
@lineno += 1
|
125
|
+
@line = @orig_line.dup
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Calculate indent for line
|
130
|
+
#
|
131
|
+
# @param [String] line
|
132
|
+
#
|
133
|
+
# @return [Int] indent size
|
134
|
+
#
|
135
|
+
def get_indent(line)
|
136
|
+
line.get_indent(@tabsize)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Append element to stacks and result tree
|
140
|
+
#
|
141
|
+
# @param [Symbol] type
|
142
|
+
#
|
143
|
+
def append_node(type, indent: @indents.length, add: false, data: nil)
|
144
|
+
while indent >= @stacks.length
|
145
|
+
@stacks << @stacks.last.dup
|
146
|
+
end
|
147
|
+
|
148
|
+
parent = @stacks[indent].last
|
149
|
+
node = Node.create(type, parent)
|
150
|
+
node.lineno = @lineno
|
151
|
+
|
152
|
+
node.data = data
|
153
|
+
|
154
|
+
if add
|
155
|
+
@stacks[indent] << node
|
156
|
+
end
|
157
|
+
|
158
|
+
node
|
159
|
+
end
|
160
|
+
|
161
|
+
def parse_line
|
162
|
+
line = @line
|
163
|
+
|
164
|
+
if line =~ /\A\s*\Z/
|
165
|
+
append_node :newline
|
166
|
+
return
|
167
|
+
end
|
168
|
+
|
169
|
+
indent = get_indent(line)
|
170
|
+
|
171
|
+
# left strip
|
172
|
+
line.remove_indent!(indent, @tabsize)
|
173
|
+
@line = line
|
174
|
+
|
175
|
+
# If there's more stacks than indents, it means that the previous
|
176
|
+
# line is expecting this line to be indented.
|
177
|
+
expecting_indentation = @stacks.length > @indents.length
|
178
|
+
|
179
|
+
if indent > @indents.last
|
180
|
+
@indents << indent
|
181
|
+
else
|
182
|
+
# This line was *not* indented more than the line before,
|
183
|
+
# so we'll just forget about the stack that the previous line pushed.
|
184
|
+
@stacks.pop if expecting_indentation
|
185
|
+
|
186
|
+
# This line was deindented.
|
187
|
+
# Now we're have to go through the all the indents and figure out
|
188
|
+
# how many levels we've deindented.
|
189
|
+
while indent < @indents.last
|
190
|
+
@indents.pop
|
191
|
+
@stacks.pop
|
192
|
+
end
|
193
|
+
|
194
|
+
# Remove old stacks we don't need
|
195
|
+
while not @stacks[indent].nil? and indent < @stacks[indent].length - 1
|
196
|
+
@stacks[indent].pop
|
197
|
+
end
|
198
|
+
|
199
|
+
# This line's indentation happens lie "between" two other line's
|
200
|
+
# indentation:
|
201
|
+
#
|
202
|
+
# hello
|
203
|
+
# world
|
204
|
+
# this # <- This should not be possible!
|
205
|
+
syntax_error('Malformed indentation') if indent != @indents.last
|
206
|
+
end
|
207
|
+
|
208
|
+
parse_line_indicators
|
209
|
+
end
|
210
|
+
|
211
|
+
def parse_line_indicators
|
212
|
+
add_new_line = true
|
213
|
+
|
214
|
+
case @line
|
215
|
+
when /\Aimport /
|
216
|
+
@line = $'
|
217
|
+
parse_import
|
218
|
+
|
219
|
+
when /\Amixin #{NAME_RE_STRING}/
|
220
|
+
# Mixin declaration
|
221
|
+
@line = $'
|
222
|
+
parse_mixin_declaration($1)
|
223
|
+
|
224
|
+
when /\A\+#{NAME_RE_STRING}/
|
225
|
+
# Mixin call
|
226
|
+
@line = $'
|
227
|
+
parse_mixin_call($1)
|
228
|
+
|
229
|
+
when /\Ablock #{NAME_RE_STRING}/
|
230
|
+
@line = $'
|
231
|
+
if @stacks.last.last.type == :mixin_call
|
232
|
+
append_node :mixin_block, data: $1, add: true
|
233
|
+
else
|
234
|
+
# keyword block used outside of mixin call
|
235
|
+
parse_tag($&)
|
236
|
+
end
|
237
|
+
|
238
|
+
when /\A\/\/! /
|
239
|
+
# HTML comment
|
240
|
+
append_node :html_comment, add: true
|
241
|
+
parse_text_block $', @indents.last + @tabsize
|
242
|
+
|
243
|
+
when /\A\/\//
|
244
|
+
# Comment
|
245
|
+
append_node :comment, add: true
|
246
|
+
parse_text_block $', @indents.last + @tabsize
|
247
|
+
|
248
|
+
when /\A\|( ?)/
|
249
|
+
# Found a text block.
|
250
|
+
parse_text_block $', @indents.last + @tabsize
|
251
|
+
|
252
|
+
when /\A</
|
253
|
+
# Inline html
|
254
|
+
append_node :text, data: @line
|
255
|
+
|
256
|
+
when /\A-\s*(.*)\Z/
|
257
|
+
# Found a code block.
|
258
|
+
code_node = append_node :ruby_code
|
259
|
+
code_node.data = $1
|
260
|
+
add_new_line = false
|
261
|
+
|
262
|
+
when /\A(&?)=/
|
263
|
+
# Found an output block.
|
264
|
+
# We expect the line to be broken or the next line to be indented.
|
265
|
+
@line = $'
|
266
|
+
output_node = append_node :output
|
267
|
+
output_node.escaped = $1.length == 1
|
268
|
+
output_node.data = parse_ruby_code("\n")
|
269
|
+
|
270
|
+
when /\A(\w+):\s*\Z/
|
271
|
+
# Embedded template detected. It is treated as block.
|
272
|
+
@stacks.last << [:slim, :embedded, $1, parse_text_block]
|
273
|
+
|
274
|
+
when /\Adoctype\s/i
|
275
|
+
# Found doctype declaration
|
276
|
+
append_node :doctype, data: $'.strip
|
277
|
+
|
278
|
+
when TAG_RE
|
279
|
+
# Found a HTML tag.
|
280
|
+
@line = $' if $1
|
281
|
+
parse_tag($&)
|
282
|
+
|
283
|
+
when /\A\./
|
284
|
+
# Found class name -> implicit div
|
285
|
+
parse_tag 'div'
|
286
|
+
|
287
|
+
when /\A#/
|
288
|
+
# Found id name -> implicit div
|
289
|
+
parse_tag 'div'
|
290
|
+
|
291
|
+
else
|
292
|
+
syntax_error 'Unknown line indicator'
|
293
|
+
end
|
294
|
+
|
295
|
+
append_node :newline if add_new_line
|
296
|
+
end
|
297
|
+
|
298
|
+
def parse_import
|
299
|
+
path = eval(@line)
|
300
|
+
import_node = append_node :import
|
301
|
+
import_node.data = path
|
302
|
+
|
303
|
+
@dependency_paths << path unless @dependency_paths.include?(path)
|
304
|
+
end
|
305
|
+
|
306
|
+
def parse_mixin_call(mixin_name)
|
307
|
+
mixin_node = append_node :mixin_call, add: true
|
308
|
+
mixin_node.data = mixin_name
|
309
|
+
|
310
|
+
parse_mixin_call_params
|
311
|
+
|
312
|
+
case @line
|
313
|
+
when /\A /
|
314
|
+
@line = $'
|
315
|
+
parse_text
|
316
|
+
|
317
|
+
when /\A:\s+/
|
318
|
+
# Block expansion
|
319
|
+
@line = $'
|
320
|
+
parse_line_indicators
|
321
|
+
|
322
|
+
when /\A(&?)=/
|
323
|
+
# Handle output code
|
324
|
+
parse_line_indicators
|
325
|
+
|
326
|
+
when /^$/
|
327
|
+
# nothing
|
328
|
+
|
329
|
+
else
|
330
|
+
syntax_error "Unknown symbol after mixin calling, line = `#{@line}'"
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
def parse_mixin_call_params
|
335
|
+
# between tag name and attribute must not be space
|
336
|
+
# and skip when is nothing other
|
337
|
+
if @line =~ /\A\(/
|
338
|
+
@line = $'
|
339
|
+
else
|
340
|
+
return
|
341
|
+
end
|
342
|
+
|
343
|
+
end_re = /\A\s*\)/
|
344
|
+
|
345
|
+
while true
|
346
|
+
case @line
|
347
|
+
when CODE_ATTR_RE
|
348
|
+
@line = $'
|
349
|
+
attr_node = append_node :mixin_key_param
|
350
|
+
attr_node.name = $1
|
351
|
+
attr_node.value = parse_ruby_code(',)')
|
352
|
+
|
353
|
+
when /\A\s*,/
|
354
|
+
# args delimiter
|
355
|
+
@line = $'
|
356
|
+
next
|
357
|
+
|
358
|
+
when /^\s*$/
|
359
|
+
# spaces and/or end of line
|
360
|
+
next_line
|
361
|
+
next
|
362
|
+
|
363
|
+
when end_re
|
364
|
+
# Find ending delimiter
|
365
|
+
@line = $'
|
366
|
+
break
|
367
|
+
|
368
|
+
else
|
369
|
+
attr_node = append_node :mixin_param
|
370
|
+
attr_node.data = parse_ruby_code(',)')
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def parse_mixin_declaration(mixin_name)
|
376
|
+
mixin_node = append_node :mixin_declaration, add: true
|
377
|
+
mixin_node.data = mixin_name
|
378
|
+
|
379
|
+
parse_mixin_declaration_params
|
380
|
+
end
|
381
|
+
|
382
|
+
def parse_mixin_declaration_params
|
383
|
+
# between tag name and attribute must not be space
|
384
|
+
# and skip when is nothing other
|
385
|
+
if @line =~ /\A\(/
|
386
|
+
@line = $'
|
387
|
+
else
|
388
|
+
return
|
389
|
+
end
|
390
|
+
|
391
|
+
end_re = /\A\s*\)/
|
392
|
+
|
393
|
+
while true
|
394
|
+
case @line
|
395
|
+
when CODE_ATTR_RE
|
396
|
+
# Value ruby code
|
397
|
+
@line = $'
|
398
|
+
attr_node = append_node :mixin_key_param
|
399
|
+
attr_node.name = $1
|
400
|
+
attr_node.value = parse_ruby_code(',)')
|
401
|
+
|
402
|
+
when /\A\s*#{NAME_RE_STRING}/
|
403
|
+
@line = $'
|
404
|
+
append_node :mixin_param, data: $1
|
405
|
+
|
406
|
+
when /\A\s*&#{NAME_RE_STRING}/
|
407
|
+
@line = $'
|
408
|
+
append_node :mixin_block_param, data: $1
|
409
|
+
|
410
|
+
when /\A\s*,/
|
411
|
+
# args delimiter
|
412
|
+
@line = $'
|
413
|
+
next
|
414
|
+
|
415
|
+
when end_re
|
416
|
+
# Find ending delimiter
|
417
|
+
@line = $'
|
418
|
+
break
|
419
|
+
|
420
|
+
else
|
421
|
+
syntax_error('wrong mixin attribute syntax')
|
422
|
+
end
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
def parse_text
|
427
|
+
text = @line
|
428
|
+
text = text.gsub(/&\{/, '#{ html_escaped ')
|
429
|
+
append_node :text, data: text
|
430
|
+
end
|
431
|
+
|
432
|
+
|
433
|
+
# @param value [String]
|
434
|
+
#
|
435
|
+
def fixed_trailing_colon(value)
|
436
|
+
if value =~ /(:)\Z/
|
437
|
+
value = value.sub /:\Z/, ''
|
438
|
+
@line.prepend ':'
|
439
|
+
end
|
440
|
+
|
441
|
+
value
|
442
|
+
end
|
443
|
+
|
444
|
+
|
445
|
+
# @param [String] tag tag name
|
446
|
+
#
|
447
|
+
def parse_tag(tag)
|
448
|
+
tag = fixed_trailing_colon(tag)
|
449
|
+
|
450
|
+
if tag.is_a? Node
|
451
|
+
tag_node = tag
|
452
|
+
else
|
453
|
+
tag_node = append_node :tag, add: true
|
454
|
+
tag_node.name = tag
|
455
|
+
end
|
456
|
+
|
457
|
+
parse_tag_attributes
|
458
|
+
|
459
|
+
case @line
|
460
|
+
when /\A:\s+/
|
461
|
+
# Block expansion
|
462
|
+
@line = $'
|
463
|
+
parse_line_indicators
|
464
|
+
|
465
|
+
when /\A(&?)=/
|
466
|
+
# Handle output code
|
467
|
+
parse_line_indicators
|
468
|
+
|
469
|
+
when CLASS_TAG_RE
|
470
|
+
# Class name
|
471
|
+
@line = $'
|
472
|
+
|
473
|
+
attr_node = append_node :tag_attribute
|
474
|
+
attr_node.name = 'class'
|
475
|
+
attr_node.value = fixed_trailing_colon($1).single_quote
|
476
|
+
|
477
|
+
parse_tag tag_node
|
478
|
+
|
479
|
+
when ID_TAG_RE
|
480
|
+
# Id name
|
481
|
+
@line = $'
|
482
|
+
|
483
|
+
attr_node = append_node :tag_attribute
|
484
|
+
attr_node.name = 'id'
|
485
|
+
attr_node.value = fixed_trailing_colon($1).single_quote
|
486
|
+
|
487
|
+
parse_tag tag_node
|
488
|
+
|
489
|
+
when /\A /
|
490
|
+
# Text content
|
491
|
+
@line = $'
|
492
|
+
parse_text
|
493
|
+
|
494
|
+
when /^$/
|
495
|
+
# nothing
|
496
|
+
|
497
|
+
else
|
498
|
+
syntax_error "Unknown symbol after tag definition #{@line}"
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
def parse_tag_attributes
|
503
|
+
# Check to see if there is a delimiter right after the tag name
|
504
|
+
|
505
|
+
# between tag name and attribute must not be space
|
506
|
+
# and skip when is nothing other
|
507
|
+
if @line =~ /\A\(/
|
508
|
+
@line = $'
|
509
|
+
else
|
510
|
+
return
|
511
|
+
end
|
512
|
+
|
513
|
+
end_re = /\A\s*\)/
|
514
|
+
|
515
|
+
while true
|
516
|
+
case @line
|
517
|
+
when CODE_ATTR_RE
|
518
|
+
# Value ruby code
|
519
|
+
@line = $'
|
520
|
+
attr_node = append_node :tag_attribute
|
521
|
+
attr_node.name = $1
|
522
|
+
attr_node.value = parse_ruby_code(',)')
|
523
|
+
|
524
|
+
when /\A\s*,/
|
525
|
+
# args delimiter
|
526
|
+
@line = $'
|
527
|
+
next
|
528
|
+
|
529
|
+
when end_re
|
530
|
+
# Find ending delimiter
|
531
|
+
@line = $'
|
532
|
+
break
|
533
|
+
|
534
|
+
else
|
535
|
+
# Found something where an attribute should be
|
536
|
+
@line.lstrip!
|
537
|
+
syntax_error('Expected attribute') unless @line.empty?
|
538
|
+
|
539
|
+
# Attributes span multiple lines
|
540
|
+
@stacks.last << [:newline]
|
541
|
+
syntax_error("Expected closing delimiter #{delimiter}") if @lines.empty?
|
542
|
+
next_line
|
543
|
+
end
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
def parse_text_block(first_line, text_indent = nil)
|
548
|
+
if !first_line || first_line.empty?
|
549
|
+
text_indent = nil
|
550
|
+
else
|
551
|
+
@line = first_line
|
552
|
+
parse_text
|
553
|
+
end
|
554
|
+
|
555
|
+
until @lines.empty?
|
556
|
+
if @lines.first =~ /\A\s*\Z/
|
557
|
+
next_line
|
558
|
+
append_node :newline
|
559
|
+
else
|
560
|
+
indent = get_indent(@lines.first)
|
561
|
+
break if indent <= @indents.last
|
562
|
+
|
563
|
+
next_line
|
564
|
+
|
565
|
+
@line.remove_indent!(text_indent ? text_indent : indent, @tabsize)
|
566
|
+
|
567
|
+
parse_text
|
568
|
+
|
569
|
+
# The indentation of first line of the text block
|
570
|
+
# determines the text base indentation.
|
571
|
+
text_indent ||= indent
|
572
|
+
end
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
# Parse ruby code, ended with outer delimiters
|
577
|
+
#
|
578
|
+
# @param [String] outer_delimiters
|
579
|
+
#
|
580
|
+
# @return [Void] parsed ruby code
|
581
|
+
#
|
582
|
+
def parse_ruby_code(outer_delimiters)
|
583
|
+
code = ''
|
584
|
+
end_re = /\A\s*[#{Regexp.escape outer_delimiters.to_s}]/
|
585
|
+
delimiters = []
|
586
|
+
|
587
|
+
until @line.empty? or (delimiters.count == 0 and @line =~ end_re)
|
588
|
+
char = @line[0]
|
589
|
+
|
590
|
+
# backslash escaped delimiter
|
591
|
+
if char == '\\' && RUBY_ALL_DELIMITERS.include?(@line[1])
|
592
|
+
code << @line.slice!(0, 2)
|
593
|
+
next
|
594
|
+
end
|
595
|
+
|
596
|
+
case char
|
597
|
+
when RUBY_START_DELIMITERS_RE
|
598
|
+
if RUBY_NOT_NESTABLE_DELIMITERS.include?(char) && delimiters.last == char
|
599
|
+
# end char of not nestable delimiter
|
600
|
+
delimiters.pop
|
601
|
+
else
|
602
|
+
# diving
|
603
|
+
delimiters << char
|
604
|
+
end
|
605
|
+
|
606
|
+
when RUBY_END_DELIMITERS_RE
|
607
|
+
# rising
|
608
|
+
if char == RUBY_DELIMITERS_REVERSE[delimiters.last]
|
609
|
+
delimiters.pop
|
610
|
+
end
|
611
|
+
end
|
612
|
+
|
613
|
+
code << @line.slice!(0)
|
614
|
+
end
|
615
|
+
|
616
|
+
unless delimiters.empty?
|
617
|
+
syntax_error('Unexpected end of ruby code')
|
618
|
+
end
|
619
|
+
|
620
|
+
code.strip
|
621
|
+
end
|
622
|
+
|
623
|
+
RUBY_DELIMITERS_REVERSE = {
|
624
|
+
'(' => ')',
|
625
|
+
'[' => ']',
|
626
|
+
'{' => '}'
|
627
|
+
}.freeze
|
628
|
+
|
629
|
+
RUBY_QUOTES = %w(' ").freeze
|
630
|
+
|
631
|
+
RUBY_NOT_NESTABLE_DELIMITERS = RUBY_QUOTES
|
632
|
+
|
633
|
+
RUBY_START_DELIMITERS = (%w(\( [ {) + RUBY_NOT_NESTABLE_DELIMITERS).freeze
|
634
|
+
RUBY_END_DELIMITERS = (%w(\) ] }) + RUBY_NOT_NESTABLE_DELIMITERS).freeze
|
635
|
+
RUBY_ALL_DELIMITERS = (RUBY_START_DELIMITERS + RUBY_END_DELIMITERS).uniq.freeze
|
636
|
+
|
637
|
+
RUBY_START_DELIMITERS_RE = /\A[#{Regexp.escape RUBY_START_DELIMITERS.join('')}]/
|
638
|
+
RUBY_END_DELIMITERS_RE = /\A[#{Regexp.escape RUBY_END_DELIMITERS.join('')}]/
|
639
|
+
|
640
|
+
|
641
|
+
# ----------- Errors ---------------
|
642
|
+
|
643
|
+
# Raise specific error
|
644
|
+
#
|
645
|
+
# @param [String] message
|
646
|
+
#
|
647
|
+
def syntax_error(message)
|
648
|
+
raise SyntaxError.new(message, @options[:file], @orig_line, @lineno,
|
649
|
+
@orig_line && @line ? @orig_line.size - @line.size : 0)
|
650
|
+
end
|
651
|
+
end
|
652
|
+
end
|