Skip to content

Commit

Permalink
Investigate parsel and css-selector-parser for new globalfication.
Browse files Browse the repository at this point in the history
  • Loading branch information
gregschmit committed May 10, 2024
1 parent 276037a commit 4ba6c5c
Show file tree
Hide file tree
Showing 7 changed files with 405 additions and 0 deletions.
1 change: 1 addition & 0 deletions globalfy/.node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
18.19.0
77 changes: 77 additions & 0 deletions globalfy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Globalfy

This is just a test directory for me to figure out how to improve the global-fication of css in this project.

Remove this before finishing PR.

## Notes

- 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.
- 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.
- 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`.

I tested two strategies (using `css-selector-parser` terminology):
1. Wrap selector rules in `:global()` (i.e., `p > a` -> `:global(p > a)`).
2. Recurse deeper and wrap each rule individually in `:global()` (i.e., `p > a` -> `:global(p) > :global(a)`).

Strategy 2 seems more in line with what is normal right now. However, I don't really understand this, and I prefer Strategy 1. 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 selector in `:global()`. In other words, Svelte is saying you cannot do `:global(p, a)` (fine), but you can do `:global(p > a)`, since the former is two selectors and the latter is a single selector.

Here is the output of `node ./globalfy.js`:

```txt
input: .foo
STRAT1: :global(.foo)
STRAT2: :global(.foo)
input: ul + p
STRAT1: :global(ul + p)
STRAT2: :global(ul) + :global(p)
input: p, a
STRAT1: :global(p), :global(a)
STRAT2: :global(p), :global(a)
input: p > a
STRAT1: :global(p > a)
STRAT2: :global(p) > :global(a)
input: p + p
STRAT1: :global(p + p)
STRAT2: :global(p) + :global(p)
input: li a
STRAT1: :global(li a)
STRAT2: :global(li) :global(a)
input: div ~ a
STRAT1: :global(div ~ a)
STRAT2: :global(div) ~ :global(a)
input: div, a
STRAT1: :global(div), :global(a)
STRAT2: :global(div), :global(a)
input: .foo.bar
STRAT1: :global(.foo.bar)
STRAT2: :global(.foo.bar)
input: [attr="with spaces"]
STRAT1: :global([attr="with spaces"])
STRAT2: :global([attr="with spaces"])
input: article :is(h1, h2)
STRAT1: :global(article :is(h1, h2))
STRAT2: :global(article) :global(:is(h1, h2))
input: tr:nth-child(2n+1)
STRAT1: :global(tr:nth-child(2n+1))
STRAT2: :global(tr:nth-child(2n+1))
input: p:nth-child(n+8):nth-child(-n+15)
STRAT1: :global(p:nth-child(n+8):nth-child(-n+15))
STRAT2: :global(p:nth-child(n+8):nth-child(-n+15))
input: #foo > .bar + div.k1.k2 [id='baz']:not(:where(#yolo))::before
STRAT1: :global(#foo > .bar + div.k1.k2 [id="baz"]:not(:where(#yolo))::before)
STRAT2: :global(#foo) > :global(.bar) + :global(div.k1.k2) :global([id="baz"]:not(:where(#yolo))::before)
```
92 changes: 92 additions & 0 deletions globalfy/globalfy-parsel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as parsel from "parsel-js"

function globalfyNode(node, opts) {
const {types, exclude} = opts

// First base case: node must be globalfied.
if (exclude ? !types.includes(node.type) : types.includes(node.type)) {
const arg = parsel.stringify(node)
return {
"name": "global",
"argument": arg,
"type": "pseudo-class",
"content": `:global(${arg})`
}
}

// For composite nodes, recursively globalfy their children.
switch (node.type) {
case "compound":
case "list":
console.log("list")
let x = {
...node,
list: node.list.map((child) => globalfyNode(child, opts)),
}
console.log(JSON.stringify(x, null, 2))
return x
case "complex":
console.log("complex")
let y = {
...node,
left: globalfyNode(node.left, opts),
right: globalfyNode(node.right, opts),
}
console.log(JSON.stringify(y, null, 2))
return y
}

// Second base case: node is not composite and doesn't need to be globalfied.
return node
}

function globalfySelector(selector, opts) {
console.log("gns")
console.log(JSON.stringify(JSON.parse(JSON.stringify(parsel.parse(selector))), null, 2))
return parsel.stringify(globalfyNode(JSON.parse(JSON.stringify(parsel.parse(selector))), opts))
}

// const TYPES = [
// "list",
// "complex",
// "compound",
// "id",
// "class",
// "comma",
// "combinator",
// "pseudo-element",
// "pseudo-class",
// "universal",
// "type",
// ]

const AST = process.argv[2] == "ast"
const STRAT1 = {types: ["list", "complex", "compound"], exclude: true}
const STRAT2 = {types: ["class", "id", "type"]}

function debugAST(input) {
if (typeof input === "string") {
input = parsel.parse(input)
}
console.log(JSON.stringify(input, null, 2))
console.log(parsel.stringify(input))
}

function debugGlobalfy(selector, opts) {
console.log(` input: ${selector}`)
console.log(` output: ${globalfySelector(selector, opts)}`)
console.log()
}

if (AST) {
// debugAST(".foo")
debugAST(".foo > .bar")
// debugAST(":global(.foo, .foo.bar)")
// debugAST(".first .second")
// debugAST("test > .first, .second)")
// debugAST("#foo > .bar + div.k1.k2 [id='baz']:hello(2):not(:where(#yolo))::before")
} else {
// debugGlobalfy(".foo", STRAT1)
debugGlobalfy(".foo > .bar", STRAT1)
// debugGlobalfy(".foo > .bar", STRAT1)
}
104 changes: 104 additions & 0 deletions globalfy/globalfy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { ast, createParser, render } from "css-selector-parser"

function globalfyNode(node, opts) {
if (!node || !node.type) {
return node
}

const {types, exclude} = opts

// First base case: node must be globalfied.
if (exclude ? !types.includes(node.type) : types.includes(node.type)) {
return {
name: "global",
type: "PseudoClass",
argument: {
type: "Selector",
rules: [{type: "Rule", items: [node]}],
},
}
}

// For composite nodes, recursively globalfy their children.
switch (node.type) {
case "Selector":
return {
...node,
rules: node.rules.map((rule) => globalfyNode(rule, opts)),
}
case "Rule":
return {
...node,
nestedRule: node.nestedRule ? globalfyNode(node.nestedRule, opts) : null,
// items: node.items.map((child) => globalfyNode(child, opts)),
items: [{
name: "global",
type: "PseudoClass",
argument: {
type: "Selector",
rules: [{type: "Rule", items: node.items}],
},
}],
}
case "PseudoClass":
case "PseudoElement":
return {
...node,
argument: globalfyNode(node.argument, opts),
}
}

// Second base case: node is not composite and doesn't need to be globalfied.
return node
}

function globalfySelector(selector, opts) {
const parse = createParser({syntax: "progressive"});
return render(ast.selector(globalfyNode(parse(selector), opts)))
}

const AST = process.argv[2] == "ast"
const STRAT1 = {types: ["Selector"], exclude: true}
const STRAT2 = {types: ["Selector", "Rule"], exclude: true}

function debugAST(selector) {
const parse = createParser({strict: false, syntax: "progressive"})
const output = parse(selector)
console.log(JSON.stringify(output, null, 2))
console.log(render(ast.selector(output)))
}

function debugGlobalfy(selector) {
console.log(` input: ${selector}`)
console.log(` STRAT1: ${globalfySelector(selector, STRAT1)}`)
console.log(` STRAT2: ${globalfySelector(selector, STRAT2)}`)
console.log()
}

if (AST) {
debugAST(".foo")
debugAST(".foo > .bar")
debugAST(".foo .bar")
debugAST(".foo.bar")
// debugAST(":global(.foo > .bar)")
// debugAST(".first .second")
// debugAST("test > .first, .second)")
// debugAST("#foo > .bar + div.k1.k2 [id='baz']:hello(2):not(:where(#yolo))::before")
} else {
[
".foo",
"ul + p",
"p, a",
"p > a",
"p + p",
"li a",
"div ~ a",
"div, a",
".foo.bar",
"[attr=\"with spaces\"]",
"article :is(h1, h2)",
"tr:nth-child(2n+1)",
"p:nth-child(n+8):nth-child(-n+15)",
"#foo > .bar + div.k1.k2 [id='baz']:not(:where(#yolo))::before"
].forEach((selector) => debugGlobalfy(selector))
}
91 changes: 91 additions & 0 deletions globalfy/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions globalfy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "module",
"dependencies": {
"css-selector-parser": "^3.0.5",
"parsel-js": "^1.1.2",
"postcss": "^8.4.38"
}
}
Loading

0 comments on commit 4ba6c5c

Please sign in to comment.