Skip to content

Instantly share code, notes, and snippets.

@tomhodgins
Last active December 20, 2019 04:57
Show Gist options
  • Save tomhodgins/6438b539245b6521c4fcb06b075caa6a to your computer and use it in GitHub Desktop.
Save tomhodgins/6438b539245b6521c4fcb06b075caa6a to your computer and use it in GitHub Desktop.
Nest CSS rules inside custom properties starting with a triple-dash, ---, which represents the containing selector in the new nested rule! https://tomhodgins.com/demo/nesting/
// deno nesting-deno.js 'a { color: red; ---\ b: { color: green }; }'
// deno nesting-deno.js --allow-read path/to/stylesheet.css
import * as parseCSS from 'https://tomhodgins.github.io/parse-css/index.js'
let file = Deno.args.slice(1)[0]
let css = file
try {
Deno.statSync(file)
css = new TextDecoder('utf-8').decode(
Deno.readFileSync(file)
)
} catch (error) {}
function expandNestedStylesheet(string, propertyName = '-') {
const output = []
const stringify = (list = [], joiner = '') => list
.map(token => token.toSource())
.join(joiner)
function expandNestedRule(rule) {
const output = []
const declarations = parseCSS.parseAListOfDeclarations(
stringify(rule.value.value)
)
if (declarations.length) {
const filteredDeclarations = declarations.filter(({name}) =>
name.startsWith(`--${propertyName}`) === false
)
if (filteredDeclarations.length) {
output.push(
parseCSS.parseARule(`
${stringify(rule.prelude)} {
${stringify(filteredDeclarations, ';')}
}
`)
)
}
// Process nested properties
declarations.forEach(declaration => {
if (declaration.name.startsWith(`--${propertyName}`)) {
output.push(
...expandNestedRule(
parseCSS.parseARule(
parseCSS.parseACommaSeparatedListOfComponentValues(
stringify(rule.prelude).trim()
).map(splitSelector =>
parseCSS.parseACommaSeparatedListOfComponentValues(
declaration.name.slice(2 + propertyName.length)
).map(selector => stringify(splitSelector) + stringify(selector)).join(', ')
).join(', ')
+ stringify(declaration.value)
)
)
)
}
})
} else {
output.push(rule)
}
return output
}
function expandNestedAtRule(rule) {
if (
rule.value
&& rule.value.value
) {
const output = []
const firstColon = rule.value.value.findIndex(({tokenType}) => tokenType === ':')
const firstAt = rule.value.value.findIndex(({tokenType}) => tokenType === 'AT-KEYWORD')
const firstBlock = rule.value.value.findIndex(({type}) => type === 'BLOCK')
let rules = []
if (
[firstAt, firstColon].every(index => index < firstBlock)
) {
rules = parseCSS.parseAListOfRules(
stringify(rule.value.value)
)
}
if (rules.length) {
rules.forEach(child => {
output.push(
expandNestedStylesheet(child.toSource())
)
})
rule.value.value = parseCSS.parseAListOfComponentValues(
output.join('\n')
)
}
}
return rule
}
// Process rules in stylesheet
parseCSS.parseAStylesheet(string).value.forEach(rule => {
if (rule.type === 'QUALIFIED-RULE') {
output.push(...expandNestedRule(rule))
}
if (rule.type === 'AT-RULE') {
output.push(expandNestedAtRule(rule))
}
})
return stringify(output, '\n')
}
console.log(
expandNestedStylesheet(css)
)
// mkdir node_modules && cd node_modules && git clone [email protected]:tomhodgins/parse-css.git
// node nesting-node.js 'a { color: red; ---\ b: { color: green }; }'
// node nesting-node.js path/to/stylesheet.css
const parseCSS = require('parse-css/index.cjs.js')
const fs = require('fs')
let file = process.argv.slice(2)[0]
let css = file
if (fs.existsSync(file)) {
css = fs.readFileSync(file).toString()
}
function expandNestedStylesheet(string, propertyName = '-') {
const output = []
const stringify = (list = [], joiner = '') => list
.map(token => token.toSource())
.join(joiner)
function expandNestedRule(rule) {
const output = []
const declarations = parseCSS.parseAListOfDeclarations(
stringify(rule.value.value)
)
if (declarations.length) {
const filteredDeclarations = declarations.filter(({name}) =>
name.startsWith(`--${propertyName}`) === false
)
if (filteredDeclarations.length) {
output.push(
parseCSS.parseARule(`
${stringify(rule.prelude)} {
${stringify(filteredDeclarations, ';')}
}
`)
)
}
// Process nested properties
declarations.forEach(declaration => {
if (declaration.name.startsWith(`--${propertyName}`)) {
output.push(
...expandNestedRule(
parseCSS.parseARule(
parseCSS.parseACommaSeparatedListOfComponentValues(
stringify(rule.prelude).trim()
).map(splitSelector =>
parseCSS.parseACommaSeparatedListOfComponentValues(
declaration.name.slice(2 + propertyName.length)
).map(selector => stringify(splitSelector) + stringify(selector)).join(', ')
).join(', ')
+ stringify(declaration.value)
)
)
)
}
})
} else {
output.push(rule)
}
return output
}
function expandNestedAtRule(rule) {
if (
rule.value
&& rule.value.value
) {
const output = []
const firstColon = rule.value.value.findIndex(({tokenType}) => tokenType === ':')
const firstAt = rule.value.value.findIndex(({tokenType}) => tokenType === 'AT-KEYWORD')
const firstBlock = rule.value.value.findIndex(({type}) => type === 'BLOCK')
let rules = []
if (
[firstAt, firstColon].every(index => index < firstBlock)
) {
rules = parseCSS.parseAListOfRules(
stringify(rule.value.value)
)
}
if (rules.length) {
rules.forEach(child => {
output.push(
expandNestedStylesheet(child.toSource())
)
})
rule.value.value = parseCSS.parseAListOfComponentValues(
output.join('\n')
)
}
}
return rule
}
// Process rules in stylesheet
parseCSS.parseAStylesheet(string).value.forEach(rule => {
if (rule.type === 'QUALIFIED-RULE') {
output.push(...expandNestedRule(rule))
}
if (rule.type === 'AT-RULE') {
output.push(expandNestedAtRule(rule))
}
})
return stringify(output, '\n')
}
console.log(
expandNestedStylesheet(css)
)
a {
level: 1;
---\ b: {
level: 2;
---\ c: {
level: 3;
---\ d: {
level: 4;
---\ e: {
level: 5;
---\ f: {
level: 6;
---\ g: {
level: 7;
---\ h: {
level: 8;
---\ i: {
level: 9;
---\ j: {
level: 10;
---\ k: {
level: 11;
---\ l: {
level: 12;
---\ m: {
level: 13;
---\ n: {
level: 14;
---\ o: {
level: 15;
---\ p: {
level: 16;
---\ q: {
level: 17;
---\ r: {
level: 18;
---\ s: {
level: 19;
---\ t: {
level: 20;
---\ u: {
level: 21;
---\ v: {
level: 22;
---\ w: {
level: 23;
---\ x: {
level: 24;
---\ y: {
level: 25;
---\ z: {
level: 26;
};
};
};
};
};
};
};
};
};
};
};
};
};
};
};
};
};
};
};
};
};
};
};
};
};
}
a { level: 1 }
a b { level: 2 }
a b c { level: 3 }
a b c d { level: 4 }
a b c d e { level: 5 }
a b c d e f { level: 6 }
a b c d e f g { level: 7 }
a b c d e f g h { level: 8 }
a b c d e f g h i { level: 9 }
a b c d e f g h i j { level: 10 }
a b c d e f g h i j k { level: 11 }
a b c d e f g h i j k l { level: 12 }
a b c d e f g h i j k l m { level: 13 }
a b c d e f g h i j k l m n { level: 14 }
a b c d e f g h i j k l m n o { level: 15 }
a b c d e f g h i j k l m n o p { level: 16 }
a b c d e f g h i j k l m n o p q { level: 17 }
a b c d e f g h i j k l m n o p q r { level: 18 }
a b c d e f g h i j k l m n o p q r s { level: 19 }
a b c d e f g h i j k l m n o p q r s t { level: 20 }
a b c d e f g h i j k l m n o p q r s t u { level: 21 }
a b c d e f g h i j k l m n o p q r s t u v { level: 22 }
a b c d e f g h i j k l m n o p q r s t u v w { level: 23 }
a b c d e f g h i j k l m n o p q r s t u v w x { level: 24 }
a b c d e f g h i j k l m n o p q r s t u v w x y { level: 25 }
a b c d e f g h i j k l m n o p q r s t u v w x y z { level: 26 }
h1, h2, h3, h4, h5, h6 {
---\ \+: {
---\ h1\,\ h2\,\ h3\,\ h4\,\ h5\,\ h6: {
margin-top: .5em;
};
};
}
h1 + h1,
h1 + h2,
h1 + h3,
h1 + h4,
h1 + h5,
h1 + h6,
h2 + h1,
h2 + h2,
h2 + h3,
h2 + h4,
h2 + h5,
h2 + h6,
h3 + h1,
h3 + h2,
h3 + h3,
h3 + h4,
h3 + h5,
h3 + h6,
h4 + h1,
h4 + h2,
h4 + h3,
h4 + h4,
h4 + h5,
h4 + h6,
h5 + h1,
h5 + h2,
h5 + h3,
h5 + h4,
h5 + h5,
h5 + h6,
h6 + h1,
h6 + h2,
h6 + h3,
h6 + h4,
h6 + h5,
h6 + h6 {
margin-top: 0.5em;
}
1 {
---a\,b: {
color: cyan;
}
}
2, 3 {
---c: {
color: magenta;
}
}
4, 5 {
---d\,e: {
color: yellow;
}
}
1a,
1b {
color: cyan;
}
2c,
3c {
color: magenta;
}
4d,
4e,
5d,
5e {
color: yellow;
}
@media print {
a {
color: black;
text-decoration: underline;
---\:\:after: {
content: ' [link: ' attr(href) ' ]';
};
}
}
@media print {
a {
color: black;
text-decoration: underline;
}
a::after {
content: ' [link: ' attr(href) ' ]';
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment