📜
scopes_copy.js
← Back
📝 Javascript ⚡ Executable Ctrl+S: Save â€ĸ Ctrl+R: Run â€ĸ Ctrl+F: Find
// Storage Editor - Scopes Module v2 (scopes.js) // Block-based visual editor with containers, scopes, and unmarked blocks (function() { console.log('🚀 Loading Scopes Block Editor v2...'); // Language styles const LANG_STYLES = { javascript: { color: '#f7df1e', bg: 'rgba(247, 223, 30, 0.1)', icon: '🟨', label: 'JS' }, css: { color: '#264de4', bg: 'rgba(38, 77, 228, 0.1)', icon: 'đŸŸĻ', label: 'CSS' }, php: { color: '#8892bf', bg: 'rgba(136, 146, 191, 0.1)', icon: 'đŸŸĒ', label: 'PHP' }, html: { color: '#e34c26', bg: 'rgba(227, 76, 38, 0.1)', icon: '🟧', label: 'HTML' }, python: { color: '#3776ab', bg: 'rgba(55, 118, 171, 0.1)', icon: '🐍', label: 'PY' }, text: { color: '#64748b', bg: 'rgba(100, 116, 139, 0.1)', icon: '📄', label: 'TXT' } }; const UNMARKED_STYLE = { color: '#6b7280', bg: 'rgba(55, 65, 81, 0.05)', border: '2px dashed #374151', icon: '📝', label: 'UNMARKED' }; const CONTAINER_STYLE = { color: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.05)', border: '3px solid #8b5cf6', icon: 'đŸ“Ļ', headerBg: '#7c3aed' }; function getLanguageStyle(lang) { return LANG_STYLES[lang] || LANG_STYLES.text; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Parse scope attributes: @key:value@ @key:value@ function parseScopeAttributes(line) { const attributes = {}; const attrPattern = /@([a-zA-Z0-9_-]+):([^@]+)@/g; let match; while ((match = attrPattern.exec(line)) !== null) { attributes[match[1]] = match[2].trim(); } return Object.keys(attributes).length > 0 ? attributes : null; } // Parse snippet header: @@container-name[position] | language | action@@ // NOTE: This is ONLY for AI chat snippets, NOT for full file parsing function parseSnippetHeader(line) { const match = line.match(/^@@([a-z0-9-]+)\[(\d+)\]\s*\|\s*(\w+)\s*\|\s*(new|edit)@@$/i); if (!match) return null; return { container: match[1], position: parseInt(match[2]), language: match[3].toLowerCase(), action: match[4].toLowerCase() }; } // Detect language from context AND scope name function detectLanguage(lines, startLine, endLine, scopeName) { // 1. Check scope name for language hints if (scopeName) { const nameLower = scopeName.toLowerCase(); if (/-js[-\d]|javascript/i.test(nameLower)) return 'javascript'; if (/-css[-\d]|styles?/i.test(nameLower)) return 'css'; if (/-php[-\d]/i.test(nameLower)) return 'php'; if (/-html[-\d]/i.test(nameLower)) return 'html'; if (/-py[-\d]|python/i.test(nameLower)) return 'python'; } // 2. Check surrounding context let inScript = false, inStyle = false, inPhp = false; for (let i = 0; i <= startLine; i++) { const line = lines[i]; if (/<script[^>]*>/i.test(line)) inScript = true; if (/<\/script>/i.test(line)) inScript = false; if (/<style[^>]*>/i.test(line)) inStyle = true; if (/<\/style>/i.test(line)) inStyle = false; if (/<\?php/i.test(line)) inPhp = true; if (/\?>/i.test(line)) inPhp = false; } if (inScript) return 'javascript'; if (inStyle) return 'css'; if (inPhp) return 'php'; // 3. Check content patterns const content = lines.slice(startLine, endLine + 1).join('\n'); if (/<\?php/i.test(content)) return 'php'; if (/<script/i.test(content)) return 'javascript'; if (/<style/i.test(content)) return 'css'; if (/<[a-z]+/i.test(content)) return 'html'; // 4. Check for JS/CSS syntax patterns if (/\bfunction\b|\bconst\b|\blet\b|\bvar\b|=>|\bconsole\./i.test(content)) return 'javascript'; if (/\{[^}]*:[^}]*;[^}]*\}|@media|\.[\w-]+\s*\{/i.test(content)) return 'css'; return 'text'; } // Parse scopes and containers from file content function parseScopes(content) { if (!content) return { scopes: [], containers: [] }; const lines = content.split('\n'); const scopes = []; const containers = []; const stack = []; const containerStack = []; // Track the last seen snippet header (@@container[pos] | lang | action@@) let lastSnippetHeader = null; let lastSnippetHeaderLine = -1; // Patterns for containers const containerOpenPatterns = [ /\/\/\s*([a-z0-9-]+):\s*container<\s*$/, /\/\*\s*([a-z0-9-]+):\s*container<\s*\*\//, /<!--\s*([a-z0-9-]+):\s*container<\s*-->/, /#\s*([a-z0-9-]+):\s*container<\s*$/ ]; // Patterns for regular scopes const openPatterns = [ /\/\/\s*([a-z0-9-]+)<\s*/, /\/\*\s*([a-z0-9-]+)<\s*\*\//, /<!--\s*([a-z0-9-]+)<\s*/, /#\s*([a-z0-9-]+)<\s*/ ]; lines.forEach((line, idx) => { const trimmedLine = line.trim(); // Check if this line is a snippet header const snippetHeader = parseSnippetHeader(trimmedLine); if (snippetHeader) { lastSnippetHeader = snippetHeader; lastSnippetHeaderLine = idx; return; // Don't process this line further } // Check for container opening for (const pattern of containerOpenPatterns) { const match = line.match(pattern); if (match) { containerStack.push({ name: match[1], startLine: idx }); break; } } // Check for container closing if (containerStack.length > 0) { const current = containerStack[containerStack.length - 1]; const closePatterns = [ new RegExp(`\\/\\/\\s*${current.name}:\\s*container>\\s*$`), new RegExp(`\\/\\*\\s*${current.name}:\\s*container>\\s*\\*\\/`), new RegExp(`<!--\\s*${current.name}:\\s*container>\\s*-->`), new RegExp(`#\\s*${current.name}:\\s*container>\\s*$`) ]; for (const pattern of closePatterns) { if (pattern.test(line)) { current.endLine = idx; containers.push(current); containerStack.pop(); break; } } } // Check for scope opening for (const pattern of openPatterns) { const match = line.match(pattern); if (match) { const scopeData = { name: match[1], startLine: idx, container: containerStack.length > 0 ? containerStack[containerStack.length - 1].name : null, header: null, attributes: parseScopeAttributes(line) // Parse @key:value@ attributes }; // Check if we have a recent snippet header (within 5 lines) if (lastSnippetHeader && (idx - lastSnippetHeaderLine) <= 5) { scopeData.header = lastSnippetHeader; scopeData.startLine = lastSnippetHeaderLine; // Include header in scope console.log(`[parseScopes] Found snippet header at line ${lastSnippetHeaderLine} for scope "${match[1]}" at line ${idx}`); // Clear the header so it's not reused lastSnippetHeader = null; lastSnippetHeaderLine = -1; } stack.push(scopeData); break; } } // Check for scope closing if (stack.length > 0) { const current = stack[stack.length - 1]; const closePatterns = [ new RegExp(`\\/\\/\\s*${current.name}>\\s*$`), new RegExp(`\\/\\*\\s*${current.name}>\\s*\\*\\/`), new RegExp(`<!--\\s*${current.name}>\\s*-->`), new RegExp(`#\\s*${current.name}>\\s*$`) ]; for (const pattern of closePatterns) { if (pattern.test(line)) { current.endLine = idx; current.lineCount = current.endLine - current.startLine + 1; current.language = detectLanguage(lines, current.startLine, current.endLine, current.name); scopes.push(current); stack.pop(); break; } } } }); return { scopes, containers }; } // Build hierarchical block structure with unmarked blocks function buildBlockStructure(content) { const lines = content.split('\n'); const parsed = parseScopes(content); const { scopes, containers } = parsed; // Create a map of all marked line ranges const markedRanges = []; // Add container ranges containers.forEach(c => { markedRanges.push({ type: 'container', start: c.startLine, end: c.endLine, data: c }); }); // Add scope ranges scopes.forEach(s => { markedRanges.push({ type: 'scope', start: s.startLine, end: s.endLine, data: s }); }); // Sort by start line markedRanges.sort((a, b) => a.start - b.start); // Build block structure const blocks = []; let currentLine = 0; markedRanges.forEach(range => { // Add unmarked block before this range if there's a gap if (currentLine < range.start) { const content = lines.slice(currentLine, range.start).join('\n'); const trimmedContent = content.trim(); // Only add unmarked block if it has actual content (not just whitespace) if (trimmedContent.length > 0) { blocks.push({ type: 'unmarked', startLine: currentLine, endLine: range.start - 1, content: content, container: null }); } } // Add the marked range if (range.type === 'container') { const container = range.data; const containerScopes = scopes.filter(s => s.container === container.name); const containerBlocks = []; let containerCurrentLine = container.startLine + 1; // Skip opening marker containerScopes.forEach(scope => { // Add unmarked block inside container before scope if (containerCurrentLine < scope.startLine) { const content = lines.slice(containerCurrentLine, scope.startLine).join('\n'); const trimmedContent = content.trim(); // Only add if it has actual content if (trimmedContent.length > 0) { containerBlocks.push({ type: 'unmarked', startLine: containerCurrentLine, endLine: scope.startLine - 1, content: content, container: container.name }); } } // Add scope block containerBlocks.push({ type: 'scope', startLine: scope.startLine, endLine: scope.endLine, content: lines.slice(scope.startLine + 1, scope.endLine).join('\n'), // Exclude markers data: scope, container: container.name }); containerCurrentLine = scope.endLine + 1; }); // Add trailing unmarked block inside container if (containerCurrentLine < container.endLine) { const content = lines.slice(containerCurrentLine, container.endLine).join('\n'); const trimmedContent = content.trim(); // Only add if it has actual content if (trimmedContent.length > 0) { containerBlocks.push({ type: 'unmarked', startLine: containerCurrentLine, endLine: container.endLine - 1, content: content, container: container.name }); } } blocks.push({ type: 'container', startLine: container.startLine, endLine: container.endLine, data: container, children: containerBlocks }); currentLine = container.endLine + 1; } else if (range.type === 'scope' && !range.data.container) { // Top-level scope (not in a container) blocks.push({ type: 'scope', startLine: range.start, endLine: range.end, content: lines.slice(range.start + 1, range.end).join('\n'), // Exclude markers data: range.data, container: null }); currentLine = range.end + 1; } }); // Add trailing unmarked block if (currentLine < lines.length) { const content = lines.slice(currentLine).join('\n'); const trimmedContent = content.trim(); // Only add if it has actual content if (trimmedContent.length > 0) { blocks.push({ type: 'unmarked', startLine: currentLine, endLine: lines.length - 1, content: content, container: null }); } } return blocks; } // Render a scope block function renderScopeBlock(block, blockId, isInChat) { const style = getLanguageStyle(block.data.language); const lineRange = `Lines ${block.startLine + 1}-${block.endLine + 1}`; // Build metadata badges for pull-up menu let metadataBadges = ''; // Show container badge if (block.data.header) { const h = block.data.header; const actionIcon = h.action === 'new' ? '✨' : 'âœī¸'; metadataBadges += ` <div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;"> <span style="color: #6b7280; font-size: 11px; font-weight: 600;">Container:</span> <span style="color: #111827; font-size: 11px; font-family: monospace;">${escapeHtml(h.container)}</span> </div> <div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;"> <span style="color: #6b7280; font-size: 11px; font-weight: 600;">Position:</span> <span style="color: #111827; font-size: 11px; font-family: monospace;">#${h.position}</span> </div> <div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;"> <span style="color: #6b7280; font-size: 11px; font-weight: 600;">Action:</span> <span style="color: #111827; font-size: 11px; font-family: monospace;">${actionIcon} ${h.action.toUpperCase()}</span> </div> `; } else if (block.data.container) { metadataBadges += ` <div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;"> <span style="color: #6b7280; font-size: 11px; font-weight: 600;">Container:</span> <span style="color: #111827; font-size: 11px; font-family: monospace;">${escapeHtml(block.data.container)}</span> </div> `; } // Add language badge metadataBadges += ` <div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;"> <span style="color: #6b7280; font-size: 11px; font-weight: 600;">Language:</span> <span style="color: #111827; font-size: 11px; font-family: monospace;">${style.label}</span> </div> `; // Add attribute badges if (block.data.attributes) { const attrs = block.data.attributes; if (attrs.editedBy) { metadataBadges += ` <div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;"> <span style="color: #6b7280; font-size: 11px; font-weight: 600;">Edited By:</span> <span style="color: #111827; font-size: 11px; font-family: monospace;">${escapeHtml(attrs.editedBy)}</span> </div> `; } if (attrs.editedAt) { const timestamp = attrs.editedAt; let displayTime = timestamp; try { const date = new Date(parseInt(timestamp)); if (!isNaN(date.getTime())) { displayTime = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); } } catch (e) {} metadataBadges += ` <div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;"> <span style="color: #6b7280; font-size: 11px; font-weight: 600;">Edited At:</span> <span style="color: #111827; font-size: 11px;">${escapeHtml(displayTime)}</span> </div> `; } Object.keys(attrs).forEach(key => { if (key !== 'editedBy' && key !== 'editedAt') { metadataBadges += ` <div style="padding: 8px 12px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between;"> <span style="color: #6b7280; font-size: 11px; font-weight: 600;">${escapeHtml(key)}:</span> <span style="color: #111827; font-size: 11px; font-family: monospace;">${escapeHtml(attrs[key])}</span> </div> `; } }); } // Add line range for block editor if (!isInChat) { metadataBadges += ` <div style="padding: 8px 12px; display: flex; justify-content: space-between;"> <span style="color: #6b7280; font-size: 11px; font-weight: 600;">Lines:</span> <span style="color: #111827; font-size: 11px; font-family: monospace;">${lineRange}</span> </div> `; } // Build action button for chat snippets let actionFooter = ''; if (isInChat && block.data.header) { const h = block.data.header; const btnText = h.action === 'new' ? '✨ INSERT INTO FILE' : 'âœī¸ REPLACE IN FILE'; const btnId = `action-btn-${blockId}`; actionFooter = ` <div style="padding: 12px; background: #ffffff; border-top: 2px solid ${style.color};"> <button id="${btnId}" class="scope-action-btn" style=" background: #ffffff; color: #111827; border: 2px solid #111827; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-weight: 700; font-size: 12px; width: 100%; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.2s; " data-action="${h.action}" data-container="${escapeHtml(h.container)}" data-position="${h.position}" data-scope-name="${escapeHtml(block.data.name)}" data-language="${escapeHtml(h.language)}" data-block-id="${blockId}" onmouseover="this.style.background='#111827'; this.style.color='#ffffff'" onmouseout="this.style.background='#ffffff'; this.style.color='#111827'" >${btnText}</button> </div> `; } const metadataMenuId = `metadata-menu-${blockId}`; return ` <div class="block-scope" data-block-id="${blockId}" style=" position: relative; display: flex; flex-direction: column; margin-bottom: 16px; border-radius: 6px; overflow: hidden; border: 2px solid ${style.color}; background: #ffffff; box-shadow: 0 1px 3px rgba(0,0,0,0.1); "> <!-- Wireframe Header --> <div style=" background: #ffffff; padding: 10px 16px; display: flex; align-items: center; justify-content: space-between; border-bottom: 2px solid ${style.color}; "> <div style="display: flex; align-items: center; gap: 10px;"> <span style="font-size: 18px;">${style.icon}</span> <div style=" font-weight: 700; color: #111827; font-family: monospace; font-size: 13px; letter-spacing: 0.3px; ">${escapeHtml(block.data.name)}</div> <div style=" background: ${style.bg}; border: 1px solid ${style.color}; padding: 2px 8px; border-radius: 3px; font-size: 10px; font-weight: 700; color: #111827; text-transform: uppercase; ">${style.label}</div> </div> <button onclick="(function(e) { e.stopPropagation(); const menu = document.getElementById('${metadataMenuId}'); const isVisible = menu.style.display === 'block'; menu.style.display = isVisible ? 'none' : 'block'; })(event)" style=" background: #ffffff; border: 1px solid #111827; color: #111827; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: 600; transition: all 0.2s; " onmouseover="this.style.background='#f3f4f6'" onmouseout="this.style.background='#ffffff'" >â„šī¸ Info</button> </div> <!-- Pull-up Metadata Menu --> <div id="${metadataMenuId}" style=" display: none; position: fixed; bottom: ${isInChat ? '100px' : '20px'}; left: 50%; transform: translateX(-50%); width: 90%; max-width: 500px; background: #ffffff; border: 2px solid ${style.color}; border-radius: 8px; box-shadow: 0 20px 50px rgba(0,0,0,0.3); z-index: 2147483647; max-height: 400px; overflow-y: auto; "> ${metadataBadges} </div> <!-- Content Area --> <textarea class="block-content" data-block-id="${blockId}" style=" width: 100%; height: 300px; background: #ffffff; color: #111827; border: none; padding: 16px; font-family: 'Consolas', 'Monaco', monospace; font-size: 13px; line-height: 1.6; resize: none; outline: none; overflow-y: auto; box-sizing: border-box; ${isInChat ? 'pointer-events: none; user-select: text;' : ''} ">${escapeHtml(block.content)}</textarea> <!-- Action Footer (chat only) --> ${actionFooter} </div> `; } // Render an unmarked block function renderUnmarkedBlock(block, blockId) { const lineRange = `Lines ${block.startLine + 1}-${block.endLine + 1}`; const lineCount = block.endLine - block.startLine + 1; return ` <div class="block-unmarked" data-block-id="${blockId}" style=" position: relative; display: flex; flex-direction: column; margin-bottom: 16px; border-radius: 6px; overflow: hidden; border: 2px dashed #6b7280; background: #ffffff; box-shadow: 0 1px 3px rgba(0,0,0,0.1); opacity: 0.8; "> <!-- Header --> <div style=" background: #ffffff; padding: 10px 16px; display: flex; align-items: center; justify-content: space-between; border-bottom: 2px dashed #6b7280; "> <div style="display: flex; align-items: center; gap: 10px;"> <span style="font-size: 18px;">📝</span> <div style=" font-weight: 700; color: #111827; font-family: monospace; font-size: 13px; letter-spacing: 0.3px; ">UNMARKED BLOCK</div> ${block.container ? ` <div style=" background: rgba(139, 92, 246, 0.1); border: 1px solid #8b5cf6; padding: 2px 8px; border-radius: 3px; font-size: 10px; font-weight: 700; color: #111827; text-transform: uppercase; ">${escapeHtml(block.container)}</div> ` : ''} </div> <div style=" font-size: 11px; color: #6b7280; font-family: monospace; ">${lineCount} lines</div> </div> <!-- Content Area --> <textarea class="block-content" data-block-id="${blockId}" style=" width: 100%; height: 300px; background: #ffffff; color: #111827; border: none; padding: 16px; font-family: 'Consolas', 'Monaco', monospace; font-size: 13px; line-height: 1.6; resize: none; outline: none; overflow-y: auto; box-sizing: border-box; ">${escapeHtml(block.content)}</textarea> </div> `; } // Render a container block // Render a container block function renderContainerBlock(block, blockId) { const lineRange = `Lines ${block.startLine + 1}-${block.endLine + 1}`; let childrenHtml = ''; block.children.forEach((child, idx) => { const childId = `${blockId}-child-${idx}`; if (child.type === 'scope') { childrenHtml += renderScopeBlock(child, childId, false); } else if (child.type === 'unmarked') { childrenHtml += renderUnmarkedBlock(child, childId); } }); return ` <div class="block-container" data-block-id="${blockId}" style=" margin-bottom: 20px; border-radius: 8px; overflow: hidden; border: 3px solid #8b5cf6; background: #ffffff; box-shadow: 0 2px 8px rgba(0,0,0,0.1); "> <!-- Container Header --> <div class="container-header" data-block-id="${blockId}" style=" background: #ffffff; padding: 14px 20px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; user-select: none; border-bottom: 3px solid #8b5cf6; "> <div style="font-weight: 700; color: #111827; display: flex; align-items: center; gap: 12px; font-size: 16px;"> <span class="container-toggle" style="font-size: 14px; color: #111827;">â–ŧ</span> <span style="font-size: 20px;">đŸ“Ļ</span> <span style="font-family: monospace; text-transform: uppercase; letter-spacing: 0.5px;"> ${escapeHtml(block.data.name)} </span> <span style=" background: rgba(139, 92, 246, 0.1); border: 1px solid #8b5cf6; padding: 3px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; color: #111827; ">${block.children.length} blocks</span> </div> <div style="font-size: 11px; color: #6b7280; font-weight: 600; font-family: monospace;"> ${lineRange} </div> </div> <!-- Container Body --> <div class="container-body" data-block-id="${blockId}" style=" padding: 16px; background: #fafafa; "> ${childrenHtml} </div> </div> `; } // Create main block editor HTML function createBlockEditorHTML() { // Check for dependencies if (!window.StorageEditor) { return ` <div style="padding: 40px; text-align: center; color: #ef4444;"> <h2>âš ī¸ Storage Editor Not Loaded</h2> <p>The core Storage Editor module must be loaded first.</p> </div> `; } const file = window.StorageEditor.getActiveFile(); if (!file) { return '<div style="padding: 40px; text-align: center; color: #64748b;">📄 No file open</div>'; } const blocks = buildBlockStructure(file.content); let html = ` <div style=" height: 100%; display: flex; flex-direction: column; background: #0a0a0a; "> <!-- Toolbar --> <div style=" background: #1a1a1a; padding: 12px 20px; border-bottom: 2px solid #2a2a2a; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; "> <div style="display: flex; align-items: center; gap: 12px;"> <h2 style="margin: 0; color: #e6edf3; font-size: 18px; font-weight: 700;"> đŸ“Ļ Block Editor </h2> <span style="color: #64748b; font-size: 14px;"> ${blocks.length} top-level blocks </span> </div> <button id="saveAllBlocks" style=" background: #10b981; color: #fff; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 700; font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px; ">💾 Save All</button> </div> <!-- Blocks Container --> <div id="blocksContainer" style=" flex: 1; overflow-y: auto; padding: 20px; "> `; blocks.forEach((block, idx) => { const blockId = `block-${idx}`; if (block.type === 'container') { html += renderContainerBlock(block, blockId); } else if (block.type === 'scope') { html += renderScopeBlock(block, blockId); } else if (block.type === 'unmarked') { html += renderUnmarkedBlock(block, blockId); } }); html += ` </div> </div> `; return html; } // Setup interactions function setupBlockEditorInteractions(container) { // Check for dependencies if (!window.StorageEditor) { console.error('StorageEditor not available'); return; } const file = window.StorageEditor.getActiveFile(); if (!file) return; const blocks = buildBlockStructure(file.content); // Container collapse/expand container.querySelectorAll('.container-header').forEach(header => { header.addEventListener('click', () => { const blockId = header.dataset.blockId; const body = container.querySelector(`.container-body[data-block-id="${blockId}"]`); const toggle = header.querySelector('.container-toggle'); if (body.style.display === 'none') { body.style.display = 'block'; toggle.textContent = 'â–ŧ'; } else { body.style.display = 'none'; toggle.textContent = 'â–ļ'; } }); }); // Save all button const saveBtn = container.querySelector('#saveAllBlocks'); if (saveBtn) { saveBtn.addEventListener('click', () => { if (!confirm('Save all changes to file?')) return; // Collect all textarea values with their block IDs const textareas = container.querySelectorAll('.block-content'); const updates = new Map(); textareas.forEach(ta => { const blockId = ta.dataset.blockId; updates.set(blockId, ta.value); }); // Reconstruct file content const lines = []; function processBlock(block, blockId) { const updatedContent = updates.get(blockId); if (block.type === 'container') { lines.push(file.content.split('\n')[block.startLine]); // Opening marker block.children.forEach((child, idx) => { const childId = `${blockId}-child-${idx}`; processBlock(child, childId); }); lines.push(file.content.split('\n')[block.endLine]); // Closing marker } else if (block.type === 'scope') { lines.push(file.content.split('\n')[block.startLine]); // Opening marker lines.push(updatedContent); lines.push(file.content.split('\n')[block.endLine]); // Closing marker } else if (block.type === 'unmarked') { lines.push(updatedContent); } } blocks.forEach((block, idx) => { processBlock(block, `block-${idx}`); }); // Save to storage const files = window.StorageEditor.loadActiveFiles(); const activeIdx = files.findIndex(f => f.active); if (activeIdx !== -1) { files[activeIdx].content = lines.join('\n'); files[activeIdx].lastModified = new Date().toISOString(); window.StorageEditor.saveActiveFiles(files); window.dispatchEvent(new Event('activeFilesUpdated')); saveBtn.textContent = '✅ SAVED'; saveBtn.style.background = '#10b981'; setTimeout(() => { saveBtn.textContent = '💾 SAVE ALL'; saveBtn.style.background = '#10b981'; }, 2000); } }); } } // Export window.StorageEditorScopes = { open: () => { if (window.AppOverlay) { AppOverlay.open([{ title: 'đŸ“Ļ Block Editor', html: createBlockEditorHTML(), onRender: setupBlockEditorInteractions }]); } }, // Core functions parseScopes, buildBlockStructure, getLanguageStyle, parseSnippetHeader, parseScopeAttributes, detectLanguage, // Expose existing block renderer renderScopeBlock, // đŸ”Ĩ NEW: Find best matching scope using fuzzy name matching findBestMatch(containerName, scopeName) { if (!window.StorageEditor) { throw new Error('StorageEditor not available'); } const file = window.StorageEditor.getActiveFile(); if (!file) { throw new Error('No active file'); } const parsed = parseScopes(file.content); const candidates = parsed.scopes.filter(s => s.container === containerName); if (candidates.length === 0) { return { match: null, score: 0 }; } // Calculate similarity scores const scores = candidates.map(scope => { const targetName = scopeName.toLowerCase(); const candidateName = scope.name.toLowerCase(); // Exact match if (targetName === candidateName) { return { scope, score: 100 }; } // Contains match if (candidateName.includes(targetName) || targetName.includes(candidateName)) { return { scope, score: 80 }; } // Levenshtein distance (simple version) const maxLen = Math.max(targetName.length, candidateName.length); let matches = 0; for (let i = 0; i < Math.min(targetName.length, candidateName.length); i++) { if (targetName[i] === candidateName[i]) matches++; } const score = (matches / maxLen) * 60; return { scope, score }; }); // Get best match scores.sort((a, b) => b.score - a.score); return { match: scores[0].scope, score: scores[0].score }; }, // đŸ”Ĩ NEW: Insert a new scope at specific position in container insertAt(containerName, position, scopeData) { if (!window.StorageEditor) { throw new Error('StorageEditor not available'); } const file = window.StorageEditor.getActiveFile(); if (!file) { throw new Error('No active file'); } const lines = file.content.split('\n'); const parsed = parseScopes(file.content); // Find the target container const container = parsed.containers.find(c => c.name === containerName); if (!container) { throw new Error(`Container "${containerName}" not found`); } // Get all scopes in this container, sorted by position const containerScopes = parsed.scopes .filter(s => s.container === containerName) .sort((a, b) => a.startLine - b.startLine); // Adjust position: 1-indexed, clamped to valid range const adjustedPosition = Math.max(0, Math.min(position - 1, containerScopes.length)); // Determine insertion point let insertLine; if (adjustedPosition === 0 || containerScopes.length === 0) { // Insert at start of container (after opening marker) insertLine = container.startLine + 1; } else if (adjustedPosition >= containerScopes.length) { // Insert at end of container (before closing marker) insertLine = container.endLine; } else { // Insert before the scope at this position insertLine = containerScopes[adjustedPosition].startLine; } // Build scope markers based on language const { name, language, content, attributes } = scopeData; const attrString = attributes ? ' ' + Object.entries(attributes) .map(([k, v]) => `@${k}:${v}@`) .join(' ') : ''; let openMarker, closeMarker; if (language === 'html') { openMarker = `<!-- ${name}<${attrString} -->`; closeMarker = `<!-- ${name}> -->`; } else if (language === 'css') { openMarker = `/* ${name}<${attrString} */`; closeMarker = `/* ${name}> */`; } else if (language === 'php') { openMarker = `// ${name}<${attrString}`; closeMarker = `// ${name}>`; } else { // Default to JS-style comments openMarker = `// ${name}<${attrString}`; closeMarker = `// ${name}>`; } // Insert the new scope const newLines = [ '', openMarker, content, closeMarker ]; lines.splice(insertLine, 0, ...newLines); // Save updated file const files = window.StorageEditor.loadActiveFiles(); const activeIdx = files.findIndex(f => f.active); if (activeIdx !== -1) { files[activeIdx].content = lines.join('\n'); files[activeIdx].lastModified = new Date().toISOString(); window.StorageEditor.saveActiveFiles(files); window.dispatchEvent(new Event('activeFilesUpdated')); } return { success: true, message: `Inserted scope "${name}" at position ${position} in container "${containerName}"`, insertedAt: insertLine }; }, // đŸ”Ĩ NEW: Replace an existing scope's content with smart matching replace(containerName, position, scopeName, newContent, attributes) { if (!window.StorageEditor) { throw new Error('StorageEditor not available'); } const file = window.StorageEditor.getActiveFile(); if (!file) { throw new Error('No active file'); } const lines = file.content.split('\n'); const parsed = parseScopes(file.content); // Get all scopes in this container const containerScopes = parsed.scopes .filter(s => s.container === containerName) .sort((a, b) => a.startLine - b.startLine); if (containerScopes.length === 0) { throw new Error(`No scopes found in container "${containerName}"`); } let targetScope = null; // If position is out of bounds, use name matching if (position < 1 || position > containerScopes.length) { const result = this.findBestMatch(containerName, scopeName); if (result.score > 50) { targetScope = result.match; console.log(`[replace] Position ${position} out of bounds. Using name match: "${result.match.name}" (score: ${result.score})`); } else { // Insert at end if no good match return this.insertAt(containerName, containerScopes.length + 1, { name: scopeName, language: parsed.scopes.find(s => s.container === containerName)?.language || 'javascript', content: newContent, attributes }); } } else { // Get scope at position (1-indexed) const scopeAtPosition = containerScopes[position - 1]; // Verify name similarity const result = this.findBestMatch(containerName, scopeName); if (result.match && result.match.name === scopeAtPosition.name && result.score > 70) { targetScope = scopeAtPosition; console.log(`[replace] Position ${position} matches scope "${scopeAtPosition.name}" (score: ${result.score})`); } else if (result.score > 70) { targetScope = result.match; console.log(`[replace] Using best name match "${result.match.name}" instead of position ${position} (score: ${result.score})`); } else { targetScope = scopeAtPosition; console.log(`[replace] Using scope at position ${position}: "${scopeAtPosition.name}" (low match score: ${result.score})`); } } if (!targetScope) { throw new Error(`Could not find scope to replace in container "${containerName}"`); } // Update attributes in opening marker if provided if (attributes) { const openLine = lines[targetScope.startLine]; const attrString = Object.entries(attributes) .map(([k, v]) => `@${k}:${v}@`) .join(' '); // Remove old attributes and add new ones const cleaned = openLine.replace(/@[a-zA-Z0-9_-]+:[^@]+@/g, '').trim(); lines[targetScope.startLine] = cleaned.replace(/(<\s*)/, `< ${attrString} `); } // Replace content (between opening and closing markers) lines.splice(targetScope.startLine + 1, targetScope.endLine - targetScope.startLine - 1, newContent); // Save updated file const files = window.StorageEditor.loadActiveFiles(); const activeIdx = files.findIndex(f => f.active); if (activeIdx !== -1) { files[activeIdx].content = lines.join('\n'); files[activeIdx].lastModified = new Date().toISOString(); window.StorageEditor.saveActiveFiles(files); window.dispatchEvent(new Event('activeFilesUpdated')); } return { success: true, message: `Replaced scope "${targetScope.name}" in container "${containerName}"`, replacedScope: targetScope.name, startLine: targetScope.startLine, endLine: targetScope.endLine }; }, // Helper: List all containers and their scopes listStructure() { if (!window.StorageEditor) { throw new Error('StorageEditor not available'); } const file = window.StorageEditor.getActiveFile(); if (!file) { throw new Error('No active file'); } const parsed = parseScopes(file.content); return { containers: parsed.containers.map(c => ({ name: c.name, scopes: parsed.scopes .filter(s => s.container === c.name) .map(s => ({ name: s.name, language: s.language, position: parsed.scopes.filter(x => x.container === c.name && x.startLine < s.startLine).length, attributes: s.attributes })) })), topLevelScopes: parsed.scopes .filter(s => !s.container) .map(s => ({ name: s.name, language: s.language, attributes: s.attributes })) }; }, // đŸ”Ĩ Universal snippet renderer used by Chat.js renderAnswer(answerText, parentElement) { if (!answerText) return "<pre></pre>"; const renderBlock = window.StorageEditorScopes.renderScopeBlock; // Only parse if scope markers exist if (renderBlock && (answerText.includes("<!--") || answerText.includes("//") || answerText.includes("/*"))) { try { const parsed = window.StorageEditorScopes.parseScopes(answerText); if (parsed.scopes && parsed.scopes.length > 0) { let html = ""; const lines = answerText.split('\n'); parsed.scopes.forEach((scope, idx) => { const scopeContent = lines.slice(scope.startLine + 1, scope.endLine).join('\n'); html += renderBlock( { data: { language: scope.language, name: scope.name, header: scope.header, container: scope.container, attributes: scope.attributes }, startLine: scope.startLine, endLine: scope.endLine, content: scopeContent }, "scope-render-" + Date.now() + "-" + idx, true // isInChat flag ); }); // Add event listeners after rendering if parent element provided if (parentElement) { // Attach action button listeners setTimeout(() => { parentElement.querySelectorAll('.scope-action-btn').forEach(btn => { btn.addEventListener('click', function() { const action = this.dataset.action; const container = this.dataset.container; const position = parseInt(this.dataset.position); const scopeName = this.dataset.scopeName; const language = this.dataset.language; const blockId = this.dataset.blockId; // Get content from textarea (already cleaned) const textarea = parentElement.querySelector(`.block-content[data-block-id="${blockId}"]`); const content = textarea ? textarea.value : ''; try { const attributes = { editedBy: 'ai', editedAt: Date.now().toString() }; let result; if (action === 'new') { result = window.StorageEditorScopes.insertAt(container, position, { name: scopeName, language: language, content: content, attributes: attributes }); } else if (action === 'edit') { result = window.StorageEditorScopes.replace(container, position, scopeName, content, attributes); } this.style.background = '#10b981'; this.textContent = '✅ APPLIED'; this.disabled = true; this.style.cursor = 'not-allowed'; console.log('[ScopeAction]', result); setTimeout(() => { this.style.transition = 'opacity 0.3s'; this.style.opacity = '0'; setTimeout(() => this.remove(), 300); }, 2000); } catch (error) { console.error('[ScopeAction] Error:', error); this.style.background = '#ef4444'; this.textContent = '❌ ERROR'; alert('Error: ' + error.message); } }); }); }, 100); // ------------------------------------------------------------ // CLEAN VISUAL MARKERS OUT OF TEXTAREAS AFTER RENDER // ------------------------------------------------------------ setTimeout(() => { parentElement.querySelectorAll("textarea.block-content").forEach(area => { area.value = area.value // HTML markers <!-- name< --> OR <!-- name> --> .replace(/<!--\s*[a-z0-9_-]+<\s*-->/gi, "") .replace(/<!--\s*[a-z0-9_-]+>\s*-->/gi, "") // CSS markers /* name< */ /* name> */ .replace(/\/\*\s*[a-z0-9_-]+<\s*\*\//gi, "") .replace(/\/\*\s*[a-z0-9_-]+>\s*\*\//gi, "") // JS markers // name< // name> .replace(/\/\/\s*[a-z0-9_-]+</gi, "") .replace(/\/\/\s*[a-z0-9_-]+>/gi, "") .trim(); }); }, 120); // slightly after button-setup } return html; } } catch (err) { console.error("Snippet parsing failed:", err); return `<pre>${escapeHtml(answerText)}</pre>`; } } // Not a scope-formatted snippet — return plain return `<pre>${escapeHtml(answerText)}</pre>`; } }; console.log('✅ Scopes Block Editor v2 loaded'); })();