deadfire 0.1.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 +3 -3
- 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_buffer.rb +30 -3
- 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 -282
- 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,368 +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
|
43
|
+
def parse
|
44
|
+
while ! buffer.eof?
|
45
|
+
process_line(buffer.gets)
|
45
46
|
end
|
46
47
|
|
47
|
-
|
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
|
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) if @nested_level > 0
|
211
|
-
add_selector_to_block_name(line)
|
212
|
-
@nested_level += 1
|
213
|
-
tmp << rewrite_line(spaces, line, @block_names[0...-1].join(" "))
|
214
|
-
remove_last_block_name_entry if line.end_with?(END_BLOCK_CHAR)
|
215
|
-
else
|
216
|
-
remove_last_block_name_entry if line.end_with?(END_BLOCK_CHAR)
|
217
|
-
tmp << "#{spaces}#{line.lstrip}"
|
218
|
-
end
|
219
|
-
|
220
|
-
line = @buffer.gets
|
221
|
-
|
222
|
-
if line.nil? || @buffer.eof? || line.empty?
|
223
|
-
break
|
224
|
-
else
|
225
|
-
line.strip!
|
226
|
-
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
|
227
151
|
end
|
228
152
|
|
229
|
-
|
230
|
-
|
153
|
+
@output << Line.new(line, @buffer.lineno) if @output_current_line
|
154
|
+
@output_current_line = true
|
231
155
|
end
|
232
156
|
|
233
|
-
|
157
|
+
to_s
|
158
|
+
end
|
159
|
+
|
160
|
+
def to_s
|
161
|
+
return "" if @output.size <= 1
|
234
162
|
|
235
|
-
|
236
|
-
|
237
|
-
@block_names.pop
|
238
|
-
end
|
163
|
+
@output.map(&:to_s)
|
164
|
+
end
|
239
165
|
|
240
|
-
|
241
|
-
line = extract_selector(line)
|
242
|
-
line = line_without_nested_block(line)
|
243
|
-
@block_names << line unless @block_names.include?(line)
|
244
|
-
end
|
166
|
+
private
|
245
167
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
end
|
250
|
-
end
|
168
|
+
def extract_mixin_name(line)
|
169
|
+
line.tr("{", "").tr(".", "").tr(":", "").strip
|
170
|
+
end
|
251
171
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
def extract_selector(line)
|
261
|
-
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
|
262
179
|
end
|
180
|
+
properties
|
181
|
+
end
|
263
182
|
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
when 0
|
271
|
-
line
|
272
|
-
when 1
|
273
|
-
"#{spaces}#{line.lstrip.gsub("&", selector)}"
|
274
|
-
else
|
275
|
-
line.strip.each_char.map do |s|
|
276
|
-
if s == NEST_SELECTOR
|
277
|
-
selector
|
278
|
-
else
|
279
|
-
s
|
280
|
-
end
|
281
|
-
end.join
|
282
|
-
end
|
283
|
-
end
|
284
|
-
|
285
|
-
def number_of_selectors_in(line)
|
286
|
-
line.split.count do |s|
|
287
|
-
# break if s == "{" # early exit, no need to read every char
|
288
|
-
s.start_with?(NEST_SELECTOR)
|
289
|
-
end
|
290
|
-
end
|
291
|
-
|
292
|
-
def find_block_name(output, lineno = nil)
|
293
|
-
lineno = output.size unless lineno
|
294
|
-
if lineno < 0
|
295
|
-
raise "Cannot find block name"
|
296
|
-
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
|
297
189
|
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
extract_selector(line)
|
302
|
-
else
|
303
|
-
find_block_name(output, lineno - 1)
|
304
|
-
end
|
305
|
-
end
|
190
|
+
class Import < Line
|
191
|
+
def initialize(...)
|
192
|
+
super(...)
|
306
193
|
end
|
307
194
|
|
308
195
|
def parse
|
309
|
-
|
310
|
-
|
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)
|
311
200
|
end
|
312
201
|
|
313
|
-
|
202
|
+
Parser.import_path_cache << import_path
|
314
203
|
|
315
|
-
|
204
|
+
Parser.new(File.read(import_path), filename: import_path).parse
|
316
205
|
end
|
206
|
+
end
|
317
207
|
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
handle_import(line)
|
326
|
-
elsif line.strip.start_with?(":root {")
|
327
|
-
handle_mixins(line)
|
328
|
-
elsif line.strip.start_with?("@apply") # or line.include?("@apply")
|
329
|
-
handle_apply(line)
|
330
|
-
elsif line.strip.start_with?("&")
|
331
|
-
handle_nestings(line)
|
332
|
-
else
|
333
|
-
@output << line
|
334
|
-
end
|
208
|
+
class Apply < Line
|
209
|
+
def initialize(...)
|
210
|
+
super
|
211
|
+
@current_line = @content.dup
|
212
|
+
@space = " "
|
213
|
+
@apply_start_char = "@"
|
214
|
+
@output = []
|
335
215
|
end
|
336
216
|
|
337
|
-
def
|
338
|
-
Deadfire.
|
339
|
-
end
|
217
|
+
def parse
|
218
|
+
raise Deadfire::EarlyApplyException.new(@content, @lineno) if Parser.cached_mixins.empty?
|
340
219
|
|
341
|
-
|
342
|
-
|
220
|
+
space_counter = calculate_number_of_spaces
|
221
|
+
ends_with_end_block_char = false
|
343
222
|
|
344
|
-
|
345
|
-
|
346
|
-
@output << Line.new(line, buffer.lineno) if keep_comments?
|
347
|
-
end
|
348
|
-
end
|
223
|
+
@current_line.split(" ").each do |css|
|
224
|
+
next if css.include?(Parser::APPLY_SELECTOR)
|
349
225
|
|
350
|
-
|
351
|
-
|
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
|
352
231
|
|
353
|
-
|
354
|
-
|
232
|
+
fetch_cached_mixin(css).each_pair do |key, value|
|
233
|
+
@output << "#{@space * space_counter}#{key}: #{value};"
|
234
|
+
end
|
355
235
|
end
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
# TODO:
|
360
|
-
# - decide on how many levels of imports we want to allow
|
361
|
-
# - make async??
|
362
|
-
@output << import.parse
|
236
|
+
@output << "#{Parser::END_BLOCK_CHAR}" if ends_with_end_block_char
|
237
|
+
@output
|
363
238
|
end
|
364
239
|
|
365
|
-
|
366
|
-
@apply = Apply.new(line, buffer.lineno)
|
367
|
-
@output << @apply.parse.join(NEWLINE)
|
368
|
-
end
|
240
|
+
private
|
369
241
|
|
370
|
-
def
|
371
|
-
|
372
|
-
@
|
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
|
373
249
|
end
|
374
250
|
|
375
|
-
|
376
|
-
|
377
|
-
@
|
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]
|
378
256
|
end
|
379
257
|
end
|
380
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
|