From 1a78992ef1b93ca8f3273d1e07dbb836b8b8a5ab Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sun, 9 Aug 2026 09:55:01 +0200 Subject: [PATCH 1/6] refactor: extract settings management --- RefactorPlan.md | 4 + src/core/main.ts | 150 ++----------------- src/core/managers/CommandManager.ts | 8 +- src/core/managers/PluginSettingsManager.ts | 165 +++++++++++++++++++++ src/core/settings.ts | 2 - src/shared/Dialogs/Messages.ts | 1 + src/view/ExcalidrawView.ts | 4 +- 7 files changed, 192 insertions(+), 142 deletions(-) create mode 100644 src/core/managers/PluginSettingsManager.ts diff --git a/RefactorPlan.md b/RefactorPlan.md index 484910c7..9764a7f7 100644 --- a/RefactorPlan.md +++ b/RefactorPlan.md @@ -20,6 +20,7 @@ validated, and what remains uncertain. | --- | --- | --- | | Assessment and baseline design | Complete | Initial architecture, risks, sequencing, and validation matrix documented | | Retire legacy AI settings and fallbacks | Complete | Removed the retired migration, schema/default fields, GPT reset, and AI runtime fallbacks without filtering unknown persisted keys; manual testing found no issues | +| Extract settings implementation service | Implemented; awaiting manual validation | `PluginSettingsManager` now owns persistence, default assembly, remaining migrations, and API-key obfuscation; plugin methods and startup readiness remain intact, while temporary autosave enablement is now explicit plugin-instance state rather than persisted settings state | | Audit and consolidate duplicate logic | In progress | Consolidated `updateFrontmatterInString()`, `arrayToMap()`, `wrapTextAtCharLength()`, `getLinkParts()`/`LinkParts`, `getBinaryFileFromDataURL()`, `svgToBase64()`, `getFontDataURL()`, `cropCanvas()`, `getImageSize()`, `promiseTry()`, `isVersionNewerThanOther()`, `repositionElementsToCursor()`, the internal `cloneElement()`, and `getBoundTextElementId()`; continue one independently testable helper family at a time | | All later phases | Planned | Begin only after the preceding checkpoint is validated | @@ -46,6 +47,9 @@ validated, and what remains uncertain. | 2026-08-08 | Consolidated `getImageSize()` and `promiseTry()` | Kept the identical embedded-asset implementations as canonical, documented their native image-loading and promise-boundary semantics, and preserved the `utils.ts` import surface through re-exports. The former `getImageSize()` differed only in bracing and comments; both `promiseTry()` copies were textually identical. No caller used distinct behavior | Repository search confirms one implementation of each helper; `npm run build` passed after each code change; targeted ESLint passed | | 2026-08-08 | Consolidated `isVersionNewerThanOther()` | Extracted the identical implementations to dependency-light `versionUtils.ts` and preserved both `utils.ts` and `sceneDataUtils.ts` import paths. Missing, malformed, prerelease, and numeric comparison behavior remains unchanged | Repository search confirms one implementation; `npm run build` and targeted ESLint passed | | 2026-08-08 | Consolidated element positioning, cloning, and bound-text helpers | Extracted `estimateBounds()`, `repositionElementsToCursor()`, the internal ID-preserving `cloneElement()`, and `getBoundTextElementId()` to documented `excalidrawElementUtils.ts`, preserving exports from both former owner modules. Repositioning and cloning were identical. The only bound-text difference was a redundant optional chain after an equivalent non-null/length guard, so callers could not observe it. The public `ExcalidrawAutomate.cloneElement()` API, which assigns a new ID, was not changed | Repository search confirms one implementation of each helper; targeted ESLint, `npm run build`, `npm run lib`, and `git diff --check` passed; bundle size remains 5,095,379 bytes because Rollup had already eliminated the duplicate paths; `npm run madge` could not run because `madge` is not installed | +| 2026-08-08 | Extracted plugin settings implementation from `main.ts` | Added documented `PluginSettingsManager` with a narrow host contract to own load/save, default assembly, existing library/Markdown-image/oEmbed/preview migrations, and API-key obfuscation. At this extraction checkpoint, retained `plugin.settings`, all public plugin method signatures, startup autosave behavior, settings readiness, settings-tab timing, external-change library invalidation, persisted format, and unknown-key behavior. After delegate TSDoc, `main.ts` decreased from 1,676 to 1,563 lines | `npm run build` passed after each code change; `npm run lib` and a scoped `git diff --check` passed; the new manager and changed delegate lines have no ESLint diagnostics, while targeted lint still reports four unrelated existing `any` diagnostics in the startup-script block of `main.ts`; the initial extraction build was 5,094,122 bytes, 1,257 bytes below the preceding checkpoint. A concurrent unrelated `nanoid` dependency update changed the final workspace build to 5,094,522 bytes; `npm run madge` could not run because `madge` is not installed; manual settings validation remains pending | +| 2026-08-09 | Reviewed and retained `reEnableAutosave` | An attempted removal was reversed after confirming that this is not migration code: it resets the session-scoped temporary disable/enable autosave commands when the plugin starts. Restored `LoadSettingsOptions`, startup `{ reEnableAutosave: true }`, and the post-load in-memory assignment exactly as extracted. No autosave behavior change remains | Repository search confirms the complete option flow is restored; `npm run build`, `npm run lib`, targeted manager ESLint, and scoped `git diff --check` passed; the existing 34 circular dependency warnings are unchanged; `main.ts` is again 1,563 lines and the bundle is again 5,094,522 bytes; manual settings validation remains pending | +| 2026-08-09 | Moved temporary autosave enablement out of persisted settings | A full runtime and history review confirmed that `settings.autosave` had no settings UI and served only as the global gate for the temporary enable/disable commands. Replaced it with documented plugin-instance state initialized to enabled, updated commands and view scheduling to use that state, removed `autosave` from `ExcalidrawSettings` and `DEFAULT_SETTINGS`, and removed `LoadSettingsOptions`/`reEnableAutosave`. Desktop and mobile interval settings remain unchanged. Existing persisted `autosave` keys are intentionally left inert under the no-sanitizer policy | Repository search confirms there are no remaining supported-setting or runtime references to `settings.autosave`; the settings interface and defaults contain the same 182 keys; `npm run build`, `npm run lib`, and scoped `git diff --check` passed with the existing 34 circular dependency warnings. Broad lint reports only the existing backlog and no diagnostics on changed lines; bundle size decreased by 151 bytes to 5,094,371 bytes; manual session-command validation remains pending | ## Executive recommendation diff --git a/src/core/main.ts b/src/core/main.ts index 40d9e032..55ebd935 100644 --- a/src/core/main.ts +++ b/src/core/main.ts @@ -39,7 +39,6 @@ import { } from "../constants/constants"; import { ExcalidrawSettings, - DEFAULT_SETTINGS, ExcalidrawSettingTab, } from "./settings"; import { ExcalidrawAutomate } from "../shared/ExcalidrawAutomate"; @@ -75,7 +74,7 @@ import { import { FieldSuggester } from "../shared/Suggesters/FieldSuggester"; import { ReleaseNotes } from "../shared/Dialogs/ReleaseNotes"; import { DeviceType, Packages } from "../types/types"; -import { PaneTarget, PreviewImageType } from "../types/utilTypes"; +import { PaneTarget } from "../types/utilTypes"; import { emulateCTRLClickForLinks, linkClickModifierType, @@ -111,10 +110,6 @@ import { getHighlightColor } from "src/utils/dynamicStyling"; import { InlineLinkSuggester } from "src/shared/Suggesters/InlineLinkSuggester"; import { KeyBlocker } from "src/types/excalidrawAutomateTypes"; import { UIMode } from "src/shared/Dialogs/UIModeSettingComponent"; -import { - decryptPersistedAPIKeys, - encryptPersistedAPIKeys, -} from "src/utils/settingsKeyObfuscation"; import { hideElement, setButtonBgColor } from "src/utils/styleUtils"; import { installButton } from "src/utils/scriptLibraryUtils"; import { isInstanceOfHTMLStyleElement } from "src/utils/typechecks"; @@ -122,6 +117,7 @@ import { insertLaTeXToView } from "src/utils/excalidrawViewHelpers"; import type { MarkdownImageData } from "src/types/markdownImageTypes"; import { StencilLibraryManager } from "./managers/StencilLibraryManager"; import type { StencilLibraryData } from "src/types/stencilLibraryTypes"; +import { PluginSettingsManager } from "./managers/PluginSettingsManager"; declare const PLUGIN_VERSION: string; declare const INITIAL_TIMESTAMP: number; @@ -144,9 +140,6 @@ const PHONE_FOOTER_SAFE_AREA_CSS = ` } `; -type PersistedExcalidrawSettings = Partial & - Record; - /** * Compatibility labels consumed by upstream Excalidraw via ExcalidrawPlugin.getLabel(). * Keep these keys present in `en.ts`, and keep maintained locales in sync. @@ -174,12 +167,15 @@ export default class ExcalidrawPlugin extends Plugin { private monkeyPatchManager: MonkeyPatchManager; private commandManager: CommandManager; private eventManager: EventManager; + private settingsManager: PluginSettingsManager; public stencilLibraryManager: StencilLibraryManager; public eaInstances = new WeakArray(); public fourthFontLoaded: boolean = false; public excalidrawConfig: ExcalidrawConfig; public excalidrawFileModes: { [file: string]: string } = {}; public declare settings: ExcalidrawSettings; + /** Session-scoped autosave gate controlled by the temporary commands. */ + public autosaveEnabled: boolean = true; public activeExcalidrawView: ExcalidrawView = null; public lastActiveExcalidrawFilePath: string = null; public lastActiveExcalidrawLeafID: string = null; @@ -237,6 +233,7 @@ export default class ExcalidrawPlugin extends Plugin { //isExcalidraw function is used already is already used by MarkdownPostProcessor in onLoad before onLayoutReady this.fileManager = new PluginFileManager(this); + this.settingsManager = new PluginSettingsManager(this); setExcalidrawPlugin(this); /*if((process.env.NODE_ENV === 'development')) { @@ -399,7 +396,7 @@ export default class ExcalidrawPlugin extends Plugin { ); try { - void this.loadSettings({ reEnableAutosave: true }).then(() => + void this.loadSettings().then(() => this.onloadCheckForOnceOffSettingsUpdates(), ); } catch (e) { @@ -1191,134 +1188,19 @@ export default class ExcalidrawPlugin extends Plugin { terminateCompressionWorker(); } - public async loadSettings( - opts: { reEnableAutosave?: boolean } = { reEnableAutosave: false }, - ) { - if (typeof opts.reEnableAutosave === "undefined") { - opts.reEnableAutosave = false; - } - const persistedSettings = ((await this.loadData()) ?? - {}) as PersistedExcalidrawSettings; - const decryptedSettings = decryptPersistedAPIKeys(persistedSettings); - let didSettingsMigration = false; - this.settings = Object.assign({}, DEFAULT_SETTINGS, decryptedSettings); - if (typeof decryptedSettings.libraryStorageMode === "undefined") { - const legacyLibrary: unknown = - typeof decryptedSettings.library === "string" && - decryptedSettings.library !== "" && - decryptedSettings.library !== "deprecated" - ? JSON_parse(decryptedSettings.library) - : decryptedSettings.library2; - const legacyLibraryRecord = - typeof legacyLibrary === "object" && legacyLibrary !== null - ? (legacyLibrary as Record) - : null; - const hasLegacyItems = Boolean( - (Array.isArray(legacyLibraryRecord?.library) && - legacyLibraryRecord.library.length) || - (Array.isArray(legacyLibraryRecord?.libraryItems) && - legacyLibraryRecord.libraryItems.length), - ); - this.settings.libraryStorageMode = hasLegacyItems ? "data-json" : "vault"; - this.settings.libraryMigrationStatus = hasLegacyItems - ? "pending" - : "not-required"; - didSettingsMigration = true; - } - const savedMarkdownImageSettings = decryptedSettings.markdownImageSettings; - if (!savedMarkdownImageSettings) { - this.settings.markdownImageSettings = { - defaults: { - ...DEFAULT_SETTINGS.markdownImageSettings.defaults, - width: this.settings.mdSVGwidth, - fontFamily: this.settings.mdFont, - fontColor: this.settings.mdFontColor ?? "#000000", - border: { - enabled: false, - color: this.settings.mdBorderColor, - }, - css: "", - transclusion: { - ...DEFAULT_SETTINGS.markdownImageSettings.defaults.transclusion, - border: { - ...DEFAULT_SETTINGS.markdownImageSettings.defaults.transclusion - .border, - }, - }, - }, - }; - didSettingsMigration = true; - } else { - this.settings.markdownImageSettings = { - defaults: { - ...DEFAULT_SETTINGS.markdownImageSettings.defaults, - ...savedMarkdownImageSettings.defaults, - border: { - ...DEFAULT_SETTINGS.markdownImageSettings.defaults.border, - ...savedMarkdownImageSettings.defaults?.border, - }, - transclusion: { - ...DEFAULT_SETTINGS.markdownImageSettings.defaults.transclusion, - ...savedMarkdownImageSettings.defaults?.transclusion, - border: { - ...DEFAULT_SETTINGS.markdownImageSettings.defaults.transclusion - .border, - ...savedMarkdownImageSettings.defaults?.transclusion?.border, - }, - }, - }, - }; - } - const markdownImageDefaults = this.settings.markdownImageSettings - .defaults as unknown as Record; - if ("theme" in markdownImageDefaults) { - delete markdownImageDefaults.theme; - didSettingsMigration = true; - } - const settingsRecord = this.settings as unknown as Record; - if ( - typeof settingsRecord.iframelyAllowed === "boolean" && - typeof this.settings.oEmbedAllowed !== "boolean" - ) { - this.settings.oEmbedAllowed = settingsRecord.iframelyAllowed; - didSettingsMigration = true; - } - if ("iframelyAllowed" in settingsRecord) { - delete settingsRecord.iframelyAllowed; - didSettingsMigration = true; - } - if (!this.settings.previewImageType) { - //migration 1.9.13 - if (typeof this.settings.displaySVGInPreview === "undefined") { - this.settings.previewImageType = PreviewImageType.SVGIMG; - } else { - this.settings.previewImageType = this.settings.displaySVGInPreview - ? PreviewImageType.SVGIMG - : PreviewImageType.PNG; - } - } - const encryptedPersistedSettings = encryptPersistedAPIKeys( - this.settings as PersistedExcalidrawSettings, - ); - const shouldPersistEncryptedSettings = - JSON.stringify(encryptedPersistedSettings) !== - JSON.stringify(persistedSettings); - if (didSettingsMigration || shouldPersistEncryptedSettings) { - await this.saveData(encryptedPersistedSettings); - } - if (opts.reEnableAutosave) { - this.settings.autosave = true; - } + /** + * Loads settings through the plugin-owned settings manager. + */ + public async loadSettings(): Promise { + await this.settingsManager.loadSettings(); } - async saveSettings() { - await this.saveData( - encryptPersistedAPIKeys( - this.settings as PersistedExcalidrawSettings, - ), - ); + /** Persists the current settings through the plugin-owned settings manager. */ + async saveSettings(): Promise { + await this.settingsManager.saveSettings(); } + /** Reloads externally changed settings and invalidates cached libraries. */ async onExternalSettingsChange() { await this.loadSettings(); this.stencilLibraryManager?.invalidate(); diff --git a/src/core/managers/CommandManager.ts b/src/core/managers/CommandManager.ts index 9ec837a2..81e91da7 100644 --- a/src/core/managers/CommandManager.ts +++ b/src/core/managers/CommandManager.ts @@ -485,13 +485,13 @@ export class CommandManager { id: "excalidraw-disable-autosave", name: t("TEMPORARY_DISABLE_AUTOSAVE"), checkCallback: (checking) => { - if (!this.settings.autosave) { + if (!this.plugin.autosaveEnabled) { return false; } //already disabled if (checking) { return true; } - this.settings.autosave = false; + this.plugin.autosaveEnabled = false; return true; }, }); @@ -500,13 +500,13 @@ export class CommandManager { id: "excalidraw-enable-autosave", name: t("TEMPORARY_ENABLE_AUTOSAVE"), checkCallback: (checking) => { - if (this.settings.autosave) { + if (this.plugin.autosaveEnabled) { return false; } //already enabled if (checking) { return true; } - this.settings.autosave = true; + this.plugin.autosaveEnabled = true; return true; }, }); diff --git a/src/core/managers/PluginSettingsManager.ts b/src/core/managers/PluginSettingsManager.ts new file mode 100644 index 00000000..fca932c8 --- /dev/null +++ b/src/core/managers/PluginSettingsManager.ts @@ -0,0 +1,165 @@ +import { JSON_parse } from "src/constants/constants"; +import { + DEFAULT_SETTINGS, + type ExcalidrawSettings, +} from "src/core/settings"; +import { PreviewImageType } from "src/types/utilTypes"; +import { + decryptPersistedAPIKeys, + encryptPersistedAPIKeys, +} from "src/utils/settingsKeyObfuscation"; + +type PersistedExcalidrawSettings = Partial & + Record; + +interface PluginSettingsHost { + settings: ExcalidrawSettings; + loadData(): Promise; + saveData(data: unknown): Promise; +} + +/** + * Owns plugin settings persistence, default assembly, and compatibility + * migrations while leaving startup readiness and settings UI registration to + * the plugin lifecycle. + */ +export class PluginSettingsManager { + public constructor(private readonly host: PluginSettingsHost) {} + + /** + * Loads persisted settings, applies defaults and migrations, and decrypts + * protected API-key fields. + * + * @remarks Unknown persisted properties are intentionally retained. + */ + public async loadSettings(): Promise { + const persistedSettings = ((await this.host.loadData()) ?? + {}) as PersistedExcalidrawSettings; + const decryptedSettings = decryptPersistedAPIKeys(persistedSettings); + let didSettingsMigration = false; + this.host.settings = Object.assign( + {}, + DEFAULT_SETTINGS, + decryptedSettings, + ); + if (typeof decryptedSettings.libraryStorageMode === "undefined") { + const legacyLibrary: unknown = + typeof decryptedSettings.library === "string" && + decryptedSettings.library !== "" && + decryptedSettings.library !== "deprecated" + ? JSON_parse(decryptedSettings.library) + : decryptedSettings.library2; + const legacyLibraryRecord = + typeof legacyLibrary === "object" && legacyLibrary !== null + ? (legacyLibrary as Record) + : null; + const hasLegacyItems = Boolean( + (Array.isArray(legacyLibraryRecord?.library) && + legacyLibraryRecord.library.length) || + (Array.isArray(legacyLibraryRecord?.libraryItems) && + legacyLibraryRecord.libraryItems.length), + ); + this.host.settings.libraryStorageMode = hasLegacyItems + ? "data-json" + : "vault"; + this.host.settings.libraryMigrationStatus = hasLegacyItems + ? "pending" + : "not-required"; + didSettingsMigration = true; + } + const savedMarkdownImageSettings = decryptedSettings.markdownImageSettings; + if (!savedMarkdownImageSettings) { + this.host.settings.markdownImageSettings = { + defaults: { + ...DEFAULT_SETTINGS.markdownImageSettings.defaults, + width: this.host.settings.mdSVGwidth, + fontFamily: this.host.settings.mdFont, + fontColor: this.host.settings.mdFontColor ?? "#000000", + border: { + enabled: false, + color: this.host.settings.mdBorderColor, + }, + css: "", + transclusion: { + ...DEFAULT_SETTINGS.markdownImageSettings.defaults.transclusion, + border: { + ...DEFAULT_SETTINGS.markdownImageSettings.defaults.transclusion + .border, + }, + }, + }, + }; + didSettingsMigration = true; + } else { + this.host.settings.markdownImageSettings = { + defaults: { + ...DEFAULT_SETTINGS.markdownImageSettings.defaults, + ...savedMarkdownImageSettings.defaults, + border: { + ...DEFAULT_SETTINGS.markdownImageSettings.defaults.border, + ...savedMarkdownImageSettings.defaults?.border, + }, + transclusion: { + ...DEFAULT_SETTINGS.markdownImageSettings.defaults.transclusion, + ...savedMarkdownImageSettings.defaults?.transclusion, + border: { + ...DEFAULT_SETTINGS.markdownImageSettings.defaults.transclusion + .border, + ...savedMarkdownImageSettings.defaults?.transclusion?.border, + }, + }, + }, + }; + } + const markdownImageDefaults = this.host.settings.markdownImageSettings + .defaults as unknown as Record; + if ("theme" in markdownImageDefaults) { + delete markdownImageDefaults.theme; + didSettingsMigration = true; + } + const settingsRecord = this.host.settings as unknown as Record< + string, + unknown + >; + if ( + typeof settingsRecord.iframelyAllowed === "boolean" && + typeof this.host.settings.oEmbedAllowed !== "boolean" + ) { + this.host.settings.oEmbedAllowed = settingsRecord.iframelyAllowed; + didSettingsMigration = true; + } + if ("iframelyAllowed" in settingsRecord) { + delete settingsRecord.iframelyAllowed; + didSettingsMigration = true; + } + if (!this.host.settings.previewImageType) { + // Migration introduced in 1.9.13. + if (typeof this.host.settings.displaySVGInPreview === "undefined") { + this.host.settings.previewImageType = PreviewImageType.SVGIMG; + } else { + this.host.settings.previewImageType = this.host.settings + .displaySVGInPreview + ? PreviewImageType.SVGIMG + : PreviewImageType.PNG; + } + } + const encryptedPersistedSettings = encryptPersistedAPIKeys( + this.host.settings as PersistedExcalidrawSettings, + ); + const shouldPersistEncryptedSettings = + JSON.stringify(encryptedPersistedSettings) !== + JSON.stringify(persistedSettings); + if (didSettingsMigration || shouldPersistEncryptedSettings) { + await this.host.saveData(encryptedPersistedSettings); + } + } + + /** Encrypts protected fields and persists the current settings object. */ + public async saveSettings(): Promise { + await this.host.saveData( + encryptPersistedAPIKeys( + this.host.settings as PersistedExcalidrawSettings, + ), + ); + } +} diff --git a/src/core/settings.ts b/src/core/settings.ts index 06b03ab0..3825798a 100644 --- a/src/core/settings.ts +++ b/src/core/settings.ts @@ -111,7 +111,6 @@ export interface ExcalidrawSettings { compress: boolean; decompressForMDView: boolean; onceOffCompressFlagReset: boolean; //used to reset compress to true in 2.2.0 - autosave: boolean; autosaveIntervalDesktop: number; autosaveIntervalMobile: number; drawingFilenamePrefix: string; @@ -558,7 +557,6 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = { compress: true, decompressForMDView: false, onceOffCompressFlagReset: false, - autosave: true, autosaveIntervalDesktop: 60000, autosaveIntervalMobile: 30000, drawingFilenamePrefix: "Drawing ", diff --git a/src/shared/Dialogs/Messages.ts b/src/shared/Dialogs/Messages.ts index 26c42ea6..76094df4 100644 --- a/src/shared/Dialogs/Messages.ts +++ b/src/shared/Dialogs/Messages.ts @@ -20,6 +20,7 @@ I build this plugin as a labor of love. Curious about the philosophy behind it? "2.27.0": ` ## Maintenance - Retired obsolete pre-profile ExcalidrawAI settings migration and fallback handling. Current provider profiles and model configurations are unchanged. +- Temporary autosave disablement is now session-scoped and no longer stored with plugin settings. `, "2.26.4": ` diff --git a/src/view/ExcalidrawView.ts b/src/view/ExcalidrawView.ts index 447d849e..a302701b 100644 --- a/src/view/ExcalidrawView.ts +++ b/src/view/ExcalidrawView.ts @@ -2740,7 +2740,7 @@ export default class ExcalidrawView this.refreshCanvasOffset(); if ( this.isDirty() && - this.plugin.settings.autosave && + this.plugin.autosaveEnabled && !this.semaphores.forceSaving && !this.semaphores.autosaving && !this.semaphores.embeddableIsEditingSelf && @@ -2761,7 +2761,7 @@ export default class ExcalidrawView timer, this.plugin.activeExcalidrawView === this && this.semaphores.dirty && - this.plugin.settings.autosave + this.plugin.autosaveEnabled ? 1000 //try again in 1 second : this.autosaveInterval, ); From fd9090562eb50664dfa53203d9b16a6e3d786f76 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sun, 9 Aug 2026 09:55:29 +0200 Subject: [PATCH 2/6] updated nanoid to patched version --- package-lock.json | 8 ++++---- package.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 89294aa3..6fb7e7ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "gl-matrix": "^3.4.3", "lz-string": "^1.5.0", "monkey-around": "^2.3.0", - "nanoid": "^5.1.11", + "nanoid": "^5.1.16", "opentype.js": "^1.3.4", "pako": "^2.1.0", "points-on-path": "^0.2.1", @@ -10239,9 +10239,9 @@ } }, "node_modules/nanoid": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz", - "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==", + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", "funding": [ { "type": "github", diff --git a/package.json b/package.json index bbdb8cb1..54c96515 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "gl-matrix": "^3.4.3", "lz-string": "^1.5.0", "monkey-around": "^2.3.0", - "nanoid": "^5.1.11", + "nanoid": "^5.1.16", "opentype.js": "^1.3.4", "pako": "^2.1.0", "points-on-path": "^0.2.1", @@ -96,7 +96,7 @@ "lodash": "^4.18.1", "lodash-es": "^4.18.1", "serialize-javascript": "^7.0.5", - "nanoid": "^5.1.11", + "nanoid": "^5.1.16", "dompurify": "^3.4.12", "fast-uri": "^3.1.4", "immutable": "^4.3.9", From e0c4eecef4f3fcaec95ae0266cf2492b7c7f3add Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sun, 9 Aug 2026 10:16:39 +0200 Subject: [PATCH 3/6] refactor: extract footer safe area manager --- RefactorPlan.md | 2 + src/core/main.ts | 60 ++------------ src/core/managers/FooterSafeAreaManager.ts | 92 ++++++++++++++++++++++ 3 files changed, 101 insertions(+), 53 deletions(-) create mode 100644 src/core/managers/FooterSafeAreaManager.ts diff --git a/RefactorPlan.md b/RefactorPlan.md index 9764a7f7..fcd2e51a 100644 --- a/RefactorPlan.md +++ b/RefactorPlan.md @@ -21,6 +21,7 @@ validated, and what remains uncertain. | Assessment and baseline design | Complete | Initial architecture, risks, sequencing, and validation matrix documented | | Retire legacy AI settings and fallbacks | Complete | Removed the retired migration, schema/default fields, GPT reset, and AI runtime fallbacks without filtering unknown persisted keys; manual testing found no issues | | Extract settings implementation service | Implemented; awaiting manual validation | `PluginSettingsManager` now owns persistence, default assembly, remaining migrations, and API-key obfuscation; plugin methods and startup readiness remain intact, while temporary autosave enablement is now explicit plugin-instance state rather than persisted settings state | +| Extract footer safe-area styling | Implemented; awaiting manual validation | `FooterSafeAreaManager` now owns device-specific stylesheet injection, open-document traversal, and unload cleanup; the plugin method remains as a settings-UI compatibility delegate | | Audit and consolidate duplicate logic | In progress | Consolidated `updateFrontmatterInString()`, `arrayToMap()`, `wrapTextAtCharLength()`, `getLinkParts()`/`LinkParts`, `getBinaryFileFromDataURL()`, `svgToBase64()`, `getFontDataURL()`, `cropCanvas()`, `getImageSize()`, `promiseTry()`, `isVersionNewerThanOther()`, `repositionElementsToCursor()`, the internal `cloneElement()`, and `getBoundTextElementId()`; continue one independently testable helper family at a time | | All later phases | Planned | Begin only after the preceding checkpoint is validated | @@ -50,6 +51,7 @@ validated, and what remains uncertain. | 2026-08-08 | Extracted plugin settings implementation from `main.ts` | Added documented `PluginSettingsManager` with a narrow host contract to own load/save, default assembly, existing library/Markdown-image/oEmbed/preview migrations, and API-key obfuscation. At this extraction checkpoint, retained `plugin.settings`, all public plugin method signatures, startup autosave behavior, settings readiness, settings-tab timing, external-change library invalidation, persisted format, and unknown-key behavior. After delegate TSDoc, `main.ts` decreased from 1,676 to 1,563 lines | `npm run build` passed after each code change; `npm run lib` and a scoped `git diff --check` passed; the new manager and changed delegate lines have no ESLint diagnostics, while targeted lint still reports four unrelated existing `any` diagnostics in the startup-script block of `main.ts`; the initial extraction build was 5,094,122 bytes, 1,257 bytes below the preceding checkpoint. A concurrent unrelated `nanoid` dependency update changed the final workspace build to 5,094,522 bytes; `npm run madge` could not run because `madge` is not installed; manual settings validation remains pending | | 2026-08-09 | Reviewed and retained `reEnableAutosave` | An attempted removal was reversed after confirming that this is not migration code: it resets the session-scoped temporary disable/enable autosave commands when the plugin starts. Restored `LoadSettingsOptions`, startup `{ reEnableAutosave: true }`, and the post-load in-memory assignment exactly as extracted. No autosave behavior change remains | Repository search confirms the complete option flow is restored; `npm run build`, `npm run lib`, targeted manager ESLint, and scoped `git diff --check` passed; the existing 34 circular dependency warnings are unchanged; `main.ts` is again 1,563 lines and the bundle is again 5,094,522 bytes; manual settings validation remains pending | | 2026-08-09 | Moved temporary autosave enablement out of persisted settings | A full runtime and history review confirmed that `settings.autosave` had no settings UI and served only as the global gate for the temporary enable/disable commands. Replaced it with documented plugin-instance state initialized to enabled, updated commands and view scheduling to use that state, removed `autosave` from `ExcalidrawSettings` and `DEFAULT_SETTINGS`, and removed `LoadSettingsOptions`/`reEnableAutosave`. Desktop and mobile interval settings remain unchanged. Existing persisted `autosave` keys are intentionally left inert under the no-sanitizer policy | Repository search confirms there are no remaining supported-setting or runtime references to `settings.autosave`; the settings interface and defaults contain the same 182 keys; `npm run build`, `npm run lib`, and scoped `git diff --check` passed with the existing 34 circular dependency warnings. Broad lint reports only the existing backlog and no diagnostics on changed lines; bundle size decreased by 151 bytes to 5,094,371 bytes; manual session-command validation remains pending | +| 2026-08-09 | Extracted footer safe-area styling from `main.ts` | Added documented `FooterSafeAreaManager` with a narrow host contract to own phone/tablet CSS injection, exact device-aware open-document traversal, setting-driven removal, and unload cleanup. Kept `plugin.updateFooterSafeAreaPadding()` and its layout-ready and settings UI call sites unchanged as delegates. Font document traversal remains in `main.ts` until the separate font extraction | `npm run build`, targeted manager ESLint, and scoped `git diff --check` passed; `main.ts` decreased by 46 lines from 1,558 to 1,512; bundle size increased by 99 bytes to 5,094,470 bytes from manager/delegate overhead; `npm run madge` could not run because `madge` is not installed; phone/tablet manual validation remains pending | ## Executive recommendation diff --git a/src/core/main.ts b/src/core/main.ts index 55ebd935..c333ffd3 100644 --- a/src/core/main.ts +++ b/src/core/main.ts @@ -112,12 +112,12 @@ import { KeyBlocker } from "src/types/excalidrawAutomateTypes"; import { UIMode } from "src/shared/Dialogs/UIModeSettingComponent"; import { hideElement, setButtonBgColor } from "src/utils/styleUtils"; import { installButton } from "src/utils/scriptLibraryUtils"; -import { isInstanceOfHTMLStyleElement } from "src/utils/typechecks"; import { insertLaTeXToView } from "src/utils/excalidrawViewHelpers"; import type { MarkdownImageData } from "src/types/markdownImageTypes"; import { StencilLibraryManager } from "./managers/StencilLibraryManager"; import type { StencilLibraryData } from "src/types/stencilLibraryTypes"; import { PluginSettingsManager } from "./managers/PluginSettingsManager"; +import { FooterSafeAreaManager } from "./managers/FooterSafeAreaManager"; declare const PLUGIN_VERSION: string; declare const INITIAL_TIMESTAMP: number; @@ -133,13 +133,6 @@ type FileMasterInfo = { colorMapJSON?: string; }; -const PHONE_FOOTER_SAFE_AREA_STYLE_ID = "excalidraw-phone-footer-safe-area"; -const PHONE_FOOTER_SAFE_AREA_CSS = ` -.excalidraw .App-bottom-bar { - padding-bottom: 50px; -} -`; - /** * Compatibility labels consumed by upstream Excalidraw via ExcalidrawPlugin.getLabel(). * Keep these keys present in `en.ts`, and keep maintained locales in sync. @@ -168,6 +161,7 @@ export default class ExcalidrawPlugin extends Plugin { private commandManager: CommandManager; private eventManager: EventManager; private settingsManager: PluginSettingsManager; + private footerSafeAreaManager: FooterSafeAreaManager; public stencilLibraryManager: StencilLibraryManager; public eaInstances = new WeakArray(); public fourthFontLoaded: boolean = false; @@ -234,6 +228,7 @@ export default class ExcalidrawPlugin extends Plugin { //isExcalidraw function is used already is already used by MarkdownPostProcessor in onLoad before onLayoutReady this.fileManager = new PluginFileManager(this); this.settingsManager = new PluginSettingsManager(this); + this.footerSafeAreaManager = new FooterSafeAreaManager(this); setExcalidrawPlugin(this); /*if((process.env.NODE_ENV === 'development')) { @@ -743,50 +738,9 @@ export default class ExcalidrawPlugin extends Plugin { }); } - public updateFooterSafeAreaPadding() { - const documents = new Set([ - mainDocument, - ...this.getOpenObsidianDocuments(), - ]); - const shouldEnable = - (DEVICE.isPhone && this.settings?.phoneFooterSafeAreaPadding) || - (DEVICE.isTablet && this.settings?.tabletFooterSafeAreaPadding); - - documents.forEach((ownerDocument) => { - const existingStylesheet = ownerDocument.getElementById( - PHONE_FOOTER_SAFE_AREA_STYLE_ID, - ); - if (!shouldEnable) { - if (existingStylesheet) { - ownerDocument.head.removeChild(existingStylesheet); - } - return; - } - if (isInstanceOfHTMLStyleElement(existingStylesheet)) { - existingStylesheet.textContent = PHONE_FOOTER_SAFE_AREA_CSS; - return; - } - - const stylesheet = deliberateCreateElement(ownerDocument, "style"); - stylesheet.id = PHONE_FOOTER_SAFE_AREA_STYLE_ID; - stylesheet.textContent = PHONE_FOOTER_SAFE_AREA_CSS; - ownerDocument.head.appendChild(stylesheet); - }); - } - - private removePhoneFooterSafeAreaPadding() { - const documents = new Set([ - mainDocument, - ...this.getOpenObsidianDocuments(), - ]); - documents.forEach((ownerDocument) => { - const existingStylesheet = ownerDocument.getElementById( - PHONE_FOOTER_SAFE_AREA_STYLE_ID, - ); - if (existingStylesheet) { - ownerDocument.head.removeChild(existingStylesheet); - } - }); + /** Updates the optional mobile footer padding across open documents. */ + public updateFooterSafeAreaPadding(): void { + this.footerSafeAreaManager.updateFooterSafeAreaPadding(); } private getOpenObsidianDocuments(): Document[] { @@ -1137,7 +1091,7 @@ export default class ExcalidrawPlugin extends Plugin { this.stylesManager = null; this.removeFonts(); - this.removePhoneFooterSafeAreaPadding(); + this.footerSafeAreaManager.destroy(); this.eaInstances.forEach((ea) => ea?.destroy()); this.eaInstances.clear(); diff --git a/src/core/managers/FooterSafeAreaManager.ts b/src/core/managers/FooterSafeAreaManager.ts new file mode 100644 index 00000000..8a71194e --- /dev/null +++ b/src/core/managers/FooterSafeAreaManager.ts @@ -0,0 +1,92 @@ +import type { App } from "obsidian"; +import { DEVICE } from "src/constants/constants"; +import type { ExcalidrawSettings } from "src/core/settings"; +import { isInstanceOfHTMLStyleElement } from "src/utils/typechecks"; + +declare const mainDocument: Document; +declare const deliberateCreateElement: ( + document: Document, + tagName: string, +) => HTMLStyleElement; + +const FOOTER_SAFE_AREA_STYLE_ID = "excalidraw-phone-footer-safe-area"; +const FOOTER_SAFE_AREA_CSS = ` +.excalidraw .App-bottom-bar { + padding-bottom: 50px; +} +`; + +interface FooterSafeAreaHost { + app: App; + settings: ExcalidrawSettings; +} + +/** + * Owns the optional phone and tablet footer-padding stylesheet across Obsidian + * documents, including cleanup when the plugin unloads. + */ +export class FooterSafeAreaManager { + public constructor(private readonly host: FooterSafeAreaHost) {} + + /** Applies or removes footer padding according to the current device and settings. */ + public updateFooterSafeAreaPadding(): void { + const documents = new Set([ + mainDocument, + ...this.getOpenObsidianDocuments(), + ]); + const shouldEnable = + (DEVICE.isPhone && this.host.settings?.phoneFooterSafeAreaPadding) || + (DEVICE.isTablet && this.host.settings?.tabletFooterSafeAreaPadding); + + documents.forEach((ownerDocument) => { + const existingStylesheet = ownerDocument.getElementById( + FOOTER_SAFE_AREA_STYLE_ID, + ); + if (!shouldEnable) { + if (existingStylesheet) { + ownerDocument.head.removeChild(existingStylesheet); + } + return; + } + if (isInstanceOfHTMLStyleElement(existingStylesheet)) { + existingStylesheet.textContent = FOOTER_SAFE_AREA_CSS; + return; + } + + const stylesheet = deliberateCreateElement(ownerDocument, "style"); + stylesheet.id = FOOTER_SAFE_AREA_STYLE_ID; + stylesheet.textContent = FOOTER_SAFE_AREA_CSS; + ownerDocument.head.appendChild(stylesheet); + }); + } + + /** Removes every footer-padding stylesheet owned by the plugin. */ + public destroy(): void { + const documents = new Set([ + mainDocument, + ...this.getOpenObsidianDocuments(), + ]); + documents.forEach((ownerDocument) => { + const existingStylesheet = ownerDocument.getElementById( + FOOTER_SAFE_AREA_STYLE_ID, + ); + if (existingStylesheet) { + ownerDocument.head.removeChild(existingStylesheet); + } + }); + } + + private getOpenObsidianDocuments(): Document[] { + const visitedDocuments = new Set(); + this.host.app.workspace.iterateAllLeaves((leaf) => { + const ownerDocument = DEVICE.isMobile + ? mainDocument + : leaf.view.containerEl.ownerDocument; + if (!ownerDocument || visitedDocuments.has(ownerDocument)) { + return; + } + visitedDocuments.add(ownerDocument); + }); + return Array.from(visitedDocuments); + } +} From 5843e88e6fff4cfc99f73df4ce58c2e3fbbb8b2b Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sun, 9 Aug 2026 10:29:01 +0200 Subject: [PATCH 4/6] refactor: extract font manager --- RefactorPlan.md | 2 + src/core/main.ts | 174 ++++---------------------- src/core/managers/FontManager.ts | 201 +++++++++++++++++++++++++++++++ 3 files changed, 225 insertions(+), 152 deletions(-) create mode 100644 src/core/managers/FontManager.ts diff --git a/RefactorPlan.md b/RefactorPlan.md index fcd2e51a..7b73a547 100644 --- a/RefactorPlan.md +++ b/RefactorPlan.md @@ -22,6 +22,7 @@ validated, and what remains uncertain. | Retire legacy AI settings and fallbacks | Complete | Removed the retired migration, schema/default fields, GPT reset, and AI runtime fallbacks without filtering unknown persisted keys; manual testing found no issues | | Extract settings implementation service | Implemented; awaiting manual validation | `PluginSettingsManager` now owns persistence, default assembly, remaining migrations, and API-key obfuscation; plugin methods and startup readiness remain intact, while temporary autosave enablement is now explicit plugin-instance state rather than persisted settings state | | Extract footer safe-area styling | Implemented; awaiting manual validation | `FooterSafeAreaManager` now owns device-specific stylesheet injection, open-document traversal, and unload cleanup; the plugin method remains as a settings-UI compatibility delegate | +| Extract font management | Implemented; awaiting manual validation | `FontManager` now owns CJK discovery/loading, custom-font registration, document stylesheets, readiness, and cleanup; plugin methods and the externally read `fourthFontLoaded` field remain intact | | Audit and consolidate duplicate logic | In progress | Consolidated `updateFrontmatterInString()`, `arrayToMap()`, `wrapTextAtCharLength()`, `getLinkParts()`/`LinkParts`, `getBinaryFileFromDataURL()`, `svgToBase64()`, `getFontDataURL()`, `cropCanvas()`, `getImageSize()`, `promiseTry()`, `isVersionNewerThanOther()`, `repositionElementsToCursor()`, the internal `cloneElement()`, and `getBoundTextElementId()`; continue one independently testable helper family at a time | | All later phases | Planned | Begin only after the preceding checkpoint is validated | @@ -52,6 +53,7 @@ validated, and what remains uncertain. | 2026-08-09 | Reviewed and retained `reEnableAutosave` | An attempted removal was reversed after confirming that this is not migration code: it resets the session-scoped temporary disable/enable autosave commands when the plugin starts. Restored `LoadSettingsOptions`, startup `{ reEnableAutosave: true }`, and the post-load in-memory assignment exactly as extracted. No autosave behavior change remains | Repository search confirms the complete option flow is restored; `npm run build`, `npm run lib`, targeted manager ESLint, and scoped `git diff --check` passed; the existing 34 circular dependency warnings are unchanged; `main.ts` is again 1,563 lines and the bundle is again 5,094,522 bytes; manual settings validation remains pending | | 2026-08-09 | Moved temporary autosave enablement out of persisted settings | A full runtime and history review confirmed that `settings.autosave` had no settings UI and served only as the global gate for the temporary enable/disable commands. Replaced it with documented plugin-instance state initialized to enabled, updated commands and view scheduling to use that state, removed `autosave` from `ExcalidrawSettings` and `DEFAULT_SETTINGS`, and removed `LoadSettingsOptions`/`reEnableAutosave`. Desktop and mobile interval settings remain unchanged. Existing persisted `autosave` keys are intentionally left inert under the no-sanitizer policy | Repository search confirms there are no remaining supported-setting or runtime references to `settings.autosave`; the settings interface and defaults contain the same 182 keys; `npm run build`, `npm run lib`, and scoped `git diff --check` passed with the existing 34 circular dependency warnings. Broad lint reports only the existing backlog and no diagnostics on changed lines; bundle size decreased by 151 bytes to 5,094,371 bytes; manual session-command validation remains pending | | 2026-08-09 | Extracted footer safe-area styling from `main.ts` | Added documented `FooterSafeAreaManager` with a narrow host contract to own phone/tablet CSS injection, exact device-aware open-document traversal, setting-driven removal, and unload cleanup. Kept `plugin.updateFooterSafeAreaPadding()` and its layout-ready and settings UI call sites unchanged as delegates. Font document traversal remains in `main.ts` until the separate font extraction | `npm run build`, targeted manager ESLint, and scoped `git diff --check` passed; `main.ts` decreased by 46 lines from 1,558 to 1,512; bundle size increased by 99 bytes to 5,094,470 bytes from manager/delegate overhead; `npm run madge` could not run because `madge` is not installed; phone/tablet manual validation remains pending | +| 2026-08-09 | Extracted font management from `main.ts` | Added documented `FontManager` to own the existing CJK asset cache, vault reads, CJK/custom stylesheet lifecycle, custom font metrics and package registration, readiness state, and device-aware document traversal. Preserved every plugin-facing font method as a delegate, retained `plugin.fourthFontLoaded` for view compatibility, kept the initial readiness value and 100ms timer unchanged, and injected lazy package-map access without changing `PackageManager` construction or ownership | `npm run build`, `npm run lib`, targeted manager ESLint, and scoped `git diff --check` passed; `main.ts` decreased by 130 lines from 1,512 to 1,382; bundle size increased by 379 bytes to 5,094,849 bytes from manager/facade overhead; `npm run madge` could not run because `madge` is not installed; desktop, mobile, CJK, custom-font, and popout manual validation remains pending | ## Executive recommendation diff --git a/src/core/main.ts b/src/core/main.ts index c333ffd3..84db237f 100644 --- a/src/core/main.ts +++ b/src/core/main.ts @@ -33,8 +33,6 @@ import { LOCALE, setExcalidrawPlugin, DEVICE, - FONTS_STYLE_ID, - CJK_STYLE_ID, setRootElementSize, } from "../constants/constants"; import { @@ -51,11 +49,9 @@ import { getNewUniqueFilepath, } from "../utils/fileUtils"; import { - getFontDataURL, errorlog, isVersionNewerThanOther, versionUpdateCheckTimer, - getFontMetrics, calculateUIModeValue, } from "../utils/utils"; import { @@ -91,7 +87,6 @@ import { terminateCompressionWorker, } from "../shared/Workers/compression-worker"; import { WeakArray } from "../shared/WeakArray"; -import { getCJKDataURLs } from "../utils/CJKLoader"; import { ExcalidrawLoading, switchToExcalidraw, @@ -118,11 +113,11 @@ import { StencilLibraryManager } from "./managers/StencilLibraryManager"; import type { StencilLibraryData } from "src/types/stencilLibraryTypes"; import { PluginSettingsManager } from "./managers/PluginSettingsManager"; import { FooterSafeAreaManager } from "./managers/FooterSafeAreaManager"; +import { FontManager } from "./managers/FontManager"; declare const PLUGIN_VERSION: string; declare const INITIAL_TIMESTAMP: number; declare const mainDocument: Document; -declare const deliberateCreateElement: (document: Document, tagName: string) => HTMLStyleElement; type FileMasterInfo = { isHyperLink: boolean; @@ -162,6 +157,7 @@ export default class ExcalidrawPlugin extends Plugin { private eventManager: EventManager; private settingsManager: PluginSettingsManager; private footerSafeAreaManager: FooterSafeAreaManager; + private fontManager: FontManager; public stencilLibraryManager: StencilLibraryManager; public eaInstances = new WeakArray(); public fourthFontLoaded: boolean = false; @@ -195,9 +191,7 @@ export default class ExcalidrawPlugin extends Plugin { public forceToOpenInMarkdownFilepath: string = null; //private slob:string; public loadTimestamp: number; - private isLocalCJKFontAvailabe: boolean = undefined; public isReady = false; - private fontsReady = true; //setting this to true allows for a race condition during startup loading fonts and rendering Excalidraw private startupAnalytics: string[] = []; private lastLogTimestamp: number; private settingsReady: boolean = false; @@ -229,6 +223,9 @@ export default class ExcalidrawPlugin extends Plugin { this.fileManager = new PluginFileManager(this); this.settingsManager = new PluginSettingsManager(this); this.footerSafeAreaManager = new FooterSafeAreaManager(this); + this.fontManager = new FontManager(this, () => + this.packageManager.getPackageMap(), + ); setExcalidrawPlugin(this); /*if((process.env.NODE_ENV === 'development')) { @@ -334,38 +331,16 @@ export default class ExcalidrawPlugin extends Plugin { ); } - 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, - }; + /** Returns the configured CJK ranges when local font assets are available. */ + public getCJKFontSettings(): { c: boolean; j: boolean; k: boolean } { + return this.fontManager.getCJKFontSettings(); } + /** Reads a configured CJK font file from the vault. */ public async loadFontFromFile( fontName: string, ): Promise { - const assetsFoler = this.settings.fontAssetsPath; - - if (!this.isLocalCJKFontAvailabe) { - return; - } - const file = this.app.vault.getFileByPath( - normalizePath(`${assetsFoler}/${fontName}`), - ); - if (!file || !(file instanceof TFile)) { - return; - } - return await this.app.vault.readBinary(file); + return await this.fontManager.loadFontFromFile(fontName); } async onload() { @@ -605,7 +580,7 @@ export default class ExcalidrawPlugin extends Plugin { public async awaitInit() { let counter = 0; - while ((!this.isReady || !this.fontsReady) && counter++ < 200) { + while ((!this.isReady || !this.fontManager.isReady) && counter++ < 200) { await sleep(50); } } @@ -631,111 +606,23 @@ export default class ExcalidrawPlugin extends Plugin { ); } - public async 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 }) => { - if (!fontMetrics) { - return; - } - excalidrawLib.registerLocalFont( - { metrics: fontMetrics }, - 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) { - window.setTimeout(() => { - this.fourthFontLoaded = true; - }, 100); - } - this.fontsReady = true; + /** Initializes configured CJK and custom fonts across open documents. */ + public async initializeFonts(): Promise { + await this.fontManager.initializeFonts(); } + /** Adds or replaces a plugin-owned font stylesheet. */ public async addFonts( declarations: string[], - ownerDocument: Document = mainDocument, - styleId: string = FONTS_STYLE_ID, - ) { - // replace the old local font