@hyperframes/parsers 0.8.142 → 0.8.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,37 +36,8 @@ declare function hasUnresolvedTemplatingToken(url: string): boolean;
36
36
  */
37
37
  declare function isUnresolvedAssetPlaceholder(rawSrc: string): boolean;
38
38
  /**
39
- * Every `data-composition-src` reference in one composition file's raw text, in
40
- * document order, deduped. The single owner of "which sub-compositions does
41
- * this file mount", so lint, telemetry, and any future scanner cannot drift
42
- * apart on the answer.
43
- *
44
- * Text-scanning rather than DOM-walking, and that is the load-bearing choice.
45
- * Every sub-composition except the render entry is authored inside a
46
- * `<template>` (the root index.html is forbidden from using that wrapper;
47
- * everything else prefers it). Template content is inert: it lives under
48
- * `template.content`, not the live document, so `document.querySelectorAll`
49
- * on a raw sub-composition file finds nothing and every nested reference
50
- * disappears. The renderer only gets away with a DOM scan because it recurses
51
- * on COMPILED html, where the wrapper is already gone.
52
- *
53
- * Callers must resolve each value against the PROJECT ROOT, never the
54
- * referencing file's directory: `data-composition-src` is root-relative at
55
- * every nesting level (see `parseSubCompositions` in htmlCompiler.ts).
56
- *
57
- * Comments, `<style>`, and `<script>` bodies are masked first so a
58
- * commented-out mount is not counted as a real one. Build-time placeholders and
59
- * remote or inline URLs are dropped: neither names a file on disk, and every
60
- * caller resolves what comes back against the project root.
61
- *
62
- * The scan walks tag by tag with `indexOf` rather than running one regex with
63
- * two open-ended `[^>]*` spans across the whole file. That shape is quadratic:
64
- * on input full of `<` with no `>`, every `<` starts a scan to end-of-string
65
- * that then backtracks, measured at 41ms / 165ms / 660ms / 2640ms for 10k /
66
- * 20k / 40k / 80k characters. This function runs on every render (via the
67
- * render plan), so a truncated download or a blob full of stray `<` would hang
68
- * the plan step before any video is produced. Bounding each regex to one
69
- * already-delimited tag makes the whole scan linear.
39
+ * Collect active, local composition paths in document order, including template content.
40
+ * Paths are physical file names relative to the project root at every nesting level.
70
41
  */
71
42
  declare function collectSubCompositionSrcs(html: string): string[];
72
43
  declare function cleanAssetUrl(url: string): string;
@@ -1,16 +1,108 @@
1
+ // src/htmlAttributeSpans.ts
2
+ import { decodeHTMLAttribute } from "entities/decode";
3
+ function readAttributeValue(html, at) {
4
+ while (/[\t\n\f\r ]/.test(html.charAt(at))) at++;
5
+ const quote = ['"', "'"].includes(html.charAt(at)) ? html.charAt(at) : "";
6
+ if (quote) at++;
7
+ const valueStart = at;
8
+ if (quote) {
9
+ const closing = html.indexOf(quote, at);
10
+ at = closing === -1 ? html.length : closing;
11
+ } else {
12
+ while (at < html.length && !/[\t\n\f\r >]/.test(html[at])) at++;
13
+ }
14
+ const valueEnd = at;
15
+ const end = quote ? Math.min(valueEnd + 1, html.length) : valueEnd;
16
+ return { value: html.slice(valueStart, valueEnd), valueStart, valueEnd, quote, end };
17
+ }
18
+ function readAttribute(html, start) {
19
+ let at = start;
20
+ while (at < html.length && !/[\t\n\f\r =/>]/.test(html[at])) at++;
21
+ if (at === start) return null;
22
+ const name = html.slice(start, at).toLowerCase();
23
+ const nameEnd = at;
24
+ while (/[\t\n\f\r ]/.test(html[at] ?? "")) at++;
25
+ if (html[at] !== "=") return { kind: "boolean", name, start, end: nameEnd };
26
+ return { kind: "value", name, start, ...readAttributeValue(html, at + 1) };
27
+ }
28
+ function readOpeningTag(html, start, name, at) {
29
+ const attributes = [];
30
+ while (at < html.length) {
31
+ while (/[\t\n\f\r /]/.test(html[at] ?? "")) at++;
32
+ if (at >= html.length || html[at] === ">") break;
33
+ const attribute = readAttribute(html, at);
34
+ if (attribute === null) {
35
+ at++;
36
+ continue;
37
+ }
38
+ attributes.push(attribute);
39
+ at = attribute.end;
40
+ }
41
+ const closed = html[at] === ">";
42
+ return {
43
+ name: name.toLowerCase(),
44
+ start,
45
+ bodyEnd: at,
46
+ end: closed ? at + 1 : at,
47
+ closed,
48
+ rawTextEnd: null,
49
+ attributes
50
+ };
51
+ }
52
+ function scanHtmlOpeningTags(html) {
53
+ const tags = [];
54
+ const opening = /<!--[\s\S]*?(?:--!?>|$)|<([a-z][^\t\n\f\r />]*)/gi;
55
+ let match;
56
+ while ((match = opening.exec(html)) !== null) {
57
+ if (!match[1]) continue;
58
+ const tag = readOpeningTag(html, match.index, match[1], opening.lastIndex);
59
+ tags.push(tag);
60
+ opening.lastIndex = tag.end;
61
+ if ([
62
+ "script",
63
+ "style",
64
+ "textarea",
65
+ "title",
66
+ "xmp",
67
+ "iframe",
68
+ "noembed",
69
+ "noframes",
70
+ "plaintext"
71
+ ].includes(tag.name)) {
72
+ if (tag.name === "plaintext") {
73
+ tag.rawTextEnd = html.length;
74
+ break;
75
+ }
76
+ const closing = new RegExp(`</${tag.name}(?=[\\t\\n\\f\\r />])`, "gi");
77
+ closing.lastIndex = tag.end;
78
+ const end = closing.exec(html);
79
+ tag.rawTextEnd = end?.index ?? html.length;
80
+ opening.lastIndex = tag.rawTextEnd;
81
+ }
82
+ }
83
+ return tags;
84
+ }
85
+ function decodeAuthoredAttribute(value) {
86
+ return decodeHTMLAttribute(value);
87
+ }
88
+
1
89
  // src/assetResolution.ts
2
90
  import { closeSync, constants, existsSync, fstatSync, openSync, readFileSync } from "fs";
3
91
  import { isAbsolute, join, posix, relative, resolve, sep } from "path";
4
92
 
5
93
  // src/utils/urlPath.ts
6
94
  function decodeUrlPathVariants(path) {
7
- const variants = [path];
8
- try {
9
- const decoded = decodeURIComponent(path);
10
- if (decoded !== path) variants.unshift(decoded);
11
- } catch {
12
- }
13
- return variants;
95
+ const decoded = decodeWellFormedEscapes(path);
96
+ return decoded === path ? [path] : [decoded, path];
97
+ }
98
+ function decodeWellFormedEscapes(path) {
99
+ return path.replace(/(?:%[0-9A-Fa-f]{2})+/g, (run) => {
100
+ try {
101
+ return decodeURIComponent(run);
102
+ } catch {
103
+ return run;
104
+ }
105
+ });
14
106
  }
15
107
 
16
108
  // src/assetResolution.ts
