deadfire 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 570d2b607521baef50439b60e78f17aafcc9a6a53a258c2248967c6308964d27
4
- data.tar.gz: ad3dc3a1ff0b925408cf64d73f41d659f7de3fbd2014d6d73ef55829d6246e68
3
+ metadata.gz: 82f838eee7d8e2ef83b302d7de509c428af8584567892296fd3011df42cb0990
4
+ data.tar.gz: 1a4b828032eda5dae38d3da5f820081402f7e66f919b2720ff8f9be65d6d8bf0
5
5
  SHA512:
6
- metadata.gz: d23aee619e6429b4e3dc964a833f46ba8cba5275aea228d3945a6a240308366e067563a1fea45310a13e69414280f3d831a0e7f298596172f1fd5da44ad67235
7
- data.tar.gz: 911f68ad428ed00b46a9062cd96af4ff50fb2459aa2fff1be796fac7651b77e88f0e405c2da8b30d38a8d21a01ae5d0c0ace7daa245deac032e6450747f08c76
6
+ metadata.gz: a724bdb7b939e5b4404ee5cee4266ad6c278ac5d4e7fc74f560884f4b30fc123c3492a52abd5028adbc9f579dd690beb26d0de2460e8cb3b8e482d44ce77e733
7
+ data.tar.gz: e353841bfb20e25ff0ff1556d555443546783a744cf96b2a2149862cf8dbebb31bb9c48f50d68f20355ea036c36e22ca4ac9f059413df500974f4888849673bd
@@ -6,7 +6,7 @@ jobs:
6
6
  strategy:
7
7
  fail-fast: false
8
8
  matrix:
9
- ruby: [head, 3.2, 3.1, 3.0, 2.7]
9
+ ruby: [head, 3.3, 3.2, 3.1, 3.0, 2.7]
10
10
  steps:
11
11
  - uses: actions/checkout@v3
