- 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>
105 lines
3.1 KiB
React
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;
|
|
}
|