WIP. inline doesn't work, but the rest does.
This commit is contained in:
371
README.md
371
README.md
@@ -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 note’s 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 it’s 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 note’s 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 note’s 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 it’s 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 note’s 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, you’ll 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 plugin’s 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
517
main.ts
@@ -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'});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
2406
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
styles.css
34
styles.css
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user