p_css 0.2.0.beta1-aarch64-linux

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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +282 -0
  3. data/Cargo.toml +3 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +357 -0
  6. data/ext/css_native/Cargo.toml +12 -0
  7. data/ext/css_native/extconf.rb +4 -0
  8. data/ext/css_native/src/lib.rs +117 -0
  9. data/ext/css_native/src/matcher.rs +356 -0
  10. data/ext/css_native/src/selectors.rs +411 -0
  11. data/ext/css_native/src/snapshot.rs +370 -0
  12. data/ext/css_native/src/state.rs +174 -0
  13. data/ext/css_native/src/tokenizer.rs +596 -0
  14. data/lib/css/3.3/css_native.so +0 -0
  15. data/lib/css/3.4/css_native.so +0 -0
  16. data/lib/css/4.0/css_native.so +0 -0
  17. data/lib/css/cascade.rb +277 -0
  18. data/lib/css/code_points.rb +59 -0
  19. data/lib/css/escape.rb +82 -0
  20. data/lib/css/media_queries/context.rb +60 -0
  21. data/lib/css/media_queries/evaluator.rb +157 -0
  22. data/lib/css/media_queries/nodes.rb +41 -0
  23. data/lib/css/media_queries/parser.rb +374 -0
  24. data/lib/css/media_queries.rb +9 -0
  25. data/lib/css/native.rb +179 -0
  26. data/lib/css/nesting.rb +229 -0
  27. data/lib/css/nodes.rb +42 -0
  28. data/lib/css/parser.rb +429 -0
  29. data/lib/css/selectors/anb_parser.rb +174 -0
  30. data/lib/css/selectors/matcher.rb +545 -0
  31. data/lib/css/selectors/nodes.rb +61 -0
  32. data/lib/css/selectors/parser.rb +395 -0
  33. data/lib/css/selectors/serializer.rb +102 -0
  34. data/lib/css/selectors/specificity.rb +81 -0
  35. data/lib/css/selectors.rb +11 -0
  36. data/lib/css/serializer.rb +167 -0
  37. data/lib/css/token.rb +107 -0
  38. data/lib/css/token_cursor.rb +49 -0
  39. data/lib/css/tokenizer.rb +447 -0
  40. data/lib/css/urange.rb +45 -0
  41. data/lib/css/version.rb +3 -0
  42. data/lib/css.rb +73 -0
  43. data/lib/p_css.rb +1 -0
  44. data/sig/css/cascade.rbs +22 -0
  45. data/sig/css/media_queries.rbs +107 -0
  46. data/sig/css/nodes.rbs +76 -0
  47. data/sig/css/selectors.rbs +164 -0
  48. data/sig/css/token.rbs +33 -0
  49. data/sig/css.rbs +99 -0
  50. metadata +113 -0
