mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-06 23:55:12 +00:00
fix: constrain clawtributors avatar rendering
This commit is contained in:
113
scripts/update-clawtributors-render.ts
Normal file
113
scripts/update-clawtributors-render.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { Entry } from "./update-clawtributors.types.js";
|
||||
|
||||
export type RenderableClawtributorEntry = Pick<Entry, "display" | "html_url" | "avatar_url">;
|
||||
|
||||
export type RenderClawtributorsBlockOptions = {
|
||||
perLine: number;
|
||||
avatarSize: number;
|
||||
startMarker: string;
|
||||
endMarker: string;
|
||||
};
|
||||
|
||||
export function renderClawtributorsBlock(
|
||||
entries: readonly RenderableClawtributorEntry[],
|
||||
options: RenderClawtributorsBlockOptions,
|
||||
): string {
|
||||
const lines = renderClawtributorsLines(entries, options.perLine, options.avatarSize);
|
||||
const block = `${options.startMarker}\n${lines.join("\n")}\n${options.endMarker}`;
|
||||
const renderedCount = parseRenderedClawtributorEntries(block).length;
|
||||
if (renderedCount !== entries.length) {
|
||||
throw new Error(
|
||||
`Rendered clawtributors count mismatch: expected ${entries.length}, got ${renderedCount}`,
|
||||
);
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
export function renderClawtributorsLines(
|
||||
entries: readonly RenderableClawtributorEntry[],
|
||||
perLine: number,
|
||||
avatarSize: number,
|
||||
): string[] {
|
||||
const lines: string[] = [];
|
||||
for (let i = 0; i < entries.length; i += perLine) {
|
||||
const chunk = entries.slice(i, i + perLine);
|
||||
const parts = chunk.map((entry) => renderClawtributorEntry(entry, avatarSize));
|
||||
lines.push(parts.join(" "));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function renderClawtributorEntry(
|
||||
entry: RenderableClawtributorEntry,
|
||||
avatarSize: number,
|
||||
): string {
|
||||
const size = String(avatarSize);
|
||||
const label = escapeHtmlAttribute(entry.display);
|
||||
return `<a href="${escapeHtmlAttribute(entry.html_url)}"><img src="${escapeHtmlAttribute(entry.avatar_url)}" width="${size}" height="${size}" alt="${label}" title="${label}"/></a>`;
|
||||
}
|
||||
|
||||
export function parseRenderedClawtributorEntries(
|
||||
content: string,
|
||||
): Array<{ display: string; html_url: string; avatar_url: string }> {
|
||||
const entries: Array<{ display: string; html_url: string; avatar_url: string }> = [];
|
||||
const markdown = /\[!\[([^\]]+)\]\(([^)]+)\)\]\(([^)]+)\)/g;
|
||||
for (const match of content.matchAll(markdown)) {
|
||||
const [, alt, src, href] = match;
|
||||
if (!href || !src || !alt) {
|
||||
continue;
|
||||
}
|
||||
entries.push({ html_url: href, avatar_url: src, display: alt.replace(/\\([\\[\]])/g, "$1") });
|
||||
}
|
||||
const linked = /<a href="([^"]+)"><img src="([^"]+)"[^>]*alt="([^"]+)"[^>]*>/g;
|
||||
for (const match of content.matchAll(linked)) {
|
||||
const [, href, src, alt] = match;
|
||||
if (!href || !src || !alt) {
|
||||
continue;
|
||||
}
|
||||
entries.push({
|
||||
html_url: decodeHtmlAttribute(href),
|
||||
avatar_url: decodeHtmlAttribute(src),
|
||||
display: decodeHtmlAttribute(alt),
|
||||
});
|
||||
}
|
||||
const standalone = /<img src="([^"]+)"[^>]*alt="([^"]+)"[^>]*>/g;
|
||||
for (const match of content.matchAll(standalone)) {
|
||||
const [, src, alt] = match;
|
||||
if (!src || !alt) {
|
||||
continue;
|
||||
}
|
||||
const decodedSrc = decodeHtmlAttribute(src);
|
||||
const decodedAlt = decodeHtmlAttribute(alt);
|
||||
if (entries.some((entry) => entry.display === decodedAlt && entry.avatar_url === decodedSrc)) {
|
||||
continue;
|
||||
}
|
||||
entries.push({
|
||||
html_url: fallbackHref(decodedAlt),
|
||||
avatar_url: decodedSrc,
|
||||
display: decodedAlt,
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function escapeHtmlAttribute(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function decodeHtmlAttribute(value: string): string {
|
||||
return value
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/&/g, "&");
|
||||
}
|
||||
|
||||
function fallbackHref(value: string): string {
|
||||
const encoded = encodeURIComponent(value.trim());
|
||||
return encoded ? `https://github.com/search?q=${encoded}` : "https://github.com";
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import { execFileSync, execSync } from "node:child_process";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import {
|
||||
parseRenderedClawtributorEntries,
|
||||
renderClawtributorsBlock,
|
||||
} from "./update-clawtributors-render.js";
|
||||
import type { ApiContributor, Entry, MapConfig, User } from "./update-clawtributors.types.js";
|
||||
|
||||
const REPO = "openclaw/openclaw";
|
||||
@@ -290,16 +294,12 @@ visibleEntries.sort((a, b) => {
|
||||
return a.display.localeCompare(b.display);
|
||||
});
|
||||
|
||||
const markdownLines: string[] = [];
|
||||
for (let i = 0; i < visibleEntries.length; i += PER_LINE) {
|
||||
const chunk = visibleEntries.slice(i, i + PER_LINE);
|
||||
const parts = chunk.map((entry) => {
|
||||
return `[](${entry.html_url})`;
|
||||
});
|
||||
markdownLines.push(parts.join(" "));
|
||||
}
|
||||
|
||||
const block = `${CLAWTRIBUTORS_START}\n${markdownLines.join("\n")}\n${CLAWTRIBUTORS_END}`;
|
||||
const block = renderClawtributorsBlock(visibleEntries, {
|
||||
perLine: PER_LINE,
|
||||
avatarSize: AVATAR_SIZE,
|
||||
startMarker: CLAWTRIBUTORS_START,
|
||||
endMarker: CLAWTRIBUTORS_END,
|
||||
});
|
||||
const hiddenBlock = buildHiddenReadmeBlock(entries, visibleEntries);
|
||||
const hiddenRange = findHiddenReadmeRange(currentReadme);
|
||||
const readmeWithoutMeta = hiddenRange
|
||||
@@ -674,10 +674,6 @@ function normalizeIdentifier(value: string): string {
|
||||
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
||||
}
|
||||
|
||||
function escapeMarkdownLabel(value: string): string {
|
||||
return value.replace(/([\\[\]])/g, "\\$1");
|
||||
}
|
||||
|
||||
function parseReadmeEntries(
|
||||
content: string,
|
||||
): Array<{ display: string; html_url: string; avatar_url: string }> {
|
||||
@@ -686,35 +682,7 @@ function parseReadmeEntries(
|
||||
return [];
|
||||
}
|
||||
const block = content.slice(range.start, range.end);
|
||||
const entries: Array<{ display: string; html_url: string; avatar_url: string }> = [];
|
||||
const markdown = /\[!\[([^\]]+)\]\(([^)]+)\)\]\(([^)]+)\)/g;
|
||||
for (const match of block.matchAll(markdown)) {
|
||||
const [, alt, src, href] = match;
|
||||
if (!href || !src || !alt) {
|
||||
continue;
|
||||
}
|
||||
entries.push({ html_url: href, avatar_url: src, display: alt.replace(/\\([\\[\]])/g, "$1") });
|
||||
}
|
||||
const linked = /<a href="([^"]+)"><img src="([^"]+)"[^>]*alt="([^"]+)"[^>]*>/g;
|
||||
for (const match of block.matchAll(linked)) {
|
||||
const [, href, src, alt] = match;
|
||||
if (!href || !src || !alt) {
|
||||
continue;
|
||||
}
|
||||
entries.push({ html_url: href, avatar_url: src, display: alt });
|
||||
}
|
||||
const standalone = /<img src="([^"]+)"[^>]*alt="([^"]+)"[^>]*>/g;
|
||||
for (const match of block.matchAll(standalone)) {
|
||||
const [, src, alt] = match;
|
||||
if (!src || !alt) {
|
||||
continue;
|
||||
}
|
||||
if (entries.some((entry) => entry.display === alt && entry.avatar_url === src)) {
|
||||
continue;
|
||||
}
|
||||
entries.push({ html_url: fallbackHref(alt), avatar_url: src, display: alt });
|
||||
}
|
||||
return entries;
|
||||
return parseRenderedClawtributorEntries(block);
|
||||
}
|
||||
|
||||
function parseHiddenReadmeLogins(content: string): string[] {
|
||||
|
||||
97
test/scripts/update-clawtributors-render.test.ts
Normal file
97
test/scripts/update-clawtributors-render.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
parseRenderedClawtributorEntries,
|
||||
renderClawtributorsBlock,
|
||||
} from "../../scripts/update-clawtributors-render.js";
|
||||
|
||||
describe("scripts/update-clawtributors-render", () => {
|
||||
it("renders explicit avatar dimensions for every entry", () => {
|
||||
const block = renderClawtributorsBlock(
|
||||
[
|
||||
{
|
||||
display: "Andy",
|
||||
html_url: "https://github.com/andyk-ms",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/91510251?v=4&s=48",
|
||||
},
|
||||
{
|
||||
display: "Rajat Joshi",
|
||||
html_url: "https://github.com/18-RAJAT",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/78920780?v=4&s=48",
|
||||
},
|
||||
],
|
||||
{
|
||||
perLine: 10,
|
||||
avatarSize: 48,
|
||||
startMarker: "<!-- clawtributors:start -->",
|
||||
endMarker: "<!-- clawtributors:end -->",
|
||||
},
|
||||
);
|
||||
|
||||
expect(block).toContain('width="48"');
|
||||
expect(block).toContain('height="48"');
|
||||
expect(block).toContain(
|
||||
'<a href="https://github.com/18-RAJAT"><img src="https://avatars.githubusercontent.com/u/78920780?v=4&s=48" width="48" height="48" alt="Rajat Joshi" title="Rajat Joshi"/></a>',
|
||||
);
|
||||
});
|
||||
|
||||
it("round-trips rendered html entries without losing contributors", () => {
|
||||
const entries = [
|
||||
{
|
||||
display: "Andy",
|
||||
html_url: "https://github.com/andyk-ms",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/91510251?v=4&s=48",
|
||||
},
|
||||
{
|
||||
display: "Rajat Joshi",
|
||||
html_url: "https://github.com/18-RAJAT",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/78920780?v=4&s=48",
|
||||
},
|
||||
{
|
||||
display: 'Tom & "Jerry"',
|
||||
html_url: "https://github.com/example",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/1?v=4&s=48",
|
||||
},
|
||||
];
|
||||
|
||||
const block = renderClawtributorsBlock(entries, {
|
||||
perLine: 2,
|
||||
avatarSize: 48,
|
||||
startMarker: "<!-- clawtributors:start -->",
|
||||
endMarker: "<!-- clawtributors:end -->",
|
||||
});
|
||||
const parsed = parseRenderedClawtributorEntries(block);
|
||||
|
||||
expect(parsed).toEqual(entries);
|
||||
});
|
||||
|
||||
it("parses legacy markdown entries for seed compatibility", () => {
|
||||
const parsed = parseRenderedClawtributorEntries(
|
||||
"[](https://github.com/18-RAJAT)",
|
||||
);
|
||||
|
||||
expect(parsed).toEqual([
|
||||
{
|
||||
display: "Rajat Joshi",
|
||||
html_url: "https://github.com/18-RAJAT",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/78920780?v=4&s=48",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps rendered contributor count aligned with the input set", () => {
|
||||
const entries = Array.from({ length: 3 }, (_, index) => ({
|
||||
display: `Contributor ${index + 1}`,
|
||||
html_url: `https://github.com/example-${index + 1}`,
|
||||
avatar_url: `https://avatars.githubusercontent.com/u/${index + 1}?v=4&s=48`,
|
||||
}));
|
||||
|
||||
const block = renderClawtributorsBlock(entries, {
|
||||
perLine: 2,
|
||||
avatarSize: 48,
|
||||
startMarker: "<!-- clawtributors:start -->",
|
||||
endMarker: "<!-- clawtributors:end -->",
|
||||
});
|
||||
|
||||
expect(parseRenderedClawtributorEntries(block)).toHaveLength(entries.length);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user