mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
refactor: use postcss for composition CSS scoping
This commit is contained in:
@@ -65,6 +65,7 @@
|
||||
"version": "0.4.11",
|
||||
"dependencies": {
|
||||
"@chenglou/pretext": "^0.0.5",
|
||||
"postcss": "^8.5.8",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsdom": "^28.0.0",
|
||||
|
||||
@@ -102,7 +102,8 @@
|
||||
"prepublishOnly": "echo skip"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chenglou/pretext": "^0.0.5"
|
||||
"@chenglou/pretext": "^0.0.5",
|
||||
"postcss": "^8.5.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsdom": "^28.0.0",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import postcss, { type AtRule, type Node, type Rule } from "postcss";
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
@@ -6,137 +8,6 @@ function escapeCssAttributeValue(value: string): string {
|
||||
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
function findNextCssToken(css: string, start: number, token: "{" | ";"): number {
|
||||
let quote: string | null = null;
|
||||
let inComment = false;
|
||||
for (let i = start; i < css.length; i++) {
|
||||
const char = css[i];
|
||||
const next = css[i + 1];
|
||||
if (inComment) {
|
||||
if (char === "*" && next === "/") {
|
||||
inComment = false;
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (quote) {
|
||||
if (char === "\\") {
|
||||
i++;
|
||||
} else if (char === quote) {
|
||||
quote = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (char === "/" && next === "*") {
|
||||
inComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (char === '"' || char === "'") {
|
||||
quote = char;
|
||||
continue;
|
||||
}
|
||||
if (char === token) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function findMatchingCssBrace(css: string, openIndex: number): number {
|
||||
let depth = 0;
|
||||
let quote: string | null = null;
|
||||
let inComment = false;
|
||||
for (let i = openIndex; i < css.length; i++) {
|
||||
const char = css[i];
|
||||
const next = css[i + 1];
|
||||
if (inComment) {
|
||||
if (char === "*" && next === "/") {
|
||||
inComment = false;
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (quote) {
|
||||
if (char === "\\") {
|
||||
i++;
|
||||
} else if (char === quote) {
|
||||
quote = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (char === "/" && next === "*") {
|
||||
inComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (char === '"' || char === "'") {
|
||||
quote = char;
|
||||
continue;
|
||||
}
|
||||
if (char === "{") {
|
||||
depth++;
|
||||
} else if (char === "}") {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function splitSelectorList(selectorText: string): string[] {
|
||||
const selectors: string[] = [];
|
||||
let current = "";
|
||||
let quote: string | null = null;
|
||||
let inComment = false;
|
||||
let bracketDepth = 0;
|
||||
let parenDepth = 0;
|
||||
for (let i = 0; i < selectorText.length; i++) {
|
||||
const char = selectorText[i];
|
||||
const next = selectorText[i + 1];
|
||||
if (inComment) {
|
||||
current += char;
|
||||
if (char === "*" && next === "/") {
|
||||
current += next;
|
||||
inComment = false;
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (quote) {
|
||||
current += char;
|
||||
if (char === "\\") {
|
||||
current += next ?? "";
|
||||
i++;
|
||||
} else if (char === quote) {
|
||||
quote = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (char === "/" && next === "*") {
|
||||
current += char + next;
|
||||
inComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (char === '"' || char === "'") {
|
||||
current += char;
|
||||
quote = char;
|
||||
continue;
|
||||
}
|
||||
if (char === "[") bracketDepth++;
|
||||
if (char === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
||||
if (char === "(") parenDepth++;
|
||||
if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
|
||||
if (char === "," && bracketDepth === 0 && parenDepth === 0) {
|
||||
selectors.push(current);
|
||||
current = "";
|
||||
continue;
|
||||
}
|
||||
current += char;
|
||||
}
|
||||
selectors.push(current);
|
||||
return selectors;
|
||||
}
|
||||
|
||||
function scopeSelector(selector: string, scope: string, compositionId: string): string {
|
||||
const selectorWithoutRootTiming = normalizeCompositionRootSelector(
|
||||
selector,
|
||||
@@ -168,60 +39,37 @@ function normalizeCompositionRootSelector(
|
||||
.replace(new RegExp(`(?:${timingAttr})+${compAttr}`, "g"), scope);
|
||||
}
|
||||
|
||||
function scopeSelectorList(selectorText: string, scope: string, compositionId: string): string {
|
||||
return splitSelectorList(selectorText)
|
||||
.map((selector) => scopeSelector(selector, scope, compositionId))
|
||||
.join(",");
|
||||
const GLOBAL_AT_RULES = new Set(["keyframes", "-webkit-keyframes", "font-face"]);
|
||||
|
||||
function isAtRuleNode(node: Node["parent"]): node is AtRule {
|
||||
return node?.type === "atrule";
|
||||
}
|
||||
|
||||
function scopeCssBlock(css: string, scope: string, compositionId: string): string {
|
||||
let output = "";
|
||||
let index = 0;
|
||||
const globalAtRules = new Set(["keyframes", "-webkit-keyframes", "font-face"]);
|
||||
|
||||
while (index < css.length) {
|
||||
const braceIndex = findNextCssToken(css, index, "{");
|
||||
if (braceIndex < 0) {
|
||||
output += css.slice(index);
|
||||
break;
|
||||
function isInsideGlobalAtRule(rule: Rule): boolean {
|
||||
let current: Node["parent"] = rule.parent;
|
||||
while (current) {
|
||||
if (isAtRuleNode(current) && GLOBAL_AT_RULES.has(current.name.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const semicolonIndex = findNextCssToken(css, index, ";");
|
||||
if (semicolonIndex >= 0 && semicolonIndex < braceIndex) {
|
||||
output += css.slice(index, semicolonIndex + 1);
|
||||
index = semicolonIndex + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const closeIndex = findMatchingCssBrace(css, braceIndex);
|
||||
if (closeIndex < 0) {
|
||||
output += css.slice(index);
|
||||
break;
|
||||
}
|
||||
|
||||
const prelude = css.slice(index, braceIndex);
|
||||
const body = css.slice(braceIndex + 1, closeIndex);
|
||||
const trimmedPrelude = prelude.trim();
|
||||
if (trimmedPrelude.startsWith("@")) {
|
||||
const atRuleName = trimmedPrelude.match(/^@([-\w]+)/)?.[1]?.toLowerCase() ?? "";
|
||||
const scopedBody = globalAtRules.has(atRuleName)
|
||||
? body
|
||||
: scopeCssBlock(body, scope, compositionId);
|
||||
output += `${prelude}{${scopedBody}}`;
|
||||
} else {
|
||||
output += `${scopeSelectorList(prelude, scope, compositionId)}{${body}}`;
|
||||
}
|
||||
index = closeIndex + 1;
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return output;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function scopeCssToComposition(css: string, compositionId: string): string {
|
||||
const trimmedCompositionId = compositionId.trim();
|
||||
if (!css || !trimmedCompositionId) return css;
|
||||
const scope = `[data-composition-id="${escapeCssAttributeValue(trimmedCompositionId)}"]`;
|
||||
return scopeCssBlock(css, scope, trimmedCompositionId);
|
||||
const root = postcss.parse(css);
|
||||
|
||||
root.walkRules((rule) => {
|
||||
if (isInsideGlobalAtRule(rule)) return;
|
||||
rule.selectors = rule.selectors.map((selector) =>
|
||||
scopeSelector(selector, scope, trimmedCompositionId),
|
||||
);
|
||||
});
|
||||
|
||||
return root.toResult({ map: false }).css;
|
||||
}
|
||||
|
||||
export function wrapScopedCompositionScript(
|
||||
|
||||
Reference in New Issue
Block a user