Search and highlight

This commit is contained in:
Simon Cambier
2022-04-09 15:16:19 +02:00
parent ae8e868215
commit 3027b6655f
4 changed files with 105 additions and 48 deletions

View File

@@ -1,4 +1,16 @@
/* Sets all the text color to red! */ .osresult__title {
body { /* font-size: var(--font-adaptive-normal); */
color: red; }
.osresult__body {
white-space: normal;
font-size: small;
word-wrap: normal;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
color: var(--text-muted);
} }

View File

@@ -48,8 +48,6 @@ esbuild
outfile: path.join('./dist', 'main.js'), outfile: path.join('./dist', 'main.js'),
plugins: [ plugins: [
copy({ copy({
// this is equal to process.cwd(), which means we use cwd path as base path to resolve `to` path
// if not specified, this plugin uses ESBuild.build outdir/outfile options as base path.
assets: { assets: {
from: ['./assets/*'], from: ['./assets/*'],
to: ['./'], to: ['./'],

View File

@@ -3,32 +3,43 @@ import MiniSearch from 'minisearch'
import removeMarkdown from 'remove-markdown' import removeMarkdown from 'remove-markdown'
type OmnisearchMatch = { type OmnisearchMatch = {
path: string
body: string
title: string
}
type Note = {
path: string path: string
content: string content: string
} }
export default class OmnisearchPlugin extends Plugin { export default class OmnisearchPlugin extends Plugin {
minisearch: MiniSearch<OmnisearchMatch> minisearch: MiniSearch<Note>
files: TFile[] files: TFile[]
contents: Record<string, string> contents: Record<string, string>
async onload(): Promise<void> { setupIndex(): void {
this.contents = {} this.minisearch = new MiniSearch<Note>({
this.minisearch = new MiniSearch<OmnisearchMatch>({
idField: 'path', idField: 'path',
fields: ['content', 'title'], fields: ['content', 'title'],
storeFields: ['path'], storeFields: ['path'],
extractField: (document, fieldName) => { extractField: (document, fieldName) => {
if (fieldName === 'title') return getNoteTitle(document.content) if (fieldName === 'title') return getFirstLine(document.content)
return (document as any)[fieldName] as string return (document as any)[fieldName] as string
}, },
}) })
}
async onload(): Promise<void> {
this.contents = {}
this.setupIndex()
this.app.workspace.onLayoutReady(async () => { this.app.workspace.onLayoutReady(async () => {
this.files = this.app.vault.getMarkdownFiles() this.files = this.app.vault.getMarkdownFiles()
for (const file of this.files) { for (const file of this.files) {
const content = await this.app.vault.cachedRead(file) const content = await this.app.vault.cachedRead(file)
this.contents[file.path] = truncateBody(getNoteBody(content)) this.contents[file.path] = clearContent(content) // truncateText(clearContent(content))
this.minisearch.add({ content, path: file.path }) this.minisearch.add({ content, path: file.path })
} }
console.log('minisearch loaded') console.log('minisearch loaded')
@@ -52,64 +63,104 @@ class OmnisearchModal extends SuggestModal<OmnisearchMatch> {
constructor(plugin: OmnisearchPlugin) { constructor(plugin: OmnisearchPlugin) {
super(plugin.app) super(plugin.app)
this.plugin = plugin this.plugin = plugin
this.setPlaceholder('Type to search through your notes')
} }
getSuggestions(query: string): OmnisearchMatch[] { getSuggestions(query: string): OmnisearchMatch[] {
const results = this.plugin.minisearch.search(query, { const results = this.plugin.minisearch
prefix: true, .search(query, {
fuzzy: term => (term.length > 4 ? 0.2 : false), prefix: true,
combineWith: 'AND', fuzzy: term => (term.length > 4 ? 0.2 : false),
// processTerm: term => term.length <= minLength ? false : term, combineWith: 'AND',
boost: { title: 2 }, // processTerm: term => term.length <= minLength ? false : term,
}) boost: { title: 2 },
})
.sort((a, b) => b.score - a.score)
return results.map(result => { return results.map(result => {
const file = this.plugin.files.find(f => f.path === result.id) const file = this.plugin.files.find(f => f.path === result.id)
const content = this.plugin.contents[file.path]
// Find position of result.terms[0]
const pos = content.toLowerCase().indexOf(result.terms[0].toLowerCase())
// Splice to get 150 chars before and after
let sliced = removeFirstLine(
content.slice(
Math.max(0, pos - 150),
Math.min(content.length - 1, pos + 150),
),
)
// Highlight the word
const reg = new RegExp(result.terms[0], 'gi')
sliced = sliced.replace(
reg,
str =>
'<span class="search-result-file-matched-text">' + str + '</span>',
)
return { return {
path: file.path, path: file.path,
content: this.plugin.contents[file.path], title: getFirstLine(content),
body: sliced,
} }
}) })
} }
renderSuggestion(value: OmnisearchMatch, el: HTMLElement) { renderSuggestion(value: OmnisearchMatch, el: HTMLElement): void {
el.createEl('div', { text: value.path }) el.createEl('div', { cls: 'osresult__title', text: value.title })
el.createEl('small', { text: value.content }) const body = el.createEl('div', { cls: 'osresult__body' })
body.innerHTML = value.body
} }
onChooseSuggestion(item: OmnisearchMatch, evt: MouseEvent | KeyboardEvent) { onChooseSuggestion(
item: OmnisearchMatch,
evt: MouseEvent | KeyboardEvent,
): void {
throw new Error('Method not implemented.') throw new Error('Method not implemented.')
} }
} }
function truncateBody(body: string): string { /**
return body.substring(0, 200) + (body.length > 0 ? '...' : '') * Strips the markdown and frontmatter
} * @param text
function getNoteTitle(note: string): string { */
return getFirstLine(removeMd(note)) function clearContent(text: string): string {
} return removeMarkdown(removeFrontMatter(text))
function getNoteBody(contents: string): string {
return truncateFirstLine(removeMd(contents))
} }
/**
* Returns the first line of the text
* @param text
* @returns
*/
function getFirstLine(text: string): string { function getFirstLine(text: string): string {
return splitLines(text.trim())[0] return splitLines(text.trim())[0]
} }
function splitLines(text: string): string[] {
return text.split(/\r?\n|\r/)
}
function removeMd(text: string): string { /**
return removeMarkdown(removeFrontMatter(text)) * Removes the first line of the text
} * @param text
function removeFrontMatter(text: string): string { * @returns
// Regex to recognize YAML Front Matter (at beginning of file, 3 hyphens, than any charecter, including newlines, then 3 hyphens). */
const YAMLFrontMatter = /^---\s*\n(.*?)\n?^---\s?/ms function removeFirstLine(text: string): string {
return text.replace(YAMLFrontMatter, '')
}
function truncateFirstLine(text: string): string {
// https://stackoverflow.com/questions/2528076/delete-a-line-of-text-in-javascript // https://stackoverflow.com/questions/2528076/delete-a-line-of-text-in-javascript
const lines = splitLines(text.trim()) const lines = splitLines(text.trim())
lines.splice(0, 1) lines.splice(0, 1)
return lines.join('\n') return lines.join('\n')
} }
function truncateText(text: string, len = 500): string {
return text.substring(0, len) + (text.length > 0 ? '...' : '')
}
function splitLines(text: string): string[] {
return text.split(/\r?\n|\r/)
}
function removeFrontMatter(text: string): string {
// Regex to recognize YAML Front Matter (at beginning of file, 3 hyphens, than any charecter, including newlines, then 3 hyphens).
const YAMLFrontMatter = /^---\s*\n(.*?)\n?^---\s?/ms
return text.replace(YAMLFrontMatter, '')
}

View File

@@ -1,4 +0,0 @@
/* Sets all the text color to red! */
body {
color: red;
}