fix: enhance markdown table cell formatting for descriptions in README and skills

This commit is contained in:
Aaron Powell
2026-01-23 10:06:44 +11:00
parent 105c0f55e2
commit a2525e3112
4 changed files with 61 additions and 23 deletions

View File

@@ -238,6 +238,39 @@ function extractDescription(filePath) {
);
}
/**
* Format arbitrary multiline text for safe rendering inside a markdown table cell.
* - Preserves line breaks by converting to <br />
* - Escapes pipe characters (|) to avoid breaking table columns
* - Trims leading/trailing whitespace on each line
* - Collapses multiple consecutive blank lines
* This should be applied to descriptions across all file types when used in tables.
*
* @param {string|null|undefined} text
* @returns {string} table-safe content
*/
function formatTableCell(text) {
if (text === null || text === undefined) return "";
let s = String(text);
// Normalize line endings
s = s.replace(/\r\n/g, "\n");
// Split lines, trim, drop empty groups while preserving intentional breaks
const lines = s
.split("\n")
.map((l) => l.trim())
.filter((_, idx, arr) => {
// Keep single blank lines, drop consecutive blanks
if (arr[idx] !== "") return true;
return arr[idx - 1] !== ""; // allow one blank, remove duplicates
});
s = lines.join("\n");
// Escape table pipes
s = s.replace(/\|/g, "&#124;");
// Convert remaining newlines to <br /> for a single-cell rendering
s = s.replace(/\n/g, "<br />");
return s.trim();
}
function makeBadges(link, type) {
const aka = AKA_INSTALL_URLS[type] || AKA_INSTALL_URLS.instructions;
@@ -298,8 +331,10 @@ function generateInstructionsSection(instructionsDir) {
const badges = makeBadges(link, "instructions");
if (customDescription && customDescription !== "null") {
// Use the description from frontmatter
instructionsContent += `| [${title}](../${link})<br />${badges} | ${customDescription} |\n`;
// Use the description from frontmatter, table-safe
instructionsContent += `| [${title}](../${link})<br />${badges} | ${formatTableCell(
customDescription
)} |\n`;
} else {
// Fallback to the default approach - use last word of title for description, removing trailing 's' if present
const topic = title.split(" ").pop().replace(/s$/, "");
@@ -356,7 +391,9 @@ function generatePromptsSection(promptsDir) {
const badges = makeBadges(link, "prompt");
if (customDescription && customDescription !== "null") {
promptsContent += `| [${title}](../${link})<br />${badges} | ${customDescription} |\n`;
promptsContent += `| [${title}](../${link})<br />${badges} | ${formatTableCell(
customDescription
)} |\n`;
} else {
promptsContent += `| [${title}](../${link})<br />${badges} | | |\n`;
}
@@ -533,7 +570,9 @@ function generateSkillsSection(skillsDir) {
? skill.assets.map((a) => `\`${a}\``).join("<br />")
: "None";
content += `| [${skill.name}](${link}) | ${skill.description} | ${assetsList} |\n`;
content += `| [${skill.name}](${link}) | ${formatTableCell(
skill.description
)} | ${assetsList} |\n`;
}
return `${TEMPLATES.skillsSection}\n${TEMPLATES.skillsUsage}\n\n${content}`;
@@ -598,14 +637,12 @@ function generateUnifiedModeSection(cfg) {
mcpServerCell = generateMcpServerLinks(servers, registryNames);
}
const descCell =
description && description !== "null" ? formatTableCell(description) : "";
if (includeMcpServers) {
content += `| [${title}](../${link})<br />${badges} | ${
description && description !== "null" ? description : ""
} | ${mcpServerCell} |\n`;
content += `| [${title}](../${link})<br />${badges} | ${descCell} | ${mcpServerCell} |\n`;
} else {
content += `| [${title}](../${link})<br />${badges} | ${
description && description !== "null" ? description : ""
} |\n`;
content += `| [${title}](../${link})<br />${badges} | ${descCell} |\n`;
}
}
@@ -677,7 +714,9 @@ function generateCollectionsSection(collectionsDir) {
// Generate table rows for each collection file
for (const entry of sortedEntries) {
const { collection, collectionId, name, isFeatured } = entry;
const description = collection.description || "No description";
const description = formatTableCell(
collection.description || "No description"
);
const itemCount = collection.items ? collection.items.length : 0;
const tags = collection.tags ? collection.tags.join(", ") : "";
@@ -719,7 +758,9 @@ function generateFeaturedCollectionsSection(collectionsDir) {
const collectionId =
collection.id || path.basename(file, ".collection.yml");
const name = collection.name || collectionId;
const description = collection.description || "No description";
const description = formatTableCell(
collection.description || "No description"
);
const tags = collection.tags ? collection.tags.join(", ") : "";
const itemCount = collection.items ? collection.items.length : 0;
@@ -904,6 +945,9 @@ function buildCollectionRow({
? `[${title}](${link})<br />${badges}`
: `[${title}](${link})`;
// Ensure description is table-safe
const safeUsage = formatTableCell(usageDescription);
if (hasAgents) {
// Only agents currently have MCP servers; future migration may extend to chat modes.
const mcpServers =
@@ -912,9 +956,9 @@ function buildCollectionRow({
mcpServers.length > 0
? generateMcpServerLinks(mcpServers, registryNames)
: "";
return `| ${titleCell} | ${typeDisplay} | ${usageDescription} | ${mcpServerCell} |\n`;
return `| ${titleCell} | ${typeDisplay} | ${safeUsage} | ${mcpServerCell} |\n`;
}
return `| ${titleCell} | ${typeDisplay} | ${usageDescription} |\n`;
return `| ${titleCell} | ${typeDisplay} | ${safeUsage} |\n`;
}
// Utility: write file only if content changed