I made a markup language that compiles to HTML called NoirCode:

const textarea = document.getElementById('code');
const previewFrame = document.getElementById('previewFrame');
const errorMessage = document.getElementById('error-message');

/**
 * Compiles custom code to HTML.
 * @param {string} customCode - The custom code to compile.
 * @returns {Object} - An object containing the compiled HTML and any errors.
 */
function compileToHTML(customCode) {
    const lines = customCode.split('\n');
    let html = '';
    let errors = [];
    let i = 0;
    const totalLines = lines.length;
    let inBlockComment = false;

    while (i < totalLines) {
        let line = lines[i].trim();

        if (inBlockComment) {
            if (line.endsWith('#}')) {
                inBlockComment = false;
            }
            i++;
            continue;
        }

        if (line.startsWith('#{')) {
            inBlockComment = true;
            i++;
            continue;
        }

        if (line.startsWith('#') && !line.startsWith('#{')) {
            i++;
            continue;
        }

        if (line === '\\}') {
            errors.push(`Unexpected block closure '\\}' at line ${i + 1}.`);
            i++;
            continue;
        }

        const blockStartMatch = line.match(/^(\w+)(\[[^\]]+\])?(#[\w\-]+)?((?:\.[\w\-]+)*?)\\\{$/);
        if (blockStartMatch) {
            const tag = blockStartMatch[1].toLowerCase();
            const attrs = blockStartMatch[2] ? replaceIndexPlaceholders(blockStartMatch[2].slice(1, -1), 1) : '';
            const id = blockStartMatch[3] ? replaceIndexPlaceholders(blockStartMatch[3].slice(1), 1) : '';
            const classesRaw = blockStartMatch[4] ? blockStartMatch[4] : '';
            const classes = classesRaw.split('.').filter(cls => cls !== '');

            let blockContent = '';
            i++;
            let openBlocks = 1;

            while (i < totalLines && openBlocks > 0) {
                let currentLine = lines[i].trim();

                if (currentLine.endsWith('\\{')) {
                    openBlocks++;
                }

                if (currentLine === '\\}') {
                    openBlocks--;
                    if (openBlocks === 0) {
                        i++;
                        break;
                    }
                }

                if (openBlocks > 0) {
                    blockContent += lines[i] + '\n';
                }

                i++;
            }

            if (openBlocks > 0) {
                errors.push(`Unclosed block starting with <${tag}>.`);
                break;
            }

            let innerHTML = '';

            if (tag === 'script' || tag === 'style' || tag === 'html') {
                innerHTML = blockContent;
            } else {
                let compiledBlock;
                try {
                    compiledBlock = compileToHTML(blockContent);
                } catch (error) {
                    errors.push(`Line ${i + 1}: Unexpected error: ${error.message}`);
                    compiledBlock = { html: '', errors: [`Line ${i + 1}: Unexpected error: ${error.message}`] };
                }

                if (compiledBlock.errors.length > 0) {
                    errors = errors.concat(compiledBlock.errors);
                }
                innerHTML = compiledBlock.html;
            }

            let attrString = '';
            if (attrs) attrString += ` ${attrs}`;
            if (id) attrString += ` id="${id}"`;
            if (classes.length > 0) attrString += ` class="${classes.join(' ')}"`;

            html += `<${tag}${attrString}>\n${innerHTML}</${tag}>\n`;

            continue;
        }

        const inlineMatch = line.match(/^(\w+)(\[[^\]]+\])?(#[\w\-]+)?((?:\.[\w\-]+)*?)(?:\{([^}]*)\})?(?:\*(\d+))?$/);
        if (inlineMatch) {
            try {
                const tag = inlineMatch[1].toLowerCase();
                const attrs = inlineMatch[2] ? replaceIndexPlaceholders(inlineMatch[2].slice(1, -1), 1) : '';
                const id = inlineMatch[3] ? replaceIndexPlaceholders(inlineMatch[3].slice(1), 1) : '';
                const classesRaw = inlineMatch[4] ? inlineMatch[4] : '';
                const classes = classesRaw.split('.').filter(cls => cls !== '');
                const content = inlineMatch[5] || '';
                const repeat = inlineMatch[6] ? parseInt(inlineMatch[6]) : 1;
    
                let finalAttrString = '';
                if (attrs) finalAttrString += ` ${attrs}`;
                if (id) finalAttrString += ` id="${id}"`;
                if (classes.length > 0) finalAttrString += ` class="${classes.join(' ')}"`;
    
                for (let j = 1; j <= repeat; j++) {
                    const currentContent = replaceIndexPlaceholders(content, j);
                    const currentAttrString = replaceIndexPlaceholders(finalAttrString, j);
                    html += `<${tag}${currentAttrString}>${currentContent}</${tag}>\n`;
                }
    
                i++;
            } catch (err) {
                errors.push(`Line ${i + 1}: ${err.message}`);
            }
            continue;
        }

        const siblingTokens = splitRespectingBlocks(line, '+');
        siblingTokens.forEach(token => {
            const nestingTokens = splitRespectingBlocks(token, '>');
            const nestedHTML = processNesting(nestingTokens, 0, errors, i + 1);
            html += nestedHTML;
        });

        const textLineMatch = line.match(/^[^+\>]+$/);
        if (!blockStartMatch && !inlineMatch && textLineMatch) {
            html += `${escapeHTML(line)}\n`;
        }

        i++;
    }

    return { html, errors };

    /**
     * Recursive function to process nested tokens.
     * @param {Array} nestingTokens - Array of tokens split by '>'.
     * @param {number} index - Current index in the nestingTokens array.
     * @param {Array} errors - Array to collect error messages.
     * @param {number} lineNumber - Current line number for error reporting.
     * @returns {string} - The generated HTML string after processing nesting.
     */
    function processNesting(nestingTokens, index, errors, lineNumber) {
        if (index >= nestingTokens.length) return '';

        const nestedToken = nestingTokens[index].trim();
        const isLast = index === nestingTokens.length - 1;

        if (nestedToken === '') return '';

        let generatedHTML = '';

        try {
            const { tag, attributes, id, classes, content, repeat } = parseElement(nestedToken, lineNumber);
            let attrString = '';
            if (attributes) attrString += ` ${attributes}`;
            if (id) attrString += ` id="${id}"`;
            if (classes.length > 0) attrString += ` class="${classes.join(' ')}"`;

            for (let j = 1; j <= repeat; j++) {
                let finalContent = replaceIndexPlaceholders(content, j);
                let finalAttrString = replaceIndexPlaceholders(attrString, j);
                let finalId = replaceIndexPlaceholders(id, j);
                let finalClasses = classes.map(cls => replaceIndexPlaceholders(cls, j));

                if (finalId) {
                    finalAttrString = finalAttrString.replace(` id="${id}"`, ` id="${finalId}"`);
                }

                if (finalClasses.length > 0) {
                    const classRegex = /class="([^"]+)"/;
                    const match = finalAttrString.match(classRegex);
                    if (match) {
                        const existingClasses = match[1].split(' ');
                        const updatedClasses = [...new Set([...existingClasses, ...finalClasses])];
                        finalAttrString = finalAttrString.replace(classRegex, `class="${updatedClasses.join(' ')}"`);
                    } else {
                        finalAttrString += ` class="${finalClasses.join(' ')}"`;
                    }
                }

                generatedHTML += `<${tag}${finalAttrString}>`;

                if (finalContent) {
                    generatedHTML += finalContent;
                }

                if (!isLast) {
                    generatedHTML += '\n';
                    generatedHTML += processNesting(nestingTokens, index + 1, errors, lineNumber);
                    generatedHTML += `</${tag}>\n`;
                } else {
                    generatedHTML += `</${tag}>\n`;
                }
            }
        } catch (err) {
            if (nestedToken.startsWith('\\}')) {
                return '';
            } else {
                errors.push(`Line ${lineNumber}: ${err.message}`);
                generatedHTML += `${escapeHTML(nestedToken)}\n`;
            }
        }

        return generatedHTML;
    }
}

