From 0180ca57a7ef758bf9cd8197b2db71d7fa89e294 Mon Sep 17 00:00:00 2001 From: Renato Mendonca Date: Sat, 22 Nov 2025 15:47:53 +1300 Subject: [PATCH 1/2] fix: sync package-lock.json with package.json version 4.0.3 --- package-lock.json | 32 ++------------------------------ 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index c7e9c8cd..0a5ef61c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tasknotes", - "version": "3.25.6", + "version": "4.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tasknotes", - "version": "3.25.6", + "version": "4.0.3", "license": "MIT", "dependencies": { "@codemirror/view": "^6.37.2", @@ -140,7 +140,6 @@ "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -742,7 +741,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", - "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -752,7 +750,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz", "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -848,7 +845,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -872,7 +868,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1469,7 +1464,6 @@ "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.19.tgz", "integrity": "sha512-z0aVlO5e4Wah6p6mouM0UEqtRf1MZZPt4mwzEyU6kusaNL+dlWQgAasF2cK23hwT4cmxkEmr4inULXgpyeExdQ==", "license": "MIT", - "peer": true, "dependencies": { "preact": "~10.12.1" } @@ -2657,7 +2651,6 @@ "integrity": "sha512-7199re3wvMAlVqXLaCyAr8IkJSXqkeVAxcYyB2rBu4Id5m2hhlGX1dQsdMBiCXLwu6/LLVqDvJggSNVQBzL6ZQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/css-font-loading-module": "^0.0.7" }, @@ -2707,7 +2700,6 @@ "integrity": "sha512-0XtvrfxHlS2T+beBBSpo7GI8+QLyyTqMVQpNmPqB4woYxzrOEJ9JaUFBaBfCvycLeUkfVih1u6HAbtF+2d1EjQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@pixi/color": "7.2.4", "@pixi/constants": "7.2.4", @@ -2730,7 +2722,6 @@ "integrity": "sha512-w5tqb8cWEO5qIDaO9GEqRvxYhL0iMk0Wsngw23bbLm1gLEQmrFkB2tpJlRAqd7H82C3DrDDeWvkrrxW6+m4apg==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@pixi/core": "7.2.4" } @@ -2741,7 +2732,6 @@ "integrity": "sha512-/JtmoB98fzIU8giN9xvlRvmvOi6u4MaD2DnKNOMHkQ1MBraj3pmrXM9fZ0JbNzi+324GraAAY76QidgHjIYoYQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@pixi/core": "7.2.4", "@pixi/display": "7.2.4" @@ -2830,7 +2820,6 @@ "integrity": "sha512-3A2EumTjWJgXlDLOyuBrl9b6v1Za/E+/IjOGUIX843HH4NYaf1a2sfDfljx6r3oiDvy+VhuBFmgynRcV5IyA0Q==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@pixi/core": "7.2.4", "@pixi/display": "7.2.4", @@ -2850,7 +2839,6 @@ "integrity": "sha512-wiALIqcRKib2BqeH9kOA5fOKWN352nqAspgbDa8gA7OyWzmNwqIedIlElixd0oLFOrIN5jOZAdzeKnoYQlt9Aw==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@pixi/core": "7.2.4", "@pixi/display": "7.2.4" @@ -2957,7 +2945,6 @@ "integrity": "sha512-DhR1B+/d0eXpxHIesJMXcVPrKFwQ+zRA1LvEIFfzewqfaRN3X6PMIuoKX8SIb6tl+Hq8Ba9Pe28zI7d2rmRzrA==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@pixi/core": "7.2.4", "@pixi/display": "7.2.4" @@ -3003,7 +2990,6 @@ "integrity": "sha512-DGu7ktpe+zHhqR2sG9NsJt4mgvSObv5EqXTtUxD4Z0li1gmqF7uktpLyn5I6vSg1TTEL4TECClRDClVDGiykWw==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@pixi/core": "7.2.4", "@pixi/sprite": "7.2.4" @@ -3054,7 +3040,6 @@ "integrity": "sha512-VUGQHBOINIS4ePzoqafwxaGPVRTa3oM/mEutIIHbNGI3b+QvSO+1Dnk40M0zcH6Bo+MxQZbOZK5X/wO9oU5+LQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@pixi/color": "7.2.4", "@pixi/constants": "7.2.4", @@ -3875,7 +3860,6 @@ "integrity": "sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.29.0", "@typescript-eslint/types": "5.29.0", @@ -4494,7 +4478,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4948,7 +4931,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -5184,7 +5166,6 @@ "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -5397,7 +5378,6 @@ "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -5832,7 +5812,6 @@ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -6578,7 +6557,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6651,7 +6629,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9007,7 +8984,6 @@ "integrity": "sha512-yC3JvpP/ZcAZX5rYCtXO/g9k6VTCQz0VFE2v1FpxytWzUqfDtu0XL/pwnNvptzYItvGwomh1ehomRNMOyhCJKw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.1.1", "@jest/types": "30.0.5", @@ -9769,7 +9745,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -11185,7 +11160,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12738,7 +12712,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12815,7 +12788,6 @@ "integrity": "sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.1.0", "@typescript-eslint/types": "7.1.0", From 988df48b4bf3202cc5279ee65af09f615bfbfc51 Mon Sep 17 00:00:00 2001 From: Renato Mendonca Date: Sat, 22 Nov 2025 15:53:56 +1300 Subject: [PATCH 2/2] fix: handle status trigger prefix in NLP parsing - Strip status trigger character once before matching candidates - Fixes issue where manually typing trigger + label fails to parse e.g. "*Active = Now*" - Fixes regression where temporal keywords in status values were misparsed "*Active = Now*" resulting in Now parsed as due date. - All 30 status extraction tests passing --- src/services/NaturalLanguageParser.ts | 21 ++- ...alLanguageParser.status-extraction.test.ts | 153 +++++++++++++++++- 2 files changed, 172 insertions(+), 2 deletions(-) diff --git a/src/services/NaturalLanguageParser.ts b/src/services/NaturalLanguageParser.ts index c7002f1f..8b4ce598 100644 --- a/src/services/NaturalLanguageParser.ts +++ b/src/services/NaturalLanguageParser.ts @@ -541,6 +541,15 @@ export class NaturalLanguageParser { (a, b) => b.label.length - a.label.length ); + // Strip the status trigger from text once before searching + const statusTrigger = this.triggerConfig.getTriggerForProperty("status"); + let textToSearch = text; + + if (statusTrigger && statusTrigger.enabled && statusTrigger.trigger) { + const escapedTrigger = this.escapeRegex(statusTrigger.trigger); + textToSearch = text.replace(new RegExp(escapedTrigger, 'g'), ''); + } + for (const config of sortedConfigs) { // Try both label and value const candidates = [config.label, config.value]; @@ -550,9 +559,19 @@ export class NaturalLanguageParser { if (!candidate || candidate.trim() === "") { continue; } - const match = this.findStatusMatch(text, candidate); + + // Try matching the candidate in the cleaned text + const match = this.findStatusMatch(textToSearch, candidate); if (match) { result.status = config.value; + + // If we stripped the trigger, remove trigger + match from original text + if (textToSearch !== text && statusTrigger) { + const triggerPlusMatch = statusTrigger.trigger + match.fullMatch; + return this.cleanupWhitespace(text.replace(triggerPlusMatch, "")); + } + + // Otherwise just remove the match return this.cleanupWhitespace(text.replace(match.fullMatch, "")); } } diff --git a/tests/unit/services/NaturalLanguageParser.status-extraction.test.ts b/tests/unit/services/NaturalLanguageParser.status-extraction.test.ts index 77860c4c..0cca7380 100644 --- a/tests/unit/services/NaturalLanguageParser.status-extraction.test.ts +++ b/tests/unit/services/NaturalLanguageParser.status-extraction.test.ts @@ -223,7 +223,7 @@ describe('NaturalLanguageParser - Status Extraction', () => { it('should maintain extraction order to prevent conflicts', () => { const result = parser.parseInput('Task Active = Now due tomorrow at 2pm @work +project'); - + expect(result.status).toBe('active'); expect(result.contexts).toContain('work'); expect(result.projects).toContain('project'); @@ -232,4 +232,155 @@ describe('NaturalLanguageParser - Status Extraction', () => { expect(result.title).toBe('Task'); // "due tomorrow at 2pm" should be parsed as date and removed }); }); + + describe('Regression Tests - Issue: Status Parsing with Markdown Special Characters', () => { + it('should extract status when autocomplete inserts the VALUE instead of LABEL', () => { + // CRITICAL: Autocomplete inserts s.value, not s.label! + // So if label is "*41🟩Done = Recent" and value is "done-recent", + // the autocomplete will insert "done-recent " into the text + const statusWithAsterisk: StatusConfig[] = [ + { id: 'done-recent', value: 'done-recent', label: '*41🟩Done = Recent', color: '#00aa00', isCompleted: true, order: 1 } + ]; + const parserWithAsterisk = new NaturalLanguageParser(statusWithAsterisk, priorityConfigs, false); + + // This is what actually gets inserted by autocomplete + const result = parserWithAsterisk.parseInput('Task done-recent '); + + expect(result.status).toBe('done-recent'); + expect(result.title).toBe('Task'); + }); + + it('should extract status when user types trigger + label manually (FIXED!)', () => { + // ROOT CAUSE: User has status with label "41🟩Done = Recent" (no asterisk) + // User types "*" (trigger) + "41🟩Done = Recent" manually + // Text becomes "Task *41🟩Done = Recent" + // FIX: Parser now tries matching both "41🟩Done = Recent" and "*41🟩Done = Recent" + const statusWithoutAsterisk: StatusConfig[] = [ + { id: 'done-recent', value: 'done-recent', label: '41🟩Done = Recent', color: '#00aa00', isCompleted: true, order: 1 } + ]; + const parserWithoutAsterisk = new NaturalLanguageParser(statusWithoutAsterisk, priorityConfigs, false); + + // User types "*41🟩Done = Recent" (trigger + label) + const result = parserWithoutAsterisk.parseInput('Task *41🟩Done = Recent'); + + // FIXED: Status should now be extracted correctly + expect(result.status).toBe('done-recent'); + expect(result.title).toBe('Task'); + }); + + it('should extract status starting with underscore (markdown bold marker)', () => { + const statusWithUnderscore: StatusConfig[] = [ + { id: 'important', value: 'important', label: '_Important_', color: '#ff0000', isCompleted: false, order: 1 } + ]; + const parserWithUnderscore = new NaturalLanguageParser(statusWithUnderscore, priorityConfigs, false); + + const result = parserWithUnderscore.parseInput('Task _Important_'); + + expect(result.status).toBe('important'); + expect(result.title).toBe('Task'); + }); + + it('should extract status starting with tilde (markdown strikethrough marker)', () => { + const statusWithTilde: StatusConfig[] = [ + { id: 'deprecated', value: 'deprecated', label: '~Deprecated~', color: '#gray', isCompleted: false, order: 1 } + ]; + const parserWithTilde = new NaturalLanguageParser(statusWithTilde, priorityConfigs, false); + + const result = parserWithTilde.parseInput('Task ~Deprecated~'); + + expect(result.status).toBe('deprecated'); + expect(result.title).toBe('Task'); + }); + + it('should extract status with brackets (markdown link markers)', () => { + const statusWithBrackets: StatusConfig[] = [ + { id: 'linked', value: 'linked', label: '[Linked]', color: '#blue', isCompleted: false, order: 1 } + ]; + const parserWithBrackets = new NaturalLanguageParser(statusWithBrackets, priorityConfigs, false); + + const result = parserWithBrackets.parseInput('Task [Linked]'); + + expect(result.status).toBe('linked'); + expect(result.title).toBe('Task'); + }); + }); + + describe('Regression Tests - Issue: Status with Temporal Keywords', () => { + it('should extract status containing "Now" without parsing it as a date', () => { + // Regression: Status "10🔥Expedite = Now" had "Now" parsed as due date + const statusWithNow: StatusConfig[] = [ + { id: 'expedite', value: 'expedite', label: '10🔥Expedite = Now', color: '#ff0000', isCompleted: false, order: 1 } + ]; + const parserWithNow = new NaturalLanguageParser(statusWithNow, priorityConfigs, false); + + const result = parserWithNow.parseInput('Task 10🔥Expedite = Now'); + + expect(result.status).toBe('expedite'); + expect(result.title).toBe('Task'); + // "Now" should NOT be parsed as a date since it's part of the status + expect(result.dueDate).toBeUndefined(); + expect(result.scheduledDate).toBeUndefined(); + }); + + it('should extract status with trigger + temporal keyword without parsing as date', () => { + // User types "*10🔥Expedite = Now" (trigger + label with temporal keyword) + const statusWithNow: StatusConfig[] = [ + { id: 'expedite', value: 'expedite', label: '10🔥Expedite = Now', color: '#ff0000', isCompleted: false, order: 1 } + ]; + const parserWithNow = new NaturalLanguageParser(statusWithNow, priorityConfigs, false); + + const result = parserWithNow.parseInput('Task *10🔥Expedite = Now'); + + expect(result.status).toBe('expedite'); + expect(result.title).toBe('Task'); + // "Now" should NOT be parsed as a date since it's part of the status + expect(result.dueDate).toBeUndefined(); + expect(result.scheduledDate).toBeUndefined(); + }); + + it('should extract status containing "Today" without parsing it as a date', () => { + const statusWithToday: StatusConfig[] = [ + { id: 'today-priority', value: 'today-priority', label: 'Due Today', color: '#ff0000', isCompleted: false, order: 1 } + ]; + const parserWithToday = new NaturalLanguageParser(statusWithToday, priorityConfigs, false); + + const result = parserWithToday.parseInput('Task Due Today'); + + expect(result.status).toBe('today-priority'); + expect(result.title).toBe('Task'); + // "Today" should NOT be parsed as a date since it's part of the status + expect(result.dueDate).toBeUndefined(); + expect(result.scheduledDate).toBeUndefined(); + }); + + it('should extract status containing "Tomorrow" without parsing it as a date', () => { + const statusWithTomorrow: StatusConfig[] = [ + { id: 'tomorrow-status', value: 'tomorrow-status', label: 'For Tomorrow', color: '#blue', isCompleted: false, order: 1 } + ]; + const parserWithTomorrow = new NaturalLanguageParser(statusWithTomorrow, priorityConfigs, false); + + const result = parserWithTomorrow.parseInput('Task For Tomorrow'); + + expect(result.status).toBe('tomorrow-status'); + expect(result.title).toBe('Task'); + // "Tomorrow" should NOT be parsed as a date since it's part of the status + expect(result.dueDate).toBeUndefined(); + expect(result.scheduledDate).toBeUndefined(); + }); + + it('should extract status with temporal keyword and still parse separate date', () => { + // Status contains "Now" but there's also a separate "tomorrow" that should be parsed + const statusWithNow: StatusConfig[] = [ + { id: 'expedite', value: 'expedite', label: '10🔥Expedite = Now', color: '#ff0000', isCompleted: false, order: 1 } + ]; + const parserWithNow = new NaturalLanguageParser(statusWithNow, priorityConfigs, false); + + const result = parserWithNow.parseInput('Task 10🔥Expedite = Now tomorrow'); + + expect(result.status).toBe('expedite'); + expect(result.title).toBe('Task'); + // "tomorrow" should be parsed as a date (separate from status) + expect(result.dueDate).toBeDefined(); + }); + }); });