Let's include the subitems, they're buggy when they come to subitems and sub subitems:

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

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 === '') {
            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), 0) : '';
            const id = blockStartMatch[3] ? replaceIndexPlaceholders(blockStartMatch[3].slice(1), 0) : '';
            const classesRaw = blockStartMatch[4] ? blockStartMatch[4] : '';
            const classes = classesRaw.split('.').filter(cls => cls !== '');

            let blockContent = '';
            i++;
            while (i < totalLines && !lines[i].trim().endsWith('\\}')) {
                blockContent += lines[i] + '\n';
                i++;
            }

            if (i >= totalLines) {
                errors.push(`Unclosed block starting with <${tag}>.`);
                break;
            }

            if (lines[i].trim().endsWith('\\}')) {
                i++;
            }

            let innerHTML = '';

            if (tag === 'script' || tag === 'style' || tag === 'html') {
                innerHTML = blockContent;
            } else {
                const compiledBlock = compileToHTML(blockContent);
                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 siblingTokens = splitRespectingBlocks(line, '+');
        siblingTokens.forEach(token => {
            const nestingTokens = splitRespectingBlocks(token, '>');
            nestingTokens.forEach((nestedToken, index) => {
                nestedToken = nestedToken.trim();
                const isLast = index === nestingTokens.length - 1;

                if (nestedToken === '') return;

                try {
                    const { tag, attributes, id, classes, content, repeat } = parseElement(nestedToken, i + 1);
                    let 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) {
                            // Replace existing class names with updated ones
                            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(' ')}"`;
                            }
                        }

                        if (isLast) {
                            if (finalContent) {
                                html += `<${tag}${finalAttrString}>${finalContent}</${tag}>\n`;
                            } else {
                                html += `<${tag}${finalAttrString}></${tag}>\n`;
                            }
                        } else {
                            // For non-last tokens in nesting, open the tag
                            html += `<${tag}${finalAttrString}>\n`;
                        }
                    }
                } catch (err) {
                    errors.push(`Line ${err.line}: ${err.message}`);
                }
            });
        });

        i++;
    }

    return { html, errors };
}

/**
 * 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;

    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 === 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.slice(1, -1) : '',
        repeat: repeat ? parseInt(repeat) : 1
    };

    return element;
}

function updatePreview() {
    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;
textarea.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');

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

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

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

    const closingChars = Object.values(openingChars);

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

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

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

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

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

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

---

Code I tried:

ul>li{item}>ul>li{subitem}
ul>li{item}>ul>li{subitem}>ul>li{sub subitem}

---

What I got instead:

<ul>
<li>
<ul>
<li>subitem</li>
<ul>
<li>
<ul>
<li>
<ul>
<li>sub subitem</li>


</ul></li></ul></li></ul></ul></li></ul>

---

What I was expecting:

<ul>
    <li>item
        <ul>
            <li>subitem</li>
        </ul>
    </li>
    <li>item
        <ul>
            <li>subitem
                <ul>
                    <li>sub subitem</li>
                </ul>
            </li>
        </ul>
    </li>
</ul>