Skip to content

Commit

Permalink
adds support for parsing tags out of per-folder metadata files
Browse files Browse the repository at this point in the history
  • Loading branch information
jacoscaz committed Dec 2, 2024
1 parent cd1fc18 commit 0a02ac6
Show file tree
Hide file tree
Showing 15 changed files with 124 additions and 32 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,19 @@ a pending task | foo | bar | 20241010-foo.md | 20241010
a completed task | foo | bar | 20241010-foo.md | 20241010
```

### Metadata file tags

Tags will also be inherited from any per-folder `.taskparser.yaml` files
present in the folder hierarchy leading to a markdown file:

Tags **must** be expressed through a simple, root-level `tags` dictionary:

```yaml
tags:
project: foo
client: bar
```
### Filtering by tag
`taskparser` accepts filter expression via the `-f` argument:
Expand Down
7 changes: 5 additions & 2 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { readFileSync } from 'node:fs';

import { ArgumentParser } from 'argparse';

import { parseFolder, watchFolder } from './parse.js';
import { parseFolder /*, watchFolder */ } from './parse.js';
import { renderCSV } from './renderers/csv.js';
import { renderJSON } from './renderers/json.js';
import { renderTabular } from './renderers/table.js';
Expand Down Expand Up @@ -178,14 +178,17 @@ const renderItems = (items: Set<Item>) => {
// ============================================================================

if (cli_args.watch) {
throw new Error('watch mode is temporarily disabled');
/*
const { stdout } = process;
if (!stdout.isTTY) {
throw new Error('cannot use -w/--watch if the terminal is not a TTY');
}
for await (const { tasks, worklogs } of watchFolder(folder_path)) {
stdout.write('\x1bc');
renderItems(cli_args.worklogs ? worklogs : tasks);
}
}
*/
} else {
const { tasks, worklogs } = await parseFolder(folder_path);
renderItems(cli_args.worklogs ? worklogs : tasks);
Expand Down
98 changes: 68 additions & 30 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { TagMap, Task, Worklog, ParseContext, ParseFileContext, InternalTag
import { readdir, readFile, watch, stat } from 'node:fs/promises';
import { resolve, relative } from 'node:path';

import { load } from 'js-yaml';

import { gfm } from 'micromark-extension-gfm';
import { frontmatter } from 'micromark-extension-frontmatter';

Expand All @@ -13,6 +15,7 @@ import { gfmFromMarkdown } from 'mdast-util-gfm';
import { frontmatterFromMarkdown } from 'mdast-util-frontmatter';

import { extractTagsFromText, extractTagsFromYaml } from './tags.js';
import { FOLDER_META_FILE } from './utils.js';

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

Expand Down Expand Up @@ -137,17 +140,44 @@ export const parseFile = async (ctx: ParseFileContext) => {
}
};

const readFolderMetadata = async (ctx: ParseContext, dir_path: string): Promise<TagMap | undefined> => {
try {
const target_path = resolve(dir_path, FOLDER_META_FILE);
const data: any = load(await readFile(target_path, 'utf8'));
if (typeof data.tags === 'object' && data.tags !== null) {
return Object.fromEntries(Object.entries(data.tags).map(([k, v]) => [k, String(v)]));
}
} catch (err) {
if ((err as any).code !== 'ENOENT') {
throw err;
}
}
return undefined;
};

const parseFolderHelper = async (ctx: ParseContext, target_path: string) => {
const target_stats = await stat(target_path);
if (target_stats.isFile() && target_path.endsWith('.md')) {
const target_rel_path = relative(ctx.folder, target_path);
await parseFile({
...ctx,
file: target_path,
tags: {},
internal_tags: { file: target_rel_path },
file: target_path,
internal_tags: {
...ctx.internal_tags,
file: target_rel_path,
},
});
} else if (target_stats.isDirectory()) {
const folder_tags = await readFolderMetadata(ctx, target_path);
if (folder_tags) {
ctx = {
...ctx,
tags: {
...ctx.tags,
...folder_tags,
},
};
}
const child_names = await readdir(target_path);
for (const child_name of child_names) {
const child_path = resolve(target_path, child_name);
Expand All @@ -161,35 +191,43 @@ export const parseFolder = async (folder_path: string): Promise<ParseContext> =>
folder: folder_path,
tasks: new Set(),
worklogs: new Set(),
tags: {},
internal_tags: {},
};

await parseFolderHelper(ctx, folder_path);
return ctx;
};

export async function* watchFolder(folder_path: string): AsyncIterable<ParseContext> {
const ctx: ParseContext = {
folder: folder_path,
tasks: new Set(),
worklogs: new Set(),
};
await parseFolderHelper(ctx, folder_path);
yield ctx;
for await (const evt of watch(ctx.folder)) {
if (evt.filename) {
const file_path = resolve(ctx.folder, evt.filename);
switch (evt.eventType) {
case 'change':
case 'rename':
await parseFile({
...ctx,
file: file_path,
internal_tags: { file: evt.filename },
tags: {},
});
yield ctx;
break;
}
}
}
};
// export async function* watchFolder(folder_path: string): AsyncIterable<ParseContext> {
// const ctx: ParseContext = {
// folder: folder_path,
// tasks: new Set(),
// worklogs: new Set(),
// tags: {},
// internal_tags: {},
// };
// await parseFolderHelper(ctx, folder_path);
// yield ctx;
// for await (const evt of watch(ctx.folder)) {

// if (evt.filename === FOLDER_META_FILE) {

// }

// if (evt.filename?.endsWith('.md') || evt.filename === FOLDER_META_FILE) {
// const file_path = resolve(ctx.folder, evt.filename);
// switch (evt.eventType) {
// case 'change':
// case 'rename':
// await parseFile({
// ...ctx,
// file: file_path,
// internal_tags: { file: evt.filename },
// tags: {},
// });
// yield ctx;
// break;
// }
// }
// }
// };
5 changes: 5 additions & 0 deletions src/test-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,20 @@ const taskparser = async (argv: string[]): Promise<string> => {
return new Promise((resolve, reject) => {
const child = spawn(`node ${bin_path}`, argv, { shell: true });
let stdout = '';
let stderr = '';
child.stdout?.on('data', (chunk) => {
stdout += chunk.toString();
});
child.stderr?.on('data', (chunk) => {
stderr += chunk;
});
child.on('exit', (code) => {
if (code === null || code === 0) {
resolve(stdout);
return;
}
console.log(stdout);
console.log(stderr);
reject(new Error(`child process exited with non-zero code`));
});
});
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export interface ParseContext {
folder: string;
tasks: TaskSet;
worklogs: WorklogSet;
tags: TagMap;
internal_tags: InternalTagMap;
}

export interface ParseFileContext extends ParseContext {
Expand Down
2 changes: 2 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
export const isNullish = (v: any) => {
return typeof v === 'undefined' || v === null || v === '';
};

export const FOLDER_META_FILE = '.taskparser.yaml';
2 changes: 2 additions & 0 deletions tests/010-folder-tags/.taskparser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tags:
foo: bar
3 changes: 3 additions & 0 deletions tests/010-folder-tags/folder-tags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

- [ ] a pending task
- [X] a completed task #foo(baz)
4 changes: 4 additions & 0 deletions tests/010-folder-tags/test-info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"description": "should read folder tags set in .taskparser.yaml",
"argv": ["-t", "text,foo"]
}
4 changes: 4 additions & 0 deletions tests/010-folder-tags/test-stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
text | foo
---- | ---
a pending task | bar
a completed task #foo(baz) | baz
3 changes: 3 additions & 0 deletions tests/011-folder-tags-nested/.taskparser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
tags:
foo: bar
xyz: abc
2 changes: 2 additions & 0 deletions tests/011-folder-tags-nested/nested/.taskparser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tags:
xyz: def
3 changes: 3 additions & 0 deletions tests/011-folder-tags-nested/nested/folder-tags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

- [ ] a pending task
- [X] a completed task #foo(baz)
4 changes: 4 additions & 0 deletions tests/011-folder-tags-nested/test-info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"description": "should override tags when in the presence of nested subfolders each having a .taskparser.yaml file",
"argv": ["-t", "text,foo,xyz"]
}
4 changes: 4 additions & 0 deletions tests/011-folder-tags-nested/test-stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
text | foo | xyz
---- | --- | ---
a pending task | bar | def
a completed task #foo(baz) | baz | def

0 comments on commit 0a02ac6

Please sign in to comment.