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",
|
"version": "0.4.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chenglou/pretext": "^0.0.5",
|
"@chenglou/pretext": "^0.0.5",
|
||||||
|
"postcss": "^8.5.8",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jsdom": "^28.0.0",
|
"@types/jsdom": "^28.0.0",
|
||||||
|
|||||||
@@ -102,7 +102,8 @@
|
|||||||
"prepublishOnly": "echo skip"
|
"prepublishOnly": "echo skip"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chenglou/pretext": "^0.0.5"
|
"@chenglou/pretext": "^0.0.5",
|
||||||
|
"postcss": "^8.5.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jsdom": "^28.0.0",
|
"@types/jsdom": "^28.0.0",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import postcss, { type AtRule, type Node, type Rule } from "postcss";
|
||||||
|
|
||||||
function escapeRegExp(value: string): string {
|
function escapeRegExp(value: string): string {
|
||||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
}
|
}
|
||||||
@@ -6,137 +8,6 @@ function escapeCssAttributeValue(value: string): string {
|
|||||||
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
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 {
|
function scopeSelector(selector: string, scope: string, compositionId: string): string {
|
||||||
const selectorWithoutRootTiming = normalizeCompositionRootSelector(
|
const selectorWithoutRootTiming = normalizeCompositionRootSelector(
|
||||||
selector,
|
selector,
|
||||||
@@ -168,60 +39,37 @@ function normalizeCompositionRootSelector(
|
|||||||
.replace(new RegExp(`(?:${timingAttr})+${compAttr}`, "g"), scope);
|
.replace(new RegExp(`(?:${timingAttr})+${compAttr}`, "g"), scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scopeSelectorList(selectorText: string, scope: string, compositionId: string): string {
|
const GLOBAL_AT_RULES = new Set(["keyframes", "-webkit-keyframes", "font-face"]);
|
||||||
return splitSelectorList(selectorText)
|
|
||||||
.map((selector) => scopeSelector(selector, scope, compositionId))
|
function isAtRuleNode(node: Node["parent"]): node is AtRule {
|
||||||
.join(",");
|
return node?.type === "atrule";
|
||||||
}
|
}
|
||||||
|
|
||||||
function scopeCssBlock(css: string, scope: string, compositionId: string): string {
|
function isInsideGlobalAtRule(rule: Rule): boolean {
|
||||||
let output = "";
|
let current: Node["parent"] = rule.parent;
|
||||||
let index = 0;
|
while (current) {
|
||||||
const globalAtRules = new Set(["keyframes", "-webkit-keyframes", "font-face"]);
|
if (isAtRuleNode(current) && GLOBAL_AT_RULES.has(current.name.toLowerCase())) {
|
||||||
|
return true;
|
||||||
while (index < css.length) {
|
|
||||||
const braceIndex = findNextCssToken(css, index, "{");
|
|
||||||
if (braceIndex < 0) {
|
|
||||||
output += css.slice(index);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
current = current.parent;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
return output;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scopeCssToComposition(css: string, compositionId: string): string {
|
export function scopeCssToComposition(css: string, compositionId: string): string {
|
||||||
const trimmedCompositionId = compositionId.trim();
|
const trimmedCompositionId = compositionId.trim();
|
||||||
if (!css || !trimmedCompositionId) return css;
|
if (!css || !trimmedCompositionId) return css;
|
||||||
const scope = `[data-composition-id="${escapeCssAttributeValue(trimmedCompositionId)}"]`;
|
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(
|
export function wrapScopedCompositionScript(
|
||||||
|
|||||||
Reference in New Issue
Block a user