diff --git a/package-lock.json b/package-lock.json index f336d6e71..e1bce5584 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 03b9f77b9..33eca06b8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/spec/search/parser.spec.ts b/spec/search/parser.spec.ts index cf5d3315d..09c7ce06b 100644 --- a/spec/search/parser.spec.ts +++ b/spec/search/parser.spec.ts @@ -1,48 +1,28 @@ -// @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"; - -function tokens(toks: Array, cur = 0): Array { - return toks.map((arg) => { - if (Array.isArray(arg)) { - return tokens(arg, cur); - } else { - cur += arg.length; - - return { - token: arg, - inQuotes: false, - startIndex: cur - arg.length, - endIndex: cur - 1 - }; - } - }); -} - -function assertIsArchived(exp: Expression) { - expect(exp.constructor.name).toEqual("PropertyComparisonExp"); - expect(exp.propertyName).toEqual("isArchived"); - expect(exp.operator).toEqual("="); - expect(exp.comparedValue).toEqual("false"); -} +import { default as parseInternal, type ParseOpts } from "../../src/services/search/services/parse.js"; describe("Parser", () => { it("fulltext parser without content", () => { const rootExp = parse({ fulltextTokens: tokens(["hello", "hi"]), expressionTokens: [], - searchContext: new SearchContext({ excludeArchived: true }) - }); + searchContext: new SearchContext() + }, AndExp); - 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"]); + 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", () => { @@ -50,20 +30,17 @@ describe("Parser", () => { fulltextTokens: tokens(["hello", "hi"]), expressionTokens: [], searchContext: new SearchContext() - }); + }, AndExp); - expect(rootExp.constructor.name).toEqual("AndExp"); assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); + const orExp = expectExpression(rootExp.subExpressions[2], OrExp); - const subs = rootExp.subExpressions[2].subExpressions; + const firstSub = expectExpression(orExp.subExpressions[0], NoteFlatTextExp); + expect(firstSub.tokens).toEqual(["hello", "hi"]); - 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"]); + const secondSub = expectExpression(orExp.subExpressions[1], NoteContentFulltextExp); + expect(secondSub.tokens).toEqual(["hello", "hi"]); }); it("simple label comparison", () => { @@ -71,14 +48,13 @@ describe("Parser", () => { fulltextTokens: [], expressionTokens: tokens(["#mylabel", "=", "text"]), searchContext: new SearchContext() - }); + }, AndExp); - 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(); + 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", () => { @@ -86,46 +62,40 @@ describe("Parser", () => { fulltextTokens: [], expressionTokens: tokens(["#!mylabel"]), searchContext: new SearchContext() - }); + }, AndExp); - 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"); + 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); - 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"); + 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(true) - }); + searchContext: new SearchContext() + }, AndExp); - 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; + const andExp = expectExpression(rootExp.subExpressions[2], AndExp); + const [firstSub, secondSub] = expectSubexpressions(andExp, LabelComparisonExp, LabelComparisonExp); - expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); expect(firstSub.attributeName).toEqual("first"); - - expect(secondSub.constructor.name).toEqual("LabelComparisonExp"); expect(secondSub.attributeName).toEqual("second"); }); @@ -134,18 +104,14 @@ describe("Parser", () => { fulltextTokens: [], expressionTokens: tokens(["#first", "=", "text", "#second", "=", "text"]), searchContext: new SearchContext() - }); + }, AndExp); - 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; + const andExp = expectExpression(rootExp.subExpressions[2], AndExp); + const [firstSub, secondSub] = expectSubexpressions(andExp, LabelComparisonExp, LabelComparisonExp); - expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); expect(firstSub.attributeName).toEqual("first"); - - expect(secondSub.constructor.name).toEqual("LabelComparisonExp"); expect(secondSub.attributeName).toEqual("second"); }); @@ -154,18 +120,13 @@ describe("Parser", () => { fulltextTokens: [], expressionTokens: tokens(["#first", "=", "text", "or", "#second", "=", "text"]), searchContext: new SearchContext() - }); + }, AndExp); - 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"); + const orExp = expectExpression(rootExp.subExpressions[2], OrExp); + const [firstSub, secondSub] = expectSubexpressions(orExp, LabelComparisonExp, LabelComparisonExp); expect(firstSub.attributeName).toEqual("first"); - - expect(secondSub.constructor.name).toEqual("LabelComparisonExp"); expect(secondSub.attributeName).toEqual("second"); }); @@ -173,20 +134,16 @@ describe("Parser", () => { const rootExp = parse({ fulltextTokens: tokens(["hello"]), expressionTokens: tokens(["#mylabel", "=", "text"]), - searchContext: new SearchContext({ excludeArchived: true }) - }); + searchContext: new SearchContext() + }, AndExp); - expect(rootExp.constructor.name).toEqual("AndExp"); - const [firstSub, secondSub, thirdSub, fourth] = rootExp.subExpressions; + const [firstSub, _, thirdSub, fourth] = expectSubexpressions(rootExp, PropertyComparisonExp, undefined, OrExp, LabelComparisonExp); - 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"]); + const noteFlatTextExp = expectExpression(thirdSub.subExpressions[0], NoteFlatTextExp); + expect(noteFlatTextExp.tokens).toEqual(["hello"]); - expect(fourth.constructor.name).toEqual("LabelComparisonExp"); expect(fourth.attributeName).toEqual("mylabel"); }); @@ -195,24 +152,17 @@ describe("Parser", () => { fulltextTokens: [], expressionTokens: tokens(["#first", "=", "text", "or", ["#second", "=", "text", "and", "#third", "=", "text"]]), searchContext: new SearchContext() - }); + }, AndExp); - 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; + const orExp = expectExpression(rootExp.subExpressions[2], OrExp); + const [firstSub, secondSub] = expectSubexpressions(orExp, LabelComparisonExp, AndExp); - 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"); + const [firstSubSub, secondSubSub] = expectSubexpressions(secondSub, LabelComparisonExp, LabelComparisonExp); expect(firstSubSub.attributeName).toEqual("second"); - - expect(secondSubSub.constructor.name).toEqual("LabelComparisonExp"); expect(secondSubSub.attributeName).toEqual("third"); }); @@ -221,36 +171,39 @@ describe("Parser", () => { fulltextTokens: [], expressionTokens: tokens(["#first", ["#second", "or", "#third"], "#fourth"]), searchContext: new SearchContext() - }); + }, AndExp); - 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; + const andExp = expectExpression(rootExp.subExpressions[2], AndExp); + const [firstSub, secondSub, thirdSub] = expectSubexpressions(andExp, AttributeExistsExp, OrExp, AttributeExistsExp); - 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"); + const [firstSubSub, secondSubSub] = expectSubexpressions(secondSub, AttributeExistsExp, 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"); }); + + 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(); - parse({ + parseInternal({ fulltextTokens: [], expressionTokens: tokens(["#first", "="]), searchContext @@ -263,7 +216,7 @@ describe("Invalid expressions", () => { let searchContext = new SearchContext(); searchContext.originalQuery = "#first = #second"; - parse({ + parseInternal({ fulltextTokens: [], expressionTokens: tokens(["#first", "=", "#second"]), searchContext @@ -274,7 +227,7 @@ describe("Invalid expressions", () => { searchContext = new SearchContext(); searchContext.originalQuery = "#first = note.relations.second"; - parse({ + parseInternal({ fulltextTokens: [], expressionTokens: tokens(["#first", "=", "note", ".", "relations", "second"]), searchContext @@ -290,21 +243,20 @@ describe("Invalid expressions", () => { { token: "#second", inQuotes: true } ], searchContext: new SearchContext() - }); + }, AndExp); - 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(); + 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(); - parse({ + parseInternal({ fulltextTokens: [], expressionTokens: tokens(["~first", "=", "text", "-", "abc"]), searchContext @@ -313,3 +265,91 @@ describe("Invalid expressions", () => { expect(searchContext.error).toEqual('Relation can be compared only with property, e.g. ~relation.title=hello in ""'); }); }); + +type ClassType = new (...args: any[]) => T; + +function tokens(toks: (string | string[])[], cur = 0): Array { + return toks.map((arg) => { + if (Array.isArray(arg)) { + return tokens(arg, cur); + } else { + cur += arg.length; + + return { + token: arg, + inQuotes: false, + startIndex: cur - arg.length, + endIndex: cur - 1 + }; + } + }); +} + +function assertIsArchived(_exp: Expression) { + const exp = expectExpression(_exp, PropertyComparisonExp); + expect(exp.propertyName).toEqual("isArchived"); + expect(exp.operator).toEqual("="); + expect(exp.comparedValue).toEqual("false"); +} + +/** + * 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(opts: ParseOpts, type: ClassType) { + return expectExpression(parseInternal(opts), type); +} + +/** + * 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(exp: Expression, type: ClassType) { + expect(exp).toBeInstanceOf(type); + return exp as T; +} + +/** + * 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( + exp: AndExp, + firstType: ClassType, + secondType?: ClassType, + thirdType?: ClassType, + fourthType?: ClassType): [ 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 + ] +} diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json index 5043a9a03..85189e03b 100644 --- a/spec/support/jasmine.json +++ b/spec/support/jasmine.json @@ -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 diff --git a/src/public/stylesheets/theme-next/shell.css b/src/public/stylesheets/theme-next/shell.css index 3abfd4c53..78eeac63c 100644 --- a/src/public/stylesheets/theme-next/shell.css +++ b/src/public/stylesheets/theme-next/shell.css @@ -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; } diff --git a/src/services/search/expressions/and.ts b/src/services/search/expressions/and.ts index 2a6e07877..72918a74f 100644 --- a/src/services/search/expressions/and.ts +++ b/src/services/search/expressions/and.ts @@ -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[]; diff --git a/src/services/search/expressions/attribute_exists.ts b/src/services/search/expressions/attribute_exists.ts index 1b0dc397b..52c866001 100644 --- a/src/services/search/expressions/attribute_exists.ts +++ b/src/services/search/expressions/attribute_exists.ts @@ -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; diff --git a/src/services/search/expressions/expression.ts b/src/services/search/expressions/expression.ts index 2bff50e95..0d3981b49 100644 --- a/src/services/search/expressions/expression.ts +++ b/src/services/search/expressions/expression.ts @@ -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; diff --git a/src/services/search/expressions/label_comparison.ts b/src/services/search/expressions/label_comparison.ts index ade1fed6c..c162aa0a2 100644 --- a/src/services/search/expressions/label_comparison.ts +++ b/src/services/search/expressions/label_comparison.ts @@ -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(); diff --git a/src/services/search/expressions/not.ts b/src/services/search/expressions/not.ts index 25f61f6db..80f2adfe3 100644 --- a/src/services/search/expressions/not.ts +++ b/src/services/search/expressions/not.ts @@ -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(); diff --git a/src/services/search/expressions/note_content_fulltext.ts b/src/services/search/expressions/note_content_fulltext.ts index 741a54a03..cd92e5178 100644 --- a/src/services/search/expressions/note_content_fulltext.ts +++ b/src/services/search/expressions/note_content_fulltext.ts @@ -34,7 +34,7 @@ type SearchRow = Pick !!exp); diff --git a/src/services/search/expressions/order_by_and_limit.ts b/src/services/search/expressions/order_by_and_limit.ts index c0cb676a7..3bbc53a67 100644 --- a/src/services/search/expressions/order_by_and_limit.ts +++ b/src/services/search/expressions/order_by_and_limit.ts @@ -18,7 +18,7 @@ interface OrderDefinition { class OrderByAndLimitExp extends Expression { private orderDefinitions: OrderDefinition[]; - private limit: number; + limit: number; subExpression: Expression | null; constructor(orderDefinitions: Pick[], limit?: number) { diff --git a/src/services/search/expressions/property_comparison.ts b/src/services/search/expressions/property_comparison.ts index a29661c47..02906f995 100644 --- a/src/services/search/expressions/property_comparison.ts +++ b/src/services/search/expressions/property_comparison.ts @@ -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) { diff --git a/src/services/search/search_context.ts b/src/services/search/search_context.ts index 5efc890a8..29fb7dbda 100644 --- a/src/services/search/search_context.ts +++ b/src/services/search/search_context.ts @@ -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; diff --git a/src/services/search/services/parse.ts b/src/services/search/services/parse.ts index 25ff8395a..34acbc7ad 100644 --- a/src/services/search/services/parse.ts +++ b/src/services/search/services/parse.ts @@ -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; diff --git a/src/share/content_renderer.spec.ts b/src/share/content_renderer.spec.ts new file mode 100644 index 000000000..d79cd7840 --- /dev/null +++ b/src/share/content_renderer.spec.ts @@ -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
", () => {
+            const result: Result = {
+                header: "",
+                content: "\tHello\nworld"
+            };
+            renderCode(result);
+            expect(result.isEmpty).toBeFalsy();
+            expect(result.content).toBe("
\tHello\nworld
"); + }); + }); +}); diff --git a/src/share/content_renderer.ts b/src/share/content_renderer.ts index 6680bacda..c9c17b67f 100644 --- a/src/share/content_renderer.ts +++ b/src/share/content_renderer.ts @@ -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 {