Files
hoponhopofftickets/viator-collector/build/scope-css.js
amir 8feab03f00 Add Astra child theme for Hop On Hop Off header & footer
- disables Astra header/footer via astra_header/astra_footer hooks and renders
  custom design markup (logo, nav dropdown, footer columns, socials, support)
- chrome.css scoped to .hoho (generated from the design source); chrome.js for
  burger/submenu/sticky/back-to-top; bundled icon sprite (reuses plugin's)
- parameterize scope-css.js scope via VC_SCOPE env

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4hNL1PSHxXbu94SCenpKj
2026-09-15 14:53:20 +03:30

160 lines
5.2 KiB
JavaScript

#!/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();