UNPKG

agda-web-docs-lib

Version:

Library for enhancing Agda-generated HTML documentation

536 lines 26.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.AgdaDocsTransformer = void 0; const fs = __importStar(require("fs")); const jsdom_1 = require("jsdom"); const indexer_1 = require("./indexer"); const search_1 = require("./search"); class AgdaDocsTransformer { constructor(config) { this.currentFile = ''; this.config = config; this.dom = new jsdom_1.JSDOM(''); } /** * Sets the HTML content to transform */ setContent(content, filename) { this.dom = new jsdom_1.JSDOM(content); if (filename) { this.currentFile = filename; } } /** * Transforms the HTML with custom navigation */ transform() { this.addThemeInitScript(); this.addStyles(); this.addHeader(); this.addSidebar(); this.addLineNumbersToCodeBlocks(); this.addSearchFunctionality(); this.addTypePreviewContainer(); // Use the global mappings from the indexer for link transformation this.transformAgdaLinks(); return this.dom.serialize(); } /** * Adds search functionality to the page */ addSearchFunctionality() { const document = this.dom.window.document; // Get the path to the search script const searchScriptPath = search_1.AgdaDocsSearcher.getSearchScriptPath(); if (searchScriptPath && fs.existsSync(searchScriptPath)) { try { // Add script reference to the document - the script will be copied when cli.ts processes files const searchScript = document.createElement('script'); searchScript.src = 'search.js'; searchScript.defer = true; document.body.appendChild(searchScript); } catch (error) { console.error('Error adding search script:', error); } } else { console.warn('Search script not found at path:', searchScriptPath); } } /** * Transforms Agda-generated numeric position references to line number references * so that they work with the line highlighting feature */ transformAgdaLinks() { const document = this.dom.window.document; // Get all links in the document const links = document.querySelectorAll('a[href]'); // Get the mappings for the current file from global mappings in the indexer const allMappings = indexer_1.AgdaDocsIndexer.getGlobalMappings(); const currentFileMappings = this.currentFile ? allMappings[this.currentFile] || {} : {}; // Track unmapped links for warning messages const unmappedLinks = []; links.forEach((link) => { const href = link.getAttribute('href'); if (!href) return; // Check if the link has a numeric fragment identifier // It could be either in the same file (#342) or another file (Leios.Abstract.html#342) const hashMatch = href.match(/(.+?\.html)?#(\d+)$/); if (hashMatch) { const filePart = hashMatch[1] || ''; const position = hashMatch[2]; // If this is a reference to the current file if (!filePart) { if (currentFileMappings[position]) { const lineNumber = currentFileMappings[position]; // Find the appropriate block that contains this position // For multi-block files (e.g., Literate Agda), we need to determine which block // contains the target line, rather than always targeting the first block const targetCodeBlock = this.findCodeBlockContainingPosition(document, position); const blockId = targetCodeBlock ? targetCodeBlock.id : 'B1'; // Default to first block if not found // Preserve original with data attribute and change the href link.setAttribute('data-original-href', href); link.setAttribute('href', `#${blockId}-L${lineNumber}`); // Set data attributes needed for hover preview link.setAttribute('data-hoverable', 'true'); link.setAttribute('data-position', position); link.setAttribute('data-block-id', blockId); // Add class for styling link.classList.add('type-hoverable'); } else { // Keep original link but track for warning unmappedLinks.push({ href: href, element: link, text: link.textContent || '[No text content]', }); } } else { // This is a reference to another file const targetFile = filePart; // Check if we have mappings for the target file if (allMappings[targetFile] && allMappings[targetFile][position]) { // If we have the mapping, use it const lineNumber = allMappings[targetFile][position]; // Preserve original with data attribute and change the href // For cross-file references we'll use the conventional first block (B1) // since we can't determine which block contains the target position in another file link.setAttribute('data-original-href', href); link.setAttribute('href', `${targetFile}#B1-L${lineNumber}`); // Set data attributes needed for hover preview link.setAttribute('data-hoverable', 'true'); link.setAttribute('data-position', position); link.setAttribute('data-target-file', targetFile); // Add class for styling link.classList.add('type-hoverable'); } else { // Keep original link but track for warning unmappedLinks.push({ href: href, element: link, text: link.textContent || '[No text content]', }); } } } }); } /** * Attempts to find which code block contains the specified position * For literate Agda files where there are multiple code blocks */ findCodeBlockContainingPosition(document, position) { // Get all code blocks const codeBlocks = document.querySelectorAll('pre.Agda'); if (codeBlocks.length <= 1) { // If there's only one block, it must be that one return codeBlocks[0] || null; } // Try to find the exact position in any code block for (const block of codeBlocks) { const positionElement = block.querySelector(`[id="${position}"]`); if (positionElement) { return block; } // Check if any elements inside this block have data-position attribute const elementsWithPosition = block.querySelectorAll(`[data-position="${position}"]`); if (elementsWithPosition.length > 0) { return block; } } // If not found by exact position matching, we need to do more heuristics // This is a complex problem that might require analyzing the content // For now, we'll return the first block as a fallback return codeBlocks[0]; } /** * Determines if the given file path refers to the current file */ isCurrentFile(filePath) { if (!filePath) return true; return filePath === this.currentFile; } /** * Adds line numbers to Agda code blocks */ addLineNumbersToCodeBlocks() { const document = this.dom.window.document; const codeBlocks = document.querySelectorAll('pre.Agda'); // Generate a unique id for each code block to ensure line numbers are unique per block codeBlocks.forEach((codeBlock, blockIndex) => { // Assign a unique ID to the code block - using B prefix for brevity const blockId = `B${blockIndex + 1}`; codeBlock.id = blockId; // Create a container for the line numbers and code const container = document.createElement('div'); container.className = 'code-container'; // Create line numbers div const lineNumbers = document.createElement('div'); lineNumbers.className = 'line-numbers'; // Get the original HTML content const originalContent = codeBlock.innerHTML; // Split by lines for processing // Ensure we handle empty lines properly const lines = originalContent.split('\n'); // Create modified content with line IDs for anchors const lineNumbersHTML = []; const linesHTML = []; // Process non-empty lines only (skip the last empty line if it exists) const actualLines = lines.filter((line, index) => !(index === lines.length - 1 && line.trim() === '')); actualLines.forEach((line, index) => { const lineNum = index + 1; // Create unique IDs for each line by combining block ID and line number const lineId = `${blockId}-L${lineNum}`; const lineContentId = `${blockId}-LC${lineNum}`; // Create line number element with anchor using the block-specific ID lineNumbersHTML.push(`<a href="#${lineId}" id="${lineId}" class="line-number" data-line-number="${lineNum}" data-block-id="${blockId}">${lineNum}</a>`); // Add the line to the content with a block-specific ID const lineContent = line.trim() === '' ? '&nbsp;' : line; linesHTML.push(`<div id="${lineContentId}" class="code-line">${lineContent}</div>`); }); // Create the line numbers container lineNumbers.innerHTML = lineNumbersHTML.join(''); // Create the code content container const codeContent = document.createElement('div'); codeContent.className = 'code-content'; codeContent.innerHTML = linesHTML.join(''); // Create copy button with improved design const copyButton = document.createElement('button'); copyButton.className = 'copy-code-button'; copyButton.innerHTML = ` <svg width="16" height="16" viewBox="0 0 16 16"> <path fill="currentColor" d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path fill="currentColor" d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path> </svg> `; copyButton.title = 'Copy code'; codeBlock.classList.add('has-copy-button'); // Replace the original pre with our new structure container.appendChild(lineNumbers); container.appendChild(codeContent); // Replace the content of the pre tag codeBlock.innerHTML = ''; codeBlock.appendChild(container); codeBlock.appendChild(copyButton); }); // Add script reference to handle copy functionality and line highlighting const script = document.createElement('script'); script.src = 'codeBlocks.js'; script.defer = true; document.body.appendChild(script); } /** * Adds a script to initialize the theme before content loads */ addThemeInitScript() { const document = this.dom.window.document; const head = document.head; // Reference the external theme init script const themeScript = document.createElement('script'); themeScript.src = 'themeInit.js'; themeScript.defer = false; // Add to head to be executed early head.insertBefore(themeScript, head.firstChild); } /** * Adds the CSS styles to the document */ addStyles() { const document = this.dom.window.document; const head = document.head; // Add charset meta tag if not present if (!head.querySelector('meta[charset]')) { const charset = document.createElement('meta'); charset.setAttribute('charset', 'UTF-8'); head.insertBefore(charset, head.firstChild); } // Add viewport meta tag for mobile responsiveness if (!head.querySelector('meta[name="viewport"]')) { const viewport = document.createElement('meta'); viewport.setAttribute('name', 'viewport'); viewport.setAttribute('content', 'width=device-width, initial-scale=1.0'); head.appendChild(viewport); } // Add base styles link const baseStyleLink = document.createElement('link'); baseStyleLink.rel = 'stylesheet'; baseStyleLink.href = 'base.css'; head.appendChild(baseStyleLink); // Add search styles link const searchStyleLink = document.createElement('link'); searchStyleLink.rel = 'stylesheet'; searchStyleLink.href = 'search.css'; head.appendChild(searchStyleLink); // Add type preview styles link const typePreviewStyleLink = document.createElement('link'); typePreviewStyleLink.rel = 'stylesheet'; typePreviewStyleLink.href = 'typePreview.css'; head.appendChild(typePreviewStyleLink); } /** * Adds the custom header with optional back button */ addHeader() { const document = this.dom.window.document; const body = document.body; // Create header const header = document.createElement('header'); // Create left section const headerLeft = document.createElement('div'); headerLeft.className = 'header-left'; // Add menu toggle button for mobile const menuToggle = document.createElement('button'); menuToggle.className = 'menu-toggle'; menuToggle.setAttribute('title', 'Toggle navigation menu'); menuToggle.setAttribute('aria-label', 'Toggle navigation menu'); menuToggle.innerHTML = ` <svg viewBox="0 0 24 24" width="24" height="24"> <path fill="currentColor" d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z"/> </svg> `; headerLeft.appendChild(menuToggle); // Add back button if URL is provided if (this.config.backButtonUrl) { const backButton = document.createElement('a'); backButton.href = this.config.backButtonUrl; backButton.textContent = '← Back'; backButton.className = 'back-button'; headerLeft.appendChild(backButton); } // Create right section const headerRight = document.createElement('div'); headerRight.className = 'header-right'; // Add GitHub link if URL is provided if (this.config.githubUrl) { const githubLink = document.createElement('a'); githubLink.href = this.config.githubUrl; githubLink.className = 'github-link'; githubLink.target = '_blank'; githubLink.rel = 'noopener noreferrer'; githubLink.title = 'View on GitHub'; // Add GitHub icon githubLink.innerHTML = ` <svg viewBox="0 0 24 24" width="24" height="24" class="github-icon"> <path fill="currentColor" d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/> </svg> `; headerRight.appendChild(githubLink); } // Add theme toggle button const themeToggle = document.createElement('button'); themeToggle.className = 'theme-toggle'; themeToggle.setAttribute('title', 'Toggle theme'); themeToggle.innerHTML = ` <span class="light-icon"> <svg viewBox="0 0 24 24" width="24" height="24"> <path fill="currentColor" d="M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"></path> </svg> </span> <span class="dark-icon"> <svg viewBox="0 0 24 24" width="24" height="24"> <path fill="currentColor" d="M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"></path> </svg> </span> `; headerRight.appendChild(themeToggle); // Add sections to header header.appendChild(headerLeft); header.appendChild(headerRight); // Insert header at the beginning of body body.insertBefore(header, body.firstChild); // Add theme toggle script reference const themeScript = document.createElement('script'); themeScript.src = 'themeToggle.js'; themeScript.defer = true; document.body.appendChild(themeScript); // Add sidebar toggle script reference const sidebarScript = document.createElement('script'); sidebarScript.src = 'sidebarToggle.js'; sidebarScript.defer = true; document.body.appendChild(sidebarScript); } /** * Checks if a module should be included in the sidebar */ shouldIncludeModule(moduleName) { if (!this.config.modules) { return true; // Include all modules if no filter is provided } // Check if the module name starts with any of the configured module prefixes return this.config.modules.some((module) => moduleName.startsWith(module)); } /** * Adds the sidebar with modules */ addSidebar() { const document = this.dom.window.document; const body = document.body; // Create sidebar const sidebar = document.createElement('aside'); sidebar.className = 'sidebar'; // Add modules section header const modulesHeader = document.createElement('h3'); modulesHeader.textContent = 'Modules'; modulesHeader.className = 'modules-header'; sidebar.appendChild(modulesHeader); // Get all module links from the input directory const inputDir = this.config.inputDir; if (!inputDir) { console.warn('Warning: No input directory specified in config'); return; } try { // Read all HTML files from the input directory const files = fs.readdirSync(inputDir).filter((f) => f.endsWith('.html')); const modules = files.map((file) => { const moduleName = file.replace('.html', ''); return { name: moduleName, path: file, isProjectModule: this.shouldIncludeModule(moduleName), }; }); // Filter and sort modules const filteredModules = modules .filter((m) => m.isProjectModule) .sort((a, b) => a.name.localeCompare(b.name)); // Group modules by their top-level namespace const groupedModules = new Map(); filteredModules.forEach((module) => { const parts = module.name.split('.'); const group = parts[0]; if (!groupedModules.has(group)) { groupedModules.set(group, []); } groupedModules.get(group).push(module); }); // Add each group of modules groupedModules.forEach((groupModules, groupName) => { const groupHeader = document.createElement('h4'); groupHeader.textContent = groupName; groupHeader.className = 'module-group-header'; sidebar.appendChild(groupHeader); const groupList = document.createElement('ul'); groupList.className = 'module-list'; groupModules.forEach((module) => { const li = document.createElement('li'); li.className = 'module-item'; const a = document.createElement('a'); a.href = module.path; // Show only the part after the group name const displayName = module.name.split('.').slice(1).join('.'); a.textContent = displayName || module.name; // Fallback to full name if no dots a.className = 'module-link'; // Add active class if this is the current page const currentPath = document.location.pathname; if (module.path === currentPath || module.path === currentPath.split('/').pop()) { a.classList.add('active'); } li.appendChild(a); groupList.appendChild(li); }); sidebar.appendChild(groupList); }); } catch (error) { console.error('Error reading input directory:', error); } // Create main wrapper const mainWrapper = document.createElement('div'); mainWrapper.className = 'main-wrapper'; // Create main content wrapper const mainContent = document.createElement('div'); mainContent.className = 'main-content'; // Move all content except header into the wrapper const header = document.querySelector('header'); const content = Array.from(body.childNodes).filter((node) => node !== header); content.forEach((node) => mainContent.appendChild(node)); // Add sidebar and main content to the wrapper mainWrapper.appendChild(sidebar); mainWrapper.appendChild(mainContent); // Add the wrapper after the header if (header) { header.after(mainWrapper); } else { body.appendChild(mainWrapper); } } /** * Adds the preview container and script for type hover functionality */ addTypePreviewContainer() { const document = this.dom.window.document; // Create the preview container that will be positioned and shown on hover const previewContainer = document.createElement('div'); previewContainer.id = 'type-preview-container'; previewContainer.className = 'type-preview-container'; previewContainer.style.display = 'none'; document.body.appendChild(previewContainer); // Add script reference to handle hover preview functionality const script = document.createElement('script'); script.src = 'typePreview.js'; script.defer = true; document.body.appendChild(script); } } exports.AgdaDocsTransformer = AgdaDocsTransformer; //# sourceMappingURL=transformer.js.map