class MarkdownReader { constructor(rootFolder = '../../', backend = 'php') { this.rootFolder = rootFolder; this.backend = backend; // 'php', 'node', or 'manual' this.files = []; this.currentFile = null; } // Initialize the reader async init() { await this.scanFiles(); this.renderFileList(); } // Scan files based on backend type async scanFiles() { switch(this.backend) { case 'php': await this.scanFilesWithPHP(); break; case 'node': await this.scanFilesWithNode(); break; case 'manual': default: await this.scanFilesManually(); break; } } // Scan files using PHP backend async scanFilesWithPHP() { try { const response = await fetch('Core/php/list-files.php'); const data = await response.json(); if (data.success) { this.files = data.files; console.log(`Loaded ${data.count} markdown files using PHP`); } else { console.error('PHP Error:', data.error); this.files = []; } } catch (error) { console.error('Error fetching file list from PHP:', error); this.files = []; } } // Scan files using Node.js backend async scanFilesWithNode() { try { const response = await fetch('/api/list-markdown-files'); const data = await response.json(); this.files = data.files || []; console.log(`Loaded ${this.files.length} markdown files using Node.js`); } catch (error) { console.error('Error fetching file list from Node.js:', error); this.files = []; } } // Manual file list (fallback method) async scanFilesManually() { // Manually define your markdown files here this.files = [ 'files/document1.md', 'files/document2.md', 'files/subfolder/document3.md', // Add more files here... ]; console.log(`Loaded ${this.files.length} markdown files manually`); } // Render the list of files renderFileList() { const fileList = document.getElementById('fileList'); fileList.innerHTML = ''; if (this.files.length === 0) { fileList.innerHTML = '
  • No markdown files found. Check console for errors.
  • '; return; } // Group files by directory const grouped = this.groupFilesByDirectory(this.files); // Render grouped files this.renderGroupedFiles(fileList, grouped); } // Group files by their directory groupFilesByDirectory(files) { const grouped = {}; files.forEach(file => { const parts = file.split('/'); const fileName = parts[parts.length - 1]; const directory = parts.slice(0, -1).join('/') || 'root'; if (!grouped[directory]) { grouped[directory] = []; } grouped[directory].push({ path: file, name: fileName.replace('.md', '') }); }); return grouped; } // Render grouped files with directory headers renderGroupedFiles(container, grouped) { Object.keys(grouped).sort().forEach(directory => { // Add directory header if not root if (directory !== 'root' && Object.keys(grouped).length > 1) { const header = document.createElement('li'); header.className = 'directory-header'; header.textContent = '📁 ' + directory.replace('files/', ''); container.appendChild(header); } // Add files in this directory grouped[directory].forEach(file => { const li = document.createElement('li'); const a = document.createElement('a'); a.href = '#'; a.textContent = file.name; a.dataset.file = file.path; a.title = file.path; a.addEventListener('click', (e) => { e.preventDefault(); this.loadFile(file.path); // Update active state document.querySelectorAll('.file-list a').forEach(link => { link.classList.remove('active'); }); a.classList.add('active'); }); li.appendChild(a); container.appendChild(li); }); }); } // Extract filename from path getFileName(path) { const parts = path.split('/'); return parts[parts.length - 1].replace('.md', ''); } // Load and display a markdown file async loadFile(filePath) { const content = document.getElementById('content'); content.innerHTML = '
    Loading...
    '; content.classList.remove('empty'); try { const response = await fetch(filePath); if (!response.ok) throw new Error('File not found'); const markdown = await response.text(); const html = this.parseMarkdown(markdown); content.innerHTML = html; this.currentFile = filePath; // Scroll to top of content content.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (error) { content.innerHTML = `

    Error loading file: ${error.message}

