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