deadfire 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/Gemfile.lock +1 -1
- data/README.md +33 -35
- data/benchmarks/basic_benchmark.rb +8 -5
- data/benchmarks/tailwind_parser.rb +23 -0
- data/bin/console +16 -3
- data/changelog.md +21 -1
- data/lib/deadfire/ast_printer.rb +58 -0
- data/lib/deadfire/configuration.rb +7 -2
- data/lib/deadfire/css_generator.rb +66 -0
- data/lib/deadfire/error_reporter.rb +24 -0
- data/lib/deadfire/errors.rb +28 -0
- data/lib/deadfire/filename_helper.rb +29 -0
- data/lib/deadfire/front_end/apply_node.rb +44 -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 +266 -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 +88 -0
- data/lib/deadfire/parser.rb +160 -289
- data/lib/deadfire/parser_engine.rb +41 -0
- data/lib/deadfire/spec.rb +136 -0
- data/lib/deadfire/version.rb +1 -1
- data/lib/deadfire.rb +27 -5
- metadata +23 -4
- data/lib/deadfire/transformers/transformer.rb +0 -17
@@ -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
|
+
# 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
|
+
while !is_at_end?
|
23
|
+
if check(:comment)
|
24
|
+
comment = add_comment
|
25
|
+
@stylesheet << comment if Deadfire.configuration.keep_comments
|
26
|
+
elsif check(:newline)
|
27
|
+
newline = add_newline
|
28
|
+
@stylesheet << newline if Deadfire.configuration.keep_whitespace
|
29
|
+
elsif matches_at_rule?
|
30
|
+
@stylesheet << at_rule_declaration
|
31
|
+
else
|
32
|
+
@stylesheet << ruleset_declaration
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@stylesheet
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def is_at_end?
|
41
|
+
peek.type == :eof
|
42
|
+
end
|
43
|
+
|
44
|
+
def peek
|
45
|
+
tokens[current]
|
46
|
+
end
|
47
|
+
|
48
|
+
def previous
|
49
|
+
tokens[current - 1]
|
50
|
+
end
|
51
|
+
|
52
|
+
def advance
|
53
|
+
@current += 1 unless is_at_end?
|
54
|
+
previous
|
55
|
+
end
|
56
|
+
|
57
|
+
def check(type)
|
58
|
+
return false if is_at_end?
|
59
|
+
|
60
|
+
peek.type == type
|
61
|
+
end
|
62
|
+
|
63
|
+
def match?(*types)
|
64
|
+
types.each do |type|
|
65
|
+
if check(type)
|
66
|
+
advance
|
67
|
+
return true
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
false
|
72
|
+
end
|
73
|
+
|
74
|
+
def consume(type, message)
|
75
|
+
if check(type)
|
76
|
+
advance
|
77
|
+
return
|
78
|
+
end
|
79
|
+
|
80
|
+
error_reporter.error(peek.lineno, message)
|
81
|
+
end
|
82
|
+
|
83
|
+
def matches_at_rule?
|
84
|
+
check(:at_rule)
|
85
|
+
end
|
86
|
+
|
87
|
+
def matches_nested_rule?
|
88
|
+
match?(:ampersand)
|
89
|
+
end
|
90
|
+
|
91
|
+
def parse_block
|
92
|
+
block = BlockNode.new
|
93
|
+
block << previous
|
94
|
+
|
95
|
+
while !is_at_end?
|
96
|
+
if match?(:right_brace)
|
97
|
+
break
|
98
|
+
elsif matches_at_rule?
|
99
|
+
block << at_rule_declaration
|
100
|
+
elsif match?(:left_brace)
|
101
|
+
block << parse_block
|
102
|
+
else
|
103
|
+
block << advance
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
block << previous
|
108
|
+
block
|
109
|
+
end
|
110
|
+
|
111
|
+
def ruleset_declaration
|
112
|
+
values = []
|
113
|
+
while !match?(:left_brace)
|
114
|
+
values << advance
|
115
|
+
end
|
116
|
+
|
117
|
+
selector = SelectorNode.new(values[0..-1])
|
118
|
+
|
119
|
+
block = parse_block
|
120
|
+
RulesetNode.new(selector, block)
|
121
|
+
end
|
122
|
+
|
123
|
+
def add_comment
|
124
|
+
consume(:comment, "Expect comment")
|
125
|
+
CommentNode.new(previous)
|
126
|
+
end
|
127
|
+
|
128
|
+
def add_newline
|
129
|
+
consume(:newline, "Expect newline")
|
130
|
+
NewlineNode.new(previous.lexeme)
|
131
|
+
end
|
132
|
+
|
133
|
+
def at_rule_declaration
|
134
|
+
consume(:at_rule, "Expect at rule")
|
135
|
+
keyword = previous
|
136
|
+
|
137
|
+
# peek until we get to ; or {, if we reach ; then add to at rule node and return
|
138
|
+
values = []
|
139
|
+
while !match?(:semicolon, :left_brace) && !is_at_end?
|
140
|
+
values << advance
|
141
|
+
end
|
142
|
+
|
143
|
+
if previous.type == :semicolon
|
144
|
+
if keyword.lexeme == "@apply"
|
145
|
+
return ApplyNode.new(keyword, values)
|
146
|
+
else
|
147
|
+
values << previous # add the semicolon to the values
|
148
|
+
return AtRuleNode.new(keyword, values, nil)
|
149
|
+
end
|
150
|
+
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
|
+
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,266 @@
|
|
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
|
+
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 }
|
90
|
+
|
91
|
+
if peek == NEWLINE
|
92
|
+
@line += 1
|
93
|
+
@error_reporter.error(@line, "at-rule cannot be on multiple lines.")
|
94
|
+
add_token(:at_rule, current_at_rule)
|
95
|
+
elsif at_rule
|
96
|
+
token = add_token(:at_rule, "at_#{at_rule[1..-1]}")
|
97
|
+
if at_rule == "@import"
|
98
|
+
prescan_import_rule(token.last)
|
99
|
+
else
|
100
|
+
token
|
101
|
+
end
|
102
|
+
else
|
103
|
+
@error_reporter.error(@line, "Invalid at-rule.")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def add_string_token
|
108
|
+
while peek != '"' && !at_end?
|
109
|
+
@line += 1 if peek == NEWLINE
|
110
|
+
advance
|
111
|
+
end
|
112
|
+
|
113
|
+
if at_end?
|
114
|
+
@error_reporter.error(@line, "Unterminated string.")
|
115
|
+
return
|
116
|
+
end
|
117
|
+
|
118
|
+
advance
|
119
|
+
|
120
|
+
# Trim the surrounding quotes.
|
121
|
+
# TODO: this does not look right... page 50 crafting interpreters.
|
122
|
+
value = @source[@start + 2..current_char_position]
|
123
|
+
add_token(:string, value)
|
124
|
+
end
|
125
|
+
|
126
|
+
# This token is very similar to the string token, but we want to explicitly
|
127
|
+
# split up text from string, because string in css is surrounded by quotes and text is free form
|
128
|
+
# which can be a property or value e.g. `color: red;`.
|
129
|
+
def add_text_token
|
130
|
+
while text?(peek) && !at_end?
|
131
|
+
advance
|
132
|
+
end
|
133
|
+
|
134
|
+
value = @source[@start+1..current_char_position]
|
135
|
+
add_token(:text, value)
|
136
|
+
end
|
137
|
+
|
138
|
+
def add_number_token
|
139
|
+
while digit?(peek)
|
140
|
+
advance
|
141
|
+
end
|
142
|
+
|
143
|
+
# Look for a fractional part.
|
144
|
+
if peek == "." && digit?(peek_next)
|
145
|
+
# Consume the "."
|
146
|
+
advance
|
147
|
+
|
148
|
+
while digit?(peek)
|
149
|
+
advance
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
add_token(:number, @source[@start..@current].to_f)
|
154
|
+
end
|
155
|
+
|
156
|
+
def add_psuedo_selector
|
157
|
+
if peek == ":"
|
158
|
+
advance
|
159
|
+
add_token(:double_colon)
|
160
|
+
else
|
161
|
+
add_token(:colon)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def add_hypen_token
|
166
|
+
if peek == "-"
|
167
|
+
advance
|
168
|
+
add_token(:double_hyphen)
|
169
|
+
else
|
170
|
+
add_token(:hyphen)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
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
|
181
|
+
|
182
|
+
if at_end? && peek != "*"
|
183
|
+
@error_reporter.error(@line, "Unterminated comment on line #{@line}.")
|
184
|
+
else
|
185
|
+
advance # consume the *
|
186
|
+
advance # consume the /
|
187
|
+
end
|
188
|
+
add_token(:comment) # Add the comment anyway, but report an error.
|
189
|
+
else
|
190
|
+
add_token(:forward_slash)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def add_whitespace_token
|
195
|
+
add_token(:whitespace) if Deadfire.configuration.keep_whitespace
|
196
|
+
end
|
197
|
+
|
198
|
+
def add_newline_token
|
199
|
+
@line += 1
|
200
|
+
add_token(:newline) if Deadfire.configuration.keep_whitespace
|
201
|
+
end
|
202
|
+
|
203
|
+
def current_char_position
|
204
|
+
@current - 1
|
205
|
+
end
|
206
|
+
|
207
|
+
def current_char
|
208
|
+
@source[current_char_position]
|
209
|
+
end
|
210
|
+
|
211
|
+
def advance
|
212
|
+
@current += 1
|
213
|
+
current_char
|
214
|
+
end
|
215
|
+
|
216
|
+
def peek
|
217
|
+
@source[@current] unless at_end?
|
218
|
+
end
|
219
|
+
|
220
|
+
def peek_next
|
221
|
+
@source[@current + 1] unless at_end?
|
222
|
+
end
|
223
|
+
|
224
|
+
def digit?(char)
|
225
|
+
char >= "0" && char <= "9"
|
226
|
+
end
|
227
|
+
|
228
|
+
def text?(char)
|
229
|
+
(char >= "a" && char <= "z") || (char >= "A" && char <= "Z")
|
230
|
+
end
|
231
|
+
|
232
|
+
def prescan_import_rule(token)
|
233
|
+
# we want to get all the text between the @import and the semicolon
|
234
|
+
# so we can parse the file and add it to the ast
|
235
|
+
reset_counter
|
236
|
+
|
237
|
+
while peek != ";" && !at_end?
|
238
|
+
advance
|
239
|
+
end
|
240
|
+
|
241
|
+
if at_end?
|
242
|
+
@error_reporter.error(@line, "Unterminated import rule.")
|
243
|
+
return
|
244
|
+
end
|
245
|
+
|
246
|
+
add_token(:text)
|
247
|
+
advance # remove the semicolon
|
248
|
+
|
249
|
+
text_token = @tokens.last
|
250
|
+
|
251
|
+
text = text_token.lexeme.gsub(/\\|"/, '')
|
252
|
+
file = FilenameHelper.resolve_import_path(text, @line)
|
253
|
+
|
254
|
+
# file is ready for scanning
|
255
|
+
content = File.read(file)
|
256
|
+
scanner = Scanner.new(content, @error_reporter)
|
257
|
+
|
258
|
+
@tokens.pop # remove the text token
|
259
|
+
@tokens.pop # remove the at_rule token
|
260
|
+
|
261
|
+
imported_tokens = scanner.tokenize[0..-2]
|
262
|
+
@tokens.concat imported_tokens
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
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
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# Frozen_string_literal: true
|
2
|
+
|
3
|
+
module Deadfire
|
4
|
+
class Interpreter # :nodoc:
|
5
|
+
singleton_class.attr_accessor :cached_apply_rules
|
6
|
+
self.cached_apply_rules = Hash.new { |h, k| h[k] = nil }
|
7
|
+
|
8
|
+
def initialize(error_reporter)
|
9
|
+
@error_reporter = error_reporter
|
10
|
+
end
|
11
|
+
|
12
|
+
def interpret(node)
|
13
|
+
node.accept(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def visit_stylesheet_node(node)
|
17
|
+
node.statements.each { |child| child.accept(self) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def visit_at_rule_node(node)
|
21
|
+
if node.block
|
22
|
+
visit_block_node(node.block, node)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def visit_ruleset_node(node)
|
27
|
+
if node.block
|
28
|
+
visit_block_node(node.block, node)
|
29
|
+
|
30
|
+
unless Interpreter.cached_apply_rules[node.selector.selector]
|
31
|
+
Interpreter.cached_apply_rules[node.selector.selector] = node.block if node.selector.cacheable?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def visit_block_node(node, parent)
|
37
|
+
node.declarations.each do |declaration|
|
38
|
+
case declaration
|
39
|
+
when ApplyNode
|
40
|
+
apply_mixin(declaration, node)
|
41
|
+
else
|
42
|
+
# we may not need to visit this as we don't process/transform/optimize
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def visit_declaration_node(node)
|
48
|
+
node.accept(self)
|
49
|
+
end
|
50
|
+
|
51
|
+
def visit_comment_node(node)
|
52
|
+
# node.accept(self)
|
53
|
+
end
|
54
|
+
|
55
|
+
def visit_apply_node(node)
|
56
|
+
# do nothing for now
|
57
|
+
end
|
58
|
+
|
59
|
+
def visit_newline_node(node)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def apply_mixin(mixin, node)
|
65
|
+
updated_declarations = []
|
66
|
+
mixin.mixin_names.each do |mixin_name|
|
67
|
+
if Interpreter.cached_apply_rules[mixin_name]
|
68
|
+
cached_block = Interpreter.cached_apply_rules[mixin_name]
|
69
|
+
|
70
|
+
# NOTE: remove the left and right brace but we probably don't need to do this, how can this be simplified?
|
71
|
+
cached_block.declarations[1...-1].each do |cached_declaration|
|
72
|
+
updated_declarations << cached_declaration
|
73
|
+
end
|
74
|
+
updated_declarations.shift if updated_declarations.first.type == :newline
|
75
|
+
updated_declarations.pop if updated_declarations.last.type == :newline
|
76
|
+
else
|
77
|
+
@error_reporter.error(mixin.lineno, "Mixin #{mixin_name} not found") # TODO: we need the declarations lineno, not the block
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
if updated_declarations.any?
|
82
|
+
index = node.declarations.index(mixin)
|
83
|
+
node.declarations.delete_at(index)
|
84
|
+
node.declarations.insert(index, *updated_declarations)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|