minicss 0.1.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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +10 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +66 -0
  5. data/ACKNOWLEDGMENTS.md +47 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/LICENSE +21 -0
  8. data/README.md +178 -0
  9. data/Rakefile +12 -0
  10. data/lib/minicss/ast/at_rule.rb +17 -0
  11. data/lib/minicss/ast/bad_token.rb +14 -0
  12. data/lib/minicss/ast/block.rb +29 -0
  13. data/lib/minicss/ast/decl.rb +17 -0
  14. data/lib/minicss/ast/decl_list.rb +18 -0
  15. data/lib/minicss/ast/dimension.rb +14 -0
  16. data/lib/minicss/ast/function.rb +15 -0
  17. data/lib/minicss/ast/number.rb +14 -0
  18. data/lib/minicss/ast/percentage.rb +8 -0
  19. data/lib/minicss/ast/rule.rb +28 -0
  20. data/lib/minicss/ast/string_token.rb +14 -0
  21. data/lib/minicss/ast/syntax_error.rb +13 -0
  22. data/lib/minicss/ast/unicode_range.rb +13 -0
  23. data/lib/minicss/ast/url.rb +13 -0
  24. data/lib/minicss/ast.rb +72 -0
  25. data/lib/minicss/css/ast/at_rule.rb +19 -0
  26. data/lib/minicss/css/ast/declaration.rb +21 -0
  27. data/lib/minicss/css/ast/declaration_list.rb +11 -0
  28. data/lib/minicss/css/ast/function.rb +20 -0
  29. data/lib/minicss/css/ast/qualified_rule.rb +19 -0
  30. data/lib/minicss/css/ast/simple_block.rb +37 -0
  31. data/lib/minicss/css/ast/stylesheet.rb +17 -0
  32. data/lib/minicss/css/ast.rb +9 -0
  33. data/lib/minicss/css/errors.rb +8 -0
  34. data/lib/minicss/css/parser.rb +360 -0
  35. data/lib/minicss/css/position.rb +15 -0
  36. data/lib/minicss/css/refinements.rb +78 -0
  37. data/lib/minicss/css/token.rb +28 -0
  38. data/lib/minicss/css/token_stream.rb +56 -0
  39. data/lib/minicss/css/tokenizer.rb +572 -0
  40. data/lib/minicss/css.rb +10 -0
  41. data/lib/minicss/errors.rb +6 -0
  42. data/lib/minicss/sel.rb +382 -0
  43. data/lib/minicss/serializer.rb +59 -0
  44. data/lib/minicss/version.rb +5 -0
  45. data/lib/minicss.rb +53 -0
  46. metadata +87 -0
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniCSS
4
+ module AST
5
+ class SyntaxError
6
+ attr_reader :reason
7
+
8
+ def initialize(reason)
9
+ @reason = reason
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniCSS
4
+ module AST
5
+ class UnicodeRange
6
+ attr_accessor :range_start, :range_end
7
+
8
+ def initialize(css)
9
+ @range_start, @range_end = css.opts.slice(:start, :end).values
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniCSS
4
+ module AST
5
+ class URL
6
+ attr_accessor :value
7
+
8
+ def initialize(value)
9
+ @value = value
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ast/rule"
4
+ require_relative "ast/decl"
5
+ require_relative "ast/decl_list"
6
+ require_relative "ast/at_rule"
7
+ require_relative "ast/function"
8
+ require_relative "ast/block"
9
+ require_relative "ast/number"
10
+ require_relative "ast/dimension"
11
+ require_relative "ast/percentage"
12
+ require_relative "ast/bad_token"
13
+ require_relative "ast/url"
14
+ require_relative "ast/unicode_range"
15
+ require_relative "ast/syntax_error"
16
+ require_relative "ast/string_token"
17
+
18
+ module MiniCSS
19
+ module AST
20
+ module_function
21
+
22
+ def convert(css)
23
+ case css
24
+ when CSS::AST::Stylesheet
25
+ css.rules.map { convert(it) }
26
+ when CSS::AST::QualifiedRule
27
+ Rule.new(css)
28
+ when CSS::AST::Declaration
29
+ Decl.new(css)
30
+ when CSS::AST::AtRule
31
+ AtRule.new(css)
32
+ when CSS::AST::DeclarationList
33
+ DeclList.new(css.map { convert(it) })
34
+ when Array
35
+ css.map { convert(it) }
36
+ when CSS::AST::Function
37
+ Function.new(css)
38
+ when CSS::Token
39
+ convert_token(css)
40
+ when CSS::AST::SimpleBlock
41
+ Block.new(css)
42
+ when CSS::SyntaxError
43
+ SyntaxError.new(css.message)
44
+ else
45
+ raise "Unexpected input type #{css.class} for conversion"
46
+ end
47
+ end
48
+
49
+ def convert_token(css)
50
+ case css.kind
51
+ when :delim, :ident, :whitespace, :cdc, :at_keyword, :hash, :cdo, :colon, :comma, :semicolon, :right_square_bracket, :right_parenthesis
52
+ css.literal
53
+ when :dimension
54
+ Dimension.new(css)
55
+ when :number
56
+ Number.new(css)
57
+ when :percentage
58
+ Percentage.new(css)
59
+ when :bad_string, :bad_url
60
+ BadToken.new(css)
61
+ when :url
62
+ URL.new(css.opts[:value])
63
+ when :unicode_range
64
+ UnicodeRange.new(css)
65
+ when :string
66
+ StringToken.new(css)
67
+ else
68
+ raise "Unexpected token in conversion pipeline: #{css.class} #{css.inspect}"
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniCSS
4
+ module CSS
5
+ module AST
6
+ class AtRule
7
+ attr_accessor :name, :prelude, :child_rules
8
+
9
+ def initialize(name:, prelude: nil, child_rules: nil)
10
+ @name = name
11
+ @prelude = prelude || []
12
+ @child_rules = child_rules || []
13
+ end
14
+
15
+ def kind = :at_rule
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniCSS
4
+ module CSS
5
+ module AST
6
+ class Declaration
7
+ attr_accessor :name, :value, :important, :original_text
8
+
9
+ def initialize(name: "", value: nil)
10
+ @value = value || []
11
+ @name = name
12
+ @important = false
13
+ @original_text = nil
14
+ end
15
+
16
+ def important? = @important
17
+ def kind = :declaration
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniCSS
4
+ module CSS
5
+ module AST
6
+ class DeclarationList < Array
7
+ def kind = :declaration_list
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniCSS
4
+ module CSS
5
+ module AST
6
+ class Function
7
+ attr_accessor :name, :value
8
+
9
+ def initialize(name: "", value: nil)
10
+ @value = value || []
11
+ @name = name
12
+ end
13
+
14
+ def kind = :function
15
+
16
+ def literal = [name.literal, value.map(&:literal)].join
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniCSS
4
+ module CSS
5
+ module AST
6
+ class QualifiedRule
7
+ attr_accessor :prelude, :decls, :child_rules
8
+
9
+ def initialize(prelude: nil, decls: nil, child_rules: nil)
10
+ @prelude = prelude || []
11
+ @decls = decls || []
12
+ @child_rules = child_rules || []
13
+ end
14
+
15
+ def kind = :qualified_rule
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniCSS
4
+ module CSS
5
+ module AST
6
+ class SimpleBlock
7
+ RIGHT_TOKENS = {
8
+ left_parenthesis: ")",
9
+ left_curly: "}",
10
+ left_square_bracket: "]"
11
+ }.freeze
12
+
13
+ LEFT_TOKENS = {
14
+ left_parenthesis: "(",
15
+ left_curly: "{",
16
+ left_square_bracket: "["
17
+ }.freeze
18
+
19
+ attr_accessor :associated_token, :value
20
+
21
+ def initialize(associated_token:)
22
+ @associated_token = associated_token
23
+ @value = []
24
+ end
25
+
26
+ def kind = :simple_block
27
+
28
+ def right_token = RIGHT_TOKENS[associated_token]
29
+ def left_token = LEFT_TOKENS[associated_token]
30
+
31
+ def literal
32
+ [left_token, @value.map(&:literal), right_token].join
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniCSS
4
+ module CSS
5
+ module AST
6
+ class Stylesheet
7
+ attr_accessor :rules
8
+
9
+ def initialize(rules: nil)
10
+ @rules = rules || []
11
+ end
12
+
13
+ def kind = :stylesheet
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ast/stylesheet"
4
+ require_relative "ast/at_rule"
5
+ require_relative "ast/qualified_rule"
6
+ require_relative "ast/declaration"
7
+ require_relative "ast/function"
8
+ require_relative "ast/declaration_list"
9
+ require_relative "ast/simple_block"
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniCSS
4
+ module CSS
5
+ class InvalidRuleError < Error; end
6
+ class SyntaxError < Error; end
7
+ end
8
+ end
@@ -0,0 +1,360 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniCSS
4
+ module CSS
5
+ class Parser
6
+ using StringRefinements
7
+
8
+ attr_reader :stream
9
+
10
+ def initialize(tokens)
11
+ @stream = TokenStream.new(tokens)
12
+ @tokens = tokens
13
+ end
14
+
15
+ # Entrypoints
16
+
17
+ def parse_stylesheet
18
+ AST::Stylesheet.new(rules: consume_stylesheet_contents)
19
+ end
20
+
21
+ def parse_stylesheet_contents
22
+ consume_stylesheet_contents
23
+ end
24
+
25
+ def parse_block_contents
26
+ consume_block_contents
27
+ end
28
+
29
+ def parse_rule
30
+ stream.discard_whitespace
31
+ return SyntaxError.new("empty") if stream.empty?
32
+
33
+ result = if stream.peek.kind == :at_keyword
34
+ consume_at_rule
35
+ else
36
+ consume_qualified_rule
37
+ end
38
+ return SyntaxError.new("invalid") unless result
39
+
40
+ stream.discard_whitespace
41
+ return SyntaxError.new("extra-input") unless stream.empty?
42
+
43
+ result
44
+ end
45
+
46
+ def parse_declaration
47
+ stream.discard_whitespace
48
+ return SyntaxError.new("empty") if stream.empty?
49
+
50
+ decl = consume_declaration
51
+ return SyntaxError.new("invalid") unless decl
52
+
53
+ decl
54
+ end
55
+
56
+ def parse_component_value
57
+ stream.discard_whitespace
58
+ return SyntaxError.new("empty") if stream.empty?
59
+
60
+ value = consume_component_value
61
+ stream.discard_whitespace
62
+ return SyntaxError.new("extra-input") unless stream.empty?
63
+
64
+ value
65
+ end
66
+
67
+ def parse_component_value_list
68
+ consume_component_value_list
69
+ end
70
+
71
+ def parse_component_value_comma_list
72
+ groups = []
73
+ until stream.empty?
74
+ groups << consume_component_value_list(stop: :comma)
75
+ stream.discard
76
+ end
77
+ groups
78
+ end
79
+
80
+ # Helpers
81
+
82
+ def assert_next_token(kind)
83
+ stream.peek.kind == kind
84
+ end
85
+
86
+ # Parsers
87
+
88
+ def consume_stylesheet_contents
89
+ rules = []
90
+ loop do
91
+ case stream.peek.kind
92
+ when :whitespace, :cdo, :cdc
93
+ stream.discard
94
+
95
+ when :eof
96
+ return rules
97
+
98
+ when :at_keyword
99
+ rule = consume_at_rule
100
+ rules << rule if rule
101
+
102
+ else
103
+ rule = consume_qualified_rule
104
+ rules << rule if rule
105
+ end
106
+ end
107
+ end
108
+
109
+ def consume_at_rule(nested: false)
110
+ return unless assert_next_token(:at_keyword)
111
+
112
+ rule = AST::AtRule.new(name: stream.consume)
113
+ loop do
114
+ case stream.peek.kind
115
+ when :semicolon, :eof
116
+ stream.discard
117
+ # TODO: If rule is valid in the current context, return it; otherwise return nothing.
118
+ return rule
119
+
120
+ when :right_curly
121
+ if nested
122
+ # TODO: If rule is valid in the current context, return it. Otherwise, return nothing.
123
+ return rule
124
+ end
125
+
126
+ rule.prelude << consume
127
+
128
+ when :left_curly
129
+ consume_block.each { rule.child_rules << it }
130
+ return rule
131
+
132
+ else
133
+ rule.prelude << consume_component_value
134
+ end
135
+ end
136
+ end
137
+
138
+ def consume_qualified_rule(stop: nil, nested: false)
139
+ rule = AST::QualifiedRule.new
140
+
141
+ loop do
142
+ case stream.peek.kind
143
+ when :eof, stop
144
+ return nil
145
+
146
+ when :right_curly
147
+ return nil if nested
148
+
149
+ rule.prelude << consume
150
+
151
+ when :left_curly
152
+ non_ws = rule.prelude.reject { it.kind == :whitespace }
153
+ first, second = non_ws.first(2)
154
+
155
+ if first&.kind == :ident && first.literal.start_with?("--") && second&.kind == :colon
156
+ nested ? consume_bad_declaration : consume_block
157
+ return nil
158
+ end
159
+
160
+ child_rules = consume_block
161
+
162
+ rule.decls = child_rules.shift if child_rules&.first.is_a?(AST::DeclarationList)
163
+
164
+ # Replace declaration lists with nested declaration rules
165
+ rule.child_rules = child_rules.map do |item|
166
+ item.is_a?(AST::DeclarationList) ? AST::NestedDeclarationRules.new(child: item) : item
167
+ end
168
+
169
+ # TODO: If rule is valid in the current context, return it; otherwise
170
+ # return an invalid rule error.
171
+ return rule
172
+
173
+ else
174
+ rule.prelude << consume_component_value
175
+ end
176
+ end
177
+ end
178
+
179
+ def consume_block
180
+ return unless assert_next_token(:left_curly)
181
+
182
+ stream.discard
183
+ rules = consume_block_contents
184
+ stream.discard
185
+ rules
186
+ end
187
+
188
+ def consume_block_contents
189
+ rules = []
190
+ decls = AST::DeclarationList.new
191
+
192
+ loop do
193
+ case stream.peek.kind
194
+ when :whitespace, :semicolon
195
+ stream.discard
196
+ when :eof, :right_curly
197
+ rules << decls unless decls.empty?
198
+ return rules
199
+ when :at_keyword
200
+ unless decls.empty?
201
+ rules << decls
202
+ decls = AST::DeclarationList.new
203
+ end
204
+ at = consume_at_rule(nested: true)
205
+ rules << at if at.is_a? AST::AtRule
206
+ else
207
+ stream.mark
208
+ decl = consume_declaration(nested: true)
209
+ if decl.is_a? AST::Declaration
210
+ decls << decl
211
+ stream.pop
212
+ next
213
+ end
214
+
215
+ stream.restore
216
+ rule = consume_qualified_rule(nested: true)
217
+ if rule.is_a? InvalidRuleError
218
+ unless decls.empty?
219
+ rules << decls
220
+ decls = AST::DeclarationList.new
221
+ end
222
+ elsif rule.is_a? AST::QualifiedRule
223
+ unless decls.empty?
224
+ rules << decls
225
+ decls = AST::DeclarationList.new
226
+ end
227
+ rules << rule
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ def consume_declaration(nested: false)
234
+ decl = AST::Declaration.new
235
+ if stream.peek.kind == :ident
236
+ decl.name = stream.consume
237
+ else
238
+ consume_bad_declaration(nested:)
239
+ return nil
240
+ end
241
+
242
+ stream.discard_whitespace
243
+
244
+ if stream.peek.kind == :colon
245
+ stream.discard
246
+ else
247
+ consume_bad_declaration(nested:)
248
+ return nil
249
+ end
250
+
251
+ stream.discard_whitespace
252
+
253
+ decl.value = consume_component_value_list(nested:, stop: :semicolon)
254
+
255
+ l1, l2 = decl.value.reject { it.kind == :whitespace }.last(2)
256
+ if l1 && l2 && l1.kind == :delim && l1.literal == "!" && l2.kind == :ident && l2.literal.downcase == "important"
257
+ decl.value.delete(l1)
258
+ decl.value.delete(l2)
259
+ decl.important = true
260
+ end
261
+ decl.value.pop while decl.value.last&.kind == :whitespace
262
+ decl.original_text = decl.value.map(&:literal).join if decl.name.literal.start_with?("--")
263
+
264
+ # Otherwise, if decl’s value contains a top-level simple block
265
+ # with an associated token of <{-token>, and also contains any other
266
+ # non-<whitespace-token> value, return nothing. (That is, a top-level
267
+ # {}-block is only allowed as the entire value of a non-custom property.)
268
+ block = decl.value.find { it.is_a?(AST::SimpleBlock) && it.associated_token == :left_curly }
269
+ if block
270
+ invalid = decl.value.any? { it != block && it.kind != :whitespace }
271
+ return nil if invalid
272
+ end
273
+
274
+ decl
275
+ end
276
+
277
+ def consume_bad_declaration(nested:)
278
+ loop do
279
+ case stream.peek.kind
280
+ when :eof, :semicolon
281
+ stream.discard
282
+ return nil
283
+ when :right_curly
284
+ return if nested
285
+
286
+ stream.discard
287
+ else
288
+ consume_component_value
289
+ end
290
+ end
291
+ end
292
+
293
+ def consume_component_value_list(stop: nil, nested: false)
294
+ values = []
295
+ loop do
296
+ case stream.peek.kind
297
+ when :eof, stop
298
+ return values
299
+
300
+ when :right_curly
301
+ return values if nested
302
+
303
+ values << stream.consume
304
+
305
+ else
306
+ values << consume_component_value
307
+ end
308
+ end
309
+ end
310
+
311
+ def consume_component_value
312
+ case stream.peek.kind
313
+ when :left_curly, :left_square_bracket, :left_parenthesis
314
+ consume_simple_block
315
+ when :function
316
+ consume_function
317
+ else
318
+ stream.consume
319
+ end
320
+ end
321
+
322
+ def consume_simple_block
323
+ return if !assert_next_token(:left_curly) && !assert_next_token(:left_square_bracket) && !assert_next_token(:left_parenthesis)
324
+
325
+ block = AST::SimpleBlock.new(associated_token: stream.consume.kind)
326
+ ending_token = {
327
+ left_curly: :right_curly,
328
+ left_parenthesis: :right_parenthesis,
329
+ left_square_bracket: :right_square_bracket
330
+ }[block.associated_token]
331
+
332
+ loop do
333
+ case stream.peek.kind
334
+ when :eof, ending_token
335
+ stream.discard
336
+ return block
337
+ else
338
+ block.value << consume_component_value
339
+ end
340
+ end
341
+ end
342
+
343
+ def consume_function
344
+ return unless assert_next_token(:function)
345
+
346
+ func = AST::Function.new(name: stream.consume)
347
+
348
+ loop do
349
+ case stream.peek.kind
350
+ when :eof, :right_parenthesis
351
+ stream.discard
352
+ return func
353
+ else
354
+ func.value << consume_component_value
355
+ end
356
+ end
357
+ end
358
+ end
359
+ end
360
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniCSS
4
+ module CSS
5
+ class Position
6
+ attr_reader :offset, :line, :column
7
+
8
+ def initialize(offset, line, column)
9
+ @offset = offset
10
+ @line = line
11
+ @column = column
12
+ end
13
+ end
14
+ end
15
+ end