make it work well
This commit is contained in:
574
main.ts
574
main.ts
@@ -1,12 +1,12 @@
|
||||
import { App, Notice, Plugin, PluginSettingTab, Setting, parseYaml } from "obsidian";
|
||||
import { App, Notice, Plugin, PluginSettingTab, Setting, parseYaml, moment } from "obsidian";
|
||||
import { RangeSetBuilder } from "@codemirror/state";
|
||||
import {
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
WidgetType,
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
WidgetType,
|
||||
} from "@codemirror/view";
|
||||
|
||||
/* =========================
|
||||
@@ -22,73 +22,114 @@ interface DateCalcSettings {
|
||||
const DEFAULT_SETTINGS: DateCalcSettings = {
|
||||
debug: false,
|
||||
hideResultWhileCursorInside: true,
|
||||
verbose: false, // default to concise
|
||||
verbose: false,
|
||||
};
|
||||
|
||||
/* =========================
|
||||
Helpers
|
||||
Moment helpers
|
||||
========================= */
|
||||
|
||||
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);
|
||||
const years = Math.floor(days / 365);
|
||||
days = days % 365;
|
||||
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 (!parts.length) return "0 seconds";
|
||||
return parts.join(", ");
|
||||
}
|
||||
type M = moment.Moment;
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse various inputs into a Moment.
|
||||
* - "YYYY-MM-DD" => local startOf("day")
|
||||
* - YAML Date objects that represent UTC midnight => keep same calendar date locally
|
||||
* - Other date-time inputs => moment(...) as-is
|
||||
*/
|
||||
export function parseMoment(input: unknown): M | null {
|
||||
if (input == null || input === "") return null;
|
||||
|
||||
// YAML can produce Date objects (often representing UTC midnight for date-only scalars)
|
||||
if (input instanceof Date) {
|
||||
if (Number.isNaN(input.getTime())) return null;
|
||||
|
||||
const isUtcMidnight =
|
||||
input.getUTCHours() === 0 &&
|
||||
input.getUTCMinutes() === 0 &&
|
||||
input.getUTCSeconds() === 0 &&
|
||||
input.getUTCMilliseconds() === 0;
|
||||
|
||||
if (isUtcMidnight) {
|
||||
return moment({
|
||||
year: input.getUTCFullYear(),
|
||||
month: input.getUTCMonth(),
|
||||
day: input.getUTCDate(),
|
||||
}).startOf("day");
|
||||
}
|
||||
|
||||
return moment(input);
|
||||
}
|
||||
|
||||
const s = String(input).trim();
|
||||
|
||||
// Strict date-only => local midnight
|
||||
const dateOnly = moment(s, "YYYY-MM-DD", true);
|
||||
if (dateOnly.isValid()) return dateOnly.startOf("day");
|
||||
|
||||
// Fallback: let Moment interpret other formats (ISO with time/offset, etc.)
|
||||
const any = moment(s);
|
||||
return any.isValid() ? any : null;
|
||||
}
|
||||
|
||||
function formatNiceDate(m: M): string {
|
||||
return m.format("MMMM Do, YYYY");
|
||||
}
|
||||
|
||||
/**
|
||||
* Calendar-aware breakdown (years, months, days, hours) with remainder behavior.
|
||||
* Returns a formatted string (verbose or concise).
|
||||
*/
|
||||
function formatSpan(from: M, to: M, verbose: boolean): { text: string; isNegative: boolean } {
|
||||
let a = from.clone();
|
||||
let b = to.clone();
|
||||
|
||||
const isNegative = b.isBefore(a);
|
||||
if (isNegative) [a, b] = [b, a];
|
||||
|
||||
const years = b.diff(a, "years"); a.add(years, "years");
|
||||
const months = b.diff(a, "months"); a.add(months, "months");
|
||||
const days = b.diff(a, "days"); a.add(days, "days");
|
||||
const hours = b.diff(a, "hours");
|
||||
|
||||
if (verbose) {
|
||||
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"}`);
|
||||
return { text: parts.join(", ") || "0 days", isNegative };
|
||||
} else {
|
||||
const parts: string[] = [];
|
||||
if (years) parts.push(`${years}y`);
|
||||
if (months) parts.push(`${months}mo`);
|
||||
if (days) parts.push(`${days}d`);
|
||||
if (hours && parts.length < 3) parts.push(`${hours}h`);
|
||||
return { text: parts.join(" ") || "0d", isNegative };
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Inline processor
|
||||
Inline args parsing
|
||||
========================= */
|
||||
|
||||
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("'"))) {
|
||||
if (
|
||||
(val.startsWith('"') && val.endsWith('"')) ||
|
||||
(val.startsWith("'") && val.endsWith("'"))
|
||||
) {
|
||||
val = val.slice(1, -1);
|
||||
}
|
||||
cfg[key] = val;
|
||||
@@ -97,125 +138,166 @@ function parseKv(params: string): Record<string, string> {
|
||||
}
|
||||
|
||||
function parseInlineArgs(params: string): any {
|
||||
// 1) If it looks like key=value syntax, parse that first (fast + reliable)
|
||||
if (!params) return {};
|
||||
|
||||
// Prefer key=value (fast, predictable)
|
||||
if (params.includes("=")) return parseKv(params);
|
||||
|
||||
// 2) Otherwise try YAML (for `{type: diff, from: ..., to: ...}` and similar)
|
||||
// Otherwise attempt YAML (supports `{ type: diff, from: ..., to: ... }`)
|
||||
try {
|
||||
const v = parseYaml(params);
|
||||
// If YAML returns a string, just return it (supports `date-calc: birthday`)
|
||||
return v ?? {};
|
||||
} catch {
|
||||
// Last resort: empty
|
||||
return {};
|
||||
// Fall back to raw string type alias like: `date-calc: birthday`
|
||||
return params.trim();
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeType(paramsRaw: string, cfg: any): { type: string; cfg: any } {
|
||||
const params = paramsRaw.trim();
|
||||
let type = (cfg?.type ?? "").toString().toLowerCase().trim();
|
||||
|
||||
// `date-calc: birthday`
|
||||
if (!type && typeof cfg === "string" && /^[A-Za-z][A-Za-z-]*$/.test(cfg.trim())) {
|
||||
type = cfg.trim().toLowerCase();
|
||||
cfg = {};
|
||||
}
|
||||
if (!type && /^[A-Za-z][A-Za-z-]*$/.test(params)) {
|
||||
type = params.toLowerCase();
|
||||
cfg = {};
|
||||
}
|
||||
|
||||
// Infer from fields
|
||||
if (!type && cfg && typeof cfg === "object") {
|
||||
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";
|
||||
}
|
||||
|
||||
// Aliases
|
||||
if (type === "bday") type = "birthday";
|
||||
if (type === "until") type = "countdown";
|
||||
if (type === "difference") type = "diff";
|
||||
|
||||
return { type, cfg };
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Inline processor
|
||||
========================= */
|
||||
|
||||
// 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 "";
|
||||
function processInlineDateCalc(
|
||||
raw: string,
|
||||
app: App,
|
||||
sourcePath: string,
|
||||
settings: DateCalcSettings
|
||||
): { text: string; tooltip?: string } {
|
||||
if (!/^date-calc\s*:/.test(raw)) return { text: "" };
|
||||
|
||||
const params = raw.replace(/^date-calc\s*:/, "").trim();
|
||||
let cfg: any = params ? parseInlineArgs(params) : {};
|
||||
const paramsRaw = raw.replace(/^date-calc\s*:/, "").trim();
|
||||
let cfg: any = parseInlineArgs(paramsRaw);
|
||||
const norm = normalizeType(paramsRaw, cfg);
|
||||
const type = norm.type;
|
||||
cfg = norm.cfg;
|
||||
|
||||
// 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 = {};
|
||||
}
|
||||
if (!type && /^[A-Za-z][A-Za-z-]*$/.test(params)) type = params.toLowerCase();
|
||||
if (!type) return { text: "" };
|
||||
|
||||
// Infer
|
||||
if (!type) {
|
||||
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";
|
||||
}
|
||||
const verbose = settings.verbose;
|
||||
|
||||
// Aliases
|
||||
if (type === "bday") type = "birthday";
|
||||
if (type === "until") type = "countdown";
|
||||
if (type === "difference") type = "diff";
|
||||
if (!type) return "";
|
||||
try {
|
||||
switch (type) {
|
||||
case "birthday": {
|
||||
const fileCache = app.metadataCache.getCache(sourcePath);
|
||||
const fm = (fileCache?.frontmatter ?? {}) as any;
|
||||
|
||||
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) return `date-calc: Missing or invalid "birthday" date.`;
|
||||
const bstr =
|
||||
cfg.birthday ||
|
||||
cfg.birthdate ||
|
||||
cfg.date ||
|
||||
fm?.birthday ||
|
||||
fm?.birthdate;
|
||||
|
||||
const today = zeroTime(new Date());
|
||||
const bd = zeroTime(birthDate);
|
||||
const bd = parseMoment(bstr)?.startOf("day");
|
||||
if (!bd) return { text: `date-calc: Missing or invalid "birthday" date.` };
|
||||
|
||||
let age = today.getFullYear() - bd.getFullYear();
|
||||
const m = today.getMonth() - bd.getMonth();
|
||||
if (m < 0 || (m === 0 && today.getDate() < bd.getDate())) age--;
|
||||
const today = moment().startOf("day");
|
||||
|
||||
const nextBirthday = zeroTime(new Date(today.getFullYear(), bd.getMonth(), bd.getDate()));
|
||||
if (today > nextBirthday) nextBirthday.setFullYear(today.getFullYear() + 1);
|
||||
const age = today.diff(bd, "years");
|
||||
|
||||
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);
|
||||
const next = bd.clone().year(today.year());
|
||||
if (next.isBefore(today, "day")) next.add(1, "year");
|
||||
|
||||
let msg = "";
|
||||
if (daysUntil > 31) {
|
||||
let monthsUntil =
|
||||
(nextBirthday.getFullYear() - today.getFullYear()) * 12 +
|
||||
(nextBirthday.getMonth() - today.getMonth());
|
||||
if (today.getDate() > nextBirthday.getDate()) monthsUntil--;
|
||||
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.`;
|
||||
const daysUntil = next.diff(today, "days");
|
||||
|
||||
return `Age: ${age} years. ${msg}`;
|
||||
}
|
||||
let msg: string;
|
||||
if (daysUntil === 0) msg = verbose ? "Wish them Happy Birthday!" : "Birthday!";
|
||||
else if (daysUntil === 1) msg = verbose ? "Their birthday is tomorrow!" : "Tomorrow!";
|
||||
else if (daysUntil > 31) {
|
||||
const monthsUntil = next.diff(today, "months");
|
||||
msg = verbose ? `Next birthday in ${monthsUntil} months.` : `Next in ${monthsUntil}mo`;
|
||||
} else {
|
||||
msg = verbose ? `Next birthday in ${daysUntil} days.` : `Next in ${daysUntil}d`;
|
||||
}
|
||||
|
||||
case "countdown": {
|
||||
const toStr = cfg.to || cfg.date || cfg.until;
|
||||
const toDate = parseDate(toStr);
|
||||
if (!toDate) return `date-calc: Missing or invalid "to" date.`;
|
||||
const ageStr = verbose ? `${age} years` : `${age}y`;
|
||||
return {
|
||||
text: `Age: ${ageStr}. ${msg}`,
|
||||
tooltip: formatNiceDate(bd),
|
||||
};
|
||||
}
|
||||
|
||||
const fromDate = parseDate(cfg.from) || new Date();
|
||||
const diffMs = toDate.getTime() - fromDate.getTime();
|
||||
const label = cfg.label ? `${cfg.label}: ` : "";
|
||||
case "countdown": {
|
||||
const to = parseMoment(cfg.to || cfg.date || cfg.until);
|
||||
if (!to) return { text: `date-calc: Missing or invalid "to" date.` };
|
||||
|
||||
return diffMs >= 0
|
||||
? `${label}Countdown: ${humanizeDuration(diffMs)}`
|
||||
: `${label}Event passed ${humanizeDuration(-diffMs)} ago`;
|
||||
}
|
||||
const from = parseMoment(cfg.from) ?? moment();
|
||||
const label = cfg.label ? `${cfg.label}: ` : "";
|
||||
|
||||
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 span = formatSpan(from, to, verbose);
|
||||
|
||||
const diffMs = toD.getTime() - fromD.getTime();
|
||||
return `Difference: ${humanizeDuration(Math.abs(diffMs))}${diffMs < 0 ? " (to is before from)" : ""}`;
|
||||
}
|
||||
const text = !span.isNegative
|
||||
? `${label}${verbose ? "Countdown: " : ""}${span.text}`
|
||||
: `${label}${verbose ? "Event passed " : ""}${span.text}${verbose ? " ago" : ""}`;
|
||||
|
||||
case "since": {
|
||||
const sinceDate = parseDate(cfg.since || cfg.from || cfg.date);
|
||||
if (!sinceDate) return `date-calc: Missing or invalid "since" date.`;
|
||||
return { text };
|
||||
}
|
||||
|
||||
const diffMs = Date.now() - sinceDate.getTime();
|
||||
return diffMs >= 0 ? `Since: ${humanizeDuration(diffMs)} ago` : `In: ${humanizeDuration(-diffMs)}`;
|
||||
}
|
||||
case "diff": {
|
||||
const from = parseMoment(cfg.from || cfg.start);
|
||||
const to = parseMoment(cfg.to || cfg.end);
|
||||
if (!from || !to) return { text: `date-calc: Provide valid "from" and "to" dates.` };
|
||||
|
||||
default:
|
||||
return "date-calc: Unknown type. Supported: birthday, countdown, diff, since";
|
||||
}
|
||||
} catch (e: any) {
|
||||
return `date-calc error: ${e?.message || e}`;
|
||||
}
|
||||
const span = formatSpan(from, to, verbose);
|
||||
|
||||
const text = verbose
|
||||
? `Difference: ${span.text}${span.isNegative ? " (to is before from)" : ""}`
|
||||
: `Diff: ${span.text}${span.isNegative ? " (reverse)" : ""}`;
|
||||
|
||||
return { text };
|
||||
}
|
||||
|
||||
case "since": {
|
||||
const since = parseMoment(cfg.since || cfg.from || cfg.date);
|
||||
if (!since) return { text: `date-calc: Missing or invalid "since" date.` };
|
||||
|
||||
const now = moment();
|
||||
const span = formatSpan(since, now, verbose);
|
||||
|
||||
const text = !span.isNegative
|
||||
? `Since: ${span.text}${verbose ? " ago" : ""}`
|
||||
: `In: ${span.text}`;
|
||||
|
||||
return { text };
|
||||
}
|
||||
|
||||
default:
|
||||
return { text: "date-calc: Unknown type. Supported: birthday, countdown, diff, since" };
|
||||
}
|
||||
} catch (e: any) {
|
||||
return { text: `date-calc error: ${e?.message || e}` };
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================
|
||||
@@ -223,22 +305,40 @@ function processInlineDateCalc(raw: string, app: App, sourcePath: string): strin
|
||||
========================= */
|
||||
|
||||
class DateCalcWidget extends WidgetType {
|
||||
constructor(private text: string) { super(); }
|
||||
eq(other: DateCalcWidget) { return other.text === this.text; }
|
||||
constructor(
|
||||
private text: string,
|
||||
private tooltip: string | undefined,
|
||||
private from: number,
|
||||
private to: number
|
||||
) { super(); }
|
||||
|
||||
eq(other: DateCalcWidget) {
|
||||
return other.text === this.text && other.tooltip === this.tooltip;
|
||||
}
|
||||
toDOM() {
|
||||
const span = document.createElement("span");
|
||||
span.className = "date-calc-inline";
|
||||
span.textContent = this.text;
|
||||
span.setAttribute("contenteditable", "false");
|
||||
|
||||
// store source range for click-to-edit
|
||||
span.dataset.dcFrom = String(this.from);
|
||||
span.dataset.dcTo = String(this.to);
|
||||
|
||||
if (this.tooltip) {
|
||||
span.className += " has-tooltip";
|
||||
span.setAttribute("aria-label", this.tooltip);
|
||||
}
|
||||
return span;
|
||||
}
|
||||
|
||||
ignoreEvent() { return false; } // important for interaction [web:64]
|
||||
}
|
||||
|
||||
export function dateCalcLivePreview(app: any, getSettings: () => { debug: boolean }) {
|
||||
// Match only inline-code expressions (backticked)
|
||||
export function dateCalcLivePreview(app: App, getSettings: () => DateCalcSettings) {
|
||||
const re = /`date-calc:[^`]*`/g;
|
||||
|
||||
// Key fix: scan beyond visibleRanges so matches don't get missed at viewport boundaries
|
||||
// Scan past viewport boundaries so matches don't flicker/miss near edges
|
||||
const margin = 2000;
|
||||
|
||||
return ViewPlugin.fromClass(
|
||||
@@ -256,18 +356,19 @@ export function dateCalcLivePreview(app: any, getSettings: () => { debug: boolea
|
||||
}
|
||||
|
||||
build(view: EditorView): DecorationSet {
|
||||
// IMPORTANT: do nothing in Source mode so raw markdown stays visible
|
||||
if (!isLivePreviewEditor(view)) return Decoration.none;
|
||||
// Do nothing in Source mode so raw markdown stays visible
|
||||
if (!isLivePreviewEditor(view)) return Decoration.none;
|
||||
|
||||
const b = new RangeSetBuilder<Decoration>();
|
||||
const settings = getSettings();
|
||||
const sourcePath = app.workspace.getActiveFile()?.path ?? "";
|
||||
const head = view.state.selection.main.head;
|
||||
|
||||
const b = new RangeSetBuilder<Decoration>();
|
||||
const used = new Set<string>();
|
||||
|
||||
let matchesSeen = 0;
|
||||
let replaced = 0;
|
||||
|
||||
const used = new Set<string>();
|
||||
|
||||
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);
|
||||
@@ -282,46 +383,67 @@ export function dateCalcLivePreview(app: any, getSettings: () => { debug: boolea
|
||||
const absStart = scanFrom + m.index;
|
||||
const absEnd = absStart + m[0].length;
|
||||
|
||||
// de-dupe overlaps from overlapping scan windows
|
||||
// De-dupe overlaps from overlapping scan windows
|
||||
const key = `${absStart}:${absEnd}`;
|
||||
if (used.has(key)) continue;
|
||||
used.add(key);
|
||||
|
||||
// If cursor is inside, don't replace (so user can edit the raw inline code)
|
||||
if (head >= absStart && head <= absEnd) continue;
|
||||
// Optional: keep raw inline code visible while editing it
|
||||
const sel = view.state.selection.main;
|
||||
const intersects = sel.from <= absEnd && sel.to >= absStart;
|
||||
|
||||
if (settings.hideResultWhileCursorInside && intersects) continue;
|
||||
|
||||
|
||||
const inner = m[0].slice(1, -1).trim(); // remove backticks
|
||||
const result = processInlineDateCalc(inner, app, sourcePath);
|
||||
if (!result) continue;
|
||||
const result = processInlineDateCalc(inner, app, sourcePath, settings);
|
||||
if (!result.text) continue;
|
||||
|
||||
// Replace the entire backticked expression with a widget
|
||||
b.add(
|
||||
absStart,
|
||||
absEnd,
|
||||
Decoration.replace({
|
||||
widget: new DateCalcWidget(result),
|
||||
inclusive: false,
|
||||
})
|
||||
widget: new DateCalcWidget(result.text, result.tooltip, absStart, absEnd),
|
||||
inclusive: false,
|
||||
})
|
||||
);
|
||||
replaced++;
|
||||
}
|
||||
}
|
||||
|
||||
if (getSettings().debug) {
|
||||
if (settings.debug) {
|
||||
console.log("[date-calc] replace scan", { matchesSeen, replaced, sourcePath });
|
||||
}
|
||||
|
||||
return b.finish();
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
{
|
||||
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),
|
||||
}
|
||||
eventHandlers: {
|
||||
mousedown: (e, view) => {
|
||||
const el = (e.target as HTMLElement | null)?.closest?.(".date-calc-inline") as HTMLElement | null;
|
||||
if (!el) return false;
|
||||
|
||||
const from = Number(el.dataset.dcFrom);
|
||||
const to = Number(el.dataset.dcTo);
|
||||
if (!Number.isFinite(from) || !Number.isFinite(to)) return false;
|
||||
|
||||
// Select the whole backticked expression so it immediately “reveals” for editing
|
||||
view.dispatch({
|
||||
selection: { anchor: from, head: to },
|
||||
scrollIntoView: true,
|
||||
});
|
||||
view.focus();
|
||||
|
||||
return true; // CodeMirror will preventDefault when handler returns true [web:64]
|
||||
},
|
||||
},
|
||||
|
||||
provide: (plugin) =>
|
||||
EditorView.atomicRanges.of((view) => view.plugin(plugin)?.decorations ?? Decoration.none),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -330,31 +452,38 @@ export function dateCalcLivePreview(app: any, getSettings: () => { debug: boolea
|
||||
========================= */
|
||||
|
||||
export default class DateCalcPlugin extends Plugin {
|
||||
settings: DateCalcSettings;
|
||||
settings: DateCalcSettings;
|
||||
|
||||
async onload() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
async onload() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
|
||||
console.log("[date-calc] loaded");
|
||||
new Notice("Date Calc loaded (Live Preview)");
|
||||
if (this.settings.debug) 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.registerEditorExtension(dateCalcLivePreview(this.app, () => this.settings));
|
||||
|
||||
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"}`);
|
||||
},
|
||||
});
|
||||
|
||||
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.addCommand({
|
||||
id: "date-calc-toggle-verbose",
|
||||
name: "Date Calc: Toggle verbose output",
|
||||
callback: async () => {
|
||||
this.settings.verbose = !this.settings.verbose;
|
||||
await this.saveData(this.settings);
|
||||
new Notice(`Date Calc verbose: ${this.settings.verbose ? "ON" : "OFF"}`);
|
||||
},
|
||||
});
|
||||
|
||||
this.addSettingTab(new DateCalcSettingTab(this.app, this));
|
||||
}
|
||||
this.addSettingTab(new DateCalcSettingTab(this.app, this));
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================
|
||||
@@ -362,47 +491,46 @@ export default class DateCalcPlugin extends Plugin {
|
||||
========================= */
|
||||
|
||||
class DateCalcSettingTab extends PluginSettingTab {
|
||||
constructor(app: App, private plugin: DateCalcPlugin) {
|
||||
super(app, plugin);
|
||||
}
|
||||
constructor(app: App, private plugin: DateCalcPlugin) {
|
||||
super(app, plugin);
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl("h2", { text: "Date Calc" });
|
||||
containerEl.createEl("h2", { text: "Date Calc" });
|
||||
|
||||
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);
|
||||
})
|
||||
);
|
||||
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);
|
||||
})
|
||||
);
|
||||
|
||||
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);
|
||||
})
|
||||
);
|
||||
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);
|
||||
})
|
||||
);
|
||||
|
||||
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);
|
||||
})
|
||||
);
|
||||
new Setting(containerEl)
|
||||
.setName("Verbose output")
|
||||
.setDesc("When enabled, show longer/wordier messages.")
|
||||
.addToggle((t) =>
|
||||
t.setValue(this.plugin.settings.verbose).onChange(async (v) => {
|
||||
this.plugin.settings.verbose = v;
|
||||
await this.plugin.saveData(this.plugin.settings);
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
containerEl.createEl("p", { text: 'Example: `date-calc: birthday=1992-08-16`' });
|
||||
}
|
||||
containerEl.createEl("p", { text: 'Example: `date-calc: birthday=1992-08-16`' });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user