import * as React from 'react' /** * Anzeige einer Markdown-Datei aus dem docs-Verzeichnis. * * Bewusst ohne Markdown-Bibliothek: Die Anwendung zeigt genau zwei solche * Dokumente an - die Datenschutzhinweise und die Anleitung. Beide schreibe * ich selbst, ich weiss also, welche Auszeichnungen darin vorkommen. Eine * Bibliothek waere hier vor allem zusaetzliche Abhaengigkeit und * zusaetzliche Angriffsflaeche. * * Unterstuetzt: Ueberschriften, Absaetze, Aufzaehlungen (auch nummeriert), * Tabellen, Zitatbloecke, Codebloecke, Trennlinien sowie **fett**, * `Code` und [Links](ziel) im Fliesstext. * * Nicht unterstuetzt und auch nicht noetig: eingebettetes HTML, Bilder, * Fussnoten. Rohes HTML wird NICHT durchgereicht - der Text wird immer als * Text ausgegeben, nie als Markup. */ type Block = | { art: 'ueberschrift'; ebene: 2 | 3 | 4; text: string } | { art: 'absatz'; text: string } | { art: 'liste'; nummeriert: boolean; punkte: string[] } | { art: 'tabelle'; kopf: string[]; zeilen: string[][] } | { art: 'zitat'; zeilen: string[] } | { art: 'code'; zeilen: string[] } | { art: 'trenner' } /** Zerlegt den Fliesstext in fett, Code und Links. */ function inline(text: string, schluessel: string): React.ReactNode[] { const teile: React.ReactNode[] = [] // Reihenfolge zaehlt: Code zuerst, damit Sternchen darin nicht greifen. const muster = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\[[^\]]+\]\([^)]+\))/g let letzte = 0 let treffer: RegExpExecArray | null let nummer = 0 while ((treffer = muster.exec(text)) !== null) { if (treffer.index > letzte) { teile.push(text.slice(letzte, treffer.index)) } const stueck = treffer[0] const id = `${schluessel}-${nummer++}` if (stueck.startsWith('`')) { teile.push( {stueck.slice(1, -1)} , ) } else if (stueck.startsWith('**')) { teile.push( {stueck.slice(2, -2)} , ) } else { const beschriftung = stueck.slice(1, stueck.indexOf(']')) const ziel = stueck.slice(stueck.indexOf('](') + 2, -1) const extern = /^https?:\/\//.test(ziel) teile.push( {beschriftung} , ) } letzte = treffer.index + stueck.length } if (letzte < text.length) teile.push(text.slice(letzte)) return teile } function zerlegen(markdown: string, ohneTitel: boolean): Block[] { const zeilen = markdown.split(/\r?\n/) const bloecke: Block[] = [] let absatz: string[] = [] const absatzSchliessen = () => { if (absatz.length === 0) return bloecke.push({ art: 'absatz', text: absatz.join(' ') }) absatz = [] } for (let i = 0; i < zeilen.length; i++) { const roh = zeilen[i] ?? '' const zeile = roh.trimEnd() const gekuerzt = zeile.trim() // --- Codeblock --------------------------------------------------------- if (gekuerzt.startsWith('```')) { absatzSchliessen() const inhalt: string[] = [] i += 1 while (i < zeilen.length && !(zeilen[i] ?? '').trim().startsWith('```')) { inhalt.push(zeilen[i] ?? '') i += 1 } bloecke.push({ art: 'code', zeilen: inhalt }) continue } if (gekuerzt === '') { absatzSchliessen() continue } if (/^-{3,}$/.test(gekuerzt) || /^\*{3,}$/.test(gekuerzt)) { absatzSchliessen() bloecke.push({ art: 'trenner' }) continue } // --- Ueberschriften ---------------------------------------------------- const ueberschrift = gekuerzt.match(/^(#{1,4})\s+(.*)$/) if (ueberschrift) { absatzSchliessen() const ebene = ueberschrift[1]!.length // Die H1 ist der Dokumenttitel - den setzt die Seite selbst. if (ebene === 1 && ohneTitel) continue bloecke.push({ art: 'ueberschrift', ebene: (Math.min(Math.max(ebene, 2), 4) as 2 | 3 | 4), text: ueberschrift[2]!, }) continue } // --- Zitatblock -------------------------------------------------------- if (gekuerzt.startsWith('>')) { absatzSchliessen() const inhalt: string[] = [] while (i < zeilen.length && (zeilen[i] ?? '').trim().startsWith('>')) { inhalt.push((zeilen[i] ?? '').trim().replace(/^>\s?/, '')) i += 1 } i -= 1 bloecke.push({ art: 'zitat', zeilen: [inhalt.join(' ')] }) continue } // --- Tabelle ----------------------------------------------------------- if (gekuerzt.startsWith('|')) { absatzSchliessen() const spalten = (z: string) => z .trim() .replace(/^\|/, '') .replace(/\|$/, '') .split('|') .map((s) => s.trim()) const kopf = spalten(gekuerzt) i += 1 // Trennzeile ueberspringen const zeilenInhalt: string[][] = [] while (i + 1 < zeilen.length && (zeilen[i + 1] ?? '').trim().startsWith('|')) { i += 1 zeilenInhalt.push(spalten(zeilen[i] ?? '')) } bloecke.push({ art: 'tabelle', kopf, zeilen: zeilenInhalt }) continue } // --- Aufzaehlung ------------------------------------------------------- const punkt = gekuerzt.match(/^(?:[-*]|\d+\.)\s+(.*)$/) if (punkt) { absatzSchliessen() const nummeriert = /^\d+\./.test(gekuerzt) const punkte: string[] = [punkt[1]!] while (i + 1 < zeilen.length) { const naechste = (zeilen[i + 1] ?? '').trim() const weiterer = naechste.match(/^(?:[-*]|\d+\.)\s+(.*)$/) if (weiterer && /^\d+\./.test(naechste) === nummeriert) { punkte.push(weiterer[1]!) i += 1 continue } // Eingerueckte Fortsetzung gehoert zum letzten Punkt. if (naechste !== '' && /^\s{2,}\S/.test(zeilen[i + 1] ?? '')) { punkte[punkte.length - 1] += ` ${naechste}` i += 1 continue } break } bloecke.push({ art: 'liste', nummeriert, punkte }) continue } absatz.push(gekuerzt) } absatzSchliessen() return bloecke } export function MarkdownSeite({ inhalt, ohneTitel = true, className, }: { inhalt: string /** Die erste H1 weglassen - die Seite setzt ihre Ueberschrift selbst. */ ohneTitel?: boolean className?: string }) { const bloecke = zerlegen(inhalt, ohneTitel) return (
{bloecke.map((block, i) => { switch (block.art) { case 'ueberschrift': { const Tag = `h${block.ebene}` as 'h2' | 'h3' | 'h4' const groesse = block.ebene === 2 ? 'mt-10 text-xl font-semibold' : block.ebene === 3 ? 'mt-7 text-lg font-semibold' : 'mt-5 text-base font-semibold' return ( {block.text} ) } case 'absatz': return (

{inline(block.text, String(i))}

) case 'liste': { const Tag = block.nummeriert ? 'ol' : 'ul' return ( {block.punkte.map((punkt, j) => (
  • {inline(punkt, `${i}-${j}`)}
  • ))}
    ) } case 'tabelle': return ( // Tabellen scrollen im eigenen Rahmen - am Handy darf nie die // ganze Seite seitlich wandern.
    {block.kopf.map((spalte, j) => ( ))} {block.zeilen.map((zeile, j) => ( {zeile.map((zelle, k) => ( ))} ))}
    {inline(spalte, `${i}-k-${j}`)}
    {inline(zelle, `${i}-${j}-${k}`)}
    ) case 'zitat': return (
    {block.zeilen.map((zeile, j) => (

    {inline(zeile, `${i}-${j}`)}

    ))}
    ) case 'code': return (
                    {block.zeilen.join('\n')}
                  
    ) case 'trenner': return
    } })}
    ) }