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.
- checksums.yaml +7 -0
- data/.editorconfig +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +66 -0
- data/ACKNOWLEDGMENTS.md +47 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE +21 -0
- data/README.md +178 -0
- data/Rakefile +12 -0
- data/lib/minicss/ast/at_rule.rb +17 -0
- data/lib/minicss/ast/bad_token.rb +14 -0
- data/lib/minicss/ast/block.rb +29 -0
- data/lib/minicss/ast/decl.rb +17 -0
- data/lib/minicss/ast/decl_list.rb +18 -0
- data/lib/minicss/ast/dimension.rb +14 -0
- data/lib/minicss/ast/function.rb +15 -0
- data/lib/minicss/ast/number.rb +14 -0
- data/lib/minicss/ast/percentage.rb +8 -0
- data/lib/minicss/ast/rule.rb +28 -0
- data/lib/minicss/ast/string_token.rb +14 -0
- data/lib/minicss/ast/syntax_error.rb +13 -0
- data/lib/minicss/ast/unicode_range.rb +13 -0
- data/lib/minicss/ast/url.rb +13 -0
- data/lib/minicss/ast.rb +72 -0
- data/lib/minicss/css/ast/at_rule.rb +19 -0
- data/lib/minicss/css/ast/declaration.rb +21 -0
- data/lib/minicss/css/ast/declaration_list.rb +11 -0
- data/lib/minicss/css/ast/function.rb +20 -0
- data/lib/minicss/css/ast/qualified_rule.rb +19 -0
- data/lib/minicss/css/ast/simple_block.rb +37 -0
- data/lib/minicss/css/ast/stylesheet.rb +17 -0
- data/lib/minicss/css/ast.rb +9 -0
- data/lib/minicss/css/errors.rb +8 -0
- data/lib/minicss/css/parser.rb +360 -0
- data/lib/minicss/css/position.rb +15 -0
- data/lib/minicss/css/refinements.rb +78 -0
- data/lib/minicss/css/token.rb +28 -0
- data/lib/minicss/css/token_stream.rb +56 -0
- data/lib/minicss/css/tokenizer.rb +572 -0
- data/lib/minicss/css.rb +10 -0
- data/lib/minicss/errors.rb +6 -0
- data/lib/minicss/sel.rb +382 -0
- data/lib/minicss/serializer.rb +59 -0
- data/lib/minicss/version.rb +5 -0
- data/lib/minicss.rb +53 -0
- metadata +87 -0
data/lib/minicss/ast.rb
ADDED
@@ -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,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,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,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
|