-
Notifications
You must be signed in to change notification settings - Fork 149
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Investigate parsel and css-selector-parser for new globalfication.
- Loading branch information
1 parent
276037a
commit 4ba6c5c
Showing
7 changed files
with
405 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
18.19.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
Oops, something went wrong.