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 +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +28 -3
- data/benchmarks/basic_benchmark.rb +39 -7
- data/changelog.md +17 -0
- data/lib/deadfire/configuration.rb +15 -7
- data/lib/deadfire/css_generator.rb +1 -2
- data/lib/deadfire/error_reporter.rb +8 -2
- data/lib/deadfire/errors.rb +1 -42
- data/lib/deadfire/filename_helper.rb +30 -10
- data/lib/deadfire/front_end/apply_node.rb +0 -1
- data/lib/deadfire/front_end/parser.rb +13 -13
- data/lib/deadfire/front_end/scanner.rb +54 -40
- data/lib/deadfire/interpreter.rb +0 -4
- data/lib/deadfire/spec.rb +26 -27
- data/lib/deadfire/version.rb +1 -1
- data/lib/deadfire.rb +7 -2
- metadata +3 -5
- data/lib/deadfire/css_buffer.rb +0 -37
- data/lib/deadfire/parser.rb +0 -258
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 82f838eee7d8e2ef83b302d7de509c428af8584567892296fd3011df42cb0990
|
4
|
+
data.tar.gz: 1a4b828032eda5dae38d3da5f820081402f7e66f919b2720ff8f9be65d6d8bf0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a724bdb7b939e5b4404ee5cee4266ad6c278ac5d4e7fc74f560884f4b30fc123c3492a52abd5028adbc9f579dd690beb26d0de2460e8cb3b8e482d44ce77e733
|
7
|
+
data.tar.gz: e353841bfb20e25ff0ff1556d555443546783a744cf96b2a2149862cf8dbebb31bb9c48f50d68f20355ea036c36e22ca4ac9f059413df500974f4888849673bd
|
data/.github/workflows/ci.yml
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -50,11 +50,11 @@ Output;
|
|
50
50
|
|
51
51
|
### @apply
|
52
52
|
|
53
|
-
|
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
|
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
|
-
|
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
|
-
#
|
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, :
|
6
|
+
attr_reader :directories, :root_path, :compressed, :logger, :supressed
|
6
7
|
|
7
8
|
def initialize
|
8
9
|
@directories = []
|
9
10
|
@root_path = ""
|
10
|
-
@
|
11
|
-
@
|
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
|
22
|
-
@
|
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
|
26
|
-
@
|
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
|
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
|
-
|
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
|
data/lib/deadfire/errors.rb
CHANGED
@@ -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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
@@ -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
|
19
|
+
@stylesheet << comment unless Deadfire.configuration.compressed
|
26
20
|
elsif check(:newline)
|
27
21
|
newline = add_newline
|
28
|
-
@stylesheet << newline
|
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
|
-
|
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
|
-
|
143
|
+
ApplyNode.new(keyword, values)
|
146
144
|
else
|
147
145
|
values << previous # add the semicolon to the values
|
148
|
-
|
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
|
-
|
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,
|
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 ==
|
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
|
-
|
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
|
-
|
166
|
+
advance # consume the *
|
167
|
+
while true
|
168
|
+
if at_end?
|
183
169
|
@error_reporter.error(@line, "Unterminated comment on line #{@line}.")
|
184
|
-
|
185
|
-
advance # consume the *
|
186
|
-
advance # consume the /
|
170
|
+
break
|
187
171
|
end
|
188
|
-
|
189
|
-
|
190
|
-
|
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)
|
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)
|
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, "
|
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
|
-
|
255
|
-
|
256
|
-
|
263
|
+
if file
|
264
|
+
# file is ready for scanning
|
265
|
+
content = File.read(file)
|
266
|
+
scanner = Scanner.new(content, @error_reporter)
|
257
267
|
|
258
|
-
|
259
|
-
|
268
|
+
advance # remove the semicolon
|
269
|
+
@tokens.pop # remove the text token
|
270
|
+
@tokens.pop # remove the at_rule token
|
260
271
|
|
261
|
-
|
262
|
-
|
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
|
data/lib/deadfire/interpreter.rb
CHANGED
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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 = [
|
data/lib/deadfire/version.rb
CHANGED
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.
|
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:
|
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.
|
85
|
+
rubygems_version: 3.5.6
|
88
86
|
signing_key:
|
89
87
|
specification_version: 4
|
90
88
|
summary: Deadfire - lightweight css preprocessor
|
data/lib/deadfire/css_buffer.rb
DELETED
@@ -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
|
data/lib/deadfire/parser.rb
DELETED
@@ -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
|