deadfire 0.2.0 → 0.4.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 +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/Gemfile.lock +1 -1
- data/README.md +55 -32
- data/benchmarks/basic_benchmark.rb +47 -12
- data/benchmarks/tailwind_parser.rb +23 -0
- data/bin/console +16 -3
- data/changelog.md +38 -1
- data/lib/deadfire/ast_printer.rb +58 -0
- data/lib/deadfire/configuration.rb +18 -5
- data/lib/deadfire/css_generator.rb +65 -0
- data/lib/deadfire/error_reporter.rb +30 -0
- data/lib/deadfire/errors.rb +1 -14
- data/lib/deadfire/filename_helper.rb +49 -0
- data/lib/deadfire/front_end/apply_node.rb +43 -0
- data/lib/deadfire/front_end/at_rule_node.rb +19 -0
- data/lib/deadfire/front_end/base_node.rb +11 -0
- data/lib/deadfire/front_end/block_node.rb +21 -0
- data/lib/deadfire/front_end/comment_node.rb +17 -0
- data/lib/deadfire/front_end/newline_node.rb +17 -0
- data/lib/deadfire/front_end/parser.rb +156 -0
- data/lib/deadfire/front_end/ruleset_node.rb +18 -0
- data/lib/deadfire/front_end/scanner.rb +280 -0
- data/lib/deadfire/front_end/selector_node.rb +52 -0
- data/lib/deadfire/front_end/stylesheet_node.rb +21 -0
- data/lib/deadfire/front_end/token.rb +20 -0
- data/lib/deadfire/interpreter.rb +84 -0
- data/lib/deadfire/parser_engine.rb +41 -0
- data/lib/deadfire/spec.rb +135 -0
- data/lib/deadfire/version.rb +1 -1
- data/lib/deadfire.rb +34 -7
- metadata +23 -6
- data/lib/deadfire/css_buffer.rb +0 -37
- data/lib/deadfire/parser.rb +0 -387
- data/lib/deadfire/transformers/transformer.rb +0 -17
data/lib/deadfire/parser.rb
DELETED
@@ -1,387 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
module Deadfire
|
3
|
-
class Parser
|
4
|
-
singleton_class.attr_accessor :cached_mixins
|
5
|
-
self.cached_mixins = Hash.new { |h, k| h[k] = {} }
|
6
|
-
|
7
|
-
singleton_class.attr_accessor :import_path_cache
|
8
|
-
self.import_path_cache = []
|
9
|
-
|
10
|
-
ROOT_SELECTOR = ":root {"
|
11
|
-
OPENING_SELECTOR_PATTERN = /\s*\{/
|
12
|
-
CLOSING_SELECTOR_PATTERN = /\s*\}/
|
13
|
-
NEST_SELECTOR = "&"
|
14
|
-
START_BLOCK_CHAR = "{"
|
15
|
-
END_BLOCK_CHAR = "}"
|
16
|
-
OPENING_SELECTOR_PATTERN_OTHER = /\..*\{/
|
17
|
-
IMPORT_SELECTOR = "@import"
|
18
|
-
CSS_FILE_EXTENSION = ".css"
|
19
|
-
APPLY_SELECTOR = "@apply"
|
20
|
-
NEWLINE = "\n"
|
21
|
-
|
22
|
-
def self.parse(content, options = {})
|
23
|
-
new(content, options).parse
|
24
|
-
end
|
25
|
-
|
26
|
-
attr_reader :output
|
27
|
-
|
28
|
-
def initialize(content, options = {})
|
29
|
-
@content = content
|
30
|
-
@filename = options[:filename]
|
31
|
-
@output = []
|
32
|
-
@imports = []
|
33
|
-
end
|
34
|
-
|
35
|
-
def buffer
|
36
|
-
@buffer ||= CssBuffer.new(@content)
|
37
|
-
end
|
38
|
-
|
39
|
-
class Line
|
40
|
-
attr_accessor :content, :line_number
|
41
|
-
|
42
|
-
def initialize(content, line_number)
|
43
|
-
@content = content
|
44
|
-
@line_number = line_number
|
45
|
-
end
|
46
|
-
|
47
|
-
def to_s
|
48
|
-
content
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
class Root < Line
|
53
|
-
def initialize(content, lineno, buffer)
|
54
|
-
super(content, lineno)
|
55
|
-
@end_tag = false
|
56
|
-
@output_current_line = true
|
57
|
-
@output = []
|
58
|
-
@buffer = buffer
|
59
|
-
end
|
60
|
-
|
61
|
-
def parse
|
62
|
-
line = @content
|
63
|
-
if line.include? ROOT_SELECTOR
|
64
|
-
@output << Line.new(line, @line_number)
|
65
|
-
end
|
66
|
-
|
67
|
-
while !@end_tag && line = @buffer.gets
|
68
|
-
if line =~ OPENING_SELECTOR_PATTERN
|
69
|
-
@output_current_line = false
|
70
|
-
name = extract_mixin_name(line)
|
71
|
-
properties = extract_properties_from_mixin(@buffer, line)
|
72
|
-
Parser.cached_mixins[name] = properties
|
73
|
-
elsif line =~ CLOSING_SELECTOR_PATTERN
|
74
|
-
@end_tag = true
|
75
|
-
end
|
76
|
-
|
77
|
-
@output << Line.new(line, @buffer.lineno) if @output_current_line
|
78
|
-
@output_current_line = true
|
79
|
-
end
|
80
|
-
|
81
|
-
to_s
|
82
|
-
end
|
83
|
-
|
84
|
-
def to_s
|
85
|
-
return "" if @output.size <= 1
|
86
|
-
|
87
|
-
@output.map(&:to_s)
|
88
|
-
end
|
89
|
-
|
90
|
-
private
|
91
|
-
|
92
|
-
def extract_mixin_name(line)
|
93
|
-
line.tr("{", "").tr(".", "").tr(":", "").strip
|
94
|
-
end
|
95
|
-
|
96
|
-
def extract_properties_from_mixin(buffer, line)
|
97
|
-
properties = {}
|
98
|
-
line = buffer.gets # skip opening {
|
99
|
-
while line !~ CLOSING_SELECTOR_PATTERN && !buffer.eof?
|
100
|
-
name, value = extract_name_and_values(line)
|
101
|
-
properties[name] = value
|
102
|
-
line = buffer.gets
|
103
|
-
end
|
104
|
-
properties
|
105
|
-
end
|
106
|
-
|
107
|
-
def extract_name_and_values(line)
|
108
|
-
name, value = line.split(":")
|
109
|
-
value = value.gsub(";", "")
|
110
|
-
[name, value].map(&:strip)
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
class Import < Line
|
115
|
-
attr_accessor :import_path
|
116
|
-
|
117
|
-
def initialize(content, lineno)
|
118
|
-
super
|
119
|
-
@import_path = self.class.resolve_import_path(content, lineno)
|
120
|
-
end
|
121
|
-
|
122
|
-
def parse
|
123
|
-
Parser.new(File.read(import_path), filename: import_path).parse
|
124
|
-
end
|
125
|
-
|
126
|
-
class << self
|
127
|
-
def resolve_import_path(line, lineno = 0)
|
128
|
-
path = normalize_import_path(line)
|
129
|
-
unless path.end_with?(Parser::CSS_FILE_EXTENSION)
|
130
|
-
path += Parser::CSS_FILE_EXTENSION
|
131
|
-
end
|
132
|
-
import_path = File.join(Deadfire.configuration.root_path, path)
|
133
|
-
|
134
|
-
unless File.exist?(import_path)
|
135
|
-
raise Deadfire::ImportException.new(import_path, lineno)
|
136
|
-
end
|
137
|
-
|
138
|
-
import_path
|
139
|
-
end
|
140
|
-
|
141
|
-
def normalize_import_path(line)
|
142
|
-
path = line.split.last
|
143
|
-
path.gsub!("\"", "")
|
144
|
-
path.gsub!("\'", "")
|
145
|
-
path.gsub!(";", "")
|
146
|
-
path
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
class Apply < Line
|
152
|
-
def initialize(...)
|
153
|
-
super
|
154
|
-
@current_line = @content.dup
|
155
|
-
@space = " "
|
156
|
-
@space_counter = 0
|
157
|
-
@import_start_tag = "@"
|
158
|
-
@output = []
|
159
|
-
end
|
160
|
-
|
161
|
-
def parse
|
162
|
-
raise Deadfire::EarlyApplyException.new(@content, @lineno) if Parser.cached_mixins.empty?
|
163
|
-
|
164
|
-
@current_line.each_char do |char|
|
165
|
-
break if char == @import_start_tag
|
166
|
-
@space_counter += 1
|
167
|
-
end
|
168
|
-
|
169
|
-
@current_line.split(" ").each do |css|
|
170
|
-
next if css.include?(APPLY_SELECTOR)
|
171
|
-
css.gsub!(";", "")
|
172
|
-
|
173
|
-
fetch_cached_mixin(css).each_pair do |key, value|
|
174
|
-
@output << "#{@space * @space_counter}#{key}: #{value};"
|
175
|
-
end
|
176
|
-
end
|
177
|
-
@output
|
178
|
-
end
|
179
|
-
|
180
|
-
private
|
181
|
-
|
182
|
-
# find css class key/val from hash, otherwise throw because the mixin is not defined
|
183
|
-
def fetch_cached_mixin(key)
|
184
|
-
raise Deadfire::EarlyApplyException.new(key, @lineno) unless Parser.cached_mixins.include?(key)
|
185
|
-
|
186
|
-
Parser.cached_mixins[key]
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
class Nesting < Line
|
191
|
-
attr_accessor :block_names
|
192
|
-
|
193
|
-
def initialize(content, lineno, buffer, output)
|
194
|
-
super(content, lineno)
|
195
|
-
@buffer = buffer
|
196
|
-
@output = output
|
197
|
-
@block_names = []
|
198
|
-
@nested_level = 0
|
199
|
-
end
|
200
|
-
|
201
|
-
def parse
|
202
|
-
line = content.dup.strip
|
203
|
-
@block_names << find_block_name(@output, @lineno)
|
204
|
-
tmp = []
|
205
|
-
|
206
|
-
while @nested_level > 0 || !@buffer.eof?
|
207
|
-
spaces = calculate_spaces_to_add(line)
|
208
|
-
|
209
|
-
if line.start_with?(NEST_SELECTOR)
|
210
|
-
add_end_block_when_no_end_block_on_prev_line(arr: tmp)
|
211
|
-
add_selector_to_block_name(line)
|
212
|
-
@nested_level += 1
|
213
|
-
tmp << rewrite_line(spaces, line, @block_names[0...-1].join(" "))
|
214
|
-
else
|
215
|
-
tmp << "#{spaces}#{line.lstrip}"
|
216
|
-
end
|
217
|
-
|
218
|
-
remove_last_block_name_entry if line.end_with?(END_BLOCK_CHAR)
|
219
|
-
|
220
|
-
if line.end_with?(END_BLOCK_CHAR)
|
221
|
-
result = @buffer.peek
|
222
|
-
if result.strip == END_BLOCK_CHAR
|
223
|
-
@buffer.gets(skip_buffer: true)
|
224
|
-
break
|
225
|
-
end
|
226
|
-
end
|
227
|
-
|
228
|
-
line = @buffer.gets
|
229
|
-
|
230
|
-
if line.nil? || @buffer.eof? || line.empty?
|
231
|
-
break
|
232
|
-
else
|
233
|
-
line.strip!
|
234
|
-
end
|
235
|
-
end
|
236
|
-
|
237
|
-
tmp.join("\n").concat("\n")
|
238
|
-
end
|
239
|
-
|
240
|
-
private
|
241
|
-
|
242
|
-
def remove_last_block_name_entry
|
243
|
-
@nested_level -= 1
|
244
|
-
@block_names.pop
|
245
|
-
end
|
246
|
-
|
247
|
-
def add_selector_to_block_name(line)
|
248
|
-
line = extract_selector(line)
|
249
|
-
line = line_without_nested_block(line)
|
250
|
-
@block_names << line unless @block_names.include?(line)
|
251
|
-
end
|
252
|
-
|
253
|
-
def add_end_block_when_no_end_block_on_prev_line(arr: @output)
|
254
|
-
unless arr[-1]&.strip&.end_with?("}")
|
255
|
-
arr << "}"
|
256
|
-
end
|
257
|
-
end
|
258
|
-
|
259
|
-
def calculate_spaces_to_add(line)
|
260
|
-
unless line =~ OPENING_SELECTOR_PATTERN || line =~ CLOSING_SELECTOR_PATTERN
|
261
|
-
" "
|
262
|
-
else
|
263
|
-
""
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
def extract_selector(line)
|
268
|
-
line.split(START_BLOCK_CHAR).first.strip
|
269
|
-
end
|
270
|
-
|
271
|
-
def line_without_nested_block(line)
|
272
|
-
line.split(NEST_SELECTOR).last.strip
|
273
|
-
end
|
274
|
-
|
275
|
-
def rewrite_line(spaces, line, selector)
|
276
|
-
case number_of_selectors_in(line)
|
277
|
-
when 0
|
278
|
-
line
|
279
|
-
when 1
|
280
|
-
"#{spaces}#{line.strip.gsub("&", selector)}"
|
281
|
-
else
|
282
|
-
line.strip.each_char.map do |s|
|
283
|
-
if s == NEST_SELECTOR
|
284
|
-
selector
|
285
|
-
else
|
286
|
-
s
|
287
|
-
end
|
288
|
-
end.join
|
289
|
-
end
|
290
|
-
end
|
291
|
-
|
292
|
-
def number_of_selectors_in(line)
|
293
|
-
line.split.count do |s|
|
294
|
-
# break if s == "{" # early exit, no need to read every char
|
295
|
-
s.start_with?(NEST_SELECTOR)
|
296
|
-
end
|
297
|
-
end
|
298
|
-
|
299
|
-
def find_block_name(output, lineno = nil)
|
300
|
-
lineno = output.size unless lineno
|
301
|
-
if lineno < 0
|
302
|
-
raise "Cannot find block name"
|
303
|
-
end
|
304
|
-
|
305
|
-
line = output[lineno]
|
306
|
-
|
307
|
-
if line.to_s =~ OPENING_SELECTOR_PATTERN
|
308
|
-
extract_selector(line)
|
309
|
-
else
|
310
|
-
find_block_name(output, lineno - 1)
|
311
|
-
end
|
312
|
-
end
|
313
|
-
end
|
314
|
-
|
315
|
-
def parse
|
316
|
-
while ! buffer.eof?
|
317
|
-
process_line(buffer.gets)
|
318
|
-
end
|
319
|
-
|
320
|
-
@output << NEWLINE
|
321
|
-
|
322
|
-
@output.join
|
323
|
-
end
|
324
|
-
|
325
|
-
private
|
326
|
-
|
327
|
-
# this method returns void, and modifies the output array directly
|
328
|
-
def process_line(line)
|
329
|
-
if line.strip.start_with?("/*")
|
330
|
-
handle_comment(line)
|
331
|
-
elsif line.strip.start_with?("@import")
|
332
|
-
handle_import(line)
|
333
|
-
elsif line.strip.start_with?(":root {")
|
334
|
-
handle_mixins(line)
|
335
|
-
elsif line.strip.start_with?("@apply") # or line.include?("@apply")
|
336
|
-
handle_apply(line)
|
337
|
-
elsif line.strip.start_with?("&")
|
338
|
-
handle_nestings(line)
|
339
|
-
else
|
340
|
-
@output << line
|
341
|
-
end
|
342
|
-
end
|
343
|
-
|
344
|
-
def keep_comments?
|
345
|
-
Deadfire.configuration.keep_comments
|
346
|
-
end
|
347
|
-
|
348
|
-
def handle_comment(line)
|
349
|
-
@output << Line.new(line, buffer.lineno) if keep_comments?
|
350
|
-
|
351
|
-
while ! line.include?("*/") && ! buffer.eof?
|
352
|
-
line = buffer.gets
|
353
|
-
@output << Line.new(line, buffer.lineno) if keep_comments?
|
354
|
-
end
|
355
|
-
end
|
356
|
-
|
357
|
-
def handle_import(line)
|
358
|
-
import = Import.new(line, buffer.lineno)
|
359
|
-
|
360
|
-
if self.class.import_path_cache.include?(import.import_path)
|
361
|
-
raise DuplicateImportException.new(import.import_path, buffer.lineno)
|
362
|
-
end
|
363
|
-
|
364
|
-
self.class.import_path_cache << import.import_path
|
365
|
-
|
366
|
-
# TODO:
|
367
|
-
# - decide on how many levels of imports we want to allow
|
368
|
-
# - make async??
|
369
|
-
@output << import.parse
|
370
|
-
end
|
371
|
-
|
372
|
-
def handle_apply(line)
|
373
|
-
@apply = Apply.new(line, buffer.lineno)
|
374
|
-
@output << @apply.parse.join(NEWLINE)
|
375
|
-
end
|
376
|
-
|
377
|
-
def handle_mixins(line)
|
378
|
-
@root = Root.new(line, buffer.lineno, buffer)
|
379
|
-
@output << @root.parse
|
380
|
-
end
|
381
|
-
|
382
|
-
def handle_nestings(line)
|
383
|
-
nesting = Nesting.new(line, buffer.lineno, buffer, @output)
|
384
|
-
@output << nesting.parse
|
385
|
-
end
|
386
|
-
end
|
387
|
-
end
|