diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 2c30c5c..fc2936f 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -53,4 +53,5 @@ if (prod) { await context.watch(); } -fs.copyFileSync("src/styles/main.css", "../plugin-full-calendar/obsidian-dev-vault/.obsidian/plugins/Latex-like-equations/styles.css"); \ No newline at end of file +fs.copyFileSync("src/styles/main.css", "../plugin-full-calendar/obsidian-dev-vault/.obsidian/plugins/Latex-like-equations/styles.css"); +fs.copyFileSync("./manifest.json", "../plugin-full-calendar/obsidian-dev-vault/.obsidian/plugins/Latex-like-equations/manifest.json"); \ No newline at end of file diff --git a/manifest.json b/manifest.json index 297539d..4a5cf4d 100644 --- a/manifest.json +++ b/manifest.json @@ -1,12 +1,12 @@ { - "id": "math-booster", + "id": "latex-referencer", "name": "LaTeX Equation Referencer", - "version": "2.2.4", + "version": "2.2.5", "minAppVersion": "1.3.5", "description": "A powerful indexing & referencing system for theorems & equations in your vault. Bring LaTeX-like workflow into Obsidian with theorem environments, automatic equation numbering, and more.", - "author": "Ryota Ushio", - "authorUrl": "https://github.com/RyotaUshio", - "fundingUrl": "https://www.buymeacoffee.com/ryotaushio", + "author": "Jovi Koikkara", + "authorUrl": "https://github.com/YouFoundJK", + "fundingUrl": "https://github.com/YouFoundJK", "helpUrl": "https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/", "isDesktopOnly": false } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bf82b66..95220ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "obsidian-math-booster", - "version": "2.2.4", + "version": "2.2.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-math-booster", - "version": "2.2.0", + "version": "2.2.4", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.5.4", "@lezer/common": "^1.0.3", "@lucide/svelte": "^0.562.0", "esbuild": "^0.25.12", @@ -59,9 +60,13 @@ } }, "node_modules/@codemirror/state": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.1.tgz", - "integrity": "sha512-RupHSZ8+OjNT38zU9fKH2sv+Dnlr8Eb8sl4NOnnqz95mCFTZUaiRP8Xv5MeeaG0px2b8Bnfe7YGwCV3nsBhbuw==" + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", + "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } }, "node_modules/@codemirror/view": { "version": "6.18.1", @@ -660,6 +665,12 @@ "svelte": "^5" } }, + "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==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index 4d8b460..e735a3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-math-booster", - "version": "2.2.4", + "version": "2.2.5", "description": "An Obsidian.md plugin that provides a powerful indexing & referencing system for theorems & equations in your vault. Bring LaTeX-like workflow into Obsidian with theorem environments, automatic equation numbering, and more.", "scripts": { "dev": "node esbuild.config.mjs", @@ -31,6 +31,7 @@ }, "dependencies": { "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.5.4", "@lezer/common": "^1.0.3", "@lucide/svelte": "^0.562.0", "esbuild": "^0.25.12", diff --git a/src/features/equations/reading-view-equations.ts b/src/features/equations/reading-view-equations.ts index 4608aff..5ac65ea 100644 --- a/src/features/equations/reading-view-equations.ts +++ b/src/features/equations/reading-view-equations.ts @@ -7,7 +7,6 @@ import LatexReferencer from 'main'; import { processActiveNoteEquations } from './numbering'; import { EquationBlock } from 'types'; - export const createEquationNumberProcessor = (plugin: LatexReferencer): MarkdownPostProcessor => { return (el, ctx) => { const file = plugin.app.vault.getAbstractFileByPath(ctx.sourcePath); diff --git a/src/features/linker/dom-observer.ts b/src/features/linker/dom-observer.ts new file mode 100644 index 0000000..49cc35b --- /dev/null +++ b/src/features/linker/dom-observer.ts @@ -0,0 +1,119 @@ +/** + * DOM Observer for Callout Equation Links + * + * When Obsidian renders a callout in Live Preview (cursor leaves the callout), + * it replaces the CodeMirror content with a native HTML Block Widget. This + * native rendering bypasses MarkdownPostProcessor hooks, so our equation link + * rendering never fires. + * + * Additionally, Obsidian copies the 'math-link-processed' class from the + * CodeMirror state into the callout HTML, which means we must strip it + * before reprocessing. + * + * This observer watches for DOM mutations and manually processes equation + * links inside dynamically rendered callout blocks. It also performs an + * initial scan on setup to catch callouts rendered before the cache was ready. + */ + +import { MarkdownView } from "obsidian"; +import LatexReferencer from "main"; +import { processInternalLink } from "./reading-view-linker"; + +/** + * Process all equation links within a given HTML element, stripping any + * stale 'math-link-processed' class and reprocessing them. + */ +function processEquationLinksInElement(node: HTMLElement, plugin: LatexReferencer): void { + let sourcePath: string | undefined = undefined; + + const resolveSourcePath = () => { + if (sourcePath) return; + const leaves = plugin.app.workspace.getLeavesOfType("markdown"); + for (const leaf of leaves) { + if (leaf.view.containerEl.contains(node)) { + sourcePath = (leaf.view as MarkdownView).file?.path; + break; + } + } + if (!sourcePath) { + const active = plugin.app.workspace.getActiveViewOfType(MarkdownView); + sourcePath = active?.file?.path; + } + }; + + const equationLinks = node.querySelectorAll?.('a.internal-link') as NodeListOf; + if (equationLinks && equationLinks.length > 0) { + for (let i = 0; i < equationLinks.length; i++) { + const link = equationLinks[i]; + const dataHref = link.getAttribute('data-href'); + if (dataHref && dataHref.includes('#^eq-')) { + link.classList.remove('math-link-processed'); + resolveSourcePath(); + if (sourcePath) { + processInternalLink(link, plugin, sourcePath); + } + } + } + } + + // The node itself could be the link + if (node.matches && node.matches('a.internal-link')) { + const dataHref = (node as HTMLAnchorElement).getAttribute('data-href'); + if (dataHref && dataHref.includes('#^eq-')) { + node.classList.remove('math-link-processed'); + resolveSourcePath(); + if (sourcePath) { + processInternalLink(node as HTMLAnchorElement, plugin, sourcePath); + } + } + } +} + +/** + * Scan all existing callout blocks in the DOM and process any equation links. + * This handles callouts that were rendered before the equation cache was ready + * (e.g., on initial page load). + */ +function scanExistingCallouts(plugin: LatexReferencer): void { + const calloutBlocks = document.querySelectorAll('.cm-embed-block.cm-callout'); + for (const block of calloutBlocks) { + if (block instanceof HTMLElement) { + processEquationLinksInElement(block, plugin); + } + } +} + +/** + * Sets up a MutationObserver on document.body to detect callout rendering + * and reprocess equation links within them. Also performs an initial scan + * after a delay to catch callouts rendered before the cache was ready. + * + * @param plugin - The LatexReferencer plugin instance + * @returns A cleanup function that disconnects the observer + */ +export function setupDOMObserver(plugin: LatexReferencer): () => void { + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type === "childList") { + for (const node of mutation.addedNodes) { + if (node instanceof HTMLElement) { + if (!node.querySelector && !node.matches) continue; + processEquationLinksInElement(node, plugin); + } + } + } + } + }); + + observer.observe(document.body, { childList: true, subtree: true }); + + // Initial scan: process callouts that were rendered before the observer + // started or before the equation cache was ready. We use onLayoutReady + // to ensure the DOM is fully initialized, plus a small delay to ensure + // the equation cache has been built. + plugin.app.workspace.onLayoutReady(() => { + setTimeout(() => scanExistingCallouts(plugin), 1000); + }); + + return () => observer.disconnect(); +} diff --git a/src/features/linker/live-preview-link-renderer.ts b/src/features/linker/live-preview-link-renderer.ts index 022ee8c..38e285c 100644 --- a/src/features/linker/live-preview-link-renderer.ts +++ b/src/features/linker/live-preview-link-renderer.ts @@ -1,5 +1,5 @@ import { editorLivePreviewField, finishRenderMath, renderMath, editorInfoField } from "obsidian"; -import { EditorSelection, RangeSetBuilder, Extension } from "@codemirror/state"; +import { EditorSelection, RangeSetBuilder, Extension, Prec } from "@codemirror/state"; import { Decoration, DecorationSet, ViewUpdate, EditorView, ViewPlugin, PluginValue, WidgetType } from "@codemirror/view"; import { syntaxTree } from "@codemirror/language"; import { getMathLink } from "./helper"; @@ -21,22 +21,22 @@ function selectionAndRangeOverlap(selection: EditorSelection, rangeFrom: number, * A helper function to render a string with inline math. */ function setMathLink(source: string, mathLinkEl: HTMLElement) { - mathLinkEl.replaceChildren(); - const mathPattern = /\$(.*?[^\s])\$/g; - let textFrom = 0, textTo = 0; - let result; - while ((result = mathPattern.exec(source)) !== null) { - const mathString = result[1]; - textTo = result.index; - if (textTo > textFrom) mathLinkEl.createSpan().replaceWith(source.slice(textFrom, textTo)); + mathLinkEl.replaceChildren(); + const mathPattern = /\$(.*?[^\s])\$/g; + let textFrom = 0, textTo = 0; + let result; + while ((result = mathPattern.exec(source)) !== null) { + const mathString = result[1]; + textTo = result.index; + if (textTo > textFrom) mathLinkEl.createSpan().replaceWith(source.slice(textFrom, textTo)); - const mathEl = renderMath(mathString, false); - mathLinkEl.createSpan({ cls: ["math", "math-inline", "is-loaded"] }).replaceWith(mathEl); + const mathEl = renderMath(mathString, false); + mathLinkEl.createSpan({ cls: ["math", "math-inline", "is-loaded"] }).replaceWith(mathEl); - textFrom = mathPattern.lastIndex; - } + textFrom = mathPattern.lastIndex; + } - if (textFrom < source.length) mathLinkEl.createSpan().replaceWith(source.slice(textFrom)); + if (textFrom < source.length) mathLinkEl.createSpan().replaceWith(source.slice(textFrom)); } /** Given a LatexReferencer plugin instance, create a CodeMirror6 view plugin that renders equation links. */ @@ -44,12 +44,12 @@ export const createLivePreviewLinkRendererPlugin = (plugin: LatexReferencer): Ex const { app } = plugin; class MathWidget extends WidgetType { - constructor(public outLinkText: string, public outLinkMathLink: string, public sourcePath: string) { + constructor(public outLinkText: string, public outLinkMathLink: string, public sourcePath: string, public from: number, public to: number) { super(); } eq(other: MathWidget) { - return this.outLinkText === other.outLinkText && this.outLinkMathLink === other.outLinkMathLink && this.sourcePath === other.sourcePath; + return this.outLinkText === other.outLinkText && this.outLinkMathLink === other.outLinkMathLink && this.sourcePath === other.sourcePath && this.from === other.from && this.to === other.to; } toDOM() { @@ -127,18 +127,16 @@ export const createLivePreviewLinkRendererPlugin = (plugin: LatexReferencer): Ex endNode?.name.includes("formatting-link-end") ) { const linkText = state.sliceDoc(linkNode.from, linkNode.to); - + if (linkText.startsWith("#^eq-")) { - const start = startNode.from; - const end = endNode.to; const outLinkMathLink = getMathLink(plugin, linkText, sourcePath); - if (outLinkMathLink && !selectionAndRangeOverlap(state.selection, start, end)) { + if (outLinkMathLink && !selectionAndRangeOverlap(state.selection, linkNode.from, linkNode.to)) { builder.add( - start, - end, + linkNode.from, + linkNode.to, Decoration.replace({ - widget: new MathWidget(linkText, outLinkMathLink, sourcePath), + widget: new MathWidget(linkText, outLinkMathLink, sourcePath, linkNode.from, linkNode.to), }) ); } @@ -156,6 +154,5 @@ export const createLivePreviewLinkRendererPlugin = (plugin: LatexReferencer): Ex } }, { decorations: v => v.decorations } ); - - return viewPlugin; + return Prec.highest(viewPlugin); } \ No newline at end of file diff --git a/src/features/linker/reading-view-linker.ts b/src/features/linker/reading-view-linker.ts index 35092bc..ef45d51 100644 --- a/src/features/linker/reading-view-linker.ts +++ b/src/features/linker/reading-view-linker.ts @@ -48,25 +48,38 @@ export class LatexRenderChild extends MarkdownRenderChild { if (mathLink) { // The containerEl is now the link element itself const linkEl = this.containerEl as HTMLElement; - // Verify it's still in the DOM (though MarkdownRenderChild handles unloading) - if (linkEl.isConnected) { - setMathLink(mathLink, linkEl); - } + setMathLink(mathLink, linkEl); } finishRenderMath(); } } +/** + * Manually process a single internal link element, creating a LatexRenderChild + * and calling onload() to render the equation number. Used by the DOM observer + * to handle links inside dynamically rendered callouts. + */ +export const processInternalLink = (link: HTMLAnchorElement, plugin: LatexReferencer, sourcePath: string) => { + if (link.classList.contains("math-link-processed")) return; + const dataHref = link.getAttribute('data-href'); + if (dataHref && dataHref.includes('#^eq-')) { + link.classList.add("math-link-processed"); + const child = new LatexRenderChild(link, plugin, sourcePath, dataHref); + child.onload(); + } +}; + export const CustomMathLinksProcessor = (plugin: LatexReferencer): MarkdownPostProcessor => { return (element: HTMLElement, context: MarkdownPostProcessorContext) => { const links = element.querySelectorAll('a.internal-link'); for (const link of links) { - const href = link.getAttribute('data-href'); - if (href && href.contains('#^eq-')) { - // This is one of our equation links. - // Pass the specific link element as the container to scope the child correctly. + if (link.classList.contains("math-link-processed")) continue; + const dataHref = link.getAttribute('data-href'); + + if (dataHref && dataHref.includes('#^eq-')) { + link.classList.add("math-link-processed"); context.addChild( - new LatexRenderChild(link, plugin, context.sourcePath, href) + new LatexRenderChild(link, plugin, context.sourcePath, dataHref) ); } } diff --git a/src/main.ts b/src/main.ts index 1a8f114..f3875bb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,6 +11,7 @@ import { Provider } from './features/linker/provider-link-render'; import { LatexLinkProvider } from 'latex-provider'; import { createEquationNumberProcessor } from 'features/equations/reading-view-equations'; import { CustomMathLinksProcessor } from './features/linker/reading-view-linker'; +import { setupDOMObserver } from './features/linker/dom-observer'; import { createEquationNumberPlugin } from 'features/equations/live-preview-equations'; import { createLivePreviewLinkRendererPlugin } from './features/linker/live-preview-link-renderer'; @@ -27,6 +28,7 @@ import { checkAndFixCalloutMath } from 'utils/fixer'; import { traverseFolder } from "./features/export-pdf/utils"; import { SnippetManager } from 'features/snippets/manager'; + const isDev = process.env.NODE_ENV === "development"; export default class LatexReferencer extends Plugin { @@ -190,6 +192,7 @@ export default class LatexReferencer extends Plugin { this.app.workspace.onLayoutReady(() => this.forceRerender()); this.patchPagePreview(); + this.register(setupDOMObserver(this)); } async loadSettings() { @@ -260,6 +263,9 @@ export default class LatexReferencer extends Plugin { this.register(uninstaller); } + + + async generateToc(root: TFolder | TFile) { // @ts-ignore const basePath = this.app.vault.adapter.basePath; diff --git a/src/utils/debug.ts b/src/utils/debug.ts new file mode 100644 index 0000000..4fba9aa --- /dev/null +++ b/src/utils/debug.ts @@ -0,0 +1,16 @@ +/** + * A simple synchronous file logger used for intense UI diagnostic tracing. + * + * Usage: + * Inject `logDebug("My message")` aggressively into CodeMirror rendering loops + * or DOM observers to output an absolute timeline of execution to `debug.log`. + * This is especially useful for capturing logs in contexts where the Obsidian + * Developer Console might be silenced, asynchronous, or overwhelmed. + */ +import * as fs from "fs"; + +export function logDebug(msg: string) { + try { + fs.appendFileSync("d:\\Codes\\plugin-latex-referencer\\debug.log", msg + "\n", { encoding: "utf-8" }); + } catch (e) { } +}