feat(frontmatter/file-io-and-serialize): implement parseTaskFile, parseTaskDirectory, and serializeFrontmatter
This commit is contained in:
77
src/frontmatter/file-io.ts
Normal file
77
src/frontmatter/file-io.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
// File I/O functions for frontmatter parsing.
|
||||
//
|
||||
// ⚠️ These functions depend on `node:fs/promises` and are only available in
|
||||
// Node.js-compatible runtimes. Consumers targeting Deno, Bun, or browsers
|
||||
// should use `parseFrontmatter` directly with their own file-reading mechanism.
|
||||
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import { join, extname } from 'node:path';
|
||||
import { parseFrontmatter } from './parse.js';
|
||||
import type { TaskInput as TaskInputType } from '../schema/task.js';
|
||||
|
||||
/**
|
||||
* Read a markdown file and parse its YAML frontmatter into a validated
|
||||
* `TaskInput` object.
|
||||
*
|
||||
* Delegates to `parseFrontmatter` for parsing and validation.
|
||||
* Throws the underlying Node.js error for I/O failures (ENOENT, EACCES, etc.).
|
||||
*
|
||||
* @param filePath — Absolute or relative path to a `.md` file
|
||||
* @returns Validated `TaskInput` object
|
||||
* @throws Node.js system errors for I/O failures
|
||||
* @throws {InvalidInputError} When frontmatter is missing or invalid
|
||||
*
|
||||
* @nodeOnly Uses `node:fs/promises.readFile`
|
||||
*/
|
||||
export async function parseTaskFile(filePath: string): Promise<TaskInputType> {
|
||||
const content = await readFile(filePath, 'utf-8');
|
||||
return parseFrontmatter(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively scan a directory for `.md` files and parse each one that
|
||||
* contains valid `---`-delimited YAML frontmatter.
|
||||
*
|
||||
* - Files without valid frontmatter are silently skipped (no error thrown,
|
||||
* just omitted from the results).
|
||||
* - Non-`.md` files are ignored.
|
||||
* - Throws the underlying Node.js error for I/O failures (ENOENT, EACCES).
|
||||
* - Uses `parseTaskFile` per file.
|
||||
*
|
||||
* @param dirPath — Absolute or relative path to a directory
|
||||
* @returns Array of validated `TaskInput` objects (one per valid file)
|
||||
* @throws Node.js system errors for I/O failures
|
||||
*
|
||||
* @nodeOnly Uses `node:fs/promises.readdir` and `node:fs/promises.stat`
|
||||
*/
|
||||
export async function parseTaskDirectory(dirPath: string): Promise<TaskInputType[]> {
|
||||
const results: TaskInputType[] = [];
|
||||
const entries = await readdir(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dirPath, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// Recurse into subdirectories
|
||||
const subResults = await parseTaskDirectory(fullPath);
|
||||
results.push(...subResults);
|
||||
} else if (entry.isFile() && extname(entry.name).toLowerCase() === '.md') {
|
||||
// Only process .md files; skip others silently
|
||||
try {
|
||||
const task = await parseTaskFile(fullPath);
|
||||
results.push(task);
|
||||
} catch (err) {
|
||||
// Silently skip files without valid frontmatter (InvalidInputError
|
||||
// from parseFrontmatter means no valid --- delimiters found, or
|
||||
// the YAML/schema is invalid). Rethrow I/O errors.
|
||||
if (err instanceof Error && err.name === 'InvalidInputError') {
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
// Non-.md files and other entry types are silently skipped
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
// Frontmatter submodule — parse and serialize
|
||||
// Frontmatter submodule — parse, serialize, and file I/O
|
||||
|
||||
export { splitFrontmatter, parseFrontmatter, parseTaskFile, parseTaskDirectory } from './parse.js';
|
||||
export { serializeFrontmatter } from './serialize.js';
|
||||
export { splitFrontmatter, parseFrontmatter } from './parse.js';
|
||||
export { serializeFrontmatter } from './serialize.js';
|
||||
export { parseTaskFile, parseTaskDirectory } from './file-io.js';
|
||||
@@ -134,12 +134,3 @@ export function parseFrontmatter(markdown: string): TaskInputType {
|
||||
return cleaned as TaskInputType;
|
||||
}
|
||||
|
||||
export function parseTaskFile(_input: string): unknown {
|
||||
// Stub — implementation pending
|
||||
return {};
|
||||
}
|
||||
|
||||
export function parseTaskDirectory(_dir: string): unknown[] {
|
||||
// Stub — implementation pending
|
||||
return [];
|
||||
}
|
||||
@@ -1,6 +1,36 @@
|
||||
// TaskInput → markdown with frontmatter
|
||||
// TaskInput → markdown with YAML frontmatter
|
||||
|
||||
export function serializeFrontmatter(_input: unknown): string {
|
||||
// Stub — implementation pending
|
||||
return '';
|
||||
import { stringify as yamlStringify } from 'yaml';
|
||||
import type { TaskInput as TaskInputType } from '../schema/task.js';
|
||||
|
||||
/**
|
||||
* Serialize a `TaskInput` object into a `---`-delimited markdown string.
|
||||
*
|
||||
* - Uses `yaml.stringify()` for the data portion (YAML 1.2 output).
|
||||
* - Includes all `TaskInput` fields in the YAML (per schema convention).
|
||||
* - Nullable fields set to `null` produce explicit `null` in YAML.
|
||||
* - Absent (undefined) fields are omitted from the YAML output.
|
||||
* - Appends optional body content (default: empty string) after the
|
||||
* closing `---`.
|
||||
*
|
||||
* @param task — The `TaskInput` object to serialize
|
||||
* @param body — Optional markdown body content after the frontmatter
|
||||
* @returns A complete markdown string with frontmatter and optional body
|
||||
*/
|
||||
export function serializeFrontmatter(task: TaskInputType, body?: string): string {
|
||||
// Build a clean object: include all defined fields, omit undefined ones.
|
||||
// This ensures `yaml.stringify()` only writes keys that are present,
|
||||
// keeping the output compact and matching what `parseFrontmatter` expects.
|
||||
const data: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(task)) {
|
||||
if (value !== undefined) {
|
||||
data[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const yaml = yamlStringify(data, { lineWidth: 0 });
|
||||
const content = body ?? '';
|
||||
|
||||
return `---\n${yaml}---\n${content}`;
|
||||
}
|
||||
Reference in New Issue
Block a user