Syntax redesign: sharpen countdown/diff, symmetric frontmatter, relative dates
- Sharpen countdown vs diff: countdown is now always relative to now and
no longer accepts a 'from' field (previously identical math to diff
with a custom from, just different message framing).
- Add symmetric frontmatter fallback to countdown (to/deadline), diff
(from/start, to/end), and since (since/created) \u2014 previously only
birthday read frontmatter.
- Add relative date support to parseMoment: today, now, tomorrow,
yesterday, and +/-N offsets (d/w/mo/y/h/m), always relative to now.
Fixes diff's main weakness of needing hardcoded, staling dates.
- Make label verbatim everywhere (was auto-appending ': ' for countdown
only, inconsistent with birthday's verbatim convention).
- Fix silent inline failures: unresolvable type now shows an explicit
error ('Missing or unrecognized type') instead of rendering nothing,
matching fenced-block behavior. Also fixes 'from=X' alone (documented
since-alias) not inferring a type.
- Remove the 'YAML-like' comma-separated inline format from docs \u2014 it
isn't valid non-flow YAML and silently produced nothing; only
key=value and {flow-mapping} are documented now.
- README rewritten to match: field references, frontmatter docs per
type, and a new relative-dates section.
Verified against the real bundled logic (12 behavioral checks) and live
in Obsidian via CDP across both Live Preview and Reading View.
This commit is contained in:
+52
-13
@@ -27,11 +27,11 @@ export function calculateDateResult(
|
||||
case "birthday":
|
||||
return calculateBirthday(cfg, app, sourcePath, useVerbose);
|
||||
case "countdown":
|
||||
return calculateCountdown(cfg, useVerbose);
|
||||
return calculateCountdown(cfg, app, sourcePath, useVerbose);
|
||||
case "diff":
|
||||
return calculateDiff(cfg, useVerbose);
|
||||
return calculateDiff(cfg, app, sourcePath, useVerbose);
|
||||
case "since":
|
||||
return calculateSince(cfg, useVerbose);
|
||||
return calculateSince(cfg, app, sourcePath, useVerbose);
|
||||
default:
|
||||
return { text: "date-calc: Unknown type. Supported: birthday, countdown, diff, since" };
|
||||
}
|
||||
@@ -76,17 +76,29 @@ function calculateBirthday(
|
||||
|
||||
const ageStr = verbose ? `${age} years old` : `${age}y`;
|
||||
// Custom label > frontmatter-name personalization > plain "Age:" default.
|
||||
// Labels are always used verbatim, including their own trailing punctuation.
|
||||
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);
|
||||
/** Always relative to now — for a fixed two-point comparison, use `diff`. */
|
||||
function calculateCountdown(
|
||||
cfg: DateCalcConfig,
|
||||
app: App,
|
||||
sourcePath: string,
|
||||
verbose: boolean
|
||||
): DateCalcResult {
|
||||
const fm = app.metadataCache.getCache(sourcePath)?.frontmatter;
|
||||
|
||||
const to = parseMoment(
|
||||
cfg.to ?? cfg.date ?? cfg.until ??
|
||||
frontmatterField(fm, "to") ?? frontmatterField(fm, "deadline")
|
||||
);
|
||||
if (!to) return { text: `date-calc: Missing or invalid "to" date.` };
|
||||
|
||||
const from = parseMoment(cfg.from) ?? moment();
|
||||
const label = cfg.label ? `${cfg.label}: ` : "";
|
||||
const from = moment();
|
||||
const label = cfg.label ? `${cfg.label} ` : "";
|
||||
const span = formatSpan(from, to, verbose);
|
||||
|
||||
const text = !span.isNegative
|
||||
@@ -96,9 +108,21 @@ function calculateCountdown(cfg: DateCalcConfig, verbose: boolean): DateCalcResu
|
||||
return { text };
|
||||
}
|
||||
|
||||
function calculateDiff(cfg: DateCalcConfig, verbose: boolean): DateCalcResult {
|
||||
const from = parseMoment(cfg.from ?? cfg.start);
|
||||
const to = parseMoment(cfg.to ?? cfg.end);
|
||||
/** A neutral two-point comparison — for "time until X from now", use `countdown`. */
|
||||
function calculateDiff(
|
||||
cfg: DateCalcConfig,
|
||||
app: App,
|
||||
sourcePath: string,
|
||||
verbose: boolean
|
||||
): DateCalcResult {
|
||||
const fm = app.metadataCache.getCache(sourcePath)?.frontmatter;
|
||||
|
||||
const from = parseMoment(
|
||||
cfg.from ?? cfg.start ?? frontmatterField(fm, "from") ?? frontmatterField(fm, "start")
|
||||
);
|
||||
const to = parseMoment(
|
||||
cfg.to ?? cfg.end ?? frontmatterField(fm, "to") ?? frontmatterField(fm, "end")
|
||||
);
|
||||
if (!from || !to) return { text: `date-calc: Provide valid "from" and "to" dates.` };
|
||||
|
||||
const span = formatSpan(from, to, verbose);
|
||||
@@ -110,8 +134,18 @@ function calculateDiff(cfg: DateCalcConfig, verbose: boolean): DateCalcResult {
|
||||
return { text };
|
||||
}
|
||||
|
||||
function calculateSince(cfg: DateCalcConfig, verbose: boolean): DateCalcResult {
|
||||
const since = parseMoment(cfg.since ?? cfg.from ?? cfg.date);
|
||||
function calculateSince(
|
||||
cfg: DateCalcConfig,
|
||||
app: App,
|
||||
sourcePath: string,
|
||||
verbose: boolean
|
||||
): DateCalcResult {
|
||||
const fm = app.metadataCache.getCache(sourcePath)?.frontmatter;
|
||||
|
||||
const since = parseMoment(
|
||||
cfg.since ?? cfg.from ?? cfg.date ??
|
||||
frontmatterField(fm, "since") ?? frontmatterField(fm, "created")
|
||||
);
|
||||
if (!since) return { text: `date-calc: Missing or invalid "since" date.` };
|
||||
|
||||
const now = moment();
|
||||
@@ -137,7 +171,12 @@ export function processInlineCode(
|
||||
const cfg = parseConfig(paramsRaw);
|
||||
const norm = normalizeConfig(paramsRaw, cfg);
|
||||
|
||||
if (!norm.type) return { text: "" };
|
||||
if (!norm.type) {
|
||||
// Surface a visible error instead of silently rendering nothing — the
|
||||
// author clearly meant to invoke date-calc (the prefix matched), so a
|
||||
// vanished result is far more confusing than an explicit message.
|
||||
return { text: 'date-calc: Missing or unrecognized "type" (birthday/countdown/diff/since).' };
|
||||
}
|
||||
|
||||
return calculateDateResult(norm.type, norm.cfg, app, sourcePath, settings.verbose);
|
||||
}
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ export function normalizeConfig(
|
||||
if (resolved.birthday || resolved.birthdate) type = "birthday";
|
||||
else if ((resolved.from && resolved.to) || (resolved.start && resolved.end)) type = "diff";
|
||||
else if (resolved.to || resolved.until) type = "countdown";
|
||||
else if (resolved.since) type = "since";
|
||||
else if (resolved.since || resolved.from) type = "since";
|
||||
}
|
||||
|
||||
if (type === "bday") type = "birthday";
|
||||
|
||||
+29
-2
@@ -1,9 +1,22 @@
|
||||
import { moment } from "obsidian";
|
||||
import type { M } from "./types";
|
||||
|
||||
/** Relative-offset shorthand: +7d, -3mo, +2w, etc. (always relative to now). */
|
||||
const RELATIVE_RE = /^([+-]\d+)\s*(d|days?|w|weeks?|mo|months?|y|years?|h|hours?|m|min|minutes?)$/i;
|
||||
|
||||
const RELATIVE_UNIT: Record<string, moment.unitOfTime.DurationConstructor> = {
|
||||
d: "days", day: "days", days: "days",
|
||||
w: "weeks", week: "weeks", weeks: "weeks",
|
||||
mo: "months", month: "months", months: "months",
|
||||
y: "years", year: "years", years: "years",
|
||||
h: "hours", hour: "hours", hours: "hours",
|
||||
m: "minutes", min: "minutes", minute: "minutes", minutes: "minutes",
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a date-ish input (frontmatter Date object, "YYYY-MM-DD" string, or any
|
||||
* moment-parseable string) into a moment. Returns null if unparseable.
|
||||
* Parse a date-ish input (frontmatter Date object, "YYYY-MM-DD" string, a
|
||||
* relative keyword/offset, or any moment-parseable string) into a moment.
|
||||
* Returns null if unparseable.
|
||||
*/
|
||||
export function parseMoment(input: unknown): M | null {
|
||||
if (input == null || input === "") return null;
|
||||
@@ -29,6 +42,20 @@ export function parseMoment(input: unknown): M | null {
|
||||
}
|
||||
|
||||
const s = String(input).trim();
|
||||
const lower = s.toLowerCase();
|
||||
|
||||
if (lower === "now") return moment();
|
||||
if (lower === "today") return moment().startOf("day");
|
||||
if (lower === "tomorrow") return moment().add(1, "day").startOf("day");
|
||||
if (lower === "yesterday") return moment().subtract(1, "day").startOf("day");
|
||||
|
||||
const rel = RELATIVE_RE.exec(s);
|
||||
if (rel) {
|
||||
const amount = parseInt(rel[1], 10);
|
||||
const unit = RELATIVE_UNIT[rel[2].toLowerCase()];
|
||||
return moment().add(amount, unit);
|
||||
}
|
||||
|
||||
const dateOnly = moment(s, "YYYY-MM-DD", true);
|
||||
if (dateOnly.isValid()) return dateOnly.startOf("day");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user