UNPKG

insta-toc

Version:

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

176 lines (140 loc) 5.57 kB
import { App, CachedMetadata, Debouncer, EventRef, MarkdownPostProcessorContext, MarkdownRenderer, Plugin, PluginManifest, TFile, debounce } from 'obsidian'; import { mergicianSettings } from './constants'; import { mergician } from 'mergician'; import { InstaTocSettings, DEFAULT_SETTINGS } from './Settings'; import { SettingTab } from './SettingsTab'; import { ManageToc } from './ManageToc'; import { configureRenderedIndent, getEditorData, handleCodeblockListItem } from './Utils'; import { listRegex, localTocSettingsRegex } from './constants'; import { EditorData } from './types'; import { Validator } from './validator'; export default class InstaTocPlugin extends Plugin { public app: App; public settings: InstaTocSettings; private validator: Validator | undefined; private modifyEventRef: EventRef | undefined; private debouncer: Debouncer<[fileCache: CachedMetadata], void>; // Flags to maintain state with updates public isPluginEdit = false; public hasTocBlock = true; public getDelay = () => this.settings.updateDelay; constructor(app: App, manifest: PluginManifest) { super(app, manifest); this.app = app; } async onload(): Promise<void> { console.log(`Loading Insta TOC Plugin`); await this.loadSettings(); this.addSettingTab(new SettingTab(this.app, this)); // Custom codeblock processor for the insta-toc codeblock this.registerMarkdownCodeBlockProcessor( "insta-toc", async (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext): Promise<void> => { if (!this.hasTocBlock) { this.hasTocBlock = true; } const pathWithFileExtension: string = ctx.sourcePath; // Includes .md const filePath: string = pathWithFileExtension.substring(0, pathWithFileExtension.lastIndexOf(".")); const file: TFile = this.app.vault.getAbstractFileByPath(pathWithFileExtension) as TFile; // TOC codeblock content const lines: string[] = source // Process only the ToC content without local settings .replace(localTocSettingsRegex, '') .split('\n'); const headingLevels: number[] = []; // To store heading levels corresponding to each line // Process the codeblock text by converting each line into a markdown link list item const processedSource: string = lines.map((line) => { const match: RegExpMatchArray | null = line.match(listRegex); if (!match) return line; const { indent, bullet, navLink } = handleCodeblockListItem(this.app, this, file, match, filePath); // Calculate heading level based on indentation const indentLevel = Math.floor(indent.length / 4); // Each indent level represents one heading level increment const headingLevel: number = indentLevel + 1; // H1 corresponds to no indentation headingLevels.push(headingLevel); return `${indent}${bullet} ${navLink}`; }).join('\n'); // Now render the markdown await MarkdownRenderer.render(this.app, processedSource, el, pathWithFileExtension, this); // Configure indentation once rendered configureRenderedIndent(el, headingLevels, this.settings.indentSize); } ); this.registerEvent( // Reset with new files to fix no detection on file open this.app.workspace.on("file-open", () => this.hasTocBlock = true) ); this.updateModifyEventListener(); } onunload(): void { console.log(`Insta TOC Plugin Unloaded.`); } async loadSettings(): Promise<void> { let mergedSettings: InstaTocSettings = DEFAULT_SETTINGS; const settingsData: InstaTocSettings = await this.loadData(); if (settingsData) { mergedSettings = mergician(mergicianSettings)(DEFAULT_SETTINGS, settingsData); } this.settings = mergedSettings; } async saveSettings(): Promise<void> { await this.saveData(this.settings); } // Dynamically update the debounce delay for ToC updates public updateModifyEventListener(): void { if (this.modifyEventRef) { // Unregister the previous event listener this.app.metadataCache.offref(this.modifyEventRef); } this.setDebouncer(); // Register the new event listener with the updated debounce delay this.modifyEventRef = this.app.metadataCache.on( "changed", // file cache (containing heading cache) has been updated (file: TFile, data: string, cache: CachedMetadata) => { if (!this.hasTocBlock) return; this.debouncer(cache); } ); this.registerEvent(this.modifyEventRef); } // Needed for dynamically setting the debounce delay public setDebouncer(): void { this.debouncer = debounce( (fileCache: CachedMetadata) => { // Ignore updates performed by ManageToc.ts if (this.isPluginEdit) { this.isPluginEdit = false; return; } const { editor, cursorPos }: EditorData = getEditorData(this.app); if (!editor || !cursorPos) return; // Reuse and update the existing validator instance if it exists if (this.validator) { this.validator.update(this, fileCache, editor, cursorPos); } else { this.validator = new Validator(this, fileCache, editor, cursorPos); } const isValid: boolean = this.validator.isValid(); if (isValid) { // Handle all active file changes for the insta-toc plaintext content new ManageToc(this, this.validator); } }, this.settings.updateDelay, false ); } public static getGlobalSetting<K extends keyof InstaTocSettings>(key: K): InstaTocSettings[K] { const plugin = (window as any).app.plugins.getPlugin('insta-toc') as InstaTocPlugin; const settings = plugin?.settings as InstaTocSettings; return settings[key]; } }