Merge branch 'master' into excalidraw

This commit is contained in:
zadam
2022-05-12 23:46:52 +02:00
committed by GitHub
20 changed files with 661 additions and 221 deletions

View File

@@ -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);

View File

@@ -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<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
@@ -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

View File

@@ -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();

View File

@@ -170,4 +170,14 @@ export default class EditableCodeTypeWidget extends TypeWidget {
});
}
}
async executeInActiveCodeEditorEvent({callback}) {
if (!this.isActive()) {
return;
}
await this.initialized;
callback(this.codeEditor);
}
}

View File

@@ -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() {

View File

@@ -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');

View File

@@ -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(/&nbsp;/g, ' ');
}
return content.trim();
}
}
module.exports = NoteContentFulltextExp;

View File

@@ -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(/&nbsp;/g, ' ');
}
return content;
}
}
module.exports = NoteContentProtectedFulltextExp;

View File

@@ -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(/&nbsp;/g, ' ');
}
return content;
}
}
module.exports = NoteContentUnprotectedFulltextExp;

View File

@@ -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 = {

View File

@@ -9,7 +9,7 @@ function lex(str) {
let currentWord = '';
function isSymbolAnOperator(chr) {
return ['=', '*', '>', '<', '!', "-", "+"].includes(chr);
return ['=', '*', '>', '<', '!', "-", "+", '%'].includes(chr);
}
function isPreviousSymbolAnOperator() {

View File

@@ -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]})
]);
}