Skip to content

Instantly share code, notes, and snippets.

@christianscott
Last active May 9, 2020 11:40
Show Gist options
  • Save christianscott/8c429e24abf4c787d6100f598cb85019 to your computer and use it in GitHub Desktop.
Save christianscott/8c429e24abf4c787d6100f598cb85019 to your computer and use it in GitHub Desktop.
CSS atomizer
import { atomizeCss } from "../";
describe(`${atomizeCss.name}`, () => {
it("works with a simple rule", () => {
const { newCss, selectorMapping } = atomizeCss(`.class { color: red; }`);
expect(newCss).toBe(".a0 { color:red; }");
expect(selectorMapping.get(".class")).toEqual([".a0"]);
});
test("de-dupes between rules", () => {
const { newCss, selectorMapping } = atomizeCss(
`.class1 { color: red; }\n.class2 { color: red; }`
);
expect(newCss).toBe(".a0 { color:red; }");
expect(selectorMapping.get(".class1")).toEqual([".a0"]);
expect(selectorMapping.get(".class2")).toEqual([".a0"]);
});
test("de-dupes between rules that don't overlap completely", () => {
const { newCss, selectorMapping } = atomizeCss(
`.class1 { color: red; }\n.class2 { color: red; border-color: blue; }`
);
expect(newCss).toBe(".a0 { color:red; }\n.a1 { border-color:blue; }");
expect(selectorMapping.get(".class1")).toEqual([".a0"]);
expect(selectorMapping.get(".class2")).toEqual([".a0", ".a1"]);
});
});
import * as cssTree from "css-tree";
export function atomizeCss(css: string) {
const ast = cssTree.parse(css);
const rules = cssTree.findAll(
ast,
(node) => node.type === "Rule"
) as cssTree.Rule[];
const stylesToSelectors = new DefaultMap<Set<string>>(() => new Set());
for (const rule of rules) {
const selectors = new Set<string>();
cssTree.walk(rule.prelude, (node) => {
if (node.type === "Selector") {
selectors.add(cssTree.generate(node));
}
});
cssTree.walk(rule.block, (node) => {
if (node.type === "Declaration") {
const style = cssTree.generate(node);
const selectorsForStyle = stylesToSelectors.get(style);
for (const selector of selectors) {
selectorsForStyle.add(selector);
}
}
});
}
const getNextSelector = (() => {
let nextId = 0;
return () => `.a${nextId++}`;
})();
const atomizedStyles = new Map<string, string>();
const selectorMapping = new DefaultMap<string[]>(() => []);
for (const [style, selectors] of stylesToSelectors) {
const outSelector = getNextSelector();
atomizedStyles.set(outSelector, style);
for (const selector of selectors) {
selectorMapping.get(selector).push(outSelector);
}
}
let newCss = "";
for (const [selector, style] of atomizedStyles) {
newCss += `${selector} { ${style}; }\n`;
}
return { newCss: newCss.trim(), selectorMapping: selectorMapping.asMap() };
}
/**
* A map that inserts a default value if a key does not exist.
* Inspired by python's defaultdict.
*/
class DefaultMap<V> {
private readonly map = new Map<string, V>();
constructor(private readonly makeDefault: () => V) {}
get(key: string): V {
if (!this.map.has(key)) {
const value = this.makeDefault();
this.map.set(key, value);
return value;
}
return this.map.get(key);
}
asMap(): Map<string, V> {
return new Map([...this.map]);
}
[Symbol.iterator]() {
return this.map[Symbol.iterator]();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment