chore: update dependencies and clean up styles

- Added @codemirror/language and @lezer/common as devDependencies in package.json and package-lock.json.
- Removed inline date-calc styling and specific CodeMirror styles from styles.css for a cleaner design.
This commit is contained in:
2026-01-04 19:59:31 -05:00
parent e6e791ad35
commit e5552e2d4c
5 changed files with 353 additions and 580 deletions

View File

@@ -3,7 +3,7 @@ import process from "process";
import builtins from "builtin-modules"; import builtins from "builtin-modules";
const banner = const banner =
`/* `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin if you want to view the source, please visit the github repository of this plugin
*/ */
@@ -21,16 +21,8 @@ const context = await esbuild.context({
"obsidian", "obsidian",
"electron", "electron",
"@codemirror/autocomplete", "@codemirror/autocomplete",
"@codemirror/collab", "@codemirror/*",
"@codemirror/commands", "@lezer/*",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins], ...builtins],
format: "cjs", format: "cjs",
target: "es2018", target: "es2018",

818
main.ts
View File

@@ -1,18 +1,34 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, parseYaml, Component, MarkdownPostProcessorContext, MarkdownRenderChild } from 'obsidian'; import { App, Notice, Plugin, PluginSettingTab, Setting, parseYaml } from "obsidian";
import { EditorView, ViewPlugin, ViewUpdate, Decoration, DecorationSet, WidgetType } from '@codemirror/view'; import { RangeSetBuilder } from "@codemirror/state";
import { RangeSetBuilder } from '@codemirror/state'; import {
Decoration,
DecorationSet,
EditorView,
ViewPlugin,
ViewUpdate,
WidgetType,
} from "@codemirror/view";
// Remember to rename these classes and interfaces! /* =========================
Settings
========================= */
interface MyPluginSettings { interface DateCalcSettings {
mySetting: string; debug: boolean;
hideResultWhileCursorInside: boolean;
verbose: boolean;
} }
const DEFAULT_SETTINGS: MyPluginSettings = { const DEFAULT_SETTINGS: DateCalcSettings = {
mySetting: 'default' debug: false,
} hideResultWhileCursorInside: true,
verbose: false, // default to concise
};
/* =========================
Helpers
========================= */
// ===== Helper functions for date calculations =====
function zeroTime(d: Date): Date { function zeroTime(d: Date): Date {
const nd = new Date(d); const nd = new Date(d);
nd.setHours(0, 0, 0, 0); nd.setHours(0, 0, 0, 0);
@@ -32,597 +48,361 @@ function humanizeDuration(ms: number): string {
let minutes = Math.floor(seconds / 60); let minutes = Math.floor(seconds / 60);
let hours = Math.floor(minutes / 60); let hours = Math.floor(minutes / 60);
let days = Math.floor(hours / 24); let days = Math.floor(hours / 24);
let years = Math.floor(days / 365); const years = Math.floor(days / 365);
days = days % 365; days = days % 365;
let months = Math.floor(days / 30); const months = Math.floor(days / 30);
days = days % 30; days = days % 30;
hours = hours % 24; hours = hours % 24;
minutes = minutes % 60; minutes = minutes % 60;
seconds = seconds % 60; seconds = seconds % 60;
const parts: string[] = []; const parts: string[] = [];
if (years) parts.push(`${years} year${years !== 1 ? 's' : ''}`); if (years) parts.push(`${years} year${years !== 1 ? "s" : ""}`);
if (months) parts.push(`${months} month${months !== 1 ? 's' : ''}`); if (months) parts.push(`${months} month${months !== 1 ? "s" : ""}`);
if (days) parts.push(`${days} day${days !== 1 ? 's' : ''}`); if (days) parts.push(`${days} day${days !== 1 ? "s" : ""}`);
if (hours && parts.length < 3) parts.push(`${hours} hour${hours !== 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 (minutes && parts.length < 3) parts.push(`${minutes} minute${minutes !== 1 ? "s" : ""}`);
if (seconds && parts.length < 3) parts.push(`${seconds} second${seconds !== 1 ? 's' : ''}`); if (seconds && parts.length < 3) parts.push(`${seconds} second${seconds !== 1 ? "s" : ""}`);
if (!parts.length) return '0 seconds'; if (!parts.length) return "0 seconds";
return parts.join(', '); return parts.join(", ");
} }
// Shared function to process inline date-calc code function isLivePreviewEditor(cm: EditorView): boolean {
// In Obsidian, the editor is inside .markdown-source-view
// Live Preview adds .is-live-preview on that container.
const container = cm.dom.closest(".markdown-source-view");
return !!container?.classList.contains("is-live-preview");
}
/* =========================
Inline processor
========================= */
function parseKv(params: string): Record<string, string> {
const cfg: Record<string, string> = {};
// key=value where value can be "..." or '...' or a bare token
const rx = /(\w+)=("([^"\\]|\\.)*"|'([^'\\]|\\.)*'|\S+)/g;
let m: RegExpExecArray | null;
while ((m = rx.exec(params)) !== null) {
const key = m[1];
let val = m[2];
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1);
}
cfg[key] = val;
}
return cfg;
}
function parseInlineArgs(params: string): any {
// 1) If it looks like key=value syntax, parse that first (fast + reliable)
if (params.includes("=")) return parseKv(params);
// 2) Otherwise try YAML (for `{type: diff, from: ..., to: ...}` and similar)
try {
const v = parseYaml(params);
// If YAML returns a string, just return it (supports `date-calc: birthday`)
return v ?? {};
} catch {
// Last resort: empty
return {};
}
}
// raw should be WITHOUT backticks: "date-calc: birthday=1992-08-16"
function processInlineDateCalc(raw: string, app: App, sourcePath: string): string { function processInlineDateCalc(raw: string, app: App, sourcePath: string): string {
if (!/^date-calc\s*:/.test(raw)) return ''; if (!/^date-calc\s*:/.test(raw)) return "";
const params = raw.replace(/^date-calc\s*:/, '').trim();
let cfg: any = {}; const params = raw.replace(/^date-calc\s*:/, "").trim();
if (params) { let cfg: any = params ? parseInlineArgs(params) : {};
// Try YAML first (supports inline map like {a: b, c: d} or multiline pasted)
try { // Support `date-calc: birthday`
cfg = parseYaml(params) || {}; let type = (cfg?.type ?? "").toString().toLowerCase().trim();
} catch { if (!type && typeof cfg === "string" && /^[A-Za-z][A-Za-z-]*$/.test(cfg.trim())) {
// Fallback: parse key=value pairs separated by whitespace type = cfg.trim().toLowerCase();
cfg = {}; 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`) if (!type && /^[A-Za-z][A-Za-z-]*$/.test(params)) type = params.toLowerCase();
let type = (cfg as any)?.type ? (cfg.type as any).toString().toLowerCase().trim() : '';
// Infer
if (!type) { if (!type) {
// If YAML parsed into a string, decide if it's a bare type or a key=value expression if (cfg.birthday || cfg.birthdate) type = "birthday";
if (typeof cfg === 'string') { else if (cfg.to || cfg.until) type = "countdown";
const s = cfg.trim(); else if (cfg.since) type = "since";
if (/^[A-Za-z][A-Za-z-]*$/.test(s)) { else if (cfg.from && cfg.to) type = "diff";
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 // Aliases
if (/^[A-Za-z][A-Za-z-]*$/.test(params)) { if (type === "bday") type = "birthday";
type = params.toLowerCase(); if (type === "until") type = "countdown";
} if (type === "difference") type = "diff";
} if (!type) return "";
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';
}
// Normalize type aliases to match fenced code block processor
if (type === 'bday') type = 'birthday';
if (type === 'until') type = 'countdown';
if (type === 'difference') type = 'diff';
if (!type) return '';
// Render result similarly to the block processor
let out = '';
try { try {
switch (type) { switch (type) {
case 'birthday': { case "birthday": {
const fileCache = app.metadataCache.getCache(sourcePath); const fileCache = app.metadataCache.getCache(sourcePath);
const fm = fileCache?.frontmatter || {} as any; const fm = (fileCache?.frontmatter ?? {}) as any;
const bstr = cfg.birthday || cfg.birthdate || cfg.date || fm?.birthday || fm?.birthdate; const bstr = cfg.birthday || cfg.birthdate || cfg.date || fm?.birthday || fm?.birthdate;
const birthDate = parseDate(bstr); const birthDate = parseDate(bstr);
if (!birthDate) { out = 'date-calc: Missing or invalid "birthday" date.'; break; } if (!birthDate) return `date-calc: Missing or invalid "birthday" date.`;
const today = zeroTime(new Date()); const today = zeroTime(new Date());
const bd = zeroTime(birthDate); const bd = zeroTime(birthDate);
let age = today.getFullYear() - bd.getFullYear(); let age = today.getFullYear() - bd.getFullYear();
const m = today.getMonth() - bd.getMonth(); const m = today.getMonth() - bd.getMonth();
if (m < 0 || (m === 0 && today.getDate() < bd.getDate())) age--; if (m < 0 || (m === 0 && today.getDate() < bd.getDate())) age--;
let nextBirthday = zeroTime(new Date(today.getFullYear(), bd.getMonth(), bd.getDate()));
const nextBirthday = zeroTime(new Date(today.getFullYear(), bd.getMonth(), bd.getDate()));
if (today > nextBirthday) nextBirthday.setFullYear(today.getFullYear() + 1); if (today > nextBirthday) nextBirthday.setFullYear(today.getFullYear() + 1);
const oneDay = 24 * 60 * 60 * 1000; const oneDay = 24 * 60 * 60 * 1000;
const isBirthdayToday = (today.getMonth() === bd.getMonth() && today.getDate() === bd.getDate()); const isBirthdayToday = today.getMonth() === bd.getMonth() && today.getDate() === bd.getDate();
const daysUntil = isBirthdayToday ? 0 : Math.round((nextBirthday.getTime() - today.getTime()) / oneDay); const daysUntil = isBirthdayToday ? 0 : Math.round((nextBirthday.getTime() - today.getTime()) / oneDay);
let msg: string;
let msg = "";
if (daysUntil > 31) { if (daysUntil > 31) {
let monthsUntil = (nextBirthday.getFullYear() - today.getFullYear()) * 12 + (nextBirthday.getMonth() - today.getMonth()); let monthsUntil =
(nextBirthday.getFullYear() - today.getFullYear()) * 12 +
(nextBirthday.getMonth() - today.getMonth());
if (today.getDate() > nextBirthday.getDate()) monthsUntil--; 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.`; msg = `Next birthday in ${monthsUntil} months.`;
} else if (daysUntil === 0) { } else if (daysUntil === 0) msg = "Wish them Happy Birthday!";
msg = 'Wish them Happy Birthday!'; else if (daysUntil === 1) msg = "Their birthday is tomorrow!";
} else if (daysUntil === 1) { else msg = `Next birthday in ${daysUntil} days.`;
msg = 'Their birthday is tomorrow!';
} else { return `Age: ${age} years. ${msg}`;
msg = `Next birthday in ${daysUntil} days.`;
}
out = `Age: ${age} years. ${msg}`;
break;
} }
case 'countdown':
case 'until': { case "countdown": {
const toStr = cfg.to || cfg.date || cfg.until; const toStr = cfg.to || cfg.date || cfg.until;
const toDate = parseDate(toStr); const toDate = parseDate(toStr);
if (!toDate) { out = 'date-calc: Missing or invalid "to" date.'; break; } if (!toDate) return `date-calc: Missing or invalid "to" date.`;
const fromDate = parseDate(cfg.from) || new Date(); const fromDate = parseDate(cfg.from) || new Date();
const diffMs = toDate.getTime() - fromDate.getTime(); const diffMs = toDate.getTime() - fromDate.getTime();
const label = cfg.label ? `${cfg.label}: ` : ''; const label = cfg.label ? `${cfg.label}: ` : "";
out = diffMs >= 0 ? `${label}Countdown: ${humanizeDuration(diffMs)}` : `${label}Event passed ${humanizeDuration(-diffMs)} ago`;
break; return diffMs >= 0
? `${label}Countdown: ${humanizeDuration(diffMs)}`
: `${label}Event passed ${humanizeDuration(-diffMs)} ago`;
} }
case 'diff':
case 'difference': { case "diff": {
const fromStr = cfg.from || cfg.start; const fromD = parseDate(cfg.from || cfg.start);
const toStr = cfg.to || cfg.end; const toD = parseDate(cfg.to || cfg.end);
const fromD = parseDate(fromStr); if (!fromD || !toD) return `date-calc: Provide valid "from" and "to" dates.`;
const toD = parseDate(toStr);
if (!fromD || !toD) { out = 'date-calc: Provide valid "from" and "to" dates.'; break; }
const diffMs = toD.getTime() - fromD.getTime(); const diffMs = toD.getTime() - fromD.getTime();
out = `Difference: ${humanizeDuration(Math.abs(diffMs))}${diffMs < 0 ? ' (to is before from)' : ''}`; return `Difference: ${humanizeDuration(Math.abs(diffMs))}${diffMs < 0 ? " (to is before from)" : ""}`;
break;
} }
case 'since': {
const sinceStr = cfg.since || cfg.from || cfg.date; case "since": {
const sinceDate = parseDate(sinceStr); const sinceDate = parseDate(cfg.since || cfg.from || cfg.date);
if (!sinceDate) { out = 'date-calc: Missing or invalid "since" date.'; break; } if (!sinceDate) return `date-calc: Missing or invalid "since" date.`;
const diffMs = new Date().getTime() - sinceDate.getTime();
out = diffMs >= 0 ? `Since: ${humanizeDuration(diffMs)} ago` : `In: ${humanizeDuration(-diffMs)}`; const diffMs = Date.now() - sinceDate.getTime();
break; return diffMs >= 0 ? `Since: ${humanizeDuration(diffMs)} ago` : `In: ${humanizeDuration(-diffMs)}`;
}
default: {
out = 'date-calc: Unknown type. Supported types: birthday, countdown, diff, since';
} }
default:
return "date-calc: Unknown type. Supported: birthday, countdown, diff, since";
} }
} catch (e: any) { } catch (e: any) {
out = `date-calc error: ${e?.message || e}`; return `date-calc error: ${e?.message || e}`;
} }
return out;
} }
// Widget for displaying date-calc results in Live Preview /* =========================
Live Preview decorations
========================= */
class DateCalcWidget extends WidgetType { class DateCalcWidget extends WidgetType {
constructor( constructor(private text: string) { super(); }
private result: string, eq(other: DateCalcWidget) { return other.text === this.text; }
private app: App toDOM() {
) { const span = document.createElement("span");
super(); span.className = "date-calc-inline";
} span.textContent = this.text;
span.setAttribute("contenteditable", "false");
eq(other: DateCalcWidget) { return span;
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 export function dateCalcLivePreview(app: any, getSettings: () => { debug: boolean }) {
const dateCalcLivePreviewPlugin = (app: App) => ViewPlugin.fromClass(class { // Match only inline-code expressions (backticked)
decorations: DecorationSet = Decoration.none; const re = /`date-calc:[^`]*`/g;
constructor(view: EditorView) { // Key fix: scan beyond visibleRanges so matches don't get missed at viewport boundaries
this.buildDecorations(view); const margin = 2000;
}
update(update: ViewUpdate) { return ViewPlugin.fromClass(
if (update.docChanged || update.viewportChanged) { class {
this.buildDecorations(update.view); decorations: DecorationSet = Decoration.none;
}
}
buildDecorations(view: EditorView) { constructor(view: EditorView) {
const builder = new RangeSetBuilder<Decoration>(); this.decorations = this.build(view);
const text = view.state.doc.toString(); }
// Find inline code spans that start with date-calc: update(u: ViewUpdate) {
// This regex looks for inline code between backticks if (u.docChanged || u.viewportChanged || u.selectionSet) {
const inlineCodeRegex = /`([^`]+)`/g; this.decorations = this.build(u.view);
let match; }
}
while ((match = inlineCodeRegex.exec(text)) !== null) { build(view: EditorView): DecorationSet {
const codeContent = match[1]; // IMPORTANT: do nothing in Source mode so raw markdown stays visible
if (/^date-calc\s*:/.test(codeContent)) { if (!isLivePreviewEditor(view)) return Decoration.none;
const from = match.index;
const to = match.index + match[0].length;
// Get the active file path for processing const b = new RangeSetBuilder<Decoration>();
const activeFile = app.workspace.getActiveFile(); const sourcePath = app.workspace.getActiveFile()?.path ?? "";
const sourcePath = activeFile?.path || ''; const head = view.state.selection.main.head;
// Process the date calculation let matchesSeen = 0;
const result = processInlineDateCalc(codeContent, app, sourcePath); let replaced = 0;
if (result) { const used = new Set<string>();
// 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);
// Add a debug log to verify when a widget is being created
console.log(`Live Preview widget created for: ${codeContent} with result: ${result}`);
}
}
}
this.decorations = builder.finish(); for (const r of view.visibleRanges) {
} const scanFrom = Math.max(0, r.from - margin);
}, { const scanTo = Math.min(view.state.doc.length, r.to + margin);
decorations: v => v.decorations
});
// DateCalc Inline Renderer - following dataview's component-based approach const slice = view.state.doc.sliceString(scanFrom, scanTo);
class DateCalcInlineRenderer extends MarkdownRenderChild { re.lastIndex = 0;
constructor(
public raw: string,
containerEl: HTMLElement,
public target: HTMLElement,
public app: App,
public sourcePath: string
) {
super(containerEl);
}
async onload() { let m: RegExpExecArray | null;
await this.render(); while ((m = re.exec(slice)) !== null) {
} matchesSeen++;
async render() { const absStart = scanFrom + m.index;
const result = processInlineDateCalc(this.raw, this.app, this.sourcePath); const absEnd = absStart + m[0].length;
if (result) {
console.log(`Rendering inline date-calc: ${this.raw} -> ${result}`);
// In Live Preview, just replace the code element directly // de-dupe overlaps from overlapping scan windows
const span = document.createElement('span'); const key = `${absStart}:${absEnd}`;
span.classList.add('date-calc-inline'); if (used.has(key)) continue;
span.setAttribute('contenteditable', 'false'); used.add(key);
span.textContent = result;
// Ensure we properly replace the target element // If cursor is inside, don't replace (so user can edit the raw inline code)
try { if (head >= absStart && head <= absEnd) continue;
this.target.parentNode?.replaceChild(span, this.target);
console.log('Successfully replaced code element with result'); const inner = m[0].slice(1, -1).trim(); // remove backticks
} catch (e) { const result = processInlineDateCalc(inner, app, sourcePath);
console.error('Failed to replace code element:', e); if (!result) continue;
// Fallback: try the original method
this.target.replaceWith(span); // Replace the entire backticked expression with a widget
} b.add(
} else { absStart,
console.log(`No result for inline date-calc: ${this.raw}`); absEnd,
} Decoration.replace({
} widget: new DateCalcWidget(result),
inclusive: false,
})
);
replaced++;
}
}
if (getSettings().debug) {
console.log("[date-calc] replace scan", { matchesSeen, replaced, sourcePath });
}
return b.finish();
}
},
{
decorations: (v) => v.decorations,
// Important for replace widgets: treat them as “atomic” so cursor navigation behaves sanely
// (same pattern as CodeMirrors decoration examples)
provide: (plugin) =>
EditorView.atomicRanges.of((view) => view.plugin(plugin)?.decorations ?? Decoration.none),
}
);
} }
// Note: Removed CodeMirror live preview plugin to preserve editability /* =========================
// Following dataview's approach of using only post-processors Plugin
========================= */
export default class MyPlugin extends Plugin { export default class DateCalcPlugin extends Plugin {
settings: MyPluginSettings; settings: DateCalcSettings;
async onload() { async onload() {
await this.loadSettings();
// Add a status bar item showing the plugin is enabled
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Date Calculator Enabled');
// Debugging command to test plugin functionality
this.addCommand({
id: 'date-calc-debug',
name: 'Debug Date Calculator',
callback: () => {
new Notice('Date Calculator plugin is working! Check console for more details.');
console.log('Date Calculator plugin debug info:');
console.log('Plugin version: 1.0.0');
console.log('Plugin enabled: true');
console.log('Current file:', this.app.workspace.getActiveFile()?.path);
}
});
// Command to refresh inline calculations (useful for troubleshooting)
this.addCommand({
id: 'refresh-date-calc-inline',
name: 'Refresh Date Calculations',
callback: () => {
// Force refresh by triggering layout
this.app.workspace.trigger('layout-change');
new Notice('Date calculations refreshed!');
}
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new SampleSettingTab(this.app, this));
// CRITICAL: Register Live Preview extension FIRST and ensure it's properly loaded
console.log('Registering Live Preview editor extension for date-calc...');
this.registerEditorExtension(dateCalcLivePreviewPlugin(this.app));
console.log('Date Calculator Live Preview extension registered successfully!');
// Add more comprehensive debugging
new Notice('Date Calculator loaded with Live Preview support!');
// 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 'bday':
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 = `next one is in ${monthsUntil} months.`;
} else if (daysUntil === 0) {
msg = `today! Wish ${pronoun} Happy Birthday!`;
} else if (daysUntil === 1) {
msg = `that's tomorrow!`;
} else {
msg = `next one is in ${daysUntil} days.`;
}
container.setText(`${name !== null ? name + ": " : ageLabel } ${age} years old. 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() {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() { console.log("[date-calc] loaded");
await this.saveData(this.settings); new Notice("Date Calc loaded (Live Preview)");
// Live Preview requires editor extensions + decorations
this.registerEditorExtension(dateCalcLivePreview(this.app, () => this.settings)); // [page:2][page:1]
this.addCommand({
id: "date-calc-toggle-debug",
name: "Date Calc: Toggle debug",
callback: async () => {
this.settings.debug = !this.settings.debug;
await this.saveData(this.settings);
new Notice(`Date Calc debug: ${this.settings.debug ? "ON" : "OFF"}`);
console.log("[date-calc] debug =", this.settings.debug);
},
});
this.addSettingTab(new DateCalcSettingTab(this.app, this));
} }
} }
class SampleModal extends Modal { /* =========================
constructor(app: App) { Settings tab
super(app); ========================= */
}
onOpen() { class DateCalcSettingTab extends PluginSettingTab {
const {contentEl} = this; constructor(app: App, private plugin: DateCalcPlugin) {
contentEl.setText('Woah!');
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin); super(app, plugin);
this.plugin = plugin;
} }
display(): void { display(): void {
const {containerEl} = this; const { containerEl } = this;
containerEl.empty(); containerEl.empty();
containerEl.createEl('h2', {text: 'Date Calc Plugin Syntax'}); containerEl.createEl("h2", { text: "Date Calc" });
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 new Setting(containerEl)
const blockSection = containerEl.createDiv(); .setName("Debug logging")
blockSection.createEl('h3', {text: 'Fenced Code Block Examples'}); .setDesc("Logs how many `date-calc:` inline-code matches were seen and decorated.")
.addToggle((t) =>
t.setValue(this.plugin.settings.debug).onChange(async (v) => {
this.plugin.settings.debug = v;
await this.plugin.saveData(this.plugin.settings);
})
);
blockSection.createEl('h4', {text: '1. Birthday Calculation'}); new Setting(containerEl)
blockSection.createEl('pre').createEl('code', {text: .setName("Hide result while cursor inside inline code")
`\`\`\`date-calc .setDesc("Prevents the widget from showing while you edit the inline backticks.")
type: birthday .addToggle((t) =>
birthday: 1992-08-16 t.setValue(this.plugin.settings.hideResultWhileCursorInside).onChange(async (v) => {
\`\`\``}); this.plugin.settings.hideResultWhileCursorInside = v;
blockSection.createEl('p', {text: 'Shows age and time until next birthday. Can also use "birthdate" or "date" field.'}); await this.plugin.saveData(this.plugin.settings);
})
);
blockSection.createEl('h4', {text: '2. Countdown to Event'}); new Setting(containerEl)
blockSection.createEl('pre').createEl('code', {text: .setName("Verbose output")
`\`\`\`date-calc .setDesc("When enabled, show the longer/wordier messages (your current output).")
type: countdown .addToggle(t =>
label: New Year t.setValue(this.plugin.settings.verbose).onChange(async (v) => {
to: 2025-12-31 23:59 this.plugin.settings.verbose = v;
\`\`\``}); await this.plugin.saveData(this.plugin.settings);
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'}); containerEl.createEl("p", { text: 'Example: `date-calc: birthday=1992-08-16`' });
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'});
} }
} }

58
package-lock.json generated
View File

@@ -9,6 +9,8 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@codemirror/language": "^6.12.1",
"@lezer/common": "^1.5.0",
"@types/node": "^16.11.6", "@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0", "@typescript-eslint/parser": "5.29.0",
@@ -19,13 +21,27 @@
"typescript": "4.7.4" "typescript": "4.7.4"
} }
}, },
"node_modules/@codemirror/language": {
"version": "6.12.1",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
"integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/state": { "node_modules/@codemirror/state": {
"version": "6.5.2", "version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@marijn/find-cluster-break": "^1.0.0" "@marijn/find-cluster-break": "^1.0.0"
} }
@@ -36,7 +52,6 @@
"integrity": "sha512-bTWAJxL6EOFLPzTx+O5P5xAO3gTqpatQ2b/ARQ8itfU/v2LlpS3pH2fkL0A3E/Fx8Y2St2KES7ZEV0sHTsSW/A==", "integrity": "sha512-bTWAJxL6EOFLPzTx+O5P5xAO3gTqpatQ2b/ARQ8itfU/v2LlpS3pH2fkL0A3E/Fx8Y2St2KES7ZEV0sHTsSW/A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/state": "^6.5.0", "@codemirror/state": "^6.5.0",
"crelt": "^1.0.6", "crelt": "^1.0.6",
@@ -526,13 +541,39 @@
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"peer": true "peer": true
}, },
"node_modules/@lezer/common": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.0.tgz",
"integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==",
"dev": true,
"license": "MIT"
},
"node_modules/@lezer/highlight": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.3.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.5.tgz",
"integrity": "sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@marijn/find-cluster-break": { "node_modules/@marijn/find-cluster-break": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
@@ -1012,8 +1053,7 @@
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
@@ -2223,8 +2263,7 @@
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/supports-color": { "node_modules/supports-color": {
"version": "7.2.0", "version": "7.2.0",
@@ -2349,8 +2388,7 @@
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",

View File

@@ -12,6 +12,8 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@codemirror/language": "^6.12.1",
"@lezer/common": "^1.5.0",
"@types/node": "^16.11.6", "@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0", "@typescript-eslint/parser": "5.29.0",

View File

@@ -2,45 +2,6 @@
Date Calc Plugin Styles Date Calc Plugin Styles
*/ */
/* Inline date-calc styling for reading view and live preview */
.date-calc-inline { .date-calc-inline {
background-color: var(--background-modifier-accent);
border-radius: 3px;
padding: 2px 6px;
color: var(--text-accent); color: var(--text-accent);
font-weight: bold; }
font-size: 0.9em;
display: inline-block;
margin: 0 2px;
font-family: var(--font-text);
}
/* Specific styles for CodeMirror Live Preview widgets */
.cm-line .date-calc-inline {
background-color: var(--background-modifier-accent);
color: var(--text-accent);
padding: 2px 6px;
margin-left: 4px;
border-radius: 3px;
font-weight: bold;
font-size: 0.9em;
display: inline;
user-select: none;
cursor: default;
vertical-align: baseline;
}
/* 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);
}