fix(lint): a media src whose file name holds # or ? says to write it percent-encoded (#6495)

* fix(lint): a media src whose file name holds # or ? says to write it percent-encoded

* refactor(lint): the url-safe hint encodes with the shared encodeUrlPath
This commit is contained in:
Miguel Ángel
2026-10-27 01:10:49 +00:00
committed by GitHub
parent 312a8fd9c7
commit 5a28b4e7eb
2 changed files with 75 additions and 9 deletions
+37
View File
@@ -1236,3 +1236,40 @@ describe("sub-composition files", () => {
);
});
});
describe("a media file whose own name holds # or ?", () => {
const name = "track #1.mp3";
async function findingFor(tag: string, code: string) {
const project = makeProject(`<html><body>
<div data-composition-id="main" data-width="1920" data-height="1080" data-start="0" data-duration="10"></div>
${tag}
</body></html>`);
mkdirSync(join(project, "assets"), { recursive: true });
writeFileSync(join(project, "assets", name), "");
writeFileSync(join(project, "assets", "pic #2.png"), "");
const { results } = await lintProject(project);
return results.flatMap((entry) => entry.result.findings).find((f) => f.code === code);
}
it("tells an audio src the name is cut at the # and how to write it", async () => {
const finding = await findingFor(
`<audio id="a1" class="clip" data-start="0" data-duration="3" data-track-index="10" src="assets/${name}"></audio>`,
"audio_src_not_found",
);
expect(finding?.fixHint).toContain('"assets/track%20%231.mp3"');
});
it("tells an image src the same", async () => {
const finding = await findingFor(`<img src="assets/pic #2.png" />`, "missing_local_asset");
expect(finding?.fixHint).toContain('"assets/pic%20%232.png"');
});
it("keeps the add-the-file hint when no file by that name exists", async () => {
const finding = await findingFor(
`<audio id="a1" class="clip" data-start="0" data-duration="3" data-track-index="10" src="assets/nope #1.mp3"></audio>`,
"audio_src_not_found",
);
expect(finding?.fixHint).toContain("Add the file");
});
});
+38 -9
View File
@@ -33,6 +33,7 @@ import {
resolveLocalAssetCandidates,
resolveProjectRelativeSrc,
} from "@hyperframes/parsers/asset-resolution";
import { encodeUrlPath } from "@hyperframes/parsers";
import {
collectLocalVideoCandidates,
lintHevcPreviewCodec,
@@ -385,6 +386,18 @@ function lintProjectAudioFiles(
return findings;
}
/** The src that loads a project file whose own name holds `#` or `?`, which a URL cuts the name at; null when the raw
* src names no such file. */
function urlSafeSrcOf(projectDir: string, rootRelative: string): string | null {
if (!/[?#]/.test(rootRelative)) return null;
const literal = resolve(projectDir, rootRelative.replace(/^\//, ""));
if (!existsSync(literal)) return null;
return encodeUrlPath(rootRelative);
}
const urlSafeHint = (safe: string) =>
`The file exists, but a "#" or "?" in a src ends the file name there. Write the src as "${safe}".`;
function lintAudioSrcNotFound(
projectDir: string,
htmlSources: HtmlSource[],
@@ -394,6 +407,7 @@ function lintAudioSrcNotFound(
const audioSrcRe = mediaSrcTagRe("audio");
const missingByFile = new Map<string, Set<string>>();
const urlSafe = new Map<string, string>();
for (const { html, compSrcPath } of htmlSources) {
const scannable = maskNonScannableRanges(html);
let match: RegExpExecArray | null;
@@ -405,6 +419,8 @@ function lintAudioSrcNotFound(
? rewriteAssetPath(compSrcPath, src, (path) => existsSync(join(projectDir, path)))
: src;
if (!existsSync(resolveProjectRelativeSrc(rootRelative, projectDir))) {
const safe = urlSafeSrcOf(projectDir, rootRelative);
if (safe) urlSafe.set(src, safe);
const file = sourceFile(projectDir, compSrcPath);
missingByFile.set(file, (missingByFile.get(file) ?? new Set()).add(src));
}
@@ -419,9 +435,11 @@ function lintAudioSrcNotFound(
file,
message: `<audio> element references file(s) not found in the project: ${unique.join(", ")}. The rendered video will be silent.`,
fixHint:
unique.length === 1
? `Add the file "${unique[0]}" to the project directory, or update the src attribute to point to an existing file.`
: `Add the missing files to the project directory, or update the src attributes to point to existing files.`,
unique.length === 1 && urlSafe.has(unique[0]!)
? urlSafeHint(urlSafe.get(unique[0]!)!)
: unique.length === 1
? `Add the file "${unique[0]}" to the project directory, or update the src attribute to point to an existing file.`
: `Add the missing files to the project directory, or update the src attributes to point to existing files.`,
});
}
@@ -441,6 +459,7 @@ function lintMissingLocalAsset(
string,
{ file: string; tagName: string; byResolved: Map<string, string> }
>();
const urlSafe = new Map<string, string>();
for (const { html, compSrcPath } of htmlSources) {
const scannable = maskNonScannableRanges(html);
@@ -460,6 +479,14 @@ function lintMissingLocalAsset(
const resolvedAsset = resolveExistingLocalAsset(projectDir, rootRelative);
if (resolvedAsset) continue;
const rawRoot = rawSrc.trim();
const safe = urlSafeSrcOf(
projectDir,
compSrcPath
? rewriteAssetPath(compSrcPath, rawRoot, (path) => existsSync(join(projectDir, path)))
: rawRoot,
);
if (safe) urlSafe.set(src, safe);
const resolvedKey = resolve(projectDir, rootRelative);
const file = sourceFile(projectDir, compSrcPath);
const bucketKey = `${file}\0${tagName}`;
@@ -482,12 +509,14 @@ function lintMissingLocalAsset(
`<${tagName}> element references local file(s) not found in the project: ${unique.join(", ")}. ` +
"The renderer will silently skip these and produce a video with missing visuals.",
fixHint:
unique.length === 1
? `Add "${unique[0]}" to the project directory, or update the src attribute to point to an existing file. ` +
"Common cause: captured asset filenames are unreliable (heygen-logo.svg often contains Google, nvidia-logo.svg may contain Autodesk, etc.). " +
"Open the contact sheets and verify the file actually exists at this path before referencing it."
: "Add the missing files to the project directory, or update the src attributes to point to existing files. " +
"Captured asset filenames are unreliable. Verify against capture/contact-sheets/ and capture/extracted/asset-descriptions.md.",
unique.length === 1 && urlSafe.has(unique[0]!)
? urlSafeHint(urlSafe.get(unique[0]!)!)
: unique.length === 1
? `Add "${unique[0]}" to the project directory, or update the src attribute to point to an existing file. ` +
"Common cause: captured asset filenames are unreliable (heygen-logo.svg often contains Google, nvidia-logo.svg may contain Autodesk, etc.). " +
"Open the contact sheets and verify the file actually exists at this path before referencing it."
: "Add the missing files to the project directory, or update the src attributes to point to existing files. " +
"Captured asset filenames are unreliable. Verify against capture/contact-sheets/ and capture/extracted/asset-descriptions.md.",
});
}