Skip to content

Instantly share code, notes, and snippets.

@Nixinova
Last active May 27, 2021 06:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Nixinova/4ac718e30629d026fac1613428198d2d to your computer and use it in GitHub Desktop.
Save Nixinova/4ac718e30629d026fac1613428198d2d to your computer and use it in GitHub Desktop.
Nested CSS Tokenizer example
let cssOutput = `
outer1 {
prop1: true;
inner {prop2: true;}
}
outer2 {
prop3: false;
}
`
type TokenName = 'Root' | 'Style' | 'Block';
type Token = { name: TokenName, content: string, body: Token[] };
let content: string = '';
let tokenTree: Token[] = [{ name: 'Root', content: '', body: [] }];
let selectorTree: string[] = [];
// loop through stylesheet and create a token tree
for (let i = 0; i < cssOutput.length; i++) {
const char: string = cssOutput[i];
const stylesMatch: RegExp = /[\w-]+:[^;]+;/g;
const trailingSelectorMatch: RegExp = /[^;]+$/;
let currentToken: Token = tokenTree[tokenTree.length - 1];
if (char === '{') {
if (!content) continue;
let selector: string = content.replace(stylesMatch, '').trim();
let styles: string = content.replace(trailingSelectorMatch, '').trim();
if (styles) {
currentToken.body.push({ name: 'Style', content: styles, body: [] });
}
const parentSelector: string = selectorTree[selectorTree.length - 1] || '';
if (selector.includes('&')) {
selector = selector.replace(/&/g, parentSelector).trim();
}
else {
selector = parentSelector.split(',').map(psel => psel + ' ' + selector).join(',');
}
selectorTree.push(selector.trim());
let newToken: Token = { name: 'Block', content: selector, body: [] };
currentToken.body.push(newToken);
currentToken = newToken;
tokenTree.push(currentToken);
content = '';
}
else if (char === '}') {
if (tokenTree.length < 1 || !content) continue;
selectorTree.pop();
currentToken = tokenTree.pop() as Token;
if (content.trim()) {
currentToken.body.push({ name: 'Style', content: content.trim(), body: [] });
}
content = '';
}
else {
content += char;
}
}
// clear parsed blocks
const blockRegex: RegExp = /[^{}]+{[^{}]*}/gs;
while (blockRegex.test(cssOutput)) {
cssOutput = cssOutput.replace(blockRegex, '');
}
// move all sub-blocks to root
let blocks: Token[] = [];
const flatten = (obj: Token): void => {
if (!obj) return;
for (const o of obj.body) {
if (o.name === 'Style') blocks.push({ name: obj.name, content: obj.content, body: [o] });
else flatten(o);
}
};
flatten(tokenTree[0]);
// create unnested CSS
let flattenedOutput: string = '';
for (const block of blocks) {
flattenedOutput += block.content + ' {' + block.body[0].content + '}';
}
# Try it out: https://cutt.ly/zzpEjNo
let cssOutput = `
outer1 {
prop1: true;
inner {prop2: true;}
}
outer2 {
prop3: false;
}
`
interface Token {
name: string,
content: string,
body: Token[],
}
const tokenized: Token[] = [{ name: 'Root', content: '', body: [] }];
let selector: string = '';
let tokenTree: Token[] = [tokenized[0]];
for (let i = 0; i < cssOutput.length; i++) {
const c = cssOutput[i];
let currentToken = tokenTree[tokenTree.length - 1];
selector = selector.trim()
if (c === ';') {
currentToken.body.push({ name: 'CSS', content: selector, body: [] })
selector = '';
}
else if (c === '{') {
if (!selector) continue;
let newToken: Token = { name: 'Selector', content: selector, body: [] };
currentToken.body.push(newToken)
currentToken = newToken;
tokenTree.push(currentToken);
selector = '';
}
else if (c === '}') {
if (tokenTree.length < 1 || !selector) continue;
currentToken = tokenTree.pop() as Token;
currentToken.body.push({ name: 'CSS', content: selector, body: [] })
selector = '';
}
else selector += c;
}
console.log(tokenized)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment