UNPKG

agda-web-docs-lib

Version:

Library for enhancing Agda-generated HTML documentation

312 lines 13.3 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.AgdaDocsSearcher = void 0; const fs = __importStar(require("fs")); const jsdom_1 = require("jsdom"); const path = __importStar(require("path")); /** * Class responsible for building and providing search functionality */ class AgdaDocsSearcher { /** * Generates the search index for all files in the inputDir */ static async buildSearchIndex(mappings, inputDir) { console.log('Building search index...'); const index = {}; let fileCount = 0; let entryCount = 0; try { // Read all HTML files from the input directory const files = fs.readdirSync(inputDir).filter((f) => f.endsWith('.html')); // Process files in batches to avoid memory issues const batchSize = 20; // Same batch size as position mapping for (let i = 0; i < files.length; i += batchSize) { const batch = files.slice(i, i + batchSize); // Process each file in the batch for (const file of batch) { try { const filePath = path.join(inputDir, file); const entries = this.extractSearchEntriesFromFile(filePath, mappings[file] || {}); if (entries.length > 0) { index[file] = entries; fileCount++; entryCount += entries.length; } } catch (error) { console.error(`Error extracting search entries from ${file}:`, error); } } // Add a small delay between batches to allow garbage collection if (i + batchSize < files.length) { await new Promise((resolve) => setTimeout(resolve, 50)); } } console.log(`Search index built with ${entryCount} entries from ${fileCount} files.`); return index; } catch (error) { console.error('Error building search index:', error); return {}; } } /** * Extracts search entries from a single file */ static extractSearchEntriesFromFile(filePath, positionMappings) { let dom = null; try { const content = fs.readFileSync(filePath, 'utf-8'); dom = new jsdom_1.JSDOM(content); const document = dom.window.document; const entries = []; // Extract module name const moduleName = path.basename(filePath, '.html'); // Add module entry entries.push({ type: 'module', content: moduleName, }); // Extract code blocks const codeBlocks = document.querySelectorAll('pre.Agda'); codeBlocks.forEach((block) => { const codeLines = block.querySelectorAll('.code-line'); const allLines = Array.from(codeLines); allLines.forEach((line, index) => { const lineId = line.id; if (!lineId) { return; } // Parse line number from ID format like "B1-LC15" (LC = Line Content) const lineMatch = lineId.match(/B\d+-LC(\d+)/); if (!lineMatch) { return; } const lineNumber = parseInt(lineMatch[1]); if (isNaN(lineNumber)) return; // Get the full line content const lineContent = line.textContent?.trim(); if (!lineContent) { return; } // Get surrounding context (1 line before and 1 line after) let contextBefore = ''; let contextAfter = ''; if (index > 0) { contextBefore = allLines[index - 1].textContent?.trim() || ''; } if (index < allLines.length - 1) { contextAfter = allLines[index + 1].textContent?.trim() || ''; } // Full context with line before, current line, and line after const fullContext = [contextBefore, lineContent, contextAfter] .filter((line) => line) // Remove empty lines .join('\n'); // Add the whole line as a searchable entry entries.push({ type: 'code', content: lineContent, lineNumber, context: fullContext, }); // Also still get individual identifiers const identifiers = line.querySelectorAll('[id]'); identifiers.forEach((identifier) => { const id = identifier.id; if (!id || /^\d+$/.test(id)) return; // Skip numeric IDs // Get the text content of the identifier const content = identifier.textContent?.trim(); if (!content) return; // Get position mapping if available const position = Object.keys(positionMappings).find((pos) => positionMappings[pos] === lineNumber); entries.push({ type: 'code', content, lineNumber, position, context: fullContext, }); }); }); }); // Extract headers const headers = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); headers.forEach((header) => { const content = header.textContent?.trim(); if (!content) return; entries.push({ type: 'header', content, context: content, }); }); return entries; } catch (error) { console.error(`Error extracting search entries from ${filePath}:`, error); return []; } finally { // Explicitly clean up JSDOM to free memory if (dom) { dom.window.close(); dom = null; } } } /** * Writes the search index to a JSON file in the output directory */ static writeSearchIndex(outputDir, index) { try { const outputPath = path.join(outputDir, 'search-index.json'); // Check if the index is too large by attempting to stringify it let jsonString; try { jsonString = JSON.stringify(index); } catch (error) { if (error instanceof RangeError && error.message.includes('Invalid string length')) { this.writeChunkedSearchIndex(outputDir, index); return; } throw error; } fs.writeFileSync(outputPath, jsonString); console.log(`Search index written to ${outputPath}`); } catch (error) { console.error('Error writing search index:', error); throw error; // Re-throw to ensure the process fails properly } } /** * Writes a large search index in chunks to handle size limitations * Now splits by entry count and recursively splits chunks that are too large. */ static writeChunkedSearchIndex(outputDir, index, chunkPrefix = 'chunk') { try { const files = Object.keys(index); const maxEntriesPerChunk = 1000; // Lower this if still too big let chunkIndex = 0; const chunks = {}; for (let i = 0; i < files.length;) { let entriesCount = 0; const chunkFiles = []; while (i < files.length && entriesCount < maxEntriesPerChunk) { const file = files[i]; const fileEntries = index[file]; if (entriesCount + fileEntries.length > maxEntriesPerChunk && chunkFiles.length > 0) { break; } chunkFiles.push(file); entriesCount += fileEntries.length; i++; } const chunk = {}; for (const file of chunkFiles) { chunk[file] = index[file]; } chunks[`${chunkPrefix}-${chunkIndex}`] = chunk; chunkIndex++; } // Write chunk metadata const metadataPath = path.join(outputDir, 'search-index-metadata.json'); const metadata = { version: '1.0', chunked: true, chunks: Object.keys(chunks), totalFiles: files.length, totalEntries: Object.values(index).reduce((sum, entries) => sum + entries.length, 0), }; fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2)); console.log(`Search index metadata written to ${metadataPath}`); // Write each chunk, with error handling for size for (const [chunkName, chunkIndexObj] of Object.entries(chunks)) { const chunkPath = path.join(outputDir, `search-index-${chunkName}.json`); let jsonString; try { jsonString = JSON.stringify(chunkIndexObj); } catch (error) { if (error instanceof RangeError && error.message.includes('Invalid string length')) { // If still too big, split further console.warn(`Chunk ${chunkName} too large, splitting further...`); // Recursively split and write this.writeChunkedSearchIndex(outputDir, chunkIndexObj, chunkName); continue; } throw error; } fs.writeFileSync(chunkPath, jsonString); console.log(`Search index chunk written to ${chunkPath}`); } console.log(`Search index successfully written in ${Object.keys(chunks).length} chunks`); } catch (error) { console.error('Error writing chunked search index:', error); throw error; } } /** * Returns the path to the search script * This will be included in the transformed HTML */ static getSearchScriptPath() { const possiblePaths = [ // Production path (when installed as a package) path.join(__dirname, 'scripts', 'search.js'), // Development path path.join(__dirname, '..', 'src', 'scripts', 'search.js'), // Alternative development path path.join(__dirname, '..', '..', 'src', 'scripts', 'search.js'), ]; for (const scriptPath of possiblePaths) { if (fs.existsSync(scriptPath)) { return scriptPath; } } console.warn('Warning: Could not find search.js script'); return ''; } } exports.AgdaDocsSearcher = AgdaDocsSearcher; //# sourceMappingURL=search.js.map