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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user