// Storage Editor - Scopes Module v2 (scopes.js)
// Block-based visual editor with containers, scopes, and unmarked blocks
(function() {
console.log('🚀 Loading Scopes Block Editor v2...');
// Language styles
const LANG_STYLES = {
javascript: { color: '#f7df1e', bg: 'rgba(247, 223, 30, 0.1)', icon: '🟨', label: 'JS' },
css: { color: '#264de4', bg: 'rgba(38, 77, 228, 0.1)', icon: '🟦', label: 'CSS' },
php: { color: '#8892bf', bg: 'rgba(136, 146, 191, 0.1)', icon: '🟪', label: 'PHP' },
html: { color: '#e34c26', bg: 'rgba(227, 76, 38, 0.1)', icon: '🟧', label: 'HTML' },
python: { color: '#3776ab', bg: 'rgba(55, 118, 171, 0.1)', icon: '🐍', label: 'PY' },
text: { color: '#64748b', bg: 'rgba(100, 116, 139, 0.1)', icon: '📄', label: 'TXT' }
};
const UNMARKED_STYLE = {
color: '#6b7280',
bg: 'rgba(55, 65, 81, 0.05)',
border: '2px dashed #374151',
icon: '📝',
label: 'UNMARKED'
};
const CONTAINER_STYLE = {
color: '#8b5cf6',
bg: 'rgba(139, 92, 246, 0.05)',
border: '3px solid #8b5cf6',
icon: '📦',
headerBg: '#7c3aed'
};
function getLanguageStyle(lang) {
return LANG_STYLES[lang] || LANG_STYLES.text;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Detect language from context
function detectLanguage(lines, startLine, endLine) {
let inScript = false, inStyle = false, inPhp = false;
for (let i = 0; i <= startLine; i++) {
const line = lines[i];
if (/<script[^>]*>/i.test(line)) inScript = true;
if (/<\/script>/i.test(line)) inScript = false;
if (/<style[^>]*>/i.test(line)) inStyle = true;
if (/<\/style>/i.test(line)) inStyle = false;
if (/<\?php/i.test(line)) inPhp = true;
if (/\?>/i.test(line)) inPhp = false;
}
if (inScript) return 'javascript';
if (inStyle) return 'css';
if (inPhp) return 'php';
const content = lines.slice(startLine, endLine + 1).join('\n');
if (/<\?php/i.test(content)) return 'php';
if (/<script/i.test(content)) return 'javascript';
if (/<style/i.test(content)) return 'css';
if (/<[a-z]+/i.test(content)) return 'html';
return 'text';
}
// Parse scopes and containers from file content
function parseScopes(content) {
if (!content) return { scopes: [], containers: [] };
const lines = content.split('\n');
const scopes = [];
const containers = [];
const stack = [];
const containerStack = [];
// Patterns for containers
const containerOpenPatterns = [
/\/\/\s*([a-z0-9-]+):\s*container<\s*$/,
/\/\*\s*([a-z0-9-]+):\s*container<\s*\*\//,
/<!--\s*([a-z0-9-]+):\s*container<\s*-->/,
/#\s*([a-z0-9-]+):\s*container<\s*$/
];
// Patterns for regular scopes
const openPatterns = [
/\/\/\s*([a-z0-9-]+)<\s*$/,
/\/\*\s*([a-z0-9-]+)<\s*\*\//,
/<!--\s*([a-z0-9-]+)<\s*-->/,
/#\s*([a-z0-9-]+)<\s*$/
];
lines.forEach((line, idx) => {
// Check for container opening
for (const pattern of containerOpenPatterns) {
const match = line.match(pattern);
if (match) {
containerStack.push({ name: match[1], startLine: idx });
break;
}
}
// Check for container closing
if (containerStack.length > 0) {
const current = containerStack[containerStack.length - 1];
const closePatterns = [
new RegExp(`\\/\\/\\s*${current.name}:\\s*container>\\s*$`),
new RegExp(`\\/\\*\\s*${current.name}:\\s*container>\\s*\\*\\/`),
new RegExp(`<!--\\s*${current.name}:\\s*container>\\s*-->`),
new RegExp(`#\\s*${current.name}:\\s*container>\\s*$`)
];
for (const pattern of closePatterns) {
if (pattern.test(line)) {
current.endLine = idx;
containers.push(current);
containerStack.pop();
break;
}
}
}
// Check for scope opening
for (const pattern of openPatterns) {
const match = line.match(pattern);
if (match) {
const scopeData = {
name: match[1],
startLine: idx,
container: containerStack.length > 0 ? containerStack[containerStack.length - 1].name : null
};
stack.push(scopeData);
break;
}
}
// Check for scope closing
if (stack.length > 0) {
const current = stack[stack.length - 1];
const closePatterns = [
new RegExp(`\\/\\/\\s*${current.name}>\\s*$`),
new RegExp(`\\/\\*\\s*${current.name}>\\s*\\*\\/`),
new RegExp(`<!--\\s*${current.name}>\\s*-->`),
new RegExp(`#\\s*${current.name}>\\s*$`)
];
for (const pattern of closePatterns) {
if (pattern.test(line)) {
current.endLine = idx;
current.lineCount = current.endLine - current.startLine + 1;
current.language = detectLanguage(lines, current.startLine, current.endLine);
scopes.push(current);
stack.pop();
break;
}
}
}
});
return { scopes, containers };
}
// Build hierarchical block structure with unmarked blocks
function buildBlockStructure(content) {
const lines = content.split('\n');
const parsed = parseScopes(content);
const { scopes, containers } = parsed;
// Create a map of all marked line ranges
const markedRanges = [];
// Add container ranges
containers.forEach(c => {
markedRanges.push({
type: 'container',
start: c.startLine,
end: c.endLine,
data: c
});
});
// Add scope ranges
scopes.forEach(s => {
markedRanges.push({
type: 'scope',
start: s.startLine,
end: s.endLine,
data: s
});
});
// Sort by start line
markedRanges.sort((a, b) => a.start - b.start);
// Build block structure
const blocks = [];
let currentLine = 0;
markedRanges.forEach(range => {
// Add unmarked block before this range if there's a gap
if (currentLine < range.start) {
const content = lines.slice(currentLine, range.start).join('\n');
const trimmedContent = content.trim();
// Only add unmarked block if it has actual content (not just whitespace)
if (trimmedContent.length > 0) {
blocks.push({
type: 'unmarked',
startLine: currentLine,
endLine: range.start - 1,
content: content,
container: null
});
}
}
// Add the marked range
if (range.type === 'container') {
const container = range.data;
const containerScopes = scopes.filter(s => s.container === container.name);
const containerBlocks = [];
let containerCurrentLine = container.startLine + 1; // Skip opening marker
containerScopes.forEach(scope => {
// Add unmarked block inside container before scope
if (containerCurrentLine < scope.startLine) {
const content = lines.slice(containerCurrentLine, scope.startLine).join('\n');
const trimmedContent = content.trim();
// Only add if it has actual content
if (trimmedContent.length > 0) {
containerBlocks.push({
type: 'unmarked',
startLine: containerCurrentLine,
endLine: scope.startLine - 1,
content: content,
container: container.name
});
}
}
// Add scope block
containerBlocks.push({
type: 'scope',
startLine: scope.startLine,
endLine: scope.endLine,
content: lines.slice(scope.startLine + 1, scope.endLine).join('\n'), // Exclude markers
data: scope,
container: container.name
});
containerCurrentLine = scope.endLine + 1;
});
// Add trailing unmarked block inside container
if (containerCurrentLine < container.endLine) {
const content = lines.slice(containerCurrentLine, container.endLine).join('\n');
const trimmedContent = content.trim();
// Only add if it has actual content
if (trimmedContent.length > 0) {
containerBlocks.push({
type: 'unmarked',
startLine: containerCurrentLine,
endLine: container.endLine - 1,
content: content,
container: container.name
});
}
}
blocks.push({
type: 'container',
startLine: container.startLine,
endLine: container.endLine,
data: container,
children: containerBlocks
});
currentLine = container.endLine + 1;
} else if (range.type === 'scope' && !range.data.container) {
// Top-level scope (not in a container)
blocks.push({
type: 'scope',
startLine: range.start,
endLine: range.end,
content: lines.slice(range.start + 1, range.end).join('\n'), // Exclude markers
data: range.data,
container: null
});
currentLine = range.end + 1;
}
});
// Add trailing unmarked block
if (currentLine < lines.length) {
const content = lines.slice(currentLine).join('\n');
const trimmedContent = content.trim();
// Only add if it has actual content
if (trimmedContent.length > 0) {
blocks.push({
type: 'unmarked',
startLine: currentLine,
endLine: lines.length - 1,
content: content,
container: null
});
}
}
return blocks;
}
// Render a scope block
function renderScopeBlock(block, blockId) {
const style = getLanguageStyle(block.data.language);
const lineRange = `Lines ${block.startLine + 1}-${block.endLine + 1}`;
const lineCount = block.content.split('\n').length;
const textareaHeight = Math.max(80, lineCount * 22 + 32); // ~22px per line + padding
return `
<div class="block-scope" data-block-id="${blockId}" style="
display: flex;
margin-bottom: 12px;
border-radius: 8px;
overflow: hidden;
background: ${style.bg};
">
<!-- Vertical Header -->
<div style="
background: ${style.color};
width: 40px;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
gap: 8px;
flex-shrink: 0;
">
<span style="font-size: 20px;">${style.icon}</span>
<div style="
writing-mode: vertical-rl;
transform: rotate(180deg);
font-weight: 700;
color: #000;
font-family: monospace;
font-size: 13px;
letter-spacing: 1px;
">${escapeHtml(block.data.name)}</div>
<div style="
background: rgba(0,0,0,0.2);
padding: 4px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
color: #000;
">${style.label}</div>
<div style="
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 9px;
color: rgba(0,0,0,0.6);
font-weight: 600;
margin-top: auto;
">${lineRange}</div>
</div>
<!-- Content Area -->
<textarea class="block-content" data-block-id="${blockId}" style="
flex: 1;
height: ${textareaHeight}px;
background: #1e1e1e;
color: #e6edf3;
border: none;
border-left: 2px solid ${style.color};
padding: 16px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
line-height: 1.6;
resize: none;
outline: none;
overflow: hidden;
">${escapeHtml(block.content)}</textarea>
</div>
`;
}
// Render an unmarked block
function renderUnmarkedBlock(block, blockId) {
const lineRange = `Lines ${block.startLine + 1}-${block.endLine + 1}`;
const lineCount = block.endLine - block.startLine + 1;
const textareaHeight = Math.max(60, lineCount * 20 + 24); // ~20px per line + padding
return `
<div class="block-unmarked" data-block-id="${blockId}" style="
display: flex;
margin-bottom: 12px;
border-radius: 8px;
overflow: hidden;
background: ${UNMARKED_STYLE.bg};
opacity: 0.7;
">
<!-- Vertical Header -->
<div style="
background: #374151;
width: 40px;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
gap: 8px;
flex-shrink: 0;
">
<span style="font-size: 18px;">${UNMARKED_STYLE.icon}</span>
<div style="
writing-mode: vertical-rl;
transform: rotate(180deg);
font-weight: 600;
color: #9ca3af;
font-size: 11px;
letter-spacing: 0.5px;
">UNMARKED</div>
${block.container ? `
<div style="
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 9px;
color: #8b5cf6;
font-weight: 600;
">${escapeHtml(block.container)}</div>
` : ''}
<div style="
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 9px;
color: #6b7280;
margin-top: auto;
">${lineCount}L</div>
</div>
<!-- Content Area -->
<textarea class="block-content" data-block-id="${blockId}" style="
flex: 1;
height: ${textareaHeight}px;
background: #1a1a1a;
color: #9ca3af;
border: none;
border-left: 2px dashed #374151;
padding: 12px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
line-height: 1.5;
resize: none;
outline: none;
overflow: hidden;
">${escapeHtml(block.content)}</textarea>
</div>
`;
}
// Render a container block
function renderContainerBlock(block, blockId) {
const lineRange = `Lines ${block.startLine + 1}-${block.endLine + 1}`;
let childrenHtml = '';
block.children.forEach((child, idx) => {
const childId = `${blockId}-child-${idx}`;
if (child.type === 'scope') {
childrenHtml += renderScopeBlock(child, childId);
} else if (child.type === 'unmarked') {
childrenHtml += renderUnmarkedBlock(child, childId);
}
});
return `
<div class="block-container" data-block-id="${blockId}" style="
background: ${CONTAINER_STYLE.bg};
border: ${CONTAINER_STYLE.border};
border-radius: 12px;
margin-bottom: 20px;
overflow: hidden;
">
<!-- Horizontal Container Header -->
<div class="container-header" data-block-id="${blockId}" style="
background: ${CONTAINER_STYLE.headerBg};
padding: 14px 20px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
">
<div style="font-weight: 700; color: #fff; display: flex; align-items: center; gap: 12px; font-size: 16px;">
<span class="container-toggle" style="font-size: 14px;">▼</span>
<span style="font-size: 20px;">${CONTAINER_STYLE.icon}</span>
<span style="font-family: monospace; text-transform: uppercase; letter-spacing: 0.5px;">
${escapeHtml(block.data.name)}
</span>
<span style="
background: rgba(255,255,255,0.2);
padding: 3px 10px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
">${block.children.length} blocks</span>
</div>
<div style="font-size: 11px; color: rgba(255,255,255,0.7); font-weight: 600;">
${lineRange}
</div>
</div>
<!-- Container Body -->
<div class="container-body" data-block-id="${blockId}" style="
padding: 16px;
">
${childrenHtml}
</div>
</div>
`;
}
// Create main block editor HTML
function createBlockEditorHTML() {
// Check for dependencies
if (!window.StorageEditor) {
return `
<div style="padding: 40px; text-align: center; color: #ef4444;">
<h2>⚠️ Storage Editor Not Loaded</h2>
<p>The core Storage Editor module must be loaded first.</p>
</div>
`;
}
const file = window.StorageEditor.getActiveFile();
if (!file) {
return '<div style="padding: 40px; text-align: center; color: #64748b;">📄 No file open</div>';
}
const blocks = buildBlockStructure(file.content);
let html = `
<div style="
height: 100%;
display: flex;
flex-direction: column;
background: #0a0a0a;
">
<!-- Toolbar -->
<div style="
background: #1a1a1a;
padding: 12px 20px;
border-bottom: 2px solid #2a2a2a;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
">
<div style="display: flex; align-items: center; gap: 12px;">
<h2 style="margin: 0; color: #e6edf3; font-size: 18px; font-weight: 700;">
📦 Block Editor
</h2>
<span style="color: #64748b; font-size: 14px;">
${blocks.length} top-level blocks
</span>
</div>
<button id="saveAllBlocks" style="
background: #10b981;
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 700;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
">💾 Save All</button>
</div>
<!-- Blocks Container -->
<div id="blocksContainer" style="
flex: 1;
overflow-y: auto;
padding: 20px;
">
`;
blocks.forEach((block, idx) => {
const blockId = `block-${idx}`;
if (block.type === 'container') {
html += renderContainerBlock(block, blockId);
} else if (block.type === 'scope') {
html += renderScopeBlock(block, blockId);
} else if (block.type === 'unmarked') {
html += renderUnmarkedBlock(block, blockId);
}
});
html += `
</div>
</div>
`;
return html;
}
// Setup interactions
function setupBlockEditorInteractions(container) {
// Check for dependencies
if (!window.StorageEditor) {
console.error('StorageEditor not available');
return;
}
const file = window.StorageEditor.getActiveFile();
if (!file) return;
const blocks = buildBlockStructure(file.content);
// Container collapse/expand
container.querySelectorAll('.container-header').forEach(header => {
header.addEventListener('click', () => {
const blockId = header.dataset.blockId;
const body = container.querySelector(`.container-body[data-block-id="${blockId}"]`);
const toggle = header.querySelector('.container-toggle');
if (body.style.display === 'none') {
body.style.display = 'block';
toggle.textContent = '▼';
} else {
body.style.display = 'none';
toggle.textContent = '▶';
}
});
});
// Save all button
const saveBtn = container.querySelector('#saveAllBlocks');
if (saveBtn) {
saveBtn.addEventListener('click', () => {
if (!confirm('Save all changes to file?')) return;
// Collect all textarea values with their block IDs
const textareas = container.querySelectorAll('.block-content');
const updates = new Map();
textareas.forEach(ta => {
const blockId = ta.dataset.blockId;
updates.set(blockId, ta.value);
});
// Reconstruct file content
const lines = [];
function processBlock(block, blockId) {
const updatedContent = updates.get(blockId);
if (block.type === 'container') {
lines.push(file.content.split('\n')[block.startLine]); // Opening marker
block.children.forEach((child, idx) => {
const childId = `${blockId}-child-${idx}`;
processBlock(child, childId);
});
lines.push(file.content.split('\n')[block.endLine]); // Closing marker
} else if (block.type === 'scope') {
lines.push(file.content.split('\n')[block.startLine]); // Opening marker
lines.push(updatedContent);
lines.push(file.content.split('\n')[block.endLine]); // Closing marker
} else if (block.type === 'unmarked') {
lines.push(updatedContent);
}
}
blocks.forEach((block, idx) => {
processBlock(block, `block-${idx}`);
});
// Save to storage
const files = window.StorageEditor.loadActiveFiles();
const activeIdx = files.findIndex(f => f.active);
if (activeIdx !== -1) {
files[activeIdx].content = lines.join('\n');
files[activeIdx].lastModified = new Date().toISOString();
window.StorageEditor.saveActiveFiles(files);
window.dispatchEvent(new Event('activeFilesUpdated'));
saveBtn.textContent = '✅ SAVED';
saveBtn.style.background = '#10b981';
setTimeout(() => {
saveBtn.textContent = '💾 SAVE ALL';
saveBtn.style.background = '#10b981';
}, 2000);
}
});
}
}
// Export
window.StorageEditorScopes = {
open: () => {
if (window.AppOverlay) {
AppOverlay.open([{
title: '📦 Block Editor',
html: createBlockEditorHTML(),
onRender: setupBlockEditorInteractions
}]);
}
},
parseScopes,
buildBlockStructure,
getLanguageStyle
};
console.log('✅ Scopes Block Editor v2 loaded');
})();