323 lines
9.3 KiB
JavaScript
323 lines
9.3 KiB
JavaScript
|
|
// Viewer.js - Document viewer functionality
|
||
|
|
|
||
|
|
// Configuration
|
||
|
|
const DOC_MAP = {
|
||
|
|
'USER_GUIDE': '/docs/USER_GUIDE.md',
|
||
|
|
'E2E_TESTING_GUIDE': '/docs/E2E_TESTING_GUIDE.md',
|
||
|
|
'IMPLEMENTATION_COMPLETE': '/docs/IMPLEMENTATION_COMPLETE.md',
|
||
|
|
'ARCHITECTURE_GUIDE': '/docs/ARCHITECTURE_GUIDE.md',
|
||
|
|
'QUICK_START': '/docs/QUICK_START.md',
|
||
|
|
'DOCUMENTATION_INDEX': '/docs/DOCUMENTATION_INDEX.md',
|
||
|
|
'IMPLEMENTATION_SUMMARY': '/docs/IMPLEMENTATION_SUMMARY.md'
|
||
|
|
};
|
||
|
|
|
||
|
|
let currentDoc = null;
|
||
|
|
let sidebarOpen = true;
|
||
|
|
|
||
|
|
// Initialize on page load
|
||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
||
|
|
initViewer();
|
||
|
|
setupEventListeners();
|
||
|
|
loadDocumentFromURL();
|
||
|
|
});
|
||
|
|
|
||
|
|
function initViewer() {
|
||
|
|
// Configure marked.js
|
||
|
|
if (typeof marked !== 'undefined') {
|
||
|
|
marked.setOptions({
|
||
|
|
breaks: true,
|
||
|
|
gfm: true,
|
||
|
|
highlight: function(code, lang) {
|
||
|
|
if (typeof hljs !== 'undefined' && lang && hljs.getLanguage(lang)) {
|
||
|
|
try {
|
||
|
|
return hljs.highlight(code, { language: lang }).value;
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Highlight error:', err);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return code;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function setupEventListeners() {
|
||
|
|
// Sidebar toggle
|
||
|
|
const toggleBtn = document.getElementById('toggle-sidebar');
|
||
|
|
if (toggleBtn) {
|
||
|
|
toggleBtn.addEventListener('click', toggleSidebar);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Document selector
|
||
|
|
const docSelector = document.getElementById('doc-selector');
|
||
|
|
if (docSelector) {
|
||
|
|
docSelector.addEventListener('change', function() {
|
||
|
|
if (this.value) {
|
||
|
|
loadDocument(this.value);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Print button
|
||
|
|
const printBtn = document.getElementById('print-doc');
|
||
|
|
if (printBtn) {
|
||
|
|
printBtn.addEventListener('click', function() {
|
||
|
|
window.print();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Download button
|
||
|
|
const downloadBtn = document.getElementById('download-doc');
|
||
|
|
if (downloadBtn) {
|
||
|
|
downloadBtn.addEventListener('click', downloadCurrentDoc);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sidebar navigation links
|
||
|
|
document.querySelectorAll('.sidebar-nav .nav-link').forEach(link => {
|
||
|
|
link.addEventListener('click', function(e) {
|
||
|
|
e.preventDefault();
|
||
|
|
const url = this.getAttribute('href');
|
||
|
|
const params = new URLSearchParams(url.split('?')[1]);
|
||
|
|
const doc = params.get('doc');
|
||
|
|
if (doc) {
|
||
|
|
loadDocument(doc);
|
||
|
|
updateURL(doc);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function loadDocumentFromURL() {
|
||
|
|
const params = new URLSearchParams(window.location.search);
|
||
|
|
const doc = params.get('doc');
|
||
|
|
if (doc && DOC_MAP[doc]) {
|
||
|
|
loadDocument(doc);
|
||
|
|
} else {
|
||
|
|
showError('No document specified. Please select a document from the navigation.');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadDocument(docKey) {
|
||
|
|
const docPath = DOC_MAP[docKey];
|
||
|
|
if (!docPath) {
|
||
|
|
showError(`Document "${docKey}" not found`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
currentDoc = docKey;
|
||
|
|
showLoading();
|
||
|
|
hideError();
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch(docPath);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const markdown = await response.text();
|
||
|
|
renderMarkdown(markdown);
|
||
|
|
generateTOC();
|
||
|
|
updateActiveNav(docKey);
|
||
|
|
updateDocSelector(docKey);
|
||
|
|
updateURL(docKey);
|
||
|
|
|
||
|
|
// Scroll to top
|
||
|
|
window.scrollTo(0, 0);
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error loading document:', error);
|
||
|
|
showError(`Failed to load document: ${error.message}`);
|
||
|
|
} finally {
|
||
|
|
hideLoading();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderMarkdown(markdown) {
|
||
|
|
const contentDiv = document.getElementById('content');
|
||
|
|
if (!contentDiv) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Parse markdown to HTML
|
||
|
|
const html = marked.parse(markdown);
|
||
|
|
|
||
|
|
// Sanitize HTML using DOMPurify if available, otherwise use trusted content
|
||
|
|
const safeHTML = (typeof DOMPurify !== 'undefined')
|
||
|
|
? DOMPurify.sanitize(html, { ADD_ATTR: ['target'] })
|
||
|
|
: html;
|
||
|
|
|
||
|
|
contentDiv.innerHTML = safeHTML;
|
||
|
|
|
||
|
|
// Syntax highlighting for code blocks
|
||
|
|
if (typeof hljs !== 'undefined') {
|
||
|
|
contentDiv.querySelectorAll('pre code').forEach((block) => {
|
||
|
|
hljs.highlightElement(block);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Make external links open in new tab
|
||
|
|
contentDiv.querySelectorAll('a[href^="http"]').forEach(link => {
|
||
|
|
link.setAttribute('target', '_blank');
|
||
|
|
link.setAttribute('rel', 'noopener noreferrer');
|
||
|
|
});
|
||
|
|
|
||
|
|
// Add IDs to headings for anchor links
|
||
|
|
contentDiv.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach((heading, index) => {
|
||
|
|
if (!heading.id) {
|
||
|
|
heading.id = `heading-${index}`;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error rendering markdown:', error);
|
||
|
|
showError(`Failed to render document: ${error.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function generateTOC() {
|
||
|
|
const tocContent = document.getElementById('toc-content');
|
||
|
|
const content = document.getElementById('content');
|
||
|
|
|
||
|
|
if (!tocContent || !content) return;
|
||
|
|
|
||
|
|
const headings = content.querySelectorAll('h2, h3');
|
||
|
|
if (headings.length === 0) {
|
||
|
|
tocContent.textContent = 'No headings found';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clear existing content
|
||
|
|
tocContent.innerHTML = '';
|
||
|
|
|
||
|
|
// Create TOC links using DOM methods
|
||
|
|
headings.forEach(heading => {
|
||
|
|
const level = heading.tagName.toLowerCase();
|
||
|
|
const text = heading.textContent;
|
||
|
|
const id = heading.id || `heading-${text.replace(/\s+/g, '-').toLowerCase()}`;
|
||
|
|
heading.id = id;
|
||
|
|
|
||
|
|
const link = document.createElement('a');
|
||
|
|
link.href = `#${id}`;
|
||
|
|
link.textContent = text;
|
||
|
|
|
||
|
|
if (level === 'h3') {
|
||
|
|
link.style.marginLeft = '1rem';
|
||
|
|
}
|
||
|
|
|
||
|
|
link.addEventListener('click', function(e) {
|
||
|
|
e.preventDefault();
|
||
|
|
const target = document.getElementById(id);
|
||
|
|
if (target) {
|
||
|
|
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
tocContent.appendChild(link);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggleSidebar() {
|
||
|
|
const sidebar = document.querySelector('.sidebar');
|
||
|
|
const icon = document.getElementById('sidebar-icon');
|
||
|
|
|
||
|
|
if (sidebar) {
|
||
|
|
sidebarOpen = !sidebarOpen;
|
||
|
|
sidebar.classList.toggle('active');
|
||
|
|
if (icon) {
|
||
|
|
icon.textContent = sidebarOpen ? '☰' : '✕';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateActiveNav(docKey) {
|
||
|
|
document.querySelectorAll('.sidebar-nav .nav-link').forEach(link => {
|
||
|
|
link.classList.remove('active');
|
||
|
|
const url = link.getAttribute('href');
|
||
|
|
if (url && url.includes(`doc=${docKey}`)) {
|
||
|
|
link.classList.add('active');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateDocSelector(docKey) {
|
||
|
|
const selector = document.getElementById('doc-selector');
|
||
|
|
if (selector) {
|
||
|
|
selector.value = docKey;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateURL(docKey) {
|
||
|
|
const newURL = `${window.location.pathname}?doc=${docKey}`;
|
||
|
|
window.history.pushState({ doc: docKey }, '', newURL);
|
||
|
|
}
|
||
|
|
|
||
|
|
function downloadCurrentDoc() {
|
||
|
|
if (!currentDoc) {
|
||
|
|
alert('No document loaded');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const docPath = DOC_MAP[currentDoc];
|
||
|
|
const filename = docPath.split('/').pop();
|
||
|
|
|
||
|
|
fetch(docPath)
|
||
|
|
.then(response => response.text())
|
||
|
|
.then(text => {
|
||
|
|
const blob = new Blob([text], { type: 'text/markdown' });
|
||
|
|
const url = URL.createObjectURL(blob);
|
||
|
|
const a = document.createElement('a');
|
||
|
|
a.href = url;
|
||
|
|
a.download = filename;
|
||
|
|
document.body.appendChild(a);
|
||
|
|
a.click();
|
||
|
|
document.body.removeChild(a);
|
||
|
|
URL.revokeObjectURL(url);
|
||
|
|
})
|
||
|
|
.catch(error => {
|
||
|
|
console.error('Download error:', error);
|
||
|
|
alert('Failed to download document');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function showLoading() {
|
||
|
|
const loading = document.getElementById('loading');
|
||
|
|
const content = document.getElementById('content');
|
||
|
|
|
||
|
|
if (loading) loading.style.display = 'block';
|
||
|
|
if (content) content.style.display = 'none';
|
||
|
|
}
|
||
|
|
|
||
|
|
function hideLoading() {
|
||
|
|
const loading = document.getElementById('loading');
|
||
|
|
const content = document.getElementById('content');
|
||
|
|
|
||
|
|
if (loading) loading.style.display = 'none';
|
||
|
|
if (content) content.style.display = 'block';
|
||
|
|
}
|
||
|
|
|
||
|
|
function showError(message) {
|
||
|
|
const error = document.getElementById('error');
|
||
|
|
const errorMessage = document.getElementById('error-message');
|
||
|
|
const content = document.getElementById('content');
|
||
|
|
|
||
|
|
if (error) error.style.display = 'block';
|
||
|
|
if (errorMessage) errorMessage.textContent = message;
|
||
|
|
if (content) content.style.display = 'none';
|
||
|
|
}
|
||
|
|
|
||
|
|
function hideError() {
|
||
|
|
const error = document.getElementById('error');
|
||
|
|
if (error) error.style.display = 'none';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle browser back/forward buttons
|
||
|
|
window.addEventListener('popstate', function(event) {
|
||
|
|
if (event.state && event.state.doc) {
|
||
|
|
loadDocument(event.state.doc);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Search functionality (future enhancement)
|
||
|
|
function searchDocumentation(query) {
|
||
|
|
// TODO: Implement search across all documentation
|
||
|
|
console.log('Search query:', query);
|
||
|
|
}
|