WIP. inline doesn't work, but the rest does.

This commit is contained in:
2025-09-19 20:52:30 -04:00
parent e2a64e0534
commit ee7be363cc
5 changed files with 3228 additions and 104 deletions

371
README.md
View File

@@ -1,94 +1,295 @@
# Obsidian Sample Plugin
# Date Calc
This is a sample plugin for Obsidian (https://obsidian.md).
This plugin adds a `date-calc` fenced code block you can embed in your notes to render date calculations.
This project uses TypeScript to provide type checking and documentation.
The repo depends on the latest plugin API (obsidian.d.ts) in TypeScript Definition format, which contains TSDoc comments describing what it does.
Example (birthday):
This sample plugin demonstrates some of the basic functionality the plugin API can do.
- Adds a ribbon icon, which shows a Notice when clicked.
- Adds a command "Open Sample Modal" which opens a Modal.
- Adds a plugin setting tab to the settings page.
- Registers a global click event and output 'click' to the console.
- Registers a global interval which logs 'setInterval' to the console.
## First time developing plugins?
Quick starting guide for new plugin devs:
- Check if [someone already developed a plugin for what you want](https://obsidian.md/plugins)! There might be an existing plugin similar enough that you can partner up with.
- Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it).
- Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder.
- Install NodeJS, then run `npm i` in the command line under your repo folder.
- Run `npm run dev` to compile your plugin from `main.ts` to `main.js`.
- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`.
- Reload Obsidian to load the new version of your plugin.
- Enable plugin in settings window.
- For updates to the Obsidian API run `npm update` in the command line under your repo folder.
## Releasing new releases
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release.
- Publish the release.
> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`.
> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json`
## Adding your plugin to the community plugin list
- Check the [plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines).
- Publish an initial version.
- Make sure you have a `README.md` file in the root of your repo.
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
## How to use
- Clone this repo.
- Make sure your NodeJS is at least v16 (`node --version`).
- `npm i` or `yarn` to install dependencies.
- `npm run dev` to start compilation in watch mode.
## Manually installing the plugin
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
## Improve code quality with eslint (optional)
- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code.
- To use eslint with this project, make sure to install eslint from terminal:
- `npm install -g eslint`
- To use eslint to analyze this project use this command:
- `eslint main.ts`
- eslint will then create a report with suggestions for code improvement by file and line number.
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder:
- `eslint ./src/`
## Funding URL
You can include funding URLs where people who use your plugin can financially support it.
The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file:
```json
{
"fundingUrl": "https://buymeacoffee.com"
}
```date-calc
type: birthday
birthday: 1992-08-16
```
If you have multiple URLs, you can also do:
Supported types and fields:
```json
{
"fundingUrl": {
"Buy Me a Coffee": "https://buymeacoffee.com",
"GitHub Sponsor": "https://github.com/sponsors",
"Patreon": "https://www.patreon.com/"
}
}
- birthday
- birthday (or birthdate or date): YYYY-MM-DD
- If not provided in the block/inline config, the plugin will also look for a `birthday` (or `birthdate`) property in the notes frontmatter.
- Output: Age in years and time until next birthday (days, or months if > 31 days, with half-month rounding)
- countdown / until
- to (or date or until): target date/time
- from (optional): starting date/time (defaults to now)
- label (optional): prefix label
- Output: Humanized time until target, or how long ago if past
- diff
- from (or start): start date/time
- to (or end): end date/time
- Output: Humanized difference between the two dates
- since
- since (or from or date): date/time of the event
- Output: Humanized time since that date (or time until if its in the future)
Notes:
- Dates are interpreted in your local timezone. For birthdays, calculations use local midnight to avoid timezone inconsistencies.
- YAML inside the code block must be valid. The plugin will show an error if the YAML cannot be parsed.
More examples:
1) Birthday basics
```date-calc
type: birthday
birthday: 2000-05-21
```
## API Documentation
Aliases for the date field also work:
See https://github.com/obsidianmd/obsidian-api
```date-calc
type: birthday
birthdate: 2000-05-21
```
2) Countdown to a date (future)
```date-calc
type: countdown
label: New Year
to: 2025-12-31 23:59
```
3) Until (alias of countdown)
```date-calc
type: until
to: 2030-01-01
```
4) Countdown from a custom start time
```date-calc
type: countdown
label: Sprint ends
to: 2025-10-15 17:00
from: 2025-10-01 09:00
```
5) Since an event (past)
```date-calc
type: since
since: 2024-12-20
```
If the date is in the future, the text will say "In: ..." instead of "Since: ... ago".
6) Difference between two dates
```date-calc
type: diff
from: 2024-02-29
to: 2025-03-01
```
7) Use times as well as dates
```date-calc
type: diff
from: 2025-09-19 08:00
to: 2025-09-19 16:30
```
8) Friendly birthday messages near the day
```date-calc
type: birthday
birthday: 1992-08-16
```
- If today is the birthday: "Wish them Happy Birthday!"
- If tomorrow is the birthday: "Their birthday is tomorrow!"
- If the birthday is > 31 days away: months are shown, with a 0.5 month added when roughly half a month remains.
9) Error handling examples
Invalid YAML:
```date-calc
this: is: not: valid
```
Missing required fields:
```date-calc
type: diff
from: 2025-01-01
# missing: to
```
The plugin will show a helpful error message in these cases.
Inline usage:
- You can also use inline code like:
- `\`date-calc: birthday=1992-08-16\`` → renders a birthday summary.
- `\`date-calc: to=2025-12-31 label=NewYear\`` → renders a countdown.
- `\`date-calc: since=2024-12-20\`` → time since.
- `\`date-calc: from=2025-09-19 to=2025-09-20\`` → difference between two dates.
- Inline also accepts YAML/inline-map after the colon, e.g. `\`date-calc: {type: birthday, birthday: 1992-08-16}\``.
- For inline birthday, if no date is given, the notes frontmatter `birthday`/`birthdate` is used when present.
Tips:
- You can style the rendered line using the CSS class `date-calc` in your snippet or theme.
- ISO-like date strings (YYYY-MM-DD or YYYY-MM-DD HH:mm) work well and are interpreted in your local timezone.
- For birthdays, calculations are normalized to local midnight to avoid off-by-one day issues due to timezones.
# Date Calc
This plugin adds a `date-calc` fenced code block you can embed in your notes to render date calculations.
Example (birthday):
```date-calc
type: birthday
birthday: 1992-08-16
```
Supported types and fields:
- birthday
- birthday (or birthdate or date): YYYY-MM-DD
- If not provided in the block/inline config, the plugin will also look for a `birthday` (or `birthdate`) property in the notes frontmatter.
- Output: Age in years and time until next birthday (days, or months if > 31 days, with half-month rounding)
- countdown / until
- to (or date or until): target date/time
- from (optional): starting date/time (defaults to now)
- label (optional): prefix label
- Output: Humanized time until target, or how long ago if past
- diff
- from (or start): start date/time
- to (or end): end date/time
- Output: Humanized difference between the two dates
- since
- since (or from or date): date/time of the event
- Output: Humanized time since that date (or time until if its in the future)
Notes:
- Dates are interpreted in your local timezone. For birthdays, calculations use local midnight to avoid timezone inconsistencies.
- YAML inside the code block must be valid. The plugin will show an error if the YAML cannot be parsed.
More examples:
1) Birthday basics
```date-calc
type: birthday
birthday: 2000-05-21
```
Aliases for the date field also work:
```date-calc
type: birthday
birthdate: 2000-05-21
```
2) Countdown to a date (future)
```date-calc
type: countdown
label: New Year
to: 2025-12-31 23:59
```
3) Until (alias of countdown)
```date-calc
type: until
to: 2030-01-01
```
4) Countdown from a custom start time
```date-calc
type: countdown
label: Sprint ends
to: 2025-10-15 17:00
from: 2025-10-01 09:00
```
5) Since an event (past)
```date-calc
type: since
since: 2024-12-20
```
If the date is in the future, the text will say "In: ..." instead of "Since: ... ago".
6) Difference between two dates
```date-calc
type: diff
from: 2024-02-29
to: 2025-03-01
```
7) Use times as well as dates
```date-calc
type: diff
from: 2025-09-19 08:00
to: 2025-09-19 16:30
```
8) Friendly birthday messages near the day
```date-calc
type: birthday
birthday: 1992-08-16
```
- If today is the birthday: "Wish them Happy Birthday!"
- If tomorrow is the birthday: "Their birthday is tomorrow!"
- If the birthday is > 31 days away: months are shown, with a 0.5 month added when roughly half a month remains.
9) Error handling examples
Invalid YAML:
```date-calc
this: is: not: valid
```
Missing required fields:
```date-calc
type: diff
from: 2025-01-01
# missing: to
```
The plugin will show a helpful error message in these cases.
Inline usage:
- You can also use inline code like:
- `\`date-calc: birthday=1992-08-16\`` → renders a birthday summary.
- `\`date-calc: to=2025-12-31 label=NewYear\`` → renders a countdown.
- `\`date-calc: since=2024-12-20\`` → time since.
- `\`date-calc: from=2025-09-19 to=2025-09-20\`` → difference between two dates.
- Inline also accepts YAML/inline-map after the colon, e.g. `\`date-calc: {type: birthday, birthday: 1992-08-16}\``.
- For inline birthday, if no date is given, the notes frontmatter `birthday`/`birthdate` is used when present.
Troubleshooting inline rendering:
- Inline replacements only run in Reading view and in Live Preview (rendered sections). In Source mode, youll just see the literal inline code.
- Type it exactly with a single pair of backticks, e.g. `\`date-calc:birthday\`` or `\`date-calc: birthday=1992-08-16\`` (no triple backticks).
- If it still shows as literal code, try: toggle the note to Reading view and back, or reload the app, or disable/enable the plugin. The plugins inline processor now runs late to avoid conflicts with other processors.
- Inline code inside fenced code blocks is intentionally ignored by the plugin so you can document examples in notes and the README.
Tips:
- You can style the rendered line using the CSS class `date-calc` in your snippet or theme.
- ISO-like date strings (YYYY-MM-DD or YYYY-MM-DD HH:mm) work well and are interpreted in your local timezone.
- For birthdays, calculations are normalized to local midnight to avoid off-by-one day issues due to timezones.

