Replace the single always-numbered viewer-template.html with a templates/
directory of named doctype templates that share partials:
- templates/_head.html — <head> + all CSS (numbering CSS now scoped behind a
body.numbered class instead of being applied unconditionally)
- templates/_doc.html — shared TOC-sidebar body (report/specification)
- templates/_scripts.html — shared JS
- templates/{report,specification}.html — TOC-layout doctypes
- templates/letter.html — single-column letterhead, no TOC
A document selects its template with `template: <name>` in YAML front matter
(default report) and turns on legal numbering with `numbering: true` (default
off). Pandoc passes both fields straight from the front matter — the numbering
toggle needs no converter code. Retire custom.css (folded into _head.html,
gated) and the old viewer-template.html.
CLI: convert md→html resolves templates/<name>.html (name from front matter,
sanitized, default report); convert-diff uses templates/report.html and no
longer passes --css=custom.css. README updated.
Server (zddc/internal/convert) still uses its own embedded copy and is
unchanged here; it migrates to this templates/ dir in the next commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
259 lines
9.4 KiB
HTML
259 lines
9.4 KiB
HTML
<!-- Embedded JavaScript -->
|
|
<script>
|
|
'use strict';
|
|
|
|
// Modern initialization with arrow functions
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// View mode toggle functionality
|
|
const buttons = document.querySelectorAll('.view-mode-btn');
|
|
const body = document.body;
|
|
|
|
buttons.forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
const mode = this.dataset.mode;
|
|
|
|
// Remove all view mode classes
|
|
body.classList.remove('view-original', 'view-final');
|
|
|
|
// Add the selected mode class (except for diff which is default)
|
|
if (mode === 'original') {
|
|
body.classList.add('view-original');
|
|
} else if (mode === 'final') {
|
|
body.classList.add('view-final');
|
|
}
|
|
|
|
// Update button states
|
|
buttons.forEach(btn => btn.classList.remove('active'));
|
|
this.classList.add('active');
|
|
});
|
|
});
|
|
|
|
const sidebar = document.getElementById('sidebar');
|
|
if (sidebar) {
|
|
initTocNavigation();
|
|
}
|
|
|
|
// Set default TOC level filtering
|
|
filterTocLevels('3');
|
|
|
|
// Setup event listeners with delegation
|
|
setupEventListeners();
|
|
|
|
// Initialize print functionality
|
|
initPrintSupport();
|
|
});
|
|
|
|
// Modern TOC Navigation with ES6+ patterns
|
|
function initTocNavigation() {
|
|
const tocLinks = document.querySelectorAll('.toc a');
|
|
const contentArea = document.querySelector('.document-content');
|
|
|
|
if (!tocLinks.length || !contentArea) return;
|
|
|
|
// Smooth scroll with event delegation (better performance)
|
|
function handleTocClick(e) {
|
|
if (!e.target.matches('.toc a')) return;
|
|
|
|
e.preventDefault();
|
|
const href = e.target.getAttribute('href');
|
|
const targetId = href ? href.slice(1) : null;
|
|
const targetElement = targetId ? document.getElementById(targetId) : null;
|
|
|
|
if (!targetElement) return;
|
|
|
|
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
|
|
// Update URL hash without adding to browser history
|
|
window.location.replace(window.location.pathname + window.location.search + href);
|
|
|
|
// Update active state
|
|
tocLinks.forEach(link => link.classList.remove('active'));
|
|
e.target.classList.add('active');
|
|
|
|
// Close mobile menu if open
|
|
const sidebar = document.getElementById('sidebar');
|
|
if (sidebar && sidebar.classList.contains('mobile-open')) toggleMobileMenu();
|
|
};
|
|
|
|
document.addEventListener('click', handleTocClick);
|
|
|
|
// TOC scroll tracking using Intersection Observer API
|
|
// NOTE: Intersection Observer is the industry-standard, recommended approach for scroll spy
|
|
// implementations as of 2024. It provides better performance (runs off main thread),
|
|
// cleaner code, and is supported by all modern browsers. Avoid scroll event listeners
|
|
// for this use case as they are performance-intensive and require complex calculations.
|
|
// Find all sections with IDs - much simpler approach
|
|
const sections = Array.from(contentArea.querySelectorAll('section[id]'));
|
|
|
|
|
|
if (sections.length === 0) {
|
|
return;
|
|
}
|
|
|
|
function updateActiveTocItem(activeSection) {
|
|
if (!activeSection || !activeSection.id) return;
|
|
|
|
// Clear all active states
|
|
tocLinks.forEach(link => link.classList.remove('active'));
|
|
|
|
// Find and activate the matching TOC link
|
|
const activeLink = document.querySelector('.toc a[href="#' + activeSection.id + '"]');
|
|
if (!activeLink) return;
|
|
|
|
activeLink.classList.add('active');
|
|
|
|
// Auto-scroll TOC to keep active item visible
|
|
activeLink.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'nearest',
|
|
inline: 'nearest'
|
|
});
|
|
};
|
|
|
|
// Create Intersection Observer with industry-standard configuration
|
|
const observer = new IntersectionObserver(function(entries) {
|
|
// Find visible sections and update active TOC item
|
|
const visibleSections = entries.filter(function(entry) { return entry.isIntersecting; });
|
|
if (visibleSections.length > 0) {
|
|
// Sort by position in viewport (topmost first)
|
|
visibleSections.sort(function(a, b) { return a.boundingClientRect.top - b.boundingClientRect.top; });
|
|
const activeSection = visibleSections[0].target;
|
|
updateActiveTocItem(activeSection);
|
|
}
|
|
}, {
|
|
root: contentArea,
|
|
rootMargin: '-20% 0px -60% 0px', // Only consider sections in the middle 20% of viewport
|
|
threshold: 0.1
|
|
});
|
|
|
|
// Observe all sections
|
|
sections.forEach(function(section) { observer.observe(section); });
|
|
|
|
// Scroll progress bar with throttling for better performance
|
|
const progressBar = document.querySelector('.scroll-progress-bar');
|
|
if (progressBar) {
|
|
let ticking = false;
|
|
|
|
function updateScrollProgress() {
|
|
const scrollTop = contentArea.scrollTop;
|
|
const scrollHeight = contentArea.scrollHeight;
|
|
const clientHeight = contentArea.clientHeight;
|
|
const scrollPercent = scrollHeight > clientHeight
|
|
? (scrollTop / (scrollHeight - clientHeight)) * 100
|
|
: 0;
|
|
progressBar.style.width = Math.min(100, Math.max(0, scrollPercent)) + '%';
|
|
ticking = false;
|
|
};
|
|
|
|
function onScroll() {
|
|
if (!ticking) {
|
|
requestAnimationFrame(updateScrollProgress);
|
|
ticking = true;
|
|
}
|
|
};
|
|
|
|
contentArea.addEventListener('scroll', onScroll, { passive: true });
|
|
updateScrollProgress(); // Initial call
|
|
}
|
|
};
|
|
|
|
// Toggle mobile menu with ARIA support
|
|
function toggleMobileMenu() {
|
|
const sidebar = document.getElementById('sidebar');
|
|
const menuToggle = document.querySelector('.mobile-menu-toggle');
|
|
|
|
if (!sidebar || !menuToggle) return;
|
|
|
|
const isOpen = sidebar.classList.toggle('mobile-open');
|
|
menuToggle.setAttribute('aria-expanded', isOpen.toString());
|
|
};
|
|
|
|
// Filter TOC levels with modern patterns
|
|
function filterTocLevels(maxLevel) {
|
|
const toc = document.querySelector('.toc');
|
|
if (!toc) return;
|
|
|
|
const allItems = toc.querySelectorAll('li');
|
|
const maxLevelNum = parseInt(maxLevel);
|
|
const showAll = maxLevel === '6';
|
|
|
|
allItems.forEach(function(item) {
|
|
const link = item.querySelector('a');
|
|
if (!link) return;
|
|
|
|
if (showAll) {
|
|
item.style.display = '';
|
|
return;
|
|
}
|
|
|
|
// Calculate nesting level more efficiently
|
|
let level = 1;
|
|
let parent = item.parentElement;
|
|
while (parent && !parent.classList.contains('toc')) {
|
|
if (parent.tagName === 'LI') level++;
|
|
parent = parent.parentElement;
|
|
}
|
|
|
|
item.style.display = level <= maxLevelNum ? '' : 'none';
|
|
});
|
|
};
|
|
|
|
|
|
// Setup event listeners with delegation
|
|
function setupEventListeners() {
|
|
// TOC level selector
|
|
const tocLevelSelect = document.getElementById('toc-level');
|
|
if (tocLevelSelect) tocLevelSelect.addEventListener('change', function(e) {
|
|
filterTocLevels(e.target.value);
|
|
});
|
|
|
|
// Mobile menu toggle
|
|
const menuToggle = document.querySelector('.mobile-menu-toggle');
|
|
if (menuToggle) menuToggle.addEventListener('click', toggleMobileMenu);
|
|
|
|
// Close mobile menu on outside click
|
|
document.addEventListener('click', function(e) {
|
|
const sidebar = document.getElementById('sidebar');
|
|
const menuToggle = document.querySelector('.mobile-menu-toggle');
|
|
|
|
if (sidebar && sidebar.classList.contains('mobile-open') &&
|
|
!sidebar.contains(e.target) &&
|
|
(!menuToggle || !menuToggle.contains(e.target))) {
|
|
toggleMobileMenu();
|
|
}
|
|
});
|
|
|
|
// Handle escape key to close mobile menu
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
const sidebar = document.getElementById('sidebar');
|
|
if (sidebar && sidebar.classList.contains('mobile-open')) {
|
|
toggleMobileMenu();
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
// Initialize print support and draft status
|
|
function initPrintSupport() {
|
|
// Handle draft status for revisions containing tilde (~)
|
|
const revision = document.querySelector('meta[name="revision"]');
|
|
const generationTime = document.querySelector('meta[name="generation_time"]');
|
|
|
|
if (revision && generationTime) {
|
|
const revisionValue = revision.getAttribute('content');
|
|
const timeValue = generationTime.getAttribute('content');
|
|
|
|
if (revisionValue && revisionValue.includes('~') && timeValue) {
|
|
const draftElements = document.querySelectorAll('.draft-status');
|
|
draftElements.forEach(function(element) {
|
|
element.textContent = ' [DRAFT Generated at ' + timeValue + ']';
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export functions for global access (maintaining backward compatibility)
|
|
window.toggleMobileMenu = toggleMobileMenu;
|
|
window.filterTocLevels = filterTocLevels;
|
|
</script>
|