From 46e373e822f9434cae480ab9f6c172b7018aad25 Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 23 Aug 2020 21:53:50 +0200 Subject: [PATCH] "smart" date values can now freely contain whitespaces --- bin/copy-trilium.sh | 3 - spec/search/lexer.spec.js | 5 ++ spec/search/parser.spec.js | 2 +- spec/search/search.spec.js | 14 +++- .../search/services/build_comparator.js | 45 ------------ src/services/search/services/lex.js | 2 +- src/services/search/services/parse.js | 71 ++++++++++++++++--- 7 files changed, 79 insertions(+), 63 deletions(-) diff --git a/bin/copy-trilium.sh b/bin/copy-trilium.sh index 8dd9e06c6..fbb1232c4 100755 --- a/bin/copy-trilium.sh +++ b/bin/copy-trilium.sh @@ -33,9 +33,6 @@ find $DIR/libraries -name "*.map" -type f -delete rm -r $DIR/src/public/app -rm -r $DIR/node_modules/sqlite3/build -rm -r $DIR/node_modules/sqlite3/deps - sed -i -e 's/app\/desktop.js/app-dist\/desktop.js/g' $DIR/src/views/desktop.ejs sed -i -e 's/app\/mobile.js/app-dist\/mobile.js/g' $DIR/src/views/mobile.ejs sed -i -e 's/app\/setup.js/app-dist\/setup.js/g' $DIR/src/views/setup.ejs diff --git a/spec/search/lexer.spec.js b/spec/search/lexer.spec.js index 0b9d83a37..d19d26994 100644 --- a/spec/search/lexer.spec.js +++ b/spec/search/lexer.spec.js @@ -65,6 +65,11 @@ describe("Lexer fulltext", () => { .toEqual(["what's", "u=p", ""]); }); + it("operator characters in expressions are separate tokens", () => { + expect(lex("# abc+=-def**-+d").expressionTokens.map(t => t.token)) + .toEqual(["#", "abc", "+=-", "def", "**-+", "d"]); + }); + it("escaping special characters", () => { expect(lex("hello \\#\\~\\'").fulltextTokens.map(t => t.token)) .toEqual(["hello", "#~'"]); diff --git a/spec/search/parser.spec.js b/spec/search/parser.spec.js index a49b07db9..3de146120 100644 --- a/spec/search/parser.spec.js +++ b/spec/search/parser.spec.js @@ -219,7 +219,7 @@ describe("Invalid expressions", () => { parsingContext }); - expect(parsingContext.error).toEqual(`Error near token "note" in "#first = note.relations.s...", it's possible to compare with constant only.`); + expect(parsingContext.error).toEqual(`Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.`); const rootExp = parse({ fulltextTokens: [], diff --git a/spec/search/search.spec.js b/spec/search/search.spec.js index 26e238157..b205b338d 100644 --- a/spec/search/search.spec.js +++ b/spec/search/search.spec.js @@ -121,11 +121,11 @@ describe("Search", () => { const parsingContext = new ParsingContext(); - let searchResults = searchService.findNotesWithQuery('#established <= 1955-01-01', parsingContext); + let searchResults = searchService.findNotesWithQuery('#established <= "1955-01-01"', parsingContext); expect(searchResults.length).toEqual(1); expect(findNoteByTitle(searchResults, "Hungary")).toBeTruthy(); - searchResults = searchService.findNotesWithQuery('#established > 1955-01-01', parsingContext); + searchResults = searchService.findNotesWithQuery('#established > "1955-01-01"', parsingContext); expect(searchResults.length).toEqual(2); expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); @@ -154,20 +154,30 @@ describe("Search", () => { } test("#year = YEAR", 1); + test("#year = 'YEAR'", 0); test("#year >= YEAR", 1); test("#year <= YEAR", 1); test("#year < YEAR+1", 1); + test("#year < YEAR + 1", 1); + test("#year < year + 1", 1); test("#year > YEAR+1", 0); test("#month = MONTH", 1); + test("#month = month", 1); + test("#month = 'MONTH'", 0); test("#date = TODAY", 1); + test("#date = today", 1); + test("#date = 'today'", 0); test("#date > TODAY", 0); test("#date > TODAY-1", 1); + test("#date > TODAY - 1", 1); test("#date < TODAY+1", 1); + test("#date < TODAY + 1", 1); test("#date < 'TODAY + 1'", 1); test("#dateTime <= NOW+10", 1); + test("#dateTime <= NOW + 10", 1); test("#dateTime < NOW-10", 0); test("#dateTime >= NOW-10", 1); test("#dateTime < NOW-10", 0); diff --git a/src/services/search/services/build_comparator.js b/src/services/search/services/build_comparator.js index ef589aacf..9f6e602f7 100644 --- a/src/services/search/services/build_comparator.js +++ b/src/services/search/services/build_comparator.js @@ -1,5 +1,3 @@ -const dayjs = require("dayjs"); - const stringComparators = { "=": comparedValue => (val => val === comparedValue), "!=": comparedValue => (val => val !== comparedValue), @@ -19,52 +17,9 @@ const numericComparators = { "<=": comparedValue => (val => parseFloat(val) <= comparedValue) }; -const smartValueRegex = /^(NOW|TODAY|WEEK|MONTH|YEAR) *([+\-] *\d+)?$/i; - -function calculateSmartValue(v) { - const match = smartValueRegex.exec(v); - if (match === null) { - return v; - } - - const keyword = match[1].toUpperCase(); - const num = match[2] ? parseInt(match[2].replace(/ /g, "")) : 0; // can contain spaces between sign and digits - - let format, date; - - if (keyword === 'NOW') { - date = dayjs().add(num, 'second'); - format = "YYYY-MM-DD HH:mm:ss"; - } - else if (keyword === 'TODAY') { - date = dayjs().add(num, 'day'); - format = "YYYY-MM-DD"; - } - else if (keyword === 'WEEK') { - // FIXME: this will always use sunday as start of the week - date = dayjs().startOf('week').add(7 * num, 'day'); - format = "YYYY-MM-DD"; - } - else if (keyword === 'MONTH') { - date = dayjs().add(num, 'month'); - format = "YYYY-MM"; - } - else if (keyword === 'YEAR') { - date = dayjs().add(num, 'year'); - format = "YYYY"; - } - else { - throw new Error("Unrecognized keyword: " + keyword); - } - - return date.format(format); -} - function buildComparator(operator, comparedValue) { comparedValue = comparedValue.toLowerCase(); - comparedValue = calculateSmartValue(comparedValue); - if (operator in numericComparators && !isNaN(comparedValue)) { return numericComparators[operator](parseFloat(comparedValue)); } diff --git a/src/services/search/services/lex.js b/src/services/search/services/lex.js index 4f6ab8b97..bb80876e5 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 abc908441..91f7baa99 100644 --- a/src/services/search/services/parse.js +++ b/src/services/search/services/parse.js @@ -1,5 +1,6 @@ "use strict"; +const dayjs = require("dayjs"); const AndExp = require('../expressions/and.js'); const OrExp = require('../expressions/or.js'); const NotExp = require('../expressions/not.js'); @@ -14,7 +15,7 @@ const NoteCacheFulltextExp = require('../expressions/note_cache_flat_text.js'); const NoteContentProtectedFulltextExp = require('../expressions/note_content_protected_fulltext.js'); const NoteContentUnprotectedFulltextExp = require('../expressions/note_content_unprotected_fulltext.js'); const OrderByAndLimitExp = require('../expressions/order_by_and_limit.js'); -const comparatorBuilder = require('./build_comparator.js'); +const buildComparator = require('./build_comparator.js'); const ValueExtractor = require('../value_extractor.js'); function getFulltext(tokens, parsingContext) { @@ -41,7 +42,7 @@ function getFulltext(tokens, parsingContext) { return new AndExp([ textSearchExpression, - new PropertyComparisonExp("isarchived", comparatorBuilder("=", "false")) + new PropertyComparisonExp("isarchived", buildComparator("=", "false")) ]); } @@ -69,6 +70,55 @@ function getExpression(tokens, parsingContext, level = 0) { + (endIndex !== parsingContext.originalQuery.length ? "..." : "") + '"'; } + function resolveConstantOperand() { + const operand = tokens[i]; + + if (!operand.inQuotes + && (operand.token.startsWith('#') || operand.token.startsWith('~') || operand.token === 'note')) { + parsingContext.addError(`Error near token "${operand.token}" in ${context(i)}, it's possible to compare with constant only.`); + return null; + } + + if (operand.inQuotes || !["now", "today", "month", "year"].includes(operand.token)) { + return operand.token; + } + + let delta = 0; + + if (i + 2 < tokens.length) { + if (tokens[i + 1].token === '+') { + delta += parseInt(tokens[i + 2].token); + } + else if (tokens[i + 1].token === '-') { + delta -= parseInt(tokens[i + 2].token); + } + } + + let format, date; + + if (operand.token === 'now') { + date = dayjs().add(delta, 'second'); + format = "YYYY-MM-DD HH:mm:ss"; + } + else if (operand.token === 'today') { + date = dayjs().add(delta, 'day'); + format = "YYYY-MM-DD"; + } + else if (operand.token === 'month') { + date = dayjs().add(delta, 'month'); + format = "YYYY-MM"; + } + else if (operand.token === 'year') { + date = dayjs().add(delta, 'year'); + format = "YYYY"; + } + else { + throw new Error("Unrecognized keyword: " + operand.token); + } + + return date.format(format); + } + function parseNoteProperty() { if (tokens[i].token !== '.') { parsingContext.addError('Expected "." to separate field path'); @@ -150,7 +200,7 @@ function getExpression(tokens, parsingContext, level = 0) { const propertyName = tokens[i].token; const operator = tokens[i + 1].token; const comparedValue = tokens[i + 2].token; - const comparator = comparatorBuilder(operator, comparedValue); + const comparator = buildComparator(operator, comparedValue); if (!comparator) { parsingContext.addError(`Can't find operator '${operator}' in ${context(i)}`); @@ -186,11 +236,12 @@ function getExpression(tokens, parsingContext, level = 0) { if (i < tokens.length - 2 && isOperator(tokens[i + 1].token)) { let operator = tokens[i + 1].token; - const comparedValue = tokens[i + 2].token; - if (!tokens[i + 2].inQuotes - && (comparedValue.startsWith('#') || comparedValue.startsWith('~') || comparedValue === 'note')) { - parsingContext.addError(`Error near token "${comparedValue}" in ${context(i)}, it's possible to compare with constant only.`); + i += 2; + + const comparedValue = resolveConstantOperand(); + + if (comparedValue === null) { return; } @@ -200,13 +251,11 @@ function getExpression(tokens, parsingContext, level = 0) { operator = '*=*'; } - const comparator = comparatorBuilder(operator, comparedValue); + const comparator = buildComparator(operator, comparedValue); if (!comparator) { - parsingContext.addError(`Can't find operator '${operator}' in ${context(i)}`); + parsingContext.addError(`Can't find operator '${operator}' in ${context(i - 1)}`); } else { - i += 2; - return new LabelComparisonExp('label', labelName, comparator); } } else {