// 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();
const attrs = parseAttributes(trimmed);
if (!attrs) return false;
// Check if line is ONLY attributes (in comments)
// CSS/JS: /* @...@ */
// HTML: <!-- @...@ -->
const cssAttrOnly = /^\s*\/\*\s*@[^*]*\*\/\s*$/;
const htmlAttrOnly = /^\s*<!--\s*@[^-]*-->\s*$/;
return cssAttrOnly.test(trimmed) || htmlAttrOnly.test(trimmed);
}
// --- 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*-->/;
for (const line of lines) {
lineNumber++;
const trimmed = line.trim();
// Check for attributes in current line
const attrs = parseAttributes(trimmed);
// 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 // Add attributes if found
});
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 (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 // Add attributes if found
});
}
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 (attrs && !lastSection.attributes) {
lastSection.attributes = attrs;
}
}
}
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');
}
// --- View Parsed Structure (Text-based) ---
function showParsedJSON(fileName, fileContent) {
const parsed = parseScopeMarkers(fileContent);
// 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: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid #2a2a2a;
`;
header.innerHTML = `
<div>
<h2 style="margin: 0; color: #e6edf3; font-size: 20px; font-weight: 700;">
๐ Document Structure
</h2>
<p style="margin: 4px 0 0 0; color: #64748b; font-size: 13px; font-family: monospace;">
${escapeHtml(fileName)}
</p>
</div>
<div style="display: flex; gap: 12px;">
<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="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;
parsed.sections.forEach((section, idx) => {
if (section.type === 'unmarked') {
const lineCount = section.content.split('\n').length;
textRepresentation += `<div style="margin-bottom: 20px; padding: 12px; background: rgba(100, 116, 139, 0.1); border-left: 3px solid #64748b; border-radius: 4px;">`;
textRepresentation += `<div style="color: #94a3b8; font-weight: 600; margin-bottom: 6px;">Block ${blockNumber++} ยท Unmarked Content</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}`;
textRepresentation += `<div style="margin-bottom: 20px; padding: 12px; background: rgba(59, 130, 246, 0.1); border-left: 3px solid #3b82f6; border-radius: 4px;">`;
// Header with attributes button
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="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}`;
textRepresentation += `<div style="margin-bottom: 10px; padding: 8px; background: rgba(16, 185, 129, 0.1); border-left: 2px solid #10b981; border-radius: 3px;">`;
// Section header with attributes button
textRepresentation += `<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px;">`;
textRepresentation += `<div style="color: #34d399; font-weight: 600; font-size: 12px;">Section: <span style="color: #6ee7b7;">${escapeHtml(subsection.autoName)}</span></div>`;
if (subHasAttrs) {
textRepresentation += `<button onclick="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);
// 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';
}
});
// 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 = {
parseScopes: parseScopeMarkers,
showParsedJSON: showParsedJSON,
reconstructFile: reconstructFile,
activeFileParsed: null // Will hold parsed JSON of active file
};
// --- 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 file updates
window.addEventListener('activeFilesUpdated', updateActiveFileParsed);
// Initial parse on load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', updateActiveFileParsed);
} else {
// DOM already loaded, run immediately
setTimeout(updateActiveFileParsed, 100);
}
console.log('[blocks] Scope Parser module loaded');
})();