Spaces:
Configuration error
Configuration error
| /** | |
| * DocumentLoader Module | |
| * Handles loading and managing search documents from JSON index | |
| */ | |
| class DocumentLoader { | |
| constructor() { | |
| this.documents = {}; | |
| this.isLoaded = false; | |
| } | |
| /** | |
| * Load documents from JSON index files | |
| */ | |
| async loadDocuments() { | |
| try { | |
| const data = await this.fetchDocumentData(); | |
| this.processDocuments(data); | |
| this.isLoaded = true; | |
| console.log(`✅ Document loader initialized with ${Object.keys(this.documents).length} documents`); | |
| } catch (error) { | |
| console.error('Failed to load search documents:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Fetch document data from various possible paths | |
| */ | |
| async fetchDocumentData() { | |
| // Try different paths to account for different page depths | |
| const possiblePaths = [ | |
| './index.json', | |
| '../index.json', | |
| '../../index.json', | |
| '../../../index.json' | |
| ]; | |
| for (const path of possiblePaths) { | |
| try { | |
| const response = await fetch(path); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| console.log(`✅ Loaded search index from: ${path}`); | |
| return data; | |
| } | |
| } catch (error) { | |
| console.log(`❌ Failed to load from ${path}: ${error.message}`); | |
| } | |
| } | |
| throw new Error('Failed to load search data from any path'); | |
| } | |
| /** | |
| * Process and filter documents from raw data | |
| * Supports three formats: | |
| * 1. Array of documents (new format): [{ id, title, ... }, ...] | |
| * 2. Object with children (legacy): { children: [...] } | |
| * 3. Single document (fallback): { id, title, ... } | |
| */ | |
| processDocuments(data) { | |
| let allDocs; | |
| if (Array.isArray(data)) { | |
| // New format: root is an array of documents | |
| allDocs = data; | |
| } else if (data.children) { | |
| // Legacy format: object with children array | |
| allDocs = data.children; | |
| } else { | |
| // Fallback: single document | |
| allDocs = [data]; | |
| } | |
| // Filter out problematic documents | |
| const filteredDocs = allDocs.filter(doc => this.isValidDocument(doc)); | |
| // Store documents by ID | |
| filteredDocs.forEach(doc => { | |
| this.documents[doc.id] = this.sanitizeDocument(doc); | |
| }); | |
| console.log(`Processed ${filteredDocs.length} documents (filtered from ${allDocs.length} total)`); | |
| } | |
| /** | |
| * Check if a document is valid for indexing | |
| */ | |
| isValidDocument(doc) { | |
| const docId = doc.id || ''; | |
| return !docId.toLowerCase().includes('readme') && | |
| !docId.startsWith('_') && | |
| doc.title && | |
| doc.content; | |
| } | |
| /** | |
| * Sanitize document content for safe indexing | |
| * Supports both new schema fields and legacy fields | |
| * Preserves dynamic facets as-is | |
| */ | |
| sanitizeDocument(doc) { | |
| const sanitized = { | |
| ...doc, | |
| title: this.sanitizeText(doc.title, 200), | |
| // Add description as separate indexed field (for improved search relevance) | |
| description: this.sanitizeText(doc.description, 300), | |
| content: this.sanitizeText(doc.content, 5000), | |
| summary: this.sanitizeText(doc.summary, 500), | |
| headings: this.sanitizeHeadings(doc.headings), | |
| headings_text: this.sanitizeText(doc.headings_text, 1000), | |
| keywords: this.sanitizeArray(doc.keywords, 300), | |
| tags: this.sanitizeArray(doc.tags, 200), | |
| // Support both topics (new) and categories (legacy) | |
| topics: this.sanitizeArray(doc.topics || doc.categories, 200), | |
| // Support both audience (new) and personas (legacy) | |
| audience: this.sanitizeArray(doc.audience || doc.personas, 200), | |
| // Content type and difficulty | |
| content_type: this.sanitizeText(doc.content_type, 50), | |
| difficulty: this.sanitizeText(doc.difficulty, 50), | |
| doc_type: this.sanitizeText(doc.doc_type, 50), | |
| section_path: this.sanitizeArray(doc.section_path, 200), | |
| author: this.sanitizeText(doc.author, 100) | |
| }; | |
| // Preserve facets object (dynamic, user-defined keys) | |
| if (doc.facets && typeof doc.facets === 'object') { | |
| sanitized.facets = this.sanitizeFacets(doc.facets); | |
| } | |
| // Preserve legacy flat modality if present and no facets.modality | |
| if (doc.modality && (!doc.facets || !doc.facets.modality)) { | |
| sanitized.modality = this.sanitizeText(doc.modality, 50); | |
| } | |
| return sanitized; | |
| } | |
| /** | |
| * Sanitize facets object (dynamic keys with string or array values) | |
| */ | |
| sanitizeFacets(facets) { | |
| const sanitized = {}; | |
| Object.entries(facets).forEach(([key, value]) => { | |
| if (Array.isArray(value)) { | |
| sanitized[key] = value.map(v => String(v).substring(0, 100)); | |
| } else if (value) { | |
| sanitized[key] = String(value).substring(0, 100); | |
| } | |
| }); | |
| return sanitized; | |
| } | |
| /** | |
| * Sanitize text content with length limits | |
| */ | |
| sanitizeText(text, maxLength) { | |
| if (!text || typeof text !== 'string') return ''; | |
| return text.substring(0, maxLength); | |
| } | |
| /** | |
| * Sanitize array content | |
| */ | |
| sanitizeArray(arr, maxLength) { | |
| if (!Array.isArray(arr)) return []; | |
| return arr.map(item => String(item)).join(' ').substring(0, maxLength); | |
| } | |
| /** | |
| * Sanitize headings array | |
| */ | |
| sanitizeHeadings(headings) { | |
| if (!Array.isArray(headings)) return []; | |
| return headings.map(heading => ({ | |
| text: this.sanitizeText(heading.text, 200), | |
| level: Number(heading.level) || 1 | |
| })); | |
| } | |
| /** | |
| * Get all loaded documents | |
| */ | |
| getDocuments() { | |
| return this.documents; | |
| } | |
| /** | |
| * Get a specific document by ID | |
| */ | |
| getDocument(id) { | |
| return this.documents[id]; | |
| } | |
| /** | |
| * Get document count | |
| */ | |
| getDocumentCount() { | |
| return Object.keys(this.documents).length; | |
| } | |
| /** | |
| * Check if documents are loaded | |
| */ | |
| isReady() { | |
| return this.isLoaded && Object.keys(this.documents).length > 0; | |
| } | |
| /** | |
| * Get documents as array for indexing | |
| */ | |
| getDocumentsArray() { | |
| return Object.values(this.documents); | |
| } | |
| /** | |
| * Filter documents by criteria | |
| */ | |
| filterDocuments(filterFn) { | |
| return this.getDocumentsArray().filter(filterFn); | |
| } | |
| /** | |
| * Get document statistics | |
| */ | |
| getStatistics() { | |
| const docs = this.getDocumentsArray(); | |
| return { | |
| totalDocuments: docs.length, | |
| documentsWithSummary: docs.filter(d => d.summary).length, | |
| documentsWithHeadings: docs.filter(d => d.headings && d.headings.length > 0).length, | |
| documentsWithTags: docs.filter(d => d.tags && d.tags.length > 0).length, | |
| averageContentLength: docs.reduce((sum, d) => sum + (d.content?.length || 0), 0) / docs.length | |
| }; | |
| } | |
| } | |
| // Make DocumentLoader available globally | |
| window.DocumentLoader = DocumentLoader; | |