/**
 * Splits a string by a delimiter but ignores delimiters inside quotes or blocks.
 * @param {string} str - The string to split.
 * @param {string} delimiter - The delimiter to split by.
 * @returns {Array} - Array of split tokens.
 */
function splitRespectingBlocks(str, delimiter) {
    const tokens = [];
    let current = '';
    let inQuotes = false;
    let quoteChar = '';
    let escape = false;
    let blockDepth = 0;

    for (let i = 0; i < str.length; i++) {
        let char = str[i];

        if (escape) {
            current += char;
            escape = false;
            continue;
        }

        if (char === '\\') {
            escape = true;
            current += char;
            continue;
        }

        if ((char === '"' || char === "'") && !inQuotes) {
            inQuotes = true;
            quoteChar = char;
            current += char;
            continue;
        }

        if (char === quoteChar && inQuotes) {
            inQuotes = false;
            quoteChar = '';
            current += char;
            continue;
        }

        if (!inQuotes && char === '{') {
            blockDepth++;
            current += char;
            continue;
        }

        if (!inQuotes && char === '}') {
            blockDepth = Math.max(blockDepth - 1, 0);
            current += char;
            continue;
        }

        if (!inQuotes && blockDepth === 0 && char === delimiter) {
            tokens.push(current);
            current = '';
            continue;
        }

        current += char;
    }

    if (current !== '') {
        tokens.push(current);
    }

    return tokens;
}

