mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-09 09:42:28 +08:00
Merge remote-tracking branch 'origin/develop' into renovate/i18next-24.x
This commit is contained in:
commit
49675c758a
171
package-lock.json
generated
171
package-lock.json
generated
@ -63,7 +63,7 @@
|
|||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"jquery-hotkeys": "0.2.2",
|
"jquery-hotkeys": "0.2.2",
|
||||||
"jquery.fancytree": "2.38.4",
|
"jquery.fancytree": "2.38.4",
|
||||||
"jsdom": "25.0.1",
|
"jsdom": "26.0.0",
|
||||||
"jsplumb": "2.15.6",
|
"jsplumb": "2.15.6",
|
||||||
"katex": "0.16.19",
|
"katex": "0.16.19",
|
||||||
"knockout": "3.5.1",
|
"knockout": "3.5.1",
|
||||||
@ -192,6 +192,28 @@
|
|||||||
"url": "https://github.com/sponsors/antfu"
|
"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": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
"version": "7.25.9",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
||||||
@ -312,6 +334,116 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/@discoveryjs/json-ext": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz",
|
||||||
@ -7026,12 +7158,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cssstyle": {
|
"node_modules/cssstyle": {
|
||||||
"version": "4.1.0",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz",
|
||||||
"integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==",
|
"integrity": "sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"rrweb-cssom": "^0.7.1"
|
"@asamuzakjp/css-color": "^2.8.2",
|
||||||
|
"rrweb-cssom": "^0.8.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@ -12187,22 +12320,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsdom": {
|
"node_modules/jsdom": {
|
||||||
"version": "25.0.1",
|
"version": "26.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.0.0.tgz",
|
||||||
"integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
|
"integrity": "sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssstyle": "^4.1.0",
|
"cssstyle": "^4.2.1",
|
||||||
"data-urls": "^5.0.0",
|
"data-urls": "^5.0.0",
|
||||||
"decimal.js": "^10.4.3",
|
"decimal.js": "^10.4.3",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.1",
|
||||||
"html-encoding-sniffer": "^4.0.0",
|
"html-encoding-sniffer": "^4.0.0",
|
||||||
"http-proxy-agent": "^7.0.2",
|
"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",
|
"is-potential-custom-element-name": "^1.0.1",
|
||||||
"nwsapi": "^2.2.12",
|
"nwsapi": "^2.2.16",
|
||||||
"parse5": "^7.1.2",
|
"parse5": "^7.2.1",
|
||||||
"rrweb-cssom": "^0.7.1",
|
"rrweb-cssom": "^0.8.0",
|
||||||
"saxes": "^6.0.0",
|
"saxes": "^6.0.0",
|
||||||
"symbol-tree": "^3.2.4",
|
"symbol-tree": "^3.2.4",
|
||||||
"tough-cookie": "^5.0.0",
|
"tough-cookie": "^5.0.0",
|
||||||
@ -12210,7 +12343,7 @@
|
|||||||
"webidl-conversions": "^7.0.0",
|
"webidl-conversions": "^7.0.0",
|
||||||
"whatwg-encoding": "^3.1.1",
|
"whatwg-encoding": "^3.1.1",
|
||||||
"whatwg-mimetype": "^4.0.0",
|
"whatwg-mimetype": "^4.0.0",
|
||||||
"whatwg-url": "^14.0.0",
|
"whatwg-url": "^14.1.0",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
"xml-name-validator": "^5.0.0"
|
"xml-name-validator": "^5.0.0"
|
||||||
},
|
},
|
||||||
@ -12218,7 +12351,7 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"canvas": "^2.11.2"
|
"canvas": "^3.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"canvas": {
|
"canvas": {
|
||||||
@ -15801,9 +15934,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rrweb-cssom": {
|
"node_modules/rrweb-cssom": {
|
||||||
"version": "0.7.1",
|
"version": "0.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||||
"integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
|
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
|
@ -108,7 +108,7 @@
|
|||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"jquery-hotkeys": "0.2.2",
|
"jquery-hotkeys": "0.2.2",
|
||||||
"jquery.fancytree": "2.38.4",
|
"jquery.fancytree": "2.38.4",
|
||||||
"jsdom": "25.0.1",
|
"jsdom": "26.0.0",
|
||||||
"jsplumb": "2.15.6",
|
"jsplumb": "2.15.6",
|
||||||
"katex": "0.16.19",
|
"katex": "0.16.19",
|
||||||
"knockout": "3.5.1",
|
"knockout": "3.5.1",
|
||||||
|
@ -1,12 +1,274 @@
|
|||||||
// @ts-nocheck
|
import AndExp from "../../src/services/search/expressions/and.js";
|
||||||
// There are many issues with the types of the parser e.g. "parse" function returns "Expression"
|
import AttributeExistsExp from "../../src/services/search/expressions/attribute_exists.js";
|
||||||
// but we access properties like "subExpressions" which is not defined in the "Expression" class.
|
|
||||||
|
|
||||||
import Expression from "../../src/services/search/expressions/expression.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 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) => {
|
return toks.map((arg) => {
|
||||||
if (Array.isArray(arg)) {
|
if (Array.isArray(arg)) {
|
||||||
return tokens(arg, cur);
|
return tokens(arg, cur);
|
||||||
@ -23,293 +285,71 @@ function tokens(toks: Array<string>, cur = 0): Array<any> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertIsArchived(exp: Expression) {
|
function assertIsArchived(_exp: Expression) {
|
||||||
expect(exp.constructor.name).toEqual("PropertyComparisonExp");
|
const exp = expectExpression(_exp, PropertyComparisonExp);
|
||||||
expect(exp.propertyName).toEqual("isArchived");
|
expect(exp.propertyName).toEqual("isArchived");
|
||||||
expect(exp.operator).toEqual("=");
|
expect(exp.operator).toEqual("=");
|
||||||
expect(exp.comparedValue).toEqual("false");
|
expect(exp.comparedValue).toEqual("false");
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Parser", () => {
|
/**
|
||||||
it("fulltext parser without content", () => {
|
* Parses the corresponding {@link Expression} from plain text, while also expecting the resulting expression to be of the given type.
|
||||||
const rootExp = parse({
|
*
|
||||||
fulltextTokens: tokens(["hello", "hi"]),
|
* @param opts the options for parsing.
|
||||||
expressionTokens: [],
|
* @param type the expected type of the expression.
|
||||||
searchContext: new SearchContext({ excludeArchived: true })
|
* @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");
|
* Expects the given {@link Expression} to be of the given type.
|
||||||
expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp");
|
*
|
||||||
expect(rootExp.subExpressions[2].subExpressions[0].constructor.name).toEqual("NoteFlatTextExp");
|
* @param exp an instance of an {@link Expression}.
|
||||||
expect(rootExp.subExpressions[2].subExpressions[0].tokens).toEqual(["hello", "hi"]);
|
* @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({
|
* For an {@link AndExp}, it goes through all its subexpressions (up to fourth) and checks their type and returns them as a typecasted array.
|
||||||
fulltextTokens: tokens(["hello", "hi"]),
|
* Each subexpression can have their own type.
|
||||||
expressionTokens: [],
|
*
|
||||||
searchContext: new SearchContext()
|
* @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.
|
||||||
expect(rootExp.constructor.name).toEqual("AndExp");
|
* @param thirdType the type of the third subexpression.
|
||||||
assertIsArchived(rootExp.subExpressions[0]);
|
* @param fourthType the type of the fourth subexpression.
|
||||||
|
* @returns an array of all the subexpressions (in order) typecasted to their expected type.
|
||||||
expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp");
|
*/
|
||||||
|
function expectSubexpressions<FirstT extends Expression,
|
||||||
const subs = rootExp.subExpressions[2].subExpressions;
|
SecondT extends Expression,
|
||||||
|
ThirdT extends Expression,
|
||||||
expect(subs[0].constructor.name).toEqual("NoteFlatTextExp");
|
FourthT extends Expression>(
|
||||||
expect(subs[0].tokens).toEqual(["hello", "hi"]);
|
exp: AndExp,
|
||||||
|
firstType: ClassType<FirstT>,
|
||||||
expect(subs[1].constructor.name).toEqual("NoteContentFulltextExp");
|
secondType?: ClassType<SecondT>,
|
||||||
expect(subs[1].tokens).toEqual(["hello", "hi"]);
|
thirdType?: ClassType<ThirdT>,
|
||||||
});
|
fourthType?: ClassType<FourthT>): [ FirstT, SecondT, ThirdT, FourthT ]
|
||||||
|
{
|
||||||
it("simple label comparison", () => {
|
expectExpression(exp.subExpressions[0], firstType);
|
||||||
const rootExp = parse({
|
if (secondType) {
|
||||||
fulltextTokens: [],
|
expectExpression(exp.subExpressions[1], secondType);
|
||||||
expressionTokens: tokens(["#mylabel", "=", "text"]),
|
}
|
||||||
searchContext: new SearchContext()
|
if (thirdType) {
|
||||||
});
|
expectExpression(exp.subExpressions[2], thirdType);
|
||||||
|
}
|
||||||
expect(rootExp.constructor.name).toEqual("AndExp");
|
if (fourthType) {
|
||||||
assertIsArchived(rootExp.subExpressions[0]);
|
expectExpression(exp.subExpressions[3], fourthType);
|
||||||
expect(rootExp.subExpressions[2].constructor.name).toEqual("LabelComparisonExp");
|
}
|
||||||
expect(rootExp.subExpressions[2].attributeType).toEqual("label");
|
return [
|
||||||
expect(rootExp.subExpressions[2].attributeName).toEqual("mylabel");
|
exp.subExpressions[0] as FirstT,
|
||||||
expect(rootExp.subExpressions[2].comparator).toBeTruthy();
|
exp.subExpressions[1] as SecondT,
|
||||||
});
|
exp.subExpressions[2] as ThirdT,
|
||||||
|
exp.subExpressions[3] as FourthT
|
||||||
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 ""');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"spec_dir": "spec",
|
"spec_dir": "",
|
||||||
"spec_files": ["./**/*.spec.ts"],
|
"spec_files": [
|
||||||
|
"spec/**/*.spec.ts",
|
||||||
|
"src/**/*.spec.ts"
|
||||||
|
],
|
||||||
"helpers": ["helpers/**/*.js"],
|
"helpers": ["helpers/**/*.js"],
|
||||||
"stopSpecOnExpectationFailure": false,
|
"stopSpecOnExpectationFailure": false,
|
||||||
"random": true
|
"random": true
|
||||||
|
@ -704,7 +704,7 @@ body.layout-horizontal .tab-row-widget-container {
|
|||||||
overflow: hidden;
|
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;
|
background-color: var(--root-background) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import Expression from "./expression.js";
|
|||||||
import TrueExp from "./true.js";
|
import TrueExp from "./true.js";
|
||||||
|
|
||||||
class AndExp extends Expression {
|
class AndExp extends Expression {
|
||||||
private subExpressions: Expression[];
|
subExpressions: Expression[];
|
||||||
|
|
||||||
static of(_subExpressions: (Expression | null | undefined)[]) {
|
static of(_subExpressions: (Expression | null | undefined)[]) {
|
||||||
const subExpressions = _subExpressions.filter((exp) => !!exp) as Expression[];
|
const subExpressions = _subExpressions.filter((exp) => !!exp) as Expression[];
|
||||||
|
@ -7,8 +7,8 @@ import becca from "../../../becca/becca.js";
|
|||||||
import Expression from "./expression.js";
|
import Expression from "./expression.js";
|
||||||
|
|
||||||
class AttributeExistsExp extends Expression {
|
class AttributeExistsExp extends Expression {
|
||||||
private attributeType: string;
|
attributeType: string;
|
||||||
private attributeName: string;
|
attributeName: string;
|
||||||
private isTemplateLabel: boolean;
|
private isTemplateLabel: boolean;
|
||||||
private prefixMatch: boolean;
|
private prefixMatch: boolean;
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import NoteSet from "../note_set.js";
|
import type NoteSet from "../note_set.js";
|
||||||
import SearchContext from "../search_context.js";
|
import type SearchContext from "../search_context.js";
|
||||||
|
|
||||||
abstract class Expression {
|
export default abstract class Expression {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -12,5 +12,3 @@ abstract class Expression {
|
|||||||
|
|
||||||
abstract execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext): NoteSet;
|
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;
|
type Comparator = (value: string) => boolean;
|
||||||
|
|
||||||
class LabelComparisonExp extends Expression {
|
class LabelComparisonExp extends Expression {
|
||||||
private attributeType: string;
|
attributeType: string;
|
||||||
private attributeName: string;
|
attributeName: string;
|
||||||
private comparator: Comparator;
|
comparator: Comparator;
|
||||||
|
|
||||||
constructor(attributeType: string, attributeName: string, comparator: Comparator) {
|
constructor(attributeType: string, attributeName: string, comparator: Comparator) {
|
||||||
super();
|
super();
|
||||||
|
@ -5,7 +5,7 @@ import SearchContext from "../search_context.js";
|
|||||||
import Expression from "./expression.js";
|
import Expression from "./expression.js";
|
||||||
|
|
||||||
class NotExp extends Expression {
|
class NotExp extends Expression {
|
||||||
private subExpression: Expression;
|
subExpression: Expression;
|
||||||
|
|
||||||
constructor(subExpression: Expression) {
|
constructor(subExpression: Expression) {
|
||||||
super();
|
super();
|
||||||
|
@ -34,7 +34,7 @@ type SearchRow = Pick<NoteRow, "noteId" | "type" | "mime" | "content" | "isProte
|
|||||||
|
|
||||||
class NoteContentFulltextExp extends Expression {
|
class NoteContentFulltextExp extends Expression {
|
||||||
private operator: string;
|
private operator: string;
|
||||||
private tokens: string[];
|
tokens: string[];
|
||||||
private raw: boolean;
|
private raw: boolean;
|
||||||
private flatText: boolean;
|
private flatText: boolean;
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import { normalize } from "../../utils.js";
|
|||||||
import beccaService from "../../../becca/becca_service.js";
|
import beccaService from "../../../becca/becca_service.js";
|
||||||
|
|
||||||
class NoteFlatTextExp extends Expression {
|
class NoteFlatTextExp extends Expression {
|
||||||
private tokens: string[];
|
tokens: string[];
|
||||||
|
|
||||||
constructor(tokens: string[]) {
|
constructor(tokens: string[]) {
|
||||||
super();
|
super();
|
||||||
|
@ -6,7 +6,7 @@ import TrueExp from "./true.js";
|
|||||||
import SearchContext from "../search_context.js";
|
import SearchContext from "../search_context.js";
|
||||||
|
|
||||||
class OrExp extends Expression {
|
class OrExp extends Expression {
|
||||||
private subExpressions: Expression[];
|
subExpressions: Expression[];
|
||||||
|
|
||||||
static of(subExpressions: Expression[]) {
|
static of(subExpressions: Expression[]) {
|
||||||
subExpressions = subExpressions.filter((exp) => !!exp);
|
subExpressions = subExpressions.filter((exp) => !!exp);
|
||||||
|
@ -18,7 +18,7 @@ interface OrderDefinition {
|
|||||||
|
|
||||||
class OrderByAndLimitExp extends Expression {
|
class OrderByAndLimitExp extends Expression {
|
||||||
private orderDefinitions: OrderDefinition[];
|
private orderDefinitions: OrderDefinition[];
|
||||||
private limit: number;
|
limit: number;
|
||||||
subExpression: Expression | null;
|
subExpression: Expression | null;
|
||||||
|
|
||||||
constructor(orderDefinitions: Pick<OrderDefinition, "direction" | "valueExtractor">[], limit?: number) {
|
constructor(orderDefinitions: Pick<OrderDefinition, "direction" | "valueExtractor">[], limit?: number) {
|
||||||
|
@ -41,9 +41,9 @@ interface SearchContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PropertyComparisonExp extends Expression {
|
class PropertyComparisonExp extends Expression {
|
||||||
private propertyName: string;
|
propertyName: string;
|
||||||
private operator: string;
|
operator: string;
|
||||||
private comparedValue: string;
|
comparedValue: string;
|
||||||
private comparator;
|
private comparator;
|
||||||
|
|
||||||
static isProperty(name: string) {
|
static isProperty(name: string) {
|
||||||
|
@ -22,7 +22,7 @@ class SearchContext {
|
|||||||
originalQuery: string;
|
originalQuery: string;
|
||||||
fulltextQuery: string;
|
fulltextQuery: string;
|
||||||
dbLoadNeeded: boolean;
|
dbLoadNeeded: boolean;
|
||||||
private error: string | null;
|
error: string | null;
|
||||||
|
|
||||||
constructor(params: SearchParams = {}) {
|
constructor(params: SearchParams = {}) {
|
||||||
this.fastSearch = !!params.fastSearch;
|
this.fastSearch = !!params.fastSearch;
|
||||||
|
@ -423,7 +423,14 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level
|
|||||||
return getAggregateExpression();
|
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;
|
let expression: Expression | undefined | null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -441,6 +448,12 @@ function parse({ fulltextTokens, expressionTokens, searchContext }: { fulltextTo
|
|||||||
expression
|
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") {
|
if (searchContext.orderBy && searchContext.orderBy !== "relevancy") {
|
||||||
const filterExp = exp;
|
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 escapeHtml from "escape-html";
|
||||||
import SNote from "./shaca/entities/snote.js";
|
import SNote from "./shaca/entities/snote.js";
|
||||||
|
|
||||||
interface Result {
|
/**
|
||||||
|
* Represents the output of the content renderer.
|
||||||
|
*/
|
||||||
|
export interface Result {
|
||||||
header: string;
|
header: string;
|
||||||
content: string | Buffer | undefined;
|
content: string | Buffer | undefined;
|
||||||
isEmpty: boolean;
|
/** Set to `true` if the provided content should be rendered as empty. */
|
||||||
|
isEmpty?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getContent(note: SNote) {
|
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()) {
|
if (typeof result.content !== "string" || !result.content?.trim()) {
|
||||||
result.isEmpty = true;
|
result.isEmpty = true;
|
||||||
} else {
|
} else {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user