@@ -23,21 +115,14 @@ function hasUnresolvedTemplatingToken(url) {
23
115
  function isUnresolvedAssetPlaceholder(rawSrc) {
24
116
  return /^__[A-Z_]+__$/.test(rawSrc.trim()) || hasUnresolvedTemplatingToken(rawSrc);
25
117
  }
26
- var COMPOSITION_SRC_ATTR = /\bdata-composition-src\s*=\s*["']([^"']+)["']/i;
27
118
  function collectSubCompositionSrcs(html) {
28
- const scannable = maskNonScannableRanges(html);
29
119
  const srcs = [];
30
120
  const seen = /* @__PURE__ */ new Set();
31
- let cursor = 0;
32
- while (cursor < scannable.length) {
33
- const open = scannable.indexOf("<", cursor);
34
- if (open === -1) break;
35
- const close = scannable.indexOf(">", open + 1);
36
- if (close === -1) break;
37
- cursor = close + 1;
38
- const match = COMPOSITION_SRC_ATTR.exec(scannable.slice(open, cursor));
39
- if (!match) continue;
40
- const src = (match[1] ?? "").trim();
121
+ for (const tag of scanHtmlOpeningTags(html)) {
122
+ if (!tag.closed) continue;
123
+ const attribute = tag.attributes.find((attr) => attr.name === "data-composition-src");
124
+ if (attribute?.kind !== "value") continue;
125
+ const src = decodeAuthoredAttribute(attribute.value).trim();
41
126
  if (!src || seen.has(src)) continue;
42
127
  if (isUnresolvedAssetPlaceholder(src)) continue;
43
128
  if (isRemoteOrInlineUrl(src)) continue;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/assetResolution.ts","../src/utils/urlPath.ts"],"sourcesContent":["import { closeSync, constants, existsSync, fstatSync, openSync, readFileSync } from \"node:fs\";\nimport { isAbsolute, join, posix, relative, resolve, sep } from \"node:path\";\nimport { decodeUrlPathVariants } from \"./composition.js\";\n\n/** The subset of `node:path` that `isWithinProjectRoot` needs to run under\n * an injected platform (tests pass `path.win32` / `path.posix`). */\ninterface PathModuleLike {\n resolve: (...segments: string[]) => string;\n relative: (from: string, to: string) => string;\n isAbsolute: (path: string) => boolean;\n}\n\n/**\n * Shared local-asset resolution helpers for every package that maps\n * composition asset URLs to files on disk (lint project rules, the HEVC\n * preview check, studio-server's media codec scan). Import via the\n * `@hyperframes/parsers/asset-resolution` subpath.\n */\n\nexport function isRemoteOrInlineUrl(url: string): boolean {\n return /^(https?:|data:|blob:|\\/\\/|#)/i.test(url);\n}\n\n/**\n * True when a URL still contains an unresolved templating placeholder —\n * `<<token>>`, `{{ token }}`, or `${token}` — that a build or templating step\n * substitutes before render. The static linter runs before that substitution,\n * so it cannot resolve such a value to a file on disk and must not report it as\n * a missing asset. Check the RAW url, before any `cleanAssetUrl()` step: that\n * splits on `?`/`#`, which also chops inside a `${...}` expression. (The `__UPPER__`\n * placeholder shape is combined with this in `isUnresolvedAssetPlaceholder` below, the\n * shared predicate asset-src sites skip on.)\n */\nexport function hasUnresolvedTemplatingToken(url: string): boolean {\n return /<<[^<>]+>>|\\{\\{[^{}]+\\}\\}|\\$\\{[^{}]+\\}/.test(url);\n}\n\n/**\n * True when an asset src is a build-time placeholder rather than a resolvable path:\n * the `__UPPER__` shape (e.g. `__DURATION__`) or an unresolved templating token\n * (`<<...>>`, `{{...}}`, `${...}`). Pass the RAW src, before any `cleanAssetUrl()` step —\n * cleanAssetUrl splits on `?`/`#`, which would chop inside a `${...}` expression and defeat\n * the token match. Remote/inline URL handling is deliberately NOT folded in: call sites\n * differ (e.g. audio uses a narrower http/data/blob check), so each keeps its own.\n *\n * This is the single skip predicate every asset-src lint / codec / compile site should\n * route through, so the placeholder rules can't drift apart across call sites.\n */\nexport function isUnresolvedAssetPlaceholder(rawSrc: string): boolean {\n return /^__[A-Z_]+__$/.test(rawSrc.trim()) || hasUnresolvedTemplatingToken(rawSrc);\n}\n\n/** `data-composition-src=\"...\"`, matched within a single already-delimited tag. */\nconst COMPOSITION_SRC_ATTR = /\\bdata-composition-src\\s*=\\s*[\"']([^\"']+)[\"']/i;\n\n/**\n * Every `data-composition-src` reference in one composition file's raw text, in\n * document order, deduped. The single owner of \"which sub-compositions does\n * this file mount\", so lint, telemetry, and any future scanner cannot drift\n * apart on the answer.\n *\n * Text-scanning rather than DOM-walking, and that is the load-bearing choice.\n * Every sub-composition except the render entry is authored inside a\n * `<template>` (the root index.html is forbidden from using that wrapper;\n * everything else prefers it). Template content is inert: it lives under\n * `template.content`, not the live document, so `document.querySelectorAll`\n * on a raw sub-composition file finds nothing and every nested reference\n * disappears. The renderer only gets away with a DOM scan because it recurses\n * on COMPILED html, where the wrapper is already gone.\n *\n * Callers must resolve each value against the PROJECT ROOT, never the\n * referencing file's directory: `data-composition-src` is root-relative at\n * every nesting level (see `parseSubCompositions` in htmlCompiler.ts).\n *\n * Comments, `<style>`, and `<script>` bodies are masked first so a\n * commented-out mount is not counted as a real one. Build-time placeholders and\n * remote or inline URLs are dropped: neither names a file on disk, and every\n * caller resolves what comes back against the project root.\n *\n * The scan walks tag by tag with `indexOf` rather than running one regex with\n * two open-ended `[^>]*` spans across the whole file. That shape is quadratic:\n * on input full of `<` with no `>`, every `<` starts a scan to end-of-string\n * that then backtracks, measured at 41ms / 165ms / 660ms / 2640ms for 10k /\n * 20k / 40k / 80k characters. This function runs on every render (via the\n * render plan), so a truncated download or a blob full of stray `<` would hang\n * the plan step before any video is produced. Bounding each regex to one\n * already-delimited tag makes the whole scan linear.\n */\nexport function collectSubCompositionSrcs(html: string): string[] {\n const scannable = maskNonScannableRanges(html);\n const srcs: string[] = [];\n const seen = new Set<string>();\n\n let cursor = 0;\n while (cursor < scannable.length) {\n const open = scannable.indexOf(\"<\", cursor);\n if (open === -1) break;\n const close = scannable.indexOf(\">\", open + 1);\n // An unterminated final tag is not a tag. The previous whole-file regex\n // also required a closing `>`, so this drops nothing it used to find.\n if (close === -1) break;\n cursor = close + 1;\n\n const match = COMPOSITION_SRC_ATTR.exec(scannable.slice(open, cursor));\n if (!match) continue;\n const src = (match[1] ?? \"\").trim();\n if (!src || seen.has(src)) continue;\n // __UPPER__ placeholder or late-bound templating token — not a real reference.\n if (isUnresolvedAssetPlaceholder(src)) continue;\n // A remote or inline mount names no file on disk. Every caller resolves\n // these against the project root, so letting one through produces a\n // nonsense path (`<projectDir>/https:/host/a.html`) that then reads as a\n // missing local file: a false \"does not exist\" for lint, and a wasted\n // visit against the telemetry walk's file budget.\n if (isRemoteOrInlineUrl(src)) continue;\n seen.add(src);\n srcs.push(src);\n }\n return srcs;\n}\n\nexport function cleanAssetUrl(url: string): string {\n return url.trim().split(/[?#]/, 1)[0] ?? \"\";\n}\n\n/**\n * `pathModule` defaults to the host's native `node:path`, so every existing\n * caller gets its actual OS's separator and drive-letter rules unchanged.\n * Tests inject `path.win32` / `path.posix` to exercise both platforms' rules\n * from a single OS (same pattern as producer/fileServer.ts's `isPathInside`).\n */\nexport function isWithinProjectRoot(\n projectDir: string,\n candidate: string,\n pathModule: PathModuleLike = { resolve, relative, isAbsolute },\n): boolean {\n const projectRoot = pathModule.resolve(projectDir);\n const relativePath = pathModule.relative(projectRoot, candidate);\n return (\n relativePath === \"\" || (!relativePath.startsWith(\"..\") && !pathModule.isAbsolute(relativePath))\n );\n}\n\nfunction addCandidate(candidates: string[], candidate: string): void {\n if (!candidates.includes(candidate)) candidates.push(candidate);\n}\n\nexport function resolveLocalAssetCandidates(projectDir: string, url: string): string[] {\n const cleanUrl = cleanAssetUrl(url);\n const projectRoot = resolve(projectDir);\n const candidates: string[] = [];\n\n for (const variant of decodeUrlPathVariants(cleanUrl)) {\n const projectRelative = variant.startsWith(\"/\") ? variant.slice(1) : variant;\n const resolved = resolve(projectRoot, projectRelative);\n if (isWithinProjectRoot(projectRoot, resolved)) {\n addCandidate(candidates, resolved);\n continue;\n }\n\n const normalized = posix.normalize(projectRelative.replace(/\\\\/g, \"/\"));\n const clamped = normalized.replace(/^(\\.\\.\\/)+/, \"\");\n if (clamped && !clamped.startsWith(\"..\")) {\n addCandidate(candidates, resolve(projectRoot, clamped));\n }\n }\n\n return candidates;\n}\n\nexport function resolveExistingLocalAsset(\n projectDir: string,\n url: string,\n): { resolved: string; rootRelativePath: string } | null {\n const projectRoot = resolve(projectDir);\n const resolved = resolveLocalAssetCandidates(projectRoot, url).find(existsSync);\n if (!resolved) return null;\n return { resolved, rootRelativePath: relative(projectRoot, resolved) };\n}\n\n// Candidates for a variant whose join escaped the project root, re-anchored at the root.\nfunction reanchoredCandidates(variant: string, baseDir: string, compiledDir?: string): string[] {\n const baseAbs = resolve(baseDir);\n const joinedAbs = resolve(join(baseDir, variant));\n if (joinedAbs === baseAbs || joinedAbs.startsWith(baseAbs + sep)) return [];\n // Normalize before stripping, or `assets/../../assets/foo` becomes `assets/assets/foo`.\n const stripped = posix.normalize(variant.replace(/\\\\/g, \"/\")).replace(/^(\\.\\.\\/)+/, \"\");\n if (!stripped || stripped === variant || stripped.startsWith(\"..\")) return [];\n return compiledDir\n ? [join(compiledDir, stripped), join(baseDir, stripped)]\n : [join(baseDir, stripped)];\n}\n\n/** Resolves a media `src` like a browser URL (`..` clamps at the project root); a miss returns the base-dir join. */\nexport function resolveProjectRelativeSrc(\n src: string,\n baseDir: string,\n compiledDir?: string,\n): string {\n const cleanSrc = cleanAssetUrl(src);\n\n // A leading slash is an origin-root URL served from the project root, unless the absolute path exists.\n if (isAbsolute(cleanSrc) && existsSync(cleanSrc)) return cleanSrc;\n\n const candidates = new Set<string>();\n for (const variant of decodeUrlPathVariants(cleanSrc)) {\n for (const candidate of reanchoredCandidates(variant, baseDir, compiledDir)) {\n candidates.add(candidate);\n }\n if (compiledDir) candidates.add(join(compiledDir, variant));\n candidates.add(join(baseDir, variant));\n }\n return [...candidates].find(existsSync) ?? join(baseDir, cleanSrc);\n}\n\nfunction maskRange(src: string, pattern: RegExp): string {\n return src.replace(pattern, (m) => \" \".repeat(m.length));\n}\n\nfunction maskHtmlComments(src: string): string {\n const chunks: string[] = [];\n let cursor = 0;\n\n while (true) {\n const start = src.indexOf(\"<!--\", cursor);\n if (start === -1) break;\n const end = src.indexOf(\"-->\", start + 4);\n if (end === -1) break;\n const afterComment = end + 3;\n chunks.push(src.slice(cursor, start), \" \".repeat(afterComment - start));\n cursor = afterComment;\n }\n\n return chunks.length === 0 ? src : chunks.join(\"\") + src.slice(cursor);\n}\n\n/** Blanks out comments, `<style>`, and `<script>` bodies so tag-scanning\n * regexes don't false-positive on commented-out or scripted markup. */\nexport function maskNonScannableRanges(html: string): string {\n let out = maskHtmlComments(html);\n out = maskRange(out, /<style\\b[^>]*>[\\s\\S]*?<\\/style\\b[^>]*>/gi);\n out = maskRange(out, /<script\\b[^>]*>[\\s\\S]*?<\\/script\\b[^>]*>/gi);\n return out;\n}\n\nexport type ProjectFileRead =\n | { kind: \"file\"; text: string }\n | { kind: \"folder\" }\n | { kind: \"missing\" };\n\n/** Reads through one descriptor, so the file-type check and the read see the same file. */\nexport function readProjectFile(path: string): ProjectFileRead {\n let fd: number;\n try {\n // Non-blocking so a named pipe is reported, not waited on; the mode never applies (no O_CREAT).\n fd = openSync(path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0), 0o600);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code ?? \"\";\n if ([\"EISDIR\", \"ENXIO\"].includes(code)) return { kind: \"folder\" };\n if ([\"ENOENT\", \"ENOTDIR\", \"ELOOP\", \"ENAMETOOLONG\"].includes(code)) return { kind: \"missing\" };\n throw error;\n }\n try {\n if (!fstatSync(fd).isFile()) return { kind: \"folder\" };\n return { kind: \"file\", text: readFileSync(fd, \"utf-8\") };\n } finally {\n closeSync(fd);\n }\n}\n","export function decodeUrlPathVariants(path: string): string[] {\n const variants = [path];\n try {\n const decoded = decodeURIComponent(path);\n if (decoded !== path) variants.unshift(decoded);\n } catch {\n // Malformed percent sequences may be literal filesystem names.\n }\n\n return variants;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/htmlAttributeSpans.ts","../src/assetResolution.ts","../src/utils/urlPath.ts"],"sourcesContent":["import { decodeHTMLAttribute } from \"entities/decode\";\n\nexport type HtmlAttributeSpan = {\n name: string;\n start: number;\n end: number;\n} & (\n | { kind: \"boolean\" }\n | { kind: \"value\"; value: string; valueStart: number; valueEnd: number; quote: string }\n);\n\nexport interface HtmlOpeningTagSpan {\n name: string;\n start: number;\n end: number;\n bodyEnd: number;\n closed: boolean;\n rawTextEnd: number | null;\n attributes: HtmlAttributeSpan[];\n}\n\nfunction readAttributeValue(html: string, at: number) {\n while (/[\\t\\n\\f\\r ]/.test(html.charAt(at))) at++;\n const quote = ['\"', \"'\"].includes(html.charAt(at)) ? html.charAt(at) : \"\";\n if (quote) at++;\n const valueStart = at;\n if (quote) {\n const closing = html.indexOf(quote, at);\n at = closing === -1 ? html.length : closing;\n } else {\n while (at < html.length && !/[\\t\\n\\f\\r >]/.test(html[at]!)) at++;\n }\n const valueEnd = at;\n const end = quote ? Math.min(valueEnd + 1, html.length) : valueEnd;\n return { value: html.slice(valueStart, valueEnd), valueStart, valueEnd, quote, end };\n}\n\nfunction readAttribute(html: string, start: number): HtmlAttributeSpan | null {\n let at = start;\n while (at < html.length && !/[\\t\\n\\f\\r =/>]/.test(html[at]!)) at++;\n if (at === start) return null;\n const name = html.slice(start, at).toLowerCase();\n const nameEnd = at;\n while (/[\\t\\n\\f\\r ]/.test(html[at] ?? \"\")) at++;\n if (html[at] !== \"=\") return { kind: \"boolean\", name, start, end: nameEnd };\n return { kind: \"value\", name, start, ...readAttributeValue(html, at + 1) };\n}\n\nfunction readOpeningTag(html: string, start: number, name: string, at: number): HtmlOpeningTagSpan {\n const attributes: HtmlAttributeSpan[] = [];\n while (at < html.length) {\n while (/[\\t\\n\\f\\r /]/.test(html[at] ?? \"\")) at++;\n if (at >= html.length || html[at] === \">\") break;\n const attribute = readAttribute(html, at);\n if (attribute === null) {\n at++;\n continue;\n }\n attributes.push(attribute);\n at = attribute.end;\n }\n const closed = html[at] === \">\";\n return {\n name: name.toLowerCase(),\n start,\n bodyEnd: at,\n end: closed ? at + 1 : at,\n closed,\n rawTextEnd: null,\n attributes,\n };\n}\n\nexport function scanHtmlOpeningTags(html: string): HtmlOpeningTagSpan[] {\n const tags: HtmlOpeningTagSpan[] = [];\n const opening = /<!--[\\s\\S]*?(?:--!?>|$)|<([a-z][^\\t\\n\\f\\r />]*)/gi;\n let match: RegExpExecArray | null;\n while ((match = opening.exec(html)) !== null) {\n if (!match[1]) continue;\n const tag = readOpeningTag(html, match.index, match[1], opening.lastIndex);\n tags.push(tag);\n opening.lastIndex = tag.end;\n if (\n [\n \"script\",\n \"style\",\n \"textarea\",\n \"title\",\n \"xmp\",\n \"iframe\",\n \"noembed\",\n \"noframes\",\n \"plaintext\",\n ].includes(tag.name)\n ) {\n if (tag.name === \"plaintext\") {\n tag.rawTextEnd = html.length;\n break;\n }\n const closing = new RegExp(`</${tag.name}(?=[\\\\t\\\\n\\\\f\\\\r />])`, \"gi\");\n closing.lastIndex = tag.end;\n const end = closing.exec(html);\n tag.rawTextEnd = end?.index ?? html.length;\n opening.lastIndex = tag.rawTextEnd;\n }\n }\n return tags;\n}\n\nexport const HTML_ATTRIBUTE_ENTITIES: Record<string, readonly string[]> = {\n \"&\": [\"&amp;\"],\n '\"': [\"&quot;\"],\n \"'\": [\"&#39;\", \"&apos;\"],\n \"<\": [\"&lt;\"],\n \">\": [\"&gt;\"],\n};\nexport function decodeAuthoredAttribute(value: string): string {\n return decodeHTMLAttribute(value);\n}\n","import { scanHtmlOpeningTags, decodeAuthoredAttribute } from \"./htmlAttributeSpans.js\";\nimport { closeSync, constants, existsSync, fstatSync, openSync, readFileSync } from \"node:fs\";\nimport { isAbsolute, join, posix, relative, resolve, sep } from \"node:path\";\nimport { decodeUrlPathVariants } from \"./composition.js\";\n\n/** The subset of `node:path` that `isWithinProjectRoot` needs to run under\n * an injected platform (tests pass `path.win32` / `path.posix`). */\ninterface PathModuleLike {\n resolve: (...segments: string[]) => string;\n relative: (from: string, to: string) => string;\n isAbsolute: (path: string) => boolean;\n}\n\n/**\n * Shared local-asset resolution helpers for every package that maps\n * composition asset URLs to files on disk (lint project rules, the HEVC\n * preview check, studio-server's media codec scan). Import via the\n * `@hyperframes/parsers/asset-resolution` subpath.\n */\n\nexport function isRemoteOrInlineUrl(url: string): boolean {\n return /^(https?:|data:|blob:|\\/\\/|#)/i.test(url);\n}\n\n/**\n * True when a URL still contains an unresolved templating placeholder —\n * `<<token>>`, `{{ token }}`, or `${token}` — that a build or templating step\n * substitutes before render. The static linter runs before that substitution,\n * so it cannot resolve such a value to a file on disk and must not report it as\n * a missing asset. Check the RAW url, before any `cleanAssetUrl()` step: that\n * splits on `?`/`#`, which also chops inside a `${...}` expression. (The `__UPPER__`\n * placeholder shape is combined with this in `isUnresolvedAssetPlaceholder` below, the\n * shared predicate asset-src sites skip on.)\n */\nexport function hasUnresolvedTemplatingToken(url: string): boolean {\n return /<<[^<>]+>>|\\{\\{[^{}]+\\}\\}|\\$\\{[^{}]+\\}/.test(url);\n}\n\n/**\n * True when an asset src is a build-time placeholder rather than a resolvable path:\n * the `__UPPER__` shape (e.g. `__DURATION__`) or an unresolved templating token\n * (`<<...>>`, `{{...}}`, `${...}`). Pass the RAW src, before any `cleanAssetUrl()` step —\n * cleanAssetUrl splits on `?`/`#`, which would chop inside a `${...}` expression and defeat\n * the token match. Remote/inline URL handling is deliberately NOT folded in: call sites\n * differ (e.g. audio uses a narrower http/data/blob check), so each keeps its own.\n *\n * This is the single skip predicate every asset-src lint / codec / compile site should\n * route through, so the placeholder rules can't drift apart across call sites.\n */\nexport function isUnresolvedAssetPlaceholder(rawSrc: string): boolean {\n return /^__[A-Z_]+__$/.test(rawSrc.trim()) || hasUnresolvedTemplatingToken(rawSrc);\n}\n\n/**\n * Collect active, local composition paths in document order, including template content.\n * Paths are physical file names relative to the project root at every nesting level.\n */\nexport function collectSubCompositionSrcs(html: string): string[] {\n const srcs: string[] = [];\n const seen = new Set<string>();\n\n for (const tag of scanHtmlOpeningTags(html)) {\n if (!tag.closed) continue;\n const attribute = tag.attributes.find((attr) => attr.name === \"data-composition-src\");\n if (attribute?.kind !== \"value\") continue;\n const src = decodeAuthoredAttribute(attribute.value).trim();\n if (!src || seen.has(src)) continue;\n // __UPPER__ placeholder or late-bound templating token — not a real reference.\n if (isUnresolvedAssetPlaceholder(src)) continue;\n // A remote or inline mount names no file on disk. Every caller resolves\n // these against the project root, so letting one through produces a\n // nonsense path (`<projectDir>/https:/host/a.html`) that then reads as a\n // missing local file: a false \"does not exist\" for lint, and a wasted\n // visit against the telemetry walk's file budget.\n if (isRemoteOrInlineUrl(src)) continue;\n seen.add(src);\n srcs.push(src);\n }\n return srcs;\n}\n\nexport function cleanAssetUrl(url: string): string {\n return url.trim().split(/[?#]/, 1)[0] ?? \"\";\n}\n\n/**\n * `pathModule` defaults to the host's native `node:path`, so every existing\n * caller gets its actual OS's separator and drive-letter rules unchanged.\n * Tests inject `path.win32` / `path.posix` to exercise both platforms' rules\n * from a single OS (same pattern as producer/fileServer.ts's `isPathInside`).\n */\nexport function isWithinProjectRoot(\n projectDir: string,\n candidate: string,\n pathModule: PathModuleLike = { resolve, relative, isAbsolute },\n): boolean {\n const projectRoot = pathModule.resolve(projectDir);\n const relativePath = pathModule.relative(projectRoot, candidate);\n return (\n relativePath === \"\" || (!relativePath.startsWith(\"..\") && !pathModule.isAbsolute(relativePath))\n );\n}\n\nfunction addCandidate(candidates: string[], candidate: string): void {\n if (!candidates.includes(candidate)) candidates.push(candidate);\n}\n\nexport function resolveLocalAssetCandidates(projectDir: string, url: string): string[] {\n const cleanUrl = cleanAssetUrl(url);\n const projectRoot = resolve(projectDir);\n const candidates: string[] = [];\n\n for (const variant of decodeUrlPathVariants(cleanUrl)) {\n const projectRelative = variant.startsWith(\"/\") ? variant.slice(1) : variant;\n const resolved = resolve(projectRoot, projectRelative);\n if (isWithinProjectRoot(projectRoot, resolved)) {\n addCandidate(candidates, resolved);\n continue;\n }\n\n const normalized = posix.normalize(projectRelative.replace(/\\\\/g, \"/\"));\n const clamped = normalized.replace(/^(\\.\\.\\/)+/, \"\");\n if (clamped && !clamped.startsWith(\"..\")) {\n addCandidate(candidates, resolve(projectRoot, clamped));\n }\n }\n\n return candidates;\n}\n\nexport function resolveExistingLocalAsset(\n projectDir: string,\n url: string,\n): { resolved: string; rootRelativePath: string } | null {\n const projectRoot = resolve(projectDir);\n const resolved = resolveLocalAssetCandidates(projectRoot, url).find(existsSync);\n if (!resolved) return null;\n return { resolved, rootRelativePath: relative(projectRoot, resolved) };\n}\n\n// Candidates for a variant whose join escaped the project root, re-anchored at the root.\nfunction reanchoredCandidates(variant: string, baseDir: string, compiledDir?: string): string[] {\n const baseAbs = resolve(baseDir);\n const joinedAbs = resolve(join(baseDir, variant));\n if (joinedAbs === baseAbs || joinedAbs.startsWith(baseAbs + sep)) return [];\n // Normalize before stripping, or `assets/../../assets/foo` becomes `assets/assets/foo`.\n const stripped = posix.normalize(variant.replace(/\\\\/g, \"/\")).replace(/^(\\.\\.\\/)+/, \"\");\n if (!stripped || stripped === variant || stripped.startsWith(\"..\")) return [];\n return compiledDir\n ? [join(compiledDir, stripped), join(baseDir, stripped)]\n : [join(baseDir, stripped)];\n}\n\n/** Resolves a media `src` like a browser URL (`..` clamps at the project root); a miss returns the base-dir join. */\nexport function resolveProjectRelativeSrc(\n src: string,\n baseDir: string,\n compiledDir?: string,\n): string {\n const cleanSrc = cleanAssetUrl(src);\n\n // A leading slash is an origin-root URL served from the project root, unless the absolute path exists.\n if (isAbsolute(cleanSrc) && existsSync(cleanSrc)) return cleanSrc;\n\n const candidates = new Set<string>();\n for (const variant of decodeUrlPathVariants(cleanSrc)) {\n for (const candidate of reanchoredCandidates(variant, baseDir, compiledDir)) {\n candidates.add(candidate);\n }\n if (compiledDir) candidates.add(join(compiledDir, variant));\n candidates.add(join(baseDir, variant));\n }\n return [...candidates].find(existsSync) ?? join(baseDir, cleanSrc);\n}\n\nfunction maskRange(src: string, pattern: RegExp): string {\n return src.replace(pattern, (m) => \" \".repeat(m.length));\n}\n\nfunction maskHtmlComments(src: string): string {\n const chunks: string[] = [];\n let cursor = 0;\n\n while (true) {\n const start = src.indexOf(\"<!--\", cursor);\n if (start === -1) break;\n const end = src.indexOf(\"-->\", start + 4);\n if (end === -1) break;\n const afterComment = end + 3;\n chunks.push(src.slice(cursor, start), \" \".repeat(afterComment - start));\n cursor = afterComment;\n }\n\n return chunks.length === 0 ? src : chunks.join(\"\") + src.slice(cursor);\n}\n\n/** Blanks out comments, `<style>`, and `<script>` bodies so tag-scanning\n * regexes don't false-positive on commented-out or scripted markup. */\nexport function maskNonScannableRanges(html: string): string {\n let out = maskHtmlComments(html);\n out = maskRange(out, /<style\\b[^>]*>[\\s\\S]*?<\\/style\\b[^>]*>/gi);\n out = maskRange(out, /<script\\b[^>]*>[\\s\\S]*?<\\/script\\b[^>]*>/gi);\n return out;\n}\n\nexport type ProjectFileRead =\n | { kind: \"file\"; text: string }\n | { kind: \"folder\" }\n | { kind: \"missing\" };\n\n/** Reads through one descriptor, so the file-type check and the read see the same file. */\nexport function readProjectFile(path: string): ProjectFileRead {\n let fd: number;\n try {\n // Non-blocking so a named pipe is reported, not waited on; the mode never applies (no O_CREAT).\n fd = openSync(path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0), 0o600);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code ?? \"\";\n if ([\"EISDIR\", \"ENXIO\"].includes(code)) return { kind: \"folder\" };\n if ([\"ENOENT\", \"ENOTDIR\", \"ELOOP\", \"ENAMETOOLONG\"].includes(code)) return { kind: \"missing\" };\n throw error;\n }\n try {\n if (!fstatSync(fd).isFile()) return { kind: \"folder\" };\n return { kind: \"file\", text: readFileSync(fd, \"utf-8\") };\n } finally {\n closeSync(fd);\n }\n}\n","export function decodeUrlPathVariants(path: string): string[] {\n const decoded = decodeWellFormedEscapes(path);\n return decoded === path ? [path] : [decoded, path];\n}\n\n// A browser sends src=\"100%.png\" as-is, so a % without two hex digits after it is the file's own.\nexport function decodeWellFormedEscapes(path: string): string {\n return path.replace(/(?:%[0-9A-Fa-f]{2})+/g, (run) => {\n try {\n return decodeURIComponent(run);\n } catch {\n return run;\n }\n });\n}\n\nexport function splitUrlSuffix(urlValue: string): { basePath: string; suffix: string } {\n const queryIdx = urlValue.indexOf(\"?\");\n const hashIdx = urlValue.indexOf(\"#\");\n if (queryIdx < 0 && hashIdx < 0) return { basePath: urlValue, suffix: \"\" };\n let cutIdx = urlValue.length;\n if (queryIdx >= 0) cutIdx = Math.min(cutIdx, queryIdx);\n if (hashIdx >= 0) cutIdx = Math.min(cutIdx, hashIdx);\n return { basePath: urlValue.slice(0, cutIdx), suffix: urlValue.slice(cutIdx) };\n}\n\nexport function decodedUrlPath(url: string): string {\n return decodeWellFormedEscapes(splitUrlSuffix(url).basePath);\n}\n\nexport function encodeUrlPath(path: string): string {\n return encodeURIComponent(path)\n .replace(/%2F/g, \"/\")\n .replace(/['()]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);\n}\n\nexport function decodeCssEscapes(text: string): string {\n return text\n .replaceAll(\"\\0\", \"\\uFFFD\")\n .replace(\n /\\\\(?:([0-9a-f]{1,6})(?:\\r\\n|[ \\t\\r\\n\\f])?|(\\r\\n|[\\n\\r\\f])|([\\s\\S]))/gi,\n (\n _escaped,\n hex: string | undefined,\n lineBreak: string | undefined,\n char: string | undefined,\n ) => {\n if (hex === undefined) return lineBreak === undefined ? char! : \"\";\n const code = Number.parseInt(hex, 16);\n if (code === 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) return \"\\uFFFD\";\n return String.fromCodePoint(code);\n },\n );\n}\n"],"mappings":"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","names":[]}
package/dist/assets.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export { d as decodeCssEscapes, a as decodeWellFormedEscapes, b as decodedUrlPath, e as encodeUrlPath, s as splitUrlSuffix } from './urlPath-C_0f_-Hp.js';
2
+
1
3
  /**
2
4
  * Shared primitives for scanning and rewriting asset paths in HTML/CSS.
3
5
  *
package/dist/assets.js CHANGED
@@ -1,5 +1,44 @@
1
1
  // src/assetPaths.ts
2
2
  import { isAbsolute, relative, resolve } from "path";
3
+
4
+ // src/utils/urlPath.ts
5
+ function decodeWellFormedEscapes(path) {
6
+ return path.replace(/(?:%[0-9A-Fa-f]{2})+/g, (run) => {
7
+ try {
8
+ return decodeURIComponent(run);
9
+ } catch {
10
+ return run;
11
+ }
12
+ });
13
+ }
14
+ function splitUrlSuffix(urlValue) {
15
+ const queryIdx = urlValue.indexOf("?");
16
+ const hashIdx = urlValue.indexOf("#");
17
+ if (queryIdx < 0 && hashIdx < 0) return { basePath: urlValue, suffix: "" };
18
+ let cutIdx = urlValue.length;
19
+ if (queryIdx >= 0) cutIdx = Math.min(cutIdx, queryIdx);
20
+ if (hashIdx >= 0) cutIdx = Math.min(cutIdx, hashIdx);
21
+ return { basePath: urlValue.slice(0, cutIdx), suffix: urlValue.slice(cutIdx) };
22
+ }
23
+ function decodedUrlPath(url) {
24
+ return decodeWellFormedEscapes(splitUrlSuffix(url).basePath);
25
+ }
26
+ function encodeUrlPath(path) {
27
+ return encodeURIComponent(path).replace(/%2F/g, "/").replace(/['()]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
28
+ }
29
+ function decodeCssEscapes(text) {
30
+ return text.replaceAll("\0", "\uFFFD").replace(
31
+ /\\(?:([0-9a-f]{1,6})(?:\r\n|[ \t\r\n\f])?|(\r\n|[\n\r\f])|([\s\S]))/gi,
32
+ (_escaped, hex, lineBreak, char) => {
33
+ if (hex === void 0) return lineBreak === void 0 ? char : "";
34
+ const code = Number.parseInt(hex, 16);
35
+ if (code === 0 || code > 1114111 || code >= 55296 && code <= 57343) return "\uFFFD";
36
+ return String.fromCodePoint(code);
37
+ }
38
+ );
39
+ }
40
+
41
+ // src/assetPaths.ts
3
42
  var CSS_URL_RE = /\burl\(\s*(["']?)([^)"'\s](?:[^)"']*[^)"'\s])?)\1\s*\)/g;
4
43
  var PATH_ATTRS = ["src", "href"];
5
44
  function isNonRelativeUrl(val) {
@@ -20,24 +59,16 @@ var isAbsoluteOrSpecial = isNonRelativeUrl;
20
59
  function needsRewrite(val) {
21
60
  return val.startsWith("../") || val === "..";
22
61
  }
23
- function splitPathSuffix(value) {
24
- const marker = value.search(/[?#]/);
25
- return marker === -1 ? [value, ""] : [value.slice(0, marker), value.slice(marker)];
26
- }
27
62
  function rewriteAssetPath(compSrcPath, relativePath, assetExists) {
28
63
  if (isAbsoluteOrSpecial(relativePath)) return relativePath;
29
64
  const compDir = dirname(compSrcPath);
30
65
  if (!compDir || compDir === ".") return relativePath;
31
- if (!needsRewrite(relativePath)) {
32
- if (!assetExists) return relativePath;
33
- const [filePart, suffix] = splitPathSuffix(relativePath);
34
- if (!filePart) return relativePath;
35
- const sibling = resolve2("/", join(compDir, filePart)).slice(1);
36
- return assetExists(sibling) ? sibling + suffix : relativePath;
37
- }
38
- const resolved = join(compDir, relativePath);
39
- const normalized = resolve2("/", resolved).slice(1);
40
- return normalized;
66
+ const { basePath, suffix } = splitUrlSuffix(relativePath);
67
+ if (!basePath) return relativePath;
68
+ const filePath = decodeWellFormedEscapes(basePath);
69
+ const sibling = resolve2("/", join(compDir, filePath)).slice(1);
70
+ if (!needsRewrite(filePath) && (!assetExists || !assetExists(sibling))) return relativePath;
71
+ return encodeUrlPath(sibling) + suffix;
41
72
  }
42
73
  function rewriteAssetPaths(elements, compSrcPath, getAttr, setAttr, assetExists) {
43
74
  for (const el of elements) {
@@ -74,11 +105,16 @@ function rewriteCssAssetUrls(cssText, compSrcPath, assetExists) {
74
105
  export {
75
106
  CSS_URL_RE,
76
107
  PATH_ATTRS,
108
+ decodeCssEscapes,
109
+ decodeWellFormedEscapes,
110
+ decodedUrlPath,
111
+ encodeUrlPath,
77
112
  isNonRelativeUrl,
78
113
  isPathInside,
79
114
  rewriteAssetPath,
80
115
  rewriteAssetPaths,
81
116
  rewriteCssAssetUrls,
82
- rewriteInlineStyleAssetUrls
117
+ rewriteInlineStyleAssetUrls,
118
+ splitUrlSuffix
83
119
  };
84
120
  //# sourceMappingURL=assets.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/assetPaths.ts","../src/rewriteSubCompPaths.ts"],"sourcesContent":["/**\n * Shared primitives for scanning and rewriting asset paths in HTML/CSS.\n *\n * Used by: rewriteSubCompPaths (core), collectExternalAssets (producer),\n * localizeExternalAssets (CLI publish).\n */\n\nimport { isAbsolute, relative, resolve } from \"node:path\";\n\n/**\n * Regex matching CSS `url(...)` references — captures the quote style and the\n * raw URL. The URL group is anchored to non-whitespace at both ends so the\n * surrounding `\\s*` can never overlap it (avoids polynomial-ReDoS backtracking);\n * the captured value is whitespace-bounded already, matching the old behavior\n * after callers `.trim()` it.\n */\nexport const CSS_URL_RE = /\\burl\\(\\s*([\"']?)([^)\"'\\s](?:[^)\"']*[^)\"'\\s])?)\\1\\s*\\)/g;\n\n/** Attributes that may contain relative asset paths. */\nexport const PATH_ATTRS = [\"src\", \"href\"] as const;\n\n/** Returns true for URLs/prefixes that should never be rewritten. */\nexport function isNonRelativeUrl(val: string): boolean {\n return (\n !val ||\n val.startsWith(\"http://\") ||\n val.startsWith(\"https://\") ||\n val.startsWith(\"//\") ||\n val.startsWith(\"data:\") ||\n val.startsWith(\"#\") ||\n val.startsWith(\"/\")\n );\n}\n\n/**\n * Cross-platform containment check: is `childPath` inside `parentPath`?\n * Equality counts as \"inside\".\n */\nexport function isPathInside(childPath: string, parentPath: string): boolean {\n const absChild = resolve(childPath);\n const absParent = resolve(parentPath);\n if (absChild === absParent) return true;\n const rel = relative(absParent, absChild);\n return rel !== \"\" && !rel.startsWith(\"..\") && !isAbsolute(rel);\n}\n","/**\n * Rewrite relative asset paths in sub-composition content so they resolve\n * correctly after the content is inlined into the root document.\n *\n * A sub-composition at \"compositions/scene.html\" referencing \"../icon.svg\"\n * means the project root — but after inlining into root index.html, the\n * \"../\" escapes the project directory and causes 404s. This function\n * resolves each relative path against the sub-composition's directory,\n * then normalizes it to be relative to the project root.\n *\n * Used by both the core bundler (preview) and the producer compiler (render)\n * to ensure consistent behavior.\n */\n\n// URL paths in HTML output are POSIX regardless of host OS — use the `posix`\n// submodule so Windows builds don't emit backslash-separated paths (or worse,\n// drive-letter-prefixed artifacts from `resolve(\"/\", ...)`).\nimport { posix } from \"path\";\nconst { join, resolve, dirname } = posix;\n\nimport { CSS_URL_RE, PATH_ATTRS, isNonRelativeUrl } from \"./assetPaths.js\";\n\nconst isAbsoluteOrSpecial = isNonRelativeUrl;\n\n/**\n * Returns true only for paths that traverse up with `../`.\n * Plain relative paths like `assets/foo.svg` are ambiguous: two conventions are\n * in the wild, and only the filesystem can tell them apart — see `AssetExists`.\n */\nfunction needsRewrite(val: string): boolean {\n return val.startsWith(\"../\") || val === \"..\";\n}\n\n/**\n * Probe for \"does this project-root-relative path exist?\", supplied by callers\n * that can see the filesystem (the studio preview builder, the bundler, the\n * producer). Keeps this module free of `node:fs` so it stays browser-safe.\n *\n * It disambiguates the two conventions for a plain relative path authored in a\n * sub-composition that lives in a subdirectory:\n *\n * 1. A SIBLING file — `<link href=\"_shared.css\">` next to the composition.\n * This is what a browser resolves when the file is opened directly, and\n * what an author means. Once the content moves into the project-root\n * document it must become `design/styleframes/_shared.css` or it 404s.\n * 2. A PROJECT-ROOT asset — registry blocks are installed into a\n * subdirectory but reference `assets/logo.png` at the root. Already\n * correct in the root document; rewriting would break it.\n *\n * A sibling that exists on disk means (1); anything else is left as authored.\n * Without a probe the behavior is unchanged — plain relative paths pass through.\n */\nexport type AssetExists = (projectRelativePath: string) => boolean;\n\n/** Split `foo.png?v=2#frag` into its path and its `?`/`#` suffix. */\nfunction splitPathSuffix(value: string): [string, string] {\n const marker = value.search(/[?#]/);\n return marker === -1 ? [value, \"\"] : [value.slice(0, marker), value.slice(marker)];\n}\n\n/**\n * Rewrite a single relative path from a sub-composition's context to the\n * project root context.\n *\n * @param compSrcPath - The `data-composition-src` value (e.g. \"compositions/scene.html\")\n * @param relativePath - The asset path to rewrite (e.g. \"../icon.svg\")\n * @param assetExists - Optional filesystem probe; see `AssetExists`.\n * @returns The rewritten path relative to project root (e.g. \"icon.svg\"), or\n * the original path if no rewriting is needed.\n */\nexport function rewriteAssetPath(\n compSrcPath: string,\n relativePath: string,\n assetExists?: AssetExists,\n): string {\n if (isAbsoluteOrSpecial(relativePath)) return relativePath;\n const compDir = dirname(compSrcPath);\n if (!compDir || compDir === \".\") return relativePath;\n if (!needsRewrite(relativePath)) {\n if (!assetExists) return relativePath;\n const [filePart, suffix] = splitPathSuffix(relativePath);\n if (!filePart) return relativePath;\n const sibling = resolve(\"/\", join(compDir, filePart)).slice(1);\n return assetExists(sibling) ? sibling + suffix : relativePath;\n }\n const resolved = join(compDir, relativePath);\n const normalized = resolve(\"/\", resolved).slice(1);\n return normalized;\n}\n\n/**\n * Rewrite all relative `src` and `href` attributes on elements within a\n * DOM tree, adjusting paths from the sub-composition's directory context\n * to the project root.\n *\n * @param elements - Iterable of DOM elements to scan (e.g. from querySelectorAll)\n * @param compSrcPath - The `data-composition-src` value\n * @param getAttr - Function to read an attribute from an element\n * @param setAttr - Function to set an attribute on an element\n */\nexport function rewriteAssetPaths<T>(\n elements: Iterable<T>,\n compSrcPath: string,\n getAttr: (el: T, attr: string) => string | null | undefined,\n setAttr: (el: T, attr: string, value: string) => void,\n assetExists?: AssetExists,\n): void {\n for (const el of elements) {\n for (const attr of PATH_ATTRS) {\n const val = (getAttr(el, attr) || \"\").trim();\n const rewritten = rewriteAssetPath(compSrcPath, val, assetExists);\n if (rewritten !== val) {\n setAttr(el, attr, rewritten);\n }\n }\n }\n}\n\n/**\n * Rewrite CSS url(...) references inside inline style attributes.\n */\nexport function rewriteInlineStyleAssetUrls<T>(\n elements: Iterable<T>,\n compSrcPath: string,\n getStyle: (el: T) => string | null | undefined,\n setStyle: (el: T, value: string) => void,\n assetExists?: AssetExists,\n): void {\n const compDir = dirname(compSrcPath);\n if (!compDir || compDir === \".\") return;\n\n for (const el of elements) {\n const style = getStyle(el);\n if (!style) continue;\n const rewritten = rewriteCssAssetUrls(style, compSrcPath, assetExists);\n if (rewritten !== style) {\n setStyle(el, rewritten);\n }\n }\n}\n\n/**\n * Rewrite CSS url(...) references in a sub-composition's inline styles so\n * ../foo.woff2 remains valid after the CSS is hoisted into the root document.\n */\nexport function rewriteCssAssetUrls(\n cssText: string,\n compSrcPath: string,\n assetExists?: AssetExists,\n): string {\n if (!cssText) return cssText;\n return cssText.replace(CSS_URL_RE, (full, quote: string, rawUrl: string) => {\n const urlValue = (rawUrl || \"\").trim();\n const rewritten = rewriteAssetPath(compSrcPath, urlValue, assetExists);\n if (rewritten === urlValue) return full;\n return `url(${quote || \"\"}${rewritten}${quote || \"\"})`;\n });\n}\n"],"mappings":";AAOA,SAAS,YAAY,UAAU,eAAe;AASvC,IAAM,aAAa;AAGnB,IAAM,aAAa,CAAC,OAAO,MAAM;AAGjC,SAAS,iBAAiB,KAAsB;AACrD,SACE,CAAC,OACD,IAAI,WAAW,SAAS,KACxB,IAAI,WAAW,UAAU,KACzB,IAAI,WAAW,IAAI,KACnB,IAAI,WAAW,OAAO,KACtB,IAAI,WAAW,GAAG,KAClB,IAAI,WAAW,GAAG;AAEtB;AAMO,SAAS,aAAa,WAAmB,YAA6B;AAC3E,QAAM,WAAW,QAAQ,SAAS;AAClC,QAAM,YAAY,QAAQ,UAAU;AACpC,MAAI,aAAa,UAAW,QAAO;AACnC,QAAM,MAAM,SAAS,WAAW,QAAQ;AACxC,SAAO,QAAQ,MAAM,CAAC,IAAI,WAAW,IAAI,KAAK,CAAC,WAAW,GAAG;AAC/D;;;AC3BA,SAAS,aAAa;AACtB,IAAM,EAAE,MAAM,SAAAA,UAAS,QAAQ,IAAI;AAInC,IAAM,sBAAsB;AAO5B,SAAS,aAAa,KAAsB;AAC1C,SAAO,IAAI,WAAW,KAAK,KAAK,QAAQ;AAC1C;AAwBA,SAAS,gBAAgB,OAAiC;AACxD,QAAM,SAAS,MAAM,OAAO,MAAM;AAClC,SAAO,WAAW,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,MAAM,GAAG,MAAM,GAAG,MAAM,MAAM,MAAM,CAAC;AACnF;AAYO,SAAS,iBACd,aACA,cACA,aACQ;AACR,MAAI,oBAAoB,YAAY,EAAG,QAAO;AAC9C,QAAM,UAAU,QAAQ,WAAW;AACnC,MAAI,CAAC,WAAW,YAAY,IAAK,QAAO;AACxC,MAAI,CAAC,aAAa,YAAY,GAAG;AAC/B,QAAI,CAAC,YAAa,QAAO;AACzB,UAAM,CAAC,UAAU,MAAM,IAAI,gBAAgB,YAAY;AACvD,QAAI,CAAC,SAAU,QAAO;AACtB,UAAM,UAAUA,SAAQ,KAAK,KAAK,SAAS,QAAQ,CAAC,EAAE,MAAM,CAAC;AAC7D,WAAO,YAAY,OAAO,IAAI,UAAU,SAAS;AAAA,EACnD;AACA,QAAM,WAAW,KAAK,SAAS,YAAY;AAC3C,QAAM,aAAaA,SAAQ,KAAK,QAAQ,EAAE,MAAM,CAAC;AACjD,SAAO;AACT;AAYO,SAAS,kBACd,UACA,aACA,SACA,SACA,aACM;AACN,aAAW,MAAM,UAAU;AACzB,eAAW,QAAQ,YAAY;AAC7B,YAAM,OAAO,QAAQ,IAAI,IAAI,KAAK,IAAI,KAAK;AAC3C,YAAM,YAAY,iBAAiB,aAAa,KAAK,WAAW;AAChE,UAAI,cAAc,KAAK;AACrB,gBAAQ,IAAI,MAAM,SAAS;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AACF;AAKO,SAAS,4BACd,UACA,aACA,UACA,UACA,aACM;AACN,QAAM,UAAU,QAAQ,WAAW;AACnC,MAAI,CAAC,WAAW,YAAY,IAAK;AAEjC,aAAW,MAAM,UAAU;AACzB,UAAM,QAAQ,SAAS,EAAE;AACzB,QAAI,CAAC,MAAO;AACZ,UAAM,YAAY,oBAAoB,OAAO,aAAa,WAAW;AACrE,QAAI,cAAc,OAAO;AACvB,eAAS,IAAI,SAAS;AAAA,IACxB;AAAA,EACF;AACF;AAMO,SAAS,oBACd,SACA,aACA,aACQ;AACR,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,QAAQ,QAAQ,YAAY,CAAC,MAAM,OAAe,WAAmB;AAC1E,UAAM,YAAY,UAAU,IAAI,KAAK;AACrC,UAAM,YAAY,iBAAiB,aAAa,UAAU,WAAW;AACrE,QAAI,cAAc,SAAU,QAAO;AACnC,WAAO,OAAO,SAAS,EAAE,GAAG,SAAS,GAAG,SAAS,EAAE;AAAA,EACrD,CAAC;AACH;","names":["resolve"]}
