mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
parser tests added
This commit is contained in:
parent
faf4daa577
commit
b26100479d
@ -2,9 +2,102 @@ const parser = require('../src/services/search/parser');
|
||||
|
||||
describe("Parser", () => {
|
||||
it("fulltext parser without content", () => {
|
||||
const exps = parser(["hello", "hi"], [], false);
|
||||
const rootExp = parser(["hello", "hi"], [], false);
|
||||
|
||||
expect(exps.constructor.name).toEqual("NoteCacheFulltextExp");
|
||||
expect(exps.tokens).toEqual(["hello", "hi"]);
|
||||
expect(rootExp.constructor.name).toEqual("NoteCacheFulltextExp");
|
||||
expect(rootExp.tokens).toEqual(["hello", "hi"]);
|
||||
});
|
||||
|
||||
it("fulltext parser with content", () => {
|
||||
const rootExp = parser(["hello", "hi"], [], true);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual("OrExp");
|
||||
const [firstSub, secondSub] = rootExp.subExpressions;
|
||||
|
||||
expect(firstSub.constructor.name).toEqual("NoteCacheFulltextExp");
|
||||
expect(firstSub.tokens).toEqual(["hello", "hi"]);
|
||||
|
||||
expect(secondSub.constructor.name).toEqual("NoteContentFulltextExp");
|
||||
expect(secondSub.tokens).toEqual(["hello", "hi"]);
|
||||
});
|
||||
|
||||
it("simple label comparison", () => {
|
||||
const rootExp = parser([], ["#mylabel", "=", "text"], true);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(rootExp.attributeType).toEqual("label");
|
||||
expect(rootExp.attributeName).toEqual("mylabel");
|
||||
expect(rootExp.comparator).toBeTruthy();
|
||||
});
|
||||
|
||||
it("simple label AND", () => {
|
||||
const rootExp = parser([], ["#first", "=", "text", "AND", "#second", "=", "text"], true);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||
const [firstSub, secondSub] = rootExp.subExpressions;
|
||||
|
||||
expect(firstSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(firstSub.attributeName).toEqual("first");
|
||||
|
||||
expect(secondSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(secondSub.attributeName).toEqual("second");
|
||||
});
|
||||
|
||||
it("simple label AND without explicit AND", () => {
|
||||
const rootExp = parser([], ["#first", "=", "text", "#second", "=", "text"], true);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||
const [firstSub, secondSub] = rootExp.subExpressions;
|
||||
|
||||
expect(firstSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(firstSub.attributeName).toEqual("first");
|
||||
|
||||
expect(secondSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(secondSub.attributeName).toEqual("second");
|
||||
});
|
||||
|
||||
it("simple label OR", () => {
|
||||
const rootExp = parser([], ["#first", "=", "text", "OR", "#second", "=", "text"], true);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual("OrExp");
|
||||
const [firstSub, secondSub] = rootExp.subExpressions;
|
||||
|
||||
expect(firstSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(firstSub.attributeName).toEqual("first");
|
||||
|
||||
expect(secondSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(secondSub.attributeName).toEqual("second");
|
||||
});
|
||||
|
||||
it("fulltext and simple label", () => {
|
||||
const rootExp = parser(["hello"], ["#mylabel", "=", "text"], false);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||
const [firstSub, secondSub] = rootExp.subExpressions;
|
||||
|
||||
expect(firstSub.constructor.name).toEqual("NoteCacheFulltextExp");
|
||||
expect(firstSub.tokens).toEqual(["hello"]);
|
||||
|
||||
expect(secondSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(secondSub.attributeName).toEqual("mylabel");
|
||||
});
|
||||
|
||||
it("label sub-expression", () => {
|
||||
const rootExp = parser([], ["#first", "=", "text", "OR", ["#second", "=", "text", "AND", "#third", "=", "text"]], false);
|
||||
|
||||
expect(rootExp.constructor.name).toEqual("OrExp");
|
||||
const [firstSub, secondSub] = rootExp.subExpressions;
|
||||
|
||||
expect(firstSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(firstSub.attributeName).toEqual("first");
|
||||
|
||||
expect(secondSub.constructor.name).toEqual("AndExp");
|
||||
const [firstSubSub, secondSubSub] = secondSub.subExpressions;
|
||||
|
||||
expect(firstSubSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(firstSubSub.attributeName).toEqual("second");
|
||||
|
||||
expect(secondSubSub.constructor.name).toEqual("FieldComparisonExp");
|
||||
expect(secondSubSub.attributeName).toEqual("third");
|
||||
});
|
||||
});
|
||||
|
@ -11,9 +11,9 @@ class Attribute {
|
||||
/** @param {string} */
|
||||
this.type = row.type;
|
||||
/** @param {string} */
|
||||
this.name = row.name;
|
||||
this.name = row.name.toLowerCase();
|
||||
/** @param {string} */
|
||||
this.value = row.value;
|
||||
this.value = row.value.toLowerCase();
|
||||
/** @param {boolean} */
|
||||
this.isInheritable = !!row.isInheritable;
|
||||
|
||||
|
66
src/services/search/comparator_builder.js
Normal file
66
src/services/search/comparator_builder.js
Normal file
@ -0,0 +1,66 @@
|
||||
const dayjs = require("dayjs");
|
||||
|
||||
const comparators = {
|
||||
"=": comparedValue => (val => val === comparedValue),
|
||||
"!=": comparedValue => (val => val !== comparedValue),
|
||||
">": comparedValue => (val => val > comparedValue),
|
||||
">=": comparedValue => (val => val >= comparedValue),
|
||||
"<": comparedValue => (val => val < comparedValue),
|
||||
"<=": comparedValue => (val => val <= comparedValue),
|
||||
"*=": comparedValue => (val => val.endsWith(comparedValue)),
|
||||
"=*": comparedValue => (val => val.startsWith(comparedValue)),
|
||||
"*=*": comparedValue => (val => val.includes(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 comparators) {
|
||||
return comparators[operator](comparedValue);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = buildComparator;
|
@ -1,19 +1,20 @@
|
||||
"use strict";
|
||||
|
||||
class AndExp {
|
||||
constructor(subExpressions) {
|
||||
this.subExpressions = subExpressions;
|
||||
}
|
||||
|
||||
static of(subExpressions) {
|
||||
subExpressions = subExpressions.filter(exp => !!exp);
|
||||
|
||||
if (subExpressions.length === 1) {
|
||||
return subExpressions[0];
|
||||
}
|
||||
else {
|
||||
} else if (subExpressions.length > 0) {
|
||||
return new AndExp(subExpressions);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(subExpressions) {
|
||||
this.subExpressions = subExpressions;
|
||||
}
|
||||
|
||||
execute(noteSet, searchContext) {
|
||||
for (const subExpression of this.subExpressions) {
|
||||
noteSet = subExpression.execute(noteSet, searchContext);
|
||||
|
@ -4,11 +4,10 @@ const NoteSet = require('../note_set');
|
||||
const noteCache = require('../../note_cache/note_cache');
|
||||
|
||||
class FieldComparisonExp {
|
||||
constructor(attributeType, attributeName, operator, attributeValue) {
|
||||
constructor(attributeType, attributeName, comparator) {
|
||||
this.attributeType = attributeType;
|
||||
this.attributeName = attributeName;
|
||||
this.operator = operator;
|
||||
this.attributeValue = attributeValue;
|
||||
this.comparator = comparator;
|
||||
}
|
||||
|
||||
execute(noteSet) {
|
||||
@ -18,7 +17,7 @@ class FieldComparisonExp {
|
||||
for (const attr of attrs) {
|
||||
const note = attr.note;
|
||||
|
||||
if (noteSet.hasNoteId(note.noteId) && attr.value === this.attributeValue) {
|
||||
if (noteSet.hasNoteId(note.noteId) && this.comparator(attr.value)) {
|
||||
if (attr.isInheritable) {
|
||||
resultNoteSet.addAll(note.subtreeNotesIncludingTemplated);
|
||||
}
|
||||
|
@ -3,6 +3,17 @@
|
||||
const NoteSet = require('../note_set');
|
||||
|
||||
class OrExp {
|
||||
static of(subExpressions) {
|
||||
subExpressions = subExpressions.filter(exp => !!exp);
|
||||
|
||||
if (subExpressions.length === 1) {
|
||||
return subExpressions[0];
|
||||
}
|
||||
else if (subExpressions.length > 0) {
|
||||
return new OrExp(subExpressions);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(subExpressions) {
|
||||
this.subExpressions = subExpressions;
|
||||
}
|
||||
|
@ -5,28 +5,32 @@ const AttributeExistsExp = require('./expressions/attribute_exists');
|
||||
const FieldComparisonExp = require('./expressions/field_comparison');
|
||||
const NoteCacheFulltextExp = require('./expressions/note_cache_fulltext');
|
||||
const NoteContentFulltextExp = require('./expressions/note_content_fulltext');
|
||||
const comparatorBuilder = require('./comparator_builder');
|
||||
|
||||
function getFulltext(tokens, includingNoteContent) {
|
||||
if (includingNoteContent) {
|
||||
return [
|
||||
new OrExp([
|
||||
new NoteCacheFulltextExp(tokens),
|
||||
new NoteContentFulltextExp(tokens)
|
||||
])
|
||||
]
|
||||
if (tokens.length === 0) {
|
||||
return null;
|
||||
}
|
||||
else if (includingNoteContent) {
|
||||
return new OrExp([
|
||||
new NoteCacheFulltextExp(tokens),
|
||||
new NoteContentFulltextExp(tokens)
|
||||
]);
|
||||
}
|
||||
else {
|
||||
return [
|
||||
new NoteCacheFulltextExp(tokens)
|
||||
]
|
||||
return new NoteCacheFulltextExp(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
function isOperator(str) {
|
||||
return str.matches(/^[=<>*]+$/);
|
||||
return str.match(/^[=<>*]+$/);
|
||||
}
|
||||
|
||||
function getExpressions(tokens) {
|
||||
function getExpression(tokens) {
|
||||
if (tokens.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expressions = [];
|
||||
let op = null;
|
||||
|
||||
@ -38,13 +42,22 @@ function getExpressions(tokens) {
|
||||
}
|
||||
|
||||
if (Array.isArray(token)) {
|
||||
expressions.push(getExpressions(token));
|
||||
expressions.push(getExpression(token));
|
||||
}
|
||||
else if (token.startsWith('#') || token.startsWith('@')) {
|
||||
const type = token.startsWith('#') ? 'label' : 'relation';
|
||||
|
||||
if (i < tokens.length - 2 && isOperator(tokens[i + 1])) {
|
||||
expressions.push(new FieldComparisonExp(type, token.substr(1), tokens[i + 1], tokens[i + 2]));
|
||||
const operator = tokens[i + 1];
|
||||
const comparedValue = tokens[i + 2];
|
||||
|
||||
const comparator = comparatorBuilder(operator, comparedValue);
|
||||
|
||||
if (!comparator) {
|
||||
throw new Error(`Can't find operator '${operator}'`);
|
||||
}
|
||||
|
||||
expressions.push(new FieldComparisonExp(type, token.substr(1), comparator));
|
||||
|
||||
i += 2;
|
||||
}
|
||||
@ -72,13 +85,18 @@ function getExpressions(tokens) {
|
||||
}
|
||||
}
|
||||
|
||||
return expressions;
|
||||
if (op === null || op === 'and') {
|
||||
return AndExp.of(expressions);
|
||||
}
|
||||
else if (op === 'or') {
|
||||
return OrExp.of(expressions);
|
||||
}
|
||||
}
|
||||
|
||||
function parse(fulltextTokens, expressionTokens, includingNoteContent) {
|
||||
return AndExp.of([
|
||||
...getFulltext(fulltextTokens, includingNoteContent),
|
||||
...getExpressions(expressionTokens)
|
||||
getFulltext(fulltextTokens, includingNoteContent),
|
||||
getExpression(expressionTokens)
|
||||
]);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user