mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
Add NLP literal quote escapes
This commit is contained in:
parent
bef9aa6eac
commit
c8bb7f5c64
4 changed files with 143 additions and 2 deletions
|
|
@ -33,6 +33,7 @@ Example:
|
|||
- ([#1541](https://github.com/callumalpass/tasknotes/issues/1541)) Added `{{currentNotePath}}` and `{{currentNoteTitle}}` support to the default tasks folder, so normal task creation can place new tasks beside the active note without creating an inline link. Thanks to @ChristianAnyanwu for suggesting this, and to @abbasou and @ttlaylor for the follow-up feedback.
|
||||
- ([#648](https://github.com/callumalpass/tasknotes/issues/648), [#1605](https://github.com/callumalpass/tasknotes/issues/1605)) Added clickable links in task-card contexts for note links, markdown links, and web URLs while keeping plain contexts as tag-search buttons. Thanks to @trdischat and @Glint-Eye for suggesting this, and to @renatomen for the follow-up feedback.
|
||||
- ([#1482](https://github.com/callumalpass/tasknotes/issues/1482)) Added clickable custom-field links on task cards for wikilinks, markdown links, autolinks, and bare web URLs. Thanks to @ptbosch-figueiredorj for suggesting this, and to @Soleone for the follow-up use case.
|
||||
- ([#1475](https://github.com/callumalpass/tasknotes/issues/1475), [#725](https://github.com/callumalpass/tasknotes/issues/725)) Added quote/backtick escapes for NLP input, so literal course codes or date words can stay in task titles without being parsed as estimates or dates. Thanks to @RumiaKitinari and @gavingwebb for suggesting this.
|
||||
- ([#1603](https://github.com/callumalpass/tasknotes/issues/1603)) Added Calendar view toggles for hiding completed or skipped recurring task instances while keeping future outstanding instances visible. Thanks to @wandererovertheseaofpiss for suggesting this.
|
||||
- ([#1625](https://github.com/callumalpass/tasknotes/issues/1625)) Added `Shift` + `Cmd`/`Ctrl` + `Enter` in the Create Task modal to save the current task and reopen the modal for the next one. Thanks to @tcb678 for suggesting this.
|
||||
- ([#1641](https://github.com/callumalpass/tasknotes/issues/1641)) Added support for list-valued property-based Calendar start and end dates, so one note can render multiple property events without extra view settings. Thanks to @jhoogeboom for suggesting this.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ Body template settings let you scaffold newly created tasks with consistent note
|
|||
|
||||
NLP settings define how text input is interpreted during task capture. **Enable natural language task input** activates date and metadata parsing, **Default to scheduled** changes ambiguous date handling, **NLP language** selects parsing patterns, and **Status suggestion trigger** controls optional status autocomplete activation.
|
||||
|
||||
Wrap text in double quotes, single quotes, or backticks to keep it as literal title text during NLP parsing. For example, `BIO "123H" - HW1` keeps `123H` in the title instead of treating it as a 123-hour estimate, while `Review "Today" notes` keeps `Today` as text instead of parsing it as a date.
|
||||
|
||||
## Pomodoro Timer
|
||||
|
||||
Pomodoro settings control interval lengths, long-break cadence, optional auto-start behavior, and end-of-session notifications/sound. **Pomodoro data storage** chooses whether history is kept in plugin data or daily notes.
|
||||
|
|
|
|||
|
|
@ -49,8 +49,94 @@ export class NaturalLanguageParser extends NaturalLanguageParserCore {
|
|||
}
|
||||
|
||||
parseInput(input: string): ParsedTaskData {
|
||||
const parsed = super.parseInput(input);
|
||||
return this.normalizeUserFieldValues(this.extractLinkedUserFields(input, parsed));
|
||||
const protectedInput = this.protectQuotedLiterals(input);
|
||||
const parsed = super.parseInput(protectedInput.text);
|
||||
const withLinkedFields = this.extractLinkedUserFields(protectedInput.text, parsed);
|
||||
this.restoreQuotedLiterals(withLinkedFields, protectedInput.literals);
|
||||
return this.normalizeUserFieldValues(withLinkedFields);
|
||||
}
|
||||
|
||||
private protectQuotedLiterals(input: string): { text: string; literals: string[] } {
|
||||
const literals: string[] = [];
|
||||
let text = "";
|
||||
let index = 0;
|
||||
|
||||
while (index < input.length) {
|
||||
const char = input[index];
|
||||
if (!this.isQuoteDelimiter(char) || !this.isEligibleQuoteStart(input, index)) {
|
||||
text += char;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const endIndex = this.findClosingQuote(input, index + 1, char);
|
||||
if (endIndex === -1 || !this.isEligibleQuoteEnd(input, endIndex)) {
|
||||
text += char;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const literal = input.slice(index + 1, endIndex);
|
||||
if (literal.trim().length === 0) {
|
||||
text += char;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const placeholder = `__TASKNOTES_LITERAL_${literals.length}__`;
|
||||
literals.push(literal);
|
||||
text += placeholder;
|
||||
index = endIndex + 1;
|
||||
}
|
||||
|
||||
return { text, literals };
|
||||
}
|
||||
|
||||
private restoreQuotedLiterals(parsed: ParsedTaskData, literals: string[]): void {
|
||||
if (literals.length === 0) return;
|
||||
|
||||
let title = parsed.title;
|
||||
literals.forEach((literal, index) => {
|
||||
title = title.replace(`__TASKNOTES_LITERAL_${index}__`, literal);
|
||||
});
|
||||
parsed.title = title.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
private isQuoteDelimiter(char: string): boolean {
|
||||
return char === "\"" || char === "'" || char === "`";
|
||||
}
|
||||
|
||||
private isEligibleQuoteStart(input: string, index: number): boolean {
|
||||
const char = input[index];
|
||||
if (char !== "'") return true;
|
||||
|
||||
const previous = index > 0 ? input[index - 1] : "";
|
||||
return !this.isLiteralBoundaryWordCharacter(previous);
|
||||
}
|
||||
|
||||
private isEligibleQuoteEnd(input: string, index: number): boolean {
|
||||
const char = input[index];
|
||||
if (char !== "'") return true;
|
||||
|
||||
const next = index + 1 < input.length ? input[index + 1] : "";
|
||||
return !this.isLiteralBoundaryWordCharacter(next);
|
||||
}
|
||||
|
||||
private findClosingQuote(input: string, startIndex: number, delimiter: string): number {
|
||||
for (let index = startIndex; index < input.length; index += 1) {
|
||||
if (input[index] !== delimiter) continue;
|
||||
|
||||
const previous = index > 0 ? input[index - 1] : "";
|
||||
if (previous === "\\") continue;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private isLiteralBoundaryWordCharacter(char: string): boolean {
|
||||
return /[\p{L}\p{N}_]/u.test(char);
|
||||
}
|
||||
|
||||
private normalizeUserFieldValues(parsed: ParsedTaskData): ParsedTaskData {
|
||||
|
|
|
|||
52
tests/unit/issues/issue-1475-nlp-literal-quotes.test.ts
Normal file
52
tests/unit/issues/issue-1475-nlp-literal-quotes.test.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* @see https://github.com/callumalpass/tasknotes/issues/1475
|
||||
* @see https://github.com/callumalpass/tasknotes/issues/725
|
||||
*/
|
||||
|
||||
import { NaturalLanguageParser } from "../../../src/services/NaturalLanguageParser";
|
||||
|
||||
function createParser(): NaturalLanguageParser {
|
||||
return new NaturalLanguageParser([], [], true, "en");
|
||||
}
|
||||
|
||||
describe("issues #1475 and #725: NLP literal quote escapes", () => {
|
||||
it("keeps quoted course-code hours in the title instead of parsing a time estimate", () => {
|
||||
const result = createParser().parseInput('BIO "123H" - HW1');
|
||||
|
||||
expect(result.title).toBe("BIO 123H - HW1");
|
||||
expect(result.estimate).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps quoted date words in the title instead of parsing dates", () => {
|
||||
const result = createParser().parseInput('Something "Today"');
|
||||
|
||||
expect(result.title).toBe("Something Today");
|
||||
expect(result.scheduledDate).toBeUndefined();
|
||||
expect(result.dueDate).toBeUndefined();
|
||||
});
|
||||
|
||||
it("supports backtick and single-quote literal spans", () => {
|
||||
const backtickResult = createParser().parseInput("Read `tomorrow` magazine");
|
||||
const singleQuoteResult = createParser().parseInput("Review 'Today is the Day' book");
|
||||
|
||||
expect(backtickResult.title).toBe("Read tomorrow magazine");
|
||||
expect(backtickResult.scheduledDate).toBeUndefined();
|
||||
expect(singleQuoteResult.title).toBe("Review Today is the Day book");
|
||||
expect(singleQuoteResult.scheduledDate).toBeUndefined();
|
||||
});
|
||||
|
||||
it("still parses unquoted NLP outside literal spans", () => {
|
||||
const result = createParser().parseInput('Review "BIO 123H" tomorrow 2h');
|
||||
|
||||
expect(result.title).toBe("Review BIO 123H");
|
||||
expect(result.scheduledDate).toBeDefined();
|
||||
expect(result.estimate).toBe(120);
|
||||
});
|
||||
|
||||
it("leaves apostrophes in normal words alone", () => {
|
||||
const result = createParser().parseInput("John's task tomorrow");
|
||||
|
||||
expect(result.title).toBe("John's task");
|
||||
expect(result.scheduledDate).toBeDefined();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue