diff --git a/.gitignore b/.gitignore index 1f13e60..ed1198a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ Gemfile.lock node_modules .wrangler .git-metadata +.cache diff --git a/package-lock.json b/package-lock.json index de7cfcf..be6d8e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "devDependencies": { "autoprefixer": "^10.5.0", "postcss": "^8.5.13", + "postcss-selector-parser": "^6.1.2", "tailwindcss": "^3.4.19" } }, diff --git a/package.json b/package.json index fb78521..cc17c03 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "devDependencies": { "autoprefixer": "^10.5.0", "postcss": "^8.5.13", + "postcss-selector-parser": "^6.1.2", "tailwindcss": "^3.4.19" } } diff --git a/scripts/combine-classes.js b/scripts/combine-classes.js new file mode 100644 index 0000000..9dcc6d6 --- /dev/null +++ b/scripts/combine-classes.js @@ -0,0 +1,697 @@ +#!/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. +// +// 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 = 1; +// 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