1
+ {"version":3,"sources":["../src/assetPaths.ts","../src/utils/urlPath.ts","../src/rewriteSubCompPaths.ts"],"sourcesContent":["/**\n * Shared primitives for scanning and rewriting asset paths in HTML/CSS.\n *\n * Used by: rewriteSubCompPaths (core), collectExternalAssets (producer),\n * localizeExternalAssets (CLI publish).\n */\n\nimport { isAbsolute, relative, resolve } from \"node:path\";\n\n/**\n * Regex matching CSS `url(...)` references — captures the quote style and the\n * raw URL. The URL group is anchored to non-whitespace at both ends so the\n * surrounding `\\s*` can never overlap it (avoids polynomial-ReDoS backtracking);\n * the captured value is whitespace-bounded already, matching the old behavior\n * after callers `.trim()` it.\n */\nexport const CSS_URL_RE = /\\burl\\(\\s*([\"']?)([^)\"'\\s](?:[^)\"']*[^)\"'\\s])?)\\1\\s*\\)/g;\n\n/** Attributes that may contain relative asset paths. */\nexport const PATH_ATTRS = [\"src\", \"href\"] as const;\n\n/** Returns true for URLs/prefixes that should never be rewritten. */\nexport function isNonRelativeUrl(val: string): boolean {\n return (\n !val ||\n val.startsWith(\"http://\") ||\n val.startsWith(\"https://\") ||\n val.startsWith(\"//\") ||\n val.startsWith(\"data:\") ||\n val.startsWith(\"#\") ||\n val.startsWith(\"/\")\n );\n}\n\n/**\n * Cross-platform containment check: is `childPath` inside `parentPath`?\n * Equality counts as \"inside\".\n */\nexport function isPathInside(childPath: string, parentPath: string): boolean {\n const absChild = resolve(childPath);\n const absParent = resolve(parentPath);\n if (absChild === absParent) return true;\n const rel = relative(absParent, absChild);\n return rel !== \"\" && !rel.startsWith(\"..\") && !isAbsolute(rel);\n}\n\nexport {\n decodeCssEscapes,\n decodeWellFormedEscapes,\n decodedUrlPath,\n splitUrlSuffix,\n encodeUrlPath,\n} from \"./utils/urlPath.js\";\n","export function decodeUrlPathVariants(path: string): string[] {\n const decoded = decodeWellFormedEscapes(path);\n return decoded === path ? [path] : [decoded, path];\n}\n\n// A browser sends src=\"100%.png\" as-is, so a % without two hex digits after it is the file's own.\nexport function decodeWellFormedEscapes(path: string): string {\n return path.replace(/(?:%[0-9A-Fa-f]{2})+/g, (run) => {\n try {\n return decodeURIComponent(run);\n } catch {\n return run;\n }\n });\n}\n\nexport function splitUrlSuffix(urlValue: string): { basePath: string; suffix: string } {\n const queryIdx = urlValue.indexOf(\"?\");\n const hashIdx = urlValue.indexOf(\"#\");\n if (queryIdx < 0 && hashIdx < 0) return { basePath: urlValue, suffix: \"\" };\n let cutIdx = urlValue.length;\n if (queryIdx >= 0) cutIdx = Math.min(cutIdx, queryIdx);\n if (hashIdx >= 0) cutIdx = Math.min(cutIdx, hashIdx);\n return { basePath: urlValue.slice(0, cutIdx), suffix: urlValue.slice(cutIdx) };\n}\n\nexport function decodedUrlPath(url: string): string {\n return decodeWellFormedEscapes(splitUrlSuffix(url).basePath);\n}\n\nexport function encodeUrlPath(path: string): string {\n return encodeURIComponent(path)\n .replace(/%2F/g, \"/\")\n .replace(/['()]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);\n}\n\nexport function decodeCssEscapes(text: string): string {\n return text\n .replaceAll(\"\\0\", \"\\uFFFD\")\n .replace(\n /\\\\(?:([0-9a-f]{1,6})(?:\\r\\n|[ \\t\\r\\n\\f])?|(\\r\\n|[\\n\\r\\f])|([\\s\\S]))/gi,\n (\n _escaped,\n hex: string | undefined,\n lineBreak: string | undefined,\n char: string | undefined,\n ) => {\n if (hex === undefined) return lineBreak === undefined ? char! : \"\";\n const code = Number.parseInt(hex, 16);\n if (code === 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) return \"\\uFFFD\";\n return String.fromCodePoint(code);\n },\n );\n}\n","/**\n * Rewrite relative asset paths in sub-composition content so they resolve\n * correctly after the content is inlined into the root document.\n *\n * A sub-composition at \"compositions/scene.html\" referencing \"../icon.svg\"\n * means the project root — but after inlining into root index.html, the\n * \"../\" escapes the project directory and causes 404s. This function\n * resolves each relative path against the sub-composition's directory,\n * then normalizes it to be relative to the project root.\n *\n * Used by both the core bundler (preview) and the producer compiler (render)\n * to ensure consistent behavior.\n */\n\n// URL paths in HTML output are POSIX regardless of host OS — use the `posix`\n// submodule so Windows builds don't emit backslash-separated paths (or worse,\n// drive-letter-prefixed artifacts from `resolve(\"/\", ...)`).\nimport { posix } from \"path\";\nconst { join, resolve, dirname } = posix;\n\nimport {\n CSS_URL_RE,\n PATH_ATTRS,\n isNonRelativeUrl,\n splitUrlSuffix,\n decodeWellFormedEscapes,\n encodeUrlPath,\n} from \"./assetPaths.js\";\n\nconst isAbsoluteOrSpecial = isNonRelativeUrl;\n\n/**\n * Returns true only for paths that traverse up with `../`.\n * Plain relative paths like `assets/foo.svg` are ambiguous: two conventions are\n * in the wild, and only the filesystem can tell them apart — see `AssetExists`.\n */\nfunction needsRewrite(val: string): boolean {\n return val.startsWith(\"../\") || val === \"..\";\n}\n\n/**\n * Probe for \"does this project-root-relative path exist?\", supplied by callers\n * that can see the filesystem (the studio preview builder, the bundler, the\n * producer). Keeps this module free of `node:fs` so it stays browser-safe.\n *\n * It disambiguates the two conventions for a plain relative path authored in a\n * sub-composition that lives in a subdirectory:\n *\n * 1. A SIBLING file — `<link href=\"_shared.css\">` next to the composition.\n * This is what a browser resolves when the file is opened directly, and\n * what an author means. Once the content moves into the project-root\n * document it must become `design/styleframes/_shared.css` or it 404s.\n * 2. A PROJECT-ROOT asset — registry blocks are installed into a\n * subdirectory but reference `assets/logo.png` at the root. Already\n * correct in the root document; rewriting would break it.\n *\n * A sibling that exists on disk means (1); anything else is left as authored.\n * Without a probe the behavior is unchanged — plain relative paths pass through.\n */\nexport type AssetExists = (projectRelativePath: string) => boolean;\n\n/**\n * Rewrite a single relative path from a sub-composition's context to the\n * project root context.\n *\n * @param compSrcPath - The `data-composition-src` value (e.g. \"compositions/scene.html\")\n * @param relativePath - The asset path to rewrite (e.g. \"../icon.svg\")\n * @param assetExists - Optional filesystem probe; see `AssetExists`.\n * @returns The rewritten path relative to project root (e.g. \"icon.svg\"), or\n * the original path if no rewriting is needed.\n */\nexport function rewriteAssetPath(\n compSrcPath: string,\n relativePath: string,\n assetExists?: AssetExists,\n): string {\n if (isAbsoluteOrSpecial(relativePath)) return relativePath;\n const compDir = dirname(compSrcPath);\n if (!compDir || compDir === \".\") return relativePath;\n const { basePath, suffix } = splitUrlSuffix(relativePath);\n if (!basePath) return relativePath;\n const filePath = decodeWellFormedEscapes(basePath);\n const sibling = resolve(\"/\", join(compDir, filePath)).slice(1);\n if (!needsRewrite(filePath) && (!assetExists || !assetExists(sibling))) return relativePath;\n return encodeUrlPath(sibling) + suffix;\n}\n\n/**\n * Rewrite all relative `src` and `href` attributes on elements within a\n * DOM tree, adjusting paths from the sub-composition's directory context\n * to the project root.\n *\n * @param elements - Iterable of DOM elements to scan (e.g. from querySelectorAll)\n * @param compSrcPath - The `data-composition-src` value\n * @param getAttr - Function to read an attribute from an element\n * @param setAttr - Function to set an attribute on an element\n */\nexport function rewriteAssetPaths<T>(\n elements: Iterable<T>,\n compSrcPath: string,\n getAttr: (el: T, attr: string) => string | null | undefined,\n setAttr: (el: T, attr: string, value: string) => void,\n assetExists?: AssetExists,\n): void {\n for (const el of elements) {\n for (const attr of PATH_ATTRS) {\n const val = (getAttr(el, attr) || \"\").trim();\n const rewritten = rewriteAssetPath(compSrcPath, val, assetExists);\n if (rewritten !== val) {\n setAttr(el, attr, rewritten);\n }\n }\n }\n}\n\n/**\n * Rewrite CSS url(...) references inside inline style attributes.\n */\nexport function rewriteInlineStyleAssetUrls<T>(\n elements: Iterable<T>,\n compSrcPath: string,\n getStyle: (el: T) => string | null | undefined,\n setStyle: (el: T, value: string) => void,\n assetExists?: AssetExists,\n): void {\n const compDir = dirname(compSrcPath);\n if (!compDir || compDir === \".\") return;\n\n for (const el of elements) {\n const style = getStyle(el);\n if (!style) continue;\n const rewritten = rewriteCssAssetUrls(style, compSrcPath, assetExists);\n if (rewritten !== style) {\n setStyle(el, rewritten);\n }\n }\n}\n\n/**\n * Rewrite CSS url(...) references in a sub-composition's inline styles so\n * ../foo.woff2 remains valid after the CSS is hoisted into the root document.\n */\nexport function rewriteCssAssetUrls(\n cssText: string,\n compSrcPath: string,\n assetExists?: AssetExists,\n): string {\n if (!cssText) return cssText;\n return cssText.replace(CSS_URL_RE, (full, quote: string, rawUrl: string) => {\n const urlValue = (rawUrl || \"\").trim();\n const rewritten = rewriteAssetPath(compSrcPath, urlValue, assetExists);\n if (rewritten === urlValue) return full;\n return `url(${quote || \"\"}${rewritten}${quote || \"\"})`;\n });\n}\n"],"mappings":"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","names":["resolve"]}
@@ -1,5 +1,6 @@
1
1
  import { C as CompositionVariable } from './types-Ejqndkej.js';
