From 4ba6c5c39469ca68e2ecc39fad2db4d7f5bd4668 Mon Sep 17 00:00:00 2001 From: "Gregory N. Schmit" Date: Thu, 9 May 2024 22:50:21 -0500 Subject: [PATCH] Investigate parsel and css-selector-parser for new globalfication. --- globalfy/.node-version | 1 + globalfy/README.md | 77 ++++++++++++++++++++++++++ globalfy/globalfy-parsel.js | 92 +++++++++++++++++++++++++++++++ globalfy/globalfy.js | 104 ++++++++++++++++++++++++++++++++++++ globalfy/package-lock.json | 91 +++++++++++++++++++++++++++++++ globalfy/package.json | 8 +++ globalfy/postcss-test.js | 32 +++++++++++ 7 files changed, 405 insertions(+) create mode 100644 globalfy/.node-version create mode 100644 globalfy/README.md create mode 100644 globalfy/globalfy-parsel.js create mode 100644 globalfy/globalfy.js create mode 100644 globalfy/package-lock.json create mode 100644 globalfy/package.json create mode 100644 globalfy/postcss-test.js diff --git a/globalfy/.node-version b/globalfy/.node-version new file mode 100644 index 0000000..a9d0873 --- /dev/null +++ b/globalfy/.node-version @@ -0,0 +1 @@ +18.19.0 diff --git a/globalfy/README.md b/globalfy/README.md new file mode 100644 index 0000000..113cc52 --- /dev/null +++ b/globalfy/README.md @@ -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) +``` \ No newline at end of file diff --git a/globalfy/globalfy-parsel.js b/globalfy/globalfy-parsel.js new file mode 100644 index 0000000..f5735df --- /dev/null +++ b/globalfy/globalfy-parsel.js @@ -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) +} diff --git a/globalfy/globalfy.js b/globalfy/globalfy.js new file mode 100644 index 0000000..f77367b --- /dev/null +++ b/globalfy/globalfy.js @@ -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)) +} \ No newline at end of file diff --git a/globalfy/package-lock.json b/globalfy/package-lock.json new file mode 100644 index 0000000..98e2e94 --- /dev/null +++ b/globalfy/package-lock.json @@ -0,0 +1,91 @@ +{ + "name": "globalfy", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "css-selector-parser": "^3.0.5", + "parsel-js": "^1.1.2", + "postcss": "^8.4.38" + } + }, + "node_modules/css-selector-parser": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.0.5.tgz", + "integrity": "sha512-3itoDFbKUNx1eKmVpYMFyqKX04Ww9osZ+dLgrk6GEv6KMVeXUhUnp4I5X+evw+u3ZxVU6RFXSSRxlTeMh8bA+g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/parsel-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/parsel-js/-/parsel-js-1.1.2.tgz", + "integrity": "sha512-D66DG2nKx4Yoq66TMEyCUHlR2STGqO7vsBrX7tgyS9cfQyO6XD5JyzOiflwmWN6a4wbUAqpmHqmrxlTQVGZcbA==" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + } + } +} diff --git a/globalfy/package.json b/globalfy/package.json new file mode 100644 index 0000000..e36319a --- /dev/null +++ b/globalfy/package.json @@ -0,0 +1,8 @@ +{ + "type": "module", + "dependencies": { + "css-selector-parser": "^3.0.5", + "parsel-js": "^1.1.2", + "postcss": "^8.4.38" + } +} diff --git a/globalfy/postcss-test.js b/globalfy/postcss-test.js new file mode 100644 index 0000000..d18d33f --- /dev/null +++ b/globalfy/postcss-test.js @@ -0,0 +1,32 @@ +import postcss from "postcss" + +// Sample CSS +const css = ` +h1, h2 { + color: blue; +} +h3 { + color: red; +} +`; + +// Process the CSS +postcss() + .use(root => { + root.walkRules(rule => { + console.log(`Rule: ${rule.selector}`); + // Splitting the selector by commas to check for multiple selectors + for (let s in rule.selectors) { + console.log(`selector: ${s}`) + } + if (rule.selectors.length > 1) { + console.log('This rule has multiple selectors:', rule.selectors); + } else { + console.log('This rule has a single selector:', rule.selectors[0]); + } + }); + }) + .process(css, { from: undefined }) + .then(result => { + console.log('Processing complete.'); + });