mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	Merge remote-tracking branch 'origin/develop' into renovate/i18next-24.x
This commit is contained in:
		
							
								
								
									
										171
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										171
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -63,7 +63,7 @@ | ||||
|         "jquery": "3.7.1", | ||||
|         "jquery-hotkeys": "0.2.2", | ||||
|         "jquery.fancytree": "2.38.4", | ||||
|         "jsdom": "25.0.1", | ||||
|         "jsdom": "26.0.0", | ||||
|         "jsplumb": "2.15.6", | ||||
|         "katex": "0.16.19", | ||||
|         "knockout": "3.5.1", | ||||
| @@ -192,6 +192,28 @@ | ||||
|         "url": "https://github.com/sponsors/antfu" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@asamuzakjp/css-color": { | ||||
|       "version": "2.8.2", | ||||
|       "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-2.8.2.tgz", | ||||
|       "integrity": "sha512-RtWv9jFN2/bLExuZgFFZ0I3pWWeezAHGgrmjqGGWclATl1aDe3yhCUaI0Ilkp6OCk9zX7+FjvDasEX8Q9Rxc5w==", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@csstools/css-calc": "^2.1.1", | ||||
|         "@csstools/css-color-parser": "^3.0.7", | ||||
|         "@csstools/css-parser-algorithms": "^3.0.4", | ||||
|         "@csstools/css-tokenizer": "^3.0.3", | ||||
|         "lru-cache": "^11.0.2" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { | ||||
|       "version": "11.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", | ||||
|       "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", | ||||
|       "license": "ISC", | ||||
|       "engines": { | ||||
|         "node": "20 || >=22" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@babel/helper-string-parser": { | ||||
|       "version": "7.25.9", | ||||
|       "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", | ||||
| @@ -312,6 +334,116 @@ | ||||
|         "node": ">=12" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@csstools/color-helpers": { | ||||
|       "version": "5.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.1.tgz", | ||||
|       "integrity": "sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==", | ||||
|       "funding": [ | ||||
|         { | ||||
|           "type": "github", | ||||
|           "url": "https://github.com/sponsors/csstools" | ||||
|         }, | ||||
|         { | ||||
|           "type": "opencollective", | ||||
|           "url": "https://opencollective.com/csstools" | ||||
|         } | ||||
|       ], | ||||
|       "license": "MIT-0", | ||||
|       "engines": { | ||||
|         "node": ">=18" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@csstools/css-calc": { | ||||
|       "version": "2.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.1.tgz", | ||||
|       "integrity": "sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==", | ||||
|       "funding": [ | ||||
|         { | ||||
|           "type": "github", | ||||
|           "url": "https://github.com/sponsors/csstools" | ||||
|         }, | ||||
|         { | ||||
|           "type": "opencollective", | ||||
|           "url": "https://opencollective.com/csstools" | ||||
|         } | ||||
|       ], | ||||
|       "license": "MIT", | ||||
|       "engines": { | ||||
|         "node": ">=18" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "@csstools/css-parser-algorithms": "^3.0.4", | ||||
|         "@csstools/css-tokenizer": "^3.0.3" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@csstools/css-color-parser": { | ||||
|       "version": "3.0.7", | ||||
|       "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.7.tgz", | ||||
|       "integrity": "sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==", | ||||
|       "funding": [ | ||||
|         { | ||||
|           "type": "github", | ||||
|           "url": "https://github.com/sponsors/csstools" | ||||
|         }, | ||||
|         { | ||||
|           "type": "opencollective", | ||||
|           "url": "https://opencollective.com/csstools" | ||||
|         } | ||||
|       ], | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@csstools/color-helpers": "^5.0.1", | ||||
|         "@csstools/css-calc": "^2.1.1" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=18" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "@csstools/css-parser-algorithms": "^3.0.4", | ||||
|         "@csstools/css-tokenizer": "^3.0.3" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@csstools/css-parser-algorithms": { | ||||
|       "version": "3.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", | ||||
|       "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", | ||||
|       "funding": [ | ||||
|         { | ||||
|           "type": "github", | ||||
|           "url": "https://github.com/sponsors/csstools" | ||||
|         }, | ||||
|         { | ||||
|           "type": "opencollective", | ||||
|           "url": "https://opencollective.com/csstools" | ||||
|         } | ||||
|       ], | ||||
|       "license": "MIT", | ||||
|       "engines": { | ||||
|         "node": ">=18" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "@csstools/css-tokenizer": "^3.0.3" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@csstools/css-tokenizer": { | ||||
|       "version": "3.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", | ||||
|       "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", | ||||
|       "funding": [ | ||||
|         { | ||||
|           "type": "github", | ||||
|           "url": "https://github.com/sponsors/csstools" | ||||
|         }, | ||||
|         { | ||||
|           "type": "opencollective", | ||||
|           "url": "https://opencollective.com/csstools" | ||||
|         } | ||||
|       ], | ||||
|       "license": "MIT", | ||||
|       "engines": { | ||||
|         "node": ">=18" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@discoveryjs/json-ext": { | ||||
|       "version": "0.6.3", | ||||
|       "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", | ||||
| @@ -7026,12 +7158,13 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/cssstyle": { | ||||
|       "version": "4.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", | ||||
|       "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", | ||||
|       "version": "4.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz", | ||||
|       "integrity": "sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "rrweb-cssom": "^0.7.1" | ||||
|         "@asamuzakjp/css-color": "^2.8.2", | ||||
|         "rrweb-cssom": "^0.8.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=18" | ||||
| @@ -12187,22 +12320,22 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/jsdom": { | ||||
|       "version": "25.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", | ||||
|       "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", | ||||
|       "version": "26.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.0.0.tgz", | ||||
|       "integrity": "sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "cssstyle": "^4.1.0", | ||||
|         "cssstyle": "^4.2.1", | ||||
|         "data-urls": "^5.0.0", | ||||
|         "decimal.js": "^10.4.3", | ||||
|         "form-data": "^4.0.0", | ||||
|         "form-data": "^4.0.1", | ||||
|         "html-encoding-sniffer": "^4.0.0", | ||||
|         "http-proxy-agent": "^7.0.2", | ||||
|         "https-proxy-agent": "^7.0.5", | ||||
|         "https-proxy-agent": "^7.0.6", | ||||
|         "is-potential-custom-element-name": "^1.0.1", | ||||
|         "nwsapi": "^2.2.12", | ||||
|         "parse5": "^7.1.2", | ||||
|         "rrweb-cssom": "^0.7.1", | ||||
|         "nwsapi": "^2.2.16", | ||||
|         "parse5": "^7.2.1", | ||||
|         "rrweb-cssom": "^0.8.0", | ||||
|         "saxes": "^6.0.0", | ||||
|         "symbol-tree": "^3.2.4", | ||||
|         "tough-cookie": "^5.0.0", | ||||
| @@ -12210,7 +12343,7 @@ | ||||
|         "webidl-conversions": "^7.0.0", | ||||
|         "whatwg-encoding": "^3.1.1", | ||||
|         "whatwg-mimetype": "^4.0.0", | ||||
|         "whatwg-url": "^14.0.0", | ||||
|         "whatwg-url": "^14.1.0", | ||||
|         "ws": "^8.18.0", | ||||
|         "xml-name-validator": "^5.0.0" | ||||
|       }, | ||||
| @@ -12218,7 +12351,7 @@ | ||||
|         "node": ">=18" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "canvas": "^2.11.2" | ||||
|         "canvas": "^3.0.0" | ||||
|       }, | ||||
|       "peerDependenciesMeta": { | ||||
|         "canvas": { | ||||
| @@ -15801,9 +15934,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/rrweb-cssom": { | ||||
|       "version": "0.7.1", | ||||
|       "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", | ||||
|       "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", | ||||
|       "version": "0.8.0", | ||||
|       "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", | ||||
|       "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/run-parallel": { | ||||
|   | ||||
| @@ -108,7 +108,7 @@ | ||||
|     "jquery": "3.7.1", | ||||
|     "jquery-hotkeys": "0.2.2", | ||||
|     "jquery.fancytree": "2.38.4", | ||||
|     "jsdom": "25.0.1", | ||||
|     "jsdom": "26.0.0", | ||||
|     "jsplumb": "2.15.6", | ||||
|     "katex": "0.16.19", | ||||
|     "knockout": "3.5.1", | ||||
|   | ||||
| @@ -1,12 +1,274 @@ | ||||
| // @ts-nocheck | ||||
| // There are many issues with the types of the parser e.g. "parse" function returns "Expression" | ||||
| // but we access properties like "subExpressions" which is not defined in the "Expression" class. | ||||
|  | ||||
| import AndExp from "../../src/services/search/expressions/and.js"; | ||||
| import AttributeExistsExp from "../../src/services/search/expressions/attribute_exists.js"; | ||||
| import Expression from "../../src/services/search/expressions/expression.js"; | ||||
| import LabelComparisonExp from "../../src/services/search/expressions/label_comparison.js"; | ||||
| import NotExp from "../../src/services/search/expressions/not.js"; | ||||
| import NoteContentFulltextExp from "../../src/services/search/expressions/note_content_fulltext.js"; | ||||
| import NoteFlatTextExp from "../../src/services/search/expressions/note_flat_text.js"; | ||||
| import OrExp from "../../src/services/search/expressions/or.js"; | ||||
| import OrderByAndLimitExp from "../../src/services/search/expressions/order_by_and_limit.js"; | ||||
| import PropertyComparisonExp from "../../src/services/search/expressions/property_comparison.js"; | ||||
| import SearchContext from "../../src/services/search/search_context.js"; | ||||
| import parse from "../../src/services/search/services/parse.js"; | ||||
| import { default as parseInternal, type ParseOpts } from "../../src/services/search/services/parse.js"; | ||||
|  | ||||
| function tokens(toks: Array<string>, cur = 0): Array<any> { | ||||
| describe("Parser", () => { | ||||
|     it("fulltext parser without content", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: tokens(["hello", "hi"]), | ||||
|             expressionTokens: [], | ||||
|             searchContext: new SearchContext() | ||||
|         }, AndExp); | ||||
|  | ||||
|         expectExpression(rootExp.subExpressions[0], PropertyComparisonExp); | ||||
|         const orExp = expectExpression(rootExp.subExpressions[2], OrExp); | ||||
|         const flatTextExp = expectExpression(orExp.subExpressions[0], NoteFlatTextExp); | ||||
|         expect(flatTextExp.tokens).toEqual(["hello", "hi"]); | ||||
|     }); | ||||
|  | ||||
|     it("fulltext parser with content", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: tokens(["hello", "hi"]), | ||||
|             expressionTokens: [], | ||||
|             searchContext: new SearchContext() | ||||
|         }, AndExp); | ||||
|  | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         const orExp = expectExpression(rootExp.subExpressions[2], OrExp); | ||||
|  | ||||
|         const firstSub = expectExpression(orExp.subExpressions[0], NoteFlatTextExp); | ||||
|         expect(firstSub.tokens).toEqual(["hello", "hi"]); | ||||
|  | ||||
|         const secondSub = expectExpression(orExp.subExpressions[1], NoteContentFulltextExp); | ||||
|         expect(secondSub.tokens).toEqual(["hello", "hi"]); | ||||
|     }); | ||||
|  | ||||
|     it("simple label comparison", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#mylabel", "=", "text"]), | ||||
|             searchContext: new SearchContext() | ||||
|         }, AndExp); | ||||
|  | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|         const labelComparisonExp = expectExpression(rootExp.subExpressions[2], LabelComparisonExp); | ||||
|         expect(labelComparisonExp.attributeType).toEqual("label"); | ||||
|         expect(labelComparisonExp.attributeName).toEqual("mylabel"); | ||||
|         expect(labelComparisonExp.comparator).toBeTruthy(); | ||||
|     }); | ||||
|  | ||||
|     it("simple attribute negation", () => { | ||||
|         let rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#!mylabel"]), | ||||
|             searchContext: new SearchContext() | ||||
|         }, AndExp); | ||||
|  | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|         let notExp = expectExpression(rootExp.subExpressions[2], NotExp); | ||||
|         let attributeExistsExp = expectExpression(notExp.subExpression, AttributeExistsExp); | ||||
|         expect(attributeExistsExp.attributeType).toEqual("label"); | ||||
|         expect(attributeExistsExp.attributeName).toEqual("mylabel"); | ||||
|  | ||||
|         rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["~!myrelation"]), | ||||
|             searchContext: new SearchContext() | ||||
|         }, AndExp); | ||||
|  | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|         notExp = expectExpression(rootExp.subExpressions[2], NotExp); | ||||
|         attributeExistsExp = expectExpression(notExp.subExpression, AttributeExistsExp); | ||||
|         expect(attributeExistsExp.attributeType).toEqual("relation"); | ||||
|         expect(attributeExistsExp.attributeName).toEqual("myrelation"); | ||||
|     }); | ||||
|  | ||||
|     it("simple label AND", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#first", "=", "text", "and", "#second", "=", "text"]), | ||||
|             searchContext: new SearchContext() | ||||
|         }, AndExp); | ||||
|  | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         const andExp = expectExpression(rootExp.subExpressions[2], AndExp); | ||||
|         const [firstSub, secondSub] = expectSubexpressions(andExp, LabelComparisonExp, LabelComparisonExp); | ||||
|  | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
|         expect(secondSub.attributeName).toEqual("second"); | ||||
|     }); | ||||
|  | ||||
|     it("simple label AND without explicit AND", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#first", "=", "text", "#second", "=", "text"]), | ||||
|             searchContext: new SearchContext() | ||||
|         }, AndExp); | ||||
|  | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         const andExp = expectExpression(rootExp.subExpressions[2], AndExp); | ||||
|         const [firstSub, secondSub] = expectSubexpressions(andExp, LabelComparisonExp, LabelComparisonExp); | ||||
|  | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
|         expect(secondSub.attributeName).toEqual("second"); | ||||
|     }); | ||||
|  | ||||
|     it("simple label OR", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#first", "=", "text", "or", "#second", "=", "text"]), | ||||
|             searchContext: new SearchContext() | ||||
|         }, AndExp); | ||||
|  | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         const orExp = expectExpression(rootExp.subExpressions[2], OrExp); | ||||
|         const [firstSub, secondSub] = expectSubexpressions(orExp, LabelComparisonExp, LabelComparisonExp); | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
|         expect(secondSub.attributeName).toEqual("second"); | ||||
|     }); | ||||
|  | ||||
|     it("fulltext and simple label", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: tokens(["hello"]), | ||||
|             expressionTokens: tokens(["#mylabel", "=", "text"]), | ||||
|             searchContext: new SearchContext() | ||||
|         }, AndExp); | ||||
|  | ||||
|         const [firstSub, _, thirdSub, fourth] = expectSubexpressions(rootExp, PropertyComparisonExp, undefined, OrExp, LabelComparisonExp); | ||||
|  | ||||
|         expect(firstSub.propertyName).toEqual("isArchived"); | ||||
|  | ||||
|         const noteFlatTextExp = expectExpression(thirdSub.subExpressions[0], NoteFlatTextExp); | ||||
|         expect(noteFlatTextExp.tokens).toEqual(["hello"]); | ||||
|  | ||||
|         expect(fourth.attributeName).toEqual("mylabel"); | ||||
|     }); | ||||
|  | ||||
|     it("label sub-expression", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#first", "=", "text", "or", ["#second", "=", "text", "and", "#third", "=", "text"]]), | ||||
|             searchContext: new SearchContext() | ||||
|         }, AndExp); | ||||
|  | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         const orExp = expectExpression(rootExp.subExpressions[2], OrExp); | ||||
|         const [firstSub, secondSub] = expectSubexpressions(orExp, LabelComparisonExp, AndExp); | ||||
|  | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
|  | ||||
|         const [firstSubSub, secondSubSub] = expectSubexpressions(secondSub, LabelComparisonExp, LabelComparisonExp); | ||||
|         expect(firstSubSub.attributeName).toEqual("second"); | ||||
|         expect(secondSubSub.attributeName).toEqual("third"); | ||||
|     }); | ||||
|  | ||||
|     it("label sub-expression without explicit operator", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#first", ["#second", "or", "#third"], "#fourth"]), | ||||
|             searchContext: new SearchContext() | ||||
|         }, AndExp); | ||||
|  | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         const andExp = expectExpression(rootExp.subExpressions[2], AndExp); | ||||
|         const [firstSub, secondSub, thirdSub] = expectSubexpressions(andExp, AttributeExistsExp, OrExp, AttributeExistsExp); | ||||
|  | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
|  | ||||
|         const [firstSubSub, secondSubSub] = expectSubexpressions(secondSub, AttributeExistsExp, AttributeExistsExp); | ||||
|         expect(firstSubSub.attributeName).toEqual("second"); | ||||
|         expect(secondSubSub.attributeName).toEqual("third"); | ||||
|  | ||||
|         expect(thirdSub.attributeName).toEqual("fourth"); | ||||
|     }); | ||||
|  | ||||
|     it("parses limit without order by", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: tokens(["hello", "hi"]), | ||||
|             expressionTokens: [], | ||||
|             searchContext: new SearchContext({ limit: 2 }) | ||||
|         }, OrderByAndLimitExp); | ||||
|  | ||||
|         expect(rootExp.limit).toBe(2); | ||||
|         expect(rootExp.subExpression).toBeInstanceOf(AndExp); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| describe("Invalid expressions", () => { | ||||
|     it("incomplete comparison", () => { | ||||
|         const searchContext = new SearchContext(); | ||||
|  | ||||
|         parseInternal({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#first", "="]), | ||||
|             searchContext | ||||
|         }); | ||||
|  | ||||
|         expect(searchContext.error).toEqual('Misplaced or incomplete expression "="'); | ||||
|     }); | ||||
|  | ||||
|     it("comparison between labels is impossible", () => { | ||||
|         let searchContext = new SearchContext(); | ||||
|         searchContext.originalQuery = "#first = #second"; | ||||
|  | ||||
|         parseInternal({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#first", "=", "#second"]), | ||||
|             searchContext | ||||
|         }); | ||||
|  | ||||
|         expect(searchContext.error).toEqual(`Error near token "#second" in "#first = #second", it's possible to compare with constant only.`); | ||||
|  | ||||
|         searchContext = new SearchContext(); | ||||
|         searchContext.originalQuery = "#first = note.relations.second"; | ||||
|  | ||||
|         parseInternal({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#first", "=", "note", ".", "relations", "second"]), | ||||
|             searchContext | ||||
|         }); | ||||
|  | ||||
|         expect(searchContext.error).toEqual(`Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.`); | ||||
|  | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: [ | ||||
|                 { token: "#first", inQuotes: false }, | ||||
|                 { token: "=", inQuotes: false }, | ||||
|                 { token: "#second", inQuotes: true } | ||||
|             ], | ||||
|             searchContext: new SearchContext() | ||||
|         }, AndExp); | ||||
|  | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         const labelComparisonExp = expectExpression(rootExp.subExpressions[2], LabelComparisonExp); | ||||
|         expect(labelComparisonExp.attributeType).toEqual("label"); | ||||
|         expect(labelComparisonExp.attributeName).toEqual("first"); | ||||
|         expect(labelComparisonExp.comparator).toBeTruthy(); | ||||
|     }); | ||||
|  | ||||
|     it("searching by relation without note property", () => { | ||||
|         const searchContext = new SearchContext(); | ||||
|  | ||||
|         parseInternal({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["~first", "=", "text", "-", "abc"]), | ||||
|             searchContext | ||||
|         }); | ||||
|  | ||||
|         expect(searchContext.error).toEqual('Relation can be compared only with property, e.g. ~relation.title=hello in ""'); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| type ClassType<T extends Expression> = new (...args: any[]) => T; | ||||
|  | ||||
| function tokens(toks: (string | string[])[], cur = 0): Array<any> { | ||||
|     return toks.map((arg) => { | ||||
|         if (Array.isArray(arg)) { | ||||
|             return tokens(arg, cur); | ||||
| @@ -23,293 +285,71 @@ function tokens(toks: Array<string>, cur = 0): Array<any> { | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function assertIsArchived(exp: Expression) { | ||||
|     expect(exp.constructor.name).toEqual("PropertyComparisonExp"); | ||||
| function assertIsArchived(_exp: Expression) { | ||||
|     const exp = expectExpression(_exp, PropertyComparisonExp); | ||||
|     expect(exp.propertyName).toEqual("isArchived"); | ||||
|     expect(exp.operator).toEqual("="); | ||||
|     expect(exp.comparedValue).toEqual("false"); | ||||
| } | ||||
|  | ||||
| describe("Parser", () => { | ||||
|     it("fulltext parser without content", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: tokens(["hello", "hi"]), | ||||
|             expressionTokens: [], | ||||
|             searchContext: new SearchContext({ excludeArchived: true }) | ||||
|         }); | ||||
| /** | ||||
|  * Parses the corresponding {@link Expression} from plain text, while also expecting the resulting expression to be of the given type. | ||||
|  * | ||||
|  * @param opts the options for parsing. | ||||
|  * @param type the expected type of the expression. | ||||
|  * @returns the expression typecasted to the expected type. | ||||
|  */ | ||||
| function parse<T extends Expression>(opts: ParseOpts, type: ClassType<T>) { | ||||
|     return expectExpression(parseInternal(opts), type); | ||||
| } | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         expect(rootExp.subExpressions[0].constructor.name).toEqual("PropertyComparisonExp"); | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); | ||||
|         expect(rootExp.subExpressions[2].subExpressions[0].constructor.name).toEqual("NoteFlatTextExp"); | ||||
|         expect(rootExp.subExpressions[2].subExpressions[0].tokens).toEqual(["hello", "hi"]); | ||||
|     }); | ||||
| /** | ||||
|  * Expects the given {@link Expression} to be of the given type. | ||||
|  * | ||||
|  * @param exp an instance of an {@link Expression}. | ||||
|  * @param type a type class such as {@link AndExp}, {@link OrExp}, etc. | ||||
|  * @returns the same expression typecasted to the expected type. | ||||
|  */ | ||||
| function expectExpression<T extends Expression>(exp: Expression, type: ClassType<T>) { | ||||
|     expect(exp).toBeInstanceOf(type); | ||||
|     return exp as T; | ||||
| } | ||||
|  | ||||
|     it("fulltext parser with content", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: tokens(["hello", "hi"]), | ||||
|             expressionTokens: [], | ||||
|             searchContext: new SearchContext() | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); | ||||
|  | ||||
|         const subs = rootExp.subExpressions[2].subExpressions; | ||||
|  | ||||
|         expect(subs[0].constructor.name).toEqual("NoteFlatTextExp"); | ||||
|         expect(subs[0].tokens).toEqual(["hello", "hi"]); | ||||
|  | ||||
|         expect(subs[1].constructor.name).toEqual("NoteContentFulltextExp"); | ||||
|         expect(subs[1].tokens).toEqual(["hello", "hi"]); | ||||
|     }); | ||||
|  | ||||
|     it("simple label comparison", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#mylabel", "=", "text"]), | ||||
|             searchContext: new SearchContext() | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(rootExp.subExpressions[2].attributeType).toEqual("label"); | ||||
|         expect(rootExp.subExpressions[2].attributeName).toEqual("mylabel"); | ||||
|         expect(rootExp.subExpressions[2].comparator).toBeTruthy(); | ||||
|     }); | ||||
|  | ||||
|     it("simple attribute negation", () => { | ||||
|         let rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#!mylabel"]), | ||||
|             searchContext: new SearchContext() | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("NotExp"); | ||||
|         expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual("AttributeExistsExp"); | ||||
|         expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual("label"); | ||||
|         expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual("mylabel"); | ||||
|  | ||||
|         rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["~!myrelation"]), | ||||
|             searchContext: new SearchContext() | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("NotExp"); | ||||
|         expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual("AttributeExistsExp"); | ||||
|         expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual("relation"); | ||||
|         expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual("myrelation"); | ||||
|     }); | ||||
|  | ||||
|     it("simple label AND", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#first", "=", "text", "and", "#second", "=", "text"]), | ||||
|             searchContext: new SearchContext(true) | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; | ||||
|  | ||||
|         expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
|  | ||||
|         expect(secondSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(secondSub.attributeName).toEqual("second"); | ||||
|     }); | ||||
|  | ||||
|     it("simple label AND without explicit AND", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#first", "=", "text", "#second", "=", "text"]), | ||||
|             searchContext: new SearchContext() | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; | ||||
|  | ||||
|         expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
|  | ||||
|         expect(secondSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(secondSub.attributeName).toEqual("second"); | ||||
|     }); | ||||
|  | ||||
|     it("simple label OR", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#first", "=", "text", "or", "#second", "=", "text"]), | ||||
|             searchContext: new SearchContext() | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; | ||||
|  | ||||
|         expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
|  | ||||
|         expect(secondSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(secondSub.attributeName).toEqual("second"); | ||||
|     }); | ||||
|  | ||||
|     it("fulltext and simple label", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: tokens(["hello"]), | ||||
|             expressionTokens: tokens(["#mylabel", "=", "text"]), | ||||
|             searchContext: new SearchContext({ excludeArchived: true }) | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         const [firstSub, secondSub, thirdSub, fourth] = rootExp.subExpressions; | ||||
|  | ||||
|         expect(firstSub.constructor.name).toEqual("PropertyComparisonExp"); | ||||
|         expect(firstSub.propertyName).toEqual("isArchived"); | ||||
|  | ||||
|         expect(thirdSub.constructor.name).toEqual("OrExp"); | ||||
|         expect(thirdSub.subExpressions[0].constructor.name).toEqual("NoteFlatTextExp"); | ||||
|         expect(thirdSub.subExpressions[0].tokens).toEqual(["hello"]); | ||||
|  | ||||
|         expect(fourth.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(fourth.attributeName).toEqual("mylabel"); | ||||
|     }); | ||||
|  | ||||
|     it("label sub-expression", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#first", "=", "text", "or", ["#second", "=", "text", "and", "#third", "=", "text"]]), | ||||
|             searchContext: new SearchContext() | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); | ||||
|         const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; | ||||
|  | ||||
|         expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
|  | ||||
|         expect(secondSub.constructor.name).toEqual("AndExp"); | ||||
|         const [firstSubSub, secondSubSub] = secondSub.subExpressions; | ||||
|  | ||||
|         expect(firstSubSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(firstSubSub.attributeName).toEqual("second"); | ||||
|  | ||||
|         expect(secondSubSub.constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(secondSubSub.attributeName).toEqual("third"); | ||||
|     }); | ||||
|  | ||||
|     it("label sub-expression without explicit operator", () => { | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#first", ["#second", "or", "#third"], "#fourth"]), | ||||
|             searchContext: new SearchContext() | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp"); | ||||
|         const [firstSub, secondSub, thirdSub] = rootExp.subExpressions[2].subExpressions; | ||||
|  | ||||
|         expect(firstSub.constructor.name).toEqual("AttributeExistsExp"); | ||||
|         expect(firstSub.attributeName).toEqual("first"); | ||||
|  | ||||
|         expect(secondSub.constructor.name).toEqual("OrExp"); | ||||
|         const [firstSubSub, secondSubSub] = secondSub.subExpressions; | ||||
|  | ||||
|         expect(firstSubSub.constructor.name).toEqual("AttributeExistsExp"); | ||||
|         expect(firstSubSub.attributeName).toEqual("second"); | ||||
|  | ||||
|         expect(secondSubSub.constructor.name).toEqual("AttributeExistsExp"); | ||||
|         expect(secondSubSub.attributeName).toEqual("third"); | ||||
|  | ||||
|         expect(thirdSub.constructor.name).toEqual("AttributeExistsExp"); | ||||
|         expect(thirdSub.attributeName).toEqual("fourth"); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| describe("Invalid expressions", () => { | ||||
|     it("incomplete comparison", () => { | ||||
|         const searchContext = new SearchContext(); | ||||
|  | ||||
|         parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#first", "="]), | ||||
|             searchContext | ||||
|         }); | ||||
|  | ||||
|         expect(searchContext.error).toEqual('Misplaced or incomplete expression "="'); | ||||
|     }); | ||||
|  | ||||
|     it("comparison between labels is impossible", () => { | ||||
|         let searchContext = new SearchContext(); | ||||
|         searchContext.originalQuery = "#first = #second"; | ||||
|  | ||||
|         parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#first", "=", "#second"]), | ||||
|             searchContext | ||||
|         }); | ||||
|  | ||||
|         expect(searchContext.error).toEqual(`Error near token "#second" in "#first = #second", it's possible to compare with constant only.`); | ||||
|  | ||||
|         searchContext = new SearchContext(); | ||||
|         searchContext.originalQuery = "#first = note.relations.second"; | ||||
|  | ||||
|         parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["#first", "=", "note", ".", "relations", "second"]), | ||||
|             searchContext | ||||
|         }); | ||||
|  | ||||
|         expect(searchContext.error).toEqual(`Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.`); | ||||
|  | ||||
|         const rootExp = parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: [ | ||||
|                 { token: "#first", inQuotes: false }, | ||||
|                 { token: "=", inQuotes: false }, | ||||
|                 { token: "#second", inQuotes: true } | ||||
|             ], | ||||
|             searchContext: new SearchContext() | ||||
|         }); | ||||
|  | ||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | ||||
|         assertIsArchived(rootExp.subExpressions[0]); | ||||
|  | ||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("LabelComparisonExp"); | ||||
|         expect(rootExp.subExpressions[2].attributeType).toEqual("label"); | ||||
|         expect(rootExp.subExpressions[2].attributeName).toEqual("first"); | ||||
|         expect(rootExp.subExpressions[2].comparator).toBeTruthy(); | ||||
|     }); | ||||
|  | ||||
|     it("searching by relation without note property", () => { | ||||
|         const searchContext = new SearchContext(); | ||||
|  | ||||
|         parse({ | ||||
|             fulltextTokens: [], | ||||
|             expressionTokens: tokens(["~first", "=", "text", "-", "abc"]), | ||||
|             searchContext | ||||
|         }); | ||||
|  | ||||
|         expect(searchContext.error).toEqual('Relation can be compared only with property, e.g. ~relation.title=hello in ""'); | ||||
|     }); | ||||
| }); | ||||
| /** | ||||
|  * For an {@link AndExp}, it goes through all its subexpressions (up to fourth) and checks their type and returns them as a typecasted array. | ||||
|  * Each subexpression can have their own type. | ||||
|  * | ||||
|  * @param exp the expression containing one or more subexpressions. | ||||
|  * @param firstType the type of the first subexpression. | ||||
|  * @param secondType the type of the second subexpression. | ||||
|  * @param thirdType the type of the third subexpression. | ||||
|  * @param fourthType the type of the fourth subexpression. | ||||
|  * @returns an array of all the subexpressions (in order) typecasted to their expected type. | ||||
|  */ | ||||
| function expectSubexpressions<FirstT extends Expression, | ||||
|                               SecondT extends Expression, | ||||
|                               ThirdT extends Expression, | ||||
|                               FourthT extends Expression>( | ||||
|                                 exp: AndExp, | ||||
|                                 firstType: ClassType<FirstT>, | ||||
|                                 secondType?: ClassType<SecondT>, | ||||
|                                 thirdType?: ClassType<ThirdT>, | ||||
|                                 fourthType?: ClassType<FourthT>): [ FirstT, SecondT, ThirdT, FourthT ] | ||||
| { | ||||
|     expectExpression(exp.subExpressions[0], firstType); | ||||
|     if (secondType) { | ||||
|         expectExpression(exp.subExpressions[1], secondType); | ||||
|     } | ||||
|     if (thirdType) { | ||||
|         expectExpression(exp.subExpressions[2], thirdType); | ||||
|     } | ||||
|     if (fourthType) { | ||||
|         expectExpression(exp.subExpressions[3], fourthType); | ||||
|     } | ||||
|     return [ | ||||
|         exp.subExpressions[0] as FirstT, | ||||
|         exp.subExpressions[1] as SecondT, | ||||
|         exp.subExpressions[2] as ThirdT, | ||||
|         exp.subExpressions[3] as FourthT | ||||
|     ] | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| { | ||||
|   "spec_dir": "spec", | ||||
|   "spec_files": ["./**/*.spec.ts"], | ||||
|   "spec_dir": "", | ||||
|   "spec_files": [ | ||||
|     "spec/**/*.spec.ts", | ||||
|     "src/**/*.spec.ts" | ||||
|   ], | ||||
|   "helpers": ["helpers/**/*.js"], | ||||
|   "stopSpecOnExpectationFailure": false, | ||||
|   "random": true | ||||
|   | ||||
| @@ -704,7 +704,7 @@ body.layout-horizontal .tab-row-widget-container { | ||||
|     overflow: hidden; | ||||
| } | ||||
|  | ||||
| body.desktop #root-widget.horizontal-layout { | ||||
| body.desktop:not(.background-effects.platform-win32) #root-widget.horizontal-layout { | ||||
|     background-color: var(--root-background) !important; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import Expression from "./expression.js"; | ||||
| import TrueExp from "./true.js"; | ||||
|  | ||||
| class AndExp extends Expression { | ||||
|     private subExpressions: Expression[]; | ||||
|     subExpressions: Expression[]; | ||||
|  | ||||
|     static of(_subExpressions: (Expression | null | undefined)[]) { | ||||
|         const subExpressions = _subExpressions.filter((exp) => !!exp) as Expression[]; | ||||
|   | ||||
| @@ -7,8 +7,8 @@ import becca from "../../../becca/becca.js"; | ||||
| import Expression from "./expression.js"; | ||||
|  | ||||
| class AttributeExistsExp extends Expression { | ||||
|     private attributeType: string; | ||||
|     private attributeName: string; | ||||
|     attributeType: string; | ||||
|     attributeName: string; | ||||
|     private isTemplateLabel: boolean; | ||||
|     private prefixMatch: boolean; | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| "use strict"; | ||||
|  | ||||
| import NoteSet from "../note_set.js"; | ||||
| import SearchContext from "../search_context.js"; | ||||
| import type NoteSet from "../note_set.js"; | ||||
| import type SearchContext from "../search_context.js"; | ||||
|  | ||||
| abstract class Expression { | ||||
| export default abstract class Expression { | ||||
|     name: string; | ||||
|  | ||||
|     constructor() { | ||||
| @@ -12,5 +12,3 @@ abstract class Expression { | ||||
|  | ||||
|     abstract execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext): NoteSet; | ||||
| } | ||||
|  | ||||
| export default Expression; | ||||
|   | ||||
| @@ -8,9 +8,9 @@ import SearchContext from "../search_context.js"; | ||||
| type Comparator = (value: string) => boolean; | ||||
|  | ||||
| class LabelComparisonExp extends Expression { | ||||
|     private attributeType: string; | ||||
|     private attributeName: string; | ||||
|     private comparator: Comparator; | ||||
|     attributeType: string; | ||||
|     attributeName: string; | ||||
|     comparator: Comparator; | ||||
|  | ||||
|     constructor(attributeType: string, attributeName: string, comparator: Comparator) { | ||||
|         super(); | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import SearchContext from "../search_context.js"; | ||||
| import Expression from "./expression.js"; | ||||
|  | ||||
| class NotExp extends Expression { | ||||
|     private subExpression: Expression; | ||||
|     subExpression: Expression; | ||||
|  | ||||
|     constructor(subExpression: Expression) { | ||||
|         super(); | ||||
|   | ||||
| @@ -34,7 +34,7 @@ type SearchRow = Pick<NoteRow, "noteId" | "type" | "mime" | "content" | "isProte | ||||
|  | ||||
| class NoteContentFulltextExp extends Expression { | ||||
|     private operator: string; | ||||
|     private tokens: string[]; | ||||
|     tokens: string[]; | ||||
|     private raw: boolean; | ||||
|     private flatText: boolean; | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import { normalize } from "../../utils.js"; | ||||
| import beccaService from "../../../becca/becca_service.js"; | ||||
|  | ||||
| class NoteFlatTextExp extends Expression { | ||||
|     private tokens: string[]; | ||||
|     tokens: string[]; | ||||
|  | ||||
|     constructor(tokens: string[]) { | ||||
|         super(); | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import TrueExp from "./true.js"; | ||||
| import SearchContext from "../search_context.js"; | ||||
|  | ||||
| class OrExp extends Expression { | ||||
|     private subExpressions: Expression[]; | ||||
|     subExpressions: Expression[]; | ||||
|  | ||||
|     static of(subExpressions: Expression[]) { | ||||
|         subExpressions = subExpressions.filter((exp) => !!exp); | ||||
|   | ||||
| @@ -18,7 +18,7 @@ interface OrderDefinition { | ||||
|  | ||||
| class OrderByAndLimitExp extends Expression { | ||||
|     private orderDefinitions: OrderDefinition[]; | ||||
|     private limit: number; | ||||
|     limit: number; | ||||
|     subExpression: Expression | null; | ||||
|  | ||||
|     constructor(orderDefinitions: Pick<OrderDefinition, "direction" | "valueExtractor">[], limit?: number) { | ||||
|   | ||||
| @@ -41,9 +41,9 @@ interface SearchContext { | ||||
| } | ||||
|  | ||||
| class PropertyComparisonExp extends Expression { | ||||
|     private propertyName: string; | ||||
|     private operator: string; | ||||
|     private comparedValue: string; | ||||
|     propertyName: string; | ||||
|     operator: string; | ||||
|     comparedValue: string; | ||||
|     private comparator; | ||||
|  | ||||
|     static isProperty(name: string) { | ||||
|   | ||||
| @@ -22,7 +22,7 @@ class SearchContext { | ||||
|     originalQuery: string; | ||||
|     fulltextQuery: string; | ||||
|     dbLoadNeeded: boolean; | ||||
|     private error: string | null; | ||||
|     error: string | null; | ||||
|  | ||||
|     constructor(params: SearchParams = {}) { | ||||
|         this.fastSearch = !!params.fastSearch; | ||||
|   | ||||
| @@ -423,7 +423,14 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level | ||||
|     return getAggregateExpression(); | ||||
| } | ||||
|  | ||||
| function parse({ fulltextTokens, expressionTokens, searchContext }: { fulltextTokens: TokenData[]; expressionTokens: TokenStructure; searchContext: SearchContext; originalQuery: string }) { | ||||
| export interface ParseOpts { | ||||
|     fulltextTokens: TokenData[]; | ||||
|     expressionTokens: TokenStructure; | ||||
|     searchContext: SearchContext; | ||||
|     originalQuery?: string | ||||
| } | ||||
|  | ||||
| function parse({ fulltextTokens, expressionTokens, searchContext }: ParseOpts) { | ||||
|     let expression: Expression | undefined | null; | ||||
|  | ||||
|     try { | ||||
| @@ -441,6 +448,12 @@ function parse({ fulltextTokens, expressionTokens, searchContext }: { fulltextTo | ||||
|         expression | ||||
|     ]); | ||||
|  | ||||
|     if (searchContext.limit && !searchContext.orderBy) { | ||||
|         const filterExp = exp; | ||||
|         exp = new OrderByAndLimitExp([], searchContext.limit || undefined ); | ||||
|         (exp as any).subExpression = filterExp; | ||||
|     } | ||||
|  | ||||
|     if (searchContext.orderBy && searchContext.orderBy !== "relevancy") { | ||||
|         const filterExp = exp; | ||||
|  | ||||
|   | ||||
							
								
								
									
										33
									
								
								src/share/content_renderer.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/share/content_renderer.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| import { renderCode, type Result } from "./content_renderer.js"; | ||||
|  | ||||
| describe("content_renderer", () => { | ||||
|     describe("renderCode", () => { | ||||
|         it("identifies empty content", () => { | ||||
|             const emptyResult: Result = { | ||||
|                 header: "", | ||||
|                 content: "   " | ||||
|             }; | ||||
|             renderCode(emptyResult); | ||||
|             expect(emptyResult.isEmpty).toBeTrue(); | ||||
|         }); | ||||
|  | ||||
|         it("identifies unsupported content type", () => { | ||||
|             const emptyResult: Result = { | ||||
|                 header: "", | ||||
|                 content: Buffer.from("Hello world") | ||||
|             }; | ||||
|             renderCode(emptyResult); | ||||
|             expect(emptyResult.isEmpty).toBeTrue(); | ||||
|         }); | ||||
|  | ||||
|         it("wraps code in <pre>", () => { | ||||
|             const result: Result = { | ||||
|                 header: "", | ||||
|                 content: "\tHello\nworld" | ||||
|             }; | ||||
|             renderCode(result); | ||||
|             expect(result.isEmpty).toBeFalsy(); | ||||
|             expect(result.content).toBe("<pre>\tHello\nworld</pre>"); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -5,10 +5,14 @@ import shareRoot from "./share_root.js"; | ||||
| import escapeHtml from "escape-html"; | ||||
| import SNote from "./shaca/entities/snote.js"; | ||||
|  | ||||
| interface Result { | ||||
| /** | ||||
|  * Represents the output of the content renderer. | ||||
|  */ | ||||
| export interface Result { | ||||
|     header: string; | ||||
|     content: string | Buffer | undefined; | ||||
|     isEmpty: boolean; | ||||
|     /** Set to `true` if the provided content should be rendered as empty. */ | ||||
|     isEmpty?: boolean; | ||||
| } | ||||
|  | ||||
| function getContent(note: SNote) { | ||||
| @@ -137,7 +141,10 @@ function handleAttachmentLink(linkEl: HTMLAnchorElement, href: string) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| function renderCode(result: Result) { | ||||
| /** | ||||
|  * Renders a code note. | ||||
|  */ | ||||
| export function renderCode(result: Result) { | ||||
|     if (typeof result.content !== "string" || !result.content?.trim()) { | ||||
|         result.isEmpty = true; | ||||
|     } else { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user