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
text/typescript
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>;
}
}