diff --git a/src/cleveref.ts b/src/cleveref.ts index 2989456..16eab3d 100644 --- a/src/cleveref.ts +++ b/src/cleveref.ts @@ -1,17 +1,15 @@ -import { EquationBlock } from 'index/typings/markdown'; -import { TFile, HeadingSubpathResult, BlockSubpathResult, App } from 'obsidian'; +import { App, MarkdownView, TFile, HeadingSubpathResult, BlockSubpathResult } from 'obsidian'; import * as MathLinks from 'obsidian-mathlinks'; - import LatexReferencer from 'main'; +import { processActiveNoteEquations } from './equations/numbering'; import { MathIndex } from 'index/math-index'; -import { MarkdownPage, MathBlock, TheoremCalloutBlock } from 'index/typings/markdown'; - export class CleverefProvider extends MathLinks.Provider { app: App; index: MathIndex; constructor(mathLinks: any, public plugin: LatexReferencer) { + // Using `any` for `mathLinks` as the exact type is unclear. This should be revisited. super(mathLinks); this.app = plugin.app; this.index = plugin.indexManager.index; @@ -22,46 +20,51 @@ export class CleverefProvider extends MathLinks.Provider { targetFile: TFile | null, targetSubpathResult: HeadingSubpathResult | BlockSubpathResult | null, ): string | null { - const { path, subpath } = parsedLinktext; - if (targetFile === null) return null; - const page = this.index.load(targetFile.path); - if (!MarkdownPage.isMarkdownPage(page)) return null + // The subpath for a block link is "#^blockid". This check is now correct. + if (!targetFile || !parsedLinktext.subpath || !parsedLinktext.subpath.startsWith('#^')) { + return null; + } - // only path, no subpath: return page.$refName if it exists, otherwise there's nothing to do - if (!subpath) return page.$refName ?? null; - - const processedPath = path ? page.$refName ?? path : ''; - - // subpath resolution failed, do nothing - if (targetSubpathResult === null) return null; - - // subpath resolution succeeded - if (targetSubpathResult.type === 'block') { - // handle block links - - // get the target block - const block = page.$blocks.get(targetSubpathResult.block.id); - - if (MathBlock.isMathBlock(block)) { - // display text set manually: higher priority - if (block.$display) return path && this.shouldShowNoteTitle(block) ? processedPath + ' > ' + block.$display : block.$display; - // display text computed automatically: lower priority - if (block.$refName) return path && this.shouldShowNoteTitle(block) ? processedPath + ' > ' + block.$refName : block.$refName; - } - } else { - // handle heading links - // just ignore (return null) if we don't need to perform any particular processing - if (path && page.$refName) { - return processedPath + ' > ' + subpath; + let content: string | null = null; + for (const leaf of this.app.workspace.getLeavesOfType("markdown")) { + const view = leaf.view; + if (view instanceof MarkdownView && view.file?.path === targetFile.path) { + content = view.editor.getValue(); + break; } } + if (content === null) { + this.app.vault.read(targetFile).then(cachedContent => { + if (cachedContent) { + content = cachedContent; + } + }).catch(error => { + console.error("Error reading file from vault:", error); + }); + + if (!content) { + return null; + } + } + + // Correctly extract the block ID by removing the first two characters ("#^"). + const blockId = parsedLinktext.subpath.substring(2); + + if (content) { + const equations = processActiveNoteEquations(this.plugin, targetFile, content); + const targetEquation = equations.get(blockId); + + if (targetEquation?.$refName) { + let result = targetEquation.$refName; + // Check if the original link was to a different file (`path` is not empty) + if (this.plugin.extraSettings.noteTitleInEquationLink && parsedLinktext.path) { + result = targetFile.basename + ' > ' + result; + } + return result; + } + } + return null; } - - shouldShowNoteTitle(block: MathBlock): boolean { - if (TheoremCalloutBlock.isTheoremCalloutBlock(block)) return this.plugin.extraSettings.noteTitleInTheoremLink; - if (EquationBlock.isEquationBlock(block)) return this.plugin.extraSettings.noteTitleInEquationLink; - return true; - } } \ No newline at end of file diff --git a/src/equations/live-preview.ts b/src/equations/live-preview.ts index 0c1d513..5a33182 100644 --- a/src/equations/live-preview.ts +++ b/src/equations/live-preview.ts @@ -1,119 +1,65 @@ -/** - * Display equation numbers in Live Preview. - */ - -import { EditorState, StateEffect } from '@codemirror/state'; -import { PluginValue, ViewPlugin, EditorView, ViewUpdate } from '@codemirror/view'; -import { EquationBlock, MarkdownBlock, MarkdownPage } from 'index/typings/markdown'; +import { Extension } from '@codemirror/state'; +import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate, WidgetType } from '@codemirror/view'; +import { editorInfoField } from 'obsidian'; +import { RangeSetBuilder } from '@codemirror/state'; import LatexReferencer from 'main'; -import { MarkdownView, TFile, editorInfoField, finishRenderMath } from 'obsidian'; -import { resolveSettings } from 'utils/plugin'; -import { replaceMathTag } from './common'; -import { DEFAULT_SETTINGS, MathContextSettings } from 'settings/settings'; +import { processActiveNoteEquations } from './numbering'; +import { EquationBlock } from 'index/typings/markdown'; - -export function createEquationNumberPlugin(plugin: LatexReferencer) { - - const { app, indexManager: { index } } = plugin; - - const forceUpdateEffect = StateEffect.define(); - - plugin.registerEvent(plugin.indexManager.on('index-updated', (file) => { - app.workspace.iterateAllLeaves((leaf) => { - if ( - leaf.view instanceof MarkdownView - && leaf.view.file?.path === file.path - && leaf.view.getMode() === 'source' - ) { - leaf.view.editor.cm?.dispatch({ effects: forceUpdateEffect.of(null) }); +export function createEquationNumberPlugin(plugin: LatexReferencer): Extension { + return ViewPlugin.fromClass( + class { + decorations: DecorationSet; + + constructor(view: EditorView) { + this.decorations = this.buildDecorations(view); } - }); - })); - return ViewPlugin.fromClass(class implements PluginValue { - file: TFile | null; - page: MarkdownPage | null; - settings: Required; - - constructor(view: EditorView) { - this.file = view.state.field(editorInfoField).file; - this.page = null; - this.settings = DEFAULT_SETTINGS; - - if (this.file) { - this.settings = resolveSettings(undefined, plugin, this.file); - const page = index.load(this.file.path); - if (MarkdownPage.isMarkdownPage(page)) { - this.page = page; - this.updateEquationNumber(view, this.page); + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = this.buildDecorations(update.view); } } - } - updateFile(state: EditorState) { - this.file = state.field(editorInfoField).file; - if (this.file) this.settings = resolveSettings(undefined, plugin, this.file); - } + buildDecorations(view: EditorView): DecorationSet { + const builder = new RangeSetBuilder(); + const file = view.state.field(editorInfoField).file; - async updatePage(file: TFile): Promise { - const page = index.load(file.path); - if (MarkdownPage.isMarkdownPage(page)) this.page = page; - if (!this.page) { - this.page = await plugin.indexManager.reload(file); - } - return this.page; - } + if (!file) return builder.finish(); - update(update: ViewUpdate) { - if (!this.file) this.updateFile(update.state); - if (!this.file) return; + const content = view.state.doc.toString(); + const equations = processActiveNoteEquations(plugin, file, content); - if (update.transactions.some(tr => tr.effects.some(effect => effect.is(forceUpdateEffect)))) { - // index updated - this.settings = resolveSettings(undefined, plugin, this.file); - this.updatePage(this.file).then((updatedPage) => this.updateEquationNumber(update.view, updatedPage)) - } else if (update.geometryChanged) { - if (this.page) this.updateEquationNumber(update.view, this.page); - else this.updatePage(this.file).then((updatedPage) => this.updateEquationNumber(update.view, updatedPage)); - } - } - - async updateEquationNumber(view: EditorView, page: MarkdownPage) { - const mjxContainerElements = view.contentDOM.querySelectorAll(':scope > .cm-embed-block.math > mjx-container.MathJax[display="true"]'); - - for (const mjxContainerEl of mjxContainerElements) { - - // skip if the equation is being edited to avoid the delay of preview - const mightBeClosingDollars = mjxContainerEl.parentElement?.previousElementSibling?.lastElementChild; - const isBeingEdited = mightBeClosingDollars?.matches('span.cm-formatting-math-end'); - if (isBeingEdited) continue; - - const pos = view.posAtDOM(mjxContainerEl); - let block: MarkdownBlock | undefined; - try { - const line = view.state.doc.lineAt(pos).number - 1; // sometimes throws an error for reasons that I don't understand - block = page.getBlockByLineNumber(line); - } catch (err) { - block = page.getBlockByOffset(pos); + for (const eq of equations.values()) { + if (eq.$printName) { + const pos = eq.$pos.end.offset; + const widget = new EquationNumberWidget(eq); + builder.add(pos, pos, Decoration.widget({ widget, side: 1 })); + } } - if (!(block instanceof EquationBlock)) continue; - - // only update if necessary - if (mjxContainerEl.getAttribute('data-equation-print-name') !== block.$printName) { - replaceMathTag(mjxContainerEl, block, this.settings); - } - if (block.$printName !== null) mjxContainerEl.setAttribute('data-equation-print-name', block.$printName); - else mjxContainerEl.removeAttribute('data-equation-print-name'); + + return builder.finish(); } - // DON'T FOREGET THIS CALL!! - // https://github.com/RyotaUshio/obsidian-latex-theorem-equation-referencer/issues/203 - // https://github.com/RyotaUshio/obsidian-latex-theorem-equation-referencer/issues/200 - finishRenderMath(); + }, + { + decorations: (v) => v.decorations, } - - destroy() { - // I don't know if this is really necessary, but just in case... - finishRenderMath(); - } - }); + ); } + +class EquationNumberWidget extends WidgetType { + constructor(public equation: EquationBlock) { + super(); + } + + eq(other: EquationNumberWidget) { + return this.equation.$printName === other.equation.$printName; + } + + toDOM() { + return createSpan({ + cls: "math-booster-equation-number", + text: this.equation.$printName || '', + }); + } +} \ No newline at end of file diff --git a/src/equations/numbering.ts b/src/equations/numbering.ts new file mode 100644 index 0000000..0aa69f6 --- /dev/null +++ b/src/equations/numbering.ts @@ -0,0 +1,51 @@ +import { TFile, App } from "obsidian"; +import { ActiveNoteEquationProvider } from "./provider"; +import { EquationBlock } from "index/typings/markdown"; +import { resolveSettings } from "utils/plugin"; +import { getEqNumberPrefix, CONVERTER } from "utils/format"; +import LatexReferencer from "main"; + +/** + * Finds all equations in a file's content, counts their backlinks, and assigns print/reference names. + */ +export function processActiveNoteEquations(plugin: LatexReferencer, file: TFile, content: string): Map { + const provider = new ActiveNoteEquationProvider(plugin.app); + const equations = provider.getEquations(file, content); + const settings = resolveSettings(undefined, plugin, file); + + const processedEquations = new Map(); + let equationCount = 0; + const eqPrefix = getEqNumberPrefix(plugin.app, file, settings); + const eqSuffix = settings.eqNumberSuffix; + + for (const eq of equations) { + let printName: string | null = null; + let refName: string | null = null; + + if (eq.$blockId) { + const backlinkRegex = new RegExp(`\\[\\[#\\^${eq.$blockId}\\]\\]`, "g"); + const backlinkCount = (content.match(backlinkRegex) || []).length; + + if (eq.$manualTag) { + printName = `(${eq.$manualTag})`; + } else if (!settings.numberOnlyReferencedEquations || backlinkCount > 0) { + eq.$index = equationCount; + const num = settings.eqNumberInit + equationCount; + printName = `(${eqPrefix}${CONVERTER[settings.eqNumberStyle](num)}${eqSuffix})`; + equationCount++; + } + + if (printName !== null) { + refName = settings.eqRefPrefix + printName + settings.eqRefSuffix; + } + } + + eq.$printName = printName; + eq.$refName = refName; + + if (eq.$blockId) { + processedEquations.set(eq.$blockId, eq); + } + } + return processedEquations; +} \ No newline at end of file diff --git a/src/equations/provider.ts b/src/equations/provider.ts index 33ce733..07f6bde 100644 --- a/src/equations/provider.ts +++ b/src/equations/provider.ts @@ -1,11 +1,11 @@ -import { Editor, TFile, App, CachedMetadata } from 'obsidian'; +import { TFile, App, CachedMetadata } from 'obsidian'; import { EquationBlock } from 'index/typings/markdown'; import { trimMathText, parseMarkdownComment, parseYamlLike } from 'utils/parse'; export class ActiveNoteEquationProvider { constructor(public app: App) {} - getEquations(file: TFile, editor: Editor): EquationBlock[] { + getEquations(file: TFile, content: string): EquationBlock[] { const cache: CachedMetadata | null = this.app.metadataCache.getFileCache(file); if (!cache?.sections) { return []; @@ -16,17 +16,17 @@ export class ActiveNoteEquationProvider { let ordinal = 0; for (const section of mathSections) { - const fromPos = { line: section.position.start.line, ch: section.position.start.col }; - const toPos = { line: section.position.end.line, ch: section.position.end.col }; - - const text = editor.getRange(fromPos, toPos); + const text = content.slice(section.position.start.offset, section.position.end.offset); const mathText = trimMathText(text); let blockId: string | undefined = section.id; - // The cache's section.id is not always up-to-date in Live Preview. + + // In Live Preview, the cache's section.id is not always up-to-date. // A manual check on the next line is more reliable. - if (section.position.end.line + 1 < editor.lineCount()) { - const nextLine = editor.getLine(section.position.end.line + 1).trim(); + const nextLineIndex = section.position.end.line + 1; + const lines = content.split('\n'); + if (nextLineIndex < lines.length) { + const nextLine = lines[nextLineIndex].trim(); const idMatch = nextLine.match(/^\^([a-zA-Z0-9\-_]+)$/); if (idMatch) { blockId = idMatch[1]; @@ -52,7 +52,7 @@ export class ActiveNoteEquationProvider { $id: EquationBlock.readableId(file.path, ordinal), $ordinal: ordinal++, $position: { start: section.position.start.line, end: section.position.end.line }, - $pos: section.position, // The position object from the cache is a complete Pos object + $pos: section.position, $links: [], $blockId: blockId, $type: 'equation', @@ -67,4 +67,4 @@ export class ActiveNoteEquationProvider { return equations; } -} +} \ No newline at end of file diff --git a/src/equations/reading-view.ts b/src/equations/reading-view.ts index 90f98ab..2d9677b 100644 --- a/src/equations/reading-view.ts +++ b/src/equations/reading-view.ts @@ -2,161 +2,45 @@ * Display equation numbers in reading view, embeds, hover page preview, and PDF export. */ -import { App, MarkdownRenderChild, finishRenderMath, MarkdownPostProcessorContext, TFile, Notice } from "obsidian"; - +import { MarkdownPostProcessor, MarkdownView, TFile } from 'obsidian'; import LatexReferencer from 'main'; -import { resolveSettings } from 'utils/plugin'; -import { EquationBlock, MarkdownPage } from "index/typings/markdown"; -import { MathIndex } from "index/math-index"; -import { isPdfExport, resolveLinktext } from "utils/obsidian"; -import { replaceMathTag } from "./common"; +import { processActiveNoteEquations } from './numbering'; -export const createEquationNumberProcessor = (plugin: LatexReferencer) => async (el: HTMLElement, ctx: MarkdownPostProcessorContext) => { - if (isPdfExport(el)) preprocessForPdfExport(plugin, el, ctx); +export const createEquationNumberProcessor = (plugin: LatexReferencer): MarkdownPostProcessor => { + return (el, ctx) => { + const file = plugin.app.vault.getAbstractFileByPath(ctx.sourcePath) as TFile; + const view = plugin.app.workspace.getActiveViewOfType(MarkdownView); - const sourceFile = plugin.app.vault.getAbstractFileByPath(ctx.sourcePath); - if (!(sourceFile instanceof TFile)) return; + if (!file || !view?.editor) return; - const mjxContainerElements = el.querySelectorAll('mjx-container.MathJax[display="true"]'); - for (const mjxContainerEl of mjxContainerElements) { - ctx.addChild( - new EquationNumberRenderer(mjxContainerEl, plugin, sourceFile, ctx) - ); - } - finishRenderMath(); -} + const content = view.editor.getValue(); + const equations = processActiveNoteEquations(plugin, file, content); + if (equations.size === 0) return; + const mathElements = el.querySelectorAll(".math.math-block.is-loaded"); -/** - * As a preprocessing for displaying equation numbers in the exported PDF, - * add an attribute representing a block ID to each numbered equation element - * so that EquationNumberRenderer can find the corresponding block from the index - * without relying on the line number. - */ -function preprocessForPdfExport(plugin: LatexReferencer, el: HTMLElement, ctx: MarkdownPostProcessorContext) { + mathElements.forEach((mathEl) => { + const section = ctx.getSectionInfo(mathEl); + if (!section) return; - try { - const topLevelMathDivs = el.querySelectorAll(':scope > div.math.math-block > mjx-container.MathJax[display="true"]'); + const cache = plugin.app.metadataCache.getFileCache(file); + if (!cache) return; - const page = plugin.indexManager.index.getMarkdownPage(ctx.sourcePath); - if (!page) { - new Notice(`${plugin.manifest.name}: Failed to fetch the metadata for PDF export; equation numbers will not be displayed in the exported PDF.`); - return; - } + const mathSection = cache.sections?.find(s => s.position.start.line === section.lineStart && s.type === 'math'); + const blockId = mathSection?.id; - let equationIndex = 0; - for (const section of page.$sections) { - for (const block of section.$blocks) { - if (!EquationBlock.isEquationBlock(block)) continue; - - const div = topLevelMathDivs[equationIndex++]; - if (block.$printName) div.setAttribute('data-equation-id', block.$id); + if (blockId) { + const equation = equations.get(blockId); + if (equation?.$printName) { + const numberEl = createSpan({ + cls: "math-booster-equation-number", + text: equation.$printName, + }); + mathEl.parentElement?.classList.add("math-booster-has-equation-number"); + mathEl.parentElement?.appendChild(numberEl); + } } - } - - if (topLevelMathDivs.length != equationIndex) { - new Notice(`${plugin.manifest.name}: Something unexpected occured while preprocessing for PDF export. Equation numbers might not be displayed properly in the exported PDF.`); - } - } catch (err) { - new Notice(`${plugin.manifest.name}: Something unexpected occured while preprocessing for PDF export. See the developer console for the details. Equation numbers might not be displayed properly in the exported PDF.`); - console.error(err); - } -} - - -export class EquationNumberRenderer extends MarkdownRenderChild { - app: App - index: MathIndex; - - constructor(containerEl: HTMLElement, public plugin: LatexReferencer, public file: TFile, public context: MarkdownPostProcessorContext) { - // containerEl, currentEL are mjx-container.MathJax elements - super(containerEl); - this.app = plugin.app; - this.index = this.plugin.indexManager.index; - - this.registerEvent(this.plugin.indexManager.on("index-initialized", () => { - setTimeout(() => this.update()); - })); - - this.registerEvent(this.plugin.indexManager.on("index-updated", (file) => { - setTimeout(() => { - if (file.path === this.file.path) this.update(); - }); - })); - } - - getEquationCache(lineOffset: number = 0): EquationBlock | null { - const info = this.context.getSectionInfo(this.containerEl); - const page = this.index.getMarkdownPage(this.file.path); - if (!info || !page) return null; - - // get block ID - const block = page.getBlockByLineNumber(info.lineStart + lineOffset) ?? page.getBlockByLineNumber(info.lineEnd + lineOffset); - if (EquationBlock.isEquationBlock(block)) return block; - - return null; - } - - async onload() { - setTimeout(() => this.update()); - } - - onunload() { - // I don't know if this is really necessary, but just in case... - finishRenderMath(); - } - - update() { - // for PDF export - const id = this.containerEl.getAttribute('data-equation-id'); - - const equation = id ? this.index.getEquationBlock(id) : this.getEquationCacheCaringHoverAndEmbed(); - if (!equation) return; - const settings = resolveSettings(undefined, this.plugin, this.file); - replaceMathTag(this.containerEl, equation, settings); - } - - getEquationCacheCaringHoverAndEmbed(): EquationBlock | null { - /** - * https://github.com/RyotaUshio/obsidian-latex-theorem-equation-referencer/issues/179 - * - * In the case of embeds or hover popovers, the line numbers contained - * in the result of MarkdownPostProcessorContext.getSectionInfo() is - * relative to the content included in the embed. - * In other words, they does not always represent the offset from the beginning of the file. - * So they require special handling. - */ - - const equation = this.getEquationCache(); - - let linktext = this.containerEl.closest('[src]')?.getAttribute('src'); // in the case of embeds - - if (!linktext) { - const hoverEl = this.containerEl.closest('.hover-popover:not(.hover-editor)'); - if (hoverEl) { - // The current context is hover page preview; read the linktext saved in the plugin instance. - linktext = this.plugin.lastHoverLinktext; - } - } - - if (linktext) { // linktext was found - const { file, subpathResult } = resolveLinktext(this.app, linktext, this.context.sourcePath) ?? {}; - - if (!file || !subpathResult) return null; - - const page = this.index.load(file.path); - if (!MarkdownPage.isMarkdownPage(page)) return null; - - if (subpathResult.type === "block") { - const block = page.$blocks.get(subpathResult.block.id); - if (!EquationBlock.isEquationBlock(block)) return null; - return block; - } else { - return this.getEquationCache(subpathResult.start.line); - } - } - - return equation; - } -} + }); + }; +}; diff --git a/src/main.ts b/src/main.ts index 6c33c83..6434a7b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -83,9 +83,24 @@ export default class LatexReferencer extends Plugin { // wait until the layout is ready to ensure MathLinks has been loaded when calling addProvider() this.app.workspace.onLayoutReady(() => { - this.addChild( - MathLinks.addProvider(this.app, (mathLinks) => new CleverefProvider(mathLinks, this)) - ); + console.log("[DEBUG main.ts] onLayoutReady triggered. Attempting to add MathLinks provider."); + try { + if (!MathLinks) { + console.error("[DEBUG main.ts] CRITICAL FAILURE: MathLinks API is not available."); + return; + } + + const providerRegistration = MathLinks.addProvider(this.app, (mathLinks) => { + console.log("[DEBUG main.ts] MathLinks provider factory function is being executed."); + return new CleverefProvider(mathLinks as any, this); + }); + + this.addChild(providerRegistration); + console.log("[DEBUG main.ts] -> SUCCESS: MathLinks provider was added successfully."); + + } catch (e) { + console.error("[DEBUG main.ts] -> CRITICAL FAILURE: An error occurred while adding the MathLinks provider:", e); + } }); diff --git a/src/search/core.ts b/src/search/core.ts index ffa5c26..ef9f854 100644 --- a/src/search/core.ts +++ b/src/search/core.ts @@ -161,7 +161,9 @@ export class ActiveNoteSearchCore extends MathSearchCore { const editor = this.parent.getEditor(); if (file && editor) { - return this.provider.getEquations(file, editor); + // This is the fix: Pass the editor's content string. + const content = editor.getValue(); + return this.provider.getEquations(file, content); } return [];