Files
d4rkbot/frontend/src/markdown.jsx
T
D4rkst3randClaude Opus 4.8 4fb3a6d02c 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>
2026-07-23 10:49:42 +02:00

105 lines
3.1 KiB
React

// Mini-Markdown-Renderer für Devlog-Prosa (Discord-Stil): **fett**, *kursiv*,
// `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) {
return text.split(TOKEN).map((part, i) => {
// Fett/Kursiv rekursiv rendern, damit z. B. Links darin funktionieren
if (part.startsWith('**') && part.endsWith('**')) {
return <strong key={i}>{renderInline(part.slice(2, -2))}</strong>;
}
if (part.startsWith('*') && part.endsWith('*') && part.length > 2) {
return <em key={i}>{renderInline(part.slice(1, -1))}</em>;
}
if (part.startsWith('`') && part.endsWith('`')) {
return <code key={i}>{part.slice(1, -1)}</code>;
}
const link = part.match(/^\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)$/);
if (link) {
return (
<a key={i} href={link[2]} target="_blank" rel="noreferrer">
{link[1]}
</a>
);
}
return part;
});
}
/** Devlog-Text als React-Elemente: Absätze, Listen, Trenner, Überschriften */
export function Markdown({ text }) {
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 < 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;
}