import { InclusionMode, KanbanSortType, MatrixConfig, MatrixMode, URGENT_TAG, IMPORTANT_TAG, IN_PROGRESS_TAG, } from "./types"; import { parseNotebookOption } from "./resolveNotebook"; const INCLUSION_MODES: InclusionMode[] = ["gtd-only", "all", "none"]; const SORT_TYPES: KanbanSortType[] = ["due-date", "title", "modified-date"]; const CARD_DETAILS = ["hover", "always", "none"]; /** * Normalise a parsed gtd-matrix YAML object into a MatrixConfig with * SPEC defaults. Invalid values produce warnings, not failures. */ export default function parseMatrixConfig(raw: any): MatrixConfig { const warnings: string[] = []; const input = raw && typeof raw === "object" ? raw : {}; const knownKeys = [ "mode", "title", "scope", "notebook", "todos", "sort-type", "sort", "urgent-tag", "important-tag", "in-progress-tag", "urgent-window", "card-detail", ]; for (const key of Object.keys(input)) { if (!knownKeys.includes(key)) warnings.push(`Unknown option "${key}"`); } // mode let mode: MatrixMode = "skeleton"; if (input.mode !== undefined) { const candidate = String(input.mode).toLowerCase(); if (candidate === "skeleton" || candidate === "eisenhower") { mode = candidate as MatrixMode; } else { warnings.push(`Invalid mode "${input.mode}" (using "skeleton")`); } } const title = input.title !== undefined && input.title !== null ? String(input.title) : null; // scope (shared semantics with gtd-calendar / gtd-kanban) 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")`); } } const notebook = parseNotebookOption(input.notebook); if (scopeAll && notebook) { warnings.push( `"notebook" is ignored when scope: all (scanning every notebook)` ); } // todos inclusion let todos: InclusionMode = "gtd-only"; if (input.todos !== undefined) { const candidate = String(input.todos).toLowerCase() as InclusionMode; if (INCLUSION_MODES.includes(candidate)) { todos = candidate; } else { warnings.push(`Invalid todos "${input.todos}" (using "gtd-only")`); } } // sort-type let sortType: KanbanSortType = "due-date"; if (input["sort-type"] !== undefined) { let candidate = String(input["sort-type"]).toLowerCase(); if (candidate === "modified_date") candidate = "modified-date"; // alias if (SORT_TYPES.includes(candidate as KanbanSortType)) { sortType = candidate as KanbanSortType; } else { warnings.push( `Invalid sort-type "${input["sort-type"]}" (using "due-date")` ); } } // sort direction let sort: MatrixConfig["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")`); } } // axis tags (configurable; lowercased for tag comparison) const urgentTag = parseTag(input["urgent-tag"], URGENT_TAG); const importantTag = parseTag(input["important-tag"], IMPORTANT_TAG); if (urgentTag === importantTag) { warnings.push( `urgent-tag and important-tag are both "${urgentTag}" — quadrants will not separate` ); } // skeleton mode: in-progress tag + urgent window const inProgressTag = parseTag(input["in-progress-tag"], IN_PROGRESS_TAG); let urgentWindow = 3; if (input["urgent-window"] !== undefined) { const value = input["urgent-window"]; if (Number.isInteger(Number(value)) && Number(value) >= 0) { urgentWindow = Number(value); } else { warnings.push(`Invalid urgent-window "${value}" (using 3)`); } } // card-detail let cardDetail: MatrixConfig["cardDetail"] = "hover"; if (input["card-detail"] !== undefined) { const candidate = String(input["card-detail"]).toLowerCase(); if (CARD_DETAILS.includes(candidate)) { cardDetail = candidate as MatrixConfig["cardDetail"]; } else { warnings.push( `Invalid card-detail "${input["card-detail"]}" (using "hover")` ); } } return { mode, title, scopeDepth, scopeAll, notebook, todos, sortType, sort, urgentTag, importantTag, inProgressTag, urgentWindow, cardDetail, warnings, }; } function parseTag(value: any, fallback: string): string { if (value === undefined || value === null) return fallback; const candidate = String(value).trim().toLowerCase(); return candidate === "" ? fallback : candidate; }