fix codeblock

This commit is contained in:
2026-01-04 21:10:35 -05:00
parent 83846e7602
commit 32daa33af6

535
main.ts
View File

@@ -1,5 +1,6 @@
import { App, Notice, Plugin, PluginSettingTab, Setting, parseYaml, moment } from "obsidian"; import { App, Notice, Plugin, PluginSettingTab, Setting, parseYaml, moment } from "obsidian";
import { RangeSetBuilder } from "@codemirror/state"; import { RangeSetBuilder } from "@codemirror/state";
import { syntaxTree } from "@codemirror/language";
import { import {
Decoration, Decoration,
DecorationSet, DecorationSet,
@@ -9,43 +10,70 @@ import {
WidgetType, WidgetType,
} from "@codemirror/view"; } from "@codemirror/view";
/* ========================= /* =========================
Settings Settings
========================= */ ========================= */
interface DateCalcSettings { interface DateCalcSettings {
debug: boolean; debug: boolean;
hideResultWhileCursorInside: boolean; hideResultWhileCursorInside: boolean;
verbose: boolean; verbose: boolean;
} }
const DEFAULT_SETTINGS: DateCalcSettings = { const DEFAULT_SETTINGS: DateCalcSettings = {
debug: false, debug: false,
hideResultWhileCursorInside: true, hideResultWhileCursorInside: true,
verbose: false, verbose: false,
}; };
/* =========================
Types & Interfaces
========================= */
interface DateCalcResult {
text: string;
tooltip?: string;
}
interface DateCalcConfig {
type?: string;
birthday?: string;
birthdate?: string;
date?: string;
from?: string;
to?: string;
until?: string;
since?: string;
start?: string;
end?: string;
label?: string;
verbose?: boolean;
}
/* ========================= /* =========================
Moment helpers Moment helpers
========================= */ ========================= */
type M = moment.Moment; type M = moment.Moment;
function isLivePreviewEditor(cm: EditorView): boolean { function isLivePreviewEditor(cm: EditorView): boolean {
const container = cm.dom.closest(".markdown-source-view"); const container = cm.dom.closest(".markdown-source-view");
return !!container?.classList.contains("is-live-preview"); 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 { export function parseMoment(input: unknown): M | null {
if (input == null || input === "") return 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 (input instanceof Date) {
if (Number.isNaN(input.getTime())) return null; if (Number.isNaN(input.getTime())) return null;
@@ -67,24 +95,19 @@ export function parseMoment(input: unknown): M | null {
} }
const s = String(input).trim(); const s = String(input).trim();
// Strict date-only => local midnight
const dateOnly = moment(s, "YYYY-MM-DD", true); const dateOnly = moment(s, "YYYY-MM-DD", true);
if (dateOnly.isValid()) return dateOnly.startOf("day"); if (dateOnly.isValid()) return dateOnly.startOf("day");
// Fallback: let Moment interpret other formats (ISO with time/offset, etc.)
const any = moment(s); const any = moment(s);
return any.isValid() ? any : null; return any.isValid() ? any : null;
} }
function formatNiceDate(m: M): string { function formatNiceDate(m: M): string {
return m.format("MMMM Do, YYYY"); 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 } { function formatSpan(from: M, to: M, verbose: boolean): { text: string; isNegative: boolean } {
let a = from.clone(); let a = from.clone();
let b = to.clone(); let b = to.clone();
@@ -114,10 +137,12 @@ function formatSpan(from: M, to: M, verbose: boolean): { text: string; isNegativ
} }
} }
/* ========================= /* =========================
Inline args parsing Config parsing & normalization
========================= */ ========================= */
function parseKv(params: string): Record<string, string> { function parseKv(params: string): Record<string, string> {
const cfg: Record<string, string> = {}; const cfg: Record<string, string> = {};
const rx = /(\w+)=("([^"\\]|\\.)*"|'([^'\\]|\\.)*'|\S+)/g; const rx = /(\w+)=("([^"\\]|\\.)*"|'([^'\\]|\\.)*'|\S+)/g;
@@ -137,27 +162,26 @@ function parseKv(params: string): Record<string, string> {
return cfg; return cfg;
} }
function parseInlineArgs(params: string): any {
function parseConfig(params: string): DateCalcConfig | string {
if (!params) return {}; if (!params) return {};
// Prefer key=value (fast, predictable)
if (params.includes("=")) return parseKv(params); if (params.includes("=")) return parseKv(params);
// Otherwise attempt YAML (supports `{ type: diff, from: ..., to: ... }`)
try { try {
const v = parseYaml(params); const v = parseYaml(params);
return v ?? {}; return v ?? {};
} catch { } catch {
// Fall back to raw string type alias like: `date-calc: birthday`
return params.trim(); return params.trim();
} }
} }
function normalizeType(paramsRaw: string, cfg: any): { type: string; cfg: any } {
function normalizeConfig(paramsRaw: string, cfg: any): { type: string; cfg: DateCalcConfig } {
const params = paramsRaw.trim(); const params = paramsRaw.trim();
let type = (cfg?.type ?? "").toString().toLowerCase().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())) { if (!type && typeof cfg === "string" && /^[A-Za-z][A-Za-z-]*$/.test(cfg.trim())) {
type = cfg.trim().toLowerCase(); type = cfg.trim().toLowerCase();
cfg = {}; cfg = {};
@@ -167,7 +191,6 @@ function normalizeType(paramsRaw: string, cfg: any): { type: string; cfg: any }
cfg = {}; cfg = {};
} }
// Infer from fields
if (!type && cfg && typeof cfg === "object") { if (!type && cfg && typeof cfg === "object") {
if (cfg.birthday || cfg.birthdate) type = "birthday"; if (cfg.birthday || cfg.birthdate) type = "birthday";
else if (cfg.to || cfg.until) type = "countdown"; else if (cfg.to || cfg.until) type = "countdown";
@@ -175,7 +198,6 @@ function normalizeType(paramsRaw: string, cfg: any): { type: string; cfg: any }
else if (cfg.from && cfg.to) type = "diff"; else if (cfg.from && cfg.to) type = "diff";
} }
// Aliases
if (type === "bday") type = "birthday"; if (type === "bday") type = "birthday";
if (type === "until") type = "countdown"; if (type === "until") type = "countdown";
if (type === "difference") type = "diff"; if (type === "difference") type = "diff";
@@ -183,115 +205,32 @@ function normalizeType(paramsRaw: string, cfg: any): { type: string; cfg: any }
return { type, cfg }; return { type, cfg };
} }
/* ========================= /* =========================
Inline processor Core date calculation logic
========================= */ ========================= */
// raw should be WITHOUT backticks: "date-calc: birthday=1992-08-16"
function processInlineDateCalc( function calculateDateResult(
raw: string, type: string,
cfg: DateCalcConfig,
app: App, app: App,
sourcePath: string, sourcePath: string,
settings: DateCalcSettings verbose: boolean
): { text: string; tooltip?: string } { ): DateCalcResult {
if (!/^date-calc\s*:/.test(raw)) return { text: "" };
const paramsRaw = raw.replace(/^date-calc\s*:/, "").trim(); const useVerbose = cfg.verbose !== undefined ? cfg.verbose : verbose;
let cfg: any = parseInlineArgs(paramsRaw);
const norm = normalizeType(paramsRaw, cfg);
const type = norm.type;
cfg = norm.cfg;
if (!type) return { text: "" };
const verbose = settings.verbose;
try { try {
switch (type) { switch (type) {
case "birthday": { case "birthday":
const fileCache = app.metadataCache.getCache(sourcePath); return calculateBirthday(cfg, app, sourcePath, useVerbose);
const fm = (fileCache?.frontmatter ?? {}) as any; case "countdown":
return calculateCountdown(cfg, useVerbose);
const bstr = case "diff":
cfg.birthday || return calculateDiff(cfg, useVerbose);
cfg.birthdate || case "since":
cfg.date || return calculateSince(cfg, useVerbose);
fm?.birthday ||
fm?.birthdate;
const bd = parseMoment(bstr)?.startOf("day");
if (!bd) return { text: `date-calc: Missing or invalid "birthday" date.` };
const today = moment().startOf("day");
const age = today.diff(bd, "years");
const next = bd.clone().year(today.year());
if (next.isBefore(today, "day")) next.add(1, "year");
const daysUntil = next.diff(today, "days");
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`;
}
const ageStr = verbose ? `${age} years` : `${age}y`;
return {
text: `Age: ${ageStr}. ${msg}`,
tooltip: formatNiceDate(bd),
};
}
case "countdown": {
const to = parseMoment(cfg.to || cfg.date || cfg.until);
if (!to) return { text: `date-calc: Missing or invalid "to" date.` };
const from = parseMoment(cfg.from) ?? moment();
const label = cfg.label ? `${cfg.label}: ` : "";
const span = formatSpan(from, to, verbose);
const text = !span.isNegative
? `${label}${verbose ? "Countdown: " : ""}${span.text}`
: `${label}${verbose ? "Event passed " : ""}${span.text}${verbose ? " ago" : ""}`;
return { text };
}
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.` };
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: default:
return { text: "date-calc: Unknown type. Supported: birthday, countdown, diff, since" }; return { text: "date-calc: Unknown type. Supported: birthday, countdown, diff, since" };
} }
@@ -300,28 +239,160 @@ function processInlineDateCalc(
} }
} }
function calculateBirthday(
cfg: DateCalcConfig,
app: App,
sourcePath: string,
verbose: boolean
): DateCalcResult {
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 bd = parseMoment(bstr)?.startOf("day");
if (!bd) return { text: `date-calc: Missing or invalid "birthday" date.` };
const today = moment().startOf("day");
const age = today.diff(bd, "years");
const next = bd.clone().year(today.year());
if (next.isBefore(today, "day")) next.add(1, "year");
const daysUntil = next.diff(today, "days");
let msg: string;
if (daysUntil === 0) {
msg = verbose ? "Wish them Happy Birthday!" : "Happy bday!";
} else if (daysUntil === 1) {
msg = verbose ? "Their birthday is tomorrow!" : "Bday's tomorrow!";
} else if (daysUntil > 31) {
const monthsUntil = next.diff(today, "months");
msg = verbose ? `Next birthday in ${monthsUntil} months.` : `Next bday in ${monthsUntil}mo`;
} else {
msg = verbose ? `Next birthday in ${daysUntil} days.` : `Next bday in ${daysUntil}d`;
}
const ageStr = verbose ? `${age} years old` : `${age}y`;
return { text: `${ageStr}. ${msg}`, tooltip: formatNiceDate(bd) };
}
function calculateCountdown(cfg: DateCalcConfig, verbose: boolean): DateCalcResult {
const to = parseMoment(cfg.to || cfg.date || cfg.until);
if (!to) return { text: `date-calc: Missing or invalid "to" date.` };
const from = parseMoment(cfg.from) ?? moment();
const label = cfg.label ? `${cfg.label}: ` : "";
const span = formatSpan(from, to, verbose);
const text = !span.isNegative
? `${label}${verbose ? "Countdown: " : ""}${span.text}`
: `${label}${verbose ? "Event passed " : ""}${span.text}${verbose ? " ago" : ""}`;
return { text };
}
function calculateDiff(cfg: DateCalcConfig, verbose: boolean): DateCalcResult {
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.` };
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 };
}
function calculateSince(cfg: DateCalcConfig, verbose: boolean): DateCalcResult {
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 };
}
/* ========================= /* =========================
Live Preview decorations Processing entry points
========================= */ ========================= */
class DateCalcWidget extends WidgetType {
function processInlineCode(
raw: string,
app: App,
sourcePath: string,
settings: DateCalcSettings
): DateCalcResult {
if (!/^date-calc\s*:/.test(raw)) return { text: "" };
const paramsRaw = raw.replace(/^date-calc\s*:/, "").trim();
const cfg: any = parseConfig(paramsRaw);
const norm = normalizeConfig(paramsRaw, cfg);
if (!norm.type) return { text: "" };
return calculateDateResult(norm.type, norm.cfg, app, sourcePath, settings.verbose);
}
function processFencedBlock(
source: string,
app: App,
sourcePath: string,
settings: DateCalcSettings
): DateCalcResult {
let cfg: any = {};
try {
cfg = parseYaml(source) ?? {};
} catch {
cfg = parseConfig(source.trim());
}
const norm = normalizeConfig("", cfg);
if (!norm.type) {
return { text: 'date-calc: Missing "type" (birthday/countdown/diff/since).' };
}
return calculateDateResult(norm.type, norm.cfg, app, sourcePath, settings.verbose);
}
/* =========================
Widgets
========================= */
class DateCalcInlineWidget extends WidgetType {
constructor( constructor(
private text: string, private text: string,
private tooltip: string | undefined, private tooltip: string | undefined,
private from: number, private from: number,
private to: number private to: number
) { super(); } ) {
super();
}
eq(other: DateCalcWidget) { eq(other: DateCalcInlineWidget) {
return other.text === this.text && other.tooltip === this.tooltip; return other.text === this.text && other.tooltip === this.tooltip;
} }
toDOM() { toDOM() {
const span = document.createElement("span"); const span = document.createElement("span");
span.className = "date-calc-inline"; span.className = "date-calc-inline";
span.textContent = this.text; span.textContent = this.text;
span.setAttribute("contenteditable", "false"); span.setAttribute("contenteditable", "false");
// store source range for click-to-edit
span.dataset.dcFrom = String(this.from); span.dataset.dcFrom = String(this.from);
span.dataset.dcTo = String(this.to); span.dataset.dcTo = String(this.to);
@@ -332,13 +403,63 @@ class DateCalcWidget extends WidgetType {
return span; return span;
} }
ignoreEvent() { return false; } // important for interaction [web:64] ignoreEvent() {
return false;
}
} }
export function dateCalcLivePreview(app: App, getSettings: () => DateCalcSettings) {
const re = /`date-calc:[^`]*`/g;
// Scan past viewport boundaries so matches don't flicker/miss near edges class DateCalcBlockWidget extends WidgetType {
constructor(private text: string, private tooltip?: string) {
super();
}
eq(other: DateCalcBlockWidget) {
return other.text === this.text && other.tooltip === this.tooltip;
}
toDOM() {
const div = document.createElement("div");
div.className = "date-calc-block";
div.textContent = this.text;
div.setAttribute("contenteditable", "false");
if (this.tooltip) div.setAttribute("aria-label", this.tooltip);
return div;
}
}
/* =========================
Helpers for fenced code
========================= */
function parseFencedCode(text: string): { lang: string; body: string } | null {
const lines = text.split("\n");
if (lines.length < 2) return null;
const m = lines[0].match(/^(\s*)(`{3,}|~{3,})(.*)$/);
if (!m) return null;
const fence = m[2];
const info = (m[3] ?? "").trim();
const lang = (info.split(/\s+/)[0] ?? "").toLowerCase();
const last = lines[lines.length - 1].trim();
if (!last.startsWith(fence)) return null;
const body = lines.slice(1, -1).join("\n");
return { lang, body };
}
/* =========================
Live Preview decorations
========================= */
export function dateCalcLivePreview(app: App, getSettings: () => DateCalcSettings) {
const inlineRe = /`date-calc:[^`]*`/g;
const margin = 2000; const margin = 2000;
return ViewPlugin.fromClass( return ViewPlugin.fromClass(
@@ -356,101 +477,133 @@ export function dateCalcLivePreview(app: App, getSettings: () => DateCalcSetting
} }
build(view: EditorView): DecorationSet { build(view: EditorView): DecorationSet {
// Do nothing in Source mode so raw markdown stays visible
if (!isLivePreviewEditor(view)) return Decoration.none; if (!isLivePreviewEditor(view)) return Decoration.none;
const settings = getSettings(); const settings = getSettings();
const sourcePath = app.workspace.getActiveFile()?.path ?? ""; const sourcePath = app.workspace.getActiveFile()?.path ?? "";
const head = view.state.selection.main.head; const sel = view.state.selection.main;
const b = new RangeSetBuilder<Decoration>(); const b = new RangeSetBuilder<Decoration>();
const used = new Set<string>(); const used = new Set<string>();
let matchesSeen = 0; // Process inline code
let replaced = 0;
for (const r of view.visibleRanges) { for (const r of view.visibleRanges) {
const scanFrom = Math.max(0, r.from - margin); const scanFrom = Math.max(0, r.from - margin);
const scanTo = Math.min(view.state.doc.length, r.to + margin); const scanTo = Math.min(view.state.doc.length, r.to + margin);
const slice = view.state.doc.sliceString(scanFrom, scanTo); const slice = view.state.doc.sliceString(scanFrom, scanTo);
re.lastIndex = 0;
inlineRe.lastIndex = 0;
let m: RegExpExecArray | null; let m: RegExpExecArray | null;
while ((m = re.exec(slice)) !== null) { while ((m = inlineRe.exec(slice)) !== null) {
matchesSeen++;
const absStart = scanFrom + m.index; const absStart = scanFrom + m.index;
const absEnd = absStart + m[0].length; const absEnd = absStart + m[0].length;
const key = `inline:${absStart}:${absEnd}`;
// De-dupe overlaps from overlapping scan windows
const key = `${absStart}:${absEnd}`;
if (used.has(key)) continue; if (used.has(key)) continue;
used.add(key); used.add(key);
// Optional: keep raw inline code visible while editing it const intersects = sel.from <= absEnd && sel.to >= absStart;
const sel = view.state.selection.main; if (settings.hideResultWhileCursorInside && intersects) continue;
const intersects = sel.from <= absEnd && sel.to >= absStart;
if (settings.hideResultWhileCursorInside && intersects) continue; const inner = m[0].slice(1, -1).trim();
const result = processInlineCode(inner, app, sourcePath, settings);
const inner = m[0].slice(1, -1).trim(); // remove backticks
const result = processInlineDateCalc(inner, app, sourcePath, settings);
if (!result.text) continue; if (!result.text) continue;
b.add( b.add(
absStart, absStart,
absEnd, absEnd,
Decoration.replace({ Decoration.replace({
widget: new DateCalcWidget(result.text, result.tooltip, absStart, absEnd), widget: new DateCalcInlineWidget(result.text, result.tooltip, absStart, absEnd),
inclusive: false, inclusive: false,
}) })
); );
replaced++;
} }
} }
// Process fenced code blocks
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);
syntaxTree(view.state).iterate({
from: scanFrom,
to: scanTo,
enter: (node) => {
if (node.name !== "FencedCode") return;
const absStart = node.from;
const absEnd = node.to;
const key = `block:${absStart}:${absEnd}`;
if (used.has(key)) return;
used.add(key);
const intersects = sel.from <= absEnd && sel.to >= absStart;
if (settings.hideResultWhileCursorInside && intersects) return;
const blockText = view.state.doc.sliceString(absStart, absEnd);
const parsed = parseFencedCode(blockText);
if (!parsed || parsed.lang !== "date-calc") return;
const result = processFencedBlock(parsed.body, app, sourcePath, settings);
if (!result.text) return;
b.add(
absEnd,
absEnd,
Decoration.widget({
widget: new DateCalcBlockWidget(result.text, result.tooltip),
side: 1,
block: true,
})
);
},
});
}
if (settings.debug) { if (settings.debug) {
console.log("[date-calc] replace scan", { matchesSeen, replaced, sourcePath }); console.log("[date-calc] decorations built", { sourcePath });
} }
return b.finish(); return b.finish();
} }
}, },
{ {
decorations: (v) => v.decorations, decorations: (v) => v.decorations,
eventHandlers: { eventHandlers: {
mousedown: (e, view) => { mousedown: (e, view) => {
const el = (e.target as HTMLElement | null)?.closest?.(".date-calc-inline") as HTMLElement | null; const el = (e.target as HTMLElement | null)?.closest?.(
if (!el) return false; ".date-calc-inline"
) as HTMLElement | null;
if (!el) return false;
const from = Number(el.dataset.dcFrom); const from = Number(el.dataset.dcFrom);
const to = Number(el.dataset.dcTo); const to = Number(el.dataset.dcTo);
if (!Number.isFinite(from) || !Number.isFinite(to)) return false; if (!Number.isFinite(from) || !Number.isFinite(to)) return false;
// Select the whole backticked expression so it immediately “reveals” for editing view.dispatch({
view.dispatch({ selection: { anchor: from, head: to },
selection: { anchor: from, head: to }, scrollIntoView: true,
scrollIntoView: true, });
}); view.focus();
view.focus();
return true; // CodeMirror will preventDefault when handler returns true [web:64] return true;
},
}, },
},
provide: (plugin) => provide: (plugin) =>
EditorView.atomicRanges.of((view) => view.plugin(plugin)?.decorations ?? Decoration.none), EditorView.atomicRanges.of(
} (view) => view.plugin(plugin)?.decorations ?? Decoration.none
),
}
); );
} }
/* ========================= /* =========================
Plugin Plugin
========================= */ ========================= */
export default class DateCalcPlugin extends Plugin { export default class DateCalcPlugin extends Plugin {
settings: DateCalcSettings; settings: DateCalcSettings;
@@ -458,10 +611,22 @@ export default class DateCalcPlugin extends Plugin {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
if (this.settings.debug) console.log("[date-calc] loaded"); if (this.settings.debug) console.log("[date-calc] loaded");
new Notice("Date Calc loaded (Live Preview)"); new Notice("Date Calc loaded (Live Preview + Reading)");
// Register Live Preview editor extension
this.registerEditorExtension(dateCalcLivePreview(this.app, () => this.settings)); this.registerEditorExtension(dateCalcLivePreview(this.app, () => this.settings));
// Register Reading mode fenced code block processor
this.registerMarkdownCodeBlockProcessor("date-calc", (source, el, ctx) => {
const result = processFencedBlock(source, this.app, ctx.sourcePath, this.settings);
el.empty();
el.createDiv({ cls: "date-calc-block", text: result.text });
if (result.tooltip) {
el.setAttribute("aria-label", result.tooltip);
}
});
// Commands
this.addCommand({ this.addCommand({
id: "date-calc-toggle-debug", id: "date-calc-toggle-debug",
name: "Date Calc: Toggle debug", name: "Date Calc: Toggle debug",
@@ -486,10 +651,12 @@ export default class DateCalcPlugin extends Plugin {
} }
} }
/* ========================= /* =========================
Settings tab Settings tab
========================= */ ========================= */
class DateCalcSettingTab extends PluginSettingTab { class DateCalcSettingTab extends PluginSettingTab {
constructor(app: App, private plugin: DateCalcPlugin) { constructor(app: App, private plugin: DateCalcPlugin) {
super(app, plugin); super(app, plugin);
@@ -503,7 +670,7 @@ class DateCalcSettingTab extends PluginSettingTab {
new Setting(containerEl) new Setting(containerEl)
.setName("Debug logging") .setName("Debug logging")
.setDesc("Logs how many `date-calc:` inline-code matches were seen and decorated.") .setDesc("Logs decoration activity to the console.")
.addToggle((t) => .addToggle((t) =>
t.setValue(this.plugin.settings.debug).onChange(async (v) => { t.setValue(this.plugin.settings.debug).onChange(async (v) => {
this.plugin.settings.debug = v; this.plugin.settings.debug = v;
@@ -512,8 +679,8 @@ class DateCalcSettingTab extends PluginSettingTab {
); );
new Setting(containerEl) new Setting(containerEl)
.setName("Hide result while cursor inside inline code") .setName("Hide result while cursor inside")
.setDesc("Prevents the widget from showing while you edit the inline backticks.") .setDesc("Prevents the widget from showing while you edit inline/fenced code.")
.addToggle((t) => .addToggle((t) =>
t.setValue(this.plugin.settings.hideResultWhileCursorInside).onChange(async (v) => { t.setValue(this.plugin.settings.hideResultWhileCursorInside).onChange(async (v) => {
this.plugin.settings.hideResultWhileCursorInside = v; this.plugin.settings.hideResultWhileCursorInside = v;
@@ -531,6 +698,8 @@ class DateCalcSettingTab extends PluginSettingTab {
}) })
); );
containerEl.createEl("p", { text: 'Example: `date-calc: birthday=1992-08-16`' }); containerEl.createEl("h3", { text: "Usage Examples" });
containerEl.createEl("p", { text: "Inline: `date-calc: birthday=1992-08-16`" });
containerEl.createEl("p", { text: "Block: ```date-calc with YAML inside" });
} }
} }