/**
 * Replaces all instances of &index; with the provided index number.
 * @param {string} str - The string to process.
 * @param {number} index - The index to replace &index; with.
 * @returns {string} - The processed string.
 */
function replaceIndexPlaceholders(str, index) {
    return str.replace(/&index;/g, index);
}

/**
 * Parses an element definition from the custom syntax.
 * @param {string} token - A token from the custom code.
 * @param {number} lineNumber - The line number for error reporting.
 * @returns {Object} - Parsed element details.
 */
function parseElement(token, lineNumber) {
    const regex = /^(\w+)(\[[^\]]+\])?(#[\w\-]+)?((?:\.[\w\-]+)*?)(?:\{([^}]*)\})?(?:\*(\d+))?$/
    const match = token.match(regex);

    if (!match) {
        throw { message: `Invalid syntax near "${token}".`, line: lineNumber };
    }

    const [, tag, attrs, id, classNames, content, repeat] = match;
    let attributes = '';
    let classes = [];

    if (attrs) {
        const attrContent = attrs.slice(1, -1);
        const attrRegex = /(\w+)="([^"]+)"/g;
        let attrMatch;
        while ((attrMatch = attrRegex.exec(attrContent)) !== null) {
            const [, attrName, attrValue] = attrMatch;
            attributes += ` ${attrName}="${attrValue}"`;
        }

        const validAttrFormat = [...attrContent.matchAll(attrRegex)].length > 0;
        if (attributes.trim() === '' && attrContent.trim() !== '') {
            throw { message: `Attributes must be in the format key="value".`, line: lineNumber };
        }
    }

    if (classNames) {
        classes = classNames.split('.').filter(cls => cls !== '').map(cls => cls.trim());
    }

    const element = {
        tag: tag,
        attributes: attributes,
        id: id ? id.slice(1) : '',
        classes: classes,
        content: content ? content : '',
        repeat: repeat ? parseInt(repeat) : 1
    };

    return element;
}

/**
 * Escapes HTML special characters in a string.
 * @param {string} str - The string to escape.
 * @returns {string} - The escaped string.
 */
