Search and highlight
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: ['./'],
|
||||||
|
|||||||
117
src/main.ts
117
src/main.ts
@@ -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
|
||||||
|
.search(query, {
|
||||||
prefix: true,
|
prefix: true,
|
||||||
fuzzy: term => (term.length > 4 ? 0.2 : false),
|
fuzzy: term => (term.length > 4 ? 0.2 : false),
|
||||||
combineWith: 'AND',
|
combineWith: 'AND',
|
||||||
// processTerm: term => term.length <= minLength ? false : term,
|
// processTerm: term => term.length <= minLength ? false : term,
|
||||||
boost: { title: 2 },
|
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, '')
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
/* Sets all the text color to red! */
|
|
||||||
body {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user