diff --git a/README.md b/README.md index 3edd3dd..0327c08 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,6 @@ An Obsidian plugin that adds dynamic date calculations to your notes using `date-calc` fenced code blocks and inline code. - -## Todo -- [ ] Add custom language (default to Obsidian language, overridable) - - ## Features ### Fenced Code Blocks @@ -18,7 +13,6 @@ type: birthday birthday: 1992-08-16 ``` - ### Inline Code Use inline code for quick calculations: `date-calc: birthday=1992-08-16` @@ -29,14 +23,14 @@ Use inline code for quick calculations: `date-calc: birthday=1992-08-16` ### Birthday (`birthday`) -Calculates age and time until next birthday, with an optional custom label and automatic personalization from your note's frontmatter. +Calculates age and time until next birthday. **Fields:** -- `birthday` (or `birthdate`, `date`): Birth date in YYYY-MM-DD format -- `label` (optional): Custom label (defaults to "Age:") +- `birthday` (or `birthdate`, `date`): Birth date +- `label` (optional): Full prefix text, used verbatim — include your own punctuation (defaults to `Age:`) -**Frontmatter Support:** If not provided in the code block, the plugin reads `birthday`/`birthdate` from the note's frontmatter. If no `label` is set, a frontmatter `name` property becomes the default label automatically (e.g. `name: John` → "John's age:"). +**Frontmatter Support:** If not provided in the code block, reads `birthday`/`birthdate` from the note's frontmatter. If no `label` is set, a frontmatter `name` property personalizes the default label automatically (e.g. `name: John` → `John's age:`). **Examples:** @@ -51,41 +45,35 @@ birthday: 2000-05-21 label: "John's age:" ``` - ### Countdown (`countdown` or `until`) -Shows time remaining until a target date. +Time remaining until a target date, **always measured from now**. For a comparison between two fixed dates, use `diff` instead. **Fields:** - `to` (or `date`, `until`): Target date/time -- `from` (optional): Starting date/time (defaults to now) -- `label` (optional): Prefix label for the output +- `label` (optional): Full prefix text, used verbatim (e.g. `label: "New Year:"`) -**Examples:** +**Frontmatter Support:** If `to` isn't provided in the code block, reads `to`/`deadline` from the note's frontmatter. + +**Example:** ```date-calc type: countdown -label: New Year +label: "New Year:" to: 2025-12-31 23:59 ``` -```date-calc -type: countdown -label: Sprint ends -to: 2025-10-15 17:00 -from: 2025-10-01 09:00 -``` - - ### Since (`since`) -Shows time elapsed since an event (or time until if the event is in the future). +Time elapsed since an event (or time until, if the event is in the future) — always measured against now. **Fields:** - `since` (or `from`, `date`): Event date/time +**Frontmatter Support:** If not provided in the code block, reads `since`/`created` from the note's frontmatter. + **Example:** ```date-calc @@ -93,16 +81,17 @@ type: since since: 2024-12-20 ``` - ### Difference (`diff`) -Calculates the time difference between two specific dates. +The time difference between two fixed dates — neither has to be "now". For a countdown relative to the present moment, use `countdown` instead. **Fields:** - `from` (or `start`): Start date/time - `to` (or `end`): End date/time +**Frontmatter Support:** If not provided in the code block, reads `from`/`start` and `to`/`end` from the note's frontmatter. + **Examples:** ```date-calc @@ -117,25 +106,32 @@ from: 2025-09-19 08:00 to: 2025-09-19 16:30 ``` - ## Inline Usage You can use inline code for quick calculations: - `date-calc: birthday=1992-08-16` → Birthday summary -- `date-calc: to=2025-12-31 label="New Year"` → Countdown +- `date-calc: to=2025-12-31 label="New Year:"` → Countdown - `date-calc: since=2024-12-20` → Time since event - `date-calc: from=2025-09-19 to=2025-09-20` → Date difference -**YAML Format:** Inline code also accepts YAML/inline-map format: -`date-calc: {type: birthday, birthday: 1992-08-16}` +**Formats:** Inline code accepts key=value pairs (`birthday=1992-08-16`) or a flow-mapping YAML object (`{type: birthday, birthday: 1992-08-16}`). -**Frontmatter Integration:** For inline birthday calculations, if no date is provided, the plugin will use the note's frontmatter `birthday`/`birthdate` property. +**Frontmatter Integration:** Applies to every type, not just birthday — see each type's "Frontmatter Support" above. ## Date Formats -- **Timezone:** All calculations use your local timezone -- **Birthday Calculations:** Normalized to local midnight to avoid timezone inconsistencies +| Format | Example | Notes | +| :------------------ | :------------------------------ | :----------------------------------------- | +| Date only | `2026-12-31` | — | +| Date + time | `2026-12-31 23:59` | — | +| ISO 8601 | `2026-12-31T23:59:00` | — | +| Relative keyword | `today`, `now`, `tomorrow`, `yesterday` | Resolved at render time | +| Relative offset | `+7d`, `-3mo`, `+2w`, `-1y` | Units: `d`, `w`, `mo`, `y`, `h`, `m` (minutes) — always relative to now | + +All dates use your local timezone. Birthday calculations normalize to midnight to avoid timezone edge cases; `today`/relative keywords resolve to your local time at render. + +Relative dates are most useful with `diff`, since `countdown`/`since` are already anchored to now by default — e.g. `type: diff` with `from: today` and `to: +30d` gives a fixed 30-day window that doesn't need updating. ## Configuration Options @@ -158,50 +154,35 @@ verbose: true - `verbose: true` → "Countdown: 1 year, 2 months, 15 days" - `verbose: false` → "1y 2mo 15d" - ### Hide While Editing Settings → "Hide result while cursor inside" prevents rendered output from showing while you're editing the code, keeping the raw syntax visible. -## Date Formats - -| Format | Example | Use Case | -| :---------- | :-------------------- | :-------------------- | -| Date only | `2026-12-31` | Birthdays, countdowns | -| Date + time | `2026-12-31 23:59` | Precise countdowns | -| ISO 8601 | `2026-12-31T23:59:00` | Also supported | - -All dates use your local timezone. Birthday calculations normalize to midnight to avoid timezone edge cases. - ## Field Reference ### Birthday -- `birthday` / `birthdate` / `date` — Birth date -- `verbose` — Override output format (optional) - +- `birthday` / `birthdate` / `date` — Birth date (falls back to frontmatter `birthday`/`birthdate`) +- `label` — Full prefix, verbatim (defaults to `Age:`, or `'s age:` if set) +- `verbose` — Override output format ### Countdown -- `to` / `until` / `date` — Target date/time -- `from` — Start date (defaults to now) -- `label` — Custom prefix +- `to` / `until` / `date` — Target date/time (falls back to frontmatter `to`/`deadline`) +- `label` — Full prefix, verbatim - `verbose` — Override output format - ### Since -- `since` / `from` / `date` — Event date/time +- `since` / `from` / `date` — Event date/time (falls back to frontmatter `since`/`created`) - `verbose` — Override output format - ### Diff -- `from` / `start` — Start date/time -- `to` / `end` — End date/time +- `from` / `start` — Start date/time (falls back to frontmatter `from`/`start`) +- `to` / `end` — End date/time (falls back to frontmatter `to`/`end`) - `verbose` — Override output format - ## Type Aliases For convenience, use these shortcuts: @@ -210,18 +191,6 @@ For convenience, use these shortcuts: - `countdown` = `until` - `diff` = `difference` - -## Inline Syntax Formats - -All three formats work: - -**Key=value:** ```date-calc: birthday=1992-08-16``` -**YAML-like:** ```date-calc: type: birthday, birthday: 1992-08-16``` -**Compact:** ```date-calc: {type: birthday, birthday: 1992-08-16}``` - - - - ## Custom Styling Target these CSS classes in your snippets: @@ -241,9 +210,7 @@ Target these CSS classes in your snippets: } ``` - ## Commands - **Toggle debug** — Log decoration activity to console - **Toggle verbose** — Switch global verbose mode - diff --git a/src/calculate.ts b/src/calculate.ts index 14a11f0..f4861da 100644 --- a/src/calculate.ts +++ b/src/calculate.ts @@ -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); } diff --git a/src/config.ts b/src/config.ts index 819ee66..516f746 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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"; diff --git a/src/moment-utils.ts b/src/moment-utils.ts index 94e60bc..31911dd 100644 --- a/src/moment-utils.ts +++ b/src/moment-utils.ts @@ -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 = { + 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");