import type { ReactNode } from "react"; import { Children } from "react"; import Markdown from "markdown-to-jsx"; import { sliceTopEntries } from "../lib/changelog"; import { CopyLinkButton } from "./CopyLinkButton"; function flattenText(children: ReactNode): string { let out = ""; Children.forEach(children, (child) => { if (child == null || typeof child === "boolean") return; if (typeof child === "string" || typeof child === "number") { out += String(child); return; } if (typeof child === "object" && "props" in child) { const props = (child as { props?: { children?: ReactNode } }).props; if (props?.children !== undefined) out += flattenText(props.children); } }); return out; } const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/; const ISO_DATE = /(\d{4}-\d{2}-\d{2})/; function slugify(input: string): string { return input .toLowerCase() .replace(/[^a-z0-9-]+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, ""); } function anchorFor(text: string): string { if (DATE_ONLY.test(text)) return text; const dateMatch = text.match(ISO_DATE); if (dateMatch) return dateMatch[1]; const slug = slugify(text); return slug || "section"; } function H2({ children }: { children?: ReactNode }) { const text = flattenText(children).trim(); const anchor = text ? anchorFor(text) : null; return (

{children} {anchor && }

); } function H1({ children }: { children?: ReactNode }) { // The top-level `# Editor Changelog` heading is already conveyed by the // page's own

; render it as a no-op so the file stays a valid // standalone document but the page doesn't get a duplicate title. return {children}; } const OVERRIDES = { h1: { component: H1 }, h2: { component: H2 }, h3: { props: { className: "mt-6 mb-2 text-sm font-semibold text-foreground", }, }, p: { props: { className: "my-2 text-sm text-muted-foreground" }, }, ul: { props: { className: "list-disc pl-5 my-2 space-y-1 text-sm text-muted-foreground marker:text-muted-foreground", }, }, ol: { props: { className: "list-decimal pl-5 my-2 space-y-1 text-sm text-muted-foreground marker:text-muted-foreground", }, }, li: { props: { className: "leading-relaxed" } }, strong: { props: { className: "font-semibold text-foreground", }, }, em: { props: { className: "italic" } }, code: { props: { className: "px-1 py-0.5 rounded bg-muted font-mono text-[0.85em]", }, }, pre: { props: { className: "my-3 p-3 rounded-md bg-muted overflow-x-auto text-xs font-mono", }, }, a: { props: { className: "underline decoration-muted-foreground hover:decoration-foreground", }, }, hr: { props: { className: "my-6 border-t border-border", }, }, blockquote: { props: { className: "my-3 pl-3 border-l-2 border-border text-muted-foreground", }, }, }; export function Changelog({ source, limit, }: { source: string; limit?: number; }) { const sliced = limit ? sliceTopEntries(source, limit) : source; return (
{sliced}
); }