mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-30 18:05:55 +01:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			copilot/fi
			...
			feat/fix-j
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | a94f0b217b | ||
|  | e8f08bcdfe | ||
|  | 843be0da22 | 
| @@ -30,6 +30,8 @@ export interface Suggestion { | |||||||
|     notePathTitle?: string; |     notePathTitle?: string; | ||||||
|     notePath?: string; |     notePath?: string; | ||||||
|     highlightedNotePathTitle?: string; |     highlightedNotePathTitle?: string; | ||||||
|  |     attributeSnippet?: string; | ||||||
|  |     highlightedAttributeSnippet?: string; | ||||||
|     action?: string | "create-note" | "search-notes" | "external-link" | "command"; |     action?: string | "create-note" | "search-notes" | "external-link" | "command"; | ||||||
|     parentNoteId?: string; |     parentNoteId?: string; | ||||||
|     icon?: string; |     icon?: string; | ||||||
| @@ -308,11 +310,12 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) { | |||||||
|                 displayKey: "notePathTitle", |                 displayKey: "notePathTitle", | ||||||
|                 templates: { |                 templates: { | ||||||
|                     suggestion: (suggestion) => { |                     suggestion: (suggestion) => { | ||||||
|  |                         // Handle different suggestion types | ||||||
|                         if (suggestion.action === "command") { |                         if (suggestion.action === "command") { | ||||||
|                             let html = `<div class="command-suggestion">`; |                             let html = `<div class="command-suggestion">`; | ||||||
|                             html += `<span class="command-icon ${suggestion.icon || "bx bx-terminal"}"></span>`; |                             html += `<span class="command-icon ${suggestion.icon || "bx bx-terminal"}"></span>`; | ||||||
|                             html += `<div class="command-content">`; |                             html += `<div class="command-content">`; | ||||||
|                             html += `<div class="command-name">${suggestion.highlightedNotePathTitle}</div>`; |                             html += `<div class="command-name">${suggestion.highlightedNotePathTitle || suggestion.noteTitle || ''}</div>`; | ||||||
|                             if (suggestion.commandDescription) { |                             if (suggestion.commandDescription) { | ||||||
|                                 html += `<div class="command-description">${suggestion.commandDescription}</div>`; |                                 html += `<div class="command-description">${suggestion.commandDescription}</div>`; | ||||||
|                             } |                             } | ||||||
| @@ -323,7 +326,20 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) { | |||||||
|                             html += '</div>'; |                             html += '</div>'; | ||||||
|                             return html; |                             return html; | ||||||
|                         } |                         } | ||||||
|                         return `<span class="${suggestion.icon ?? "bx bx-note"}"></span> ${suggestion.highlightedNotePathTitle}`; |                          | ||||||
|  |                         // For note suggestions, match Quick Search structure | ||||||
|  |                         // Title row with icon | ||||||
|  |                         let html = `<div style="display: flex; align-items: center; gap: 6px;">`; | ||||||
|  |                         html += `<span class="${suggestion.icon ?? "bx bx-note"}" style="flex-shrink: 0;"></span>`; | ||||||
|  |                         html += `<span class="search-result-title" style="flex: 1;">${suggestion.highlightedNotePathTitle || ''}</span>`; | ||||||
|  |                         html += `</div>`; | ||||||
|  |                          | ||||||
|  |                         // Add attribute snippet if available (inline display) | ||||||
|  |                         if (suggestion.highlightedAttributeSnippet && suggestion.highlightedAttributeSnippet.trim()) { | ||||||
|  |                             html += `<div class="search-result-attributes" style="margin-left: 20px; margin-top: 2px; color: var(--muted-text-color); font-size: 0.9em;">${suggestion.highlightedAttributeSnippet}</div>`; | ||||||
|  |                         } | ||||||
|  |                          | ||||||
|  |                         return html; | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 // we can't cache identical searches because notes can be created / renamed, new recent notes can be added |                 // we can't cache identical searches because notes can be created / renamed, new recent notes can be added | ||||||
|   | |||||||
							
								
								
									
										62
									
								
								apps/client/src/services/quick_search_renderer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								apps/client/src/services/quick_search_renderer.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | /** | ||||||
|  |  * Quick Search specific result renderer | ||||||
|  |  *  | ||||||
|  |  * This module provides HTML rendering functionality specifically for the Quick Search widget. | ||||||
|  |  * The Jump To dialog (note_autocomplete) intentionally has its own inline rendering logic | ||||||
|  |  * with different styling and layout requirements. | ||||||
|  |  *  | ||||||
|  |  * SECURITY NOTE: HTML Snippet Handling | ||||||
|  |  * The highlighted snippet fields (highlightedAttributeSnippet) contain | ||||||
|  |  * pre-sanitized HTML from the server. The server-side processing: | ||||||
|  |  * 1. Escapes all HTML using the escape-html library | ||||||
|  |  * 2. Adds safe HTML tags for display: <b> for search term highlighting | ||||||
|  |  * 3. See apps/server/src/services/search/services/search.ts for implementation | ||||||
|  |  *  | ||||||
|  |  * This means the HTML snippets can be safely inserted without additional escaping on the client side. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | import type { Suggestion } from "./note_autocomplete.js"; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Creates HTML for a Quick Search result item | ||||||
|  |  *  | ||||||
|  |  * @param result - The search result item to render | ||||||
|  |  * @returns HTML string formatted for Quick Search widget display | ||||||
|  |  */ | ||||||
|  | export function createSearchResultHtml(result: Suggestion): string { | ||||||
|  |     // Handle command action | ||||||
|  |     if (result.action === "command") { | ||||||
|  |         let html = `<div class="command-suggestion">`; | ||||||
|  |         html += `<span class="command-icon ${result.icon || "bx bx-terminal"}"></span>`; | ||||||
|  |         html += `<div class="command-content">`; | ||||||
|  |         html += `<div class="command-name">${result.highlightedNotePathTitle || ''}</div>`; | ||||||
|  |         if (result.commandDescription) { | ||||||
|  |             html += `<div class="command-description">${result.commandDescription}</div>`; | ||||||
|  |         } | ||||||
|  |         html += `</div>`; | ||||||
|  |         if (result.commandShortcut) { | ||||||
|  |             html += `<kbd class="command-shortcut">${result.commandShortcut}</kbd>`; | ||||||
|  |         } | ||||||
|  |         html += '</div>'; | ||||||
|  |         return html; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Default: render as note result | ||||||
|  |     // Wrap everything in a flex column container | ||||||
|  |     let itemHtml = `<div style="display: flex; flex-direction: column; gap: 2px;">`; | ||||||
|  |      | ||||||
|  |     // Title row with icon | ||||||
|  |     itemHtml += `<div style="display: flex; align-items: center; gap: 6px;">`; | ||||||
|  |     itemHtml += `<span class="${result.icon || 'bx bx-note'}" style="flex-shrink: 0;"></span>`; | ||||||
|  |     itemHtml += `<span class="search-result-title" style="flex: 1;">${result.highlightedNotePathTitle || result.notePathTitle || ''}</span>`; | ||||||
|  |     itemHtml += `</div>`; | ||||||
|  |      | ||||||
|  |     // Add attribute snippet if available (inline display) | ||||||
|  |     if (result.highlightedAttributeSnippet && result.highlightedAttributeSnippet.trim()) { | ||||||
|  |         itemHtml += `<div class="search-result-attributes" style="margin-left: 20px; color: var(--muted-text-color); font-size: 0.9em;">${result.highlightedAttributeSnippet}</div>`; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     itemHtml += `</div>`; | ||||||
|  |      | ||||||
|  |     return itemHtml; | ||||||
|  | } | ||||||
| @@ -840,8 +840,25 @@ table.promoted-attributes-in-tooltip th { | |||||||
|  |  | ||||||
| .aa-dropdown-menu .aa-suggestion { | .aa-dropdown-menu .aa-suggestion { | ||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|     padding: 5px; |     padding: 12px 16px; | ||||||
|     margin: 0; |     margin: 0; | ||||||
|  |     line-height: 1.4; | ||||||
|  |     position: relative; | ||||||
|  |     white-space: normal; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Add separator between Jump To suggestions like Quick Search */ | ||||||
|  | .jump-to-note-results .aa-suggestion:not(:last-child)::after { | ||||||
|  |     content: ''; | ||||||
|  |     position: absolute; | ||||||
|  |     bottom: 0; | ||||||
|  |     left: 50%; | ||||||
|  |     transform: translateX(-50%); | ||||||
|  |     width: 80%; | ||||||
|  |     height: 2px; | ||||||
|  |     background: var(--main-border-color); | ||||||
|  |     border-radius: 1px; | ||||||
|  |     opacity: 0.4; | ||||||
| } | } | ||||||
|  |  | ||||||
| .aa-dropdown-menu .aa-suggestion p { | .aa-dropdown-menu .aa-suggestion p { | ||||||
| @@ -1786,7 +1803,7 @@ textarea { | |||||||
| } | } | ||||||
|  |  | ||||||
| .jump-to-note-results .aa-suggestions { | .jump-to-note-results .aa-suggestions { | ||||||
|     padding: 1rem; |     padding: 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| /* Command palette styling */ | /* Command palette styling */ | ||||||
| @@ -2260,13 +2277,43 @@ footer.webview-footer button { | |||||||
|     padding: 1px 10px 1px 10px; |     padding: 1px 10px 1px 10px; | ||||||
| } | } | ||||||
|  |  | ||||||
| /* Search result highlighting */ | /* Search result highlighting - applies to both Quick Search and Jump To */ | ||||||
| .search-result-title b, | .search-result-title b, | ||||||
| .search-result-content b { | .search-result-content b, | ||||||
|  | .search-result-attributes b, | ||||||
|  | .quick-search .search-result-title b, | ||||||
|  | .quick-search .search-result-content b, | ||||||
|  | .quick-search .search-result-attributes b { | ||||||
|     font-weight: 900; |     font-weight: 900; | ||||||
|     color: var(--admonition-warning-accent-color); |     color: var(--admonition-warning-accent-color); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /* Quick Search specific snippet styling */ | ||||||
|  | .quick-search .search-result-content { | ||||||
|  |     font-size: 0.85em; | ||||||
|  |     color: var(--main-text-color); | ||||||
|  |     opacity: 0.7; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .quick-search .search-result-attributes { | ||||||
|  |     font-size: 0.75em; | ||||||
|  |     color: var(--muted-text-color); | ||||||
|  |     opacity: 0.5; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Jump To (autocomplete) specific snippet styling */ | ||||||
|  | .aa-dropdown-menu .search-result-content { | ||||||
|  |     font-size: 0.82em; | ||||||
|  |     color: var(--main-text-color); | ||||||
|  |     opacity: 0.6; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .aa-dropdown-menu .search-result-attributes { | ||||||
|  |     font-size: 0.75em; | ||||||
|  |     color: var(--muted-text-color); | ||||||
|  |     opacity: 0.5; | ||||||
|  | } | ||||||
|  |  | ||||||
| /* Customized icons */ | /* Customized icons */ | ||||||
|  |  | ||||||
| .bx-tn-toc::before { | .bx-tn-toc::before { | ||||||
|   | |||||||
| @@ -530,17 +530,16 @@ body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after { | |||||||
| } | } | ||||||
|  |  | ||||||
| /* List item */ | /* List item */ | ||||||
| .jump-to-note-dialog .aa-suggestions div, | .jump-to-note-dialog .aa-suggestions .aa-suggestion, | ||||||
| .note-detail-empty .aa-suggestions div { | .note-detail-empty .aa-suggestions .aa-suggestion { | ||||||
|     border-radius: 6px; |     border-radius: 6px; | ||||||
|     padding: 6px 12px; |  | ||||||
|     color: var(--menu-text-color); |     color: var(--menu-text-color); | ||||||
|     cursor: default; |     cursor: default; | ||||||
| } | } | ||||||
|  |  | ||||||
| /* Selected list item */ | /* Selected list item */ | ||||||
| .jump-to-note-dialog .aa-suggestions div.aa-cursor, | .jump-to-note-dialog .aa-suggestions .aa-suggestion.aa-cursor, | ||||||
| .note-detail-empty .aa-suggestions div.aa-cursor { | .note-detail-empty .aa-suggestions .aa-suggestion.aa-cursor { | ||||||
|     background: var(--hover-item-background-color); |     background: var(--hover-item-background-color); | ||||||
|     color: var(--hover-item-text-color); |     color: var(--hover-item-text-color); | ||||||
| } | } | ||||||
| @@ -7,6 +7,7 @@ import appContext from "../components/app_context.js"; | |||||||
| import shortcutService from "../services/shortcuts.js"; | import shortcutService from "../services/shortcuts.js"; | ||||||
| import { t } from "../services/i18n.js"; | import { t } from "../services/i18n.js"; | ||||||
| import { Dropdown, Tooltip } from "bootstrap"; | import { Dropdown, Tooltip } from "bootstrap"; | ||||||
|  | import { createSearchResultHtml } from "../services/quick_search_renderer.js"; | ||||||
|  |  | ||||||
| const TPL = /*html*/` | const TPL = /*html*/` | ||||||
| <div class="quick-search input-group input-group-sm"> | <div class="quick-search input-group input-group-sm"> | ||||||
| @@ -91,8 +92,6 @@ interface QuickSearchResponse { | |||||||
|         noteTitle: string; |         noteTitle: string; | ||||||
|         notePathTitle: string; |         notePathTitle: string; | ||||||
|         highlightedNotePathTitle: string; |         highlightedNotePathTitle: string; | ||||||
|         contentSnippet?: string; |  | ||||||
|         highlightedContentSnippet?: string; |  | ||||||
|         attributeSnippet?: string; |         attributeSnippet?: string; | ||||||
|         highlightedAttributeSnippet?: string; |         highlightedAttributeSnippet?: string; | ||||||
|         icon: string; |         icon: string; | ||||||
| @@ -236,24 +235,14 @@ export default class QuickSearchWidget extends BasicWidget { | |||||||
|  |  | ||||||
|                 const $item = $('<a class="dropdown-item" tabindex="0" href="javascript:">'); |                 const $item = $('<a class="dropdown-item" tabindex="0" href="javascript:">'); | ||||||
|                  |                  | ||||||
|                 // Build the display HTML with content snippet below the title |                 // Use the shared renderer for consistent display | ||||||
|                 let itemHtml = `<div style="display: flex; flex-direction: column;"> |                 const itemHtml = createSearchResultHtml({ | ||||||
|                     <div style="display: flex; align-items: flex-start; gap: 6px;"> |                     icon: result.icon, | ||||||
|                         <span class="${result.icon}" style="flex-shrink: 0; margin-top: 1px;"></span> |                     notePathTitle: result.notePathTitle, | ||||||
|                         <span style="flex: 1;" class="search-result-title">${result.highlightedNotePathTitle}</span> |                     highlightedNotePathTitle: result.highlightedNotePathTitle, | ||||||
|                     </div>`; |                     highlightedAttributeSnippet: result.highlightedAttributeSnippet, | ||||||
|                  |                     highlightedContentSnippet: result.highlightedContentSnippet | ||||||
|                 // Add attribute snippet (tags/attributes) below the title if available |                 }); | ||||||
|                 if (result.highlightedAttributeSnippet) { |  | ||||||
|                     itemHtml += `<div style="font-size: 0.75em; color: var(--muted-text-color); opacity: 0.5; margin-left: 20px; margin-top: 2px; line-height: 1.2;" class="search-result-attributes">${result.highlightedAttributeSnippet}</div>`; |  | ||||||
|                 } |  | ||||||
|                  |  | ||||||
|                 // Add content snippet below the attributes if available |  | ||||||
|                 if (result.highlightedContentSnippet) { |  | ||||||
|                     itemHtml += `<div style="font-size: 0.85em; color: var(--main-text-color); opacity: 0.7; margin-left: 20px; margin-top: 4px; line-height: 1.3;" class="search-result-content">${result.highlightedContentSnippet}</div>`; |  | ||||||
|                 } |  | ||||||
|                  |  | ||||||
|                 itemHtml += `</div>`; |  | ||||||
|                  |                  | ||||||
|                 $item.html(itemHtml); |                 $item.html(itemHtml); | ||||||
|                  |                  | ||||||
|   | |||||||
| @@ -33,8 +33,6 @@ class SearchResult { | |||||||
|     score: number; |     score: number; | ||||||
|     notePathTitle: string; |     notePathTitle: string; | ||||||
|     highlightedNotePathTitle?: string; |     highlightedNotePathTitle?: string; | ||||||
|     contentSnippet?: string; |  | ||||||
|     highlightedContentSnippet?: string; |  | ||||||
|     attributeSnippet?: string; |     attributeSnippet?: string; | ||||||
|     highlightedAttributeSnippet?: string; |     highlightedAttributeSnippet?: string; | ||||||
|     private fuzzyScore: number; // Track fuzzy score separately |     private fuzzyScore: number; // Track fuzzy score separately | ||||||
|   | |||||||
| @@ -285,15 +285,19 @@ function performSearch(expression: Expression, searchContext: SearchContext, ena | |||||||
|  |  | ||||||
|     const noteSet = expression.execute(allNoteSet, executionContext, searchContext); |     const noteSet = expression.execute(allNoteSet, executionContext, searchContext); | ||||||
|  |  | ||||||
|     const searchResults = noteSet.notes.map((note) => { |     const searchResults = noteSet.notes | ||||||
|  |         .map((note) => { | ||||||
|             const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath(); |             const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath(); | ||||||
|  |  | ||||||
|             if (!notePathArray) { |             if (!notePathArray) { | ||||||
|             throw new Error(`Can't find note path for note ${JSON.stringify(note.getPojo())}`); |                 // Log the orphaned note but don't throw - just skip it | ||||||
|  |                 log.info(`Skipping orphaned note without path: ${note.noteId} "${note.title}"`); | ||||||
|  |                 return null; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             return new SearchResult(notePathArray); |             return new SearchResult(notePathArray); | ||||||
|     }); |         }) | ||||||
|  |         .filter(result => result !== null) as SearchResult[]; | ||||||
|  |  | ||||||
|     for (const res of searchResults) { |     for (const res of searchResults) { | ||||||
|         res.computeScore(searchContext.fulltextQuery, searchContext.highlightedTokens, enableFuzzyMatching); |         res.computeScore(searchContext.fulltextQuery, searchContext.highlightedTokens, enableFuzzyMatching); | ||||||
| @@ -497,6 +501,12 @@ function extractContentSnippet(noteId: string, searchTokens: string[], maxLength | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         // If no match found in content, always show the beginning of the note | ||||||
|  |         // This ensures users always get context even when searching by tags/attributes | ||||||
|  |         if (!matchFound) { | ||||||
|  |             snippetStart = 0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         // Extract snippet |         // Extract snippet | ||||||
|         let snippet = content.substring(snippetStart, snippetStart + maxLength); |         let snippet = content.substring(snippetStart, snippetStart + maxLength); | ||||||
|          |          | ||||||
| @@ -575,11 +585,29 @@ function extractAttributeSnippet(noteId: string, searchTokens: string[], maxLeng | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         // If no matching attributes found but we have attributes, show the first few | ||||||
|  |         // This provides context even when searching didn't match attributes | ||||||
|  |         if (matchingAttributes.length === 0 && attributes.length > 0) { | ||||||
|  |             // Filter out internal attributes if not searching for them | ||||||
|  |             const visibleAttributes = attributes.filter(attr =>  | ||||||
|  |                 !attr.name?.startsWith("internal") &&  | ||||||
|  |                 !attr.name?.startsWith("dateCreated") &&  | ||||||
|  |                 !attr.name?.startsWith("dateModified") | ||||||
|  |             ); | ||||||
|  |              | ||||||
|  |             // Take up to 4 visible attributes | ||||||
|  |             matchingAttributes = visibleAttributes.slice(0, 4).map(attr => ({ | ||||||
|  |                 name: attr.name || "", | ||||||
|  |                 value: attr.value || "", | ||||||
|  |                 type: attr.type || "" | ||||||
|  |             })); | ||||||
|  |         } | ||||||
|  |          | ||||||
|         if (matchingAttributes.length === 0) { |         if (matchingAttributes.length === 0) { | ||||||
|             return ""; |             return ""; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Limit to 4 lines maximum, similar to content snippet logic |         // Display attributes inline, separated by spaces | ||||||
|         const lines: string[] = []; |         const lines: string[] = []; | ||||||
|         for (const attr of matchingAttributes.slice(0, 4)) { |         for (const attr of matchingAttributes.slice(0, 4)) { | ||||||
|             let line = ""; |             let line = ""; | ||||||
| @@ -597,7 +625,7 @@ function extractAttributeSnippet(noteId: string, searchTokens: string[], maxLeng | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         let snippet = lines.join('\n'); |         let snippet = lines.join(' '); // Join with spaces instead of newlines | ||||||
|          |          | ||||||
|         // Apply length limit while preserving line structure |         // Apply length limit while preserving line structure | ||||||
|         if (snippet.length > maxLength) { |         if (snippet.length > maxLength) { | ||||||
| @@ -637,9 +665,8 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) { | |||||||
|  |  | ||||||
|     const trimmed = allSearchResults.slice(0, 200); |     const trimmed = allSearchResults.slice(0, 200); | ||||||
|  |  | ||||||
|     // Extract content and attribute snippets |     // Extract attribute snippets only (content snippets removed for performance) | ||||||
|     for (const result of trimmed) { |     for (const result of trimmed) { | ||||||
|         result.contentSnippet = extractContentSnippet(result.noteId, searchContext.highlightedTokens); |  | ||||||
|         result.attributeSnippet = extractAttributeSnippet(result.noteId, searchContext.highlightedTokens); |         result.attributeSnippet = extractAttributeSnippet(result.noteId, searchContext.highlightedTokens); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -652,8 +679,6 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) { | |||||||
|             noteTitle: title, |             noteTitle: title, | ||||||
|             notePathTitle: result.notePathTitle, |             notePathTitle: result.notePathTitle, | ||||||
|             highlightedNotePathTitle: result.highlightedNotePathTitle, |             highlightedNotePathTitle: result.highlightedNotePathTitle, | ||||||
|             contentSnippet: result.contentSnippet, |  | ||||||
|             highlightedContentSnippet: result.highlightedContentSnippet, |  | ||||||
|             attributeSnippet: result.attributeSnippet, |             attributeSnippet: result.attributeSnippet, | ||||||
|             highlightedAttributeSnippet: result.highlightedAttributeSnippet, |             highlightedAttributeSnippet: result.highlightedAttributeSnippet, | ||||||
|             icon: icon ?? "bx bx-note" |             icon: icon ?? "bx bx-note" | ||||||
| @@ -679,17 +704,9 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens | |||||||
|     for (const result of searchResults) { |     for (const result of searchResults) { | ||||||
|         result.highlightedNotePathTitle = result.notePathTitle.replace(/[<{}]/g, ""); |         result.highlightedNotePathTitle = result.notePathTitle.replace(/[<{}]/g, ""); | ||||||
|          |          | ||||||
|         // Initialize highlighted content snippet |  | ||||||
|         if (result.contentSnippet) { |  | ||||||
|             // Escape HTML but preserve newlines for later conversion to <br> |  | ||||||
|             result.highlightedContentSnippet = escapeHtml(result.contentSnippet); |  | ||||||
|             // Remove any stray < { } that might interfere with our highlighting markers |  | ||||||
|             result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/[<{}]/g, ""); |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         // Initialize highlighted attribute snippet |         // Initialize highlighted attribute snippet | ||||||
|         if (result.attributeSnippet) { |         if (result.attributeSnippet) { | ||||||
|             // Escape HTML but preserve newlines for later conversion to <br> |             // Escape HTML | ||||||
|             result.highlightedAttributeSnippet = escapeHtml(result.attributeSnippet); |             result.highlightedAttributeSnippet = escapeHtml(result.attributeSnippet); | ||||||
|             // Remove any stray < { } that might interfere with our highlighting markers |             // Remove any stray < { } that might interfere with our highlighting markers | ||||||
|             result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/[<{}]/g, ""); |             result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/[<{}]/g, ""); | ||||||
| @@ -721,16 +738,6 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // Highlight in content snippet |  | ||||||
|             if (result.highlightedContentSnippet) { |  | ||||||
|                 const contentRegex = new RegExp(escapeRegExp(token), "gi"); |  | ||||||
|                 while ((match = contentRegex.exec(normalizeString(result.highlightedContentSnippet))) !== null) { |  | ||||||
|                     result.highlightedContentSnippet = wrapText(result.highlightedContentSnippet, match.index, token.length, "{", "}"); |  | ||||||
|                     // 2 characters are added, so we need to adjust the index |  | ||||||
|                     contentRegex.lastIndex += 2; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // Highlight in attribute snippet |             // Highlight in attribute snippet | ||||||
|             if (result.highlightedAttributeSnippet) { |             if (result.highlightedAttributeSnippet) { | ||||||
|                 const attributeRegex = new RegExp(escapeRegExp(token), "gi"); |                 const attributeRegex = new RegExp(escapeRegExp(token), "gi"); | ||||||
| @@ -748,18 +755,10 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens | |||||||
|             result.highlightedNotePathTitle = result.highlightedNotePathTitle.replace(/{/g, "<b>").replace(/}/g, "</b>"); |             result.highlightedNotePathTitle = result.highlightedNotePathTitle.replace(/{/g, "<b>").replace(/}/g, "</b>"); | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         if (result.highlightedContentSnippet) { |  | ||||||
|             // Replace highlighting markers with HTML tags |  | ||||||
|             result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/{/g, "<b>").replace(/}/g, "</b>"); |  | ||||||
|             // Convert newlines to <br> tags for HTML display |  | ||||||
|             result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/\n/g, "<br>"); |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         if (result.highlightedAttributeSnippet) { |         if (result.highlightedAttributeSnippet) { | ||||||
|             // Replace highlighting markers with HTML tags |             // Replace highlighting markers with HTML tags | ||||||
|             result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/{/g, "<b>").replace(/}/g, "</b>"); |             result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/{/g, "<b>").replace(/}/g, "</b>"); | ||||||
|             // Convert newlines to <br> tags for HTML display |             // Keep inline display - no conversion of newlines needed | ||||||
|             result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/\n/g, "<br>"); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user