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", () => {
|
describe("Parser", () => {
|
||||||
it("fulltext parser without content", () => {
|
it("fulltext parser without content", () => {
|
||||||
const exps = parser(["hello", "hi"], [], false);
|
const rootExp = parser(["hello", "hi"], [], false);
|
||||||
|
|
||||||
expect(exps.constructor.name).toEqual("NoteCacheFulltextExp");
|
expect(rootExp.constructor.name).toEqual("NoteCacheFulltextExp");
|
||||||
expect(exps.tokens).toEqual(["hello", "hi"]);
|
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} */
|
/** @param {string} */
|
||||||
this.type = row.type;
|
this.type = row.type;
|
||||||
/** @param {string} */
|
/** @param {string} */
|
||||||
this.name = row.name;
|
this.name = row.name.toLowerCase();
|
||||||
/** @param {string} */
|
/** @param {string} */
|
||||||
this.value = row.value;
|
this.value = row.value.toLowerCase();
|
||||||
/** @param {boolean} */
|
/** @param {boolean} */
|
||||||
this.isInheritable = !!row.isInheritable;
|
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";
|
"use strict";
|
||||||
|
|
||||||
class AndExp {
|
class AndExp {
|
||||||
constructor(subExpressions) {
|
|
||||||
this.subExpressions = subExpressions;
|
|
||||||
}
|
|
||||||
|
|
||||||
static of(subExpressions) {
|
static of(subExpressions) {
|
||||||
|
subExpressions = subExpressions.filter(exp => !!exp);
|
||||||
|
|
||||||
if (subExpressions.length === 1) {
|
if (subExpressions.length === 1) {
|
||||||
return subExpressions[0];
|
return subExpressions[0];
|
||||||
}
|
} else if (subExpressions.length > 0) {
|
||||||
else {
|
|
||||||
return new AndExp(subExpressions);
|
return new AndExp(subExpressions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(subExpressions) {
|
||||||
|
this.subExpressions = subExpressions;
|
||||||
|
}
|
||||||
|
|
||||||
execute(noteSet, searchContext) {
|
execute(noteSet, searchContext) {
|
||||||
for (const subExpression of this.subExpressions) {
|
for (const subExpression of this.subExpressions) {
|
||||||
noteSet = subExpression.execute(noteSet, searchContext);
|
noteSet = subExpression.execute(noteSet, searchContext);
|
||||||
|
@ -4,11 +4,10 @@ const NoteSet = require('../note_set');
|
|||||||
const noteCache = require('../../note_cache/note_cache');
|
const noteCache = require('../../note_cache/note_cache');
|
||||||
|
|
||||||
class FieldComparisonExp {
|
class FieldComparisonExp {
|
||||||
constructor(attributeType, attributeName, operator, attributeValue) {
|
constructor(attributeType, attributeName, comparator) {
|
||||||
this.attributeType = attributeType;
|
this.attributeType = attributeType;
|
||||||
this.attributeName = attributeName;
|
this.attributeName = attributeName;
|
||||||
this.operator = operator;
|
this.comparator = comparator;
|
||||||
this.attributeValue = attributeValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(noteSet) {
|
execute(noteSet) {
|
||||||
@ -18,7 +17,7 @@ class FieldComparisonExp {
|
|||||||
for (const attr of attrs) {
|
for (const attr of attrs) {
|
||||||
const note = attr.note;
|
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) {
|
if (attr.isInheritable) {
|
||||||
resultNoteSet.addAll(note.subtreeNotesIncludingTemplated);
|
resultNoteSet.addAll(note.subtreeNotesIncludingTemplated);
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,17 @@
|
|||||||
const NoteSet = require('../note_set');
|
const NoteSet = require('../note_set');
|
||||||
|
|
||||||
class OrExp {
|
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) {
|
constructor(subExpressions) {
|
||||||
this.subExpressions = subExpressions;
|
this.subExpressions = subExpressions;
|
||||||
}
|
}
|
||||||
|
@ -5,28 +5,32 @@ const AttributeExistsExp = require('./expressions/attribute_exists');
|
|||||||
const FieldComparisonExp = require('./expressions/field_comparison');
|
const FieldComparisonExp = require('./expressions/field_comparison');
|
||||||
const NoteCacheFulltextExp = require('./expressions/note_cache_fulltext');
|
const NoteCacheFulltextExp = require('./expressions/note_cache_fulltext');
|
||||||
const NoteContentFulltextExp = require('./expressions/note_content_fulltext');
|
const NoteContentFulltextExp = require('./expressions/note_content_fulltext');
|
||||||
|
const comparatorBuilder = require('./comparator_builder');
|
||||||
|
|
||||||
function getFulltext(tokens, includingNoteContent) {
|
function getFulltext(tokens, includingNoteContent) {
|
||||||
if (includingNoteContent) {
|
if (tokens.length === 0) {
|
||||||
return [
|
return null;
|
||||||
new OrExp([
|
}
|
||||||
new NoteCacheFulltextExp(tokens),
|
else if (includingNoteContent) {
|
||||||
new NoteContentFulltextExp(tokens)
|
return new OrExp([
|
||||||
])
|
new NoteCacheFulltextExp(tokens),
|
||||||
]
|
new NoteContentFulltextExp(tokens)
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return [
|
return new NoteCacheFulltextExp(tokens);
|
||||||
new NoteCacheFulltextExp(tokens)
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOperator(str) {
|
function isOperator(str) {
|
||||||
return str.matches(/^[=<>*]+$/);
|
return str.match(/^[=<>*]+$/);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExpressions(tokens) {
|
function getExpression(tokens) {
|
||||||
|
if (tokens.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const expressions = [];
|
const expressions = [];
|
||||||
let op = null;
|
let op = null;
|
||||||
|
|
||||||
@ -38,13 +42,22 @@ function getExpressions(tokens) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(token)) {
|
if (Array.isArray(token)) {
|
||||||
expressions.push(getExpressions(token));
|
expressions.push(getExpression(token));
|
||||||
}
|
}
|
||||||
else if (token.startsWith('#') || token.startsWith('@')) {
|
else if (token.startsWith('#') || token.startsWith('@')) {
|
||||||
const type = token.startsWith('#') ? 'label' : 'relation';
|
const type = token.startsWith('#') ? 'label' : 'relation';
|
||||||
|
|
||||||
if (i < tokens.length - 2 && isOperator(tokens[i + 1])) {
|
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;
|
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) {
|
function parse(fulltextTokens, expressionTokens, includingNoteContent) {
|
||||||
return AndExp.of([
|
return AndExp.of([
|
||||||
...getFulltext(fulltextTokens, includingNoteContent),
|
getFulltext(fulltextTokens, includingNoteContent),
|
||||||
...getExpressions(expressionTokens)
|
getExpression(expressionTokens)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user