diff --git a/.claude/settings.local.json b/.claude/settings.local.json index aa1f154ac4..851ab35678 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,8 @@ "Bash(mv:*)", "Bash(rmdir:*)", "Bash(curl:*)", - "Bash(node:*)" + "Bash(node:*)", + "Bash(git checkout:*)" ], "deny": [] } diff --git a/index.html b/index.html index d652859ae6..5dfc15a5cb 100644 --- a/index.html +++ b/index.html @@ -336,6 +336,118 @@ 50% { opacity: 0.4; } } + /* Search Results */ + .search-results { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + box-shadow: var(--shadow-lg); + max-height: 500px; + overflow-y: auto; + z-index: 1000; + display: none; + } + + .search-results.show { + display: block; + } + + .search-result-item { + padding: 1rem; + border-bottom: 1px solid var(--border-light); + cursor: pointer; + transition: background-color 0.2s ease; + } + + .search-result-item:hover, + .search-result-item.active { + background: var(--bg-secondary); + } + + .search-result-item:last-child { + border-bottom: none; + } + + .search-result-title { + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.25rem; + display: flex; + align-items: center; + gap: 0.5rem; + } + + .search-result-path { + font-size: 0.85rem; + color: var(--text-muted); + margin-bottom: 0.5rem; + font-family: var(--font-mono); + } + + .search-result-snippet { + font-size: 0.9rem; + color: var(--text-secondary); + line-height: 1.4; + font-family: var(--font-mono); + background: var(--bg-secondary); + padding: 0.5rem; + border-radius: 4px; + white-space: pre-wrap; + overflow: hidden; + max-height: 60px; + } + + .search-highlight { + background: var(--accent-color); + color: var(--text-primary); + padding: 0 2px; + border-radius: 2px; + font-weight: 600; + } + + .search-loading { + padding: 2rem; + text-align: center; + color: var(--text-muted); + } + + .search-no-results { + padding: 2rem; + text-align: center; + color: var(--text-muted); + } + + .search-error { + padding: 1rem; + background: rgba(234, 67, 53, 0.1); + color: var(--danger-color); + border-radius: var(--border-radius); + margin: 1rem; + text-align: center; + } + + .file-type-badge { + background: var(--primary-color); + color: white; + padding: 0.2rem 0.5rem; + border-radius: 12px; + font-size: 0.7rem; + font-weight: 500; + text-transform: uppercase; + } + + .search-stats { + padding: 0.75rem 1rem; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-light); + font-size: 0.9rem; + color: var(--text-secondary); + } + /* Utilities */ .sr-only { position: absolute; @@ -385,7 +497,11 @@

ServiceNow Code Snippets

placeholder="Search for ServiceNow code snippets..." id="search-input" aria-label="Search code snippets" + autocomplete="off" > +
+ +
@@ -565,14 +681,611 @@

Hacktoberfest

} } - // Search functionality - document.getElementById('search-input').addEventListener('input', function(e) { - const query = e.target.value.toLowerCase(); - // This would be enhanced with actual search functionality - console.log('Searching for:', query); - - // Future: Implement search across all snippets - // This could use GitHub's API or a pre-built search index + // Search System Implementation + class CodeSnippetSearch { + constructor() { + this.searchInput = document.getElementById('search-input'); + this.searchResults = document.getElementById('search-results'); + this.fileCache = new Map(); + this.searchCache = new Map(); + this.debounceTimer = null; + this.isSearching = false; + this.currentQuery = ''; + + this.initializeSearch(); + } + + initializeSearch() { + // Add event listeners + this.searchInput.addEventListener('input', (e) => this.handleSearchInput(e)); + this.searchInput.addEventListener('focus', () => this.handleSearchFocus()); + this.searchInput.addEventListener('blur', (e) => this.handleSearchBlur(e)); + + // Close search results when clicking outside + document.addEventListener('click', (e) => { + if (!e.target.closest('.search-box')) { + this.hideSearchResults(); + } + }); + + // Handle keyboard navigation + this.searchInput.addEventListener('keydown', (e) => this.handleKeyNavigation(e)); + } + + handleSearchInput(e) { + const query = e.target.value.trim(); + this.currentQuery = query; + + // Clear previous debounce timer + if (this.debounceTimer) { + clearTimeout(this.debounceTimer); + } + + if (query.length === 0) { + this.hideSearchResults(); + return; + } + + if (query.length < 2) { + this.showMessage('Type at least 2 characters to search...'); + return; + } + + // Debounce search + this.debounceTimer = setTimeout(() => { + this.performSearch(query); + }, 300); + } + + handleSearchFocus() { + if (this.currentQuery.length >= 2 && this.searchResults.children.length > 0) { + this.showSearchResults(); + } + } + + handleSearchBlur(e) { + // Delay hiding to allow clicking on results + setTimeout(() => { + if (!this.searchResults.contains(document.activeElement) && + !this.searchResults.matches(':hover')) { + this.hideSearchResults(); + } + }, 150); + } + + handleKeyNavigation(e) { + const results = this.searchResults.querySelectorAll('.search-result-item'); + if (results.length === 0) return; + + let currentIndex = Array.from(results).findIndex(item => item.classList.contains('active')); + + switch(e.key) { + case 'ArrowDown': + e.preventDefault(); + currentIndex = (currentIndex + 1) % results.length; + this.highlightResult(results, currentIndex); + break; + case 'ArrowUp': + e.preventDefault(); + currentIndex = currentIndex <= 0 ? results.length - 1 : currentIndex - 1; + this.highlightResult(results, currentIndex); + break; + case 'Enter': + e.preventDefault(); + if (currentIndex >= 0) { + results[currentIndex].click(); + } + break; + case 'Escape': + this.hideSearchResults(); + this.searchInput.blur(); + break; + } + } + + highlightResult(results, index) { + results.forEach(item => item.classList.remove('active')); + if (index >= 0 && index < results.length) { + results[index].classList.add('active'); + results[index].scrollIntoView({ block: 'nearest' }); + } + } + + async performSearch(query) { + if (this.isSearching) return; + + this.isSearching = true; + this.showLoading(); + + try { + // Check cache first + const cacheKey = query.toLowerCase(); + if (this.searchCache.has(cacheKey)) { + this.displayResults(this.searchCache.get(cacheKey), query); + return; + } + + // Perform search + const results = await this.searchRepository(query); + + // Cache results + this.searchCache.set(cacheKey, results); + + // Display results + this.displayResults(results, query); + + } catch (error) { + console.error('Search error:', error); + this.showError('Search failed. Please try again or check your connection.'); + } finally { + this.isSearching = false; + } + } + + async searchRepository(query) { + const searchableExtensions = ['.js', '.ts', '.md', '.html', '.css', '.json', '.xml']; + const results = []; + + try { + // Use GitHub's search API for better performance + // Search in filename and content + const fileExtensions = searchableExtensions.map(ext => `extension:${ext.slice(1)}`).join(' OR '); + const searchQuery = `repo:${REPO} (${query}) (${fileExtensions})`; + + console.log('GitHub search query:', searchQuery); + + const response = await fetch( + `https://api.github.com/search/code?q=${encodeURIComponent(searchQuery)}&per_page=100`, + { + headers: { + 'Accept': 'application/vnd.github.v3+json' + } + } + ); + + if (response.status === 403) { + // Rate limit hit, fall back to manual search + return await this.fallbackSearch(query); + } + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status}`); + } + + const data = await response.json(); + + // Process search results + for (const item of data.items) { + if (this.shouldIncludeFile(item.path)) { + // Add file match first + results.push({ + type: 'file', + title: this.getFileName(item.path), + path: item.path, + snippet: `File: ${item.path}`, + url: `https://github.com/${REPO}/blob/${BRANCH}/${encodeURIComponent(item.path)}` + }); + + // Then try to get content for code matches + try { + const fileContent = await this.getFileContent(item); + if (fileContent) { + const matches = this.findMatches(fileContent, query, item.path); + results.push(...matches); + } + } catch (error) { + console.log(`Could not fetch content for ${item.path}:`, error); + } + } + } + + return results.slice(0, 20); // Limit to 20 results + + } catch (error) { + console.error('GitHub search failed:', error); + return await this.fallbackSearch(query); + } + } + + async fallbackSearch(query) { + // Fallback to recursive directory search when GitHub API fails + const results = []; + const searchTerms = query.toLowerCase().split(/\s+/); + + console.log('Using fallback search for:', query); + + try { + // Search the entire repository recursively + const allResults = await this.recursiveDirectorySearch('', searchTerms, query); + return allResults.slice(0, 20); + } catch (error) { + console.error('Fallback search failed:', error); + + // Ultimate fallback - search just category names + const mainPaths = [ + 'Core ServiceNow APIs', + 'Server-Side Components', + 'Client-Side Components', + 'Modern Development', + 'Integration', + 'Specialized Areas' + ]; + + for (const path of mainPaths) { + if (searchTerms.some(term => path.toLowerCase().includes(term))) { + results.push({ + type: 'category', + title: path, + path: path, + snippet: `Category: ${path}`, + url: this.getCategoryUrl(path) + }); + } + } + + return results; + } + } + + async recursiveDirectorySearch(path, searchTerms, originalQuery, depth = 0, maxDepth = 4) { + if (depth > maxDepth) return []; + + const results = []; + + try { + const contents = await fetchGitHubDirectory(path); + + for (const item of contents) { + if (shouldExcludeFolder(item.name) || this.shouldExcludeFile(item.name)) { + continue; + } + + const itemPath = path ? `${path}/${item.name}` : item.name; + + if (item.type === 'dir') { + // Check if directory name matches search terms + if (this.matchesSearchTerms(item.name, searchTerms)) { + results.push({ + type: 'folder', + title: item.name, + path: itemPath, + snippet: `Folder: ${itemPath}`, + url: `https://github.com/${REPO}/tree/${BRANCH}/${itemPath.split('/').map(encodeURIComponent).join('/')}` + }); + } + + // Recursively search subdirectories + const subResults = await this.recursiveDirectorySearch(itemPath, searchTerms, originalQuery, depth + 1, maxDepth); + results.push(...subResults); + + } else if (item.type === 'file') { + // Check if filename matches + if (this.matchesSearchTerms(item.name, searchTerms)) { + results.push({ + type: 'file', + title: item.name, + path: itemPath, + snippet: `File: ${itemPath}`, + url: `https://github.com/${REPO}/blob/${BRANCH}/${itemPath.split('/').map(encodeURIComponent).join('/')}` + }); + } + + // For code files, also search content + if (this.isSearchableFile(item.name)) { + try { + const fileContent = await this.getFileContentByPath(itemPath); + if (fileContent) { + const contentMatches = this.findContentMatches(fileContent, originalQuery, itemPath); + results.push(...contentMatches); + } + } catch (error) { + console.log(`Could not search content of ${itemPath}:`, error); + } + } + } + + // Limit results to prevent overwhelming the API + if (results.length >= 50) break; + } + } catch (error) { + console.error(`Error searching directory ${path}:`, error); + } + + return results; + } + + async getFileContentByPath(filePath) { + try { + const encodedPath = filePath.split('/').map(encodeURIComponent).join('/'); + const response = await fetch(`${GITHUB_API_BASE}/repos/${REPO}/contents/${encodedPath}?ref=${BRANCH}`, { + headers: { + 'Accept': 'application/vnd.github.v3+json' + } + }); + + if (!response.ok) return null; + + const data = await response.json(); + + // Handle large files + if (data.size > 100000) { // Skip files larger than 100KB + return null; + } + + return atob(data.content.replace(/\n/g, '')); + } catch (error) { + console.error('Error fetching file content:', error); + return null; + } + } + + matchesSearchTerms(text, searchTerms) { + const textLower = text.toLowerCase(); + return searchTerms.some(term => textLower.includes(term)); + } + + isSearchableFile(fileName) { + const searchableExtensions = ['.js', '.ts', '.md', '.html', '.css', '.json', '.xml', '.txt']; + return searchableExtensions.some(ext => fileName.toLowerCase().endsWith(ext)); + } + + shouldExcludeFile(fileName) { + return EXCLUDED_ROOT_FILES.includes(fileName); + } + + findContentMatches(content, query, filePath) { + const results = []; + const lines = content.split('\n'); + const queryLower = query.toLowerCase(); + const queryTerms = queryLower.split(/\s+/); + let matchCount = 0; + + // Search for matches in content + lines.forEach((line, index) => { + const lineLower = line.toLowerCase(); + + // Check if line contains any query terms + if (queryTerms.some(term => lineLower.includes(term))) { + if (matchCount < 2) { // Limit matches per file + results.push({ + type: 'code', + title: this.getFileName(filePath), + path: filePath, + lineNumber: index + 1, + snippet: this.getContextSnippet(lines, index, query), + url: `https://github.com/${REPO}/blob/${BRANCH}/${encodeURIComponent(filePath)}#L${index + 1}` + }); + matchCount++; + } + } + }); + + return results; + } + + getCategoryUrl(categoryName) { + const categoryMap = { + 'Core ServiceNow APIs': 'pages/core-apis.html', + 'Server-Side Components': 'pages/server-side-components.html', + 'Client-Side Components': 'pages/client-side-components.html', + 'Modern Development': 'pages/modern-development.html', + 'Integration': 'pages/integration.html', + 'Specialized Areas': 'pages/specialized-areas.html' + }; + return categoryMap[categoryName] || '#'; + } + + async getFileContent(item) { + try { + // Check cache first + if (this.fileCache.has(item.sha)) { + return this.fileCache.get(item.sha); + } + + const response = await fetch(item.url, { + headers: { + 'Accept': 'application/vnd.github.v3+json' + } + }); + + if (!response.ok) return null; + + const data = await response.json(); + const content = atob(data.content.replace(/\n/g, '')); + + // Cache the content + this.fileCache.set(item.sha, content); + + return content; + } catch (error) { + console.error('Error fetching file content:', error); + return null; + } + } + + shouldIncludeFile(path) { + // Skip excluded files and paths + if (EXCLUDED_ROOT_FILES.some(file => path.endsWith(file))) { + return false; + } + + if (EXCLUDED_FOLDERS.some(folder => path.includes(folder))) { + return false; + } + + return true; + } + + findMatches(content, query, filePath) { + const results = []; + const lines = content.split('\n'); + const queryLower = query.toLowerCase(); + const queryTerms = queryLower.split(/\s+/); + + // Search for matches in content + lines.forEach((line, index) => { + const lineLower = line.toLowerCase(); + + // Check if line contains all query terms + if (queryTerms.every(term => lineLower.includes(term))) { + results.push({ + type: 'code', + title: this.getFileName(filePath), + path: filePath, + lineNumber: index + 1, + snippet: this.getContextSnippet(lines, index, query), + url: `https://github.com/${REPO}/blob/${BRANCH}/${encodeURIComponent(filePath)}#L${index + 1}` + }); + } + }); + + // Also check filename + if (queryTerms.some(term => filePath.toLowerCase().includes(term))) { + results.unshift({ + type: 'file', + title: this.getFileName(filePath), + path: filePath, + snippet: `File: ${filePath}`, + url: `https://github.com/${REPO}/blob/${BRANCH}/${encodeURIComponent(filePath)}` + }); + } + + return results.slice(0, 3); // Limit matches per file + } + + getFileName(path) { + return path.split('/').pop(); + } + + getContextSnippet(lines, matchIndex, query) { + const start = Math.max(0, matchIndex - 1); + const end = Math.min(lines.length, matchIndex + 2); + const contextLines = lines.slice(start, end); + + return contextLines + .map((line, i) => { + const lineNum = start + i + 1; + const prefix = i === (matchIndex - start) ? '→ ' : ' '; + return `${lineNum.toString().padStart(3)}: ${prefix}${line}`; + }) + .join('\n'); + } + + displayResults(results, query) { + this.searchResults.innerHTML = ''; + + if (results.length === 0) { + this.showNoResults(query); + return; + } + + // Add stats + const stats = document.createElement('div'); + stats.className = 'search-stats'; + stats.textContent = `Found ${results.length} result${results.length !== 1 ? 's' : ''} for "${query}"`; + this.searchResults.appendChild(stats); + + // Add results + results.forEach((result, index) => { + const item = this.createResultItem(result, query); + this.searchResults.appendChild(item); + }); + + this.showSearchResults(); + } + + createResultItem(result, query) { + const item = document.createElement('div'); + item.className = 'search-result-item'; + + const title = document.createElement('div'); + title.className = 'search-result-title'; + + const badge = document.createElement('span'); + badge.className = 'file-type-badge'; + badge.textContent = result.type; + + title.appendChild(badge); + title.appendChild(document.createTextNode(result.title)); + + const path = document.createElement('div'); + path.className = 'search-result-path'; + path.textContent = result.path; + + const snippet = document.createElement('div'); + snippet.className = 'search-result-snippet'; + snippet.innerHTML = this.highlightText(result.snippet, query); + + item.appendChild(title); + item.appendChild(path); + item.appendChild(snippet); + + // Add click handler + item.addEventListener('click', () => { + window.open(result.url, '_blank'); + this.hideSearchResults(); + }); + + return item; + } + + highlightText(text, query) { + const queryTerms = query.toLowerCase().split(/\s+/); + let highlightedText = text; + + queryTerms.forEach(term => { + if (term.length > 1) { + const regex = new RegExp(`(${this.escapeRegex(term)})`, 'gi'); + highlightedText = highlightedText.replace(regex, '$1'); + } + }); + + return highlightedText; + } + + escapeRegex(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + + showLoading() { + this.searchResults.innerHTML = '
Searching...
'; + this.showSearchResults(); + } + + showError(message) { + this.searchResults.innerHTML = `
${message}
`; + this.showSearchResults(); + } + + showNoResults(query) { + this.searchResults.innerHTML = ` +
+
+ No results found for "${query}"
+ Try different keywords or check spelling +
+ `; + this.showSearchResults(); + } + + showMessage(message) { + this.searchResults.innerHTML = `
${message}
`; + this.showSearchResults(); + } + + showSearchResults() { + this.searchResults.classList.add('show'); + } + + hideSearchResults() { + this.searchResults.classList.remove('show'); + } + } + + // Initialize search system + let searchSystem; + document.addEventListener('DOMContentLoaded', function() { + searchSystem = new CodeSnippetSearch(); }); // Smooth scrolling for anchor links