ZDDC/pandoc/templates/_scripts.html
ZDDC c765fe9183 feat(pandoc): named doctype templates + front-matter numbering toggle
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>
2026-06-04 14:07:36 -05:00

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>