joplin-plugin-gtd-calendar/src/Gtd/parseMatrixConfig.ts
Victor Wiebe 187fd34531 v0.7.0: Gantt chart, notebook targeting & profile-wide scope
Single release covering two feature groups (CHANGELOG 0.7.0 + 0.6.0):

- gtd-gantt block: project-grouped timeline of bars (notes with begin/end
  dates) and milestones (to-dos on their due date); dual-role fence,
  swimlanes, month tick header, today line, read-only.
- notebook: targeting across all four view blocks; scope: all for
  profile-wide scans with size guardrails; note body fetched only when the
  inclusion mode actually needs the gtd block.

jest 140/140.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 12:11:45 -04:00

177 lines
4.7 KiB
TypeScript

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;
}