// 中文搜索擴展 - 基於 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 += `${text.slice(currentIndex, currentIndex + query.length)}`; 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 = '
No results found
'; if (mainContent) { mainContent.classList.remove('main-content-hidden'); } return; } const searchInput = document.getElementById('search-query'); const query = searchInput ? searchInput.value.trim() : ''; const html = `