From e5552e2d4cb37b4ba7d965052a88cdb47260a2d2 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sun, 4 Jan 2026 19:59:31 -0500 Subject: [PATCH] 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. --- esbuild.config.mjs | 14 +- main.ts | 818 +++++++++++++++++---------------------------- package-lock.json | 58 +++- package.json | 2 + styles.css | 41 +-- 5 files changed, 353 insertions(+), 580 deletions(-) diff --git a/esbuild.config.mjs b/esbuild.config.mjs index a5de8b8..aface9c 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -3,7 +3,7 @@ import process from "process"; import builtins from "builtin-modules"; const banner = -`/* + `/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 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", "electron", "@codemirror/autocomplete", - "@codemirror/collab", - "@codemirror/commands", - "@codemirror/language", - "@codemirror/lint", - "@codemirror/search", - "@codemirror/state", - "@codemirror/view", - "@lezer/common", - "@lezer/highlight", - "@lezer/lr", + "@codemirror/*", + "@lezer/*", ...builtins], format: "cjs", target: "es2018", diff --git a/main.ts b/main.ts index a67f00f..2419fcb 100644 --- a/main.ts +++ b/main.ts @@ -1,18 +1,34 @@ -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'; +import { App, Notice, Plugin, PluginSettingTab, Setting, parseYaml } from "obsidian"; +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 { - mySetting: string; +interface DateCalcSettings { + debug: boolean; + hideResultWhileCursorInside: boolean; + verbose: boolean; } -const DEFAULT_SETTINGS: MyPluginSettings = { - mySetting: 'default' -} +const DEFAULT_SETTINGS: DateCalcSettings = { + debug: false, + hideResultWhileCursorInside: true, + verbose: false, // default to concise +}; + +/* ========================= + Helpers +========================= */ -// ===== Helper functions for date calculations ===== function zeroTime(d: Date): Date { const nd = new Date(d); nd.setHours(0, 0, 0, 0); @@ -32,597 +48,361 @@ function humanizeDuration(ms: number): string { let minutes = Math.floor(seconds / 60); let hours = Math.floor(minutes / 60); let days = Math.floor(hours / 24); - let years = Math.floor(days / 365); + const years = Math.floor(days / 365); days = days % 365; - let months = Math.floor(days / 30); + const 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 (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(', '); + if (!parts.length) return "0 seconds"; + 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 { + const cfg: Record = {}; + // 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 { - 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; - } - }); - } + if (!/^date-calc\s*:/.test(raw)) return ""; + + const params = raw.replace(/^date-calc\s*:/, "").trim(); + let cfg: any = params ? parseInlineArgs(params) : {}; + + // Support `date-calc: birthday` + let type = (cfg?.type ?? "").toString().toLowerCase().trim(); + if (!type && typeof cfg === "string" && /^[A-Za-z][A-Za-z-]*$/.test(cfg.trim())) { + type = cfg.trim().toLowerCase(); + cfg = {}; } - // 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 && /^[A-Za-z][A-Za-z-]*$/.test(params)) type = params.toLowerCase(); + + // Infer 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 (cfg.birthday || cfg.birthdate) type = "birthday"; + else if (cfg.to || cfg.until) type = "countdown"; + else if (cfg.since) type = "since"; + else if (cfg.from && cfg.to) type = "diff"; } - 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'; - } - - // 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 = ''; + + // Aliases + if (type === "bday") type = "birthday"; + if (type === "until") type = "countdown"; + if (type === "difference") type = "diff"; + if (!type) return ""; + try { switch (type) { - case 'birthday': { + case "birthday": { 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 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 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())); + + const 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 isBirthdayToday = today.getMonth() === bd.getMonth() && today.getDate() === bd.getDate(); const daysUntil = isBirthdayToday ? 0 : Math.round((nextBirthday.getTime() - today.getTime()) / oneDay); - let msg: string; + + let msg = ""; 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--; - 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; + } 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.`; + + return `Age: ${age} years. ${msg}`; } - case 'countdown': - case 'until': { + + case "countdown": { const toStr = cfg.to || cfg.date || cfg.until; 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 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; + const label = cfg.label ? `${cfg.label}: ` : ""; + + return diffMs >= 0 + ? `${label}Countdown: ${humanizeDuration(diffMs)}` + : `${label}Event passed ${humanizeDuration(-diffMs)} ago`; } - 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; } + + case "diff": { + const fromD = parseDate(cfg.from || cfg.start); + const toD = parseDate(cfg.to || cfg.end); + if (!fromD || !toD) return `date-calc: Provide valid "from" and "to" dates.`; + const diffMs = toD.getTime() - fromD.getTime(); - out = `Difference: ${humanizeDuration(Math.abs(diffMs))}${diffMs < 0 ? ' (to is before from)' : ''}`; - break; + return `Difference: ${humanizeDuration(Math.abs(diffMs))}${diffMs < 0 ? " (to is before from)" : ""}`; } - 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'; + + case "since": { + const sinceDate = parseDate(cfg.since || cfg.from || cfg.date); + if (!sinceDate) return `date-calc: Missing or invalid "since" date.`; + + const diffMs = Date.now() - sinceDate.getTime(); + return diffMs >= 0 ? `Since: ${humanizeDuration(diffMs)} ago` : `In: ${humanizeDuration(-diffMs)}`; } + + default: + return "date-calc: Unknown type. Supported: birthday, countdown, diff, since"; } } 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 { - 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; - } + constructor(private text: string) { super(); } + eq(other: DateCalcWidget) { return other.text === this.text; } + toDOM() { + const span = document.createElement("span"); + span.className = "date-calc-inline"; + span.textContent = this.text; + span.setAttribute("contenteditable", "false"); + return span; + } } -// Live Preview ViewPlugin for inline date-calc processing -const dateCalcLivePreviewPlugin = (app: App) => ViewPlugin.fromClass(class { - decorations: DecorationSet = Decoration.none; +export function dateCalcLivePreview(app: any, getSettings: () => { debug: boolean }) { + // Match only inline-code expressions (backticked) + const re = /`date-calc:[^`]*`/g; - constructor(view: EditorView) { - this.buildDecorations(view); - } + // Key fix: scan beyond visibleRanges so matches don't get missed at viewport boundaries + const margin = 2000; - update(update: ViewUpdate) { - if (update.docChanged || update.viewportChanged) { - this.buildDecorations(update.view); - } - } + return ViewPlugin.fromClass( + class { + decorations: DecorationSet = Decoration.none; - buildDecorations(view: EditorView) { - const builder = new RangeSetBuilder(); - const text = view.state.doc.toString(); + constructor(view: EditorView) { + this.decorations = this.build(view); + } - // Find inline code spans that start with date-calc: - // This regex looks for inline code between backticks - const inlineCodeRegex = /`([^`]+)`/g; - let match; + update(u: ViewUpdate) { + if (u.docChanged || u.viewportChanged || u.selectionSet) { + this.decorations = this.build(u.view); + } + } - 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; + build(view: EditorView): DecorationSet { + // IMPORTANT: do nothing in Source mode so raw markdown stays visible + if (!isLivePreviewEditor(view)) return Decoration.none; - // Get the active file path for processing - const activeFile = app.workspace.getActiveFile(); - const sourcePath = activeFile?.path || ''; + const b = new RangeSetBuilder(); + const sourcePath = app.workspace.getActiveFile()?.path ?? ""; + const head = view.state.selection.main.head; - // Process the date calculation - const result = processInlineDateCalc(codeContent, app, sourcePath); + let matchesSeen = 0; + let replaced = 0; - 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); - - // Add a debug log to verify when a widget is being created - console.log(`Live Preview widget created for: ${codeContent} with result: ${result}`); - } - } - } + const used = new Set(); - this.decorations = builder.finish(); - } -}, { - decorations: v => v.decorations -}); + 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); -// 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); - } + const slice = view.state.doc.sliceString(scanFrom, scanTo); + re.lastIndex = 0; - async onload() { - await this.render(); - } + let m: RegExpExecArray | null; + while ((m = re.exec(slice)) !== null) { + matchesSeen++; - async render() { - const result = processInlineDateCalc(this.raw, this.app, this.sourcePath); - if (result) { - console.log(`Rendering inline date-calc: ${this.raw} -> ${result}`); + const absStart = scanFrom + m.index; + const absEnd = absStart + m[0].length; - // In Live Preview, just replace the code element directly - const span = document.createElement('span'); - span.classList.add('date-calc-inline'); - span.setAttribute('contenteditable', 'false'); - span.textContent = result; + // de-dupe overlaps from overlapping scan windows + const key = `${absStart}:${absEnd}`; + if (used.has(key)) continue; + used.add(key); - // Ensure we properly replace the target element - try { - this.target.parentNode?.replaceChild(span, this.target); - console.log('Successfully replaced code element with result'); - } catch (e) { - console.error('Failed to replace code element:', e); - // Fallback: try the original method - this.target.replaceWith(span); - } - } else { - console.log(`No result for inline date-calc: ${this.raw}`); - } - } + // If cursor is inside, don't replace (so user can edit the raw inline code) + if (head >= absStart && head <= absEnd) continue; + + const inner = m[0].slice(1, -1).trim(); // remove backticks + const result = processInlineDateCalc(inner, app, sourcePath); + if (!result) continue; + + // Replace the entire backticked expression with a widget + b.add( + absStart, + 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 CodeMirror’s 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 { - settings: MyPluginSettings; +export default class DateCalcPlugin extends Plugin { + settings: DateCalcSettings; 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()); - } - async saveSettings() { - await this.saveData(this.settings); + console.log("[date-calc] loaded"); + 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) { - super(app); - } +/* ========================= + Settings tab +========================= */ - onOpen() { - const {contentEl} = this; - contentEl.setText('Woah!'); - } - - onClose() { - const {contentEl} = this; - contentEl.empty(); - } -} - -class SampleSettingTab extends PluginSettingTab { - plugin: MyPlugin; - - constructor(app: App, plugin: MyPlugin) { +class DateCalcSettingTab extends PluginSettingTab { + constructor(app: App, private plugin: DateCalcPlugin) { super(app, plugin); - this.plugin = plugin; } display(): void { - const {containerEl} = this; - + const { containerEl } = this; containerEl.empty(); - 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.'}); + containerEl.createEl("h2", { text: "Date Calc" }); - // Fenced code block examples - const blockSection = containerEl.createDiv(); - blockSection.createEl('h3', {text: 'Fenced Code Block Examples'}); + new Setting(containerEl) + .setName("Debug logging") + .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'}); - 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.'}); + new Setting(containerEl) + .setName("Hide result while cursor inside inline code") + .setDesc("Prevents the widget from showing while you edit the inline backticks.") + .addToggle((t) => + t.setValue(this.plugin.settings.hideResultWhileCursorInside).onChange(async (v) => { + this.plugin.settings.hideResultWhileCursorInside = v; + await this.plugin.saveData(this.plugin.settings); + }) + ); - 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.'}); + new Setting(containerEl) + .setName("Verbose output") + .setDesc("When enabled, show the longer/wordier messages (your current output).") + .addToggle(t => + t.setValue(this.plugin.settings.verbose).onChange(async (v) => { + this.plugin.settings.verbose = v; + await this.plugin.saveData(this.plugin.settings); + }) + ); - 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 = '`date-calc: birthday=1992-08-16` → Birthday summary'; - inlineExamples.createEl('li').innerHTML = '`date-calc: to=2025-12-31 label="New Year"` → Countdown'; - inlineExamples.createEl('li').innerHTML = '`date-calc: since=2024-01-01` → Time since'; - inlineExamples.createEl('li').innerHTML = '`date-calc: {type: diff, from: 2024-01-01, to: 2024-12-31}` → 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'}); + containerEl.createEl("p", { text: 'Example: `date-calc: birthday=1992-08-16`' }); } } diff --git a/package-lock.json b/package-lock.json index 291d42a..d25b9c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { + "@codemirror/language": "^6.12.1", + "@lezer/common": "^1.5.0", "@types/node": "^16.11.6", "@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/parser": "5.29.0", @@ -19,13 +21,27 @@ "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": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -36,7 +52,6 @@ "integrity": "sha512-bTWAJxL6EOFLPzTx+O5P5xAO3gTqpatQ2b/ARQ8itfU/v2LlpS3pH2fkL0A3E/Fx8Y2St2KES7ZEV0sHTsSW/A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -526,13 +541,39 @@ "license": "BSD-3-Clause", "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": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -1012,8 +1053,7 @@ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -2223,8 +2263,7 @@ "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/supports-color": { "version": "7.2.0", @@ -2349,8 +2388,7 @@ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/which": { "version": "2.0.2", diff --git a/package.json b/package.json index 6a00766..1725537 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "author": "", "license": "MIT", "devDependencies": { + "@codemirror/language": "^6.12.1", + "@lezer/common": "^1.5.0", "@types/node": "^16.11.6", "@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/parser": "5.29.0", diff --git a/styles.css b/styles.css index 49169e6..ec5cc3d 100644 --- a/styles.css +++ b/styles.css @@ -2,45 +2,6 @@ Date Calc Plugin Styles */ -/* Inline date-calc styling for reading view and live preview */ .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; - 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); -} +} \ No newline at end of file