Split up monolith index.js into multiple files

This commit is contained in:
2026-01-02 13:18:20 +11:00
parent 5764e41db3
commit 1d978e3244
10 changed files with 1460 additions and 1041 deletions

176
js/SearchService.js Normal file
View File

@@ -0,0 +1,176 @@
/**
* Service responsible for building and querying the search index.
* @class SearchService
*/
export class SearchService {
/**
* Creates a new SearchService instance.
* @param {EventBus} eventBus - The event bus for communication.
* @param {IndexService} indexService - The index service for document lookups.
*/
constructor(eventBus, indexService) {
/** @type {EventBus} */
this.eventBus = eventBus;
/** @type {IndexService} */
this.indexService = indexService;
/** @type {Array<SearchIndexItem>} */
this.searchIndex = [];
}
/**
* Builds the search index from a list of documents.
* @param {Array<Document>} documents - The documents to index.
*/
buildSearchIndex(documents) {
this.searchIndex = [];
this.processDocuments(documents);
}
/**
* Recursively processes documents and adds them to the search index.
* @param {Array<Document>} documents - The documents to process.
* @param {string} [parentPath=''] - The parent path for breadcrumb display.
* @private
*/
processDocuments(documents, parentPath = '') {
documents.forEach(doc => {
if (doc.type === 'folder') {
this.processFolderDocument(doc, parentPath);
} else {
this.processFileDocument(doc, parentPath);
}
});
}
/**
* Processes a folder document and its children.
* @param {Document} doc - The folder document to process.
* @param {string} parentPath - The parent path for breadcrumb display.
* @private
*/
processFolderDocument(doc, parentPath) {
const currentPath = parentPath ? `${parentPath} / ${doc.title}` : doc.title;
if (doc.path) {
if (doc.showfolderpage !== 'false') {
this.searchIndex.push({
title: doc.title,
path: doc.path,
slug: doc.slug,
location: currentPath,
type: 'folder'
});
}
if (doc.headers) {
this.addHeadersToIndex(doc, currentPath, doc.title);
}
}
if (doc.items) {
this.processDocuments(doc.items, currentPath);
}
}
/**
* Processes a file document.
* @param {Document} doc - The file document to process.
* @param {string} parentPath - The parent path for breadcrumb display.
* @private
*/
processFileDocument(doc, parentPath) {
this.searchIndex.push({
title: doc.title,
path: doc.path,
slug: doc.slug,
location: parentPath,
type: 'file'
});
if (doc.headers) {
this.addHeadersToIndex(doc, parentPath, doc.title);
}
}
/**
* Adds document headers to the search index.
* @param {Document} doc - The document containing headers.
* @param {string} location - The location path for display.
* @param {string} docTitle - The document title.
* @private
*/
addHeadersToIndex(doc, location, docTitle) {
doc.headers.forEach(header => {
this.searchIndex.push({
title: header,
path: doc.path,
slug: `${doc.slug}#${this.slugifyHeader(header)}`,
location: `${location} / ${docTitle}`,
type: 'header'
});
});
}
/**
* Converts a header text to a URL-friendly slug.
* @param {string} header - The header text to slugify.
* @returns {string} The slugified header.
* @private
*/
slugifyHeader(header) {
return header.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-');
}
/**
* Searches the index for documents matching the query.
* @param {string} query - The search query.
* @returns {Array<SearchResult>} The search results sorted by relevance.
*/
search(query) {
if (!query) return [];
query = query.toLowerCase();
return this.searchIndex
.map(item => this.scoreItem(item, query))
.filter(item => item.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, 10);
}
/**
* Calculates a relevance score for a search index item.
* @param {SearchIndexItem} item - The item to score.
* @param {string} query - The search query (lowercase).
* @returns {SearchResult} The item with its score.
* @private
*/
scoreItem(item, query) {
const titleLower = item.title.toLowerCase();
let score = 0;
if (titleLower === query) score = 100;
else if (titleLower.startsWith(query)) score = 80;
else if (titleLower.includes(query)) score = 60;
else if (item.path.toLowerCase().includes(query)) score = 40;
else if (item.location.toLowerCase().includes(query)) score = 20;
if (item.type === 'header') score += 5;
return { ...item, score };
}
}
/**
* @typedef {Object} SearchIndexItem
* @property {string} title - The item title.
* @property {string} path - The file path.
* @property {string} slug - The URL slug.
* @property {string} location - The breadcrumb location.
* @property {'folder'|'file'|'header'} type - The item type.
*/
/**
* @typedef {SearchIndexItem & {score: number}} SearchResult
*/