๐Ÿ“œ
blocks.js
โ† Back
๐Ÿ“ Javascript โšก Executable Ctrl+S: Save โ€ข Ctrl+R: Run โ€ข Ctrl+F: Find
// blocks.js - Scope Parser and Block Management (function() { console.log("[blocks] Loading Scope Parser module..."); // --- Utility Functions --- function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // --- Attribute Parser --- function parseAttributes(line) { const attributes = {}; // Pattern: @key:value@ (repeating) // Works with both /* @attr:val@ */ and <!-- @attr:val@ --> const attrPattern = /@([a-zA-Z0-9_-]+):([^@]+)@/g; let match; while ((match = attrPattern.exec(line)) !== null) { const key = match[1]; const value = match[2].trim(); attributes[key] = value; } return Object.keys(attributes).length > 0 ? attributes : null; } // --- Check if line is attribute-only --- function isAttributeOnlyLine(line) { const trimmed = line.trim(); // Must contain at least one @attribute const attrs = parseAttributes(trimmed); if (!attrs) return false; // CSS/JS style: /* @attr:val@ @attr2:val2@ */ const cssAttrOnly = /^\/\*\s*(?:@[a-zA-Z0-9_-]+:[^@]+@)+\s*\*\/$/; // HTML style: <!-- @attr:val@ @attr2:val2@ --> const htmlAttrOnly = /^<!--\s*(?:@[a-zA-Z0-9_-]+:[^@]+@)+\s*-->$/; return cssAttrOnly.test(trimmed) || htmlAttrOnly.test(trimmed); } // --- Metadata Extraction - JS, HTML, CSS, and PHP --- function extractJSMetadata(js) { const metadata = { functions: [], classes: [], eventListeners: [], domSelectors: [] }; // --- Function Declarations (traditional) --- const functionMatches = js.matchAll(/function\s+([a-zA-Z0-9_]+)\s*\(/g); for (const match of functionMatches) { metadata.functions.push(match[1]); } // --- Arrow Functions (const/let name = () => {}) --- const arrowMatches = js.matchAll(/(?:const|let|var)\s+([a-zA-Z0-9_]+)\s*=\s*\([^)]*\)\s*=>/g); for (const match of arrowMatches) { metadata.functions.push(match[1]); } // --- Classes --- const classMatches = js.matchAll(/class\s+([A-Za-z0-9_]+)/g); for (const match of classMatches) { metadata.classes.push(match[1]); } // --- Event Listeners --- const eventMatches = js.matchAll(/addEventListener\s*\(\s*["']([^"']+)["']/g); for (const match of eventMatches) { metadata.eventListeners.push(match[1]); } // --- DOM Selectors --- const querySelectorMatches = js.matchAll(/querySelector(?:All)?\s*\(\s*["']([^"']+)["']/g); for (const match of querySelectorMatches) { metadata.domSelectors.push(match[1]); } const getElementMatches = js.matchAll(/getElementById\s*\(\s*["']([^"']+)["']/g); for (const match of getElementMatches) { metadata.domSelectors.push('#' + match[1]); } // Deduplicate all arrays metadata.functions = [...new Set(metadata.functions)]; metadata.classes = [...new Set(metadata.classes)]; metadata.eventListeners = [...new Set(metadata.eventListeners)]; metadata.domSelectors = [...new Set(metadata.domSelectors)]; return metadata; } function extractHTMLMetadata(html) { const metadata = { ids: [], classes: [], tags: [] }; // Remove HTML comments to avoid noise const clean = html.replace(/<!--[\s\S]*?-->/g, ""); // --- IDs --- const idMatches = clean.matchAll(/\bid=["']([^"']+)["']/g); for (const match of idMatches) { metadata.ids.push(match[1]); } // --- Classes --- const classMatches = clean.matchAll(/\bclass=["']([^"']+)["']/g); for (const match of classMatches) { const classes = match[1].split(/\s+/).filter(c => c); metadata.classes.push(...classes); } // --- Tag Names --- const tagMatches = clean.matchAll(/<([a-zA-Z0-9-]+)[\s>]/g); for (const match of tagMatches) { metadata.tags.push(match[1]); } // Deduplicate all arrays metadata.ids = [...new Set(metadata.ids)]; metadata.classes = [...new Set(metadata.classes)]; metadata.tags = [...new Set(metadata.tags)]; return metadata; } function extractCSSMetadata(css) { const metadata = { classes: [], ids: [], cssVariables: [], keyframes: [], mediaQueries: [] }; // Remove CSS comments const clean = css.replace(/\/\*[\s\S]*?\*\//g, ""); // --- Class Selectors --- const classMatches = clean.matchAll(/\.([a-zA-Z0-9_-]+)/g); for (const match of classMatches) { metadata.classes.push(match[1]); } // --- ID Selectors --- const idMatches = clean.matchAll(/#([a-zA-Z0-9_-]+)/g); for (const match of idMatches) { metadata.ids.push(match[1]); } // --- CSS Variables --- const varMatches = clean.matchAll(/--([a-zA-Z0-9_-]+)\s*:/g); for (const match of varMatches) { metadata.cssVariables.push(match[1]); } // --- Keyframes --- const keyframeMatches = clean.matchAll(/@keyframes\s+([a-zA-Z0-9_-]+)/g); for (const match of keyframeMatches) { metadata.keyframes.push(match[1]); } // --- Media Queries --- const mediaMatches = clean.matchAll(/@media\s*\(([^)]+)\)/g); for (const match of mediaMatches) { metadata.mediaQueries.push(match[1].trim()); } // Deduplicate all arrays metadata.classes = [...new Set(metadata.classes)]; metadata.ids = [...new Set(metadata.ids)]; metadata.cssVariables = [...new Set(metadata.cssVariables)]; metadata.keyframes = [...new Set(metadata.keyframes)]; metadata.mediaQueries = [...new Set(metadata.mediaQueries)]; return metadata; } function extractPHPMetadata(php) { const metadata = { functions: [], classes: [], methods: [], includes: [], variables: [] }; // Remove comments const clean = php .replace(/\/\*[\s\S]*?\*\//g, "") .replace(/\/\/.*$/gm, "") .replace(/#.*$/gm, ""); // --- Functions --- const functionMatches = clean.matchAll(/\bfunction\s+([a-zA-Z0-9_]+)\s*\(/g); for (const match of functionMatches) { metadata.functions.push(match[1]); } // --- Classes --- const classMatches = clean.matchAll(/\bclass\s+([a-zA-Z0-9_]+)/g); for (const match of classMatches) { metadata.classes.push(match[1]); } // --- Methods --- const methodMatches = clean.matchAll(/\b(?:public|private|protected)\s+function\s+([a-zA-Z0-9_]+)\s*\(/g); for (const match of methodMatches) { metadata.methods.push(match[1]); } // --- Includes/Requires --- const includeMatches = [ ...clean.matchAll(/\b(?:include|require)(?:_once)?\s*\(?["']([^"']+)["']\)?/g) ]; for (const match of includeMatches) { metadata.includes.push(match[1]); } // --- Variables ($var) --- const varMatches = clean.matchAll(/\$([a-zA-Z_][a-zA-Z0-9_]*)\b/g); for (const match of varMatches) { if (match[1] !== 'this') { metadata.variables.push('$' + match[1]); } } // Deduplicate all arrays metadata.functions = [...new Set(metadata.functions)]; metadata.classes = [...new Set(metadata.classes)]; metadata.methods = [...new Set(metadata.methods)]; metadata.includes = [...new Set(metadata.includes)]; metadata.variables = [...new Set(metadata.variables)]; return metadata; } // --- Extract metadata based on language --- function extractMetadataForSection(section) { if (!section.fullContent || !section.autoName) return null; const languageMatch = section.autoName.match(/-([a-zA-Z]+)-/); const language = languageMatch ? languageMatch[1].toLowerCase() : null; if (language === 'js' || language === 'javascript') { return extractJSMetadata(section.fullContent); } else if (language === 'html') { return extractHTMLMetadata(section.fullContent); } else if (language === 'css') { return extractCSSMetadata(section.fullContent); } else if (language === 'php') { return extractPHPMetadata(section.fullContent); } return null; } // --- Scope Parser --- function parseScopeMarkers(content) { const lines = content.split('\n'); const result = { sections: [] // Single ordered array maintaining file order }; const stack = []; // Track nested containers let currentUnmarked = []; let lineNumber = 0; let containerCount = 0; // Track container numbers globally // Regex patterns for markers const containerOpenPattern = /\/\*\s*([a-zA-Z0-9_-]+):\s*container<\s*\*\//; const containerClosePattern = /\/\*\s*([a-zA-Z0-9_-]+):\s*container>\s*\*\//; const sectionOpenPattern = /\/\*\s*([a-zA-Z0-9_-]+)<\s*\*\//; const sectionClosePattern = /\/\*\s*([a-zA-Z0-9_-]+)>\s*\*\//; // HTML comment patterns const htmlContainerOpenPattern = /<!--\s*([a-zA-Z0-9_-]+):\s*container<\s*-->/; const htmlContainerClosePattern = /<!--\s*([a-zA-Z0-9_-]+):\s*container>\s*-->/; const htmlSectionOpenPattern = /<!--\s*([a-zA-Z0-9_-]+)<\s*-->/; const htmlSectionClosePattern = /<!--\s*([a-zA-Z0-9_-]+)>\s*-->/; // Helper to generate scopeFrame based on language function generateScopeFrame(autoName) { // Removed for simplification - we'll add back later return null; } for (const line of lines) { lineNumber++; const trimmed = line.trim(); // Check for attributes in current line const attrs = parseAttributes(trimmed); // --- Handle attribute-only lines --- if (isAttributeOnlyLine(trimmed)) { if (stack.length > 0 && attrs) { const parent = stack[stack.length - 1]; const lastSection = parent.sections[parent.sections.length - 1]; // If a section is open and not yet closed, attach metadata to it if (lastSection && !lastSection.fullContent) { lastSection.attributes = { ...(lastSection.attributes || {}), ...attrs }; } // Otherwise attach to the container itself else { parent.attributes = { ...(parent.attributes || {}), ...attrs }; } } // Skip adding attribute line to content / unmarked continue; } // Check for container open let match = trimmed.match(containerOpenPattern) || trimmed.match(htmlContainerOpenPattern); if (match) { // Save any unmarked content before this container if (currentUnmarked.length > 0) { result.sections.push({ type: 'unmarked', content: currentUnmarked.join('\n'), startLine: lineNumber - currentUnmarked.length, endLine: lineNumber - 1 }); currentUnmarked = []; } containerCount++; const baseName = match[1].split(':')[0]; // Get base name without ": container" stack.push({ name: match[1], autoName: `${baseName}-c${containerCount}`, type: 'container', sections: [], startLine: lineNumber, content: [], containerNumber: containerCount, sectionCount: 0, // Track sections within this container attributes: attrs || null // Add attributes if found on same line }); continue; } // Check for container close match = trimmed.match(containerClosePattern) || trimmed.match(htmlContainerClosePattern); if (match) { if (stack.length > 0) { const container = stack.pop(); container.endLine = lineNumber; container.fullContent = container.content.join('\n'); delete container.content; // Clean up temp array // Check for attributes on closing tag (if none already) if (attrs && !container.attributes) { container.attributes = attrs; } if (stack.length === 0) { // Top-level container - add to sections in order result.sections.push(container); } else { // Nested container - add to parent stack[stack.length - 1].sections.push(container); } } continue; } // Check for section open match = trimmed.match(sectionOpenPattern) || trimmed.match(htmlSectionOpenPattern); if (match) { if (stack.length > 0) { const parent = stack[stack.length - 1]; parent.sectionCount++; const baseName = match[1]; const autoName = `${parent.autoName}s${parent.sectionCount}`; parent.sections.push({ name: match[1], autoName: autoName, type: 'section', content: [], startLine: lineNumber, sectionNumber: parent.sectionCount, attributes: attrs || null }); } continue; } // Check for section close match = trimmed.match(sectionClosePattern) || trimmed.match(htmlSectionClosePattern); if (match) { if (stack.length > 0) { const parent = stack[stack.length - 1]; const lastSection = parent.sections[parent.sections.length - 1]; if (lastSection && lastSection.type === 'section') { lastSection.endLine = lineNumber; lastSection.fullContent = lastSection.content.join('\n'); delete lastSection.content; // Clean up temp array // Check for attributes on closing tag (if none already) if (attrs && !lastSection.attributes) { lastSection.attributes = attrs; } // Extract metadata from content const metadata = extractMetadataForSection(lastSection); if (metadata) { lastSection.scopeData = metadata; } } } continue; } // Regular content line if (stack.length > 0) { const parent = stack[stack.length - 1]; // Add to current section if one is open const lastSection = parent.sections[parent.sections.length - 1]; if (lastSection && lastSection.type === 'section' && !lastSection.fullContent) { lastSection.content.push(line); } // Always add to container content parent.content.push(line); } else { // Outside any container - unmarked content currentUnmarked.push(line); } } // Handle any remaining unmarked content if (currentUnmarked.length > 0) { result.sections.push({ type: 'unmarked', content: currentUnmarked.join('\n'), startLine: lineNumber - currentUnmarked.length + 1, endLine: lineNumber }); } return result; } // --- Reconstruct File from Blocks --- function reconstructFile(parsed) { const lines = []; parsed.sections.forEach(section => { if (section.type === 'unmarked') { lines.push(section.content); } else if (section.type === 'container') { lines.push(section.fullContent); } }); return lines.join('\n'); } // --- Reconstruct File WITHOUT Attributes (for AI) --- function reconstructFileWithoutAttributes(parsed) { const lines = []; function rebuildContainer(container) { const containerLines = []; // Determine if this is HTML based on autoName or check content const isHTML = container.autoName && ( container.autoName.includes('-html-') || container.autoName.toLowerCase().includes('html') ); // Add container opening with autoName if (isHTML) { containerLines.push(`<!-- ${container.autoName}: container< -->`); } else { containerLines.push(`/* ${container.autoName}: container< */`); } // Process sections if (container.sections && container.sections.length > 0) { container.sections.forEach(section => { if (section.type === 'section') { // Determine if section is HTML const isSectionHTML = section.autoName && ( section.autoName.includes('-html-') || section.autoName.toLowerCase().includes('html') ); // Add section opening with autoName if (isSectionHTML) { containerLines.push(`<!-- ${section.autoName}< -->`); } else { containerLines.push(`/* ${section.autoName}< */`); } // Add section content (without attribute lines) if (section.fullContent) { containerLines.push(section.fullContent); } // Add section closing with autoName if (isSectionHTML) { containerLines.push(`<!-- ${section.autoName}> -->`); } else { containerLines.push(`/* ${section.autoName}> */`); } } else if (section.type === 'container') { // Nested container containerLines.push(rebuildContainer(section)); } }); } // Add container closing with autoName if (isHTML) { containerLines.push(`<!-- ${container.autoName}: container> -->`); } else { containerLines.push(`/* ${container.autoName}: container> */`); } return containerLines.join('\n'); } parsed.sections.forEach(section => { if (section.type === 'unmarked') { lines.push(section.content); } else if (section.type === 'container') { lines.push(rebuildContainer(section)); } }); return lines.join('\n'); } // --- Rebuild with only SELECTED sections (AI Frame mode) --- function reconstructFileWithSelectedOnly(parsed) { const lines = []; // Helper to generate comment block for scopeData function generateScopeDataComment(scopeData, description, isHTML) { const commentLines = []; // Add description first if it exists if (description) { commentLines.push(`desc: ${description}`); } // Only include scopeData fields that have data if (scopeData) { for (const [key, values] of Object.entries(scopeData)) { if (values && values.length > 0) { commentLines.push(`${key}: ${values.join(', ')}`); } } } if (commentLines.length === 0) return null; // Format as multi-line comment block if (isHTML) { const block = ['<!-- [scopeMeta]']; commentLines.forEach(line => block.push(line)); block.push('-->'); return block.join('\n'); } else { const block = ['/* [scopeMeta]']; commentLines.forEach(line => block.push(line)); block.push('*/'); return block.join('\n'); } } function rebuildContainer(container, depth = 0) { const containerLines = []; // Determine if this is HTML based on autoName const isHTML = container.autoName && ( container.autoName.includes('-html-') || container.autoName.toLowerCase().includes('html') ); // Add container opening with autoName if (isHTML) { containerLines.push(`<!-- ${container.autoName}: container< -->`); } else { containerLines.push(`/* ${container.autoName}: container< */`); } // Process sections if (container.sections && container.sections.length > 0) { container.sections.forEach(section => { if (section.type === 'section') { // Determine if section is HTML const isSectionHTML = section.autoName && ( section.autoName.includes('-html-') || section.autoName.toLowerCase().includes('html') ); // Add section opening with autoName if (isSectionHTML) { containerLines.push(`<!-- ${section.autoName}< -->`); } else { containerLines.push(`/* ${section.autoName}< */`); } // ONLY add content if section is selected if (section.selected === true && section.fullContent) { containerLines.push(section.fullContent); } else if (section.selected !== true) { // Section not selected - add scopeData comment with description if available const description = section.attributes && section.attributes.desc ? section.attributes.desc : null; const scopeComment = generateScopeDataComment(section.scopeData, description, isSectionHTML); if (scopeComment) { containerLines.push(scopeComment); } else { // Just add empty space containerLines.push(' '); } } // Add section closing with autoName if (isSectionHTML) { containerLines.push(`<!-- ${section.autoName}> -->`); } else { containerLines.push(`/* ${section.autoName}> */`); } } else if (section.type === 'container') { // Nested container - recursively rebuild containerLines.push(rebuildContainer(section, depth + 1)); } }); } // Add container closing with autoName if (isHTML) { containerLines.push(`<!-- ${container.autoName}: container> -->`); } else { containerLines.push(`/* ${container.autoName}: container> */`); } return containerLines.join('\n'); } parsed.sections.forEach(section => { if (section.type === 'unmarked') { // ALWAYS include unmarked content (HTML structure, etc.) lines.push(section.content); } else if (section.type === 'container') { lines.push(rebuildContainer(section)); } }); return lines.join('\n'); } // --- View Parsed Structure (Text-based) --- function showParsedJSON(fileName, fileContent) { const parsed = parseScopeMarkers(fileContent); // Store parsed data temporarily for selection updates window._tempParsedData = parsed; // Create modal const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.9); display: flex; align-items: center; justify-content: center; z-index: 2147483647; padding: 20px; `; const dialog = document.createElement('div'); dialog.style.cssText = ` background: #1a1a1a; border: 2px solid #3a3a3a; border-radius: 12px; padding: 24px; max-width: 1200px; width: 95%; max-height: 90vh; display: flex; flex-direction: column; `; const header = document.createElement('div'); header.style.cssText = ` display: flex; justify-content: flex-end; align-items: center; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 2px solid #2a2a2a; `; header.innerHTML = ` <div style="display: flex; gap: 12px; align-items: center;"> <select id="bulkSelectCombo" style=" padding: 8px 12px; background: #1a1a1a; border: 1px solid #3a3a3a; border-radius: 6px; color: #e0e0e0; cursor: pointer; font-size: 13px; font-weight: 600; min-width: 150px; "> <option value="">Bulk Select...</option> <option value="all">Select All</option> <option value="none">Select None</option> <option value="selected">Apply Selected</option> </select> <button id="toggleViewBtn" style=" padding: 8px 16px; background: #1e40af; border: 1px solid #2563eb; border-radius: 6px; color: #e0e0e0; cursor: pointer; font-size: 13px; font-weight: 600; ">View JSON</button> <button id="viewAIBtn" style=" padding: 8px 16px; background: #9333ea; border: 1px solid #a855f7; border-radius: 6px; color: #f5e8ff; cursor: pointer; font-size: 13px; font-weight: 600; display: block; ">View AI File</button> <button id="closeBtn" style=" padding: 8px 16px; background: #374151; border: 1px solid #4b5563; border-radius: 6px; color: #e0e0e0; cursor: pointer; font-size: 13px; font-weight: 600; ">Close</button> </div> `; // Text-based structure view const structureContainer = document.createElement('div'); structureContainer.style.cssText = ` background: #0a0a0a; border: 1px solid #2a2a2a; border-radius: 6px; padding: 20px; color: #e0e0e0; font-size: 13px; font-family: 'Courier New', monospace; overflow: auto; flex: 1; line-height: 1.6; `; // Build text representation let textRepresentation = ''; let blockNumber = 1; // Helper function to toggle selection function createSelectableBlock(section, idx, isSection = false, parentIdx = null) { const blockId = isSection ? `section-${parentIdx}-${idx}` : `block-${idx}`; const selectId = `select-${blockId}`; return { blockId, selectId, onClick: ` (function() { const checkbox = document.getElementById('${selectId}'); checkbox.checked = !checkbox.checked; const block = document.getElementById('${blockId}'); if (checkbox.checked) { block.style.borderLeft = '3px solid #10b981'; block.style.background = 'rgba(16, 185, 129, 0.15)'; } else { block.style.borderLeft = block.dataset.originalBorder; block.style.background = block.dataset.originalBg; } // Mark in JSON using temporary storage if (window._tempParsedData) { ${isSection ? `if (window._tempParsedData.sections[${parentIdx}] && window._tempParsedData.sections[${parentIdx}].sections[${idx}]) { window._tempParsedData.sections[${parentIdx}].sections[${idx}].selected = checkbox.checked; console.log('Section selected:', checkbox.checked, 'at [${parentIdx}].sections[${idx}]'); }` : `if (window._tempParsedData.sections[${idx}]) { window._tempParsedData.sections[${idx}].selected = checkbox.checked; console.log('Block selected:', checkbox.checked, 'at sections[${idx}]'); }` } } })(); ` }; } parsed.sections.forEach((section, idx) => { if (section.type === 'unmarked') { const lineCount = section.content.split('\n').length; // NOT SELECTABLE - just display textRepresentation += `<div style="margin-bottom: 20px; padding: 12px; background: rgba(100, 116, 139, 0.1); border-left: 3px solid #64748b; border-radius: 4px; opacity: 0.6;">`; textRepresentation += `<div style="color: #94a3b8; font-weight: 600; margin-bottom: 6px;">Block ${blockNumber++} ยท Unmarked Content (always included)</div>`; textRepresentation += `<div style="color: #64748b; font-size: 12px;">Lines ${section.startLine}โ€“${section.endLine} (${lineCount} line${lineCount !== 1 ? 's' : ''})</div>`; textRepresentation += `<div style="margin-top: 8px; padding: 8px; background: rgba(0,0,0,0.3); border-radius: 3px; color: #9ca3af; font-size: 11px; max-height: 60px; overflow: hidden;">`; textRepresentation += escapeHtml(section.content.substring(0, 200)); if (section.content.length > 200) textRepresentation += '...'; textRepresentation += `</div></div>`; } else if (section.type === 'container') { const lineCount = section.endLine - section.startLine + 1; const hasAttrs = section.attributes && Object.keys(section.attributes).length > 0; const containerId = `container-${idx}`; // NOT SELECTABLE - just display container header textRepresentation += `<div style="margin-bottom: 20px; padding: 12px; background: rgba(59, 130, 246, 0.05); border-left: 3px solid #3b82f6; border-radius: 4px;">`; // Header with attributes button (no checkbox) textRepresentation += `<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">`; textRepresentation += `<div style="color: #60a5fa; font-weight: 600;">Block ${blockNumber++} ยท Container: <span style="color: #93c5fd;">${escapeHtml(section.autoName)}</span></div>`; if (hasAttrs) { textRepresentation += `<button onclick="event.stopPropagation(); document.getElementById('${containerId}-attrs').style.display = document.getElementById('${containerId}-attrs').style.display === 'none' ? 'block' : 'none'" style="padding: 4px 8px; background: rgba(168, 85, 247, 0.2); border: 1px solid rgba(168, 85, 247, 0.4); border-radius: 4px; color: #c084fc; cursor: pointer; font-size: 10px; font-weight: 600;">๐Ÿ“Œ ${Object.keys(section.attributes).length} Attr${Object.keys(section.attributes).length !== 1 ? 's' : ''}</button>`; } textRepresentation += `</div>`; textRepresentation += `<div style="color: #64748b; font-size: 11px; font-style: italic; margin-bottom: 4px;">Original: ${escapeHtml(section.name)}</div>`; // Display attributes (hidden by default) if (hasAttrs) { textRepresentation += `<div id="${containerId}-attrs" style="display: none; margin: 8px 0; padding: 8px; background: rgba(168, 85, 247, 0.15); border: 1px solid rgba(168, 85, 247, 0.3); border-radius: 4px;">`; textRepresentation += `<div style="color: #c084fc; font-size: 11px; font-weight: 600; margin-bottom: 4px;">๐Ÿ“Œ Attributes:</div>`; for (const [key, value] of Object.entries(section.attributes)) { textRepresentation += `<div style="color: #e9d5ff; font-size: 11px; margin-left: 8px;">`; textRepresentation += `<span style="color: #d8b4fe; font-weight: 600;">${escapeHtml(key)}:</span> `; textRepresentation += `<span style="color: #f3e8ff;">${escapeHtml(value)}</span>`; textRepresentation += `</div>`; } textRepresentation += `</div>`; } textRepresentation += `<div style="color: #64748b; font-size: 12px;">Lines ${section.startLine}โ€“${section.endLine} (${lineCount} line${lineCount !== 1 ? 's' : ''})</div>`; if (section.sections && section.sections.length > 0) { textRepresentation += `<div style="margin-top: 12px; margin-left: 16px; padding-left: 12px; border-left: 2px solid rgba(59, 130, 246, 0.3);">`; section.sections.forEach((subsection, subIdx) => { const subLineCount = subsection.endLine - subsection.startLine + 1; const subHasAttrs = subsection.attributes && Object.keys(subsection.attributes).length > 0; const sectionId = `section-${idx}-${subIdx}`; const { blockId: subBlockId, selectId: subSelectId, onClick: subOnClick } = createSelectableBlock(subsection, subIdx, true, idx); // SECTIONS ARE SELECTABLE textRepresentation += `<div id="${subBlockId}" data-original-border="2px solid #10b981" data-original-bg="rgba(16, 185, 129, 0.1)" style="margin-bottom: 10px; padding: 8px; background: rgba(16, 185, 129, 0.1); border-left: 2px solid #10b981; border-radius: 3px; cursor: pointer; transition: all 0.2s;" onclick="${subOnClick}">`; // Section header with checkbox and attributes button textRepresentation += `<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px;">`; textRepresentation += `<div style="display: flex; align-items: center; gap: 6px;">`; textRepresentation += `<input type="checkbox" id="${subSelectId}" style="cursor: pointer; width: 14px; height: 14px;" onchange=" const block = document.getElementById('${subBlockId}'); if (this.checked) { block.style.borderLeft = '3px solid #10b981'; block.style.background = 'rgba(16, 185, 129, 0.15)'; } else { block.style.borderLeft = block.dataset.originalBorder; block.style.background = block.dataset.originalBg; } if (window._tempParsedData && window._tempParsedData.sections[${idx}] && window._tempParsedData.sections[${idx}].sections[${subIdx}]) { window._tempParsedData.sections[${idx}].sections[${subIdx}].selected = this.checked; console.log('Section', window._tempParsedData.sections[${idx}].sections[${subIdx}].autoName, 'selected:', this.checked); } " onclick="event.stopPropagation();">`; textRepresentation += `<div style="color: #34d399; font-weight: 600; font-size: 12px;">Section: <span style="color: #6ee7b7;">${escapeHtml(subsection.autoName)}</span></div>`; textRepresentation += `</div>`; if (subHasAttrs) { textRepresentation += `<button onclick="event.stopPropagation(); document.getElementById('${sectionId}-attrs').style.display = document.getElementById('${sectionId}-attrs').style.display === 'none' ? 'block' : 'none'" style="padding: 3px 6px; background: rgba(168, 85, 247, 0.2); border: 1px solid rgba(168, 85, 247, 0.4); border-radius: 3px; color: #c084fc; cursor: pointer; font-size: 9px; font-weight: 600;">๐Ÿ“Œ ${Object.keys(subsection.attributes).length}</button>`; } textRepresentation += `</div>`; textRepresentation += `<div style="color: #64748b; font-size: 10px; font-style: italic; margin-bottom: 3px;">Original: ${escapeHtml(subsection.name)}</div>`; // Display attributes (hidden by default) if (subHasAttrs) { textRepresentation += `<div id="${sectionId}-attrs" style="display: none; margin: 6px 0; padding: 6px; background: rgba(168, 85, 247, 0.15); border: 1px solid rgba(168, 85, 247, 0.3); border-radius: 3px;">`; textRepresentation += `<div style="color: #c084fc; font-size: 10px; font-weight: 600; margin-bottom: 3px;">๐Ÿ“Œ Attributes:</div>`; for (const [key, value] of Object.entries(subsection.attributes)) { textRepresentation += `<div style="color: #e9d5ff; font-size: 10px; margin-left: 6px;">`; textRepresentation += `<span style="color: #d8b4fe; font-weight: 600;">${escapeHtml(key)}:</span> `; textRepresentation += `<span style="color: #f3e8ff;">${escapeHtml(value)}</span>`; textRepresentation += `</div>`; } textRepresentation += `</div>`; } textRepresentation += `<div style="color: #64748b; font-size: 11px;">Lines ${subsection.startLine}โ€“${subsection.endLine} (${subLineCount} line${subLineCount !== 1 ? 's' : ''})</div>`; if (subsection.fullContent) { textRepresentation += `<div style="margin-top: 6px; padding: 6px; background: rgba(0,0,0,0.3); border-radius: 3px; color: #9ca3af; font-size: 10px; max-height: 50px; overflow: hidden;">`; textRepresentation += escapeHtml(subsection.fullContent.substring(0, 150)); if (subsection.fullContent.length > 150) textRepresentation += '...'; textRepresentation += `</div>`; } textRepresentation += `</div>`; }); textRepresentation += `</div>`; } textRepresentation += `</div>`; } }); structureContainer.innerHTML = textRepresentation; // JSON view (hidden by default) const jsonContainer = document.createElement('pre'); jsonContainer.style.cssText = ` background: #0a0a0a; border: 1px solid #2a2a2a; border-radius: 6px; padding: 16px; color: #e0e0e0; font-size: 12px; font-family: 'Courier New', monospace; overflow: auto; flex: 1; white-space: pre-wrap; word-wrap: break-word; display: none; `; jsonContainer.textContent = JSON.stringify(parsed, null, 2); const stats = document.createElement('div'); stats.style.cssText = ` margin-top: 16px; padding: 12px; background: #0a0a0a; border: 1px solid #2a2a2a; border-radius: 6px; display: flex; gap: 20px; font-size: 13px; flex-wrap: wrap; `; // Count containers and unmarked sections const containers = parsed.sections.filter(s => s.type === 'container'); const unmarked = parsed.sections.filter(s => s.type === 'unmarked'); const totalSections = containers.reduce((sum, c) => sum + c.sections.length, 0); stats.innerHTML = ` <div style="color: #3b82f6;"> <strong>๐Ÿ“ฆ Containers:</strong> ${containers.length} </div> <div style="color: #10b981;"> <strong>๐Ÿ“„ Sections:</strong> ${totalSections} </div> <div style="color: #64748b;"> <strong>โšช Unmarked:</strong> ${unmarked.length} </div> <div style="color: #8b5cf6;"> <strong>๐Ÿ“‹ Total Blocks:</strong> ${parsed.sections.length} </div> `; dialog.appendChild(header); dialog.appendChild(structureContainer); dialog.appendChild(jsonContainer); dialog.appendChild(stats); overlay.appendChild(dialog); document.body.appendChild(overlay); // Build bulk select options based on section names const bulkSelectCombo = header.querySelector('#bulkSelectCombo'); const sectionPrefixes = new Set(); parsed.sections.forEach(section => { if (section.type === 'container' && section.sections) { section.sections.forEach(subsection => { // Extract just the first part before any dash (e.g., "buttons" from "buttons-html-c6s1") const match = subsection.autoName.match(/^([a-zA-Z0-9_]+)/); if (match) { sectionPrefixes.add(match[1]); } }); } }); // Add prefix options to combo Array.from(sectionPrefixes).sort().forEach(prefix => { const option = document.createElement('option'); option.value = `prefix:${prefix}`; option.textContent = prefix; // Just show "buttons", "header", etc. bulkSelectCombo.appendChild(option); }); // Handle bulk selection - automatically applies to AI bulkSelectCombo.addEventListener('change', function() { const value = this.value; if (!value) return; if (value === 'all') { // Select all sections parsed.sections.forEach((section, idx) => { if (section.type === 'container' && section.sections) { section.sections.forEach((subsection, subIdx) => { const checkbox = document.getElementById(`select-section-${idx}-${subIdx}`); if (checkbox && !checkbox.checked) { checkbox.checked = true; checkbox.dispatchEvent(new Event('change')); } }); } }); console.log('[blocks] Selected all sections'); } else if (value === 'none') { // Deselect all sections parsed.sections.forEach((section, idx) => { if (section.type === 'container' && section.sections) { section.sections.forEach((subsection, subIdx) => { const checkbox = document.getElementById(`select-section-${idx}-${subIdx}`); if (checkbox && checkbox.checked) { checkbox.checked = false; checkbox.dispatchEvent(new Event('change')); } }); } }); console.log('[blocks] Deselected all sections'); } else if (value === 'selected') { // Just apply current selections to AI (don't change checkboxes) if (window._tempParsedData) { window.BlocksManager.activeFileParsed = window._tempParsedData; window.BlocksManager.applySelectionsToAI(window._tempParsedData); console.log('[blocks] Applied current selections to AI'); } // Reset combo and return early (don't do the auto-apply at the end) this.value = ''; return; } else if (value.startsWith('prefix:')) { // FIRST: Deselect ALL sections parsed.sections.forEach((section, idx) => { if (section.type === 'container' && section.sections) { section.sections.forEach((subsection, subIdx) => { const checkbox = document.getElementById(`select-section-${idx}-${subIdx}`); if (checkbox && checkbox.checked) { checkbox.checked = false; checkbox.dispatchEvent(new Event('change')); } }); } }); // THEN: Select sections matching prefix (just the first part before any dash) const prefix = value.substring(7); let count = 0; parsed.sections.forEach((section, idx) => { if (section.type === 'container' && section.sections) { section.sections.forEach((subsection, subIdx) => { // Check if autoName starts with prefix (e.g., "buttons" matches "buttons-html-c6s1") const firstPart = subsection.autoName.match(/^([a-zA-Z0-9_]+)/); if (firstPart && firstPart[1] === prefix) { const checkbox = document.getElementById(`select-section-${idx}-${subIdx}`); if (checkbox) { checkbox.checked = true; checkbox.dispatchEvent(new Event('change')); count++; } } }); } }); console.log(`[blocks] Selected ${count} sections matching: ${prefix}`); } // Auto-apply selections to AI (except for "selected" option) if (window._tempParsedData) { window.BlocksManager.activeFileParsed = window._tempParsedData; window.BlocksManager.applySelectionsToAI(window._tempParsedData); console.log('[blocks] Auto-applied selections to AI'); } // Reset combo to placeholder this.value = ''; }); // Remove the manual "Apply Selections to AI" button code since it auto-applies now // Toggle between structure and JSON view let showingJSON = false; const toggleBtn = header.querySelector('#toggleViewBtn'); toggleBtn.addEventListener('click', () => { showingJSON = !showingJSON; if (showingJSON) { structureContainer.style.display = 'none'; jsonContainer.style.display = 'block'; toggleBtn.textContent = 'View Structure'; } else { structureContainer.style.display = 'block'; jsonContainer.style.display = 'none'; toggleBtn.textContent = 'View JSON'; } }); // Handle "View AI File" modal - FULLSCREEN VERSION const viewAIBtn = header.querySelector('#viewAIBtn'); if (viewAIBtn) { viewAIBtn.addEventListener('click', () => { const aiOverlay = document.createElement('div'); aiOverlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.95); display: flex; align-items: center; justify-content: center; z-index: 2147483648; padding: 0; `; const aiDialog = document.createElement('div'); aiDialog.style.cssText = ` background: #1a1a1a; border: 2px solid #6d28d9; width: 100vw; height: 100vh; display: flex; flex-direction: column; padding: 0; `; const aiHeader = document.createElement('div'); aiHeader.style.cssText = ` padding: 20px 24px; border-bottom: 2px solid #6d28d9; display: flex; justify-content: space-between; align-items: center; background: #0a0a0a; `; aiHeader.innerHTML = ` <div> <h2 style="margin:0; color:#e9d5ff; font-size:24px; font-weight:700;"> ๐Ÿค– AI Active File View </h2> <p style="margin-top:6px; margin-bottom:0; color:#c084fc; font-size:14px;"> This is the filtered version shown to the AI. </p> </div> <button id="closeAIBtn" style=" padding:10px 24px; background:#6d28d9; border:1px solid #a855f7; border-radius:6px; color:#fff; cursor:pointer; font-size:14px; font-weight:700; ">Close</button> `; const aiContent = document.createElement('textarea'); aiContent.style.cssText = ` flex: 1; width: 100%; background: #0a0a0a; color: #f3e8ff; border: none; padding: 24px; font-size: 14px; line-height: 1.6; font-family: 'Courier New', 'Monaco', monospace; resize: none; overflow: auto; margin: 0; `; aiContent.readOnly = true; aiContent.value = window.BlocksManager.aiActiveContent || ''; aiDialog.appendChild(aiHeader); aiDialog.appendChild(aiContent); aiOverlay.appendChild(aiDialog); document.body.appendChild(aiOverlay); aiHeader.querySelector('#closeAIBtn').addEventListener('click', () => { document.body.removeChild(aiOverlay); }); aiOverlay.addEventListener('click', (e) => { if (e.target === aiOverlay) { document.body.removeChild(aiOverlay); } }); }); } // Close handlers header.querySelector('#closeBtn').addEventListener('click', () => { document.body.removeChild(overlay); }); overlay.addEventListener('click', (e) => { if (e.target === overlay) { document.body.removeChild(overlay); } }); } // --- Expose API --- window.BlocksManager = { // --- Existing methods --- parseScopes: parseScopeMarkers, showParsedJSON: showParsedJSON, reconstructFile: reconstructFile, reconstructFileWithoutAttributes: reconstructFileWithoutAttributes, reconstructFileWithSelectedOnly: reconstructFileWithSelectedOnly, // --- Real parsed file --- activeFileParsed: null, // --- NEW: AI State --- aiActiveContent: null, // String: filtered AI-safe version of the file aiActiveParsed: null, // Parsed version of the above // --- NEW: Get AI Context (for AI to grab file content) --- getAIContext: function () { return { content: this.aiActiveContent || "", parsed: this.aiActiveParsed || null }; }, // --- NEW: Insert/Replace/Delete Scope --- modifyScope: function (options) { /* options = { action: 'insert' | 'replace' | 'delete', containerIndex: number, // Index of container in sections array sectionIndex: number, // Index of section within container (optional for insert at end) content: string, // New content (for insert/replace) name: string, // Section name (for insert) attributes: object // Section attributes (for insert, optional) } */ if (!this.activeFileParsed) { console.error("[blocks] No active file parsed"); return false; } const container = this.activeFileParsed.sections[options.containerIndex]; if (!container || container.type !== 'container') { console.error("[blocks] Invalid container index"); return false; } const currentPrefix = container.autoName.match(/^([a-zA-Z0-9_]+)/)?.[1]; let modified = false; switch (options.action) { case 'insert': // Insert new section const newSectionNumber = container.sections.length + 1; const newSection = { name: options.name || `new-section-${newSectionNumber}`, autoName: `${container.autoName}s${newSectionNumber}`, type: 'section', fullContent: options.content || '', sectionNumber: newSectionNumber, attributes: options.attributes || null, selected: true // Auto-select new sections if they match current filter }; // Extract metadata for the new section const metadata = extractMetadataForSection(newSection); if (metadata) { newSection.scopeData = metadata; } // Insert at specified index or at end if (options.sectionIndex !== undefined) { container.sections.splice(options.sectionIndex, 0, newSection); } else { container.sections.push(newSection); } container.sectionCount = container.sections.length; modified = true; console.log("[blocks] Inserted new section:", newSection.autoName); break; case 'replace': // Replace existing section if (options.sectionIndex === undefined || !container.sections[options.sectionIndex]) { console.error("[blocks] Invalid section index for replace"); return false; } const existingSection = container.sections[options.sectionIndex]; existingSection.fullContent = options.content || ''; // Re-extract metadata const newMetadata = extractMetadataForSection(existingSection); if (newMetadata) { existingSection.scopeData = newMetadata; } modified = true; console.log("[blocks] Replaced section:", existingSection.autoName); break; case 'delete': // Delete section if (options.sectionIndex === undefined || !container.sections[options.sectionIndex]) { console.error("[blocks] Invalid section index for delete"); return false; } const deletedSection = container.sections.splice(options.sectionIndex, 1)[0]; container.sectionCount = container.sections.length; modified = true; console.log("[blocks] Deleted section:", deletedSection.autoName); break; default: console.error("[blocks] Invalid action:", options.action); return false; } if (modified) { // Rebuild the full file content const rebuiltContent = this.reconstructFile(this.activeFileParsed); // Update the active file in FilesManager if (window.FilesManager) { const activeFile = window.FilesManager.getActiveFile(); if (activeFile) { activeFile.content = rebuiltContent; window.FilesManager.saveFile(activeFile.path); } } // Re-apply selections to AI file (keeping current filter) this.applySelectionsToAI(this.activeFileParsed); console.log("[blocks] File updated and AI context refreshed"); return true; } return false; }, // --- NEW: Reset AI content from real active file --- resetAIActiveContent: function () { const activeFile = window.FilesManager.getActiveFile(); if (!activeFile || !activeFile.content) { console.warn("[blocks] No active file to clone for AI content."); this.aiActiveContent = null; this.aiActiveParsed = null; return; } // 1. Parse the real file into JSON structure const parsed = this.parseScopes(activeFile.content); // 2. Rebuild file from JSON WITHOUT attributes but WITH autoNames const rebuilt = this.reconstructFileWithoutAttributes(parsed); // 3. Save rebuilt version as the AI file this.aiActiveContent = rebuilt; // 4. Parse the rebuilt version into JSON (AI's JSON) this.aiActiveParsed = this.parseScopes(rebuilt); console.log("[blocks] AI active file rebuilt from parsed JSON (no attributes, with autoNames)."); }, // --- NEW: Apply selections to AI file (Frame mode) --- applySelectionsToAI: function (parsedWithSelections) { if (!parsedWithSelections) { console.warn("[blocks] No parsed file with selections available."); return; } console.log("[blocks] Applying selections. Parsed data:", parsedWithSelections); // Count selected sections for debugging let selectedCount = 0; parsedWithSelections.sections.forEach(section => { if (section.type === 'container' && section.sections) { section.sections.forEach(subsection => { if (subsection.selected === true) { selectedCount++; console.log("[blocks] Selected section:", subsection.autoName); } }); } }); console.log(`[blocks] Total selected sections: ${selectedCount}`); // Rebuild with only selected sections const filteredContent = this.reconstructFileWithSelectedOnly(parsedWithSelections); // Update AI content this.aiActiveContent = filteredContent; this.aiActiveParsed = this.parseScopes(filteredContent); console.log("[blocks] AI file updated with selected sections only (Frame mode)."); console.log("[blocks] AI content length:", filteredContent.length); // Dispatch event for other systems to react window.dispatchEvent(new CustomEvent('aiContentFiltered', { detail: { filteredContent: filteredContent, parsed: this.aiActiveParsed } })); }, // --- NEW: Clear AI state when AI mode is turned off --- clearAIState: function () { this.aiActiveContent = null; this.aiActiveParsed = null; console.log("[blocks] AI state cleared."); } }; // --- Auto-parse active file --- function updateActiveFileParsed() { // Check if FilesManager is available if (!window.FilesManager) { window.BlocksManager.activeFileParsed = null; return; } const files = window.FilesManager.getFiles(); const activeFile = files.find(f => f.active); if (activeFile && activeFile.content) { window.BlocksManager.activeFileParsed = parseScopeMarkers(activeFile.content); console.log('[blocks] Active file parsed:', activeFile.name); console.log('[blocks] Access via: window.BlocksManager.activeFileParsed'); } else { window.BlocksManager.activeFileParsed = null; console.log('[blocks] No active file'); } } // Listen for AI mode changes window.addEventListener('aiModeChanged', () => { if (window.FilesManager.aiMode) { // AI mode ON โ†’ make fresh copy of real file BlocksManager.resetAIActiveContent(); } else { // AI mode OFF โ†’ erase AI copy BlocksManager.clearAIState(); } }); // Listen for file updates window.addEventListener('activeFilesUpdated', updateActiveFileParsed); // Initial parse on load if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { updateActiveFileParsed(); // If an active file exists at startup โ†’ enable AI mode if (window.FilesManager.getActiveFile()) { window.FilesManager.setAIMode(true); } }); } else { setTimeout(() => { updateActiveFileParsed(); // If an active file exists at startup โ†’ enable AI mode if (window.FilesManager.getActiveFile()) { window.FilesManager.setAIMode(true); } }, 100); } console.log('[blocks] Scope Parser module loaded'); })();