2
2
  export { A as AddElementData, b as Asset, B as BooleanVariable, c as CANVAS_DIMENSIONS, d as COMPOSITION_VARIABLE_TYPES, a as CanvasResolution, e as ColorVariable, f as CompositionAPI, g as CompositionAsset, h as CompositionSpec, i as CompositionVariableBase, j as CompositionVariableType, D as DEFAULT_DURATIONS, E as ElementKeyframes, k as EnumVariable, F as FontVariable, I as ImageVariable, K as Keyframe, l as KeyframeProperties, M as MediaElementType, m as MediaFile, N as NumberVariable, P as PlayerAPI, R as ResolvedResolutionFlag, n as StageZoom, S as StageZoomKeyframe, o as StringVariable, p as TIMELINE_COLORS, q as TimelineCompositionElement, T as TimelineElement, r as TimelineElementBase, s as TimelineElementType, t as TimelineMediaElement, u as TimelineTextElement, v as VALID_CANVAS_RESOLUTIONS, V as ValidationResult, W as WaveformData, w as getDefaultStageZoom, x as isAspectAgnosticResolutionAlias, y as isCompositionElement, z as isMediaElement, G as isTextElement, H as normalizeResolutionFlag, J as resolveResolutionFlagPair } from './types-Ejqndkej.js';
