/**
* Editor Index Module v2
* Hierarchical, language-aware document structure navigation
*/
(function() {
'use strict';
console.log("[editor_index.js v2] Loading editor index module...");
// =========================================================================
// DEPENDENCIES (injected from main editor)
// =========================================================================
let deps = {
getGlobalEditor: null,
getEl: null,
showOverlay: null,
hideOverlay: null,
escapeHtml: null,
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' };
}
// =========================================================================
// 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
};
}
// =========================================================================
// INDEX GENERATION
// =========================================================================
function generateDocumentIndex() {
const globalEditorInstance = deps.getGlobalEditor();
if (!globalEditorInstance) {
console.error("[editor_index.js v2] Global editor instance not available");
return { components: {}, unmarked: [] };
}
const session = globalEditorInstance.getSession();
const lineCount = session.getLength();
const components = {}; // Hierarchical structure
const unmarked = []; // Items without proper markers
for (let row = 0; row < lineCount; row++) {
const line = session.getLine(row);
const trimmed = line.trim();
if (!trimmed) continue;
let match;
// =====================================================================
// MARKERS (with language awareness)
// =====================================================================
if (trimmed.includes('<') && (trimmed.includes('<!--') || trimmed.includes('/*') || trimmed.includes('//'))) {
let markerMatch = trimmed.match(/(?:<!--|\/\*|\/\/\/|\/\/)\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 ? '...' : '')
};
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);
}
}
}
// =====================================================================
// LOOSE ITEMS (not in markers)
// =====================================================================
// HTML tags
else 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}>`);
unmarked.push({
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];
unmarked.push({
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*\(/))) {
unmarked.push({
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*\{/))) {
unmarked.push({
type: 'css',
row: row,
label: `.${match[1]}`,
icon: '🎨',
preview: trimmed.substring(0, 60) + (trimmed.length > 60 ? '...' : '')
});
}
// CSS element selectors
else if ((match = trimmed.match(/^(body|html|header|footer|main|section|nav|article|aside|h[1-6]|p|div|span|a|button|input|form)\s*\{/i))) {
unmarked.push({
type: 'css',
row: row,
label: match[1],
icon: '🎨',
preview: trimmed.substring(0, 60) + (trimmed.length > 60 ? '...' : '')
});
}
}
return { components, unmarked };
}
// =========================================================================
// MARKER UTILITIES
// =========================================================================
function findMarkerEnd(startRow, markerName) {
const globalEditorInstance = deps.getGlobalEditor();
if (!globalEditorInstance) return startRow;
const session = globalEditorInstance.getSession();
const lineCount = session.getLength();
const cleanMarker = markerName.replace(/[\[\]]/g, '').trim();
for (let row = startRow + 1; row < lineCount; row++) {
const line = session.getLine(row);
if (line.includes('>')) {
const closingMatch = line.match(/(?:<!--|\/\*|\/\/\/|\/\/)\s*(.+?)>/);
if (closingMatch && closingMatch[1].trim() === cleanMarker) {
return row;
}
}
}
return startRow;
}
// =========================================================================
// INDEX OVERLAY (HIERARCHICAL VIEW)
// =========================================================================
function showIndexOverlay() {
const { components, unmarked } = generateDocumentIndex();
const componentCount = Object.keys(components).length;
const unmarkedCount = unmarked.length;
const totalCount = componentCount + unmarkedCount;
if (totalCount === 0) {
deps.showOverlay(
'Document Index',
'<div style="color: #888; text-align: center; padding: 40px;">No sections found in document</div>'
);
return;
}
let indexHtml = '';
// =========================================================================
// COMPONENTS (Hierarchical)
// =========================================================================
if (componentCount > 0) {
indexHtml += `<div style="margin-bottom: 20px;">`;
indexHtml += `<div style="
color: #888;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
margin-bottom: 8px;
font-family: 'Segoe UI', sans-serif;
">📦 Components (${componentCount})</div>`;
for (const [componentName, languages] of Object.entries(components)) {
const languageCount = Object.keys(languages).length;
const totalSections = Object.values(languages).reduce((sum, sections) => sum + sections.length, 0);
indexHtml += `
<div class="component-group" style="
margin-bottom: 12px;
border: 1px solid #444;
border-radius: 6px;
overflow: hidden;
background: #2d2d2d;
">
<div class="component-header" data-component="${deps.escapeHtml(componentName)}" style="
padding: 12px;
background: #3d3d3d;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-family: 'Segoe UI', sans-serif;
">
<span class="collapse-icon" style="font-size: 14px; transition: transform 0.2s;">▼</span>
<span style="font-size: 16px;">📦</span>
<span style="color: #e0e0e0; font-weight: 600; flex: 1;">${deps.escapeHtml(componentName)}</span>
<span style="color: #888; font-size: 12px;">${languageCount} lang • ${totalSections} sections</span>
</div>
<div class="component-content" data-component="${deps.escapeHtml(componentName)}" style="
display: block;
">`;
// Languages within component
for (const [language, sections] of Object.entries(languages)) {
const langInfo = getLanguageInfo(language);
indexHtml += `
<div class="language-group" style="
border-top: 1px solid #444;
">
<div class="language-header" data-language="${deps.escapeHtml(language)}" style="
padding: 10px 12px;
background: #353535;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-family: 'Segoe UI', sans-serif;
">
<span style="font-size: 12px; transition: transform 0.2s;">▼</span>
<span style="font-size: 14px;">${langInfo.icon}</span>
<span style="color: #e0e0e0; font-size: 14px; flex: 1;">${deps.escapeHtml(language)}</span>
<span style="color: #888; font-size: 11px;">${sections.length} section${sections.length > 1 ? 's' : ''}</span>
</div>
<div class="language-content" style="display: block;">`;
// Sections within language
sections.forEach(section => {
indexHtml += `
<div class="index-item" data-row="${section.row}" data-is-marker="true" data-label="${deps.escapeHtml(section.label)}" style="
padding: 10px 12px 10px 40px;
background: #2d2d2d;
border-top: 1px solid #3d3d3d;
cursor: pointer;
transition: background 0.15s;
font-family: 'Segoe UI', monospace;
">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 14px;">🏷️</span>
<div style="flex: 1; min-width: 0;">
<div style="color: #e0e0e0; font-size: 13px;">${deps.escapeHtml(section.label)}</div>
<div style="color: #888; font-size: 11px;">Line ${section.row + 1} - ${section.endRow + 1}</div>
</div>
</div>
</div>`;
});
indexHtml += `
</div>
</div>`;
}
indexHtml += `
</div>
</div>`;
}
indexHtml += `</div>`;
}
// =========================================================================
// UNMARKED SECTIONS
// =========================================================================
if (unmarkedCount > 0) {
indexHtml += `<div>`;
indexHtml += `<div style="
color: #888;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
margin-bottom: 8px;
font-family: 'Segoe UI', sans-serif;
">📂 Unmarked Sections (${unmarkedCount})</div>`;
unmarked.forEach(item => {
indexHtml += `
<div class="index-item" data-row="${item.row}" data-is-marker="${item.type === 'marker'}" data-label="${deps.escapeHtml(item.label)}" style="
padding: 10px 12px;
margin: 4px 0;
background: #3d3d3d;
border-radius: 4px;
cursor: pointer;
transition: background 0.15s;
font-family: 'Segoe UI', monospace;
">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 16px;">${item.icon || '📝'}</span>
<div style="flex: 1; min-width: 0;">
<div style="color: #e0e0e0; font-weight: 500; font-size: 14px;">${deps.escapeHtml(item.label)}</div>
<div style="color: #888; font-size: 12px;">Line ${item.row + 1}</div>
</div>
</div>
</div>`;
});
indexHtml += `</div>`;
}
deps.showOverlay('Document Index', indexHtml);
// =========================================================================
// EVENT LISTENERS
// =========================================================================
setTimeout(() => {
const el = deps.getEl();
const contentEl = el.querySelector('#overlayContent');
if (!contentEl) return;
// Component collapse/expand
contentEl.querySelectorAll('.component-header').forEach(header => {
header.addEventListener('click', (e) => {
const component = header.dataset.component;
const content = contentEl.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
contentEl.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
contentEl.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);
}
deps.hideOverlay();
});
});
}, 50);
}
// =========================================================================
// NAVIGATION
// =========================================================================
function navigateToMarker(startRow, label) {
const globalEditorInstance = deps.getGlobalEditor();
if (!globalEditorInstance) return;
const session = globalEditorInstance.getSession();
const Range = ace.require('ace/range').Range;
const endRow = findMarkerEnd(startRow, label);
const range = new Range(
startRow,
0,
endRow,
session.getLine(endRow).length
);
globalEditorInstance.selection.setRange(range, false);
globalEditorInstance.scrollToLine(startRow, true, true, () => {});
globalEditorInstance.focus();
}
function navigateToRow(row) {
const globalEditorInstance = deps.getGlobalEditor();
if (!globalEditorInstance) return;
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 line = session.getLine(row);
const range = new Range(row, 0, row, line.length);
globalEditorInstance.selection.setRange(range, false);
}
globalEditorInstance.scrollToLine(row, true, true, () => {});
globalEditorInstance.focus();
}
// =========================================================================
// INITIALIZATION
// =========================================================================
function init(dependencies) {
deps = { ...deps, ...dependencies };
console.log("[editor_index.js v2] Initialized with dependencies");
}
// =========================================================================
// PUBLIC API
// =========================================================================
window.EditorIndex = {
init: init,
generateDocumentIndex: generateDocumentIndex,
findMarkerEnd: findMarkerEnd,
showIndexOverlay: showIndexOverlay,
navigateToMarker: navigateToMarker,
navigateToRow: navigateToRow,
parseMarkerName: parseMarkerName // Export for use by other modules
};
console.log("[editor_index.js v2] Module loaded successfully");
})();