tune
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
// 中文搜索擴展 - 基於 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);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user