mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	parens handler + parser in progress
This commit is contained in:
		| @@ -1,61 +1,61 @@ | |||||||
| const lexerSpec = require('../src/services/search/lexer'); | const lexer = require('../src/services/search/lexer'); | ||||||
|  |  | ||||||
| describe("Lexer fulltext", () => { | describe("Lexer fulltext", () => { | ||||||
|     it("simple lexing", () => { |     it("simple lexing", () => { | ||||||
|         expect(lexerSpec("hello world").fulltextTokens) |         expect(lexer("hello world").fulltextTokens) | ||||||
|             .toEqual(["hello", "world"]); |             .toEqual(["hello", "world"]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("use quotes to keep words together", () => { |     it("use quotes to keep words together", () => { | ||||||
|         expect(lexerSpec("'hello world' my friend").fulltextTokens) |         expect(lexer("'hello world' my friend").fulltextTokens) | ||||||
|             .toEqual(["hello world", "my", "friend"]); |             .toEqual(["hello world", "my", "friend"]); | ||||||
|  |  | ||||||
|         expect(lexerSpec('"hello world" my friend').fulltextTokens) |         expect(lexer('"hello world" my friend').fulltextTokens) | ||||||
|             .toEqual(["hello world", "my", "friend"]); |             .toEqual(["hello world", "my", "friend"]); | ||||||
|  |  | ||||||
|         expect(lexerSpec('`hello world` my friend').fulltextTokens) |         expect(lexer('`hello world` my friend').fulltextTokens) | ||||||
|             .toEqual(["hello world", "my", "friend"]); |             .toEqual(["hello world", "my", "friend"]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("you can use different quotes and other special characters inside quotes", () => { |     it("you can use different quotes and other special characters inside quotes", () => { | ||||||
|         expect(lexerSpec("'I can use \" or ` or #@=*' without problem").fulltextTokens) |         expect(lexer("'I can use \" or ` or #@=*' without problem").fulltextTokens) | ||||||
|             .toEqual(["I can use \" or ` or #@=*", "without", "problem"]); |             .toEqual(["I can use \" or ` or #@=*", "without", "problem"]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("if quote is not ended then it's just one long token", () => { |     it("if quote is not ended then it's just one long token", () => { | ||||||
|         expect(lexerSpec("'unfinished quote").fulltextTokens) |         expect(lexer("'unfinished quote").fulltextTokens) | ||||||
|             .toEqual(["unfinished quote"]); |             .toEqual(["unfinished quote"]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("parenthesis and symbols in fulltext section are just normal characters", () => { |     it("parenthesis and symbols in fulltext section are just normal characters", () => { | ||||||
|         expect(lexerSpec("what's u=p <b(r*t)h>").fulltextTokens) |         expect(lexer("what's u=p <b(r*t)h>").fulltextTokens) | ||||||
|             .toEqual(["what's", "u=p", "<b(r*t)h>"]); |             .toEqual(["what's", "u=p", "<b(r*t)h>"]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("escaping special characters", () => { |     it("escaping special characters", () => { | ||||||
|         expect(lexerSpec("hello \\#\\@\\'").fulltextTokens) |         expect(lexer("hello \\#\\@\\'").fulltextTokens) | ||||||
|             .toEqual(["hello", "#@'"]); |             .toEqual(["hello", "#@'"]); | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| describe("Lexer expression", () => { | describe("Lexer expression", () => { | ||||||
|     it("simple attribute existence", () => { |     it("simple attribute existence", () => { | ||||||
|         expect(lexerSpec("#label @relation").expressionTokens) |         expect(lexer("#label @relation").expressionTokens) | ||||||
|             .toEqual(["#label", "@relation"]); |             .toEqual(["#label", "@relation"]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("simple label operators", () => { |     it("simple label operators", () => { | ||||||
|         expect(lexerSpec("#label*=*text").expressionTokens) |         expect(lexer("#label*=*text").expressionTokens) | ||||||
|             .toEqual(["#label", "*=*", "text"]); |             .toEqual(["#label", "*=*", "text"]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("spaces in attribute names and values", () => { |     it("spaces in attribute names and values", () => { | ||||||
|         expect(lexerSpec(`#'long label'="hello o' world" @'long relation'`).expressionTokens) |         expect(lexer(`#'long label'="hello o' world" @'long relation'`).expressionTokens) | ||||||
|             .toEqual(["#long label", "=", "hello o' world", "@long relation"]); |             .toEqual(["#long label", "=", "hello o' world", "@long relation"]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("complex expressions with and, or and parenthesis", () => { |     it("complex expressions with and, or and parenthesis", () => { | ||||||
|         expect(lexerSpec(`# (#label=text OR #second=text) AND @relation`).expressionTokens) |         expect(lexer(`# (#label=text OR #second=text) AND @relation`).expressionTokens) | ||||||
|             .toEqual(["#", "(", "#label", "=", "text", "OR", "#second", "=", "text", ")", "AND", "@relation"]); |             .toEqual(["#", "(", "#label", "=", "text", "OR", "#second", "=", "text", ")", "AND", "@relation"]); | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
|   | |||||||
							
								
								
									
										21
									
								
								spec/parens.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								spec/parens.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | const parens = require('../src/services/search/parens'); | ||||||
|  |  | ||||||
|  | describe("Parens handler", () => { | ||||||
|  |     it("handles parens", () => {console.log(parens(["(", "hello", ")", "and", "(", "(", "pick", "one", ")", "and", "another", ")"])) | ||||||
|  |         expect(parens(["(", "hello", ")", "and", "(", "(", "pick", "one", ")", "and", "another", ")"])) | ||||||
|  |             .toEqual([ | ||||||
|  |                 [ | ||||||
|  |                     "hello" | ||||||
|  |                 ], | ||||||
|  |                 "and", | ||||||
|  |                 [ | ||||||
|  |                     [ | ||||||
|  |                         "pick", | ||||||
|  |                         "one" | ||||||
|  |                     ], | ||||||
|  |                     "and", | ||||||
|  |                     "another" | ||||||
|  |                 ] | ||||||
|  |             ]); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
| @@ -5,6 +5,15 @@ class AndExp { | |||||||
|         this.subExpressions = subExpressions; |         this.subExpressions = subExpressions; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     static of(subExpressions) { | ||||||
|  |         if (subExpressions.length === 1) { | ||||||
|  |             return subExpressions[0]; | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             return new AndExp(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,9 +4,10 @@ const NoteSet = require('../note_set'); | |||||||
| const noteCache = require('../../note_cache/note_cache'); | const noteCache = require('../../note_cache/note_cache'); | ||||||
|  |  | ||||||
| class EqualsExp { | class EqualsExp { | ||||||
|     constructor(attributeType, attributeName, attributeValue) { |     constructor(attributeType, attributeName, operator, attributeValue) { | ||||||
|         this.attributeType = attributeType; |         this.attributeType = attributeType; | ||||||
|         this.attributeName = attributeName; |         this.attributeName = attributeName; | ||||||
|  |         this.operator = operator; | ||||||
|         this.attributeValue = attributeValue; |         this.attributeValue = attributeValue; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
|  | const NoteSet = require('../note_set'); | ||||||
|  |  | ||||||
| class OrExp { | class OrExp { | ||||||
|     constructor(subExpressions) { |     constructor(subExpressions) { | ||||||
|         this.subExpressions = subExpressions; |         this.subExpressions = subExpressions; | ||||||
|   | |||||||
							
								
								
									
										43
									
								
								src/services/search/parens.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/services/search/parens.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | /** | ||||||
|  |  * This will create a recursive object from list of tokens - tokens between parenthesis are grouped in a single array | ||||||
|  |  */ | ||||||
|  | function parens(tokens) { | ||||||
|  |     if (tokens.length === 0) { | ||||||
|  |         throw new Error("Empty expression."); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     while (true) { | ||||||
|  |         const leftIdx = tokens.findIndex(token => token === '('); | ||||||
|  |  | ||||||
|  |         if (leftIdx === -1) { | ||||||
|  |             return tokens; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let rightIdx; | ||||||
|  |         let parensLevel = 0 | ||||||
|  |  | ||||||
|  |         for (rightIdx = leftIdx; rightIdx < tokens.length; rightIdx++) { | ||||||
|  |             if (tokens[rightIdx] === ')') { | ||||||
|  |                 parensLevel--; | ||||||
|  |  | ||||||
|  |                 if (parensLevel === 0) { | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |             } else if (tokens[rightIdx] === '(') { | ||||||
|  |                 parensLevel++; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (rightIdx >= tokens.length) { | ||||||
|  |             throw new Error("Did not find matching right parenthesis."); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         tokens = [ | ||||||
|  |             ...tokens.slice(0, leftIdx), | ||||||
|  |             parens(tokens.slice(leftIdx + 1, rightIdx)), | ||||||
|  |             ...tokens.slice(rightIdx + 1) | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = parens; | ||||||
							
								
								
									
										81
									
								
								src/services/search/parser.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/services/search/parser.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | const AndExp = require('./expressions/and'); | ||||||
|  | const OrExp = require('./expressions/or'); | ||||||
|  | const NotExp = require('./expressions/not'); | ||||||
|  | const ExistsExp = require('./expressions/exists'); | ||||||
|  | const EqualsExp = require('./expressions/equals'); | ||||||
|  | const NoteCacheFulltextExp = require('./expressions/note_cache_fulltext'); | ||||||
|  | const NoteContentFulltextExp = require('./expressions/note_content_fulltext'); | ||||||
|  |  | ||||||
|  | function getFulltext(tokens, includingNoteContent) { | ||||||
|  |     if (includingNoteContent) { | ||||||
|  |         return [ | ||||||
|  |             new OrExp([ | ||||||
|  |                 new NoteCacheFulltextExp(tokens), | ||||||
|  |                 new NoteContentFulltextExp(tokens) | ||||||
|  |             ]) | ||||||
|  |         ] | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         return [ | ||||||
|  |             new NoteCacheFulltextExp(tokens) | ||||||
|  |         ] | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function isOperator(str) { | ||||||
|  |     return str.matches(/^[=<>*]+$/); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getExpressions(tokens) { | ||||||
|  |     const expressions = []; | ||||||
|  |     let op = null; | ||||||
|  |  | ||||||
|  |     for (let i = 0; i < tokens.length; i++) { | ||||||
|  |         const token = tokens[i]; | ||||||
|  |  | ||||||
|  |         if (token === '#' || token === '@') { | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (Array.isArray(token)) { | ||||||
|  |             expressions.push(getExpressions(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 EqualsExp(type, token.substr(1), tokens[i + 1], tokens[i + 2])); | ||||||
|  |  | ||||||
|  |                 i += 2; | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 expressions.push(new ExistsExp(type, token.substr(1))); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         else if (['and', 'or'].includes(token.toLowerCase())) { | ||||||
|  |             if (!op) { | ||||||
|  |                 op = token.toLowerCase(); | ||||||
|  |             } | ||||||
|  |             else if (op !== token.toLowerCase()) { | ||||||
|  |                 throw new Error('Mixed usage of AND/OR - always use parenthesis to group AND/OR expressions.'); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         else if (isOperator(token)) { | ||||||
|  |             throw new Error(`Misplaced or incomplete expression "${token}"`); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             throw new Error(`Unrecognized expression "${token}"`); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!op && expressions.length > 1) { | ||||||
|  |             op = 'and'; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function parse(fulltextTokens, expressionTokens, includingNoteContent) { | ||||||
|  |     return AndExp.of([ | ||||||
|  |         ...getFulltext(fulltextTokens, includingNoteContent), | ||||||
|  |         ...getExpressions(expressionTokens) | ||||||
|  |     ]); | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user