import { App, moment } from "obsidian"; import { formatSpan, parseMoment } from "./moment-utils"; import { normalizeConfig, parseConfig, parseYamlConfig } from "./config"; import type { DateCalcConfig, DateCalcResult, DateCalcSettings } from "./types"; /** Read a string or Date value out of frontmatter, ignoring any other shape. */ function frontmatterField( fm: Record | undefined, key: string ): string | Date | undefined { const value = fm?.[key]; return typeof value === "string" || value instanceof Date ? value : undefined; } /** Dispatch to the calculator for `type`, catching and surfacing any error inline. */ export function calculateDateResult( type: string, cfg: DateCalcConfig, app: App, sourcePath: string, verbose: boolean ): DateCalcResult { const useVerbose = cfg.verbose !== undefined ? cfg.verbose : verbose; try { switch (type) { case "birthday": return calculateBirthday(cfg, app, sourcePath, useVerbose); case "countdown": return calculateCountdown(cfg, useVerbose); case "diff": return calculateDiff(cfg, useVerbose); case "since": return calculateSince(cfg, useVerbose); default: return { text: "date-calc: Unknown type. Supported: birthday, countdown, diff, since" }; } } catch (e: unknown) { const message = e instanceof Error ? e.message : String(e); return { text: `date-calc error: ${message}` }; } } function calculateBirthday( cfg: DateCalcConfig, app: App, sourcePath: string, verbose: boolean ): DateCalcResult { const fm = app.metadataCache.getCache(sourcePath)?.frontmatter; const bstr = cfg.birthday ?? cfg.birthdate ?? cfg.date ?? frontmatterField(fm, "birthday") ?? frontmatterField(fm, "birthdate"); const bd = parseMoment(bstr)?.startOf("day"); if (!bd) return { text: `date-calc: Missing or invalid "birthday" date.` }; const today = moment().startOf("day"); const age = today.diff(bd, "years"); const next = bd.clone().year(today.year()); if (next.isBefore(today, "day")) next.add(1, "year"); const daysUntil = next.diff(today, "days"); let msg: string; if (daysUntil === 0) { msg = verbose ? "Wish them Happy Birthday!" : "Happy bday!"; } else if (daysUntil === 1) { msg = verbose ? "Their birthday is tomorrow!" : "Bday's tomorrow!"; } else if (daysUntil > 31) { const monthsUntil = next.diff(today, "months"); msg = verbose ? `Next birthday in ${monthsUntil} months.` : `Next bday in ${monthsUntil}mo`; } else { msg = verbose ? `Next birthday in ${daysUntil} days.` : `Next bday in ${daysUntil}d`; } const ageStr = verbose ? `${age} years old` : `${age}y`; // Custom label > frontmatter-name personalization > plain "Age:" default. const name = frontmatterField(fm, "name"); const label = cfg.label ?? (typeof name === "string" ? `${name}'s age:` : "Age:"); return { text: `${label} ${ageStr}. ${msg}`, tooltip: bd.format("MMMM Do, YYYY") }; } function calculateCountdown(cfg: DateCalcConfig, verbose: boolean): DateCalcResult { const to = parseMoment(cfg.to ?? cfg.date ?? cfg.until); if (!to) return { text: `date-calc: Missing or invalid "to" date.` }; const from = parseMoment(cfg.from) ?? moment(); const label = cfg.label ? `${cfg.label}: ` : ""; const span = formatSpan(from, to, verbose); const text = !span.isNegative ? `${label}${verbose ? "Countdown: " : ""}${span.text}` : `${label}${verbose ? "Event passed " : ""}${span.text}${verbose ? " ago" : ""}`; return { text }; } function calculateDiff(cfg: DateCalcConfig, verbose: boolean): DateCalcResult { const from = parseMoment(cfg.from ?? cfg.start); const to = parseMoment(cfg.to ?? cfg.end); if (!from || !to) return { text: `date-calc: Provide valid "from" and "to" dates.` }; const span = formatSpan(from, to, verbose); const text = verbose ? `Difference: ${span.text}${span.isNegative ? " (to is before from)" : ""}` : `Diff: ${span.text}${span.isNegative ? " (reverse)" : ""}`; return { text }; } function calculateSince(cfg: DateCalcConfig, verbose: boolean): DateCalcResult { const since = parseMoment(cfg.since ?? cfg.from ?? cfg.date); if (!since) return { text: `date-calc: Missing or invalid "since" date.` }; const now = moment(); const span = formatSpan(since, now, verbose); const text = !span.isNegative ? `Since: ${span.text}${verbose ? " ago" : ""}` : `In: ${span.text}`; return { text }; } /** Parse and evaluate a `date-calc: ...` inline code span's raw text. */ export function processInlineCode( raw: string, app: App, sourcePath: string, settings: DateCalcSettings ): DateCalcResult { if (!/^date-calc\s*:/.test(raw)) return { text: "" }; const paramsRaw = raw.replace(/^date-calc\s*:/, "").trim(); const cfg = parseConfig(paramsRaw); const norm = normalizeConfig(paramsRaw, cfg); if (!norm.type) return { text: "" }; return calculateDateResult(norm.type, norm.cfg, app, sourcePath, settings.verbose); } /** Parse and evaluate a fenced ```date-calc``` block's YAML body. */ export function processFencedBlock( source: string, app: App, sourcePath: string, settings: DateCalcSettings ): DateCalcResult { const cfg = parseYamlConfig(source); const norm = normalizeConfig("", cfg); if (!norm.type) { return { text: 'date-calc: Missing "type" (birthday/countdown/diff/since).' }; } return calculateDateResult(norm.type, norm.cfg, app, sourcePath, settings.verbose); }