@@ -0,0 +1,229 @@
1
+ module CSS
2
+ # Desugars CSS Nesting Module Level 1 — replaces `&` with the parent
3
+ # selector and lifts every nested rule out of its parent. The result is
4
+ # a flat Stylesheet whose rules contain only declarations and at-rules.
5
+ #
6
+ # The substitution favors readable output over conservative correctness
7
+ # padding: `:is(parent)` wrapping is only emitted when the parent has
8
+ # multiple selectors, or when a non-lone `&` is mixed into a compound and
9
+ # the parent has combinators. In simple cases the parent is inlined
10
+ # directly (`& .x` with parent `.a` → `.a .x`, not `:is(.a) .x`).
11
+ module Nesting
12
+ extend self
13
+
14
+ def desugar(stylesheet)
15
+ Nodes::Stylesheet.new(rules: stylesheet.rules.flat_map { desugar_top_level(_1) })
16
+ end
17
+
18
+ private
19
+
20
+ def desugar_top_level(rule)
21
+ case rule
22
+ when Nodes::QualifiedRule
23
+ desugar_qualified_rule(rule, parent_list: nil)
24
+ when Nodes::AtRule
25
+ [desugar_at_rule(rule, parent_list: nil)]
26
+ else
27
+ [rule]
28
+ end
29
+ end
30
+
31
+ # Returns an Array of rules. The first emitted rule (when there are
32
+ # declarations) carries the parent's own declarations under the
33
+ # effective selector; subsequent rules are the nested ones recursively
34
+ # desugared.
35
+ def desugar_qualified_rule(rule, parent_list:)
36
+ own = Selectors::Parser.parse_selector_list(rule.prelude)
37
+ effective = parent_list ? substitute_nesting(own, parent_list) : own
38
+
39
+ decls, rules = partition_block_items(rule.block.items, parent_list: effective)
40
+
41
+ output = []
42
+ output << build_rule_with(effective, decls) unless decls.empty?
43
+ output.concat(rules)
44
+ output
45
+ end
46
+
47
+ # Desugars a top-level (or nested) at-rule, recursing into its block
48
+ # if any.
49
+ def desugar_at_rule(rule, parent_list:)
50
+ return rule unless rule.block
51
+
52
+ new_items = desugar_at_rule_block_items(rule.block.items, parent_list:)
53
+
54
+ Nodes::AtRule.new(
55
+ name: rule.name,
56
+ prelude: rule.prelude,
57
+ block: Nodes::Block.new(items: new_items)
58
+ )
59
+ end
60
+
61
+ # Inside an at-rule's block (e.g. `@media`), declarations need to be
62
+ # wrapped in a synthesized rule using the parent selector if there is
63
+ # one (the at-rule itself doesn't carry a selector).
64
+ def desugar_at_rule_block_items(items, parent_list:)
65
+ decls, rules = partition_block_items(items, parent_list:)
66
+
67
+ output = []
68
+
69
+ unless decls.empty?
70
+ if parent_list
71
+ output << build_rule_with(parent_list, decls)
72
+ else
73
+ output.concat(decls)
74
+ end
75
+ end
76
+
77
+ output.concat(rules)
78
+ output
79
+ end
80
+
81
+ # Splits items into (declarations, nested rules). Nested qualified
82
+ # rules and at-rules are recursively desugared.
83
+ def partition_block_items(items, parent_list:)
84
+ decls = []
85
+ rules = []
86
+
87
+ items.each {|item|
88
+ case item
89
+ when Nodes::Declaration
90
+ decls << item
91
+ when Nodes::QualifiedRule
92
+ rules.concat(desugar_qualified_rule(item, parent_list:))
93
+ when Nodes::AtRule
94
+ rules << desugar_at_rule(item, parent_list:)
95
+ else
96
+ decls << item
97
+ end
98
+ }
99
+
100
+ [decls, rules]
101
+ end
102
+
103
+ def build_rule_with(selector_list, items)
104
+ Nodes::QualifiedRule.new(
105
+ prelude: Parser.parse_component_values(Selectors::Serializer.serialize(selector_list)),
106
+ block: Nodes::Block.new(items:)
107
+ )
108
+ end
109
+
110
+ # Substitution
111
+ # ----------------------------------------------------------------
112
+
113
+ def substitute_nesting(own_list, parent_list)
114
+ Selectors::SelectorList.new(
115
+ selectors: own_list.selectors.map {|sel|
116
+ substitute_complex(ensure_nesting(sel), parent_list)
117
+ }
118
+ )
119
+ end
120
+
121
+ # Per CSS Nesting §3.1, a selector that doesn't reference `&` has it
122
+ # implicitly prepended with the descendant combinator. `.b` becomes
123
+ # `& .b`.
124
+ def ensure_nesting(complex_selector)
125
+ return complex_selector if contains_nesting?(complex_selector)
126
+
127
+ Selectors::ComplexSelector.new(
128
+ compounds: [
129
+ Selectors::CompoundSelector.new(components: [Selectors::NestingSelector.new]),
130
+ *complex_selector.compounds
131
+ ],
132
+ combinators: [:descendant, *complex_selector.combinators]
133
+ )
134
+ end
135
+
136
+ def contains_nesting?(complex_selector)
137
+ complex_selector.compounds.any? {|c|
138
+ c.components.any? { _1.is_a?(Selectors::NestingSelector) }
139
+ }
140
+ end
141
+
142
+ def substitute_complex(own_complex, parent_list)
143
+ if parent_list.selectors.size > 1
144
+ substitute_with_is(own_complex, parent_list)
145
+ else
146
+ parent = parent_list.selectors.first
147
+
148
+ if parent.compounds.size > 1
149
+ substitute_inline_compounds(own_complex, parent, parent_list)
150
+ else
151
+ substitute_inline_components(own_complex, parent.compounds.first)
152
+ end
153
+ end
154
+ end
155
+
156
+ # Multi-selector parent: every `&` becomes `:is(parent_list)`, regardless
157
+ # of the surrounding compound shape.
158
+ def substitute_with_is(own_complex, parent_list)
159
+ replacement = Selectors::PseudoClass.new(name: 'is', argument: parent_list)
160
+
161
+ Selectors::ComplexSelector.new(
162
+ compounds: own_complex.compounds.map { swap_components_in_compound(_1, replacement) },
163
+ combinators: own_complex.combinators
164
+ )
165
+ end
166
+
167
+ # Single complex parent with multiple compounds: a lone `&` compound is
168
+ # spliced in by the parent's compound chain (carrying combinators);
169
+ # mixed compounds use `:is()` wrapping.
170
+ def substitute_inline_compounds(own_complex, parent_complex, parent_list)
171
+ replacement = Selectors::PseudoClass.new(name: 'is', argument: parent_list)
172
+ new_compounds = []
173
+ new_combos = []
174
+ pair_combos = [nil, *own_complex.combinators]
175
+
176
+ own_complex.compounds.each_with_index {|compound, i|
177
+ incoming = pair_combos[i]
178
+
179
+ if lone_nesting?(compound)
180
+ parent_complex.compounds.each_with_index {|pc, j|
181
+ new_compounds << pc
182
+
183
+ if j.zero?
184
+ new_combos << incoming unless incoming.nil?
185
+ else
186
+ new_combos << parent_complex.combinators[j - 1]
187
+ end
188
+ }
189
+ else
190
+ new_compounds << swap_components_in_compound(compound, replacement)
191
+ new_combos << incoming unless incoming.nil?
192
+ end
193
+ }
194
+
195
+ Selectors::ComplexSelector.new(compounds: new_compounds, combinators: new_combos)
196
+ end
197
+
198
+ # Single compound parent: components are spliced in place of `&`,
199
+ # producing a clean compound (`&.x` with parent `.a` → `.a.x`).
200
+ def substitute_inline_components(own_complex, parent_compound)
201
+ Selectors::ComplexSelector.new(
202
+ compounds: own_complex.compounds.map {|compound|
203
+ if lone_nesting?(compound)
204
+ Selectors::CompoundSelector.new(components: parent_compound.components.dup)
205
+ else
206
+ Selectors::CompoundSelector.new(
207
+ components: compound.components.flat_map {|x|
208
+ x.is_a?(Selectors::NestingSelector) ? parent_compound.components : [x]
209
+ }
210
+ )
211
+ end
212
+ },
213
+ combinators: own_complex.combinators
214
+ )
215
+ end
216
+
217
+ def swap_components_in_compound(compound, replacement)
218
+ Selectors::CompoundSelector.new(
219
+ components: compound.components.map {|x|
220
+ x.is_a?(Selectors::NestingSelector) ? replacement : x
221
+ }
222
+ )
223
+ end
224
+
225
+ def lone_nesting?(compound)
226
+ compound.components.size == 1 && compound.components.first.is_a?(Selectors::NestingSelector)
227
+ end
228
+ end
229
+ end
data/lib/css/nodes.rb ADDED
@@ -0,0 +1,42 @@
1
+ module CSS
2
+ module Nodes
3
+ # A complete stylesheet: a list of top-level rules.
4
+ Stylesheet = Data.define(:rules)
5
+
6
+ # An at-rule, e.g. `@media (min-width: 600px) { ... }` or `@charset "UTF-8";`.
7
+ # `prelude` is an array of component values, `block` is a Block or nil.
8
+ AtRule = Data.define(:name, :prelude, :block)
9
+
10
+ # A qualified rule, i.e. a style rule. `prelude` is the selector list as
11
+ # raw component values, `block` is the body.
12
+ QualifiedRule = Data.define(:prelude, :block)
13
+
14
+ # The body of a qualified rule or at-rule. Contains a list of declarations,
15
+ # nested qualified rules, and nested at-rules in source order.
16
+ Block = Data.define(:items)
17
+
18
+ # `name: value [!important]`.
19
+ Declaration = Data.define(:name, :value, :important)
20
+
21
+ # A function reference like `rgb(255, 0, 0)`. `value` is an array of
22
+ # component values.
23
+ Function = Data.define(:name, :value)
24
+
25
+ # A `( ... )`, `[ ... ]`, or `{ ... }` block as a component value.
26
+ # `open` is one of `(`, `[`, `{`. `value` is an array of component values.
27
+ SimpleBlock = Data.define(:open, :value) do
28
+ def braced? = open == '{'
29
+ def bracketed? = open == '['
30
+ def parenthesized? = open == '('
31
+ end
32
+
33
+ # An inclusive code-point range, e.g. `U+0-7F`. Result of CSS.parse_urange.
34
+ UnicodeRange = Data.define(:first, :last) do
35
+ def cover?(cp) = (first..last).cover?(cp)
36
+
37
+ def to_s
38
+ first == last ? format('U+%X', first) : format('U+%X-%X', first, last)
39
+ end
40
+ end
41
+ end
42
+ end
data/lib/css/parser.rb ADDED
@@ -0,0 +1,429 @@
1
+ module CSS
2
+ # Parser based on CSS Syntax Module Level 4 §5, with the nesting extensions
3
+ # described in CSS Nesting Module Level 1 / Syntax 4.
4
+ # https://drafts.csswg.org/css-syntax/
5
+ class Parser
6
+ include Nodes
7
+
8
+ # Shared sentinel returned past the end of the token stream. It has no
9
+ # position, which is why ParseError messages at EOF show no `line:col:`
10
+ # prefix.
11
+ EOF_TOKEN = Token.new(:eof).freeze
12
+
13
+ class << self
14
+ def parse_stylesheet(input, **opts) = build(input, **opts).parse_stylesheet
15
+ def parse_rule(input, **opts) = build(input, **opts).parse_rule
16
+ def parse_declaration(input, **opts) = build(input, **opts).parse_declaration
17
+ def parse_block_contents(input, **opts) = build(input, **opts).parse_block_contents
18
+ def parse_component_value(input, **opts) = build(input, **opts).parse_component_value
19
+ def parse_component_values(input, **opts) = build(input, **opts).parse_component_values
20
+ def parse_comma_separated_values(input, **opts) = build(input, **opts).parse_comma_separated_values
21
+
22
+ private
23
+
24
+ def build(input, **opts)
25
+ tokens = input.is_a?(String) ? Tokenizer.new(input, **opts).tokenize : input.to_a
26
+ new(tokens)
27
+ end
28
+ end
29
+
30
+ def initialize(tokens)
31
+ @tokens = tokens
32
+ @pos = 0
33
+ end
34
+
35
+ # §5.3.3.
36
+ def parse_stylesheet
37
+ Stylesheet.new(rules: consume_rule_list(top_level: true))
38
+ end
39
+
40
+ # §5.3.4.
41
+ def parse_rule
42
+ skip_whitespace
43
+
44
+ parse_error!('expected a rule, got end of input') if peek.type == :eof
45
+
46
+ rule = if peek.type == :at_keyword
47
+ consume_at_rule(nested: false)
48
+ else
49
+ consume_qualified_rule(nested: false)
50
+ end
51
+
52
+ parse_error!('invalid rule') if rule.nil?
53
+
54
+ skip_whitespace
55
+
56
+ parse_error!("unexpected token after rule: #{peek.type}") unless peek.type == :eof
57
+
58
+ rule
59
+ end
60
+
61
+ # §5.3.5. Per spec, trailing tokens after the declaration are ignored.
62
+ def parse_declaration
63
+ skip_whitespace
64
+
65
+ parse_error!('expected a declaration') unless peek.type == :ident
66
+
67
+ decl = try_consume_declaration
68
+
69
+ parse_error!('invalid declaration') unless decl
70
+
71
+ decl
72
+ end
73
+
74
+ # §5.4.4. Used for parsing the contents of a `style="..."` attribute,
75
+ # `@page` blocks, and similar contexts where there is no enclosing `{}`.
76
+ def parse_block_contents
77
+ Block.new(items: collect_block_items(stop_at_close_brace: false))
78
+ end
79
+
80
+ # §5.3.7.
81
+ def parse_component_value
82
+ skip_whitespace
83
+
84
+ parse_error!('expected a component value') if peek.type == :eof
85
+
86
+ cv = consume_component_value
87
+
88
+ skip_whitespace
89
+
90
+ parse_error!("unexpected token after component value: #{peek.type}") unless peek.type == :eof
91
+
92
+ cv
93
+ end
94
+
95
+ # §5.3.8.
96
+ def parse_component_values
97
+ values = []
98
+ values << consume_component_value until peek.type == :eof
99
+ values
100
+ end
101
+
102
+ # §5.3.9. Empty input produces `[[]]`; a trailing comma produces a
103
+ # trailing empty group.
104
+ def parse_comma_separated_values
105
+ groups = []
106
+ current = []
107
+
108
+ loop do
109
+ case peek.type
110
+ when :eof
111
+ groups << current
112
+ return groups
113
+ when :comma
114
+ consume
115
+ groups << current
116
+ current = []
117
+ else
118
+ current << consume_component_value
119
+ end
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def parse_error!(message)
126
+ raise ParseError.new(message, position: peek.position)
127
+ end
128
+
129
+ def peek(offset = 0)
130
+ @tokens[@pos + offset] || EOF_TOKEN
131
+ end
132
+
133
+ def consume
134
+ tok = @tokens[@pos] || EOF_TOKEN
135
+ @pos += 1
136
+ tok
137
+ end
138
+
139
+ def reconsume
140
+ @pos -= 1
141
+ end
142
+
143
+ # Skips whitespace and (when comments are preserved) comment tokens.
144
+ def skip_whitespace
145
+ consume while peek.trivia?
146
+ end
147
+
148
+ # CDO/CDC tokens are dropped at the top level (legacy HTML wrapping);
149
+ # inside a block they are treated as the start of a qualified rule.
150
+ # Comment tokens are passed through into the rules list when present.
151
+ def consume_rule_list(top_level:)
152
+ rules = []
153
+
154
+ loop do
155
+ t = peek
156
+
157
+ case t.type
158
+ when :eof
159
+ return rules
160
+ when :whitespace, :semicolon
161
+ consume
162
+ when :comment
163
+ rules << consume
164
+ when :cdo, :cdc
165
+ if top_level
166
+ consume
167
+ else
168
+ rule = consume_qualified_rule(nested: !top_level)
169
+ rules << rule if rule
170
+ end
171
+ when :at_keyword
172
+ rule = consume_at_rule(nested: !top_level)
173
+ rules << rule if rule
174
+ else
175
+ rule = consume_qualified_rule(nested: !top_level)
176
+ rules << rule if rule
177
+ end
178
+ end
179
+ end
180
+
181
+ def consume_at_rule(nested:)
182
+ name = consume.value
183
+ prelude = []
184
+ block = nil
185
+
186
+ loop do
187
+ t = peek
188
+
189
+ case t.type
190
+ when :semicolon, :eof
191
+ consume if t.type == :semicolon
192
+ break
193
+ when :rbrace
194
+ break if nested
195
+
196
+ prelude << consume
197
+ when :lbrace
198
+ consume
199
+ block = consume_braced_block
200
+ break
201
+ else
202
+ prelude << consume_component_value
203
+ end
204
+ end
205
+
206
+ strip_whitespace!(prelude)
207
+ AtRule.new(name:, prelude:, block:)
208
+ end
209
+
210
+ # On EOF or a stop token (`}` while nested), the rule is dropped per
211
+ # §5.4.3 — but already-consumed prelude tokens are NOT put back. Rewinding
212
+ # would leave the caller's cursor at the same starting token and loop
213
+ # forever on input like `style="hidden"` (no `:` and no `{`).
214
+ def consume_qualified_rule(nested:)
215
+ prelude = []
216
+
217
+ loop do
218
+ t = peek
219
+
220
+ case t.type
221
+ when :eof
222
+ return nil
223
+ when :rbrace
224
+ return nil if nested
225
+
226
+ prelude << consume
227
+ when :semicolon
228
+ if nested
229
+ consume
230
+ return nil
231
+ end
232
+
233
+ prelude << consume
234
+ when :lbrace
235
+ consume
236
+ block = consume_braced_block
237
+ strip_whitespace!(prelude)
238
+ return QualifiedRule.new(prelude:, block:)
239
+ else
240
+ prelude << consume_component_value
241
+ end
242
+ end
243
+ end
244
+
245
+ # Assumes the opening `{` has already been consumed; consumes through
246
+ # the matching `}`.
247
+ def consume_braced_block
248
+ Block.new(items: collect_block_items(stop_at_close_brace: true))
249
+ end
250
+
251
+ # Shared loop for both `{}`-terminated blocks (inside a rule) and
252
+ # EOF-terminated block contents (style attribute, `@page`, etc.).
253
+ # Comment tokens are passed through into the items list when present.
254
+ def collect_block_items(stop_at_close_brace:)
255
+ items = []
256
+
257
+ loop do
258
+ t = peek
259
+
260
+ case t.type
261
+ when :eof
262
+ return items
263
+ when :rbrace
264
+ consume
265
+ return items if stop_at_close_brace
266
+ # Stray `}` outside any block: parse error per spec; skip.
267
+ next
268
+ when :whitespace, :semicolon
269
+ consume
270
+ when :comment
271
+ items << consume
272
+ when :at_keyword
273
+ rule = consume_at_rule(nested: true)
274
+ items << rule if rule
275
+ else
276
+ decl = try_consume_declaration
277
+ if decl
278
+ items << decl
279
+ else
280
+ rule = consume_qualified_rule(nested: true)
281
+ items << rule if rule
282
+ end
283
+ end
284
+ end
285
+ end
286
+
287
+ # In nested context a token sequence like `a:hover { ... }` looks like
288
+ # the start of a declaration (`<ident> : ...`). We detect such cases by
289
+ # noticing a `{}` simple block in the value and rolling back so the
290
+ # caller can re-parse it as a nested qualified rule.
291
+ def try_consume_declaration
292
+ saved = @pos
293
+
294
+ return nil unless peek.type == :ident
295
+
296
+ name = consume.value
297
+
298
+ skip_whitespace
299
+
300
+ unless peek.type == :colon
301
+ @pos = saved
302
+ return nil
303
+ end
304
+
305
+ consume
306
+
307
+ value = []
308
+
309
+ loop do
310
+ t = peek
311
+
312
+ case t.type
313
+ when :semicolon
314
+ consume
315
+ break
316
+ when :eof, :rbrace
317
+ break
318
+ else
319
+ value << consume_component_value
320
+ end
321
+ end
322
+
323
+ if value.any? { _1.is_a?(SimpleBlock) && _1.braced? }
324
+ @pos = saved
325
+ return nil
326
+ end
327
+
328
+ important = extract_important!(value)
329
+ strip_whitespace!(value)
330
+
331
+ Declaration.new(name:, value:, important:)
332
+ end
333
+
334
+ def consume_component_value
335
+ t = peek
336
+
337
+ case t.type
338
+ when :lbrace, :lbracket, :lparen
339
+ consume_simple_block
340
+ when :function
341
+ consume_function
342
+ else
343
+ consume
344
+ end
345
+ end
346
+
347
+ def consume_simple_block
348
+ open_type = consume.type
349
+ open_char = BRACKET_OPEN_CHAR.fetch(open_type)
350
+ end_type = BRACKET_CLOSE_TYPE.fetch(open_type)
351
+ values = []
352
+
353
+ loop do
354
+ t = peek
355
+
356
+ case t.type
357
+ when :eof
358
+ break
359
+ when end_type
360
+ consume
361
+ break
362
+ else
363
+ values << consume_component_value
364
+ end
365
+ end
366
+
367
+ SimpleBlock.new(open: open_char, value: values)
368
+ end
369
+
370
+ def consume_function
371
+ name = consume.value
372
+ values = []
373
+
374
+ loop do
375
+ t = peek
376
+
377
+ case t.type
378
+ when :eof
379
+ break
380
+ when :rparen
381
+ consume
382
+ break
383
+ else
384
+ values << consume_component_value
385
+ end
386
+ end
387
+
388
+ Function.new(name:, value: values)
389
+ end
390
+
391
+ # Strips only whitespace tokens — comments, when present, are preserved
392
+ # as significant content of preludes/values.
393
+ def strip_whitespace!(value)
394
+ value.shift while whitespace_item?(value.first)
395
+ value.pop while whitespace_item?(value.last)
396
+ end
397
+
398
+ def whitespace_item?(item)
399
+ item.is_a?(Token) && item.whitespace?
400
+ end
401
+
402
+ # Strips a trailing `! important` from `value` and returns whether it
403
+ # was present.
404
+ def extract_important!(value)
405
+ i = value.length - 1
406
+ i -= 1 while i >= 0 && trivia_item?(value[i])
407
+
408
+ return false unless i >= 1
409
+
410
+ ident = value[i]
411
+
412
+ return false unless ident.is_a?(Token) && ident.type == :ident && ident.value.casecmp('important').zero?
413
+
414
+ j = i - 1
415
+ j -= 1 while j >= 0 && trivia_item?(value[j])
416
+
417
+ bang = value[j]
418
+
419
+ return false unless bang.is_a?(Token) && bang.type == :delim && bang.value == '!'
420
+
421
+ value.slice!(j..)
422
+ true
423
+ end
424
+
425
+ def trivia_item?(item)
426
+ item.is_a?(Token) && item.trivia?
427
+ end
428
+ end
429
+ end