mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(core): preserve SVG icons in loaded compositions (#5096)
* fix(core): namespace SVG ids in runtime-mounted compositions * fix(core): keep SVG selectors and captured ids working after loaded scenes rename them * chore(core): generated artifacts carry a plain do-not-edit banner * test(catalog): record the font subsets the new scene-script installer requests * test(catalog): keep earlier mirror recordings, as re-records only add
This commit is contained in:
+3
-1
@@ -33,6 +33,8 @@
|
||||
// Same arrangement for the audio FX graph builders, bundled by
|
||||
// build-audio-fx-runtime.ts for the offline render page.
|
||||
"packages/core/stubs/audio-fx-runtime-entry.ts",
|
||||
// And for the SVG selector alias installer compiled scene scripts carry.
|
||||
"packages/core/stubs/svg-selector-aliases-entry.ts",
|
||||
// In-page audit scripts read as raw strings and injected via
|
||||
// page.addScriptTag (see layout.ts / validate.ts) — referenced by file
|
||||
// path, never imported, so they have no import-graph referrer.
|
||||
@@ -938,7 +940,7 @@
|
||||
"packages/studio/src/player/components/TimelineClipDiamonds.tsx",
|
||||
// Sub-composition html/body scoping fix: these files carry pre-existing
|
||||
// CRITICAL/HIGH functions (inlineSubCompositions, bundleToSingleHtml,
|
||||
// mountCompositionContent/loadExternalCompositions, scopeSelector/
|
||||
// mountCompositionContent/loadCompositions, scopeSelector/
|
||||
// replaceAuthoredRootIdSelectors) that pre-date this PR. The change only
|
||||
// threads a scopeRootSelectors option through the existing scoping calls;
|
||||
// the line-shift fingerprint re-flags the inherited complexity.
|
||||
|
||||
@@ -75,6 +75,8 @@ tmp/
|
||||
packages/core/src/generated/*
|
||||
!packages/core/src/generated/position-edits-render-inline.ts
|
||||
!packages/core/src/generated/position-edits-render-inline.test.ts
|
||||
!packages/core/src/generated/svg-selector-aliases-inline.ts
|
||||
!packages/core/src/generated/svg-selector-aliases-inline.test.ts
|
||||
packages/core/src/fonts/fontData.generated.ts
|
||||
|
||||
# Plan and contract documents (working artifacts, not committed)
|
||||
|
||||
@@ -848,9 +848,9 @@
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun run build:fonts && bun run build:hyperframes-runtime && bun run build:position-edits-render && bun run build:audio-fx-runtime && tsc && tsx scripts/rewrite-esm-extensions.ts",
|
||||
"build": "bun run build:fonts && bun run build:svg-selector-aliases && bun run build:hyperframes-runtime && bun run build:position-edits-render && bun run build:audio-fx-runtime && tsc && tsx scripts/rewrite-esm-extensions.ts",
|
||||
"build:fonts": "node scripts/build-fonts.mjs",
|
||||
"test": "bun run check:position-edits-render && bun run build:audio-fx-runtime && vitest run",
|
||||
"test": "bun run check:position-edits-render && bun run check:svg-selector-aliases && bun run build:audio-fx-runtime && vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:runtime-coverage": "vitest run --coverage src/runtime",
|
||||
@@ -861,6 +861,8 @@
|
||||
"build:hyperframes-runtime": "tsx scripts/build-hyperframes-runtime-artifact.ts",
|
||||
"build:position-edits-render": "tsx scripts/build-position-edits-render.ts",
|
||||
"check:position-edits-render": "bun run build:position-edits-render && git diff --exit-code -- src/generated/position-edits-render-inline.ts",
|
||||
"build:svg-selector-aliases": "tsx scripts/build-svg-selector-aliases.ts",
|
||||
"check:svg-selector-aliases": "bun run build:svg-selector-aliases && git diff --exit-code -- src/generated/svg-selector-aliases-inline.ts",
|
||||
"build:hyperframes-runtime:modular": "SANDBOX_RUNTIME_VARIANT=modular tsx scripts/build-hyperframes-runtime-artifact.ts",
|
||||
"build:hyperframe-runtime": "tsx scripts/build-hyperframes-runtime-artifact.ts",
|
||||
"test:hyperframe-runtime-contract": "tsx scripts/test-hyperframe-runtime-contract.ts",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/** Build the SVG selector alias installer that compiled scene scripts carry. */
|
||||
|
||||
import { buildInjectedArtifact } from "./buildInjectedArtifact.js";
|
||||
|
||||
buildInjectedArtifact({
|
||||
scriptUrl: import.meta.url,
|
||||
entry: "stubs/svg-selector-aliases-entry.ts",
|
||||
out: "svg-selector-aliases-inline.ts",
|
||||
constName: "SVG_SELECTOR_ALIASES_IIFE",
|
||||
fnName: "getSvgSelectorAliasesScript",
|
||||
what: "SVG selector alias installer IIFE",
|
||||
event: "svg_selector_aliases_generated",
|
||||
});
|
||||
@@ -52,7 +52,7 @@ export function buildInjectedArtifact(spec: InjectedArtifact): void {
|
||||
if (!iife) throw new Error(`esbuild produced no output for ${spec.entry.split("/").pop()}`);
|
||||
|
||||
const source = [
|
||||
`// AUTO-GENERATED by scripts/${scriptName} - do not edit`,
|
||||
`// Generated by scripts/${scriptName}; do not edit.`,
|
||||
`const ${spec.constName}: string = ${JSON.stringify(iife)};`,
|
||||
"",
|
||||
`/** Returns the pre-built ${spec.what} as a string constant. */`,
|
||||
|
||||
@@ -168,6 +168,19 @@ body { margin: 0; }
|
||||
expect(fakeWindow.__captured).toEqual({ title: "Pro", price: "$29" });
|
||||
});
|
||||
|
||||
it("keeps a classic authored script in sloppy mode", () => {
|
||||
const { document } = parseHTML(`<div data-composition-id="scene"></div>`);
|
||||
const fakeWindow: Record<string, unknown> = { document, __timelines: {} };
|
||||
const wrapped = wrapScopedCompositionScript(
|
||||
`window.__captured = (function() { return this !== undefined; })();`,
|
||||
"scene",
|
||||
);
|
||||
|
||||
new Function("window", wrapped)(fakeWindow);
|
||||
|
||||
expect(fakeWindow.__captured).toBe(true);
|
||||
});
|
||||
|
||||
it("gives a mounted module script its composition's own __hyperframes", () => {
|
||||
const { document } = parseHTML(`<div></div>`);
|
||||
Object.defineProperty(document, "baseURI", { value: "https://p.test/preview/" });
|
||||
@@ -1302,8 +1315,9 @@ describe("composition scoping – renamed-id selector runtime", () => {
|
||||
const window = dom.window as unknown as Window & typeof globalThis & Record<string, unknown>;
|
||||
window.__captured = {};
|
||||
if (gsap) window.gsap = gsap;
|
||||
const nativeQuery = window.Element.prototype.querySelector;
|
||||
window.eval(wrapScopedCompositionScript(script, compId));
|
||||
return { window, captured: window.__captured as Record<string, unknown> };
|
||||
return { window, nativeQuery, captured: window.__captured as Record<string, unknown> };
|
||||
}
|
||||
|
||||
const TWO_INSTANCES = `
|
||||
@@ -1313,17 +1327,17 @@ describe("composition scoping – renamed-id selector runtime", () => {
|
||||
</div>`;
|
||||
|
||||
it("leaves Element.prototype untouched when no id was renamed", () => {
|
||||
const { window, captured } = bootWindow(
|
||||
const { window, captured, nativeQuery } = bootWindow(
|
||||
`<div data-composition-id="scene"><svg><path id="shape"/></svg></div>`,
|
||||
`window.__captured.hit = document.querySelector("svg").querySelector("#shape");`,
|
||||
"scene",
|
||||
);
|
||||
expect(window.__hfRenamedIdSelectorShim).toBeUndefined();
|
||||
expect(window.Element.prototype.querySelector).toBe(nativeQuery);
|
||||
expect(captured.hit).toBe(window.document.querySelector("path"));
|
||||
});
|
||||
|
||||
it("rewrites #authoredId to match a renamed element from element and document lookups", () => {
|
||||
const { window, captured } = bootWindow(
|
||||
const { window, captured, nativeQuery } = bootWindow(
|
||||
TWO_INSTANCES,
|
||||
`var svg = document.querySelector("svg.art");
|
||||
window.__captured.viaElement = svg.querySelector("#shape");
|
||||
@@ -1336,7 +1350,7 @@ describe("composition scoping – renamed-id selector runtime", () => {
|
||||
);
|
||||
const renamed = window.document.querySelector('[data-hf-authored-id="shape"]');
|
||||
const other = window.document.querySelector('[data-composition-id="other"] path');
|
||||
expect(window.__hfRenamedIdSelectorShim).toBe(true);
|
||||
expect(window.Element.prototype.querySelector).not.toBe(nativeQuery);
|
||||
expect(captured.viaElement).toBe(renamed);
|
||||
expect(captured.viaElementAll).toBe(1);
|
||||
expect(captured.viaDocument).toBe(renamed);
|
||||
@@ -1399,10 +1413,11 @@ describe("composition scoping – renamed-id selector runtime", () => {
|
||||
expect(captured.suffix).toBeNull();
|
||||
});
|
||||
|
||||
it("refreshes renamed ids after a scene is replaced and its script runs again", () => {
|
||||
it("uses the lifecycle alias refresh before a replaced scene script runs", () => {
|
||||
const { window } = bootWindow(TWO_INSTANCES, "", "scene");
|
||||
window.document.querySelector('[data-composition-id="scene"]')!.innerHTML =
|
||||
'<svg><path id="scene--new" data-hf-authored-id="new"/></svg>';
|
||||
window.__hfSvgSelectorAliases!.refresh();
|
||||
window.eval(
|
||||
wrapScopedCompositionScript(
|
||||
'window.__captured.hit = document.querySelector("svg").querySelector("#new").id;',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getSvgSelectorAliasesScript } from "../generated/svg-selector-aliases-inline";
|
||||
import postcss, { type AtRule, type Node, type Rule } from "postcss";
|
||||
import { escapeCssIdentifier, replaceSelectorIdTokens } from "./selectorIdTokens";
|
||||
import { SCENE_PARTS_META } from "../sceneParts";
|
||||
@@ -312,7 +313,9 @@ export function wrapScopedCompositionScript(
|
||||
const timingSelectorPatternLiteral = jsonScriptLiteral(
|
||||
String.raw`\s*\[\s*data-(?:start|duration)\s*=\s*(?:"[^"]*"|'[^']*')\s*\]`,
|
||||
);
|
||||
const aliasInstallerOutsideAuthoredStrictness = `(function(){ ${getSvgSelectorAliasesScript()} })();`;
|
||||
return `(function(){
|
||||
${aliasInstallerOutsideAuthoredStrictness}
|
||||
var __hfCompId = ${compositionIdLiteral};
|
||||
var __hfTimelineCompId = ${timelineCompositionIdLiteral};
|
||||
var __hfErrorLabel = ${errorLabelLiteral};
|
||||
@@ -331,131 +334,15 @@ export function wrapScopedCompositionScript(
|
||||
var __hfAuthoredRootSelector = __hfAuthoredRootId
|
||||
? "[" + __hfAuthoredRootAttr + '="' + __hfEscapeAttr(__hfAuthoredRootId) + '"]'
|
||||
: "";
|
||||
var __hfCssEscape = function(value) {
|
||||
var text = value + "";
|
||||
if (typeof CSS !== "undefined" && CSS && typeof CSS.escape === "function") {
|
||||
try { return CSS.escape(text); } catch {}
|
||||
}
|
||||
return text.replace(/[^a-zA-Z0-9_-]/g, function(char) { return "\\\\" + char; });
|
||||
};
|
||||
// Decode complete CSS identifiers before comparing authored ids.
|
||||
var __hfRewriteIdSelectors = function(selector, entries) {
|
||||
if (!entries || !entries.length || typeof selector !== "string" || selector.indexOf("#") === -1) {
|
||||
return selector;
|
||||
}
|
||||
var result = "";
|
||||
var bracketDepth = 0;
|
||||
var quote = null;
|
||||
for (var index = 0; index < selector.length; index += 1) {
|
||||
var char = selector[index];
|
||||
if (char === "\\\\") {
|
||||
var escape = selector.slice(index).match(/^\\\\(?:[0-9a-fA-F]{1,6}(?:\\r\\n|[ \\t\\r\\n\\f])?|[\\s\\S])/);
|
||||
if (escape) {
|
||||
result += escape[0];
|
||||
index += escape[0].length - 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (quote) {
|
||||
result += char;
|
||||
if (char === quote) {
|
||||
quote = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (char === '"' || char === "'") {
|
||||
quote = char;
|
||||
result += char;
|
||||
continue;
|
||||
}
|
||||
if (char === "[") {
|
||||
bracketDepth += 1;
|
||||
result += char;
|
||||
continue;
|
||||
}
|
||||
if (char === "]") {
|
||||
bracketDepth = Math.max(0, bracketDepth - 1);
|
||||
result += char;
|
||||
continue;
|
||||
}
|
||||
if (char === "#" && bracketDepth === 0) {
|
||||
var token = selector.slice(index + 1).match(/^(?:[\\w\\u0080-\\uffff-]|\\\\(?:[0-9a-fA-F]{1,6}(?:\\r\\n|[ \\t\\r\\n\\f])?|[^\\r\\n\\f]))+/);
|
||||
if (token) {
|
||||
var decoded = token[0].replace(/\\\\([0-9a-fA-F]{1,6})(?:\\r\\n|[ \\t\\r\\n\\f])?|\\\\([^\\r\\n\\f])/g, function(_, hex, escaped) {
|
||||
if (!hex) return escaped;
|
||||
var code = parseInt(hex, 16);
|
||||
return String.fromCodePoint(!code || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff) ? 0xfffd : code);
|
||||
});
|
||||
var matched = entries.find(function(entry) { return entry.id === decoded; });
|
||||
result += matched ? matched.replacement : "#" + token[0];
|
||||
index += token[0].length;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result += char;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
var __hfAuthoredRootEntries = __hfAuthoredRootSelector
|
||||
? [{ id: __hfAuthoredRootId, replacement: __hfAuthoredRootSelector }]
|
||||
: [];
|
||||
// Include both renamed and untouched instances. Each wrapper refreshes this
|
||||
// document-wide cache because preview scene swaps can introduce new ids.
|
||||
var __hfRenamedIdEntries = function() {
|
||||
if (window.__hfRenamedIdSelectorEntries) return window.__hfRenamedIdSelectorEntries;
|
||||
var entries = [];
|
||||
var seen = Object.create(null);
|
||||
var nodes;
|
||||
try {
|
||||
nodes = window.document.querySelectorAll("[" + __hfAuthoredRootAttr + "][id]");
|
||||
} catch {
|
||||
nodes = [];
|
||||
}
|
||||
for (var i = 0; i < nodes.length; i += 1) {
|
||||
var authored = nodes[i].getAttribute(__hfAuthoredRootAttr);
|
||||
if (!authored || seen[authored] || authored === nodes[i].id) continue;
|
||||
seen[authored] = true;
|
||||
var escaped = __hfCssEscape(authored);
|
||||
entries.push({
|
||||
id: authored,
|
||||
replacement: ":is(#" + escaped + ", [" + __hfAuthoredRootAttr + '="' + __hfEscapeAttr(authored) + '"])',
|
||||
});
|
||||
}
|
||||
window.__hfRenamedIdSelectorEntries = entries;
|
||||
return window.__hfRenamedIdSelectorEntries;
|
||||
};
|
||||
// element.querySelector("#authored") on an arbitrary Element never passes
|
||||
// through the scoped document proxy, so the same rewrite is installed once
|
||||
// per document on Element.prototype — only when at least one id was
|
||||
// actually renamed, so a document without collisions runs untouched
|
||||
// natives. Document.prototype is deliberately NOT patched: an unscoped
|
||||
// document-wide lookup cannot know which instance it means, and rewriting
|
||||
// it would only widen the ambiguity.
|
||||
var __hfInstallRenamedIdSelectorShim = function() {
|
||||
if (window.__hfRenamedIdSelectorShim) return;
|
||||
var entries = __hfRenamedIdEntries();
|
||||
if (!entries.length) return;
|
||||
var proto = window.Element && window.Element.prototype;
|
||||
if (!proto || typeof proto.querySelector !== "function" || typeof proto.querySelectorAll !== "function") {
|
||||
return;
|
||||
}
|
||||
var nativeQuerySelector = proto.querySelector;
|
||||
var nativeQuerySelectorAll = proto.querySelectorAll;
|
||||
proto.querySelector = function(selector) {
|
||||
return nativeQuerySelector.call(this, __hfRewriteIdSelectors(selector, __hfRenamedIdEntries()));
|
||||
};
|
||||
proto.querySelectorAll = function(selector) {
|
||||
return nativeQuerySelectorAll.call(this, __hfRewriteIdSelectors(selector, __hfRenamedIdEntries()));
|
||||
};
|
||||
window.__hfRenamedIdSelectorShim = true;
|
||||
};
|
||||
var __hfNormalizeSelector = function(selector) {
|
||||
if (!__hfCompId || typeof selector !== "string") return selector;
|
||||
var normalized = selector
|
||||
.replace(new RegExp(__hfRootSelectorPattern + '(?:' + __hfTimingSelectorPattern + ')+', 'g'), __hfRootSelector)
|
||||
.replace(new RegExp('(?:' + __hfTimingSelectorPattern + ')+' + __hfRootSelectorPattern, 'g'), __hfRootSelector);
|
||||
normalized = __hfRewriteIdSelectors(normalized, __hfAuthoredRootEntries);
|
||||
return __hfRewriteIdSelectors(normalized, __hfRenamedIdEntries());
|
||||
return window.__hfSvgSelectorAliases.rewrite(normalized, __hfAuthoredRootEntries);
|
||||
};
|
||||
var __hfFindRoot = function() {
|
||||
if (!__hfRoot && __hfRootSelector) {
|
||||
@@ -718,8 +605,6 @@ ${source.replace(/<\/(script)/gi, "<\\/$1")}
|
||||
});
|
||||
};
|
||||
__hfFindRoot();
|
||||
window.__hfRenamedIdSelectorEntries = null;
|
||||
__hfInstallRenamedIdSelectorShim();
|
||||
__hfRecordAnimations(__hfRun);
|
||||
})();`;
|
||||
}
|
||||
|
||||
@@ -906,12 +906,13 @@ describe("inlineSubCompositions – renamed SVG ids stay reachable from author s
|
||||
const window = dom.window as unknown as Window & typeof globalThis & Record<string, unknown>;
|
||||
window.__captured = {};
|
||||
if (fakeGsap) (window as Record<string, unknown>).gsap = fakeGsap(window);
|
||||
const nativeQuery = window.Element.prototype.querySelector;
|
||||
for (const script of result.scripts) window.eval(script);
|
||||
return { window, result, captured: window.__captured as Record<string, unknown> };
|
||||
return { window, result, nativeQuery, captured: window.__captured as Record<string, unknown> };
|
||||
}
|
||||
|
||||
it("single composition: native and script references to the same element both keep resolving", () => {
|
||||
const { window, captured } = inlineAndBoot(
|
||||
const { window, captured, nativeQuery } = inlineAndBoot(
|
||||
{
|
||||
"scene.html": scriptedScene(
|
||||
"scene",
|
||||
@@ -929,7 +930,7 @@ describe("inlineSubCompositions – renamed SVG ids stay reachable from author s
|
||||
expect(path.getAttribute("id")).toBe("shape");
|
||||
expect(path.hasAttribute("data-hf-authored-id")).toBe(false);
|
||||
expect(window.document.querySelector("use")!.getAttribute("href")).toBe("#shape");
|
||||
expect((window as Record<string, unknown>).__hfRenamedIdSelectorShim).toBeUndefined();
|
||||
expect(window.Element.prototype.querySelector).toBe(nativeQuery);
|
||||
expect(captured.viaDocument).toBe(path);
|
||||
expect(captured.viaElement).toBe(path);
|
||||
expect(captured.byId).toBe(path);
|
||||
@@ -946,7 +947,7 @@ describe("inlineSubCompositions – renamed SVG ids stay reachable from author s
|
||||
gsapTargets: gsap.to(["#shape", ".ref"], {}),
|
||||
toArray: gsap.utils.toArray("#shape"),
|
||||
};`;
|
||||
const { window, captured } = inlineAndBoot(
|
||||
const { window, captured, nativeQuery } = inlineAndBoot(
|
||||
{
|
||||
"scene-a.html": scriptedScene("scene-a", `var __hfProbeName = "a"; ${probe}`),
|
||||
"scene-b.html": scriptedScene("scene-b", `var __hfProbeName = "b"; ${probe}`),
|
||||
@@ -990,7 +991,7 @@ describe("inlineSubCompositions – renamed SVG ids stay reachable from author s
|
||||
expect(b.gsapTargets).toEqual([pathB, rootB!.querySelector("use.ref")]);
|
||||
expect(a.toArray).toEqual([pathA]);
|
||||
expect(b.toArray).toEqual([pathB]);
|
||||
expect((window as Record<string, unknown>).__hfRenamedIdSelectorShim).toBe(true);
|
||||
expect(window.Element.prototype.querySelector).not.toBe(nativeQuery);
|
||||
});
|
||||
|
||||
it("escaped CSS id: a styled element that is also natively referenced keeps its rule after rename", () => {
|
||||
@@ -1027,13 +1028,11 @@ describe("inlineSubCompositions – renamed SVG ids stay reachable from author s
|
||||
expect(cssA).toContain("url(#fx.1)");
|
||||
expect(cssB).toContain(String.raw`#scene-b--fx\.1`);
|
||||
expect(cssB).toContain("url(#scene-b--fx.1)");
|
||||
expect(cssB).not.toContain(String.raw`#fx\.1`);
|
||||
expect(cssB).toContain(String.raw`:is(#fx\.1, #scene-b--fx\.1)`);
|
||||
|
||||
// The rewritten selector actually matches the renamed element in a real
|
||||
// selector engine.
|
||||
const ruleSelector = /(\[data-composition-id="scene-b"\][^{]*#scene-b--fx\\\.1)\s*\{/.exec(
|
||||
cssB!,
|
||||
)?.[1];
|
||||
const ruleSelector = cssB!.slice(0, cssB!.indexOf("{")).trim();
|
||||
expect(ruleSelector).toBeTruthy();
|
||||
expect(window.document.querySelector(ruleSelector!)).toBe(filterB);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SVG_REFERENCE_ALIASES_ATTR, readSvgReferenceAliases } from "./svgSelectorAliases";
|
||||
import { readExternalScriptAttributes, type ExternalScriptAttributes } from "./externalScripts";
|
||||
import { parseImportMap, type ImportMap } from "./importMaps";
|
||||
import {
|
||||
@@ -332,6 +333,7 @@ export function inlineSubCompositions(
|
||||
tagScenes = false,
|
||||
} = options;
|
||||
|
||||
const rootStyles = [...document.querySelectorAll("style")];
|
||||
const styles: CompositionStyle[] = [];
|
||||
const styleScenes: string[] = [];
|
||||
const scripts: string[] = [];
|
||||
@@ -676,23 +678,44 @@ export function inlineSubCompositions(
|
||||
// `<style>` text gets the same substitution its DOM just received. See
|
||||
// svgIdNamespacing.ts for why this is a rename, unlike the sibling
|
||||
// getElementById/media-id fixes.
|
||||
const svgIdMaps = namespaceCollidingSvgIds(
|
||||
document,
|
||||
svgIdScopes.map(({ root, namespace, exclude, styleStart, styleEnd }) => ({
|
||||
const svgIdMaps = namespaceCollidingSvgIds(document, [
|
||||
...svgIdScopes.map(({ root, namespace, exclude, styleStart, styleEnd }) => ({
|
||||
root,
|
||||
namespace,
|
||||
exclude,
|
||||
cssTexts: styles.slice(styleStart, styleEnd).map((style) => style.css),
|
||||
})),
|
||||
);
|
||||
svgIdMaps.forEach((idMap, index) => {
|
||||
if (idMap.size === 0) return;
|
||||
{
|
||||
root: document.documentElement,
|
||||
namespace: "",
|
||||
referenceTarget: "document",
|
||||
exclude: svgIdScopes.map(({ root }) => root),
|
||||
cssTexts: rootStyles.map((style) => style.textContent ?? ""),
|
||||
},
|
||||
]);
|
||||
svgIdMaps.slice(0, svgIdScopes.length).forEach((idMap, index) => {
|
||||
const { styleStart, styleEnd } = svgIdScopes[index]!;
|
||||
const references = readSvgReferenceAliases(
|
||||
svgIdScopes[index]!.root,
|
||||
SVG_REFERENCE_ALIASES_ATTR,
|
||||
);
|
||||
for (let i = styleStart; i < styleEnd; i += 1) {
|
||||
styles[i]!.css = rewriteSvgIdReferencesInCss(styles[i]!.css, idMap);
|
||||
styles[i]!.css = rewriteSvgIdReferencesInCss(styles[i]!.css, idMap, references);
|
||||
}
|
||||
});
|
||||
|
||||
const rootReferences = readSvgReferenceAliases(
|
||||
document.documentElement,
|
||||
SVG_REFERENCE_ALIASES_ATTR,
|
||||
);
|
||||
for (const style of rootStyles) {
|
||||
style.textContent = rewriteSvgIdReferencesInCss(
|
||||
style.textContent ?? "",
|
||||
svgIdMaps[svgIdScopes.length]!,
|
||||
rootReferences,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
styles,
|
||||
styleScenes,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
decodeCssIdentifierAt,
|
||||
escapeCssAttributeValue,
|
||||
escapeCssIdentifier,
|
||||
replaceSelectorAttributeTokens,
|
||||
replaceSelectorIdTokens,
|
||||
} from "./selectorIdTokens";
|
||||
|
||||
@@ -20,6 +22,12 @@ describe("decodeCssIdentifierAt", () => {
|
||||
expect(decodeCssIdentifierAt(String.raw`\31 st`, 0)).toEqual({ value: "1st", end: 6 });
|
||||
});
|
||||
|
||||
it("rejects CSS newline escapes in identifiers", () => {
|
||||
for (const newline of ["\n", "\r", "\f", "\r\n"]) {
|
||||
expect(decodeCssIdentifierAt("\\" + newline, 0)).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("maps out-of-range and surrogate code points to U+FFFD", () => {
|
||||
expect(decodeCssIdentifierAt(String.raw`a\0 b`, 0)?.value).toBe("a�b");
|
||||
expect(decodeCssIdentifierAt(String.raw`a\d800 b`, 0)?.value).toBe("a�b");
|
||||
@@ -87,3 +95,18 @@ it("does not rewrite escaped hashes inside class names", () => {
|
||||
String.raw`.foo\[bar #new`,
|
||||
);
|
||||
});
|
||||
|
||||
it("serializes control characters inside CSS attribute string values", () => {
|
||||
expect(escapeCssAttributeValue('a\nb\rc\fd"e\\f')).toBe('a\\a b\\d c\\c d\\"e\\\\f');
|
||||
});
|
||||
|
||||
it("keeps escaped closing brackets inside unquoted attribute tokens", () => {
|
||||
expect(
|
||||
replaceSelectorAttributeTokens(String.raw`use[href=\#sym\]bol]`, () => '[href="#renamed"]'),
|
||||
).toBe('use[href="#renamed"]');
|
||||
});
|
||||
|
||||
it("keeps commented brackets bounded when the closing bracket is missing", () => {
|
||||
const selector = "[" + "/*x*/".repeat(2000);
|
||||
expect(replaceSelectorAttributeTokens(selector, () => "changed")).toBe(selector);
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
|
||||
/** Unescaped name code points: `[A-Za-z0-9_-]` plus any non-ASCII. */
|
||||
function isNameChar(char: string | undefined): boolean {
|
||||
function isNameChar(char: string | undefined): char is string {
|
||||
if (!char) return false;
|
||||
return /[\w-]/.test(char) || char.charCodeAt(0) >= 0x80;
|
||||
}
|
||||
@@ -56,7 +56,7 @@ function skipEscapeWhitespace(text: string, index: number): number {
|
||||
*/
|
||||
function consumeCssEscape(text: string, index: number): { value: string; end: number } | null {
|
||||
const next = text[index + 1];
|
||||
if (next === undefined || next === "\n") return null;
|
||||
if (next === undefined || /[\n\r\f]/.test(next)) return null;
|
||||
const hex = HEX_ESCAPE_RE.exec(text.slice(index + 1, index + 7));
|
||||
if (!hex) return { value: next, end: index + 2 };
|
||||
const codePoint = sanitizeCodePoint(Number.parseInt(hex[0], 16));
|
||||
@@ -66,6 +66,29 @@ function consumeCssEscape(text: string, index: number): { value: string; end: nu
|
||||
};
|
||||
}
|
||||
|
||||
function startsCssIdentifier(text: string, start: number): boolean {
|
||||
const isStart = (char: string | undefined) => isNameChar(char) && !/[0-9-]/.test(char);
|
||||
const escape = (index: number) => text[index] === "\\" && consumeCssEscape(text, index) !== null;
|
||||
if (text[start] === "-")
|
||||
return text[start + 1] === "-" || isStart(text[start + 1]) || escape(start + 1);
|
||||
return isStart(text[start]) || escape(start);
|
||||
}
|
||||
|
||||
function skipCssComments(text: string, start: number): number {
|
||||
let index = start;
|
||||
while (text.startsWith("/*", index)) {
|
||||
const end = text.indexOf("*/", index + 2);
|
||||
index = end < 0 ? text.length : end + 2;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
function skipCssTrivia(text: string, start: number): number {
|
||||
let index = skipCssComments(text, start);
|
||||
while (isCssWhitespace(text[index])) index = skipCssComments(text, index + 1);
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read one CSS identifier starting at `start` and return its decoded value
|
||||
* plus the offset just past it, or `null` when no identifier starts there.
|
||||
@@ -75,6 +98,7 @@ export function decodeCssIdentifierAt(
|
||||
text: string,
|
||||
start: number,
|
||||
): { value: string; end: number } | null {
|
||||
if (!startsCssIdentifier(text, start)) return null;
|
||||
let index = start;
|
||||
let value = "";
|
||||
while (index < text.length) {
|
||||
@@ -152,7 +176,7 @@ export function escapeCssIdentifier(value: string): string {
|
||||
* branch (no ambiguous backtracking).
|
||||
*/
|
||||
const GUARDED_SELECTOR_SEGMENT_RE =
|
||||
/\\(?:[0-9a-fA-F]{1,6}(?:\r\n|[ \t\r\n\f])?|[\s\S])|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\[(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\]"'])*\]/g;
|
||||
/\/\*(?:[^*]|\*(?!\/))*\*\/|\\(?:[0-9a-fA-F]{1,6}(?:\r\n|[ \t\r\n\f])?|[\s\S])|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'|\[(?:\/\*(?:[^*]|\*(?!\/))*\*\/|\\[\s\S]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'|[^\]"'\\/]|\/(?!\*))*\]/g;
|
||||
|
||||
/**
|
||||
* `mask[i]` is `true` when `selector[i]` sits outside both a quoted string
|
||||
@@ -206,3 +230,121 @@ export function replaceSelectorIdTokens(
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function decodeCssString(value: string): string {
|
||||
let result = "";
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
if (value[index] === "\\") {
|
||||
const next = value[index + 1];
|
||||
if (next !== undefined && /[\n\r\f]/.test(next)) {
|
||||
index = skipEscapeWhitespace(value, index + 1) - 1;
|
||||
continue;
|
||||
}
|
||||
const escaped = consumeCssEscape(value, index);
|
||||
if (escaped) {
|
||||
result += escaped.value;
|
||||
index = escaped.end - 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result += value[index];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function escapeCssAttributeValue(value: string): string {
|
||||
return Array.from(value, (char) => {
|
||||
const code = char.charCodeAt(0);
|
||||
if (code === 0) return "\uFFFD";
|
||||
if (code < 0x20 || code === 0x7f) return `\\${code.toString(16)} `;
|
||||
return char === '"' || char === "\\" ? `\\${char}` : char;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
type AttributeNamespace = { kind: "any" } | { kind: "none" } | { kind: "named"; prefix: string };
|
||||
|
||||
function attributeNamespace(wildcard: boolean, prefix: string | undefined): AttributeNamespace {
|
||||
if (wildcard) return { kind: "any" };
|
||||
return prefix === undefined ? { kind: "none" } : { kind: "named", prefix };
|
||||
}
|
||||
|
||||
function isNamespaceSeparator(text: string, offset: number): boolean {
|
||||
return text[offset] === "|" && text[offset + 1] !== "=";
|
||||
}
|
||||
|
||||
function readQualifiedAttributeName(text: string, start: number) {
|
||||
const wildcard = text[start] === "*";
|
||||
const first = wildcard ? { value: "*", end: start + 1 } : decodeCssIdentifierAt(text, start);
|
||||
const separator = skipCssComments(text, first?.end ?? start);
|
||||
if (!isNamespaceSeparator(text, separator)) {
|
||||
if (wildcard) return null;
|
||||
return first ? { token: first, namespace: { kind: "none" } as AttributeNamespace } : null;
|
||||
}
|
||||
const token = decodeCssIdentifierAt(text, skipCssComments(text, separator + 1));
|
||||
if (!token) return null;
|
||||
return { token, namespace: attributeNamespace(wildcard, first?.value) };
|
||||
}
|
||||
|
||||
function readAttributeName(predicate: string) {
|
||||
if (predicate[0] !== "[") return null;
|
||||
const start = skipCssTrivia(predicate, 1);
|
||||
const qualified = readQualifiedAttributeName(predicate, start);
|
||||
if (!qualified) return null;
|
||||
const { token, namespace } = qualified;
|
||||
return {
|
||||
name: token.value,
|
||||
rawName: predicate.slice(start, token.end),
|
||||
namespace,
|
||||
end: token.end,
|
||||
};
|
||||
}
|
||||
|
||||
function readAttributeValue(predicate: string, start: number) {
|
||||
const quoted = /^(?:"((?:\\[\s\S]|[^"\\])*)"|'((?:\\[\s\S]|[^'\\])*)')/.exec(
|
||||
predicate.slice(start),
|
||||
);
|
||||
if (quoted)
|
||||
return { value: decodeCssString(quoted[1] ?? quoted[2]!), end: start + quoted[0].length };
|
||||
return decodeCssIdentifierAt(predicate, start);
|
||||
}
|
||||
|
||||
function readAttributeEnding(predicate: string, start: number) {
|
||||
let offset = skipCssTrivia(predicate, start);
|
||||
const token = decodeCssIdentifierAt(predicate, offset);
|
||||
if (token) {
|
||||
if (!/^[is]$/i.test(token.value)) return null;
|
||||
offset = skipCssTrivia(predicate, token.end);
|
||||
}
|
||||
if (predicate[offset] !== "]" || offset !== predicate.length - 1) return null;
|
||||
return { flag: token?.value };
|
||||
}
|
||||
|
||||
export function parseAttributeSelector(predicate: string) {
|
||||
const name = readAttributeName(predicate);
|
||||
if (!name) return null;
|
||||
const offset = skipCssTrivia(predicate, name.end);
|
||||
const operators = ["=", "~=", "|=", "^=", "$=", "*="] as const;
|
||||
const operator = operators.find((candidate) => predicate.startsWith(candidate, offset));
|
||||
if (!operator) return null;
|
||||
const value = readAttributeValue(predicate, skipCssTrivia(predicate, offset + operator.length));
|
||||
if (!value) return null;
|
||||
const ending = readAttributeEnding(predicate, value.end);
|
||||
if (!ending) return null;
|
||||
return {
|
||||
name: name.name,
|
||||
rawName: name.rawName,
|
||||
namespace: name.namespace,
|
||||
operator,
|
||||
value: value.value,
|
||||
flag: ending.flag,
|
||||
};
|
||||
}
|
||||
|
||||
export function replaceSelectorAttributeTokens(
|
||||
selector: string,
|
||||
rewrite: (predicate: string) => string,
|
||||
): string {
|
||||
return selector.replace(GUARDED_SELECTOR_SEGMENT_RE, (segment) =>
|
||||
segment.startsWith("[") ? rewrite(segment) : segment,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -317,10 +317,10 @@ describe("rewriteSvgIdReferencesInCss", () => {
|
||||
expect(rewriteSvgIdReferencesInCss(css, new Map())).toBe(css);
|
||||
});
|
||||
|
||||
it("rewrites a bare id selector to the namespaced id", () => {
|
||||
it("keeps authored and namespaced targets in a bare id selector", () => {
|
||||
const idMap = new Map([["clip", "scene-a--clip"]]);
|
||||
const css = "#clip rect { fill: red; }";
|
||||
expect(rewriteSvgIdReferencesInCss(css, idMap)).toContain("#scene-a--clip rect");
|
||||
expect(rewriteSvgIdReferencesInCss(css, idMap)).toContain(":is(#clip, #scene-a--clip) rect");
|
||||
});
|
||||
|
||||
it("rewrites a url(#id) declaration value", () => {
|
||||
@@ -353,7 +353,7 @@ describe("rewriteSvgIdReferencesInCss", () => {
|
||||
const idMap = new Map([["fx.1", "scene-b--fx.1"]]);
|
||||
const css = String.raw`#fx\.1 { opacity: .5; } .x { filter: url(#fx.1); }`;
|
||||
const result = rewriteSvgIdReferencesInCss(css, idMap);
|
||||
expect(result).toContain(String.raw`#scene-b--fx\.1 { opacity: .5; }`);
|
||||
expect(result).toContain(String.raw`:is(#fx\.1, #scene-b--fx\.1) { opacity: .5; }`);
|
||||
expect(result).toContain("filter: url(#scene-b--fx.1)");
|
||||
});
|
||||
|
||||
@@ -363,14 +363,16 @@ describe("rewriteSvgIdReferencesInCss", () => {
|
||||
const css = String.raw`#fx\2e\31 { opacity: .5; } #fx\2e 1 { color: red }`;
|
||||
const result = rewriteSvgIdReferencesInCss(css, idMap);
|
||||
// The whitespace after `\31 ` belongs to the escape, so it is consumed.
|
||||
expect(result).toMatch(/#scene-b--fx\\\.1\s*\{ opacity: \.5; \}/);
|
||||
expect(result).toMatch(/#scene-b--fx\\\.1\s*\{ color: red \}/);
|
||||
expect(result).toMatch(/#scene-b--fx\\\.1\)\s*\{ opacity: \.5; \}/);
|
||||
expect(result).toMatch(/#scene-b--fx\\\.1\)\s*\{ color: red \}/);
|
||||
});
|
||||
|
||||
it("re-escapes characters the renamed id still carries", () => {
|
||||
const idMap = new Map([["a b", "scene-b--a b"]]);
|
||||
const css = String.raw`#a\ b { opacity: .5; }`;
|
||||
expect(rewriteSvgIdReferencesInCss(css, idMap)).toContain(String.raw`#scene-b--a\ b {`);
|
||||
expect(rewriteSvgIdReferencesInCss(css, idMap)).toContain(
|
||||
String.raw`:is(#a\ b, #scene-b--a\ b) {`,
|
||||
);
|
||||
});
|
||||
|
||||
it("leaves a #id inside an attribute selector or string untouched", () => {
|
||||
@@ -394,3 +396,36 @@ it("rewrites case-insensitive CSS URL functions in attributes and styles", () =>
|
||||
"Url(#b--clip)",
|
||||
);
|
||||
});
|
||||
|
||||
it("finalizes initially referenced keepers while allowing first-use references after scripts", () => {
|
||||
const { document } = parseHTML(`<!doctype html><html><body>
|
||||
<div class="a"><svg><clipPath id="clip"/><g clip-path="url(#clip)"/><path id="late"/><path id="script-only"/></svg></div>
|
||||
<div class="b"><svg><clipPath id="clip"/><g clip-path="url(#clip)"/><path id="late"/><path id="script-only"/></svg></div>
|
||||
</body></html>`);
|
||||
const roots = [document.querySelector(".a")!, document.querySelector(".b")!];
|
||||
const scopes = roots.map((root, index) => ({ root, namespace: index === 0 ? "a" : "b" }));
|
||||
const finalized = new Set<Element>();
|
||||
namespaceCollidingSvgIds(document, scopes, finalized);
|
||||
const definitions = roots.map((root) => root.querySelector("clipPath")!);
|
||||
const captured = definitions.map((target) => target.id);
|
||||
const html = document.createElement("div");
|
||||
html.id = "clip";
|
||||
document.body.appendChild(html);
|
||||
for (const root of roots) {
|
||||
const use = document.createElementNS("http://www.w3.org/2000/svg", "use");
|
||||
use.setAttribute("href", "#late");
|
||||
root.querySelector("svg")!.appendChild(use);
|
||||
}
|
||||
namespaceCollidingSvgIds(document, scopes, finalized);
|
||||
expect(definitions.map((target) => target.id)).toEqual(captured);
|
||||
expect(captured).toEqual(["clip", "b--clip"]);
|
||||
expect(roots.map((root) => root.querySelector("use")!.getAttribute("href"))).toEqual([
|
||||
"#late",
|
||||
"#b--late",
|
||||
]);
|
||||
expect(roots.map((root) => root.querySelector('path[id="script-only"]')?.id)).toEqual([
|
||||
"script-only",
|
||||
"script-only",
|
||||
]);
|
||||
expect(roots[1]!.querySelector("g")!.getAttribute("clip-path")).toBe("url(#b--clip)");
|
||||
});
|
||||
|
||||
@@ -7,7 +7,13 @@
|
||||
*/
|
||||
|
||||
import postcss from "postcss";
|
||||
import { escapeCssIdentifier, replaceSelectorIdTokens } from "./selectorIdTokens";
|
||||
import { escapeCssIdentifier } from "./selectorIdTokens";
|
||||
import {
|
||||
SVG_REFERENCE_ALIASES_ATTR,
|
||||
readSvgReferenceAliases,
|
||||
rewriteSvgSelectors,
|
||||
type SvgReferenceAlias,
|
||||
} from "./svgSelectorAliases";
|
||||
|
||||
const ID_ATTR = "id";
|
||||
|
||||
@@ -53,33 +59,79 @@ function rewriteUrlHashRefs(value: string, idMap: ReadonlyMap<string, string>):
|
||||
});
|
||||
}
|
||||
|
||||
function rewriteHrefValue(value: string, idMap: ReadonlyMap<string, string>): string | null {
|
||||
if (!value.startsWith("#")) return null;
|
||||
const mapped = idMap.get(value.slice(1));
|
||||
return mapped ? `#${mapped}` : null;
|
||||
type ReferenceDecision =
|
||||
| { kind: "skip" }
|
||||
| { kind: "retain"; alias: SvgReferenceAlias }
|
||||
| { kind: "managed"; before: string; after: string };
|
||||
|
||||
function nativeReferenceValue(
|
||||
name: string,
|
||||
value: string,
|
||||
idMap: ReadonlyMap<string, string>,
|
||||
): string | null {
|
||||
if (isHrefAttrName(name) && value.startsWith("#")) {
|
||||
const mapped = idMap.get(value.slice(1));
|
||||
return mapped ? `#${mapped}` : value;
|
||||
}
|
||||
URL_HASH_REF_RE.lastIndex = 0;
|
||||
const hasUrl = URL_HASH_REF_RE.test(value);
|
||||
URL_HASH_REF_RE.lastIndex = 0;
|
||||
return hasUrl ? rewriteUrlHashRefs(value, idMap) : null;
|
||||
}
|
||||
|
||||
/** Rewrite every id reference carried on one element's attributes. CSS text
|
||||
* inside a `<style>` element is handled separately by
|
||||
* `rewriteSvgIdReferencesInCss` — it is extracted and scoped as a raw
|
||||
* string elsewhere in the inline pipeline, never visited by this walk. */
|
||||
function decideReferenceWrite(
|
||||
attr: Attr,
|
||||
previous: SvgReferenceAlias | undefined,
|
||||
idMap: ReadonlyMap<string, string>,
|
||||
): ReferenceDecision {
|
||||
if (attr.name === SVG_REFERENCE_ALIASES_ATTR || attr.name === ID_ATTR) return { kind: "skip" };
|
||||
const before = previous && attr.value === previous.after ? previous.before : attr.value;
|
||||
const after = nativeReferenceValue(attr.name, before, idMap);
|
||||
if (after !== null) return { kind: "managed", before, after };
|
||||
return previous ? { kind: "retain", alias: previous } : { kind: "skip" };
|
||||
}
|
||||
|
||||
function referenceAlias(
|
||||
attr: Attr,
|
||||
before: string,
|
||||
after: string,
|
||||
inSvg: boolean,
|
||||
): SvgReferenceAlias {
|
||||
// The compiler DOM omits Attr namespace APIs; serialized SVG restores XLink.
|
||||
const serializedXlink = attr.namespaceURI === undefined && attr.name === "xlink:href" && inSvg;
|
||||
return {
|
||||
name: attr.name,
|
||||
localName: serializedXlink ? "href" : attr.localName,
|
||||
namespaceURI: serializedXlink ? "http://www.w3.org/1999/xlink" : (attr.namespaceURI ?? null),
|
||||
before,
|
||||
after,
|
||||
};
|
||||
}
|
||||
|
||||
/** Apply managed reference writes while retaining provenance for later assembly phases. */
|
||||
function rewriteElementIdReferences(el: Element, idMap: ReadonlyMap<string, string>): void {
|
||||
const attrs = el.attributes ? Array.from(el.attributes) : [];
|
||||
for (const attr of attrs) {
|
||||
const { name, value } = attr;
|
||||
if (!value) continue;
|
||||
if (isHrefAttrName(name)) {
|
||||
const rewritten = rewriteHrefValue(value, idMap);
|
||||
if (rewritten) {
|
||||
el.setAttribute(name, rewritten);
|
||||
continue;
|
||||
}
|
||||
const prior = new Map(
|
||||
readSvgReferenceAliases(el, SVG_REFERENCE_ALIASES_ATTR, false).map((alias) => [
|
||||
alias.name,
|
||||
alias,
|
||||
]),
|
||||
);
|
||||
const aliases = [...prior.values()].filter((alias) => alias.name === ID_ATTR);
|
||||
const inSvg = !!el.closest("svg");
|
||||
for (const attr of el.attributes ? Array.from(el.attributes) : []) {
|
||||
const decision = decideReferenceWrite(attr, prior.get(attr.name), idMap);
|
||||
if (decision.kind === "skip") continue;
|
||||
if (decision.kind === "retain") {
|
||||
aliases.push(decision.alias);
|
||||
continue;
|
||||
}
|
||||
if (value.toLowerCase().includes("url(")) {
|
||||
const rewritten = rewriteUrlHashRefs(value, idMap);
|
||||
if (rewritten !== value) el.setAttribute(name, rewritten);
|
||||
if (decision.after !== attr.value) {
|
||||
if (attr.namespaceURI) el.setAttributeNS(attr.namespaceURI, attr.name, decision.after);
|
||||
else el.setAttribute(attr.name, decision.after);
|
||||
}
|
||||
aliases.push(referenceAlias(attr, decision.before, decision.after, inSvg));
|
||||
}
|
||||
if (aliases.length) el.setAttribute(SVG_REFERENCE_ALIASES_ATTR, JSON.stringify(aliases));
|
||||
}
|
||||
|
||||
/** Structural shape both a linkedom/live-DOM `Element` and `Document`
|
||||
@@ -96,11 +148,13 @@ export interface SvgIdScope {
|
||||
root: Element;
|
||||
/**
|
||||
* Document-unique prefix for ids renamed in this scope — the instance's
|
||||
* runtime composition id. An empty namespace makes the scope read-only:
|
||||
* runtime composition id. An empty namespace freezes IDs while still repairing references:
|
||||
* an anonymous host has no identity to prefix with, the same guard
|
||||
* `scopeCssToComposition` and `wrapScopedCompositionScript` apply.
|
||||
*/
|
||||
namespace: string;
|
||||
/** Root document references follow document order; instances prefer local definitions. */
|
||||
referenceTarget?: "document";
|
||||
/**
|
||||
* Nested composition hosts inside `root` whose content belongs to their
|
||||
* OWN scope. Their subtrees are skipped both when collecting this scope's
|
||||
@@ -166,7 +220,7 @@ function collectNativelyReferencedIds(
|
||||
const referenced = new Set<string>();
|
||||
for (const el of elements) {
|
||||
for (const attr of el.attributes ? Array.from(el.attributes) : []) {
|
||||
if (!attr.value) continue;
|
||||
if (!attr.value || attr.name === SVG_REFERENCE_ALIASES_ATTR) continue;
|
||||
collectHrefFragmentRef(attr, svgIds, referenced);
|
||||
if (attr.value.toLowerCase().includes("url("))
|
||||
collectUrlHashRefsFromText(attr.value, svgIds, referenced);
|
||||
@@ -234,6 +288,15 @@ interface IdCensus {
|
||||
usedIds: Set<string>;
|
||||
}
|
||||
|
||||
function currentAuthoredSvgId(el: Element): string | null {
|
||||
if (!el.closest("svg")) return null;
|
||||
const idWrite = readSvgReferenceAliases(el, SVG_REFERENCE_ALIASES_ATTR, false).find(
|
||||
(alias) => alias.name === ID_ATTR,
|
||||
);
|
||||
if (idWrite) return el.getAttribute(ID_ATTR) === idWrite.after ? idWrite.before : null;
|
||||
return el.getAttribute(SVG_AUTHORED_ID_ATTR);
|
||||
}
|
||||
|
||||
function buildIdCensus(document: SvgIdQueryable): IdCensus {
|
||||
const elementsById = new Map<string, Element[]>();
|
||||
const usedIds = new Set<string>();
|
||||
@@ -244,17 +307,16 @@ function buildIdCensus(document: SvgIdQueryable): IdCensus {
|
||||
const list = elementsById.get(id);
|
||||
if (list) list.push(el);
|
||||
else elementsById.set(id, [el]);
|
||||
const authored = currentAuthoredSvgId(el);
|
||||
if (authored && authored !== id) {
|
||||
const aliases = elementsById.get(authored);
|
||||
if (aliases) aliases.push(el);
|
||||
else elementsById.set(authored, [el]);
|
||||
}
|
||||
}
|
||||
return { elementsById, usedIds };
|
||||
}
|
||||
|
||||
function hasAnyCollision(census: IdCensus): boolean {
|
||||
for (const elements of census.elementsById.values()) {
|
||||
if (elements.length > 1) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** A namespaced id that is not already taken anywhere in the document. */
|
||||
function mintNamespacedId(namespace: string, originalId: string, usedIds: Set<string>): string {
|
||||
const base = `${sanitizeNamespaceSegment(namespace)}--${originalId}`;
|
||||
@@ -264,6 +326,17 @@ function mintNamespacedId(namespace: string, originalId: string, usedIds: Set<st
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function collisionScopes(
|
||||
id: string,
|
||||
elements: readonly Element[],
|
||||
scopeIndexByRenamable: ReadonlyMap<Element, number>,
|
||||
): Set<number> {
|
||||
const keeper = elements.find((el) => !scopeIndexByRenamable.has(el)) ?? elements[0];
|
||||
const candidates = elements.filter((el) => el !== keeper && el.getAttribute(ID_ATTR) === id);
|
||||
const indices = candidates.map((el) => scopeIndexByRenamable.get(el));
|
||||
return new Set(indices.filter((index): index is number => index !== undefined));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide, per scope, which ids get renamed and to what. For every colliding
|
||||
* id the keeper is the first element in document order that no scope can
|
||||
@@ -278,20 +351,46 @@ function planRenames(
|
||||
const idMaps = scopes.map(() => new Map<string, string>());
|
||||
for (const [id, elements] of census.elementsById) {
|
||||
if (elements.length < 2) continue;
|
||||
const keeper = elements.find((el) => !scopeIndexByRenamable.has(el)) ?? elements[0]!;
|
||||
for (const el of elements) {
|
||||
if (el === keeper) continue;
|
||||
const scopeIndex = scopeIndexByRenamable.get(el);
|
||||
if (scopeIndex === undefined) continue;
|
||||
const idMap = idMaps[scopeIndex]!;
|
||||
if (!idMap.has(id)) {
|
||||
idMap.set(id, mintNamespacedId(scopes[scopeIndex]!.namespace, id, census.usedIds));
|
||||
}
|
||||
for (const index of collisionScopes(id, elements, scopeIndexByRenamable)) {
|
||||
idMaps[index]!.set(id, mintNamespacedId(scopes[index]!.namespace, id, census.usedIds));
|
||||
}
|
||||
}
|
||||
return idMaps;
|
||||
}
|
||||
|
||||
function documentReferenceTargets(
|
||||
census: IdCensus,
|
||||
idMaps: readonly ReadonlyMap<string, string>[],
|
||||
scopeIndexByRenamable: ReadonlyMap<Element, number>,
|
||||
): Map<string, string> {
|
||||
const targets = new Map<string, string>();
|
||||
for (const [id, elements] of census.elementsById) {
|
||||
const target = elements[0]!;
|
||||
const actual = target.getAttribute(ID_ATTR)!;
|
||||
if (actual !== id) {
|
||||
targets.set(id, actual);
|
||||
continue;
|
||||
}
|
||||
const index = scopeIndexByRenamable.get(target);
|
||||
const renamed = index === undefined ? undefined : idMaps[index]!.get(id);
|
||||
if (renamed) targets.set(id, renamed);
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
function localReferenceBindings(elements: readonly Element[]) {
|
||||
const ids = new Set(elements.map((el) => el.getAttribute(ID_ATTR)));
|
||||
const renamed = new Map<string, string>();
|
||||
for (const el of elements) {
|
||||
const authored = currentAuthoredSvgId(el);
|
||||
const current = el.getAttribute(ID_ATTR);
|
||||
if (!authored || !current) continue;
|
||||
ids.add(authored);
|
||||
renamed.set(authored, current);
|
||||
}
|
||||
return { ids, renamed };
|
||||
}
|
||||
|
||||
/** Rename the planned elements in one scope and rewrite every reference in
|
||||
* that scope's attributes to match. */
|
||||
function applyRenames(resolved: ResolvedScope, idMap: ReadonlyMap<string, string>): void {
|
||||
@@ -299,7 +398,17 @@ function applyRenames(resolved: ResolvedScope, idMap: ReadonlyMap<string, string
|
||||
const currentId = el.getAttribute(ID_ATTR);
|
||||
if (currentId && resolved.renamable.has(el) && idMap.has(currentId)) {
|
||||
if (!el.hasAttribute(SVG_AUTHORED_ID_ATTR)) el.setAttribute(SVG_AUTHORED_ID_ATTR, currentId);
|
||||
el.setAttribute(ID_ATTR, idMap.get(currentId)!);
|
||||
const after = idMap.get(currentId)!;
|
||||
const aliases = readSvgReferenceAliases(el, SVG_REFERENCE_ALIASES_ATTR, false);
|
||||
aliases.push({
|
||||
name: ID_ATTR,
|
||||
localName: ID_ATTR,
|
||||
namespaceURI: null,
|
||||
before: currentId,
|
||||
after,
|
||||
});
|
||||
el.setAttribute(SVG_REFERENCE_ALIASES_ATTR, JSON.stringify(aliases));
|
||||
el.setAttribute(ID_ATTR, after);
|
||||
}
|
||||
rewriteElementIdReferences(el, idMap);
|
||||
}
|
||||
@@ -325,60 +434,40 @@ function applyRenames(resolved: ResolvedScope, idMap: ReadonlyMap<string, string
|
||||
export function namespaceCollidingSvgIds(
|
||||
document: SvgIdQueryable,
|
||||
scopes: readonly SvgIdScope[],
|
||||
finalizedIds?: Set<Element>,
|
||||
): Map<string, string>[] {
|
||||
const untouched = scopes.map(() => new Map<string, string>());
|
||||
if (scopes.length === 0) return untouched;
|
||||
if (scopes.length === 0) return [];
|
||||
|
||||
const census = buildIdCensus(document);
|
||||
if (!hasAnyCollision(census)) return untouched;
|
||||
|
||||
const resolved = scopes.map(resolveScope);
|
||||
const newlyEligible = resolved.flatMap(({ renamable }) => [...renamable]);
|
||||
if (finalizedIds) {
|
||||
for (const scope of resolved) {
|
||||
for (const el of scope.renamable) if (finalizedIds.has(el)) scope.renamable.delete(el);
|
||||
}
|
||||
}
|
||||
const scopeIndexByRenamable = new Map<Element, number>();
|
||||
resolved.forEach(({ renamable }, index) => {
|
||||
for (const el of renamable) scopeIndexByRenamable.set(el, index);
|
||||
});
|
||||
if (scopeIndexByRenamable.size === 0) return untouched;
|
||||
|
||||
const idMaps = planRenames(census, scopes, scopeIndexByRenamable);
|
||||
// A reference without a local definition keeps its original document-order target,
|
||||
// even if that target was renamed in an ancestor or another composition.
|
||||
const documentTargets = documentReferenceTargets(census, idMaps, scopeIndexByRenamable);
|
||||
const referenceMaps = resolved.map((scope, index) => {
|
||||
const references = new Map(idMaps[index]);
|
||||
const localIds = new Set(scope.elements.map((el) => el.getAttribute(ID_ATTR)));
|
||||
for (const [id, elements] of census.elementsById) {
|
||||
if (localIds.has(id)) continue;
|
||||
const targetScope = scopeIndexByRenamable.get(elements[0]!);
|
||||
if (targetScope === undefined) continue;
|
||||
const mapped = idMaps[targetScope]!.get(id);
|
||||
if (mapped) references.set(id, mapped);
|
||||
}
|
||||
return references;
|
||||
const local = localReferenceBindings(
|
||||
scopes[index]!.referenceTarget === "document" ? [] : scope.elements,
|
||||
);
|
||||
const inherited = [...documentTargets].filter(([id]) => !local.ids.has(id));
|
||||
return new Map([...inherited, ...local.renamed, ...idMaps[index]!]);
|
||||
});
|
||||
referenceMaps.forEach((idMap, index) => {
|
||||
if (idMap.size > 0) applyRenames(resolved[index]!, idMap);
|
||||
applyRenames(resolved[index]!, idMap);
|
||||
});
|
||||
for (const el of newlyEligible) finalizedIds?.add(el);
|
||||
return referenceMaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whole-token `#id` replacement inside a CSS selector, generalized to many
|
||||
* ids at once and to a literal `#newId` swap instead of an attribute-selector
|
||||
* expansion (a plain descendant/id selector already resolves correctly once
|
||||
* the id itself is unique — no extra scoping needed). Shares its quote- and
|
||||
* bracket-aware scan with `compositionScoping.ts`'s
|
||||
* `replaceAuthoredRootIdSelectors` via `selectorIdTokens.ts`, rather than a
|
||||
* second copy of the same state machine. The scanner decodes CSS identifier
|
||||
* escapes before matching, so `#fx\.1` matches the raw id `fx.1`, and the
|
||||
* replacement is re-escaped so the emitted selector stays valid.
|
||||
*/
|
||||
function renameIdTokensInSelector(selector: string, idMap: ReadonlyMap<string, string>): string {
|
||||
return replaceSelectorIdTokens(
|
||||
selector,
|
||||
[...idMap.keys()],
|
||||
(matchedId) => `#${escapeCssIdentifier(idMap.get(matchedId)!)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the same id substitution `namespaceCollidingSvgIds` computed to a
|
||||
* composition's `<style>` text.
|
||||
@@ -395,9 +484,10 @@ function renameIdTokensInSelector(selector: string, idMap: ReadonlyMap<string, s
|
||||
export function rewriteSvgIdReferencesInCss(
|
||||
css: string,
|
||||
idMap: ReadonlyMap<string, string>,
|
||||
references: readonly SvgReferenceAlias[] = [],
|
||||
): string {
|
||||
if (!css || idMap.size === 0) return css;
|
||||
if (!css.includes("#") && !css.toLowerCase().includes("url(")) return css;
|
||||
if (!css || (idMap.size === 0 && references.length === 0)) return css;
|
||||
if (!css.includes("#") && !css.includes("[") && !css.toLowerCase().includes("url(")) return css;
|
||||
|
||||
let root: postcss.Root;
|
||||
try {
|
||||
@@ -408,9 +498,19 @@ export function rewriteSvgIdReferencesInCss(
|
||||
return css;
|
||||
}
|
||||
let mutated = false;
|
||||
|
||||
const recordedIds = new Set(
|
||||
references.filter((alias) => alias.name === ID_ATTR).map((alias) => alias.before),
|
||||
);
|
||||
const ids = [...idMap]
|
||||
.filter(([id]) => !recordedIds.has(id))
|
||||
.map(([id, renamed]) => ({
|
||||
id,
|
||||
replacement: `:is(#${escapeCssIdentifier(id)}, #${escapeCssIdentifier(renamed)})`,
|
||||
}));
|
||||
root.walkRules((rule) => {
|
||||
const rewritten = rule.selectors.map((selector) => renameIdTokensInSelector(selector, idMap));
|
||||
const rewritten = rule.selectors.map((selector) =>
|
||||
rewriteSvgSelectors(selector, ids, references),
|
||||
);
|
||||
if (rewritten.some((selector, index) => selector !== rule.selectors[index])) {
|
||||
rule.selectors = rewritten;
|
||||
mutated = true;
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import * as tokens from "./selectorIdTokens";
|
||||
export const SVG_REFERENCE_ALIASES_ATTR = "data-hf-svg-reference-aliases";
|
||||
|
||||
export interface SvgReferenceAlias {
|
||||
name: string;
|
||||
localName: string;
|
||||
namespaceURI: string | null;
|
||||
before: string;
|
||||
after: string;
|
||||
}
|
||||
|
||||
interface IdSelectorAlias {
|
||||
id: string;
|
||||
replacement: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* With `namespaces` (a live sheet's bindings) only prefixed attribute selectors are
|
||||
* rewritten; without it they stay as authored and everything else is rewritten.
|
||||
*/
|
||||
export function rewriteSvgSelectors(
|
||||
selector: string,
|
||||
ids: readonly IdSelectorAlias[],
|
||||
references: readonly SvgReferenceAlias[],
|
||||
namespaces?: ReadonlyMap<string, string | null>,
|
||||
): string {
|
||||
type AttributeOperator = NonNullable<
|
||||
ReturnType<typeof tokens.parseAttributeSelector>
|
||||
>["operator"];
|
||||
const attributeOperators = {
|
||||
"=": (before, value) => before === value,
|
||||
"~=": (before, value) => before.split(/[ \t\r\n\f]+/).includes(value),
|
||||
"|=": (before, value) => before === value || before.startsWith(value + "-"),
|
||||
"^=": (before, value) => before.startsWith(value),
|
||||
"$=": (before, value) => before.endsWith(value),
|
||||
"*=": (before, value) => before.includes(value),
|
||||
} satisfies Record<AttributeOperator, (before: string, value: string) => boolean>;
|
||||
const matchesValue = (before: string, value: string, operator: AttributeOperator): boolean => {
|
||||
if (!value && operator !== "=" && operator !== "|=") return false;
|
||||
return attributeOperators[operator](before, value);
|
||||
};
|
||||
const lower = (text: string) => text.replace(/[A-Z]/g, (char) => char.toLowerCase());
|
||||
const rewriteAttribute = (predicate: string): string => {
|
||||
const parsed = tokens.parseAttributeSelector(predicate);
|
||||
if (!parsed) return predicate;
|
||||
const { name, rawName, namespace, operator, value, flag } = parsed;
|
||||
if ((namespace.kind === "named") !== (namespaces !== undefined)) return predicate;
|
||||
const fold = flag?.toLowerCase() === "i" ? lower : (text: string) => text;
|
||||
const attributeName = lower(name);
|
||||
const candidates = references.filter((reference) => {
|
||||
const actualName = namespace.kind === "none" ? reference.name : reference.localName;
|
||||
const uri = namespace.kind === "named" ? namespaces!.get(namespace.prefix) : null;
|
||||
return (
|
||||
(namespace.kind === "any" || reference.namespaceURI === uri) &&
|
||||
lower(actualName) === attributeName
|
||||
);
|
||||
});
|
||||
const alternatives = new Set([predicate]);
|
||||
for (const reference of candidates) {
|
||||
if (!matchesValue(fold(reference.before), fold(value), operator)) continue;
|
||||
alternatives.add(
|
||||
`[${rawName}="${tokens.escapeCssAttributeValue(reference.after)}"${flag ? ` ${flag}` : ""}]`,
|
||||
);
|
||||
}
|
||||
return alternatives.size === 1 ? predicate : `:is(${[...alternatives].join(", ")})`;
|
||||
};
|
||||
if (namespaces) return tokens.replaceSelectorAttributeTokens(selector, rewriteAttribute);
|
||||
const replacements = new Map(ids.map(({ id, replacement }) => [id, replacement]));
|
||||
const targets = new Map<string, Set<string>>();
|
||||
for (const reference of references) {
|
||||
if (
|
||||
reference.name !== "id" ||
|
||||
reference.before === reference.after ||
|
||||
replacements.has(reference.before)
|
||||
)
|
||||
continue;
|
||||
let alternatives = targets.get(reference.before);
|
||||
if (!alternatives) {
|
||||
alternatives = new Set([`#${tokens.escapeCssIdentifier(reference.before)}`]);
|
||||
targets.set(reference.before, alternatives);
|
||||
}
|
||||
alternatives.add(`#${tokens.escapeCssIdentifier(reference.after)}`);
|
||||
}
|
||||
for (const [id, alternatives] of targets)
|
||||
replacements.set(id, `:is(${[...alternatives].join(", ")})`);
|
||||
const rewrittenIds = tokens.replaceSelectorIdTokens(
|
||||
selector,
|
||||
[...replacements.keys()],
|
||||
(id) => replacements.get(id)!,
|
||||
);
|
||||
return tokens.replaceSelectorAttributeTokens(rewrittenIds, rewriteAttribute);
|
||||
}
|
||||
|
||||
/** Metadata records actual writes; selectors still match current native attribute values. */
|
||||
export function readSvgReferenceAliases(
|
||||
root: {
|
||||
querySelectorAll(selector: string): Iterable<Element>;
|
||||
getAttribute?: (name: string) => string | null;
|
||||
},
|
||||
attribute: string,
|
||||
includeDescendants = true,
|
||||
): SvgReferenceAlias[] {
|
||||
const aliases: SvgReferenceAlias[] = [];
|
||||
const nodes = includeDescendants ? [...root.querySelectorAll(`[${attribute}]`)] : [];
|
||||
const texts = nodes.map((node) => node.getAttribute(attribute)!);
|
||||
const own = root.getAttribute?.(attribute);
|
||||
if (own) texts.unshift(own);
|
||||
for (const text of texts) {
|
||||
let value: unknown;
|
||||
try {
|
||||
value = JSON.parse(text);
|
||||
} catch {
|
||||
continue; // authored HTML can carry this attribute; a bad value must not stop boot
|
||||
}
|
||||
if (!Array.isArray(value)) continue;
|
||||
const entries: unknown[] = value;
|
||||
for (const entry of entries) {
|
||||
if (!isSvgReferenceAlias(entry)) continue;
|
||||
const { name, localName, namespaceURI, before, after } = entry;
|
||||
aliases.push({ name, localName, namespaceURI, before, after });
|
||||
}
|
||||
}
|
||||
return aliases;
|
||||
}
|
||||
|
||||
function isSvgReferenceAlias(entry: unknown): entry is SvgReferenceAlias {
|
||||
if (!entry || typeof entry !== "object") return false;
|
||||
const { name, localName, namespaceURI, before, after } = entry as Record<string, unknown>;
|
||||
return (
|
||||
typeof name === "string" &&
|
||||
typeof localName === "string" &&
|
||||
(namespaceURI === null || typeof namespaceURI === "string") &&
|
||||
typeof before === "string" &&
|
||||
typeof after === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export interface SvgSelectorAliases {
|
||||
refresh(): void;
|
||||
rewrite(selector: string, additional?: readonly IdSelectorAlias[]): string;
|
||||
}
|
||||
|
||||
type AliasWindow = Pick<Window, "document"> & {
|
||||
Element: typeof Element;
|
||||
__hfSvgSelectorAliases?: SvgSelectorAliases;
|
||||
};
|
||||
|
||||
function installSvgSelectorAliases(win: AliasWindow): void {
|
||||
let ids: IdSelectorAlias[] = [];
|
||||
let references: SvgReferenceAlias[] = [];
|
||||
let installed = false;
|
||||
let validateSyntax: ((selector: string) => boolean) | undefined;
|
||||
const ruleSelectors = new WeakMap<CSSStyleRule, { authored: string; applied: string }>();
|
||||
const repairRules = (rules: CSSRuleList, namespaces: ReadonlyMap<string, string | null>) => {
|
||||
for (const rule of rules) {
|
||||
if ("selectorText" in rule) {
|
||||
const styleRule = rule as CSSStyleRule;
|
||||
const record = ruleSelectors.get(styleRule);
|
||||
const current = styleRule.selectorText;
|
||||
const authored = record?.applied === current ? record.authored : current;
|
||||
const next = rewriteSvgSelectors(authored, [], references, namespaces);
|
||||
if (next !== current) styleRule.selectorText = next;
|
||||
if (next === authored) ruleSelectors.delete(styleRule);
|
||||
else ruleSelectors.set(styleRule, { authored, applied: styleRule.selectorText });
|
||||
}
|
||||
if ("cssRules" in rule) repairRules((rule as CSSGroupingRule).cssRules, namespaces);
|
||||
}
|
||||
};
|
||||
// Only the browser knows which @namespace rules a sheet kept, so prefixed attribute
|
||||
// selectors are repaired in the live CSSOM; compiled CSS text leaves them as authored.
|
||||
const repairNamespacedSelectors = () => {
|
||||
if (!references.length) return;
|
||||
for (const sheet of win.document.styleSheets) {
|
||||
let rules: CSSRuleList;
|
||||
try {
|
||||
rules = sheet.cssRules;
|
||||
} catch {
|
||||
continue; // cross-origin sheets are unreadable by design
|
||||
}
|
||||
const namespaces = new Map<string, string | null>();
|
||||
for (const rule of rules) {
|
||||
if (!("namespaceURI" in rule)) continue;
|
||||
const { prefix, namespaceURI } = rule as CSSNamespaceRule;
|
||||
if (prefix) namespaces.set(prefix, namespaceURI || null);
|
||||
}
|
||||
if (namespaces.size) repairRules(rules, namespaces);
|
||||
}
|
||||
};
|
||||
const aliases: SvgSelectorAliases = {
|
||||
rewrite: (selector, additional = []) => {
|
||||
if (typeof selector !== "string" || (!selector.includes("#") && !selector.includes("[")))
|
||||
return selector;
|
||||
const rewritten = rewriteSvgSelectors(
|
||||
selector,
|
||||
additional.length ? [...additional, ...ids] : ids,
|
||||
references,
|
||||
);
|
||||
if (rewritten !== selector) {
|
||||
if (!validateSyntax) {
|
||||
const syntaxProbe = win.document.createElement("div");
|
||||
validateSyntax = syntaxProbe.matches.bind(syntaxProbe);
|
||||
}
|
||||
validateSyntax(selector);
|
||||
}
|
||||
return rewritten;
|
||||
},
|
||||
refresh() {
|
||||
references = readSvgReferenceAliases(win.document, SVG_REFERENCE_ALIASES_ATTR).filter(
|
||||
(alias) => alias.before !== alias.after,
|
||||
);
|
||||
const recordedIds = new Set(
|
||||
references.filter((alias) => alias.name === "id").map((alias) => alias.before),
|
||||
);
|
||||
const seen = new Set<string>();
|
||||
ids = [];
|
||||
for (const node of win.document.querySelectorAll("[data-hf-authored-id][id]")) {
|
||||
const authored = node.getAttribute("data-hf-authored-id")!;
|
||||
if (!authored || authored === node.id || seen.has(authored) || recordedIds.has(authored))
|
||||
continue;
|
||||
seen.add(authored);
|
||||
const value = tokens.escapeCssAttributeValue(authored);
|
||||
ids.push({
|
||||
id: authored,
|
||||
replacement: `:is(#${tokens.escapeCssIdentifier(authored)}, [data-hf-authored-id="${value}"])`,
|
||||
});
|
||||
}
|
||||
repairNamespacedSelectors();
|
||||
if (installed || (!ids.length && !references.length)) return;
|
||||
const proto = win.Element.prototype;
|
||||
proto.querySelector = new Proxy(proto.querySelector, {
|
||||
apply(target, thisArg, args) {
|
||||
return Reflect.apply(target, thisArg, [aliases.rewrite(args[0])]);
|
||||
},
|
||||
});
|
||||
proto.querySelectorAll = new Proxy(proto.querySelectorAll, {
|
||||
apply(target, thisArg, args) {
|
||||
return Reflect.apply(target, thisArg, [aliases.rewrite(args[0])]);
|
||||
},
|
||||
});
|
||||
installed = true;
|
||||
},
|
||||
};
|
||||
win.__hfSvgSelectorAliases = aliases;
|
||||
aliases.refresh();
|
||||
}
|
||||
|
||||
export function refreshSvgSelectorAliases(): void {
|
||||
const win: AliasWindow = window;
|
||||
if (win.__hfSvgSelectorAliases) win.__hfSvgSelectorAliases.refresh();
|
||||
else installSvgSelectorAliases(win);
|
||||
}
|
||||
|
||||
/** Compiled scene scripts install once; refreshes stay with the runtime's lifecycle. */
|
||||
export function ensureSvgSelectorAliases(): void {
|
||||
const win: AliasWindow = window;
|
||||
if (!win.__hfSvgSelectorAliases) installSvgSelectorAliases(win);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// AUTO-GENERATED by scripts/build-position-edits-render.ts - do not edit
|
||||
// Generated by scripts/build-position-edits-render.ts; do not edit.
|
||||
const POSITION_EDITS_RENDER_IIFE: string =
|
||||
'"use strict";(()=>{function T(){return globalThis}function P(e,t){if(typeof window>"u")return;let o=T(),r=o.__hf?.onSwallowed;if(r)try{r({label:e,error:t})}catch(s){}(o.__hfDebug||o.__HYPERFRAMES_DEBUG)&&console.debug(`[hyperframes] ${e} swallowed:`,t)}var x=null;function R(e,t){if(x)try{x({source:"hf-preview",type:"analytics",event:e,properties:t??{}})}catch(o){P("runtime.analytics.site1",o)}}var W="http://www.w3.org/1999/xhtml";function N(e){return e!=null&&e.nodeType===1}function _(e){return N(e)&&(e.namespaceURI===W||e.namespaceURI==="http://www.w3.org/2000/svg")}var w="data-hf-edit-base-x",y="data-hf-edit-base-y",m="data-hf-edit-original-translate",k=e=>{let t=parseFloat(e??"");return Number.isFinite(t)?t:0},O=e=>{let t=[],o=0,r="";for(let s of e.trim())s==="("&&(o+=1),s===")"&&(o=Math.max(0,o-1)),/\\s/.test(s)&&o===0?(r&&t.push(r),r=""):r+=s;return r&&t.push(r),t},L=/^-?(?:\\d+(?:\\.\\d+)?|\\.\\d+)px$/,E=(e,t)=>L.test(e)&&L.test(t)?`${parseFloat(e)+parseFloat(t)}px`:`calc(${e} + ${t})`,V=(e,t,o)=>{if(!e||e==="none")return`${t} ${o}`;let[r,s,a]=O(e);if(r===void 0)return`${t} ${o}`;if(s===void 0)return`${E(r,t)} ${o}`;let i=a===void 0?"":` ${a}`;return`${E(r,t)} ${E(s,o)}${i}`},j=e=>{try{e.ownerDocument.defaultView?.gsap?.getProperty?.(e,"x")}catch{}},G=e=>{let t=e.style.getPropertyValue("translate").trim();if(t)return t==="none"?"":t;try{let o=e.ownerDocument.defaultView,r=o?o.getComputedStyle(e).getPropertyValue("translate").trim():"";return r==="none"?"":r}catch{return""}},b=new WeakMap;function B(e,t){let o=b.get(e);if(!t?.force&&o!==void 0&&e.style.getPropertyValue("translate")!==o){R("position_edit_fold_skipped",{hfId:e.getAttribute("data-hf-id")});return}let r=k(e.getAttribute("data-x"))-k(e.getAttribute(w)),s=k(e.getAttribute("data-y"))-k(e.getAttribute(y));e.hasAttribute(m)||e.setAttribute(m,G(e)),o===void 0&&j(e);let a=e.getAttribute(m)??"",i=V(a,`${r}px`,`${s}px`);e.style.setProperty("translate",i),b.set(e,e.style.getPropertyValue("translate"))}function S(e,t){let o=e.querySelectorAll(`[${m}]:not([${w}]):not([${y}])`);for(let a=0;a<o.length;a++){let i=o[a];if(i===void 0||!_(i))continue;let g=i.getAttribute(m)??"";g===""?i.style.removeProperty("translate"):i.style.setProperty("translate",g),i.removeAttribute(m),b.delete(i)}let r=e.querySelectorAll(`[${w}], [${y}]`),s=0;for(let a=0;a<r.length;a++){let i=r[a];i===void 0||!_(i)||(B(i,t),s+=1)}return s}var $="__hfPositionEditsSeekReapplyWrapped",D=new WeakSet,M=new WeakMap,H=new WeakMap;function F(e){let t=e,o=()=>{try{S(t.document)}catch{}},r=n=>typeof n=="function"&&(D.has(n)||!!n[$]),s=n=>{D.add(n);try{Object.defineProperty(n,$,{value:!0})}catch{}},a=n=>{if(typeof n!="function"||r(n))return n;let l=function(...u){let d=n.apply(this,u);return o(),d};return s(l),l},i=(n,l)=>{let u=M.get(n);if(u?.has(l))return!0;let d=Object.getOwnPropertyDescriptor(n,l);if(d?.configurable===!1){let c=n[l];return typeof c=="function"&&(n[l]=a(c),o()),!1}let p=n[l],f=d?.set;return Object.defineProperty(n,l,{configurable:!0,enumerable:d?.enumerable??!0,get:()=>p,set:c=>{p=a(c),f?.call(n,c)}}),p=a(p),u??(u=new Set),u.add(l),M.set(n,u),o(),!0},g=(n,l)=>{let u=H.get(t),d=Object.getOwnPropertyDescriptor(t,n);if(!u?.has(n)){if(d?.configurable===!1){let c=t[n];return c?i(c,l):!1}let f=t[n];Object.defineProperty(t,n,{configurable:!0,enumerable:d?.enumerable??!0,get:()=>f,set:c=>{f=c,f&&i(f,l)}}),u??(u=new Set),u.add(n),H.set(t,u)}let p=t[n];return p?i(p,l):!1},v=()=>{let n=g("__hf","seek"),l=g("__player","renderSeek");return n&&l};if(v())return;let h=120,A=t.setInterval(()=>{if(v()){t.clearInterval(A);return}h-=1,h<=0&&t.clearInterval(A)},50)}function I(){document.querySelector(`[${w}], [${y}]`)&&(S(document),F(window))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",I,{once:!0}):I();})();\n';
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { getSvgSelectorAliasesScript } from "./svg-selector-aliases-inline";
|
||||
|
||||
const nativeQuerySelector = Element.prototype.querySelector;
|
||||
const renamedUse = `<symbol id="scene--icon"/><use href="#scene--icon" data-hf-svg-reference-aliases='[{"name":"href","localName":"href","namespaceURI":null,"before":"#icon","after":"#scene--icon"}]'/>`;
|
||||
|
||||
afterEach(() => {
|
||||
Element.prototype.querySelector = nativeQuerySelector;
|
||||
delete window.__hfSvgSelectorAliases;
|
||||
});
|
||||
|
||||
describe("getSvgSelectorAliasesScript", () => {
|
||||
it("installs aliases that follow a renamed SVG reference", () => {
|
||||
document.body.innerHTML = `<svg>${renamedUse}</svg>`;
|
||||
new Function(getSvgSelectorAliasesScript())();
|
||||
expect(document.body.querySelector('use[href="#icon"]')).toBe(document.querySelector("use"));
|
||||
});
|
||||
|
||||
it("skips malformed alias attributes from authored HTML", () => {
|
||||
const wrongType = `[{"name":1,"localName":"href","namespaceURI":null,"before":"#icon","after":"#x"}]`;
|
||||
document.body.innerHTML = `<svg><g data-hf-svg-reference-aliases="not json"/><g data-hf-svg-reference-aliases='${wrongType}'/>${renamedUse}</svg>`;
|
||||
new Function(getSvgSelectorAliasesScript())();
|
||||
expect(document.body.querySelector('use[href="#icon"]')).toBe(document.querySelector("use"));
|
||||
});
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -47,6 +47,20 @@ Determinism baseline:
|
||||
|
||||
- `renderSeek` is the producer-canonical seek path.
|
||||
- 30fps quantization and readiness gates are correctness requirements.
|
||||
- Preview automation uses `?hf-capture=1`, `hyperframes snapshot`, or runtime readiness
|
||||
(`window.__renderReady` and the seek contract), never a wait on all of `document.images`.
|
||||
Interactive preview leaves images in hidden future clips unloaded until they are needed.
|
||||
|
||||
Runtime-loaded compositions finalize IDs already used by native SVG references before scene
|
||||
scripts run. IDs used only by JavaScript keep their authored values. If an initial script
|
||||
introduces an ID's first native `url()` or `href` reference, that ID can be renamed after
|
||||
initial scripts finish. A string captured before that first native use is not guaranteed
|
||||
stable; read the target element's current `id` after runtime readiness for deferred writes.
|
||||
|
||||
Attribute selectors with a declared namespace prefix (`@namespace xl url(...)` plus
|
||||
`[xl|href="#id"]`) follow renamed references through the live CSSOM before scene scripts run,
|
||||
in runtime and compiled output alike. Compiled CSS text leaves them as authored, so these
|
||||
selectors need JavaScript; with scripts disabled they match only unrenamed values.
|
||||
|
||||
## Build
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
SVG_REFERENCE_ALIASES_ATTR,
|
||||
readSvgReferenceAliases,
|
||||
refreshSvgSelectorAliases,
|
||||
} from "../compiler/svgSelectorAliases";
|
||||
import {
|
||||
planCompositionAssembly,
|
||||
EXTRACTED_COMPOSITION_ASSET_SELECTOR,
|
||||
@@ -9,6 +14,10 @@ import {
|
||||
} from "../compiler/compositionScoping";
|
||||
import { parseImportMap } from "../compiler/importMaps";
|
||||
import { hasSameLink } from "../compiler/scriptRuns";
|
||||
import {
|
||||
namespaceCollidingSvgIds,
|
||||
rewriteSvgIdReferencesInCss,
|
||||
} from "../compiler/svgIdNamespacing";
|
||||
import { waitForFonts } from "./afterFonts";
|
||||
import { parseLayoutDimension } from "./compositionDimension";
|
||||
import { markFlattenedInnerRoot } from "./flattenedRoot";
|
||||
@@ -23,7 +32,7 @@ import {
|
||||
} from "./getVariables";
|
||||
import { isElementNode, isHtmlElement, isLinkElement, isStyleElement } from "./domRealm";
|
||||
|
||||
type LoadExternalCompositionsParams = {
|
||||
type LoadCompositionsParams = {
|
||||
injectedStyles: HTMLStyleElement[];
|
||||
injectedScripts: HTMLScriptElement[];
|
||||
injectedLinks: HTMLLinkElement[];
|
||||
@@ -34,6 +43,18 @@ type LoadExternalCompositionsParams = {
|
||||
}) => void;
|
||||
};
|
||||
|
||||
type MountedSvgScope = {
|
||||
host: Element;
|
||||
namespace: string;
|
||||
styles: Array<{ element: HTMLStyleElement; authored: string; applied: string }>;
|
||||
referenceTarget?: "document";
|
||||
};
|
||||
|
||||
type MountedComposition = MountedSvgScope & {
|
||||
isCurrent: () => boolean;
|
||||
runScripts: () => Promise<void>;
|
||||
};
|
||||
|
||||
type PendingScript =
|
||||
| {
|
||||
kind: "inline";
|
||||
@@ -329,15 +350,28 @@ function cleanupDetachedScopedVariables() {
|
||||
|
||||
function assignRuntimeCompositionIds(
|
||||
hosts: Element[],
|
||||
hostCountsByCompositionId: Map<string, number> = countAuthoredCompositionIds(hosts),
|
||||
mountedHosts: ReadonlySet<Element>,
|
||||
): Map<Element, HostCompositionIdentity> {
|
||||
const hostCountsByCompositionId = countAuthoredCompositionIds(hosts);
|
||||
const reserved = new Set(
|
||||
hosts.flatMap((host) => {
|
||||
const identity = getHostCompositionIdentity(host);
|
||||
const ids = identity.authoredCompositionId ? [identity.authoredCompositionId] : [];
|
||||
if (
|
||||
(mountedHosts.has(host) || !shouldAssignRuntimeCompositionId(host)) &&
|
||||
identity.runtimeCompositionId
|
||||
)
|
||||
ids.push(identity.runtimeCompositionId);
|
||||
return ids;
|
||||
}),
|
||||
);
|
||||
const hostInstanceByCompositionId = new Map<string, number>();
|
||||
const hostIdentityByElement = new Map<Element, HostCompositionIdentity>();
|
||||
|
||||
for (const host of hosts) {
|
||||
const { authoredCompositionId, runtimeCompositionId: previousRuntimeCompositionId } =
|
||||
getHostCompositionIdentity(host);
|
||||
const shouldAssign = shouldAssignRuntimeCompositionId(host);
|
||||
const shouldAssign = !mountedHosts.has(host) && shouldAssignRuntimeCompositionId(host);
|
||||
if (!authoredCompositionId) {
|
||||
hostIdentityByElement.set(host, {
|
||||
authoredCompositionId: null,
|
||||
@@ -349,16 +383,18 @@ function assignRuntimeCompositionIds(
|
||||
const duplicateInstance = (hostCountsByCompositionId.get(authoredCompositionId) || 0) > 1;
|
||||
let runtimeCompositionId = previousRuntimeCompositionId || authoredCompositionId;
|
||||
if (shouldAssign) {
|
||||
const instanceIndex = duplicateInstance
|
||||
let instanceIndex = duplicateInstance
|
||||
? (hostInstanceByCompositionId.get(authoredCompositionId) || 0) + 1
|
||||
: 0;
|
||||
if (duplicateInstance) {
|
||||
while (reserved.has(uniqueCompositionId(authoredCompositionId, instanceIndex)))
|
||||
instanceIndex += 1;
|
||||
hostInstanceByCompositionId.set(authoredCompositionId, instanceIndex);
|
||||
}
|
||||
|
||||
runtimeCompositionId = duplicateInstance
|
||||
? uniqueCompositionId(authoredCompositionId, instanceIndex)
|
||||
: authoredCompositionId;
|
||||
reserved.add(runtimeCompositionId);
|
||||
|
||||
if (duplicateInstance) {
|
||||
host.setAttribute("data-hf-original-composition-id", authoredCompositionId);
|
||||
@@ -366,13 +402,6 @@ function assignRuntimeCompositionIds(
|
||||
host.removeAttribute("data-hf-original-composition-id");
|
||||
}
|
||||
host.setAttribute("data-composition-id", runtimeCompositionId);
|
||||
if (
|
||||
previousRuntimeCompositionId &&
|
||||
previousRuntimeCompositionId !== runtimeCompositionId &&
|
||||
window.__hfVariablesByComp
|
||||
) {
|
||||
delete window.__hfVariablesByComp[previousRuntimeCompositionId];
|
||||
}
|
||||
}
|
||||
|
||||
hostIdentityByElement.set(host, {
|
||||
@@ -381,6 +410,7 @@ function assignRuntimeCompositionIds(
|
||||
});
|
||||
}
|
||||
|
||||
cleanupDetachedScopedVariables();
|
||||
return hostIdentityByElement;
|
||||
}
|
||||
|
||||
@@ -408,7 +438,7 @@ async function mountCompositionContent(params: {
|
||||
* `<html data-composition-variables="...">` attribute. Layered under the
|
||||
* host element's `data-variable-values` to produce the per-instance
|
||||
* variables visible inside the sub-comp's scoped `getVariables()`.
|
||||
* Populated only by `loadExternalCompositions`; inline templates have no
|
||||
* Populated only by `loadCompositions`; inline templates have no
|
||||
* separate document root so no declared defaults are passed.
|
||||
*/
|
||||
declaredVariableDefaults?: Record<string, unknown>;
|
||||
@@ -422,7 +452,7 @@ async function mountCompositionContent(params: {
|
||||
code: string;
|
||||
details: Record<string, string | number | boolean | null | string[]>;
|
||||
}) => void;
|
||||
}): Promise<void> {
|
||||
}): Promise<MountedComposition> {
|
||||
// Which node is the composition root, which id its CSS scopes to, which id
|
||||
// its scripts scope to, where its assets come from and in what order: the
|
||||
// shared assembly module answers all of it, so this path and the compiler's
|
||||
@@ -467,6 +497,7 @@ async function mountCompositionContent(params: {
|
||||
params.injectedLinks.push(clonedLink);
|
||||
}
|
||||
|
||||
const styles: MountedComposition["styles"] = [];
|
||||
const injectScopedStyles = (styleEls: Iterable<Element>): void => {
|
||||
for (const style of styleEls) {
|
||||
const clonedStyle = style.cloneNode(true);
|
||||
@@ -486,6 +517,8 @@ async function mountCompositionContent(params: {
|
||||
}
|
||||
document.head.appendChild(clonedStyle);
|
||||
params.injectedStyles.push(clonedStyle);
|
||||
const authored = clonedStyle.textContent || "";
|
||||
styles.push({ element: clonedStyle, authored, applied: authored });
|
||||
}
|
||||
};
|
||||
// Already in injection order: <head> styles from a non-template composition
|
||||
@@ -555,71 +588,87 @@ async function mountCompositionContent(params: {
|
||||
stashInstanceVariables(params, contentNode, runtimeScopeCompositionId);
|
||||
}
|
||||
|
||||
if (scriptPayloads.length > 0) await waitForFonts();
|
||||
for (const scriptPayload of scriptPayloads) {
|
||||
const injectedScript = document.createElement("script");
|
||||
if (scriptPayload.type) {
|
||||
injectedScript.type = scriptPayload.type;
|
||||
}
|
||||
// Preserve deterministic script execution order across injected composition scripts.
|
||||
injectedScript.async = false;
|
||||
if (scriptPayload.kind === "external") {
|
||||
injectedScript.src = scriptPayload.src;
|
||||
} else if (scriptPayload.type.toLowerCase() === "importmap") {
|
||||
const map = parseImportMap(scriptPayload.content, (url) =>
|
||||
resolveScriptSourceUrl(url, params.compositionUrl),
|
||||
);
|
||||
injectedScript.textContent = map ? JSON.stringify(map) : scriptPayload.content;
|
||||
} else if (scriptPayload.type.toLowerCase() === "module") {
|
||||
const prelude = scriptPayload.scopeCompositionId
|
||||
? scopedModulePrelude(
|
||||
const mountedNodes = Array.from(params.host.childNodes);
|
||||
const isCurrent = () =>
|
||||
params.host.isConnected &&
|
||||
(mountedNodes.length > 0 || params.host.childNodes.length === 0) &&
|
||||
mountedNodes.every((node) => node.parentNode === params.host);
|
||||
return {
|
||||
host: params.host,
|
||||
namespace: runtimeScopeCompositionId || authoredScopeCompositionId || "",
|
||||
styles,
|
||||
isCurrent,
|
||||
runScripts: async () => {
|
||||
if (scriptPayloads.length > 0) await waitForFonts();
|
||||
if (!isCurrent()) return;
|
||||
for (const scriptPayload of scriptPayloads) {
|
||||
const injectedScript = document.createElement("script");
|
||||
if (scriptPayload.type) {
|
||||
injectedScript.type = scriptPayload.type;
|
||||
}
|
||||
// Preserve deterministic script execution order across injected composition scripts.
|
||||
injectedScript.async = false;
|
||||
if (scriptPayload.kind === "external") {
|
||||
injectedScript.src = scriptPayload.src;
|
||||
} else if (scriptPayload.type.toLowerCase() === "importmap") {
|
||||
const map = parseImportMap(scriptPayload.content, (url) =>
|
||||
resolveScriptSourceUrl(url, params.compositionUrl),
|
||||
);
|
||||
injectedScript.textContent = map ? JSON.stringify(map) : scriptPayload.content;
|
||||
} else if (scriptPayload.type.toLowerCase() === "module") {
|
||||
const prelude = scriptPayload.scopeCompositionId
|
||||
? scopedModulePrelude(
|
||||
runtimeScopeCompositionId || scriptPayload.scopeCompositionId,
|
||||
params.compositionUrl?.href,
|
||||
)
|
||||
: "";
|
||||
injectedScript.textContent = prelude + scriptPayload.content;
|
||||
} else if (scriptPayload.scopeCompositionId) {
|
||||
injectedScript.textContent = wrapScopedCompositionScript(
|
||||
scriptPayload.content,
|
||||
scriptPayload.scopeCompositionId,
|
||||
"[HyperFrames] composition script error:",
|
||||
runtimeScopeSelector,
|
||||
runtimeScopeCompositionId || scriptPayload.scopeCompositionId,
|
||||
authoredRootId,
|
||||
params.compositionUrl?.href,
|
||||
)
|
||||
: "";
|
||||
injectedScript.textContent = prelude + scriptPayload.content;
|
||||
} else if (scriptPayload.scopeCompositionId) {
|
||||
injectedScript.textContent = wrapScopedCompositionScript(
|
||||
scriptPayload.content,
|
||||
scriptPayload.scopeCompositionId,
|
||||
"[HyperFrames] composition script error:",
|
||||
runtimeScopeSelector,
|
||||
runtimeScopeCompositionId || scriptPayload.scopeCompositionId,
|
||||
authoredRootId,
|
||||
params.compositionUrl?.href,
|
||||
);
|
||||
} else {
|
||||
injectedScript.textContent = `(function(){${scriptPayload.content}})();`;
|
||||
}
|
||||
document.body.appendChild(injectedScript);
|
||||
params.injectedScripts.push(injectedScript);
|
||||
if (scriptPayload.kind === "external") {
|
||||
const loadResult = await waitForExternalScriptLoad(injectedScript);
|
||||
if (loadResult.status !== "load") {
|
||||
params.onDiagnostic?.({
|
||||
code: "external_composition_script_load_issue",
|
||||
details: {
|
||||
hostCompositionId: params.authoredCompositionId,
|
||||
runtimeCompositionId: params.runtimeCompositionId,
|
||||
hostCompositionSrc: params.hostCompositionSrc,
|
||||
resolvedScriptSrc: scriptPayload.src,
|
||||
loadStatus: loadResult.status,
|
||||
elapsedMs: loadResult.elapsedMs,
|
||||
},
|
||||
});
|
||||
);
|
||||
} else {
|
||||
injectedScript.textContent = `(function(){${scriptPayload.content}})();`;
|
||||
}
|
||||
document.body.appendChild(injectedScript);
|
||||
params.injectedScripts.push(injectedScript);
|
||||
if (scriptPayload.kind === "external") {
|
||||
const loadResult = await waitForExternalScriptLoad(injectedScript);
|
||||
if (loadResult.status !== "load") {
|
||||
params.onDiagnostic?.({
|
||||
code: "external_composition_script_load_issue",
|
||||
details: {
|
||||
hostCompositionId: params.authoredCompositionId,
|
||||
runtimeCompositionId: params.runtimeCompositionId,
|
||||
hostCompositionSrc: params.hostCompositionSrc,
|
||||
resolvedScriptSrc: scriptPayload.src,
|
||||
loadStatus: loadResult.status,
|
||||
elapsedMs: loadResult.elapsedMs,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadInlineTemplateCompositions(
|
||||
params: LoadExternalCompositionsParams,
|
||||
): Promise<void> {
|
||||
async function mountInlineTemplateCompositions(
|
||||
params: LoadCompositionsParams,
|
||||
mountedHosts: ReadonlySet<Element>,
|
||||
): Promise<MountedComposition[]> {
|
||||
const trackedHosts = getTrackedCompositionHosts();
|
||||
cleanupDetachedScopedVariables();
|
||||
if (trackedHosts.length === 0) return;
|
||||
const hostIdentityByElement = assignRuntimeCompositionIds(trackedHosts);
|
||||
if (trackedHosts.length === 0) return [];
|
||||
const hostIdentityByElement = assignRuntimeCompositionIds(trackedHosts, mountedHosts);
|
||||
const hosts = trackedHosts.filter((host) => {
|
||||
if (mountedHosts.has(host)) return false;
|
||||
if (host.hasAttribute("data-composition-src")) return false;
|
||||
if (host.children.length > 0) return false;
|
||||
const compId = hostIdentityByElement.get(host)?.authoredCompositionId;
|
||||
@@ -627,8 +676,7 @@ export async function loadInlineTemplateCompositions(
|
||||
return !!document.querySelector(`template#${CSS.escape(compId)}-template`);
|
||||
});
|
||||
|
||||
if (hosts.length === 0) return;
|
||||
|
||||
const mounted: MountedComposition[] = [];
|
||||
for (const host of hosts) {
|
||||
const hostIdentity = hostIdentityByElement.get(host);
|
||||
const compId = hostIdentity?.authoredCompositionId;
|
||||
@@ -638,7 +686,7 @@ export async function loadInlineTemplateCompositions(
|
||||
)!;
|
||||
|
||||
resetCompositionHost(host);
|
||||
await mountCompositionContent({
|
||||
const composition = await mountCompositionContent({
|
||||
host,
|
||||
authoredCompositionId: compId,
|
||||
runtimeCompositionId: hostIdentity?.runtimeCompositionId || compId,
|
||||
@@ -653,23 +701,24 @@ export async function loadInlineTemplateCompositions(
|
||||
parseDimensionPx: params.parseDimensionPx,
|
||||
onDiagnostic: params.onDiagnostic,
|
||||
});
|
||||
mounted.push(composition);
|
||||
}
|
||||
return mounted;
|
||||
}
|
||||
|
||||
export async function loadExternalCompositions(
|
||||
params: LoadExternalCompositionsParams,
|
||||
): Promise<void> {
|
||||
async function mountExternalCompositions(
|
||||
params: LoadCompositionsParams,
|
||||
): Promise<MountedComposition[]> {
|
||||
const trackedHosts = getTrackedCompositionHosts();
|
||||
cleanupDetachedScopedVariables();
|
||||
if (trackedHosts.length === 0) return;
|
||||
const hostIdentityByElement = assignRuntimeCompositionIds(trackedHosts);
|
||||
if (trackedHosts.length === 0) return [];
|
||||
const hostIdentityByElement = assignRuntimeCompositionIds(trackedHosts, new Set());
|
||||
const hosts = trackedHosts.filter((host) => host.hasAttribute("data-composition-src"));
|
||||
if (hosts.length === 0) return;
|
||||
|
||||
await Promise.all(
|
||||
hosts.map(async (host) => {
|
||||
const mounted = await Promise.all(
|
||||
hosts.map(async (host): Promise<MountedComposition | null> => {
|
||||
const src = host.getAttribute("data-composition-src");
|
||||
if (!src) return;
|
||||
if (!src) return null;
|
||||
const hostIdentity = hostIdentityByElement.get(host);
|
||||
const authoredCompositionId = hostIdentity?.authoredCompositionId || null;
|
||||
const runtimeCompositionId =
|
||||
@@ -681,6 +730,31 @@ export async function loadExternalCompositions(
|
||||
compositionUrl = null;
|
||||
}
|
||||
resetCompositionHost(host);
|
||||
const failed = (error: unknown) => {
|
||||
params.onDiagnostic?.({
|
||||
code: "external_composition_load_failed",
|
||||
details: {
|
||||
hostCompositionId: authoredCompositionId,
|
||||
runtimeCompositionId,
|
||||
hostCompositionSrc: src,
|
||||
errorMessage: error instanceof Error ? error.message : "unknown_error",
|
||||
},
|
||||
});
|
||||
// Keep host empty on load failures to avoid rendering escaped fallback HTML.
|
||||
resetCompositionHost(host);
|
||||
};
|
||||
const mount = async (mountParams: Parameters<typeof mountCompositionContent>[0]) => {
|
||||
const composition = await mountCompositionContent(mountParams);
|
||||
const runScripts = composition.runScripts;
|
||||
composition.runScripts = async () => {
|
||||
try {
|
||||
await runScripts();
|
||||
} catch (error) {
|
||||
failed(error);
|
||||
}
|
||||
};
|
||||
return composition;
|
||||
};
|
||||
try {
|
||||
const localTemplate =
|
||||
authoredCompositionId != null
|
||||
@@ -689,7 +763,7 @@ export async function loadExternalCompositions(
|
||||
)
|
||||
: null;
|
||||
if (localTemplate) {
|
||||
await mountCompositionContent({
|
||||
return await mount({
|
||||
host,
|
||||
authoredCompositionId,
|
||||
runtimeCompositionId,
|
||||
@@ -704,7 +778,6 @@ export async function loadExternalCompositions(
|
||||
parseDimensionPx: params.parseDimensionPx,
|
||||
onDiagnostic: params.onDiagnostic,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const response = await fetch(src);
|
||||
if (!response.ok) {
|
||||
@@ -713,15 +786,7 @@ export async function loadExternalCompositions(
|
||||
const html = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
// Rewrite project-root-traversing (`../`) asset paths against the
|
||||
// sub-composition's URL before extracting any nodes. Without this,
|
||||
// `<video src="../../assets/x.mp4">` authored from
|
||||
// `compositions/frames/scene.html` resolves against the main
|
||||
// document's base (the project preview root) and climbs above it
|
||||
// to 404 — the Studio-preview-vs-render divergence reported by
|
||||
// OSS users. The server-side bundler already does this for the
|
||||
// baked render via `inlineSubCompositions`; this is the runtime
|
||||
// mirror so live preview matches.
|
||||
// Resolve parent-relative assets against the fetched composition before mounting.
|
||||
rewriteSubCompositionAssetPaths(doc, compositionUrl);
|
||||
const template =
|
||||
(authoredCompositionId
|
||||
@@ -730,7 +795,7 @@ export async function loadExternalCompositions(
|
||||
)
|
||||
: null) ?? doc.querySelector<HTMLTemplateElement>("template");
|
||||
const sourceNode = template ? template.content : doc.body;
|
||||
await mountCompositionContent({
|
||||
return await mount({
|
||||
host,
|
||||
authoredCompositionId,
|
||||
runtimeCompositionId,
|
||||
@@ -757,20 +822,101 @@ export async function loadExternalCompositions(
|
||||
onDiagnostic: params.onDiagnostic,
|
||||
});
|
||||
} catch (error) {
|
||||
params.onDiagnostic?.({
|
||||
code: "external_composition_load_failed",
|
||||
details: {
|
||||
hostCompositionId: authoredCompositionId,
|
||||
runtimeCompositionId,
|
||||
hostCompositionSrc: src,
|
||||
errorMessage: error instanceof Error ? error.message : "unknown_error",
|
||||
},
|
||||
});
|
||||
// Keep host empty on load failures to avoid rendering escaped fallback HTML.
|
||||
resetCompositionHost(host);
|
||||
failed(error);
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
return mounted.filter((composition): composition is MountedComposition => composition !== null);
|
||||
}
|
||||
|
||||
/** Finalize authored DOM before scripts observe ids, then retain script-created inline discovery. */
|
||||
export async function loadCompositions(params: LoadCompositionsParams): Promise<void> {
|
||||
const finalizedIds = new Set<Element>();
|
||||
const rootScope: MountedSvgScope = {
|
||||
host: document.documentElement,
|
||||
namespace: "",
|
||||
referenceTarget: "document",
|
||||
styles: [...document.querySelectorAll("style")].map((element) => ({
|
||||
element,
|
||||
authored: element.textContent ?? "",
|
||||
applied: element.textContent ?? "",
|
||||
})),
|
||||
};
|
||||
const external = await mountExternalCompositions(params);
|
||||
const inline = await mountInlineTemplateCompositions(
|
||||
params,
|
||||
new Set(external.map(({ host }) => host)),
|
||||
);
|
||||
const initial = [...external, ...inline];
|
||||
namespaceMountedSvgIds([...initial, rootScope], finalizedIds);
|
||||
await Promise.all(external.map((composition) => composition.runScripts()));
|
||||
|
||||
const activeInline = inline.filter((composition) => {
|
||||
if (composition.isCurrent()) return true;
|
||||
for (const style of composition.styles) style.element.remove();
|
||||
if (window.__hfVariablesByComp) delete window.__hfVariablesByComp[composition.namespace];
|
||||
return false;
|
||||
});
|
||||
const live = [...external.filter((composition) => composition.host.isConnected), ...activeInline];
|
||||
const discovered = await mountInlineTemplateCompositions(
|
||||
params,
|
||||
new Set(live.map(({ host }) => host)),
|
||||
);
|
||||
if (discovered.length) {
|
||||
namespaceMountedSvgIds([...live, ...discovered, rootScope], finalizedIds);
|
||||
}
|
||||
const byHost = new Map(
|
||||
[...activeInline, ...discovered].map((composition) => [composition.host, composition]),
|
||||
);
|
||||
for (const host of getTrackedCompositionHosts()) {
|
||||
const composition = byHost.get(host);
|
||||
if (composition) await composition.runScripts();
|
||||
}
|
||||
// Newly referenced IDs retain the initial-script repair; earlier eligible IDs stay final.
|
||||
const finalScopes = [...live, ...discovered].filter(
|
||||
(composition) => composition.host.isConnected,
|
||||
);
|
||||
namespaceMountedSvgIds([...finalScopes, rootScope], finalizedIds);
|
||||
}
|
||||
|
||||
/** Reuse one finalization set across authored mounting and initial-script discovery. */
|
||||
function namespaceMountedSvgIds(
|
||||
mounted: readonly MountedSvgScope[],
|
||||
finalizedIds: Set<Element>,
|
||||
): void {
|
||||
for (const { styles } of mounted) {
|
||||
for (const style of styles) {
|
||||
const current = style.element.textContent ?? "";
|
||||
if (current !== style.applied) {
|
||||
style.authored = current;
|
||||
style.applied = current;
|
||||
}
|
||||
}
|
||||
}
|
||||
const idMaps = namespaceCollidingSvgIds(
|
||||
document,
|
||||
mounted.map(({ host, namespace, styles, referenceTarget }) => ({
|
||||
root: host,
|
||||
namespace,
|
||||
referenceTarget,
|
||||
exclude: mounted
|
||||
.filter((nested) => nested.host !== host && host.contains(nested.host))
|
||||
.map((nested) => nested.host),
|
||||
cssTexts: styles.map((style) => style.authored),
|
||||
})),
|
||||
finalizedIds,
|
||||
);
|
||||
idMaps.forEach((idMap, index) => {
|
||||
const references = readSvgReferenceAliases(mounted[index]!.host, SVG_REFERENCE_ALIASES_ATTR);
|
||||
if (idMap.size === 0 && references.length === 0) return;
|
||||
for (const style of mounted[index]!.styles) {
|
||||
const rewritten = rewriteSvgIdReferencesInCss(style.authored, idMap, references);
|
||||
if (rewritten !== style.applied) style.element.textContent = rewritten;
|
||||
style.applied = rewritten;
|
||||
}
|
||||
});
|
||||
refreshSvgSelectorAliases();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { refreshSvgSelectorAliases } from "../compiler/svgSelectorAliases";
|
||||
import {
|
||||
initSandboxRuntimeModular,
|
||||
installAuthoredMediaCapture,
|
||||
@@ -65,6 +66,7 @@ function bootstrapHyperframeRuntime(): void {
|
||||
|
||||
// Compiled composition scripts wait for web fonts, so what they measure matches every run.
|
||||
function startAfterCompositionScripts(): void {
|
||||
refreshSvgSelectorAliases();
|
||||
const deferred = Array.from(document.querySelectorAll(AFTER_FONTS_SCRIPTS));
|
||||
if (deferred.length === 0) bootstrapHyperframeRuntime();
|
||||
else void runScriptsAfterFonts(deferred, bootstrapHyperframeRuntime);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// fallow-ignore-file code-duplication complexity
|
||||
import { refreshSvgSelectorAliases } from "../compiler/svgSelectorAliases";
|
||||
import { RUNTIME_FILLER } from "./protocol";
|
||||
import { preloadMedia, releaseMedia, lengthIsAuthored, stopMediaDownload } from "./preloadMedia";
|
||||
import { installRuntimeControlBridge, postRuntimeMessage, setRuntimeProtocolFps } from "./bridge";
|
||||
@@ -65,7 +66,7 @@ import {
|
||||
} from "./compositionLength";
|
||||
import { createRuntimeStartTimeResolver } from "./startResolver";
|
||||
import { createClipTree } from "./clipTree";
|
||||
import { loadExternalCompositions, loadInlineTemplateCompositions } from "./compositionLoader";
|
||||
import { loadCompositions } from "./compositionLoader";
|
||||
import { runScriptsAfterFonts } from "./afterFonts";
|
||||
import {
|
||||
applyCaptionOverrides,
|
||||
@@ -3385,13 +3386,11 @@ export function initSandboxRuntimeModular(): void {
|
||||
});
|
||||
},
|
||||
};
|
||||
void loadExternalCompositions(compositionLoaderParams)
|
||||
.then(() => loadInlineTemplateCompositions(compositionLoaderParams))
|
||||
.finally(() => {
|
||||
externalCompositionsReady = true;
|
||||
void settleSceneDom();
|
||||
maybePublishRenderReady();
|
||||
});
|
||||
void loadCompositions(compositionLoaderParams).finally(() => {
|
||||
externalCompositionsReady = true;
|
||||
void settleSceneDom();
|
||||
maybePublishRenderReady();
|
||||
});
|
||||
} else {
|
||||
// No external/inline compositions to load — apply caption overrides immediately
|
||||
void applyCaptionOverrides();
|
||||
@@ -3666,6 +3665,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
if (host.querySelector(".caption-group")) captionHosts.push(host);
|
||||
}
|
||||
// Run once every host is replaced, so no new script binds to a scene still to be swapped.
|
||||
refreshSvgSelectorAliases();
|
||||
const sceneScripts = swaps.flatMap(({ newParts }) =>
|
||||
newParts
|
||||
.filter((el) => el.tagName === "SCRIPT")
|
||||
@@ -4129,7 +4129,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
maybePublishRenderReady();
|
||||
|
||||
// When the bundler inlines compositions, data-composition-src is removed so
|
||||
// loadExternalCompositions() is skipped. But inline scripts registering child
|
||||
// loadCompositions() is skipped. But inline scripts registering child
|
||||
// timelines in __timelines haven't executed yet (they run in the browser's next
|
||||
// microtask). Defer a rebinding attempt to catch them.
|
||||
if (externalCompositionsReady) {
|
||||
|
||||
+2
@@ -9,6 +9,7 @@ import type { RuntimeColorGradingApi } from "./colorGrading";
|
||||
import type { HyperframePickerApi } from "../inline-scripts/pickerApi";
|
||||
import type { PlayerAPI } from "../core.types";
|
||||
import type { ClipTree } from "./clipTree";
|
||||
import type { SvgSelectorAliases } from "../compiler/svgSelectorAliases";
|
||||
|
||||
type ThreeClockLike = {
|
||||
elapsedTime: number;
|
||||
@@ -49,6 +50,7 @@ declare global {
|
||||
};
|
||||
__clipManifest?: RuntimeTimelineMessage;
|
||||
__clipTree?: ClipTree;
|
||||
__hfSvgSelectorAliases?: SvgSelectorAliases;
|
||||
__hf?: {
|
||||
colorGrading?: RuntimeColorGradingApi;
|
||||
onSwallowed?: (label: string, err: unknown) => void;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ensureSvgSelectorAliases } from "../src/compiler/svgSelectorAliases";
|
||||
|
||||
ensureSvgSelectorAliases();
|
||||
@@ -519,7 +519,7 @@ export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||
// root. `hyperframes check`'s layout audits can't see it: they measure
|
||||
// against the root's own (already-correct) rect, not the body's.
|
||||
//
|
||||
// Sub-compositions are exempt: loadExternalCompositions (packages/core/src/
|
||||
// Sub-compositions are exempt: loadCompositions (packages/core/src/
|
||||
// runtime/compositionLoader.ts) mounts only the matched <template>/<body>
|
||||
// subtree, so a sub-comp's own <html>/<head>/<meta viewport> never reach
|
||||
// the rendering document, even when it's a full standalone document.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { bundleToSingleHtml } from "@hyperframes/core/compiler";
|
||||
import { resolve } from "node:path";
|
||||
import puppeteer, { type Browser, type Page } from "puppeteer";
|
||||
|
||||
@@ -266,6 +268,722 @@ describe("core runtime browser contract", () => {
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it("keeps each loaded sub-composition's SVG clip ids pointing into its own section", async () => {
|
||||
// Figma exports restart clip ids per file, so both scenes declare clip0_1_2..clip2_1_2.
|
||||
const origin = "https://fixture.test/";
|
||||
const icon = (clip: string | null) =>
|
||||
clip
|
||||
? `<svg viewBox="0 0 24 24"><g clip-path="url(#${clip})"><path d="M0 0h24v24H0z"/></g>` +
|
||||
`<defs><clipPath id="${clip}"><rect width="24" height="24" rx="6"/></clipPath></defs></svg>`
|
||||
: `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>`;
|
||||
const clipAt = new Map([
|
||||
[0, "clip0_1_2"],
|
||||
[1, "clip1_1_2"],
|
||||
[15, "clip2_1_2"],
|
||||
]);
|
||||
const deferredLookup = (section: string) => `<script>
|
||||
var svg = document.querySelector("svg");
|
||||
var target = svg.querySelector("clipPath");
|
||||
window.__svgLookups = window.__svgLookups || {};
|
||||
window.__svgLookups[${JSON.stringify(section)}] = function() {
|
||||
return {
|
||||
document: document.querySelector("#clip0_1_2") === target,
|
||||
documentAll: document.querySelectorAll("#clip0_1_2")[0] === target,
|
||||
element: svg.querySelector("#clip0_1_2") === target,
|
||||
elementAll: svg.querySelectorAll("#clip0_1_2")[0] === target,
|
||||
};
|
||||
};
|
||||
</script>`;
|
||||
const scenes: Record<string, string> = {
|
||||
"scenes/intro.html": `<template id="intro-template"><div data-composition-id="intro" data-width="320" data-height="180" data-duration="3">${[...clipAt.values()].map(icon).join("")}${deferredLookup("intro")}</div></template>`,
|
||||
"scenes/grid.html": `<template id="grid-template"><div data-composition-id="grid" data-width="320" data-height="180" data-duration="5">${Array.from(
|
||||
{ length: 16 },
|
||||
(_, i) => `<div class="tile">${icon(clipAt.get(i) ?? null)}</div>`,
|
||||
).join("")}${deferredLookup("grid")}</div></template>`,
|
||||
"index.html": `<!doctype html><html><body>
|
||||
<div data-composition-id="main" data-start="0" data-duration="8" data-width="320" data-height="180">
|
||||
<div id="intro" class="clip" data-composition-id="intro" data-composition-src="scenes/intro.html" data-start="0" data-duration="3" data-track-index="0"></div>
|
||||
<div id="grid" class="clip" data-composition-id="grid" data-composition-src="scenes/grid.html" data-start="3" data-duration="5" data-track-index="0"></div>
|
||||
</div>
|
||||
<script src="${origin}runtime.js"></script></body></html>`,
|
||||
};
|
||||
const scenePage = await browser.newPage();
|
||||
try {
|
||||
await scenePage.setRequestInterception(true);
|
||||
scenePage.on("request", (request) => {
|
||||
const path = request.url().slice(origin.length);
|
||||
if (path === "runtime.js")
|
||||
void request.respond({
|
||||
contentType: "text/javascript",
|
||||
body: readFileSync(RUNTIME_PATH),
|
||||
});
|
||||
else if (scenes[path])
|
||||
void request.respond({ contentType: "text/html", body: scenes[path] });
|
||||
else void request.continue();
|
||||
});
|
||||
await scenePage.goto(`${origin}index.html`);
|
||||
await scenePage.waitForFunction(
|
||||
() => (window as unknown as { __renderReady?: boolean }).__renderReady === true,
|
||||
);
|
||||
|
||||
const strays = await scenePage.evaluate(() =>
|
||||
["intro", "grid"].map((section) => {
|
||||
const host = document.getElementById(section)!;
|
||||
const refs = [...host.querySelectorAll("[clip-path]")].map(
|
||||
(el) => /#([^)"']+)/.exec(el.getAttribute("clip-path")!)![1]!,
|
||||
);
|
||||
return {
|
||||
section,
|
||||
refs: refs.length,
|
||||
outside: refs.filter((id) => !host.contains(document.getElementById(id))),
|
||||
lookups: (
|
||||
window as unknown as {
|
||||
__svgLookups: Record<string, () => Record<string, boolean>>;
|
||||
}
|
||||
).__svgLookups[section]!(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
expect(strays).toEqual([
|
||||
{
|
||||
section: "intro",
|
||||
refs: 3,
|
||||
outside: [],
|
||||
lookups: { document: true, documentAll: true, element: true, elementAll: true },
|
||||
},
|
||||
{
|
||||
section: "grid",
|
||||
refs: 3,
|
||||
outside: [],
|
||||
lookups: { document: true, documentAll: true, element: true, elementAll: true },
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
await scenePage.close();
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
const cssTokenSelectors = [
|
||||
'use[/**/href="#symbol"]',
|
||||
'use[href/**/="#symbol"]',
|
||||
'use[href=/**/"#symbol"]',
|
||||
'use[href="#symbol"/**/i]',
|
||||
'use[href="#symbol"/**/]',
|
||||
'use[href/* ] " */="#symbol"]',
|
||||
String.raw`use[href="#SYMBOL" \69]`,
|
||||
];
|
||||
const invalidCssSelectors = [
|
||||
"[id=1]",
|
||||
"#1",
|
||||
"#-1",
|
||||
'use[hr/**/ef="#symbol"]',
|
||||
"use[href=foo/**/bar]",
|
||||
'use[href="#symbol" s]',
|
||||
'use[xl|href="#symbol"]',
|
||||
];
|
||||
|
||||
const svgReviewCases: Array<{
|
||||
name: string;
|
||||
external: boolean;
|
||||
content: string;
|
||||
script: string;
|
||||
expected: Record<string, string | boolean>;
|
||||
lateHtml?: boolean;
|
||||
}> = [
|
||||
{
|
||||
name: "CSS token grammar and native errors",
|
||||
external: false,
|
||||
content: `<style>${cssTokenSelectors.map((selector, index) => `${selector} { --case-${index}: yes }`).join("\n")}</style><svg><symbol id="symbol"/><path id="1"/><path id="-1"/><use href="#symbol"/><use href="#1"/><use href="#-1"/></svg><div id="1"></div><div id="-1"></div>`,
|
||||
script: `var svg = document.querySelector('svg');
|
||||
var target = svg.querySelector('use');
|
||||
window.__svgReviewProbe = function() {
|
||||
var selectors = ${JSON.stringify(cssTokenSelectors)};
|
||||
var invalid = ${JSON.stringify(invalidCssSelectors)};
|
||||
var native = window.document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||
native.setAttribute('href', '#symbol');
|
||||
function syntaxError(fn) { try { fn(); return false; } catch(error) { return error.name === 'SyntaxError'; } }
|
||||
function bits(values) { return values.map(function(value) { return value ? '1' : '0'; }).join(''); }
|
||||
return {
|
||||
native: bits(selectors.map(function(selector) { return native.matches(selector); })),
|
||||
scoped: bits(selectors.map(function(selector) { return svg.querySelector(selector) === target; })),
|
||||
css: bits(selectors.map(function(_, index) { return getComputedStyle(target).getPropertyValue('--case-' + index).trim() === 'yes'; })),
|
||||
invalidNative: bits(invalid.map(function(selector) { return syntaxError(function() { native.matches(selector); }); })),
|
||||
invalidScoped: bits(invalid.map(function(selector) { return syntaxError(function() { svg.querySelector(selector); }); })),
|
||||
};
|
||||
};`,
|
||||
expected: {
|
||||
native: "1111111",
|
||||
scoped: "1111111",
|
||||
css: "1111111",
|
||||
invalidNative: "1111111",
|
||||
invalidScoped: "1111111",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Declared attribute namespace CSS",
|
||||
external: false,
|
||||
content: `<style>@charset "UTF-8"; @unknown foo; @layer empty; #1 {} @NAMESPACE xl url("http://www.w3.org/1999/xlink"); @namespace ed "urn:editor"; use[xl|href="#symbol"] { opacity: .4 } use[ed|href="#symbol"] { --wrong-namespace: yes } @media all { use[xl|href="#symbol"] { --nested: yes } } @media all { @namespace xl "urn:ignored"; } @namespace xl "urn:late";</style><svg xmlns:xlink="http://www.w3.org/1999/xlink"><symbol id="symbol"/><use xlink:href="#symbol" href="#symbol"/></svg>`,
|
||||
script: `var target = document.querySelector('svg use');
|
||||
target.setAttributeNS('urn:editor', 'ed:href', target.getAttribute('href'));
|
||||
window.__svgReviewProbe = function() {
|
||||
var style = window.document.createElement('style');
|
||||
style.textContent = '@namespace xl url("http://www.w3.org/1999/xlink"); .namespace-native[xl|href="#symbol"] { opacity: .4 }';
|
||||
window.document.head.appendChild(style);
|
||||
var svg = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
var use = window.document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||
use.setAttribute('class', 'namespace-native');
|
||||
use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#symbol');
|
||||
svg.appendChild(use); window.document.body.appendChild(svg);
|
||||
var repaired = Array.from(window.document.styleSheets).flatMap(function(sheet) { return Array.from(sheet.cssRules); })
|
||||
.find(function(rule) { var text = rule.selectorText || ''; return text.indexOf('xl|href') !== -1 && !/namespace-native/.test(text) && /opacity/.test(rule.cssText); });
|
||||
var repairedBefore = repaired.selectorText;
|
||||
window.__hfSvgSelectorAliases.refresh();
|
||||
var stable = repaired.selectorText === repairedBefore;
|
||||
repaired.selectorText = 'svg > use[xl|href="#symbol"]';
|
||||
window.__hfSvgSelectorAliases.refresh();
|
||||
var result = {
|
||||
stable: stable,
|
||||
authorEdit: repaired.selectorText.indexOf('svg > use') === 0,
|
||||
native: getComputedStyle(use).opacity,
|
||||
css: getComputedStyle(target).opacity,
|
||||
nested: getComputedStyle(target).getPropertyValue('--nested').trim(),
|
||||
crossNamespace: getComputedStyle(target).getPropertyValue('--wrong-namespace').trim(),
|
||||
};
|
||||
svg.remove(); style.remove();
|
||||
return result;
|
||||
};`,
|
||||
expected: {
|
||||
stable: true,
|
||||
authorEdit: true,
|
||||
native: "0.4",
|
||||
css: "0.4",
|
||||
nested: "yes",
|
||||
crossNamespace: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Initial script retargets owned styles",
|
||||
external: false,
|
||||
content: `<style>.rebound { clip-path: url(#clip) }</style><svg><clipPath id="clip"/><clipPath id="late"/><g class="rebound"/></svg>`,
|
||||
script: `var svg = document.querySelector('svg');
|
||||
var style = Array.from(window.document.querySelectorAll('style')).find(function(style) { return style.textContent.includes('.rebound'); });
|
||||
style.textContent = style.textContent.replace(/url\\([^)]*\\)/, 'url(#late)');
|
||||
window.__svgReviewProbe = function() {
|
||||
var id = /#([^)]*)/.exec(style.textContent)[1];
|
||||
return { own: window.document.getElementById(id) === svg.querySelector('clipPath:last-of-type') };
|
||||
};`,
|
||||
expected: { own: true },
|
||||
},
|
||||
{
|
||||
name: "Initial script retargets native references",
|
||||
external: false,
|
||||
content: `<svg><clipPath id="clip"/><clipPath id="late"/><g clip-path="url(#clip)"/></svg>`,
|
||||
script: `var svg = document.querySelector('svg');
|
||||
var group = svg.querySelector('g');
|
||||
group.setAttribute('clip-path', 'url(#late)');
|
||||
window.__svgReviewProbe = function() {
|
||||
var id = /#([^)]*)/.exec(group.getAttribute('clip-path'))[1];
|
||||
return { own: window.document.getElementById(id) === svg.querySelector('clipPath:last-of-type') };
|
||||
};`,
|
||||
expected: { own: true },
|
||||
},
|
||||
{
|
||||
name: "CSS string continuations",
|
||||
external: false,
|
||||
content: `<style>use[href="#sym${String.fromCharCode(92, 13, 10)}bol"] { opacity: .4 }</style><svg><symbol id="symbol"/><use href="#symbol"/></svg>`,
|
||||
script: `var svg = document.querySelector("svg");
|
||||
var use = svg.querySelector("use");
|
||||
window.__svgReviewProbe = function() {
|
||||
var endings = [[10], [13], [12], [13, 10]];
|
||||
var selectors = endings.map(function(ending) { return 'use[href="#sym' + String.fromCharCode(92) + String.fromCharCode.apply(null, ending) + 'bol"]'; });
|
||||
return {
|
||||
native: selectors.every(function(selector) { return window.document.querySelector('#intro ' + selector) !== null; }),
|
||||
scoped: selectors.every(function(selector) { return svg.querySelector(selector) === use; }),
|
||||
css: getComputedStyle(use).opacity,
|
||||
invalid: endings.every(function(ending) {
|
||||
try { svg.querySelector('#sym' + String.fromCharCode(92) + String.fromCharCode.apply(null, ending) + 'bol'); return false; }
|
||||
catch (error) { return error.name === 'SyntaxError'; }
|
||||
})
|
||||
};
|
||||
};`,
|
||||
expected: { native: true, scoped: true, css: "0.4", invalid: true },
|
||||
},
|
||||
{
|
||||
name: "Escaped closing bracket attribute values",
|
||||
external: false,
|
||||
content: `<style>${String.raw`use[href=\#sym\]bol]`} { opacity: .4 }</style><svg><symbol id="sym]bol"/><use href="#sym]bol"/></svg><div id="sym]bol"></div>`,
|
||||
script: `var svg = document.querySelector('svg');
|
||||
var target = svg.querySelector('use');
|
||||
window.__svgReviewProbe = function() {
|
||||
var selector = 'use[href=' + String.fromCharCode(92) + '#sym' + String.fromCharCode(92) + ']bol]';
|
||||
var native = window.document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||
native.setAttribute('href', '#sym]bol');
|
||||
return { native: native.matches(selector), scoped: svg.querySelector(selector) === target,
|
||||
css: getComputedStyle(target).opacity };
|
||||
};`,
|
||||
expected: { native: true, scoped: true, css: "0.4" },
|
||||
},
|
||||
{
|
||||
name: "Multiline native reference attributes",
|
||||
external: false,
|
||||
content: `<style>.multiline[style*="url(#clip)"] { opacity: .4 }</style><svg><clipPath id="clip"/><g clip-path="url(#clip)"/></svg><div class="multiline" style="\n clip-path:url(#clip);\n"></div>`,
|
||||
script: `var target = document.querySelector('.multiline');
|
||||
window.__svgReviewProbe = function() {
|
||||
var native = window.document.createElement('div');
|
||||
native.setAttribute('style', String.fromCharCode(10) + 'clip-path:url(#clip);' + String.fromCharCode(10));
|
||||
return { native: native.matches('[style*="url(#clip)"]'),
|
||||
scoped: document.querySelector('.multiline[style*="url(#clip)"]') === target,
|
||||
css: getComputedStyle(target).opacity };
|
||||
};`,
|
||||
expected: { native: true, scoped: true, css: "0.4" },
|
||||
},
|
||||
{
|
||||
name: "HTML attribute name casing",
|
||||
external: false,
|
||||
content: `<style>.html-reference[STYLE*="url(#clip)"] { opacity: .4 }</style><svg><clipPath id="clip"/><g clip-path="url(#clip)"/></svg><div class="html-reference" style="clip-path:url(#clip)"></div>`,
|
||||
script: `var target = document.querySelector('.html-reference');
|
||||
window.__svgReviewProbe = function() {
|
||||
var native = window.document.createElement('div');
|
||||
native.setAttribute('style', 'clip-path:url(#clip)');
|
||||
return { native: native.matches('[STYLE*="url(#clip)"]'),
|
||||
scoped: document.querySelector('.html-reference[STYLE*="url(#clip)"]') === target,
|
||||
css: getComputedStyle(target).opacity };
|
||||
};`,
|
||||
expected: { native: true, scoped: true, css: "0.4" },
|
||||
},
|
||||
{
|
||||
name: "external-only scripts retain authored SVG selectors",
|
||||
external: true,
|
||||
content: `<svg><clipPath id="clip"><rect width="24" height="24"/></clipPath><g clip-path="url(#clip)"></g></svg>`,
|
||||
script: `var host = document.getElementById("grid");
|
||||
var svg = host.querySelector("svg");
|
||||
var target = svg.querySelector("clipPath");
|
||||
window.__svgReviewProbe = function() {
|
||||
return { single: svg.querySelector("#clip") === target,
|
||||
all: svg.querySelectorAll("#clip")[0] === target };
|
||||
};`,
|
||||
expected: { single: true, all: true },
|
||||
},
|
||||
{
|
||||
name: "authored native-reference attribute selectors work in scripts and CSS",
|
||||
external: false,
|
||||
content: `<style>
|
||||
use[href="#symbol"] { opacity: .4; }
|
||||
use.xlink[*|href="#symbol"] { stroke-width: 7px; }
|
||||
g[clip-path="url(#clip)"] { opacity: .6; }
|
||||
</style><svg xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<symbol id="symbol"><rect width="24" height="24"/></symbol><symbol id="Ā"/><use class="unicode" href="#Ā"/>
|
||||
<clipPath id="clip"><rect width="24" height="24"/></clipPath>
|
||||
<use href="#symbol"></use><use class="xlink" xlink:href="#symbol"></use>
|
||||
<g clip-path="url(#clip)"></g><text data-ref="#symbol">label</text>
|
||||
</svg><div id="Ā"></div>`,
|
||||
script: `var svg = document.querySelector("svg");
|
||||
var uses = svg.querySelectorAll("use:not(.unicode)");
|
||||
var clipped = svg.querySelector("g");
|
||||
window.__svgReviewProbe = function() {
|
||||
var result = {
|
||||
operators: ['use[href^="#sym"]', 'use[href$="symbol"]', 'use[href*="symb"]', 'use[href~="#symbol"]', 'use[href|="#symbol"]', 'use[href="#SYMBOL" i]', 'use[href=' + String.fromCharCode(92) + '23 symbol]'].every(function(selector) { return svg.querySelector(selector) === uses[0]; }),
|
||||
unicode: svg.querySelector('use.unicode[href="#ā" i]') === null,
|
||||
href: document.querySelector('use[href="#symbol"]') === uses[0],
|
||||
xlink: svg.querySelector('use.xlink[*|href="#symbol"]') === uses[1],
|
||||
url: svg.querySelector('g[clip-path="url(#clip)"]') === clipped,
|
||||
arbitrary: svg.querySelector('text[data-ref="#symbol"]') !== null,
|
||||
hrefCss: getComputedStyle(uses[0]).opacity,
|
||||
xlinkCss: getComputedStyle(uses[1]).strokeWidth,
|
||||
urlCss: getComputedStyle(clipped).opacity,
|
||||
};
|
||||
uses[0].setAttribute("href", "#unrelated");
|
||||
result.afterChange = svg.querySelector('use[href="#symbol"]') === null;
|
||||
result.afterChangeCss = getComputedStyle(uses[0]).opacity;
|
||||
uses[1].setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", "#unrelated");
|
||||
result.xlinkAfterChange = svg.querySelector('use.xlink[*|href="#symbol"]') === null;
|
||||
result.xlinkAfterChangeCss = getComputedStyle(uses[1]).strokeWidth;
|
||||
return result;
|
||||
};`,
|
||||
expected: {
|
||||
operators: true,
|
||||
unicode: true,
|
||||
href: true,
|
||||
xlink: true,
|
||||
url: true,
|
||||
arbitrary: true,
|
||||
hrefCss: "0.4",
|
||||
xlinkCss: "7px",
|
||||
urlCss: "0.6",
|
||||
afterChange: true,
|
||||
afterChangeCss: "1",
|
||||
xlinkAfterChange: true,
|
||||
xlinkAfterChangeCss: "1px",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "scripts capture the final SVG id before a deferred reference write",
|
||||
external: false,
|
||||
content: `<svg><clipPath id="clip"><rect width="24" height="24"/></clipPath><g clip-path="url(#clip)"></g></svg><div id="clip"></div>`,
|
||||
script: `var svg = document.querySelector("svg");
|
||||
var target = svg.querySelector("clipPath");
|
||||
var capturedId = target.id;
|
||||
window.__svgReviewProbe = function() {
|
||||
svg.querySelector("g").setAttribute("clip-path", "url(#" + capturedId + ")");
|
||||
return { capturedStable: capturedId === target.id,
|
||||
correctTarget: window.document.getElementById(capturedId) === target };
|
||||
};`,
|
||||
expected: { capturedStable: true, correctTarget: true },
|
||||
},
|
||||
{
|
||||
name: "an earlier HTML host finishes mounting before a later SVG script captures its id",
|
||||
external: false,
|
||||
lateHtml: true,
|
||||
content: `<div id="clip">later HTML</div>`,
|
||||
script: `var svg = document.querySelector("svg");
|
||||
var target = svg.querySelector("clipPath");
|
||||
var capturedId = target.id;
|
||||
window.__svgReviewProbe = function() {
|
||||
svg.querySelector("g").setAttribute("clip-path", "url(#" + capturedId + ")");
|
||||
return { capturedStable: capturedId === target.id,
|
||||
correctTarget: window.document.getElementById(capturedId) === target };
|
||||
};`,
|
||||
expected: { capturedStable: true, correctTarget: true },
|
||||
},
|
||||
{
|
||||
name: "initial inline hosts retain borrowed parent SVG references",
|
||||
external: false,
|
||||
content: `<svg><linearGradient id="paint"><stop stop-color="red"/></linearGradient><rect fill="url(#paint)"/></svg>
|
||||
<template id="child-template"><div data-composition-id="child"><style>.borrowed { fill: url(#paint) }</style><svg><rect class="borrowed" fill="url(#paint)"/></svg></div></template>
|
||||
<div data-composition-id="child"></div>`,
|
||||
script: `var target = document.querySelector("linearGradient");
|
||||
window.__svgReviewProbe = function() {
|
||||
var child = document.querySelector(".borrowed");
|
||||
var attr = /#([^)'"]+)/.exec(child.getAttribute("fill"))[1];
|
||||
var css = /#([^)'"]+)/.exec(getComputedStyle(child).fill)[1];
|
||||
return { attribute: window.document.getElementById(attr) === target,
|
||||
css: window.document.getElementById(css) === target };
|
||||
};`,
|
||||
expected: { attribute: true, css: true },
|
||||
},
|
||||
{
|
||||
name: "external scripts still create inline hosts that borrow finalized SVG references",
|
||||
external: false,
|
||||
content: `<svg><linearGradient id="paint"><stop stop-color="red"/></linearGradient><rect fill="url(#paint)"/></svg>`,
|
||||
script: `var target = document.querySelector("linearGradient");
|
||||
var captured = target.id;
|
||||
var template = document.createElement("template");
|
||||
template.id = "dynamic-template";
|
||||
template.innerHTML = '<div data-composition-id="dynamic"><style>.borrowed { fill: url(#paint) }</style><svg><clipPath id="clip"/><g clip-path="url(#clip)"/><rect class="borrowed" fill="url(#paint)"/></svg></div>';
|
||||
var payload = document.createElement("script");
|
||||
payload.textContent = 'window.__dynamicMounted = true;';
|
||||
template.content.appendChild(payload);
|
||||
document.body.appendChild(template);
|
||||
var host = document.createElement("div");
|
||||
host.setAttribute("data-composition-id", "dynamic");
|
||||
document.querySelector("svg").parentElement.appendChild(host);
|
||||
window.__svgReviewProbe = function() {
|
||||
var child = host.querySelector(".borrowed");
|
||||
var attr = /#([^)'"]+)/.exec(child.getAttribute("fill"))[1];
|
||||
var css = /#([^)'"]+)/.exec(getComputedStyle(child).fill)[1];
|
||||
var clip = /#([^)'"]+)/.exec(host.querySelector("g").getAttribute("clip-path"))[1];
|
||||
return { mounted: window.__dynamicMounted === true,
|
||||
capturedStable: captured === target.id,
|
||||
attribute: window.document.getElementById(attr) === target,
|
||||
css: window.document.getElementById(css) === target,
|
||||
ownClip: host.contains(window.document.getElementById(clip)) };
|
||||
};`,
|
||||
expected: { mounted: true, capturedStable: true, attribute: true, css: true, ownClip: true },
|
||||
},
|
||||
{
|
||||
name: "external replacement of an inline host does not run its stale queued script",
|
||||
external: false,
|
||||
content: `<template id="child-template"><div data-composition-id="child"><style>.replacement { color: red }</style><script>window.__staleInlineRan = true;</script><span>old</span></div></template><div data-composition-id="child"></div>`,
|
||||
script: `var child = document.querySelector('[data-composition-id="child"]');
|
||||
child.innerHTML = '<span class="replacement">new</span>';
|
||||
window.__svgReviewProbe = function() {
|
||||
return { replaced: !!child.querySelector(".replacement"), staleRan: window.__staleInlineRan === true,
|
||||
color: getComputedStyle(child.querySelector(".replacement")).color };
|
||||
};`,
|
||||
expected: { replaced: true, staleRan: false, color: "rgb(0, 0, 0)" },
|
||||
},
|
||||
{
|
||||
name: "an inline host emptied by an external script is mounted before its script runs",
|
||||
external: false,
|
||||
content: `<template id="child-template"><div data-composition-id="child"><script>window.__emptyInlineRan = true;</script><span class="restored">restored</span></div></template><div data-composition-id="child"></div>`,
|
||||
script: `var child = document.querySelector('[data-composition-id="child"]');
|
||||
child.innerHTML = '';
|
||||
window.__svgReviewProbe = function() {
|
||||
return { restored: !!child.querySelector(".restored"), ran: window.__emptyInlineRan === true };
|
||||
};`,
|
||||
expected: { restored: true, ran: true },
|
||||
},
|
||||
{
|
||||
name: "a script-created duplicate preserves the first inline instance identity and variables",
|
||||
external: false,
|
||||
content: `<template id="scene-template"><div data-composition-id="scene"><style>.instance-probe { color: red }</style><script>window.__instanceValues = window.__instanceValues || []; window.__instanceValues.push(__hyperframes.getVariables().label);</script><span class="instance-probe">instance</span></div></template><div data-composition-id="scene" data-variable-values='{"label":"first"}'></div>`,
|
||||
script: `var first = document.querySelector('[data-composition-id="scene"]');
|
||||
var second = document.createElement("div");
|
||||
second.setAttribute("data-composition-id", "scene");
|
||||
second.setAttribute("data-variable-values", JSON.stringify({ label: "second" }));
|
||||
first.parentElement.appendChild(second);
|
||||
window.__svgReviewProbe = function() {
|
||||
return { values: (window.__instanceValues || []).join(","),
|
||||
colors: [first, second].every(function(host) { return getComputedStyle(host.querySelector(".instance-probe")).color === "rgb(255, 0, 0)"; }),
|
||||
distinct: first.getAttribute("data-composition-id") !== second.getAttribute("data-composition-id") };
|
||||
};`,
|
||||
expected: { values: "first,second", colors: true, distinct: true },
|
||||
},
|
||||
{
|
||||
name: "a discovered earlier definition preserves existing authored borrowed references",
|
||||
external: false,
|
||||
content: `<style>.borrowed { fill: url(#paint) }</style><svg><rect class="borrowed" fill="url(#paint)"/></svg><template id="local-template"><div data-composition-id="local"><svg><linearGradient id="paint"><stop stop-color="green"/></linearGradient><rect class="owned-paint" fill="url(#paint)"/></svg></div></template><div data-composition-id="local"></div>`,
|
||||
script: `var borrowed = document.querySelector(".borrowed");
|
||||
var local = document.querySelector(".owned-paint");
|
||||
var localTarget = local.parentElement.querySelector("linearGradient");
|
||||
var localId = localTarget.id;
|
||||
var template = document.createElement("template");
|
||||
template.id = "newpaint-template";
|
||||
template.innerHTML = '<div data-composition-id="newpaint"><svg><linearGradient id="paint"><stop stop-color="red"/></linearGradient><rect fill="url(#paint)"/></svg></div>';
|
||||
document.body.appendChild(template);
|
||||
var host = document.createElement("div");
|
||||
host.setAttribute("data-composition-id", "newpaint");
|
||||
var own = document.getElementById("grid");
|
||||
own.parentElement.insertBefore(host, own);
|
||||
window.__svgReviewProbe = function() {
|
||||
var target = host.querySelector("linearGradient");
|
||||
var attr = /#([^)'"]+)/.exec(borrowed.getAttribute("fill"))[1];
|
||||
var css = /#([^)'"]+)/.exec(getComputedStyle(borrowed).fill)[1];
|
||||
return { attribute: window.document.getElementById(attr) === target,
|
||||
css: window.document.getElementById(css) === target,
|
||||
local: localTarget.id === localId && window.document.getElementById(/#([^ )]+)/.exec(local.getAttribute("fill"))[1]) === localTarget };
|
||||
};`,
|
||||
expected: { attribute: true, css: true, local: true },
|
||||
},
|
||||
{
|
||||
name: "root attributes and root CSS retain the original mounted SVG target",
|
||||
external: false,
|
||||
content: `<svg><linearGradient id="paint"><stop stop-color="red"/></linearGradient><rect fill="url(#paint)"/></svg>`,
|
||||
script: `var target = document.querySelector("linearGradient");
|
||||
window.__svgReviewProbe = function() {
|
||||
var borrowed = window.document.querySelector(".root-borrowed");
|
||||
var attr = /#([^)'"]+)/.exec(borrowed.getAttribute("fill"))[1];
|
||||
var css = /#([^)'"]+)/.exec(getComputedStyle(borrowed).fill)[1];
|
||||
return { attribute: window.document.getElementById(attr) === target,
|
||||
css: window.document.getElementById(css) === target,
|
||||
rootId: window.document.querySelector(".outside-paint").id };
|
||||
};`,
|
||||
expected: { attribute: true, css: true, rootId: "paint" },
|
||||
},
|
||||
{
|
||||
name: "first native use of a resource",
|
||||
external: false,
|
||||
content: `<svg><clipPath id="late"/></svg>`,
|
||||
script: `var svg = document.querySelector("svg");
|
||||
var target = svg.querySelector("clipPath");
|
||||
var group = window.document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
group.setAttribute("clip-path", "url(#late)");
|
||||
svg.appendChild(group);
|
||||
window.__svgReviewProbe = function() {
|
||||
var reference = /#([^)'"]+)/.exec(group.getAttribute("clip-path"))[1];
|
||||
return { own: window.document.getElementById(reference) === target };
|
||||
};`,
|
||||
expected: { own: true },
|
||||
},
|
||||
{
|
||||
name: "first native use of a path",
|
||||
external: false,
|
||||
content: `<svg><path id="late-path"/></svg>`,
|
||||
script: `var svg = document.querySelector("svg");
|
||||
var target = svg.querySelector("path");
|
||||
var use = window.document.createElementNS("http://www.w3.org/2000/svg", "use");
|
||||
use.setAttribute("href", "#late-path");
|
||||
svg.appendChild(use);
|
||||
window.__svgReviewProbe = function() {
|
||||
return { own: window.document.getElementById(use.getAttribute("href").slice(1)) === target };
|
||||
};`,
|
||||
expected: { own: true },
|
||||
},
|
||||
{
|
||||
name: "initial script native writes resolve within their already-namespaced instance",
|
||||
external: false,
|
||||
content: `<svg><clipPath id="clip"/><symbol id="symbol"/><g clip-path="url(#clip)"/><use href="#symbol"/></svg>`,
|
||||
script: `var svg = document.querySelector("svg");
|
||||
var clip = svg.querySelector("clipPath");
|
||||
var symbol = svg.querySelector("symbol");
|
||||
var captured = clip.id;
|
||||
svg.querySelector("g").setAttribute("clip-path", "url(#clip)");
|
||||
var use = window.document.createElementNS("http://www.w3.org/2000/svg", "use");
|
||||
use.setAttribute("href", "#symbol");
|
||||
svg.appendChild(use);
|
||||
window.__svgReviewProbe = function() {
|
||||
var clipRef = /#([^)'"]+)/.exec(svg.querySelector("g").getAttribute("clip-path"))[1];
|
||||
return { clip: window.document.getElementById(clipRef) === clip,
|
||||
symbol: window.document.getElementById(use.getAttribute("href").slice(1)) === symbol,
|
||||
stable: captured === clip.id };
|
||||
};`,
|
||||
expected: { clip: true, symbol: true, stable: true },
|
||||
},
|
||||
{
|
||||
name: "ID selectors follow actual renamed IDs and stop matching unrelated writes",
|
||||
external: false,
|
||||
content: `<style>clipPath[id="clip"] { color: red } #clip { opacity: .4 }</style><svg><clipPath id="clip"/><g clip-path="url(#clip)"/></svg>`,
|
||||
script: `var svg = document.querySelector("svg");
|
||||
var target = svg.querySelector("clipPath");
|
||||
window.__svgReviewProbe = function() {
|
||||
var result = { id: svg.querySelector("#clip") === target,
|
||||
attribute: svg.querySelector('[id="clip"]') === target,
|
||||
color: getComputedStyle(target).color, opacity: getComputedStyle(target).opacity };
|
||||
target.id = "unrelated";
|
||||
result.afterId = svg.querySelector("#clip") === null;
|
||||
result.afterAttribute = svg.querySelector('[id="clip"]') === null;
|
||||
result.afterColor = getComputedStyle(target).color;
|
||||
result.afterOpacity = getComputedStyle(target).opacity;
|
||||
return result;
|
||||
};`,
|
||||
expected: {
|
||||
id: true,
|
||||
attribute: true,
|
||||
color: "rgb(255, 0, 0)",
|
||||
opacity: "0.4",
|
||||
afterId: true,
|
||||
afterAttribute: true,
|
||||
afterColor: "rgb(0, 0, 0)",
|
||||
afterOpacity: "1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "root ID rules keep every renamed definition when the first target keeps its ID",
|
||||
external: false,
|
||||
content: `<svg><clipPath id="clip"/><g clip-path="url(#clip)"/></svg><template id="child-template"><div data-composition-id="child"><svg><clipPath id="clip"/><g clip-path="url(#clip)"/></svg></div></template><div data-composition-id="child"></div>`,
|
||||
script: `window.__svgReviewProbe = function() {
|
||||
var definitions = [...window.document.querySelectorAll('clipPath:not([id="late"])')];
|
||||
return { count: String(definitions.length), colors: definitions.every(function(target) {
|
||||
return getComputedStyle(target).stroke === "rgb(0, 128, 0)";
|
||||
}) };
|
||||
};`,
|
||||
expected: { count: "3", colors: true },
|
||||
},
|
||||
{
|
||||
name: "replacement of a script-only inline host discards its empty staged queue",
|
||||
external: false,
|
||||
content: `<template id="empty-template"><script>window.__emptyStale = true;</script></template><div data-composition-id="empty"></div>`,
|
||||
script: `var host = document.querySelector('[data-composition-id="empty"]');
|
||||
host.innerHTML = '<span class="replacement">new</span>';
|
||||
window.__svgReviewProbe = function() { return { stale: window.__emptyStale === true, replaced: !!host.querySelector(".replacement") }; };`,
|
||||
expected: { stale: false, replaced: true },
|
||||
},
|
||||
{
|
||||
name: "initial inline discovery freezes the external owner's composition identity",
|
||||
external: false,
|
||||
content: `<style>.external-probe { color: red }</style><span class="external-probe">external</span><template id="grid-template"><div data-composition-id="grid"><span class="child-probe">child</span></div></template><div data-composition-id="grid"></div>`,
|
||||
script: `window.__svgReviewProbe = function() { return {
|
||||
scoped: !!document.querySelector(".external-probe"),
|
||||
color: getComputedStyle(document.querySelector(".external-probe")).color,
|
||||
child: !!document.querySelector(".child-probe") }; };`,
|
||||
expected: { scoped: true, color: "rgb(255, 0, 0)", child: true },
|
||||
},
|
||||
{
|
||||
name: "CSS id rules keep unchanged HTML and renamed SVG matches",
|
||||
external: false,
|
||||
content: `<style>#fx { color: rgb(255, 0, 0) }</style>
|
||||
<svg><filter id="fx"></filter><rect filter="url(#fx)"/></svg><div id="fx">text</div>`,
|
||||
script: `var svg = document.querySelector("svg");
|
||||
var html = document.querySelector("div#fx");
|
||||
window.__svgReviewProbe = function() {
|
||||
return { htmlColor: getComputedStyle(html).color,
|
||||
svgColor: getComputedStyle(svg.querySelector("filter")).color };
|
||||
};`,
|
||||
expected: { htmlColor: "rgb(255, 0, 0)", svgColor: "rgb(255, 0, 0)" },
|
||||
},
|
||||
];
|
||||
|
||||
it.each(
|
||||
svgReviewCases.flatMap((fixture) => [
|
||||
{ ...fixture, compiled: false },
|
||||
...([
|
||||
"CSS string continuations",
|
||||
"CSS token grammar and native errors",
|
||||
"Declared attribute namespace CSS",
|
||||
"HTML attribute name casing",
|
||||
"Multiline native reference attributes",
|
||||
"Escaped closing bracket attribute values",
|
||||
"external-only scripts retain authored SVG selectors",
|
||||
"authored native-reference attribute selectors work in scripts and CSS",
|
||||
"root attributes and root CSS retain the original mounted SVG target",
|
||||
"root ID rules keep every renamed definition when the first target keeps its ID",
|
||||
"ID selectors follow actual renamed IDs and stop matching unrelated writes",
|
||||
].includes(fixture.name)
|
||||
? [{ ...fixture, compiled: true }]
|
||||
: []),
|
||||
]),
|
||||
)(
|
||||
"SVG revision: $name (compiled=$compiled)",
|
||||
async ({ content, script, external, expected, lateHtml = false, compiled }) => {
|
||||
const origin = "https://svg-review.test/";
|
||||
let gridPayload = `<script>${script}</script>`;
|
||||
if (external) gridPayload = '<script src="scene.js"></script>';
|
||||
if (lateHtml) gridPayload = "";
|
||||
const introHost =
|
||||
'<div id="intro" data-composition-id="intro" data-composition-src="intro.html"></div>';
|
||||
const gridHost =
|
||||
'<div id="grid" data-composition-id="grid" data-composition-src="grid.html"></div>';
|
||||
const hosts = lateHtml ? gridHost + introHost : introHost + gridHost;
|
||||
const assets: Record<string, { contentType: string; body: string | Buffer }> = {
|
||||
"runtime.js": { contentType: "text/javascript", body: readFileSync(RUNTIME_PATH) },
|
||||
"scene.js": { contentType: "text/javascript", body: script },
|
||||
"intro.html": {
|
||||
contentType: "text/html",
|
||||
body: `<template id="intro-template"><div data-composition-id="intro" data-duration="2"><svg><clipPath id="clip"/><clipPath id="late"/><path id="late-path"/><symbol id="symbol"/><g clip-path="url(#clip)"/><use href="#symbol"/></svg>${lateHtml ? `<script>${script}</script>` : ""}</div></template>`,
|
||||
},
|
||||
"grid.html": {
|
||||
contentType: "text/html",
|
||||
body: `<template id="grid-template"><div data-composition-id="grid" data-duration="2">${content}${gridPayload}</div></template>`,
|
||||
},
|
||||
"index.html": {
|
||||
contentType: "text/html",
|
||||
body: `<!doctype html><html><body><div data-composition-id="main" data-duration="2" data-width="320" data-height="180">${hosts}</div><style>.root-borrowed { fill: url(#paint) } #clip { stroke: green }</style><svg><linearGradient class="outside-paint" id="paint"><stop stop-color="blue"/></linearGradient><rect class="root-borrowed" fill="url(#paint)"/></svg><script src="runtime.js"></script></body></html>`,
|
||||
},
|
||||
};
|
||||
let compiledDirectory: string | undefined;
|
||||
if (compiled) {
|
||||
compiledDirectory = mkdtempSync(resolve(tmpdir(), "svg-alias-browser-"));
|
||||
for (const [path, asset] of Object.entries(assets))
|
||||
writeFileSync(resolve(compiledDirectory, path), asset.body);
|
||||
assets["index.html"]!.body = await bundleToSingleHtml(compiledDirectory);
|
||||
}
|
||||
const scenePage = await browser.newPage();
|
||||
try {
|
||||
await scenePage.setRequestInterception(true);
|
||||
let releaseGrid: ((release: () => void) => void) | undefined;
|
||||
const gridResponse = new Promise<() => void>((resolve) => {
|
||||
releaseGrid = resolve;
|
||||
});
|
||||
scenePage.on("request", (request) => {
|
||||
const path = request.url().slice(origin.length);
|
||||
const asset = assets[path];
|
||||
if (lateHtml && path === "grid.html") {
|
||||
releaseGrid!(() => {
|
||||
void request.respond(asset!);
|
||||
});
|
||||
} else if (asset) void request.respond(asset);
|
||||
else void request.continue();
|
||||
});
|
||||
await scenePage.goto(`${origin}index.html`);
|
||||
if (lateHtml) {
|
||||
await scenePage.waitForFunction(() => document.querySelector("#intro clipPath") !== null);
|
||||
(await gridResponse)();
|
||||
}
|
||||
await scenePage.waitForFunction(
|
||||
() => (window as unknown as { __renderReady?: boolean }).__renderReady === true,
|
||||
);
|
||||
const result = await scenePage.evaluate(() =>
|
||||
(window as unknown as { __svgReviewProbe: () => unknown }).__svgReviewProbe(),
|
||||
);
|
||||
expect(result).toEqual(expected);
|
||||
} finally {
|
||||
await scenePage.close();
|
||||
if (compiledDirectory) rmSync(compiledDirectory, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
30_000,
|
||||
);
|
||||
|
||||
it("removes the control bridge during teardown", async () => {
|
||||
const result = await page.evaluate(async () => {
|
||||
const runtimeWindow = window as unknown as {
|
||||
|
||||
@@ -11,12 +11,20 @@ import {
|
||||
// Deep import: the mount path is not part of core's published export map (it is
|
||||
// bundled into the runtime IIFE, not imported by consumers). Same shape as
|
||||
// engine/src/services/videoFrameExtractor.test.ts reaching into core's runtime.
|
||||
import { loadExternalCompositions } from "../../../core/src/runtime/compositionLoader.js";
|
||||
import { loadCompositions } from "../../../core/src/runtime/compositionLoader.js";
|
||||
import { compileForRender } from "./htmlCompiler.js";
|
||||
import { getVerifiedHyperframeRuntimeSource } from "./hyperframeRuntimeLoader.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
const nativeQuerySelector = Element.prototype.querySelector;
|
||||
const nativeQuerySelectorAll = Element.prototype.querySelectorAll;
|
||||
afterEach(() => {
|
||||
Element.prototype.querySelector = nativeQuerySelector;
|
||||
Element.prototype.querySelectorAll = nativeQuerySelectorAll;
|
||||
delete window.__hfSvgSelectorAliases;
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
// The mount path scopes CSS with CSS.escape, which the DOM stub omits.
|
||||
if (typeof globalThis.CSS === "undefined") {
|
||||
@@ -66,7 +74,7 @@ async function mountContract(dir: string, indexHtml: string): Promise<ParityCont
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(async (input) =>
|
||||
Promise.resolve(new Response(readFileSync(join(dir, String(input)), "utf8"), { status: 200 })),
|
||||
);
|
||||
await loadExternalCompositions({
|
||||
await loadCompositions({
|
||||
injectedStyles: [],
|
||||
injectedScripts: [],
|
||||
injectedLinks: [],
|
||||
@@ -413,11 +421,10 @@ const subCompositions = (files: Record<string, string>) =>
|
||||
*
|
||||
* - `runtimeBootstrap` / `variableBootstrap` — the runtime IIFE and the variable
|
||||
* bootstrap script are injected by the player and the producer AROUND a mount,
|
||||
* never by `loadExternalCompositions`. Comparing them would compare harnesses.
|
||||
* never by `loadCompositions`. Comparing them would compare harnesses.
|
||||
*
|
||||
* Nothing else is excluded. The templated-head-`<link>` divergence this file
|
||||
* used to carve out is closed and gated by a fixture above; so is the
|
||||
* anonymous-host one. Two divergences remain ungated HERE rather than
|
||||
* Nothing else is excluded. The templated-head-`<link>` and anonymous-host
|
||||
* divergences are gated by fixtures above. Two divergences remain ungated HERE rather than
|
||||
* unfixed — an inline `<head>` script and the split between the CSS scope id
|
||||
* and the script scope id both live in script bodies, and this contract
|
||||
* carries no script-body field. Their gates are the unit suites in
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk5L7W0Q5n-Z4sX78MLpiqZCmvMf9UxqhNKllH1cNgx0bTsIBOBs7SNX-j4s4ylNm2UCwCZNIPBLj_BHVI3u89wkVczLFGLd6G0t36lHOtbPXenGd4StCfEyMjMdBkkEqHiUdbEm-sNOJof6ThkzLT3FAzwFJXA&skey=e5dfd0c3910c7ec9&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk5L7W0Q5n-Z4sX78MLpiqZCmvMf9UxqhNKllH1cNgx0bTsIBOBs7SNX-j4s4ylNm2UCwCZNIPBLj_BHVI3u89wkVczLFGLd6G0t36lHOtbPXenGd4StCfEyMjMdBkkEqHiUdbEm-sNOJof6ThkzLT3FAzwFJXA&skey=e5dfd0c3910c7ec9&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD8t0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4jxkhSpeRcu3GQ&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD8t0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4jxkhSpeRcu3GQ&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD8t0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4jxkhSpeRcu3GQ&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD8t0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4jxkhSpeRcu3GQ&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD8t0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4jxkhSpeRcu3GQ&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD8t0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4jxkhSpeRcu3GQ&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD8t0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4jxkhSpeRcu3GQ&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD8t0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4jxkhSpeRcu3GQ&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD8t0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4jxkhSpeRcu3GQ&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk5L7W0Q5n-ZxsX78MLpiqZCmvMf9UxqhNKllH1cNgx0bTsIBOBs7SNX-j4s4ylNm2UCwCZNIPBLj_BHVI3u89wkVczLFGLd6G0t36lHOtbPXenGd4StCfEyMjMdBkkEqHiUdbEm-sNOJof6Thg&skey=e5dfd0c3910c7ec9&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk5L7W0Q5n-ZxsX78MLpiqZCmvMf9UxqhNKllH1cNgx0bTsIBOBs7SNX-j4s4ylNm2UCwCZNIPBLj_BHVI3u89wkVczLFGLd6G0t36lHOtbPXenGd4StCfEyMjMdBkkEqHiUdbEm-sNOJof6Thg&skey=e5dfd0c3910c7ec9&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-90fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4g&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-90fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4g&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-90fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4g&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-90fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4g&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-90fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4g&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-90fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4g&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-90fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4g&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-90fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4g&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-90fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4g&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,88 @@
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmUBHMdazTqPu8GDmixiyv7sHsO9kUd_1ZZwvfxd8PedhYMCPK-7KsgrD_GTTHR6jtWpE1QSC-n0tGj5Y0Iea5lPneAWMuTqRLj9_emvoF8BoqCq3MRviSSf0DASiOS6kszpAyy8pOPJOwPd1c&skey=c608c610063635f9&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmUBHMdazTqPu8GDmixiyv7sHsO9kUd_1ZZwvfxd8PedhYMCPK-7KsgrD_GTTHR6jtWpE1QSC-n0tGj5Y0Iea5lPneAWMuTqRLj9_emvoF8BoqCq3MRviSSf0DASiOS6kszpAyy8pOPJOwPd1c&skey=c608c610063635f9&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVDRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVDRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVDRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVDRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVDRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVDRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVDRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVDRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVDRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=tDbp2o-flEEny0FZhsfKu5WU4xD-IQ-PuZJJXxfpAO-LfjGbsVNLG7DGdF6OZ1Pcwgknk-6tesCuTUT9YD4b2KmIZSE1FNq_PdUTe-vhgNSBv5wANctW9XhQvuMvrqbZ4ujEG3DkCVl51v9aNp3Rr_aV3gtb0mHrFFQra5onpxICOnxkknmsw6RLuooi33FCy6LBrS7g4a_qRL8O8GTwUkRLlR8&skey=74ed26de24979d51&v=v24) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=tDbp2o-flEEny0FZhsfKu5WU4xD-IQ-PuZJJXxfpAO-LfjGbsVNLG7DGdF6OZ1Pcwgknk-6tesCuTUT9YD4b2KmIZSE1FNq_PdUTe-vhgNSBv5wANctW9XhQvuMvrqbZ4ujEG3DkCVl51v9aNp3Rr_aV3gtb0mHrFFQra5onpxICOnxkknmsw6RLuooi33FCy6LBrS7g4a_qRL8O8GTwUkRLlR8&skey=74ed26de24979d51&v=v24) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=tDbV2o-flEEny0FZhsfKu5WU4yD5MQaPTXC57RrcIc1gGVa2no-yrCDtTHChHybO9BcQrdmnLvOfUVjhfFp_vM3kCU1ZcL7bWalvB5eF5LDl0_BsWa8ykRxMov8zioL9xsToN1zALX1d6sNmCrn1i9K58id39kXPMEg3dxvDVpbl6PeCffdWKg&skey=48ad01c60053c2ae&v=v24) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=tDbV2o-flEEny0FZhsfKu5WU4yD5MQaPTXC57RrcIc1gGVa2no-yrCDtTHChHybO9BcQrdmnLvOfUVjhfFp_vM3kCU1ZcL7bWalvB5eF5LDl0_BsWa8ykRxMov8zioL9xsToN1zALX1d6sNmCrn1i9K58id39kXPMEg3dxvDVpbl6PeCffdWKg&skey=48ad01c60053c2ae&v=v24) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=tDbV2o-flEEny0FZhsfKu5WU4yD5MQaPTXC57RrcIc1gGVa2no-yrCDtTHChHybO9BcQrdmnLvOfUVjhfFp_vM3kCU1ZcL7bWalvB5eF5LDl0_BsWa8ykRxMov8zioL9xsToN1zALX1d6sNmCrn1i9K58id39kXPMEg3dxvDVpbl6PeCffdWKg&skey=48ad01c60053c2ae&v=v24) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=tDbV2o-flEEny0FZhsfKu5WU4yD5MQaPTXC57RrcIc1gGVa2no-yrCDtTHChHybO9BcQrdmnLvOfUVjhfFp_vM3kCU1ZcL7bWalvB5eF5LDl0_BsWa8ykRxMov8zioL9xsToN1zALX1d6sNmCrn1i9K58id39kXPMEg3dxvDVpbl6PeCffdWKg&skey=48ad01c60053c2ae&v=v24) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=tDbV2o-flEEny0FZhsfKu5WU4yD5MQaPTXC57RrcIc1gGVa2no-yrCDtTHChHybO9BcQrdmnLvOfUVjhfFp_vM3kCU1ZcL7bWalvB5eF5LDl0_BsWa8ykRxMov8zioL9xsToN1zALX1d6sNmCrn1i9K58id39kXPMEg3dxvDVpbl6PeCffdWKg&skey=48ad01c60053c2ae&v=v24) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=tDbV2o-flEEny0FZhsfKu5WU4yD5MQaPTXC57RrcIc1gGVa2no-yrCDtTHChHybO9BcQrdmnLvOfUVjhfFp_vM3kCU1ZcL7bWalvB5eF5LDl0_BsWa8ykRxMov8zioL9xsToN1zALX1d6sNmCrn1i9K58id39kXPMEg3dxvDVpbl6PeCffdWKg&skey=48ad01c60053c2ae&v=v24) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=tDbV2o-flEEny0FZhsfKu5WU4yD5MQaPTXC57RrcIc1gGVa2no-yrCDtTHChHybO9BcQrdmnLvOfUVjhfFp_vM3kCU1ZcL7bWalvB5eF5LDl0_BsWa8ykRxMov8zioL9xsToN1zALX1d6sNmCrn1i9K58id39kXPMEg3dxvDVpbl6PeCffdWKg&skey=48ad01c60053c2ae&v=v24) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=tDbV2o-flEEny0FZhsfKu5WU4yD5MQaPTXC57RrcIc1gGVa2no-yrCDtTHChHybO9BcQrdmnLvOfUVjhfFp_vM3kCU1ZcL7bWalvB5eF5LDl0_BsWa8ykRxMov8zioL9xsToN1zALX1d6sNmCrn1i9K58id39kXPMEg3dxvDVpbl6PeCffdWKg&skey=48ad01c60053c2ae&v=v24) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,88 @@
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmUBHMdazTqN-8GDmixiyv7sHsO9kUd_1ZZwvfxd8PedhYMCPK-7KsgrD_GTTHR6jtWpE1QSC-n0tGj5Y0Iea5lPneAWMuTqRLj9_emvoF8BoqCq3MRviSSf0DASiOS6kszpAyy8pOPJOwPd1dN0IOI3ReiouI&skey=c608c610063635f9&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmUBHMdazTqN-8GDmixiyv7sHsO9kUd_1ZZwvfxd8PedhYMCPK-7KsgrD_GTTHR6jtWpE1QSC-n0tGj5Y0Iea5lPneAWMuTqRLj9_emvoF8BoqCq3MRviSSf0DASiOS6kszpAyy8pOPJOwPd1dN0IOI3ReiouI&skey=c608c610063635f9&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVnRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5FUMVsrXuswTH&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVnRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5FUMVsrXuswTH&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVnRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5FUMVsrXuswTH&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVnRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5FUMVsrXuswTH&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVnRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5FUMVsrXuswTH&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVnRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5FUMVsrXuswTH&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVnRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5FUMVsrXuswTH&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVnRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5FUMVsrXuswTH&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-stretch: 100%;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVnRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5FUMVsrXuswTH&skey=a0a0114a1dcab3ac&v=v51) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+2014, U+203a, U+2304, U+25c7;
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,77 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk5L7W0Q5n-ZzsX78MLpiqZCmvMf9UxqhNKllH1cNgx0bTsIBOBs7SNX-j4s4ylNm2UCwCZNIPBLj_BHVI3u89wkVczLFGLd6G0t36lHOtbPXenGd4StCfEyMjMdBkkEqHiUdbEm-sNOJod6k8C7f&skey=e5dfd0c3910c7ec9&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+b7, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk5L7W0Q5n-ZzsX78MLpiqZCmvMf9UxqhNKllH1cNgx0bTsIBOBs7SNX-j4s4ylNm2UCwCZNIPBLj_BHVI3u89wkVczLFGLd6G0t36lHOtbPXenGd4StCfEyMjMdBkkEqHiUdbEm-sNOJod6k8C7f&skey=e5dfd0c3910c7ec9&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+b7, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-d0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttE2aP6Thg&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+b7, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-d0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttE2aP6Thg&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+b7, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-d0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttE2aP6Thg&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+b7, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-d0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttE2aP6Thg&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+b7, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-d0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttE2aP6Thg&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+b7, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-d0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttE2aP6Thg&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+b7, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-d0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttE2aP6Thg&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+b7, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-d0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttE2aP6Thg&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+b7, U+2014;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url(https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-d0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttE2aP6Thg&skey=c491285d6722e4fa&v=v20) format('woff2');
|
||||
unicode-range: U+9-a, U+20-7e, U+b7, U+2014;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,124 +4,114 @@
|
||||
"contentType": "application/javascript; charset=utf-8",
|
||||
"file": "c174bfce53a729418d57a8ad.bin"
|
||||
},
|
||||
"https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23%5B%5D()6zZqQ'75%25_%7C%2B%5C%3F%5E%26%E2%80%94%60": {
|
||||
"https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23%5B%5D()6zZqQ'75%25_%7C%3F%5C%09%60%5E%26%24%2B~%E2%80%94": {
|
||||
"status": 200,
|
||||
"contentType": "text/css; charset=utf-8",
|
||||
"file": "21363c70fe0f5ada0387fc10.bin"
|
||||
"file": "13ea284e2ea4ac4ab37c98e5.bin"
|
||||
},
|
||||
"https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23%5B%5DzZ(5)6%257%3F_%7C%2B%5C'%5E%26qQ%E2%80%94": {
|
||||
"https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23%5B%5DzZ(5)6%257%3F_%7C%5C%09%60%5E%26%24%2B'Qq~%E2%80%94": {
|
||||
"status": 200,
|
||||
"contentType": "text/css; charset=utf-8",
|
||||
"file": "41acfa6f109d3a5946f742d9.bin"
|
||||
"file": "13ea284e2ea4ac4ab37c98e5.bin"
|
||||
},
|
||||
"https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23zZ%5B%5D()765%25%60_%E2%8C%84%E2%97%87%E2%80%BA%7C%2B%5C%3F'%5E%26qQ%E2%80%94": {
|
||||
"https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23zZ%5B%5D()765%25%60_%E2%8C%84%E2%97%87%E2%80%BA%7C%3F%5C%09%5E%26%24%2B'Qq~%E2%80%94": {
|
||||
"status": 200,
|
||||
"contentType": "text/css; charset=utf-8",
|
||||
"file": "d46936f54ebde49ebb44d3a9.bin"
|
||||
"file": "0529bdbdcf740f78d5b9e331.bin"
|
||||
},
|
||||
"https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23zZ%5B6%5D()75%25%60_%E2%8C%84%E2%97%87%E2%80%BA%7C%2B%5C%3F'%5E%26qQ%E2%80%94": {
|
||||
"https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23zZ%5B6%5D()75%25%60_%E2%8C%84%E2%97%87%E2%80%BA%7C%3F%5C%09%5E%26%24%2B'Qq~%E2%80%94": {
|
||||
"status": 200,
|
||||
"contentType": "text/css; charset=utf-8",
|
||||
"file": "d46936f54ebde49ebb44d3a9.bin"
|
||||
"file": "0529bdbdcf740f78d5b9e331.bin"
|
||||
},
|
||||
"https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034zZ%3B%23kK5%25%5B%5D()76%C2%B7_%7C%2B%5C%3F'%5E%26qQ%E2%80%94": {
|
||||
"https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034zZ%3B%23kK5%25%5B%5D()76%C2%B7_%7C%3F%5C%09%60%5E%26%24%2B'Qq~%E2%80%94": {
|
||||
"status": 200,
|
||||
"contentType": "text/css; charset=utf-8",
|
||||
"file": "73f0f6259c8c64e613d401f4.bin"
|
||||
"file": "bed471e981d73b3db0c86a41.bin"
|
||||
},
|
||||
"https://fonts.googleapis.com/css2?family=JetBrains%20Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23zZ%5B%5D()765%25%60_%E2%8C%84%E2%97%87%E2%80%BA%7C%2B%5C%3F'%5E%26qQ%E2%80%94": {
|
||||
"https://fonts.googleapis.com/css2?family=JetBrains%20Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23zZ%5B%5D()765%25%60_%E2%8C%84%E2%97%87%E2%80%BA%7C%3F%5C%09%5E%26%24%2B'Qq~%E2%80%94": {
|
||||
"status": 200,
|
||||
"contentType": "text/css; charset=utf-8",
|
||||
"file": "c01c545a99d9d5e629e4e793.bin"
|
||||
"file": "89a83af9f5863c5898d0ba50.bin"
|
||||
},
|
||||
"https://fonts.googleapis.com/css2?family=JetBrains%20Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23zZ%5B6%5D()75%25%60_%E2%8C%84%E2%97%87%E2%80%BA%7C%2B%5C%3F'%5E%26qQ%E2%80%94": {
|
||||
"https://fonts.googleapis.com/css2?family=JetBrains%20Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23zZ%5B6%5D()75%25%60_%E2%8C%84%E2%97%87%E2%80%BA%7C%3F%5C%09%5E%26%24%2B'Qq~%E2%80%94": {
|
||||
"status": 200,
|
||||
"contentType": "text/css; charset=utf-8",
|
||||
"file": "c01c545a99d9d5e629e4e793.bin"
|
||||
"file": "89a83af9f5863c5898d0ba50.bin"
|
||||
},
|
||||
"https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23%5B%5DzZ(5)6%257%3F_%7C%2B%5C'%5E%26qQ%E2%80%94": {
|
||||
"https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23%5B%5DzZ(5)6%257%3F_%7C%5C%09%60%5E%26%24%2B'Qq~%E2%80%94": {
|
||||
"status": 200,
|
||||
"contentType": "text/css; charset=utf-8",
|
||||
"file": "4319419b25fdd00a2b54650a.bin"
|
||||
"file": "852b31c7d0aecc0f1843bc09.bin"
|
||||
},
|
||||
"https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23zZ%5B%5D()765%25%60_%E2%8C%84%E2%97%87%E2%80%BA%7C%2B%5C%3F'%5E%26qQ%E2%80%94": {
|
||||
"https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23zZ%5B%5D()765%25%60_%E2%8C%84%E2%97%87%E2%80%BA%7C%3F%5C%09%5E%26%24%2B'Qq~%E2%80%94": {
|
||||
"status": 200,
|
||||
"contentType": "text/css; charset=utf-8",
|
||||
"file": "30d4b72c02fd3924be31f70d.bin"
|
||||
"file": "ae13656ef251eb2b9f869407.bin"
|
||||
},
|
||||
"https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23zZ%5B6%5D()75%25%60_%E2%8C%84%E2%97%87%E2%80%BA%7C%2B%5C%3F'%5E%26qQ%E2%80%94": {
|
||||
"https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,700&text=%3C!DdOoCcTtYyPpEe%20hHmMlL%3E%0AaAnNgG%3D%22sS-rRfFxXiIuU%2CbB*%7B%3A%7D%2F8vVwW1920.jJ%4034%3BkK%23zZ%5B6%5D()75%25%60_%E2%8C%84%E2%97%87%E2%80%BA%7C%3F%5C%09%5E%26%24%2B'Qq~%E2%80%94": {
|
||||
"status": 200,
|
||||
"contentType": "text/css; charset=utf-8",
|
||||
"file": "30d4b72c02fd3924be31f70d.bin"
|
||||
"file": "ae13656ef251eb2b9f869407.bin"
|
||||
},
|
||||
"https://fonts.gstatic.com/l/font?kit=KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmUBHMdazTqMOwsD2uwjSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JUR2QYPd3lN3L2IxhQ&skey=c608c610063635f9&v=v51": {
|
||||
"https://fonts.gstatic.com/l/font?kit=KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmUBHMdazTqN-8GDmixiyv7sHsO9kUd_1ZZwvfxd8PedhYMCPK-7KsgrD_GTTHR6jtWpE1QSC-n0tGj5Y0Iea5lPneAWMuTqRLj9_emvoF8BoqCq3MRviSSf0DASiOS6kszpAyy8pOPJOwPd1dN0IOI3ReiouI&skey=c608c610063635f9&v=v51": {
|
||||
"status": 200,
|
||||
"contentType": "font/woff2",
|
||||
"file": "95061b23401efc9ba7409666.bin"
|
||||
"file": "33e36a8542f94c359351e55c.bin"
|
||||
},
|
||||
"https://fonts.gstatic.com/l/font?kit=KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmUBHMdazTqOuwsD2uwjSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDcyvSjuYR_C4WPqHYWuyePYF3DTySX6UY8qQ-39QpzzQ&skey=c608c610063635f9&v=v51": {
|
||||
"https://fonts.gstatic.com/l/font?kit=KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmUBHMdazTqPu8GDmixiyv7sHsO9kUd_1ZZwvfxd8PedhYMCPK-7KsgrD_GTTHR6jtWpE1QSC-n0tGj5Y0Iea5lPneAWMuTqRLj9_emvoF8BoqCq3MRviSSf0DASiOS6kszpAyy8pOPJOwPd1c&skey=c608c610063635f9&v=v51": {
|
||||
"status": 200,
|
||||
"contentType": "font/woff2",
|
||||
"file": "36535127950d69fb03da1a51.bin"
|
||||
"file": "fa10510792f83e658fd729ad.bin"
|
||||
},
|
||||
"https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUV7SescuDWy1jyf3vHcK8kEZ40pF3vP1c8fSehoADPa66Ndc0EPCSTXV5jdaqElUTCu7zs2_4YkMfaJpMnuEXM-XlS7fy_OiuoVwCoaOr3cVujiOY1zETieW5kc_qAi2amhnuxJXFU8rsq7t&skey=a0a0114a1dcab3ac&v=v51": {
|
||||
"status": 200,
|
||||
"contentType": "font/woff2",
|
||||
"file": "e3c9ceb1da81254fab3982d1.bin"
|
||||
},
|
||||
"https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVTSescuDWy1jyf3vHcK8kEZ40pF3vP1c8fSehoADPa66Ndc0EPCSTXV5jdaqElUTCu7zs2_4YkMfaJpMnuEXM-XlC3ezPKhu4pxCYeJrnQUpTmNYlvFTSaZ50Q-rwkpCHw&skey=a0a0114a1dcab3ac&v=v51": {
|
||||
"https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVDRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5&skey=a0a0114a1dcab3ac&v=v51": {
|
||||
"status": 200,
|
||||
"contentType": "text/html; charset=utf-8",
|
||||
"file": "788a403350b85d98aec587a3.bin"
|
||||
"file": "b7ee14896537ab8f9ffaa90b.bin"
|
||||
},
|
||||
"https://fonts.gstatic.com/l/font?kit=UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk5L7W0Q5n-Z0slT9M7tkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4tk10Sw&skey=e5dfd0c3910c7ec9&v=v20": {
|
||||
"https://fonts.gstatic.com/l/font?kit=KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMW36eA1EfUVnRUMYtDGq3jSn5vnUM9EMb_VRH3PXzccXcdBgCCvC46qki0kHETzfX6DlYqk9STiml0M-9548Of6xnMHmCWs2VqxDdyfWkuId-BISMqXEXuCaQYV7CSCWU6Ek9qg6w9JWNuxJ5FUMVsrXuswTH&skey=a0a0114a1dcab3ac&v=v51": {
|
||||
"status": 200,
|
||||
"contentType": "font/woff2",
|
||||
"file": "f664c5b9216de5d0d4ece5a0.bin"
|
||||
"file": "bf2af2f207d8289f5d95f4ba.bin"
|
||||
},
|
||||
"https://fonts.gstatic.com/l/font?kit=UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk5L7W0Q5n-Z1slT9M7tkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXRK6VTJsLDadXye5CxHf1GTkcRElUQpEyoQb0y5KS9g&skey=e5dfd0c3910c7ec9&v=v20": {
|
||||
"https://fonts.gstatic.com/l/font?kit=UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk5L7W0Q5n-Z4sX78MLpiqZCmvMf9UxqhNKllH1cNgx0bTsIBOBs7SNX-j4s4ylNm2UCwCZNIPBLj_BHVI3u89wkVczLFGLd6G0t36lHOtbPXenGd4StCfEyMjMdBkkEqHiUdbEm-sNOJof6ThkzLT3FAzwFJXA&skey=e5dfd0c3910c7ec9&v=v20": {
|
||||
"status": 200,
|
||||
"contentType": "font/woff2",
|
||||
"file": "68c23b7290680c2b6ef37032.bin"
|
||||
},
|
||||
"https://fonts.gstatic.com/l/font?kit=UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk5L7W0Q5n-ZxsX78MLpiqZCmvMf9UxqhNKllH1cNgx0bTsIBOBs7SNX-j4s4ylNm2UCwCZNIPBLj_BHVI3u89wkVczLFGLd6G0t36lHOtbPXenGd4StCfEyMjMdBkkEqHiUdbEm-sNOJof6Thg&skey=e5dfd0c3910c7ec9&v=v20": {
|
||||
"status": 200,
|
||||
"contentType": "font/woff2",
|
||||
"file": "d3b6c159f462183ba7796270.bin"
|
||||
},
|
||||
"https://fonts.gstatic.com/l/font?kit=UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk5L7W0Q5n-ZzsX78MLpiqZCmvMf9UxqhNKllH1cNgx0bTsIBOBs7SNX-j4s4ylNm2UCwCZNIPBLj_BHVI3u89wkVczLFGLd6G0t36lHOtbPXenGd4StCfEyMjMdBkkEqHiUdbEm-sNOJod6k8C7f&skey=e5dfd0c3910c7ec9&v=v20": {
|
||||
"status": 200,
|
||||
"contentType": "text/html; charset=utf-8",
|
||||
"file": "b13fa08305318ea628453cec.bin"
|
||||
"file": "9fca1e9095ed294968aa18f7.bin"
|
||||
},
|
||||
"https://fonts.gstatic.com/l/font?kit=UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk5L7W0Q5n-Z3slT9M7tkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXRK6VTJsLDadXye5CxHf1GTkcRElUQpEyoQb0y5CRgWX4g&skey=e5dfd0c3910c7ec9&v=v20": {
|
||||
"status": 200,
|
||||
"contentType": "font/woff2",
|
||||
"file": "2427868cae6e8cf32d2e8d8b.bin"
|
||||
},
|
||||
"https://fonts.gstatic.com/l/font?kit=UcC53FwrK3iLTcvneQg7Ca725JhhKnNqk5L7W0Q5n-Z_slT9M7tkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4tk10S_6TqEzHcXFbzA&skey=e5dfd0c3910c7ec9&v=v20": {
|
||||
"status": 200,
|
||||
"contentType": "font/woff2",
|
||||
"file": "ae40a5f8f9f604f443e21213.bin"
|
||||
},
|
||||
"https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD9d41mVb_NL5mpZyqsMP5Vx69KLV5G1MJhxEXQs4FPB8_NKmC8488zldq1Uy8DZdMOA7_4A3RJ3-4-wUZfzbBHLNGJ3dL7lXKsb_bdn2Z5S9GYFCQkMNFlkUmEikRaE0mSzsWX6bxniqpYhQ&skey=c491285d6722e4fa&v=v20": {
|
||||
"status": 200,
|
||||
"contentType": "font/woff2",
|
||||
"file": "42e5ad0b3cee71d5f01953d8.bin"
|
||||
},
|
||||
"https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD_941mVb_NL5mpZyqsMP5Vx69KLV5G1MJhxEXQs4FPB8_NKmC8488zldq1Uy8DZdMOA7_4A3RJ3-4-wUZfzbBHLNHJHZM71bLvr7Yd3qY5i5ZYVORl8JGl0onESgWadJEXw&skey=c491285d6722e4fa&v=v20": {
|
||||
"https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-90fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4g&skey=c491285d6722e4fa&v=v20": {
|
||||
"status": 200,
|
||||
"contentType": "text/html; charset=utf-8",
|
||||
"file": "5905d90fd449470dac1b291d.bin"
|
||||
"file": "8bfba0d6e85d9f059d9b5982.bin"
|
||||
},
|
||||
"https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD_d41mVb_NL5mpZyqsMP5Vx69KLV5G1MJhxEXQs4FPB8_NKmC8488zldq1Uy8DZdMOA7_4A3RJ3-4-wUZfzbBHLNHJHZM71bLvr7Yd3qY5i5ZYVORl8JGl0onESgWafJzKS9g&skey=c491285d6722e4fa&v=v20": {
|
||||
"https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD-d0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttE2aP6Thg&skey=c491285d6722e4fa&v=v20": {
|
||||
"status": 200,
|
||||
"contentType": "font/woff2",
|
||||
"file": "e59d60b150022666efbc6da6.bin"
|
||||
"file": "57dacd2ef96ca543d3e0e6a3.bin"
|
||||
},
|
||||
"https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD_t41mVb_NL5mpZyqsMP5Vx69KLV5G1MJhxEXQs4FPB8_NKmC8488zldq1Uy8DZdMOA7_4A3RJ3-4-wUZfzbBHLNGJ3dL7lXKsb_bdn2Z5S9GYFCQkMNFlkUmEikRaE0mSzs&skey=c491285d6722e4fa&v=v20": {
|
||||
"https://fonts.gstatic.com/l/font?kit=UcC73FwrK3iLTeHuS_nVMrMxCp50SjIq15j8eUYD8t0fmFX-Mrxkq5KossX_VRyjNrd7HVULhR8ZQMwDOh09SteA8Yk6zFVk206-C5FOOhDh4g_XIX269QsbfTDHHrF4GXVJ6FPIs7HVdH-f4y1Efk6SksVDlEcoHCsTbku4ttEWX4jxkhSpeRcu3GQ&skey=c491285d6722e4fa&v=v20": {
|
||||
"status": 200,
|
||||
"contentType": "font/woff2",
|
||||
"file": "47d5160342b9998840916131.bin"
|
||||
"file": "e3292e3f83690316b015865b.bin"
|
||||
},
|
||||
"https://fonts.gstatic.com/l/font?kit=tDbV2o-flEEny0FZhsfKu5WU4yD5MQaPTXC-7jDdIsxmG1S4kI2wqibvTm6_HSTI8hUSo9elLPWZU1qfAlh9usvmC0NXcrzdX6ttGYmH5rbj0fJiV60wlxpOoMENiID7wMbqOVLCL3tb6MF4FLv3jdS78Cl59EfJNtbKnRvDeJbp1veZfg&skey=48ad01c60053c2ae&v=v24": {
|
||||
"https://fonts.gstatic.com/l/font?kit=tDbV2o-flEEny0FZhsfKu5WU4yD5MQaPTXC57RrcIc1gGVa2no-yrCDtTHChHybO9BcQrdmnLvOfUVjhfFp_vM3kCU1ZcL7bWalvB5eF5LDl0_BsWa8ykRxMov8zioL9xsToN1zALX1d6sNmCrn1i9K58id39kXPMEg3dxvDVpbl6PeCffdWKg&skey=48ad01c60053c2ae&v=v24": {
|
||||
"status": 200,
|
||||
"contentType": "font/woff2",
|
||||
"file": "c356c27cca02fa3bfd0c2bef.bin"
|
||||
"file": "6ea12fa77b5d9e7cef868d13.bin"
|
||||
},
|
||||
"https://fonts.gstatic.com/l/font?kit=tDbp2o-flEEny0FZhsfKu5WU4xD-IQ-PuZJJXxfpAO-LfjGbsVNLG7DGdF6OZ1Pcwgknk-6tfcOETEf8ZjwZ1qeKZyczFtihI9cRfe3jgtqPvZ4GM8lUiwZSvOUprKTX7OrGHXbmC0dn1P1cMJ_TofiX3A1d0GPVKlYpbZwlpRwMOH5ilHuu3bpJuIwk3XNMxaDDqyh-HEXqRJEO_FrwSUc&skey=74ed26de24979d51&v=v24": {
|
||||
"https://fonts.gstatic.com/l/font?kit=tDbp2o-flEEny0FZhsfKu5WU4xD-IQ-PuZJJXxfpAO-LfjGbsVNLG7DGdF6OZ1Pcwgknk-6tesCuTUT9YD4b2KmIZSE1FNq_PdUTe-vhgNSBv5wANctW9XhQvuMvrqbZ4ujEG3DkCVl51v9aNp3Rr_aV3gtb0mHrFFQra5onpxICOnxkknmsw6RLuooi33FCy6LBrS7g4a_qRL8O8GTwUkRLlR8&skey=74ed26de24979d51&v=v24": {
|
||||
"status": 200,
|
||||
"contentType": "font/woff2",
|
||||
"file": "728492d30bf60f21ae04abd1.bin"
|
||||
"file": "d446a78da686ab7e4cd84394.bin"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,12 @@ import { commentBlocks } from "./check-comment-citations.mjs";
|
||||
|
||||
const MAX_BLOCK_LINES = 12;
|
||||
|
||||
// Product source only: a test's table of cases reads as comment and is the test's content.
|
||||
// Product source only: a test's table of cases reads as comment and is the test's content, and a
|
||||
// generated file's comments are its generator's.
|
||||
export const isSource = (path) =>
|
||||
/^packages\/[^/]+\/src\/.+\.tsx?$/.test(path) && !/\.(d|test|spec)\.tsx?$/.test(path);
|
||||
/^packages\/[^/]+\/src\/.+\.tsx?$/.test(path) &&
|
||||
!/\.(d|test|spec)\.tsx?$/.test(path) &&
|
||||
!/^packages\/[^/]+\/src\/generated\//.test(path);
|
||||
|
||||
const packageOf = (path) => path.split("/").slice(0, 2).join("/");
|
||||
|
||||
|
||||
@@ -176,6 +176,8 @@ test("only package source is graded, never tests or declarations", () => {
|
||||
assert.equal(isSource("packages/core/src/runtime/init.test.ts"), false);
|
||||
assert.equal(isSource("packages/core/src/types.d.ts"), false);
|
||||
assert.equal(isSource("packages/core/scripts/build.ts"), false);
|
||||
assert.equal(isSource("packages/core/src/generated/svg-selector-aliases-inline.ts"), false);
|
||||
assert.equal(isSource("packages/studio/src/editor/generated/labels.ts"), true);
|
||||
assert.equal(isSource("scripts/contrast.ts"), false);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user