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.
@@ -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
- 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
43
+ def parse
44
+ while ! buffer.eof?
45
+ process_line(buffer.gets)
49
46
  end
50
- end
51
47
 
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
- def parse
62
- line = @content
63
- if line.include? ROOT_SELECTOR
64
- @output << Line.new(line, @line_number)
65
- end
50
+ @output.join
51
+ end
66
52
 
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
53
+ def errors?
54
+ @errors_list.errors.any?
55
+ end
80
56
 
81
- to_s
82
- end
57
+ private
83
58
 
84
- def to_s
85
- return "" if @output.size <= 1
59
+ def preprocess
60
+ @content.gsub(/\r\n?|\f/, "\n").gsub("\u{0000}", "\u{FFFD}")
61
+ end
86
62
 
87
- @output.map(&:to_s)
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
- 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
78
+ def keep_comments?
79
+ Deadfire.configuration.keep_comments
112
80
  end
113
81
 
114
- class Import < Line
115
- attr_accessor :import_path
82
+ def handle_comment(line)
83
+ @output << Line.new(line, buffer.lineno) if keep_comments?
116
84
 
117
- def initialize(content, lineno)
118
- super
119
- @import_path = self.class.resolve_import_path(content, lineno)
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
- def parse
123
- Parser.new(File.read(import_path), filename: import_path).parse
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
- 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
95
+ def handle_import(line)
96
+ import = Import.new(line, buffer.lineno)
137
97
 
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
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
- 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
104
+ def handle_apply(line)
105
+ @apply = Apply.new(line, buffer.lineno)
106
+ @output << @apply.parse.join(NEWLINE)
107
+ end
160
108
 
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
109
+ def handle_mixins(line)
110
+ @root = Root.new(line, buffer.lineno, buffer)
111
+ @output << @root.parse
112
+ end
113
+ end
179
114
 
180
- private
115
+ class Line
116
+ attr_accessor :content, :line_number
181
117
 
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)
118
+ def initialize(content, line_number)
119
+ @content = content
120
+ @line_number = line_number
121
+ end
185
122
 
186
- Parser.cached_mixins[key]
187
- end
123
+ def to_s
124
+ content
188
125
  end
126
+ end
189
127
 
190
- class Nesting < Line
191
- attr_accessor :block_names
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
- def initialize(content, lineno, buffer, output)
194
- super(content, lineno)
195
- @buffer = buffer
196
- @output = 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
- 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
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
- tmp.join("\n").concat("\n")
153
+ @output << Line.new(line, @buffer.lineno) if @output_current_line
154
+ @output_current_line = true
238
155
  end
239
156
 
240
- private
157
+ to_s
158
+ end
159
+
160
+ def to_s
161
+ return "" if @output.size <= 1
241
162
 
242
- def remove_last_block_name_entry
243
- @nested_level -= 1
244
- @block_names.pop
245
- end
163
+ @output.map(&:to_s)
164
+ end
246
165
 
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
166
+ private
252
167
 
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
168
+ def extract_mixin_name(line)
169
+ line.tr("{", "").tr(".", "").tr(":", "").strip
170
+ end
258
171
 
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
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
- 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
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
- 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
190
+ class Import < Line
191
+ def initialize(...)
192
+ super(...)
313
193
  end
314
194
 
315
195
  def parse
316
- while ! buffer.eof?
317
- process_line(buffer.gets)
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
- @output << NEWLINE
202
+ Parser.import_path_cache << import_path
321
203
 
322
- @output.join
204
+ Parser.new(File.read(import_path), filename: import_path).parse
323
205
  end
206
+ end
324
207
 
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
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 keep_comments?
345
- Deadfire.configuration.keep_comments
346
- end
217
+ def parse
218
+ raise Deadfire::EarlyApplyException.new(@content, @lineno) if Parser.cached_mixins.empty?
347
219
 
348
- def handle_comment(line)
349
- @output << Line.new(line, buffer.lineno) if keep_comments?
220
+ space_counter = calculate_number_of_spaces
221
+ ends_with_end_block_char = false
350
222
 
351
- while ! line.include?("*/") && ! buffer.eof?
352
- line = buffer.gets
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
- def handle_import(line)
358
- import = Import.new(line, buffer.lineno)
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
- if self.class.import_path_cache.include?(import.import_path)
361
- raise DuplicateImportException.new(import.import_path, buffer.lineno)
232
+ fetch_cached_mixin(css).each_pair do |key, value|
233
+ @output << "#{@space * space_counter}#{key}: #{value};"
234
+ end
362
235
  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
236
+ @output << "#{Parser::END_BLOCK_CHAR}" if ends_with_end_block_char
237
+ @output
370
238
  end
371
239
 
372
- def handle_apply(line)
373
- @apply = Apply.new(line, buffer.lineno)
374
- @output << @apply.parse.join(NEWLINE)
375
- end
240
+ private
376
241
 
377
- def handle_mixins(line)
378
- @root = Root.new(line, buffer.lineno, buffer)
379
- @output << @root.parse
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
- def handle_nestings(line)
383
- nesting = Nesting.new(line, buffer.lineno, buffer, @output)
384
- @output << nesting.parse
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