UNPKG

@raven-js/glean

Version:

Glean documentation gold from your codebase - JSDoc parsing, validation, and beautiful doc generation

564 lines (513 loc) • 16.4 kB
/** * @author Anonyfox <max@anonyfox.com> * @license MIT * @see {@link https://github.com/Anonyfox/ravenjs} * @see {@link https://ravenjs.dev} * @see {@link https://anonyfox.com} */ /** * Base HTML template with Bootstrap 5, SEO, and responsive design. * * Foundation template providing consistent structure for all documentation pages * with navigation, footer, and optimized developer UX. */ import { html } from "@raven-js/beak"; /** * Generate base HTML template with Bootstrap 5 layout, SEO, and navigation. * * @param {Object} options - Template configuration * @param {string} options.title - Page title * @param {string} options.description - Meta description * @param {string} options.packageName - Package name for branding * @param {string} options.content - Main page content HTML * @param {Object} [options.seo] - SEO configuration * @param {string} [options.seo.url] - Canonical URL * @param {string} [options.seo.image] - Open Graph image * @param {Object} [options.navigation] - Navigation data * @param {string} [options.navigation.current] - Current active page * @param {boolean} [options.navigation.showSearch] - Show search form * @param {string} [options.navigation.sidebar] - Sidebar content * @param {Object} [options.packageMetadata] - Package metadata for footer * @param {string|Object} [options.packageMetadata.author] - Author information * @param {string} [options.packageMetadata.homepage] - Homepage URL * @param {Object} [options.packageMetadata.repository] - Repository info * @param {string} [options.packageMetadata.repository.url] - Repository URL * @param {Object} [options.packageMetadata.bugs] - Bug tracker info * @param {string} [options.packageMetadata.bugs.url] - Bug tracker URL * @param {Object} [options.packageMetadata.funding] - Funding info * @param {string} [options.packageMetadata.funding.url] - Funding URL * @param {string} [options.generationTimestamp] - Generation timestamp * @param {Object} [options.urlBuilder] - URL builder for base path handling * @returns {string} Complete HTML document * * @example * // Basic page template * baseTemplate({ * title: 'My Package', * description: 'Package documentation', * packageName: 'my-package', * content: '<h1>Content</h1>' * }); */ export function baseTemplate({ title, description, packageName, content, seo = {}, navigation = {}, packageMetadata = null, generationTimestamp = null, urlBuilder = null, }) { const fullTitle = title; const canonicalUrl = seo.url || ""; const ogImage = seo.image || ""; // Process package metadata for footer let authorName = ""; let authorEmail = ""; if (packageMetadata?.author) { const author = packageMetadata.author; if (typeof author === "string") { // Parse "Name <email>" format const match = author.match(/^(.+?)\s*<([^>]+)>$/); if (match) { authorName = match[1].trim(); authorEmail = match[2].trim(); } else { authorName = author; } } else if ( author && typeof author === "object" && /** @type {any} */ (author).name ) { /** @type {any} */ const authorObj = author; authorName = authorObj.name; authorEmail = authorObj.email || ""; } } // Generate package links const homepageUrl = packageMetadata?.homepage; const repositoryUrl = typeof packageMetadata?.repository === "string" ? packageMetadata.repository : packageMetadata?.repository?.url || ""; const issuesUrl = typeof packageMetadata?.bugs === "string" ? packageMetadata.bugs : packageMetadata?.bugs?.url || ""; const fundingUrl = typeof packageMetadata?.funding === "string" ? packageMetadata.funding : packageMetadata?.funding?.url || ""; // Generate timestamp string const timestamp = generationTimestamp ? new Date(generationTimestamp).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", }) : new Date().toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", }); return html`<!DOCTYPE html> <html lang="en" data-bs-theme="light"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <!-- Core Meta Tags --> <title>${fullTitle}</title> <meta name="description" content="${description}"> <meta name="generator" content="Glean Documentation Generator"> <meta name="theme-color" content="#6366f1"> <!-- Open Graph Meta Tags --> <meta property="og:type" content="website"> <meta property="og:title" content="${title}"> <meta property="og:description" content="${description}"> ${canonicalUrl ? html`<meta property="og:url" content="${canonicalUrl}">` : ""} ${ogImage ? html`<meta property="og:image" content="${ogImage}">` : ""} <meta property="og:site_name" content="${packageName} Documentation"> <!-- Twitter Card Meta Tags --> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:title" content="${title}"> <meta name="twitter:description" content="${description}"> ${ogImage ? html`<meta name="twitter:image" content="${ogImage}">` : ""} <!-- SEO Meta Tags --> ${canonicalUrl ? html`<link rel="canonical" href="${canonicalUrl}">` : ""} <meta name="robots" content="index, follow"> <meta name="author" content="Generated by Glean"> <!-- Favicon --> <link rel="icon" type="image/x-icon" href="/favicon.ico"> <!-- Bootstrap 5 CSS --> <link href="/bootstrap.min.css" rel="stylesheet"> <!-- Custom documentation enhancements --> <style> /* CSS variables for theming */ :root { --glean-primary: #0d6efd; --glean-secondary: #6c757d; --glean-background: #ffffff; --glean-surface: #f8f9fa; } /* Copy functionality styling */ .code-copy-btn { position: absolute; top: 0.5rem; right: 0.5rem; opacity: 0.7; transition: opacity 0.2s ease; } .code-copy-btn:hover { opacity: 1; } /* Search highlighting */ .search-highlight { background-color: rgba(255, 193, 7, 0.2); padding: 0.125rem 0.25rem; border-radius: 0.25rem; } /* Enhanced styling for documentation */ .navbar-brand { font-weight: 700; color: var(--glean-primary) !important; } .code-block { background-color: var(--glean-surface); border: 1px solid #dee2e6; border-radius: 0.375rem; padding: 1rem; overflow-x: auto; } /* README content styling */ .readme-content h1, .readme-content h2, .readme-content h3, .readme-content h4, .readme-content h5, .readme-content h6 { margin-top: 1.5rem; margin-bottom: 1rem; } .readme-content h1:first-child, .readme-content h2:first-child, .readme-content h3:first-child { margin-top: 0; } .readme-content p { margin-bottom: 1rem; } .readme-content pre { background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 0.375rem; padding: 1rem; overflow-x: auto; } .readme-content code { background-color: #f8f9fa; color: #e83e8c; padding: 0.125rem 0.25rem; border-radius: 0.25rem; font-size: 0.875em; } .readme-content pre code { background-color: transparent; color: inherit; padding: 0; } .readme-content table { width: 100%; margin-bottom: 1rem; border-collapse: collapse; } .readme-content th, .readme-content td { padding: 0.5rem; border: 1px solid #dee2e6; } .readme-content th { background-color: #f8f9fa; font-weight: 600; } /* Mobile responsive adjustments */ @media (max-width: 768px) { .container-fluid { padding-left: 0.75rem; padding-right: 0.75rem; } .sidebar { border-end: none !important; border-bottom: 1px solid #dee2e6; } } </style> </head> <body> <!-- Main Navigation --> <nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom"> <div class="container-fluid"> <a class="navbar-brand fw-bold text-primary" href="${urlBuilder ? /** @type {any} */ (urlBuilder).homeUrl() : "/"}"> ${packageName} <small class="text-muted">Documentation</small> </a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav me-auto"> <li class="nav-item"> <a class="nav-link ${navigation.current === "overview" ? "active" : ""}" href="${urlBuilder ? /** @type {any} */ (urlBuilder).homeUrl() : "/"}">Overview</a> </li> <li class="nav-item"> <a class="nav-link ${navigation.current === "modules" ? "active" : ""}" href="${urlBuilder ? /** @type {any} */ (urlBuilder).modulesUrl() : "/modules/"}">Modules</a> </li> </ul> </div> </div> </nav> <!-- Main Content Area --> <div class="container-fluid"> <div class="row"> <!-- Sidebar Navigation (if provided) --> ${ navigation.sidebar ? html` <div class="col-md-3 col-lg-2 bg-light border-end sidebar"> <div class="p-3" style="min-height: calc(100vh - 56px);"> ${navigation.sidebar} </div> </div> ` : "" } <!-- Main Content --> <div class="${navigation.sidebar ? "col-md-9 col-lg-10" : "col-12"}"> <main class="p-4" style="min-height: calc(100vh - 56px);"> ${content} </main> </div> </div> </div> <!-- Footer --> <footer class="bg-light border-top py-5 mt-5"> <div class="container"> <div class="row align-items-start"> <!-- Package Info (Left) --> ${ packageName || packageMetadata ? html` <div class="col-lg-4 col-md-6 mb-3 mb-lg-0"> <div class="mb-2"> ${ packageName ? html`<h6 class="mb-2 fw-semibold text-dark">šŸ“¦ ${packageName}</h6>` : "" } <div class="d-flex flex-wrap gap-3"> ${ homepageUrl ? html` <a href="${homepageUrl}" target="_blank" rel="noopener noreferrer" class="text-decoration-none text-muted small"> šŸ  Homepage </a> ` : "" } ${ repositoryUrl ? html` <a href="${repositoryUrl}" target="_blank" rel="noopener noreferrer" class="text-decoration-none text-muted small"> šŸ“ Repository </a> ` : "" } ${ issuesUrl ? html` <a href="${issuesUrl}" target="_blank" rel="noopener noreferrer" class="text-decoration-none text-muted small"> šŸ› Issues </a> ` : "" } ${ fundingUrl ? html` <a href="${fundingUrl}" target="_blank" rel="noopener noreferrer" class="text-decoration-none text-muted small"> šŸ’– Funding </a> ` : "" } </div> </div> </div> ` : "" } <!-- Author Attribution (Center) --> ${ authorName ? html` <div class="col-lg-4 col-md-6 mb-3 mb-lg-0 text-lg-center"> <div class="text-muted small"> <span class="fw-medium">by ${authorName}</span> ${ authorEmail ? html`<br><a href="mailto:${authorEmail}" class="text-muted text-decoration-none">${authorEmail}</a>` : "" } </div> </div> ` : "" } <!-- Glean & Timestamp (Right) --> <div class="${authorName || packageName || packageMetadata ? "col-lg-4 col-12 text-lg-end" : "col-12 text-center"}"> <div class="text-muted small"> <div class="mb-1">Generated ${timestamp}</div> <a href="https://github.com/Anonyfox/ravenjs/tree/main/packages/glean" target="_blank" rel="noopener noreferrer" class="text-decoration-none text-muted fw-medium"> ⚔ Powered by Glean </a> </div> </div> </div> </div> </footer> <!-- Bootstrap 5 JavaScript --> <script src="/popper.js" type="module"></script> <script src="/bootstrap.esm.js" type="module"></script> <!-- Enhanced Documentation Features --> <script> // Smooth scrolling for anchor links document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { e.preventDefault(); const target = document.querySelector(this.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth' }); } }); }); // Copy code block functionality (only for blocks without existing copy buttons) document.querySelectorAll('pre').forEach(block => { if (block.querySelector('code') && !block.parentElement.querySelector('button[onclick*="copyCodeBlock"]')) { const copyBtn = document.createElement('button'); copyBtn.className = 'btn btn-sm btn-outline-secondary code-copy-btn'; copyBtn.textContent = 'šŸ“‹'; copyBtn.title = 'Copy code'; copyBtn.onclick = () => { navigator.clipboard.writeText(block.textContent); copyBtn.textContent = 'āœ“'; setTimeout(() => copyBtn.textContent = 'šŸ“‹', 2000); }; block.style.position = 'relative'; block.appendChild(copyBtn); } }); // Copy import statement functionality window.copyImportStatement = function(inputId) { const input = document.getElementById(inputId); if (input) { input.select(); navigator.clipboard.writeText(input.value); const btn = input.nextElementSibling; if (btn) { const originalText = btn.textContent; btn.textContent = 'āœ“ Copied!'; setTimeout(() => btn.textContent = originalText, 2000); } } }; // Copy code block functionality window.copyCodeBlock = function(blockId) { const block = document.getElementById(blockId); if (block) { const codeElement = block.querySelector('code'); const text = codeElement ? codeElement.textContent : block.textContent; navigator.clipboard.writeText(text).then(() => { const btn = block.parentElement.querySelector('button[onclick*="copyCodeBlock"]'); if (btn) { const originalText = btn.textContent; btn.textContent = 'āœ“ Copied!'; setTimeout(() => btn.textContent = originalText, 2000); } }).catch(() => { console.error('Failed to copy code'); }); } }; // Search highlighting (basic implementation) const searchParams = new URLSearchParams(window.location.search); const searchTerm = searchParams.get('search'); if (searchTerm) { const walker = document.createTreeWalker( document.querySelector('main'), NodeFilter.SHOW_TEXT ); let node; while (node = walker.nextNode()) { if (node.textContent.toLowerCase().includes(searchTerm.toLowerCase())) { const highlightedText = node.textContent.replace( new RegExp(searchTerm, 'gi'), '<span class="search-highlight">$&</span>' ); const wrapper = document.createElement('span'); wrapper.innerHTML = highlightedText; node.parentNode.replaceChild(wrapper, node); } } } // Copy to clipboard functionality function copyToClipboard(text) { if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(() => { showCopyFeedback(); }).catch(() => { fallbackCopyTextToClipboard(text); }); } else { fallbackCopyTextToClipboard(text); } } function fallbackCopyTextToClipboard(text) { const textArea = document.createElement("textarea"); textArea.value = text; textArea.style.top = "0"; textArea.style.left = "0"; textArea.style.position = "fixed"; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); showCopyFeedback(); } catch (err) { console.error('Fallback: Could not copy text'); } document.body.removeChild(textArea); } function showCopyFeedback() { // Create temporary feedback element const feedback = document.createElement('div'); feedback.textContent = 'Copied!'; feedback.className = 'position-fixed top-50 start-50 translate-middle bg-success text-white px-3 py-2 rounded'; feedback.style.zIndex = '9999'; document.body.appendChild(feedback); setTimeout(() => { if (feedback.parentNode) { feedback.parentNode.removeChild(feedback); } }, 2000); } </script> </body> </html>`; }