mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix(plugins): rewrite relative README image URLs to source-host raw URLs
This commit is contained in:
@@ -680,6 +680,43 @@ describe("plugins publish route", () => {
|
||||
expect(screen.getByText(/\.\/images\/bar\.png/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("warns when README picture source srcset references relative image paths", async () => {
|
||||
renderPublishRoute();
|
||||
|
||||
const packageJson = withRelativePath(
|
||||
new File(
|
||||
[makeCodePluginPackageJson({ name: "demo-plugin", version: "1.0.0" })],
|
||||
"package.json",
|
||||
{
|
||||
type: "application/json",
|
||||
},
|
||||
),
|
||||
"demo-plugin/package.json",
|
||||
);
|
||||
const manifest = withRelativePath(
|
||||
new File(['{"id":"demo.plugin"}'], "openclaw.plugin.json", { type: "application/json" }),
|
||||
"demo-plugin/openclaw.plugin.json",
|
||||
);
|
||||
const readme = withRelativePath(
|
||||
new File(
|
||||
[
|
||||
'# Demo Plugin\n\n<picture><source media="(prefers-color-scheme: dark)" srcset="./images/dark.png 1x, ./images/dark@2x.png 2x"><img src="https://example.com/fallback.png" alt="x"></picture>',
|
||||
],
|
||||
"README.md",
|
||||
{ type: "text/markdown" },
|
||||
),
|
||||
"demo-plugin/README.md",
|
||||
);
|
||||
|
||||
fireEvent.change(getFileInput(), { target: { files: [packageJson, manifest, readme] } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/2 package-relative image paths/i)).toBeTruthy();
|
||||
});
|
||||
expect(screen.getByText(/\.\/images\/dark\.png/)).toBeTruthy();
|
||||
expect(screen.getByText(/\.\/images\/dark@2x\.png/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("swaps the missing-source warning for a Package-path reminder once Source repo and a valid 40-hex Source commit are filled", async () => {
|
||||
renderPublishRoute();
|
||||
|
||||
|
||||
@@ -30,6 +30,25 @@ describe("detectRelativeReadmeAssets", () => {
|
||||
expect(report.unresolvableSamples).toEqual([]);
|
||||
});
|
||||
|
||||
it("flags relative <source srcset> candidates in raw HTML", () => {
|
||||
const report = detectRelativeReadmeAssets(
|
||||
`<picture><source media="(prefers-color-scheme: dark)" srcset="./dark.png 1x, ./dark@2x.png 2x, https://example.com/remote.png 3x"/><img src="https://example.com/fallback.png"/></picture>`,
|
||||
);
|
||||
expect(report.samples).toEqual(["./dark.png", "./dark@2x.png"]);
|
||||
expect(report.total).toBe(2);
|
||||
expect(report.unresolvableSamples).toEqual([]);
|
||||
});
|
||||
|
||||
it("flags root-absolute <source srcset> candidates separately", () => {
|
||||
const report = detectRelativeReadmeAssets(
|
||||
`<source srcset="/dark.png 1x, ./light.png 2x, data:image/svg+xml,%3Csvg%3E 3x"/>`,
|
||||
);
|
||||
expect(report.samples).toEqual(["/dark.png", "./light.png"]);
|
||||
expect(report.total).toBe(2);
|
||||
expect(report.unresolvableSamples).toEqual(["/dark.png"]);
|
||||
expect(report.unresolvableTotal).toBe(1);
|
||||
});
|
||||
|
||||
it("flags root-absolute paths separately as unresolvable", () => {
|
||||
const report = detectRelativeReadmeAssets("");
|
||||
expect(report.samples).toEqual(["/static/logo.png"]);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Scans README markdown text for relative image references — both Markdown
|
||||
* `` syntax and raw HTML `<img src="./path">` tags — and returns
|
||||
* the unique set of relative paths it finds (capped to keep UI warnings short).
|
||||
* `` syntax, raw HTML `<img src="./path">` tags, and
|
||||
* `<source srcset="./path 1x">` candidates — and returns the unique set of
|
||||
* relative paths it finds (capped to keep UI warnings short).
|
||||
*
|
||||
* Why: ClawHub does not host package binary assets. When a publisher uploads
|
||||
* a zip/tgz whose README references local images via relative paths, those
|
||||
@@ -30,12 +31,17 @@
|
||||
|
||||
const MARKDOWN_IMAGE = /!\[[^\]]*\]\(\s*([^)\s]+)(?:\s+"[^"]*")?\s*\)/g;
|
||||
const HTML_IMG_SRC = /<img\b[^>]*?\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)')[^>]*?>/gi;
|
||||
const HTML_SOURCE_SRCSET = /<source\b[^>]*?\bsrcset\s*=\s*(?:"([^"]+)"|'([^']+)')[^>]*?>/gi;
|
||||
|
||||
const ABSOLUTE_URL = /^[a-z][a-z0-9+\-.]*:/i;
|
||||
const PROTOCOL_RELATIVE = /^\/\//;
|
||||
|
||||
const MAX_REPORTED = 5;
|
||||
|
||||
function isAsciiWhitespace(char: string): boolean {
|
||||
return char === " " || char === "\n" || char === "\t" || char === "\r" || char === "\f";
|
||||
}
|
||||
|
||||
function classifyRelativeAsset(rawSrc: string): "package-relative" | "root-absolute" | null {
|
||||
const src = rawSrc.trim();
|
||||
if (!src) return null;
|
||||
@@ -94,6 +100,45 @@ export function detectRelativeReadmeAssets(readmeText: string): RelativeReadmeAs
|
||||
}
|
||||
};
|
||||
|
||||
const recordSrcset = (srcset: string | undefined) => {
|
||||
if (!srcset) return;
|
||||
let index = 0;
|
||||
while (index < srcset.length) {
|
||||
while (index < srcset.length) {
|
||||
const char = srcset[index];
|
||||
if (isAsciiWhitespace(char) || char === ",") {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (index >= srcset.length) break;
|
||||
|
||||
const urlStart = index;
|
||||
while (index < srcset.length && !isAsciiWhitespace(srcset[index])) {
|
||||
index += 1;
|
||||
}
|
||||
let url = srcset.slice(urlStart, index);
|
||||
const endedWithComma = url.endsWith(",");
|
||||
if (endedWithComma) {
|
||||
url = url.slice(0, -1);
|
||||
}
|
||||
record(url);
|
||||
|
||||
if (!endedWithComma) {
|
||||
while (index < srcset.length && isAsciiWhitespace(srcset[index])) {
|
||||
index += 1;
|
||||
}
|
||||
while (index < srcset.length && srcset[index] !== ",") {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
if (srcset[index] === ",") {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
MARKDOWN_IMAGE.lastIndex = 0;
|
||||
for (
|
||||
let match = MARKDOWN_IMAGE.exec(readmeText);
|
||||
@@ -108,5 +153,14 @@ export function detectRelativeReadmeAssets(readmeText: string): RelativeReadmeAs
|
||||
record(match[1] ?? match[2]);
|
||||
}
|
||||
|
||||
HTML_SOURCE_SRCSET.lastIndex = 0;
|
||||
for (
|
||||
let match = HTML_SOURCE_SRCSET.exec(readmeText);
|
||||
match;
|
||||
match = HTML_SOURCE_SRCSET.exec(readmeText)
|
||||
) {
|
||||
recordSrcset(match[1] ?? match[2]);
|
||||
}
|
||||
|
||||
return { samples, total, unresolvableSamples, unresolvableTotal };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user