#!/usr/bin/env node /** * scope-css.js — prefix every selector in the "new front" stylesheet with * `#vc-app` so the ported design cannot leak into (or fight) the WordPress * theme. Dependency-free brace-aware parser. * * node build/scope-css.js "../new front/css/style.css" assets/css/vc-design.css * * Rules: * - `:root { ... }` is left untouched (global custom properties). * - `@keyframes` names and step selectors (`0%`, `from`, `to`) are untouched. * - `@font-face` descriptors are untouched. * - `@media` / `@supports` wrappers are kept; their inner rules are scoped. * - Leading `html` / `body` / `*` tokens are mapped into the `#vc-app` scope. * - Every other selector gets a `#vc-app ` prefix. */ 'use strict'; const fs = require('fs'); const SCOPE = process.env.VC_SCOPE || '.vc-app'; function prefixSelector(sel) { sel = sel.trim(); if (!sel) return sel; // Leave :root and keyframe-ish untouched (keyframes handled by caller). if (sel === ':root' || sel.startsWith(':root')) return sel; // html -> scope root if (sel === 'html' || sel.startsWith('html ') || sel.startsWith('html.') || sel.startsWith('html:') || sel.startsWith('html[')) { return SCOPE + sel.slice(4); } // body -> scope root if (sel === 'body' || sel.startsWith('body ') || sel.startsWith('body.') || sel.startsWith('body:') || sel.startsWith('body[') || sel.startsWith('body>')) { return SCOPE + sel.slice(4); } // Universal selector: `*`, `*::before`, `* + *` ... if (sel === '*' || sel.startsWith('*')) { return SCOPE + ' ' + sel; } return SCOPE + ' ' + sel; } function prefixSelectorList(prelude) { return prelude .split(',') .map(function (s) { return prefixSelector(s); }) .join(',\n'); } /** * Transform a run of CSS text at a given nesting context. * @param {string} css * @param {boolean} inKeyframes when true, rule preludes are step selectors -> keep as-is */ function transform(css, inKeyframes) { let out = ''; let i = 0; const n = css.length; let buf = ''; while (i < n) { const ch = css[i]; // Preserve comments verbatim. if (ch === '/' && css[i + 1] === '*') { const end = css.indexOf('*/', i + 2); const stop = end === -1 ? n : end + 2; buf += css.slice(i, stop); i = stop; continue; } if (ch === '{') { const prelude = buf; buf = ''; // Find the matching closing brace for this block. let depth = 1; let j = i + 1; while (j < n && depth > 0) { if (css[j] === '/' && css[j + 1] === '*') { const e = css.indexOf('*/', j + 2); j = e === -1 ? n : e + 2; continue; } if (css[j] === '{') depth++; else if (css[j] === '}') depth--; if (depth === 0) break; j++; } const inner = css.slice(i + 1, j); // Pull any comments out of the prelude and emit them before the rule, // so a comment sitting in front of a selector never gets scoped into it. const split = extractComments(prelude); out += split.comments; const trimmed = split.clean.trim(); const lower = trimmed.toLowerCase(); if (lower.startsWith('@keyframes') || lower.startsWith('@-webkit-keyframes')) { // Keep name; do not scope step selectors inside. out += trimmed + ' {' + inner + '}'; } else if (lower.startsWith('@font-face')) { out += trimmed + ' {' + inner + '}'; } else if (lower.startsWith('@media') || lower.startsWith('@supports') || lower.startsWith('@container') || lower.startsWith('@layer')) { // Recurse into the block to scope inner rules. out += trimmed + ' {\n' + transform(inner, false) + '}'; } else if (lower.startsWith('@')) { // Any other at-rule with a block: leave prelude, recurse cautiously. out += trimmed + ' {' + transform(inner, inKeyframes) + '}'; } else if (inKeyframes) { out += trimmed + ' {' + inner + '}'; } else { // Ordinary style rule -> scope the selector list. out += prefixSelectorList(trimmed) + ' {' + inner + '}'; } out += '\n\n'; i = j + 1; continue; } if (ch === '}') { // Stray close (end of a recursion slice) — flush and stop-ish. out += buf; buf = ''; i++; continue; } buf += ch; i++; } out += buf; return out; } // Separate comments from the selector prelude. Comments are returned intact // (to be emitted before the rule); `clean` is the comment-free selector text. function extractComments(prelude) { let comments = ''; const clean = prelude.replace(/\/\*[\s\S]*?\*\//g, function (c) { comments += c + '\n'; return ' '; }); return { comments: comments, clean: clean }; } function main() { const src = process.argv[2] || '../new front/css/style.css'; const dest = process.argv[3] || 'assets/css/vc-design.css'; const css = fs.readFileSync(src, 'utf8'); const header = '/* Generated by build/scope-css.js from ' + src + ' — do not edit by hand. */\n\n'; const out = header + transform(css, false).replace(/\n{3,}/g, '\n\n'); fs.writeFileSync(dest, out, 'utf8'); process.stdout.write('Wrote ' + dest + ' (' + out.length + ' bytes)\n'); } main();