import type { ReactNode, AnchorHTMLAttributes } from "react"; import { Children } from "react"; import Link from "next/link"; import Markdown from "markdown-to-jsx"; import { CopyLinkButton } from "yt-dlp-transcript-common/components/CopyLinkButton"; // The documentation renderer. // // WHY NOT common/components/Markdown.tsx. Three reasons, each disqualifying on // its own: it hard-codes `target="_blank"` on EVERY anchor, so an internal // cross-link between doc pages would open a new tab; it is tuned for chat // density (small headings, tight rhythm), which is wrong for a document someone // reads top to bottom; and it has no heading anchors, so a section can't be // linked to. This borrows Changelog.tsx's anchored-heading pattern instead. // // A TRAP WORTH NAMING: markdown-to-jsx does NOT escape raw HTML by default // (`disableParsingRawHTML` is off), despite the header comment on Markdown.tsx // claiming it does. That is safe here only because these files are hand-written // and in-repo — never render untrusted Markdown through this component. 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; } function slugify(input: string): string { return input .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, ""); } function anchored( Tag: "h2" | "h3", className: string, ): (p: { children?: ReactNode }) => ReactNode { return function Heading({ children }) { const anchor = slugify(flattenText(children).trim()) || null; return ( {children} {anchor && } ); }; } // A leading `# Title` is stripped by the loader, but a stray one shouldn't // out-shout the page's own title if a file grows a second top-level heading. function H1({ children }: { children?: ReactNode }) { return (

{children}

); } // Internal links navigate in place; external ones open in a new tab. Getting // this backwards is exactly the bug that ruled out the shared Markdown // component, so the test suite asserts an internal cross-link has no `target`. function Anchor({ href, children, ...rest }: AnchorHTMLAttributes) { const className = "text-[var(--foreground)] underline decoration-[var(--border-strong)] underline-offset-[3px] hover:decoration-[var(--brand)] transition-colors"; if (!href) return {children}; if (/^https?:\/\//i.test(href)) { return ( {children} ); } // In-page fragments stay plain anchors; next/link would push a history entry // for what is just a scroll. if (href.startsWith("#")) { return ( {children} ); } return ( {children} ); } const OVERRIDES = { h1: { component: H1 }, h2: { component: anchored( "h2", "scroll-mt-24 mt-12 mb-3 font-display text-lg font-semibold text-[var(--foreground)] flex items-baseline", ), }, h3: { component: anchored( "h3", "scroll-mt-24 mt-8 mb-2 font-sans text-base font-semibold text-[var(--foreground)] flex items-baseline", ), }, h4: { props: { className: "mt-6 mb-2 label-machine", }, }, p: { props: { className: "my-4 leading-[1.7] text-[var(--muted-foreground)]" } }, ul: { props: { className: "list-disc pl-5 my-4 space-y-2 leading-[1.7] text-[var(--muted-foreground)] marker:text-[var(--faint)]", }, }, ol: { props: { className: "list-decimal pl-5 my-4 space-y-2 leading-[1.7] text-[var(--muted-foreground)] marker:text-[var(--faint)]", }, }, li: { props: { className: "leading-[1.7]" } }, strong: { props: { className: "font-semibold text-[var(--foreground)]" } }, em: { props: { className: "italic" } }, code: { props: { className: "px-1 py-0.5 rounded-[var(--radius)] bg-[var(--surface)] border border-[var(--border)] font-mono text-[0.85em] text-[var(--foreground)]", }, }, pre: { props: { className: "my-5 p-4 rounded-[var(--radius)] bg-[var(--surface)] border border-[var(--border)] overflow-x-auto text-[0.8125rem] leading-relaxed font-mono [&_code]:p-0 [&_code]:border-0 [&_code]:bg-transparent", }, }, a: { component: Anchor }, hr: { props: { className: "my-10 border-t border-[var(--border)]" } }, blockquote: { props: { className: "my-5 pl-4 border-l-2 border-[var(--border-strong)] text-[var(--muted-foreground)]", }, }, table: { props: { className: "my-5 block w-full overflow-x-auto text-left border-collapse text-sm", }, }, th: { props: { className: "border-b border-[var(--border-strong)] px-3 py-2 font-mono text-[0.6875rem] uppercase tracking-[0.12em] text-[var(--faint)] whitespace-nowrap", }, }, td: { props: { className: "border-b border-[var(--border)] px-3 py-2 align-top text-[var(--muted-foreground)]", }, }, }; export function DocBody({ source }: { source: string }) { return (
{source}
); }