p_css 0.2.0.beta1-x86_64-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.
- checksums.yaml +7 -0
- data/Cargo.lock +282 -0
- data/Cargo.toml +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +357 -0
- data/ext/css_native/Cargo.toml +12 -0
- data/ext/css_native/extconf.rb +4 -0
- data/ext/css_native/src/lib.rs +117 -0
- data/ext/css_native/src/matcher.rs +356 -0
- data/ext/css_native/src/selectors.rs +411 -0
- data/ext/css_native/src/snapshot.rs +370 -0
- data/ext/css_native/src/state.rs +174 -0
- data/ext/css_native/src/tokenizer.rs +596 -0
- data/lib/css/3.3/css_native.so +0 -0
- data/lib/css/3.4/css_native.so +0 -0
- data/lib/css/4.0/css_native.so +0 -0
- data/lib/css/cascade.rb +277 -0
- data/lib/css/code_points.rb +59 -0
- data/lib/css/escape.rb +82 -0
- data/lib/css/media_queries/context.rb +60 -0
- data/lib/css/media_queries/evaluator.rb +157 -0
- data/lib/css/media_queries/nodes.rb +41 -0
- data/lib/css/media_queries/parser.rb +374 -0
- data/lib/css/media_queries.rb +9 -0
- data/lib/css/native.rb +179 -0
- data/lib/css/nesting.rb +229 -0
- data/lib/css/nodes.rb +42 -0
- data/lib/css/parser.rb +429 -0
- data/lib/css/selectors/anb_parser.rb +174 -0
- data/lib/css/selectors/matcher.rb +545 -0
- data/lib/css/selectors/nodes.rb +61 -0
- data/lib/css/selectors/parser.rb +395 -0
- data/lib/css/selectors/serializer.rb +102 -0
- data/lib/css/selectors/specificity.rb +81 -0
- data/lib/css/selectors.rb +11 -0
- data/lib/css/serializer.rb +167 -0
- data/lib/css/token.rb +107 -0
- data/lib/css/token_cursor.rb +49 -0
- data/lib/css/tokenizer.rb +447 -0
- data/lib/css/urange.rb +45 -0
- data/lib/css/version.rb +3 -0
- data/lib/css.rb +73 -0
- data/lib/p_css.rb +1 -0
- data/sig/css/cascade.rbs +22 -0
- data/sig/css/media_queries.rbs +107 -0
- data/sig/css/nodes.rbs +76 -0
- data/sig/css/selectors.rbs +164 -0
- data/sig/css/token.rbs +33 -0
- data/sig/css.rbs +99 -0
- metadata +113 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4c5a5a2904cd70b46f46157acd730b1f7966393ad5373989f9d87f3fadb017d0
|
|
4
|
+
data.tar.gz: 5b6a69bc1e3595b508089625ba3b659447ceec838c487a153b3d987ad3ec1368
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 73cd67efa76660863932e9d1a6a55558aac2398fd8fdf540f0011aa963a128fa03df3728bd7261862928f137d710b3166fb399844f37ee6cb10255085d33b036
|
|
7
|
+
data.tar.gz: bad67db0dfb1ac797c88e8a566611260aefd7912975a621ff3dfd410acbd22c59d2514cdd1e0c6f42e2c08fb01a463e309af6b4306ddaec24693f8e6f273e909
|
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
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.
|