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 @@
+
+
+
+
+
+
+
+
+ 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:
+
+
+
+
+
+
+ Name |
+
+
+ Type |
+
+
+
+
+
+ Description |
+
+
+
+
+
+
+
+
+ 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