joplin-plugin-gtd-calendar/src/Gtd/parseCalendarConfig.ts

181 lines
4.6 KiB
TypeScript

import { CalendarConfig, InclusionMode } from "./types";
import {
parseExcludeNotebookOption,
parseNotebookOption,
} from "./resolveNotebook";
const VIEW_ALIASES: Record<string, CalendarConfig["view"]> = {
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";
}