Last active
June 27, 2025 03:20
-
-
Save thomaswilburn/b32f7b4104b11daaebdd5dc95e5cde04 to your computer and use it in GitHub Desktop.
Convert CSS to nested CSS
This file contains hidden or 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
import { parse } from "https://deno.land/x/[email protected]/mod.ts"; | |
var file = Deno.args[0]; | |
var input = await Deno.readTextFile(file); | |
var parsed = parse(input, { value: true }); | |
var root = { | |
rules: [], | |
nest: {} | |
}; | |
var notNested = []; | |
function spaces(count = 0) { | |
return "".padStart(count, " "); | |
} | |
function parseSelector(selector) { | |
var buffer = ""; | |
var chunks = []; | |
var inPseudo = false; | |
for (var i = 0; i < selector.length; i++) { | |
var letter = selector[i]; | |
switch (letter) { | |
case "(": | |
case ")": | |
inPseudo = letter == "("; | |
buffer += letter; | |
break; | |
case " ": | |
if (inPseudo) { | |
buffer += letter; | |
} else { | |
if (buffer) chunks.push(buffer); | |
buffer = ""; | |
} | |
break; | |
case "+": | |
case ">": | |
case "~": | |
buffer = chunks.pop() + " "; | |
buffer += letter + " "; | |
while (selector[++i] == " " && i < selector.length); | |
i--; | |
break; | |
default: | |
buffer += letter; | |
} | |
} | |
if (buffer) { | |
chunks.push(buffer); | |
} | |
return chunks; | |
} | |
for (var rule of parsed.stylesheet.rules) { | |
// currently we skip media queries or keyframes and selector lists | |
// at some point it would be nice to handle these | |
if (!rule.selectors) { | |
notNested.push(rule); | |
continue; | |
} | |
var parts; | |
if (rule.selectors.length > 1) { | |
parts = [rule.selectors.join(", ")]; | |
} else { | |
parts = parseSelector(rule.selectors[0]); | |
} | |
var branch = root; | |
for (var part of parts) { | |
// check to see if we can `&` match on anything | |
var splitIndex = part.search(/\w[\[.#]/) + 1; | |
var prefix = part.slice(0, splitIndex); | |
if (prefix) { | |
for (var possible in branch.nest) { | |
if (possible == prefix) { | |
part = "&" + part.slice(splitIndex); | |
branch = branch.nest[prefix]; | |
} | |
} | |
} | |
if (!branch.nest[part]) { | |
branch.nest[part] = { | |
rules: [], | |
nest: {} | |
} | |
} | |
branch = branch.nest[part]; | |
} | |
branch.rules.push(rule); | |
} | |
function writeDeclarations(rule, pad = "") { | |
for (var { name, value } of rule.declarations) { | |
output += `${pad}${name}: ${value};\n` | |
} | |
} | |
var output = ""; | |
function walk(node, context = "", indentation = 0) { | |
var pad = spaces(indentation); | |
for (var rule of node.rules) { | |
writeDeclarations(rule, pad); | |
} | |
for (var selector in node.nest) { | |
var merged = context + " " + selector; | |
var branch = node.nest[selector]; | |
if (node != root && selector.match(/^[a-z]/i)) { | |
selector = "& " + selector; | |
} | |
output += "\n" + pad + selector + " {\n"; | |
walk(branch, context = merged, indentation + 2); | |
output += pad + "}\n"; | |
} | |
} | |
walk(root, 0); | |
function writeRule(rule, indentation = 0) { | |
var outer = spaces(indentation); | |
var inner = spaces(indentation + 2); | |
var selector = rule.selectors.join(", "); | |
output += `\n${outer}${selector} {\n`; | |
writeDeclarations(rule, inner); | |
output += `${outer}}\n`; | |
} | |
output += "\n/*==== excluded from nesting ===*/\n"; | |
for (var other of notNested) { | |
if (other.declarations) { | |
writeRule(other); | |
} else if (other.rules) { | |
output += `\n@${other.type} ${other.name} {`; | |
for (var rule of other.rules) { | |
writeRule(rule, 2); | |
} | |
output += "}\n"; | |
} | |
} | |
console.log(output); |
This would be a great website.
does this work in both ways?
You mean can it un-nest CSS? No, although that would probably not be too hard to write.
Thanks for publishing this! I was fiddling with nesting css today (running it over a large css-file with many edge-cases, like svgs in background-urls and scoped:not(.selectors)) and went a little further down the rabbit-hole: https://gist.github.com/schuhwerk/6358bd8b1c25cc82f8f62e36f524fcba
If worked for me, code is messy though ;)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Run with
deno run pigeon.js inputfile.css
.