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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 143a83f5f4b1e41498a8b21bbaea7f30bfb31b296cd82a0cf05ed012bdbcb0c6
4
+ data.tar.gz: d0bde87ccab385bdb2d7e039bc6f8ad6d36f182d9c43fb71c5e99349031ddff2
5
+ SHA512:
6
+ metadata.gz: '05822ec44df80a8a11f14e3f8b4250ce646dc2f22f2f91db8f8934edca504eb415dbefced2d9ac6d79b0731d37b55df7fae94e7cf71715867865f8bf11345fbd'
7
+ data.tar.gz: 1599c8b805cc3f8c50a07863fafc295bd886d9e9ab5730a1b649a12802afa72a7c7a5639450af4c8445fffa554c478e660b73f1d5d6ac76748ff7d1cea6914d9
data/Cargo.lock ADDED
@@ -0,0 +1,282 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "aho-corasick"
7
+ version = "1.1.4"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
10
+ dependencies = [
11
+ "memchr",
12
+ ]
13
+
14
+ [[package]]
15
+ name = "bindgen"
16
+ version = "0.72.1"
17
+ source = "registry+https://github.com/rust-lang/crates.io-index"
18
+ checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
19
+ dependencies = [
20
+ "bitflags",
21
+ "cexpr",
22
+ "clang-sys",
23
+ "itertools",
24
+ "proc-macro2",
25
+ "quote",
26
+ "regex",
27
+ "rustc-hash",
28
+ "shlex",
29
+ "syn",
30
+ ]
31
+
32
+ [[package]]
33
+ name = "bitflags"
34
+ version = "2.11.1"
35
+ source = "registry+https://github.com/rust-lang/crates.io-index"
36
+ checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
37
+
38
+ [[package]]
39
+ name = "cexpr"
40
+ version = "0.6.0"
41
+ source = "registry+https://github.com/rust-lang/crates.io-index"
42
+ checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
43
+ dependencies = [
44
+ "nom",
45
+ ]
46
+
47
+ [[package]]
48
+ name = "cfg-if"
49
+ version = "1.0.4"
50
+ source = "registry+https://github.com/rust-lang/crates.io-index"
51
+ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
52
+
53
+ [[package]]
54
+ name = "clang-sys"
55
+ version = "1.8.1"
56
+ source = "registry+https://github.com/rust-lang/crates.io-index"
57
+ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
58
+ dependencies = [
59
+ "glob",
60
+ "libc",
61
+ "libloading",
62
+ ]
63
+
64
+ [[package]]
65
+ name = "css_native"
66
+ version = "0.1.0"
67
+ dependencies = [
68
+ "magnus",
69
+ "rb-sys",
70
+ ]
71
+
72
+ [[package]]
73
+ name = "either"
74
+ version = "1.15.0"
75
+ source = "registry+https://github.com/rust-lang/crates.io-index"
76
+ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
77
+
78
+ [[package]]
79
+ name = "glob"
80
+ version = "0.3.3"
81
+ source = "registry+https://github.com/rust-lang/crates.io-index"
82
+ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
83
+
84
+ [[package]]
85
+ name = "itertools"
86
+ version = "0.13.0"
87
+ source = "registry+https://github.com/rust-lang/crates.io-index"
88
+ checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
89
+ dependencies = [
90
+ "either",
91
+ ]
92
+
93
+ [[package]]
94
+ name = "lazy_static"
95
+ version = "1.5.0"
96
+ source = "registry+https://github.com/rust-lang/crates.io-index"
97
+ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
98
+
99
+ [[package]]
100
+ name = "libc"
101
+ version = "0.2.186"
102
+ source = "registry+https://github.com/rust-lang/crates.io-index"
103
+ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
104
+
105
+ [[package]]
106
+ name = "libloading"
107
+ version = "0.8.9"
108
+ source = "registry+https://github.com/rust-lang/crates.io-index"
109
+ checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
110
+ dependencies = [
111
+ "cfg-if",
112
+ "windows-link",
113
+ ]
114
+
115
+ [[package]]
116
+ name = "magnus"
117
+ version = "0.8.2"
118
+ source = "registry+https://github.com/rust-lang/crates.io-index"
119
+ checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012"
120
+ dependencies = [
121
+ "magnus-macros",
122
+ "rb-sys",
123
+ "rb-sys-env",
124
+ "seq-macro",
125
+ ]
126
+
127
+ [[package]]
128
+ name = "magnus-macros"
129
+ version = "0.8.0"
130
+ source = "registry+https://github.com/rust-lang/crates.io-index"
131
+ checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892"
132
+ dependencies = [
133
+ "proc-macro2",
134
+ "quote",
135
+ "syn",
136
+ ]
137
+
138
+ [[package]]
139
+ name = "memchr"
140
+ version = "2.8.0"
141
+ source = "registry+https://github.com/rust-lang/crates.io-index"
142
+ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
143
+
144
+ [[package]]
145
+ name = "minimal-lexical"
146
+ version = "0.2.1"
147
+ source = "registry+https://github.com/rust-lang/crates.io-index"
148
+ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
149
+
150
+ [[package]]
151
+ name = "nom"
152
+ version = "7.1.3"
153
+ source = "registry+https://github.com/rust-lang/crates.io-index"
154
+ checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
155
+ dependencies = [
156
+ "memchr",
157
+ "minimal-lexical",
158
+ ]
159
+
160
+ [[package]]
161
+ name = "proc-macro2"
162
+ version = "1.0.106"
163
+ source = "registry+https://github.com/rust-lang/crates.io-index"
164
+ checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
165
+ dependencies = [
166
+ "unicode-ident",
167
+ ]
168
+
169
+ [[package]]
170
+ name = "quote"
171
+ version = "1.0.45"
172
+ source = "registry+https://github.com/rust-lang/crates.io-index"
173
+ checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
174
+ dependencies = [
175
+ "proc-macro2",
176
+ ]
177
+
178
+ [[package]]
179
+ name = "rb-sys"
180
+ version = "0.9.128"
181
+ source = "registry+https://github.com/rust-lang/crates.io-index"
182
+ checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a"
183
+ dependencies = [
184
+ "rb-sys-build",
185
+ ]
186
+
187
+ [[package]]
188
+ name = "rb-sys-build"
189
+ version = "0.9.128"
190
+ source = "registry+https://github.com/rust-lang/crates.io-index"
191
+ checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a"
192
+ dependencies = [
193
+ "bindgen",
194
+ "lazy_static",
195
+ "proc-macro2",
196
+ "quote",
197
+ "regex",
198
+ "shell-words",
199
+ "syn",
200
+ ]
201
+
202
+ [[package]]
203
+ name = "rb-sys-env"
204
+ version = "0.2.3"
205
+ source = "registry+https://github.com/rust-lang/crates.io-index"
206
+ checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a"
207
+
208
+ [[package]]
209
+ name = "regex"
210
+ version = "1.12.3"
211
+ source = "registry+https://github.com/rust-lang/crates.io-index"
212
+ checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
213
+ dependencies = [
214
+ "aho-corasick",
215
+ "memchr",
216
+ "regex-automata",
217
+ "regex-syntax",
218
+ ]
219
+
220
+ [[package]]
221
+ name = "regex-automata"
222
+ version = "0.4.14"
223
+ source = "registry+https://github.com/rust-lang/crates.io-index"
224
+ checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
225
+ dependencies = [
226
+ "aho-corasick",
227
+ "memchr",
228
+ "regex-syntax",
229
+ ]
230
+
231
+ [[package]]
232
+ name = "regex-syntax"
233
+ version = "0.8.10"
234
+ source = "registry+https://github.com/rust-lang/crates.io-index"
235
+ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
236
+
237
+ [[package]]
238
+ name = "rustc-hash"
239
+ version = "2.1.2"
240
+ source = "registry+https://github.com/rust-lang/crates.io-index"
241
+ checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
242
+
243
+ [[package]]
244
+ name = "seq-macro"
245
+ version = "0.3.6"
246
+ source = "registry+https://github.com/rust-lang/crates.io-index"
247
+ checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
248
+
249
+ [[package]]
250
+ name = "shell-words"
251
+ version = "1.1.1"
252
+ source = "registry+https://github.com/rust-lang/crates.io-index"
253
+ checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
254
+
255
+ [[package]]
256
+ name = "shlex"
257
+ version = "1.3.0"
258
+ source = "registry+https://github.com/rust-lang/crates.io-index"
259
+ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
260
+
261
+ [[package]]
262
+ name = "syn"
263
+ version = "2.0.117"
264
+ source = "registry+https://github.com/rust-lang/crates.io-index"
265
+ checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
266
+ dependencies = [
267
+ "proc-macro2",
268
+ "quote",
269
+ "unicode-ident",
270
+ ]
271
+
272
+ [[package]]
273
+ name = "unicode-ident"
274
+ version = "1.0.24"
275
+ source = "registry+https://github.com/rust-lang/crates.io-index"
276
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
277
+
278
+ [[package]]
279
+ name = "windows-link"
280
+ version = "0.2.1"
281
+ source = "registry+https://github.com/rust-lang/crates.io-index"
282
+ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
data/Cargo.toml ADDED
@@ -0,0 +1,3 @@
1
+ [workspace]
2
+ members = ['ext/css_native']
3
+ resolver = '2'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Keita Urashima
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,357 @@
1
+ # p CSS
2
+
3
+ A CSS toolkit for Ruby — tokenizer, parser, serializer, selector matcher, and
4
+ cascade resolver. Targets CSS Syntax Level 4 (with nesting), Selectors Level 4,
5
+ and Media Queries Level 4.
6
+
7
+ The name reads as **p CSS** — Ruby's `p` method (puts-inspect) applied to CSS.
8
+ Installed under the gem name `p_css`; the top-level module is `CSS`.
9
+
10
+ ```ruby
11
+ require 'p_css'
12
+
13
+ CSS.parse_stylesheet('.foo { color: red }')
14
+ ```
15
+
16
+ ## Why this exists
17
+
18
+ The Ruby ecosystem already has a few CSS parsers (crass, sass-rb's grammar,
19
+ nokogiri's selector-to-XPath compiler), but each stops at a different layer
20
+ and none of them currently:
21
+
22
+ - parse modern CSS nesting (`& .child { ... }`),
23
+ - expose a Selectors Level 4 AST you can inspect,
24
+ - match selectors against a DOM in pure Ruby,
25
+ - resolve the cascade so `display: none` in a `<style>` tag actually
26
+ influences a visibility judgement.
27
+
28
+ p CSS fills that gap. The first concrete user is
29
+ [capybara-simulated](https://github.com/ursm/capybara-simulated) (a
30
+ Nokogiri + QuickJS Capybara driver that needs to know whether an element
31
+ is hidden without a real browser), but the gem is intentionally general —
32
+ no DOM library is hardwired in.
33
+
34
+ ## What's in the box
35
+
36
+ | Layer | Entry point | Spec |
37
+ | --- | --- | --- |
38
+ | Tokenizer | `CSS.tokenize` | Syntax 4 §4 |
39
+ | Parser (with nesting) | `CSS.parse_stylesheet` and §5.3 entry points | Syntax 4 §5, Nesting 1 |
40
+ | Serializer (round-trip) | `CSS.serialize` | Syntax 4 §9 |
41
+ | `urange` | `CSS.parse_urange` | Syntax 4 §6 |
42
+ | Selector parser | `CSS.parse_selector_list`, `CSS.parse_selector` | Selectors 4 |
43
+ | AnB microsyntax | `CSS.parse_anb` | Syntax 4 §6.7 |
44
+ | Specificity | `CSS.specificity` | Selectors §16 |
45
+ | Selector matcher | `CSS.matches?` | Selectors 4 |
46
+ | Nesting de-sugar | `CSS.desugar` | Nesting 1 |
47
+ | Media query parser | `CSS.parse_media_query_list` | Media Queries 4 |
48
+ | Media query evaluator | `CSS.media_matches?` | Media Queries 4 |
49
+ | Cascade resolver | `CSS.cascade(...).resolve(element)` | Cascade & Inheritance 4 (subset) |
50
+
51
+ ## Install
52
+
53
+ ```ruby
54
+ # Gemfile
55
+ gem 'p_css'
56
+ ```
57
+
58
+ Or:
59
+
60
+ ```sh
61
+ bundle add p_css
62
+ ```
63
+
64
+ Ruby 3.3+ is required. The matcher works against any object that quacks like
65
+ a DOM element (`Nokogiri::XML::Element` works out of the box); Nokogiri is not
66
+ a hard dependency.
67
+
68
+ ## Quick tour
69
+
70
+ ### Parse a stylesheet
71
+
72
+ ```ruby
73
+ ss = CSS.parse_stylesheet(<<~CSS)
74
+ .card, .panel {
75
+ color: red;
76
+ & .title { font-weight: 700; }
77
+ @media (min-width: 600px) { padding: 2rem; }
78
+ }
79
+ CSS
80
+
81
+ ss.rules.size # => 1
82
+ ss.rules.first.block.items.count # => 3 (1 declaration + 1 nested rule + 1 nested at-rule)
83
+ ```
84
+
85
+ `CSS.parse` is an alias of `CSS.parse_stylesheet`.
86
+
87
+ ### Round-trip
88
+
89
+ `CSS.serialize` accepts any AST node, Token, or array of component values, and
90
+ emits CSS that re-parses to the same AST.
91
+
92
+ ```ruby
93
+ src = '.foo { color: #abc; & .x { font-weight: 700 !important; } }'
94
+ CSS.serialize(CSS.parse_stylesheet(src))
95
+ # => ".foo {\n color: #abc;\n & .x {\n font-weight: 700 !important;\n }\n}"
96
+ ```
97
+
98
+ ### Spec entry points
99
+
100
+ ```ruby
101
+ CSS.parse_rule('@charset "UTF-8";') # one rule
102
+ CSS.parse_declaration('color: red !important') # one declaration
103
+ CSS.parse_block_contents('color: red; padding: 1em') # for `style="..."` etc.
104
+ CSS.parse_component_value('rgb(1, 2, 3)') # one component value
105
+ CSS.parse_component_values('1px solid red') # array of component values
106
+ CSS.parse_comma_separated_values('1px, 2px, 3px') # array of arrays
107
+ ```
108
+
109
+ ### Comments and source positions
110
+
111
+ ```ruby
112
+ ts = CSS.tokenize("a /* hi */ b\n c", preserve_comments: true)
113
+ ts.map { [it.type, it.value, it.position.to_s] }
114
+ # => [[:ident, "a", "1:1"],
115
+ # [:whitespace, nil, "1:2"],
116
+ # [:comment, " hi ", "1:3"],
117
+ # ...]
118
+ ```
119
+
120
+ `Token#position` is set during tokenization (`line`, `column`, `offset`,
121
+ `end_offset`). Equality on `Token` ignores position, so hand-built tokens still
122
+ compare equal to parsed ones.
123
+
124
+ `ParseError#position` carries the same information, and the message is prefixed
125
+ `line:col:` when available.
126
+
127
+ ### Selectors
128
+
129
+ ```ruby
130
+ sl = CSS.parse_selector_list('.card > a:hover, [data-x="y" i]:nth-child(2n+1)')
131
+ sl.selectors.size # => 2
132
+ sl.selectors[0].combinators # => [:child]
133
+
134
+ compound = sl.selectors[1].compounds[0]
135
+ attr = compound.components[0]
136
+ attr.matcher # => :exact
137
+ attr.case_flag # => :i
138
+
139
+ nth = compound.components[1]
140
+ nth.argument # => CSS::Selectors::AnB(step: 2, offset: 1)
141
+ ```
142
+
143
+ The selector parser also accepts the prelude of a parsed rule directly (the
144
+ prelude can contain `Function` / `SimpleBlock` nodes from the main parser; they
145
+ are flattened back into a token stream automatically):
146
+
147
+ ```ruby
148
+ ss = CSS.parse_stylesheet('.x { ... }')
149
+ CSS.parse_selector_list(ss.rules.first.prelude)
150
+ ```
151
+
152
+ ### Specificity
153
+
154
+ ```ruby
155
+ CSS.specificity(CSS.parse_selector_list('div.a#b')) # => Specificity(1, 1, 1)
156
+ CSS.specificity(CSS.parse_selector_list(':where(#x)')) # => Specificity(0, 0, 0)
157
+ CSS.specificity(CSS.parse_selector_list(':is(.a, #b)')) # => Specificity(1, 0, 0)
158
+ ```
159
+
160
+ `Specificity` is `Comparable`, so `>`, `<`, `==` work as expected.
161
+
162
+ ### Matcher
163
+
164
+ `CSS.matches?(element, selector)` checks whether a duck-typed element matches a
165
+ selector. The element must respond to `name` (or `tag_name`), `[]`, `parent`,
166
+ sibling navigation (`previous_element` / `next_element` if defined; otherwise
167
+ `previous_sibling` / `next_sibling`), and `children`. Nokogiri elements satisfy
168
+ this without any wrapping.
169
+
170
+ ```ruby
171
+ require 'nokogiri'
172
+
173
+ doc = Nokogiri::HTML(<<~HTML)
174
+ <ul>
175
+ <li>one</li>
176
+ <li class="active">two</li>
177
+ <li>three</li>
178
+ </ul>
179
+ HTML
180
+
181
+ active = doc.at_css('li.active')
182
+ CSS.matches?(active, 'li:nth-child(2n)') # => true
183
+ CSS.matches?(active, ':is(.active, .selected)') # => true
184
+ CSS.matches?(active, 'ul > li:not(:first-child)') # => true
185
+ ```
186
+
187
+ Stateful pseudo-classes (`:hover`, `:focus`, `:visited`, validity API states,
188
+ etc.) return `false` by default — there's no UA in the loop. Pass a `state:`
189
+ Hash to opt in; see [Stateful pseudo-classes](#stateful-pseudo-classes)
190
+ below. `:has()` is not yet implemented (its argument is kept as opaque
191
+ component values).
192
+
193
+ ### Nesting de-sugar
194
+
195
+ `CSS.desugar` returns a flat Stylesheet with `&` substituted by the parent
196
+ selector. Single-compound parents inline directly; multi-selector parents
197
+ collapse to `:is(...)`.
198
+
199
+ ```ruby
200
+ src = <<~CSS
201
+ .card, .panel {
202
+ color: red;
203
+ & .title { font-weight: 700; }
204
+ }
205
+ CSS
206
+
207
+ CSS.serialize(CSS.desugar(CSS.parse_stylesheet(src)))
208
+ # .card, .panel {
209
+ # color: red;
210
+ # }
211
+ # :is(.card, .panel) .title {
212
+ # font-weight: 700;
213
+ # }
214
+ ```
215
+
216
+ ### Media queries
217
+
218
+ ```ruby
219
+ ql = CSS.parse_media_query_list('screen and (600px <= width < 1200px)')
220
+
221
+ ctx = CSS::MediaQueries::Context.default('width' => 800)
222
+ CSS.media_matches?(ql, ctx) # => true
223
+
224
+ ctx = CSS::MediaQueries::Context.default('width' => 1500)
225
+ CSS.media_matches?(ql, ctx) # => false
226
+ ```
227
+
228
+ `Context` is a feature-name-keyed Hash with sensible defaults (1024×768
229
+ landscape light-mode screen). Override per call:
230
+
231
+ ```ruby
232
+ ctx = CSS::MediaQueries::Context.default(
233
+ 'width' => 1200,
234
+ 'prefers-color-scheme' => 'dark'
235
+ )
236
+ ```
237
+
238
+ Length units (px, em, rem, pt, pc, in, cm, mm, Q) are converted to CSS px
239
+ against a 16-px root assumption; resolution units (dppx, x, dpi, dpcm) to
240
+ dppx.
241
+
242
+ ### Cascade
243
+
244
+ `Cascade` resolves the winning declaration per property for one element.
245
+ Construct once per stylesheet (selectors, media queries, and specificities are
246
+ pre-computed); call `resolve(element)` cheaply per element.
247
+
248
+ ```ruby
249
+ ss = CSS.parse_stylesheet(<<~CSS)
250
+ p { color: black; }
251
+ .lead { color: blue; }
252
+ p.special { color: red !important; }
253
+ @media (max-width: 600px) {
254
+ .lead { font-size: 0.875rem; }
255
+ }
256
+ CSS
257
+
258
+ ctx = CSS::MediaQueries::Context.default('width' => 1024)
259
+ cascade = CSS.cascade(ss, context: ctx)
260
+
261
+ el = Nokogiri::HTML('<p class="lead special">…</p>').at_css('p')
262
+ winners = cascade.resolve(el, inline_style: el['style'])
263
+
264
+ CSS.serialize(winners['color'].value) # => "red"
265
+ winners['color'].important # => true
266
+ winners['font-size'] # => nil (only fires for max-width: 600px)
267
+ ```
268
+
269
+ The cascade sort follows: `!important` > inline > stylesheet > specificity >
270
+ source order. Cascade layers, `@scope` proximity, and Shadow DOM
271
+ encapsulation are not modeled — `@layer` / `@supports` / `@scope` /
272
+ `@container` / `@starting-style` blocks are descended into unconditionally.
273
+
274
+ ### Stateful pseudo-classes
275
+
276
+ `:hover`, `:focus`, `:focus-within`, `:focus-visible`, `:active`, `:visited`,
277
+ and `:target` return `false` from the matcher by default. Pass a `state:`
278
+ Hash to override:
279
+
280
+ ```ruby
281
+ state = {
282
+ hover: Set[hovered_element], # match these and their ancestors
283
+ focus: Set[focused_element], # match only this element
284
+ 'focus-within' => Set[el], # propagates to ancestors
285
+ active: true # match every element
286
+ }
287
+
288
+ CSS.matches?(element, ':hover', state: state)
289
+ cascade.resolve(element, state: state)
290
+ ```
291
+
292
+ Values:
293
+
294
+ - `Set` or `Array` of elements — matches those elements (and, for
295
+ `:hover`, `:active`, `:focus-within`, their ancestors per Selectors §10)
296
+ - `true` — matches every element
297
+ - falsy / missing — default behavior; never matches
298
+
299
+ Symbol and String keys are both accepted. Hyphenated names (`focus-within`,
300
+ `focus-visible`) read more naturally as String keys.
301
+
302
+ #### Limits of stateful matching
303
+
304
+ The API gives you the primitives but not a policy. Two patterns are
305
+ inherently hard:
306
+
307
+ - **`hover: true` over-reveals.** Every `:hover`-gated rule matches every
308
+ element, so multiple dropdowns / popovers / menus all become "visible"
309
+ simultaneously. Useful for "is this element *potentially* visible
310
+ somehow?" but not for unique-match queries.
311
+
312
+ - **Peer-row reveal patterns are unsolvable without mouse position.**
313
+ Stylesheets like `.row:hover .icon-copy { display: block }` reveal one
314
+ icon per row when its row is hovered. Per-candidate evaluation (giving
315
+ each candidate its own ancestor chain in the hover Set) doesn't break
316
+ the symmetry — every candidate sees its own `.row` ancestor as hovered
317
+ and reports itself visible. Real browsers disambiguate via the actual
318
+ mouse position; a headless analyzer can't reproduce that without the
319
+ test explicitly recording which element it treats as hovered (e.g. via
320
+ Capybara's `element.hover`).
321
+
322
+ The recommendation for tools layered on top of p CSS: track explicit hover
323
+ actions and pass the corresponding Set; for queries that depend on
324
+ hover-based uniqueness without an explicit hover, treat them as fragile
325
+ and disambiguate by `text:` / `id:` / data attributes instead of relying
326
+ on stateful CSS.
327
+
328
+ ### `urange`
329
+
330
+ ```ruby
331
+ r = CSS.parse_urange('U+10??')
332
+ r.first # => 0x1000
333
+ r.last # => 0x10FF
334
+ r.cover?(0x10AB) # => true
335
+ r.to_s # => "U+1000-10FF"
336
+ ```
337
+
338
+ ## Out of scope
339
+
340
+ These are deliberate omissions; pull requests welcome:
341
+
342
+ - Selectors Level 4 namespace prefixes (`ns|*`)
343
+ - The column combinator `||`
344
+ - `:has()` (relative selector list — needs a small AST extension)
345
+ - Strict/forgiving selector list distinction
346
+ - `@scope` proximity and the rest of the Cascade Layers spec
347
+ - Layout calculations (`display: block` vs flex sizing, `overflow: hidden`
348
+ clipping). p CSS reports the resolved property values; deciding whether
349
+ those values produce a zero-sized box is outside its scope.
350
+
351
+ ## Compatibility
352
+
353
+ Ruby 3.3+. Tested on the current MRI. No mandatory runtime dependencies.
354
+
355
+ ## License
356
+
357
+ MIT.
@@ -0,0 +1,12 @@
1
+ [package]
2
+ name = "css_native"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ publish = false
6
+
7
+ [lib]
8
+ crate-type = ["cdylib"]
9
+
10
+ [dependencies]
11
+ magnus = "0.8"
12
+ rb-sys = "0.9"
@@ -0,0 +1,4 @@
1
+ require 'mkmf'
2
+ require 'rb_sys/mkmf'
3
+
4
+ create_rust_makefile('css/css_native')