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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +118 -0
- data/LICENSE +21 -0
- data/LICENSE-slim +22 -0
- data/README.md +217 -0
- data/config/rubocop.yml +212 -0
- data/exe/super-slim +6 -0
- data/lib/super-slim.rb +4 -0
- data/lib/super_slim/ast.rb +34 -0
- data/lib/super_slim/cli.rb +71 -0
- data/lib/super_slim/compiler.rb +467 -0
- data/lib/super_slim/formatter/line_wrapper.rb +131 -0
- data/lib/super_slim/formatter.rb +125 -0
- data/lib/super_slim/linter/rules.rb +585 -0
- data/lib/super_slim/linter.rb +81 -0
- data/lib/super_slim/parallel.rb +36 -0
- data/lib/super_slim/parser.rb +556 -0
- data/lib/super_slim/rails_handler.rb +31 -0
- data/lib/super_slim/railtie.rb +12 -0
- data/lib/super_slim/rubocop.rb +128 -0
- data/lib/super_slim/runtime.rb +103 -0
- data/lib/super_slim/shortcuts.rb +28 -0
- data/lib/super_slim/version.rb +5 -0
- data/lib/super_slim.rb +53 -0
- metadata +86 -0
|
@@ -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
|