3
+ export { c as decodeUrlPathVariants } from './urlPath-C_0f_-Hp.js';
3
4
  export { CANONICAL_AUTHORED_TIMING_ATTRIBUTES, COMPOSITION_ATTRIBUTES, COMPOSITION_CONTRACT_VERSION, ClipAttributeReader, ClipAttributeWriter, ClipTiming, ClipTimingDiagnostic, ClipTimingDiagnosticCode, ClipTimingUpdate, ClipTimingWriteError, DERIVED_TIMING_ATTRIBUTES, LEGACY_TIMING_ATTRIBUTES, ReadClipTimingOptions, ReferenceExpression, parseNumeric, parseStartExpression, readClipTiming, writeClipTiming } from './compositionContract.js';
4
5
 
5
6
  /**
@@ -56,8 +57,6 @@ declare const HTML_BODY_CSS_HEIGHT_FIRST_RE: RegExp;
56
57
  /** Viewport meta content. Shared by lint (reads groups 2/4) and cli (replaces via 1/3). */
57
58
  declare const VIEWPORT_META_SIZE_RE: RegExp;
58
59
 
59
- declare function decodeUrlPathVariants(path: string): string[];
60
-
61
60
  /**
62
61
  * Browser-safe static scan for composition-variable reads in script text.
63
62
  *
@@ -184,4 +183,4 @@ declare const CANONICAL_FONT_DISPLAY_NAMES: Readonly<Record<string, string>>;
184
183
  */