function escapeHTML(str) {
    const replacements = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#039;'
    };
    return str.replace(/[&<>"']/g, match => replacements[match]);
}

function updatePreview() {
    const textarea = document.getElementById('code');
    const previewFrame = document.getElementById('previewFrame');
    const errorMessage = document.getElementById('error-message');

    const customCode = textarea.value;
    let compilationResult;
    try {
        compilationResult = compileToHTML(customCode);
    } catch (error) {
        compilationResult = { html: '', errors: [`Unexpected error: ${error.message}`] };
    }

    const { html, errors } = compilationResult;

    if (errors && errors.length > 0) {
        errorMessage.textContent = errors.join(' | ');
        errorMessage.style.display = 'block';
        previewFrame.srcdoc = '';
    } else {
        errorMessage.textContent = '';
        errorMessage.style.display = 'none';
        const fullHTML = `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Preview</title>
    <style>
        body {
            background-color: #1e1e1e;
            color: #cccccc;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            padding: 20px;
        }
        h1, h2, h3, h4, h5, h6 {
            color: #ffffff;
        }
        p {
            color: #dcdcdc;
            margin-bottom: 10px;
        }
        button {
            padding: 10px 20px;
            background-color: #3498db;
            color: #fff;
            border: none;
            cursor: pointer;
            border-radius: 4px;
            margin-top: 10px;
        }
        button:hover {
            background-color: #2980b9;
        }
        a {
            color: #9b59b6;
        }
        a:hover {
            color: #8e44ad;
        }
        /* Additional styles for various elements */
        ul {
            list-style-type: disc;
            padding-left: 20px;
            margin-bottom: 10px;
        }
        li {
            margin-bottom: 5px;
        }
        input, textarea {
            background-color: #333333;
            color: #ffffff;
            border: 1px solid #555555;
            padding: 8px;
            border-radius: 4px;
            margin-top: 5px;
        }
        input:focus, textarea:focus {
            border-color: #3498db;
            outline: none;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 10px;
        }
        th, td {
            border: 1px solid #555555;
            padding: 8px;
            text-align: left;
        }
        th {
            background-color: #444444;
        }
        tr:nth-child(even) {
            background-color: #2d2d2d;
        }
        /* Embedded Styles */
        ${extractEmbeddedStyles(html)}
    </style>
</head>
<body>
${html}
</body>
</html>`;
        previewFrame.srcdoc = fullHTML;
    }
}

/**
 * Extracts embedded <style> blocks from the generated HTML to include in the preview's head.
 * @param {string} html - The generated HTML content.
 * @returns {string} - Extracted CSS from <style> blocks.
 */
function extractEmbeddedStyles(html) {
    const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
    let styles = '';
    let match;
    while ((match = styleRegex.exec(html)) !== null) {
        styles += match[1] + '\n';
    }
    return styles;
}

updatePreview();

let timeout;
const textareaElement = document.getElementById('code');

textareaElement.addEventListener('input', () => {
    clearTimeout(timeout);
    timeout = setTimeout(updatePreview, 300);
});

const tabEditor = document.getElementById('tab-editor');
const tabPreview = document.getElementById('tab-preview');
const editor = document.querySelector('.editor');
const preview = document.querySelector('.preview');

/**
 * Activates the specified tab and deactivates the other.
 * @param {string} tab - The tab to activate ('editor' or 'preview').
 */
function activateTab(tab) {
    tabEditor.classList.remove('active');
    tabPreview.classList.remove('active');

    editor.classList.remove('active');
    preview.classList.remove('active');

    if (tab === 'editor') {
        tabEditor.classList.add('active');
        editor.classList.add('active');
    } else if (tab === 'preview') {
        tabPreview.classList.add('active');
        preview.classList.add('active');
    }

    handleResponsiveTabs();
}

function handleResponsiveTabs() {
    const isMobile = window.innerWidth <= 768;

    if (isMobile) {
        if (!editor.classList.contains('active') && !preview.classList.contains('active')) {
            editor.classList.add('active');
        }
    } else {
        editor.classList.add('active');
        preview.classList.add('active');
    }
}

handleResponsiveTabs();

window.addEventListener('resize', handleResponsiveTabs);

tabEditor.addEventListener('click', () => activateTab('editor'));
tabPreview.addEventListener('click', () => activateTab('preview'));

textareaElement.addEventListener('keydown', e => {
    const openingChars = {
        '"': '"',
        "'": "'",
        '(': ')',
        '{': '}',
        '[': ']',
        '<': '>'
    };

    const closingChars = Object.values(openingChars);

    if (e.key === 'Backspace') {
        const cursorPos = textareaElement.selectionStart;
        const textBeforeCursor = textareaElement.value.substring(0, cursorPos);
        const lastTwoChars = textBeforeCursor.slice(-2);
        return;
    }

    if (e.key === '"' || e.key === "'") {
        const closingChar = openingChars[e.key];
        const cursorPos = textareaElement.selectionStart;
        const beforeCursor = textareaElement.value.substring(0, cursorPos);
        const afterCursor = textareaElement.value.substring(cursorPos);

        if (afterCursor.startsWith(closingChar)) {
            e.preventDefault();
            textareaElement.setSelectionRange(cursorPos + 1, cursorPos + 1);
        } else {
            e.preventDefault();
            textareaElement.value = beforeCursor + e.key + closingChar + afterCursor;
            setTimeout(() => {
                textareaElement.setSelectionRange(cursorPos + 1, cursorPos + 1);
                textareaElement.dispatchEvent(new Event('input'));
            }, 0);
        }
        return;
    }

    if (e.key in openingChars) {
        e.preventDefault();
        const closingChar = openingChars[e.key];
        const cursorPos = textareaElement.selectionStart;
        const beforeCursor = textareaElement.value.substring(0, cursorPos);
        const afterCursor = textareaElement.value.substring(cursorPos);

        textareaElement.value = beforeCursor + e.key + closingChar + afterCursor;
        setTimeout(() => {
            textareaElement.setSelectionRange(cursorPos + e.key.length, cursorPos + e.key.length);
            textareaElement.dispatchEvent(new Event('input'));
        }, 0);
    }

    if (closingChars.includes(e.key)) {
        const cursorPos = textareaElement.selectionStart;
        const nextChar = textareaElement.value.substring(cursorPos, cursorPos + 1);
        if (e.key === nextChar) {
            e.preventDefault();
            textareaElement.setSelectionRange(cursorPos + 1, cursorPos + 1);
        }
    }
});