diff --git a/db/migrations/0195__remove_recent_notes_from_entity_changes.sql b/db/migrations/0195__remove_recent_notes_from_entity_changes.sql new file mode 100644 index 000000000..834db7fe9 --- /dev/null +++ b/db/migrations/0195__remove_recent_notes_from_entity_changes.sql @@ -0,0 +1,2 @@ +-- removing potential remnants of recent notes in entity changes, see https://github.com/zadam/trilium/issues/2842 +DELETE FROM entity_changes WHERE entityName = 'recent_notes'; diff --git a/docs/backend_api/becca_entities_note.js.html b/docs/backend_api/becca_entities_note.js.html index ad2571123..3af1da710 100644 --- a/docs/backend_api/becca_entities_note.js.html +++ b/docs/backend_api/becca_entities_note.js.html @@ -266,7 +266,7 @@ class Note extends AbstractEntity { setContent(content, ignoreMissingProtectedSession = false) { if (content === null || content === undefined) { - throw new Error(`Cannot set null content to note ${this.noteId}`); + throw new Error(`Cannot set null content to note '${this.noteId}'`); } if (this.isStringNote()) { @@ -288,7 +288,7 @@ class Note extends AbstractEntity { pojo.content = protectedSessionService.encrypt(pojo.content); } else if (!ignoreMissingProtectedSession) { - throw new Error(`Cannot update content of noteId=${this.noteId} since we're out of protected session.`); + throw new Error(`Cannot update content of noteId '${this.noteId}' since we're out of protected session.`); } } diff --git a/docs/frontend_api/FrontendScriptApi.html b/docs/frontend_api/FrontendScriptApi.html index 59f3b0fff..4a1c6b2da 100644 --- a/docs/frontend_api/FrontendScriptApi.html +++ b/docs/frontend_api/FrontendScriptApi.html @@ -1928,7 +1928,7 @@
Source:
@@ -2479,6 +2479,223 @@ +

getActiveNoteDetailWidget() → {Promise.<NoteDetailWidget>}

+ + + + + + +
+ Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the +implementation of actual widget type. +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Promise.<NoteDetailWidget> + + +
+
+ + + + + + + + + + + + + +

getActiveTabCodeEditor() → {Promise.<CodeMirror>}

+ + + + + + +
+ See https://codemirror.net/doc/manual.html#api +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ instance of CodeMirror +
+ + + +
+
+ Type +
+
+ +Promise.<CodeMirror> + + +
+
+ + + + + + + + + + + + +

getActiveTabNote() → {NoteShort}

@@ -2633,7 +2850,7 @@
Source:
@@ -2691,7 +2908,7 @@ -

getActiveTabTextEditor(callback)

+

getActiveTabTextEditor(callbackopt) → {Promise.<CKEditor>}

@@ -2723,6 +2940,8 @@ Type + Attributes + @@ -2743,10 +2962,20 @@ + + + <optional>
+ + + + + + + - method receiving "textEditor" instance + deprecated (use returned promise): callback receiving "textEditor" instance @@ -2787,7 +3016,7 @@
Source:
@@ -2812,6 +3041,183 @@ +
Returns:
+ + +
+ instance of CKEditor +
+ + + +
+
+ Type +
+
+ +Promise.<CKEditor> + + +
+
+ + + + + + + + + + + + + +

getComponentByEl(el) → {Component}

