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.
@@ -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