    `; } } parseMarkdown(markdown) { // STEP 1: Extract and protect code blocks FIRST const codeBlocks = []; let codeBlockIndex = 0; let html = markdown.replace(/```(\w+)?\n?([\s\S]*?)```/g, (match, lang, code) => { const language = lang || 'text'; const placeholder = `___CODE_BLOCK_${codeBlockIndex}___`; const escapedCode = this.escapeHtml(code); const codeId = 'code-' + Math.random().toString(36).substr(2, 9); codeBlocks.push(`
    ${escapedCode}
    `); codeBlockIndex++; return placeholder; }); // STEP 1.5: Extract and protect tables (they're now outside code blocks) const tables = []; let tableIndex = 0; const lines = html.split('\n'); const processedLines = []; let currentTable = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Skip code block placeholders entirely if (line.trim().includes('___CODE_BLOCK_')) { // Save any pending table first if (currentTable.length > 0) { tables.push(this.processTable(currentTable)); processedLines.push(`___TABLE_${tableIndex}___`); tableIndex++; currentTable = []; } processedLines.push(line); continue; } // Check if this is a table row if (/^\|(.+)\|$/.test(line.trim())) { currentTable.push(line.trim()); } else { // Not a table line - save pending table if exists if (currentTable.length > 0) { tables.push(this.processTable(currentTable)); processedLines.push(`___TABLE_${tableIndex}___`); tableIndex++; currentTable = []; } processedLines.push(line); } } // Handle table at end of document if (currentTable.length > 0) { tables.push(this.processTable(currentTable)); processedLines.push(`___TABLE_${tableIndex}___`); } html = processedLines.join('\n'); // STEP 2: Process inline code html = html.replace(/`([^`]+)`/g, '$1'); // STEP 3: Process everything else html = html.replace(/^###### (.*$)/gim, (match, content) => { const id = this.generateId(content); return `
    ${content}
    `; }); html = html.replace(/^##### (.*$)/gim, (match, content) => { const id = this.generateId(content); return `
    ${content}
    `; }); html = html.replace(/^#### (.*$)/gim, (match, content) => { const id = this.generateId(content); return `

    ${content}

    `; }); html = html.replace(/^### (.*$)/gim, (match, content) => { const id = this.generateId(content); return `

    ${content}

    `; }); html = html.replace(/^## (.*$)/gim, (match, content) => { const id = this.generateId(content); return `

    ${content}

    `; }); html = html.replace(/^# (.*$)/gim, (match, content) => { const id = this.generateId(content); return `

    ${content}

    `; }); // Bold, italic, strikethrough html = html.replace(/\*\*\*(.*?)\*\*\*/g, '$1'); html = html.replace(/\*\*(.*?)\*\*/g, '$1'); html = html.replace(/\*(.*?)\*/g, '$1'); html = html.replace(/~~(.*?)~~/g, '$1'); // Links html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // Images // html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1'); // Blockquotes html = html.replace(/^> (.*$)/gim, '
    $1
    '); // Horizontal rules html = html.replace(/^---$/gim, '
    '); html = html.replace(/^\*\*\*$/gim, '
    '); // Task lists html = html.replace(/^\- \[ \] (.*$)/gim, ''); html = html.replace(/^\- \[x\] (.*$)/gim, ''); // Lists (unordered and ordered) html = html.replace(/^\* (.*$)/gim, '
  • $1
  • ___NEWLINE___'); html = html.replace(/^\- (.*$)/gim, '
  • $1
  • ___NEWLINE___'); html = html.replace(/^\d+\. (.*$)/gim, '
  • $1
  • ___NEWLINE___'); html = html.replace(/((?:
  • .*<\/li>___NEWLINE___)+)/g, (match) => { const cleanMatch = match.replace(/___NEWLINE___/g, ''); const lines = markdown.split('\n'); const firstListLine = lines.find(line => /^\* /.test(line.trim()) || /^\- /.test(line.trim()) || /^\d+\. /.test(line.trim()) ); if (firstListLine && /^\d+\./.test(firstListLine.trim())) { return `
      ${cleanMatch}
    `; } return ``; }); // Line breaks html = html.replace(/\n\n/g, '

    '); html = html.replace(/\n/g, '
    '); html = `

    ${html}

    `; // Clean up: Remove
    tags after closing tags html = html.replace(/(<\/li>)
    /g, '$1'); html = html.replace(/(<\/ul>)
    /g, '$1'); html = html.replace(/(<\/ol>)
    /g, '$1'); html = html.replace(/(<\/tr>)
    /g, '$1'); html = html.replace(/(<\/table>)
    /g, '$1'); html = html.replace(/(<\/h[1-6]>)
    /g, '$1'); html = html.replace(/(
    )
    /g, '$1'); html = html.replace(/(<\/blockquote>)
    /g, '$1'); // Clean up empty paragraphs html = html.replace(/

    <\/p>/g, ''); html = html.replace(/

    ()<\/p>/g, '$1'); html = html.replace(/

    ()/g, '$1'); html = html.replace(/(<\/table>)<\/p>/g, '$1'); html = html.replace(/

    (

    ' + cells.map(cell => ``).join('') + '' ).join('\n'); return `
    ${cell}
    ${bodyRows}
    `; } // Has header const headerCells = rows[sepIndex - 1]; const header = '' + headerCells.map(cell => `${cell}`).join('') + ''; const bodyRows = rows.slice(sepIndex + 1).map(cells => '' + cells.map(cell => `${cell}`).join('') + '' ).join('\n'); return `\n${header}\n${bodyRows}\n
    `; } escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, m => map[m]); } // Generate ID from text for anchor links generateId(text) { return text.toLowerCase() .replace(/[^\w\s-]/g, '') .replace(/\s+/g, '-') .replace(/--+/g, '-') .trim(); } // Escape HTML special characters escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, m => map[m]); } // Merge adjacent tags mergeAdjacentTags(html, tag) { const regex = new RegExp(`\\s*<${tag}>`, 'g'); return html.replace(regex, '\n'); } // Wrap list items in ul/ol tags wrapLists(html) { // Wrap task lists html = html.replace(/(

  • [\s\S]*?<\/li>(?:\s*(?=
  • ))*)/g, ''); // Wrap regular lists html = html.replace(/(
  • (?:(?!
  • )[\s\S])*?<\/li>(?:\s*(?=
  • (?!.*task-list)))*)/g, (match) => { return ``; }); // Clean up nested ul tags html = html.replace(/<\/ul>\s*