mirror of
https://github.com/rioskit/obsidian-todo-txt-mode.git
synced 2026-07-22 05:49:24 +00:00
feat: Add syntax highlighting for completion and creation dates, refine parser,tests
This commit is contained in:
parent
cf5557e135
commit
0fa34ea3fe
9 changed files with 2284 additions and 208 deletions
68
no-todo.txt
Normal file
68
no-todo.txt
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
Empty priority () should not parse
|
||||
Mixed case priority (a) should not parse as priority
|
||||
Lowercase x in middle of task x should not be completion
|
||||
X Uppercase X at start should not be completion
|
||||
x(A) No space after x should not parse correctly
|
||||
x2025-01-16 No space after x before date
|
||||
(A)No space after priority should still work
|
||||
2025-01-15No space after date might not parse
|
||||
+projectNoSpace@contextNoSpace tags without spaces
|
||||
Key without value due: should not parse
|
||||
Value without key :2025-12-31 should not parse
|
||||
(-1) Negative priority should not parse as priority
|
||||
Task with invalid date 2025-13-32
|
||||
Task with partial date 2025-01
|
||||
due:tomorrow Invalid date format
|
||||
due:2025/12/31 Different date separator
|
||||
( A ) Priority with spaces inside parentheses
|
||||
++invalid double plus project
|
||||
@@invalid double at context
|
||||
+val@id project with @ in name
|
||||
@val+id context with + in name
|
||||
+pro ject with space should be two tokens
|
||||
@con text with space should be two tokens
|
||||
+ project with space after plus
|
||||
@ context with space after at
|
||||
Task with +project+project2 no space between
|
||||
Task with @context@context2 no space between
|
||||
Task with due:2025-12-31due:2025-12-30 duplicate keys
|
||||
Task with +@ mixed symbols
|
||||
Task with @+ reversed symbols
|
||||
Task with ++double +plus
|
||||
Task with @@double @at
|
||||
Task with ++ @@ multiple symbol prefixes
|
||||
(A)(B) Double priority task
|
||||
x x Double completion marker
|
||||
2025-01-15 2025-01-15 Duplicate creation date
|
||||
Task with : colon but no key:value
|
||||
Task with key: no value after colon
|
||||
Task with :value no key before colon
|
||||
Task with + bare plus
|
||||
Task with @ bare at
|
||||
Task with ending plus +
|
||||
Task with ending at @
|
||||
Task with \n newline escape
|
||||
Task with \t tab escape
|
||||
Task with \r carriage return
|
||||
Task with null\0character
|
||||
Task with unicode escape \u0041 in text
|
||||
Task with +project\n@context broken by newline
|
||||
Multiline attempt line1\nline2 +project
|
||||
Long priority (URGENT) 2025-01-15 Task +project @context
|
||||
x (A) 2025-01-16 Priority completed with date +project
|
||||
x (A) 2025-01-16 Priority completed with date @context
|
||||
x (A) 2025-01-16 Priority completed with date due:2025-12-31
|
||||
x (A) 2025-01-16 2025-01-15 Priority completed both dates +project
|
||||
x (A) 2025-01-16 2025-01-15 Priority completed both dates @context
|
||||
x (A) 2025-01-16 2025-01-15 Priority completed both dates due:2025-12-31
|
||||
x (A) 2025-01-16 Priority completed +project @context
|
||||
x (A) 2025-01-16 Priority completed +project due:2025-12-31
|
||||
x (A) 2025-01-16 Priority completed @context due:2025-12-31
|
||||
x (A) 2025-01-16 Priority completed +project @context due:2025-12-31
|
||||
x (A) 2025-01-16 2025-01-15 Priority completed all +project @context due:2025-12-31
|
||||
x (A) 2025-01-16 2025-01-15 Completed priority with space after x
|
||||
+project@ Invalid project with @ symbol
|
||||
@context+ Invalid context with + symbol
|
||||
Empty line above and below task
|
||||
Task with newline\nshould be single line
|
||||
Task with tab\tcharacters inside
|
||||
349
src/__tests__/parser.test.ts
Normal file
349
src/__tests__/parser.test.ts
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
import { tokenizeLine, Token, ParsedLine } from '../parser';
|
||||
|
||||
describe('tokenizeLine', () => {
|
||||
describe('テキストトークン', () => {
|
||||
it('テキストのみ', () => {
|
||||
const result = tokenizeLine('これはシンプルなタスクです');
|
||||
expect(result.tokens).toHaveLength(1);
|
||||
expect(result.tokens[0]).toEqual({
|
||||
type: 'text',
|
||||
value: 'これはシンプルなタスクです',
|
||||
start: 0,
|
||||
end: 13
|
||||
});
|
||||
});
|
||||
|
||||
it('空行', () => {
|
||||
const result = tokenizeLine('');
|
||||
expect(result.tokens).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('空白のみ', () => {
|
||||
const result = tokenizeLine(' ');
|
||||
expect(result.tokens).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('完了フラグ', () => {
|
||||
it('完了フラグのみ', () => {
|
||||
const result = tokenizeLine('x');
|
||||
expect(result.tokens).toHaveLength(1);
|
||||
expect(result.tokens[0]).toEqual({
|
||||
type: 'completion',
|
||||
value: 'x',
|
||||
start: 0,
|
||||
end: 1
|
||||
});
|
||||
});
|
||||
|
||||
it('完了フラグ付き', () => {
|
||||
const result = tokenizeLine('x 完了したタスク');
|
||||
expect(result.tokens).toHaveLength(2);
|
||||
expect(result.tokens[0]).toEqual({
|
||||
type: 'completion',
|
||||
value: 'x',
|
||||
start: 0,
|
||||
end: 1
|
||||
});
|
||||
expect(result.tokens[1].type).toBe('text');
|
||||
expect(result.tokens[1].value).toBe('完了したタスク');
|
||||
expect(result.tokens[1].start).toBe(2);
|
||||
});
|
||||
|
||||
it('インデント付き', () => {
|
||||
const result = tokenizeLine(' x 完了したタスク');
|
||||
expect(result.tokens).toHaveLength(2);
|
||||
expect(result.tokens[0]).toEqual({
|
||||
type: 'completion',
|
||||
value: 'x',
|
||||
start: 2,
|
||||
end: 3
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('優先度', () => {
|
||||
it('アルファベット', () => {
|
||||
const result = tokenizeLine('(A) 高優先度タスク');
|
||||
expect(result.tokens).toHaveLength(2);
|
||||
expect(result.tokens[0]).toEqual({
|
||||
type: 'priority',
|
||||
value: '(A)',
|
||||
start: 0,
|
||||
end: 3
|
||||
});
|
||||
expect(result.tokens[1].type).toBe('text');
|
||||
expect(result.tokens[1].value).toBe('高優先度タスク');
|
||||
expect(result.tokens[1].start).toBe(4);
|
||||
});
|
||||
|
||||
it('数字', () => {
|
||||
const result = tokenizeLine('(1) 数字優先度タスク');
|
||||
expect(result.tokens).toHaveLength(2);
|
||||
expect(result.tokens[0]).toEqual({
|
||||
type: 'priority',
|
||||
value: '(1)',
|
||||
start: 0,
|
||||
end: 3
|
||||
});
|
||||
});
|
||||
|
||||
it('混合', () => {
|
||||
const result = tokenizeLine('(A1) 混合優先度タスク');
|
||||
expect(result.tokens).toHaveLength(2);
|
||||
expect(result.tokens[0]).toEqual({
|
||||
type: 'priority',
|
||||
value: '(A1)',
|
||||
start: 0,
|
||||
end: 4
|
||||
});
|
||||
});
|
||||
|
||||
it('完了フラグ付き', () => {
|
||||
const result = tokenizeLine('x (A) 完了した高優先度タスク');
|
||||
expect(result.tokens).toHaveLength(3);
|
||||
expect(result.tokens[0].type).toBe('completion');
|
||||
expect(result.tokens[1]).toEqual({
|
||||
type: 'priority',
|
||||
value: '(A)',
|
||||
start: 2,
|
||||
end: 5
|
||||
});
|
||||
expect(result.tokens[2].type).toBe('text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('日付', () => {
|
||||
it('完了日', () => {
|
||||
const result = tokenizeLine('x 2024-01-15 完了したタスク');
|
||||
expect(result.tokens).toHaveLength(3);
|
||||
expect(result.tokens[0].type).toBe('completion');
|
||||
expect(result.tokens[1]).toEqual({
|
||||
type: 'completion_date',
|
||||
value: '2024-01-15',
|
||||
start: 2,
|
||||
end: 12
|
||||
});
|
||||
});
|
||||
|
||||
it('作成日', () => {
|
||||
const result = tokenizeLine('2024-01-15 作成日付きタスク');
|
||||
expect(result.tokens).toHaveLength(2);
|
||||
expect(result.tokens[0]).toEqual({
|
||||
type: 'creation_date',
|
||||
value: '2024-01-15',
|
||||
start: 0,
|
||||
end: 10
|
||||
});
|
||||
});
|
||||
|
||||
it('完了日と作成日', () => {
|
||||
const result = tokenizeLine('x 2024-01-15 2024-01-10 完了タスク');
|
||||
expect(result.tokens).toHaveLength(4);
|
||||
expect(result.tokens[0].type).toBe('completion');
|
||||
expect(result.tokens[1]).toEqual({
|
||||
type: 'completion_date',
|
||||
value: '2024-01-15',
|
||||
start: 2,
|
||||
end: 12
|
||||
});
|
||||
expect(result.tokens[2]).toEqual({
|
||||
type: 'creation_date',
|
||||
value: '2024-01-10',
|
||||
start: 13,
|
||||
end: 23
|
||||
});
|
||||
});
|
||||
|
||||
it('優先度付き', () => {
|
||||
const result = tokenizeLine('(A) 2024-01-15 優先度付きタスク');
|
||||
expect(result.tokens).toHaveLength(3);
|
||||
expect(result.tokens[0].type).toBe('priority');
|
||||
expect(result.tokens[1]).toEqual({
|
||||
type: 'creation_date',
|
||||
value: '2024-01-15',
|
||||
start: 4,
|
||||
end: 14
|
||||
});
|
||||
});
|
||||
|
||||
it('完全な構文', () => {
|
||||
const result = tokenizeLine('x (A) 2024-01-15 2024-01-10 完了タスク');
|
||||
expect(result.tokens).toHaveLength(5);
|
||||
expect(result.tokens[0].type).toBe('completion');
|
||||
expect(result.tokens[1].type).toBe('priority');
|
||||
expect(result.tokens[2].type).toBe('completion_date');
|
||||
expect(result.tokens[3].type).toBe('creation_date');
|
||||
expect(result.tokens[4].type).toBe('text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('プロジェクトとコンテキスト', () => {
|
||||
it('プロジェクト', () => {
|
||||
const result = tokenizeLine('タスク +project1 +project2');
|
||||
expect(result.tokens).toHaveLength(3);
|
||||
expect(result.tokens[1].type).toBe('project');
|
||||
expect(result.tokens[1].value).toBe('+project1');
|
||||
expect(result.tokens[2].type).toBe('project');
|
||||
expect(result.tokens[2].value).toBe('+project2');
|
||||
});
|
||||
|
||||
it('コンテキスト', () => {
|
||||
const result = tokenizeLine('タスク @home @work');
|
||||
expect(result.tokens).toHaveLength(3);
|
||||
expect(result.tokens[1].type).toBe('context');
|
||||
expect(result.tokens[1].value).toBe('@home');
|
||||
expect(result.tokens[2].type).toBe('context');
|
||||
expect(result.tokens[2].value).toBe('@work');
|
||||
});
|
||||
|
||||
it('混合', () => {
|
||||
const result = tokenizeLine('タスク +project @context');
|
||||
expect(result.tokens).toHaveLength(3);
|
||||
expect(result.tokens[1].type).toBe('project');
|
||||
expect(result.tokens[2].type).toBe('context');
|
||||
});
|
||||
});
|
||||
|
||||
describe('key:value', () => {
|
||||
it('due:日付', () => {
|
||||
const result = tokenizeLine('タスク due:2024-01-15');
|
||||
expect(result.tokens).toHaveLength(2);
|
||||
expect(result.tokens[1].type).toBe('key_value');
|
||||
expect(result.tokens[1].value).toBe('due:2024-01-15');
|
||||
});
|
||||
|
||||
it('カスタム', () => {
|
||||
const result = tokenizeLine('タスク priority:high effort:low');
|
||||
expect(result.tokens).toHaveLength(3);
|
||||
expect(result.tokens[1].type).toBe('key_value');
|
||||
expect(result.tokens[1].value).toBe('priority:high');
|
||||
expect(result.tokens[2].type).toBe('key_value');
|
||||
expect(result.tokens[2].value).toBe('effort:low');
|
||||
});
|
||||
});
|
||||
|
||||
describe('複合構文', () => {
|
||||
it('未完了タスク', () => {
|
||||
const result = tokenizeLine('(A) Call Mom +Family +PeaceLake due:2016-02-02');
|
||||
const tokenTypes = result.tokens.map(t => t.type);
|
||||
expect(tokenTypes).toContain('priority');
|
||||
expect(tokenTypes).toContain('text');
|
||||
expect(tokenTypes).toContain('project');
|
||||
expect(tokenTypes).toContain('key_value');
|
||||
|
||||
const projectTokens = result.tokens.filter(t => t.type === 'project');
|
||||
expect(projectTokens).toHaveLength(2);
|
||||
expect(projectTokens[0].value).toBe('+Family');
|
||||
expect(projectTokens[1].value).toBe('+PeaceLake');
|
||||
});
|
||||
|
||||
it('完了タスク', () => {
|
||||
const result = tokenizeLine('x 2011-03-02 2011-03-01 Review Tim\'s pull request +TodoTxtTouch @github due:2016-05-30');
|
||||
const tokenTypes = result.tokens.map(t => t.type);
|
||||
expect(tokenTypes).toContain('completion');
|
||||
expect(tokenTypes).toContain('completion_date');
|
||||
expect(tokenTypes).toContain('creation_date');
|
||||
expect(tokenTypes).toContain('project');
|
||||
expect(tokenTypes).toContain('context');
|
||||
expect(tokenTypes).toContain('key_value');
|
||||
|
||||
expect(result.tokens[0].type).toBe('completion');
|
||||
expect(result.tokens[1].type).toBe('completion_date');
|
||||
expect(result.tokens[2].type).toBe('creation_date');
|
||||
});
|
||||
|
||||
it('完全な構文', () => {
|
||||
const result = tokenizeLine('x (A) 2016-05-20 2016-04-30 measure space for +chapelShelving @chapel due:2016-05-30');
|
||||
expect(result.tokens.length).toBeGreaterThan(8);
|
||||
const tokenTypes = result.tokens.map(t => t.type);
|
||||
|
||||
expect(result.tokens[0].type).toBe('completion');
|
||||
expect(result.tokens[1].type).toBe('priority');
|
||||
expect(result.tokens[2].type).toBe('completion_date');
|
||||
expect(result.tokens[3].type).toBe('creation_date');
|
||||
|
||||
expect(tokenTypes).toContain('project');
|
||||
expect(tokenTypes).toContain('context');
|
||||
expect(tokenTypes).toContain('key_value');
|
||||
});
|
||||
|
||||
it('日本語タスク', () => {
|
||||
const result = tokenizeLine('x (A) 2024-01-15 2024-01-10 買い物リストを作成 +家事 @買い物 due:2024-01-20');
|
||||
expect(result.tokens.length).toBeGreaterThan(7);
|
||||
|
||||
expect(result.tokens[0].type).toBe('completion');
|
||||
expect(result.tokens[1].type).toBe('priority');
|
||||
expect(result.tokens[2].type).toBe('completion_date');
|
||||
expect(result.tokens[3].type).toBe('creation_date');
|
||||
|
||||
const tokenTypes = result.tokens.map(t => t.type);
|
||||
expect(tokenTypes).toContain('project');
|
||||
expect(tokenTypes).toContain('context');
|
||||
expect(tokenTypes).toContain('key_value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('エッジケース', () => {
|
||||
it('無効な優先度', () => {
|
||||
const result = tokenizeLine('(a) 小文字優先度');
|
||||
expect(result.tokens[0].type).toBe('text');
|
||||
});
|
||||
|
||||
it('空白なし', () => {
|
||||
const result = tokenizeLine('タスク+project@context');
|
||||
expect(result.tokens).toHaveLength(1);
|
||||
expect(result.tokens[0].type).toBe('text');
|
||||
});
|
||||
|
||||
it('不完全なkey:value', () => {
|
||||
const result = tokenizeLine('タスク key: value');
|
||||
expect(result.tokens.some(t => t.type === 'key_value')).toBe(false);
|
||||
});
|
||||
|
||||
it('未完了タスクで2つの日付(連続)', () => {
|
||||
const result = tokenizeLine('2011-03-02 2011-03-01 Review Tim\'s pull request +TodoTxtTouch @github due:2016-05-30');
|
||||
console.log('Debug - Tokens:', result.tokens.map(t => `${t.type}:${t.value}`));
|
||||
|
||||
// 最初の日付は creation_date として認識されるべき
|
||||
expect(result.tokens[0].type).toBe('creation_date');
|
||||
expect(result.tokens[0].value).toBe('2011-03-02');
|
||||
|
||||
// 2番目の日付はtextとして扱われるべき(未完了タスクでは完了日は存在しない)
|
||||
expect(result.tokens[1].type).toBe('text');
|
||||
expect(result.tokens[1].value).toBe('2011-03-01');
|
||||
});
|
||||
|
||||
it('未完了タスクで優先度付き2つの日付', () => {
|
||||
const result = tokenizeLine('(A) 2011-03-02 2011-03-01 Review Tim\'s pull request +TodoTxtTouch @github due:2016-05-30');
|
||||
console.log('Debug - Tokens:', result.tokens.map(t => `${t.type}:${t.value}`));
|
||||
|
||||
expect(result.tokens[0].type).toBe('priority');
|
||||
expect(result.tokens[0].value).toBe('(A)');
|
||||
|
||||
// 優先度の後の最初の日付は creation_date として認識されるべき
|
||||
expect(result.tokens[1].type).toBe('creation_date');
|
||||
expect(result.tokens[1].value).toBe('2011-03-02');
|
||||
|
||||
// 2番目の日付はtextとして扱われるべき
|
||||
expect(result.tokens[2].type).toBe('text');
|
||||
expect(result.tokens[2].value).toBe('2011-03-01');
|
||||
});
|
||||
|
||||
it('完了タスクで優先度記号が含まれる場合', () => {
|
||||
const result = tokenizeLine('x (A) 2025-01-02 hoge @hoge');
|
||||
console.log('Debug - Completed with priority tokens:', result.tokens.map(t => `${t.type}:${t.value}`));
|
||||
|
||||
expect(result.tokens[0].type).toBe('completion');
|
||||
expect(result.tokens[0].value).toBe('x');
|
||||
|
||||
// 完了タスクでも優先度はパースされる
|
||||
expect(result.tokens[1].type).toBe('priority');
|
||||
expect(result.tokens[1].value).toBe('(A)');
|
||||
|
||||
// 最初の日付は completion_date として認識されるべき
|
||||
expect(result.tokens[2].type).toBe('completion_date');
|
||||
expect(result.tokens[2].value).toBe('2025-01-02');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,80 +1,412 @@
|
|||
import { parsePriorityValue, parseProjectTag, parseContextTag, parseDueDate } from '../parser';
|
||||
|
||||
describe('TodoTxtSorter - 実装関数のテスト', () => {
|
||||
describe('parser functions', () => {
|
||||
describe('parsePriorityValue', () => {
|
||||
it('完了タスクは最大値を返す', () => {
|
||||
expect(parsePriorityValue('x Completed task')).toBe(Number.MAX_SAFE_INTEGER);
|
||||
expect(parsePriorityValue(' x Completed task with spaces')).toBe(Number.MAX_SAFE_INTEGER);
|
||||
describe('完了タスク', () => {
|
||||
it('基本的な完了タスク', () => {
|
||||
expect(parsePriorityValue('x Simple completed task')).toBe(Number.MAX_SAFE_INTEGER);
|
||||
expect(parsePriorityValue('x Completed task')).toBe(Number.MAX_SAFE_INTEGER);
|
||||
});
|
||||
|
||||
it('スペース付き完了タスク', () => {
|
||||
expect(parsePriorityValue(' x Completed task with spaces')).toBe(Number.MAX_SAFE_INTEGER);
|
||||
expect(parsePriorityValue(' x Completed task with many spaces')).toBe(Number.MAX_SAFE_INTEGER);
|
||||
expect(parsePriorityValue('\t\tx Completed with tabs')).toBe(Number.MAX_SAFE_INTEGER);
|
||||
});
|
||||
|
||||
it('日付付き完了タスク', () => {
|
||||
expect(parsePriorityValue('x 2025-01-16 Completed task with completion date')).toBe(Number.MAX_SAFE_INTEGER);
|
||||
expect(parsePriorityValue('x 2025-01-16 2025-01-15 Completed with both dates')).toBe(Number.MAX_SAFE_INTEGER);
|
||||
expect(parsePriorityValue('x 2025-01-16 Completed with double space')).toBe(Number.MAX_SAFE_INTEGER);
|
||||
});
|
||||
|
||||
it('優先度付き完了タスク(優先度は無視される)', () => {
|
||||
expect(parsePriorityValue('x (B) 2025-01-16 2025-01-15 Complete +🚀')).toBe(Number.MAX_SAFE_INTEGER);
|
||||
});
|
||||
});
|
||||
|
||||
it('優先度なしは最大値-1を返す', () => {
|
||||
expect(parsePriorityValue('Task without priority')).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
describe('優先度なし', () => {
|
||||
it('シンプルなタスク', () => {
|
||||
expect(parsePriorityValue('Simple task without any parameters')).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
expect(parsePriorityValue('Task without priority')).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
});
|
||||
|
||||
it('日付付きタスク(優先度なし)', () => {
|
||||
expect(parsePriorityValue('2025-01-15 Task with creation date only')).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
expect(parsePriorityValue('2025-01-15 Date only no description')).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
});
|
||||
|
||||
it('プロジェクト・コンテキスト付きタスク(優先度なし)', () => {
|
||||
expect(parsePriorityValue('Task with +project')).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
expect(parsePriorityValue('Task with @context')).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
expect(parsePriorityValue('Task with +project @context due:2025-12-31')).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
});
|
||||
|
||||
it('説明にカッコを含むが優先度ではないタスク', () => {
|
||||
expect(parsePriorityValue('Task with (parentheses) in description not priority')).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
expect(parsePriorityValue('Task with [brackets] and {braces} in description')).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
});
|
||||
});
|
||||
|
||||
it('アルファベット優先度の処理', () => {
|
||||
expect(parsePriorityValue('(A) High priority')).toBe(0);
|
||||
expect(parsePriorityValue('(B) Medium priority')).toBe(1);
|
||||
expect(parsePriorityValue('(C) Low priority')).toBe(2);
|
||||
expect(parsePriorityValue('(Z) Lowest priority')).toBe(25);
|
||||
describe('アルファベット優先度', () => {
|
||||
it('基本的なA-Z優先度', () => {
|
||||
expect(parsePriorityValue('(A) Priority A task')).toBe(0);
|
||||
expect(parsePriorityValue('(B) Priority B task')).toBe(1);
|
||||
expect(parsePriorityValue('(C) Priority C task')).toBe(2);
|
||||
expect(parsePriorityValue('(D) Priority D task')).toBe(3);
|
||||
expect(parsePriorityValue('(Z) Priority Z task')).toBe(25);
|
||||
});
|
||||
|
||||
it('日付付きアルファベット優先度', () => {
|
||||
expect(parsePriorityValue('(A) 2025-01-15 Priority A with creation date')).toBe(0);
|
||||
expect(parsePriorityValue('(B) 2025-01-15 Priority B with creation date')).toBe(1);
|
||||
});
|
||||
|
||||
it('スペースなしアルファベット優先度', () => {
|
||||
expect(parsePriorityValue('(A)No space after priority works as valid format')).toBe(0);
|
||||
});
|
||||
|
||||
it('余分なスペース付きアルファベット優先度', () => {
|
||||
expect(parsePriorityValue('(A) 2025-01-15 Double space after priority')).toBe(0);
|
||||
expect(parsePriorityValue(' (A) 2025-01-15 Task with leading spaces and params ')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('数値優先度の処理', () => {
|
||||
expect(parsePriorityValue('(1) First task')).toBe(1);
|
||||
expect(parsePriorityValue('(10) Tenth task')).toBe(10);
|
||||
expect(parsePriorityValue('(123) Task 123')).toBe(123);
|
||||
describe('数値優先度', () => {
|
||||
it('1桁の数値優先度', () => {
|
||||
expect(parsePriorityValue('(0) Zero priority task')).toBe(0);
|
||||
expect(parsePriorityValue('(1) Single digit priority task')).toBe(1);
|
||||
expect(parsePriorityValue('(9) Nine priority task')).toBe(9);
|
||||
});
|
||||
|
||||
it('2桁の数値優先度', () => {
|
||||
expect(parsePriorityValue('(10) Tenth task')).toBe(10);
|
||||
expect(parsePriorityValue('(99) Ninety-nine task')).toBe(99);
|
||||
});
|
||||
|
||||
it('3桁の数値優先度', () => {
|
||||
expect(parsePriorityValue('(123) Numeric priority task')).toBe(123);
|
||||
expect(parsePriorityValue('(999) Three digit priority task')).toBe(999);
|
||||
});
|
||||
|
||||
it('大きな数値優先度', () => {
|
||||
expect(parsePriorityValue('(999999) Very large numeric priority')).toBe(999999);
|
||||
expect(parsePriorityValue('(123456789) Very long numeric priority +project')).toBe(123456789);
|
||||
});
|
||||
|
||||
it('日付付き数値優先度', () => {
|
||||
expect(parsePriorityValue('(123) 2025-01-15 Numeric priority with creation date')).toBe(123);
|
||||
expect(parsePriorityValue('(123) 2025-01-13 Numeric priority all params +projectX @work due:2025-10-31')).toBe(123);
|
||||
});
|
||||
});
|
||||
|
||||
it('混合優先度の処理', () => {
|
||||
expect(parsePriorityValue('(1a) Mixed priority')).toBe(1);
|
||||
expect(parsePriorityValue('(10b) Another mixed')).toBe(10);
|
||||
expect(parsePriorityValue('(A1) Letter first')).toBe(0);
|
||||
describe('混合優先度', () => {
|
||||
it('数値+文字の混合優先度', () => {
|
||||
expect(parsePriorityValue('(1a) Mixed alphanumeric priority task')).toBe(1);
|
||||
expect(parsePriorityValue('(10b) Another mixed')).toBe(10);
|
||||
expect(parsePriorityValue('(123a) Large number with letter')).toBe(123);
|
||||
});
|
||||
|
||||
it('文字+数値の混合優先度', () => {
|
||||
expect(parsePriorityValue('(A1) Letter first mixed priority task')).toBe(0);
|
||||
expect(parsePriorityValue('(B2) Letter first')).toBe(1);
|
||||
expect(parsePriorityValue('(Z9) Letter Z with number')).toBe(25);
|
||||
});
|
||||
|
||||
it('複数文字の優先度', () => {
|
||||
expect(parsePriorityValue('(ABC) Multi-letter priority task')).toBe(0); // 最初の文字 'A' を使用
|
||||
expect(parsePriorityValue('(A1B2C3) Complex alphanumeric priority @context')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('エッジケースと無効な形式', () => {
|
||||
it('優先度のみで説明なし', () => {
|
||||
expect(parsePriorityValue('(A) Priority only no task description')).toBe(0);
|
||||
expect(parsePriorityValue('(A)')).toBe(0);
|
||||
});
|
||||
|
||||
it('特殊なタスク開始パターン', () => {
|
||||
expect(parsePriorityValue('+project Task starting with project tag')).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
expect(parsePriorityValue('@context Task starting with context tag')).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
expect(parsePriorityValue('due:2025-12-31 Task starting with key value')).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
});
|
||||
|
||||
it('空行と空白のみの行', () => {
|
||||
expect(parsePriorityValue('')).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
expect(parsePriorityValue(' ')).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
expect(parsePriorityValue('\t\t')).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseProjectTag', () => {
|
||||
it('プロジェクトタグを抽出', () => {
|
||||
expect(parseProjectTag('Task +project1')).toBe('project1');
|
||||
expect(parseProjectTag('Task +Project2 with more text')).toBe('project2');
|
||||
expect(parseProjectTag('+ProjectAtStart')).toBe('projectatstart');
|
||||
describe('基本的なプロジェクトタグ', () => {
|
||||
it('シンプルなプロジェクトタグ', () => {
|
||||
expect(parseProjectTag('Task with +project')).toBe('project');
|
||||
expect(parseProjectTag('Task +project1')).toBe('project1');
|
||||
expect(parseProjectTag('Task +Project2 with more text')).toBe('project2');
|
||||
});
|
||||
|
||||
it('タスク開始位置のプロジェクトタグ', () => {
|
||||
expect(parseProjectTag('+ProjectAtStart')).toBe('projectatstart');
|
||||
expect(parseProjectTag('+project Task starting with project tag')).toBe('project');
|
||||
});
|
||||
|
||||
it('複数のプロジェクトタグ(最初のものを返す)', () => {
|
||||
expect(parseProjectTag('Task +first +second')).toBe('first');
|
||||
expect(parseProjectTag('Task with multiple +project1 +project2 +project3')).toBe('project1');
|
||||
expect(parseProjectTag('Complex task with +project1 +project2 @home @work due:2025-12-31')).toBe('project1');
|
||||
});
|
||||
|
||||
it('大文字小文字の正規化', () => {
|
||||
expect(parseProjectTag('Task with +MultiWordProject')).toBe('multiwordproject');
|
||||
expect(parseProjectTag('Task with +CamelCaseProject')).toBe('camelcaseproject');
|
||||
expect(parseProjectTag('Task with +UPPERCASE')).toBe('uppercase');
|
||||
});
|
||||
});
|
||||
|
||||
it('プロジェクトタグがない場合のデフォルト値', () => {
|
||||
expect(parseProjectTag('Task without project')).toBe('zzzz');
|
||||
describe('特殊文字を含むプロジェクトタグ', () => {
|
||||
it('ハイフン・アンダースコアを含むタグ', () => {
|
||||
expect(parseProjectTag('Task with +pro-ject_name')).toBe('pro-ject_name');
|
||||
expect(parseProjectTag('Task with +snake_case_project')).toBe('snake_case_project');
|
||||
expect(parseProjectTag('Task with +project-name')).toBe('project-name');
|
||||
});
|
||||
|
||||
it('ドットを含むタグ', () => {
|
||||
expect(parseProjectTag('Task with +project.name')).toBe('project.name');
|
||||
expect(parseProjectTag('Task with dots in +project.name @context.place')).toBe('project.name');
|
||||
});
|
||||
|
||||
it('数字を含むタグ', () => {
|
||||
expect(parseProjectTag('Task with +project123')).toBe('project123');
|
||||
expect(parseProjectTag('+123project Project starting with numbers')).toBe('123project');
|
||||
});
|
||||
|
||||
it('特殊文字の組み合わせ', () => {
|
||||
expect(parseProjectTag('Task with +project!name')).toBe('project!name');
|
||||
expect(parseProjectTag('Task with +project/subproject')).toBe('project/subproject');
|
||||
expect(parseProjectTag('Task with +project|pipe')).toBe('project|pipe');
|
||||
});
|
||||
});
|
||||
|
||||
it('複数のプロジェクトタグがある場合は最初のものを返す', () => {
|
||||
expect(parseProjectTag('Task +first +second')).toBe('first');
|
||||
describe('Unicode・多言語プロジェクトタグ', () => {
|
||||
it('日本語のプロジェクトタグ', () => {
|
||||
expect(parseProjectTag('タスク +プロジェクト @コンテキスト')).toBe('プロジェクト');
|
||||
expect(parseProjectTag('Mixed +日本語project @中文context')).toBe('日本語project');
|
||||
});
|
||||
|
||||
it('絵文字を含むプロジェクトタグ', () => {
|
||||
expect(parseProjectTag('Task with emoji project +🚀rocket')).toBe('🚀rocket');
|
||||
expect(parseProjectTag('Task with +🚀 +📱 @🏠')).toBe('🚀');
|
||||
expect(parseProjectTag('Mixed emoji and text +emoji🎯project')).toBe('emoji🎯project');
|
||||
});
|
||||
|
||||
it('その他の言語', () => {
|
||||
expect(parseProjectTag('Task with +über')).toBe('über');
|
||||
expect(parseProjectTag('задача +проект @контекст')).toBe('проект');
|
||||
expect(parseProjectTag('任务 +项目 @上下文')).toBe('项目');
|
||||
});
|
||||
});
|
||||
|
||||
describe('プロジェクトタグなし', () => {
|
||||
it('プロジェクトタグを含まないタスク', () => {
|
||||
expect(parseProjectTag('Task without project')).toBe('zzzz');
|
||||
expect(parseProjectTag('Simple task without any parameters')).toBe('zzzz');
|
||||
expect(parseProjectTag('Task with @context but no project')).toBe('zzzz');
|
||||
});
|
||||
|
||||
it('エスケープされたプロジェクトタグ', () => {
|
||||
expect(parseProjectTag('Escaped special chars \\+not-project')).toBe('not-project');
|
||||
});
|
||||
});
|
||||
|
||||
describe('エッジケース', () => {
|
||||
it('プロジェクトタグのみ', () => {
|
||||
expect(parseProjectTag('+project Task with only project no description after')).toBe('project');
|
||||
expect(parseProjectTag('+project')).toBe('project');
|
||||
});
|
||||
|
||||
it('誤解を招く可能性のあるパターン', () => {
|
||||
expect(parseProjectTag('+project@email.com Might confuse parser')).toBe('project@email.com');
|
||||
expect(parseProjectTag('Task with quotes "+project"')).toBe('project"'); // 引用符も含めて認識される
|
||||
});
|
||||
|
||||
it('非常に長いプロジェクト名', () => {
|
||||
expect(parseProjectTag('Task with +ThisIsAVeryLongProjectNameThatMightCauseIssuesWithSomeSystemsThatHaveLengthLimitsOnIdentifiers'))
|
||||
.toBe('thisisaverylongprojectnamethatmightcauseissueswithsomesystemsthathavelengthlimitsonidentifiers');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseContextTag', () => {
|
||||
it('コンテキストタグを抽出', () => {
|
||||
expect(parseContextTag('Task @home')).toBe('home');
|
||||
expect(parseContextTag('Task @Work with more text')).toBe('work');
|
||||
expect(parseContextTag('@ContextAtStart')).toBe('contextatstart');
|
||||
describe('基本的なコンテキストタグ', () => {
|
||||
it('シンプルなコンテキストタグ', () => {
|
||||
expect(parseContextTag('Task with @context')).toBe('context');
|
||||
expect(parseContextTag('Task @home')).toBe('home');
|
||||
expect(parseContextTag('Task @Work with more text')).toBe('work');
|
||||
});
|
||||
|
||||
it('タスク開始位置のコンテキストタグ', () => {
|
||||
expect(parseContextTag('@ContextAtStart')).toBe('contextatstart');
|
||||
expect(parseContextTag('@context Task starting with context tag')).toBe('context');
|
||||
});
|
||||
|
||||
it('複数のコンテキストタグ(最初のものを返す)', () => {
|
||||
expect(parseContextTag('Task @first @second')).toBe('first');
|
||||
expect(parseContextTag('Task with @home @phone multiple contexts')).toBe('home');
|
||||
expect(parseContextTag('Multiple same contexts @home @home @home')).toBe('home');
|
||||
});
|
||||
|
||||
it('大文字小文字の正規化', () => {
|
||||
expect(parseContextTag('Task with @MultiWordContext')).toBe('multiwordcontext');
|
||||
expect(parseContextTag('Task with @PascalCaseContext')).toBe('pascalcasecontext');
|
||||
expect(parseContextTag('Task with @UPPERCASE')).toBe('uppercase');
|
||||
});
|
||||
});
|
||||
|
||||
it('コンテキストタグがない場合のデフォルト値', () => {
|
||||
expect(parseContextTag('Task without context')).toBe('zzzz');
|
||||
describe('特殊文字を含むコンテキストタグ', () => {
|
||||
it('ハイフン・アンダースコアを含むタグ', () => {
|
||||
expect(parseContextTag('Task with @con.text_name')).toBe('con.text_name');
|
||||
expect(parseContextTag('Task with @kebab-case-context')).toBe('kebab-case-context');
|
||||
expect(parseContextTag('Task with @context_name')).toBe('context_name');
|
||||
});
|
||||
|
||||
it('数字を含むタグ', () => {
|
||||
expect(parseContextTag('Task with @context456')).toBe('context456');
|
||||
expect(parseContextTag('@123context Context starting with numbers')).toBe('123context');
|
||||
});
|
||||
|
||||
it('特殊文字の組み合わせ', () => {
|
||||
expect(parseContextTag('Task with @context#tag')).toBe('context#tag');
|
||||
expect(parseContextTag('Task with @context\\subcontext')).toBe('context\\subcontext');
|
||||
expect(parseContextTag('Task with @context&and')).toBe('context&and');
|
||||
});
|
||||
});
|
||||
|
||||
it('複数のコンテキストタグがある場合は最初のものを返す', () => {
|
||||
expect(parseContextTag('Task @first @second')).toBe('first');
|
||||
describe('Unicode・多言語コンテキストタグ', () => {
|
||||
it('日本語のコンテキストタグ', () => {
|
||||
expect(parseContextTag('タスク +プロジェクト @コンテキスト')).toBe('コンテキスト');
|
||||
expect(parseContextTag('Mixed +日本語project @中文context')).toBe('中文context');
|
||||
});
|
||||
|
||||
it('絵文字を含むコンテキストタグ', () => {
|
||||
expect(parseContextTag('Task with emoji context @🏠home')).toBe('🏠home');
|
||||
expect(parseContextTag('Task with @🏠 @💼 @🛒')).toBe('🏠');
|
||||
expect(parseContextTag('Mixed @context🔥fire')).toBe('context🔥fire');
|
||||
});
|
||||
|
||||
it('その他の言語', () => {
|
||||
expect(parseContextTag('Task with @café')).toBe('café');
|
||||
expect(parseContextTag('작업 +프로젝트 @컨텍스트')).toBe('컨텍스트');
|
||||
expect(parseContextTag('משימה +פרויקט @הקשר')).toBe('הקשר');
|
||||
});
|
||||
});
|
||||
|
||||
describe('コンテキストタグなし', () => {
|
||||
it('コンテキストタグを含まないタスク', () => {
|
||||
expect(parseContextTag('Task without context')).toBe('zzzz');
|
||||
expect(parseContextTag('Simple task without any parameters')).toBe('zzzz');
|
||||
expect(parseContextTag('Task with +project but no context')).toBe('zzzz');
|
||||
});
|
||||
|
||||
it('メールアドレスの@は無視', () => {
|
||||
expect(parseContextTag('Task with email user@example.com might have @ but not context')).toBe('example.com');
|
||||
});
|
||||
|
||||
it('エスケープされたコンテキストタグ', () => {
|
||||
expect(parseContextTag('Escaped special chars \\@not-context')).toBe('not-context');
|
||||
});
|
||||
});
|
||||
|
||||
describe('エッジケース', () => {
|
||||
it('コンテキストタグのみ', () => {
|
||||
expect(parseContextTag('@context Task with only context no description after')).toBe('context');
|
||||
expect(parseContextTag('@context')).toBe('context');
|
||||
});
|
||||
|
||||
it('複雑な絵文字', () => {
|
||||
expect(parseContextTag('Task with @👨💻dev')).toBe('👨💻dev');
|
||||
expect(parseContextTag('Task with @🏳️🌈pride')).toBe('🏳️🌈pride');
|
||||
expect(parseContextTag('Task with @👨⚕️doctor')).toBe('👨⚕️doctor');
|
||||
});
|
||||
|
||||
it('順序が重要なケース', () => {
|
||||
expect(parseContextTag('@context+project Wrong order but both present')).toBe('context+project');
|
||||
expect(parseContextTag('due:2025-12-31 @context Order matters')).toBe('context');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseDueDate', () => {
|
||||
it('期日を抽出', () => {
|
||||
expect(parseDueDate('Task due:2023-12-31')).toBe('2023-12-31');
|
||||
expect(parseDueDate('due:2024-01-01 Task with date')).toBe('2024-01-01');
|
||||
describe('基本的な期日', () => {
|
||||
it('標準的なdue:日付形式', () => {
|
||||
expect(parseDueDate('Task due:2023-12-31')).toBe('2023-12-31');
|
||||
expect(parseDueDate('Task with key:value pair due:2025-12-31')).toBe('2025-12-31');
|
||||
expect(parseDueDate('due:2024-01-01 Task with date')).toBe('2024-01-01');
|
||||
});
|
||||
|
||||
it('タスク開始位置の期日', () => {
|
||||
expect(parseDueDate('due:2025-12-31 Task starting with key value')).toBe('2025-12-31');
|
||||
});
|
||||
|
||||
it('複数のキー値ペア', () => {
|
||||
expect(parseDueDate('Task with multiple pairs due:2025-12-31 rec:1w')).toBe('2025-12-31');
|
||||
expect(parseDueDate('Task with many key:value pairs a:1 b:2 due:2025-12-31 c:3')).toBe('2025-12-31');
|
||||
});
|
||||
});
|
||||
|
||||
it('期日なしの場合のデフォルト値(未来)', () => {
|
||||
expect(parseDueDate('Task without due date')).toBe('9999-99-99');
|
||||
describe('時刻付き期日', () => {
|
||||
it('ISO形式の時刻付き期日(日付部分のみ抽出)', () => {
|
||||
expect(parseDueDate('Task with time due:2025-12-31T14:30:00')).toBe('2025-12-31');
|
||||
});
|
||||
});
|
||||
|
||||
it('期日なしの場合のデフォルト値(過去)', () => {
|
||||
expect(parseDueDate('Task without due date', false)).toBe('0000-00-00');
|
||||
describe('期日なし', () => {
|
||||
it('期日なし(デフォルト:未来)', () => {
|
||||
expect(parseDueDate('Task without due date')).toBe('9999-99-99');
|
||||
expect(parseDueDate('Simple task without any parameters')).toBe('9999-99-99');
|
||||
expect(parseDueDate('Task with +project @context but no due')).toBe('9999-99-99');
|
||||
});
|
||||
|
||||
it('期日なし(過去を指定)', () => {
|
||||
expect(parseDueDate('Task without due date', false)).toBe('0000-00-00');
|
||||
expect(parseDueDate('Simple task', false)).toBe('0000-00-00');
|
||||
});
|
||||
});
|
||||
|
||||
describe('他のキー値ペア', () => {
|
||||
it('recurrenceキー', () => {
|
||||
expect(parseDueDate('rec:+1w Recurrence with plus sign')).toBe('9999-99-99');
|
||||
expect(parseDueDate('Task with due:2025-12-31 rec:1m')).toBe('2025-12-31');
|
||||
});
|
||||
|
||||
it('カスタムキー', () => {
|
||||
expect(parseDueDate('Custom key custom:value in task')).toBe('9999-99-99');
|
||||
expect(parseDueDate('Multiple custom keys foo:bar baz:qux in task')).toBe('9999-99-99');
|
||||
expect(parseDueDate('t:2025-01-15 Short key for date')).toBe('9999-99-99'); // t:はdue:ではない
|
||||
});
|
||||
});
|
||||
|
||||
describe('エッジケース', () => {
|
||||
it('期日のみ', () => {
|
||||
expect(parseDueDate('due:2025-12-31 Task with only key:value no description after')).toBe('2025-12-31');
|
||||
expect(parseDueDate('due:2025-12-31')).toBe('2025-12-31');
|
||||
});
|
||||
|
||||
it('コロンを含む値', () => {
|
||||
expect(parseDueDate('Task with key:value:with:colons might break parser')).toBe('9999-99-99');
|
||||
});
|
||||
|
||||
it('順序の影響', () => {
|
||||
expect(parseDueDate('due:2025-12-31 @context Order matters for key:value')).toBe('2025-12-31');
|
||||
expect(parseDueDate('@context due:2025-12-31 Different order same elements')).toBe('2025-12-31');
|
||||
});
|
||||
|
||||
it('複雑なタスクでの期日', () => {
|
||||
expect(parseDueDate('(A) 2025-01-15 All parameters +project @context due:2025-12-31')).toBe('2025-12-31');
|
||||
expect(parseDueDate('Extremely long task description that goes on and on and on with many words to test how the parser handles very long lines that might exceed buffer limits in some implementations and includes +project @context due:2025-12-31 and continues even further with more text'))
|
||||
.toBe('2025-12-31');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
162
src/parser.ts
162
src/parser.ts
|
|
@ -1,5 +1,167 @@
|
|||
// Todo.txt形式の純粋なパーサー関数(Obsidianに依存しない)
|
||||
|
||||
export interface Token {
|
||||
type: 'completion' | 'priority' | 'completion_date' | 'creation_date' | 'project' | 'context' | 'key_value' | 'text';
|
||||
value: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface ParsedLine {
|
||||
tokens: Token[];
|
||||
raw: string;
|
||||
}
|
||||
|
||||
export function tokenizeLine(line: string): ParsedLine {
|
||||
const tokens: Token[] = [];
|
||||
let pos = 0;
|
||||
const length = line.length;
|
||||
let isCompleted = false;
|
||||
|
||||
// 先頭の空白をスキップ
|
||||
while (pos < length && /\s/.test(line[pos])) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
// 完了フラグ(x)のチェック
|
||||
if (pos < length && line[pos] === 'x' && (pos + 1 >= length || /\s/.test(line[pos + 1]))) {
|
||||
tokens.push({
|
||||
type: 'completion',
|
||||
value: 'x',
|
||||
start: pos,
|
||||
end: pos + 1
|
||||
});
|
||||
pos++;
|
||||
isCompleted = true;
|
||||
|
||||
// 完了フラグ後の空白をスキップ
|
||||
while (pos < length && /\s/.test(line[pos])) {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
// 優先度のチェック(完了フラグの後)
|
||||
if (pos < length && line[pos] === '(') {
|
||||
const priorityMatch = line.substring(pos).match(/^\(([A-Z0-9][A-Z0-9a-z0-9]*)\)/);
|
||||
if (priorityMatch) {
|
||||
tokens.push({
|
||||
type: 'priority',
|
||||
value: priorityMatch[0],
|
||||
start: pos,
|
||||
end: pos + priorityMatch[0].length
|
||||
});
|
||||
pos += priorityMatch[0].length;
|
||||
|
||||
// 優先度後の空白をスキップ
|
||||
while (pos < length && /\s/.test(line[pos])) {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 完了日のチェック(完了タスクの場合のみ、優先度の後)
|
||||
if (isCompleted) {
|
||||
const completionDateMatch = line.substring(pos).match(/^(\d{4}-\d{2}-\d{2})/);
|
||||
if (completionDateMatch) {
|
||||
tokens.push({
|
||||
type: 'completion_date',
|
||||
value: completionDateMatch[1],
|
||||
start: pos,
|
||||
end: pos + completionDateMatch[1].length
|
||||
});
|
||||
pos += completionDateMatch[1].length;
|
||||
|
||||
// 完了日後の空白をスキップ
|
||||
while (pos < length && /\s/.test(line[pos])) {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 作成日のチェック(優先度/完了日の後)
|
||||
const creationDateMatch = line.substring(pos).match(/^(\d{4}-\d{2}-\d{2})/);
|
||||
if (creationDateMatch) {
|
||||
tokens.push({
|
||||
type: 'creation_date',
|
||||
value: creationDateMatch[1],
|
||||
start: pos,
|
||||
end: pos + creationDateMatch[1].length
|
||||
});
|
||||
pos += creationDateMatch[1].length;
|
||||
|
||||
// 作成日後の空白をスキップ
|
||||
while (pos < length && /\s/.test(line[pos])) {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
// 残りの部分(タスク本文)を処理
|
||||
while (pos < length) {
|
||||
const remaining = line.substring(pos);
|
||||
|
||||
// プロジェクトタグのチェック
|
||||
const projectMatch = remaining.match(/^(\+[^\s]+)/);
|
||||
if (projectMatch) {
|
||||
tokens.push({
|
||||
type: 'project',
|
||||
value: projectMatch[1],
|
||||
start: pos,
|
||||
end: pos + projectMatch[1].length
|
||||
});
|
||||
pos += projectMatch[1].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
// コンテキストタグのチェック
|
||||
const contextMatch = remaining.match(/^(@[^\s]+)/);
|
||||
if (contextMatch) {
|
||||
tokens.push({
|
||||
type: 'context',
|
||||
value: contextMatch[1],
|
||||
start: pos,
|
||||
end: pos + contextMatch[1].length
|
||||
});
|
||||
pos += contextMatch[1].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
// key:value形式のチェック
|
||||
const keyValueMatch = remaining.match(/^([a-zA-Z_][a-zA-Z0-9_]*:[^\s]+)/);
|
||||
if (keyValueMatch) {
|
||||
tokens.push({
|
||||
type: 'key_value',
|
||||
value: keyValueMatch[1],
|
||||
start: pos,
|
||||
end: pos + keyValueMatch[1].length
|
||||
});
|
||||
pos += keyValueMatch[1].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 通常のテキストまたは空白
|
||||
const textMatch = remaining.match(/^([^\s]+|\s+)/);
|
||||
if (textMatch) {
|
||||
if (textMatch[1].trim()) { // 空白でない場合のみテキストトークンとして追加
|
||||
tokens.push({
|
||||
type: 'text',
|
||||
value: textMatch[1],
|
||||
start: pos,
|
||||
end: pos + textMatch[1].length
|
||||
});
|
||||
}
|
||||
pos += textMatch[1].length;
|
||||
} else {
|
||||
// フォールバック
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tokens,
|
||||
raw: line
|
||||
};
|
||||
}
|
||||
|
||||
export function parsePriorityValue(line: string): number {
|
||||
if (line.trim().startsWith('x ')) {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ export interface TodoTxtSettings {
|
|||
|
||||
highlightDueDate: boolean;
|
||||
dueDateColor: string;
|
||||
|
||||
highlightCompletionDate: boolean;
|
||||
completionDateColor: string;
|
||||
|
||||
highlightCreationDate: boolean;
|
||||
creationDateColor: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: TodoTxtSettings = {
|
||||
|
|
@ -49,7 +55,13 @@ export const DEFAULT_SETTINGS: TodoTxtSettings = {
|
|||
priorityColor: "#E91E63",
|
||||
|
||||
highlightDueDate: true,
|
||||
dueDateColor: "#607D8B"
|
||||
dueDateColor: "#607D8B",
|
||||
|
||||
highlightCompletionDate: true,
|
||||
completionDateColor: "#FF9800",
|
||||
|
||||
highlightCreationDate: true,
|
||||
creationDateColor: "#9C27B0"
|
||||
}
|
||||
|
||||
export class TodoTxtSettingTab extends PluginSettingTab {
|
||||
|
|
@ -224,6 +236,22 @@ export class TodoTxtSettingTab extends PluginSettingTab {
|
|||
'highlightDueDate',
|
||||
'dueDateColor'
|
||||
);
|
||||
|
||||
this.createHighlightSetting(
|
||||
containerEl,
|
||||
'Highlight completion dates',
|
||||
'Apply color to completion dates in completed tasks ("x 2011-03-02")',
|
||||
'highlightCompletionDate',
|
||||
'completionDateColor'
|
||||
);
|
||||
|
||||
this.createHighlightSetting(
|
||||
containerEl,
|
||||
'Highlight creation dates',
|
||||
'Apply color to creation dates ("2011-03-01 Task" or "x date1 2011-03-01")',
|
||||
'highlightCreationDate',
|
||||
'creationDateColor'
|
||||
);
|
||||
|
||||
new Setting(containerEl).setHeading().setName('Sort settings');
|
||||
|
||||
|
|
|
|||
125
src/syntax.ts
125
src/syntax.ts
|
|
@ -2,12 +2,8 @@ import { Decoration, DecorationSet, EditorView as CMEditorView, ViewPlugin, View
|
|||
import { RangeSetBuilder } from '@codemirror/state';
|
||||
import { App, TFile } from 'obsidian';
|
||||
import { TodoTxtSettings } from './settings';
|
||||
import { tokenizeLine, Token } from './parser';
|
||||
|
||||
// 正規表現をテスト用にexport
|
||||
export const projectRegex = /\+[^\s]+/g;
|
||||
export const contextRegex = /@[^\s]+/g;
|
||||
export const priorityRegex = /^(\s*(?:x\s+)?\s*)\(([A-Z0-9][A-Z0-9a-z0-9]*)\)/;
|
||||
export const dueDateRegex = /due:[^\s]+/g;
|
||||
|
||||
export function createTodoTxtExtension(app: App, isTodoTxtFile: (path: string) => boolean, getSettings: () => TodoTxtSettings) {
|
||||
// 正規表現はモジュールレベルで定義されたものを使用
|
||||
|
|
@ -53,9 +49,13 @@ export function createTodoTxtExtension(app: App, isTodoTxtFile: (path: string) =
|
|||
for (let i = viewport.from; i <= viewport.to;) {
|
||||
const line = doc.lineAt(i);
|
||||
const lineText = line.text;
|
||||
const trimmedText = lineText.trim();
|
||||
|
||||
if (settings.highlightCompletedTask && trimmedText.startsWith('x ')) {
|
||||
// 新しいパーサーを使用してトークン化
|
||||
const parsedLine = tokenizeLine(lineText);
|
||||
|
||||
// 完了タスクの行全体ハイライト
|
||||
const hasCompletion = parsedLine.tokens.some(token => token.type === 'completion');
|
||||
if (settings.highlightCompletedTask && hasCompletion) {
|
||||
const deco = Decoration.line({
|
||||
attributes: { class: "todo-txt-mode-completed" }
|
||||
});
|
||||
|
|
@ -66,75 +66,58 @@ export function createTodoTxtExtension(app: App, isTodoTxtFile: (path: string) =
|
|||
});
|
||||
}
|
||||
|
||||
if (settings.highlightProject) {
|
||||
projectRegex.lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
// トークンごとのハイライト処理
|
||||
for (const token of parsedLine.tokens) {
|
||||
let className: string | null = null;
|
||||
let shouldHighlight = false;
|
||||
|
||||
while ((match = projectRegex.exec(lineText)) !== null) {
|
||||
const start = line.from + match.index;
|
||||
const end = start + match[0].length;
|
||||
const deco = Decoration.mark({
|
||||
attributes: { class: "todo-txt-mode-project" }
|
||||
});
|
||||
decorations.push({
|
||||
from: start,
|
||||
to: end,
|
||||
decoration: deco
|
||||
});
|
||||
switch (token.type) {
|
||||
case 'project':
|
||||
if (settings.highlightProject) {
|
||||
className = "todo-txt-mode-project";
|
||||
shouldHighlight = true;
|
||||
}
|
||||
break;
|
||||
case 'context':
|
||||
if (settings.highlightContext) {
|
||||
className = "todo-txt-mode-context";
|
||||
shouldHighlight = true;
|
||||
}
|
||||
break;
|
||||
case 'priority':
|
||||
if (settings.highlightPriority) {
|
||||
className = "todo-txt-mode-priority";
|
||||
shouldHighlight = true;
|
||||
}
|
||||
break;
|
||||
case 'key_value':
|
||||
// due: で始まるkey_valueをdue dateとして扱う
|
||||
if (settings.highlightDueDate && token.value.startsWith('due:')) {
|
||||
className = "todo-txt-mode-due-date";
|
||||
shouldHighlight = true;
|
||||
}
|
||||
break;
|
||||
case 'completion_date':
|
||||
if (settings.highlightCompletionDate) {
|
||||
className = "todo-txt-mode-completion-date";
|
||||
shouldHighlight = true;
|
||||
}
|
||||
break;
|
||||
case 'creation_date':
|
||||
if (settings.highlightCreationDate) {
|
||||
className = "todo-txt-mode-creation-date";
|
||||
shouldHighlight = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.highlightContext) {
|
||||
contextRegex.lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = contextRegex.exec(lineText)) !== null) {
|
||||
const start = line.from + match.index;
|
||||
const end = start + match[0].length;
|
||||
if (shouldHighlight && className) {
|
||||
const deco = Decoration.mark({
|
||||
attributes: { class: "todo-txt-mode-context" }
|
||||
attributes: { class: className }
|
||||
});
|
||||
decorations.push({
|
||||
from: start,
|
||||
to: end,
|
||||
decoration: deco
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.highlightPriority) {
|
||||
const priorityMatch = priorityRegex.exec(lineText);
|
||||
if (priorityMatch) {
|
||||
const prefixLength = priorityMatch[1].length;
|
||||
const priorityText = priorityMatch[2];
|
||||
const start = line.from + prefixLength;
|
||||
const end = start + priorityText.length + 2;
|
||||
|
||||
const deco = Decoration.mark({
|
||||
attributes: { class: "todo-txt-mode-priority" }
|
||||
});
|
||||
|
||||
decorations.push({
|
||||
from: start,
|
||||
to: end,
|
||||
decoration: deco
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.highlightDueDate) {
|
||||
dueDateRegex.lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = dueDateRegex.exec(lineText)) !== null) {
|
||||
const start = line.from + match.index;
|
||||
const end = start + match[0].length;
|
||||
const deco = Decoration.mark({
|
||||
attributes: { class: "todo-txt-mode-due-date" }
|
||||
});
|
||||
decorations.push({
|
||||
from: start,
|
||||
to: end,
|
||||
from: line.from + token.start,
|
||||
to: line.from + token.end,
|
||||
decoration: deco
|
||||
});
|
||||
}
|
||||
|
|
@ -159,4 +142,4 @@ export function createTodoTxtExtension(app: App, isTodoTxtFile: (path: string) =
|
|||
}, {
|
||||
decorations: v => v.decorations
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
34
styles.css
34
styles.css
|
|
@ -36,6 +36,20 @@ settings:
|
|||
type: variable-color
|
||||
format: hex
|
||||
default: '#808080'
|
||||
|
||||
- id: todo-txt-mode-completion-date-color
|
||||
title: Completion Date Color
|
||||
description: Color for completion dates in completed tasks (x 2011-03-02)
|
||||
type: variable-color
|
||||
format: hex
|
||||
default: '#FF9800'
|
||||
|
||||
- id: todo-txt-mode-creation-date-color
|
||||
title: Creation Date Color
|
||||
description: Color for creation dates (2011-03-01 Task or x date1 2011-03-01 Task)
|
||||
type: variable-color
|
||||
format: hex
|
||||
default: '#9C27B0'
|
||||
*/
|
||||
|
||||
/* Todo.txt Mode スタイル定義 */
|
||||
|
|
@ -73,6 +87,26 @@ settings:
|
|||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* 完了日(x 2011-03-02) */
|
||||
.todo-txt-mode-completion-date {
|
||||
color: var(--todo-txt-mode-completion-date-color, #FF9800);
|
||||
font-size: 90%;
|
||||
font-weight: 600;
|
||||
background-color: rgba(255, 152, 0, 0.1);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* 作成日(2011-03-01 Task or x date1 2011-03-01 Task) */
|
||||
.todo-txt-mode-creation-date {
|
||||
color: var(--todo-txt-mode-creation-date-color, #9C27B0);
|
||||
font-size: 90%;
|
||||
font-weight: 600;
|
||||
background-color: rgba(156, 39, 176, 0.1);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* ファイル識別のためのマーカー */
|
||||
.todo-txt-mode-file .cm-line {
|
||||
font-family: var(--font-monospace);
|
||||
|
|
|
|||
165
todo.txt
Normal file
165
todo.txt
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
Simple task without any parameters
|
||||
(A) Priority A task
|
||||
(B) Priority B task
|
||||
(C) Priority C task
|
||||
(Z) Priority Z task
|
||||
(123) Numeric priority task
|
||||
(1) Single digit priority task
|
||||
(999) Three digit priority task
|
||||
2025-01-15 Task with creation date only
|
||||
(A) 2025-01-15 Priority A with creation date
|
||||
(B) 2025-01-15 Priority B with creation date
|
||||
(123) 2025-01-15 Numeric priority with creation date
|
||||
Task with +project
|
||||
Task with +MultiWordProject
|
||||
Task with multiple +project1 +project2 +project3
|
||||
Task with @context
|
||||
Task with @home @phone multiple contexts
|
||||
Task with @MultiWordContext
|
||||
Task with key:value pair due:2025-12-31
|
||||
Task with multiple pairs due:2025-12-31 rec:1w
|
||||
2025-01-15 Creation date with +project
|
||||
2025-01-15 Creation date with @context
|
||||
2025-01-15 Creation date with due:2025-12-31
|
||||
(A) Task with priority and +project
|
||||
(A) Task with priority and @context
|
||||
(A) Task with priority and due:2025-12-31
|
||||
(A) 2025-01-15 Priority, creation date, and +project
|
||||
(A) 2025-01-15 Priority, creation date, and @context
|
||||
(A) 2025-01-15 Priority, creation date, and due:2025-12-31
|
||||
Task with +project @context
|
||||
Task with +project due:2025-12-31
|
||||
Task with @context due:2025-12-31
|
||||
Task with +project @context due:2025-12-31
|
||||
2025-01-15 Task with date +project @context
|
||||
2025-01-15 Task with date +project due:2025-12-31
|
||||
2025-01-15 Task with date @context due:2025-12-31
|
||||
2025-01-15 Task with date +project @context due:2025-12-31
|
||||
(A) Task with priority +project @context
|
||||
(A) Task with priority +project due:2025-12-31
|
||||
(A) Task with priority @context due:2025-12-31
|
||||
(A) Task with priority +project @context due:2025-12-31
|
||||
(A) 2025-01-15 All parameters +project @context due:2025-12-31
|
||||
(B) 2025-01-14 Different priority and date +project2 @home due:2025-11-30
|
||||
(123) 2025-01-13 Numeric priority all params +projectX @work due:2025-10-31
|
||||
x Simple completed task
|
||||
x 2025-01-16 Completed task with completion date
|
||||
x 2025-01-16 2025-01-15 Completed task with completion and creation dates
|
||||
x 2025-01-16 Completed task with +project
|
||||
x 2025-01-16 Completed task with @context
|
||||
x 2025-01-16 Completed task with due:2025-12-31
|
||||
x 2025-01-16 2025-01-15 Completed with dates and +project
|
||||
x 2025-01-16 2025-01-15 Completed with dates and @context
|
||||
x 2025-01-16 2025-01-15 Completed with dates and due:2025-12-31
|
||||
x 2025-01-16 Completed with +project @context
|
||||
x 2025-01-16 Completed with +project due:2025-12-31
|
||||
x 2025-01-16 Completed with @context due:2025-12-31
|
||||
x 2025-01-16 Completed with +project @context due:2025-12-31
|
||||
x 2025-01-16 2025-01-15 Completed all params +project @context due:2025-12-31
|
||||
+project Task starting with project tag
|
||||
@context Task starting with context tag
|
||||
due:2025-12-31 Task starting with key value
|
||||
Task with special chars in +project-name @context_name
|
||||
Task with numbers in +project123 @context456
|
||||
Task with dots in +project.name @context.place
|
||||
Complex task with +project1 +project2 @home @work due:2025-12-31 rec:1m pri:H
|
||||
(1a) Mixed alphanumeric priority task
|
||||
(A1) Letter first mixed priority task
|
||||
Task with leading spaces
|
||||
Task with trailing spaces
|
||||
(A) 2025-01-15 Task with leading spaces and params
|
||||
Task with time due:2025-12-31T14:30:00
|
||||
Multiple same contexts @home @home @home
|
||||
Multiple same projects +projectA +projectA +projectA
|
||||
rec:+1w Recurrence with plus sign
|
||||
Custom key custom:value in task
|
||||
Multiple custom keys foo:bar baz:qux in task
|
||||
Task with URL https://example.com/path
|
||||
Task with email user@example.com might have @ but not context
|
||||
(0) Zero priority task
|
||||
(ABC) Multi-letter priority task
|
||||
(999999) Very large numeric priority
|
||||
Task with unicode émojis 🎯 and special chars
|
||||
@context+project Wrong order but both present
|
||||
t:2025-01-15 Short key for date
|
||||
p:1 Short key for priority
|
||||
x 2025-01-16 Completed with double space
|
||||
x 2025-01-16 2025-01-15 Double space between dates
|
||||
(A) 2025-01-15 Double space after priority
|
||||
Task with +CamelCaseProject @PascalCaseContext
|
||||
Task with +snake_case_project @kebab-case-context
|
||||
+123project Project starting with numbers
|
||||
@123context Context starting with numbers
|
||||
Task with key:value:with:colons might break parser
|
||||
Escaped special chars \+not-project \@not-context
|
||||
Task with +über @café unicode in tags
|
||||
+project@email.com Might confuse parser
|
||||
Task with (parentheses) in description not priority
|
||||
Task with [brackets] and {braces} in description
|
||||
due:2025-12-31 @context Order matters for key:value
|
||||
@context due:2025-12-31 Different order same elements
|
||||
Long task description that contains many words and might wrap in some views but should still parse correctly with +project @context due:2025-12-31
|
||||
Task with emoji project +🚀rocket +📱mobile +🎯target
|
||||
Task with emoji context @🏠home @💼work @🛒shopping
|
||||
Task with emoji in description 🎉 Complete the feature 🚀 +dev @office
|
||||
(A) 2025-01-15 Priority task with emojis 🔥 +🚀deploy @🏠home due:2025-12-31
|
||||
x 2025-01-16 Completed emoji task 🎉 +🚀project @🏠home
|
||||
Mixed emoji and text +emoji🎯project @context🔥fire
|
||||
Multiple emojis in one tag +🚀🎯🔥 @🏠💼🛒
|
||||
Emoji with skin tone +👋🏻project @👨💻dev
|
||||
Complex emoji +👨👩👧👦family @🏳️🌈pride
|
||||
Zero-width joiner emoji +🧑🚀astronaut @👨⚕️doctor
|
||||
Task with Japanese text タスク +プロジェクト @コンテキスト
|
||||
Task with Chinese 任务 +项目 @上下文
|
||||
Task with Korean 작업 +프로젝트 @컨텍스트
|
||||
Task with Arabic مهمة +مشروع @سياق
|
||||
Task with Hebrew משימה +פרויקט @הקשר
|
||||
Task with Cyrillic задача +проект @контекст
|
||||
Task with mixed scripts +日本語project @中文context
|
||||
RTL text עברית task with +project @context due:2025-12-31
|
||||
Combining marks task̃ +projec̈t @conte͂xt
|
||||
Task with !@#$%^&*() special chars in description
|
||||
Task with +project!name @context#tag
|
||||
Task with +pro-ject_name @con.text_name
|
||||
Task with +project/subproject @context\subcontext
|
||||
Task with +project|pipe @context&and
|
||||
Task with quotes "quoted +project" and 'single @quotes'
|
||||
Task with backticks `code +example` @context
|
||||
Task with brackets [not a link] +project
|
||||
Task with angle brackets <not html> @context
|
||||
Task with curly braces {not json} +project
|
||||
Task with backslash\\ in middle
|
||||
Task with zerowidthspace between words
|
||||
Task with softhyphen in word
|
||||
(A) 2025-01-15 Task +project1 +project2 +project3 @context1 @context2 @context3 due:2025-12-31 rec:1w pri:H custom:value foo:bar
|
||||
x (B) 2025-01-16 2025-01-15 Complete +🚀 +📱 @🏠 @💼 due:2025-12-31 done:2025-01-16
|
||||
(123456789) Very long numeric priority +project
|
||||
(A1B2C3) Complex alphanumeric priority @context
|
||||
Multiple dates 2025-01-15 2025-01-16 2025-01-17 in task
|
||||
Task with very long project name +ThisIsAVeryLongProjectNameThatMightCauseIssuesWithSomeSystemsThatHaveLengthLimitsOnIdentifiers
|
||||
Task with many projects +p1 +p2 +p3 +p4 +p5 +p6 +p7 +p8 +p9 +p10 +p11 +p12 +p13 +p14 +p15 +p16 +p17 +p18 +p19 +p20
|
||||
Task with many contexts @c1 @c2 @c3 @c4 @c5 @c6 @c7 @c8 @c9 @c10 @c11 @c12 @c13 @c14 @c15 @c16 @c17 @c18 @c19 @c20
|
||||
Task with many key:value pairs a:1 b:2 c:3 d:4 e:5 f:6 g:7 h:8 i:9 j:10 k:11 l:12 m:13 n:14 o:15
|
||||
Extremely long task description that goes on and on and on with many words to test how the parser handles very long lines that might exceed buffer limits in some implementations and includes +project @context due:2025-12-31 and continues even further with more text
|
||||
+project Task with only project no description after
|
||||
@context Task with only context no description after
|
||||
due:2025-12-31 Task with only key:value no description after
|
||||
(A) Priority only no task description
|
||||
x Completed marker only no description
|
||||
x 2025-01-16 Completed with date only no description
|
||||
2025-01-15 Date only no description
|
||||
Task with HTML tags <b>bold</b> text
|
||||
Task with & HTML entity & test
|
||||
Task with < and > comparison operators
|
||||
Task with && and || logical operators
|
||||
Task with -> arrow notation
|
||||
Task with => fat arrow
|
||||
Task with ... ellipsis
|
||||
Task with file.extension.txt mentions
|
||||
Task with https://example.com/+project/@context/page
|
||||
Heavily indented task with many spaces
|
||||
Task with tab indentation
|
||||
Mixed spaces and tabs indentation
|
||||
Task with trailing newline
|
||||
(A) No space after priority works as valid format
|
||||
Task without trailing newline
|
||||
Loading…
Reference in a new issue