Task counting and rollover were doing pure line-by-line regex matching, so `- [ ] foo` inside a fenced code block or YAML frontmatter was counted as a real pending task and migrated by rollover. Add a shared classifier that marks each line as frontmatter, code, or content, and skip non-content lines in detectTasks and rolloverCore's parser and heading helpers.
100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
import {describe, expect, it} from "bun:test";
|
||
import {detectTasks} from "../src/pendingTasks";
|
||
import type {MarkdownTasksSettings} from "../src/settings";
|
||
|
||
const baseSettings = {
|
||
completedSymbol: "x",
|
||
movedSymbol: ">",
|
||
countRootTasksOnly: false,
|
||
} as unknown as MarkdownTasksSettings;
|
||
|
||
const settings = (overrides: Partial<MarkdownTasksSettings> = {}): MarkdownTasksSettings => ({
|
||
...baseSettings,
|
||
...overrides,
|
||
});
|
||
|
||
describe("detectTasks – basic counting", () => {
|
||
it("counts open tasks as pending", () => {
|
||
const result = detectTasks("- [ ] one\n- [ ] two\n", settings());
|
||
expect(result.totalTasks).toBe(2);
|
||
expect(result.pendingTasks).toBe(2);
|
||
expect(result.hasPendingTasks).toBe(true);
|
||
});
|
||
|
||
it("treats completed and moved tasks as not pending", () => {
|
||
const result = detectTasks("- [ ] open\n- [x] done\n- [>] moved\n", settings());
|
||
expect(result.totalTasks).toBe(3);
|
||
expect(result.pendingTasks).toBe(1);
|
||
});
|
||
|
||
it("returns no tasks for empty content", () => {
|
||
const result = detectTasks("", settings());
|
||
expect(result.totalTasks).toBe(0);
|
||
expect(result.pendingTasks).toBe(0);
|
||
expect(result.hasPendingTasks).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe("detectTasks – code block exclusion", () => {
|
||
it("does not count tasks inside a backtick-fenced code block", () => {
|
||
const input = [
|
||
"# Notes",
|
||
"",
|
||
"```markdown",
|
||
"- [ ] not a real task",
|
||
"- [ ] also not real",
|
||
"```",
|
||
"",
|
||
"- [ ] real task",
|
||
].join("\n");
|
||
const result = detectTasks(input, settings());
|
||
expect(result.totalTasks).toBe(1);
|
||
expect(result.pendingTasks).toBe(1);
|
||
});
|
||
|
||
it("does not count tasks inside a tilde-fenced code block", () => {
|
||
const input = ["~~~", "- [ ] not real", "~~~", "- [x] done"].join("\n");
|
||
const result = detectTasks(input, settings());
|
||
expect(result.totalTasks).toBe(1);
|
||
expect(result.pendingTasks).toBe(0);
|
||
});
|
||
|
||
it("returns no pending tasks when only code-block tasks exist", () => {
|
||
const input = ["```", "- [ ] not real", "```"].join("\n");
|
||
const result = detectTasks(input, settings());
|
||
expect(result.totalTasks).toBe(0);
|
||
expect(result.pendingTasks).toBe(0);
|
||
expect(result.hasPendingTasks).toBe(false);
|
||
});
|
||
|
||
it("handles tasks before, inside, and after a code block", () => {
|
||
const input = [
|
||
"- [ ] before",
|
||
"```",
|
||
"- [ ] inside (ignored)",
|
||
"```",
|
||
"- [x] after done",
|
||
].join("\n");
|
||
const result = detectTasks(input, settings());
|
||
expect(result.totalTasks).toBe(2);
|
||
expect(result.pendingTasks).toBe(1);
|
||
});
|
||
});
|
||
|
||
describe("detectTasks – frontmatter exclusion", () => {
|
||
it("ignores task-like lines inside YAML frontmatter", () => {
|
||
const input = ["---", "- [ ] not a task", "---", "- [ ] real task"].join("\n");
|
||
const result = detectTasks(input, settings());
|
||
expect(result.totalTasks).toBe(1);
|
||
expect(result.pendingTasks).toBe(1);
|
||
});
|
||
});
|
||
|
||
describe("detectTasks – root-only counting still skips code blocks", () => {
|
||
it("skips code-block tasks when countRootTasksOnly is enabled", () => {
|
||
const input = ["```", "- [ ] inside", "```", "- [ ] real"].join("\n");
|
||
const result = detectTasks(input, settings({countRootTasksOnly: true}));
|
||
expect(result.totalTasks).toBe(1);
|
||
expect(result.pendingTasks).toBe(1);
|
||
});
|
||
});
|