12
12
  - name: Set up Ruby
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- deadfire (0.3.0)
4
+ deadfire (0.4.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -50,11 +50,11 @@ Output;
50
50
 
51
51
  ### @apply
52
52
 
53
- @apply inlines your classes into your custom css.
53
+ The `@apply` directive inlines your classes into your custom CSS, simplifying the process of applying existing styles to a new class.
54
54
 
55
55
  The CSS apply rule was [proposed to be included into CSS](https://tabatkins.github.io/specs/css-apply-rule/) however it was abandoned. Mixins simplify applying existing css to a new class.
56
56
 
57
- Let's see an example of how to declare mixins and use the @apply directive.
57
+ Let's take a look at an example of how to use the @apply directive. Note that all utility classes are automatically cached.
58
58
 
59
59
  ```CSS
60
60
  .font-bold: {
@@ -67,7 +67,7 @@ Let's see an example of how to declare mixins and use the @apply directive.
67
67
  }
68
68
  ```
69
69
 
70
- How can we use mixins? Using @apply...
70
+ Re-use the styles using @apply:
71
71
 
72
72
  ```CSS
73
73
  .btn-blue {
@@ -79,6 +79,31 @@ How can we use mixins? Using @apply...
79
79
  }
80
80
  ```
81
81
 
82
+ ### Nesting
83
+
84
+ The CSS nesting feature, as described in the CSS Nesting specification (https://drafts.csswg.org/css-nesting/), allows for more intuitive and concise styling by enabling the nesting of CSS rules within one another. This feature simplifies the structure of stylesheets and improves readability.
85
+
86
+ Now that nesting has been upstreamed to CSS, meaning it is now a part of the official CSS specification. As a result, Deafire will leverage the native CSS nesting feature instead of implementing this feature (which was the original goal of this project).
87
+
88
+ Example:
89
+
90
+ ```css
91
+ .foo {
92
+ color: green;
93
+ }
94
+ .foo .bar {
95
+ font-size: 1.4rem;
96
+ }
97
+
98
+ /* can be simplified to */
99
+ .foo {
100
+ color: green;
101
+ .bar {
102
+ font-size: 1.4rem;
103
+ }
104
+ }
105
+ ```
106
+
82
107
  ### Fault tolerant
83
108
 
84
109
  When Deadfire encounters an error, such as a missing mixin or other issues, it does not immediately raise an error that would halt the execution. Instead, it continues processing the CSS code and collects the encountered errors. These errors are then reported through the ErrorReporter class, allowing you to handle or display them as needed.
@@ -8,6 +8,7 @@ gemfile(true) do
8
8
  gem "sassc"
9
9
  gem "deadfire", github: "hahmed/deadfire", branch: "main"
10
10
  gem "syntax_tree-css"
11
+ gem "sass-embedded"
11
12
 
12
13
  gem "benchmark-ips"
13
14
  end
@@ -25,7 +26,7 @@ body {
25
26
  }
26
27
 
27
28
  h1 {
28
- font-size: 40px;
29
+ font-size: 40px;
29
30
  }
30
31
  /* Just
31
32
  a
@@ -59,21 +60,52 @@ code {
59
60
  }
60
61
  CSS
61
62
 
62
- def dartsass
63
- system "sass benchmarks/input.scss output.css", exception: true
64
- end
65
-
66
63
  Benchmark.ips do |x|
67
64
  x.config(:time => 5, :warmup => 2)
68
65
 
69
- x.report("dartsass") { dartsass }
66
+ # x.report("dartsass") { dartsass }
70
67
  x.report("deadfire") { Deadfire.parse(css) }
71
68
  x.report("sassc") { SassC::Engine.new(css).render }
72
69
  x.report("sytanx_tree") { SyntaxTree::CSS.parse(css) }
70
+ x.report("dart sass") { Sass.compile_string(css) }
73
71
  x.compare!
74
72
  end
75
73
 
76
- # FYI
74
+ # May 2024: Re-added dart sass
75
+ # Warming up --------------------------------------
76
+ # deadfire 172.000 i/100ms
77
+ # sassc 85.000 i/100ms
78
+ # sytanx_tree 79.000 i/100ms
79
+ # dart sass 520.000 i/100ms
80
+ # Calculating -------------------------------------
81
+ # deadfire 1.680k (± 0.9%) i/s - 8.428k in 5.018094s
82
+ # sassc 816.292 (± 0.2%) i/s - 4.165k in 5.102378s
83
+ # sytanx_tree 750.421 (± 1.9%) i/s - 3.792k in 5.054908s
84
+ # dart sass 5.225k (± 4.7%) i/s - 26.520k in 5.090927s
85
+
86
+ # Comparison:
87
+ # dart sass: 5224.7 i/s
88
+ # deadfire: 1679.7 i/s - 3.11x slower
89
+ # sassc: 816.3 i/s - 6.40x slower
90
+ # sytanx_tree: 750.4 i/s - 6.96x slower
91
+
92
+ # Nov 2023: (Note: removed dart sass because I don't have it installed, need to re-run again)
93
+ # Warming up --------------------------------------
94
+ # deadfire 116.000 i/100ms
95
+ # sassc 69.000 i/100ms
96
+ # sytanx_tree 64.000 i/100ms
97
+ # Calculating -------------------------------------
98
+ # deadfire 1.164k (± 1.2%) i/s - 5.916k in 5.084777s
99
+ # sassc 695.721 (± 1.3%) i/s - 3.519k in 5.059025s
100
+ # sytanx_tree 635.684 (± 3.3%) i/s - 3.200k in 5.040489s
101
+
102
+ # Comparison:
103
+ # deadfire: 1163.6 i/s
104
+ # sassc: 695.7 i/s - 1.67x slower
105
+ # sytanx_tree: 635.7 i/s - 1.83x slower
106
+
107
+
108
+ # Sep 2022:
77
109
  # Warming up --------------------------------------
78
110
  # dartsass 1.000 i/100ms
79
111
  # deadfire 1.088k i/100ms
data/changelog.md CHANGED
@@ -1,4 +1,21 @@
1
1
  ## Changelog
2
+ ### 0.5.0 (current)
3
+
4
+ ### 0.4.0 (18 May 2024)
5
+ - Fix parsing comments that have 2 stars e.g. /**
6
+ - Adds a logger and a default setting that suppresses the logs which can be configured to report errors.
7
+ - Fixes issue with import's not parsing correctly when there is no ending semicolon.
8
+ - Added ci for ruby 3.3
9
+ - Add support for importing .scss files, making it easier to migrate from other libraries.
10
+ ```
11
+ @import "nav"
12
+ @import "sidebar.scss"
13
+ .image { padding: 2px; }
14
+ ```
15
+ Deadfire will look for the file nav.css, then nav.scss in the `config.root_path` in the case when a file extension is not included.
16
+
17
+ - Simplify the configuration by having one option called compressed instead of keep_newlines and keep_comments.
18
+
2
19
  ### 0.3.0 (15 November 2023)
3
20
 
4
21
  - Redo the parser by splitting up the tokenizer, parser, interpreter and generator phases which makes each step simpler. It's still faster than sassc but much slower than it was previously which is something I hope to address soon.
@@ -1,29 +1,37 @@
1
1
  # frozen_string_literal: true
2
+ require "logger"
2
3
 
3
4
  module Deadfire
4
5
  class Configuration
5
- attr_reader :directories, :root_path, :keep_comments, :keep_whitespace
6
+ attr_reader :directories, :root_path, :compressed, :logger, :supressed
6
7
 
7
8
  def initialize
8
9
  @directories = []
9
10
  @root_path = ""
10
- @keep_comments = true
11
- @keep_whitespace = true
11
+ @compressed = false
12
+ @logger = Logger.new(STDOUT, level: :warn)
13
+ @supressed = true
12
14
  end
13
15
 
14
16
  def root_path=(value)
17
+ return if value.nil?
18
+
15
19
  unless Dir.exist?(value)
16
20
  raise DirectoryNotFoundError.new("Root not found #{value}")
17
21
  end
18
22
  @root_path = value
19
23
  end
20
24
 
21
- def keep_comments=(value)
22
- @keep_comments = value
25
+ def compressed=(value)
26
+ @compressed = value unless value.nil?
27
+ end
28
+
29
+ def logger=(value)
30
+ @logger = value
23
31
  end
24
32
 
25
- def keep_whitespace=(value)
26
- @keep_whitespace = value
33
+ def supressed=(value)
34
+ @supressed = value
27
35
  end
28
36
  end
29
37
  end
@@ -19,7 +19,6 @@ module Deadfire
19
19
 
20
20
  def visit_at_rule_node(node)
21
21
  @output << node.at_keyword.lexeme
22
- @output << " "
23
22
  node.value.each do |value|
24
23
  @output << value.lexeme
25
24
  end
@@ -60,7 +59,7 @@ module Deadfire
60
59
  end
61
60
 
62
61
  def visit_comment_node(node)
63
- @output << node.comment.lexeme if Deadfire.configuration.keep_comments
62
+ @output << node.comment.lexeme unless Deadfire.configuration.compressed
64
63
  end
65
64
  end
66
65
  end
@@ -9,7 +9,9 @@ module Deadfire
9
9
  end
10
10
 
11
11
  def error(line, message)
12
- @errors << Error.new(line, message)
12
+ error = Error.new(line, message)
13
+ Deadfire.configuration.logger.error(error.to_s) unless Deadfire.configuration.supressed
14
+ @errors << error
13
15
  end
14
16
 
15
17
  def errors?
@@ -19,6 +21,10 @@ module Deadfire
19
21
  private
20
22
 
21
23
  # create error struct with line and message
22
- Error = Struct.new(:line, :message)
24
+ Error = Struct.new(:line, :message) do
25
+ def to_s
26
+ "Line #{line}: #{message}"
27
+ end
28
+ end
23
29
  end
24
30
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Deadfire
4
4
  class DirectoryNotFoundError < StandardError; end
5
- class Error < StandardError; end
6
5
 
7
6
  class DuplicateImportException < StandardError
8
7
  def initialize(filename = "", lineno = "")
@@ -27,44 +26,4 @@ module Deadfire
27
26
  super(msg)
28
27
  end
29
28
  end
30
-
31
- class EarlyApplyException < StandardError
32
- def initialize(input = "", lineno = "")
33
- msg = if input
34
- "Error with input: `#{input}` line: #{lineno}"
35
- else
36
- "Apply called too early in css. There are no mixins defined."
37
- end
38
-
39
- super(msg)
40
- end
41
- end
42
-
43
- class SyntaxError < StandardError
44
- def initialize(message = "", lineno = "", original_line = "")
45
- msg = if message
46
- "#{original_line}\nline: #{lineno}: #{message}"
47
- else
48
- "Syntax "
49
- end
50
-
51
- super(msg)
52
- end
53
- end
54
-
55
- class ErrorsList
56
- attr_reader :errors
57
-
58
- def initialize
59
- @errors = []
60
- end
61
-
62
- def add(message:, lineno:, original_line:)
63
- @errors << SyntaxError.new(message, lineno, original_line)
64
- end
65
-
66
- def empty?
67
- @errors.empty?
68
- end
69
- end
70
- end
29
+ end
@@ -5,18 +5,16 @@ module Deadfire
5
5
  class << self
6
6
  def resolve_import_path(line, lineno = 0)
7
7
  path = normalize_path(line)
8
- unless path.end_with?(Parser::CSS_FILE_EXTENSION)
9
- path += Parser::CSS_FILE_EXTENSION
10
- end
11
- import_path = File.join(Deadfire.configuration.root_path, path)
12
-
13
- unless File.exist?(import_path)
14
- raise Deadfire::ImportException.new(import_path, lineno)
8
+ potential = potential_path(path)
9
+ ext = File.extname(path)
10
+
11
+ if ext && valid_file?(potential)
12
+ potential
13
+ else
14
+ possible_paths(path)
15
15
  end
16
-
17
- import_path
18
16
  end
19
-
17
+
20
18
  def normalize_path(line)
21
19
  path = line.split.last
22
20
  path.gsub!("\"", "")
@@ -24,6 +22,28 @@ module Deadfire
24
22
  path.gsub!(";", "")
25
23
  path
26
24
  end
25
+
26
+ private
27
+
28
+ def valid_file_extension?(ext)
29
+ Deadfire::PERMISSIBLE_FILE_EXTENSIONS.include?(ext)
30
+ end
31
+
32
+ def valid_file?(path)
33
+ File.exist?(path)
34
+ end
35
+
36
+ def possible_paths(path)
37
+ Deadfire::PERMISSIBLE_FILE_EXTENSIONS.each do |ext|
38
+ option = File.join(Deadfire.configuration.root_path, path + ext)
39
+ return option if valid_file?(option)
40
+ end
41
+ nil
42
+ end
43
+
44
+ def potential_path(path)
45
+ File.join(Deadfire.configuration.root_path, path)
46
+ end
27
47
  end
28
48
  end
29
49
  end
@@ -5,7 +5,6 @@ module Deadfire
5
5
  attr_reader :node, :mixin_names
6
6
 
7
7
  def initialize(node, mixin_names)
8
- # TODO: mixin name can be single or multiple names, separated by a comma
9
8
  @node = node
10
9
  @mixin_names = fetch_mixin_name_from(mixin_names)
11
10
  end
@@ -13,19 +13,13 @@ module Deadfire
13
13
  end
14
14
 
15
15
  def parse
16
- # top level it's a list of statements
17
- # statements are either rules or at-rules
18
- # rules are selectors + declarations
19
- # at-rules are at-keyword + block
20
- # block is a list of declarations?
21
- # declarations are property + value
22
16
  while !is_at_end?
23
17
  if check(:comment)
24
18
  comment = add_comment
25
- @stylesheet << comment if Deadfire.configuration.keep_comments
19
+ @stylesheet << comment unless Deadfire.configuration.compressed
26
20
  elsif check(:newline)
27
21
  newline = add_newline
28
- @stylesheet << newline if Deadfire.configuration.keep_whitespace
22
+ @stylesheet << newline unless Deadfire.configuration.compressed
29
23
  elsif matches_at_rule?
30
24
  @stylesheet << at_rule_declaration
31
25
  else
@@ -111,7 +105,11 @@ module Deadfire
111
105
  def ruleset_declaration
112
106
  values = []
113
107
  while !match?(:left_brace)
114
- values << advance
108
+ unless match?(:comment)
109
+ values << advance
110
+ else
111
+ values << advance unless Deadfire.configuration.compressed
112
+ end
115
113
  end
116
114
 
117
115
  selector = SelectorNode.new(values[0..-1])
@@ -142,14 +140,16 @@ module Deadfire
142
140
 
143
141
  if previous.type == :semicolon
144
142
  if keyword.lexeme == "@apply"
145
- return ApplyNode.new(keyword, values)
143
+ ApplyNode.new(keyword, values)
146
144
  else
147
145
  values << previous # add the semicolon to the values
148
- return AtRuleNode.new(keyword, values, nil)
146
+ AtRuleNode.new(keyword, values, nil)
149
147
  end
148
+ elsif is_at_end?
149
+ AtRuleNode.new(keyword, values, nil)
150
+ else
151
+ AtRuleNode.new(keyword, values[0..-1], parse_block) # remove the left brace, because it's not a value, but part of the block
150
152
  end
151
-
152
- AtRuleNode.new(keyword, values[0..-1], parse_block) # remove the left brace, because it's not a value, but part of the block
153
153
  end
154
154
  end
155
155
  end
@@ -75,26 +75,15 @@ module Deadfire
75
75
  end
76
76
 
77
77
  def add_at_rule(literal = nil)
78
- selector = [current_char]
79
-
80
- while Spec::CSS_AT_RULES.none? { |kwrd| kwrd == selector.join + peek } && !at_end?
81
- break if peek == NEWLINE
82
- selector << advance
83
- end
84
-
85
- # final char in at-rule
86
- selector << advance
87
-
88
- current_at_rule = selector.join
89
- at_rule = Spec::CSS_AT_RULES.find { |kwrd| kwrd == current_at_rule }
78
+ at_rule = determine_at_rule
90
79
 
91
80
  if peek == NEWLINE
92
81
  @line += 1
93
82
  @error_reporter.error(@line, "at-rule cannot be on multiple lines.")
94
- add_token(:at_rule, current_at_rule)
83
+ add_token(:at_rule, at_rule)
95
84
  elsif at_rule
96
85
  token = add_token(:at_rule, "at_#{at_rule[1..-1]}")
97
- if at_rule == "@import"
86
+ if at_rule == Spec::IMPORT
98
87
  prescan_import_rule(token.last)
99
88
  else
100
89
  token
@@ -172,32 +161,38 @@ module Deadfire
172
161
  end
173
162
 
174
163
  def add_forward_slash_or_comment
175
- if peek == "*"
176
- advance # consume the *
177
- while peek != "*" && peek_next != "/" && !at_end?
178
- @line += 1 if peek == NEWLINE
179
- advance
180
- end
164
+ return add_token(:forward_slash) unless peek == "*"
181
165
 
182
- if at_end? && peek != "*"
166
+ advance # consume the *
167
+ while true
168
+ if at_end?
183
169
  @error_reporter.error(@line, "Unterminated comment on line #{@line}.")
184
- else
185
- advance # consume the *
186
- advance # consume the /
170
+ break
187
171
  end
188
- add_token(:comment) # Add the comment anyway, but report an error.
189
- else
190
- add_token(:forward_slash)
172
+
173
+ case peek
174
+ when NEWLINE
175
+ @line += 1
176
+ when "*"
177
+ if peek_next == "/"
178
+ advance # consume the *
179
+ advance # consume the /
180
+ break
181
+ end
182
+ end
183
+
184
+ advance
191
185
  end
186
+ add_token(:comment) # Add the comment anyway, but report an error.
192
187
  end
193
188
 
194
189
  def add_whitespace_token
195
- add_token(:whitespace) if Deadfire.configuration.keep_whitespace
190
+ add_token(:whitespace) unless Deadfire.configuration.compressed
196
191
  end
197
192
 
198
193
  def add_newline_token
199
194
  @line += 1
200
- add_token(:newline) if Deadfire.configuration.keep_whitespace
195
+ add_token(:newline) unless Deadfire.configuration.compressed
201
196
  end
202
197
 
203
198
  def current_char_position
@@ -229,6 +224,21 @@ module Deadfire
229
224
  (char >= "a" && char <= "z") || (char >= "A" && char <= "Z")
230
225
  end
231
226
 
227
+ def determine_at_rule
228
+ selector = [current_char]
229
+
230
+ while Spec::CSS_AT_RULES.none? { |kwrd| kwrd == selector.join + peek } && !at_end?
231
+ break if peek == NEWLINE
232
+ selector << advance
233
+ end
234
+
235
+ # final char in at-rule
236
+ selector << advance
237
+
238
+ current_at_rule = selector.join
239
+ Spec::CSS_AT_RULES.find { |kwrd| kwrd == current_at_rule }
240
+ end
241
+
232
242
  def prescan_import_rule(token)
233
243
  # we want to get all the text between the @import and the semicolon
234
244
  # so we can parse the file and add it to the ast
@@ -237,29 +247,33 @@ module Deadfire
237
247
  while peek != ";" && !at_end?
238
248
  advance
239
249
  end
250
+
251
+ add_token(:text)
240
252
 
241
253
  if at_end?
242
- @error_reporter.error(@line, "Unterminated import rule.")
254
+ @error_reporter.error(@line, "Imports must be terminated correctly with a ';'.")
243
255
  return
244
256
  end
245
257
 
246
- add_token(:text)
247
- advance # remove the semicolon
248
-
249
258
  text_token = @tokens.last
250
259
 
251
260
  text = text_token.lexeme.gsub(/\\|"/, '')
252
261
  file = FilenameHelper.resolve_import_path(text, @line)
253
262
 
254
- # file is ready for scanning
255
- content = File.read(file)
256
- scanner = Scanner.new(content, @error_reporter)
263
+ if file
264
+ # file is ready for scanning
265
+ content = File.read(file)
266
+ scanner = Scanner.new(content, @error_reporter)
257
267
 
258
- @tokens.pop # remove the text token
259
- @tokens.pop # remove the at_rule token
268
+ advance # remove the semicolon
269
+ @tokens.pop # remove the text token
270
+ @tokens.pop # remove the at_rule token
260
271
 
261
- imported_tokens = scanner.tokenize[0..-2]
262
- @tokens.concat imported_tokens
272
+ imported_tokens = scanner.tokenize[0..-2]
273
+ @tokens.concat imported_tokens
274
+ else
275
+ @error_reporter.error(@line, "File not found '#{text}'")
276
+ end
263
277
  end
264
278
  end
265
279
  end
@@ -44,10 +44,6 @@ module Deadfire
44
44
  end
45
45
  end
46
46
 
47
- def visit_declaration_node(node)
48
- node.accept(self)
49
- end
50
-
51
47
  def visit_comment_node(node)
52
48
  # node.accept(self)
53
49
  end
data/lib/deadfire/spec.rb CHANGED
@@ -17,35 +17,34 @@ module Deadfire
17
17
  # value = any-value [ ',' S* any-value ]*;
18
18
  # any-value = IDENT | STRING | NUMBER | PERCENTAGE | DIMENSION | COLOR | URI | FUNCTION any-value* ')' | '(' any-value* ')' | '[' any-value* ']' | '{' any-value* '}' | ';';
19
19
 
20
- # -- SASS features
21
-
22
- # Example: @apply button;
23
- # apply-rule = "@apply" S* mixin-name S* ";" S*
24
- # mixin-name = IDENT
25
-
26
- # Example: button { color: red; &.active { color: blue } }
27
- # nested-selector = selector S* "{" S* declaration-list S* "}"
28
- # selector = simple-selector [ "&" simple-selector ]
29
- # simple-selector = element-name [ "#" id ] [ "." class ]*
30
- # element-name = IDENT
31
- # id = IDENT
32
- # class = "." IDENT
33
- # declaration-list = declaration [ ";" S* declaration ]*
20
+ CHARSET = "@charset"
21
+ IMPORT = "@import"
22
+ MEDIA = "@media"
23
+ PAGE = "@page"
24
+ FONT_FACE = "@font-face"
25
+ KEYFRAMES = "@keyframes"
26
+ SUPPORTS = "@supports"
27
+ NAMESPACE = "@namespace"
28
+ COUNTER_STYLE = "@counter-style"
29
+ VIEWPORT = "@viewport"
30
+ DOCUMENT = "@document"
31
+ APPLY = "@apply"
32
+ LAYER = "@layer"
34
33
 
35
34
  CSS_AT_RULES = [
36
- "@charset",
37
- "@import",
38
- "@media",
39
- "@page",
40
- "@font-face",
41
- "@keyframes",
42
- "@supports",
43
- "@namespace",
44
- "@counter-style",
45
- "@viewport",
46
- "@document",
47
- "@apply",
48
- "@layer"
35
+ CHARSET,
36
+ IMPORT,
37
+ MEDIA,
38
+ PAGE,
39
+ FONT_FACE,
40
+ KEYFRAMES,
41
+ SUPPORTS,
42
+ NAMESPACE,
43
+ COUNTER_STYLE,
44
+ VIEWPORT,
45
+ DOCUMENT,
46
+ APPLY,
47
+ LAYER
49
48
  ]
50
49
 
51
50
  CSS_SELECTORS = [
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Deadfire
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/deadfire.rb CHANGED
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "deadfire/ast_printer"
4
- require_relative "deadfire/css_buffer"
5
4
  require_relative "deadfire/css_generator"
6
5
  require_relative "deadfire/configuration"
7
6
  require_relative "deadfire/errors"
8
7
  require_relative "deadfire/error_reporter"
9
8
  require_relative "deadfire/interpreter"
10
- require_relative "deadfire/parser"
11
9
  require_relative "deadfire/parser_engine"
12
10
  require_relative "deadfire/spec"
13
11
  require_relative "deadfire/filename_helper"
@@ -26,6 +24,8 @@ require_relative "deadfire/front_end/selector_node"
26
24
  require_relative "deadfire/front_end/stylesheet_node"
27
25
 
28
26
  module Deadfire
27
+ PERMISSIBLE_FILE_EXTENSIONS = [".css", ".scss"].freeze
28
+
29
29
  class << self
30
30
  attr_reader :config
31
31
 
@@ -42,6 +42,11 @@ module Deadfire
42
42
  end
43
43
 
44
44
  def parse(content, options = {})
45
+ configure do |config|
46
+ config.root_path = options[:root_path]
47
+ config.compressed = options[:compressed]
48
+ end
49
+
45
50
  parser = ParserEngine.new(content)
46
51
  # TODO: hook into a logger and report the errors
47
52
  parser.parse
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deadfire
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Haroon Ahmed
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-15 00:00:00.000000000 Z
11
+ date: 2024-05-18 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -40,7 +40,6 @@ files:
40
40
  - lib/deadfire.rb
41
41
  - lib/deadfire/ast_printer.rb
42
42
  - lib/deadfire/configuration.rb
43
- - lib/deadfire/css_buffer.rb
44
43
  - lib/deadfire/css_generator.rb
45
44
  - lib/deadfire/error_reporter.rb
46
45
  - lib/deadfire/errors.rb
@@ -58,7 +57,6 @@ files:
58
57
  - lib/deadfire/front_end/stylesheet_node.rb
59
58
  - lib/deadfire/front_end/token.rb
60
59
  - lib/deadfire/interpreter.rb
61
- - lib/deadfire/parser.rb
62
60
  - lib/deadfire/parser_engine.rb
63
61
  - lib/deadfire/spec.rb
64
62
  - lib/deadfire/version.rb
@@ -84,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
84
82
  - !ruby/object:Gem::Version
85
83
  version: '0'
86
84
  requirements: []
87
- rubygems_version: 3.4.10
85
+ rubygems_version: 3.5.6
88
86
  signing_key:
89
87
  specification_version: 4
90
88
  summary: Deadfire - lightweight css preprocessor
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
- require "stringio"
3
-
4
- module Deadfire
5
- class CssBuffer
6
- attr_reader :lineno, :buffer
7
-
8
- def initialize(content)
9
- @content = StringIO.new(content)
10
- @buffer = []
11
- @lineno = 0
12
- end
13
-
14
- def gets(skip_buffer: false)
15
- output = content.gets
16
- if output && !skip_buffer
17
- buffer << output
18
- end
19
- @lineno += 1
20
- output
21
- end
22
-
23
- def peek
24
- output = content.gets
25
- content.ungetc(output)
26
- output
27
- end
28
-
29
- def eof?
30
- content.eof?
31
- end
32
-
33
- private
34
-
35
- attr_reader :content
36
- end
37
- end
@@ -1,258 +0,0 @@
1
- # frozen_string_literal: true
2
- module Deadfire
3
- # NOTE: Legacy parser, will be replaced by ParserEngine
4
- class Parser
5
- singleton_class.attr_accessor :cached_mixins
6
- self.cached_mixins = Hash.new { |h, k| h[k] = {} }
7
-
8
- singleton_class.attr_accessor :import_path_cache
9
- self.import_path_cache = []
10
-
11
- ROOT_SELECTOR = ":root {"
12
- OPENING_SELECTOR_PATTERN = /\s*\{/
13
- CLOSING_SELECTOR_PATTERN = /\s*\}/
14
- NEST_SELECTOR = "&"
15
- START_BLOCK_CHAR = "{"
16
- END_BLOCK_CHAR = "}"
17
- IMPORT_SELECTOR = "@import"
18
- CSS_FILE_EXTENSION = ".css"
19
- APPLY_SELECTOR = "@apply"
20
- NEWLINE = "\n"
21
- OPEN_COMMENT_SELECTOR = "/*"
22
- CLOSE_COMMENT_SELECTOR = "*/"
23
-
24
- def self.parse(content, options = {})
25
- new(content, options).parse
26
- end
27
-
28
- attr_reader :output, :errors_list
29
-
30
- def initialize(content, options = {})
31
- @content = content
32
- @errors_list = ErrorsList.new
33
- @filename = options[:filename]
34
- @output = []
35
- @imports = []
36
- end
37
-
38
- def buffer
39
- @content = preprocess
40
- @buffer ||= CssBuffer.new(@content)
41
- end
42
-
43
- def parse
44
- while ! buffer.eof?
45
- process_line(buffer.gets)
46
- end
47
-
48
- @output << NEWLINE
49
-
50
- @output.join
51
- end
52
-
53
- def errors?
54
- @errors_list.errors.any?
55
- end
56
-
57
- private
58
-
59
- def preprocess
60
- @content.gsub(/\r\n?|\f/, "\n").gsub("\u{0000}", "\u{FFFD}")
61
- end
62
-
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
75
- end
76
- end
77
-
78
- def keep_comments?
79
- Deadfire.configuration.keep_comments
80
- end
81
-
82
- def handle_comment(line)
83
- @output << Line.new(line, buffer.lineno) if keep_comments?
84
-
85
- while ! line.include?(CLOSE_COMMENT_SELECTOR) && ! buffer.eof?
86
- line = buffer.gets
87
- @output << Line.new(line, buffer.lineno) if keep_comments?
88
- end
89
-
90
- if buffer.eof?
91
- @errors_list.add(message: "Unclosed comment error", lineno: buffer.lineno, original_line: line)
92
- end
93
- end
94
-
95
- def handle_import(line)
96
- import = Import.new(line, buffer.lineno)
97
-
98
- # TODO:
99
- # - decide on how many levels of imports we want to allow
100
- # - make async??
101
- @output << import.parse
102
- end
103
-
104
- def handle_apply(line)
105
- @apply = Apply.new(line, buffer.lineno)
106
- @output << @apply.parse.join(NEWLINE)
107
- end
108
-
109
- def handle_mixins(line)
110
- @root = Root.new(line, buffer.lineno, buffer)
111
- @output << @root.parse
112
- end
113
- end
114
-
115
- class Line
116
- attr_accessor :content, :line_number
117
-
118
- def initialize(content, line_number)
119
- @content = content
120
- @line_number = line_number
121
- end
122
-
123
- def to_s
124
- content
125
- end
126
- end
127
-
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
136
-
137
- def parse
138
- line = @content
139
- if line.include? Parser::ROOT_SELECTOR
140
- @output << Line.new(line, @line_number)
141
- end
142
-
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
151
- end
152
-
153
- @output << Line.new(line, @buffer.lineno) if @output_current_line
154
- @output_current_line = true
155
- end
156
-
157
- to_s
158
- end
159
-
160
- def to_s
161
- return "" if @output.size <= 1
162
-
163
- @output.map(&:to_s)
164
- end
165
-
166
- private
167
-
168
- def extract_mixin_name(line)
169
- line.tr("{", "").tr(".", "").tr(":", "").strip
170
- end
171
-
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
179
- end
180
- properties
181
- end
182
-
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
189
-
190
- class Import < Line
191
- def initialize(...)
192
- super(...)
193
- end
194
-
195
- def parse
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)
200
- end
201
-
202
- Parser.import_path_cache << import_path
203
-
204
- Parser.new(File.read(import_path), filename: import_path).parse
205
- end
206
- end
207
-
208
- class Apply < Line
209
- def initialize(...)
210
- super
211
- @current_line = @content.dup
212
- @space = " "
213
- @apply_start_char = "@"
214
- @output = []
215
- end
216
-
217
- def parse
218
- raise Deadfire::EarlyApplyException.new(@content, @lineno) if Parser.cached_mixins.empty?
219
-
220
- space_counter = calculate_number_of_spaces
221
- ends_with_end_block_char = false
222
-
223
- @current_line.split(" ").each do |css|
224
- next if css.include?(Parser::APPLY_SELECTOR)
225
-
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
231
-
232
- fetch_cached_mixin(css).each_pair do |key, value|
233
- @output << "#{@space * space_counter}#{key}: #{value};"
234
- end
235
- end
236
- @output << "#{Parser::END_BLOCK_CHAR}" if ends_with_end_block_char
237
- @output
238
- end
239
-
240
- private
241
-
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
249
- end
250
-
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]
256
- end
257
- end
258
- end