JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 195
  • Score
    100M100P100Q62283F
  • License MIT

Core parsing, filtering, scanning, and file writing for md2do

Package Exports

  • @md2do/core

Readme

@md2do/core

npm version npm downloads License: MIT

Core library for md2do - parsing, filtering, scanning, and writing markdown tasks.

Overview

@md2do/core provides the fundamental building blocks for working with TODO items in markdown files. This is a library package designed for developers who want to build their own task management tools or integrate md2do functionality into their applications.

For end users: Use @md2do/cli instead - it provides a ready-to-use command-line interface.

For developers: This package gives you programmatic access to all core functionality.

Features

  • ๐Ÿ“ Parser - Extract TODO items from markdown with rich metadata (assignees, priorities, tags, due dates)
  • ๐Ÿ” Scanner - Recursively scan directories for markdown files with glob patterns
  • ๐ŸŽฏ Filters - Filter tasks by any metadata field with type-safe functions
  • ๐Ÿ“Š Sorting - Sort tasks by priority, due date, assignee, file, etc.
  • โœ๏ธ Writer - Atomically update markdown files while preserving formatting
  • ๐Ÿ”ง Utilities - Date parsing, ID generation, and helper functions
  • โœ… Type Safe - Full TypeScript support with strict mode
  • ๐Ÿงช Well Tested - 200+ tests with comprehensive coverage

Installation

npm install @md2do/core

Quick Start

import { scanMarkdownFile, filters, sorting } from '@md2do/core';

// Scan a markdown file
const result = scanMarkdownFile('tasks.md', 'tasks.md');

console.log(`Found ${result.tasks.length} tasks`);

// Filter urgent tasks assigned to nick
const urgentNickTasks = result.tasks
  .filter(filters.byAssignee('nick'))
  .filter(filters.byPriority('urgent'));

// Sort by due date
const sorted = sorting.sortTasks(urgentNickTasks, 'due');

console.log(sorted);

API Reference

Parser

Parse markdown content to extract tasks.

parseMarkdown(content, filePath?)

Parse markdown content and extract all tasks.

import { parseMarkdown } from '@md2do/core';

const content = `
- [ ] Fix bug @nick !!! #backend #due/2026-01-25
- [x] Write docs @jane !! #docs {completed:2026-01-20}
`;

const tasks = parseMarkdown(content, 'tasks.md');
// [
//   {
//     id: 'abc123',
//     text: 'Fix bug',
//     completed: false,
//     file: 'tasks.md',
//     line: 2,
//     assignee: 'nick',
//     priority: 'urgent',
//     tags: ['backend'],
//     dueDate: Date('2026-01-25'),
//     ...
//   },
//   ...
// ]

parseTaskLine(line, lineNumber, filePath?)

Parse a single markdown task line.

import { parseTaskLine } from '@md2do/core';

const task = parseTaskLine(
  '- [ ] Fix bug @nick !!! #backend #due/2026-01-25',
  5,
  'tasks.md',
);

Pattern Exports

import {
  TASK_PATTERN,
  ASSIGNEE_PATTERN,
  PRIORITY_PATTERN,
  TAG_PATTERN,
  DUE_DATE_PATTERN,
  TODOIST_ID_PATTERN,
} from '@md2do/core';

Scanner

Scan directories for markdown files and extract tasks.

scanMarkdownFile(filePath, rootPath?)

Scan a single markdown file.

import { scanMarkdownFile } from '@md2do/core';

const result = scanMarkdownFile('./notes/tasks.md', './notes');
// {
//   tasks: [...],
//   metadata: {
//     filesScanned: 1,
//     totalTasks: 15,
//     completed: 3,
//     incomplete: 12
//   }
// }

scanMarkdownFiles(options)

Scan multiple markdown files with glob patterns.

import { scanMarkdownFiles } from '@md2do/core';

