import { CalendarConfig, InclusionMode } from "./types"; import { parseExcludeNotebookOption, parseNotebookOption, } from "./resolveNotebook"; const VIEW_ALIASES: Record = { day: "day", d: "day", week: "week", w: "week", month: "month", m: "month", "month-grid": "month-grid", grid: "month-grid", }; 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", "notebook", "exclude-notebook", "notes", "todos", "sort", "sort-type", "unscheduled-todos", "unscheduled-notes", ]; 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; let scopeAll = false; if (input.scope !== undefined) { const value = input.scope; if (value === "all") { scopeAll = true; } else 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")`); } } // notebook (id / title / Parent/Child path); resolved later against the // folder tree. Empty → null (host folder). Ignored under scope: all. const notebook = parseNotebookOption(input.notebook); const excludeNotebooks = parseExcludeNotebookOption( input["exclude-notebook"], warnings ); if (scopeAll && notebook) { warnings.push( `"notebook" is ignored when scope: all (scanning every notebook)` ); } // 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")` ); } } // Unscheduled visibility toggles (default: shown). const unscheduledTodos = parseBoolean( input["unscheduled-todos"], "unscheduled-todos", warnings ); const unscheduledNotes = parseBoolean( input["unscheduled-notes"], "unscheduled-notes", warnings ); return { view, title, scopeDepth, scopeAll, notebook, excludeNotebooks, notes, todos, sort, sortType, unscheduledTodos, unscheduledNotes, warnings, }; } /** * Parse a permissive boolean: yes/no, true/false, show/hide (case- * insensitive). Absent → true (sections shown by default). Invalid → * true, with a warning. */ function parseBoolean( value: any, optionName: string, warnings: string[] ): boolean { if (value === undefined || value === null) return true; if (typeof value === "boolean") return value; const candidate = String(value).trim().toLowerCase(); if (["yes", "true", "show", "on"].includes(candidate)) return true; if (["no", "false", "hide", "off"].includes(candidate)) return false; warnings.push(`Invalid ${optionName} "${value}" (using "yes")`); return true; } 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"; }