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.
@@ -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
- class Line
40
- attr_accessor :content, :line_number
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
- 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
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
- add_end_block_when_no_end_block_on_prev_line(arr: tmp)
207
- while @nested_level > 0 || !@buffer.eof?
208
- spaces = calculate_spaces_to_add(line)
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
- tmp.pop if tmp[-1] == END_BLOCK_CHAR
230
- tmp.join("\n")
153
+ @output << Line.new(line, @buffer.lineno) if @output_current_line
154
+ @output_current_line = true
231
155
  end
232
156
 
233
- private
157
+ to_s
158
+ end
159
+
160
+ def to_s
161
+ return "" if @output.size <= 1
234
162
 
235
- def remove_last_block_name_entry
236
- @nested_level -= 1
237
- @block_names.pop
238
- end
163
+ @output.map(&:to_s)
164
+ end
239
165
 
240
- def add_selector_to_block_name(line)
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
- def add_end_block_when_no_end_block_on_prev_line(arr: @output)
247
- unless arr[-1]&.strip&.end_with?("}")
248
- arr << "}"
249
- end
250
- end
168
+ def extract_mixin_name(line)
169
+ line.tr("{", "").tr(".", "").tr(":", "").strip
170
+ end
251
171
 
252
- def calculate_spaces_to_add(line)
253
- unless line =~ OPENING_SELECTOR_PATTERN || line =~ CLOSING_SELECTOR_PATTERN
254
- " "
255
- else
256
- ""
257
- end
258
- end
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
- def line_without_nested_block(line)
265
- line.split(NEST_SELECTOR).last.strip
266
- end
267
-
268
- def rewrite_line(spaces, line, selector)
269
- case number_of_selectors_in(line)
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
- line = output[lineno]
299
-
300
- if line.to_s =~ OPENING_SELECTOR_PATTERN
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
- while ! buffer.eof?
310
- process_line(buffer.readline)
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
- @output << NEWLINE
202
+ Parser.import_path_cache << import_path
314
203
 
315
- @output.join
204
+ Parser.new(File.read(import_path), filename: import_path).parse
316
205
  end
206
+ end
317
207
 
318
- private
319
-
320
- # this method returns void, and modifies the output array directly
321
- def process_line(line)
322
- if line.strip.start_with?("/*")
323
- handle_comment(line)
324
- elsif line.strip.start_with?("@import")
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 keep_comments?
338
- Deadfire.configuration.keep_comments
339
- end
217
+ def parse
218
+ raise Deadfire::EarlyApplyException.new(@content, @lineno) if Parser.cached_mixins.empty?
340
219
 
341
- def handle_comment(line)
342
- @output << Line.new(line, buffer.lineno) if keep_comments?
220
+ space_counter = calculate_number_of_spaces
221
+ ends_with_end_block_char = false
343
222
 
344
- while ! line.include?("*/") && ! buffer.eof?
345
- line = buffer.gets
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
- def handle_import(line)
351
- 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
352
231
 
353
- if self.class.import_path_cache.include?(import.import_path)
354
- 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
355
235
  end
356
-
357
- self.class.import_path_cache << import.import_path
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
- def handle_apply(line)
366
- @apply = Apply.new(line, buffer.lineno)
367
- @output << @apply.parse.join(NEWLINE)
368
- end
240
+ private
369
241
 
370
- def handle_mixins(line)
371
- @root = Root.new(line, buffer.lineno, buffer)
372
- @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
373
249
  end
374
250
 
375
- def handle_nestings(line)
376
- nesting = Nesting.new(line, buffer.lineno, buffer, @output)
377
- @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]
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