mirror of
https://github.com/rioskit/obsidian-todo-txt-mode.git
synced 2026-07-22 05:49:24 +00:00
feat: add Jest configuration and update dependencies for TypeScript
This commit is contained in:
parent
00596bcec5
commit
f587b3543f
9 changed files with 5094 additions and 84 deletions
15
jest.config.js
Normal file
15
jest.config.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/src'],
|
||||
testMatch: ['**/__tests__/**/*.test.ts'],
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.ts',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/**/__tests__/**',
|
||||
'!src/main.ts'
|
||||
]
|
||||
};
|
||||
4824
package-lock.json
generated
4824
package-lock.json
generated
File diff suppressed because it is too large
Load diff
13
package.json
13
package.json
|
|
@ -6,9 +6,15 @@
|
|||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"keywords": ["todo.txt", "task"],
|
||||
"keywords": [
|
||||
"todo.txt",
|
||||
"task"
|
||||
],
|
||||
"author": "Riosk It",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
|
@ -16,12 +22,15 @@
|
|||
"url": "https://github.com/rioskit/obsidian-todo-txt-mode"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"jest": "^29.7.0",
|
||||
"obsidian": "latest",
|
||||
"ts-jest": "^29.4.0",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
|
|
|
|||
80
src/__tests__/sort.test.ts
Normal file
80
src/__tests__/sort.test.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { parsePriorityValue, parseProjectTag, parseContextTag, parseDueDate } from '../parser';
|
||||
|
||||
describe('TodoTxtSorter - 実装関数のテスト', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
it('優先度なしは最大値-1を返す', () => {
|
||||
expect(parsePriorityValue('Task without priority')).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);
|
||||
});
|
||||
|
||||
it('数値優先度の処理', () => {
|
||||
expect(parsePriorityValue('(1) First task')).toBe(1);
|
||||
expect(parsePriorityValue('(10) Tenth task')).toBe(10);
|
||||
expect(parsePriorityValue('(123) Task 123')).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('parseProjectTag', () => {
|
||||
it('プロジェクトタグを抽出', () => {
|
||||
expect(parseProjectTag('Task +project1')).toBe('project1');
|
||||
expect(parseProjectTag('Task +Project2 with more text')).toBe('project2');
|
||||
expect(parseProjectTag('+ProjectAtStart')).toBe('projectatstart');
|
||||
});
|
||||
|
||||
it('プロジェクトタグがない場合のデフォルト値', () => {
|
||||
expect(parseProjectTag('Task without project')).toBe('zzzz');
|
||||
});
|
||||
|
||||
it('複数のプロジェクトタグがある場合は最初のものを返す', () => {
|
||||
expect(parseProjectTag('Task +first +second')).toBe('first');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseContextTag', () => {
|
||||
it('コンテキストタグを抽出', () => {
|
||||
expect(parseContextTag('Task @home')).toBe('home');
|
||||
expect(parseContextTag('Task @Work with more text')).toBe('work');
|
||||
expect(parseContextTag('@ContextAtStart')).toBe('contextatstart');
|
||||
});
|
||||
|
||||
it('コンテキストタグがない場合のデフォルト値', () => {
|
||||
expect(parseContextTag('Task without context')).toBe('zzzz');
|
||||
});
|
||||
|
||||
it('複数のコンテキストタグがある場合は最初のものを返す', () => {
|
||||
expect(parseContextTag('Task @first @second')).toBe('first');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
it('期日なしの場合のデフォルト値(未来)', () => {
|
||||
expect(parseDueDate('Task without due date')).toBe('9999-99-99');
|
||||
});
|
||||
|
||||
it('期日なしの場合のデフォルト値(過去)', () => {
|
||||
expect(parseDueDate('Task without due date', false)).toBe('0000-00-00');
|
||||
});
|
||||
});
|
||||
});
|
||||
140
src/__tests__/syntax.test.ts
Normal file
140
src/__tests__/syntax.test.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import { projectRegex, contextRegex, priorityRegex, dueDateRegex } from '../syntax';
|
||||
|
||||
describe('Todo.txt Syntax - 実装正規表現のテスト', () => {
|
||||
describe('projectRegex', () => {
|
||||
beforeEach(() => {
|
||||
// グローバル正規表現のlastIndexをリセット
|
||||
projectRegex.lastIndex = 0;
|
||||
});
|
||||
|
||||
it('プロジェクトタグを正しくマッチ', () => {
|
||||
expect('+project'.match(projectRegex)).toEqual(['+project']);
|
||||
projectRegex.lastIndex = 0;
|
||||
expect('Task +project1 +project2'.match(projectRegex)).toEqual(['+project1', '+project2']);
|
||||
projectRegex.lastIndex = 0;
|
||||
expect('+ProjectAtStart and +ProjectAtEnd'.match(projectRegex)).toEqual(['+ProjectAtStart', '+ProjectAtEnd']);
|
||||
});
|
||||
|
||||
it('プロジェクトタグがない場合はnull', () => {
|
||||
expect('No project here'.match(projectRegex)).toBeNull();
|
||||
projectRegex.lastIndex = 0;
|
||||
expect('+ space after plus'.match(projectRegex)).toBeNull();
|
||||
});
|
||||
|
||||
it('特殊文字を含むプロジェクトタグ', () => {
|
||||
expect('Task +project-name'.match(projectRegex)).toEqual(['+project-name']);
|
||||
projectRegex.lastIndex = 0;
|
||||
expect('Task +project_name'.match(projectRegex)).toEqual(['+project_name']);
|
||||
projectRegex.lastIndex = 0;
|
||||
expect('Task +project123'.match(projectRegex)).toEqual(['+project123']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('contextRegex', () => {
|
||||
beforeEach(() => {
|
||||
contextRegex.lastIndex = 0;
|
||||
});
|
||||
|
||||
it('コンテキストタグを正しくマッチ', () => {
|
||||
expect('@home'.match(contextRegex)).toEqual(['@home']);
|
||||
contextRegex.lastIndex = 0;
|
||||
expect('Task @context1 @context2'.match(contextRegex)).toEqual(['@context1', '@context2']);
|
||||
contextRegex.lastIndex = 0;
|
||||
expect('@ContextAtStart and @ContextAtEnd'.match(contextRegex)).toEqual(['@ContextAtStart', '@ContextAtEnd']);
|
||||
});
|
||||
|
||||
it('コンテキストタグがない場合はnull', () => {
|
||||
expect('No context here'.match(contextRegex)).toBeNull();
|
||||
contextRegex.lastIndex = 0;
|
||||
expect('@ space after at'.match(contextRegex)).toBeNull();
|
||||
});
|
||||
|
||||
it('特殊文字を含むコンテキストタグ', () => {
|
||||
expect('Task @context-name'.match(contextRegex)).toEqual(['@context-name']);
|
||||
contextRegex.lastIndex = 0;
|
||||
expect('Task @context_name'.match(contextRegex)).toEqual(['@context_name']);
|
||||
contextRegex.lastIndex = 0;
|
||||
expect('Task @context123'.match(contextRegex)).toEqual(['@context123']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('priorityRegex', () => {
|
||||
it('アルファベット優先度をマッチ', () => {
|
||||
expect('(A) Task'.match(priorityRegex)).toBeTruthy();
|
||||
expect('(B) Task'.match(priorityRegex)).toBeTruthy();
|
||||
expect('(Z) Task'.match(priorityRegex)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('数値優先度をマッチ', () => {
|
||||
expect('(1) Task'.match(priorityRegex)).toBeTruthy();
|
||||
expect('(10) Task'.match(priorityRegex)).toBeTruthy();
|
||||
expect('(123) Task'.match(priorityRegex)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('混合優先度をマッチ', () => {
|
||||
expect('(A1) Task'.match(priorityRegex)).toBeTruthy();
|
||||
expect('(1a) Task'.match(priorityRegex)).toBeTruthy();
|
||||
expect('(A1b2) Task'.match(priorityRegex)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('スペースありでもマッチ', () => {
|
||||
expect(' (A) Task with spaces'.match(priorityRegex)).toBeTruthy();
|
||||
expect(' (B) Another task'.match(priorityRegex)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('完了タスクでもマッチ', () => {
|
||||
expect('x (A) Completed task'.match(priorityRegex)).toBeTruthy();
|
||||
expect(' x (B) Completed with spaces'.match(priorityRegex)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('無効な優先度はマッチしない', () => {
|
||||
expect('(a) Lower case'.match(priorityRegex)).toBeNull();
|
||||
expect('No priority here'.match(priorityRegex)).toBeNull();
|
||||
expect('Middle (A) priority'.match(priorityRegex)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('dueDateRegex', () => {
|
||||
beforeEach(() => {
|
||||
dueDateRegex.lastIndex = 0;
|
||||
});
|
||||
|
||||
it('標準的な期日フォーマットをマッチ', () => {
|
||||
expect('due:2024-12-31'.match(dueDateRegex)).toEqual(['due:2024-12-31']);
|
||||
dueDateRegex.lastIndex = 0;
|
||||
expect('Task due:2024-01-01 text'.match(dueDateRegex)).toEqual(['due:2024-01-01']);
|
||||
});
|
||||
|
||||
it('様々な期日フォーマットをマッチ', () => {
|
||||
expect('due:tomorrow'.match(dueDateRegex)).toEqual(['due:tomorrow']);
|
||||
dueDateRegex.lastIndex = 0;
|
||||
expect('due:today'.match(dueDateRegex)).toEqual(['due:today']);
|
||||
dueDateRegex.lastIndex = 0;
|
||||
expect('due:2024/12/31'.match(dueDateRegex)).toEqual(['due:2024/12/31']);
|
||||
});
|
||||
|
||||
it('複数の期日をマッチ', () => {
|
||||
expect('Task due:2024-01-01 and due:2024-12-31'.match(dueDateRegex))
|
||||
.toEqual(['due:2024-01-01', 'due:2024-12-31']);
|
||||
});
|
||||
|
||||
it('期日がない場合はnull', () => {
|
||||
expect('No due date here'.match(dueDateRegex)).toBeNull();
|
||||
dueDateRegex.lastIndex = 0;
|
||||
expect('due: space after colon'.match(dueDateRegex)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('完了タスク判定', () => {
|
||||
it('完了タスクを正しく判定', () => {
|
||||
expect('x Completed task'.trim().startsWith('x ')).toBe(true);
|
||||
expect('x 2024-01-01 Completed task with date'.trim().startsWith('x ')).toBe(true);
|
||||
});
|
||||
|
||||
it('未完了タスクは判定しない', () => {
|
||||
expect('Active task'.trim().startsWith('x ')).toBe(false);
|
||||
expect('(A) Priority task'.trim().startsWith('x ')).toBe(false);
|
||||
expect('Task with x in middle'.trim().startsWith('x ')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
44
src/parser.ts
Normal file
44
src/parser.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Todo.txt形式の純粋なパーサー関数(Obsidianに依存しない)
|
||||
|
||||
export function parsePriorityValue(line: string): number {
|
||||
if (line.trim().startsWith('x ')) {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
const priorityMatch = line.match(/^\s*\(([A-Z0-9][A-Z0-9a-z0-9]*)\)/);
|
||||
if (!priorityMatch) {
|
||||
return Number.MAX_SAFE_INTEGER - 1;
|
||||
}
|
||||
|
||||
const priority = priorityMatch[1];
|
||||
if (/^\d+$/.test(priority)) {
|
||||
return parseInt(priority, 10);
|
||||
}
|
||||
|
||||
if (/^\d/.test(priority)) {
|
||||
const numericPart = priority.match(/^\d+/);
|
||||
if (numericPart) {
|
||||
return parseInt(numericPart[0], 10);
|
||||
}
|
||||
}
|
||||
|
||||
return priority.charCodeAt(0) - 'A'.charCodeAt(0);
|
||||
}
|
||||
|
||||
export function parseProjectTag(line: string): string {
|
||||
const projectMatch = line.match(/\+([^\s]+)/);
|
||||
return projectMatch ? projectMatch[1].toLowerCase() : 'zzzz';
|
||||
}
|
||||
|
||||
export function parseContextTag(line: string): string {
|
||||
const contextMatch = line.match(/@([^\s]+)/);
|
||||
return contextMatch ? contextMatch[1].toLowerCase() : 'zzzz';
|
||||
}
|
||||
|
||||
export function parseDueDate(line: string, defaultToFuture: boolean = true): string {
|
||||
const dueDateMatch = line.match(/due:(\d{4}-\d{2}-\d{2})/);
|
||||
if (!dueDateMatch) {
|
||||
return defaultToFuture ? '9999-99-99' : '0000-00-00';
|
||||
}
|
||||
return dueDateMatch[1];
|
||||
}
|
||||
41
src/sort.ts
41
src/sort.ts
|
|
@ -1,5 +1,6 @@
|
|||
import { App, MarkdownView, Notice, Plugin, TFile, normalizePath } from 'obsidian';
|
||||
import { SortType, TodoTxtSettings } from './settings';
|
||||
import { parsePriorityValue, parseProjectTag, parseContextTag, parseDueDate } from './parser';
|
||||
|
||||
export class TodoTxtSorter {
|
||||
constructor(
|
||||
|
|
@ -9,51 +10,19 @@ export class TodoTxtSorter {
|
|||
) {}
|
||||
|
||||
getPriorityValue(line: string): number {
|
||||
if (line.trim().startsWith('x ')) {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
const priorityMatch = line.match(/^\s*\(([A-Z0-9][A-Z0-9a-z0-9]*)\)/);
|
||||
if (!priorityMatch) {
|
||||
return Number.MAX_SAFE_INTEGER - 1; // 優先度なしは完了タスクの上
|
||||
}
|
||||
|
||||
const priority = priorityMatch[1];
|
||||
// 数字だけの場合はその値を返す
|
||||
if (/^\d+$/.test(priority)) {
|
||||
return parseInt(priority, 10);
|
||||
}
|
||||
|
||||
// 先頭が数字の場合は数値として解釈
|
||||
if (/^\d/.test(priority)) {
|
||||
const numericPart = priority.match(/^\d+/);
|
||||
if (numericPart) {
|
||||
return parseInt(numericPart[0], 10);
|
||||
}
|
||||
}
|
||||
|
||||
// 先頭がアルファベットの場合は順序に変換(A=0, B=1, ...)
|
||||
// 複数のアルファベットの場合は先頭文字のみを考慮
|
||||
return priority.charCodeAt(0) - 'A'.charCodeAt(0);
|
||||
return parsePriorityValue(line);
|
||||
}
|
||||
|
||||
getProjectTag(line: string): string {
|
||||
const projectMatch = line.match(/\+([^\s]+)/);
|
||||
return projectMatch ? projectMatch[1].toLowerCase() : 'zzzz'; // プロジェクトなしは最後
|
||||
return parseProjectTag(line);
|
||||
}
|
||||
|
||||
getContextTag(line: string): string {
|
||||
const contextMatch = line.match(/@([^\s]+)/);
|
||||
return contextMatch ? contextMatch[1].toLowerCase() : 'zzzz'; // コンテキストなしは最後
|
||||
return parseContextTag(line);
|
||||
}
|
||||
|
||||
getDueDate(line: string, defaultToFuture: boolean = true): string {
|
||||
const dueDateMatch = line.match(/due:(\d{4}-\d{2}-\d{2})/);
|
||||
// 期日なしの場合、ソートの種類によって異なる値を返す
|
||||
if (!dueDateMatch) {
|
||||
return defaultToFuture ? '9999-99-99' : '0000-00-00'; // ソート種別によって最初か最後に
|
||||
}
|
||||
return dueDateMatch[1];
|
||||
return parseDueDate(line, defaultToFuture);
|
||||
}
|
||||
|
||||
registerSortCommands(plugin: Plugin) {
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import { RangeSetBuilder } from '@codemirror/state';
|
|||
import { App, TFile } from 'obsidian';
|
||||
import { TodoTxtSettings } from './settings';
|
||||
|
||||
// 正規表現をテスト用に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) {
|
||||
const projectRegex = /\+[^\s]+/g;
|
||||
const contextRegex = /@[^\s]+/g;
|
||||
const priorityRegex = /^(\s*(?:x\s+)?\s*)\(([A-Z0-9][A-Z0-9a-z0-9]*)\)/;
|
||||
// const dueDateRegex = /due:(\d{4}-\d{2}-\d{2})/g;
|
||||
const dueDateRegex = /due:[^\s]+/g;
|
||||
// 正規表現はモジュールレベルで定義されたものを使用
|
||||
|
||||
return ViewPlugin.fromClass(class implements PluginValue {
|
||||
decorations: DecorationSet;
|
||||
|
|
|
|||
9
tsconfig.test.json
Normal file
9
tsconfig.test.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue