Merge branch 'develop'
This commit is contained in:
11
README.md
11
README.md
@@ -29,6 +29,7 @@ Under the hood, it uses the excellent [MiniSearch](https://github.com/lucaong/mi
|
|||||||
- Resistance to typos
|
- Resistance to typos
|
||||||
- Switch between Vault and In-file search to quickly skim multiple results in a single note
|
- Switch between Vault and In-file search to quickly skim multiple results in a single note
|
||||||
- Supports `"expressions in quotes"` and `-exclusions`
|
- Supports `"expressions in quotes"` and `-exclusions`
|
||||||
|
- Filters file types with '.jpg' or '.md'
|
||||||
- Directly Insert a `[[link]]` from the search results
|
- Directly Insert a `[[link]]` from the search results
|
||||||
- Supports Vim navigation keys
|
- Supports Vim navigation keys
|
||||||
|
|
||||||
@@ -74,9 +75,15 @@ object `omnisearch` (`window.omnisearch`)
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
// API:
|
// API:
|
||||||
{
|
type OmnisearchApi = {
|
||||||
// Returns a promise that will contain the same results as the Vault modal
|
// Returns a promise that will contain the same results as the Vault modal
|
||||||
search: (query: string) => Promise<ResultNoteApi[]>
|
search: (query: string) => Promise<ResultNoteApi[]>,
|
||||||
|
// Refreshes the index
|
||||||
|
refreshIndex: () => Promise<void>
|
||||||
|
// Register a callback that will be called when the indexing is done
|
||||||
|
registerOnIndexed: (callback: () => void) => void,
|
||||||
|
// Unregister a callback that was previously registered
|
||||||
|
unregisterOnIndexed: (callback: () => void) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResultNoteApi = {
|
type ResultNoteApi = {
|
||||||
|
|||||||
@@ -4,14 +4,41 @@
|
|||||||
.omnisearch-result {
|
.omnisearch-result {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.omnisearch-result__title-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
column-gap: 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.omnisearch-result__title {
|
.omnisearch-result__title {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.omnisearch-result__folder-path {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.omnisearch-result__extension {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.omnisearch-result__counter {
|
.omnisearch-result__counter {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.omnisearch-result__body {
|
.omnisearch-result__body {
|
||||||
@@ -25,14 +52,23 @@
|
|||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
|
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.omnisearch-result__image-container {
|
||||||
|
flex-basis: 20%;
|
||||||
|
text-align: right
|
||||||
}
|
}
|
||||||
|
|
||||||
.omnisearch-highlight {
|
.omnisearch-highlight {
|
||||||
}
|
}
|
||||||
|
|
||||||
.omnisearch-default-highlight {
|
.omnisearch-default-highlight {
|
||||||
color: var(--text-normal);
|
text-decoration: underline;
|
||||||
background-color: var(--text-highlight-bg);
|
text-decoration-color: var(--text-highlight-bg);
|
||||||
|
text-decoration-thickness: 3px;
|
||||||
|
text-underline-offset: -1px;
|
||||||
|
text-decoration-skip-ink: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.omnisearch-input-container {
|
.omnisearch-input-container {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "scambier.obsidian-search",
|
"name": "scambier.obsidian-search",
|
||||||
"version": "1.11.1",
|
"version": "1.12.1-beta.1",
|
||||||
"description": "A search engine for Obsidian",
|
"description": "A search engine for Obsidian",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ResultNote } from '../globals'
|
import type { ResultNote } from '../globals'
|
||||||
import { highlighter, makeExcerpt, stringsToRegex } from '../tools/utils'
|
import {
|
||||||
|
highlighterGroups,
|
||||||
|
makeExcerpt,
|
||||||
|
stringsToRegex,
|
||||||
|
} from '../tools/utils'
|
||||||
import ResultItemContainer from './ResultItemContainer.svelte'
|
import ResultItemContainer from './ResultItemContainer.svelte'
|
||||||
|
|
||||||
export let offset: number
|
export let offset: number
|
||||||
@@ -12,8 +16,12 @@
|
|||||||
$: cleanedContent = makeExcerpt(note?.content ?? '', offset)
|
$: cleanedContent = makeExcerpt(note?.content ?? '', offset)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ResultItemContainer id={index.toString()} {selected} on:mousemove on:click>
|
<ResultItemContainer
|
||||||
|
id="{index.toString()}"
|
||||||
|
selected="{selected}"
|
||||||
|
on:mousemove
|
||||||
|
on:click>
|
||||||
<div class="omnisearch-result__body">
|
<div class="omnisearch-result__body">
|
||||||
{@html cleanedContent.replace(reg, highlighter)}
|
{@html cleanedContent.replace(reg, highlighterGroups)}
|
||||||
</div>
|
</div>
|
||||||
</ResultItemContainer>
|
</ResultItemContainer>
|
||||||
|
|||||||
@@ -2,19 +2,27 @@
|
|||||||
import { settings, showExcerpt } from 'src/settings'
|
import { settings, showExcerpt } from 'src/settings'
|
||||||
import type { ResultNote } from '../globals'
|
import type { ResultNote } from '../globals'
|
||||||
import {
|
import {
|
||||||
highlighter,
|
getExtension,
|
||||||
|
highlighterGroups,
|
||||||
|
isFileCanvas,
|
||||||
isFileImage,
|
isFileImage,
|
||||||
|
isFilePDF,
|
||||||
makeExcerpt,
|
makeExcerpt,
|
||||||
|
pathWithoutFilename,
|
||||||
removeDiacritics,
|
removeDiacritics,
|
||||||
stringsToRegex,
|
stringsToRegex,
|
||||||
} from '../tools/utils'
|
} from '../tools/utils'
|
||||||
import ResultItemContainer from './ResultItemContainer.svelte'
|
import ResultItemContainer from './ResultItemContainer.svelte'
|
||||||
|
import { setIcon } from 'obsidian'
|
||||||
|
|
||||||
export let selected = false
|
export let selected = false
|
||||||
export let note: ResultNote
|
export let note: ResultNote
|
||||||
|
|
||||||
let imagePath: string | null = null
|
let imagePath: string | null = null
|
||||||
let title = ''
|
let title = ''
|
||||||
|
let notePath = ''
|
||||||
|
let elFolderPathIcon: HTMLElement
|
||||||
|
let elFilePathIcon: HTMLElement
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
imagePath = null
|
imagePath = null
|
||||||
@@ -31,10 +39,23 @@
|
|||||||
$: cleanedContent = makeExcerpt(note.content, note.matches[0]?.offset ?? -1)
|
$: cleanedContent = makeExcerpt(note.content, note.matches[0]?.offset ?? -1)
|
||||||
$: glyph = false //cacheManager.getLiveDocument(note.path)?.doesNotExist
|
$: glyph = false //cacheManager.getLiveDocument(note.path)?.doesNotExist
|
||||||
$: {
|
$: {
|
||||||
title = settings.showShortName ? note.basename : note.path
|
title = note.basename
|
||||||
|
notePath = pathWithoutFilename(note.path)
|
||||||
if (settings.ignoreDiacritics) {
|
if (settings.ignoreDiacritics) {
|
||||||
title = removeDiacritics(title)
|
title = removeDiacritics(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
if (elFolderPathIcon) {
|
||||||
|
setIcon(elFolderPathIcon, 'folder-open')
|
||||||
|
}
|
||||||
|
if (elFilePathIcon) {
|
||||||
|
if (isFileImage(note.path)) setIcon(elFilePathIcon, 'image')
|
||||||
|
else if (isFilePDF(note.path)) setIcon(elFilePathIcon, 'file-text')
|
||||||
|
else if (isFileCanvas(note.path))
|
||||||
|
setIcon(elFilePathIcon, 'layout-dashboard')
|
||||||
|
else setIcon(elFilePathIcon, 'file')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -44,13 +65,15 @@
|
|||||||
on:click
|
on:click
|
||||||
on:mousemove
|
on:mousemove
|
||||||
selected="{selected}">
|
selected="{selected}">
|
||||||
<div style="display:flex">
|
<div>
|
||||||
<div>
|
<div class="omnisearch-result__title-container">
|
||||||
<div>
|
<span class="omnisearch-result__title">
|
||||||
<span class="omnisearch-result__title">
|
<span bind:this="{elFilePathIcon}"></span>
|
||||||
{@html title.replace(reg, highlighter)}
|
<span>{@html title.replace(reg, highlighterGroups)}</span>
|
||||||
</span>
|
<span class="omnisearch-result__extension"
|
||||||
|
>.{getExtension(note.path)}</span>
|
||||||
|
|
||||||
|
<!-- Counter -->
|
||||||
{#if note.matches.length > 0}
|
{#if note.matches.length > 0}
|
||||||
<span class="omnisearch-result__counter">
|
<span class="omnisearch-result__counter">
|
||||||
{note.matches.length} {note.matches.length > 1
|
{note.matches.length} {note.matches.length > 1
|
||||||
@@ -58,17 +81,30 @@
|
|||||||
: 'match'}
|
: 'match'}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Folder path -->
|
||||||
|
{#if notePath}
|
||||||
|
<div class="omnisearch-result__folder-path">
|
||||||
|
<span bind:this="{elFolderPathIcon}"></span>
|
||||||
|
<span>{notePath}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: row;">
|
||||||
{#if $showExcerpt}
|
{#if $showExcerpt}
|
||||||
<div class="omnisearch-result__body">
|
<div class="omnisearch-result__body">
|
||||||
{@html cleanedContent.replace(reg, highlighter)}
|
{@html cleanedContent.replace(reg, highlighterGroups)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Image -->
|
||||||
|
{#if imagePath}
|
||||||
|
<div class="omnisearch-result__image-container">
|
||||||
|
<img style="width: 100px" src="{imagePath}" alt="" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if imagePath}
|
|
||||||
<img style="width: 100px" src="{imagePath}" alt="" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</ResultItemContainer>
|
</ResultItemContainer>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const regexLineSplit = /\r?\n|\r|((\.|\?|!)( |\r?\n|\r))/g
|
|||||||
export const regexYaml = /^---\s*\n(.*?)\n?^---\s?/ms
|
export const regexYaml = /^---\s*\n(.*?)\n?^---\s?/ms
|
||||||
export const regexStripQuotes = /^"|"$|^'|'$/g
|
export const regexStripQuotes = /^"|"$|^'|'$/g
|
||||||
export const chsRegex = /[\u4e00-\u9fa5]/
|
export const chsRegex = /[\u4e00-\u9fa5]/
|
||||||
|
export const regexExtensions = /(?:^|\s)\.(\w+)/g
|
||||||
|
|
||||||
export const excerptBefore = 100
|
export const excerptBefore = 100
|
||||||
export const excerptAfter = 300
|
export const excerptAfter = 300
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
IndexingStepType,
|
IndexingStepType,
|
||||||
isCacheEnabled,
|
isCacheEnabled,
|
||||||
} from './globals'
|
} from './globals'
|
||||||
import api from './tools/api'
|
import api, { notifyOnIndexed } from './tools/api'
|
||||||
import { isFileIndexable } from './tools/utils'
|
import { isFileIndexable } from './tools/utils'
|
||||||
import { database, OmnisearchCache } from './database'
|
import { database, OmnisearchCache } from './database'
|
||||||
import * as NotesIndex from './notes-index'
|
import * as NotesIndex from './notes-index'
|
||||||
@@ -210,6 +210,7 @@ export default class OmnisearchPlugin extends Plugin {
|
|||||||
new Notice(`Omnisearch - Your files have been indexed.`)
|
new Notice(`Omnisearch - Your files have been indexed.`)
|
||||||
}
|
}
|
||||||
indexingStep.set(IndexingStepType.Done)
|
indexingStep.set(IndexingStepType.Done)
|
||||||
|
notifyOnIndexed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,22 @@ const tokenize = (text: string): string[] => {
|
|||||||
export class Omnisearch {
|
export class Omnisearch {
|
||||||
public static readonly options: Options<IndexedDocument> = {
|
public static readonly options: Options<IndexedDocument> = {
|
||||||
tokenize,
|
tokenize,
|
||||||
|
extractField: (doc, fieldName) => {
|
||||||
|
if (fieldName === 'directory') {
|
||||||
|
// return path without the filename
|
||||||
|
const parts = doc.path.split('/')
|
||||||
|
parts.pop()
|
||||||
|
return parts.join('/')
|
||||||
|
}
|
||||||
|
return (doc as any)[fieldName]
|
||||||
|
},
|
||||||
processTerm: (term: string) =>
|
processTerm: (term: string) =>
|
||||||
(settings.ignoreDiacritics ? removeDiacritics(term) : term).toLowerCase(),
|
(settings.ignoreDiacritics ? removeDiacritics(term) : term).toLowerCase(),
|
||||||
idField: 'path',
|
idField: 'path',
|
||||||
fields: [
|
fields: [
|
||||||
'basename',
|
'basename',
|
||||||
|
// Different from `path`, since `path` is the unique index and needs to include the filename
|
||||||
|
'directory',
|
||||||
'aliases',
|
'aliases',
|
||||||
'content',
|
'content',
|
||||||
'headings1',
|
'headings1',
|
||||||
@@ -168,6 +179,7 @@ export class Omnisearch {
|
|||||||
combineWith: 'AND',
|
combineWith: 'AND',
|
||||||
boost: {
|
boost: {
|
||||||
basename: settings.weightBasename,
|
basename: settings.weightBasename,
|
||||||
|
directory: settings.weightDirectory,
|
||||||
aliases: settings.weightBasename,
|
aliases: settings.weightBasename,
|
||||||
headings1: settings.weightH1,
|
headings1: settings.weightH1,
|
||||||
headings2: settings.weightH2,
|
headings2: settings.weightH2,
|
||||||
@@ -175,6 +187,13 @@ export class Omnisearch {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Filter query results to only keep files that match query.extensions (if any)
|
||||||
|
if (query.extensions.length) {
|
||||||
|
results = results.filter(r =>
|
||||||
|
query.extensions.some(e => r.id.endsWith(e))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// If the query does not return any result,
|
// If the query does not return any result,
|
||||||
// retry but with a shorter prefix limit
|
// retry but with a shorter prefix limit
|
||||||
if (!results.length) {
|
if (!results.length) {
|
||||||
@@ -348,7 +367,7 @@ export class Omnisearch {
|
|||||||
|
|
||||||
// Tags, starting with #
|
// Tags, starting with #
|
||||||
...tags,
|
...tags,
|
||||||
].filter(w => w.length > 1)
|
].filter(w => w.length > 1 || /\p{Emoji}/u.test(w))
|
||||||
|
|
||||||
// console.log(foundWords)
|
// console.log(foundWords)
|
||||||
const matches = this.getMatches(
|
const matches = this.getMatches(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { settings } from '../settings'
|
import { settings } from '../settings'
|
||||||
import { removeDiacritics, stripSurroundingQuotes } from '../tools/utils'
|
import { removeDiacritics, stripSurroundingQuotes } from '../tools/utils'
|
||||||
import { parseQuery } from '../vendor/parse-query'
|
import { parseQuery } from '../vendor/parse-query'
|
||||||
|
import { regexExtensions } from '../globals'
|
||||||
|
|
||||||
type QueryToken = {
|
type QueryToken = {
|
||||||
/**
|
/**
|
||||||
@@ -20,8 +21,13 @@ type QueryToken = {
|
|||||||
export class Query {
|
export class Query {
|
||||||
public segments: QueryToken[] = []
|
public segments: QueryToken[] = []
|
||||||
public exclusions: QueryToken[] = []
|
public exclusions: QueryToken[] = []
|
||||||
|
public extensions: string[] = []
|
||||||
|
|
||||||
constructor(text = '') {
|
constructor(text = '') {
|
||||||
|
// Extract & remove extensions from the query
|
||||||
|
this.extensions = this.extractExtensions(text)
|
||||||
|
text = this.removeExtensions(text)
|
||||||
|
|
||||||
if (settings.ignoreDiacritics) text = removeDiacritics(text)
|
if (settings.ignoreDiacritics) text = removeDiacritics(text)
|
||||||
const tokens = parseQuery(text.toLowerCase(), { tokenize: true })
|
const tokens = parseQuery(text.toLowerCase(), { tokenize: true })
|
||||||
this.exclusions = tokens.exclude.text
|
this.exclusions = tokens.exclude.text
|
||||||
@@ -59,4 +65,19 @@ export class Query {
|
|||||||
exact: stripped !== str,
|
exact: stripped !== str,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts an array of extensions like ".png" from a string
|
||||||
|
*/
|
||||||
|
private extractExtensions(str: string): string[] {
|
||||||
|
const extensions = (str.match(regexExtensions) ?? []).map(o => o.trim())
|
||||||
|
if (extensions) {
|
||||||
|
return extensions.map(ext => ext.toLowerCase())
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeExtensions(str: string): string {
|
||||||
|
return str.replace(regexExtensions, '')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type OmnisearchPlugin from './main'
|
|||||||
|
|
||||||
interface WeightingSettings {
|
interface WeightingSettings {
|
||||||
weightBasename: number
|
weightBasename: number
|
||||||
|
weightDirectory: number
|
||||||
weightH1: number
|
weightH1: number
|
||||||
weightH2: number
|
weightH2: number
|
||||||
weightH3: number
|
weightH3: number
|
||||||
@@ -32,8 +33,6 @@ export interface OmnisearchSettings extends WeightingSettings {
|
|||||||
imagesIndexing: boolean
|
imagesIndexing: boolean
|
||||||
/** Activate the small 🔍 button on Obsidian's ribbon */
|
/** Activate the small 🔍 button on Obsidian's ribbon */
|
||||||
ribbonIcon: boolean
|
ribbonIcon: boolean
|
||||||
/** Display short filenames in search results, instead of the full path */
|
|
||||||
showShortName: boolean
|
|
||||||
/** Display the small contextual excerpt in search results */
|
/** Display the small contextual excerpt in search results */
|
||||||
showExcerpt: boolean
|
showExcerpt: boolean
|
||||||
/** Render line returns with <br> in excerpts */
|
/** Render line returns with <br> in excerpts */
|
||||||
@@ -295,19 +294,6 @@ export class SettingsTab extends PluginSettingTab {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// Display note names without the full path
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Hide full path in results list')
|
|
||||||
.setDesc(
|
|
||||||
'In the search results, only show the note name, without the full path.'
|
|
||||||
)
|
|
||||||
.addToggle(toggle =>
|
|
||||||
toggle.setValue(settings.showShortName).onChange(async v => {
|
|
||||||
settings.showShortName = v
|
|
||||||
await saveSettings(this.plugin)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// Highlight results
|
// Highlight results
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName('Highlight matching words in results')
|
.setName('Highlight matching words in results')
|
||||||
@@ -333,6 +319,10 @@ export class SettingsTab extends PluginSettingTab {
|
|||||||
)
|
)
|
||||||
.addSlider(cb => this.weightSlider(cb, 'weightBasename'))
|
.addSlider(cb => this.weightSlider(cb, 'weightBasename'))
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(`File directory (default: ${DEFAULT_SETTINGS.weightDirectory})`)
|
||||||
|
.addSlider(cb => this.weightSlider(cb, 'weightDirectory'))
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(`Headings level 1 (default: ${DEFAULT_SETTINGS.weightH1})`)
|
.setName(`Headings level 1 (default: ${DEFAULT_SETTINGS.weightH1})`)
|
||||||
.addSlider(cb => this.weightSlider(cb, 'weightH1'))
|
.addSlider(cb => this.weightSlider(cb, 'weightH1'))
|
||||||
@@ -390,7 +380,6 @@ export const DEFAULT_SETTINGS: OmnisearchSettings = {
|
|||||||
PDFIndexing: false,
|
PDFIndexing: false,
|
||||||
imagesIndexing: false,
|
imagesIndexing: false,
|
||||||
|
|
||||||
showShortName: false,
|
|
||||||
ribbonIcon: true,
|
ribbonIcon: true,
|
||||||
showExcerpt: true,
|
showExcerpt: true,
|
||||||
renderLineReturnInExcerpts: true,
|
renderLineReturnInExcerpts: true,
|
||||||
@@ -399,7 +388,8 @@ export const DEFAULT_SETTINGS: OmnisearchSettings = {
|
|||||||
showPreviousQueryResults: true,
|
showPreviousQueryResults: true,
|
||||||
simpleSearch: false,
|
simpleSearch: false,
|
||||||
|
|
||||||
weightBasename: 2,
|
weightBasename: 3,
|
||||||
|
weightDirectory: 2,
|
||||||
weightH1: 1.5,
|
weightH1: 1.5,
|
||||||
weightH2: 1.3,
|
weightH2: 1.3,
|
||||||
weightH3: 1.1,
|
weightH3: 1.1,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { ResultNote } from '../globals'
|
|||||||
import { Query } from '../search/query'
|
import { Query } from '../search/query'
|
||||||
import { searchEngine } from '../search/omnisearch'
|
import { searchEngine } from '../search/omnisearch'
|
||||||
import { makeExcerpt } from './utils'
|
import { makeExcerpt } from './utils'
|
||||||
|
import { refreshIndex } from '../notes-index'
|
||||||
|
|
||||||
type ResultNoteApi = {
|
type ResultNoteApi = {
|
||||||
score: number
|
score: number
|
||||||
@@ -17,6 +18,13 @@ export type SearchMatchApi = {
|
|||||||
offset: number
|
offset: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let notified = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callbacks to be called when the search index is ready
|
||||||
|
*/
|
||||||
|
let onIndexedCallbacks: Array<() => void> = []
|
||||||
|
|
||||||
function mapResults(results: ResultNote[]): ResultNoteApi[] {
|
function mapResults(results: ResultNote[]): ResultNoteApi[] {
|
||||||
return results.map(result => {
|
return results.map(result => {
|
||||||
const { score, path, basename, foundWords, matches, content } = result
|
const { score, path, basename, foundWords, matches, content } = result
|
||||||
@@ -39,13 +47,27 @@ function mapResults(results: ResultNote[]): ResultNoteApi[] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function search(
|
async function search(q: string): Promise<ResultNoteApi[]> {
|
||||||
q: string,
|
|
||||||
options: Partial<{ excerpt: boolean }> = {}
|
|
||||||
): Promise<ResultNoteApi[]> {
|
|
||||||
const query = new Query(q)
|
const query = new Query(q)
|
||||||
const raw = await searchEngine.getSuggestions(query)
|
const raw = await searchEngine.getSuggestions(query)
|
||||||
return mapResults(raw)
|
return mapResults(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { search }
|
function registerOnIndexed(cb: () => void): void {
|
||||||
|
onIndexedCallbacks.push(cb)
|
||||||
|
// Immediately call the callback if the indexing is already ready done
|
||||||
|
if (notified) {
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unregisterOnIndexed(cb: () => void): void {
|
||||||
|
onIndexedCallbacks = onIndexedCallbacks.filter(o => o !== cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notifyOnIndexed(): void {
|
||||||
|
notified = true
|
||||||
|
onIndexedCallbacks.forEach(cb => cb())
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { search, registerOnIndexed, unregisterOnIndexed, refreshIndex }
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
regexLineSplit,
|
regexLineSplit,
|
||||||
regexStripQuotes,
|
regexStripQuotes,
|
||||||
regexYaml,
|
regexYaml,
|
||||||
|
SPACE_OR_PUNCTUATION,
|
||||||
type SearchMatch,
|
type SearchMatch,
|
||||||
} from '../globals'
|
} from '../globals'
|
||||||
import { settings } from '../settings'
|
import { settings } from '../settings'
|
||||||
@@ -25,6 +26,12 @@ export function highlighter(str: string): string {
|
|||||||
return `<span class="${highlightClass}">${str}</span>`
|
return `<span class="${highlightClass}">${str}</span>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function highlighterGroups(...args: any[]) {
|
||||||
|
if (args[1] && args[2])
|
||||||
|
return `${args[1]}<span class="${highlightClass}">${args[2]}</span>`
|
||||||
|
return '<no content>'
|
||||||
|
}
|
||||||
|
|
||||||
export function escapeHTML(html: string): string {
|
export function escapeHTML(html: string): string {
|
||||||
return html
|
return html
|
||||||
.replaceAll('&', '&')
|
.replaceAll('&', '&')
|
||||||
@@ -43,6 +50,12 @@ export function removeFrontMatter(text: string): string {
|
|||||||
return text.replace(regexYaml, '')
|
return text.replace(regexYaml, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pathWithoutFilename(path: string): string {
|
||||||
|
const split = path.split('/')
|
||||||
|
split.pop()
|
||||||
|
return split.join('/')
|
||||||
|
}
|
||||||
|
|
||||||
export function wait(ms: number): Promise<void> {
|
export function wait(ms: number): Promise<void> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
setTimeout(resolve, ms)
|
setTimeout(resolve, ms)
|
||||||
@@ -72,12 +85,16 @@ export function getAllIndices(text: string, regex: RegExp): SearchMatch[] {
|
|||||||
*/
|
*/
|
||||||
export function stringsToRegex(strings: string[]): RegExp {
|
export function stringsToRegex(strings: string[]): RegExp {
|
||||||
if (!strings.length) return /^$/g
|
if (!strings.length) return /^$/g
|
||||||
// \\b is "word boundary", and is not applied if the user uses the cm-chs-patch plugin
|
// Default word split is not applied if the user uses the cm-chs-patch plugin
|
||||||
const joined = strings
|
const joined =
|
||||||
.map(s => (getChsSegmenter() ? '' : '\\b') + escapeRegex(s))
|
'(' +
|
||||||
.join('|')
|
(getChsSegmenter() ? '' : SPACE_OR_PUNCTUATION.source) +
|
||||||
const reg = new RegExp(`(${joined})`, 'gi')
|
')' +
|
||||||
// console.log(reg)
|
'(' +
|
||||||
|
strings.map(s => escapeRegex(s)).join('|') +
|
||||||
|
')'
|
||||||
|
|
||||||
|
const reg = new RegExp(`${joined}`, 'giu')
|
||||||
return reg
|
return reg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,13 +266,12 @@ export function isFileIndexable(path: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isFileImage(path: string): boolean {
|
export function isFileImage(path: string): boolean {
|
||||||
return (
|
const ext = getExtension(path)
|
||||||
path.endsWith('.png') || path.endsWith('.jpg') || path.endsWith('.jpeg')
|
return ext === 'png' || ext === 'jpg' || ext === 'jpeg'
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFilePDF(path: string): boolean {
|
export function isFilePDF(path: string): boolean {
|
||||||
return path.endsWith('.pdf')
|
return getExtension(path) === 'pdf'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFilePlaintext(path: string): boolean {
|
export function isFilePlaintext(path: string): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user