youfoundjk_TeXcore/src/main.ts
2026-06-04 21:26:06 +02:00

356 lines
12 KiB
TypeScript

import {
MarkdownView,
Plugin,
TFile,
parseLinktext,
Menu,
TFolder,
Editor,
MarkdownFileInfo,
EventRef
} from 'obsidian';
import type { Extension } from '@codemirror/state';
import { around } from 'monkey-around';
import { PluginSettings, DEFAULT_SETTINGS } from './settings/settings';
import { MathSettingTab } from './settings/tab';
import { Provider } from './core/linker/provider-link-render';
import { LatexLinkProvider } from './core/linker/latex-provider';
import { createEquationNumberProcessor } from 'core/equations/reading-view-equations';
import { CustomMathLinksProcessor } from './core/linker/reading-view-linker';
import { setupDOMObserver } from './core/linker/dom-observer';
import { createEquationNumberPlugin } from 'core/equations/live-preview-equations';
import { createLivePreviewLinkRendererPlugin } from './core/linker/live-preview-link-renderer';
import { insertDisplayMath } from 'utils/plugin';
import { LinkAutocomplete } from 'ui/search/editor-suggest';
import { MathSearchModal } from 'ui/search/modal';
import { EquationBlock } from 'types';
// ADDED: Import our new internal patcher function
import { patchSuggesterWithQuickPreview } from 'ui/quick-preview/patcher';
import { processActiveNoteEquations } from './core/equations/numbering';
import { ExportConfigModal } from './ui/export-pdf/modal';
import { checkAndFixCalloutMath } from 'utils/fixer';
import { showNotice } from 'utils/obsidian';
import { traverseFolder } from 'features/export-pdf/utils';
import { SnippetManager } from 'features/snippets/manager';
import { processZoteroCleanup } from 'features/zotero-cleanup';
import { CustomNoteManager } from 'features/custom-notes/manager';
import { TikzRenderer } from './features/tikz/renderer';
import {
createRowLayoutProcessor,
createLivePreviewRowLayoutPlugin
} from './features/tikz/row-layout';
import { createTikzLivePreviewPlugin } from './features/tikz/live-preview-overlay';
declare const process: { env: { NODE_ENV?: string } };
const isDev = process.env.NODE_ENV === 'development';
export default class LatexReferencer extends Plugin {
declare settings: PluginSettings;
editorExtensions!: Extension[];
internalProviders: Provider[] = [];
snippetManager!: SnippetManager;
customNoteManager!: CustomNoteManager;
tikzRenderer!: TikzRenderer;
async onload() {
await this.loadSettings();
this.internalProviders.push(new LatexLinkProvider(this));
// Snippets
this.snippetManager = new SnippetManager(this);
this.snippetManager.onLoad();
// Custom Notes
this.customNoteManager = new CustomNoteManager(this);
this.customNoteManager.onLoad();
// TikZJax Rendering
this.tikzRenderer = new TikzRenderer(this);
await this.tikzRenderer.onLoad();
this.addSettingTab(new MathSettingTab(this.app, this));
// Commands
this.addCommand({
id: 'remove-duplicate-zotero-annotations',
name: 'Remove duplicate Zotero annotations',
editorCallback: (editor: Editor, ctx: MarkdownView | MarkdownFileInfo) => {
if (ctx instanceof MarkdownView) {
void processZoteroCleanup(this, ctx);
}
}
});
this.addCommand({
id: 'fix-callout-equations',
name: 'Fix callout equations in active note',
editorCallback: (editor: Editor, ctx: MarkdownView | MarkdownFileInfo) => {
const content = editor.getValue();
const fixed = checkAndFixCalloutMath(content);
if (fixed) {
editor.setValue(fixed);
showNotice('Fixed callout equations.');
} else {
showNotice('No issues found or no changes needed.');
}
}
});
this.addCommand({
id: 'insert-display-math',
name: 'Insert display math',
editorCallback: insertDisplayMath
});
this.addCommand({
id: 'search-equations',
name: 'Search equations in active note',
callback: () => {
new MathSearchModal(this).open();
}
});
this.addCommand({
id: 'export-current-file-to-pdf',
name: 'Export current file to pdf',
checkCallback: (checking: boolean) => {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
const file = view?.file;
if (!file) {
return false;
}
if (checking) {
return true;
}
new ExportConfigModal(this, file).open();
return true;
}
});
// Menu items for file export
this.registerEvent(
(
this.app.workspace as unknown as {
on(name: 'file-menu', callback: (menu: Menu, file: TFile | TFolder) => void): EventRef;
}
).on('file-menu', (menu: Menu, file: TFile | TFolder) => {
let title = file instanceof TFolder ? 'Export folder to PDF' : 'Better Export PDF';
if (isDev) {
title = `${title} (dev)`;
}
menu.addItem(item => {
item
.setTitle(title)
.setIcon('download')
.setSection('action')
.onClick(async () => {
new ExportConfigModal(this, file).open();
});
});
})
);
this.registerEvent(
(
this.app.workspace as unknown as {
on(name: 'file-menu', callback: (menu: Menu, file: TFile | TFolder) => void): EventRef;
}
).on('file-menu', (menu: Menu, file: TFile | TFolder) => {
if (file instanceof TFolder) {
let title = 'Export to PDF...';
if (isDev) {
title = `${title} (dev)`;
}
menu.addItem(item => {
item.setTitle(title).setIcon('lucide-folder-down').setSection('action');
const subMenu = (item as unknown as { setSubmenu: () => Menu }).setSubmenu();
subMenu.addItem(item =>
item
.setTitle('Export each file to pdf')
.setIcon('lucide-file-stack')
.onClick(async () => {
new ExportConfigModal(this, file, true).open();
})
);
subMenu.addItem(item =>
item
.setTitle('Generate TOC.md file')
.setIcon('lucide-file-text')
.onClick(async () => {
await this.generateToc(file);
})
);
});
}
})
);
// Editor Extensions
this.editorExtensions = [];
this.registerEditorExtension(this.editorExtensions);
this.updateEditorExtensions();
// Link autocompletion
this.registerEditorSuggest(new LinkAutocomplete(this));
// REPLACED: The old external plugin logic is gone.
// We now call our internal patcher directly.
const itemNormalizer = (item: EquationBlock) => ({
linktext: `${item.$file}#^${item.$blockId}`, // Use the block ID for more precise linking
sourcePath: item.$file,
line: item.$position.start
});
patchSuggesterWithQuickPreview(this, LinkAutocomplete, itemNormalizer);
patchSuggesterWithQuickPreview(this, MathSearchModal, itemNormalizer);
// Markdown post processors for Reading View
this.registerMarkdownPostProcessor(createEquationNumberProcessor(this));
this.registerMarkdownPostProcessor(CustomMathLinksProcessor(this));
this.registerMarkdownPostProcessor(createRowLayoutProcessor(this));
this.app.workspace.onLayoutReady(() => {
void this.forceRerender();
});
this.patchPagePreview();
this.register(setupDOMObserver(this));
}
onunload() {
this.tikzRenderer.onUnload();
}
async loadSettings() {
this.settings = {
...DEFAULT_SETTINGS,
...((await this.loadData()) as Partial<PluginSettings>)
};
}
async saveSettings() {
await this.saveData(this.settings);
}
updateEditorExtensions() {
this.editorExtensions.length = 0;
// PUSH ALL PLUGINS
this.editorExtensions.push(createEquationNumberPlugin(this));
this.editorExtensions.push(createLivePreviewLinkRendererPlugin(this));
this.editorExtensions.push(createLivePreviewRowLayoutPlugin(this));
this.editorExtensions.push(createTikzLivePreviewPlugin(this));
this.app.workspace.updateOptions();
}
private forceRerender() {
this.app.workspace.iterateAllLeaves(leaf => {
const view = leaf.view;
if (view instanceof MarkdownView) {
view.previewMode.rerender(true);
}
});
}
private patchPagePreview() {
const pagePreviewPlugin = this.app.internalPlugins.getPluginById('page-preview') as unknown as {
enabled: boolean;
instance: unknown;
} | null;
if (!pagePreviewPlugin?.enabled) {
return;
}
const instance = pagePreviewPlugin.instance;
const app = this.app;
const getEquations = (file: TFile, content: string) =>
processActiveNoteEquations(this, file, content);
const uninstaller = around(instance as Record<string, unknown>, {
onLinkHover(old: unknown) {
const oldFunc = old as (
this: unknown,
hoverParent: unknown,
targetEl: unknown,
linktext: string,
sourcePath: string,
state: Record<string, unknown>
) => unknown;
return function (
this: unknown,
hoverParent: unknown,
targetEl: unknown,
linktext: string,
sourcePath: string,
state: Record<string, unknown>
) {
const { path, subpath } = parseLinktext(linktext);
// Check if it's our custom equation link (e.g., [[#^eq-...]] or [[file#^eq-...]])
if (subpath && subpath.startsWith('^eq-')) {
const subpathText = subpath.substring(1); // Remove '^', leaving 'eq-...'
const subIndexMatch = subpathText.match(/-(\d+)$/);
let blockId = subpathText;
if (subIndexMatch) {
blockId = subpathText.substring(0, subIndexMatch.index);
}
const targetFile = app.metadataCache.getFirstLinkpathDest(path, sourcePath);
if (targetFile instanceof TFile) {
const activeView = app.workspace.getActiveViewOfType(MarkdownView);
const activeFile = activeView?.file;
const activeContent =
typeof activeView?.getViewData === 'function' ? activeView.getViewData() : null;
if (!activeFile || targetFile.path !== activeFile.path || activeContent === null) {
return oldFunc.call(this, hoverParent, targetEl, linktext, sourcePath, state);
}
const equations = getEquations(activeFile, activeContent);
const targetEquation = equations.get(blockId);
if (targetEquation) {
const line = targetEquation.$position.start;
const newState = { ...state, scroll: line };
// Immediately call the original function with the correct line number
return oldFunc.call(this, hoverParent, targetEl, linktext, sourcePath, newState);
}
}
}
// If it's not our link, or if we couldn't find it in the cache,
// call the original function without modification.
return oldFunc.call(this, hoverParent, targetEl, linktext, sourcePath, state);
};
}
});
this.register(uninstaller);
}
async generateToc(root: TFolder | TFile) {
const tocPath = root.path === '/' || root.path === '.' ? '_TOC_.md' : `${root.path}/_TOC_.md`;
let content = `---\ntoc: true\ntitle: ${root.name || 'Root'}\n---\n`;
if (root instanceof TFolder) {
const files = traverseFolder(root);
for (const file of files) {
if (file.name === '_TOC_.md') {
continue;
}
content += `[[${file.path}]]\n`;
}
}
const abstractFile = this.app.vault.getAbstractFileByPath(tocPath);
if (abstractFile instanceof TFile) {
await this.app.vault.modify(abstractFile, content);
} else {
await this.app.vault.create(tocPath, content);
}
}
}