Replace manual hashtags and color cycling with content-tone heuristics for blockquotes and tables, and refine blockquote layout with a centered icon, larger sizing, and clearer contrast over background decorations. Co-authored-by: Cursor <cursoragent@cursor.com>
81 lines
2.4 KiB
TypeScript
81 lines
2.4 KiB
TypeScript
import type { ChangelogSection, ChangelogSectionTitle } from "@/lib/changelog-types";
|
|
|
|
/** Use a short paragraph when a release has very few notes. */
|
|
const PARAGRAPH_ITEM_THRESHOLD = 4;
|
|
|
|
export function stripChangelogMarkdown(text: string): string {
|
|
return text
|
|
.replace(/`([^`]+)`/g, "$1")
|
|
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
export function countReleaseItems(sections: ChangelogSection[]): number {
|
|
return sections.reduce((total, section) => total + section.items.length, 0);
|
|
}
|
|
|
|
export function shouldUseParagraphSummary(sections: ChangelogSection[]): boolean {
|
|
return countReleaseItems(sections) <= PARAGRAPH_ITEM_THRESHOLD;
|
|
}
|
|
|
|
const SECTION_VERBS: Partial<Record<ChangelogSectionTitle, string>> = {
|
|
Added: "introduces",
|
|
Changed: "improves",
|
|
Fixed: "fixes",
|
|
Removed: "removes",
|
|
Deprecated: "deprecates",
|
|
Security: "patches",
|
|
};
|
|
|
|
function joinNaturalList(items: string[]): string {
|
|
if (items.length === 0) return "";
|
|
if (items.length === 1) return items[0];
|
|
if (items.length === 2) return `${items[0]} and ${items[1]}`;
|
|
return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
|
|
}
|
|
|
|
export function buildReleaseParagraph(sections: ChangelogSection[]): string {
|
|
const clauses = sections
|
|
.map((section) => {
|
|
const items = section.items.map((item) =>
|
|
stripChangelogMarkdown(item).replace(/\.$/, "").toLowerCase()
|
|
);
|
|
const verb = SECTION_VERBS[section.title] ?? "updates";
|
|
return `${verb} ${joinNaturalList(items)}`;
|
|
})
|
|
.filter(Boolean);
|
|
|
|
if (clauses.length === 0) return "";
|
|
if (clauses.length === 1) {
|
|
return `This release ${clauses[0]}.`;
|
|
}
|
|
|
|
const last = clauses[clauses.length - 1];
|
|
const rest = clauses.slice(0, -1);
|
|
return `This release ${rest.join(", ")}, and ${last}.`;
|
|
}
|
|
|
|
function toLatinDigits(value: string): string {
|
|
return value.replace(/[\u0660-\u0669\u06f0-\u06f9]/g, (digit) =>
|
|
String(
|
|
digit.charCodeAt(0) -
|
|
(digit <= "\u0669" ? 0x0660 : 0x06f0)
|
|
)
|
|
);
|
|
}
|
|
|
|
export function formatReleaseDate(date: string): string {
|
|
const parsed = new Date(`${date}T00:00:00`);
|
|
if (Number.isNaN(parsed.getTime())) return date;
|
|
|
|
const formatted = new Intl.DateTimeFormat("en-US", {
|
|
month: "long",
|
|
day: "numeric",
|
|
year: "numeric",
|
|
numberingSystem: "latn",
|
|
}).format(parsed);
|
|
|
|
return toLatinDigits(formatted);
|
|
}
|