Files
ComfyAssets-ComfyUI_PromptM…/tools/vendor/sync-concept-css.mjs
Vito Sansevero 12aa1e51d6 test(workspace): accept only #249 decision markers and state the lint scope
The overlay lint accepted any D-number, so another issue's ruling such
as "D5 (#245)" could mark a concept override. A marker now has to be a
D-number with no other issue cited; the vocabulary rule that cited #245
reads "#245 decision 5". TESTING.md now states that the lint covers
workspace.css only, lists the unmarked overrides still in images.css,
and says PRs C and D run the lint over each page sheet they keep.
2026-10-19 22:48:39 -07:00

131 lines
5.8 KiB
JavaScript

import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { repositoryRoot } from './vendor-viewerjs.mjs';
const CONCEPT = 'docs/concepts/workspace/style.css';
const COPY = 'web/js/workspace/concept.css';
const MARKER = /\/\*[^*]*\bD\d+\b[^*]*\*\//;
const SPEC_ISSUE = '#249';
// A decision marker names one of this spec's (#249) D-numbers. A comment that cites any other issue is that issue's
// ruling (for example #245's own D5), so it never marks a concept override here.
function isDecisionMarker(comment) {
return MARKER.test(comment) && (comment.match(/#\d+\b/g) ?? []).every(issue => issue === SPEC_ISSUE);
}
// Minimal tokenizer: walks top-level rules and the bodies of @media/@supports,
// returning each rule's normalized selectors and the comments attached to it
// (those before its selector and those inside its block).
function parseRules(css) {
const rules = [];
let index = 0;
const length = css.length;
function readBlock(start) {
let depth = 0;
let cursor = start;
for (; cursor < length; cursor += 1) {
if (css.startsWith('/*', cursor)) { cursor = commentEnd(cursor) - 1; continue; }
const char = css[cursor];
if (char === '"' || char === "'") { cursor = stringEnd(cursor) - 1; continue; }
if (char === '{') depth += 1;
else if (char === '}') { depth -= 1; if (depth === 0) return cursor; }
}
return length - 1;
}
function commentEnd(at) { const end = css.indexOf('*/', at + 2); return end === -1 ? length : end + 2; }
function stringEnd(at) {
for (let cursor = at + 1; cursor < length; cursor += 1) {
if (css[cursor] === '\\') cursor += 1;
else if (css[cursor] === css[at]) return cursor + 1;
}
return length;
}
function walk(from, to) {
let cursor = from;
let comments = [];
let prelude = '';
while (cursor < to) {
if (css.startsWith('/*', cursor)) {
const end = commentEnd(cursor);
comments.push(css.slice(cursor, end));
cursor = end;
continue;
}
const char = css[cursor];
if (char === ';') { prelude = ''; comments = []; cursor += 1; continue; }
if (char === '{') {
const close = readBlock(cursor);
const head = prelude.trim();
const body = css.slice(cursor + 1, close);
if (/^@(media|supports|layer)\b/i.test(head)) walk(cursor + 1, close);
else if (!head.startsWith('@')) {
rules.push({ selectors: splitSelectors(head), comments: [...comments, ...(body.match(/\/\*[\s\S]*?\*\//g) ?? [])] });
}
prelude = ''; comments = []; cursor = close + 1;
continue;
}
prelude += char;
cursor += 1;
}
}
walk(index, length);
return rules;
}
function splitSelectors(head) {
const out = [];
let depth = 0;
let current = '';
for (const char of head) {
if (char === '(' || char === '[') depth += 1;
else if (char === ')' || char === ']') depth -= 1;
if (char === ',' && depth === 0) { out.push(current); current = ''; } else current += char;
}
out.push(current);
return out.map(selector => selector.replace(/\s+/g, ' ').trim()).filter(Boolean);
}
export function lintOverlay(conceptCss, overlayCss) {
const owned = new Set(parseRules(conceptCss).flatMap(rule => rule.selectors));
const violations = [];
for (const rule of parseRules(overlayCss)) {
const shared = rule.selectors.filter(selector => owned.has(selector));
if (shared.length && !rule.comments.some(isDecisionMarker)) {
violations.push(`${shared.join(', ')}: overlay rule on a concept selector needs a /* D<n> */ (#249) decision marker`);
}
}
return { violations };
}
export function syncConceptCss({ root = repositoryRoot, check = false } = {}) {
const source = readFileSync(join(root, CONCEPT));
const target = join(root, COPY);
if (check) {
let current = null;
try { current = readFileSync(target); } catch (error) { if (error.code !== 'ENOENT') throw error; }
if (!current) throw new Error(`Concept stylesheet drift: ${COPY} is missing; run sync-concept-css.mjs --write`);
if (!current.equals(source)) {
let offset = 0;
while (offset < source.length && offset < current.length && source[offset] === current[offset]) offset += 1;
throw new Error(`Concept stylesheet drift: ${COPY} (${current.length} bytes) differs from ${CONCEPT} (${source.length} bytes) at byte ${offset}; run sync-concept-css.mjs --write`);
}
} else {
mkdirSync(join(root, 'web/js/workspace'), { recursive: true });
writeFileSync(target, source);
}
return source.length;
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
try {
const args = process.argv.slice(2);
const rootAt = args.indexOf('--root');
const root = rootAt === -1 ? repositoryRoot : resolve(args[rootAt + 1]);
const mode = args.filter((arg, at) => arg.startsWith('--') && arg !== '--root' && !(rootAt !== -1 && at === rootAt + 1));
if (mode.length !== 1 || !['--write', '--check'].includes(mode[0])) throw new Error('Usage: sync-concept-css.mjs --write | --check [--root <dir>]');
const bytes = syncConceptCss({ root, check: mode[0] === '--check' });
console.log(`Concept stylesheet ${mode[0] === '--check' ? 'verified' : 'written'} (${bytes} bytes)`);
} catch (error) { console.error(error.message); process.exitCode = 1; }
}