@knowcode/doc-builder
Version:
Reusable documentation builder for markdown-based sites with Vercel deployment support
1,533 lines (1,300 loc) • 57.8 kB
JavaScript
// Documentation Builder - Main JavaScript
// Preview Banner Management
// Set up banner state immediately to prevent flash
const bannerDismissed = localStorage.getItem('banner-dismissed') === 'true';
// Apply styles immediately if banner should be visible
if (!bannerDismissed) {
document.documentElement.style.setProperty('--banner-offset', '3.5rem');
} else {
document.documentElement.style.setProperty('--banner-offset', '0rem');
}
document.addEventListener('DOMContentLoaded', function() {
const banner = document.getElementById('preview-banner');
const dismissButton = document.getElementById('dismiss-banner');
const mainWrapper = document.querySelector('.main-wrapper');
const sidebar = document.querySelector('.sidebar');
const breadcrumbs = document.querySelector('.breadcrumbs');
if (banner) {
if (bannerDismissed) {
banner.classList.add('hidden');
} else {
// Show banner and adjust layout
banner.classList.add('visible');
mainWrapper?.classList.add('banner-visible');
sidebar?.classList.add('banner-visible');
breadcrumbs?.classList.add('banner-visible');
}
}
// Handle banner dismissal
if (dismissButton) {
dismissButton.addEventListener('click', function() {
banner.classList.remove('visible');
banner.classList.add('hidden');
mainWrapper.classList.remove('banner-visible');
sidebar.classList.remove('banner-visible');
breadcrumbs?.classList.remove('banner-visible');
document.documentElement.style.setProperty('--banner-offset', '0rem');
// Remember that the banner was dismissed
localStorage.setItem('banner-dismissed', 'true');
});
}
// Handle Escape key to dismiss banner
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && banner.classList.contains('visible')) {
dismissButton.click();
}
});
// Initialize Mermaid Full Screen Functionality
initializeMermaidFullScreen();
});
// Mermaid Theme Configuration
function configureMermaidTheme() {
// Check if enhanced styling is enabled (passed from config)
const enhancedStyling = window.docBuilderConfig?.features?.mermaidEnhanced !== false;
// Set data attribute for CSS styling
document.documentElement.setAttribute('data-mermaid-enhanced', enhancedStyling.toString());
// Get current theme (light/dark mode)
const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark';
// Notion-inspired color palette
const lightTheme = {
primaryColor: '#F7F6F3', // Light background for shapes
primaryTextColor: '#37352F', // Dark text
primaryBorderColor: '#E3E2E0', // Subtle borders
lineColor: '#787774', // Connection lines
secondaryColor: '#EDEBE9', // Secondary backgrounds
tertiaryColor: '#E9E9E7', // Tertiary elements
background: '#FFFFFF', // Diagram background
fontSize: '16px', // Text size
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
};
const darkTheme = {
primaryColor: '#2F2F2F', // Dark background for shapes
primaryTextColor: '#E3E2E0', // Light text
primaryBorderColor: '#454545', // Darker borders
lineColor: '#9B9A97', // Lighter connection lines
secondaryColor: '#404040', // Secondary backgrounds
tertiaryColor: '#4A4A4A', // Tertiary elements
background: '#1A1A1A', // Dark diagram background
fontSize: '16px', // Text size
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
};
const currentTheme = isDarkMode ? darkTheme : lightTheme;
// Initialize Mermaid with custom theme (only if enhanced styling is enabled)
const mermaidConfig = {
startOnLoad: false, // We'll manually start it after configuration
theme: enhancedStyling ? 'base' : 'default',
...(enhancedStyling && { themeVariables: currentTheme }),
flowchart: {
curve: 'basis', // Smoother curves
padding: 20, // More padding around elements
nodeSpacing: 50, // Space between nodes
rankSpacing: 50, // Space between ranks
marginX: 20, // Horizontal margins
marginY: 20 // Vertical margins
},
sequence: {
actorMargin: 50,
width: 150,
height: 65,
boxMargin: 10,
boxTextMargin: 5,
noteMargin: 10,
messageMargin: 35
},
...(enhancedStyling && {
gantt: {
leftPadding: 75,
gridLineStartPadding: 35,
fontSize: 12,
sectionFontSize: 24
}
})
};
mermaid.initialize(mermaidConfig);
// Manually render all mermaid diagrams after configuration
setTimeout(() => {
mermaid.run();
}, 100);
}
// Mermaid Full Screen Viewer
function initializeMermaidFullScreen() {
// Wait for Mermaid to initialize
if (typeof mermaid === 'undefined') {
setTimeout(initializeMermaidFullScreen, 100);
return;
}
// Configure Mermaid with enhanced styling
configureMermaidTheme();
// Find all Mermaid diagrams and wrap them with full-screen controls
const mermaidDivs = document.querySelectorAll('.mermaid');
mermaidDivs.forEach((mermaidDiv, index) => {
// Skip if already processed
if (mermaidDiv.closest('.mermaid-container')) {
return;
}
// Create container
const container = document.createElement('div');
container.className = 'mermaid-container';
// Create toolbar
const toolbar = document.createElement('div');
toolbar.className = 'mermaid-toolbar';
const actions = document.createElement('div');
actions.className = 'mermaid-actions';
// Full screen button
const fullScreenBtn = document.createElement('button');
fullScreenBtn.className = 'mermaid-btn';
fullScreenBtn.innerHTML = '<i class="fas fa-expand"></i>';
fullScreenBtn.title = 'Full Screen';
fullScreenBtn.addEventListener('click', () => openMermaidFullScreen(mermaidDiv, index));
actions.appendChild(fullScreenBtn);
toolbar.appendChild(actions);
// Create wrapper for the diagram
const wrapper = document.createElement('div');
wrapper.className = 'mermaid-wrapper';
// Insert container before mermaid div
mermaidDiv.parentNode.insertBefore(container, mermaidDiv);
// Move mermaid div into wrapper
wrapper.appendChild(mermaidDiv);
// Assemble container
container.appendChild(toolbar);
container.appendChild(wrapper);
});
// Create fullscreen modal (only once)
if (!document.getElementById('mermaid-fullscreen-modal')) {
createMermaidFullScreenModal();
}
}
function createMermaidFullScreenModal() {
const modal = document.createElement('div');
modal.id = 'mermaid-fullscreen-modal';
modal.className = 'mermaid-fullscreen';
modal.innerHTML = `
<div class="mermaid-fullscreen-toolbar">
<div class="mermaid-fullscreen-title">Mermaid Diagram - Full Screen View</div>
<div class="mermaid-fullscreen-controls">
<div class="mermaid-zoom-controls">
<button class="mermaid-zoom-btn" id="zoom-out">
<i class="fas fa-minus"></i>
</button>
<div class="mermaid-zoom-level" id="zoom-level">100%</div>
<button class="mermaid-zoom-btn" id="zoom-in">
<i class="fas fa-plus"></i>
</button>
<button class="mermaid-zoom-btn" id="zoom-reset">
<i class="fas fa-expand-arrows-alt"></i>
</button>
</div>
<button class="mermaid-close-btn" id="close-fullscreen">
<i class="fas fa-times"></i> Close
</button>
</div>
</div>
<div class="mermaid-fullscreen-content">
<div class="mermaid-fullscreen-wrapper" id="fullscreen-wrapper">
<div class="mermaid-fullscreen-diagram" id="fullscreen-diagram">
<!-- Diagram will be inserted here -->
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// Set up event listeners - store zoom in modal element
modal.currentZoom = 1;
const wrapper = document.getElementById('fullscreen-wrapper');
const zoomLevel = document.getElementById('zoom-level');
function updateZoom() {
const currentZoom = modal.currentZoom || 1;
wrapper.style.transform = `scale(${currentZoom})`;
zoomLevel.textContent = `${Math.round(currentZoom * 100)}%`;
if (currentZoom > 1) {
wrapper.classList.add('zoomed');
} else {
wrapper.classList.remove('zoomed');
}
}
// Zoom controls
document.getElementById('zoom-in').addEventListener('click', () => {
modal.currentZoom = Math.min((modal.currentZoom || 1) + 0.25, 3);
updateZoom();
});
document.getElementById('zoom-out').addEventListener('click', () => {
modal.currentZoom = Math.max((modal.currentZoom || 1) - 0.25, 0.25);
updateZoom();
});
document.getElementById('zoom-reset').addEventListener('click', () => {
modal.currentZoom = 1;
updateZoom();
});
// Close functionality
document.getElementById('close-fullscreen').addEventListener('click', closeMermaidFullScreen);
// Close on backdrop click
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeMermaidFullScreen();
}
});
// Close on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal.classList.contains('active')) {
closeMermaidFullScreen();
}
});
}
function openMermaidFullScreen(mermaidDiv, index) {
const modal = document.getElementById('mermaid-fullscreen-modal');
const diagramContainer = document.getElementById('fullscreen-diagram');
const wrapper = document.getElementById('fullscreen-wrapper');
const zoomLevel = document.getElementById('zoom-level');
// Reset zoom to 100% when opening new diagram
modal.currentZoom = 1;
wrapper.style.transform = `scale(${modal.currentZoom})`;
zoomLevel.textContent = `${Math.round(modal.currentZoom * 100)}%`;
wrapper.classList.remove('zoomed');
// Clone the mermaid diagram
const clonedDiagram = mermaidDiv.cloneNode(true);
// Reset all styles that might interfere
clonedDiagram.style.cssText = '';
// Find the SVG and make it scale properly
const svg = clonedDiagram.querySelector('svg');
if (svg) {
// Store original dimensions for reference
const originalWidth = svg.getAttribute('width');
const originalHeight = svg.getAttribute('height');
const originalViewBox = svg.getAttribute('viewBox');
// Reset SVG styles
svg.style.cssText = '';
// Ensure we have a proper viewBox for scaling
if (!originalViewBox && originalWidth && originalHeight) {
svg.setAttribute('viewBox', `0 0 ${originalWidth} ${originalHeight}`);
}
// Remove fixed dimensions to enable responsive scaling
svg.removeAttribute('width');
svg.removeAttribute('height');
// Set responsive attributes
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
// Apply CSS for proper scaling
svg.style.width = '100%';
svg.style.height = '100%';
svg.style.maxWidth = '100%';
svg.style.maxHeight = '100%';
svg.style.display = 'block';
}
// Apply proper styles to the cloned diagram
clonedDiagram.style.width = '100%';
clonedDiagram.style.height = '100%';
clonedDiagram.style.display = 'flex';
clonedDiagram.style.justifyContent = 'center';
clonedDiagram.style.alignItems = 'center';
// Clear previous content and add new diagram
diagramContainer.innerHTML = '';
diagramContainer.appendChild(clonedDiagram);
// Show modal
modal.classList.add('active');
document.body.style.overflow = 'hidden';
// Update title
const title = document.querySelector('.mermaid-fullscreen-title');
const container = mermaidDiv.closest('.mermaid-container');
const originalTitle = container ? container.querySelector('.mermaid-toolbar div').textContent : 'Mermaid Diagram';
title.textContent = `${originalTitle} - Full Screen View`;
// Debug logging
console.log('Fullscreen opened with diagram:', clonedDiagram);
console.log('SVG found:', svg);
if (svg) {
console.log('SVG viewBox:', svg.getAttribute('viewBox'));
console.log('SVG dimensions:', svg.getBoundingClientRect());
}
}
function closeMermaidFullScreen() {
const modal = document.getElementById('mermaid-fullscreen-modal');
modal.classList.remove('active');
document.body.style.overflow = '';
// Clear diagram content
setTimeout(() => {
const diagramContainer = document.getElementById('fullscreen-diagram');
diagramContainer.innerHTML = '';
}, 300);
}
function copyMermaidSVG(mermaidDiv) {
try {
const svg = mermaidDiv.querySelector('svg');
if (svg) {
const svgString = new XMLSerializer().serializeToString(svg);
// Try to use the modern clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(svgString).then(() => {
showCopySuccess('SVG copied to clipboard!');
}).catch(() => {
fallbackCopy(svgString, 'SVG');
});
} else {
fallbackCopy(svgString, 'SVG');
}
}
} catch (error) {
console.error('Error copying SVG:', error);
showCopyError();
}
}
function copyMermaidSource(mermaidDiv) {
try {
// Find the original Mermaid source code
let mermaidSource = '';
// Try to get from data attribute first
if (mermaidDiv.dataset.mermaidSource) {
mermaidSource = mermaidDiv.dataset.mermaidSource;
} else {
// Try to find in the page content - look for the nearest pre code block
const container = mermaidDiv.closest('.content');
if (container) {
const codeBlocks = container.querySelectorAll('pre code');
for (const block of codeBlocks) {
if (block.textContent.includes('flowchart') || block.textContent.includes('graph')) {
mermaidSource = block.textContent;
break;
}
}
}
// Fallback: extract from the SVG if available
if (!mermaidSource) {
const svg = mermaidDiv.querySelector('svg');
if (svg) {
// Try to reconstruct basic Mermaid from SVG elements
mermaidSource = reconstructMermaidFromSVG(svg);
}
}
}
if (mermaidSource) {
// Try to use the modern clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(mermaidSource).then(() => {
showCopySuccess('Mermaid source copied to clipboard!');
}).catch(() => {
fallbackCopy(mermaidSource, 'Mermaid');
});
} else {
fallbackCopy(mermaidSource, 'Mermaid');
}
} else {
showCopyError('Could not find Mermaid source');
}
} catch (error) {
console.error('Error copying Mermaid source:', error);
showCopyError();
}
}
function reconstructMermaidFromSVG(svg) {
// Basic reconstruction - this is a fallback method
let mermaidCode = 'flowchart TD\n';
// Try to extract node information from SVG
const nodes = svg.querySelectorAll('g.node');
const edges = svg.querySelectorAll('g.edgePath');
// Add nodes
nodes.forEach((node, index) => {
const label = node.querySelector('span, text, foreignObject');
if (label) {
const nodeText = label.textContent.trim();
const nodeId = `N${index + 1}`;
if (nodeText.includes('?')) {
mermaidCode += ` ${nodeId}{{"${nodeText}"}}\n`;
} else {
mermaidCode += ` ${nodeId}["${nodeText}"]\n`;
}
}
});
mermaidCode += '\n %% Note: This is a reconstructed version - original source may differ\n';
return mermaidCode;
}
function fallbackCopy(text, type = 'content') {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
showCopySuccess(`${type} copied to clipboard!`);
} catch (error) {
showCopyError(`Failed to copy ${type.toLowerCase()}`);
}
document.body.removeChild(textArea);
}
function showCopySuccess(message = 'Content copied to clipboard!') {
// Create temporary success message
const messageDiv = document.createElement('div');
messageDiv.textContent = message;
messageDiv.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--success);
color: white;
padding: 1rem 2rem;
border-radius: 0.5rem;
z-index: 10001;
font-size: 0.875rem;
box-shadow: var(--shadow-lg);
max-width: 300px;
text-align: center;
`;
document.body.appendChild(messageDiv);
setTimeout(() => {
messageDiv.remove();
}, 2000);
}
function showCopyError(message = 'Failed to copy content') {
// Create temporary error message
const messageDiv = document.createElement('div');
messageDiv.textContent = message;
messageDiv.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--danger);
color: white;
padding: 1rem 2rem;
border-radius: 0.5rem;
z-index: 10001;
font-size: 0.875rem;
box-shadow: var(--shadow-lg);
max-width: 300px;
text-align: center;
`;
document.body.appendChild(messageDiv);
setTimeout(() => {
messageDiv.remove();
}, 2000);
}
// Theme Management
const themeToggle = document.getElementById('theme-toggle');
const html = document.documentElement;
// Check for saved theme preference or default to 'light'
const currentTheme = localStorage.getItem('theme') || 'light';
html.setAttribute('data-theme', currentTheme);
updateThemeIcon(currentTheme);
themeToggle.addEventListener('click', () => {
const newTheme = html.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
});
function updateThemeIcon(theme) {
const icon = themeToggle.querySelector('i');
if (icon) {
icon.className = theme === 'light' ? 'fas fa-moon' : 'fas fa-sun';
}
}
// Mobile Menu Toggle
const menuToggle = document.getElementById('menu-toggle');
const sidebar = document.querySelector('.sidebar');
// Set initial menu state based on configuration
const menuDefaultOpen = window.docBuilderConfig?.features?.menuDefaultOpen !== false;
if (sidebar && window.innerWidth > 768) {
if (!menuDefaultOpen) {
sidebar.classList.add('closed');
// Add class to body to show menu toggle on desktop when menu starts closed
document.body.classList.add('menu-starts-closed');
}
}
// Create overlay element for mobile
let overlay = document.querySelector('.sidebar-overlay');
if (!overlay && window.innerWidth <= 768) {
overlay = document.createElement('div');
overlay.className = 'sidebar-overlay';
document.body.appendChild(overlay);
}
if (menuToggle) {
menuToggle.addEventListener('click', () => {
if (window.innerWidth <= 768) {
// Mobile: toggle 'open' class
sidebar.classList.toggle('open');
} else {
// Desktop: toggle 'closed' class
sidebar.classList.toggle('closed');
// Update visibility of menu toggle based on sidebar state
updateMenuToggleVisibility();
}
if (overlay) {
overlay.classList.toggle('active');
}
});
}
// Function to update menu toggle visibility
function updateMenuToggleVisibility() {
if (window.innerWidth > 768) {
if (!menuDefaultOpen || sidebar.classList.contains('closed')) {
document.body.classList.add('show-menu-toggle');
} else {
document.body.classList.remove('show-menu-toggle');
}
}
}
// Initial check
updateMenuToggleVisibility();
// Update on window resize
window.addEventListener('resize', updateMenuToggleVisibility);
// Close menu when clicking overlay
if (overlay) {
overlay.addEventListener('click', () => {
sidebar.classList.remove('open');
overlay.classList.remove('active');
});
}
// Floating Menu Button for Mobile
function initFloatingMenuButton() {
// Only initialize on mobile
if (window.innerWidth > 768) return;
// Check if button already exists
if (document.getElementById('floating-menu-toggle')) return;
// Create floating button
const floatingButton = document.createElement('button');
floatingButton.id = 'floating-menu-toggle';
floatingButton.className = 'floating-menu-toggle';
floatingButton.setAttribute('aria-label', 'Toggle menu');
floatingButton.innerHTML = '<i class="fas fa-bars"></i>';
floatingButton.style.display = 'flex'; // Always visible on mobile
floatingButton.classList.add('visible'); // Start visible
// Add to body
document.body.appendChild(floatingButton);
// Toggle sidebar on click
floatingButton.addEventListener('click', () => {
sidebar.classList.toggle('open');
// Handle overlay
let overlay = document.querySelector('.sidebar-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'sidebar-overlay';
document.body.appendChild(overlay);
// Add overlay click handler
overlay.addEventListener('click', () => {
sidebar.classList.remove('open');
overlay.classList.remove('active');
floatingButton.querySelector('i').className = 'fas fa-bars';
});
}
if (overlay) {
overlay.classList.toggle('active');
}
// Update icon based on state
const icon = floatingButton.querySelector('i');
if (sidebar.classList.contains('open')) {
icon.className = 'fas fa-times';
} else {
icon.className = 'fas fa-bars';
}
});
// Remove scroll-based visibility - button is always visible on mobile
// Update icon when sidebar state changes from other sources
const observer = new MutationObserver(() => {
const icon = floatingButton.querySelector('i');
if (sidebar.classList.contains('open')) {
icon.className = 'fas fa-times';
} else {
icon.className = 'fas fa-bars';
}
});
observer.observe(sidebar, {
attributes: true,
attributeFilter: ['class']
});
}
// Initialize floating button on load and resize
document.addEventListener('DOMContentLoaded', initFloatingMenuButton);
window.addEventListener('resize', () => {
const existingButton = document.getElementById('floating-menu-toggle');
if (window.innerWidth > 768 && existingButton) {
existingButton.remove();
} else if (window.innerWidth <= 768 && !existingButton) {
initFloatingMenuButton();
}
});
// Prevent sidebar from closing when clicking nav items
// Only close when clicking outside the sidebar or the close button
document.addEventListener('click', (e) => {
// Check if we're on mobile
if (window.innerWidth <= 768) {
const isClickInsideSidebar = sidebar && sidebar.contains(e.target);
const isMenuToggle = e.target.closest('#menu-toggle');
const isFloatingButton = e.target.closest('#floating-menu-toggle');
const isNavItem = e.target.closest('.nav-item, .nav-title');
const overlay = document.querySelector('.sidebar-overlay');
// Close sidebar only if clicking outside AND not on menu toggle AND not on nav items
if (!isClickInsideSidebar && !isMenuToggle && !isFloatingButton && !isNavItem && sidebar?.classList.contains('open')) {
sidebar.classList.remove('open');
if (overlay) {
overlay.classList.remove('active');
}
// Update floating button icon if it exists
const floatingBtn = document.getElementById('floating-menu-toggle');
if (floatingBtn) {
floatingBtn.querySelector('i').className = 'fas fa-bars';
}
}
}
});
// Smooth Scrolling for Anchor Links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const href = this.getAttribute('href');
// Skip if href is just '#' (prevents querySelector error)
if (href && href !== '#') {
const target = document.querySelector(href);
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}
});
});
// Active Navigation Highlighting
const sections = document.querySelectorAll('section[id]');
const navItems = document.querySelectorAll('.nav-item');
function highlightNavigation() {
const scrollY = window.pageYOffset;
sections.forEach(section => {
const sectionHeight = section.offsetHeight;
const sectionTop = section.offsetTop - 100;
const sectionId = section.getAttribute('id');
if (scrollY > sectionTop && scrollY <= sectionTop + sectionHeight) {
navItems.forEach(item => {
item.classList.remove('active');
if (item.getAttribute('href') === `#${sectionId}`) {
item.classList.add('active');
}
});
}
});
}
window.addEventListener('scroll', highlightNavigation);
// Search Functionality (Basic Implementation)
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
if (query.length < 2) {
searchResults.style.display = 'none';
return;
}
// This would be replaced with actual search logic
performSearch(query);
});
// Close search results when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.search-box')) {
searchResults.style.display = 'none';
}
});
}
function performSearch(query) {
// Placeholder for search functionality
// In a real implementation, this would search through all content
searchResults.innerHTML = `
<div class="search-result-item">
<strong>Search results for "${query}"</strong>
<p>Search functionality will be implemented here...</p>
</div>
`;
searchResults.style.display = 'block';
}
// Copy Code Blocks
document.querySelectorAll('pre').forEach(block => {
const wrapper = document.createElement('div');
wrapper.className = 'code-block-wrapper';
block.parentNode.insertBefore(wrapper, block);
wrapper.appendChild(block);
const button = document.createElement('button');
button.className = 'copy-button';
button.textContent = 'Copy';
wrapper.appendChild(button);
button.addEventListener('click', () => {
const code = block.textContent;
navigator.clipboard.writeText(code).then(() => {
button.textContent = 'Copied!';
setTimeout(() => {
button.textContent = 'Copy';
}, 2000);
});
});
});
// Table of Contents Generation
function generateTableOfContents() {
const toc = document.getElementById('table-of-contents');
if (!toc) return;
const headings = document.querySelectorAll('.content h2, .content h3');
const tocList = document.createElement('ul');
tocList.className = 'toc-list';
headings.forEach(heading => {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = `#${heading.id}`;
a.textContent = heading.textContent;
a.className = heading.tagName.toLowerCase() === 'h3' ? 'toc-h3' : 'toc-h2';
li.appendChild(a);
tocList.appendChild(li);
});
toc.appendChild(tocList);
}
// Keyboard Shortcuts
document.addEventListener('keydown', (e) => {
// Cmd/Ctrl + K for search
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
searchInput?.focus();
}
// Escape to close mobile menu
if (e.key === 'Escape') {
sidebar?.classList.remove('open');
}
});
// Fade-in Animation on Scroll
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -100px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('fade-in');
observer.unobserve(entry.target);
}
});
}, observerOptions);
document.querySelectorAll('.feature-card, .timeline-item, .metric-card').forEach(el => {
observer.observe(el);
});
// Sidebar Resizing
function initSidebarResize() {
const sidebar = document.querySelector('.sidebar');
const resizeHandle = document.querySelector('.resize-handle');
const content = document.querySelector('.content');
if (!sidebar || !resizeHandle || !content) return;
let isResizing = false;
let startX = 0;
let startWidth = 0;
// Restore saved width on load
const savedWidth = localStorage.getItem('sidebarWidth');
if (savedWidth && savedWidth >= 200 && savedWidth <= 500) {
sidebar.style.width = `${savedWidth}px`;
// Don't set margin-left - flexbox handles the layout
}
// Mouse down on resize handle
resizeHandle.addEventListener('mousedown', (e) => {
isResizing = true;
startX = e.clientX;
startWidth = parseInt(document.defaultView.getComputedStyle(sidebar).width, 10);
// Add global event listeners
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
// Prevent text selection during resize
document.body.style.userSelect = 'none';
document.body.style.cursor = 'col-resize';
e.preventDefault();
});
function handleMouseMove(e) {
if (!isResizing) return;
const width = startWidth + e.clientX - startX;
// Constrain width between 200px and 500px
const constrainedWidth = Math.max(200, Math.min(500, width));
sidebar.style.width = `${constrainedWidth}px`;
// Don't set margin-left - flexbox handles the layout
e.preventDefault();
}
function handleMouseUp() {
if (!isResizing) return;
isResizing = false;
// Remove global event listeners
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
// Restore normal cursor and text selection
document.body.style.userSelect = '';
document.body.style.cursor = '';
// Save the current width
const currentWidth = parseInt(document.defaultView.getComputedStyle(sidebar).width, 10);
localStorage.setItem('sidebarWidth', currentWidth);
}
// Touch events for mobile support
resizeHandle.addEventListener('touchstart', (e) => {
isResizing = true;
startX = e.touches[0].clientX;
startWidth = parseInt(document.defaultView.getComputedStyle(sidebar).width, 10);
document.addEventListener('touchmove', handleTouchMove);
document.addEventListener('touchend', handleTouchEnd);
e.preventDefault();
});
function handleTouchMove(e) {
if (!isResizing) return;
const width = startWidth + e.touches[0].clientX - startX;
const constrainedWidth = Math.max(200, Math.min(500, width));
sidebar.style.width = `${constrainedWidth}px`;
// Don't set margin-left - flexbox handles the layout
e.preventDefault();
}
function handleTouchEnd() {
if (!isResizing) return;
isResizing = false;
document.removeEventListener('touchmove', handleTouchMove);
document.removeEventListener('touchend', handleTouchEnd);
// Save the current width
const currentWidth = parseInt(document.defaultView.getComputedStyle(sidebar).width, 10);
localStorage.setItem('sidebarWidth', currentWidth);
}
}
// Collapsible Navigation
function initCollapsibleNavigation() {
// Debug: Log initial state
console.log('[Navigation] Initializing collapsible navigation');
const allNavSections = document.querySelectorAll('.nav-section');
console.log(`[Navigation] Found ${allNavSections.length} nav sections`);
// First, ensure sections with active items are expanded on page load
expandActiveNavSections();
// Also run it again after a short delay to handle any timing issues
setTimeout(() => {
console.log('[Navigation] Running delayed expandActiveNavSections');
expandActiveNavSections();
}, 100);
// Additional fallback: if no active items found, try to expand based on current URL
setTimeout(() => {
const activeItems = document.querySelectorAll('.nav-item.active');
if (activeItems.length === 0) {
console.log('[Navigation] No active items found, trying URL-based expansion');
expandSectionByCurrentURL();
}
}, 200);
// Handle toggle-all button for root navigation
const toggleAllButton = document.getElementById('nav-toggle-all');
if (toggleAllButton) {
toggleAllButton.addEventListener('click', (e) => {
e.preventDefault();
const icon = document.getElementById('toggle-all-icon');
const isExpanded = toggleAllButton.classList.contains('expanded');
// Get all collapsible sections (excluding the root)
const allSections = document.querySelectorAll('.nav-section[data-level]:not([data-level="0"]) .nav-title.collapsible');
const allContents = document.querySelectorAll('.nav-section[data-level]:not([data-level="0"]) .nav-content.collapsed, .nav-section[data-level]:not([data-level="0"]) .nav-content:not(.collapsed)');
if (isExpanded) {
// Collapse all sections
toggleAllButton.classList.remove('expanded');
icon.className = 'ph ph-caret-right';
allSections.forEach(section => {
section.classList.remove('expanded');
});
allContents.forEach(content => {
if (content.id) { // Only collapse if it has an id (is collapsible)
content.classList.add('collapsed');
}
});
} else {
// Expand all sections
toggleAllButton.classList.add('expanded');
icon.className = 'ph ph-caret-down';
allSections.forEach(section => {
section.classList.add('expanded');
});
allContents.forEach(content => {
content.classList.remove('collapsed');
});
}
});
}
const collapsibleTitles = document.querySelectorAll('.nav-title.collapsible');
collapsibleTitles.forEach(title => {
title.addEventListener('click', (e) => {
// Prevent default link behavior for collapsible titles
e.preventDefault();
// Get the target content to toggle
const targetId = title.getAttribute('data-target');
const content = document.getElementById(targetId);
if (content) {
const isExpanded = title.classList.contains('expanded');
if (isExpanded) {
// Collapse this section
title.classList.remove('expanded');
content.classList.add('collapsed');
// Also collapse all child sections within this content
const childSections = content.querySelectorAll('.nav-title.collapsible');
childSections.forEach(childTitle => {
const childTargetId = childTitle.getAttribute('data-target');
const childContent = document.getElementById(childTargetId);
if (childContent) {
childTitle.classList.remove('expanded');
childContent.classList.add('collapsed');
}
});
} else {
// Expand this section
title.classList.add('expanded');
content.classList.remove('collapsed');
}
}
});
});
// Prevent nav items from triggering collapse and maintain parent expansion
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', (e) => {
// Only stop propagation to prevent collapse, but allow normal link navigation
e.stopPropagation(); // Prevent event from bubbling up to the nav-title
// Ensure ALL parent sections stay expanded when navigating within them
let currentElement = item;
while (currentElement) {
const parentContent = currentElement.closest('.nav-content');
if (!parentContent) break;
const parentTitle = parentContent.parentElement?.querySelector('.nav-title.collapsible');
if (parentTitle && parentContent) {
parentTitle.classList.add('expanded');
parentContent.classList.remove('collapsed');
}
// Move up to check for nested sections
currentElement = parentContent.parentElement;
}
// Allow normal link navigation - no preventDefault or manual navigation needed
});
});
}
// Expand navigation section based on current URL
function expandSectionByCurrentURL() {
try {
const currentPath = window.location.pathname;
console.log('[Navigation] Current path:', currentPath);
// Find all nav items and check if any match the current URL
const navItems = document.querySelectorAll('.nav-item');
let foundMatch = false;
navItems.forEach(item => {
const href = item.getAttribute('href');
if (href) {
// Normalize paths for comparison
const itemPath = new URL(href, window.location.href).pathname;
if (itemPath === currentPath) {
console.log('[Navigation] Found matching nav item by URL:', item.textContent.trim());
item.classList.add('active');
foundMatch = true;
// Expand parent sections
let currentElement = item;
while (currentElement && currentElement !== document.body) {
if (currentElement.classList && currentElement.classList.contains('nav-content')) {
const parentSection = currentElement.closest('.nav-section');
if (parentSection) {
const parentTitle = parentSection.querySelector('.nav-title.collapsible');
if (parentTitle && currentElement.classList.contains('collapsed')) {
parentTitle.classList.add('expanded');
currentElement.classList.remove('collapsed');
console.log('[Navigation] Expanded section by URL:', parentTitle.textContent.trim());
}
}
}
currentElement = currentElement.parentElement;
}
}
}
});
if (!foundMatch) {
console.warn('[Navigation] No matching nav item found for current URL');
}
} catch (error) {
console.error('[Navigation] Error in expandSectionByCurrentURL:', error);
}
}
// Ensure sections containing active nav items stay expanded
function expandActiveNavSections() {
try {
const activeNavItems = document.querySelectorAll('.nav-item.active');
console.log(`[Navigation] Found ${activeNavItems.length} active nav items`);
if (activeNavItems.length === 0) {
console.warn('[Navigation] No active navigation items found!');
return;
}
activeNavItems.forEach(activeItem => {
console.log(`[Navigation] Expanding sections for: ${activeItem.textContent.trim()}`);
// Start from the active item and work up the DOM tree
let currentElement = activeItem;
let sectionsExpanded = 0;
while (currentElement && currentElement !== document.body) {
// Check if we're inside a nav-content element
if (currentElement.classList && currentElement.classList.contains('nav-content')) {
console.log('[Navigation] Found nav-content element with id:', currentElement.id);
// Find the corresponding nav-title in the parent nav-section
const parentSection = currentElement.closest('.nav-section');
if (parentSection) {
const parentTitle = parentSection.querySelector('.nav-title.collapsible');
if (parentTitle && currentElement.classList.contains('collapsed')) {
// Expand this section
parentTitle.classList.add('expanded');
currentElement.classList.remove('collapsed');
sectionsExpanded++;
console.log(`[Navigation] Expanded section: ${parentTitle.textContent.trim()}`);
} else if (parentTitle && !currentElement.classList.contains('collapsed')) {
console.log(`[Navigation] Section already expanded: ${parentTitle.textContent.trim()}`);
}
}
}
// Move up to the parent element
currentElement = currentElement.parentElement;
}
if (sectionsExpanded === 0) {
console.warn('[Navigation] No sections were expanded for active item:', activeItem.textContent.trim());
} else {
console.log(`[Navigation] Successfully expanded ${sectionsExpanded} sections`);
}
});
} catch (error) {
console.error('[Navigation] Error in expandActiveNavSections:', error);
}
}
// Navigation Filter
function initNavigationFilter() {
const filterInput = document.getElementById('nav-filter');
if (!filterInput) return;
filterInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase().trim();
const navItems = document.querySelectorAll('.nav-item');
const navSections = document.querySelectorAll('.nav-section');
if (query === '') {
// Show all items and restore original state
navItems.forEach(item => {
item.style.display = 'flex';
});
navSections.forEach(section => {
section.style.display = 'block';
});
} else {
// Filter items
navItems.forEach(item => {
const text = item.textContent.toLowerCase();
const shouldShow = text.includes(query);
item.style.display = shouldShow ? 'flex' : 'none';
});
// Show/hide sections based on whether they have visible items
navSections.forEach(section => {
const visibleItems = section.querySelectorAll('.nav-item[style*="flex"]');
const hasVisibleItems = Array.from(section.querySelectorAll('.nav-item')).some(item =>
item.style.display !== 'none'
);
section.style.display = hasVisibleItems ? 'block' : 'none';
// Expand sections with matches
if (hasVisibleItems && query !== '') {
const navContent = section.querySelector('.nav-content');
const navTitle = section.querySelector('.nav-title.collapsible');
if (navContent && navTitle) {
navContent.classList.remove('collapsed');
navTitle.classList.add('expanded');
}
}
});
}
});
}
// PDF Export functionality
function exportToPDF() {
// Hide UI elements for printing
const elementsToHide = [
'.sidebar',
'.header',
'.preview-banner',
'.resize-handle',
'.copy-button'
];
elementsToHide.forEach(selector => {
const elements = document.querySelectorAll(selector);
elements.forEach(el => el.style.display = 'none');
});
// Adjust content for printing
const content = document.querySelector('.content');
const mainWrapper = document.querySelector('.main-wrapper');
if (content) {
content.style.padding = '20px';
content.style.maxWidth = 'none';
}
if (mainWrapper) {
mainWrapper.style.paddingTop = '0';
}
// Add print-specific styles
const printStyles = document.createElement('style');
printStyles.id = 'print-styles';
printStyles.textContent = `
@media print {
body {
font-size: 12pt;
line-height: 1.4;
color: black !important;
}
.content {
margin: 0 !important;
padding: 0 !important;
max-width: none !important;
}
.main-wrapper {
padding-top: 0 !important;
}
h1, h2, h3, h4, h5, h6 {
color: black !important;
page-break-after: avoid;
}
.hero {
background: none !important;
color: black !important;
padding: 20px 0 !important;
margin: 0 !important;
}
.hero h1 {
color: black !important;
text-shadow: none !important;
font-size: 24pt !important;
}
.hero-subtitle {
color: black !important;
text-shadow: none !important;
}
.feature-grid, .metrics-grid {
display: block !important;
}
.feature-card, .metric-card {
break-inside: avoid;
margin-bottom: 10px;
border: 1px solid #ccc;
padding: 10px;
}
.mermaid {
break-inside: avoid;
background: white !important;
border: 1px solid #ccc;
}
pre, code {
background: #f5f5f5 !important;
border: 1px solid #ddd;
font-size: 10pt;
}
table {
break-inside: avoid;
}
.timeline {
display: block !important;
}
.timeline-item {
break-inside: avoid;
margin-bottom: 15px;
padding-left: 0 !important;
}
.timeline::before {
display: none;
}
.timeline-item::before {
display: none;
}
a {
color: black !important;
text-decoration: underline;
}
.gradient-text {
color: black !important;
background: none !important;
-webkit-text-fill-color: black !important;
}
}
`;
document.head.appendChild(printStyles);
// Trigger print dialog
setTimeout(() => {
window.print();
// Restore UI after print dialog
setTimeout(() => {
// Remove print styles
const printStylesEl = document.getElementById('print-styles');
if (printStylesEl) {
printStylesEl.remove();
}
// Restore hidden elements
elementsToHide.forEach(selector => {
const elements = document.querySelectorAll(selector);
elements.forEach(el => el.style.display = '');
});
// Restore content styles
if (content) {
content.style.padding = '';
content.style.maxWidth = '';
}
if (mainWrapper) {
mainWrapper.style.paddingTop = '';
}
}, 500);
}, 100);
}
// Add PDF export button functionality
function addPDFExportButton() {
// Check configuration - default to true if not set
const showPdfDownload = window.docBuilderConfig?.features?.showPdfDownload !== false;
if (!showPdfDownload) return;
const headerActions = document.querySelector('.header-actions');
if (headerActions) {
const pdfButton = document.createElement('button');
pdfButton.innerHTML = '<i class="fas fa-file-pdf"></i>';
pdfButton.className = 'theme-toggle';
pdfButton.title = 'Export to PDF';
pdfButton.setAttribute('aria-label', 'Export to PDF');
pdfButton.addEventListener('click', exportToPDF);
// Insert before theme toggle
const themeToggle = document.getElementById('theme-toggle');
headerActions.insertBefore(pdfButton, themeToggle);
}
}
// Breadcrumb Generation
function generateBreadcrumbs() {
const breadcrumbContainer = document.getElementById('breadcrumbs');
if (!breadcrumbContainer) return;
// Check if this is a static build (has data attributes)
const isStaticBuild = breadcrumbContainer.classList.contains('breadcrumbs-static');
const buildDate = breadcrumbContainer.getAttribute('data-build-date');
const docBuilderVersion = breadcrumbContainer.getAttribute('data-doc-builder-version');
// For static builds, check if content is already pre-rendered
if (isStaticBuild && breadcrumbContainer.querySelector('.breadcrumbs-content')) {
// Content is pre-rendered, just ensure exportToPDF is available globally
window.exportToPDF = exportToPDF;
return; // Don't override pre-rendered content
}
// Decode the URL to handle special characters and spaces
const currentPath = decodeURIComponent(window.location.pathname);
let pathSegments = currentPath.split('/').filter(segment => segment !== '');
// Find the index of 'html' directory and slice from there
const htmlIndex = pathSegments.findIndex(segment => segment === 'html');
if (htmlIndex !== -1) {
// Remove everything before and including 'html'
pathSegments = pathSegments.slice(htmlIndex + 1);
}
// Remove .html extension from the last segment
if (pathSegments.length > 0) {
const lastSegment = pathSegments[pathSegments.length - 1];
if (lastSegment.endsWith('.html')) {
pathSegments[pathSegments.length - 1] = lastSegment.slice(0, -5);
}
}
const breadcrumbs = [];
// Calculate relative path to root for proper navigation
const depth = pathSegments.length;
const relativeRoot = depth > 0 ? '../'.repeat(depth) : './';
// Always start with Home (relative to current page)
breadcrumbs.push({
text: 'Home',
href: relativeRoot + 'index.html',
icon: 'fas fa-home'
});
// Build breadcrumb path
let currentUrl = '';
pathSegments.forEach((segment, index) => {
currentUrl += '/' + segment;
// Calculate relative path for this breadcrumb level
const remainingDepth = pathSegments.length - index - 1;
const relativePath = remainingDepth > 0 ? '../'.repeat(remainingDepth) : './';
// For the last segment, don't add .html back if it's not index
const href = index === pathSegments.length - 1 && segment !== 'index'
? '#' // Current page, no navigation needed
: relativePath + segment + '.html';
// Prettify segment names
const text = segment
.replace(/[-_]/g, ' ')
.split(' ')
.map(word => word.charAt(0).to