From 96b4ac631df8c1db33a71e5564f74c8505ccead8 Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Fri, 24 Feb 2023 12:16:40 +0100 Subject: [PATCH 1/8] #176 - WIP tokenization of CamelCase words Technically works, but highlighting needs a rework --- src/search/omnisearch.ts | 3 ++- src/tools/utils.ts | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/search/omnisearch.ts b/src/search/omnisearch.ts index c9a3421..a057604 100644 --- a/src/search/omnisearch.ts +++ b/src/search/omnisearch.ts @@ -10,6 +10,7 @@ import { settings } from '../settings' import { chunkArray, removeDiacritics, + splitCamelCase, stringsToRegex, stripMarkdownCharacters, } from '../tools/utils' @@ -25,7 +26,7 @@ const tokenize = (text: string): string[] => { return tokens.flatMap(word => chsRegex.test(word) ? chsSegmenter.cut(word) : [word] ) - } else return tokens + } else return tokens.flatMap(splitCamelCase) } export class Omnisearch { diff --git a/src/tools/utils.ts b/src/tools/utils.ts index 9254408..66ee8d2 100644 --- a/src/tools/utils.ts +++ b/src/tools/utils.ts @@ -307,3 +307,11 @@ export function chunkArray(arr: T[], len: number): T[][] { return chunks } + +/** + * Converts a 'fooBarBAZLorem' into ['foo', 'Bar', 'BAZ', 'Lorem] + * @param text + */ +export function splitCamelCase(text: string): string[] { + return text.replace(/([a-z](?=[A-Z]))/g, '$1 ').split(' ') +} From 0529b978cb07d8f04bbfba534802ea76f645f1f7 Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Thu, 2 Mar 2023 20:06:40 +0100 Subject: [PATCH 2/8] Fixed typo --- src/globals.ts | 2 +- src/settings.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/globals.ts b/src/globals.ts index a9e77fc..49ce0be 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -14,7 +14,7 @@ export const excerptBefore = 100 export const excerptAfter = 300 export const highlightClass = `suggestion-highlight omnisearch-highlight ${ - settings.hightlight ? 'omnisearch-default-highlight' : '' + settings.highlight ? 'omnisearch-default-highlight' : '' }` export const eventBus = new EventBus() diff --git a/src/settings.ts b/src/settings.ts index f3778a0..b0a7e58 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -45,7 +45,7 @@ export interface OmnisearchSettings extends WeightingSettings { welcomeMessage: string /** If a query returns 0 result, try again with more relax conditions */ simpleSearch: boolean - hightlight: boolean + highlight: boolean } /** @@ -301,8 +301,8 @@ export class SettingsTab extends PluginSettingTab { 'Will highlight matching results when enabled. See README for more customization options.' ) .addToggle(toggle => - toggle.setValue(settings.hightlight).onChange(async v => { - settings.hightlight = v + toggle.setValue(settings.highlight).onChange(async v => { + settings.highlight = v await saveSettings(this.plugin) }) ) @@ -384,7 +384,7 @@ export const DEFAULT_SETTINGS: OmnisearchSettings = { showExcerpt: true, renderLineReturnInExcerpts: true, showCreateButton: false, - hightlight: true, + highlight: true, showPreviousQueryResults: true, simpleSearch: false, From 36ccb52982fd55d74850a3f4851f7a94409fcf6b Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Thu, 2 Mar 2023 21:32:37 +0100 Subject: [PATCH 3/8] #176 - Option to index CamelCaseWords --- src/search/omnisearch.ts | 6 +++++- src/settings.ts | 21 +++++++++++++++++++++ src/tools/utils.ts | 13 ++++++++----- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/search/omnisearch.ts b/src/search/omnisearch.ts index 902607d..f621fbf 100644 --- a/src/search/omnisearch.ts +++ b/src/search/omnisearch.ts @@ -26,7 +26,11 @@ const tokenize = (text: string): string[] => { return tokens.flatMap(word => chsRegex.test(word) ? chsSegmenter.cut(word) : [word] ) - } else return tokens.flatMap(splitCamelCase) + } else { + if (settings.splitCamelCase) + return [...tokens, ...tokens.flatMap(splitCamelCase)] + return tokens + } } export class Omnisearch { diff --git a/src/settings.ts b/src/settings.ts index b0a7e58..5a6ca90 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -46,6 +46,7 @@ export interface OmnisearchSettings extends WeightingSettings { /** If a query returns 0 result, try again with more relax conditions */ simpleSearch: boolean highlight: boolean + splitCamelCase: boolean } /** @@ -204,6 +205,25 @@ 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.
+ Needs a restart to fully take effect. + ` + }) + new Setting(containerEl) + .setName('Split CamelCaseWords') + .setDesc(camelCaseDesc) + .addToggle(toggle => + toggle.setValue(settings.splitCamelCase).onChange(async v => { + await database.clearCache() + settings.splitCamelCase = v + await saveSettings(this.plugin) + }) + ) + // Simpler search new Setting(containerEl) .setName('Simpler search') @@ -379,6 +399,7 @@ export const DEFAULT_SETTINGS: OmnisearchSettings = { indexedFileTypes: [] as string[], PDFIndexing: false, imagesIndexing: false, + splitCamelCase: false, ribbonIcon: true, showExcerpt: true, diff --git a/src/tools/utils.ts b/src/tools/utils.ts index 73eed56..035ae57 100644 --- a/src/tools/utils.ts +++ b/src/tools/utils.ts @@ -91,14 +91,17 @@ export function getAllIndices(text: string, regex: RegExp): SearchMatch[] { */ export function stringsToRegex(strings: string[]): RegExp { if (!strings.length) return /^$/g - // Default word split is not applied if the user uses the cm-chs-patch plugin const joined = '(' + - (getChsSegmenter() ? '' : `^|${SPACE_OR_PUNCTUATION.source}`) + + // Default word split is not applied if the user uses the cm-chs-patch plugin + (getChsSegmenter() + ? '' + : // Split on start of line, spaces, punctuation, or capital letters (for camelCase) + settings.splitCamelCase + ? `^|${SPACE_OR_PUNCTUATION.source}|[A-Z]` + : `^|${SPACE_OR_PUNCTUATION.source}`) + ')' + - '(' + - strings.map(s => escapeRegex(s)).join('|') + - ')' + `(${strings.map(s => escapeRegex(s)).join('|')})` const reg = new RegExp(`${joined}`, 'giu') return reg From eb1576c972fd660ae210cea231fdcb7ac991d2c8 Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Thu, 2 Mar 2023 21:41:34 +0100 Subject: [PATCH 4/8] 1.13.0-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 07d033e..0e16eac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scambier.obsidian-search", - "version": "1.12.2", + "version": "1.13.0-beta.1", "description": "A search engine for Obsidian", "main": "dist/main.js", "scripts": { From 58db940b68ecdbc1584dd95d33b6b2b7c0f06db1 Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Sat, 11 Mar 2023 13:32:26 +0100 Subject: [PATCH 5/8] Sort then slice --- src/components/ModalVault.svelte | 4 +--- src/search/omnisearch.ts | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/ModalVault.svelte b/src/components/ModalVault.svelte index 5ff314d..8c50fae 100644 --- a/src/components/ModalVault.svelte +++ b/src/components/ModalVault.svelte @@ -130,9 +130,7 @@ async function updateResults() { query = new Query(searchQuery) - resultNotes = (await searchEngine.getSuggestions(query)).sort( - (a, b) => b.score - a.score - ) + resultNotes = await searchEngine.getSuggestions(query) selectedIndex = 0 await scrollIntoView() } diff --git a/src/search/omnisearch.ts b/src/search/omnisearch.ts index f621fbf..880e65f 100644 --- a/src/search/omnisearch.ts +++ b/src/search/omnisearch.ts @@ -247,7 +247,8 @@ export class Omnisearch { } } - results = results.slice(0, 50) + // Sort results and keep the 50 best + results = results.sort((a, b) => b.score - a.score).slice(0, 50) const documents = await Promise.all( results.map(async result => await cacheManager.getDocument(result.id)) From 24bfb94b71ca13fa544bf81f0c81de2360a0ca9f Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Sat, 11 Mar 2023 14:43:29 +0100 Subject: [PATCH 6/8] #184 - Verbose logging --- src/cache-manager.ts | 2 ++ src/components/ModalVault.svelte | 23 ++++----------------- src/main.ts | 6 +++++- src/search/omnisearch.ts | 34 ++++++++++++++++++++++++-------- src/settings.ts | 18 +++++++++++++++++ src/tools/utils.ts | 6 ++++++ 6 files changed, 61 insertions(+), 28 deletions(-) diff --git a/src/cache-manager.ts b/src/cache-manager.ts index 10132f1..e2a7ca5 100644 --- a/src/cache-manager.ts +++ b/src/cache-manager.ts @@ -11,6 +11,7 @@ import { getTagsFromMetadata, isFileCanvas, isFilePlaintext, + logDebug, makeMD5, removeDiacritics, } from './tools/utils' @@ -150,6 +151,7 @@ class CacheManager { if (this.documents.has(path)) { return this.documents.get(path)! } + logDebug('Generating IndexedDocument from', path) await this.addToLiveCache(path) return this.documents.get(path)! } diff --git a/src/components/ModalVault.svelte b/src/components/ModalVault.svelte index 8c50fae..241bf81 100644 --- a/src/components/ModalVault.svelte +++ b/src/components/ModalVault.svelte @@ -15,6 +15,7 @@ getCtrlKeyLabel, getExtension, isFilePDF, + logDebug, loopIndex, } from 'src/tools/utils' import { @@ -27,7 +28,6 @@ import * as NotesIndex from '../notes-index' import { cacheManager } from '../cache-manager' import { searchEngine } from 'src/search/omnisearch' - import CancelablePromise, { cancelable } from 'cancelable-promise' export let modal: OmnisearchVaultModal export let previousQuery: string | undefined @@ -40,28 +40,13 @@ let searching = true let refInput: InputSearch | undefined - let pWaitingResults: CancelablePromise | null = null - $: selectedNote = resultNotes[selectedIndex] $: searchQuery = searchQuery ?? previousQuery $: if (searchQuery) { - if (pWaitingResults) { - pWaitingResults.cancel() - pWaitingResults = null - } searching = true - pWaitingResults = cancelable( - new Promise((resolve, reject) => { - updateResults() - .then(() => { - searching = false - resolve(null) - }) - .catch(e => { - reject(e) - }) - }) - ) + updateResults().then(() => { + searching = false + }) } else { searching = false resultNotes = [] diff --git a/src/main.ts b/src/main.ts index 039ea86..0f1e655 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,7 +18,7 @@ import { isCacheEnabled, } from './globals' import api, { notifyOnIndexed } from './tools/api' -import { isFileIndexable } from './tools/utils' +import { isFileIndexable, logDebug } from './tools/utils' import { database, OmnisearchCache } from './database' import * as NotesIndex from './notes-index' import { searchEngine } from './search/omnisearch' @@ -69,6 +69,7 @@ export default class OmnisearchPlugin extends Plugin { this.registerEvent( this.app.vault.on('create', file => { if (isFileIndexable(file.path)) { + logDebug('Indexing new file', file.path) // await cacheManager.addToLiveCache(file.path) searchEngine.addFromPaths([file.path]) } @@ -76,6 +77,7 @@ export default class OmnisearchPlugin extends Plugin { ) this.registerEvent( this.app.vault.on('delete', file => { + logDebug('Removing file', file.path) cacheManager.removeFromLiveCache(file.path) searchEngine.removeFromPaths([file.path]) }) @@ -83,6 +85,7 @@ export default class OmnisearchPlugin extends Plugin { this.registerEvent( this.app.vault.on('modify', async file => { if (isFileIndexable(file.path)) { + logDebug('Updating file', file.path) await cacheManager.addToLiveCache(file.path) NotesIndex.markNoteForReindex(file) } @@ -91,6 +94,7 @@ export default class OmnisearchPlugin extends Plugin { this.registerEvent( this.app.vault.on('rename', async (file, oldPath) => { if (isFileIndexable(file.path)) { + logDebug('Renaming file', file.path) cacheManager.removeFromLiveCache(oldPath) cacheManager.addToLiveCache(file.path) searchEngine.removeFromPaths([oldPath]) diff --git a/src/search/omnisearch.ts b/src/search/omnisearch.ts index 880e65f..a7d7eb4 100644 --- a/src/search/omnisearch.ts +++ b/src/search/omnisearch.ts @@ -9,6 +9,7 @@ import { chsRegex, getChsSegmenter, SPACE_OR_PUNCTUATION } from '../globals' import { settings } from '../settings' import { chunkArray, + logDebug, removeDiacritics, splitCamelCase, stringsToRegex, @@ -122,11 +123,13 @@ export class Omnisearch { * @param paths */ public async addFromPaths(paths: string[]): Promise { + logDebug('Adding files', paths) let documents = ( await Promise.all( paths.map(async path => await cacheManager.getDocument(path)) ) ).filter(d => !!d?.path) + logDebug('Sorting documents to first index markdown') // Index markdown files first documents = sortBy(documents, d => (d.path.endsWith('.md') ? 0 : 1)) @@ -138,13 +141,14 @@ export class Omnisearch { // Split the documents in smaller chunks to add them to minisearch const chunkedDocs = chunkArray(documents, 500) for (const docs of chunkedDocs) { + logDebug('Indexing into search engine', docs) // Update the list of indexed docs docs.forEach(doc => this.indexedDocuments.set(doc.path, doc.mtime)) - + // Discard files that may have been already added (though it shouldn't happen) const alreadyAdded = docs.filter(doc => this.minisearch.has(doc.path)) this.removeFromPaths(alreadyAdded.map(o => o.path)) - + // Add docs to minisearch await this.minisearch.addAllAsync(docs) } @@ -175,6 +179,8 @@ export class Omnisearch { return [] } + logDebug('Starting search for', query) + let results = this.minisearch.search(query.segmentsToStr(), { prefix: term => term.length >= options.prefixLength, // length <= 3: no fuzziness @@ -192,6 +198,8 @@ export class Omnisearch { }, }) + logDebug('Found', results.length, 'results') + // Filter query results to only keep files that match query.extensions (if any) if (query.extensions.length) { results = results.filter(r => { @@ -247,6 +255,8 @@ export class Omnisearch { } } + logDebug('Sorting and limiting results') + // Sort results and keep the 50 best results = results.sort((a, b) => b.score - a.score).slice(0, 50) @@ -257,6 +267,7 @@ export class Omnisearch { // If the search query contains quotes, filter out results that don't have the exact match const exactTerms = query.getExactTerms() if (exactTerms.length) { + logDebug('Filtering with quoted terms') results = results.filter(r => { const document = documents.find(d => d.path === r.id) const title = document?.path.toLowerCase() ?? '' @@ -270,6 +281,7 @@ export class Omnisearch { // If the search query contains exclude terms, filter out results that have them const exclusions = query.exclusions if (exclusions.length) { + logDebug('Filtering with exclusions') results = results.filter(r => { const content = stripMarkdownCharacters( documents.find(d => d.path === r.id)?.content ?? '' @@ -277,6 +289,8 @@ export class Omnisearch { return exclusions.every(q => !content.includes(q.value)) }) } + + logDebug('Deduping') // FIXME: // Dedupe results - clutch for https://github.com/scambier/obsidian-omnisearch/issues/129 results = results.filter( @@ -337,17 +351,13 @@ export class Omnisearch { }) } - // Extract tags from the query - const tags = query.segments - .filter(s => s.value.startsWith('#')) - .map(s => s.value) - const documents = await Promise.all( results.map(async result => await cacheManager.getDocument(result.id)) ) // Map the raw results to get usable suggestions const resultNotes = results.map(result => { + logDebug('Locating matches for', result.id) let note = documents.find(d => d.path === result.id) if (!note) { // throw new Error(`Omnisearch - Note "${result.id}" not indexed`) @@ -363,6 +373,12 @@ export class Omnisearch { query.segments.forEach(s => { s.value = s.value.replace(/^#/, '') }) + + // Extract tags from the query + const tags = query.segments + .filter(s => s.value.startsWith('#')) + .map(s => s.value) + // Clean search matches that match quoted expressions, // and inject those expressions instead const foundWords = [ @@ -376,13 +392,15 @@ export class Omnisearch { // Tags, starting with # ...tags, ].filter(w => w.length > 1 || /\p{Emoji}/u.test(w)) + logDebug('Matching tokens:', foundWords) - // console.log(foundWords) + logDebug('Getting matches locations...') const matches = this.getMatches( note.content, stringsToRegex(foundWords), query ) + logDebug('Matches:', matches) const resultNote: ResultNote = { score: result.score, foundWords, diff --git a/src/settings.ts b/src/settings.ts index 5a6ca90..9f8b536 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -47,6 +47,7 @@ export interface OmnisearchSettings extends WeightingSettings { simpleSearch: boolean highlight: boolean splitCamelCase: boolean + verboseLogging: boolean } /** @@ -357,6 +358,22 @@ export class SettingsTab extends PluginSettingTab { //#endregion Results Weighting + //#region Debugging + + new Setting(containerEl).setName('Debugging').setHeading() + + new Setting(containerEl) + .setName('Enable verbose logging') + .setDesc('Adds a LOT of logs for debugging purposes. Don\'t forget to disable it.') + .addToggle(toggle => + toggle.setValue(settings.verboseLogging).onChange(async v => { + settings.verboseLogging = v + await saveSettings(this.plugin) + }) + ) + + //#endregion Debugginh + //#region Danger Zone if (isCacheEnabled()) { new Setting(containerEl).setName('Danger Zone').setHeading() @@ -416,6 +433,7 @@ export const DEFAULT_SETTINGS: OmnisearchSettings = { weightH3: 1.1, welcomeMessage: '', + verboseLogging: false, } as const export let settings = Object.assign({}, DEFAULT_SETTINGS) as OmnisearchSettings diff --git a/src/tools/utils.ts b/src/tools/utils.ts index 035ae57..6d160d0 100644 --- a/src/tools/utils.ts +++ b/src/tools/utils.ts @@ -324,3 +324,9 @@ export function chunkArray(arr: T[], len: number): T[][] { export function splitCamelCase(text: string): string[] { return text.replace(/([a-z](?=[A-Z]))/g, '$1 ').split(' ') } + +export function logDebug(...attr: any[]): void { + if (settings.verboseLogging) { + console.log(...['Omnisearch -', ...attr]) + } +} From 7bb99263b3c0973236ebafe84a724ea75cc21146 Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Sat, 11 Mar 2023 14:44:15 +0100 Subject: [PATCH 7/8] 1.13.0-beta.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e16eac..9428772 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scambier.obsidian-search", - "version": "1.13.0-beta.1", + "version": "1.13.0-beta.2", "description": "A search engine for Obsidian", "main": "dist/main.js", "scripts": { From 23640f4b0b09635e4d3870a2e8893b0e9912bbc2 Mon Sep 17 00:00:00 2001 From: Simon Cambier Date: Sun, 12 Mar 2023 11:30:56 +0100 Subject: [PATCH 8/8] #208 - Added a time limits for regex matchers --- src/search/omnisearch.ts | 12 +++++++++--- src/tools/utils.ts | 14 ++++++++++++-- src/vendor/parse-query.ts | 25 ++++++++++++++++--------- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/search/omnisearch.ts b/src/search/omnisearch.ts index a7d7eb4..0a06b8f 100644 --- a/src/search/omnisearch.ts +++ b/src/search/omnisearch.ts @@ -14,6 +14,7 @@ import { splitCamelCase, stringsToRegex, stripMarkdownCharacters, + warnDebug, } from '../tools/utils' import { Notice } from 'obsidian' import type { Query } from './query' @@ -144,11 +145,11 @@ export class Omnisearch { logDebug('Indexing into search engine', docs) // Update the list of indexed docs docs.forEach(doc => this.indexedDocuments.set(doc.path, doc.mtime)) - + // Discard files that may have been already added (though it shouldn't happen) const alreadyAdded = docs.filter(doc => this.minisearch.has(doc.path)) this.removeFromPaths(alreadyAdded.map(o => o.path)) - + // Add docs to minisearch await this.minisearch.addAllAsync(docs) } @@ -304,11 +305,16 @@ export class Omnisearch { } public getMatches(text: string, reg: RegExp, query: Query): SearchMatch[] { + const startTime = new Date().getTime() let match: RegExpExecArray | null = null const matches: SearchMatch[] = [] let count = 0 while ((match = reg.exec(text)) !== null) { - if (++count >= 100) break // Avoid infinite loops, stop looking after 100 matches + // Avoid infinite loops, stop looking after 100 matches or if we're taking too much time + if (++count >= 100 || new Date().getTime() - startTime > 50) { + warnDebug('Stopped getMatches at', count, 'results') + break + } const m = match[0] if (m) matches.push({ match: m, offset: match.index }) } diff --git a/src/tools/utils.ts b/src/tools/utils.ts index 6d160d0..8ab8131 100644 --- a/src/tools/utils.ts +++ b/src/tools/utils.ts @@ -325,8 +325,18 @@ export function splitCamelCase(text: string): string[] { return text.replace(/([a-z](?=[A-Z]))/g, '$1 ').split(' ') } -export function logDebug(...attr: any[]): void { +export function logDebug(...args: any[]): void { + printDebug(console.log, ...args) +} + +export function warnDebug(...args: any[]): void { + printDebug(console.warn, ...args) +} + +function printDebug(fn: (...args: any[]) => any, ...args: any[]): void { if (settings.verboseLogging) { - console.log(...['Omnisearch -', ...attr]) + const t = new Date() + const ts = `${t.getMinutes()}:${t.getSeconds()}:${t.getMilliseconds()}` + fn(...['Omnisearch -', ts + ' -', ...args]) } } diff --git a/src/vendor/parse-query.ts b/src/vendor/parse-query.ts index 0679133..a0c846a 100644 --- a/src/vendor/parse-query.ts +++ b/src/vendor/parse-query.ts @@ -6,6 +6,8 @@ * MIT Licensed */ +import { warnDebug } from "../tools/utils"; + interface SearchParserOptions { offsets?: boolean tokenize: true @@ -30,7 +32,7 @@ type SearchParserTextOffset = { type SearchParserOffset = ( | SearchParserKeyWordOffset | SearchParserTextOffset -) & { + ) & { offsetStart: number offsetEnd: number } @@ -43,7 +45,7 @@ interface SearchParserResult extends ISearchParserDictionary { export function parseQuery( string: string, - options: SearchParserOptions + options: SearchParserOptions, ): SearchParserResult { // Set a default options object when none is provided if (!options) { @@ -74,9 +76,14 @@ export function parseQuery( const regex = /(\S+:'(?:[^'\\]|\\.)*')|(\S+:"(?:[^"\\]|\\.)*")|(-?"(?:[^"\\]|\\.)*")|(-?'(?:[^'\\]|\\.)*')|\S+|\S+:\S+/g let match - let count = 0 // TODO: FIXME: this is a hack to avoid infinite loops + let count = 0 + const startTime = new Date().getTime() + while ((match = regex.exec(string)) !== null) { - if (++count >= 100) break + if (++count >= 100 || new Date().getTime() - startTime > 50) { + warnDebug('Stopped SearchParserResult at', count, 'results') + break + } let term = match[0] const sepIndex = term.indexOf(':') @@ -291,11 +298,11 @@ export function parseQuery( query[key].from = rangeValues[0] query[key].to = rangeValues[1] } - // When pairs of ranges are specified - // keyword:XXXX-YYYY,AAAA-BBBB - // else if (!rangeValues.length % 2) { - // } - // When only getting a single value, + // When pairs of ranges are specified + // keyword:XXXX-YYYY,AAAA-BBBB + // else if (!rangeValues.length % 2) { + // } + // When only getting a single value, // or an odd number of values else { query[key].from = value