+ + + + + + +
+ Returns component which owns given DOM element (the nearest parent component in DOM tree) +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
el + + +Element + + + + DOM element
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + + + +
+
+ Type +
+
+ +Component + + +
+
+ + @@ -2926,7 +3332,7 @@
Source:
@@ -3081,7 +3487,7 @@
Source:
@@ -3343,7 +3749,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -3806,7 +4212,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -3961,7 +4367,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4116,7 +4522,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4553,7 +4959,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4709,7 +5115,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4865,7 +5271,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5002,7 +5408,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5156,7 +5562,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -6097,7 +6503,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -6248,7 +6654,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -6614,7 +7020,7 @@ Typical use case is when new note has been created, we should wait until it is s
Source:
diff --git a/docs/frontend_api/entities_note_short.js.html b/docs/frontend_api/entities_note_short.js.html index 20a9ad597..724618c18 100644 --- a/docs/frontend_api/entities_note_short.js.html +++ b/docs/frontend_api/entities_note_short.js.html @@ -152,7 +152,7 @@ class NoteShort { return JSON.parse(content); } catch (e) { - console.log(`Cannot parse content of note ${this.noteId}: `, e.message); + console.log(`Cannot parse content of note '${this.noteId}': `, e.message); return null; } diff --git a/docs/frontend_api/services_frontend_script_api.js.html b/docs/frontend_api/services_frontend_script_api.js.html index 451ca46bc..0f47ab437 100644 --- a/docs/frontend_api/services_frontend_script_api.js.html +++ b/docs/frontend_api/services_frontend_script_api.js.html @@ -364,9 +364,27 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance. * * @method - * @param callback - method receiving "textEditor" instance + * @param [callback] - deprecated (use returned promise): callback receiving "textEditor" instance + * @returns {Promise<CKEditor>} instance of CKEditor */ - this.getActiveTabTextEditor = callback => appContext.triggerCommand('executeInActiveEditor', {callback}); + this.getActiveTabTextEditor = callback => new Promise(resolve => appContext.triggerCommand('executeInActiveTextEditor', {callback, resolve})); + + /** + * See https://codemirror.net/doc/manual.html#api + * + * @method + * @returns {Promise<CodeMirror>} instance of CodeMirror + */ + this.getActiveTabCodeEditor = () => new Promise(resolve => appContext.triggerCommand('executeInActiveCodeEditor', {callback: resolve})); + + /** + * Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the + * implementation of actual widget type. + * + * @method + * @returns {Promise<NoteDetailWidget>} + */ + this.getActiveNoteDetailWidget = () => new Promise(resolve => appContext.triggerCommand('executeInActiveNoteDetailWidget', {callback: resolve})); /** * @method @@ -374,6 +392,15 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain */ this.getActiveTabNotePath = () => appContext.tabManager.getActiveContextNotePath(); + /** + * Returns component which owns given DOM element (the nearest parent component in DOM tree) + * + * @method + * @param {Element} el - DOM element + * @returns {Component} + */ + this.getComponentByEl = el => appContext.getComponentByEl(el); + /** * @method * @param {object} $el - jquery object on which to setup the tooltip diff --git a/package.json b/package.json index 8e4220b73..05f51efe7 100644 --- a/package.json +++ b/package.json @@ -27,24 +27,24 @@ "@excalidraw/excalidraw": "0.11.0", "archiver": "5.3.1", "async-mutex": "0.3.2", - "axios": "0.26.1", + "axios": "0.27.2", "better-sqlite3": "7.4.5", "chokidar": "3.5.3", "cls-hooked": "4.2.2", "commonmark": "0.30.0", "cookie-parser": "1.4.6", "csurf": "1.11.0", - "dayjs": "1.11.1", - "ejs": "3.1.6", + "dayjs": "1.11.2", + "ejs": "3.1.8", "electron-debug": "3.2.0", "electron-dl": "3.3.1", "electron-find": "1.0.7", "electron-window-state": "5.0.3", "@electron/remote": "2.0.8", - "express": "4.17.3", + "express": "4.18.1", "express-partial-content": "1.0.2", - "express-rate-limit": "6.3.0", - "express-session": "1.17.2", + "express-rate-limit": "6.4.0", + "express-session": "1.17.3", "fs-extra": "10.1.0", "helmet": "5.0.2", "html": "1.0.0", @@ -80,21 +80,21 @@ "tmp": "0.2.1", "turndown": "7.1.1", "unescape": "1.0.1", - "ws": "8.5.0", + "ws": "8.6.0", "yauzl": "2.10.0" }, "devDependencies": { "cross-env": "7.0.3", - "electron": "16.2.1", + "electron": "16.2.6", "electron-builder": "23.0.3", - "electron-packager": "15.5.0", + "electron-packager": "15.5.1", "electron-rebuild": "3.2.7", "esm": "3.2.25", "jasmine": "4.1.0", "jsdoc": "3.6.10", "lorem-ipsum": "2.0.4", "rcedit": "3.0.1", - "webpack": "5.72.0", + "webpack": "5.72.1", "webpack-cli": "4.9.2" }, "optionalDependencies": { diff --git a/spec/search/parser.spec.js b/spec/search/parser.spec.js index 493fc5d5f..05df9ef40 100644 --- a/spec/search/parser.spec.js +++ b/spec/search/parser.spec.js @@ -58,11 +58,8 @@ describe("Parser", () => { expect(subs[0].constructor.name).toEqual("NoteFlatTextExp"); expect(subs[0].tokens).toEqual(["hello", "hi"]); - expect(subs[1].constructor.name).toEqual("NoteContentProtectedFulltextExp"); + expect(subs[1].constructor.name).toEqual("NoteContentFulltextExp"); expect(subs[1].tokens).toEqual(["hello", "hi"]); - - expect(subs[2].constructor.name).toEqual("NoteContentUnprotectedFulltextExp"); - expect(subs[2].tokens).toEqual(["hello", "hi"]); }); it("simple label comparison", () => { diff --git a/src/public/app/dialogs/markdown_import.js b/src/public/app/dialogs/markdown_import.js index 6faa7737f..e83522cf7 100644 --- a/src/public/app/dialogs/markdown_import.js +++ b/src/public/app/dialogs/markdown_import.js @@ -16,7 +16,7 @@ async function convertMarkdownToHtml(text) { const result = writer.render(parsed); - appContext.triggerCommand('executeInActiveEditor', { + appContext.triggerCommand('executeInActiveTextEditor', { callback: textEditor => { const viewFragment = textEditor.data.processor.toView(result); const modelFragment = textEditor.data.toModel(viewFragment); diff --git a/src/public/app/services/frontend_script_api.js b/src/public/app/services/frontend_script_api.js index bcb8b5301..2cd12f8bc 100644 --- a/src/public/app/services/frontend_script_api.js +++ b/src/public/app/services/frontend_script_api.js @@ -336,9 +336,27 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance. * * @method - * @param callback - method receiving "textEditor" instance + * @param [callback] - deprecated (use returned promise): callback receiving "textEditor" instance + * @returns {Promise} instance of CKEditor */ - this.getActiveTabTextEditor = callback => appContext.triggerCommand('executeInActiveEditor', {callback}); + this.getActiveTabTextEditor = callback => new Promise(resolve => appContext.triggerCommand('executeInActiveTextEditor', {callback, resolve})); + + /** + * See https://codemirror.net/doc/manual.html#api + * + * @method + * @returns {Promise} instance of CodeMirror + */ + this.getActiveTabCodeEditor = () => new Promise(resolve => appContext.triggerCommand('executeInActiveCodeEditor', {callback: resolve})); + + /** + * Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the + * implementation of actual widget type. + * + * @method + * @returns {Promise} + */ + this.getActiveNoteDetailWidget = () => new Promise(resolve => appContext.triggerCommand('executeInActiveNoteDetailWidget', {callback: resolve})); /** * @method @@ -346,6 +364,15 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain */ this.getActiveTabNotePath = () => appContext.tabManager.getActiveContextNotePath(); + /** + * Returns component which owns given DOM element (the nearest parent component in DOM tree) + * + * @method + * @param {Element} el - DOM element + * @returns {Component} + */ + this.getComponentByEl = el => appContext.getComponentByEl(el); + /** * @method * @param {object} $el - jquery object on which to setup the tooltip diff --git a/src/public/app/widgets/note_detail.js b/src/public/app/widgets/note_detail.js index 60dac4202..0a0790fa8 100644 --- a/src/public/app/widgets/note_detail.js +++ b/src/public/app/widgets/note_detail.js @@ -307,6 +307,16 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { } } + async executeInActiveNoteDetailWidgetEvent({callback}) { + if (!this.isActiveNoteContext()) { + return; + } + + await this.initialized; + + callback(this); + } + async cutIntoNoteCommand() { const note = appContext.tabManager.getActiveContextNote(); diff --git a/src/public/app/widgets/type_widgets/editable_code.js b/src/public/app/widgets/type_widgets/editable_code.js index 605d0db2b..7bbc356c3 100644 --- a/src/public/app/widgets/type_widgets/editable_code.js +++ b/src/public/app/widgets/type_widgets/editable_code.js @@ -170,4 +170,14 @@ export default class EditableCodeTypeWidget extends TypeWidget { }); } } + + async executeInActiveCodeEditorEvent({callback}) { + if (!this.isActive()) { + return; + } + + await this.initialized; + + callback(this.codeEditor); + } } diff --git a/src/public/app/widgets/type_widgets/editable_text.js b/src/public/app/widgets/type_widgets/editable_text.js index d497ecbbe..44469fdc6 100644 --- a/src/public/app/widgets/type_widgets/editable_text.js +++ b/src/public/app/widgets/type_widgets/editable_text.js @@ -229,14 +229,18 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { return !selection.isCollapsed; } - async executeInActiveEditorEvent({callback}) { + async executeInActiveTextEditorEvent({callback, resolve}) { if (!this.isActive()) { return; } await this.initialized; - callback(this.textEditor); + if (callback) { + callback(this.textEditor); + } + + resolve(this.textEditor); } addLinkToTextCommand() { diff --git a/src/services/entity_changes.js b/src/services/entity_changes.js index 209e0c7f5..62eabc944 100644 --- a/src/services/entity_changes.js +++ b/src/services/entity_changes.js @@ -135,7 +135,6 @@ function fillAllEntityChanges() { fillEntityChanges("branches", "branchId"); fillEntityChanges("note_revisions", "noteRevisionId"); fillEntityChanges("note_revision_contents", "noteRevisionId"); - fillEntityChanges("recent_notes", "noteId"); fillEntityChanges("attributes", "attributeId"); fillEntityChanges("etapi_tokens", "etapiTokenId"); fillEntityChanges("options", "name", 'isSynced = 1'); diff --git a/src/services/search/expressions/note_content_fulltext.js b/src/services/search/expressions/note_content_fulltext.js new file mode 100644 index 000000000..548578c2b --- /dev/null +++ b/src/services/search/expressions/note_content_fulltext.js @@ -0,0 +1,116 @@ +"use strict"; + +const Expression = require('./expression'); +const NoteSet = require('../note_set'); +const log = require('../../log'); +const becca = require('../../../becca/becca'); +const protectedSessionService = require('../../protected_session'); +const striptags = require('striptags'); +const utils = require("../../utils"); + +const ALLOWED_OPERATORS = ['*=*', '=', '*=', '=*', '%=']; + +const cachedRegexes = {}; + +function getRegex(str) { + if (!(str in cachedRegexes)) { + cachedRegexes[str] = new RegExp(str, 'ms'); // multiline, dot-all + } + + return cachedRegexes[str]; +} + +class NoteContentFulltextExp extends Expression { + constructor(operator, {tokens, raw, flatText}) { + super(); + + if (!ALLOWED_OPERATORS.includes(operator)) { + throw new Error(`Note content can be searched only with operators: ` + ALLOWED_OPERATORS.join(", ") + `, operator ${operator} given.`); + } + + this.operator = operator; + this.tokens = tokens; + this.raw = !!raw; + this.flatText = !!flatText; + } + + execute(inputNoteSet) { + const resultNoteSet = new NoteSet(); + const sql = require('../../sql'); + + for (let {noteId, type, mime, content, isProtected} of sql.iterateRows(` + SELECT noteId, type, mime, content, isProtected + FROM notes JOIN note_contents USING (noteId) + WHERE type IN ('text', 'code', 'mermaid') AND isDeleted = 0`)) { + + if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) { + continue; + } + + if (isProtected) { + if (!protectedSessionService.isProtectedSessionAvailable()) { + continue; + } + + try { + content = protectedSessionService.decryptString(content); + } catch (e) { + log.info(`Cannot decrypt content of note ${noteId}`); + continue; + } + } + + content = this.preprocessContent(content, type, mime); + + if (this.tokens.length === 1) { + const [token] = this.tokens; + + if ((this.operator === '=' && token === content) + || (this.operator === '*=' && content.endsWith(token)) + || (this.operator === '=*' && content.startsWith(token)) + || (this.operator === '*=*' && content.includes(token)) + || (this.operator === '%=' && getRegex(token).test(content))) { + + resultNoteSet.add(becca.notes[noteId]); + } + } + else { + const nonMatchingToken = this.tokens.find(token => + !content.includes(token) && + ( + // in case of default fulltext search we should consider both title, attrs and content + // so e.g. "hello world" should match when "hello" is in title and "world" in content + !this.flatText + || !becca.notes[noteId].getFlatText().includes(token) + ) + ); + + if (!nonMatchingToken) { + resultNoteSet.add(becca.notes[noteId]); + } + } + } + + return resultNoteSet; + } + + preprocessContent(content, type, mime) { + content = utils.normalize(content.toString()); + + if (type === 'text' && mime === 'text/html') { + if (!this.raw && content.length < 20000) { // striptags is slow for very large notes + // allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412 + content = striptags(content, ['a']); + + // at least the closing tag can be easily stripped + content = content.replace(/<\/a>/ig, ""); + } + + content = content.replace(/ /g, ' '); + } + + return content.trim(); + } +} + +module.exports = NoteContentFulltextExp; diff --git a/src/services/search/expressions/note_content_protected_fulltext.js b/src/services/search/expressions/note_content_protected_fulltext.js deleted file mode 100644 index 05130e087..000000000 --- a/src/services/search/expressions/note_content_protected_fulltext.js +++ /dev/null @@ -1,89 +0,0 @@ -"use strict"; - -const Expression = require('./expression'); -const NoteSet = require('../note_set'); -const log = require('../../log'); -const becca = require('../../../becca/becca'); -const protectedSessionService = require('../../protected_session'); -const striptags = require('striptags'); -const utils = require("../../utils"); - -// FIXME: create common subclass with NoteContentUnprotectedFulltextExp to avoid duplication -class NoteContentProtectedFulltextExp extends Expression { - constructor(operator, {tokens, raw, flatText}) { - super(); - - if (operator !== '*=*') { - throw new Error(`Note content can be searched only with *=* operator`); - } - - this.tokens = tokens; - this.raw = !!raw; - this.flatText = !!flatText; - } - - execute(inputNoteSet) { - const resultNoteSet = new NoteSet(); - - if (!protectedSessionService.isProtectedSessionAvailable()) { - return resultNoteSet; - } - - const sql = require('../../sql'); - - for (let {noteId, type, mime, content} of sql.iterateRows(` - SELECT noteId, type, mime, content - FROM notes JOIN note_contents USING (noteId) - WHERE type IN ('text', 'code', 'mermaid') AND isDeleted = 0 AND isProtected = 1`)) { - - if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) { - continue; - } - - try { - content = protectedSessionService.decryptString(content); - } - catch (e) { - log.info(`Cannot decrypt content of note ${noteId}`); - continue; - } - - content = this.preprocessContent(content, type, mime); - - const nonMatchingToken = this.tokens.find(token => - !content.includes(token) && - ( - // in case of default fulltext search we should consider both title, attrs and content - // so e.g. "hello world" should match when "hello" is in title and "world" in content - !this.flatText - || !becca.notes[noteId].getFlatText().includes(token) - ) - ); - - if (!nonMatchingToken) { - resultNoteSet.add(becca.notes[noteId]); - } - } - - return resultNoteSet; - } - - preprocessContent(content, type, mime) { - content = utils.normalize(content.toString()); - - if (type === 'text' && mime === 'text/html') { - if (!this.raw && content.length < 20000) { // striptags is slow for very large notes - // allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412 - content = striptags(content, ['a']); - - // at least the closing tag can be easily stripped - content = content.replace(/<\/a>/ig, ""); - } - - content = content.replace(/ /g, ' '); - } - return content; - } -} - -module.exports = NoteContentProtectedFulltextExp; diff --git a/src/services/search/expressions/note_content_unprotected_fulltext.js b/src/services/search/expressions/note_content_unprotected_fulltext.js deleted file mode 100644 index 7abbd0d78..000000000 --- a/src/services/search/expressions/note_content_unprotected_fulltext.js +++ /dev/null @@ -1,75 +0,0 @@ -"use strict"; - -const Expression = require('./expression'); -const NoteSet = require('../note_set'); -const becca = require('../../../becca/becca'); -const striptags = require('striptags'); -const utils = require("../../utils"); - -// FIXME: create common subclass with NoteContentProtectedFulltextExp to avoid duplication -class NoteContentUnprotectedFulltextExp extends Expression { - constructor(operator, {tokens, raw, flatText}) { - super(); - - if (operator !== '*=*') { - throw new Error(`Note content can be searched only with *=* operator`); - } - - this.tokens = tokens; - this.raw = !!raw; - this.flatText = !!flatText; - } - - execute(inputNoteSet) { - const resultNoteSet = new NoteSet(); - - const sql = require('../../sql'); - - for (let {noteId, type, mime, content} of sql.iterateRows(` - SELECT noteId, type, mime, content - FROM notes JOIN note_contents USING (noteId) - WHERE type IN ('text', 'code', 'mermaid') AND isDeleted = 0 AND isProtected = 0`)) { - - if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) { - continue; - } - - content = this.preprocessContent(content, type, mime); - - const nonMatchingToken = this.tokens.find(token => - !content.includes(token) && - ( - // in case of default fulltext search we should consider both title, attrs and content - // so e.g. "hello world" should match when "hello" is in title and "world" in content - !this.flatText - || !becca.notes[noteId].getFlatText().includes(token) - ) - ); - - if (!nonMatchingToken) { - resultNoteSet.add(becca.notes[noteId]); - } - } - - return resultNoteSet; - } - - preprocessContent(content, type, mime) { - content = utils.normalize(content.toString()); - - if (type === 'text' && mime === 'text/html') { - if (!this.raw && content.length < 20000) { // striptags is slow for very large notes - // allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412 - content = striptags(content, ['a']); - - // at least the closing tag can be easily stripped - content = content.replace(/<\/a>/ig, ""); - } - - content = content.replace(/ /g, ' '); - } - return content; - } -} - -module.exports = NoteContentUnprotectedFulltextExp; diff --git a/src/services/search/services/build_comparator.js b/src/services/search/services/build_comparator.js index 561f3df66..6d3ba463a 100644 --- a/src/services/search/services/build_comparator.js +++ b/src/services/search/services/build_comparator.js @@ -1,3 +1,13 @@ +const cachedRegexes = {}; + +function getRegex(str) { + if (!(str in cachedRegexes)) { + cachedRegexes[str] = new RegExp(str); + } + + return cachedRegexes[str]; +} + const stringComparators = { "=": comparedValue => (val => val === comparedValue), "!=": comparedValue => (val => val !== comparedValue), @@ -8,6 +18,7 @@ const stringComparators = { "*=": comparedValue => (val => val && val.endsWith(comparedValue)), "=*": comparedValue => (val => val && val.startsWith(comparedValue)), "*=*": comparedValue => (val => val && val.includes(comparedValue)), + "%=": comparedValue => (val => val && !!getRegex(comparedValue).test(val)), }; const numericComparators = { diff --git a/src/services/search/services/lex.js b/src/services/search/services/lex.js index 5234900f7..c6bdc2dfd 100644 --- a/src/services/search/services/lex.js +++ b/src/services/search/services/lex.js @@ -9,7 +9,7 @@ function lex(str) { let currentWord = ''; function isSymbolAnOperator(chr) { - return ['=', '*', '>', '<', '!', "-", "+"].includes(chr); + return ['=', '*', '>', '<', '!', "-", "+", '%'].includes(chr); } function isPreviousSymbolAnOperator() { diff --git a/src/services/search/services/parse.js b/src/services/search/services/parse.js index 9ba5ce506..8ab99fe31 100644 --- a/src/services/search/services/parse.js +++ b/src/services/search/services/parse.js @@ -12,8 +12,7 @@ const PropertyComparisonExp = require('../expressions/property_comparison'); const AttributeExistsExp = require('../expressions/attribute_exists'); const LabelComparisonExp = require('../expressions/label_comparison'); const NoteFlatTextExp = require('../expressions/note_flat_text'); -const NoteContentProtectedFulltextExp = require('../expressions/note_content_protected_fulltext'); -const NoteContentUnprotectedFulltextExp = require('../expressions/note_content_unprotected_fulltext'); +const NoteContentFulltextExp = require('../expressions/note_content_fulltext.js'); const OrderByAndLimitExp = require('../expressions/order_by_and_limit'); const AncestorExp = require("../expressions/ancestor"); const buildComparator = require('./build_comparator'); @@ -32,8 +31,7 @@ function getFulltext(tokens, searchContext) { if (!searchContext.fastSearch) { return new OrExp([ new NoteFlatTextExp(tokens), - new NoteContentProtectedFulltextExp('*=*', {tokens, flatText: true}), - new NoteContentUnprotectedFulltextExp('*=*', {tokens, flatText: true}) + new NoteContentFulltextExp('*=*', {tokens, flatText: true}) ]); } else { @@ -42,7 +40,7 @@ function getFulltext(tokens, searchContext) { } function isOperator(str) { - return str.match(/^[!=<>*]+$/); + return str.match(/^[!=<>*%]+$/); } function getExpression(tokens, searchContext, level = 0) { @@ -140,10 +138,7 @@ function getExpression(tokens, searchContext, level = 0) { i++; - return new OrExp([ - new NoteContentUnprotectedFulltextExp(operator, {tokens: [tokens[i].token], raw }), - new NoteContentProtectedFulltextExp(operator, {tokens: [tokens[i].token], raw }) - ]); + return new NoteContentFulltextExp(operator, {tokens: [tokens[i].token], raw }); } if (tokens[i].token === 'parents') { @@ -196,8 +191,7 @@ function getExpression(tokens, searchContext, level = 0) { return new OrExp([ new PropertyComparisonExp(searchContext, 'title', '*=*', tokens[i].token), - new NoteContentProtectedFulltextExp('*=*', {tokens: [tokens[i].token]}), - new NoteContentUnprotectedFulltextExp('*=*', {tokens: [tokens[i].token]}) + new NoteContentFulltextExp('*=*', {tokens: [tokens[i].token]}) ]); } diff --git a/trilium.iml b/trilium.iml index 6905b014e..cffe441d0 100644 --- a/trilium.iml +++ b/trilium.iml @@ -13,9 +13,10 @@ + - + \ No newline at end of file