deadfire 0.2.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 +2 -2
- data/Gemfile.lock +1 -1
- data/README.md +55 -32
- data/benchmarks/basic_benchmark.rb +47 -12
- data/benchmarks/tailwind_parser.rb +23 -0
- data/bin/console +16 -3
- data/changelog.md +38 -1
- data/lib/deadfire/ast_printer.rb +58 -0
- data/lib/deadfire/configuration.rb +18 -5
- data/lib/deadfire/css_generator.rb +65 -0
- data/lib/deadfire/error_reporter.rb +30 -0
- data/lib/deadfire/errors.rb +1 -14
- data/lib/deadfire/filename_helper.rb +49 -0
- data/lib/deadfire/front_end/apply_node.rb +43 -0
- data/lib/deadfire/front_end/at_rule_node.rb +19 -0
- data/lib/deadfire/front_end/base_node.rb +11 -0
- data/lib/deadfire/front_end/block_node.rb +21 -0
- data/lib/deadfire/front_end/comment_node.rb +17 -0
- data/lib/deadfire/front_end/newline_node.rb +17 -0
- data/lib/deadfire/front_end/parser.rb +156 -0
- data/lib/deadfire/front_end/ruleset_node.rb +18 -0
- data/lib/deadfire/front_end/scanner.rb +280 -0
- data/lib/deadfire/front_end/selector_node.rb +52 -0
- data/lib/deadfire/front_end/stylesheet_node.rb +21 -0
- data/lib/deadfire/front_end/token.rb +20 -0
- data/lib/deadfire/interpreter.rb +84 -0
- data/lib/deadfire/parser_engine.rb +41 -0
- data/lib/deadfire/spec.rb +135 -0
- data/lib/deadfire/version.rb +1 -1
- data/lib/deadfire.rb +34 -7
- metadata +23 -6
- data/lib/deadfire/css_buffer.rb +0 -37
- data/lib/deadfire/parser.rb +0 -387
- data/lib/deadfire/transformers/transformer.rb +0 -17
@@ -0,0 +1,43 @@
|
|
1
|
+
# Frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deadfire
|
4
|
+
class ApplyNode
|
5
|
+
attr_reader :node, :mixin_names
|
6
|
+
|
7
|
+
def initialize(node, mixin_names)
|
8
|
+
@node = node
|
9
|
+
@mixin_names = fetch_mixin_name_from(mixin_names)
|
10
|
+
end
|
11
|
+
|
12
|
+
def accept(visitor)
|
13
|
+
visitor.visit_apply_node(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def lineno
|
17
|
+
node.lineno
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def fetch_mixin_name_from(tokens)
|
23
|
+
@_cached_mixin_name ||= begin
|
24
|
+
names = []
|
25
|
+
current = []
|
26
|
+
tokens.each do |token|
|
27
|
+
case token.type
|
28
|
+
when :comma
|
29
|
+
names << current.join("")
|
30
|
+
current = []
|
31
|
+
current << token.lexeme
|
32
|
+
when :whitespace
|
33
|
+
# ignore whitespace
|
34
|
+
else
|
35
|
+
current << token.lexeme
|
36
|
+
end
|
37
|
+
end
|
38
|
+
names << current.join("")
|
39
|
+
names
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deadfire
|
4
|
+
module FrontEnd
|
5
|
+
class AtRuleNode < BaseNode
|
6
|
+
attr_reader :at_keyword, :value, :block
|
7
|
+
|
8
|
+
def initialize(at_keyword, value, block)
|
9
|
+
@at_keyword = at_keyword
|
10
|
+
@value = value
|
11
|
+
@block = block
|
12
|
+
end
|
13
|
+
|
14
|
+
def accept(visitor)
|
15
|
+
visitor.visit_at_rule_node(self)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deadfire
|
4
|
+
module FrontEnd
|
5
|
+
class BlockNode < BaseNode
|
6
|
+
attr_reader :declarations
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@declarations = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def <<(node)
|
13
|
+
@declarations << node
|
14
|
+
end
|
15
|
+
|
16
|
+
def accept(visitor)
|
17
|
+
visitor.visit_block_node(self)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deadfire
|
4
|
+
module FrontEnd
|
5
|
+
class CommentNode < BaseNode
|
6
|
+
attr_reader :comment
|
7
|
+
|
8
|
+
def initialize(comment)
|
9
|
+
@comment = comment
|
10
|
+
end
|
11
|
+
|
12
|
+
def accept(visitor)
|
13
|
+
visitor.visit_comment_node(self)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deadfire
|
4
|
+
module FrontEnd
|
5
|
+
class NewlineNode < BaseNode
|
6
|
+
attr_reader :text
|
7
|
+
|
8
|
+
def initialize(text)
|
9
|
+
@text = text
|
10
|
+
end
|
11
|
+
|
12
|
+
def accept(visitor)
|
13
|
+
visitor.visit_newline_node(self)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deadfire
|
4
|
+
module FrontEnd
|
5
|
+
class Parser
|
6
|
+
attr_reader :error_reporter, :tokens, :options, :current
|
7
|
+
|
8
|
+
def initialize(tokens, error_reporter)
|
9
|
+
@error_reporter = error_reporter
|
10
|
+
@tokens = tokens
|
11
|
+
@current = 0
|
12
|
+
@stylesheet = StylesheetNode.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse
|
16
|
+
while !is_at_end?
|
17
|
+
if check(:comment)
|
18
|
+
comment = add_comment
|
19
|
+
@stylesheet << comment unless Deadfire.configuration.compressed
|
20
|
+
elsif check(:newline)
|
21
|
+
newline = add_newline
|
22
|
+
@stylesheet << newline unless Deadfire.configuration.compressed
|
23
|
+
elsif matches_at_rule?
|
24
|
+
@stylesheet << at_rule_declaration
|
25
|
+
else
|
26
|
+
@stylesheet << ruleset_declaration
|
27
|
+
end
|
28
|
+
end
|
29
|
+
@stylesheet
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def is_at_end?
|
35
|
+
peek.type == :eof
|
36
|
+
end
|
37
|
+
|
38
|
+
def peek
|
39
|
+
tokens[current]
|
40
|
+
end
|
41
|
+
|
42
|
+
def previous
|
43
|
+
tokens[current - 1]
|
44
|
+
end
|
45
|
+
|
46
|
+
def advance
|
47
|
+
@current += 1 unless is_at_end?
|
48
|
+
previous
|
49
|
+
end
|
50
|
+
|
51
|
+
def check(type)
|
52
|
+
return false if is_at_end?
|
53
|
+
|
54
|
+
peek.type == type
|
55
|
+
end
|
56
|
+
|
57
|
+
def match?(*types)
|
58
|
+
types.each do |type|
|
59
|
+
if check(type)
|
60
|
+
advance
|
61
|
+
return true
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
false
|
66
|
+
end
|
67
|
+
|
68
|
+
def consume(type, message)
|
69
|
+
if check(type)
|
70
|
+
advance
|
71
|
+
return
|
72
|
+
end
|
73
|
+
|
74
|
+
error_reporter.error(peek.lineno, message)
|
75
|
+
end
|
76
|
+
|
77
|
+
def matches_at_rule?
|
78
|
+
check(:at_rule)
|
79
|
+
end
|
80
|
+
|
81
|
+
def matches_nested_rule?
|
82
|
+
match?(:ampersand)
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse_block
|
86
|
+
block = BlockNode.new
|
87
|
+
block << previous
|
88
|
+
|
89
|
+
while !is_at_end?
|
90
|
+
if match?(:right_brace)
|
91
|
+
break
|
92
|
+
elsif matches_at_rule?
|
93
|
+
block << at_rule_declaration
|
94
|
+
elsif match?(:left_brace)
|
95
|
+
block << parse_block
|
96
|
+
else
|
97
|
+
block << advance
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
block << previous
|
102
|
+
block
|
103
|
+
end
|
104
|
+
|
105
|
+
def ruleset_declaration
|
106
|
+
values = []
|
107
|
+
while !match?(:left_brace)
|
108
|
+
unless match?(:comment)
|
109
|
+
values << advance
|
110
|
+
else
|
111
|
+
values << advance unless Deadfire.configuration.compressed
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
selector = SelectorNode.new(values[0..-1])
|
116
|
+
|
117
|
+
block = parse_block
|
118
|
+
RulesetNode.new(selector, block)
|
119
|
+
end
|
120
|
+
|
121
|
+
def add_comment
|
122
|
+
consume(:comment, "Expect comment")
|
123
|
+
CommentNode.new(previous)
|
124
|
+
end
|
125
|
+
|
126
|
+
def add_newline
|
127
|
+
consume(:newline, "Expect newline")
|
128
|
+
NewlineNode.new(previous.lexeme)
|
129
|
+
end
|
130
|
+
|
131
|
+
def at_rule_declaration
|
132
|
+
consume(:at_rule, "Expect at rule")
|
133
|
+
keyword = previous
|
134
|
+
|
135
|
+
# peek until we get to ; or {, if we reach ; then add to at rule node and return
|
136
|
+
values = []
|
137
|
+
while !match?(:semicolon, :left_brace) && !is_at_end?
|
138
|
+
values << advance
|
139
|
+
end
|
140
|
+
|
141
|
+
if previous.type == :semicolon
|
142
|
+
if keyword.lexeme == "@apply"
|
143
|
+
ApplyNode.new(keyword, values)
|
144
|
+
else
|
145
|
+
values << previous # add the semicolon to the values
|
146
|
+
AtRuleNode.new(keyword, values, nil)
|
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
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deadfire
|
4
|
+
module FrontEnd
|
5
|
+
class RulesetNode < BaseNode
|
6
|
+
attr_reader :selector, :block
|
7
|
+
|
8
|
+
def initialize(selector, block)
|
9
|
+
@selector = selector
|
10
|
+
@block = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def accept(visitor)
|
14
|
+
visitor.visit_ruleset_node(self)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,280 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deadfire
|
4
|
+
module FrontEnd
|
5
|
+
class Scanner
|
6
|
+
def initialize(source, error_reporter)
|
7
|
+
@source = source
|
8
|
+
@total_chars = @source.length
|
9
|
+
@tokens = []
|
10
|
+
@start = 0
|
11
|
+
@current = 0
|
12
|
+
@line = 1
|
13
|
+
@error_reporter = error_reporter
|
14
|
+
end
|
15
|
+
|
16
|
+
def tokenize
|
17
|
+
until at_end?
|
18
|
+
reset_counter
|
19
|
+
scan_token
|
20
|
+
end
|
21
|
+
|
22
|
+
@tokens << Token.new(:eof, "", nil, @line)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
NEWLINE = "\n"
|
28
|
+
|
29
|
+
def reset_counter
|
30
|
+
@start = @current - 1
|
31
|
+
end
|
32
|
+
|
33
|
+
def at_end?
|
34
|
+
@current >= @total_chars
|
35
|
+
end
|
36
|
+
|
37
|
+
def scan_token
|
38
|
+
token = advance
|
39
|
+
case token
|
40
|
+
when "@" then add_at_rule
|
41
|
+
when "{" then add_token(:left_brace)
|
42
|
+
when "}" then add_token(:right_brace)
|
43
|
+
when "#" then add_token(:id_selector)
|
44
|
+
when "." then add_token(:class_selector)
|
45
|
+
when ";" then add_token(:semicolon)
|
46
|
+
when "," then add_token(:comma)
|
47
|
+
when "(" then add_token(:left_paren)
|
48
|
+
when ")" then add_token(:right_paren)
|
49
|
+
when "_" then add_token(:underscore)
|
50
|
+
when "=" then add_token(:equal)
|
51
|
+
when "~" then add_token(:tilde)
|
52
|
+
when "*" then add_token(:asterisk)
|
53
|
+
when "&" then add_token(:ampersand)
|
54
|
+
when ":" then add_psuedo_selector
|
55
|
+
when "-" then add_hypen_token
|
56
|
+
when "/" then add_forward_slash_or_comment
|
57
|
+
when "'" then add_token(:single_quote)
|
58
|
+
when NEWLINE then add_newline_token
|
59
|
+
when " ", "\r", "\t" then add_whitespace_token
|
60
|
+
when '"' then add_string_token
|
61
|
+
else
|
62
|
+
if digit?(token)
|
63
|
+
add_number_token
|
64
|
+
elsif text?(token)
|
65
|
+
add_text_token # or word token?
|
66
|
+
else
|
67
|
+
add_token(:other)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_token(type, literal = nil)
|
73
|
+
text = @source[@start + 1..current_char_position]
|
74
|
+
@tokens << Token.new(type, text, literal, @line)
|
75
|
+
end
|
76
|
+
|
77
|
+
def add_at_rule(literal = nil)
|
78
|
+
at_rule = determine_at_rule
|
79
|
+
|
80
|
+
if peek == NEWLINE
|
81
|
+
@line += 1
|
82
|
+
@error_reporter.error(@line, "at-rule cannot be on multiple lines.")
|
83
|
+
add_token(:at_rule, at_rule)
|
84
|
+
elsif at_rule
|
85
|
+
token = add_token(:at_rule, "at_#{at_rule[1..-1]}")
|
86
|
+
if at_rule == Spec::IMPORT
|
87
|
+
prescan_import_rule(token.last)
|
88
|
+
else
|
89
|
+
token
|
90
|
+
end
|
91
|
+
else
|
92
|
+
@error_reporter.error(@line, "Invalid at-rule.")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def add_string_token
|
97
|
+
while peek != '"' && !at_end?
|
98
|
+
@line += 1 if peek == NEWLINE
|
99
|
+
advance
|
100
|
+
end
|
101
|
+
|
102
|
+
if at_end?
|
103
|
+
@error_reporter.error(@line, "Unterminated string.")
|
104
|
+
return
|
105
|
+
end
|
106
|
+
|
107
|
+
advance
|
108
|
+
|
109
|
+
# Trim the surrounding quotes.
|
110
|
+
# TODO: this does not look right... page 50 crafting interpreters.
|
111
|
+
value = @source[@start + 2..current_char_position]
|
112
|
+
add_token(:string, value)
|
113
|
+
end
|
114
|
+
|
115
|
+
# This token is very similar to the string token, but we want to explicitly
|
116
|
+
# split up text from string, because string in css is surrounded by quotes and text is free form
|
117
|
+
# which can be a property or value e.g. `color: red;`.
|
118
|
+
def add_text_token
|
119
|
+
while text?(peek) && !at_end?
|
120
|
+
advance
|
121
|
+
end
|
122
|
+
|
123
|
+
value = @source[@start+1..current_char_position]
|
124
|
+
add_token(:text, value)
|
125
|
+
end
|
126
|
+
|
127
|
+
def add_number_token
|
128
|
+
while digit?(peek)
|
129
|
+
advance
|
130
|
+
end
|
131
|
+
|
132
|
+
# Look for a fractional part.
|
133
|
+
if peek == "." && digit?(peek_next)
|
134
|
+
# Consume the "."
|
135
|
+
advance
|
136
|
+
|
137
|
+
while digit?(peek)
|
138
|
+
advance
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
add_token(:number, @source[@start..@current].to_f)
|
143
|
+
end
|
144
|
+
|
145
|
+
def add_psuedo_selector
|
146
|
+
if peek == ":"
|
147
|
+
advance
|
148
|
+
add_token(:double_colon)
|
149
|
+
else
|
150
|
+
add_token(:colon)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def add_hypen_token
|
155
|
+
if peek == "-"
|
156
|
+
advance
|
157
|
+
add_token(:double_hyphen)
|
158
|
+
else
|
159
|
+
add_token(:hyphen)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def add_forward_slash_or_comment
|
164
|
+
return add_token(:forward_slash) unless peek == "*"
|
165
|
+
|
166
|
+
advance # consume the *
|
167
|
+
while true
|
168
|
+
if at_end?
|
169
|
+
@error_reporter.error(@line, "Unterminated comment on line #{@line}.")
|
170
|
+
break
|
171
|
+
end
|
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
|
185
|
+
end
|
186
|
+
add_token(:comment) # Add the comment anyway, but report an error.
|
187
|
+
end
|
188
|
+
|
189
|
+
def add_whitespace_token
|
190
|
+
add_token(:whitespace) unless Deadfire.configuration.compressed
|
191
|
+
end
|
192
|
+
|
193
|
+
def add_newline_token
|
194
|
+
@line += 1
|
195
|
+
add_token(:newline) unless Deadfire.configuration.compressed
|
196
|
+
end
|
197
|
+
|
198
|
+
def current_char_position
|
199
|
+
@current - 1
|
200
|
+
end
|
201
|
+
|
202
|
+
def current_char
|
203
|
+
@source[current_char_position]
|
204
|
+
end
|
205
|
+
|
206
|
+
def advance
|
207
|
+
@current += 1
|
208
|
+
current_char
|
209
|
+
end
|
210
|
+
|
211
|
+
def peek
|
212
|
+
@source[@current] unless at_end?
|
213
|
+
end
|
214
|
+
|
215
|
+
def peek_next
|
216
|
+
@source[@current + 1] unless at_end?
|
217
|
+
end
|
218
|
+
|
219
|
+
def digit?(char)
|
220
|
+
char >= "0" && char <= "9"
|
221
|
+
end
|
222
|
+
|
223
|
+
def text?(char)
|
224
|
+
(char >= "a" && char <= "z") || (char >= "A" && char <= "Z")
|
225
|
+
end
|
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
|
+
|
242
|
+
def prescan_import_rule(token)
|
243
|
+
# we want to get all the text between the @import and the semicolon
|
244
|
+
# so we can parse the file and add it to the ast
|
245
|
+
reset_counter
|
246
|
+
|
247
|
+
while peek != ";" && !at_end?
|
248
|
+
advance
|
249
|
+
end
|
250
|
+
|
251
|
+
add_token(:text)
|
252
|
+
|
253
|
+
if at_end?
|
254
|
+
@error_reporter.error(@line, "Imports must be terminated correctly with a ';'.")
|
255
|
+
return
|
256
|
+
end
|
257
|
+
|
258
|
+
text_token = @tokens.last
|
259
|
+
|
260
|
+
text = text_token.lexeme.gsub(/\\|"/, '')
|
261
|
+
file = FilenameHelper.resolve_import_path(text, @line)
|
262
|
+
|
263
|
+
if file
|
264
|
+
# file is ready for scanning
|
265
|
+
content = File.read(file)
|
266
|
+
scanner = Scanner.new(content, @error_reporter)
|
267
|
+
|
268
|
+
advance # remove the semicolon
|
269
|
+
@tokens.pop # remove the text token
|
270
|
+
@tokens.pop # remove the at_rule token
|
271
|
+
|
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
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deadfire
|
4
|
+
module FrontEnd
|
5
|
+
class SelectorNode < BaseNode
|
6
|
+
attr_reader :selector, :mixin_name
|
7
|
+
|
8
|
+
def initialize(tokens)
|
9
|
+
@selector = tokens_to_selector(tokens)
|
10
|
+
@mixin_name = fetch_mixin_name_from(tokens)
|
11
|
+
end
|
12
|
+
|
13
|
+
def accept(visitor)
|
14
|
+
visitor.visit_selector_node(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def cacheable?
|
18
|
+
selector.start_with?(".")
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# TODO:
|
24
|
+
# For descendant values such as `a b`, we need to add a space between the tokens,
|
25
|
+
# otherwise all other values will be concatenated together.
|
26
|
+
def tokens_to_selector(tokens)
|
27
|
+
tokens.map(&:lexeme).join("").strip
|
28
|
+
end
|
29
|
+
|
30
|
+
# https://sass-lang.com/guide
|
31
|
+
# https://sass-lang.com/documentation/style-rules/selector
|
32
|
+
# TODO: this needs some tests and a lot more work
|
33
|
+
# not all selectors are valid mixin names
|
34
|
+
def fetch_mixin_name_from(tokens)
|
35
|
+
@_cached_mixin_name ||= begin
|
36
|
+
name = []
|
37
|
+
tokens.each do |token|
|
38
|
+
case token.type
|
39
|
+
when :right_paren, :left_paren
|
40
|
+
break
|
41
|
+
when :colon
|
42
|
+
name << token.lexeme
|
43
|
+
else
|
44
|
+
name << token.lexeme
|
45
|
+
end
|
46
|
+
end
|
47
|
+
name.join("")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deadfire
|
4
|
+
module FrontEnd
|
5
|
+
class StylesheetNode
|
6
|
+
attr_reader :statements
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@statements = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def accept(visitor)
|
13
|
+
visitor.visit_stylesheet_node(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def << (node)
|
17
|
+
@statements << node
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deadfire
|
4
|
+
module FrontEnd
|
5
|
+
class Token
|
6
|
+
attr_reader :type, :lexeme, :literal, :lineno
|
7
|
+
|
8
|
+
def initialize(type, lexeme, literal, lineno)
|
9
|
+
@type = type
|
10
|
+
@lexeme = lexeme
|
11
|
+
@literal = literal
|
12
|
+
@lineno = lineno
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
"[:#{type}] #{lexeme}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|