185
184
  declare function resolveAliasDisplayName(alias: string): string | undefined;
186
185
 
187
- export { CANONICAL_FONT_DISPLAY_NAMES, CompositionVariable, FONT_ALIAS_KEYS, FONT_ALIAS_MAP, HTML_BODY_CSS_HEIGHT_FIRST_RE, HTML_BODY_CSS_WIDTH_FIRST_RE, type MediaSrcMutation, VIEWPORT_META_SIZE_RE, type VariableUsageScan, decodeUrlPathVariants, extractMediaSrcMutations, isCompositionVariable, isSafeMediaUrl, isScalarVariableValue, parseCompositionVariables, resolveAliasDisplayName, scanVariableUsage };
186
+ export { CANONICAL_FONT_DISPLAY_NAMES, CompositionVariable, FONT_ALIAS_KEYS, FONT_ALIAS_MAP, HTML_BODY_CSS_HEIGHT_FIRST_RE, HTML_BODY_CSS_WIDTH_FIRST_RE, type MediaSrcMutation, VIEWPORT_META_SIZE_RE, type VariableUsageScan, extractMediaSrcMutations, isCompositionVariable, isSafeMediaUrl, isScalarVariableValue, parseCompositionVariables, resolveAliasDisplayName, scanVariableUsage };
@@ -195,13 +195,17 @@ function resolveAliasDisplayName(alias) {
195
195
 
196
196
  // src/utils/urlPath.ts
197
197
  function decodeUrlPathVariants(path) {
198
- const variants = [path];
199
- try {
200
- const decoded = decodeURIComponent(path);
201
- if (decoded !== path) variants.unshift(decoded);
202
- } catch {
203
- }
204
- return variants;
198
+ const decoded = decodeWellFormedEscapes(path);
199
+ return decoded === path ? [path] : [decoded, path];
200
+ }
201
+ function decodeWellFormedEscapes(path) {
202
+ return path.replace(/(?:%[0-9A-Fa-f]{2})+/g, (run) => {
203
+ try {
204
+ return decodeURIComponent(run);
205
+ } catch {
206
+ return run;
207
+ }
208
+ });
205
209
  }
206
210
 
207
211
  // src/compositionVariables.ts