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,447 @@
1
+ module CSS
2
+ # Tokenizer based on CSS Syntax Module Level 3/4 §4.
3
+ # https://www.w3.org/TR/css-syntax-3/#tokenization
4
+ #
5
+ # Not thread-safe: an instance carries a mutable cursor (`@pos`) that
6
+ # advances over the input. Allocate one tokenizer per thread.
7
+ class Tokenizer
8
+ include CodePoints
9
+
10
+ PUNCTUATION = {
11
+ '(' => :lparen,
12
+ ')' => :rparen,
13
+ ',' => :comma,
14
+ ':' => :colon,
15
+ ';' => :semicolon,
16
+ '[' => :lbracket,
17
+ ']' => :rbracket,
18
+ '{' => :lbrace,
19
+ '}' => :rbrace
20
+ }.freeze
21
+
22
+ # CR / FF (and CR LF) collapse to LF; NUL collapses to U+FFFD. Done in
23
+ # one pass.
24
+ PREPROCESS_RE = /\r\n?|\f|\0/.freeze
25
+
26
+ def initialize(input, preserve_comments: false)
27
+ @chars = preprocess(input)
28
+ @length = @chars.length
29
+ @pos = 0
30
+ @newlines = collect_newline_offsets(@chars)
31
+ @preserve_comments = preserve_comments
32
+ end
33
+
34
+ def tokenize
35
+ tokens = []
36
+
37
+ loop do
38
+ token = next_token
39
+ break if token.type == :eof
40
+
41
+ tokens << token
42
+ end
43
+
44
+ tokens
45
+ end
46
+
47
+ def next_token
48
+ consume_comments unless @preserve_comments
49
+
50
+ return Token.new(:eof) if @pos >= @length
51
+
52
+ start_offset = @pos
53
+ tok = consume_one_token
54
+
55
+ tok.assign_source!(start_offset, @pos, @newlines)
56
+ end
57
+
58
+ private
59
+
60
+ def consume_one_token
61
+ return consume_comment_token if peek == '/' && peek(1) == '*'
62
+
63
+ c = consume
64
+
65
+ return consume_whitespace if whitespace?(c)
66
+ return consume_string_token(c) if c == '"' || c == "'"
67
+
68
+ if (c == '+' || c == '-' || c == '.') && number_starts?(c, peek, peek(1))
69
+ reconsume
70
+ return consume_numeric_token
71
+ end
72
+
73
+ if (type = PUNCTUATION[c])
74
+ return Token.new(type)
75
+ end
76
+
77
+ case c
78
+ when '#'
79
+ if ident_code_point?(peek) || valid_escape?(peek, peek(1))
80
+ flag = ident_sequence_starts?(peek, peek(1), peek(2)) ? :id : :unrestricted
81
+ Token.new(:hash, consume_ident_sequence, flag:)
82
+ else
83
+ Token.new(:delim, c)
84
+ end
85
+ when '+', '.'
86
+ Token.new(:delim, c)
87
+ when '-'
88
+ if peek == '-' && peek(1) == '>'
89
+ consume
90
+ consume
91
+ Token.new(:cdc)
92
+ elsif ident_sequence_starts?(c, peek, peek(1))
93
+ reconsume
94
+ consume_ident_like_token
95
+ else
96
+ Token.new(:delim, c)
97
+ end
98
+ when '<'
99
+ if peek == '!' && peek(1) == '-' && peek(2) == '-'
100
+ consume
101
+ consume
102
+ consume
103
+ Token.new(:cdo)
104
+ else
105
+ Token.new(:delim, c)
106
+ end
107
+ when '@'
108
+ if ident_sequence_starts?(peek, peek(1), peek(2))
109
+ Token.new(:at_keyword, consume_ident_sequence)
110
+ else
111
+ Token.new(:delim, c)
112
+ end
113
+ when '\\'
114
+ if valid_escape?(c, peek)
115
+ reconsume
116
+ consume_ident_like_token
117
+ else
118
+ Token.new(:delim, c)
119
+ end
120
+ when '0'..'9'
121
+ reconsume
122
+ consume_numeric_token
123
+ else
124
+ if ident_start_code_point?(c)
125
+ reconsume
126
+ consume_ident_like_token
127
+ else
128
+ Token.new(:delim, c)
129
+ end
130
+ end
131
+ end
132
+
133
+ # Random access on a non-ascii-only UTF-8 String is O(distance from
134
+ # the cached character index), and the peek-ahead pattern (`peek`,
135
+ # `peek(1)`, `peek(2)`) defeats the cache — empirically ~200× slower
136
+ # than indexing a flat Array. Splitting into `chars` once amortizes
137
+ # the UTF-8 walk and gives us O(1) random access for the rest of
138
+ # tokenization.
139
+ def preprocess(input)
140
+ input
141
+ .encode('UTF-8')
142
+ .gsub(PREPROCESS_RE) { $~[0] == "\0" ? CodePoints::REPLACEMENT : "\n" }
143
+ .chars
144
+ end
145
+
146
+ def peek(offset = 0)
147
+ @chars[@pos + offset]
148
+ end
149
+
150
+ def consume
151
+ c = @chars[@pos]
152
+ return nil if c.nil?
153
+
154
+ @pos += 1
155
+ c
156
+ end
157
+
158
+ def reconsume
159
+ @pos -= 1
160
+ end
161
+
162
+ def collect_newline_offsets(chars)
163
+ offsets = []
164
+ i = 0
165
+ n = chars.length
166
+
167
+ while i < n
168
+ offsets << i if chars[i] == "\n"
169
+ i += 1
170
+ end
171
+
172
+ offsets
173
+ end
174
+
175
+ def whitespace?(c)
176
+ c == ' ' || c == "\n" || c == "\t"
177
+ end
178
+
179
+ def non_printable?(c)
180
+ return false if c.nil?
181
+
182
+ o = c.ord
183
+ o <= 0x08 || o == 0x0B || (0x0E..0x1F).cover?(o) || o == 0x7F
184
+ end
185
+
186
+ # §4.3.8.
187
+ def valid_escape?(c1, c2)
188
+ c1 == '\\' && c2 != "\n" && !c2.nil?
189
+ end
190
+
191
+ # §4.3.9.
192
+ def ident_sequence_starts?(c1, c2, c3)
193
+ case c1
194
+ when '-'
195
+ ident_start_code_point?(c2) || c2 == '-' || valid_escape?(c2, c3)
196
+ when '\\'
197
+ valid_escape?(c1, c2)
198
+ else
199
+ ident_start_code_point?(c1)
200
+ end
201
+ end
202
+
203
+ # §4.3.10.
204
+ def number_starts?(c1, c2, c3)
205
+ case c1
206
+ when '+', '-'
207
+ digit?(c2) || (c2 == '.' && digit?(c3))
208
+ when '.'
209
+ digit?(c2)
210
+ else
211
+ digit?(c1)
212
+ end
213
+ end
214
+
215
+ # §4.3.2. Skips through `/* ... */` comments without producing tokens.
216
+ def consume_comments
217
+ while peek == '/' && peek(1) == '*'
218
+ consume
219
+ consume
220
+
221
+ until eof?
222
+ if consume == '*' && peek == '/'
223
+ consume
224
+ break
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ # When `preserve_comments` is on, comments are emitted as tokens whose
231
+ # value is the body between `/*` and `*/`.
232
+ def consume_comment_token
233
+ consume
234
+ consume
235
+ buf = +''
236
+
237
+ until eof?
238
+ c = consume
239
+ if c == '*' && peek == '/'
240
+ consume
241
+ break
242
+ end
243
+
244
+ buf << c
245
+ end
246
+
247
+ Token.new(:comment, buf)
248
+ end
249
+
250
+ def eof?
251
+ @pos >= @length
252
+ end
253
+
254
+ def consume_whitespace
255
+ consume while whitespace?(peek)
256
+
257
+ Token.new(:whitespace)
258
+ end
259
+
260
+ # §4.3.5.
261
+ def consume_string_token(ending)
262
+ buf = +''
263
+
264
+ loop do
265
+ c = consume
266
+
267
+ case c
268
+ when nil, ending
269
+ return Token.new(:string, buf)
270
+ when "\n"
271
+ reconsume
272
+ return Token.new(:bad_string)
273
+ when '\\'
274
+ n = peek
275
+
276
+ if n.nil?
277
+ next
278
+ elsif n == "\n"
279
+ consume
280
+ else
281
+ buf << consume_escaped_code_point
282
+ end
283
+ else
284
+ buf << c
285
+ end
286
+ end
287
+ end
288
+
289
+ # §4.3.7. Assumes the backslash has already been consumed.
290
+ def consume_escaped_code_point
291
+ c = consume
292
+
293
+ return CodePoints::REPLACEMENT if c.nil?
294
+ return c unless hex_digit?(c)
295
+
296
+ hex = c.dup
297
+ hex << consume while hex.length < 6 && hex_digit?(peek)
298
+ consume if whitespace?(peek)
299
+
300
+ n = hex.to_i(16)
301
+
302
+ if n.zero? || (0xD800..0xDFFF).cover?(n) || n > 0x10FFFF
303
+ CodePoints::REPLACEMENT
304
+ else
305
+ [n].pack('U')
306
+ end
307
+ end
308
+
309
+ # §4.3.11.
310
+ def consume_ident_sequence
311
+ buf = +''
312
+
313
+ loop do
314
+ c = consume
315
+
316
+ if ident_code_point?(c)
317
+ buf << c
318
+ elsif valid_escape?(c, peek)
319
+ buf << consume_escaped_code_point
320
+ else
321
+ reconsume unless c.nil?
322
+ return buf
323
+ end
324
+ end
325
+ end
326
+
327
+ # §4.3.4.
328
+ def consume_ident_like_token
329
+ name = consume_ident_sequence
330
+
331
+ if name.casecmp('url').zero? && peek == '('
332
+ consume
333
+
334
+ consume while whitespace?(peek) && whitespace?(peek(1))
335
+
336
+ n1 = peek
337
+ n2 = whitespace?(n1) ? peek(1) : n1
338
+
339
+ if n1 == '"' || n1 == "'" || (whitespace?(n1) && (n2 == '"' || n2 == "'"))
340
+ Token.new(:function, name)
341
+ else
342
+ consume_url_token
343
+ end
344
+ elsif peek == '('
345
+ consume
346
+ Token.new(:function, name)
347
+ else
348
+ Token.new(:ident, name)
349
+ end
350
+ end
351
+
352
+ # §4.3.6. Assumes "url(" has already been consumed.
353
+ def consume_url_token
354
+ buf = +''
355
+
356
+ consume while whitespace?(peek)
357
+
358
+ loop do
359
+ c = consume
360
+
361
+ case c
362
+ when nil, ')'
363
+ return Token.new(:url, buf)
364
+ when '"', "'", '('
365
+ consume_bad_url_remnants
366
+ return Token.new(:bad_url)
367
+ when ' ', "\t", "\n"
368
+ consume while whitespace?(peek)
369
+
370
+ n = peek
371
+
372
+ if n.nil? || n == ')'
373
+ consume unless n.nil?
374
+ return Token.new(:url, buf)
375
+ else
376
+ consume_bad_url_remnants
377
+ return Token.new(:bad_url)
378
+ end
379
+ when '\\'
380
+ if valid_escape?(c, peek)
381
+ buf << consume_escaped_code_point
382
+ else
383
+ consume_bad_url_remnants
384
+ return Token.new(:bad_url)
385
+ end
386
+ else
387
+ if non_printable?(c)
388
+ consume_bad_url_remnants
389
+ return Token.new(:bad_url)
390
+ end
391
+
392
+ buf << c
393
+ end
394
+ end
395
+ end
396
+
397
+ # §4.3.14.
398
+ def consume_bad_url_remnants
399
+ loop do
400
+ c = consume
401
+
402
+ return if c.nil? || c == ')'
403
+
404
+ consume_escaped_code_point if valid_escape?(c, peek)
405
+ end
406
+ end
407
+
408
+ # §4.3.3.
409
+ def consume_numeric_token
410
+ number, flag = consume_number
411
+
412
+ if ident_sequence_starts?(peek, peek(1), peek(2))
413
+ Token.new(:dimension, number, flag:, unit: consume_ident_sequence)
414
+ elsif peek == '%'
415
+ consume
416
+ Token.new(:percentage, number)
417
+ else
418
+ Token.new(:number, number, flag:)
419
+ end
420
+ end
421
+
422
+ # §4.3.12. Returns [numeric_value, :integer | :number].
423
+ def consume_number
424
+ repr = +''
425
+ flag = :integer
426
+
427
+ repr << consume if peek == '+' || peek == '-'
428
+ repr << consume while digit?(peek)
429
+
430
+ if peek == '.' && digit?(peek(1))
431
+ repr << consume
432
+ repr << consume while digit?(peek)
433
+ flag = :number
434
+ end
435
+
436
+ if (peek == 'E' || peek == 'e') &&
437
+ (digit?(peek(1)) || ((peek(1) == '+' || peek(1) == '-') && digit?(peek(2))))
438
+ repr << consume
439
+ repr << consume if peek == '+' || peek == '-'
440
+ repr << consume while digit?(peek)
441
+ flag = :number
442
+ end
443
+
444
+ [flag == :integer ? repr.to_i : repr.to_f, flag]
445
+ end
446
+ end
447
+ end
data/lib/css/urange.rb ADDED
@@ -0,0 +1,45 @@
1
+ module CSS
2
+ # Parser for CSS <urange> tokens, e.g. `U+0-7F`, `U+26`, `U+10??`.
3
+ # https://drafts.csswg.org/css-syntax/#urange-syntax
4
+ #
5
+ # Operates on the source string rather than a token stream because the
6
+ # tokenizer destructively normalizes shapes like `U+0` (the `+` is
7
+ # absorbed into a number-token whose sign is lost on serialization).
8
+ # Sticking with the source preserves the exact form.
9
+ module Urange
10
+ URANGE_RE = /\Au\+([0-9a-f?]{1,6})(?:-([0-9a-f]{1,6}))?\z/i.freeze
11
+ WILDCARD_RE = /\A[0-9a-f]*\?+\z/i.freeze
12
+
13
+ MAX_CODEPOINT = 0x10FFFF
14
+
15
+ extend self
16
+
17
+ def parse(input)
18
+ s = input.to_s.strip
19
+ m = URANGE_RE.match(s)
20
+
21
+ raise ParseError, "invalid urange: #{input.inspect}" unless m
22
+
23
+ start_str, end_str = m[1], m[2]
24
+
25
+ first, last =
26
+ if end_str
27
+ raise ParseError, 'wildcards are not allowed in range form' if start_str.include?('?')
28
+
29
+ [start_str.to_i(16), end_str.to_i(16)]
30
+ elsif start_str.include?('?')
31
+ raise ParseError, 'wildcards must be trailing' unless start_str.match?(WILDCARD_RE)
32
+
33
+ [start_str.tr('?', '0').to_i(16), start_str.tr('?', 'f').to_i(16)]
34
+ else
35
+ n = start_str.to_i(16)
36
+ [n, n]
37
+ end
38
+
39
+ raise ParseError, "codepoint out of range: U+#{format('%X', last)}" if last > MAX_CODEPOINT
40
+ raise ParseError, "urange start must be <= end (U+#{format('%X', first)} > U+#{format('%X', last)})" if first > last
41
+
42
+ Nodes::UnicodeRange.new(first:, last:)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,3 @@
1
+ module CSS
2
+ VERSION = '0.2.0.beta1'
3
+ end
data/lib/css.rb ADDED
@@ -0,0 +1,73 @@
1
+ module CSS
2
+ # Bracket information for the three "simple block" pairs. Indexed both by
3
+ # opening token type (for the parser) and by opening character (for the
4
+ # serializer).
5
+ BRACKET_OPEN_CHAR = {lbrace: '{', lbracket: '[', lparen: '('}.freeze
6
+ BRACKET_CLOSE_TYPE = {lbrace: :rbrace, lbracket: :rbracket, lparen: :rparen}.freeze
7
+ BRACKET_PAIRS = {'{' => '}', '[' => ']', '(' => ')'}.freeze
8
+ end
9
+
10
+ require_relative 'css/version'
11
+ require_relative 'css/code_points'
12
+ require_relative 'css/escape'
13
+ require_relative 'css/token'
14
+ require_relative 'css/tokenizer'
15
+ require_relative 'css/token_cursor'
16
+ require_relative 'css/nodes'
17
+ require_relative 'css/parser'
18
+ require_relative 'css/selectors'
19
+ require_relative 'css/media_queries'
20
+ require_relative 'css/serializer'
21
+ require_relative 'css/urange'
22
+ require_relative 'css/nesting'
23
+ require_relative 'css/cascade'
24
+
25
+ module CSS
26
+ class ParseError < StandardError
27
+ attr_reader :position
28
+
29
+ def initialize(message, position: nil)
30
+ super(position ? "#{position}: #{message}" : message)
31
+ @position = position
32
+ end
33
+ end
34
+
35
+ class << self
36
+ def tokenize(input, **opts) = Tokenizer.new(input, **opts).tokenize
37
+ def parse_stylesheet(input, **opts) = Parser.parse_stylesheet(input, **opts)
38
+ def parse_rule(input, **opts) = Parser.parse_rule(input, **opts)
39
+ def parse_declaration(input, **opts) = Parser.parse_declaration(input, **opts)
40
+ def parse_block_contents(input, **opts) = Parser.parse_block_contents(input, **opts)
41
+ def parse_component_value(input, **opts) = Parser.parse_component_value(input, **opts)
42
+ def parse_component_values(input, **opts) = Parser.parse_component_values(input, **opts)
43
+ def parse_comma_separated_values(input, **opts) = Parser.parse_comma_separated_values(input, **opts)
44
+
45
+ def parse_urange(input) = Urange.parse(input)
46
+
47
+ def parse_selector_list(input) = Selectors::Parser.parse_selector_list(input)
48
+ def parse_selector(input) = Selectors::Parser.parse_selector(input)
49
+ def parse_anb(input) = Selectors::AnBParser.parse(input)
50
+
51
+ def specificity(selector) = Selectors::SpecificityCalculator.calculate(selector)
52
+
53
+ def matches?(element, selector, state: nil) = Selectors::Matcher.matches?(element, selector, state: state)
54
+
55
+ def parse_media_query_list(input) = MediaQueries::Parser.parse(input)
56
+
57
+ def media_matches?(query_list, context)
58
+ ql = query_list.is_a?(String) ? MediaQueries::Parser.parse(query_list) : query_list
59
+ ctx = context.is_a?(MediaQueries::Context) ? context : MediaQueries::Context.default(**context.to_h)
60
+ MediaQueries::Evaluator.evaluate(ql, ctx)
61
+ end
62
+
63
+ def cascade(stylesheet, context: MediaQueries::Context.default)
64
+ Cascade.new(stylesheet, context:)
65
+ end
66
+
67
+ def desugar(stylesheet) = Nesting.desugar(stylesheet)
68
+
69
+ def serialize(node) = Serializer.serialize(node)
70
+
71
+ alias parse parse_stylesheet
72
+ end
73
+ end
data/lib/p_css.rb ADDED
@@ -0,0 +1 @@
1
+ require_relative 'css'
@@ -0,0 +1,22 @@
1
+ module CSS
2
+ # See `CSS.cascade` for the typical entry point.
3
+ class Cascade
4
+ type inline_style = String | Nodes::Block | Array[Nodes::Declaration]
5
+
6
+ class Match < Data
7
+ attr_reader declaration: Nodes::Declaration
8
+ attr_reader specificity: Selectors::Specificity
9
+ attr_reader inline: bool
10
+ attr_reader order: Integer
11
+
12
+ def self.new: (declaration: Nodes::Declaration, specificity: Selectors::Specificity, inline: bool, order: Integer) -> Match
13
+ end
14
+
15
+ def initialize: (Nodes::Stylesheet stylesheet, ?context: MediaQueries::Context) -> void
16
+
17
+ # Returns Hash<String, Declaration> of winning declarations after
18
+ # !important > inline > stylesheet > specificity > source-order
19
+ # sorting.
20
+ def resolve: (untyped element, ?inline_style: inline_style?, ?state: matcher_state?) -> Hash[String, Nodes::Declaration]
21
+ end
22
+ end
@@ -0,0 +1,107 @@
1
+ module CSS
2
+ module MediaQueries
3
+ module Node
4
+ end
5
+
6
+ type modifier = :not | :only
7
+
8
+ type comparison_op = :eq | :lt | :le | :gt | :ge
9
+
10
+ type media_condition = MediaNot | MediaAnd | MediaOr | MediaFeature | GeneralEnclosed
11
+
12
+ type feature_value = Token | Ratio
13
+
14
+ class MediaQueryList < Data
15
+ include Node
16
+
17
+ attr_reader queries: Array[MediaQuery]
18
+
19
+ def self.new: (queries: Array[MediaQuery]) -> MediaQueryList
20
+ end
21
+
22
+ class MediaQuery < Data
23
+ include Node
24
+
25
+ attr_reader modifier: modifier?
26
+ attr_reader type: String?
27
+ attr_reader condition: media_condition?
28
+
29
+ def self.new: (modifier: modifier?, type: String?, condition: media_condition?) -> MediaQuery
30
+ end
31
+
32
+ class MediaNot < Data
33
+ include Node
34
+
35
+ attr_reader operand: media_condition
36
+
37
+ def self.new: (operand: media_condition) -> MediaNot
38
+ end
39
+
40
+ class MediaAnd < Data
41
+ include Node
42
+
43
+ attr_reader operands: Array[media_condition]
44
+
45
+ def self.new: (operands: Array[media_condition]) -> MediaAnd
46
+ end
47
+
48
+ class MediaOr < Data
49
+ include Node
50
+
51
+ attr_reader operands: Array[media_condition]
52
+
53
+ def self.new: (operands: Array[media_condition]) -> MediaOr
54
+ end
55
+
56
+ # `op` is `nil` for boolean form (`(color)`), `:eq` for plain
57
+ # (`(min-width: 600px)`) and explicit `=`, otherwise a comparison.
58
+ # `value` mirrors that — `nil` for boolean form.
59
+ class MediaFeature < Data
60
+ include Node
61
+
62
+ attr_reader name: String
63
+ attr_reader op: comparison_op?
64
+ attr_reader value: feature_value?
65
+
66
+ def self.new: (name: String, op: comparison_op?, value: feature_value?) -> MediaFeature
67
+ end
68
+
69
+ class GeneralEnclosed < Data
70
+ include Node
71
+
72
+ attr_reader tokens: Array[component_value]
73
+
74
+ def self.new: (tokens: Array[component_value]) -> GeneralEnclosed
75
+ end
76
+
77
+ class Ratio < Data
78
+ include Node
79
+
80
+ attr_reader numerator: Numeric
81
+ attr_reader denominator: Numeric
82
+
83
+ def self.new: (numerator: Numeric, denominator: Numeric) -> Ratio
84
+
85
+ def to_f: () -> Float
86
+ end
87
+
88
+ # User-agent context against which a `MediaQueryList` is evaluated.
89
+ # `features` is keyed by feature name (no `min-`/`max-` prefix);
90
+ # values follow Media Queries 4 conventions (lengths in CSS px,
91
+ # resolution in `dppx`, identifiers as Strings, booleans as 1/0).
92
+ class Context < Data
93
+ DEFAULTS: Hash[String, untyped]
94
+
95
+ attr_reader features: Hash[String, untyped]
96
+
97
+ def self.new: (features: Hash[String, untyped]) -> Context
98
+
99
+ # `Context.default('width' => 1200, 'prefers-color-scheme' => 'dark')`
100
+ def self.default: (**untyped overrides) -> Context
101
+
102
+ def []: ((String | Symbol) name) -> untyped
103
+ def media_type: () -> String
104
+ def with: (**untyped overrides) -> Context
105
+ end
106
+ end
107
+ end