1.1.0: fix Live Preview crash, add Reading View inline support, birthday labels

- 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).
This commit is contained in:
2026-09-07 11:45:45 -04:00
parent 978181b27d
commit 526f54a640
14 changed files with 784 additions and 746 deletions
+159
View File
@@ -0,0 +1,159 @@
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);
}