// Wandelt Markdown in Pango-Markup für St.Label um.
// Unterstützt: Überschriften, Fett, Kursiv, Durchgestrichen, Code (inline und Block),
// Listen, Checkboxen, Zitate, Links, Trennlinien.
const CODE_BG = '#2b2b2b';
const LINK_COLOR = '#8ab4f8';
const QUOTE_COLOR = '#b0b0b0';
// Platzhalter für herausgenommene Code-Spans (Private-Use-Zeichen, kommt in Text nicht vor).
const HOLE = '';
export function escapeMarkup(text) {
return text.replace(/&/g, '&').replace(//g, '>');
}
function inline(text) {
// Inline-Code zuerst herausnehmen, damit Sternchen darin unberührt bleiben.
const codes = [];
let out = escapeMarkup(text).replace(/`([^`]+)`/g, (_, code) => {
codes.push(`${code}`);
return `${HOLE}${codes.length - 1}${HOLE}`;
});
out = out
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '[Bild: $1]')
.replace(/\[([^\]]+)\]\([^)]*\)/g, `$1`)
.replace(/\[\[([^\]]+)\]\]/g, `$1`)
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/__(.+?)__/g, '$1')
.replace(/~~(.+?)~~/g, '$1')
.replace(/(^|[^*\w])\*([^*\s](?:[^*]*[^*\s])?)\*(?!\w)/g, '$1$2')
.replace(/(^|[^_\w])_([^_\s](?:[^_]*[^_\s])?)_(?!\w)/g, '$1$2');
return out.replace(new RegExp(`${HOLE}(\\d+)${HOLE}`, 'g'), (_, i) => codes[Number(i)]);
}
// Absolute Punktgrößen: Schlüsselwörter wie xx-large skalieren im ClutterText den ganzen Text.
const HEADING_SIZE = ['18pt', '15pt', '12.5pt', '11pt', '11pt', '11pt'];
export function renderMarkdown(text) {
const lines = text.replace(/\r\n/g, '\n').split('\n');
const out = [];
let inCode = false;
for (const line of lines) {
if (/^\s*```/.test(line)) {
inCode = !inCode;
continue;
}
if (inCode) {
out.push(`${escapeMarkup(line) || ' '}`);
continue;
}
let match;
if ((match = line.match(/^(#{1,6})\s+(.*)$/))) {
const level = match[1].length;
out.push(`${inline(match[2])}`);
} else if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
out.push('────────────────────────────');
} else if ((match = line.match(/^(\s*)[-*+]\s+\[([ xX])\]\s+(.*)$/))) {
const box = match[2] === ' ' ? '☐' : '☑';
const body = match[2] === ' ' ? inline(match[3]) : `${inline(match[3])}`;
out.push(`${indent(match[1])}${box} ${body}`);
} else if ((match = line.match(/^(\s*)[-*+]\s+(.*)$/))) {
out.push(`${indent(match[1])}• ${inline(match[2])}`);
} else if ((match = line.match(/^(\s*)(\d+)[.)]\s+(.*)$/))) {
out.push(`${indent(match[1])}${match[2]}. ${inline(match[3])}`);
} else if ((match = line.match(/^>\s?(.*)$/))) {
out.push(`▎ ${inline(match[1])}`);
} else {
out.push(inline(line));
}
}
return out.join('\n');
}
// Verschachtelte Listen: je 2 Leerzeichen oder 1 Tab eine Ebene tiefer.
function indent(spaces) {
const level = spaces.replace(/\t/g, ' ').length / 2;
return ' '.repeat(Math.floor(level) + 1);
}