2025-07-18 15:38:28 +08:00

393 lines
11 KiB
JavaScript

// 中文搜索擴展 - 基於 lunr.js 和 TinySegmenter
// 此文件將被 DocFX modern template 自動引入
// Substring search implementation for DocFX
export default {
// 啟動腳本
start: async () => {
// Override default search function
if (typeof window.searchData !== 'undefined') {
window._originalSearchData = window.searchData;
}
// Override the search function
window.searchData = {};
window.searchDataRequest = null;
await initSubstringSearch();
}
}
async function initSubstringSearch() {
// Wait for search box to be available
let attempts = 0;
while (!document.getElementById('search-query')) {
await new Promise(resolve => setTimeout(resolve, 100));
attempts++;
if (attempts > 100) { // 10 seconds timeout
return;
}
}
const searchInput = document.getElementById('search-query');
const searchButton = document.querySelector('.search-query-button');
// Create search results container if it doesn't exist
let resultsContainer = document.getElementById('search-results');
if (!resultsContainer) {
resultsContainer = document.createElement('div');
resultsContainer.id = 'search-results';
resultsContainer.className = 'search-results';
// Insert after the search input's parent container
const searchContainer = searchInput.closest('.search-container') || searchInput.parentElement;
if (searchContainer && searchContainer.parentElement) {
searchContainer.parentElement.insertBefore(resultsContainer, searchContainer.nextSibling);
}
}
// Add some basic styles
const style = document.createElement('style');
style.textContent = `
.search-results {
border: 1px solid var(--md-sys-color-outline-variant, #ddd);
border-radius: 4px;
box-shadow: var(--md-sys-elevation-level2, 0 2px 4px rgba(0,0,0,0.1));
padding: 16px;
margin: 16px;
max-width: calc(100% - 32px);
box-sizing: border-box;
}
.search-result {
padding: 12px;
border-bottom: 1px solid var(--md-sys-color-outline-variant, #eee);
margin-bottom: 8px;
}
.search-result:last-child {
border-bottom: none;
}
.search-result-item {
text-decoration: none;
color: inherit;
display: block;
}
.search-result h3 {
color: var(--md-sys-color-primary, #0366d6);
margin: 0 0 8px 0;
font-size: 18px;
}
.search-result p {
margin: 0;
font-size: 14px;
}
.search-highlight {
background-color: var(--md-sys-color-tertiary-container, #0c0);
padding: 2px 0;
border-radius: 2px;
font-weight: bold;
}
.no-results {
padding: 16px;
text-align: center;
font-size: 16px;
}
.search-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
border-bottom: 1px solid var(--md-sys-color-outline-variant, #eee);
padding-bottom: 8px;
}
.search-title {
font-size: 20px;
margin: 0;
}
.search-close {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
color: var(--md-sys-color-on-surface-variant, #666);
padding: 4px 8px;
border-radius: 4px;
}
.search-close:hover {
background-color: var(--md-sys-color-surface-variant, #f0f0f0);
color: var(--md-sys-color-on-surface, #000);
}
.main-content-hidden {
display: none;
}
`;
document.head.appendChild(style);
// Enable search input if it's disabled
if (searchInput.disabled) {
searchInput.disabled = false;
}
// Try to get search data
try {
const searchData = await loadSearchData();
if (!searchData) {
return;
}
// Remove existing event listeners
const newSearchInput = searchInput.cloneNode(true);
searchInput.parentNode.replaceChild(newSearchInput, searchInput);
// Prevent form submission
const searchForm = newSearchInput.closest('form');
if (searchForm) {
searchForm.addEventListener('submit', function(e) {
e.preventDefault();
return false;
});
}
// Handle Enter key
newSearchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
const query = e.target.value.toLowerCase().trim();
if (query.length >= 2) {
const results = performSubstringSearch(query, searchData);
displaySearchResults(results);
}
}
});
// Bind our search event
newSearchInput.addEventListener('input', debounce(async function(e) {
const query = e.target.value.toLowerCase().trim();
if (query.length < 2) {
clearSearchResults();
return;
}
const results = performSubstringSearch(query, searchData);
displaySearchResults(results);
}, 300));
// Handle search button click
if (searchButton) {
const newSearchButton = searchButton.cloneNode(true);
searchButton.parentNode.replaceChild(newSearchButton, searchButton);
newSearchButton.addEventListener('click', function(e) {
e.preventDefault();
const query = newSearchInput.value.toLowerCase().trim();
if (query.length >= 2) {
const results = performSubstringSearch(query, searchData);
displaySearchResults(results);
}
});
}
// Position the search results container
const searchContainer = newSearchInput.closest('.search-container') || newSearchInput.parentElement;
searchContainer.style.position = 'relative';
} catch (error) {
console.error('Failed to initialize search:', error);
}
}
// Load search index from window.searchData
async function loadSearchData() {
try {
const baseUrl = document.querySelector('meta[name="docfx:rel"]')?.content || '';
const response = await fetch(baseUrl + 'index.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return transformSearchData(data, baseUrl);
} catch (error) {
console.error('Error loading search data:', {
message: error.message,
stack: error.stack
});
return null;
}
}
// Transform the data into searchable format
function transformSearchData(data, baseUrl = '/') {
if (!data || typeof data !== 'object') {
return null;
}
const searchableData = Object.values(data)
.filter(item => item && (item.title || item.summary))
.map(item => ({
title: item.title || '',
summary: item.summary || '',
href: baseUrl + (item.href || '').replace(/^\//, '') // Ensure proper base URL prefix
}));
return searchableData;
}
// Perform substring search
function performSubstringSearch(query, data) {
return data.filter(item => {
// 組合所有可搜索的文字
const searchableText = [
item.title,
item.summary
]
.filter(Boolean)
.join(' ')
.toLowerCase();
// 執行搜索匹配
return searchableText.includes(query.toLowerCase());
}).slice(0, 10); // 限制返回前10筆結果
}
// Get context around search query
function getSearchContext(text, query, contextLength = 50) {
if (!text) return '';
const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase();
const index = lowerText.indexOf(lowerQuery);
if (index === -1) return text.slice(0, 100) + '...';
const start = Math.max(0, index - contextLength);
const end = Math.min(text.length, index + query.length + contextLength);
let context = text.slice(start, end);
// Add ellipsis if we're not at the start/end
if (start > 0) context = '...' + context;
if (end < text.length) context = context + '...';
return context;
}
// Highlight search query in text
function highlightText(text, query) {
if (!text || !query) return text;
const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase();
let lastIndex = 0;
let result = '';
let currentIndex = lowerText.indexOf(lowerQuery);
while (currentIndex !== -1) {
// Add text before match
result += text.slice(lastIndex, currentIndex);
// Add highlighted match
result += `<mark class="search-highlight">${text.slice(currentIndex, currentIndex + query.length)}</mark>`;
lastIndex = currentIndex + query.length;
currentIndex = lowerText.indexOf(lowerQuery, lastIndex);
}
// Add remaining text
result += text.slice(lastIndex);
return result;
}
// Display search results with better Chinese support
function displaySearchResults(results) {
const mainContent = document.querySelector('main') || document.querySelector('.main') || document.querySelector('#main');
let resultsContainer = document.getElementById('search-results');
if (!resultsContainer) {
resultsContainer = document.createElement('div');
resultsContainer.id = 'search-results';
resultsContainer.className = 'search-results card';
if (mainContent) {
mainContent.parentElement.insertBefore(resultsContainer, mainContent);
} else {
document.body.appendChild(resultsContainer);
}
}
if (!results || results.length === 0) {
resultsContainer.innerHTML = '<p class="no-results">No results found</p>';
if (mainContent) {
mainContent.classList.remove('main-content-hidden');
}
return;
}
const searchInput = document.getElementById('search-query');
const query = searchInput ? searchInput.value.trim() : '';
const html = `
<div class="search-header">
<h2 class="search-title">Search Results (${results.length})</h2>
<button class="search-close" title="Close search results">✕</button>
</div>
<div class="sr-items">
${results.map(result => `
<div class="sr-item">
<div class="item-title">
<a href="${result.href}">
${highlightText(result.title, query) || 'No Title'}
</a>
</div>
<div class="item-href">${result.href}</div>
<div class="item-brief">
${result.summary ? `${highlightText(getSearchContext(result.summary, query), query)}` : ''}
</div>
</div>
`).join('')}
</div>
`;
resultsContainer.innerHTML = html;
if (mainContent) {
mainContent.classList.add('main-content-hidden');
}
const closeButton = resultsContainer.querySelector('.search-close');
if (closeButton) {
closeButton.addEventListener('click', function() {
clearSearchResults();
if (mainContent) {
mainContent.classList.remove('main-content-hidden');
}
});
}
resultsContainer.style.display = 'block';
}
// Clear search results
function clearSearchResults() {
const resultsContainer = document.getElementById('search-results');
if (resultsContainer) {
resultsContainer.innerHTML = '';
resultsContainer.style.display = 'none';
}
// Show main content again
const mainContent = document.querySelector('main') || document.querySelector('.main') || document.querySelector('#main');
if (mainContent) {
mainContent.classList.remove('main-content-hidden');
}
}
// Debounce helper
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}