// 自定义侧栏工具代码处理: // - parseMixedWidgetCode / mergeWidgetCode:「混写一框」(内联 `); if (parts.js.trim()) out.push(``); return out.join("\n\n"); } /* ─────────────────────────── CSS 作用域 ─────────────────────────── */ // 按 , 分割选择器列表,忽略 () / [] 与引号内的逗号(:is(a,b)、[attr="a,b"]) function splitSelectorList(sel: string): string[] { const out: string[] = []; let depth = 0; let quote = ""; let start = 0; for (let i = 0; i < sel.length; i++) { const ch = sel[i]; if (quote) { if (ch === "\\") i++; else if (ch === quote) quote = ""; continue; } if (ch === '"' || ch === "'") { quote = ch; continue; } if (ch === "(" || ch === "[") depth++; else if (ch === ")" || ch === "]") depth = Math.max(0, depth - 1); else if (ch === "," && depth === 0) { out.push(sel.slice(start, i)); start = i + 1; } } out.push(sel.slice(start)); return out; } // 单个选择器加前缀: // - :root / :scope / html / body 开头 → 视作容器本身(变量定义在容器上,板块内可用); // - 以伪类/伪元素(:)开头 → 紧贴容器(容器自身状态,如 .j13-widget-1:hover); // - 其余 → 后代选择器。 function scopeOneSelector(sel: string, prefix: string): string | null { const s = sel.trim(); if (!s) return null; const rootLike = s.match(/^(?::root\b|:scope\b|html\b|body\b)([\s\S]*)$/); if (rootLike) { const rest = rootLike[1].replace(/^\s+/, ""); return rest ? `${prefix} ${rest}` : prefix; } if (s.startsWith(":")) { // 功能型伪类开头(:is( / :where( / :has( / :not( )→ 按容器后代处理; // 简单伪类/伪元素(:hover、::before…)→ 紧贴容器(容器自身状态)。 return /^::?[a-zA-Z-]+\(/.test(s) ? `${prefix} ${s}` : prefix + s; } return `${prefix} ${s}`; } /** * 把 CSS 限定到容器内: * - 普通规则:选择器逐个加 `.j13-widget-{id}` 前缀; * - @media/@supports/@container/@layer 块:头部原样,内部规则递归前缀化; * - @keyframes/@font-face/@property 等:整块原样(内部是帧/描述符,不是选择器); * - 注释、引号内容(如 content:"{")不影响括号配对。 * 已知取舍:同名 @keyframes 在多个工具间仍是全局的,命名请自带区分。 */ export function scopeWidgetCss(css: string, id: number): string { const prefix = `.j13-widget-${id}`; const n = css.length; let out = ""; let i = 0; // 从 from 起读一个平衡的 {...} 块(引号与转义内的括号不计入深度) const readBlock = (from: number): { body: string; end: number } => { let depth = 0; let quote = ""; for (let j = from; j < n; j++) { const ch = css[j]; if (quote) { if (ch === "\\") j++; else if (ch === quote) quote = ""; continue; } if (ch === '"' || ch === "'") quote = ch; else if (ch === "{") depth++; else if (ch === "}") { depth--; if (depth === 0) return { body: css.slice(from, j + 1), end: j + 1 }; } } return { body: css.slice(from), end: n }; }; while (i < n) { if (css.startsWith("/*", i)) { const close = css.indexOf("*/", i + 2); const stop = close === -1 ? n : close + 2; out += css.slice(i, stop); i = stop; continue; } if (css[i] === "}") { // 多余闭括号容错,原样透传 out += css[i]; i++; continue; } // 读取选择器/语句头直到 { ; } 或结尾(引号内不截断) let j = i; let quote = ""; for (; j < n; j++) { const c = css[j]; if (quote) { if (c === "\\") j++; else if (c === quote) quote = ""; continue; } if (c === '"' || c === "'") { quote = c; continue; } if (c === "{" || c === ";" || c === "}") break; } const head = css.slice(i, j); if (j >= n) { out += head; break; } if (css[j] === ";") { // @import / @charset / @layer a, b; 等语句原样保留 out += head + ";"; i = j + 1; continue; } if (css[j] === "}") { out += head; i = j + 1; continue; } const { body, end } = readBlock(j); const trimmedHead = head.trim(); if (trimmedHead.startsWith("@")) { const at = trimmedHead.split(/[\s(]/)[0].toLowerCase(); if (at === "@media" || at === "@supports" || at === "@container" || at === "@layer") { const inner = body.slice(1, -1); out += head + "{" + scopeWidgetCss(inner, id) + "}"; } else { out += head + body; } } else { const scoped = splitSelectorList(head) .map((s) => scopeOneSelector(s, prefix)) .filter((s): s is string => s !== null); if (scoped.length) { // 保留选择器前后的原始空白,尽量不改变站长代码的排版 const lead = head.match(/^\s*/)?.[0] ?? ""; const trail = head.match(/\s*$/)?.[0] ?? ""; out += lead + scoped.join(", ") + trail + body; } else { out += head + body; } } i = end; } return out; }