Skip to content

Commit

Permalink
first pass at worklog support
Browse files Browse the repository at this point in the history
  • Loading branch information
jacoscaz committed Oct 24, 2024
1 parent 3a6352e commit 22b8aea
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 40 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,31 @@ Sorting expressions can be combined for nested sorting:
foo(asc),bar(desc)
```

## Worklogs

In addition to tasks, `taskparser` can also collect and display _worklogs_.
A worklog is a list item detailing a given amount of hours spent working.

```markdown
- WL:3h this is a simple worklog
```

Worklogs can be tagged, filtered and sorted exactly as tasks. For each worklog
it encounters, `taskparser` automatically generates the following tags:

| tag | description | internal |
| --- | --- | --- |
| `text` | the textual content of the task (first line only) | yes |
| `file` | the file that contains the task | yes |
| `date` | the date of creation of the task | no |
| `hours` | amount of hours logged | yes |

The `-l` or `--worklogs` flag may be used to enable worklog mode:

```
taskparser -l -t text,hours,file,date"
```

## License

Released under the LGPL v3.0 (`LGPL-3.0-only`) license.
Expand Down
9 changes: 6 additions & 3 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ const arg_parser = new ArgumentParser({

arg_parser.add_argument('-t', '--tags', {
required: false,
default: 'text,done,file,date',
help: 'comma-separated list of tags to show',
});
arg_parser.add_argument('-f', '--filter', {
Expand All @@ -39,7 +38,7 @@ arg_parser.add_argument('-w', '--watch', {
action: 'store_true',
help: 'enable watch mode'
});
arg_parser.add_argument('-W', '--worklogs', {
arg_parser.add_argument('-l', '--worklogs', {
required: false,
action: 'store_true',
help: 'enable worklogs mode',
Expand All @@ -62,7 +61,11 @@ const folder_path = resolve(cwd(), cli_args.path);
const sorter = cli_args.sort ? compileTagSortExpressions(parseTagSortExpressions(cli_args.sort)) : null;
const filter = cli_args.filter ? compileTagFilterExpressions(parseTagFilterExpressions(cli_args.filter)) : null;

const show_tags = cli_args.tags.split(',');
const show_tags = cli_args.tags
? cli_args.tags.split(',')
: cli_args.worklogs
? ['text', 'hours', 'file', 'date']
: ['text', 'done', 'file', 'date'];

const renderItems = (items: Set<Item>) => {
let as_arr = Array.from(items);
Expand Down
97 changes: 61 additions & 36 deletions src/parse.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

import type { Parent, Node, Yaml, ListItem } from 'mdast';
import type { Parent, Node, Yaml, ListItem, Text } from 'mdast';
import type { TagMap, Task, TaskSet, Worklog, WorklogSet, ParseResult } from './types.js';

import { readdir, readFile, watch, stat } from 'node:fs/promises';
Expand All @@ -24,67 +24,91 @@ interface ParseFileContext extends ParseContext {
internal_tags: TagMap;
}

const parseTaskNode = (node: Node, task: Task, worklogs: WorklogSet) => {
if (node.type === 'text') {
if (!('text' in task.internal_tags)) {
task.internal_tags.text = (node as any).value;
}
const node_tags: TagMap = Object.create(null);
extractTagsFromText((node as any).value, node_tags);
if ('worklog' in node_tags) {
const worklog: Worklog = {
task,
tags: { ...task.tags, ...node_tags },
internal_tags: { ...task.internal_tags },
file: task.file,
};
task.worklogs.push(worklog);
worklogs.add(worklog);

const WL_REGEXP = /^WL:(\d{1,2}(?:\.\d{1,2})?)[hH]\s/;

const isListNodeWorklog = (node: ListItem): boolean => {
const paragraph = node.children[0];
if (!paragraph || paragraph.type !== 'paragraph') {
return false;
}
const worklog = paragraph.children[0];
if (!worklog || worklog.type !== 'text') {
return false;
}
return WL_REGEXP.test(worklog.value);
};

const parseTextNode = (node: Text, ctx: ParseFileContext, curr_task: Task | null, curr_wlog: Worklog | null) => {
if (curr_wlog) {
let match;
if (!('text' in curr_wlog.internal_tags) && (match = node.value.match(WL_REGEXP))) {
const [full, hours] = match;
const text = node.value.slice(full.length);
curr_wlog.internal_tags.hours = hours;
curr_wlog.internal_tags.text = text;
extractTagsFromText(text, curr_wlog.tags);
} else {
Object.assign(task.tags, node_tags);
extractTagsFromText(node.value, curr_wlog.tags);
}
}
if ('children' in node) {
for (const child of (node.children as Node[])) {
parseTaskNode(child, task, worklogs);
if (curr_task) {
if (!('text' in curr_task.internal_tags)) {
curr_task.internal_tags.text = node.value;
}
extractTagsFromText(node.value, curr_task.tags);
}
};

const parseYamlNode = (node: Yaml, ctx: ParseFileContext) => {
extractTagsFromYaml(node.value, ctx.tags);
};

const parseParentNode = (node: Parent, ctx: ParseFileContext, tasks: TaskSet, worklogs: WorklogSet) => {
node.children.forEach(node => parseNode(node, ctx, tasks, worklogs));
};

const parseListItemNode = (node: ListItem, ctx: ParseFileContext, tasks: TaskSet, worklogs: WorklogSet) => {
if (typeof node.checked === 'boolean') {
const parseListItemNode = (node: ListItem, ctx: ParseFileContext, curr_task: Task | null, curr_wlog: Worklog | null, tasks: TaskSet, worklogs: WorklogSet) => {
if (!curr_task && typeof node.checked === 'boolean') {
const tags: TagMap = { ...ctx.tags };
const internal_tags: TagMap = {
...ctx.internal_tags,
line: String(node.position!.start.line),
done: String(node.checked),
};
const task: Task = { tags, internal_tags, file: ctx.file, worklogs: [] };
parseTaskNode(node, task, worklogs);
parseParentNode(node as Parent, ctx, task, curr_wlog, tasks, worklogs);
Object.assign(tags, internal_tags);
tasks.add(task);
} else if (!curr_wlog && isListNodeWorklog(node)) {
const tags: TagMap = { ...ctx.tags };
const internal_tags: TagMap = {
...ctx.internal_tags,
line: String(node.position!.start.line),
done: String(node.checked),
};
const worklog: Worklog = { tags, internal_tags, file: ctx.file, task: curr_task };
parseParentNode(node as Parent, ctx, curr_task, worklog, tasks, worklogs);
Object.assign(tags, internal_tags);
worklogs.add(worklog);
} else {
parseParentNode(node, ctx, curr_task, curr_wlog, tasks, worklogs);
}
};

const parseNode = (node: Node, ctx: ParseFileContext, tasks: TaskSet, worklogs: WorklogSet) => {
const parseParentNode = (node: Parent, ctx: ParseFileContext, curr_task: Task | null, curr_wlog: Worklog | null, tasks: TaskSet, worklogs: WorklogSet) => {
node.children.forEach((node) => {
parseNode(node, ctx, curr_task, curr_wlog, tasks, worklogs);
});
};

const parseNode = (node: Node, ctx: ParseFileContext, curr_task: Task | null, curr_wlog: Worklog | null, tasks: TaskSet, worklogs: WorklogSet) => {
switch (node.type) {
case 'yaml':
parseYamlNode(node as Yaml, ctx);
extractTagsFromYaml((node as Yaml).value, ctx.tags);
break;
case 'listItem':
parseListItemNode(node as ListItem, ctx, tasks, worklogs);
parseListItemNode(node as ListItem, ctx, curr_task, curr_wlog, tasks, worklogs);
break;
case 'text':
parseTextNode(node as Text, ctx, curr_task, curr_wlog);
break;
default:
if ('children' in node) {
parseParentNode(node as Parent, ctx, tasks, worklogs);
parseParentNode(node as Parent, ctx, curr_task, curr_wlog, tasks, worklogs);
}
}
};
Expand All @@ -110,11 +134,12 @@ export const parseFile = async (ctx: ParseFileContext, tasks: TaskSet, worklogs:
try {
const data = await readFile(ctx.file, { encoding: 'utf8' });
const root_node = fromMarkdown(data, from_markdown_opts);
// console.log(JSON.stringify(root_node, null, 2));
const date_match = ctx.file.match(DATE_IN_FILENAME_REGEXP);
if (date_match) {
ctx.tags['date'] = date_match[1].replaceAll('-', '');
}
parseNode(root_node, ctx, tasks, worklogs);
parseNode(root_node, ctx, null, null, tasks, worklogs);
} catch (err) {
if ((err as any).code !== 'ENOENT') {
throw err;
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface Task extends Item {
}

export interface Worklog extends Item {
task: Task;
task: Task | null;
}

export type TaskSet = Set<Task>;
Expand Down
3 changes: 3 additions & 0 deletions tests/008-worklog-simple/20241024-worklog-simple.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

- WL:2h some other worklog
- WL:3h some worklog
4 changes: 4 additions & 0 deletions tests/008-worklog-simple/test-info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"description": "should find simple worklogs",
"argv": ["-l"]
}
4 changes: 4 additions & 0 deletions tests/008-worklog-simple/test-stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
text | hours | file | date
---- | ----- | ---- | ----
some other worklog | 2 | 20241024-worklog-simple.md | 20241024
some worklog | 3 | 20241024-worklog-simple.md | 20241024

0 comments on commit 22b8aea

Please sign in to comment.