517
main.ts
View File

@@ -1,4 +1,6 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, parseYaml, Component, MarkdownPostProcessorContext, MarkdownRenderChild } from 'obsidian';
import { EditorView, ViewPlugin, ViewUpdate, Decoration, DecorationSet, WidgetType } from '@codemirror/view';
import { RangeSetBuilder } from '@codemirror/state';
// Remember to rename these classes and interfaces!
@@ -10,6 +12,293 @@ const DEFAULT_SETTINGS: MyPluginSettings = {
mySetting: 'default'
}
// ===== Helper functions for date calculations =====
function zeroTime(d: Date): Date {
const nd = new Date(d);
nd.setHours(0, 0, 0, 0);
return nd;
}
function parseDate(input: any): Date | null {
if (!input) return null;
const d = new Date(input);
if (isNaN(d.getTime())) return null;
return d;
}
function humanizeDuration(ms: number): string {
const abs = Math.abs(ms);
let seconds = Math.floor(abs / 1000);
let minutes = Math.floor(seconds / 60);
let hours = Math.floor(minutes / 60);
let days = Math.floor(hours / 24);
let years = Math.floor(days / 365);
days = days % 365;
let months = Math.floor(days / 30);
days = days % 30;
hours = hours % 24;
minutes = minutes % 60;
seconds = seconds % 60;
const parts: string[] = [];
if (years) parts.push(`${years} year${years !== 1 ? 's' : ''}`);
if (months) parts.push(`${months} month${months !== 1 ? 's' : ''}`);
if (days) parts.push(`${days} day${days !== 1 ? 's' : ''}`);
if (hours && parts.length < 3) parts.push(`${hours} hour${hours !== 1 ? 's' : ''}`);
if (minutes && parts.length < 3) parts.push(`${minutes} minute${minutes !== 1 ? 's' : ''}`);
if (seconds && parts.length < 3) parts.push(`${seconds} second${seconds !== 1 ? 's' : ''}`);
if (!parts.length) return '0 seconds';
return parts.join(', ');
}
// Shared function to process inline date-calc code
function processInlineDateCalc(raw: string, app: App, sourcePath: string): string {
if (!/^date-calc\s*:/.test(raw)) return '';
const params = raw.replace(/^date-calc\s*:/, '').trim();
let cfg: any = {};
if (params) {
// Try YAML first (supports inline map like {a: b, c: d} or multiline pasted)
try {
cfg = parseYaml(params) || {};
} catch {
// Fallback: parse key=value pairs separated by whitespace
cfg = {};
params.split(/\s+/).forEach(pair => {
const eq = pair.indexOf('=');
if (eq > 0) {
const k = pair.slice(0, eq).trim();
const v = pair.slice(eq + 1).trim();
if (k) (cfg as any)[k] = v;
}
});
}
}
// Infer type if missing (also support bare-word like `date-calc:birthday`)
let type = (cfg as any)?.type ? (cfg.type as any).toString().toLowerCase().trim() : '';
if (!type) {
// If YAML parsed into a string, decide if it's a bare type or a key=value expression
if (typeof cfg === 'string') {
const s = cfg.trim();
if (/^[A-Za-z][A-Za-z-]*$/.test(s)) {
type = s.toLowerCase();
cfg = {};
} else if (s.includes('=')) {
// Parse as key=value pairs
const tmp: any = {};
s.split(/\s+/).forEach(pair => {
const eq = pair.indexOf('=');
if (eq > 0) {
const k = pair.slice(0, eq).trim();
const v = pair.slice(eq + 1).trim();
if (k) tmp[k] = v;
}
});
cfg = tmp;
}
}
}
if (!type) {
// If params is a single bare word without separators, treat as type
if (/^[A-Za-z][A-Za-z-]*$/.test(params)) {
type = params.toLowerCase();
}
}
if (!type) {
if ((cfg as any).birthday || (cfg as any).birthdate) type = 'birthday';
else if ((cfg as any).to || (cfg as any).until) type = 'countdown';
else if ((cfg as any).since) type = 'since';
else if ((cfg as any).from && (cfg as any).to) type = 'diff';
}
if (!type) return '';
// Render result similarly to the block processor
let out = '';
try {
switch (type) {
case 'birthday': {
const fileCache = app.metadataCache.getCache(sourcePath);
const fm = fileCache?.frontmatter || {} as any;
const bstr = cfg.birthday || cfg.birthdate || cfg.date || fm?.birthday || fm?.birthdate;
const birthDate = parseDate(bstr);
if (!birthDate) { out = 'date-calc: Missing or invalid "birthday" date.'; break; }
const today = zeroTime(new Date());
const bd = zeroTime(birthDate);
let age = today.getFullYear() - bd.getFullYear();
const m = today.getMonth() - bd.getMonth();
if (m < 0 || (m === 0 && today.getDate() < bd.getDate())) age--;
let nextBirthday = zeroTime(new Date(today.getFullYear(), bd.getMonth(), bd.getDate()));
if (today > nextBirthday) nextBirthday.setFullYear(today.getFullYear() + 1);
const oneDay = 24 * 60 * 60 * 1000;
const isBirthdayToday = (today.getMonth() === bd.getMonth() && today.getDate() === bd.getDate());
const daysUntil = isBirthdayToday ? 0 : Math.round((nextBirthday.getTime() - today.getTime()) / oneDay);
let msg: string;
if (daysUntil > 31) {
let monthsUntil = (nextBirthday.getFullYear() - today.getFullYear()) * 12 + (nextBirthday.getMonth() - today.getMonth());
if (today.getDate() > nextBirthday.getDate()) monthsUntil--;
const refDate = zeroTime(new Date(nextBirthday.getFullYear(), nextBirthday.getMonth() - monthsUntil, today.getDate()));
const partialMonthDays = Math.round((nextBirthday.getTime() - refDate.getTime()) / oneDay);
if (partialMonthDays > 15) monthsUntil += 0.5;
msg = `Next birthday in ${monthsUntil} months.`;
} else if (daysUntil === 0) {
msg = 'Wish them Happy Birthday!';
} else if (daysUntil === 1) {
msg = 'Their birthday is tomorrow!';
} else {
msg = `Next birthday in ${daysUntil} days.`;
}
out = `Age: ${age} years. ${msg}`;
break;
}
case 'countdown':
case 'until': {
const toStr = cfg.to || cfg.date || cfg.until;
const toDate = parseDate(toStr);
if (!toDate) { out = 'date-calc: Missing or invalid "to" date.'; break; }
const fromDate = parseDate(cfg.from) || new Date();
const diffMs = toDate.getTime() - fromDate.getTime();
const label = cfg.label ? `${cfg.label}: ` : '';
out = diffMs >= 0 ? `${label}Countdown: ${humanizeDuration(diffMs)}` : `${label}Event passed ${humanizeDuration(-diffMs)} ago`;
break;
}
case 'diff':
case 'difference': {
const fromStr = cfg.from || cfg.start;
const toStr = cfg.to || cfg.end;
const fromD = parseDate(fromStr);
const toD = parseDate(toStr);
if (!fromD || !toD) { out = 'date-calc: Provide valid "from" and "to" dates.'; break; }
const diffMs = toD.getTime() - fromD.getTime();
out = `Difference: ${humanizeDuration(Math.abs(diffMs))}${diffMs < 0 ? ' (to is before from)' : ''}`;
break;
}
case 'since': {
const sinceStr = cfg.since || cfg.from || cfg.date;
const sinceDate = parseDate(sinceStr);
if (!sinceDate) { out = 'date-calc: Missing or invalid "since" date.'; break; }
const diffMs = new Date().getTime() - sinceDate.getTime();
out = diffMs >= 0 ? `Since: ${humanizeDuration(diffMs)} ago` : `In: ${humanizeDuration(-diffMs)}`;
break;
}
default: {
out = 'date-calc: Unknown type. Supported types: birthday, countdown, diff, since';
}
}
} catch (e: any) {
out = `date-calc error: ${e?.message || e}`;
}
return out;
}
// Widget for displaying date-calc results in Live Preview
class DateCalcWidget extends WidgetType {
constructor(
private result: string,
private app: App
) {
super();
}
eq(other: DateCalcWidget) {
return other.result === this.result;
}
toDOM() {
const span = document.createElement('span');
span.classList.add('date-calc-inline');
span.setAttribute('contenteditable', 'false');
span.textContent = this.result;
return span;
}
ignoreEvent() {
return false;
}
}
// Live Preview ViewPlugin for inline date-calc processing
const dateCalcLivePreviewPlugin = (app: App) => ViewPlugin.fromClass(class {
decorations: DecorationSet = Decoration.none;
constructor(view: EditorView) {
this.buildDecorations(view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.buildDecorations(update.view);
}
}
buildDecorations(view: EditorView) {
const builder = new RangeSetBuilder<Decoration>();
const text = view.state.doc.toString();
// Find inline code spans that start with date-calc:
const inlineCodeRegex = /`([^`]+)`/g;
let match;
while ((match = inlineCodeRegex.exec(text)) !== null) {
const codeContent = match[1];
if (/^date-calc\s*:/.test(codeContent)) {
const from = match.index;
const to = match.index + match[0].length;
// Get the active file path for processing
const activeFile = app.workspace.getActiveFile();
const sourcePath = activeFile?.path || '';
// Process the date calculation
const result = processInlineDateCalc(codeContent, app, sourcePath);
if (result) {
// Create a decoration that shows the result after the inline code
const widget = Decoration.widget({
widget: new DateCalcWidget(`${result}`, app),
side: 1
});
builder.add(to, to, widget);
}
}
}
this.decorations = builder.finish();
}
}, {
decorations: v => v.decorations
});
// DateCalc Inline Renderer - following dataview's component-based approach
class DateCalcInlineRenderer extends MarkdownRenderChild {
constructor(
public raw: string,
containerEl: HTMLElement,
public target: HTMLElement,
public app: App,
public sourcePath: string
) {
super(containerEl);
}
async onload() {
await this.render();
}
async render() {
const result = processInlineDateCalc(this.raw, this.app, this.sourcePath);
if (result) {
const span = document.createElement('span');
span.classList.add('date-calc-inline');
span.setAttribute('contenteditable', 'false');
span.textContent = result;
this.target.replaceWith(span);
}
}
}
// Note: Removed CodeMirror live preview plugin to preserve editability
// Following dataview's approach of using only post-processors
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
@@ -76,6 +365,150 @@ export default class MyPlugin extends Plugin {
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
// Register the Live Preview editor extension for inline date-calc processing
this.registerEditorExtension(dateCalcLivePreviewPlugin(this.app));
// Register the `date-calc` code block processor
this.registerMarkdownCodeBlockProcessor('date-calc', (source, el, ctx) => {
let cfg: any = {};
try {
cfg = parseYaml(source) || {};
} catch (e: any) {
el.createEl('pre', { text: `date-calc: Invalid YAML: ${e?.message || e}` });
return;
}
const type = (cfg.type || '').toString().toLowerCase().trim();
const container = el.createDiv({ cls: 'date-calc' });
try {
switch (type) {
case 'birthday': {
// Accept from config or frontmatter fallback
const fileCache = this.app.metadataCache.getCache(ctx.sourcePath);
const fm = fileCache?.frontmatter || {} as any;
const bstr = cfg.birthday || cfg.birthdate || cfg.date || fm?.birthday || fm?.birthdate;
const birthDate = bstr ? new Date(bstr + "T00:00:00") : null;
if (!birthDate) {
container.setText('date-calc: Missing or invalid "birthday" date.');
return;
}
// Get name and gender from config or frontmatter
const name = cfg.name || fm?.name || "Person";
const gender = cfg.gender || fm?.gender || "neutral";
// Configurable label with default
const ageLabel = cfg.label || "Age:";
// Format birthday as "July 6, 1998"
const birthdayFormatted = birthDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
// Determine pronouns based on gender
let pronoun = "their";
let possessive = "their";
if (gender.toLowerCase() === "male" || gender.toLowerCase() === "m") {
pronoun = "his";
possessive = "his";
} else if (gender.toLowerCase() === "female" || gender.toLowerCase() === "f") {
pronoun = "her";
possessive = "her";
}
const today = zeroTime(new Date());
const bd = zeroTime(birthDate);
let age = today.getFullYear() - bd.getFullYear();
const m = today.getMonth() - bd.getMonth();
if (m < 0 || (m === 0 && today.getDate() < bd.getDate())) age--;
let nextBirthday = zeroTime(new Date(today.getFullYear(), bd.getMonth(), bd.getDate()));
if (today > nextBirthday) nextBirthday.setFullYear(today.getFullYear() + 1);
const oneDay = 24 * 60 * 60 * 1000;
const daysUntil = Math.round((nextBirthday.getTime() - today.getTime()) / oneDay);
let msg: string;
if (daysUntil > 31) {
let monthsUntil = (nextBirthday.getFullYear() - today.getFullYear()) * 12 + (nextBirthday.getMonth() - today.getMonth());
if (today.getDate() > nextBirthday.getDate()) monthsUntil--;
const refDate = zeroTime(new Date(nextBirthday.getFullYear(), nextBirthday.getMonth() - monthsUntil, today.getDate()));
const partialMonthDays = Math.round((nextBirthday.getTime() - refDate.getTime()) / oneDay);
if (partialMonthDays > 15) monthsUntil += 0.5;
msg = `${possessive} birthday is in ${monthsUntil} months.`;
} else if (daysUntil === 0) {
msg = `wish ${pronoun} Happy Birthday!`;
} else if (daysUntil === 1) {
msg = `${possessive} birthday is tomorrow!`;
} else {
msg = `next one in ${daysUntil} days.`;
}
container.setText(`${name !== null ? name + ": " : ageLabel } ${age}. Birthday is ${birthdayFormatted}${msg}`);
break;
}
case 'countdown':
case 'until': {
const toStr = cfg.to || cfg.date || cfg.until;
const toDate = parseDate(toStr);
if (!toDate) { container.setText('date-calc: Missing or invalid "to" date.'); return; }
const fromDate = parseDate(cfg.from) || new Date();
const diffMs = toDate.getTime() - fromDate.getTime();
const label = cfg.label ? `${cfg.label}: ` : '';
container.setText(diffMs >= 0 ? `${label}Countdown: ${humanizeDuration(diffMs)}` : `${label}Event passed ${humanizeDuration(-diffMs)} ago`);
break;
}
case 'diff':
case 'difference': {
const fromStr = cfg.from || cfg.start;
const toStr = cfg.to || cfg.end;
const fromD = parseDate(fromStr);
const toD = parseDate(toStr);
if (!fromD || !toD) { container.setText('date-calc: Provide valid "from" and "to" dates.'); return; }
const diffMs = toD.getTime() - fromD.getTime();
container.setText(`Difference: ${humanizeDuration(Math.abs(diffMs))}${diffMs < 0 ? ' (to is before from)' : ''}`);
break;
}
case 'since': {
const sinceStr = cfg.since || cfg.from || cfg.date;
const sinceDate = parseDate(sinceStr);
if (!sinceDate) { container.setText('date-calc: Missing or invalid "since" date.'); return; }
const diffMs = new Date().getTime() - sinceDate.getTime();
container.setText(diffMs >= 0 ? `Since: ${humanizeDuration(diffMs)} ago` : `In: ${humanizeDuration(-diffMs)}`);
break;
}
default: {
container.setText('date-calc: Unknown type. Supported types: birthday, countdown, diff, since');
}
}
} catch (e: any) {
container.setText(`date-calc error: ${e?.message || e}`);
}
});
// Inline code support: `date-calc: key=value ...` or YAML/inline-map
// Following dataview's component-based approach - works in both reading view and live preview
this.registerMarkdownPostProcessor((el, ctx) => {
const codeNodes = el.querySelectorAll('code');
codeNodes.forEach((codeEl) => {
// Skip code inside of pre elements (fenced code blocks)
if (codeEl.parentElement && codeEl.parentElement.nodeName.toLowerCase() === 'pre') return;
const raw = codeEl.textContent?.trim() || '';
if (/^date-calc\s*:/.test(raw)) {
const renderer = new DateCalcInlineRenderer(raw, el, codeEl, this.app, ctx.sourcePath);
ctx.addChild(renderer);
}
});
});
}
onunload() {
@@ -120,15 +553,77 @@ class SampleSettingTab extends PluginSettingTab {
containerEl.empty();
new Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(text => text
.setPlaceholder('Enter your secret')
.setValue(this.plugin.settings.mySetting)
.onChange(async (value) => {
this.plugin.settings.mySetting = value;
await this.plugin.saveSettings();
}));
containerEl.createEl('h2', {text: 'Date Calc Plugin Syntax'});
containerEl.createEl('p', {text: 'This plugin allows you to embed dynamic date calculations in your notes using code blocks or inline code.'});
// Fenced code block examples
const blockSection = containerEl.createDiv();
blockSection.createEl('h3', {text: 'Fenced Code Block Examples'});
blockSection.createEl('h4', {text: '1. Birthday Calculation'});
blockSection.createEl('pre').createEl('code', {text:
`\`\`\`date-calc
type: birthday
birthday: 1992-08-16
\`\`\``});
blockSection.createEl('p', {text: 'Shows age and time until next birthday. Can also use "birthdate" or "date" field.'});
blockSection.createEl('h4', {text: '2. Countdown to Event'});
blockSection.createEl('pre').createEl('code', {text:
`\`\`\`date-calc
type: countdown
label: New Year
to: 2025-12-31 23:59
\`\`\``});
blockSection.createEl('p', {text: 'Shows time remaining until the target date. Can also use "until" type.'});
blockSection.createEl('h4', {text: '3. Time Since Event'});
blockSection.createEl('pre').createEl('code', {text:
`\`\`\`date-calc
type: since
since: 2024-01-01
\`\`\``});
blockSection.createEl('p', {text: 'Shows how much time has passed since an event.'});
blockSection.createEl('h4', {text: '4. Date Difference'});
blockSection.createEl('pre').createEl('code', {text:
`\`\`\`date-calc
type: diff
from: 2024-01-01
to: 2024-12-31
\`\`\``});
blockSection.createEl('p', {text: 'Shows the difference between two specific dates.'});
// Inline examples
const inlineSection = containerEl.createDiv();
inlineSection.createEl('h3', {text: 'Inline Code Examples'});
inlineSection.createEl('p', {text: 'You can also use inline code for quick calculations:'});
const inlineExamples = inlineSection.createEl('ul');
inlineExamples.createEl('li').innerHTML = '<code>`date-calc: birthday=1992-08-16`</code> → Birthday summary';
inlineExamples.createEl('li').innerHTML = '<code>`date-calc: to=2025-12-31 label="New Year"`</code> → Countdown';
inlineExamples.createEl('li').innerHTML = '<code>`date-calc: since=2024-01-01`</code> → Time since';
inlineExamples.createEl('li').innerHTML = '<code>`date-calc: {type: diff, from: 2024-01-01, to: 2024-12-31}`</code> → YAML format';
// Notes section
const notesSection = containerEl.createDiv();
notesSection.createEl('h3', {text: 'Important Notes'});
const notesList = notesSection.createEl('ul');
notesList.createEl('li', {text: 'Date formats: Use YYYY-MM-DD or YYYY-MM-DD HH:mm format'});
notesList.createEl('li', {text: 'For birthdays: Can use frontmatter properties (birthday/birthdate) if not specified in the block'});
notesList.createEl('li', {text: 'Inline code only renders in Reading view or Live Preview mode'});
notesList.createEl('li', {text: 'All calculations use your local timezone'});
notesList.createEl('li', {text: 'YAML syntax must be valid - the plugin will show errors for invalid syntax'});
// Troubleshooting
const troubleSection = containerEl.createDiv();
troubleSection.createEl('h3', {text: 'Troubleshooting'});
const troubleList = troubleSection.createEl('ul');
troubleList.createEl('li', {text: 'If inline code isn\'t rendering, try switching to Reading view'});
troubleList.createEl('li', {text: 'Use single backticks for inline code (not triple backticks)'});
troubleList.createEl('li', {text: 'If calculations seem wrong, check your date format and timezone'});
troubleList.createEl('li', {text: 'Error messages will appear in place of the calculation if something is wrong'});
}
}

View File

@@ -1,6 +1,6 @@
{
"id": "sample-plugin",
"name": "Sample Plugin",
"id": "date-calc",
"name": "Date calculator",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Demonstrates some of the capabilities of the Obsidian API.",

2406
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,30 @@
/*
This CSS file will be included with your plugin, and
available in the app when your plugin is enabled.
If your plugin does not need CSS, delete this file.
Date Calc Plugin Styles
*/
/* Inline date-calc styling for live preview - now styles actual span elements */
.date-calc-inline {
background-color: var(--background-modifier-accent);
border-radius: 3px;
padding: 2px 6px;
color: var(--text-accent);
font-weight: bold;
font-size: 0.9em;
display: inline-block;
margin: 0 2px;
}
/* Additional styling for fenced code blocks */
.date-calc {
background-color: var(--background-modifier-accent);
padding: 8px 12px;
border-radius: 4px;
border-left: 3px solid var(--text-accent);
margin: 8px 0;
}
/* Hover effect to make the result more prominent */
.date-calc-inline:hover {
background-color: var(--background-modifier-hover);
color: var(--text-accent-hover);
}