// 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');
})();