diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 77cefeb..974b243 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,17 +14,20 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - - uses: pnpm/action-setup@v2.1.0 + - uses: actions/checkout@v4 with: - version: 7.17.0 + fetch-depth: 0 + + - uses: pnpm/action-setup@v4 + with: + version: 9.3.0 run_install: true - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: - node-version: "18.x" + cache: 'pnpm' + node-version: "20.x" - name: Build id: build @@ -37,6 +40,18 @@ jobs: ls echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" + - name: Generate a changelog + uses: orhun/git-cliff-action@v3 + id: git-cliff + with: + config: cliff.toml + args: --verbose + env: + GITHUB_REPO: ${{ github.repository }} + + - name: Print the changelog + run: cat "${{ steps.git-cliff.outputs.changelog }}" + - name: Create Release id: create_release uses: actions/create-release@v1 @@ -46,7 +61,8 @@ jobs: with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} - draft: false + body: ${{ steps.git-cliff.outputs.changelog }} + draft: true prerelease: false - name: Upload zip file diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..8a8ce7c --- /dev/null +++ b/cliff.toml @@ -0,0 +1,12 @@ +[changelog] +header = "Changelog" +body = """ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | upper_first }} + {% for commit in commits %} + - {{ commit.message | upper_first }} + {% endfor %} +{% endfor %} +""" +trim = true +footer = "" diff --git a/package.json b/package.json index 382d86a..436e24d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scambier.obsidian-search", - "version": "1.23.1", + "version": "1.24.0-beta.3", "description": "A search engine for Obsidian", "main": "dist/main.js", "scripts": { @@ -52,6 +52,5 @@ "overrides": { "moment@>=2.18.0 <2.29.4": ">=2.29.4" } - }, - "packageManager": "pnpm@9.1.0+sha512.67f5879916a9293e5cf059c23853d571beaf4f753c707f40cb22bed5fb1578c6aad3b6c4107ccb3ba0b35be003eb621a16471ac836c87beb53f9d54bb4612724" + } } diff --git a/src/__tests__/query-tests.ts b/src/__tests__/query-tests.ts index 8ef016e..b2a5b42 100644 --- a/src/__tests__/query-tests.ts +++ b/src/__tests__/query-tests.ts @@ -6,7 +6,10 @@ describe('The Query class', () => { it('should correctly parse string queries', () => { // Act - const query = new Query(stringQuery, { ignoreDiacritics: true }) + const query = new Query(stringQuery, { + ignoreDiacritics: true, + ignoreArabicDiacritics: true, + }) // Assert const segments = query.query.text @@ -25,7 +28,10 @@ describe('The Query class', () => { it('should not exclude words when there is no space before', () => { // Act - const query = new Query('foo bar-baz', { ignoreDiacritics: true }) + const query = new Query('foo bar-baz', { + ignoreDiacritics: true, + ignoreArabicDiacritics: true, + }) // Assert expect(query.query.exclude.text).toHaveLength(0) @@ -34,7 +40,10 @@ describe('The Query class', () => { describe('.getExactTerms()', () => { it('should an array of strings containg "exact" values', () => { // Act - const query = new Query(stringQuery, { ignoreDiacritics: true }) + const query = new Query(stringQuery, { + ignoreDiacritics: true, + ignoreArabicDiacritics: true, + }) // Assert expect(query.getExactTerms()).toEqual(['lorem ipsum', 'sit amet']) diff --git a/src/cache-manager.ts b/src/cache-manager.ts index f580fe4..5cbfb4e 100644 --- a/src/cache-manager.ts +++ b/src/cache-manager.ts @@ -5,7 +5,7 @@ import { getAliasesFromMetadata, getTagsFromMetadata, isFileCanvas, - isFileFromDataloomPlugin, + isFileFromDataloom, isFileImage, isFileOffice, isFilePDF, @@ -136,7 +136,7 @@ export class CacheManager { } // ** Dataloom plugin ** - else if (isFileFromDataloomPlugin(path)) { + else if (isFileFromDataloom(path)) { try { const data = JSON.parse(await app.vault.cachedRead(file)) // data is a json object, we recursively iterate the keys @@ -230,10 +230,11 @@ export class CacheManager { } } } - + const displayTitle = metadata?.frontmatter?.[this.plugin.settings.displayTitle] ?? '' const tags = getTagsFromMetadata(metadata) return { basename: file.basename, + displayTitle, content, /** Content without diacritics and markdown chars */ cleanedContent: stripMarkdownCharacters(removeDiacritics(content)), diff --git a/src/components/ModalVault.svelte b/src/components/ModalVault.svelte index 8f17a75..e992d99 100644 --- a/src/components/ModalVault.svelte +++ b/src/components/ModalVault.svelte @@ -141,6 +141,7 @@ } query = new Query(searchQuery, { ignoreDiacritics: plugin.settings.ignoreDiacritics, + ignoreArabicDiacritics: plugin.settings.ignoreArabicDiacritics, }) cancelableQuery = cancelable( new Promise(resolve => { diff --git a/src/components/ResultItemVault.svelte b/src/components/ResultItemVault.svelte index d5a6387..a0290df 100644 --- a/src/components/ResultItemVault.svelte +++ b/src/components/ResultItemVault.svelte @@ -3,7 +3,7 @@ import type { ResultNote } from '../globals' import { getExtension, - isFileCanvas, + isFileCanvas, isFileExcalidraw, isFileImage, isFilePDF, pathWithoutFilename, @@ -36,7 +36,7 @@ $: cleanedContent = plugin.textProcessor.makeExcerpt(note.content, note.matches[0]?.offset ?? -1) $: glyph = false //cacheManager.getLiveDocument(note.path)?.doesNotExist $: { - title = note.basename + title = note.displayTitle || note.basename notePath = pathWithoutFilename(note.path) // Icons @@ -44,11 +44,18 @@ 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)) + if (isFileImage(note.path)) { + setIcon(elFilePathIcon, 'image') + } + else if (isFilePDF(note.path)) { + setIcon(elFilePathIcon, 'file-text') + } + else if (isFileCanvas(note.path) || isFileExcalidraw(note.path)) { setIcon(elFilePathIcon, 'layout-dashboard') - else setIcon(elFilePathIcon, 'file') + } + else { + setIcon(elFilePathIcon, 'file') + } } } diff --git a/src/components/modals.ts b/src/components/modals.ts index e439866..a4ecd41 100644 --- a/src/components/modals.ts +++ b/src/components/modals.ts @@ -119,7 +119,7 @@ abstract class OmnisearchModal extends Modal { }) // Open in background - this.scope.register(['Alt'], 'O', e => { + this.scope.register(['Ctrl'], 'O', e => { if (!isInputComposition()) { // Check if the user is still typing e.preventDefault() diff --git a/src/globals.ts b/src/globals.ts index 0ef1580..df769f9 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -46,6 +46,7 @@ export type DocumentRef = { path: string; mtime: number } export type IndexedDocument = { path: string basename: string + displayTitle: string mtime: number content: string @@ -76,6 +77,7 @@ export type ResultNote = { score: number path: string basename: string + displayTitle: string content: string foundWords: string[] matches: SearchMatch[] diff --git a/src/main.ts b/src/main.ts index 0feb676..3eeac04 100644 --- a/src/main.ts +++ b/src/main.ts @@ -138,7 +138,9 @@ export default class OmnisearchPlugin extends Plugin { }) ) - this.refreshIndexCallback = this.notesIndexer.refreshIndex.bind(this.notesIndexer) + this.refreshIndexCallback = this.notesIndexer.refreshIndex.bind( + this.notesIndexer + ) addEventListener('blur', this.refreshIndexCallback) removeEventListener @@ -272,15 +274,20 @@ export default class OmnisearchPlugin extends Plugin { indexingStep.set(IndexingStepType.WritingCache) // Disable settings.useCache while writing the cache, in case it freezes - this.settings.useCache = false - await saveSettings(this) + const cacheEnabled = this.settings.useCache + if (cacheEnabled && !this.settings.DANGER_forceSaveCache) { + this.settings.useCache = false + await saveSettings(this) + } // Write the cache await searchEngine.writeToCache() // Re-enable settings.caching - this.settings.useCache = true - await saveSettings(this) + if (cacheEnabled) { + this.settings.useCache = true + await saveSettings(this) + } } console.timeEnd('Omnisearch - Indexing total time') diff --git a/src/notes-indexer.ts b/src/notes-indexer.ts index 33ccfde..a0383ea 100644 --- a/src/notes-indexer.ts +++ b/src/notes-indexer.ts @@ -4,7 +4,7 @@ import { removeAnchors } from './tools/notes' import type { IndexedDocument } from './globals' import { isFileCanvas, - isFileFromDataloomPlugin, + isFileFromDataloom, isFileImage, isFilePDF, logDebug, @@ -51,7 +51,7 @@ export class NotesIndexer { return ( this.isFilePlaintext(path) || isFileCanvas(path) || - isFileFromDataloomPlugin(path) || + isFileFromDataloom(path) || (canIndexPDF && isFilePDF(path)) || (canIndexImages && isFileImage(path)) || (canIndexImagesAI && isFileImage(path)) @@ -63,7 +63,7 @@ export class NotesIndexer { this.canIndexUnsupportedFiles() || this.isFilePlaintext(path) || isFileCanvas(path) || - isFileFromDataloomPlugin(path) + isFileFromDataloom(path) ) } @@ -91,6 +91,7 @@ export class NotesIndexer { return { path: filename, basename: name, + displayTitle: '', mtime: 0, content: '', diff --git a/src/search/query.ts b/src/search/query.ts index 2264832..f822d46 100644 --- a/src/search/query.ts +++ b/src/search/query.ts @@ -13,9 +13,9 @@ export class Query { } #inQuotes: string[] - constructor(text = '', options: { ignoreDiacritics: boolean }) { + constructor(text = '', options: { ignoreDiacritics: boolean, ignoreArabicDiacritics: boolean}) { if (options.ignoreDiacritics) { - text = removeDiacritics(text) + text = removeDiacritics(text, options.ignoreArabicDiacritics) } const parsed = parse(text.toLowerCase(), { tokenize: true, diff --git a/src/search/search-engine.ts b/src/search/search-engine.ts index dba9a80..bfaae53 100644 --- a/src/search/search-engine.ts +++ b/src/search/search-engine.ts @@ -154,8 +154,9 @@ export class SearchEngine { term.length <= 3 ? 0 : term.length <= 5 ? fuzziness / 2 : fuzziness, boost: { basename: settings.weightBasename, - directory: settings.weightDirectory, aliases: settings.weightBasename, + displayTitle: settings.weightBasename, + directory: settings.weightDirectory, headings1: settings.weightH1, headings2: settings.weightH2, headings3: settings.weightH3, @@ -304,7 +305,12 @@ export class SearchEngine { const title = document?.path.toLowerCase() ?? '' const content = (document?.cleanedContent ?? '').toLowerCase() return exactTerms.every( - q => content.includes(q) || removeDiacritics(title).includes(q) + q => + content.includes(q) || + removeDiacritics( + title, + this.plugin.settings.ignoreArabicDiacritics + ).includes(q) ) }) } @@ -434,7 +440,7 @@ export class SearchEngine { }, processTerm: (term: string) => (this.plugin.settings.ignoreDiacritics - ? removeDiacritics(term) + ? removeDiacritics(term, this.plugin.settings.ignoreArabicDiacritics) : term ).toLowerCase(), idField: 'path', diff --git a/src/search/tokenizer.ts b/src/search/tokenizer.ts index 9ede21a..a155025 100644 --- a/src/search/tokenizer.ts +++ b/src/search/tokenizer.ts @@ -15,36 +15,41 @@ export class Tokenizer { * @returns */ public tokenizeForIndexing(text: string): string[] { - const words = this.tokenizeWords(text) - let urls: string[] = [] - if (this.plugin.settings.tokenizeUrls) { - try { - urls = markdownLinkExtractor(text) - } catch (e) { - logDebug('Error extracting urls', e) + try { + const words = this.tokenizeWords(text) + let urls: string[] = [] + if (this.plugin.settings.tokenizeUrls) { + try { + urls = markdownLinkExtractor(text) + } catch (e) { + logDebug('Error extracting urls', e) + } } + + let tokens = this.tokenizeTokens(text, { skipChs: true }) + + // Split hyphenated tokens + tokens = [...tokens, ...tokens.flatMap(splitHyphens)] + + // Split camelCase tokens into "camel" and "case + tokens = [...tokens, ...tokens.flatMap(splitCamelCase)] + + // Add whole words (aka "not tokens") + tokens = [...tokens, ...words] + + // Add urls + if (urls.length) { + tokens = [...tokens, ...urls] + } + + // Remove duplicates + tokens = [...new Set(tokens)] + + return tokens + } catch (e) { + console.error('Error tokenizing text, skipping document', e) + return [] } - - let tokens = this.tokenizeTokens(text, { skipChs: true }) - - // Split hyphenated tokens - tokens = [...tokens, ...tokens.flatMap(splitHyphens)] - - // Split camelCase tokens into "camel" and "case - tokens = [...tokens, ...tokens.flatMap(splitCamelCase)] - - // Add whole words (aka "not tokens") - tokens = [...tokens, ...words] - - // Add urls - if (urls.length) { - tokens = [...tokens, ...urls] - } - - // Remove duplicates - tokens = [...new Set(tokens)] - - return tokens } /** diff --git a/src/settings.ts b/src/settings.ts index 8f05d45..767af73 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -11,7 +11,8 @@ import { import { writable } from 'svelte/store' import { K_DISABLE_OMNISEARCH } from './globals' import type OmnisearchPlugin from './main' -import { enablePrintDebug } from "./tools/utils"; +import { enablePrintDebug } from './tools/utils' +import { debounce } from 'lodash-es' interface WeightingSettings { weightBasename: number @@ -32,8 +33,12 @@ export interface OmnisearchSettings extends WeightingSettings { downrankedFoldersFilters: string[] /** Ignore diacritics when indexing files */ ignoreDiacritics: boolean + ignoreArabicDiacritics: boolean + /** Extensions of plain text files to index, in addition to .md */ indexedFileTypes: string[] + /** Custom title field */ + displayTitle: string /** Enable PDF indexing */ PDFIndexing: boolean /** Enable Images indexing */ @@ -71,6 +76,7 @@ export interface OmnisearchSettings extends WeightingSettings { httpApiNotice: boolean DANGER_httpHost: string | null + DANGER_forceSaveCache: boolean } /** @@ -97,6 +103,11 @@ export class SettingsTab extends PluginSettingTab { const { containerEl } = this const database = this.plugin.database const textExtractor = this.plugin.getTextExtractor() + + const clearCacheDebounced = debounce(async () => { + await database.clearCache() + }, 1000) + const aiImageAnalyzer = this.plugin.getAIImageAnalyzer() containerEl.empty() @@ -138,18 +149,24 @@ export class SettingsTab extends PluginSettingTab { new Setting(containerEl) .setName('Indexing') .setHeading() - .setDesc(indexingDesc) + .setDesc( + htmlDescription(`⚠️ Changing indexing settings will clear the cache, and requires a restart of Obsidian.

+ ${ + textExtractor + ? `👍 You have installed Text Extractor, Omnisearch can use it to index PDFs and images contents. +
Text extraction only works on desktop, but the cache can be synchronized with your mobile device.` + : `⚠️ Omnisearch requires Text Extractor to index PDFs and images.` + }`) + ) // PDF Indexing - const indexPDFsDesc = new DocumentFragment() - indexPDFsDesc.createSpan({}, span => { - span.innerHTML = `Omnisearch will use Text Extractor to index the content of your PDFs.` - }) new Setting(containerEl) - .setName( - `PDFs content indexing ${textExtractor ? '' : '⚠️ Disabled'}` + .setName(`PDFs content indexing ${textExtractor ? '' : '⚠️ Disabled'}`) + .setDesc( + htmlDescription( + `Omnisearch will use Text Extractor to index the content of your PDFs.` + ) ) - .setDesc(indexPDFsDesc) .addToggle(toggle => toggle.setValue(settings.PDFIndexing).onChange(async v => { await database.clearCache() @@ -160,13 +177,13 @@ export class SettingsTab extends PluginSettingTab { .setDisabled(!textExtractor) // Images Indexing - const indexImagesDesc = new DocumentFragment() - indexImagesDesc.createSpan({}, span => { - span.innerHTML = `Omnisearch will use Text Extractor to OCR your images and index their content.` - }) new Setting(containerEl) .setName(`Images OCR indexing ${textExtractor ? '' : '⚠️ Disabled'}`) - .setDesc(indexImagesDesc) + .setDesc( + htmlDescription( + `Omnisearch will use Text Extractor to OCR your images and index their content.` + ) + ) .addToggle(toggle => toggle.setValue(settings.imagesIndexing).onChange(async v => { await database.clearCache() @@ -213,38 +230,49 @@ export class SettingsTab extends PluginSettingTab { .setDisabled(!aiImageAnalyzer) // Index filenames of unsupported files - const indexUnsupportedDesc = new DocumentFragment() - indexUnsupportedDesc.createSpan({}, span => { - span.innerHTML = ` - Omnisearch can index filenames of "unsupported" files, such as e.g.
.mp4
- or non-extracted PDFs & images.
- "Obsidian setting" will respect the value of "Files & Links > Detect all file extensions".` - }) new Setting(containerEl) .setName('Index paths of unsupported files') - .setDesc(indexUnsupportedDesc) + .setDesc( + htmlDescription(` + Omnisearch can index filenames of "unsupported" files, such as e.g.
.mp4
+ or non-extracted PDFs & images.
+ "Obsidian setting" will respect the value of "Files & Links > Detect all file extensions".`) + ) .addDropdown(dropdown => { dropdown .addOptions({ yes: 'Yes', no: 'No', default: 'Obsidian setting' }) .setValue(settings.unsupportedFilesIndexing) .onChange(async v => { - await database.clearCache() + await clearCacheDebounced() ;(settings.unsupportedFilesIndexing as any) = v await saveSettings(this.plugin) }) }) + // Custom display title + new Setting(containerEl) + .setName('Set frontmatter property key as title') + .setDesc( + htmlDescription(`If you have a custom property in your notes that you want to use as the title in search results.
+ Leave empty to disable.`) + ) + .addText(component => { + component.setValue(settings.displayTitle).onChange(async v => { + await clearCacheDebounced() + settings.displayTitle = v + await saveSettings(this.plugin) + }) + }) + // Additional text files to index - const indexedFileTypesDesc = new DocumentFragment() - indexedFileTypesDesc.createSpan({}, span => { - span.innerHTML = `In addition to standard md files, Omnisearch can also index other PLAINTEXT files.
- Add extensions separated by a space, without the dot. Example: "txt org csv".
- ⚠️ Using extensions of non-plaintext files (like .pptx) WILL cause crashes, - because Omnisearch will try to index their content.` - }) new Setting(containerEl) .setName('Additional TEXT files to index') - .setDesc(indexedFileTypesDesc) + .setDesc( + htmlDescription(`In addition to standard md files, Omnisearch can also index other PLAINTEXT files.
+ Add extensions separated by a space, without the dot. Example: "txt org csv".
+ ⚠️ Using extensions of non-plaintext files (like .pptx) WILL cause crashes, + because Omnisearch will try to index their content.`) + ) .addText(component => { component .setValue(settings.indexedFileTypes.join(' ')) @@ -319,16 +347,13 @@ export class SettingsTab extends PluginSettingTab { }) // Split CamelCaseWords - const camelCaseDesc = new DocumentFragment() - camelCaseDesc.createSpan({}, span => { - span.innerHTML = `Enable this if you want to be able to search for CamelCaseWords as separate words.
- ⚠️ Changing this setting will clear the cache.
- ${needsARestart} - ` - }) new Setting(containerEl) .setName('Split CamelCaseWords') - .setDesc(camelCaseDesc) + .setDesc( + htmlDescription(`Enable this if you want to be able to search for CamelCaseWords as separate words.
+ ⚠️ Changing this setting will clear the cache.
+ ${needsARestart}`) + ) .addToggle(toggle => toggle.setValue(settings.splitCamelCase).onChange(async v => { await database.clearCache() @@ -469,14 +494,12 @@ export class SettingsTab extends PluginSettingTab { ) // Show "Create note" button - const createBtnDesc = new DocumentFragment() - createBtnDesc.createSpan({}, span => { - span.innerHTML = `Shows a button next to the search input, to create a note. - Acts the same as the shift ↵ shortcut, can be useful for mobile device users.` - }) new Setting(containerEl) .setName('Show "Create note" button') - .setDesc(createBtnDesc) + .setDesc( + htmlDescription(`Shows a button next to the search input, to create a note. + Acts the same as the shift ↵ shortcut, can be useful for mobile device users.`) + ) .addToggle(toggle => toggle.setValue(settings.showCreateButton).onChange(async v => { settings.showCreateButton = v @@ -589,14 +612,14 @@ export class SettingsTab extends PluginSettingTab { //#region HTTP Server if (!Platform.isMobile) { - const httpServerDesc = new DocumentFragment() - httpServerDesc.createSpan({}, span => { - span.innerHTML = `Omnisearch can be used through a simple HTTP server (more information).` - }) new Setting(containerEl) .setName('API Access Through HTTP') .setHeading() - .setDesc(httpServerDesc) + .setDesc( + htmlDescription( + `Omnisearch can be used through a simple HTTP server (more information).` + ) + ) new Setting(containerEl) .setName('Enable the HTTP server') @@ -668,17 +691,14 @@ export class SettingsTab extends PluginSettingTab { new Setting(containerEl).setName('Danger Zone').setHeading() // Ignore diacritics - const diacriticsDesc = new DocumentFragment() - diacriticsDesc.createSpan({}, span => { - span.innerHTML = `Normalize diacritics in search terms. Words like "brûlée" or "žluťoučký" will be indexed as "brulee" and "zlutoucky".
- ⚠️ You probably should NOT disable this.
- ⚠️ Changing this setting will clear the cache.
- ${needsARestart} - ` - }) new Setting(containerEl) .setName('Ignore diacritics') - .setDesc(diacriticsDesc) + .setDesc( + htmlDescription(`Normalize diacritics in search terms. Words like "brûlée" or "žluťoučký" will be indexed as "brulee" and "zlutoucky".
+ ⚠️ You probably should NOT disable this.
+ ⚠️ Changing this setting will clear the cache.
+ ${needsARestart}`) + ) .addToggle(toggle => toggle.setValue(settings.ignoreDiacritics).onChange(async v => { await database.clearCache() @@ -687,6 +707,16 @@ export class SettingsTab extends PluginSettingTab { }) ) + new Setting(containerEl) + .setName('Ignore Arabic diacritics (beta)') + .addToggle(toggle => + toggle.setValue(settings.ignoreArabicDiacritics).onChange(async v => { + await database.clearCache() + settings.ignoreArabicDiacritics = v + await saveSettings(this.plugin) + }) + ) + // Disable Omnisearch const disableDesc = new DocumentFragment() disableDesc.createSpan({}, span => { @@ -700,24 +730,38 @@ export class SettingsTab extends PluginSettingTab { toggle.setValue(isPluginDisabled(this.app)).onChange(async v => { if (v) { this.app.saveLocalStorage(K_DISABLE_OMNISEARCH, '1') + new Notice('Omnisearch - Disabled. Please restart Obsidian.') } else { this.app.saveLocalStorage(K_DISABLE_OMNISEARCH) // No value = unset + new Notice('Omnisearch - Enabled. Please restart Obsidian.') } - new Notice('Omnisearch - Disabled. Please restart Obsidian.') + }) + ) + + // Force save cache + new Setting(containerEl) + .setName('Force save the cache') + .setDesc( + htmlDescription(`Omnisearch has a security feature that automatically disables cache writing if it cannot fully perform the operation.
+ Use this option to force the cache to be saved, even if it causes a crash.
+ ⚠️ Enabling this setting could lead to crash loops`) + ) + .addToggle(toggle => + toggle.setValue(settings.DANGER_forceSaveCache).onChange(async v => { + settings.DANGER_forceSaveCache = v + await saveSettings(this.plugin) }) ) // Clear cache data if (isCacheEnabled()) { - const resetCacheDesc = new DocumentFragment() - resetCacheDesc.createSpan({}, span => { - span.innerHTML = `Erase all Omnisearch cache data. - Use this if Omnisearch results are inconsistent, missing, or appear outdated.
- ${needsARestart}` - }) new Setting(containerEl) .setName('Clear cache data') - .setDesc(resetCacheDesc) + .setDesc( + htmlDescription(`Erase all Omnisearch cache data. + Use this if Omnisearch results are inconsistent, missing, or appear outdated.
+ ${needsARestart}`) + ) .addButton(btn => { btn.setButtonText('Clear cache') btn.onClick(async () => { @@ -725,6 +769,7 @@ export class SettingsTab extends PluginSettingTab { }) }) } + //#endregion Danger Zone } @@ -745,7 +790,9 @@ export function getDefaultSettings(app: App): OmnisearchSettings { hideExcluded: false, downrankedFoldersFilters: [] as string[], ignoreDiacritics: true, + ignoreArabicDiacritics: false, indexedFileTypes: [] as string[], + displayTitle: '', PDFIndexing: false, officeIndexing: false, imagesIndexing: false, @@ -781,11 +828,20 @@ export function getDefaultSettings(app: App): OmnisearchSettings { verboseLogging: false, DANGER_httpHost: null, + DANGER_forceSaveCache: false, } } let settings: OmnisearchSettings +function htmlDescription(innerHTML: string): DocumentFragment { + const desc = new DocumentFragment() + desc.createSpan({}, span => { + span.innerHTML = innerHTML + }) + return desc +} + // /** // * @deprecated // */ diff --git a/src/tools/api.ts b/src/tools/api.ts index 7761c8a..5a480e6 100644 --- a/src/tools/api.ts +++ b/src/tools/api.ts @@ -88,6 +88,7 @@ export function getApi(plugin: OmnisearchPlugin) { async search(q: string): Promise { const query = new Query(q, { ignoreDiacritics: plugin.settings.ignoreDiacritics, + ignoreArabicDiacritics: plugin.settings.ignoreArabicDiacritics, }) const raw = await plugin.searchEngine.getSuggestions(query) return mapResults(plugin, raw) diff --git a/src/tools/text-processing.ts b/src/tools/text-processing.ts index 64574f3..497c513 100644 --- a/src/tools/text-processing.ts +++ b/src/tools/text-processing.ts @@ -115,7 +115,7 @@ export class TextProcessor { const originalText = text // text = text.toLowerCase().replace(new RegExp(SEPARATORS, 'gu'), ' ') if (this.plugin.settings.ignoreDiacritics) { - text = removeDiacritics(text) + text = removeDiacritics(text, this.plugin.settings.ignoreArabicDiacritics) } const startTime = new Date().getTime() let match: RegExpExecArray | null = null diff --git a/src/tools/utils.ts b/src/tools/utils.ts index 664d1de..74d7beb 100644 --- a/src/tools/utils.ts +++ b/src/tools/utils.ts @@ -109,15 +109,30 @@ export function getTagsFromMetadata(metadata: CachedMetadata | null): string[] { /** * https://stackoverflow.com/a/37511463 */ -export function removeDiacritics(str: string): string { - // Japanese diacritics that should be distinguished - const excludeDiacritics: string[] = ['\\u30FC', '\\u309A', '\\u3099'] - const regexpExclude: string = excludeDiacritics.join('|') - const regexp: RegExp = new RegExp(`(?!${regexpExclude})\\p{Diacritic}`, 'gu') - +export function removeDiacritics(str: string, arabic = false): string { if (str === null || str === undefined) { return '' } + + // Japanese diacritics that should be distinguished + const japaneseDiacritics: string[] = ['\\u30FC', '\\u309A', '\\u3099'] + const regexpExclude: string = japaneseDiacritics.join('|') + const regexp: RegExp = new RegExp(`(?!${regexpExclude})\\p{Diacritic}`, 'gu') + + if (arabic) { + // Arabic diacritics + // https://stackoverflow.com/a/40959537 + str = str + .replace(/([^\u0621-\u063A\u0641-\u064A\u0660-\u0669a-zA-Z 0-9])/g, '') + .replace(/(آ|إ|أ)/g, 'ا') + .replace(/(ة)/g, 'ه') + .replace(/(ئ|ؤ)/g, 'ء') + .replace(/(ى)/g, 'ي') + for (let i = 0; i < 10; i++) { + str.replace(String.fromCharCode(0x660 + i), String.fromCharCode(48 + i)) + } + } + // Keep backticks for code blocks, because otherwise they are removed by the .normalize() function // https://stackoverflow.com/a/36100275 str = str.replaceAll('`', '[__omnisearch__backtick__]') @@ -152,7 +167,11 @@ export function isFileCanvas(path: string): boolean { return path.endsWith('.canvas') } -export function isFileFromDataloomPlugin(path: string): boolean { +export function isFileExcalidraw(path: string): boolean { + return path.endsWith('.excalidraw') +} + +export function isFileFromDataloom(path: string): boolean { return path.endsWith('.loom') } @@ -219,7 +238,7 @@ export function warnDebug(...args: any[]): void { printDebug(console.warn, ...args) } -let printDebugEnabled= false +let printDebugEnabled = false export function enablePrintDebug(enable: boolean): void { printDebugEnabled = enable }