const result = await scanMarkdownFiles({
  path: './docs',
  pattern: '**/*.md',
  exclude: ['node_modules/**', 'dist/**'],
});

console.log(
  `Found ${result.metadata.totalTasks} tasks in ${result.metadata.filesScanned} files`,
);

Options:

{
  path?: string;           // Root path to scan (default: '.')
  pattern?: string;        // Glob pattern (default: '**/*.md')
  exclude?: string[];      // Patterns to exclude
}

Filters

Type-safe filter functions for tasks.

import { filters } from '@md2do/core';

// All filters return (task: Task) => boolean

// By assignee
const nickTasks = tasks.filter(filters.byAssignee('nick'));

// By priority
const urgentTasks = tasks.filter(filters.byPriority('urgent'));

// By tag
const backendTasks = tasks.filter(filters.byTag('backend'));

// By project
const acmeTasks = tasks.filter(filters.byProject('acme-app'));

// By person (from 1-1s)
const janeTasks = tasks.filter(filters.byPerson('jane'));

// By completion status
const incomplete = tasks.filter(filters.incomplete());
const completed = tasks.filter(filters.completed());

// By due date
const overdue = tasks.filter(filters.overdue());
const dueToday = tasks.filter(filters.dueToday());
const dueThisWeek = tasks.filter(filters.dueThisWeek());
const dueWithin = tasks.filter(filters.dueWithinDays(7));

// Combine filters
const urgentBackendTasks = tasks
  .filter(filters.byAssignee('nick'))
  .filter(filters.byPriority('urgent'))
  .filter(filters.byTag('backend'))
  .filter(filters.incomplete());

Sorting

Sort tasks by various fields.

import { sorting } from '@md2do/core';

// Sort by priority (urgent โ†’ high โ†’ normal โ†’ low)
const byPriority = sorting.sortTasks(tasks, 'due');

// Sort by due date (earliest first)
const byDue = sorting.sortTasks(tasks, 'due');

// Sort by creation date
const byCreated = sorting.sortTasks(tasks, 'created');

// Sort by file path
const byFile = sorting.sortTasks(tasks, 'file');

// Sort by project
const byProject = sorting.sortTasks(tasks, 'project');

// Sort by assignee (alphabetical)
const byAssignee = sorting.sortTasks(tasks, 'assignee');

// Reverse order
const reversed = sorting.sortTasks(tasks, 'priority', true);

Sort fields:

  • 'due' - Due date (earliest first)
  • 'priority' - Priority (urgent โ†’ high โ†’ normal โ†’ low)
  • 'created' - Creation date (oldest first)
  • 'file' - File path (alphabetical)
  • 'project' - Project name (alphabetical)
  • 'assignee' - Assignee (alphabetical)

Writer

Atomically update markdown files.

updateTask(options)

Update a task in a markdown file.

import { updateTask } from '@md2do/core';

await updateTask({
  file: 'tasks.md',
  line: 5,
  updates: {
    completed: true,
    text: 'Fix bug @nick !!! #backend #due/2026-01-25 {todoist:123456} {completed:2026-01-26}',
  },
});

Options:

{
  file: string;            // File path
  line: number;            // Line number (1-indexed)
  updates: {
    completed?: boolean;   // Toggle completion
    text?: string;         // Replace entire task text
  };
}

Features:

  • Atomic writes (uses temp file + rename)
  • Preserves file formatting
  • Updates timestamps for completed tasks
  • Safe error handling

Types

All types are exported for TypeScript users.

import type {
  Task,
  Priority,
  ScanResult,
  ScanOptions,
  UpdateTaskOptions,
  TaskUpdate,
} from '@md2do/core';

interface Task {
  id: string;
  text: string;
  completed: boolean;
  file: string;
  line: number;
  assignee?: string;
  priority?: Priority;
  tags: string[];
  dueDate?: Date;
  createdDate?: Date;
  completedDate?: Date;
  todoistId?: string;
  project?: string;
  person?: string;
  heading?: string;
}

