From 9a28f042a13c4c4f7f086539e049db728bfecb0f Mon Sep 17 00:00:00 2001 From: Litruv Date: Sun, 9 Feb 2025 04:48:55 +1100 Subject: [PATCH] complete refactor --- index.js | 997 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 520 insertions(+), 477 deletions(-) diff --git a/index.js b/index.js index 48b4122..0c6402a 100644 --- a/index.js +++ b/index.js @@ -1,375 +1,424 @@ -let originalDocTitle = document.title; - -let markedPromise = new Promise((resolve) => { - if (typeof marked !== 'undefined') { - resolve(marked); - } else { - window.addEventListener('load', () => resolve(marked)); +class EventBus { + constructor() { + this.events = {}; } -}); - -async function initializeMarked() { - const marked = await markedPromise; - const renderer = new marked.Renderer(); - const originalLink = renderer.link.bind(renderer); - - renderer.code = (code, language) => { - let highlighted; - if (language && hljs.getLanguage(language)) { - highlighted = hljs.highlight(code, { language }).value; - language = language; - } else { - highlighted = hljs.highlightAuto(code).value; - language = ''; + on(event, callback) { + if (!this.events[event]) { + this.events[event] = []; } - return `
${highlighted}
`; - }; + this.events[event].push(callback); + return () => this.off(event, callback); + } - renderer.link = (href, title, text) => { - const isExternal = href.startsWith('http') || href.startsWith('https'); - const attrs = isExternal ? ' target="_blank" rel="noopener noreferrer"' : ''; - const link = originalLink(href, title, text); - if (!isExternal && href.startsWith('?')) { - return link.replace(/^ cb !== callback); + } + + emit(event, data) { + if (!this.events[event]) return; + this.events[event].forEach(callback => callback(data)); + } } -function createFileIndexItem(doc, container, level = 0) { - if (doc.type === 'folder') { - const folderDiv = document.createElement('div'); - folderDiv.className = 'folder' + (doc.defaultOpen !== false ? ' open' : ''); - folderDiv.dataset.path = doc.title; - folderDiv.style.paddingLeft = `${level * 0.8}rem`; +class IndexService { + findDocumentBySlug(documents, slug) { + for (const doc of documents) { + if (doc.slug === slug) return doc; + if (doc.type === 'folder') { + const found = this.findDocumentBySlug(doc.items, slug); + if (found) return found; + } + } + return null; + } - const folderHeader = document.createElement('div'); - folderHeader.className = 'folder-header'; + findDocumentByTitle(documents, title) { + for (const doc of documents) { + if (doc.type === 'folder') { + const found = this.findDocumentByTitle(doc.items, title); + if (found) return found; + } else if (doc.title === title || + doc.path.endsWith(title + '.md') || + doc.slug === title.toLowerCase().replace(/ /g, '-')) { + return doc; + } + } + return null; + } + + findParentFolders(documents, path, parentFolders = []) { + for (const doc of documents) { + if (doc.type === 'folder') { + const found = doc.items.find(item => { + if (item.path === path) return true; + if (item.type === 'folder') { + return this.findParentFolders([item], path).length > 0; + } + return false; + }); + + if (found) { + parentFolders.push(doc); + doc.items.forEach(item => { + if (item.type === 'folder') { + this.findParentFolders([item], path, parentFolders); + } + }); + } + } + } + return parentFolders; + } +} + +class DOMService { + constructor(eventBus) { + this.eventBus = eventBus; + this.elements = { + content: document.getElementById('document-content'), + outline: document.getElementById('document-outline'), + fileIndex: document.getElementById('file-index'), + titleText: document.querySelector('.title-text .page-title'), + leftSidebar: document.querySelector('.left-sidebar'), + menuButton: document.querySelector('.menu-button') + }; + } + + setupMobileMenu() { + this.elements.menuButton.addEventListener('click', () => + this.elements.leftSidebar.classList.toggle('show')); + + this.elements.content.addEventListener('click', () => + this.elements.leftSidebar.classList.remove('show')); + } + + setContent(html) { + this.elements.content.innerHTML = html; + this.elements.content.className = 'markdown-content'; + hljs.highlightAll(); + } + + setTitle(title) { + document.title = `${window.originalDocTitle} / ${title}`; + this.elements.titleText.textContent = title; + } + + setError(message) { + this.elements.content.innerHTML = `
${message}
`; + } + + createFileIndexItem(doc, container, level = 0) { + if (doc.type === 'folder') { + const folderDiv = document.createElement('div'); + folderDiv.className = 'folder' + (doc.defaultOpen !== false ? ' open' : ''); + folderDiv.dataset.path = doc.title; + folderDiv.style.paddingLeft = `${level * 0.8}rem`; + + const folderHeader = document.createElement('div'); + folderHeader.className = 'folder-header'; + + const iconClass = doc.icon || `fas fa-folder${doc.defaultOpen !== false ? '-open' : ''}`; + + folderHeader.innerHTML = doc.path ? + this.createFolderHeaderWithFile(iconClass, doc) : + this.createFolderHeaderBasic(iconClass, doc); + + this.setupFolderListeners(folderDiv, folderHeader, doc); + + const folderContent = document.createElement('div'); + folderContent.className = 'folder-content'; + doc.items.forEach(item => this.createFileIndexItem(item, folderContent, level + 1)); + + folderDiv.appendChild(folderHeader); + folderDiv.appendChild(folderContent); + container.appendChild(folderDiv); + } else { + this.createFileItem(doc, container, level); + } + } + + createFileItem(doc, container, level) { + const link = document.createElement('a'); + link.href = `?${doc.slug}`; + link.textContent = doc.title || doc.path.split('/').pop().replace('.md', ''); + link.dataset.path = doc.path; + link.style.paddingLeft = `${level * 0.8 + 1.2}rem`; - const iconClass = doc.icon || `fas fa-folder${doc.defaultOpen !== false ? '-open' : ''}`; + link.onclick = (e) => { + e.preventDefault(); + this.eventBus.emit('navigation:requested', { slug: doc.slug }); + history.pushState(null, '', link.href); + if (window.innerWidth <= 1000) { + this.elements.leftSidebar.classList.remove('show'); + } + }; - // Put file icon before folder icon - const headerContent = doc.path ? - `
- - - - -
- ${doc.title}` : - `
- -
- ${doc.title}`; - - folderHeader.innerHTML = headerContent; - folderDiv.appendChild(folderHeader); + container.appendChild(link); + } - const folderContent = document.createElement('div'); - folderContent.className = 'folder-content'; - doc.items.forEach(item => createFileIndexItem(item, folderContent, level + 1)); - folderDiv.appendChild(folderContent); + updateActiveDocument(path) { + this.elements.fileIndex.querySelectorAll('a').forEach(link => { + link.classList.toggle('active', link.dataset.path === path); + }); + } - // Make entire header toggle folder + createOutline(headings) { + this.elements.outline.innerHTML = ''; + const headingLinks = new Map(); + + headings.forEach(heading => { + if (!heading.id) { + heading.id = heading.textContent.toLowerCase() + .replace(/[^\w\s-]/g, '') + .replace(/\s+/g, '-'); + } + + const link = this.createOutlineLink(heading); + headingLinks.set(heading, link); + this.elements.outline.appendChild(link); + this.addHeadingFoldToggle(heading); + }); + + return headingLinks; + } + + createFolderHeaderWithFile(iconClass, doc) { + return `
+ + + + +
+ ${doc.title}`; + } + + createFolderHeaderBasic(iconClass, doc) { + return `
+ +
+ ${doc.title}`; + } + + setupFolderListeners(folderDiv, folderHeader, doc) { folderHeader.addEventListener('click', (e) => { - // Don't toggle if clicking the folder link if (e.target.closest('.folder-link')) { e.preventDefault(); - loadDocument(doc.path); - history.pushState({ - slug: doc.slug, - path: doc.path, - type: doc.type - }, '', `?${doc.slug}`); + this.eventBus.emit('navigation:requested', { slug: doc.slug }); + history.pushState(null, '', `?${doc.slug}`); if (window.innerWidth <= 1000) { - document.querySelector('.left-sidebar').classList.remove('show'); + this.elements.leftSidebar.classList.remove('show'); } return; } folderDiv.classList.toggle('open'); if (!doc.icon) { - folderHeader.querySelector('.folder-icon').classList.toggle('fa-folder-closed'); - folderHeader.querySelector('.folder-icon').classList.toggle('fa-folder-open'); + const icon = folderHeader.querySelector('.folder-icon'); + icon.classList.toggle('fa-folder-closed'); + icon.classList.toggle('fa-folder-open'); } }); + } - container.appendChild(folderDiv); - } else { + createOutlineLink(heading) { const link = document.createElement('a'); - link.href = `?${doc.slug}`; - const displayTitle = doc.title || doc.path.split('/').pop().replace('.md', ''); - link.textContent = displayTitle; - link.dataset.path = doc.path; - link.style.paddingLeft = `${level * 0.8 + 1.2}rem`; + link.href = `${window.location.pathname}${window.location.search}#${heading.id}`; + link.textContent = heading.textContent; + link.style.paddingLeft = (heading.tagName[1] * 15) + 'px'; link.onclick = (e) => { e.preventDefault(); - loadDocument(doc.path); - history.pushState({ - slug: doc.slug, - path: doc.path, - type: doc.type - }, '', link.href); - const leftSidebar = document.querySelector('.left-sidebar'); - if (window.innerWidth <= 1000) { - leftSidebar.classList.remove('show'); - } + history.pushState(null, '', link.href); + heading.scrollIntoView({ behavior: 'smooth', block: 'start' }); + heading.classList.remove('highlight'); + void heading.offsetWidth; + heading.classList.add('highlight'); }; - container.appendChild(link); - } -} - -function findParentFolders(documents, path, parentFolders = []) { - for (const doc of documents) { - if (doc.type === 'folder') { - const found = doc.items.find(item => { - if (item.path === path) return true; - if (item.type === 'folder') { - return findParentFolders([item], path).length > 0; - } - return false; - }); - - if (found) { - parentFolders.push(doc); - doc.items.forEach(item => { - if (item.type === 'folder') { - findParentFolders([item], path, parentFolders); - } - }); - } - } - } - return parentFolders; -} - -function findDocumentBySlug(documents, slug) { - for (const doc of documents) { - if (doc.slug === slug) { - return doc; - } - if (doc.type === 'folder') { - const found = findDocumentBySlug(doc.items, slug); - if (found) return found; - } - } - return null; -} - -async function loadIndex() { - try { - const response = await fetch('index.json'); - const data = await response.json(); - window._indexData = data; // Cache the index data - const fileIndex = document.getElementById('file-index'); - - fileIndex.innerHTML = ''; - - data.documents.forEach(doc => createFileIndexItem(doc, fileIndex)); - - const queryString = window.location.search; - if (queryString) { - const slug = queryString.substring(1); - const matchingDoc = findDocumentBySlug(data.documents, slug); - - if (matchingDoc) { - history.replaceState({ - slug, - path: matchingDoc.path, - type: matchingDoc.type - }, '', window.location.href); - - if (matchingDoc.type === 'folder') { - // If the folder has a path, load that document. Otherwise, load the first item. - if (matchingDoc.path) { - await loadDocument(matchingDoc.path); - } else if (matchingDoc.items && matchingDoc.items.length > 0) { - await loadDocument(matchingDoc.items[0].path); - } else { - // If the folder has no items, display a default message. - document.getElementById('document-content').innerHTML = - '
This folder is empty.
'; - } - } else { - await loadDocument(matchingDoc.path); - } - } else { - // If no matching doc is found, display an error message. - document.getElementById('document-content').innerHTML = - '
Document not found.
'; - } - } else { - const defaultDoc = findDocumentBySlug(data.documents, 'welcome'); - if (defaultDoc) { - await loadDocument(defaultDoc.path); - history.replaceState({ - slug: 'welcome', - path: defaultDoc.path, - type: defaultDoc.type - }, '', '?welcome'); - } - } - } catch (error) { - document.getElementById('document-content').innerHTML = - '
Failed to load documentation index.
'; - } -} - -window.addEventListener('popstate', async (event) => { - let slug; - - // First try to get slug from state - if (event.state && event.state.slug) { - slug = event.state.slug; - } else { - // Fall back to URL parsing if no state - slug = window.location.search.replace(/^\?/, ''); - } - - if (!slug) { - const defaultDoc = findDocumentBySlug(window._indexData.documents, 'welcome'); - if (defaultDoc) { - await loadDocument(defaultDoc.path); - } - return; - } - - const matchingDoc = findDocumentBySlug(window._indexData.documents, slug); - if (matchingDoc) { - if (matchingDoc.type === 'folder') { - if (matchingDoc.path) { - await loadDocument(matchingDoc.path); - } else if (matchingDoc.items && matchingDoc.items.length > 0) { - await loadDocument(matchingDoc.items[0].path); - } - } else { - await loadDocument(matchingDoc.path); - } - } -}); - -function extractMetadata(content) { - const lines = content.trim().split('\n'); - let metadata = {}; - let contentStart = 0; - - if (lines[0].trim() === '---') { - let endMetadata = -1; - for (let i = 1; i < lines.length; i++) { - if (lines[i].trim() === '---') { - endMetadata = i; - break; - } - const line = lines[i].trim(); - if (line) { - const match = line.match(/^([\w-]+):\s*(.*)$/); - if (match) { - metadata[match[1]] = match[2].trim(); + return link; + } + + addHeadingFoldToggle(heading) { + const toggleBtn = document.createElement('span'); + toggleBtn.innerHTML = ` + + `; + toggleBtn.style.cursor = 'pointer'; + toggleBtn.style.userSelect = 'none'; + toggleBtn.style.marginLeft = '0.5em'; + toggleBtn.style.display = 'inline-flex'; + toggleBtn.style.alignItems = 'center'; + + toggleBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const svg = toggleBtn.querySelector('svg'); + const isFolded = svg.style.transform === 'rotate(90deg)'; + svg.style.transform = isFolded ? 'rotate(0deg)' : 'rotate(90deg)'; + + let next = heading.nextElementSibling; + const currentLevel = parseInt(heading.tagName[1]); + + while (next) { + if (!/^H[1-6]$/.test(next.tagName)) { + next.style.display = isFolded ? 'none' : ''; + next = next.nextElementSibling; + } else { + const nextLevel = parseInt(next.tagName[1]); + if (nextLevel <= currentLevel) break; + next.style.display = isFolded ? 'none' : ''; + next = next.nextElementSibling; } } - } - - if (endMetadata !== -1) { - contentStart = endMetadata + 1; - } + }); + + heading.appendChild(toggleBtn); } - - return { - metadata, - content: lines.slice(contentStart).join('\n').trim() - }; } -function findDocumentByTitle(documents, title) { - for (const doc of documents) { - if (doc.type === 'folder') { - const found = findDocumentByTitle(doc.items, title); - if (found) return found; - } else { - if (doc.title === title) { - return doc; - } else if (doc.path.endsWith(title + '.md')) { - return doc; - } else if (doc.slug === title.toLowerCase().replace(/ /g, '-')) { - return doc; - } - } +class DocumentService { + constructor(eventBus, indexService) { + this.eventBus = eventBus; + this.indexService = indexService; + this.markedPromise = this.initializeMarked(); } - return null; -} -async function loadDocument(path) { - try { - const documentContent = document.getElementById('document-content'); - const documentOutline = document.getElementById('document-outline'); - const basePath = path.substring(0, path.lastIndexOf('/')); - - document.querySelectorAll('#file-index a').forEach(link => { - link.classList.toggle('active', link.dataset.path === path); - - if (link.dataset.path === path) { - const response = fetch('index.json') - .then(res => res.json()) - .then(data => { - const parentFolders = findParentFolders(data.documents, path); - parentFolders.forEach(folder => { - const folderDiv = document.querySelector(`.folder[data-path="${folder.title}"]`); - if (folderDiv) { - folderDiv.classList.add('open'); - const icon = folderDiv.querySelector('.folder-icon'); - icon.classList.remove('fa-folder-closed'); - icon.classList.add('fa-folder-open'); - } - }); - }); + async initializeMarked() { + const marked = await new Promise((resolve) => { + if (typeof window.marked !== 'undefined') { + resolve(window.marked); + } else { + window.addEventListener('load', () => resolve(window.marked)); } }); - const [response, marked] = await Promise.all([ - fetch(path), - initializeMarked() - ]); + const renderer = new marked.Renderer(); + this.setupRenderer(renderer); - let rawContent = await response.text(); - const { metadata, content } = extractMetadata(rawContent); - - const indexResponse = await fetch('index.json'); - const indexData = await indexResponse.json(); - const docEntry = findDocumentByPath(indexData.documents, path); - - const titleContent = metadata.title || (docEntry && docEntry.title) || path.split('/').pop().replace('.md', ''); - - document.title = `${originalDocTitle} / ${titleContent}`; - document.querySelector('.title-text .page-title').textContent = titleContent; + marked.setOptions({ + breaks: true, + gfm: true, + renderer: renderer + }); - let processedContent = content.replace(/\[\[(.*?)\]\]/g, (match, linkText) => { + return marked; + } + + setupRenderer(renderer) { + const originalLink = renderer.link.bind(renderer); + + renderer.code = this.renderCode; + renderer.link = (href, title, text) => { + const isExternal = href.startsWith('http'); + const attrs = isExternal ? ' target="_blank" rel="noopener noreferrer"' : ''; + const link = originalLink(href, title, text); + if (!isExternal && href.startsWith('?')) { + return link.replace(/^${highlighted}`; + } + + extractMetadata(content) { + const lines = content.trim().split('\n'); + let metadata = {}; + let contentStart = 0; + + if (lines[0].trim() === '---') { + let endMetadata = lines.findIndex((line, index) => index > 0 && line.trim() === '---'); + if (endMetadata !== -1) { + metadata = Object.fromEntries( + lines.slice(1, endMetadata) + .map(line => line.match(/^([\w-]+):\s*(.*)$/)) + .filter(Boolean) + .map(([, key, value]) => [key, value.trim()]) + ); + contentStart = endMetadata + 1; + } + } + + return { + metadata, + content: lines.slice(contentStart).join('\n').trim() + }; + } + + async loadDocument(path) { + try { + const [response, marked] = await Promise.all([ + fetch(path), + this.markedPromise + ]); + + let rawContent = await response.text(); + const { metadata, content } = this.extractMetadata(rawContent); + const basePath = path.substring(0, path.lastIndexOf('/')); + const indexDoc = this.findDocInIndex(path); + + let processedContent = this.processWikiLinks(content); + processedContent = this.processImages(processedContent, basePath); + const titleContent = metadata.title || indexDoc?.title || path.split('/').pop().replace('.md', ''); + processedContent = this.ensureTitle(processedContent, titleContent); + + return { + content: processedContent, + metadata, + marked, + title: titleContent + }; + } catch (error) { + throw new Error('Failed to load document'); + } + } + + findDocInIndex(path) { + let doc = window._indexData.documents.find(d => d.path === path); + + if (!doc) { + for (const d of window._indexData.documents) { + if (d.type === 'folder' && d.items) { + doc = d.items.find(item => item.path === path); + if (doc) break; + } + } + } + return doc; + } + + ensureTitle(content, title) { + content = content.replace(/^#\s+.*$/m, '').trim(); + return `# ${title}\n\n${content}`; + } + + processWikiLinks(content) { + return content.replace(/\[\[(.*?)\]\]/g, (match, linkText) => { const [targetTitle, displayText] = linkText.split('|').map(s => s.trim()); if (targetTitle.match(/\.(png|jpg|jpeg|gif|mp4|webm)$/i)) { return match; } - const doc = findDocumentByTitle(indexData.documents, targetTitle); - if (doc) { - return `[${displayText || doc.title}](?${doc.slug})`; - } else { - return match; - } + const doc = this.indexService.findDocumentByTitle( + window._indexData.documents, + targetTitle + ); + return doc ? `[${displayText || doc.title}](?${doc.slug})` : match; }); + } - processedContent = `# ${titleContent}\n\n${processedContent}`; - - let finalContent = processedContent.replace(/!\[\[(.*?)\]\]/g, (match, filename) => { + processImages(content, basePath) { + return content.replace(/!\[\[(.*?)\]\]/g, (match, filename) => { const mediaPath = `${basePath}/images/${filename}`; if (filename.toLowerCase().endsWith('.mp4')) { @@ -381,169 +430,163 @@ async function loadDocument(path) { return `\n![${filename}](${mediaPath})\n\n`; }); + } +} - // Track current page for navigation - window._currentPath = path; +class NavigationService { + constructor(eventBus, documentService) { + this.eventBus = eventBus; + this.documentService = documentService; + this.setupEventListeners(); + } - // Add Discord-style underline support: __text__ -> text - finalContent = finalContent.replace(/__(.*?)__/g, '$1'); + setupEventListeners() { + window.addEventListener('popstate', async () => { + const slug = new URLSearchParams(window.location.search).toString() + .replace(/^=/, '').replace(/^\?/, ''); + this.eventBus.emit('navigation:requested', { slug }); + }); - document.querySelector('.title-text .page-title').textContent = titleContent; - - documentContent.className = 'markdown-content'; - documentContent.innerHTML = marked.parse(finalContent); - hljs.highlightAll(); - - documentOutline.innerHTML = ''; - const headings = documentContent.querySelectorAll('h2, h3, h4, h5, h6'); - const headingLinks = new Map(); - - headings.forEach(heading => { - if (!heading.id) { - heading.id = heading.textContent.toLowerCase() - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-'); - } - - const link = document.createElement('a'); - link.href = `${window.location.pathname}${window.location.search}#${heading.id}`; - link.textContent = heading.textContent; - link.style.paddingLeft = (heading.tagName[1] * 15) + 'px'; - headingLinks.set(heading, link); - - heading.style.scrollMarginTop = 'var(--title-bar-height)'; - - link.onclick = (e) => { + document.addEventListener('click', async (e) => { + const target = e.target.closest('a[data-internal="true"]'); + if (target) { e.preventDefault(); - history.pushState(null, '', link.href); - if (heading) { - heading.scrollIntoView({ behavior: 'smooth', block: 'start' }); - heading.classList.remove('highlight'); - void heading.offsetWidth; - heading.classList.add('highlight'); - } else { - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - }; - - documentOutline.appendChild(link); - - /* Add header folding toggle */ - const toggleBtn = document.createElement('span'); - toggleBtn.innerHTML = ` - - `; - toggleBtn.style.cursor = 'pointer'; - toggleBtn.style.userSelect = 'none'; - toggleBtn.style.marginLeft = '0.5em'; - toggleBtn.style.display = 'inline-flex'; - toggleBtn.style.alignItems = 'center'; - heading.appendChild(toggleBtn); - - toggleBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const svg = toggleBtn.querySelector('svg'); - const isFolded = svg.style.transform === 'rotate(90deg)'; - svg.style.transform = isFolded ? 'rotate(0deg)' : 'rotate(90deg)'; - const currentLevel = parseInt(heading.tagName[1]); - let next = heading.nextElementSibling; - while (next) { - if (!/^H[1-6]$/.test(next.tagName)) { - next.style.display = isFolded ? 'none' : ''; - next = next.nextElementSibling; - } else { - const nextLevel = parseInt(next.tagName[1]); - if (nextLevel <= currentLevel) { - break; - } - next.style.display = isFolded ? 'none' : ''; - next = next.nextElementSibling; - } - } - }); + const slug = target.href.split('?').pop(); + history.pushState({ slug }, '', target.href); + this.eventBus.emit('navigation:requested', { slug }); + } + }); + } +} + +class Documentation { + constructor() { + this.eventBus = new EventBus(); + this.indexService = new IndexService(); + this.domService = new DOMService(this.eventBus); + this.documentService = new DocumentService(this.eventBus, this.indexService); + this.navigationService = new NavigationService(this.eventBus, this.documentService); + + this.setupEventListeners(); + } + + setupEventListeners() { + this.eventBus.on('navigation:requested', async ({ slug }) => { + slug = slug.replace(/^[?=]/, ''); + await this.loadDocumentBySlug(slug); }); - const observer = new IntersectionObserver((entries) => { - const visibleHeadings = entries - .filter(entry => entry.isIntersecting) - .sort((a, b) => a.target.offsetTop - b.target.offsetTop); - if (visibleHeadings.length) { - documentOutline.querySelectorAll('a').forEach(a => a.classList.remove('active')); - const topHeading = visibleHeadings[0].target; - const link = headingLinks.get(topHeading); - if (link) link.classList.add('active'); - } - }, { - rootMargin: '-48px 0px -60% 0px', - threshold: [0, 0.25, 0.5, 0.75, 1] + this.eventBus.on('document:load', async ({ path }) => { + await this.loadDocument(path); }); + window.addEventListener('load', () => { + this.domService.setupMobileMenu(); + this.initialize(); + }); + + document.addEventListener('click', async (e) => { + const target = e.target.closest('a[data-internal="true"]'); + if (target) { + e.preventDefault(); + const slug = target.href.split('?').pop(); + history.pushState(null, '', target.href); + await this.loadDocumentBySlug(slug); + } + }); + } + + async initialize() { + try { + const response = await fetch('index.json'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + this.indexData = data; + window._indexData = data; + + this.domService.elements.fileIndex.innerHTML = ''; + this.indexData.documents.forEach(doc => + this.domService.createFileIndexItem(doc, this.domService.elements.fileIndex)); + + const urlParams = new URLSearchParams(window.location.search); + const slug = urlParams.get('') || urlParams.get('page') || 'welcome'; + await this.loadDocumentBySlug(slug); + } catch (error) { + this.domService.setError(`Failed to load documentation index: ${error.message}`); + } + } + + async loadDocumentBySlug(slug) { + const doc = this.indexService.findDocumentBySlug(this.indexData.documents, slug); + + if (!doc) { + this.domService.setError(`Document not found: ${slug}`); + return; + } + + if (doc.type === 'folder') { + if (doc.path) { + await this.loadDocument(doc.path); + } else if (doc.items?.length > 0) { + await this.loadDocument(doc.items[0].path); + } else { + this.domService.setError('This folder is empty.'); + } + return; + } + + await this.loadDocument(doc.path); + } + + async loadDocument(path) { + try { + const { content, metadata, marked, title } = await this.documentService.loadDocument(path); + + this.domService.setTitle(title); + this.domService.setContent(marked.parse(content)); + this.domService.updateActiveDocument(path); + + const headings = document.querySelectorAll('h2, h3, h4, h5, h6'); + const headingLinks = this.domService.createOutline(headings); + + this.setupScrollObserver(headings, headingLinks); + + window._currentPath = path; + + window.scrollTo({ top: 0, behavior: 'smooth' }); + } catch (error) { + this.domService.setError('Error loading document. Please try again.'); + } + } + + setupScrollObserver(headings, headingLinks) { + const observer = new IntersectionObserver( + (entries) => { + const visibleHeadings = entries + .filter(entry => entry.isIntersecting) + .sort((a, b) => a.target.offsetTop - b.target.offsetTop); + + if (visibleHeadings.length) { + this.domService.elements.outline + .querySelectorAll('a') + .forEach(a => a.classList.remove('active')); + + const link = headingLinks.get(visibleHeadings[0].target); + if (link) link.classList.add('active'); + } + }, + { + rootMargin: '-48px 0px -60% 0px', + threshold: [0, 0.25, 0.5, 0.75, 1] + } + ); + headings.forEach(heading => observer.observe(heading)); - - return () => observer.disconnect(); - - } catch (error) { - document.getElementById('document-content').innerHTML = - '
Error loading document. Please try again.
'; + return observer; } } -function findDocumentByPath(documents, path) { - for (const doc of documents) { - if (doc.type === 'folder') { - const found = findDocumentByPath(doc.items, path); - if (found) return found; - } else if (doc.path === path) { - return doc; - } - } - return null; -} - -function fixImagePaths(basePath, content) { - return content.replace(/!\[\[(.*?)\]\]/g, (match, filename) => { - const imagePath = basePath + '/images/' + filename; - return `![${filename}](${imagePath})`; - }); -} - -function setupMobileMenu() { - const menuButton = document.querySelector('.menu-button'); - const leftSidebar = document.querySelector('.left-sidebar'); - const content = document.querySelector('.content'); - - menuButton.addEventListener('click', () => { - leftSidebar.classList.toggle('show'); - }); - - content.addEventListener('click', () => { - if (leftSidebar.classList.contains('show')) { - leftSidebar.classList.remove('show'); - } - }); -} - -window.addEventListener('load', () => { - setupMobileMenu(); - loadIndex().catch(error => { - document.getElementById('document-content').innerHTML = - '
Failed to load documentation. Please try refreshing the page.
'; - }); -}); - -document.addEventListener('click', async (e) => { - const target = e.target.closest('a[data-internal="true"]'); - if (target) { - e.preventDefault(); - const slug = target.href.split('?').pop(); - const matchingDoc = findDocumentBySlug(window._indexData.documents, slug); - if (matchingDoc) { - await loadDocument(matchingDoc.path); - history.pushState({ - slug, - path: matchingDoc.path, - type: matchingDoc.type - }, '', target.href); - } - } -}); +window.originalDocTitle = document.title; +const docs = new Documentation();