mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 19:26:08 +08:00 
			
		
		
		
	feat(quick_search): format multi-line results better (#6672)
This commit is contained in:
		
						commit
						ed56ed2be0
					
				
					 3 changed files with 166 additions and 19 deletions
				
			
		|  | @ -93,6 +93,8 @@ interface QuickSearchResponse { | |||
|         highlightedNotePathTitle: string; | ||||
|         contentSnippet?: string; | ||||
|         highlightedContentSnippet?: string; | ||||
|         attributeSnippet?: string; | ||||
|         highlightedAttributeSnippet?: string; | ||||
|         icon: string; | ||||
|     }>; | ||||
|     error: string; | ||||
|  | @ -241,7 +243,12 @@ export default class QuickSearchWidget extends BasicWidget { | |||
|                         <span style="flex: 1;" class="search-result-title">${result.highlightedNotePathTitle}</span> | ||||
|                     </div>`;
 | ||||
|                  | ||||
|                 // Add content snippet below the title if available
 | ||||
|                 // 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>`; | ||||
|                 } | ||||
|  |  | |||
|  | @ -35,6 +35,8 @@ class SearchResult { | |||
|     highlightedNotePathTitle?: string; | ||||
|     contentSnippet?: string; | ||||
|     highlightedContentSnippet?: string; | ||||
|     attributeSnippet?: string; | ||||
|     highlightedAttributeSnippet?: string; | ||||
|     private fuzzyScore: number; // Track fuzzy score separately
 | ||||
| 
 | ||||
|     constructor(notePathArray: string[]) { | ||||
|  |  | |||
|  | @ -468,8 +468,13 @@ function extractContentSnippet(noteId: string, searchTokens: string[], maxLength | |||
|             content = striptags(content); | ||||
|         } | ||||
| 
 | ||||
|         // Normalize whitespace
 | ||||
|         content = content.replace(/\s+/g, " ").trim(); | ||||
|         // Normalize whitespace while preserving paragraph breaks
 | ||||
|         // First, normalize multiple newlines to double newlines (paragraph breaks)
 | ||||
|         content = content.replace(/\n\s*\n/g, "\n\n"); | ||||
|         // Then normalize spaces within lines
 | ||||
|         content = content.split('\n').map(line => line.replace(/\s+/g, " ").trim()).join('\n'); | ||||
|         // Finally trim the whole content
 | ||||
|         content = content.trim(); | ||||
| 
 | ||||
|         if (!content) { | ||||
|             return ""; | ||||
|  | @ -495,21 +500,36 @@ function extractContentSnippet(noteId: string, searchTokens: string[], maxLength | |||
|         // Extract snippet
 | ||||
|         let snippet = content.substring(snippetStart, snippetStart + maxLength); | ||||
|          | ||||
|         // Try to start/end at word boundaries
 | ||||
|         if (snippetStart > 0) { | ||||
|             const firstSpace = snippet.indexOf(" "); | ||||
|             if (firstSpace > 0 && firstSpace < 20) { | ||||
|                 snippet = snippet.substring(firstSpace + 1); | ||||
|             } | ||||
|             snippet = "..." + snippet; | ||||
|         } | ||||
|          | ||||
|         if (snippetStart + maxLength < content.length) { | ||||
|             const lastSpace = snippet.lastIndexOf(" "); | ||||
|             if (lastSpace > snippet.length - 20) { | ||||
|                 snippet = snippet.substring(0, lastSpace); | ||||
|             } | ||||
|         // If snippet contains linebreaks, limit to max 4 lines and override character limit
 | ||||
|         const lines = snippet.split('\n'); | ||||
|         if (lines.length > 4) { | ||||
|             snippet = lines.slice(0, 4).join('\n'); | ||||
|             // Add ellipsis if we truncated lines
 | ||||
|             snippet = snippet + "..."; | ||||
|         } else if (lines.length > 1) { | ||||
|             // For multi-line snippets, just limit to 4 lines (keep existing snippet)
 | ||||
|             snippet = lines.slice(0, 4).join('\n'); | ||||
|             if (lines.length > 4) { | ||||
|                 snippet = snippet + "..."; | ||||
|             } | ||||
|         } else { | ||||
|             // Single line content - apply original word boundary logic
 | ||||
|             // Try to start/end at word boundaries
 | ||||
|             if (snippetStart > 0) { | ||||
|                 const firstSpace = snippet.search(/\s/); | ||||
|                 if (firstSpace > 0 && firstSpace < 20) { | ||||
|                     snippet = snippet.substring(firstSpace + 1); | ||||
|                 } | ||||
|                 snippet = "..." + snippet; | ||||
|             } | ||||
|              | ||||
|             if (snippetStart + maxLength < content.length) { | ||||
|                 const lastSpace = snippet.search(/\s[^\s]*$/); | ||||
|                 if (lastSpace > snippet.length - 20 && lastSpace > 0) { | ||||
|                     snippet = snippet.substring(0, lastSpace); | ||||
|                 } | ||||
|                 snippet = snippet + "..."; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return snippet; | ||||
|  | @ -519,6 +539,90 @@ function extractContentSnippet(noteId: string, searchTokens: string[], maxLength | |||
|     } | ||||
| } | ||||
| 
 | ||||
| function extractAttributeSnippet(noteId: string, searchTokens: string[], maxLength: number = 200): string { | ||||
|     const note = becca.notes[noteId]; | ||||
|     if (!note) { | ||||
|         return ""; | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|         // Get all attributes for this note
 | ||||
|         const attributes = note.getAttributes(); | ||||
|         if (!attributes || attributes.length === 0) { | ||||
|             return ""; | ||||
|         } | ||||
| 
 | ||||
|         let matchingAttributes: Array<{name: string, value: string, type: string}> = []; | ||||
|          | ||||
|         // Look for attributes that match the search tokens
 | ||||
|         for (const attr of attributes) { | ||||
|             const attrName = attr.name?.toLowerCase() || ""; | ||||
|             const attrValue = attr.value?.toLowerCase() || ""; | ||||
|             const attrType = attr.type || ""; | ||||
|              | ||||
|             // Check if any search token matches the attribute name or value
 | ||||
|             const hasMatch = searchTokens.some(token => { | ||||
|                 const normalizedToken = normalizeString(token.toLowerCase()); | ||||
|                 return attrName.includes(normalizedToken) || attrValue.includes(normalizedToken); | ||||
|             }); | ||||
|              | ||||
|             if (hasMatch) { | ||||
|                 matchingAttributes.push({ | ||||
|                     name: attr.name || "", | ||||
|                     value: attr.value || "", | ||||
|                     type: attrType | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (matchingAttributes.length === 0) { | ||||
|             return ""; | ||||
|         } | ||||
| 
 | ||||
|         // Limit to 4 lines maximum, similar to content snippet logic
 | ||||
|         const lines: string[] = []; | ||||
|         for (const attr of matchingAttributes.slice(0, 4)) { | ||||
|             let line = ""; | ||||
|             if (attr.type === "label") { | ||||
|                 line = attr.value ? `#${attr.name}="${attr.value}"` : `#${attr.name}`; | ||||
|             } else if (attr.type === "relation") { | ||||
|                 // For relations, show the target note title if possible
 | ||||
|                 const targetNote = attr.value ? becca.notes[attr.value] : null; | ||||
|                 const targetTitle = targetNote ? targetNote.title : attr.value; | ||||
|                 line = `~${attr.name}="${targetTitle}"`; | ||||
|             } | ||||
|              | ||||
|             if (line) { | ||||
|                 lines.push(line); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         let snippet = lines.join('\n'); | ||||
|          | ||||
|         // Apply length limit while preserving line structure
 | ||||
|         if (snippet.length > maxLength) { | ||||
|             // Try to truncate at word boundaries but keep lines intact
 | ||||
|             const truncated = snippet.substring(0, maxLength); | ||||
|             const lastNewline = truncated.lastIndexOf('\n'); | ||||
|              | ||||
|             if (lastNewline > maxLength / 2) { | ||||
|                 // If we can keep most content by truncating to last complete line
 | ||||
|                 snippet = truncated.substring(0, lastNewline); | ||||
|             } else { | ||||
|                 // Otherwise just truncate and add ellipsis
 | ||||
|                 const lastSpace = truncated.lastIndexOf(' '); | ||||
|                 snippet = truncated.substring(0, lastSpace > maxLength / 2 ? lastSpace : maxLength - 3); | ||||
|                 snippet = snippet + "..."; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return snippet; | ||||
|     } catch (e) { | ||||
|         log.error(`Error extracting attribute snippet for note ${noteId}: ${e}`); | ||||
|         return ""; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) { | ||||
|     const searchContext = new SearchContext({ | ||||
|         fastSearch: fastSearch, | ||||
|  | @ -533,9 +637,10 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) { | |||
| 
 | ||||
|     const trimmed = allSearchResults.slice(0, 200); | ||||
| 
 | ||||
|     // Extract content snippets
 | ||||
|     // Extract content and attribute snippets
 | ||||
|     for (const result of trimmed) { | ||||
|         result.contentSnippet = extractContentSnippet(result.noteId, searchContext.highlightedTokens); | ||||
|         result.attributeSnippet = extractAttributeSnippet(result.noteId, searchContext.highlightedTokens); | ||||
|     } | ||||
| 
 | ||||
|     highlightSearchResults(trimmed, searchContext.highlightedTokens, searchContext.ignoreInternalAttributes); | ||||
|  | @ -549,6 +654,8 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) { | |||
|             highlightedNotePathTitle: result.highlightedNotePathTitle, | ||||
|             contentSnippet: result.contentSnippet, | ||||
|             highlightedContentSnippet: result.highlightedContentSnippet, | ||||
|             attributeSnippet: result.attributeSnippet, | ||||
|             highlightedAttributeSnippet: result.highlightedAttributeSnippet, | ||||
|             icon: icon ?? "bx bx-note" | ||||
|         }; | ||||
|     }); | ||||
|  | @ -574,7 +681,18 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens | |||
|          | ||||
|         // Initialize highlighted content snippet
 | ||||
|         if (result.contentSnippet) { | ||||
|             result.highlightedContentSnippet = escapeHtml(result.contentSnippet).replace(/[<{}]/g, ""); | ||||
|             // 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
 | ||||
|         if (result.attributeSnippet) { | ||||
|             // Escape HTML but preserve newlines for later conversion to <br>
 | ||||
|             result.highlightedAttributeSnippet = escapeHtml(result.attributeSnippet); | ||||
|             // Remove any stray < { } that might interfere with our highlighting markers
 | ||||
|             result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/[<{}]/g, ""); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -612,6 +730,16 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens | |||
|                     contentRegex.lastIndex += 2; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Highlight in attribute snippet
 | ||||
|             if (result.highlightedAttributeSnippet) { | ||||
|                 const attributeRegex = new RegExp(escapeRegExp(token), "gi"); | ||||
|                 while ((match = attributeRegex.exec(normalizeString(result.highlightedAttributeSnippet))) !== null) { | ||||
|                     result.highlightedAttributeSnippet = wrapText(result.highlightedAttributeSnippet, match.index, token.length, "{", "}"); | ||||
|                     // 2 characters are added, so we need to adjust the index
 | ||||
|                     attributeRegex.lastIndex += 2; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -621,7 +749,17 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens | |||
|         } | ||||
|          | ||||
|         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) { | ||||
|             // Replace highlighting markers with HTML tags
 | ||||
|             result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/{/g, "<b>").replace(/}/g, "</b>"); | ||||
|             // Convert newlines to <br> tags for HTML display
 | ||||
|             result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/\n/g, "<br>"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue