Skip to content

Commit fc1dbe9

Browse files
committed
Investigate parsel and css-selector-parser for new globalfication.
1 parent 276037a commit fc1dbe9

7 files changed

+401
-0
lines changed

globalfy/.node-version

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
18.19.0

globalfy/README.md

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Globalfy
2+
3+
This is just a test directory for me to figure out how to improve the global-fication of css in this project.
4+
5+
Remove this before finishing PR.
6+
7+
## Notes
8+
9+
- parsel didn't work for me... it parses fine but then cannot convert AST back into a selector after modification. The stringify "helper" just smushes the tokens together omitting combinators, commas, and everything else.
10+
- I tried `css-selector-parser` and it works great. In postcss's terms, `h1, h2` is a rule made up of two selectors, `h1` and `h2`. Since `svelte-preprocess` calls `globalifySelector` on each individual selector (i.e., `rule.selectors.map(globalifySelector)`), that means we don't actually need to worry about parsing the top-level rule into selectors. However, `css-selector-parser` does do it perfectly well, so I designed it to handle both cases.
11+
- The terminology is a little different in `css-selector-parser`. In their lingo, a selector is the top level thing, and it is composed of `rules`.
12+
13+
I tested two strategies (using `css-selector-parser` terminology):
14+
1. Wrap entire rules in `:global()` (i.e., `h1, h2` -> `:global(h1, h2)`).
15+
2. Recurse deeper and wrap each rule in `:global()` (i.e., `h1, h2` -> `:global(h1), :global(h2)`).
16+
17+
Strategy 2 seems more in line with what is normal right now. However, I don't really understand this. The only constraint I've seen from Svelte is from the error that was the genesis of my work on this: `:global(...) must contain a single selector`. This seems to suggest that the most reasonable thing to do (and which is also faster) is to wrap the entire rule (remember, what svelte calls a "selector" `css-selector-parser` calls a "rule") in `:global()`. In other words, Svelte is saying you cannot do `:global(h1, h2)`, but you can do `:global(h1 > h2)`, since the former is two selectors and the latter is a single selector.
18+
19+
Here is the output of `node ./globalfy.js`:
20+
21+
```txt
22+
input: .foo
23+
STRAT1: :global(.foo)
24+
STRAT2: :global(.foo)
25+
26+
input: ul + p
27+
STRAT1: :global(ul + p)
28+
STRAT2: :global(ul) + :global(p)
29+
30+
input: p > a
31+
STRAT1: :global(p > a)
32+
STRAT2: :global(p) > :global(a)
33+
34+
input: p + p
35+
STRAT1: :global(p + p)
36+
STRAT2: :global(p) + :global(p)
37+
38+
input: li a
39+
STRAT1: :global(li a)
40+
STRAT2: :global(li) :global(a)
41+
42+
input: div ~ a
43+
STRAT1: :global(div ~ a)
44+
STRAT2: :global(div) ~ :global(a)
45+
46+
input: div, a
47+
STRAT1: :global(div), :global(a)
48+
STRAT2: :global(div), :global(a)
49+
50+
input: .foo.bar
51+
STRAT1: :global(.foo.bar)
52+
STRAT2: :global(.foo.bar)
53+
54+
input: [attr="with spaces"]
55+
STRAT1: :global([attr="with spaces"])
56+
STRAT2: :global([attr="with spaces"])
57+
58+
input: article :is(h1, h2)
59+
STRAT1: :global(article :is(h1, h2))
60+
STRAT2: :global(article) :global(:is(h1, h2))
61+
62+
input: tr:nth-child(2n+1)
63+
STRAT1: :global(tr:nth-child(2n+1))
64+
STRAT2: :global(tr:nth-child(2n+1))
65+
66+
input: p:nth-child(n+8):nth-child(-n+15)
67+
STRAT1: :global(p:nth-child(n+8):nth-child(-n+15))
68+
STRAT2: :global(p:nth-child(n+8):nth-child(-n+15))
69+
70+
input: #foo > .bar + div.k1.k2 [id='baz']:not(:where(#yolo))::before
71+
STRAT1: :global(#foo > .bar + div.k1.k2 [id="baz"]:not(:where(#yolo))::before)
72+
STRAT2: :global(#foo) > :global(.bar) + :global(div.k1.k2) :global([id="baz"]:not(:where(#yolo))::before)
73+
```

