Markdown-Renderer: Bullet-Listen, ---Trenner und ##-Überschriften

- Devlogs im strukturierten Stil (Sektionen, Listen, Icons) rendern jetzt sauber
  auf der Webseite: Listen mit Neon-Markern, Trenner als Gradient-Linie,
  Überschriften im Mono-Tag-Stil
- Weiterhin ohne Library, Input wird nie als HTML interpretiert

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 10:49:42 +02:00
co-authored by Claude Opus 4.8
parent 3c8f442d6c
commit 4fb3a6d02c
2 changed files with 108 additions and 12 deletions
+70 -7
View File
@@ -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) => (
<p key={i}>
{block.split('\n').map((line, j, arr) => (
const out = [];
let para = [];
let list = [];
let key = 0;
const flushPara = () => {
if (para.length === 0) return;
out.push(
<p key={key++}>
{para.map((line, j) => (
<span key={j}>
{renderInline(line)}
{j < arr.length - 1 && <br />}
{j < para.length - 1 && <br />}
</span>
))}
</p>
));
);
para = [];
};
const flushList = () => {
if (list.length === 0) return;
out.push(
<ul key={key++} className="md-list">
{list.map((item, j) => (
<li key={j}>{renderInline(item)}</li>
))}
</ul>
);
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(<hr key={key++} className="md-hr" />);
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(
<h3 key={key++} className={`md-h md-h${heading[1].length}`}>
{renderInline(heading[2])}
</h3>
);
continue;
}
flushList();
para.push(raw);
}
flushPara();
flushList();
return out;
}
+33
View File
@@ -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 {