Skip to content

Instantly share code, notes, and snippets.

@thomaswilburn
Last active March 28, 2025 12:16
Show Gist options
  • Save thomaswilburn/b32f7b4104b11daaebdd5dc95e5cde04 to your computer and use it in GitHub Desktop.
Save thomaswilburn/b32f7b4104b11daaebdd5dc95e5cde04 to your computer and use it in GitHub Desktop.
Convert CSS to nested CSS
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);
@thomaswilburn
Copy link
Author

Run with deno run pigeon.js inputfile.css.

@TimChinye
Copy link

This would be a great website.

@ShadowDara
Copy link

does this work in both ways?

@thomaswilburn
Copy link
Author

You mean can it un-nest CSS? No, although that would probably not be too hard to write.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment