I made a markup language that compiles to HTML, called it NoirCode, and I made an editor for it, called NoirCode Studio:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>NoirCode Studio</title>
    <style>
        /* Reset some default styles */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        html, body {
            height: 100%;
        }

        body {
            background-color: #1e1e1e;
            color: #cccccc;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            display: flex;
            flex-direction: column;
        }

        header {
            background-color: #252526;
            padding: 15px 20px;
            display: flex;
            align-items: center;
            justify-content: space-between;
            font-size: 1.5rem;
            font-weight: bold;
            border-bottom: 1px solid #333;
            position: relative;
        }

        /* Tabs for Mobile */
        .tabs {
            display: none; /* Hidden by default, shown on mobile */
        }

        .tabs button {
            background: none;
            border: none;
            color: #cccccc;
            padding: 10px 15px;
            cursor: pointer;
            font-size: 1rem;
            transition: background-color 0.3s;
        }

        .tabs button.active, .tabs button:hover {
            background-color: #3a3a3a;
            border-radius: 4px;
        }

        .container {
            flex: 1;
            display: flex;
            overflow: hidden;
        }

        .editor, .preview {
            flex: 1;
            padding: 20px;
            overflow: auto;
        }

        .editor {
            background-color: #1e1e1e;
            border-right: 1px solid #333;
            display: flex;
            flex-direction: column;
        }

        .editor textarea {
            width: 100%;
            height: 100%;
            background-color: #1e1e1e;
            color: #cccccc;
            border: none;
            resize: none;
            font-family: 'Courier New', Courier, monospace;
            font-size: 1rem;
            outline: none;
        }

        .error {
            color: #e74c3c;
            margin-top: 10px;
            font-size: 0.9rem;
            display: none;
        }

        .preview {
            background-color: #2d2d2d;
            overflow: hidden;
        }

        /* Scrollbar styling for WebKit */
        ::-webkit-scrollbar {
            width: 8px;
            height: 8px;
        }

        ::-webkit-scrollbar-track {
            background: #1e1e1e;
        }

        ::-webkit-scrollbar-thumb {
            background-color: #555;
            border-radius: 4px;
        }

        /* Responsive Design */
        @media (max-width: 768px) {
            .container {
                flex-direction: column;
            }
            .editor, .preview {
                flex: 1; /* Allow flex items to grow */
                display: none; /* Hide both by default */
                height: 100%; /* Ensure they can take full height when displayed */
            }
            .editor.active, .preview.active {
                display: flex;
                flex-direction: column;
            }
            .editor {
                border-right: none;
                border-bottom: 1px solid #333;
            }
            .tabs {
                display: flex;
            }
            .error {
                display: block;
            }
        }
    </style>
</head>
<body>
    <header>
        <div>NoirCode Studio</div>
        <div class="tabs">
            <button id="tab-editor" class="active">Editor</button>
            <button id="tab-preview">Preview</button>
        </div>
    </header>
    <div class="container">
        <div class="editor active">
            <textarea id="code" placeholder="Type your custom code here...">
# This is a single-line comment

#{
This is a multi-line comment.
It spans multiple lines.
#}

h1{Welcome to NoirCode Studio}

p{NoirCode Studio is a powerful and intuitive editor designed for NoirCode, a streamlined markup language that enhances HTML development.}

h2{Features}

ul\{
    li{Real-time preview}
    li{Error detection}
    li{Sleek design}
\}

h2{Getting Started}

p{Begin by typing your NoirCode in the editor. Use the preview pane to see the rendered HTML instantly. NoirCode Studio supports advanced functionalities like nesting elements, adding attributes, and repeating elements efficiently.}

div[title="Index: &index;"]{&index;. Hi}*5

button[role="button" title="Click me"].click-me.click-me2{Click me}
button[role="button" title="Click me"].click-me.click-me1{Click me}

html\{
<button class="test">Test 2</button>
\}

script\{
document.querySelectorAll('.click-me').forEach(btn => {
    btn.addEventListener('click', () => {
        alert('Button clicked!');
    });
});
\}

