diff --git a/frontend/src/markdown.jsx b/frontend/src/markdown.jsx index dc19c0a..0b53339 100644 --- a/frontend/src/markdown.jsx +++ b/frontend/src/markdown.jsx @@ -1,5 +1,6 @@ // Mini-Markdown-Renderer für Devlog-Prosa (Discord-Stil): **fett**, *kursiv*, -// `code`, [Links](url). Bewusst ohne Library — Input wird escaped, kein HTML-Injection. +// `code`, [Links](url), * Bullet-Listen, --- Trenner, ## Überschriften. +// Bewusst ohne Library — Input wird nie als HTML interpretiert, kein Injection. const TOKEN = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`|\[[^\]]+\]\(https?:\/\/[^\s)]+\))/g; function renderInline(text) { @@ -26,16 +27,78 @@ function renderInline(text) { }); } -/** Devlog-Text als React-Elemente (Absätze + Inline-Formatierung) */ +/** Devlog-Text als React-Elemente: Absätze, Listen, Trenner, Überschriften */ export function Markdown({ text }) { - return text.split(/\n{2,}/).map((block, i) => ( -

- {block.split('\n').map((line, j, arr) => ( - - {renderInline(line)} - {j < arr.length - 1 &&
} -
- ))} -

- )); + const out = []; + let para = []; + let list = []; + let key = 0; + + const flushPara = () => { + if (para.length === 0) return; + out.push( +

+ {para.map((line, j) => ( + + {renderInline(line)} + {j < para.length - 1 &&
} +
+ ))} +

+ ); + para = []; + }; + const flushList = () => { + if (list.length === 0) return; + out.push( + + ); + list = []; + }; + + for (const raw of text.split('\n')) { + const line = raw.trim(); + + if (!line) { + flushPara(); + flushList(); + continue; + } + // --- → Trenner + if (/^-{3,}$/.test(line)) { + flushPara(); + flushList(); + out.push(
); + continue; + } + // * Punkt / - Punkt → Liste (aber nicht *kursiv am Zeilenanfang*) + const li = line.match(/^[*-]\s+(.*)$/); + if (li) { + flushPara(); + list.push(li[1]); + continue; + } + // ## Überschrift (1-3 Raute-Ebenen) + const heading = line.match(/^(#{1,3})\s+(.*)$/); + if (heading) { + flushPara(); + flushList(); + out.push( +

+ {renderInline(heading[2])} +

+ ); + continue; + } + flushList(); + para.push(raw); + } + flushPara(); + flushList(); + + return out; } diff --git a/frontend/src/style.css b/frontend/src/style.css index f3aef19..e78727f 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -300,6 +300,39 @@ body::after { padding: .06rem .4rem; color: var(--success); } +.card-body .md-list { + margin: .55rem 0; + padding-left: 1.2rem; + list-style: none; +} +.card-body .md-list li { + position: relative; + padding-left: .2rem; + margin: .25rem 0; +} +.card-body .md-list li::before { + content: '▪'; + position: absolute; + left: -1.1rem; + color: var(--neon); + font-size: .8em; + top: .15em; +} +.card-body .md-hr { + border: none; + height: 1px; + background: linear-gradient(to right, var(--border), transparent); + margin: 1.1rem 0; +} +.card-body .md-h { + font-family: var(--mono); + font-size: .78rem; + letter-spacing: .2em; + text-transform: uppercase; + color: var(--neon); + margin: 1rem 0 .4rem; +} +.card-body .md-h2, .card-body .md-h3 { color: var(--muted); } /* Bildergalerie (heruntergeladene Devlog-Screenshots) */ .card-images {