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.
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deadfire
4
+ module FrontEnd
5
+ class BaseNode
6
+ def accept
7
+ raise NotImplementedError
8
+ end
9
+ end
10
+ end
11
+ 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