globalfy/globalfy-parsel.js

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import * as parsel from "./parsel.mjs"
2+
3+
function globalfyNode(node, opts) {
4+
const {types, exclude} = opts
5+
6+
// First base case: node must be globalfied.
7+
if (exclude ? !types.includes(node.type) : types.includes(node.type)) {
8+
const arg = parsel.stringify(node)
9+
return {
10+
"name": "global",
11+
"argument": arg,
12+
"type": "pseudo-class",
13+
"content": `:global(${arg})`
14+
}
15+
}
16+
17+
// For composite nodes, recursively globalfy their children.
18+
switch (node.type) {
19+
case "compound":
20+
case "list":
21+
console.log("list")
22+
let x = {
23+
...node,
24+
list: node.list.map((child) => globalfyNode(child, opts)),
25+
}
26+
console.log(JSON.stringify(x, null, 2))
27+
return x
28+
case "complex":
29+
console.log("complex")
30+
let y = {
31+
...node,
32+
left: globalfyNode(node.left, opts),
33+
right: globalfyNode(node.right, opts),
34+
}
35+
console.log(JSON.stringify(y, null, 2))
36+
return y
37+
}
38+
39+
// Second base case: node is not composite and doesn't need to be globalfied.
40+
return node
41+
}
42+
43+
function globalfySelector(selector, opts) {
44+
console.log("gns")
45+
console.log(JSON.stringify(JSON.parse(JSON.stringify(parsel.parse(selector))), null, 2))
46+
return parsel.stringify(globalfyNode(JSON.parse(JSON.stringify(parsel.parse(selector))), opts))
47+
}
48+
49+
// const TYPES = [
50+
// "list",
51+
// "complex",
52+
// "compound",
53+
// "id",
54+
// "class",
55+
// "comma",
56+
// "combinator",
57+
// "pseudo-element",
58+
// "pseudo-class",
59+
// "universal",
60+
// "type",
61+
// ]
62+
63+
const AST = process.argv[2] == "ast"
64+
const STRAT1 = {types: ["list", "complex", "compound"], exclude: true}
65+
const STRAT2 = {types: ["class", "id", "type"]}
66+
67+
function debugAST(input) {
68+
if (typeof input === "string") {
69+
input = parsel.parse(input)
70+
}
71+
console.log(JSON.stringify(input, null, 2))
72+
console.log(parsel.stringify(input))
73+
}
74+
75+
function debugGlobalfy(selector, opts) {
76+
console.log(` input: ${selector}`)
77+
console.log(` output: ${globalfySelector(selector, opts)}`)
78+
console.log()
79+
}
80+
81+
if (AST) {
82+
// debugAST(".foo")
83+
debugAST(".foo > .bar")
84+
// debugAST(":global(.foo, .foo.bar)")
85+
// debugAST(".first .second")
86+
// debugAST("test > .first, .second)")
87+
// debugAST("#foo > .bar + div.k1.k2 [id='baz']:hello(2):not(:where(#yolo))::before")
88+
} else {
89+
// debugGlobalfy(".foo", STRAT1)
90+
debugGlobalfy(".foo > .bar", STRAT1)
91+
// debugGlobalfy(".foo > .bar", STRAT1)
92+
}

globalfy/globalfy.js

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { ast, createParser, render } from "css-selector-parser"
2+
3+
function globalfyNode(node, opts) {
4+
if (!node || !node.type) {
5+
return node
6+
}
7+
8+
const {types, exclude} = opts
9+
10+
// First base case: node must be globalfied.
11+
if (exclude ? !types.includes(node.type) : types.includes(node.type)) {
12+
return {
13+
name: "global",
14+
type: "PseudoClass",
15+
argument: {
16+
type: "Selector",
17+
rules: [{type: "Rule", items: [node]}],
18+
},
19+
}
20+
}
21+
22+
// For composite nodes, recursively globalfy their children.
23+
switch (node.type) {
24+
case "Selector":
25+
return {
26+
...node,
27+
rules: node.rules.map((rule) => globalfyNode(rule, opts)),
28+
}
29+
case "Rule":
30+
return {
31+
...node,
32+
nestedRule: node.nestedRule ? globalfyNode(node.nestedRule, opts) : null,
33+
// items: node.items.map((child) => globalfyNode(child, opts)),
34+
items: [{
35+
name: "global",
36+
type: "PseudoClass",
37+
argument: {
38+
type: "Selector",
39+
rules: [{type: "Rule", items: node.items}],
40+
},
41+
}],
42+
}
43+
case "PseudoClass":
44+
case "PseudoElement":
45+
return {
46+
...node,
47+
argument: globalfyNode(node.argument, opts),
48+
}
49+
}
50+
51+
// Second base case: node is not composite and doesn't need to be globalfied.
52+
return node
53+
}
54+
55+
function globalfySelector(selector, opts) {
56+
const parse = createParser({syntax: "progressive"});
57+
return render(ast.selector(globalfyNode(parse(selector), opts)))
58+
}
59+
60+
const AST = process.argv[2] == "ast"
61+
const STRAT1 = {types: ["Selector"], exclude: true}
62+
const STRAT2 = {types: ["Selector", "Rule"], exclude: true}
63+
64+
function debugAST(selector) {
65+
const parse = createParser({strict: false, syntax: "progressive"})
66+
const output = parse(selector)
67+
console.log(JSON.stringify(output, null, 2))
68+
console.log(render(ast.selector(output)))
69+
}
70+
71+
function debugGlobalfy(selector) {
72+
console.log(` input: ${selector}`)
73+
console.log(` STRAT1: ${globalfySelector(selector, STRAT1)}`)
74+
console.log(` STRAT2: ${globalfySelector(selector, STRAT2)}`)
75+
console.log()
76+
}
77+
78+
if (AST) {
79+
debugAST(".foo")
80+
debugAST(".foo > .bar")
81+
debugAST(".foo .bar")
82+
debugAST(".foo.bar")
83+
// debugAST(":global(.foo > .bar)")
84+
// debugAST(".first .second")
85+
// debugAST("test > .first, .second)")
86+
// debugAST("#foo > .bar + div.k1.k2 [id='baz']:hello(2):not(:where(#yolo))::before")
87+
} else {
88+
[
89+
".foo",
90+
"ul + p",
91+
"p > a",
92+
"p + p",
93+
"li a",
94+
"div ~ a",
95+
"div, a",
96+
".foo.bar",
97+
"[attr=\"with spaces\"]",
98+
"article :is(h1, h2)",
99+
"tr:nth-child(2n+1)",
100+
"p:nth-child(n+8):nth-child(-n+15)",
101+
"#foo > .bar + div.k1.k2 [id='baz']:not(:where(#yolo))::before"
102+
].forEach((selector) => debugGlobalfy(selector))
103+
}

globalfy/package-lock.json

+92
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

globalfy/package.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"type": "module",
3+
"dependencies": {
4+
"css-selector-parser": "^3.0.5",
5+
"parsel": "^0.3.0",
6+
"postcss": "^8.4.38"
7+
}
8+
}

0 commit comments

Comments
 (0)