Skip to content

Commit 5843e88

Browse files
committed
refactor: extract font manager
1 parent e0c4eec commit 5843e88

3 files changed

Lines changed: 225 additions & 152 deletions

File tree

RefactorPlan.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ validated, and what remains uncertain.
2222
| 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 |
2323
| 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 |
2424
| 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 |
25+
| 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 |
2526
| 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 |
2627
| All later phases | Planned | Begin only after the preceding checkpoint is validated |
2728

@@ -52,6 +53,7 @@ validated, and what remains uncertain.
5253
| 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 |
5354
| 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 |
5455
| 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 |
56+
| 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 |
5557

5658
## Executive recommendation
5759

src/core/main.ts

Lines changed: 22 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ import {
3333
LOCALE,
3434
setExcalidrawPlugin,
3535
DEVICE,
36-
FONTS_STYLE_ID,
37-
CJK_STYLE_ID,
3836
setRootElementSize,
3937
} from "../constants/constants";
4038
import {
@@ -51,11 +49,9 @@ import {
5149
getNewUniqueFilepath,
5250
} from "../utils/fileUtils";
5351
import {
54-
getFontDataURL,
5552
errorlog,
5653
isVersionNewerThanOther,
5754
versionUpdateCheckTimer,
58-
getFontMetrics,
5955
calculateUIModeValue,
6056
} from "../utils/utils";
6157
import {
@@ -91,7 +87,6 @@ import {
9187
terminateCompressionWorker,
9288
} from "../shared/Workers/compression-worker";
9389
import { WeakArray } from "../shared/WeakArray";
94-
import { getCJKDataURLs } from "../utils/CJKLoader";
9590
import {
9691
ExcalidrawLoading,
9792
switchToExcalidraw,
@@ -118,11 +113,11 @@ import { StencilLibraryManager } from "./managers/StencilLibraryManager";
118113
import type { StencilLibraryData } from "src/types/stencilLibraryTypes";
119114
import { PluginSettingsManager } from "./managers/PluginSettingsManager";
120115
import { FooterSafeAreaManager } from "./managers/FooterSafeAreaManager";
116+
import { FontManager } from "./managers/FontManager";
121117

122118
declare const PLUGIN_VERSION: string;
123119
declare const INITIAL_TIMESTAMP: number;
124120
declare const mainDocument: Document;
125-
declare const deliberateCreateElement: (document: Document, tagName: string) => HTMLStyleElement;
126121

127122
type FileMasterInfo = {
128123
isHyperLink: boolean;
@@ -162,6 +157,7 @@ export default class ExcalidrawPlugin extends Plugin {
162157
private eventManager: EventManager;
163158
private settingsManager: PluginSettingsManager;
164159
private footerSafeAreaManager: FooterSafeAreaManager;
160+
private fontManager: FontManager;
165161
public stencilLibraryManager: StencilLibraryManager;
166162
public eaInstances = new WeakArray<ExcalidrawAutomate>();
167163
public fourthFontLoaded: boolean = false;
@@ -195,9 +191,7 @@ export default class ExcalidrawPlugin extends Plugin {
195191
public forceToOpenInMarkdownFilepath: string = null;
196192
//private slob:string;
197193
public loadTimestamp: number;
198-
private isLocalCJKFontAvailabe: boolean = undefined;
199194
public isReady = false;
200-
private fontsReady = true; //setting this to true allows for a race condition during startup loading fonts and rendering Excalidraw
201195
private startupAnalytics: string[] = [];
202196
private lastLogTimestamp: number;
203197
private settingsReady: boolean = false;
@@ -229,6 +223,9 @@ export default class ExcalidrawPlugin extends Plugin {
229223
this.fileManager = new PluginFileManager(this);
230224
this.settingsManager = new PluginSettingsManager(this);
231225
this.footerSafeAreaManager = new FooterSafeAreaManager(this);
226+
this.fontManager = new FontManager(this, () =>
227+
this.packageManager.getPackageMap(),
228+
);
232229

233230
setExcalidrawPlugin(this);
234231
/*if((process.env.NODE_ENV === 'development')) {
@@ -334,38 +331,16 @@ export default class ExcalidrawPlugin extends Plugin {
334331
);
335332
}
336333

337-
public getCJKFontSettings() {
338-
const assetsFoler = this.settings.fontAssetsPath;
339-
if (typeof this.isLocalCJKFontAvailabe === "undefined") {
340-
this.isLocalCJKFontAvailabe = this.app.vault
341-
.getFiles()
342-
.some((f) => f.path.startsWith(assetsFoler));
343-
}
344-
if (!this.isLocalCJKFontAvailabe) {
345-
return { c: false, j: false, k: false };
346-
}
347-
return {
348-
c: this.settings.loadChineseFonts,
349-
j: this.settings.loadJapaneseFonts,
350-
k: this.settings.loadKoreanFonts,
351-
};
334+
/** Returns the configured CJK ranges when local font assets are available. */
335+
public getCJKFontSettings(): { c: boolean; j: boolean; k: boolean } {
336+
return this.fontManager.getCJKFontSettings();
352337
}
353338

339+
/** Reads a configured CJK font file from the vault. */
354340
public async loadFontFromFile(
355341
fontName: string,
356342
): Promise<ArrayBuffer | undefined> {
357-
const assetsFoler = this.settings.fontAssetsPath;
358-
359-
if (!this.isLocalCJKFontAvailabe) {
360-
return;
361-
}
362-
const file = this.app.vault.getFileByPath(
363-
normalizePath(`${assetsFoler}/${fontName}`),
364-
);
365-
if (!file || !(file instanceof TFile)) {
366-
return;
367-
}
368-
return await this.app.vault.readBinary(file);
343+
return await this.fontManager.loadFontFromFile(fontName);
369344
}
370345

371346
async onload() {
@@ -605,7 +580,7 @@ export default class ExcalidrawPlugin extends Plugin {
605580

606581
public async awaitInit() {
607582
let counter = 0;
608-
while ((!this.isReady || !this.fontsReady) && counter++ < 200) {
583+
while ((!this.isReady || !this.fontManager.isReady) && counter++ < 200) {
609584
await sleep(50);
610585
}
611586
}
@@ -631,135 +606,30 @@ export default class ExcalidrawPlugin extends Plugin {
631606
);
632607
}
633608

634-
public async initializeFonts() {
635-
const cjkFontDataURLs = await getCJKDataURLs(this);
636-
if (typeof cjkFontDataURLs === "boolean" && !cjkFontDataURLs) {
637-
new Notice(t("FONTS_LOAD_ERROR") + this.settings.fontAssetsPath, 6000);
638-
}
639-
640-
if (typeof cjkFontDataURLs === "object") {
641-
const fontDeclarations = cjkFontDataURLs.map(
642-
(dataURL) =>
643-
`@font-face { font-family: 'Xiaolai'; src: url("${dataURL}"); font-display: swap; font-weight: 400; }`,
644-
);
645-
for (const ownerDocument of this.getOpenObsidianDocuments()) {
646-
await this.addFonts(fontDeclarations, ownerDocument, CJK_STYLE_ID);
647-
}
648-
new Notice(t("FONTS_LOADED"));
649-
}
650-
651-
const font = await getFontDataURL(
652-
this.app,
653-
this.settings.experimantalFourthFont,
654-
"",
655-
"Local Font",
656-
);
657-
658-
if (font.dataURL === "") {
659-
this.fourthFontLoaded = true;
660-
return;
661-
}
662-
663-
const fourthFontDataURL = font.dataURL;
664-
665-
const f = this.app.metadataCache.getFirstLinkpathDest(
666-
this.settings.experimantalFourthFont,
667-
"",
668-
);
669-
// Call getFontMetrics with the fourthFontDataURL
670-
let fontMetrics = f.extension.startsWith("woff")
671-
? undefined
672-
: await getFontMetrics(fourthFontDataURL, "Local Font");
673-
674-
if (!fontMetrics) {
675-
//console.log("Font Metrics not found, using default");
676-
fontMetrics = {
677-
unitsPerEm: 1000,
678-
ascender: 750,
679-
descender: -250,
680-
lineHeight: 1.2,
681-
fontName: "Local Font",
682-
};
683-
}
684-
this.packageManager.getPackageMap().forEach(({ excalidrawLib }) => {
685-
if (!fontMetrics) {
686-
return;
687-
}
688-
excalidrawLib.registerLocalFont(
689-
{ metrics: fontMetrics },
690-
fourthFontDataURL,
691-
);
692-
});
693-
// Add fonts to open Obsidian documents
694-
for (const ownerDocument of this.getOpenObsidianDocuments()) {
695-
await this.addFonts(
696-
[
697-
`@font-face{font-family:'Local Font';src:url("${fourthFontDataURL}");font-display: swap;font-weight: 400;`,
698-
],
699-
ownerDocument,
700-
);
701-
}
702-
if (!this.fourthFontLoaded) {
703-
window.setTimeout(() => {
704-
this.fourthFontLoaded = true;
705-
}, 100);
706-
}
707-
this.fontsReady = true;
609+
/** Initializes configured CJK and custom fonts across open documents. */
610+
public async initializeFonts(): Promise<void> {
611+
await this.fontManager.initializeFonts();
708612
}
709613

614+
/** Adds or replaces a plugin-owned font stylesheet. */
710615
public async addFonts(
711616
declarations: string[],
712-
ownerDocument: Document = mainDocument,
713-
styleId: string = FONTS_STYLE_ID,
714-
) {
715-
// replace the old local font <style> element with the one we just created
716-
const newStylesheet = deliberateCreateElement(ownerDocument, "style");
717-
newStylesheet.id = styleId;
718-
newStylesheet.textContent = declarations.join("");
719-
const oldStylesheet = ownerDocument.getElementById(styleId);
720-
ownerDocument.head.appendChild(newStylesheet);
721-
if (oldStylesheet) {
722-
ownerDocument.head.removeChild(oldStylesheet);
723-
}
724-
await ownerDocument.fonts.load("20px Local Font");
617+
ownerDocument?: Document,
618+
styleId?: string,
619+
): Promise<void> {
620+
await this.fontManager.addFonts(declarations, ownerDocument, styleId);
725621
}
726622

727-
public removeFonts() {
728-
this.getOpenObsidianDocuments().forEach((ownerDocument) => {
729-
const oldCustomFontStylesheet =
730-
ownerDocument.getElementById(FONTS_STYLE_ID);
731-
if (oldCustomFontStylesheet) {
732-
ownerDocument.head.removeChild(oldCustomFontStylesheet);
733-
}
734-
const oldCJKFontStylesheet = ownerDocument.getElementById(CJK_STYLE_ID);
735-
if (oldCJKFontStylesheet) {
736-
ownerDocument.head.removeChild(oldCJKFontStylesheet);
737-
}
738-
});
623+
/** Removes plugin-owned font stylesheets from all open documents. */
624+
public removeFonts(): void {
625+
this.fontManager.removeFonts();
739626
}
740627

741628
/** Updates the optional mobile footer padding across open documents. */
742629
public updateFooterSafeAreaPadding(): void {
743630
this.footerSafeAreaManager.updateFooterSafeAreaPadding();
744631
}
745632

746-
private getOpenObsidianDocuments(): Document[] {
747-
const visitedDocs = new Set<Document>();
748-
this.app.workspace.iterateAllLeaves((leaf) => {
749-
const ownerDocument = DEVICE.isMobile
750-
? mainDocument
751-
: leaf.view.containerEl.ownerDocument;
752-
if (!ownerDocument) {
753-
return;
754-
}
755-
if (visitedDocs.has(ownerDocument)) {
756-
return;
757-
}
758-
visitedDocs.add(ownerDocument);
759-
});
760-
return Array.from(visitedDocs);
761-
}
762-
763633
/**
764634
* Must be called after the workspace is ready
765635
*/

0 commit comments

Comments
 (0)