From 796bc0d45023a7c9c834667b946b0ffc49dda957 Mon Sep 17 00:00:00 2001 From: simonpipe Date: Tue, 7 Jul 2026 20:50:52 +0200 Subject: [PATCH] Add Core.js --- Core.js | 380 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 Core.js diff --git a/Core.js b/Core.js new file mode 100644 index 0000000..c3441b3 --- /dev/null +++ b/Core.js @@ -0,0 +1,380 @@ +/** + * 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) + * @param {Object} plugin - Ein Objekt mit optionalen 'before'- und 'after'-Hooks + */ + use(plugin) { + this.plugins.push(plugin); + return this; // Ermöglicht Method Chaining + } + + /** + * Hauptfunktion: Wandelt SimonUp-Text (.up) in valides HTML um + * @param {string} text - Der rohe SimonUp-Text + * @returns {string} Das gerenderte HTML + */ + parse(text) { + // Zustand bei jedem Parse-Vorgang zurücksetzen + this.headings = []; + this.counters = [0, 0, 0, 0, 0, 0]; + + let html = text; + + // 1. Vorverarbeitung durch Plugins + for (const plugin of this.plugins) { + if (plugin.before) html = plugin.before(html); + } + + // 2. Block-Parsing (Strukturelle Blöcke wie &t, &toc, &-, etc.) + html = this._parseBlocks(html); + + // 3. Nachverarbeitung des Gesamtergebnisses & TOC-Injektion + html = this._injectTOC(html); + + // 4. Nachverarbeitung durch Plugins + for (const plugin of this.plugins) { + if (plugin.after) html = plugin.after(html); + } + + return html; + } + + /** + * INTERN: Verarbeitet strukturelle Blöcke zeilenweise + */ + _parseBlocks(text) { + // Normalisiere Zeilenumbrüche und teile den Text auf + const lines = text.replace(/\r\n/g, '\n').split('\n'); + let result = []; + let inBlock = false; + let blockType = null; + let blockMeta = ""; // Für zusätzliche Infos in der Startzeile (z.B. "3x" oder "Titel") + let blockBuffer = []; + + for (let i = 0; i < lines.length; i++) { + let line = lines[i]; + + // Block-Start erkennen (Zeile beginnt mit '&' und hat danach Inhalt) + 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; + } + + // Block-Ende erkennen (Ein einzelnes '&' auf einer eigenen Zeile) + if (inBlock && line.trim() === '&') { + result.push(this._renderBlock(blockType, blockMeta, blockBuffer)); + inBlock = false; + blockType = null; + blockMeta = ""; + continue; + } + + // Inhalt sammeln oder normalen Fließtext verarbeiten + if (inBlock) { + blockBuffer.push(line); + } else { + // Horizontale Linie als blockfreie Ausnahme abfangen + if (line.trim() === '___') { + result.push('
'); + } else { + // Normaler Fließtext außerhalb von Blöcken wandert in den Inline-Parser + result.push(this._parseInline(line)); + } + } + } + + // Am Ende leere Zeilen (Soft Breaks vs. Hard Breaks) korrekt verknüpfen + // Zwei aufeinanderfolgende leere Absätze erzeugen

, einfache Zeilenumbrüche
+ 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': + // Unser neuer, intelligenter TOC-Block + let tocHtml = `

\n`; + if (meta) tocHtml += `

${meta}

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

${intro}

\n`; + } + // Ein eindeutiger HTML-Kommentar als Platzhalter für die spätere Injektion + tocHtml += `\n
`; + return tocHtml; + + case '-': // Unsortierte Liste + return ''; + + case '+': // Nummerierte Liste + return '
    \n' + lines.filter(l => l.trim()).map(l => `
  1. ${this._parseInline(l)}
  2. `).join('\n') + '\n
'; + + case 'c': // Code-Block (Ignoriert Fluss-Formatierungen) + const langClass = meta ? ` class="language-${meta}"` : ''; + // Escaping für HTML-Sonderzeichen im Code-Block + const escapedCode = content.replace(/&/g, '&').replace(//g, '>'); + return `
${escapedCode}
`; + + case 'q': // Zitatblock + return `
${lines.map(l => this._parseInline(l)).join('
')}
`; + + case 'b': // Bibeltext + return `
${lines.map(l => this._parseInline(l)).join('
')}
`; + + case 'f': // Spoiler / Fold + return `
\n${meta || 'Details'}\n

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

\n
`; + + case 'i': // Infobox + return `
\n${lines.map(l => this._parseInline(l)).join('
')}\n
`; + + case 't': // Komplexe Multi-line-Tabellen-Logik & Galerien + return this._renderTable(meta, lines); + + default: + // Fallback für unbekannte Blocktypen + return `
${lines.map(l => this._parseInline(l)).join('
')}
`; + } + } + + /** + * INTERN: Hilfsfunktion zum Rendern von Multi-line-Tabellen und Galerien + */ + _renderTable(meta, lines) { + let tableHtml = '\n'; + // Einfache Breiten-Zuweisung über CSS (z.B. "3x" oder Prozentangaben) im Stylesheet spiegeln + // Der Kürze halber generieren wir hier das Grundgerüst: + let inRow = false; + let currentCells = []; + let cellBuffer = []; + + for (let line of lines) { + if (line.trim() === '') continue; + + // Zerlege die Zeile anhand des Spaltenrenners '|' + // Achtung: Ein escaped '|' (falls vorhanden) müsste beachtet werden + let parts = line.split('|'); + + for (let i = 0; i < parts.length; i++) { + let part = parts[i]; + if (i === parts.length - 1 && part.trim() === '') { + // Zeile wurde mit einem | sauber beendet + if (cellBuffer.length > 0 || part !== '') { + cellBuffer.push(part); + } + if (cellBuffer.length > 0) { + currentCells.push(cellBuffer.join('\n')); + cellBuffer = []; + } + // Zeile ist komplett! + 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, damit Formatierungen sie nicht überschreiben + html = html.replace(/\\(.)/g, (match, char) => ``); + + // 2. ÜBERSCHRIFTEN (Exakt nach unseren neuen Regeln verarbeitet) + + // Regel A: Automatische Nummerierung (3#x Titel 3#x) + html = html.replace(/([1-6])#x\s*(.*?)\s*\1#x/g, (match, level, title) => { + const lvl = parseInt(level); + const numStr = this._getAutoNumber(lvl); + const fullTitle = `${numStr} ${title}`; + const id = this._slugify(fullTitle); + + // Für das TOC erfassen + this.headings.push({ level: lvl, text: fullTitle, id: id }); + return `${fullTitle}`; + }); + + // Regel B: Vom TOC ausgeschlossene Titel (3#- Titel 3#-) + html = html.replace(/([1-6])#-\s*(.*?)\s*\1#-/g, (match, level, title) => { + const lvl = parseInt(level); + return `${title}`; // Wird NICHT in this.headings gespeichert + }); + + // Regel C: Manuelle Nummerierung (3# Titel 3#) + html = html.replace(/([1-6])#\s*(.*?)\s*\1#/g, (match, level, title) => { + const lvl = parseInt(level); + const id = this._slugify(title); + + this.headings.push({ level: lvl, text: title, id: id }); + return `${title}`; + }); + + // Regel D: Das schnelle, solitäre # für H1 (# Titel #) + html = html.replace(/#([^#]+)#/g, (match, title) => { + const id = this._slugify(title); + this.headings.push({ level: 1, text: title, id: id }); + return `

${title}

`; + }); + + // 3. STANDARD FLUSS-FORMATIERUNGEN + html = html.replace(/\*\*([^*]+)\*\*/g, '$1'); // Fett + html = html.replace(/\*([^*]+)\*/g, '$1'); // Medium + 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 + + // 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 + // Bilder mit definierter Breite: ![Alt w:300](URL) + html = html.replace(/!\[([^\]]+)\s+w:(\d+)\]\(([^)]+)\)/g, '$1'); + // Standard Bilder: ![Alt](URL) + html = html.replace(/!\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + // Komfort-Links (Neuer Tab): [Text]{URL} + html = html.replace(/\[([^\]]+)\]\{([^}]+)\}/g, '$1'); + // Standard-Links: [Text](URL) + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + + // 6. FUSSNOTEN (Manuell ^1[...]^ und Automatisch ^x[...]^) + html = html.replace(/\^x\[([^\]]+)\]\^/g, '[*]'); // Logik für Fußnoten-Endleiste optional + html = html.replace(/\^(\d+)\[([^\]]+)\]\^/g, '[$1]'); + + // 7. ESCAPE ZURÜCKWANDELN + html = html.replace(//g, (match, code) => String.fromCharCode(parseInt(code))); + + return html; + } + + /** + * INTERN: Errechnet die nächste fortlaufende Kapitelnummer (z.B. "1.2.1.") + */ + _getAutoNumber(level) { + let idx = level - 1; + this.counters[idx]++; + // Alle untergeordneten Ebenen radikal auf 0 zurücksetzen + for (let i = idx + 1; i < 6; i++) this.counters[i] = 0; + + // Array filtern, um nur die bis hierhin aktiven Nummernzähler zu extrahieren + let activeParts = this.counters.slice(0, level); + return activeParts.join('.') + '.'; + } + + /** + * INTERN: Macht aus Text eine URL-konforme ID (Anker für das TOC) + */ + _slugify(text) { + return text.toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') // Entferne Sonderzeichen + .replace(/\s+/g, '-'); // Ersetze Leerzeichen durch Bindestriche + } + + /** + * INTERN: Erzeugt die fertige HTML-Liste des TOC und injiziert sie in den Platzhalter + */ + _injectTOC(html) { + // Falls kein TOC-Block deklariert wurde oder keine Überschriften da sind, Platzhalter entfernen + 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) { + // Wenn es ein fertiger HTML-Block (z.B. Table, List, H1) ist, schließe offenen Absatz ab + 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() === '') { + // Bei einer echten Leerzeile (2x Enter) wird der Absatz gebunden + 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'); + } +} \ No newline at end of file