#!/usr/bin/env node // Class combiner - runs after post-build.js has minified class names. // // For every page, finds sets of 1, 2, 3, ... classes that appear together on // elements and replaces each chosen set with one short combo class (e.g. // class="c8 c26 c27 c15" -> class="a c15"). A combo only pays off when the // class characters it removes from the HTML outweigh the CSS characters it // adds, so the gain of every candidate set is: // // (class chars saved on every element that uses the set) // - (selector chars appended to every CSS rule that targets the set) // // CSS is never copied into new rules. The combo name is appended to the // selector list of each existing rule (`.c8{display:flex}` -> // `.c8,.a{display:flex}`), so every declaration keeps its original position in // the cascade and its original specificity (`.c61.c62` becomes `.a.a`). This is // what broke the old combineClasses() in post-build.js: it moved declarations // into a new rule at the end of the stylesheet, which changed which rule won // against dark: and responsive variants. // // Every page inlines the whole site's CSS, so selectors that cannot match // anything on the page are dropped, and rules left without selectors are // deleted. This also counts toward a combo's gain: once a combo replaces the // last use of `.c8` on a page, `.c8,.a{...}` becomes `.a{...}` and the combo // costs no CSS at all. Classes that scripts may add at runtime (`dark`) and // classes inside :not() are never treated as missing. // // Results are cached in .cache/class-combos.json. On the next build a page is // only re-analysed for the class lists that changed since the cached run; a // page whose class lists and CSS are both unchanged reuses its cached combos. // // Usage: node scripts/combine-classes.js [--site _site] [--cache path] // [--no-cache] [--dry-run] [--only index.html] [--quiet] const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const postcss = require('postcss'); const selectorParser = require('postcss-selector-parser'); const CACHE_VERSION = 2; // Elements with more eligible classes than this only get subsets up to // LARGE_ELEMENT_MAX_SUBSET in size, so the subset count stays bounded. const FULL_ENUM_LIMIT = 16; const LARGE_ELEMENT_MAX_SUBSET = 5; // Selectors whose variant count would exceed this are left alone. const MAX_VARIANTS_PER_SELECTOR = 64; const CLASS_ATTR = /(\sclass=")([^"]*)(")/g; const STYLE_BLOCK = /(]*>)([\s\S]*?)(<\/style>)/g; const SCRIPT_BLOCK = /]*>([\s\S]*?)<\/script>/g; function sha1(s) { return crypto.createHash('sha1').update(s).digest('hex'); } // --------------------------------------------------------------------------- // Page analysis // --------------------------------------------------------------------------- // Collects every complex selector in the page's