- Added a lightbox feature for gallery images, allowing users to view photos in a modal with navigation controls. - Updated the SVG sprite to include inline styles for better accessibility. - Increased CSS specificity by doubling the `.vc-app` class to ensure styles override Astra theme defaults. - Introduced new shortcodes for bus tours, about section, and articles in the VC_Sections class. - Improved the handling of featured images in the single tour template for sharper display. - Updated version number to 1.8.15.
164 lines
5.5 KiB
JavaScript
164 lines
5.5 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');
|
|
|
|
// Doubled class (`.vc-app.vc-app`) so every ported rule carries an extra class
|
|
// of specificity — enough to win against Astra's content selectors (e.g.
|
|
// `.ast-… .entry-content a`) without touching the markup, which still only
|
|
// needs a single `class="vc-app"`. Override with VC_SCOPE if ever needed.
|
|
const SCOPE = process.env.VC_SCOPE || '.vc-app.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();
|