import { TFile, Plugin, WorkspaceLeaf, addIcon, App, PluginManifest, MarkdownView, normalizePath, ViewState, Notice, request, MetadataCache, Workspace, TAbstractFile, FrontMatterCache, } from "obsidian"; import { VIEW_TYPE_EXCALIDRAW, EXCALIDRAW_ICON, ICON_NAME, SCRIPTENGINE_ICON, SCRIPTENGINE_ICON_NAME, RERENDER_EVENT, FRONTMATTER_KEYS, FRONTMATTER, JSON_parse, SCRIPT_INSTALL_CODEBLOCK, SCRIPT_INSTALL_FOLDER, EXPORT_TYPES, EXPORT_IMG_ICON_NAME, EXPORT_IMG_ICON, LOCALE, setExcalidrawPlugin, DEVICE, FONTS_STYLE_ID, CJK_STYLE_ID, loadMermaid, setRootElementSize, } from "../constants/constants"; import { ExcalidrawSettings, DEFAULT_SETTINGS, ExcalidrawSettingTab } from "./settings"; import { ExcalidrawAutomate } from "../shared/ExcalidrawAutomate"; import { initExcalidrawAutomate } from "src/utils/excalidrawAutomateUtils"; import { around, dedupe } from "monkey-around"; import { t } from "../lang/helpers"; import { checkAndCreateFolder, fileShouldDefaultAsExcalidraw, getDrawingFilename, getIMGFilename, getNewUniqueFilepath, } from "../utils/fileUtils"; import { getFontDataURL, errorlog, setLeftHandedMode, sleep, isVersionNewerThanOther, isCallerFromTemplaterPlugin, versionUpdateCheckTimer, getFontMetrics, } from "../utils/utils"; import { foldExcalidrawSection, getExcalidrawViews, setExcalidrawView } from "../utils/obsidianUtils"; import { FileId } from "@zsviczian/excalidraw/types/element/src/types"; import { ScriptEngine } from "../shared/Scripts"; import { hoverEvent, initializeMarkdownPostProcessor, markdownPostProcessor, legacyExcalidrawPopoverObserver } from "./managers/MarkdownPostProcessor"; import { FieldSuggester } from "../shared/Suggesters/FieldSuggester"; import { ReleaseNotes } from "../shared/Dialogs/ReleaseNotes"; import { Packages } from "../types/types"; import { PreviewImageType } from "../types/utilTypes"; import { emulateCTRLClickForLinks, linkClickModifierType, PaneTarget } from "../utils/modifierkeyHelper"; import { imageCache } from "../shared/ImageCache"; import { StylesManager } from "./managers/StylesManager"; import { CustomMutationObserver, debug, log, DEBUGGING, setDebugging, ts } from "../utils/debugHelper"; import { ExcalidrawConfig } from "../shared/ExcalidrawConfig"; import { EditorHandler } from "./editor/EditorHandler"; import { ExcalidrawLib } from "../types/excalidrawLib"; import { Rank, SwordColors } from "../constants/actionIcons"; import { RankMessage } from "../shared/Dialogs/RankMessage"; import { initCompressionWorker, terminateCompressionWorker } from "../shared/Workers/compression-worker"; import { WeakArray } from "../shared/WeakArray"; import { getCJKDataURLs } from "../utils/CJKLoader"; import { ExcalidrawLoading, switchToExcalidraw } from "../view/ExcalidrawLoading"; import { clearMathJaxVariables } from "../shared/LaTeX"; import { PluginFileManager } from "./managers/FileManager"; import { ObserverManager } from "./managers/ObserverManager"; import { PackageManager } from "./managers/PackageManager"; import ExcalidrawView from "../view/ExcalidrawView"; import { CommandManager } from "./managers/CommandManager"; import { EventManager } from "./managers/EventManager"; declare const PLUGIN_VERSION:string; declare const INITIAL_TIMESTAMP: number; type FileMasterInfo = { isHyperLink: boolean; isLocalLink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string, colorMapJSON?: string } export default class ExcalidrawPlugin extends Plugin { private fileManager: PluginFileManager; private observerManager: ObserverManager; private packageManager: PackageManager; private commandManager: CommandManager; private eventManager: EventManager; public eaInstances = new WeakArray(); public fourthFontLoaded: boolean = false; public excalidrawConfig: ExcalidrawConfig; public excalidrawFileModes: { [file: string]: string } = {}; public settings: ExcalidrawSettings; public activeExcalidrawView: ExcalidrawView = null; public lastActiveExcalidrawFilePath: string = null; public hover: { linkText: string; sourcePath: string } = { linkText: null, sourcePath: null, }; private legacyExcalidrawPopoverObserver: MutationObserver | CustomMutationObserver; private fileExplorerObserver: MutationObserver | CustomMutationObserver; public opencount: number = 0; public ea: ExcalidrawAutomate; //A master list of fileIds to facilitate copy / paste public filesMaster: Map = null; //fileId, path public equationsMaster: Map = null; //fileId, formula public mermaidsMaster: Map = null; //fileId, mermaidText public scriptEngine: ScriptEngine; private stylesManager:StylesManager; public editorHandler: EditorHandler; //if set, the next time this file is opened it will be opened as markdown public forceToOpenInMarkdownFilepath: string = null; //private slob:string; public loadTimestamp:number; private isLocalCJKFontAvailabe:boolean = undefined public isReady = false; private startupAnalytics: string[] = []; private lastLogTimestamp: number; private settingsReady: boolean = false; public wasPenModeActivePreviously: boolean = false; public popScope: Function = null; public lastPDFLeafID: string = null; constructor(app: App, manifest: PluginManifest) { super(app, manifest); this.loadTimestamp = INITIAL_TIMESTAMP; this.lastLogTimestamp = this.loadTimestamp; this.filesMaster = new Map< FileId, { isHyperLink: boolean; isLocalLink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string; colorMapJSON?: string } >(); this.equationsMaster = new Map(); this.mermaidsMaster = new Map(); //isExcalidraw function is used already is already used by MarkdownPostProcessor in onLoad before onLayoutReady this.fileManager = new PluginFileManager(this); setExcalidrawPlugin(this); /*if((process.env.NODE_ENV === 'development')) { this.slob = new Array(200 * 1024 * 1024 + 1).join('A'); // Create a 200MB blob }*/ } public logStartupEvent(message:string) { const timestamp = Date.now(); this.startupAnalytics.push(`${message}\nTotal: ${timestamp - this.loadTimestamp}ms Delta: ${timestamp - this.lastLogTimestamp}ms\n`); this.lastLogTimestamp = timestamp; } public printStarupBreakdown() { console.log(`Excalidraw ${PLUGIN_VERSION} startup breakdown:\n`+this.startupAnalytics.join("\n")); } get locale() { return LOCALE; } get window(): Window { return window; }; get document(): Document { return document; }; // by adding the wrapper like this, likely in debug mode I am leaking memory because my code removes // the original event handlers, not the wrapped ones. I will only uncomment this if I need to debug /*public registerEvent(event: any) { if (process.env.NODE_ENV !== 'development') { super.registerEvent(event); return; } else { if(!DEBUGGING) { super.registerEvent(event); return; } const originalHandler = event.fn; // Wrap the original event handler const wrappedHandler = async (...args: any[]) => { const startTime = performance.now(); // Get start time // Invoke the original event handler const result = await originalHandler(...args); const endTime = performance.now(); // Get end time const executionTime = endTime - startTime; if(executionTime > durationTreshold) { console.log(`Excalidraw Event '${event.name}' took ${executionTime}ms to execute`); } return result; } // Replace the original event handler with the wrapped one event.fn = wrappedHandler; // Register the modified event super.registerEvent(event); }; }*/ /** * used by Excalidraw to getSharedMermaidInstance * @returns shared mermaid instance */ public async getMermaid() { return await loadMermaid(); } public isPenMode() { return this.wasPenModeActivePreviously || (this.settings.defaultPenMode === "always") || (this.settings.defaultPenMode === "mobile" && DEVICE.isMobile); } public getCJKFontSettings() { const assetsFoler = this.settings.fontAssetsPath; if(typeof this.isLocalCJKFontAvailabe === "undefined") { this.isLocalCJKFontAvailabe = this.app.vault.getFiles().some(f=>f.path.startsWith(assetsFoler)); } if(!this.isLocalCJKFontAvailabe) { return { c: false, j: false, k: false }; } return { c: this.settings.loadChineseFonts, j: this.settings.loadJapaneseFonts, k: this.settings.loadKoreanFonts, } } public async loadFontFromFile(fontName: string): Promise { const assetsFoler = this.settings.fontAssetsPath; if(!this.isLocalCJKFontAvailabe) { return; } const file = this.app.vault.getAbstractFileByPath(normalizePath(assetsFoler + "/" + fontName)); if(!file || !(file instanceof TFile)) { return; } return await this.app.vault.readBinary(file); } async onload() { this.logStartupEvent("Plugin Constructor ready, starting onload()"); this.registerView( VIEW_TYPE_EXCALIDRAW, (leaf: WorkspaceLeaf) => { if(this.isReady) { return new ExcalidrawView(leaf, this); } else { return new ExcalidrawLoading(leaf, this); } }, ); //Compatibility mode with .excalidraw files this.registerExtensions(["excalidraw"], VIEW_TYPE_EXCALIDRAW); addIcon(ICON_NAME, EXCALIDRAW_ICON); addIcon(SCRIPTENGINE_ICON_NAME, SCRIPTENGINE_ICON); addIcon(EXPORT_IMG_ICON_NAME, EXPORT_IMG_ICON); this.addRibbonIcon(ICON_NAME, t("CREATE_NEW"), this.actionRibbonClick.bind(this)); try { this.loadSettings({reEnableAutosave:true}) .then(this.onloadCheckForOnceOffSettingsUpdates.bind(this)); } catch (e) { new Notice("Error loading plugin settings", 6000); console.error("Error loading plugin settings", e); } this.logStartupEvent("Settings loaded"); try { // need it her for ExcaliBrain this.ea = initExcalidrawAutomate(this); } catch (e) { new Notice("Error initializing Excalidraw Automate", 6000); console.error("Error initializing Excalidraw Automate", e); } this.logStartupEvent("Excalidraw Automate initialized"); try { //Licat: Are you registering your post processors in onLayoutReady? You should register them in onload instead this.addMarkdownPostProcessor(); } catch (e) { new Notice("Error adding markdown post processor", 6000); console.error("Error adding markdown post processor", e); } this.logStartupEvent("Markdown post processor added"); this.app.workspace.onLayoutReady(this.onloadOnLayoutReady.bind(this)); this.logStartupEvent("Workspace ready event handler added"); } private async onloadCheckForOnceOffSettingsUpdates() { const updateSettings = !this.settings.onceOffCompressFlagReset || !this.settings.onceOffGPTVersionReset; if(!this.settings.onceOffCompressFlagReset) { this.settings.compress = true; this.settings.onceOffCompressFlagReset = true; } if(!this.settings.onceOffGPTVersionReset) { this.settings.onceOffGPTVersionReset = true; if(this.settings.openAIDefaultVisionModel === "gpt-4-vision-preview") { this.settings.openAIDefaultVisionModel = "gpt-4o"; } } if(updateSettings) { await this.saveSettings(); } this.addSettingTab(new ExcalidrawSettingTab(this.app, this)); this.settingsReady = true; } private async onloadOnLayoutReady() { this.loadTimestamp = Date.now(); this.lastLogTimestamp = this.loadTimestamp; this.logStartupEvent("\n----------------------------------\nWorkspace onLayoutReady event fired (these actions are outside the plugin initialization)"); await this.awaitSettings(); this.logStartupEvent("Settings awaited"); if(!this.settings.overrideObsidianFontSize) { setRootElementSize(); } this.packageManager = new PackageManager(this); this.eventManager = new EventManager(this); this.observerManager = new ObserverManager(this); this.commandManager = new CommandManager(this); try { initCompressionWorker(); } catch (e) { new Notice("Error initializing compression worker", 6000); console.error("Error initializing compression worker", e); } this.logStartupEvent("Compression worker initialized"); try { this.excalidrawConfig = new ExcalidrawConfig(this); } catch (e) { new Notice("Error initializing Excalidraw config", 6000); console.error("Error initializing Excalidraw config", e); } this.logStartupEvent("Excalidraw config initialized"); this.observerManager.initialize(); try { //inspiration taken from kanban: //https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267 this.registerMonkeyPatches(); } catch (e) { new Notice("Error registering monkey patches", 6000); console.error("Error registering monkey patches", e); } this.logStartupEvent("Monkey patches registered"); try { this.stylesManager = new StylesManager(this); } catch (e) { new Notice("Error initializing styles manager", 6000); console.error("Error initializing styles manager", e); } this.logStartupEvent("Styles manager initialized"); try { this.scriptEngine = new ScriptEngine(this); } catch (e) { new Notice("Error initializing script engine", 6000); console.error("Error initializing script engine", e); } this.logStartupEvent("Script engine initialized"); try { await this.initializeFonts(); } catch (e) { new Notice("Error initializing fonts", 6000); console.error("Error initializing fonts", e); } this.logStartupEvent("Fonts initialized"); try { imageCache.initializeDB(this); } catch (e) { new Notice("Error initializing image cache", 6000); console.error("Error initializing image cache", e); } this.logStartupEvent("Image cache initialized"); try { this.isReady = true; await switchToExcalidraw(this.app); this.switchToExcalidarwAfterLoad(); } catch (e) { new Notice("Error switching views to Excalidraw", 6000); console.error("Error switching views to Excalidraw", e); } this.logStartupEvent("Switched to Excalidraw views"); try { if (this.settings.showReleaseNotes) { //I am repurposing imageElementNotice, if the value is true, this means the plugin was just newly installed to Obsidian. const obsidianJustInstalled = (this.settings.previousRelease === "0.0.0") || !this.settings.previousRelease; if (isVersionNewerThanOther(PLUGIN_VERSION, this.settings.previousRelease ?? "0.0.0")) { new ReleaseNotes( this.app, this, obsidianJustInstalled ? null : PLUGIN_VERSION, ).open(); } } } catch (e) { new Notice("Error opening release notes", 6000); console.error("Error opening release notes", e); } this.logStartupEvent("Release notes opened"); //--------------------------------------------------------------------- //initialization that can happen after Excalidraw views are initialized //--------------------------------------------------------------------- this.fileManager.initialize(); //fileManager will preLoad the filecache this.eventManager.initialize(); //eventManager also adds event listner to filecache try { this.runStartupScript(); } catch (e) { new Notice("Error running startup script", 6000); console.error("Error running startup script", e); } this.logStartupEvent("Startup script run"); try { this.editorHandler = new EditorHandler(this); this.editorHandler.setup(); } catch (e) { new Notice("Error setting up editor handler", 6000); console.error("Error setting up editor handler", e); } this.logStartupEvent("Editor handler initialized"); try { this.registerInstallCodeblockProcessor(); } catch (e) { new Notice("Error registering script install-codeblock processor", 6000); console.error("Error registering script install-codeblock processor", e); } this.logStartupEvent("Script install-codeblock processor registered"); this.commandManager.initialize(); try { this.registerEditorSuggest(new FieldSuggester(this)); } catch (e) { new Notice("Error registering editor suggester", 6000); console.error("Error registering editor suggester", e); } this.logStartupEvent("Editor suggester registered"); try { this.setPropertyTypes(); } catch (e) { new Notice("Error setting up property types", 6000); console.error("Error setting up property types", e); } this.logStartupEvent("Property types set"); } public async awaitSettings() { let counter = 0; while(!this.settingsReady && counter < 150) { await sleep(20); } } public async awaitInit() { let counter = 0; while(!this.isReady && counter < 150) { await sleep(50); } } /** * Loads the Excalidraw frontmatter tags to Obsidian property suggester so people can more easily find relevant front matter switches * Must run after the workspace is ready * @returns */ private async setPropertyTypes() { if(!this.settings.loadPropertySuggestions) return; const app = this.app; (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setPropertyTypes, `ExcalidrawPlugin.setPropertyTypes`); Object.keys(FRONTMATTER_KEYS).forEach((key) => { if(FRONTMATTER_KEYS[key].depricated === true) return; const {name, type} = FRONTMATTER_KEYS[key]; app.metadataTypeManager.setType(name,type); }); } public async initializeFonts() { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.initializeFonts, `ExcalidrawPlugin.initializeFonts`); const cjkFontDataURLs = await getCJKDataURLs(this); if(typeof cjkFontDataURLs === "boolean" && !cjkFontDataURLs) { new Notice(t("FONTS_LOAD_ERROR") + this.settings.fontAssetsPath,6000); } if(typeof cjkFontDataURLs === "object") { const fontDeclarations = cjkFontDataURLs.map(dataURL => `@font-face { font-family: 'Xiaolai'; src: url("${dataURL}"); font-display: swap; font-weight: 400; }` ); for(const ownerDocument of this.getOpenObsidianDocuments()) { await this.addFonts(fontDeclarations, ownerDocument, CJK_STYLE_ID); }; new Notice(t("FONTS_LOADED")); } const font = await getFontDataURL( this.app, this.settings.experimantalFourthFont, "", "Local Font", ); if(font.dataURL === "") { this.fourthFontLoaded = true; return; } const fourthFontDataURL = font.dataURL; const f = this.app.metadataCache.getFirstLinkpathDest(this.settings.experimantalFourthFont, ""); // Call getFontMetrics with the fourthFontDataURL let fontMetrics = f.extension.startsWith("woff") ? undefined : await getFontMetrics(fourthFontDataURL, "Local Font"); if (!fontMetrics) { console.log("Font Metrics not found, using default"); fontMetrics = { unitsPerEm: 1000, ascender: 750, descender: -250, lineHeight: 1.2, fontName: "Local Font", } } this.packageManager.getPackageMap().forEach(({excalidrawLib}) => { (excalidrawLib as typeof ExcalidrawLib).registerLocalFont({metrics: fontMetrics as any}, fourthFontDataURL); }); // Add fonts to open Obsidian documents for(const ownerDocument of this.getOpenObsidianDocuments()) { await this.addFonts([ `@font-face{font-family:'Local Font';src:url("${fourthFontDataURL}");font-display: swap;font-weight: 400;`, ], ownerDocument); }; if(!this.fourthFontLoaded) setTimeout(()=>{this.fourthFontLoaded = true},100); } public async addFonts(declarations: string[],ownerDocument:Document = document, styleId:string = FONTS_STYLE_ID) { // replace the old local font