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:
@@ -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 `<frontmatter name>'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
|
||||
|
||||
|
||||
+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