import React from "react"; // --------------------------------------------------------------------------- // A deliberately small Markdown renderer, for the per-song README.md. // // Not a general one, and it should not grow into one. It covers exactly what // the READMEs in this tree use -- headings, pipe tables, bullets, fenced code, // and inline `code`/**bold**/*em* -- because those files are the only // documentation of how each song was built, and a table shown as monospace // text throws away the thing that makes them worth reading. // // Output is React nodes, never dangerouslySetInnerHTML. The input is a file on // a local disk rather than anything hostile, but building a habit of returning // escaped nodes costs nothing here and means a README copied in from elsewhere // can never inject markup. // --------------------------------------------------------------------------- const INLINE = /(`[^`]+`|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\))/g; function inline(text: string, keyBase: string): React.ReactNode[] { return text.split(INLINE).filter(Boolean).map((tok, i) => { const key = `${keyBase}-${i}`; if (tok.startsWith("`") && tok.endsWith("`") && tok.length > 1) { return ( {tok.slice(1, -1)} ); } if (tok.startsWith("**") && tok.endsWith("**")) { return ( {tok.slice(2, -2)} ); } if (tok.startsWith("*") && tok.endsWith("*") && tok.length > 2) { return {tok.slice(1, -1)}; } const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(tok); if (link) { // Relative links point at sibling files on disk, which this app does not // serve -- render them as plain text rather than as a dead link. const [, label, href] = link; return /^https?:\/\//.test(href) ? ( {label} ) : ( {label} ); } return {tok}; }); } const isTableRow = (l: string) => l.trimStart().startsWith("|"); /** The `|---|---|` separator, which is layout and never content. */ const isTableRule = (l: string) => /^\s*\|[\s:|-]+\|\s*$/.test(l); const cells = (l: string) => l.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim()); export function Markdown({ text }: { text: string }) { const lines = text.replace(/\r\n/g, "\n").split("\n"); const out: React.ReactNode[] = []; let i = 0; while (i < lines.length) { const line = lines[i]; if (!line.trim()) { i += 1; continue; } // fenced code if (line.startsWith("```")) { const body: string[] = []; i += 1; while (i < lines.length && !lines[i].startsWith("```")) body.push(lines[i++]); i += 1; out.push(
          {body.join("\n")}
        
, ); continue; } const h = /^(#{1,4})\s+(.*)$/.exec(line); if (h) { const level = h[1].length; const size = level === 1 ? "text-[15px]" : level === 2 ? "text-[13px]" : "text-[12px]"; out.push(
{inline(h[2], `h${i}`)}
, ); i += 1; continue; } if (isTableRow(line)) { const rows: string[][] = []; let head: string[] | null = null; while (i < lines.length && isTableRow(lines[i])) { if (isTableRule(lines[i])) { head = rows.pop() ?? null; } else { rows.push(cells(lines[i])); } i += 1; } out.push(
{head && ( {head.map((c, k) => ( ))} )} {rows.map((r, ri) => ( {r.map((c, k) => ( ))} ))}
{inline(c, `th${i}-${k}`)}
{inline(c, `td${i}-${ri}-${k}`)}
, ); continue; } if (/^\s*[-*]\s+/.test(line)) { const items: { depth: number; text: string }[] = []; while (i < lines.length && /^\s*[-*]\s+/.test(lines[i])) { const indent = lines[i].length - lines[i].trimStart().length; items.push({ depth: Math.min(2, Math.floor(indent / 2)), text: lines[i].trim().slice(2) }); i += 1; } out.push( , ); continue; } const para: string[] = []; while ( i < lines.length && lines[i].trim() && !isTableRow(lines[i]) && !lines[i].startsWith("#") && !lines[i].startsWith("```") && !/^\s*[-*]\s+/.test(lines[i]) ) { para.push(lines[i++]); } out.push(

{inline(para.join(" "), `p${i}`)}

, ); } return
{out}
; }