super-slim 3.0.0

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,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "etc"
4
+
5
+ module SuperSlim
6
+ # Maps items in forked processes, one per core, and returns the results in item order. Below
7
+ # minimum_per_process items per process, forking costs more than it saves, and the map stays in this process.
8
+ module Parallel
9
+ module_function
10
+
11
+ def map(items, minimum_per_process: 100, &block)
12
+ process_count = [Etc.nprocessors, items.size / minimum_per_process].min
13
+ return items.map(&block) if process_count < 2 || !Process.respond_to?(:fork)
14
+
15
+ readers = items.each_slice((items.size.to_f / process_count).ceil).map do |slice|
16
+ reader, writer = IO.pipe
17
+ fork do
18
+ reader.close
19
+ results = begin
20
+ slice.map(&block)
21
+ rescue Exception => exception # rubocop:disable Lint/RescueException -- re-raised by the parent
22
+ exception
23
+ end
24
+ writer.binmode.write(Marshal.dump(results))
25
+ exit!(0)
26
+ end
27
+ writer.close
28
+ reader
29
+ end
30
+ results_by_process = readers.map { |reader| Marshal.load(reader.binmode.read) }
31
+ Process.waitall
32
+ results_by_process.each { |results| raise results if results.is_a?(Exception) }
33
+ results_by_process.flatten(1)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,556 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module SuperSlim
6
+ # Reads each line with one StringScanner that moves forward: no MatchData, and no copy of the rest of the
7
+ # line after each token, so parsing allocates little more than the nodes it builds.
8
+ class Parser
9
+ include AST
10
+
11
+ TAB_SIZE = 4
12
+ TAB = /\G((?: {#{TAB_SIZE}})*) {0,#{TAB_SIZE - 1}}\t/
13
+ DELIMITERS = {"(" => ")", "[" => "]", "{" => "}"}.freeze
14
+ SUPPORTED_ENGINES = %w[javascript css ruby markdown].freeze
15
+ # Engines Slim ships that SuperSlim deliberately refuses, so they fail loudly instead of parsing as a tag.
16
+ UNSUPPORTED_ENGINES = %w[asciidoc textile rdoc coffee sass scss less erb builder creole wiki org].freeze
17
+ ENGINE = /(#{Regexp.union(SUPPORTED_ENGINES + UNSUPPORTED_ENGINES)})(?:\s*(?:(.*)))?:(\s*)/
18
+ DOCTYPE = /doctype\b/
19
+
20
+ ATTRIBUTE_NAME = /\s*([^\0\s"'><\/=()\[\]{}]+)/
21
+ QUOTED_ATTRIBUTE = /#{ATTRIBUTE_NAME}\s*=(=?)\s*("|')/
22
+ CODE_ATTRIBUTE = /#{ATTRIBUTE_NAME}\s*=(=?)\s*/
23
+ SPLAT = /\s*\*(?=[^\s]+)/
24
+ ATTRIBUTE_LIST_OPENING = /\s*([(\[{])/
25
+ BLANK_LINE = /\A\s*\z/
26
+ BLANK_REST = /\s*\z/
27
+ WHITESPACE = /\s*/
28
+ SPACE_BYTE = " ".ord
29
+ TAB_BYTE = "\t".ord
30
+ # At the scanner, the rest of the line is only this character; on a whole line, the line ends with it.
31
+ LINE_CONTINUATION = /[,\\]\z/
32
+ BACKSLASH_AT_END = /\\\z/
33
+ WHITESPACE_MARKERS = /[<>']*/
34
+ TEXT_LINE_INDICATOR = /([|'])([<>]{1,2}(?: |\z)| ?)/
35
+ HTML_COMMENT = /\/!( ?)/
36
+ CONDITIONAL_COMMENT = /\/\[\s*(.*?)\s*\]\s*\z/
37
+ NESTED_TAG = /\s*:\s*/
38
+ TAG_OUTPUT = /\s*=(=?)(['<>]*)/
39
+ CLOSED_TAG = /\s*\/\s*/
40
+ SHORTCUT_VALUE_TO_ESCAPE = /[&<>"']/
41
+ # Runs of characters the scanners would otherwise copy one by one: no delimiter, no line continuation.
42
+ CODE_CHUNK_BY_CLOSING = [nil, *DELIMITERS.values].to_h { |closing| [closing, /(?:[^\s(\[{,\\#{Regexp.escape(closing.to_s)}]|[,\\](?!\z))+/] }.freeze
43
+ NESTED_CODE_CHUNK_BY_OPENING = DELIMITERS.to_h { |opening, closing| [opening, /(?:[^,\\#{Regexp.escape(opening + closing)}]|[,\\](?!\z))+/] }.freeze
44
+ QUOTED_CHUNK_BY_QUOTE = %w[" '].to_h { |quote| [quote, /(?:[^{}\\#{quote}]|\\(?!\z))+/] }.freeze
45
+ SLASH, PIPE, APOSTROPHE, LESS_THAN, GREATER_THAN, DASH, EQUALS = "/|'<>-=".bytes
46
+ OPENING_BRACE, CLOSING_BRACE = "{}".bytes
47
+ WHITESPACE_MARKER_BYTES = "<>'".bytes.freeze
48
+ BOOLEAN_ATTRIBUTE_BY_CLOSING = DELIMITERS.values.to_h { |closing| [closing, /#{ATTRIBUTE_NAME}(?=(\s|#{Regexp.escape(closing)}|\z))/] }.freeze
49
+ ATTRIBUTE_LIST_END_BY_CLOSING = DELIMITERS.values.to_h { |closing| [closing, /\s*#{Regexp.escape(closing)}/] }.freeze
50
+ CODE_END_BY_CLOSING = [nil, *DELIMITERS.values].to_h { |closing| [closing, /[\s#{Regexp.escape(closing.to_s)}]/] }.freeze
51
+
52
+ # What the formatter and the linter need to know about each source line, indexed by line number. A tag's
53
+ # attribute list on one line is [first, last, [[first, last], …]], in columns of that line.
54
+ LineInfo = Struct.new(:kind, :depth, :owner, :text, :output_column, :attribute_lists)
55
+
56
+ # Ruby code and interpolated text, as [kind, first, last] character offsets in the source: what the RuboCop plugin lints.
57
+ attr_reader :outline, :line_by_node, :ruby_ranges
58
+
59
+ def initialize(file: nil, outline: false, tailwind_classes: SuperSlim.tailwind_classes)
60
+ @file = file
61
+ @attribute_shortcut = tailwind_classes ? Shortcuts::TAILWIND_ATTRIBUTE_PATTERN : Shortcuts::ATTRIBUTE_PATTERN
62
+ @record_outline = outline
63
+ end
64
+
65
+ def call(source)
66
+ root = []
67
+ if @record_outline
68
+ @outline = [nil]
69
+ @line_by_node = {}.compare_by_identity
70
+ @ruby_ranges = []
71
+ end
72
+ normalized = normalize(source)
73
+ @line_starts = line_starts(normalized, source.size - normalized.size) if @record_outline
74
+ # A string separator splits 6x faster than the regexp, which only templates with \r need.
75
+ @lines = normalized.include?("\r") ? normalized.split(/\r?\n/) : normalized.split("\n")
76
+ @scanner = StringScanner.new("")
77
+ @stacks = [root]
78
+ @indents = []
79
+ @lineno = 0
80
+ parse_line while next_line
81
+ root
82
+ ensure
83
+ @lines = @stacks = @line = @scanner = nil
84
+ end
85
+
86
+ private
87
+
88
+ def normalize(source)
89
+ source = +source
90
+ unless source.encoding == Encoding::UTF_8
91
+ original_encoding = source.encoding
92
+ source.force_encoding(Encoding::UTF_8)
93
+ source.force_encoding(original_encoding) unless source.valid_encoding?
94
+ end
95
+ source.delete_prefix("")
96
+ end
97
+
98
+ def line_starts(source, byte_order_mark_size)
99
+ line_starts = [nil, byte_order_mark_size]
100
+ source.each_line { |line| line_starts << line_starts.last + line.size }
101
+ line_starts
102
+ end
103
+
104
+ def next_line
105
+ if @lines.empty?
106
+ @line = nil
107
+ else
108
+ @line = @lines.shift
109
+ @lineno += 1
110
+ @scanner.string = @line
111
+ @scanner_column = 0
112
+ @outline << LineInfo.new(:continuation, nil, @structure_lineno) if @outline
113
+ @line
114
+ end
115
+ end
116
+
117
+ # Continuation lines are read stripped at both ends.
118
+ def expect_next_line
119
+ next_line || syntax_error!("Unexpected end of file")
120
+ @scanner.string = @line.strip
121
+ @scanner_column = @line.size - @line.lstrip.size
122
+ end
123
+
124
+ def outline_line(kind)
125
+ @outline[@lineno].kind = kind if @outline
126
+ end
127
+
128
+ # Characters of the line read so far, even when the scanner reads a stripped copy or attribute source.
129
+ def column
130
+ @line.size - (@scanner.string.size - @scanner.charpos)
131
+ end
132
+
133
+ # Where the scanner stands in the source, unlike `column` exact on a stripped line or on attribute source.
134
+ def source_offset
135
+ @line_starts[@lineno] + @scanner_column + @scanner.charpos
136
+ end
137
+
138
+ def line_end_offset
139
+ @line_starts[@lineno] + @line.rstrip.size
140
+ end
141
+
142
+ def record_ruby(kind, first, last)
143
+ @ruby_ranges << [kind, first, last] if last > first
144
+ end
145
+
146
+ def located(node, lineno = @lineno)
147
+ @line_by_node[node] = lineno if @outline
148
+ node
149
+ end
150
+
151
+ # Counts bytes rather than searching for a regexp, which would allocate a MatchData for every line.
152
+ def indent_of(line)
153
+ indent = 0
154
+ indent += 1 while line.getbyte(indent) == SPACE_BYTE
155
+ return indent unless line.getbyte(indent) == TAB_BYTE
156
+
157
+ line[/\A[ \t]*/].gsub(TAB, "\\1#{" " * TAB_SIZE}").size
158
+ end
159
+
160
+ def parse_line
161
+ if @line.match?(BLANK_LINE)
162
+ outline_line(:blank)
163
+ return @stacks.last << NEWLINE
164
+ end
165
+
166
+ indent = indent_of(@line)
167
+ @indents << indent if @indents.empty?
168
+ @scanner.skip(WHITESPACE)
169
+ expecting_indentation = @stacks.size > @indents.size
170
+
171
+ if indent > @indents.last
172
+ syntax_error!("Unexpected indentation") unless expecting_indentation
173
+ @indents << indent
174
+ else
175
+ @stacks.pop if expecting_indentation
176
+ while indent < @indents.last && @indents.size > 1
177
+ @indents.pop
178
+ @stacks.pop
179
+ end
180
+ syntax_error!("Malformed indentation") if indent != @indents.last
181
+ end
182
+
183
+ if @outline
184
+ @structure_lineno = @lineno
185
+ @outline[@lineno] = LineInfo.new(:structure, @indents.size - 1)
186
+ end
187
+ parse_line_indicators
188
+ end
189
+
190
+ def parse_line_indicators
191
+ case @scanner.peek_byte
192
+ when SLASH then parse_comment
193
+ when PIPE, APOSTROPHE then parse_text_line
194
+ when LESS_THAN then parse_inline_html
195
+ when DASH then parse_control
196
+ when EQUALS then parse_output
197
+ else parse_tag_line
198
+ end
199
+ @stacks.last << NEWLINE
200
+ end
201
+
202
+ def parse_comment
203
+ if @scanner.skip(HTML_COMMENT)
204
+ text_indent = @indents.last + @scanner[1].size + 2
205
+ @stacks.last << Comment.new(parse_text_block(@scanner.rest, text_indent))
206
+ elsif @scanner.match?(CONDITIONAL_COMMENT)
207
+ syntax_error!("Conditional comments are not supported")
208
+ else
209
+ skip_comment_block
210
+ end
211
+ end
212
+
213
+ def parse_text_line
214
+ @scanner.skip(TEXT_LINE_INDICATOR)
215
+ indicator = @scanner[1]
216
+ markers = @scanner[2]
217
+ leading_whitespace = markers.include?("<")
218
+ trailing_whitespace = indicator == "'" || markers.include?(">")
219
+ @stacks.last << Static.new(" ") if leading_whitespace
220
+ lineno = @lineno
221
+ @stacks.last << located(TextBlock.new(parse_text_block(@scanner.rest, @indents.last + markers.count(" ") + 1)), lineno)
222
+ @stacks.last << Static.new(" ") if trailing_whitespace
223
+ end
224
+
225
+ def parse_inline_html
226
+ record_ruby(:text, source_offset, line_end_offset) if @outline
227
+ children = []
228
+ @stacks.last << located(InlineHtml.new(@scanner.rest, children))
229
+ @stacks << children
230
+ end
231
+
232
+ def parse_control
233
+ lineno = @lineno
234
+ @scanner.pos += 1
235
+ children = []
236
+ @stacks.last << located(Control.new(parse_broken_line, children), lineno)
237
+ @stacks << children
238
+ end
239
+
240
+ def parse_output
241
+ position = @scanner.pos + 1
242
+ escape = @line.getbyte(position) != EQUALS
243
+ position += 1 unless escape
244
+ leading_whitespace = trailing_whitespace = false
245
+ while (byte = @line.getbyte(position)) == LESS_THAN || byte == GREATER_THAN
246
+ leading_whitespace ||= byte == LESS_THAN
247
+ trailing_whitespace ||= byte == GREATER_THAN
248
+ position += 1
249
+ end
250
+ @scanner.pos = position
251
+ children = []
252
+ @stacks.last << Static.new(" ") if leading_whitespace
253
+ lineno = @lineno
254
+ @stacks.last << located(Output.new(parse_broken_line, escape, children), lineno)
255
+ @stacks.last << Static.new(" ") if trailing_whitespace
256
+ @stacks << children
257
+ end
258
+
259
+ def parse_tag_line
260
+ if @scanner.skip(ENGINE)
261
+ @stacks.last << parse_embedded(@scanner[1], @scanner[2], @scanner.rest)
262
+ elsif @scanner.skip(DOCTYPE)
263
+ @stacks.last << Doctype.new(@scanner.rest.strip)
264
+ elsif (tag = scan_tag)
265
+ parse_tag(tag)
266
+ else
267
+ syntax_error!("Unknown line indicator")
268
+ end
269
+ end
270
+
271
+ # A tag name is consumed with its match; a shortcut is left for parse_tag to read as an attribute.
272
+ def scan_tag
273
+ tag = @scanner.check(Shortcuts::TAG_PATTERN) or return
274
+ @scanner.pos += tag.bytesize if @scanner[1]
275
+ tag
276
+ end
277
+
278
+ def parse_embedded(engine, attributes_source, text)
279
+ syntax_error!("Embedded engine #{engine} is not supported") if UNSUPPORTED_ENGINES.include?(engine)
280
+
281
+ lineno = @lineno
282
+ text_indent = @line.size - text.size + attributes_source.size
283
+ @scanner_column += @scanner.charpos - @scanner[3].size - 1 - attributes_source.size
284
+ @scanner.string = attributes_source
285
+ attributes = parse_attributes
286
+ located Embedded.new(engine, parse_text_block(text, text_indent, (engine == "ruby") ? :embedded_ruby : :text), attributes), lineno
287
+ end
288
+
289
+ def skip_comment_block
290
+ while !@lines.empty? && (@lines.first.match?(BLANK_LINE) || indent_of(@lines.first) > @indents.last)
291
+ next_line
292
+ @stacks.last << NEWLINE
293
+ end
294
+ end
295
+
296
+ def parse_text_block(first_line = nil, text_indent = nil, kind = :text)
297
+ nodes = []
298
+ if !first_line || first_line.empty?
299
+ text_indent = nil
300
+ else
301
+ nodes << Text.new(first_line)
302
+ if @outline
303
+ @outline[@lineno].text = true
304
+ first = @line_starts[@lineno] + @line.rstrip.size - first_line.rstrip.size
305
+ last = line_end_offset
306
+ end
307
+ end
308
+
309
+ empty_lines = 0
310
+ # Blank lines inside a text block render only when more text follows them.
311
+ rendered_blank_linenos = [] if @outline
312
+ until @lines.empty?
313
+ if @lines.first.match?(BLANK_LINE)
314
+ next_line
315
+ nodes << NEWLINE
316
+ if text_indent
317
+ empty_lines += 1
318
+ rendered_blank_linenos << @lineno if @outline
319
+ else
320
+ outline_line(:blank)
321
+ end
322
+ else
323
+ indent = indent_of(@lines.first)
324
+ break if indent <= @indents.last
325
+
326
+ if empty_lines > 0
327
+ nodes << Text.new("\n" * empty_lines)
328
+ empty_lines = 0
329
+ end
330
+
331
+ if @outline
332
+ rendered_blank_linenos.each { @outline[_1].kind = :text_blank }
333
+ rendered_blank_linenos.clear
334
+ end
335
+ next_line
336
+ outline_line(:text)
337
+ if @outline
338
+ first ||= @line_starts[@lineno] + @line.size - @line.lstrip.size
339
+ last = line_end_offset
340
+ end
341
+
342
+ offset = text_indent ? indent - text_indent : 0
343
+ if offset < 0
344
+ text_indent += offset
345
+ offset = 0
346
+ end
347
+ nodes << NEWLINE << Text.new("#{"\n" if text_indent}#{" " * offset}#{@line.lstrip}")
348
+ text_indent ||= indent
349
+ end
350
+ end
351
+ if @outline
352
+ rendered_blank_linenos.each { @outline[_1].kind = :blank }
353
+ record_ruby(kind, first, last) if first
354
+ end
355
+ nodes
356
+ end
357
+
358
+ def parse_broken_line
359
+ first = source_offset + @scanner.rest[/\A\s*/].size if @outline
360
+ broken_line = @scanner.rest
361
+ broken_line.strip!
362
+ while broken_line.match?(LINE_CONTINUATION)
363
+ expect_next_line
364
+ broken_line << "\n" << @scanner.string
365
+ end
366
+ record_ruby(:code, first, line_end_offset) if @outline
367
+ broken_line
368
+ end
369
+
370
+ def parse_tag(tag)
371
+ tag_lineno = @lineno
372
+ tag = Shortcuts::DEFAULT_TAG if Shortcuts::ATTRIBUTE_BY_SHORTCUT.key?(tag)
373
+
374
+ attributes = []
375
+ # Read before it is consumed, so that an illegal shortcut is reported where it starts.
376
+ while (shortcut_length = @scanner.match?(@attribute_shortcut))
377
+ shortcut = @scanner[1]
378
+ value = @scanner[2]
379
+ syntax_error!("Illegal shortcut") unless (name = Shortcuts::ATTRIBUTE_BY_SHORTCUT[shortcut])
380
+
381
+ # Tailwind arbitrary values may hold quotes and ampersands, which Slim names never do.
382
+ value = ERB::Escape.html_escape(value) if value.match?(SHORTCUT_VALUE_TO_ESCAPE)
383
+ attributes << StaticAttribute.new(name, value)
384
+ @scanner.pos += shortcut_length
385
+ end
386
+
387
+ leading_whitespace = trailing_whitespace = false
388
+ if WHITESPACE_MARKER_BYTES.include?(@scanner.peek_byte)
389
+ markers = @scanner.scan(WHITESPACE_MARKERS)
390
+ trailing_whitespace = markers.include?(">")
391
+ leading_whitespace = markers.include?("<")
392
+ end
393
+
394
+ parse_attributes(attributes, outline: true)
395
+
396
+ siblings = @stacks.last
397
+ content, block, output_whitespace = parse_tag_content
398
+ siblings << Static.new(" ") if leading_whitespace
399
+ siblings << located(Tag.new(tag, attributes, content), tag_lineno)
400
+ siblings << Static.new(" ") if trailing_whitespace
401
+ if output_whitespace
402
+ output_leading_whitespace, output_trailing_whitespace = output_whitespace
403
+ siblings.insert(-2, Static.new(" ")) if output_leading_whitespace && !leading_whitespace
404
+ siblings << Static.new(" ") if output_trailing_whitespace && !trailing_whitespace
405
+ end
406
+ @stacks << block if block
407
+ end
408
+
409
+ # Returns the tag content, the block expecting indented children, and the whitespace an `=<>` output adds.
410
+ def parse_tag_content
411
+ output_column = column if @outline
412
+ if @scanner.skip(NESTED_TAG)
413
+ return [[parse_embedded(@scanner[1], @scanner[2], @scanner.rest)]] if @scanner.skip(ENGINE)
414
+
415
+ (tag = scan_tag) || syntax_error!("Expected tag")
416
+ content = []
417
+ @stacks << content
418
+ stack_index = @stacks.size - 1
419
+ parse_tag(tag)
420
+ @stacks.delete_at(stack_index)
421
+ [content]
422
+ elsif @scanner.skip(TAG_OUTPUT)
423
+ @outline[@lineno].output_column = output_column if @outline
424
+ escape = @scanner[1] != "="
425
+ markers = @scanner[2]
426
+ output_whitespace = [markers.include?("<"), markers.include?(">")]
427
+ children = []
428
+ lineno = @lineno
429
+ [[located(Output.new(parse_broken_line, escape, children), lineno)], children, output_whitespace]
430
+ elsif @scanner.skip(CLOSED_TAG)
431
+ syntax_error!("Unexpected text after closed tag") unless @scanner.eos?
432
+ [nil]
433
+ elsif @scanner.match?(BLANK_REST)
434
+ content = []
435
+ [content, content]
436
+ else
437
+ @scanner.pos += 1 if @scanner.peek_byte == SPACE_BYTE
438
+ text_indent = column
439
+ [[TextBlock.new(parse_text_block(@scanner.rest, text_indent))]]
440
+ end
441
+ end
442
+
443
+ def parse_attributes(attributes = [], outline: false)
444
+ outline &&= @outline
445
+ if outline
446
+ lineno = @lineno
447
+ list_first = column + @scanner.match?(WHITESPACE)
448
+ spans = []
449
+ end
450
+ closing = DELIMITERS[@scanner[1]] if @scanner.skip(ATTRIBUTE_LIST_OPENING)
451
+
452
+ loop do
453
+ first = column + @scanner.match?(WHITESPACE) if outline
454
+ attribute =
455
+ if @scanner.skip(SPLAT)
456
+ Splat.new(parse_ruby_code(closing))
457
+ elsif @scanner.skip(QUOTED_ATTRIBUTE)
458
+ name = @scanner[1]
459
+ escape = @scanner[2].empty?
460
+ QuotedAttribute.new(name, escape, parse_quoted_attribute(@scanner[3]))
461
+ elsif @scanner.skip(CODE_ATTRIBUTE)
462
+ name = @scanner[1]
463
+ escape = @scanner[2].empty?
464
+ code = parse_ruby_code(closing)
465
+ syntax_error!("Invalid empty attribute") if code.empty?
466
+ CodeAttribute.new(name, escape, code)
467
+ elsif closing && @scanner.skip(BOOLEAN_ATTRIBUTE_BY_CLOSING[closing])
468
+ BooleanAttribute.new(@scanner[1])
469
+ end
470
+ if attribute
471
+ attributes << attribute
472
+ spans << [first, column] if outline
473
+ next
474
+ end
475
+ break unless closing
476
+
477
+ if @scanner.skip(ATTRIBUTE_LIST_END_BY_CLOSING[closing])
478
+ break
479
+ else
480
+ @scanner.skip(WHITESPACE)
481
+ syntax_error!("Expected attribute") unless @scanner.eos?
482
+ @stacks.last << NEWLINE
483
+ syntax_error!("Expected closing delimiter #{closing}") if @lines.empty?
484
+ next_line
485
+ end
486
+ end
487
+ if outline && lineno == @lineno && !spans.empty?
488
+ list_first = spans.first.first unless closing
489
+ (@outline[lineno].attribute_lists ||= []) << [list_first, column, spans]
490
+ end
491
+ attributes
492
+ end
493
+
494
+ def parse_ruby_code(outer_closing)
495
+ first = source_offset if @outline
496
+ code = +""
497
+ depth = 0
498
+ opening = closing = nil
499
+ code_end = CODE_END_BY_CLOSING[outer_closing]
500
+
501
+ until @scanner.eos? || (depth == 0 && @scanner.match?(code_end))
502
+ if @scanner.match?(LINE_CONTINUATION)
503
+ code << @scanner.getch << "\n"
504
+ expect_next_line
505
+ elsif (chunk = @scanner.scan((depth == 0) ? CODE_CHUNK_BY_CLOSING[outer_closing] : NESTED_CODE_CHUNK_BY_OPENING[opening]))
506
+ code << chunk
507
+ else
508
+ character = @scanner.getch
509
+ if depth > 0
510
+ depth += (character == opening) ? 1 : -1
511
+ else
512
+ depth = 1
513
+ opening = character
514
+ closing = DELIMITERS[opening]
515
+ end
516
+ code << character
517
+ end
518
+ end
519
+ syntax_error!("Expected closing delimiter #{closing}") if depth != 0
520
+ record_ruby(:code, first, source_offset) if @outline
521
+ code
522
+ end
523
+
524
+ def parse_quoted_attribute(quote)
525
+ first = source_offset if @outline
526
+ value = +""
527
+ depth = 0
528
+ quote_byte = quote.ord
529
+
530
+ until depth == 0 && @scanner.peek_byte == quote_byte
531
+ if @scanner.eos? || @scanner.match?(BACKSLASH_AT_END)
532
+ value << (@scanner.eos? ? "\n" : " ")
533
+ @stacks.last << NEWLINE
534
+ expect_next_line
535
+ elsif (chunk = @scanner.scan(QUOTED_CHUNK_BY_QUOTE[quote]))
536
+ value << chunk
537
+ else
538
+ byte = @scanner.peek_byte
539
+ depth += 1 if byte == OPENING_BRACE
540
+ depth -= 1 if byte == CLOSING_BRACE
541
+ value << @scanner.getch
542
+ end
543
+ end
544
+
545
+ record_ruby(:text, first, source_offset) if @outline
546
+ @scanner.pos += 1
547
+ value
548
+ end
549
+
550
+ def syntax_error!(message)
551
+ error = SyntaxError.new(message, @file, @line, @lineno, (@line && @scanner) ? column : 0)
552
+ error.set_backtrace(["#{@file}:#{@lineno}", *caller])
553
+ raise error
554
+ end
555
+ end
556
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperSlim
4
+ class RailsHandler
5
+ def call(template, source = nil)
6
+ SuperSlim.compile(
7
+ source || template.source,
8
+ file: template.identifier,
9
+ format: xml?(template) ? :xml : :html,
10
+ rails: true,
11
+ **annotations(template)
12
+ )
13
+ end
14
+
15
+ def supports_streaming?
16
+ true
17
+ end
18
+
19
+ private
20
+
21
+ def xml?(template)
22
+ template.format && ::Mime[template.format].to_s.end_with?("xml")
23
+ end
24
+
25
+ def annotations(template)
26
+ return {} unless ActionView::Base.annotate_rendered_view_with_filenames && template.format == :html
27
+
28
+ {preamble: "<!-- BEGIN #{template.short_identifier} -->\n", postamble: "<!-- END #{template.short_identifier} -->\n"}
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperSlim
4
+ class Railtie < Rails::Railtie
5
+ initializer "super_slim.register_template_handler" do
6
+ ActiveSupport.on_load(:action_view) do
7
+ require_relative "rails_handler"
8
+ ActionView::Template.register_template_handler(:slim, SuperSlim::RailsHandler.new)
9
+ end
10
+ end
11
+ end
12
+ end