📜
scopes.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// scopes.js - Pure Logic Engine (NO RENDERING, ALL LAYOUT MOVED TO active_file.js) (function() { console.log("⚙️ Loading Scopes Logic Engine... (no rendering)"); // --------------------------------------------------------------------------- // LANGUAGE STYLE LOOKUP (still needed by active_file.js) // --------------------------------------------------------------------------- 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' } }; function getLanguageStyle(lang) { return LANG_STYLES[lang] || LANG_STYLES.text; } // --------------------------------------------------------------------------- // UTILITY // --------------------------------------------------------------------------- function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } // --------------------------------------------------------------------------- // ATTRIBUTE PARSER (@key:value@) // --------------------------------------------------------------------------- function parseScopeAttributes(line) { const attributes = {}; const pattern = /@([a-zA-Z0-9_-]+):([^@]+)@/g; let m; while ((m = pattern.exec(line))) { attributes[m[1]] = m[2].trim(); } return Object.keys(attributes).length ? attributes : null; } // Extract metadata lines (@key:value@) from the top of a scope's body function extractMetadataAndCleanLines(lines) { const metadata = {}; const cleanedLines = []; let stillInMetadata = true; for (let i = 0; i < lines.length; i++) { const raw = lines[i]; const trimmed = raw.trim(); // Match lines like: @key:value@ const m = trimmed.match(/^@([a-zA-Z0-9_-]+)\s*:(.*?)@$/); if (m && stillInMetadata) { const key = m[1].trim(); let value = m[2].trim(); if (key === 'relatedScopes') { metadata.relatedScopes = value ? value.split(',').map(s => s.trim()).filter(Boolean) : []; } else if (key === 'position') { const num = parseInt(value, 10); metadata.position = isNaN(num) ? value : num; } else if (key === 'updatedAt') { metadata.updatedAt = value; // keep as raw string/timestamp } else if (key === 'updatedBy') { metadata.updatedBy = value; } else if (key === 'container') { metadata.container = value; } else { metadata[key] = value; } } else { stillInMetadata = false; cleanedLines.push(raw); } } return { metadata, cleanedLines }; } // --------------------------------------------------------------------------- // SNIPPET HEADER PARSER @@container[pos] | lang | action@@ // --------------------------------------------------------------------------- function parseSnippetHeader(line) { const match = line.match(/^@@([a-z0-9-]+)\[(\d+)\]\s*\|\s*(\w+)\s*\|\s*(new|edit)@@$/i); if (!match) return null; return { container: match[1], position: parseInt(match[2]), language: match[3].toLowerCase(), action: match[4].toLowerCase() }; } // --------------------------------------------------------------------------- // LANGUAGE DETECTOR // --------------------------------------------------------------------------- function detectLanguage(lines, startLine, endLine, scopeName) { if (scopeName) { const name = scopeName.toLowerCase(); if (name.includes("js") || name.includes("javascript")) return "javascript"; if (name.includes("css")) return "css"; if (name.includes("php")) return "php"; if (name.includes("html")) return "html"; if (name.includes("py") || name.includes("python")) return "python"; } 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 snippet = lines.slice(startLine, endLine + 1).join("\n"); if (/<\?php/i.test(snippet)) return "php"; if (/<script/i.test(snippet)) return "javascript"; if (/<style/i.test(snippet)) return "css"; if (/<[a-z]+/i.test(snippet)) return "html"; if (/\b(function|const|let|var|=>|console\.)/.test(snippet)) return "javascript"; if (/:[^;]+;/.test(snippet)) return "css"; return "text"; } // --------------------------------------------------------------------------- // PARSE SCOPES + CONTAINERS // --------------------------------------------------------------------------- function parseScopes(content) { if (!content) return { scopes: [], containers: [] }; const lines = content.split("\n"); const scopes = []; const containers = []; const stack = []; const containerStack = []; let lastHeader = null; let lastHeaderLine = -1; const patterns = { containerOpen: [ /\/\/\s*([a-z0-9-]+):\s*container</, /\/\*\s*([a-z0-9-]+):\s*container<\s*\*\//, /<!--\s*([a-z0-9-]+):\s*container</, /#\s*([a-z0-9-]+):\s*container</ ], open: [ /\/\/\s*([a-z0-9-]+)</, /\/\*\s*([a-z0-9-]+)<\s*\*\//, /<!--\s*([a-z0-9-]+)</, /#\s*([a-z0-9-]+)</ ] }; lines.forEach((line, idx) => { const trimmed = line.trim(); const snippet = parseSnippetHeader(trimmed); if (snippet) { lastHeader = snippet; lastHeaderLine = idx; return; } // Container open for (const pat of patterns.containerOpen) { const m = line.match(pat); if (m) { containerStack.push({ name: m[1], startLine: idx }); } } // Container close if (containerStack.length) { const top = containerStack[containerStack.length - 1]; const closePats = [ new RegExp(`\\/\\/\\s*${top.name}:\\s*container>`), new RegExp(`\\/\\*\\s*${top.name}:\\s*container>\\s*\\*\\/`), new RegExp(`<!--\\s*${top.name}:\\s*container>`), new RegExp(`#\\s*${top.name}:\\s*container>`) ]; for (const p of closePats) { if (p.test(line)) { top.endLine = idx; containers.push(top); containerStack.pop(); } } } // Scope open for (const pat of patterns.open) { const m = line.match(pat); if (m) { const scope = { name: m[1], startLine: idx, container: containerStack.length ? containerStack[containerStack.length - 1].name : null, attributes: parseScopeAttributes(line), header: null }; if (lastHeader && idx - lastHeaderLine <= 4) { scope.header = lastHeader; scope.startLine = lastHeaderLine; lastHeader = null; } stack.push(scope); } } // Scope close if (stack.length) { const current = stack[stack.length - 1]; const closePats = [ new RegExp(`\\/\\/\\s*${current.name}>`), new RegExp(`\\/\\*\\s*${current.name}>\\s*\\*\\/`), new RegExp(`<!--\\s*${current.name}>`), new RegExp(`#\\s*${current.name}>`) ]; for (const p of closePats) { if (p.test(line)) { current.endLine = idx; current.lineCount = current.endLine - current.startLine + 1; current.language = detectLanguage(lines, current.startLine, current.endLine, current.name); scopes.push(current); stack.pop(); } } } }); return { scopes, containers }; } // --------------------------------------------------------------------------- // BUILD FULL BLOCK STRUCTURE (NO RENDERING) // --------------------------------------------------------------------------- function buildBlockStructure(content) { const lines = content.split("\n"); const { scopes, containers } = parseScopes(content); const marked = []; // Mark containers containers.forEach(c => { marked.push({ type: "container", start: c.startLine, end: c.endLine, data: c }); }); // Mark scopes scopes.forEach(s => { marked.push({ type: "scope", start: s.startLine, end: s.endLine, data: s }); }); marked.sort((a, b) => a.start - b.start); const blocks = []; let lineIndex = 0; // -------------------------------------------------- // Helper: extract metadata & cleaned scope body // - supports: // /* ... @key:val@ ... */ // <!-- ... @key:val@ ... --> // @key:val@ (bare at top) // -------------------------------------------------- function extractMetadataAndCleanLines(linesArr, language) { const metadata = {}; const cleaned = []; const lang = (language || "").toLowerCase(); const isHTML = (lang === "html" || lang === "htm"); let inMetaComment = false; let metaCommentConsumed = false; for (let i = 0; i < linesArr.length; i++) { const raw = linesArr[i]; const trimmed = raw.trim(); // -------- COMMENTED METADATA BLOCKS -------- if (!metaCommentConsumed) { if (!inMetaComment) { // Start of JS/PHP/CSS block comment if (!isHTML && trimmed.startsWith("/*")) { inMetaComment = true; continue; } // Start of HTML comment if (isHTML && trimmed.startsWith("<!--")) { inMetaComment = true; continue; } } else { // Inside metadata comment block // Normalize line (strip leading "*" or similar) let inner = trimmed.replace(/^\/\*+/, "") .replace(/^\*+/, "") .replace(/^<!--/, "") .replace(/--\>$/, "") .trim(); // Try @key:value@ within the comment const m = inner.match(/^@([a-zA-Z0-9_-]+)\s*:(.*?)@$/); if (m) { const key = m[1].trim(); let value = m[2].trim(); if (key === "relatedScopes") { metadata.relatedScopes = value ? value.split(",").map(s => s.trim()).filter(Boolean) : []; } else if (key === "position") { const num = parseInt(value, 10); metadata.position = isNaN(num) ? value : num; } else if (!isNaN(Number(value))) { metadata[key] = value; } else { metadata[key] = value; } } // End of comment block if (!isHTML && trimmed.endsWith("*/")) { inMetaComment = false; metaCommentConsumed = true; } else if (isHTML && trimmed.endsWith("-->")) { inMetaComment = false; metaCommentConsumed = true; } continue; // skip comment lines from content } } // -------- BARE @key:value@ LINES (fallback) -------- if (!metaCommentConsumed) { const bare = trimmed.match(/^@([a-zA-Z0-9_-]+)\s*:(.*?)@$/); if (bare) { const key = bare[1].trim(); let value = bare[2].trim(); if (key === "relatedScopes") { metadata.relatedScopes = value ? value.split(",").map(s => s.trim()).filter(Boolean) : []; } else if (key === "position") { const num = parseInt(value, 10); metadata.position = isNaN(num) ? value : num; } else if (!isNaN(Number(value))) { metadata[key] = value; } else { metadata[key] = value; } // Do NOT push this line into cleaned continue; } } // Normal content line cleaned.push(raw); } return { metadata, cleaned }; } // -------------------------------------------------- // MAIN PASS // -------------------------------------------------- marked.forEach(range => { // Unmarked stuff before this range if (lineIndex < range.start) { const chunk = lines.slice(lineIndex, range.start).join("\n").trim(); if (chunk.length) { blocks.push({ type: "unmarked", startLine: lineIndex, endLine: range.start - 1, content: lines.slice(lineIndex, range.start).join("\n"), container: null }); } } // ---------- CONTAINER ---------- if (range.type === "container") { const c = range.data; const children = []; let childIndex = c.startLine + 1; const childScopes = scopes.filter(s => s.container === c.name); childScopes.forEach(sc => { // Unmarked region before this scope if (childIndex < sc.startLine) { const unChunk = lines.slice(childIndex, sc.startLine).join("\n").trim(); if (unChunk.length) { children.push({ type: "unmarked", startLine: childIndex, endLine: sc.startLine - 1, content: lines.slice(childIndex, sc.startLine).join("\n"), container: c.name }); } } const lang = sc.language || "js"; const scopeBody = lines.slice(sc.startLine + 1, sc.endLine); const { metadata, cleaned } = extractMetadataAndCleanLines(scopeBody, lang); sc.metadata = metadata; children.push({ type: "scope", startLine: sc.startLine, endLine: sc.endLine, content: cleaned.join("\n"), data: sc, container: c.name }); childIndex = sc.endLine + 1; }); // trailing unmarked inside container if (childIndex < c.endLine) { const lastChunk = lines.slice(childIndex, c.endLine).join("\n").trim(); if (lastChunk.length) { children.push({ type: "unmarked", startLine: childIndex, endLine: c.endLine - 1, content: lines.slice(childIndex, c.endLine).join("\n"), container: c.name }); } } blocks.push({ type: "container", startLine: c.startLine, endLine: c.endLine, children, data: c }); // ---------- TOP-LEVEL SCOPE ---------- } else if (range.type === "scope" && !range.data.container) { const sc = range.data; const lang = sc.language || "js"; const scopeBody = lines.slice(range.start + 1, range.end); const { metadata, cleaned } = extractMetadataAndCleanLines(scopeBody, lang); sc.metadata = metadata; blocks.push({ type: "scope", startLine: range.start, endLine: range.end, content: cleaned.join("\n"), data: sc, container: null }); } lineIndex = range.end + 1; }); // trailing unmarked after last mark if (lineIndex < lines.length) { const lastChunk = lines.slice(lineIndex).join("\n").trim(); if (lastChunk.length) { blocks.push({ type: "unmarked", startLine: lineIndex, endLine: lines.length - 1, content: lines.slice(lineIndex).join("\n"), container: null }); } } return blocks; } /* function buildBlockStructure(content) { const lines = content.split("\n"); const { scopes, containers } = parseScopes(content); const marked = []; // Mark containers first containers.forEach(c => { marked.push({ type: "container", start: c.startLine, end: c.endLine, data: c }); }); // Mark scopes scopes.forEach(s => { marked.push({ type: "scope", start: s.startLine, end: s.endLine, data: s }); }); // Sort in top-to-bottom order marked.sort((a, b) => a.start - b.start); const blocks = []; let lineIndex = 0; // Helper: extract metadata lines (top of scope body) function extractMetadataAndCleanLines(linesArr) { const metadata = {}; const cleaned = []; let inMeta = true; for (let i = 0; i < linesArr.length; i++) { const raw = linesArr[i]; const trimmed = raw.trim(); // Metadata line format: @key:value@ const m = trimmed.match(/^@([a-zA-Z0-9_-]+)\s*:(.*?)@$/); if (m && inMeta) { const key = m[1].trim(); let value = m[2].trim(); if (key === "relatedScopes") { metadata.relatedScopes = value ? value.split(",").map(s => s.trim()).filter(Boolean) : []; } else if (key === "position") { const num = parseInt(value, 10); metadata.position = isNaN(num) ? value : num; } else if (!isNaN(Number(value))) { metadata[key] = value; } else { metadata[key] = value; } } else { inMeta = false; cleaned.push(raw); } } return { metadata, cleaned }; } // Main pass: resolve all marked structures marked.forEach(range => { // UNMARKED ABOVE THIS RANGE if (lineIndex < range.start) { const chunk = lines.slice(lineIndex, range.start).join("\n").trim(); if (chunk.length) { blocks.push({ type: "unmarked", startLine: lineIndex, endLine: range.start - 1, content: lines.slice(lineIndex, range.start).join("\n"), container: null }); } } //--------------------------------------------------------------------- // CONTAINER BLOCK //--------------------------------------------------------------------- if (range.type === "container") { const c = range.data; const children = []; let childIndex = c.startLine + 1; // Find scopes belonging to this container const childScopes = scopes.filter(s => s.container === c.name); childScopes.forEach(sc => { // Unmarked region before this child scope if (childIndex < sc.startLine) { const unChunk = lines.slice(childIndex, sc.startLine).join("\n").trim(); if (unChunk.length) { children.push({ type: "unmarked", startLine: childIndex, endLine: sc.startLine - 1, content: lines.slice(childIndex, sc.startLine).join("\n"), container: c.name }); } } // Extract metadata from scope body const scopeBody = lines.slice(sc.startLine + 1, sc.endLine); const { metadata, cleaned } = extractMetadataAndCleanLines(scopeBody); // Attach metadata sc.metadata = metadata; children.push({ type: "scope", startLine: sc.startLine, endLine: sc.endLine, content: cleaned.join("\n"), data: sc, container: c.name }); childIndex = sc.endLine + 1; }); // Any trailing unmarked section inside container if (childIndex < c.endLine) { const lastChunk = lines.slice(childIndex, c.endLine).join("\n").trim(); if (lastChunk.length) { children.push({ type: "unmarked", startLine: childIndex, endLine: c.endLine - 1, content: lines.slice(childIndex, c.endLine).join("\n"), container: c.name }); } } blocks.push({ type: "container", startLine: c.startLine, endLine: c.endLine, children, data: c }); //--------------------------------------------------------------------- // TOP-LEVEL SCOPE (NO CONTAINER) //--------------------------------------------------------------------- } else if (range.type === "scope" && !range.data.container) { const sc = range.data; const scopeBody = lines.slice(range.start + 1, range.end); const { metadata, cleaned } = extractMetadataAndCleanLines(scopeBody); sc.metadata = metadata; blocks.push({ type: "scope", startLine: range.start, endLine: range.end, content: cleaned.join("\n"), data: sc, container: null }); } lineIndex = range.end + 1; }); // UNMARKED AFTER LAST RANGE if (lineIndex < lines.length) { const lastChunk = lines.slice(lineIndex).join("\n").trim(); if (lastChunk.length) { blocks.push({ type: "unmarked", startLine: lineIndex, endLine: lines.length - 1, content: lines.slice(lineIndex).join("\n"), container: null }); } } return blocks; }*/ // --------------------------------------------------------------------------- // FUZZY MATCH (SELECT BEST SCOPE FOR REPLACE) // --------------------------------------------------------------------------- function findBestMatch(containerName, scopeName) { const file = window.StorageEditor.getActiveFile(); if (!file) throw new Error("No active file"); const parsed = parseScopes(file.content); const candidates = parsed.scopes.filter(s => s.container === containerName); if (!candidates.length) { return { match: null, score: 0 }; } const target = scopeName.toLowerCase(); const scored = candidates.map(s => { const name = s.name.toLowerCase(); if (name === target) return { scope: s, score: 100 }; if (name.includes(target) || target.includes(name)) return { scope: s, score: 80 }; let matches = 0; for (let i = 0; i < Math.min(name.length, target.length); i++) { if (name[i] === target[i]) matches++; } return { scope: s, score: (matches / Math.max(name.length, target.length)) * 60 }; }); scored.sort((a, b) => b.score - a.score); return { match: scored[0].scope, score: scored[0].score }; } // --------------------------------------------------------------------------- // INSERT NEW SCOPE INTO CONTAINER // --------------------------------------------------------------------------- function insertAt(containerName, position, scopeData) { const file = window.StorageEditor.getActiveFile(); if (!file) throw new Error("No active file"); const lines = file.content.split("\n"); const parsed = parseScopes(file.content); const container = parsed.containers.find(c => c.name === containerName); if (!container) throw new Error(`Container "${containerName}" not found`); let scopes = parsed.scopes.filter(s => s.container === containerName); scopes.sort((a, b) => a.startLine - b.startLine); const adjPos = Math.max(0, Math.min(position - 1, scopes.length)); let insertLine; if (adjPos === 0) insertLine = container.startLine + 1; else if (adjPos >= scopes.length) insertLine = container.endLine; else insertLine = scopes[adjPos].startLine; const { name, language, content, attributes } = scopeData; const attrString = attributes ? " " + Object.entries(attributes).map(([k, v]) => `@${k}:${v}@`).join(" ") : ""; let open, close; if (language === "html") { open = `<!-- ${name}<${attrString} -->`; close = `<!-- ${name}> -->`; } else if (language === "css") { open = `/* ${name}<${attrString} */`; close = `/* ${name}> */`; } else { open = `// ${name}<${attrString}`; close = `// ${name}>`; } const newLines = ["", open, content, close]; lines.splice(insertLine, 0, ...newLines); const files = window.StorageEditor.loadActiveFiles(); const idx = files.findIndex(f => f.active); files[idx].content = lines.join("\n"); files[idx].lastModified = new Date().toISOString(); window.StorageEditor.saveActiveFiles(files); window.dispatchEvent(new Event("activeFilesUpdated")); return { success: true, message: `Inserted "${name}" at position ${position} in "${containerName}"`, insertedAt: insertLine }; } // --------------------------------------------------------------------------- // REPLACE EXISTING SCOPE // --------------------------------------------------------------------------- function replace(containerName, position, scopeName, newContent, attributes) { const file = window.StorageEditor.getActiveFile(); if (!file) throw new Error("No active file"); const lines = file.content.split("\n"); const parsed = parseScopes(file.content); let scopes = parsed.scopes.filter(s => s.container === containerName); scopes.sort((a, b) => a.startLine - b.startLine); if (!scopes.length) { throw new Error(`No scopes found in container "${containerName}"`); } let target = null; if (position < 1 || position > scopes.length) { const best = findBestMatch(containerName, scopeName); if (best.score > 50) { target = best.match; } else { return insertAt(containerName, scopes.length + 1, { name: scopeName, language: scopes[0]?.language || "javascript", content: newContent, attributes }); } } else { const atPos = scopes[position - 1]; const best = findBestMatch(containerName, scopeName); if (best.score > 70 && best.match.name !== atPos.name) { target = best.match; } else { target = atPos; } } if (!target) throw new Error("Could not determine target scope to replace"); if (attributes) { const open = lines[target.startLine]; const cleaned = open.replace(/@[a-zA-Z0-9_-]+:[^@]+@/g, ""); const attrString = Object.entries(attributes) .map(([k, v]) => `@${k}:${v}@`) .join(" "); lines[target.startLine] = cleaned + " " + attrString; } lines.splice( target.startLine + 1, target.endLine - target.startLine - 1, newContent ); const files = window.StorageEditor.loadActiveFiles(); const idx = files.findIndex(f => f.active); files[idx].content = lines.join("\n"); files[idx].lastModified = new Date().toISOString(); window.StorageEditor.saveActiveFiles(files); window.dispatchEvent(new Event("activeFilesUpdated")); return { success: true, message: `Replaced scope "${target.name}"`, startLine: target.startLine, endLine: target.endLine }; } // --------------------------------------------------------------------------- // LIST STRUCTURE (debug) // --------------------------------------------------------------------------- function listStructure() { const file = window.StorageEditor.getActiveFile(); if (!file) throw new Error("No active file"); const parsed = parseScopes(file.content); return { containers: parsed.containers.map(c => ({ name: c.name, scopes: parsed.scopes .filter(s => s.container === c.name) .map(s => ({ name: s.name, language: s.language, attributes: s.attributes })) })), topLevelScopes: parsed.scopes .filter(s => !s.container) .map(s => ({ name: s.name, language: s.language, attributes: s.attributes })) }; } // --------------------------------------------------------------------------- // EXPORT API // --------------------------------------------------------------------------- window.StorageEditorScopes = { getLanguageStyle, parseScopes, buildBlockStructure, parseSnippetHeader, parseScopeAttributes, detectLanguage, findBestMatch, insertAt, replace, listStructure }; console.log("✅ Scopes Logic Engine Loaded (NO RENDERING)"); })();