mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	parser tests added
This commit is contained in:
		| @@ -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) | ||||||
|     ]); |     ]); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user