joplin-plugin-gtd-calendar/src/Gtd/parseCalendarConfig.ts
Victor Wiebe fc1b2fcabf Phase 2: real data layer — folder scope, gtd blocks, inclusion matrix
- src/Gtd/: pure, unit-tested modules
  - parseCalendarConfig: SPEC defaults + warnings for invalid options
  - folderScope: BFS scope resolution (this-folder / children / depth N)
  - gtdBlock: extract + YAML-parse the first ```gtd fence (empty block
    is a valid opt-in; malformed YAML opts in with a surfaced warning)
  - resolveDate: gtd date (iso + mm-dd-yyyy) overrides todo_due; nothing
    resolvable -> unscheduled (null date)
  - collectEvents: orchestrates scope -> fetch -> inclusion matrix ->
    date resolution -> sort; always excludes the calendar note
- index.ts: JoplinDataAdapter with pagination; getEvents now returns real
  events plus warnings and scan stats
- Webview debug view shows real data: counts, warnings, colours, icons,
  strikethrough for completed todos, tooltip from text:
- 24 new unit tests (33 total)
2026-06-12 18:37:47 -04:00

110 lines
2.9 KiB
TypeScript

import { CalendarConfig, InclusionMode } from "./types";
const VIEW_ALIASES: Record<string, CalendarConfig["view"]> = {
day: "day",
d: "day",
week: "week",
w: "week",
month: "month",
m: "month",
};
const INCLUSION_MODES: InclusionMode[] = ["gtd-only", "all", "none"];
/**
* Normalise a parsed gtd-calendar YAML object into a CalendarConfig,
* applying SPEC.md defaults. Unknown keys and invalid values produce
* warnings rather than failures.
*/
export default function parseCalendarConfig(raw: any): CalendarConfig {
const warnings: string[] = [];
const input = raw && typeof raw === "object" ? raw : {};
const knownKeys = [
"view",
"title",
"scope",
"notes",
"todos",
"sort",
"sort-type",
];
for (const key of Object.keys(input)) {
if (!knownKeys.includes(key)) warnings.push(`Unknown option "${key}"`);
}
// view
let view: CalendarConfig["view"] = "day";
if (input.view !== undefined) {
const candidate = String(input.view).toLowerCase();
if (VIEW_ALIASES[candidate]) {
view = VIEW_ALIASES[candidate];
} else {
warnings.push(`Invalid view "${input.view}" (using "day")`);
}
}
// title
const title =
input.title !== undefined && input.title !== null
? String(input.title)
: null;
// scope
let scopeDepth = 0;
if (input.scope !== undefined) {
const value = input.scope;
if (value === "this-folder") {
scopeDepth = 0;
} else if (value === "children") {
scopeDepth = Infinity;
} else if (Number.isInteger(Number(value)) && Number(value) >= 0) {
scopeDepth = Number(value);
} else {
warnings.push(`Invalid scope "${value}" (using "this-folder")`);
}
}
// notes / todos
const notes = parseInclusion(input.notes, "notes", warnings);
const todos = parseInclusion(input.todos, "todos", warnings);
// sort
let sort: CalendarConfig["sort"] = "asc";
if (input.sort !== undefined) {
const candidate = String(input.sort).toLowerCase();
if (candidate === "asc" || candidate === "desc") {
sort = candidate;
} else {
warnings.push(`Invalid sort "${input.sort}" (using "asc")`);
}
}
// sort-type
let sortType: CalendarConfig["sortType"] = "title";
if (input["sort-type"] !== undefined) {
const candidate = String(input["sort-type"]).toLowerCase();
if (candidate === "title" || candidate === "modified_date") {
sortType = candidate as CalendarConfig["sortType"];
} else {
warnings.push(
`Invalid sort-type "${input["sort-type"]}" (using "title")`
);
}
}
return { view, title, scopeDepth, notes, todos, sort, sortType, warnings };
}
function parseInclusion(
value: any,
optionName: string,
warnings: string[]
): InclusionMode {
if (value === undefined) return "gtd-only";
const candidate = String(value).toLowerCase() as InclusionMode;
if (INCLUSION_MODES.includes(candidate)) return candidate;
warnings.push(`Invalid ${optionName} "${value}" (using "gtd-only")`);
return "gtd-only";
}