3 Commits

Author SHA1 Message Date
olivier 3df01c6ede 1.2.0 2026-09-07 12:11:03 -04:00
olivier 537198c4f1 Add remaining type aliases, rewrite settings tab as syntax TL;DR
- Add age (birthday), remaining (countdown), elapsed (since) aliases,
  alongside the existing bday/until/difference.
- README: document new aliases in each type's header and the Type
  Aliases section.
- Settings tab rewritten: drop the stale GitHub doc link and hardcoded
  pre-redesign examples (countdown+from, auto-colon labels). Replace
  with a compact syntax TL;DR \u2014 all four types with their aliases,
  fields, and one-line descriptions, plus relative dates, frontmatter
  fallback, and inline/fenced examples \u2014 and link to the README on
  Gitea (was pointing at a stale GitHub URL).

Verified: aliases tested against the real shipped config parser; the
settings tab's actual display() (found via app.setting.pluginTabs) was
invoked live in Obsidian and rendered exactly the intended content with
no errors.
2026-09-07 12:09:55 -04:00
olivier 68e7b3ab04 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.
2026-09-07 12:04:35 -04:00
8 changed files with 194 additions and 135 deletions
+45 -77
View File
@@ -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`
@@ -27,16 +21,16 @@ Use inline code for quick calculations: `date-calc: birthday=1992-08-16`
## Supported Calculation Types
### Birthday (`birthday`)
### Birthday (`birthday`, `bday`, `age`)
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`, `until`, `remaining`)
### 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`, `elapsed`)
### 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`, `difference`)
### 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,70 +154,44 @@ 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:
- `birthday` = `bday`
- `countdown` = `until`
- `birthday` = `bday` = `age`
- `countdown` = `until` = `remaining`
- `since` = `elapsed`
- `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 +211,7 @@ Target these CSS classes in your snippets:
}
```
## Commands
- **Toggle debug** — Log decoration activity to console
- **Toggle verbose** — Switch global verbose mode
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "date-calc",
"name": "Date calculator",
"version": "1.1.0",
"version": "1.2.0",
"minAppVersion": "0.15.0",
"description": "Calculate ages, countdowns, and date differences dynamically in your notes using inline code or fenced blocks.",
"author": "Olivier Legendre",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "date-calculator",
"version": "1.1.0",
"version": "1.2.0",
"description": "Calculate ages, countdowns, and date differences dynamically in your notes using inline code or fenced blocks.",
"main": "main.js",
"scripts": {
+52 -13
View File
@@ -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);
}
+4 -3
View File
@@ -84,12 +84,13 @@ 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";
if (type === "until") type = "countdown";
if (type === "bday" || type === "age") type = "birthday";
if (type === "until" || type === "remaining") type = "countdown";
if (type === "difference") type = "diff";
if (type === "elapsed") type = "since";
return { type, cfg: resolved };
}
+29 -2
View File
@@ -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");
+60 -37
View File
@@ -1,6 +1,8 @@
import { App, PluginSettingTab, Setting } from "obsidian";
import type DateCalcPlugin from "../main";
const README_URL = "https://gitea.mithrilforge.dev/olivier/date-calculator";
export class DateCalcSettingTab extends PluginSettingTab {
constructor(app: App, private plugin: DateCalcPlugin) {
super(app, plugin);
@@ -12,14 +14,10 @@ export class DateCalcSettingTab extends PluginSettingTab {
containerEl.createEl("h2", { text: "Date Calc" });
// Documentation link
const docsDiv = containerEl.createDiv({
cls: "setting-item-description date-calc-settings-docs",
});
docsDiv.createEl("a", {
text: "📖 View Full Documentation",
href: "https://github.com/thelegend09/date-calculator",
});
docsDiv.createEl("a", { text: "📖 Full syntax reference (README)", href: README_URL });
new Setting(containerEl)
.setName("Debug logging")
@@ -53,43 +51,68 @@ export class DateCalcSettingTab extends PluginSettingTab {
})
);
// Usage examples section
containerEl.createEl("h3", { text: "Usage Examples" });
containerEl.createEl("h3", { text: "Syntax at a glance" });
const tldr = containerEl.createDiv({ cls: "date-calc-examples" });
const examplesDiv = containerEl.createDiv({ cls: "date-calc-examples" });
tldr.createEl("p", {
text: "Use `date-calc: ...` inline, or a fenced ```date-calc block with type: set explicitly (or inferred from which fields you give).",
});
// Inline examples
examplesDiv.createEl("h4", { text: "Inline Code (single backticks)" });
const inlineList = examplesDiv.createEl("ul");
inlineList.createEl("li").setText("`date-calc: birthday=2003-02-15`");
inlineList.createEl("li").setText("`date-calc: countdown to=2026-12-31 label=\"New Year\"`");
inlineList.createEl("li").setText("`date-calc: since=2024-01-01`");
inlineList.createEl("li").setText("`date-calc: diff from=2024-01-01 to=2024-12-31`");
inlineList.createEl("li").setText("`date-calc: birthday=1992-08-16 verbose=true`");
// Types table
const typesList = tldr.createEl("ul");
const addType = (name: string, aliases: string, desc: string, fields: string) => {
const li = typesList.createEl("li");
li.createEl("strong", { text: name });
li.appendText(` (${aliases}) — ${desc} `);
li.createEl("br");
li.createEl("code", { text: fields });
};
addType(
"birthday", "bday, age",
"age + days until next birthday.",
"birthday/birthdate/date, label"
);
addType(
"countdown", "until, remaining",
"time until a date, always from now.",
"to/until/date, label"
);
addType(
"since", "elapsed",
"time since a date, always from now.",
"since/from/date"
);
addType(
"diff", "difference",
"time between two fixed dates (neither has to be now).",
"from/start, to/end"
);
// Block examples
examplesDiv.createEl("h4", { text: "Fenced Blocks (triple backticks)" });
tldr.createEl("h4", { text: "Relative dates" });
tldr.createEl("p", {
text: "today, now, tomorrow, yesterday, or an offset like +7d / -3mo / +2w (always relative to now).",
});
examplesDiv
tldr.createEl("h4", { text: "Frontmatter fallback" });
tldr.createEl("p", {
text: "Every type reads its date(s) from frontmatter if not given inline: birthday/birthdate, to/deadline, since/created, from/start + to/end.",
});
tldr.createEl("h4", { text: "Examples" });
const examplesList = tldr.createEl("ul");
examplesList.createEl("li").createEl("code", { text: "date-calc: birthday=1992-08-16" });
examplesList.createEl("li").createEl("code", { text: "date-calc: to=2026-12-31 label=\"New Year:\"" });
examplesList.createEl("li").createEl("code", { text: "date-calc: since=2024-01-01" });
examplesList.createEl("li").createEl("code", { text: "date-calc: from=2024-01-01 to=today" });
examplesList
.createEl("li")
.createEl("pre", { cls: "date-calc-example-block" })
.setText("```date-calc\ntype: birthday\nbirthday: 1992-08-16\n```");
.setText("```date-calc\ntype: countdown\nto: +30d\nlabel: \"Sprint:\"\nverbose: true\n```");
examplesDiv
.createEl("pre", { cls: "date-calc-example-block" })
.setText(
"```date-calc\ntype: countdown\nto: 2026-12-31 23:59\nlabel: New Year\nverbose: true\n```"
);
examplesDiv
.createEl("pre", { cls: "date-calc-example-block" })
.setText("```date-calc\ntype: diff\nfrom: 2024-01-01\nto: 2024-12-31\n```");
// Tips section
examplesDiv.createEl("h4", { text: "Tips" });
const tipsList = examplesDiv.createEl("ul");
tipsList.createEl("li").setText("Use YYYY-MM-DD format for dates");
tipsList.createEl("li").setText("Add time with YYYY-MM-DD HH:mm for precise countdowns");
tipsList.createEl("li").setText("Supported types: birthday, countdown, since, diff");
tipsList.createEl("li").setText("Add verbose=true/false to override global setting");
const fullDocs = tldr.createEl("p");
fullDocs.appendText("Full field reference, custom styling, and more examples: ");
fullDocs.createEl("a", { text: "README on Gitea", href: README_URL });
fullDocs.appendText(".");
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
{
"1.0.0": "0.15.0",
"1.1.0": "0.15.0"
"1.1.0": "0.15.0",
"1.2.0": "0.15.0"
}