UNPKG

insta-toc

Version:

Simultaneously generate, update, and maintain a table of contents for your notes in real time.

153 lines (124 loc) 5.85 kB
import { PluginSettingsManagerBase } from "obsidian-dev-utils/obsidian/plugin/plugin-settings-manager-base"; import type InstaTocPlugin from "../Plugin"; import type { FileKey, FoldKey, PluginTypes } from "../types"; import type UiStateManager from "../uiStateManager"; import { DEFAULT_SETTINGS, type InstaTocPersistedData, type InstaTocSettings } from "./Settings"; type LoadedPersistedData = { settings: Partial<InstaTocSettings>; tocFoldState: Map<FoldKey, boolean>; tocBlockCollapseState: Map<FileKey, boolean>; }; export class PluginSettingsManager extends PluginSettingsManagerBase<PluginTypes> { private _uiStateManager: UiStateManager | undefined; public constructor(plugin: InstaTocPlugin) { super(plugin); } private get uiStateManager(): UiStateManager { if (!this._uiStateManager) { this._uiStateManager = this.plugin.uiStateManager; } return this._uiStateManager; } protected override createDefaultSettings(): InstaTocSettings { return { ...DEFAULT_SETTINGS, excludedChars: [ ...DEFAULT_SETTINGS.excludedChars ], excludedHeadingLevels: [ ...DEFAULT_SETTINGS.excludedHeadingLevels ], excludedHeadingText: [ ...DEFAULT_SETTINGS.excludedHeadingText ] }; } protected override async onLoadRecord(rawRecord: Record<string, unknown>): Promise<void> { const persistedData = this.parsePersistedData(rawRecord); this.uiStateManager.setPersistedUiState(persistedData.tocFoldState, persistedData.tocBlockCollapseState); // Convert persisted shape -> flat settings record expected by base manager for (const key of Object.keys(rawRecord)) { delete rawRecord[key]; } Object.assign(rawRecord, persistedData.settings); } protected override async onSavingRecord(rawRecord: Record<string, unknown>): Promise<void> { const persistedData = this.buildPersistedData({ ...rawRecord } as Partial<InstaTocSettings>); for (const key of Object.keys(rawRecord)) { delete rawRecord[key]; } Object.assign(rawRecord, persistedData as Record<string, unknown>); } public async savePersistedData(): Promise<void> { await this.plugin.saveData( this.buildPersistedData(this.settingsWrapper.settings as InstaTocSettings) as Record<string, unknown> ); } private buildPersistedData(settings: Partial<InstaTocSettings>): InstaTocPersistedData { const { tocBlockCollapseState, tocFoldState } = this.uiStateManager.getPersistedUiState(); return { settings: { ...settings }, tocBlockCollapseState: this.stateMapToRecord(tocBlockCollapseState), tocFoldState: this.stateMapToRecord(tocFoldState, ([ a ], [ b ]) => { const [ aPath, aIndex ] = a.split("::") as [FileKey, `${number}`]; const [ bPath, bIndex ] = b.split("::") as [FileKey, `${number}`]; const aSortable = `${aPath}::${Number(aIndex).toString().padStart(2, "0")}`; const bSortable = `${bPath}::${Number(bIndex).toString().padStart(2, "0")}`; return aSortable.localeCompare(bSortable); }) }; } public override registerValidators(): void { super.registerValidators(); this.registerValidator("updateDelay", (value) => { if (value < 500 || value > 10000 || value % 500 !== 0) { return "Update delay must be between 500 and 10000 milliseconds in 500 ms steps."; } }); this.registerValidator("tocTitleLevel", (value) => { if (value < 1 || value > 6) { return "ToC heading level must be between 1 and 6."; } }); this.registerValidator("excludedHeadingLevels", (value) => { if (value.some((level) => level < 1 || level > 6)) { return "Excluded heading levels must contain only values from 1 to 6."; } }); this.registerValidator("excludedHeadingText", (value) => { if (value.some((item) => item.trim() === "")) { return "Excluded heading text cannot contain empty values."; } }); this.registerValidator("excludedChars", (value) => { if (value.some((item) => item.trim() === "")) { return "Excluded characters cannot contain empty values."; } }); } private parsePersistedData(data: unknown): LoadedPersistedData { if (!isRecord(data)) { return { settings: {}, tocBlockCollapseState: new Map<FileKey, boolean>(), tocFoldState: new Map<FoldKey, boolean>() }; } return { settings: isRecord<Partial<InstaTocSettings>>(data.settings) ? (data.settings) : {}, tocBlockCollapseState: this.recordToStateMap<FileKey>(data.tocBlockCollapseState), tocFoldState: this.recordToStateMap<FoldKey>(data.tocFoldState) }; } private recordToStateMap<T extends FoldKey | FileKey>(rawState: unknown): Map<T, boolean> { const state: Map<T, boolean> = new Map(); if (isRecord(rawState)) { for (const [ key, value ] of Object.entries(rawState)) { if (typeof value === "boolean") { state.set(key as T, value); } } } return state; } private stateMapToRecord<T extends FoldKey | FileKey>( state: Map<T, boolean>, sortFn?: (a: [T, boolean], b: [T, boolean]) => number ): Record<T, boolean> { return Object.fromEntries(sortMap(state, sortFn)) as Record<T, boolean>; } }