style\{
.click-me {
    background-color: #3498db;
    color: white;
    padding: 10px 20px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    margin: 5px;
}
.click-me:hover {
    background-color: #2980b9;
}
\}
            </textarea>
            <div id="error-message" class="error"></div>
        </div>
        <div class="preview">
            <iframe id="previewFrame" srcdoc="" frameborder="0" style="width: 100%; height: 100%; background-color: transparent;"></iframe>
        </div>
    </div>

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

        /**
         * Compiler Function:
         * Transforms custom NoirCode syntax into standard HTML.
         * Supports:
         * - Tag definitions (e.g., h1{Hello})
         * - Attributes and IDs (e.g., div[class="container"]#main{Content})
         * - Classes (e.g., .class1.class2)
         * - Nesting with '>' (e.g., div>p{Text})
         * - Sibling with '+' (e.g., h1{Title}+p{Subtitle})
         * - Repetition with '*' (e.g., div{&index;. Hi}*5)
         * - Raw HTML, CSS, and JS blocks (html\{ \}, style\{ \}, script\{ \})
         * - Single-line comments starting with '#'
         * - Multi-line comments enclosed within '#{ ... #}'
         */
        function compileToHTML(customCode) {
            const lines = customCode.split('\n');
            let html = '';
            let errors = [];
            let i = 0;
            const totalLines = lines.length;
            let inBlockComment = false; // Flag to track multi-line comments

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

                // Handle multi-line comments
                if (inBlockComment) {
                    if (line.endsWith('#}')) {
                        inBlockComment = false;
                    }
                    i++;
                    continue; // Skip lines inside multi-line comments
                }

                // Check for the start of a multi-line comment
                if (line.startsWith('#{')) {
                    inBlockComment = true;
                    i++;
                    continue; // Skip the line that starts the multi-line comment
                }

                // Skip single-line comments
                if (line.startsWith('#') && !line.startsWith('#{')) {
                    i++;
                    continue;
                }

                // Skip empty lines
                if (line === '') {
                    i++;
                    continue;
                }

                // Check for block start (e.g., html\{ )
                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 !== '');

                    // Collect block content until \}
                    let blockContent = '';
                    i++; // Move to the next line after block start
                    while (i < totalLines && !lines[i].trim().endsWith('\\}')) {
                        blockContent += lines[i] + '\n';
                        i++;
                    }

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

                    // If the line ends with '\}', exclude it
                    if (lines[i].trim().endsWith('\\}')) {
                        i++; // Skip the closing \}
                    }

                    let innerHTML = '';

                    if (tag === 'script' || tag === 'style' || tag === 'html') {
                        // Treat content as raw code
                        innerHTML = blockContent;
                    } else {
                        // Recursively compile the block content
                        const compiledBlock = compileToHTML(blockContent);
                        if (compiledBlock.errors.length > 0) {
                            errors = errors.concat(compiledBlock.errors);
                        }
                        innerHTML = compiledBlock.html;
                    }

                    // Append the compiled block to the 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;
                }

                // Handle sibling elements using '+'
                const siblingTokens = splitRespectingBlocks(line, '+');
                siblingTokens.forEach(token => {
                    // Handle nesting with '>'
                    const nestingTokens = splitRespectingBlocks(token, '>');
                    nestingTokens.forEach((nestedToken, index) => {
                        nestedToken = nestedToken.trim();
                        const isLast = index === nestingTokens.length - 1;

                        if (nestedToken === '') return; // Skip empty tokens

                        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; // Keep the backslash for block parsing
                    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) {
            // Updated regex to include classes
            // Example matches:
            // div[role="main"]#container.class1.class2{Content}*3
            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) {
                // Remove the surrounding brackets
                const attrContent = attrs.slice(1, -1);
                // Validate attribute format
                const attrRegex = /(\w+)="([^"]+)"/g;
                let attrMatch;
                while ((attrMatch = attrRegex.exec(attrContent)) !== null) {
                    const [ , attrName, attrValue ] = attrMatch;
                    attributes += ` ${attrName}="${attrValue}"`;
                }

                // Check if attributes are correctly formatted
                const validAttrFormat = [...attrContent.matchAll(attrRegex)].length > 0;
                // If attributes are present but no valid attributes were parsed
                if (attributes.trim() === '' && attrContent.trim() !== '') {
                    throw { message: `Attributes must be in the format key="value".`, line: lineNumber };
                }
            }

            if (classNames) {
                // Remove the leading dots and split into individual classes
                classes = classNames.split('.').filter(cls => cls !== '').map(cls => cls.trim());
            }

            const element = {
                tag: tag,
                attributes: attributes,
                id: id ? id.slice(1) : '', // Remove '#' from id
                classes: classes, // Array of classes
                content: content ? content.slice(1, -1) : '',
                repeat: repeat ? parseInt(repeat) : 1
            };

            return element;
        }

        /**
         * Updates the preview iframe based on the compiled HTML.
         */
        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 = ''; // Clear preview if there are errors
            } else {
                errorMessage.textContent = '';
                errorMessage.style.display = 'none';
                // Wrap with basic HTML structure and custom dark theme
                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;
        }

        // Initialize preview
        updatePreview();

        // Update preview on input with a slight debounce
        let timeout;
        textarea.addEventListener('input', () => {
            clearTimeout(timeout);
            timeout = setTimeout(updatePreview, 300);
        });

        // Tab functionality for mobile
        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'));

        /**
         * Auto-Close Feature:
         * Automatically closes quotes, brackets, braces, and handles \{ ... \} blocks.
         */
        textarea.addEventListener('keydown', function(e) {
            const openingChars = {
                '"': '"',
                "'": "'",
                '(': ')',
                '{': '}',
                '[': ']',
                '<': '>',
                '\\{': '\\}'
            };

            const closingChars = Object.values(openingChars);

            // Handle backspace for \{ case
            if (e.key === 'Backspace') {
                const cursorPos = textarea.selectionStart;
                const textBeforeCursor = textarea.value.substring(0, cursorPos);
                const lastTwoChars = textBeforeCursor.slice(-2);
                if (lastTwoChars === '\\}') {
                    e.preventDefault();
                    textarea.value = textarea.value.slice(0, -2);
                    textarea.setSelectionRange(cursorPos - 2, cursorPos - 2);
                }
                return;
            }

            // Separate handling for quotes to ensure proper caret positioning
            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);

                // Check if the next character is already a closing quote to prevent duplication
                if (afterCursor.startsWith(closingChar)) {
                    e.preventDefault();
                    // Move cursor over the existing closing quote
                    textarea.setSelectionRange(cursorPos + 1, cursorPos + 1);
                } else {
                    e.preventDefault();
                    // Insert opening and closing quotes
                    textarea.value = beforeCursor + e.key + closingChar + afterCursor;
                    // Place the caret between the quotes using setTimeout to ensure it happens after value update
                    setTimeout(() => {
                        textarea.setSelectionRange(cursorPos + 1, cursorPos + 1);
                        // Dispatch the 'input' event to mimic user typing
                        textarea.dispatchEvent(new Event('input'));
                    }, 0);
                }
                return;
            }

            // Check if the key pressed is one of the other opening characters
            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);

                // Special handling for \{ ... \}
                if (e.key === '\\') {
                    // Look ahead to see if the next character is '{'
                    if (afterCursor.startsWith('{')) {
                        // Move cursor past '\{'
                        textarea.value = beforeCursor + '\\{' + afterCursor.slice(1);
                        textarea.setSelectionRange(cursorPos + 2, cursorPos + 2);
                        return;
                    }
                }

                // Insert the opening and closing character
                textarea.value = beforeCursor + e.key + closingChar + afterCursor;
                // Place the caret between the opening and closing character using setTimeout
                setTimeout(() => {
                    textarea.setSelectionRange(cursorPos + e.key.length, cursorPos + e.key.length);
                    // Dispatch the 'input' event to mimic user typing
                    textarea.dispatchEvent(new Event('input'));
                }, 0);
            }

            // Optionally, prevent typing a closing character if it's already there
            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);
                }
            }
        });

        /**
         * Input Event Listener for handling \{ ... \} auto-closing.
         * Ensures that when a user types \{, a corresponding \} is added.
         * This is to handle multi-line NoirCode blocks.
         */
        textarea.addEventListener('input', function(e) {
            const cursorPos = textarea.selectionStart;
            const textBeforeCursor = textarea.value.substring(0, cursorPos);
            const lastTwoChars = textBeforeCursor.slice(-2);

            // If user typed '\{', auto-insert '\}' after
            if (lastTwoChars === '\\{') {
                // Insert '\}' after the cursor
                const afterCursor = textarea.value.substring(cursorPos);
                textarea.value = textBeforeCursor + '\\}' + afterCursor;
                // Move cursor back to between '\{' and '\}'
                textarea.setSelectionRange(cursorPos, cursorPos);
                
                // Dispatch the 'input' event to mimic user typing
                textarea.dispatchEvent(new Event('input'));
            }
        });
    </script>
</body>
</html>