📜
editor_index_copy4.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
/** * Editor Index Module v3 * Hierarchical, language-aware document structure navigation * Uses localStorage for data access - no direct Ace dependency */ (function() { 'use strict'; console.log("[editor_index.js v3] Loading editor index module..."); // ========================================================================= // LOCALSTORAGE KEYS // ========================================================================= const ACTIVE_FILES_KEY = "sftp_active_files"; // ========================================================================= // DEPENDENCIES (injected from main editor) // ========================================================================= let deps = { getGlobalEditor: null, // Optional - for navigation if available showToast: null }; // ========================================================================= // LANGUAGE DETECTION // ========================================================================= const LANGUAGE_INFO = { php: { icon: '📄', color: '#8892BF' }, js: { icon: '⚙️', color: '#F7DF1E' }, javascript: { icon: '⚙️', color: '#F7DF1E' }, css: { icon: '🎨', color: '#264DE4' }, html: { icon: '📦', color: '#E34F26' }, htm: { icon: '📦', color: '#E34F26' } }; function getLanguageInfo(lang) { const normalized = lang?.toLowerCase() || 'unknown'; return LANGUAGE_INFO[normalized] || { icon: '📝', color: '#888' }; } // ========================================================================= // LOCALSTORAGE ACCESS // ========================================================================= /** * Get the active file content from localStorage */ function getActiveFileContent() { try { const files = JSON.parse(localStorage.getItem(ACTIVE_FILES_KEY) || "[]"); const active = files.find(f => f.active); return { content: active?.content || "", name: active?.name || "Untitled", path: active?.path || "" }; } catch (err) { console.error("[editor_index.js v3] Failed to load file from localStorage:", err); return { content: "", name: "Untitled", path: "" }; } } /** * Get all lines from the active file */ function getLines() { const { content } = getActiveFileContent(); return content.split('\n'); } /** * Get a specific line by index */ function getLine(index) { const lines = getLines(); return lines[index] || ""; } /** * Get total line count */ function getLineCount() { return getLines().length; } // ========================================================================= // MARKER PARSING // ========================================================================= /** * Parse marker name into components * Examples: * "buttons_css_1" -> { component: "buttons", language: "css", number: 1 } * "buttons_css" -> { component: "buttons", language: "css", number: null } * "buttons" -> { component: "buttons", language: null, number: null } */ function parseMarkerName(markerName) { // Remove brackets if present const cleaned = markerName.replace(/[\[\]]/g, '').trim(); const parts = cleaned.split('_'); if (parts.length === 1) { // Just component name: "buttons" return { component: parts[0], language: null, number: null, fullName: cleaned }; } if (parts.length === 2) { // Component + language: "buttons_css" return { component: parts[0], language: parts[1], number: null, fullName: cleaned }; } if (parts.length >= 3) { // Component + language + number: "buttons_css_1" const number = parseInt(parts[2]); return { component: parts[0], language: parts[1], number: isNaN(number) ? null : number, fullName: cleaned }; } return { component: cleaned, language: null, number: null, fullName: cleaned }; } /** * Find the closing marker for a given opening marker */ function findMarkerEnd(startRow, markerName) { const lineCount = getLineCount(); for (let row = startRow + 1; row < lineCount; row++) { const line = getLine(row); const trimmed = line.trim(); // Look for closing marker with > if (trimmed.includes('>')) { const closeMatch = trimmed.match(/(?:<!--|\/\*|\/\/\/|\/\/)\s*([\w\-\[\]_]+)\s*>/); if (closeMatch && closeMatch[1].trim() === markerName) { return row; } } } return startRow; // No closing found, return start } // ========================================================================= // INDEX GENERATION (HIERARCHICAL MODE) // ========================================================================= function generateHierarchicalIndex() { const lineCount = getLineCount(); const components = {}; // Hierarchical structure const unmarked = []; // Items without proper markers const markerRanges = []; // Track which rows are inside markers // ===================================================================== // FIRST PASS: Find all markers and their ranges // ===================================================================== for (let row = 0; row < lineCount; row++) { const line = getLine(row); const trimmed = line.trim(); if (!trimmed) continue; // Improved marker detection - opening markers with < if (trimmed.includes('<')) { let markerMatch = trimmed.match(/(?:<!--|\/\*|\/\/\/|\/\/)\s*([\w\-\[\]_]+)\s*</); if (markerMatch) { const markerName = markerMatch[1].trim(); const parsed = parseMarkerName(markerName); // Find closing marker const endRow = findMarkerEnd(row, markerName); const markerItem = { type: 'marker', row: row, endRow: endRow, label: markerName, parsed: parsed, preview: trimmed.substring(0, 60) + (trimmed.length > 60 ? '...' : ''), children: [] // Will hold items found inside this marker }; // Track this range markerRanges.push({ startRow: row, endRow: endRow, markerItem: markerItem }); if (parsed.language) { // Has language specification - add to components if (!components[parsed.component]) { components[parsed.component] = {}; } if (!components[parsed.component][parsed.language]) { components[parsed.component][parsed.language] = []; } components[parsed.component][parsed.language].push(markerItem); } else { // No language - add to unmarked unmarked.push(markerItem); } } } } // ===================================================================== // SECOND PASS: Find all items and assign them to markers or unmarked // ===================================================================== for (let row = 0; row < lineCount; row++) { const line = getLine(row); const trimmed = line.trim(); if (!trimmed) continue; // Skip marker opening/closing lines if (trimmed.includes('<') || trimmed.includes('>')) { if (trimmed.match(/(?:<!--|\/\*|\/\/\/|\/\/)\s*[\w\-\[\]_]+\s*[<>]/)) { continue; } } let match; let item = null; // HTML tags if ((match = trimmed.match(/^<(div|section|nav|header|footer|main|article|aside|button)[^>]*(?:id=["']([^"']+)["']|class=["']([^"']+)["'])?[^>]*>/i))) { const tag = match[1]; const id = match[2]; const className = match[3]; const label = id ? `#${id}` : (className ? `.${className.split(' ')[0]}` : `<${tag}>`); item = { type: 'html', row: row, label: label, icon: '📦', preview: trimmed.substring(0, 60) + (trimmed.length > 60 ? '...' : '') }; } // JavaScript functions else if ((match = trimmed.match(/(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:function|\([^)]*\)\s*=>))/))) { const funcName = match[1] || match[2]; item = { type: 'function', row: row, label: `${funcName}()`, icon: '⚙️', preview: trimmed.substring(0, 60) + (trimmed.length > 60 ? '...' : '') }; } // PHP functions else if ((match = trimmed.match(/(?:public|private|protected|static)?\s*function\s+(\w+)\s*\(/))) { item = { type: 'function', row: row, label: `${match[1]}()`, icon: '🔧', preview: trimmed.substring(0, 60) + (trimmed.length > 60 ? '...' : '') }; } // CSS classes else if ((match = trimmed.match(/^\.([a-zA-Z0-9_-]+)\s*\{/))) { item = { type: 'css', row: row, label: `.${match[1]}`, icon: '🎨', preview: trimmed.substring(0, 60) + (trimmed.length > 60 ? '...' : '') }; } // CSS IDs else if ((match = trimmed.match(/^#([a-zA-Z0-9_-]+)\s*\{/))) { item = { type: 'css', row: row, label: `#${match[1]}`, icon: '🎨', preview: trimmed.substring(0, 60) + (trimmed.length > 60 ? '...' : '') }; } if (!item) continue; // Find which marker this item belongs to let belongsToMarker = null; for (const range of markerRanges) { if (row > range.startRow && row < range.endRow) { belongsToMarker = range.markerItem; break; } } if (belongsToMarker) { belongsToMarker.children.push(item); } else { unmarked.push(item); } } return { components, unmarked }; } // ========================================================================= // HTML GENERATION // ========================================================================= function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function generateHierarchicalHTML(data) { const { components, unmarked } = data; let html = ` <div style="padding: 20px; color: #e0e0e0;"> <h2 style="margin: 0 0 20px 0; color: #fff;">📑 Document Index</h2> `; // Render components const componentNames = Object.keys(components).sort(); if (componentNames.length > 0) { componentNames.forEach(componentName => { const languages = components[componentName]; const languageNames = Object.keys(languages).sort(); html += ` <div style="margin-bottom: 16px; background: #2d2d2d; border-radius: 6px; overflow: hidden;"> <div class="component-header" data-component="${escapeHtml(componentName)}" style=" padding: 12px 16px; background: #3a3a3a; cursor: pointer; display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 15px; user-select: none; "> <span class="collapse-icon">▼</span> <span>📦 ${escapeHtml(componentName)}</span> <span style="color: #888; font-size: 12px; font-weight: normal;">(${languageNames.length} language${languageNames.length !== 1 ? 's' : ''})</span> </div> <div class="component-content" data-component="${escapeHtml(componentName)}" style="padding: 8px;"> `; // Render languages within component languageNames.forEach(langName => { const markers = languages[langName]; const langInfo = getLanguageInfo(langName); html += ` <div style="margin-bottom: 8px;"> <div class="language-header" style=" padding: 8px 12px; background: #3a3a3a; cursor: pointer; display: flex; align-items: center; gap: 8px; border-radius: 4px; font-size: 13px; user-select: none; "> <span>▼</span> <span>${langInfo.icon}</span> <span style="color: ${langInfo.color}; font-weight: 500;">${escapeHtml(langName)}</span> <span style="color: #888; font-size: 11px;">(${markers.length} marker${markers.length !== 1 ? 's' : ''})</span> </div> <div class="language-content" style="padding-left: 16px;"> `; // Render markers markers.forEach(marker => { html += ` <div class="index-item" data-row="${marker.row}" data-is-marker="true" data-label="${escapeHtml(marker.label)}" style=" padding: 8px 12px; margin: 4px 0; background: #2d2d2d; border-radius: 4px; cursor: pointer; transition: background 0.2s; "> <div style="display: flex; align-items: center; gap: 8px;"> <span style="font-size: 16px;">🏷️</span> <div style="flex: 1; min-width: 0;"> <div style="color: #e0e0e0; font-weight: 500; font-size: 13px;">${escapeHtml(marker.label)}</div> <div style="color: #888; font-size: 11px;">Lines ${marker.row + 1}–${marker.endRow + 1}</div> </div> </div> `; // Render children (items inside this marker) if (marker.children && marker.children.length > 0) { html += `<div style="margin-top: 8px; padding-left: 20px; border-left: 2px solid #444;">`; marker.children.forEach(child => { html += ` <div class="index-item-child" data-row="${child.row}" style=" padding: 6px 10px; margin: 2px 0; background: #252525; border-radius: 3px; cursor: pointer; font-size: 12px; "> <div style="display: flex; align-items: center; gap: 6px;"> <span>${child.icon}</span> <span style="color: #d0d0d0;">${escapeHtml(child.label)}</span> <span style="color: #666; font-size: 10px;">:${child.row + 1}</span> </div> </div> `; }); html += `</div>`; } html += `</div>`; }); html += ` </div> </div> `; }); html += ` </div> </div> `; }); } // Render unmarked items if (unmarked.length > 0) { html += ` <div style="margin-top: 20px;"> <h3 style="color: #aaa; font-size: 14px; margin-bottom: 10px;">📌 Unmarked Items</h3> `; unmarked.forEach(item => { const icon = item.type === 'marker' ? '🏷️' : item.icon; const isMarker = item.type === 'marker'; html += ` <div class="index-item" data-row="${item.row}" data-is-marker="${isMarker}" data-label="${isMarker ? escapeHtml(item.label) : ''}" style=" padding: 8px 12px; margin: 4px 0; background: #3d3d3d; border-radius: 4px; cursor: pointer; transition: background 0.2s; "> <div style="display: flex; align-items: center; gap: 8px;"> <span style="font-size: 16px;">${icon}</span> <div style="flex: 1; min-width: 0;"> <div style="color: #e0e0e0; font-weight: 500; font-size: 14px;">${escapeHtml(item.label)}</div> <div style="color: #888; font-size: 12px;">Line ${item.row + 1}</div> </div> </div> </div> `; }); html += `</div>`; } if (componentNames.length === 0 && unmarked.length === 0) { html += ` <div style="padding: 40px; text-align: center; color: #888;"> <div style="font-size: 48px; margin-bottom: 16px;">📄</div> <div style="font-size: 16px;">No structure found</div> <div style="font-size: 13px; margin-top: 8px;">Add markers or structural elements to see the index</div> </div> `; } html += `</div>`; return html; } // ========================================================================= // SHOW INDEX (Integrated into editor overlay) // ========================================================================= function showIndexOverlay() { console.log("[editor_index.js v3] Generating hierarchical index..."); const data = generateHierarchicalIndex(); const html = generateHierarchicalHTML(data); // Use AppOverlay if available (should be the editor's overlay system) if (window.AppOverlay && window.AppOverlay.open) { console.log("[editor_index.js v3] Opening index in AppOverlay"); const indexItem = { title: "📑 Document Index", html: html, onRender: (el) => { attachEventListeners(el); } }; // This will show the index in the same overlay system as the editor window.AppOverlay.open([indexItem], 0); } else { console.error("[editor_index.js v3] AppOverlay not available"); if (deps.showToast) { deps.showToast("⚠️ Overlay system not loaded", "error"); } } } // ========================================================================= // EVENT LISTENERS // ========================================================================= function attachEventListeners(el) { setTimeout(() => { // Component collapse/expand el.querySelectorAll('.component-header').forEach(header => { header.addEventListener('click', (e) => { const component = header.dataset.component; const content = el.querySelector(`.component-content[data-component="${component}"]`); const icon = header.querySelector('.collapse-icon'); if (content.style.display === 'none') { content.style.display = 'block'; icon.textContent = '▼'; } else { content.style.display = 'none'; icon.textContent = '▶'; } }); }); // Language collapse/expand el.querySelectorAll('.language-header').forEach(header => { header.addEventListener('click', (e) => { e.stopPropagation(); const content = header.nextElementSibling; const icon = header.querySelector('span:first-child'); if (content.style.display === 'none') { content.style.display = 'block'; icon.textContent = '▼'; } else { content.style.display = 'none'; icon.textContent = '▶'; } }); }); // Item click - navigate directly el.querySelectorAll('.index-item').forEach(item => { item.addEventListener('mouseenter', () => { item.style.background = '#4a5568'; }); item.addEventListener('mouseleave', () => { item.style.background = item.closest('.language-content') ? '#2d2d2d' : '#3d3d3d'; }); item.addEventListener('click', () => { const row = parseInt(item.dataset.row); const isMarker = item.dataset.isMarker === 'true'; const label = item.dataset.label; if (isMarker) { navigateToMarker(row, label); } else { navigateToRow(row); } // Navigation function handles closing the overlay }); }); // Child item click - navigate directly el.querySelectorAll('.index-item-child').forEach(item => { item.addEventListener('mouseenter', () => { item.style.background = '#3a3a3a'; }); item.addEventListener('mouseleave', () => { item.style.background = '#252525'; }); item.addEventListener('click', (e) => { e.stopPropagation(); // Don't trigger parent marker click const row = parseInt(item.dataset.row); navigateToRow(row); // Navigation function handles closing the overlay }); }); }, 50); } // ========================================================================= // NAVIGATION (FAST - Direct editor communication) // ========================================================================= /** * Navigate directly to marker and close index */ function navigateToMarker(startRow, label) { const globalEditorInstance = deps.getGlobalEditor ? deps.getGlobalEditor() : null; if (!globalEditorInstance) { console.error("[editor_index.js v3] No editor instance available"); if (deps.showToast) { deps.showToast("⚠️ Editor not available", "error"); } return; } const endRow = findMarkerEnd(startRow, label); console.log(`[editor_index.js v3] Direct navigation to ${label} at rows ${startRow}-${endRow}`); // Navigate immediately if Ace is available if (typeof ace !== 'undefined') { const session = globalEditorInstance.getSession(); const Range = ace.require('ace/range').Range; const range = new Range( startRow, 0, endRow, session.getLine(endRow).length ); globalEditorInstance.selection.setRange(range, false); globalEditorInstance.scrollToLine(startRow, true, true, () => {}); globalEditorInstance.focus(); // Show toast if (deps.showToast) { deps.showToast(`📍 ${label}`, "success"); } console.log(`[editor_index.js v3] ✓ Navigated to ${label}`); } // Close the index by going back to previous item in AppOverlay if (window.AppOverlay && window.AppOverlay.prev) { window.AppOverlay.prev(); } } /** * Navigate directly to row and close index */ function navigateToRow(row) { const globalEditorInstance = deps.getGlobalEditor ? deps.getGlobalEditor() : null; if (!globalEditorInstance) { console.error("[editor_index.js v3] No editor instance available"); if (deps.showToast) { deps.showToast("⚠️ Editor not available", "error"); } return; } const line = getLine(row); // Determine type and label from line content let type = 'line'; let label = `Line ${row + 1}`; if (line.includes('function')) { type = 'function'; const match = line.match(/function\s+(\w+)|(\w+)\s*=\s*function/); if (match) label = (match[1] || match[2]) + '()'; } else if (line.includes('class=') || line.includes('id=')) { type = 'html'; const idMatch = line.match(/id=["']([^"']+)["']/); const classMatch = line.match(/class=["']([^"']+)["']/); if (idMatch) label = `#${idMatch[1]}`; else if (classMatch) label = `.${classMatch[1].split(' ')[0]}`; } else if (line.match(/^[\.\#][\w-]+\s*\{/)) { type = 'css'; const match = line.match(/^([\.\#][\w-]+)/); if (match) label = match[1]; } console.log(`[editor_index.js v3] Direct navigation to ${label} (${type}) at row ${row}`); // Navigate immediately if Ace is available if (typeof ace !== 'undefined') { const session = globalEditorInstance.getSession(); const Range = ace.require('ace/range').Range; const foldRange = session.getFoldWidgetRange(row); if (foldRange) { const extended = new Range( foldRange.start.row, 0, foldRange.end.row, session.getLine(foldRange.end.row).length ); globalEditorInstance.selection.setRange(extended, false); } else { const range = new Range(row, 0, row, line.length); globalEditorInstance.selection.setRange(range, false); } globalEditorInstance.scrollToLine(row, true, true, () => {}); globalEditorInstance.focus(); // Show toast if (deps.showToast) { deps.showToast(`📍 ${label}`, "success"); } console.log(`[editor_index.js v3] ✓ Navigated to ${label}`); } // Close the index by going back to previous item in AppOverlay if (window.AppOverlay && window.AppOverlay.prev) { window.AppOverlay.prev(); } } // ========================================================================= // INITIALIZATION // ========================================================================= function init(dependencies) { deps = { ...deps, ...dependencies }; console.log("[editor_index.js v3] Initialized with dependencies"); } // ========================================================================= // PUBLIC API // ========================================================================= window.EditorIndex = { init: init, generateDocumentIndex: generateHierarchicalIndex, findMarkerEnd: findMarkerEnd, showIndexOverlay: showIndexOverlay, navigateToMarker: navigateToMarker, navigateToRow: navigateToRow, parseMarkerName: parseMarkerName, // Data access methods for external use getActiveFileContent: getActiveFileContent, getLines: getLines, getLine: getLine, getLineCount: getLineCount }; console.log("[editor_index.js v3] Module loaded successfully"); })();