bade 0.1.3

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