File size: 7,466 Bytes
0722e92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
/**
 * 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;