181 lines
4.6 KiB
TypeScript
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";
|
|
}
|