<!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...">
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{&index;. Hi}*5
            </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})
         * - Nesting with '>' (e.g., div>p{Text})
         * - Sibling with '+' (e.g., h1{Title}+p{Subtitle})
         * - Repetition with '*' (e.g., div{&index;. Hi}*5)
         */
        function compileToHTML(customCode) {
            const lines = customCode.split('\n');
            let html = '';
            let errors = [];
            let i = 0;
            const totalLines = lines.length;

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

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

                // Check for block start (e.g., ul\{)
                const blockStartMatch = line.match(/^(\w+)(\[[^\]]+\])?(#[\w\-]+)?\\\{$/);
                if (blockStartMatch) {
                    const tag = blockStartMatch[1];
                    const attrs = blockStartMatch[2] ? replaceIndexPlaceholders(blockStartMatch[2].slice(1, -1), 0) : '';
                    const id = blockStartMatch[3] ? replaceIndexPlaceholders(blockStartMatch[3].slice(1), 0) : '';

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

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

                    // Recursively compile the block content
                    const compiledBlock = compileToHTML(blockContent);
                    if (compiledBlock.errors.length > 0) {
                        errors = errors.concat(compiledBlock.errors);
                    }

                    // Append the compiled block to the HTML
                    let attrString = '';
                    if (attrs) attrString += ` ${attrs}`;
                    if (id) attrString += ` id="${id}"`;

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

                    i++; // Skip the closing \}
                    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, content, repeat } = parseElement(nestedToken, i + 1);
                            let attrString = attributes || '';
                            if (id) attrString += ` id="${id}"`;

                            for (let j = 1; j <= repeat; j++) {
                                let finalContent = replaceIndexPlaceholders(content, j);
                                let finalAttrString = replaceIndexPlaceholders(attrString, j);
                                let finalId = replaceIndexPlaceholders(id, j);

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

                                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 char of str) {
                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) {
            // Regex to match the element definition with optional attributes, ID, content, and repetition
            // Example matches:
            // div[role="main"]#container{Content}*3
            const regex = /^(\w+)(\[[^\]]+\])?(#[\w\-]+)?(\{[^}]*\})?(?:\*(\d+))?$/;
            const match = token.match(regex);

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

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

            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 };
                }
            }

            const element = {
                tag: tag,
                attributes: attributes,
                id: id ? id.slice(1) : '', // Remove '#' from id
                content: content ? content.slice(1, -1) : '',
                repeat: repeat ? parseInt(repeat.slice(1)) : 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'));
    </script>
</body>
</html>

---

I'm trying to implement loop feature:

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

Output would be:

<div title="Index: 1">1. Hi</div>
<div title="Index: 2">2. Hi</div>
<div title="Index: 3">3. Hi</div>
<div title="Index: 4">4. Hi</div>
<div title="Index: 5">5. Hi</div>

---

However, it's not working as expected, instead, it's just ignoring the fact that this line "div[title="Index: &index;"]{&index;. Hi}*5" was even written...