@hyperframes/parsers 0.8.61 → 0.8.63

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.
@@ -1,3 +1,10 @@
1
+ /** The subset of `node:path` that `isWithinProjectRoot` needs to run under
2
+ * an injected platform (tests pass `path.win32` / `path.posix`). */
3
+ interface PathModuleLike {
4
+ resolve: (...segments: string[]) => string;
5
+ relative: (from: string, to: string) => string;
6
+ isAbsolute: (path: string) => boolean;
7
+ }
1
8
  /**
2
9
  * Shared local-asset resolution helpers for every package that maps
3
10
  * composition asset URLs to files on disk (lint project rules, the HEVC
@@ -63,7 +70,13 @@ declare function isUnresolvedAssetPlaceholder(rawSrc: string): boolean;
63
70
  */
64
71
  declare function collectSubCompositionSrcs(html: string): string[];
65
72
  declare function cleanAssetUrl(url: string): string;
66
- declare function isWithinProjectRoot(projectDir: string, candidate: string): boolean;
73
+ /**
74
+ * `pathModule` defaults to the host's native `node:path`, so every existing
75
+ * caller gets its actual OS's separator and drive-letter rules unchanged.
76
+ * Tests inject `path.win32` / `path.posix` to exercise both platforms' rules
77
+ * from a single OS (same pattern as producer/fileServer.ts's `isPathInside`).
78
+ */
79
+ declare function isWithinProjectRoot(projectDir: string, candidate: string, pathModule?: PathModuleLike): boolean;
67
80
  declare function resolveLocalAssetCandidates(projectDir: string, url: string): string[];
68
81
  declare function resolveExistingLocalAsset(projectDir: string, url: string): {
69
82
  resolved: string;
@@ -49,10 +49,10 @@ function collectSubCompositionSrcs(html) {
49
49
  function cleanAssetUrl(url) {
50
50
  return url.trim().split(/[?#]/, 1)[0] ?? "";
51
51
  }
52
- function isWithinProjectRoot(projectDir, candidate) {
53
- const projectRoot = resolve(projectDir);
54
- const relativePath = relative(projectRoot, candidate);
55
- return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
52
+ function isWithinProjectRoot(projectDir, candidate, pathModule = { resolve, relative, isAbsolute }) {
53
+ const projectRoot = pathModule.resolve(projectDir);
54
+ const relativePath = pathModule.relative(projectRoot, candidate);
55
+ return relativePath === "" || !relativePath.startsWith("..") && !pathModule.isAbsolute(relativePath);
56
56
  }
57
57
  function addCandidate(candidates, candidate) {
58
58
  if (!candidates.includes(candidate)) candidates.push(candidate);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/assetResolution.ts","../src/utils/urlPath.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { isAbsolute, join, posix, relative, resolve, sep } from \"node:path\";\nimport { decodeUrlPathVariants } from \"./composition.js\";\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\nexport function isWithinProjectRoot(projectDir: string, candidate: string): boolean {\n const projectRoot = resolve(projectDir);\n const relativePath = relative(projectRoot, candidate);\n return relativePath === \"\" || (!relativePath.startsWith(\"..\") && !isAbsolute(relativePath));\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","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/assetResolution.ts","../src/utils/urlPath.ts"],"sourcesContent":["import { existsSync } 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","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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/parsers",
3
- "version": "0.8.61",
3
+ "version": "0.8.63",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/heygen-com/hyperframes",
@@ -114,7 +114,7 @@
114
114
  "tsx": "^4.21.0",
115
115
  "typescript": "^5.0.0",
116
116
  "vitest": "^4.1.11",
117
- "@hyperframes/core": "0.8.61"
117
+ "@hyperframes/core": "0.8.63"
118
118
  },
119
119
  "scripts": {
120
120
  "build": "tsup",