- 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)
110 lines
2.9 KiB
TypeScript
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";
|
|
}
|