/**
* 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 '\n' + lines.filter(l => l.trim()).map(l => `- ${this._parseInline(l)}
`).join('\n') + '\n
';
case '+':
return '\n' + lines.filter(l => l.trim()).map(l => `- ${this._parseInline(l)}
`).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 => `| ${this._parseInline(c.trim())} | `).join('\n') + '\n
\n';
currentCells = [];
} else {
cellBuffer.push(part);
if (i < parts.length - 1) {
currentCells.push(cellBuffer.join('\n'));
cellBuffer = [];
}
}
}
}
tableHtml += '
';
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, '
');
html = html.replace(/!\[([^\]]+)\]\(([^)]+)\)/g, '
');
html = html.replace(/\[([^\]]+)\]\{([^}]+)\}/g, '$1');
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');
// 6. FUSSNOTEN
html = html.replace(/\^x\[([^\]]+)\]\^/g, '');
html = html.replace(/\^(\d+)\[([^\]]+)\]\^/g, '');
// 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';
let currentLevel = 1;
for (const heading of this.headings) {
if (heading.level > currentLevel) {
tocListHtml += '\n'.repeat(heading.level - currentLevel);
} else if (heading.level < currentLevel) {
tocListHtml += '
\n'.repeat(currentLevel - heading.level);
}
currentLevel = heading.level;
tocListHtml += `- ${heading.text}
\n`;
}
if (currentLevel > 1) {
tocListHtml += '
\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');
}
}