/** * SimonUp Core Compiler (Core.js) * Eine moderne, fluss- und blockorientierte Auszeichnungssprache. */ class SimonUp { constructor() { this.plugins = []; this.headings = []; // Speichert alle gefundenen Überschriften für das TOC this.counters = [0, 0, 0, 0, 0, 0]; // Zähler für die hierarchischen Ebenen 1 bis 6 } /** * Registriert eine Erweiterung (Plugin) */ use(plugin) { this.plugins.push(plugin); return this; } /** * Hauptfunktion: Wandelt SimonUp-Text (.up) in valides HTML um */ parse(text) { this.headings = []; this.counters = [0, 0, 0, 0, 0, 0]; let html = text; for (const plugin of this.plugins) { if (plugin.before) html = plugin.before(html); } html = this._parseBlocks(html); html = this._injectTOC(html); for (const plugin of this.plugins) { if (plugin.after) html = plugin.after(html); } return html; } /** * INTERN: Verarbeitet strukturelle Blöcke zeilenweise */ _parseBlocks(text) { const lines = text.replace(/\r\n/g, '\n').split('\n'); let result = []; let inBlock = false; let blockType = null; let blockMeta = ""; let blockBuffer = []; for (let i = 0; i < lines.length; i++) { let line = lines[i]; if (!inBlock && line.startsWith('&') && line.trim().length > 1) { inBlock = true; const fullHeader = line.substring(1).trim(); const spaceIndex = fullHeader.indexOf(' '); if (spaceIndex !== -1) { blockType = fullHeader.substring(0, spaceIndex).toLowerCase(); blockMeta = fullHeader.substring(spaceIndex).trim(); } else { blockType = fullHeader.toLowerCase(); blockMeta = ""; } blockBuffer = []; continue; } if (inBlock && line.trim() === '&') { result.push(this._renderBlock(blockType, blockMeta, blockBuffer)); inBlock = false; blockType = null; blockMeta = ""; continue; } if (inBlock) { blockBuffer.push(line); } else { if (line.trim() === '___') { result.push('
'); } else { result.push(this._parseInline(line)); } } } return this._processParagraphs(result); } /** * INTERN: Erzeugt das HTML für einen geschlossenen Block */ _renderBlock(type, meta, lines) { const content = lines.join('\n'); switch (type) { case 'toc': let tocHtml = `
\n`; if (meta) tocHtml += `

${meta}

\n`; if (content.trim()) { const intro = lines.map(l => this._parseInline(l)).join('
'); tocHtml += `

${intro}

\n`; } tocHtml += `\n
`; return tocHtml; case '-': return ''; case '+': return '
    \n' + lines.filter(l => l.trim()).map(l => `
  1. ${this._parseInline(l)}
  2. `).join('\n') + '\n
'; case 'c': const langClass = meta ? ` class="language-${meta}"` : ''; const poolCode = content.replace(/&/g, '&').replace(//g, '>'); return `
${poolCode}
`; case 'q': return `
${lines.map(l => this._parseInline(l)).join('
')}
`; case 'b': return `
${lines.map(l => this._parseInline(l)).join('
')}
`; case 'f': return `
\n${meta || 'Details'}\n

${lines.map(l => this._parseInline(l)).join('
')}

\n
`; case 'i': return `
\n${lines.map(l => this._parseInline(l)).join('
')}\n
`; case 't': return this._renderTable(meta, lines); default: return `
${lines.map(l => this._parseInline(l)).join('
')}
`; } } /** * INTERN: Hilfsfunktion zum Rendern von Multi-line-Tabellen und Galerien */ _renderTable(meta, lines) { let tableHtml = '\n'; let currentCells = []; let cellBuffer = []; for (let line of lines) { if (line.trim() === '') continue; let parts = line.split('|'); for (let i = 0; i < parts.length; i++) { let part = parts[i]; if (i === parts.length - 1 && part.trim() === '') { if (cellBuffer.length > 0 || part !== '') { cellBuffer.push(part); } if (cellBuffer.length > 0) { currentCells.push(cellBuffer.join('\n')); cellBuffer = []; } tableHtml += '\n' + currentCells.map(c => ``).join('\n') + '\n\n'; currentCells = []; } else { cellBuffer.push(part); if (i < parts.length - 1) { currentCells.push(cellBuffer.join('\n')); cellBuffer = []; } } } } tableHtml += '
${this._parseInline(c.trim())}
'; return tableHtml; } /** * INTERN: Verarbeitet Fluss-Formatierungen (Inline) mittels RegEx */ _parseInline(text) { if (!text) return ''; let html = text; // 1. ESCAPE: Temporär schützen html = html.replace(/\\(.)/g, (match, char) => ``); // 2. ÜBERSCHRIFTEN (Flusssicher angepasst) // Regel A: Automatische Nummerierung (3#x Titel 3#x Restlicher Text) html = html.replace(/([1-6])#x\s*([^#]+?)\s*\1#x(.*)/g, (match, level, title, rest) => { const lvl = parseInt(level); const numStr = this._getAutoNumber(lvl); const fullTitle = `${numStr} ${title.trim()}`; const id = this._slugify(fullTitle); this.headings.push({ level: lvl, text: fullTitle, id: id }); return `${fullTitle}${rest}`; }); // Regel B: Vom TOC ausgeschlossene Titel (3#- Titel 3#- Restlicher Text) html = html.replace(/([1-6])#-\s*([^#]+?)\s*\1#-(.*)/g, (match, level, title, rest) => { const lvl = parseInt(level); return `${title.trim()}${rest}`; }); // Regel C: Manuelle Nummerierung (3# Titel 3# Restlicher Text) html = html.replace(/([1-6])#\s*([^#]+?)\s*\1#(.*)/g, (match, level, title, rest) => { const lvl = parseInt(level); const id = this._slugify(title.trim()); this.headings.push({ level: lvl, text: title.trim(), id: id }); return `${title.trim()}${rest}`; }); // Regel D: Das schnelle, solitäre # für H1 (# Titel # Restlicher Text) html = html.replace(/#([^#]+)#(.*)/g, (match, title, rest) => { const id = this._slugify(title.trim()); this.headings.push({ level: 1, text: title.trim(), id: id }); return `

${title.trim()}

${rest}`; }); // 3. STANDARD FLUSS-FORMATIERUNGEN (In priorisierter Reihenfolge) html = html.replace(/\*\*([^*]+)\*\*/g, '$1'); // Fett html = html.replace(/\/([^/]+)\//g, '$1'); // Kursiv html = html.replace(/_([^_]+)_/g, '$1'); // Unterstrichen html = html.replace(/~([^~]+)~/g, '$1'); // Durchgestrichen html = html.replace(/=([^=]+)=/g, '$1'); // Highlight html = html.replace(/\^([^^]+)\^/g, '$1'); // Hochgestellt html = html.replace(/°([^°]+)°/g, '$1'); // Tiefgestellt html = html.replace(/`([^`]+)`/g, '$1'); // Inline-Code // 3.1 MEDIUM-FORMATIERUNG (Sucht nach *, ignoriert aber bereits generiertes HTML) html = html.replace(/(?]*)\*([^*<\/>]+)\*(?![^<]*>)/g, '$1'); // 4. AUSRICHTUNG & EINRÜCKUNG (Basiszeichen '>') html = html.replace(/>l ([^>]+)>/g, '
$1
'); html = html.replace(/>r ([^>]+)>/g, '
$1
'); html = html.replace(/>c ([^>]+)>/g, '
$1
'); html = html.replace(/>j ([^>]+)>/g, '
$1
'); html = html.replace(/>tt ([^>]+)>/g, '
$1
'); html = html.replace(/>t ([^>]+)>/g, '
$1
'); // 5. LINKS & MEDIEN html = html.replace(/!\[([^\]]+)\s+w:(\d+)\]\(([^)]+)\)/g, '$1'); html = html.replace(/!\[([^\]]+)\]\(([^)]+)\)/g, '$1'); html = html.replace(/\[([^\]]+)\]\{([^}]+)\}/g, '$1'); html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // 6. FUSSNOTEN html = html.replace(/\^x\[([^\]]+)\]\^/g, '[*]'); html = html.replace(/\^(\d+)\[([^\]]+)\]\^/g, '[$1]'); // 7. ESCAPE ZURÜCKWANDELN html = html.replace(/<\!--SU_ESC_(\d+)-->/g, (match, code) => String.fromCharCode(parseInt(code))); return html; } /** * INTERN: Errechnet die nächste fortlaufende Kapitelnummer */ _getAutoNumber(level) { let idx = level - 1; this.counters[idx]++; for (let i = idx + 1; i < 6; i++) this.counters[i] = 0; let activeParts = this.counters.slice(0, level); return activeParts.join('.') + '.'; } /** * INTERN: Macht aus Text eine URL-konforme ID */ _slugify(text) { return text.toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-'); } /** * INTERN: Erzeugt die fertige HTML-Liste des TOC und injiziert sie in den Platzhalter */ _injectTOC(html) { if (this.headings.length === 0) { return html.replace('', ''); } let tocListHtml = '
\n\n'.repeat(currentLevel - 1); } tocListHtml += '\n
'; return html.replace('', tocListHtml); } /** * INTERN: Analysiert leere Zeilen, um Absätze (

) und Zeilenumbrüche (
) zu trennen */ _processParagraphs(elements) { let processed = []; let paragraphBuffer = []; for (let element of elements) { if (element.startsWith('<') && !element.startsWith('') && !element.startsWith('') && !element.startsWith('') && !element.startsWith('') && !element.startsWith('') && !element.startsWith(' 0) { processed.push(`

${paragraphBuffer.join('
')}

`); paragraphBuffer = []; } processed.push(element); } else if (element.trim() === '') { if (paragraphBuffer.length > 0) { processed.push(`

${paragraphBuffer.join('
')}

`); paragraphBuffer = []; } } else { paragraphBuffer.push(element); } } if (paragraphBuffer.length > 0) { processed.push(`

${paragraphBuffer.join('
')}

`); } return processed.join('\n'); } }