526f54a640
- Fix RangeSetBuilder ordering crash: inline and fenced-block decorations
are now merged and sorted before being added, instead of two unsorted
passes (crashed whenever a fenced block preceded inline code in a note).
- Add explicit @codemirror/state and @codemirror/view devDependencies
(previously only transitive via @codemirror/language).
- Add Reading View support for inline `date-calc:` spans via
registerMarkdownPostProcessor (previously Live Preview only), mirroring
the approach used by Dataview's inline queries.
- Fix verbose=false being silently truthy (non-empty string) in inline
key=value config, which meant it never actually disabled verbose output.
- Fix diff type auto-inference: from/to was dead code (countdown's check
always matched first); reordered so from+to or start+end infer diff.
- Implement documented but missing birthday label support: cfg.label,
falling back to frontmatter name ("Jane's age:"), then "Age:".
- Split main.ts (777 lines) into src/ modules per AGENTS.md conventions.
- Fix duplicated command-palette prefixes, move inline JS styles to
styles.css, fix package.json author, drop orphaned showArrow setting.
Verified: unit tests against the real bundled logic (mocked obsidian
module), a reproduction of the RangeSetBuilder crash against the real
@codemirror/state package, and live end-to-end testing in Obsidian via
CDP/Playwright (Tchernobyl vault).
160 lines
5.4 KiB
TypeScript
160 lines
5.4 KiB
TypeScript
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<string, unknown> | 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);
|
|
}
|