type Priority = 'urgent' | 'high' | 'normal' | 'low';

Utilities

Date Utilities

import {
  parseDate,
  formatDate,
  isOverdue,
  isDueToday,
  isDueThisWeek,
  isDueWithinDays,
} from '@md2do/core';

// Parse dates
const date = parseDate('2026-01-25'); // Date object
const relative = parseDate('tomorrow'); // Relative dates
const natural = parseDate('next friday'); // Natural language

// Format dates
const formatted = formatDate(new Date()); // '2026-01-21'

// Check due dates
isOverdue(task); // boolean
isDueToday(task); // boolean
isDueThisWeek(task); // boolean
isDueWithinDays(task, 7); // boolean

ID Generation

import { generateId } from '@md2do/core';

const id = generateId('tasks.md', 5); // Generate deterministic ID
// 'f7a3b2c1'

Usage Examples

Build a Custom CLI

import { scanMarkdownFiles, filters, sorting } from '@md2do/core';

async function listTasks(options: {
  assignee?: string;
  priority?: string;
  tag?: string;
}) {
  // Scan all markdown files
  const result = await scanMarkdownFiles({
    path: process.cwd(),
    pattern: '**/*.md',
    exclude: ['node_modules/**'],
  });

  let filtered = result.tasks;

  // Apply filters
  if (options.assignee) {
    filtered = filtered.filter(filters.byAssignee(options.assignee));
  }
  if (options.priority) {
    filtered = filtered.filter(filters.byPriority(options.priority as any));
  }
  if (options.tag) {
    filtered = filtered.filter(filters.byTag(options.tag));
  }

  // Sort by priority
  const sorted = sorting.sortTasks(filtered, 'priority');

  // Display
  console.log(`Found ${sorted.length} tasks`);
  sorted.forEach((task) => {
    console.log(`- ${task.text}`);
  });
}

listTasks({ assignee: 'nick', priority: 'urgent' });

Generate Statistics

import { scanMarkdownFiles } from '@md2do/core';

async function getStats() {
  const result = await scanMarkdownFiles({ path: '.' });

  // Group by assignee
  const byAssignee = new Map<string, number>();
  result.tasks.forEach((task) => {
    if (task.assignee) {
      const count = byAssignee.get(task.assignee) || 0;
      byAssignee.set(task.assignee, count + 1);
    }
  });

  console.log('Tasks by assignee:');
  byAssignee.forEach((count, assignee) => {
    console.log(`  ${assignee}: ${count}`);
  });
}

Sync Task Completion

import { scanMarkdownFile, updateTask } from '@md2do/core';

async function markComplete(filePath: string, taskId: string) {
  const result = scanMarkdownFile(filePath, filePath);
  const task = result.tasks.find((t) => t.id === taskId);

  if (!task) {
    throw new Error(`Task ${taskId} not found`);
  }

  await updateTask({
    file: task.file,
    line: task.line,
    updates: { completed: true },
  });

  console.log(`โœ“ Marked task as complete: ${task.text}`);
}

Watch for Changes

import { watch } from 'fs';
import { scanMarkdownFile } from '@md2do/core';

function watchFile(filePath: string, onChange: (tasks: Task[]) => void) {
  watch(filePath, async () => {
    const result = scanMarkdownFile(filePath, filePath);
    onChange(result.tasks);
  });
}

watchFile('./tasks.md', (tasks) => {
  console.log(`File updated: ${tasks.length} tasks found`);
});

Testing

Run tests:

# Run all tests
pnpm test

# Run tests in watch mode
pnpm test

# Run specific test suite
pnpm test parser

# Run with coverage
pnpm test:coverage

Test coverage:

  • Parser: 70 tests
  • Scanner: 43 tests
  • Filters: 41 tests
  • Sorting: 26 tests
  • Writer: 15 tests
  • Utilities: 45 tests

Documentation

License

MIT ยฉ Nick Hart