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