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}
);
}