diff --git a/common/data.ts b/common/data.ts index 9c197f4..0b6d23e 100644 --- a/common/data.ts +++ b/common/data.ts @@ -213,6 +213,31 @@ export function defaultSourceHeadingDecoratorSettings(): SourceHeadingDecoratorS }; } +/** + * Default plugin settings. + * + * @returns default plugin settings. + */ +export function defalutSettings(): HeadingPluginSettings { + return { + metadataKeyword: "heading", + fileRegexBlacklist: [], + folderBlacklist: [], + enabledInReading: true, + readingSettings: defaultHeadingDecoratorSettings(), + enabledInPreview: true, + previewSettings: defaultHeadingDecoratorSettings(), + enabledInSource: false, + sourceSettings: defaultSourceHeadingDecoratorSettings(), + enabledInOutline: false, + outlineSettings: defaultHeadingDecoratorSettings(), + enabledInQuietOutline: false, + quietOutlineSettings: defaultHeadingDecoratorSettings(), + enabledInFileExplorer: false, + fileExplorerSettings: defaultHeadingDecoratorSettings(), + }; +} + /** * Get unordered level headings from settings. * diff --git a/components/plugin.ts b/components/plugin.ts new file mode 100644 index 0000000..ffc34c6 --- /dev/null +++ b/components/plugin.ts @@ -0,0 +1,973 @@ +import { + Editor, + FrontMatterCache, + MarkdownView, + Plugin, + TFile, + WorkspaceLeaf, + debounce, +} from "obsidian"; +import { EditorView, ViewPlugin } from "@codemirror/view"; +import type { HeadingPluginSettings, HeadingPluginData } from "../common/data"; +import { + headingsSelector, + getUnorderedLevelHeadings, + getOrderedCustomIdents, + diffLevel, + getBoolean, + checkEnabledCSS, + stringToRegex, + defalutSettings, +} from "../common/data"; +import { Counter, Querier } from "../common/counter"; +import { Heading } from "../common/heading"; +import { + decorateHTMLElement, + decorateOutlineElement, + cancelOutlineDecorator, + decorateFileHeadingElement, + cancelFileHeadingDecorator, + queryHeadingLevelByElement, + getTreeItemLevel, + getTreeItemText, + getFileHeadingItemLevel, + compareHeadingText, +} from "../common/dom"; +import { + HeadingViewPlugin, + headingDecorationsField, + editorModeField, + updateEditorMode, +} from "./view"; +import { ViewChildComponent } from "./child"; +import { + quietOutlineHandler, + cancelQuietOutlineDecoration, +} from "./quiet-outline"; +import { HeadingSettingTab } from "./setting-tab"; + +interface ObsidianEditor extends Editor { + cm: EditorView; +} + +interface ObsidianWorkspaceLeaf extends WorkspaceLeaf { + id: string; +} + +interface SettingsChangeState { + outline?: boolean; + quietOutline?: boolean; + fileExplorer?: boolean; +} + +export class HeadingPlugin extends Plugin { + settings: HeadingPluginSettings; + + private outlineIdSet: Set = new Set(); + private outlineComponents: ViewChildComponent[] = []; + + private quietOutlineIdSet: Set = new Set(); + private quietOutlineComponents: ViewChildComponent[] = []; + + private fileExplorerIdSet: Set = new Set(); + private fileExplorerComponents: ViewChildComponent[] = []; + + async onload() { + await this.loadSettings(); + + // Register editor extension + this.registerEditorExtension([ + headingDecorationsField, + editorModeField, + this.craeteHeadingViewPlugin(this.getPluginData.bind(this)), + ]); + + // Register markdown post processor + this.registerMarkdownPostProcessor(async (element, context) => { + if (!this.settings.enabledInReading) { + return; + } + + const metadataEnabled = this.getEnabledFromFrontmatter( + "reading", + context.frontmatter + ); + + if (metadataEnabled == null) { + if ( + this.settings.readingSettings.enabledInEachNote != undefined && + !this.settings.readingSettings.enabledInEachNote + ) { + return; + } + + if (this.getEnabledFromBlacklist(context.sourcePath)) { + return; + } + } else if (!metadataEnabled) { + return; + } + + const headingElements = element.findAll(headingsSelector); + if (headingElements.length === 0) { + return; + } + + const { + readingSettings: { + opacity, + position, + ordered, + orderedDelimiter, + orderedTrailingDelimiter, + orderedStyleType, + orderedSpecifiedString, + orderedCustomIdents, + orderedIgnoreSingle, + orderedIgnoreMaximum = 6, + orderedBasedOnExisting, + orderedAllowZeroLevel, + unorderedLevelHeadings, + }, + } = this.settings; + + if (ordered) { + const file = this.getActiveFile(context.sourcePath); + if (!file) { + return; + } + + const sourceContent = await this.app.vault.cachedRead(file); + const sourceArr = sourceContent.split("\n"); + if (sourceArr.length === 0) { + return; + } + + let ignoreTopLevel = 0; + if (orderedIgnoreSingle || orderedBasedOnExisting) { + const queier = new Querier(orderedAllowZeroLevel); + const heading = new Heading(); + for (let lineIndex = 1; lineIndex <= sourceArr.length; lineIndex++) { + const lineText = sourceArr[lineIndex - 1]; + const nextLineIndex = lineIndex + 1; + const nextLineText = + nextLineIndex <= sourceArr.length + ? sourceArr[nextLineIndex - 1] + : ""; + const level = heading.handler(lineIndex, lineText, nextLineText); + if (level === -1) { + continue; + } + + queier.handler(level); + + ignoreTopLevel = queier.query( + orderedIgnoreSingle, + orderedIgnoreMaximum + ); + if (ignoreTopLevel === 0) { + break; + } + } + } + + const counter = new Counter({ + ordered: true, + delimiter: orderedDelimiter, + trailingDelimiter: orderedTrailingDelimiter, + styleType: orderedStyleType, + customIdents: getOrderedCustomIdents(orderedCustomIdents), + specifiedString: orderedSpecifiedString, + ignoreTopLevel, + allowZeroLevel: orderedAllowZeroLevel, + }); + const heading = new Heading(); + let headingIndex = 1; + + headingElements.forEach((headingElement) => { + const sectionInfo = context.getSectionInfo(headingElement); + if (!sectionInfo) { + return; + } + + const lineStart = sectionInfo.lineStart + 1; + if (lineStart > sourceArr.length) { + return; + } + + for ( + let lineIndex = headingIndex; + lineIndex <= lineStart; + lineIndex++ + ) { + const lineText = sourceArr[lineIndex - 1]; + const nextLineIndex = lineIndex + 1; + const nextLineText = + nextLineIndex <= sourceArr.length + ? sourceArr[nextLineIndex - 1] + : ""; + const level = heading.handler(lineIndex, lineText, nextLineText); + if (lineIndex === lineStart) { + //? 1. When using the page preview feature to only reference fragments, the + //? relative position of rows is incorrect. + //? 2. In the split editing scenario, there may be a delay in the information + //? on the reading tab. + const elementLevel = queryHeadingLevelByElement(headingElement); + if (elementLevel === level) { + const decoratorContent = counter.decorator(level); + decorateHTMLElement( + headingElement, + decoratorContent, + opacity, + position + ); + } + + headingIndex = lineIndex + 1; + } else { + counter.handler(level); + } + } + }); + } else { + const counter = new Counter({ + ordered: false, + levelHeadings: getUnorderedLevelHeadings(unorderedLevelHeadings), + }); + + headingElements.forEach((headingElement) => { + const level = queryHeadingLevelByElement(headingElement); + const decoratorContent = counter.decorator(level); + decorateHTMLElement( + headingElement, + decoratorContent, + opacity, + position + ); + }); + } + }); + + // Listen for metadata changes + this.registerEvent( + this.app.metadataCache.on( + "changed", + debounce( + (file) => { + //! Delay trigger rerender + if (file && file.path === this.getActiveFile()?.path) { + this.rerenderPreviewMarkdown(file); + } + }, + 250, + true + ) + ) + ); + + // Listen for editor mode changes + this.registerEvent( + this.app.workspace.on("active-leaf-change", () => { + this.handleModeChange(); + this.loadOutlineComponents(); + this.loadQuietOutlineComponents(); + this.loadFileExplorerComponents(); + }) + ); + this.registerEvent( + this.app.workspace.on("layout-change", () => { + this.handleModeChange(); + this.loadOutlineComponents(); + this.loadQuietOutlineComponents(); + this.loadFileExplorerComponents(); + }) + ); + + this.loadOutlineComponents(); + this.loadQuietOutlineComponents(); + this.loadFileExplorerComponents(); + + this.addSettingTab(new HeadingSettingTab(this.app, this)); + } + + onunload(): void { + this.unloadOutlineComponents(); + this.unloadQuietOutlineComponents(); + this.unloadFileExplorerComponents(); + } + + async loadSettings() { + this.settings = Object.assign({}, defalutSettings(), await this.loadData()); + } + + async saveSettings(state?: SettingsChangeState) { + await this.saveData(this.settings); + + if (state) { + if (state.outline != undefined) { + if (state.outline) { + this.loadOutlineComponents(); + } else { + this.unloadOutlineComponents(); + } + } + + if (state.quietOutline != undefined) { + if (state.quietOutline) { + this.loadQuietOutlineComponents(); + } else { + this.unloadQuietOutlineComponents(); + } + } + + if (state.fileExplorer != undefined) { + if (state.fileExplorer) { + this.loadFileExplorerComponents(); + } else { + this.unloadFileExplorerComponents(); + } + } + } + + this.debouncedSaveSettings(); + } + + private loadOutlineComponents() { + if (this.settings.enabledInOutline) { + this.handleOutline(); + } + } + + private unloadOutlineComponents() { + this.outlineComponents.forEach((child) => child.detach()); + this.outlineComponents = []; + this.outlineIdSet.clear(); + } + + private loadQuietOutlineComponents() { + if (this.settings.enabledInQuietOutline) { + this.handleQuietOutline(); + } + } + + private unloadQuietOutlineComponents() { + this.quietOutlineComponents.forEach((child) => child.detach()); + this.quietOutlineComponents = []; + this.quietOutlineIdSet.clear(); + } + + private loadFileExplorerComponents() { + if (this.settings.enabledInFileExplorer) { + this.handleHeadingsInExplorer(); + } + } + + private unloadFileExplorerComponents() { + this.fileExplorerComponents.forEach((child) => child.detach()); + this.fileExplorerComponents = []; + this.fileExplorerIdSet.clear(); + } + + private craeteHeadingViewPlugin( + getPluginData: () => Promise + ) { + return ViewPlugin.fromClass( + class extends HeadingViewPlugin { + constructor(view: EditorView) { + super(view, getPluginData); + } + } + ); + } + + private handleModeChange() { + const view = this.app.workspace.getActiveViewOfType(MarkdownView); + if (view) { + //? "source" for editing view (Live Preview & Source mode), + //? "preview" for reading view. + const isLivePreview = view.getMode() === "source"; + + //* Get CodeMirror editor instance + const cmEditor = (view.editor as ObsidianEditor).cm; + if (cmEditor) { + cmEditor.dispatch({ + effects: updateEditorMode.of(isLivePreview), + }); + } + } + } + + private handleOutline() { + const leaves = this.app.workspace.getLeavesOfType("outline"); + leaves.forEach((leaf: ObsidianWorkspaceLeaf) => { + const leafId = leaf.id; + if (!leafId || this.outlineIdSet.has(leafId)) { + return; + } + + const view = leaf.view; + const viewContent = view.containerEl.querySelector( + '[data-type="outline"] .view-content' + ); + if (!viewContent) { + return; + } + + this.outlineIdSet.add(leafId); + + const vc = new ViewChildComponent( + leafId, + view, + viewContent, + () => { + const headingElements = + viewContent.querySelectorAll(".tree-item"); + if (headingElements.length === 0) { + return; + } + + const state = view.getState(); + if (typeof state.file !== "string") { + return; + } + + const file = this.app.vault.getFileByPath(state.file); + if (!file) { + return; + } + + const fileCache = this.app.metadataCache.getFileCache(file); + if (!fileCache) { + return; + } + + const cacheHeadings = fileCache.headings || []; + if (cacheHeadings.length === 0) { + return; + } + + const frontmatter = fileCache.frontmatter; + const metadataEnabled = this.getEnabledFromFrontmatter( + "outline", + frontmatter + ); + + const { + enabledInEachNote, + opacity, + position, + ordered, + orderedDelimiter, + orderedTrailingDelimiter, + orderedStyleType, + orderedSpecifiedString, + orderedCustomIdents, + orderedIgnoreSingle, + orderedIgnoreMaximum = 6, + orderedBasedOnExisting, + orderedAllowZeroLevel, + unorderedLevelHeadings, + } = this.settings.outlineSettings; + + if (metadataEnabled == null) { + if (enabledInEachNote != undefined && !enabledInEachNote) { + return; + } + + if (this.getEnabledFromBlacklist(state.file)) { + return; + } + } else if (!metadataEnabled) { + return; + } + + let ignoreTopLevel = 0; + if (ordered && (orderedIgnoreSingle || orderedBasedOnExisting)) { + const queier = new Querier(orderedAllowZeroLevel); + for (const cacheHeading of cacheHeadings) { + queier.handler(cacheHeading.level); + ignoreTopLevel = queier.query( + orderedIgnoreSingle, + orderedIgnoreMaximum + ); + if (ignoreTopLevel === 0) { + break; + } + } + } + + const counter = new Counter({ + ordered, + delimiter: orderedDelimiter, + trailingDelimiter: orderedTrailingDelimiter, + styleType: orderedStyleType, + customIdents: getOrderedCustomIdents(orderedCustomIdents), + specifiedString: orderedSpecifiedString, + ignoreTopLevel, + allowZeroLevel: orderedAllowZeroLevel, + levelHeadings: getUnorderedLevelHeadings(unorderedLevelHeadings), + }); + + let lastCacheLevel = 0; + let lastReadLevel = 0; + for ( + let i = 0, j = 0; + i < headingElements.length && j < cacheHeadings.length; + i++, j++ + ) { + const readLevel = getTreeItemLevel(headingElements[i]); + const readText = getTreeItemText(headingElements[i]); + let cacheLevel = cacheHeadings[j].level; + if (i > 0) { + const diff = diffLevel(readLevel, lastReadLevel); + while ( + j < cacheHeadings.length - 1 && + (diffLevel(cacheLevel, lastCacheLevel) !== diff || + !compareHeadingText(cacheHeadings[j].heading, readText)) + ) { + counter.handler(cacheLevel); + j++; + cacheLevel = cacheHeadings[j].level; + } + } + + const decoratorContent = counter.decorator(cacheLevel); + decorateOutlineElement( + headingElements[i], + decoratorContent, + opacity, + position + ); + + lastCacheLevel = cacheLevel; + lastReadLevel = readLevel; + } + }, + () => { + const headingElements = + viewContent.querySelectorAll(".tree-item"); + headingElements.forEach((ele) => { + cancelOutlineDecorator(ele); + }); + } + ); + + this.outlineComponents.push(vc); + view.addChild(vc); + view.register(() => { + this.outlineComponents = this.outlineComponents.filter( + (item) => !item.equal(leafId) + ); + }); + }); + } + + private handleQuietOutline() { + const leaves = this.app.workspace.getLeavesOfType("quiet-outline"); + leaves.forEach((leaf: ObsidianWorkspaceLeaf) => { + const leafId = leaf.id; + if (!leafId || this.quietOutlineIdSet.has(leafId)) { + return; + } + + const view = leaf.view; + const viewContent = view.containerEl.querySelector( + '[data-type="quiet-outline"] .view-content' + ); + if (!viewContent) { + return; + } + + this.quietOutlineIdSet.add(leafId); + + const vc = new ViewChildComponent( + leafId, + view, + viewContent, + () => { + const containerElement = + viewContent.querySelector(".n-tree"); + if (!containerElement) { + return; + } + + const headingELements = + viewContent.querySelectorAll(".n-tree-node"); + if (headingELements.length === 0) { + return; + } + + const file = this.getActiveFile(); + if (!file) { + return; + } + + const fileCache = this.app.metadataCache.getFileCache(file); + if (!fileCache) { + return; + } + + const frontmatter = fileCache.frontmatter; + const metadataEnabled = this.getEnabledFromFrontmatter( + "quiet-outline", + frontmatter + ); + + const { enabledInEachNote } = this.settings.quietOutlineSettings; + + let enabled = true; + if (metadataEnabled == null) { + if (enabledInEachNote != undefined && !enabledInEachNote) { + enabled = false; + } + + if (this.getEnabledFromBlacklist(file.path)) { + enabled = false; + } + } else if (!metadataEnabled) { + enabled = false; + } + + if (!enabled) { + cancelQuietOutlineDecoration(containerElement); + return; + } + + quietOutlineHandler( + this.settings.quietOutlineSettings, + containerElement, + headingELements + ); + }, + () => { + const containerElement = + viewContent.querySelector(".n-tree"); + if (containerElement) { + cancelQuietOutlineDecoration(containerElement); + } + } + ); + + this.quietOutlineComponents.push(vc); + view.addChild(vc); + view.register(() => { + this.quietOutlineComponents = this.quietOutlineComponents.filter( + (item) => !item.equal(leafId) + ); + }); + }); + } + + private handleHeadingsInExplorer() { + const leaves = this.app.workspace.getLeavesOfType("file-explorer"); + leaves.forEach((leaf: ObsidianWorkspaceLeaf) => { + const leafId = leaf.id; + if (!leafId || this.fileExplorerIdSet.has(leafId)) { + return; + } + + const view = leaf.view; + const navFilesContainer = view.containerEl.querySelector( + '[data-type="file-explorer"] .nav-files-container' + ); + if (!navFilesContainer) { + return; + } + + this.fileExplorerIdSet.add(leafId); + + const vc = new ViewChildComponent( + leafId, + view, + navFilesContainer, + () => { + const navFileTitles = + navFilesContainer.querySelectorAll(".nav-file-title"); + + navFileTitles.forEach((navFile) => { + const headingElements = navFile.querySelectorAll( + ".file-heading-container .clickable-heading" + ); + if (headingElements.length === 0) { + return; + } + + const filePath = navFile.dataset.path; + if (!filePath) { + return; + } + + const file = this.app.vault.getFileByPath(filePath); + if (!file) { + return; + } + + const fileCache = this.app.metadataCache.getFileCache(file); + if (!fileCache) { + return; + } + + const cacheHeadings = fileCache.headings || []; + if (cacheHeadings.length === 0) { + return; + } + + const frontmatter = fileCache.frontmatter; + const metadataEnabled = this.getEnabledFromFrontmatter( + "file-explorer", + frontmatter + ); + + const { + enabledInEachNote, + opacity, + position, + ordered, + orderedDelimiter, + orderedTrailingDelimiter, + orderedStyleType, + orderedSpecifiedString, + orderedCustomIdents, + orderedIgnoreSingle, + orderedIgnoreMaximum = 6, + orderedBasedOnExisting, + orderedAllowZeroLevel, + unorderedLevelHeadings, + } = this.settings.fileExplorerSettings; + + if (metadataEnabled == null) { + if (enabledInEachNote != undefined && !enabledInEachNote) { + return; + } + + if (this.getEnabledFromBlacklist(filePath)) { + return; + } + } else if (!metadataEnabled) { + return; + } + + let ignoreTopLevel = 0; + if (ordered && (orderedIgnoreSingle || orderedBasedOnExisting)) { + const queier = new Querier(orderedAllowZeroLevel); + for (const cacheHeading of cacheHeadings) { + queier.handler(cacheHeading.level); + ignoreTopLevel = queier.query( + orderedIgnoreSingle, + orderedIgnoreMaximum + ); + if (ignoreTopLevel === 0) { + break; + } + } + } + + const counter = new Counter({ + ordered, + delimiter: orderedDelimiter, + trailingDelimiter: orderedTrailingDelimiter, + styleType: orderedStyleType, + customIdents: getOrderedCustomIdents(orderedCustomIdents), + specifiedString: orderedSpecifiedString, + ignoreTopLevel, + allowZeroLevel: orderedAllowZeroLevel, + levelHeadings: getUnorderedLevelHeadings(unorderedLevelHeadings), + }); + + const marginMultiplier = + parseInt( + getComputedStyle(document.body).getPropertyValue( + "--clickable-heading-margin-multiplier" + ) + ) || 10; + + for ( + let i = 0, j = 0; + i < headingElements.length && j < cacheHeadings.length; + i++, j++ + ) { + const readLevel = getFileHeadingItemLevel( + headingElements[i], + marginMultiplier + ); + const readText = headingElements[i].innerText; + let cacheLevel = cacheHeadings[j].level; + + while ( + j < cacheHeadings.length - 1 && + (cacheLevel !== readLevel || + cacheHeadings[j].heading !== readText) + ) { + counter.handler(cacheLevel); + j++; + cacheLevel = cacheHeadings[j].level; + } + + const decoratorContent = counter.decorator(cacheLevel); + decorateFileHeadingElement( + headingElements[i], + decoratorContent, + opacity, + position + ); + } + }); + }, + () => { + const headingElements = + navFilesContainer.querySelectorAll( + ".nav-file-title .file-heading-container .clickable-heading" + ); + headingElements.forEach((ele) => { + cancelFileHeadingDecorator(ele); + }); + } + ); + + this.fileExplorerComponents.push(vc); + view.addChild(vc); + view.register(() => { + this.fileExplorerComponents = this.fileExplorerComponents.filter( + (item) => !item.equal(leafId) + ); + }); + }); + } + + private debouncedSaveSettings = debounce( + this.rerenderPreviewMarkdown.bind(this), + 1000, + true + ); + + /** + * Rerender Preview Markdown. + * @param file + */ + private rerenderPreviewMarkdown(file?: TFile) { + const view = this.app.workspace.getActiveViewOfType(MarkdownView); + if (view) { + if (!file || file === view.file) { + view.previewMode.rerender(true); + } + } + } + + async getPluginData(): Promise { + const { + metadataKeyword, + enabledInPreview: _enabledInPreview, + enabledInSource: _enabledInSource, + previewSettings, + sourceSettings, + } = this.settings; + + let enabledInPreview = _enabledInPreview; + let enabledInSource = _enabledInSource; + + if (metadataKeyword) { + const file = this.getActiveFile(); + if (file) { + const frontmatter = + this.app.metadataCache.getFileCache(file)?.frontmatter; + + if (_enabledInPreview) { + enabledInPreview = + this.getEnabledFromFrontmatter("preview", frontmatter) ?? + previewSettings.enabledInEachNote ?? + !this.getEnabledFromBlacklist(file.path); + } + + if (_enabledInSource) { + enabledInSource = + this.getEnabledFromFrontmatter("source", frontmatter) ?? + sourceSettings.enabledInEachNote ?? + !this.getEnabledFromBlacklist(file.path); + } + } + } + + return { + enabledInPreview, + enabledInSource, + previewSettings, + sourceSettings, + }; + } + + private getActiveFile(sourcePath?: string) { + if (sourcePath) { + return this.app.vault.getFileByPath(sourcePath); + } else { + return this.app.workspace.getActiveFile(); + } + } + + private getEnabledFromFrontmatter( + mode: HeadingMetadataSettingsType, + frontmatter?: FrontMatterCache + ): null | boolean { + const keyword = this.settings.metadataKeyword; + if (keyword && typeof frontmatter === "object" && frontmatter) { + const metadataSettings = frontmatter[keyword]; + if (typeof metadataSettings === "object" && metadataSettings) { + return ( + getBoolean(metadataSettings[mode]) ?? getBoolean(metadataSettings.all) + ); + } + if (metadataSettings != null) { + return getBoolean(metadataSettings); + } else { + const cssclasses = frontmatter.cssclasses; + if (Array.isArray(cssclasses)) { + const cssStatus: CSSEnabledStatus = { + mode: null, + all: null, + }; + + for (const cssItem of cssclasses) { + if (typeof cssItem === "string") { + const { mode: m, all: a } = checkEnabledCSS(cssItem, mode); + if (m != null) { + cssStatus.mode = m; + } + if (a != null) { + cssStatus.all = a; + } + } + } + + return cssStatus.mode ?? cssStatus.all; + } else if (typeof cssclasses === "string") { + const { mode: m, all: a } = checkEnabledCSS(cssclasses, mode); + return m ?? a; + } + } + } + + return null; + } + + private getEnabledFromBlacklist(filepath: string): boolean { + for (const folder of this.settings.folderBlacklist) { + if (filepath.startsWith(`${folder}/`)) { + return true; + } + } + + const filename = filepath.substring( + filepath.lastIndexOf("/") + 1, + filepath.lastIndexOf(".") + ); + for (const regexStr of this.settings.fileRegexBlacklist) { + const regex = stringToRegex(regexStr); + if (regex) { + if (regex.test(filename)) { + return true; + } + } + } + + return false; + } +} diff --git a/components/setting-tab.ts b/components/setting-tab.ts new file mode 100644 index 0000000..ce6fdbb --- /dev/null +++ b/components/setting-tab.ts @@ -0,0 +1,667 @@ +import { App, PluginSettingTab, Setting } from "obsidian"; +import type { HeadingPlugin } from "./plugin"; +import type { OrderedCounterStyleType } from "../common/data"; +import { orderedStyleTypeOptions } from "../common/data"; +import { FolderSuggest } from "./suggest"; + +export class HeadingSettingTab extends PluginSettingTab { + plugin: HeadingPlugin; + + constructor(app: App, plugin: HeadingPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const { containerEl } = this; + + containerEl.empty(); + + //* metadataKeyword + const metadataKeywordSetting = new Setting(containerEl) + .setName("Metadata keyword") + .addText((text) => + text + .setPlaceholder("Enter the keyword") + .setValue(this.plugin.settings.metadataKeyword) + .onChange(async (value) => { + this.plugin.settings.metadataKeyword = value.trim(); + await this.plugin.saveSettings(); + }) + ); + + const metadataKeywordDesc = createFragment(); + metadataKeywordDesc.append( + "The key name that reads the enabled status from the ", + createEl("a", { + href: "https://help.obsidian.md/Editing+and+formatting/Properties", + text: "properties", + }), + "." + ); + metadataKeywordSetting.descEl.appendChild(metadataKeywordDesc); + + new Setting(containerEl).setName("Reading view").setHeading(); + + //* enabledInReading + new Setting(containerEl) + .setName("Enabled in reading view") + .setDesc("Allow to decorate the heading under the reading view.") + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.enabledInReading) + .onChange(async (value) => { + this.plugin.settings.enabledInReading = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName("Manage reading view heading decorator") + .addButton((button) => { + button.setButtonText("Manage").onClick(() => { + this.manageHeadingDecoratorSettings("readingSettings"); + }); + }); + + new Setting(containerEl).setName("Live preview").setHeading(); + + //* enabledInPreview + new Setting(containerEl) + .setName("Enabled in live preview") + .setDesc("Allow to decorate the heading under the live preview.") + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.enabledInPreview) + .onChange(async (value) => { + this.plugin.settings.enabledInPreview = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName("Manage live preview heading decorator") + .addButton((button) => { + button.setButtonText("Manage").onClick(() => { + this.manageHeadingDecoratorSettings("previewSettings"); + }); + }); + + new Setting(containerEl).setName("Source mode").setHeading(); + + //* enabledInSource + new Setting(containerEl) + .setName("Enabled in source mode") + .setDesc("Allow to decorate the heading under the source mode.") + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.enabledInSource) + .onChange(async (value) => { + this.plugin.settings.enabledInSource = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName("Manage source mode heading decorator") + .addButton((button) => { + button.setButtonText("Manage").onClick(() => { + this.manageHeadingDecoratorSettings("sourceSettings"); + }); + }); + + new Setting(containerEl).setName("Outline plugin").setHeading(); + + //* enabledInOutline + new Setting(containerEl) + .setName("Enabled in outline plugin") + .setDesc("Allow to decorate the heading under the outline plugin.") + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.enabledInOutline) + .onChange(async (value) => { + this.plugin.settings.enabledInOutline = value; + await this.plugin.saveSettings({ + outline: this.plugin.settings.enabledInOutline, + }); + }) + ); + + new Setting(containerEl) + .setName("Manage outline plugin heading decorator") + .addButton((button) => { + button.setButtonText("Manage").onClick(() => { + this.manageHeadingDecoratorSettings("outlineSettings"); + }); + }); + + new Setting(containerEl).setName("Quiet Outline plugin").setHeading(); + + //* enabledInQuietOutline + const enabledInQuietOutlineSetting = new Setting(containerEl) + .setName('Enabled in "Quiet Outline" plugin') + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.enabledInQuietOutline) + .onChange(async (value) => { + this.plugin.settings.enabledInQuietOutline = value; + await this.plugin.saveSettings({ + quietOutline: this.plugin.settings.enabledInQuietOutline, + }); + }) + ); + + const enabledInQuietOutlineDesc = createFragment(); + enabledInQuietOutlineDesc.append( + "Allow to decorate the heading under the ", + createEl("a", { + href: "https://github.com/guopenghui/obsidian-quiet-outline", + text: "Quiet Outline", + }), + " plugin." + ); + enabledInQuietOutlineSetting.descEl.appendChild(enabledInQuietOutlineDesc); + + new Setting(containerEl) + .setName('Manage "Quiet Outline" plugin heading decorator') + .addButton((button) => { + button.setButtonText("Manage").onClick(() => { + this.manageHeadingDecoratorSettings("quietOutlineSettings"); + }); + }); + + new Setting(containerEl) + .setName("Headings in Explorer plugin") + .setHeading(); + + //* enabledInFileExplorer + const enabledInFileExplorerSetting = new Setting(containerEl) + .setName('Enabled in "Headings in Explorer" plugin') + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.enabledInFileExplorer) + .onChange(async (value) => { + this.plugin.settings.enabledInFileExplorer = value; + await this.plugin.saveSettings({ + fileExplorer: this.plugin.settings.enabledInFileExplorer, + }); + }) + ); + + const enabledInFileExplorerDesc = createFragment(); + enabledInFileExplorerDesc.append( + "Allow to decorate the heading under the ", + createEl("a", { + href: "https://github.com/patrickchiang/obsidian-headings-in-explorer", + text: "Headings in Explorer", + }), + " plugin." + ); + enabledInFileExplorerSetting.descEl.appendChild(enabledInFileExplorerDesc); + + new Setting(containerEl) + .setName('Manage "Headings in Explorer" plugin heading decorator') + .addButton((button) => { + button.setButtonText("Manage").onClick(() => { + this.manageHeadingDecoratorSettings("fileExplorerSettings"); + }); + }); + + new Setting(containerEl).setName("Blocklist").setHeading(); + + //* folderBlacklist + new Setting(containerEl) + .setName("Manage folder blocklist") + .addButton((button) => { + button.setButtonText("Manage").onClick(() => { + this.manageFolderBlacklist(true); + }); + }); + + //* fileRegexBlacklist + new Setting(containerEl) + .setName("Manage note name regex blocklist") + .addButton((button) => { + button.setButtonText("Manage").onClick(() => { + this.manageFileRegexBlacklist(true); + }); + }); + } + + private isOpacityValue(value: number): value is OpacityOptions { + if (value >= 10 && value <= 100 && value % 10 === 0) { + return true; + } + return false; + } + + private isPositionValue(value: string): value is PostionOptions { + return ["before", "after", "before-inside", "after-inside"].includes(value); + } + + private manageHeadingDecoratorSettings( + settingsType: PluginDecoratorSettingsType + ) { + const { containerEl } = this; + + containerEl.empty(); + + let tabName = ""; + switch (settingsType) { + case "readingSettings": + tabName = "Manage reading view heading decorator"; + break; + case "previewSettings": + tabName = "Manage live preview heading decorator"; + break; + case "sourceSettings": + tabName = "Manage source mode heading decorator"; + break; + case "outlineSettings": + tabName = "Manage outline plugin heading decorator"; + break; + case "quietOutlineSettings": + tabName = 'Manage "Quiet Outline" plugin heading decorator'; + break; + case "fileExplorerSettings": + tabName = 'Manage "Headings in Explorer" plugin heading decorator'; + break; + } + + new Setting(containerEl) + .setName(tabName) + .setHeading() + .addButton((button) => { + button.setButtonText("Back").onClick(() => { + this.display(); + }); + }); + + //* enabledInEachNote + new Setting(containerEl) + .setName("Enabled in each note") + .setDesc( + "Toggle this setting to enable the decoration of headings in each note." + ) + .addToggle((toggle) => + toggle + .setValue( + this.plugin.settings[settingsType].enabledInEachNote ?? true + ) + .onChange(async (value) => { + this.plugin.settings[settingsType].enabledInEachNote = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl).setName("Effect").setHeading(); + + //* ordered + new Setting(containerEl) + .setName("Ordered") + .setDesc( + "Toggle this setting to enable the decoration of headings as an ordered or unordered list." + ) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings[settingsType].ordered) + .onChange(async (value) => { + this.plugin.settings[settingsType].ordered = value; + await this.plugin.saveSettings(); + }) + ); + + //* opacity + new Setting(containerEl) + .setName("Opacity") + .setDesc( + "Set the opacity of the heading decorator. The value is the form of percentage." + ) + .addSlider((slider) => + slider + .setLimits(10, 100, 10) + .setValue(this.plugin.settings[settingsType].opacity) + .onChange(async (value) => { + this.plugin.settings[settingsType].opacity = this.isOpacityValue( + value + ) + ? value + : 20; + await this.plugin.saveSettings(); + }) + .setDynamicTooltip() + ); + + //* position + new Setting(containerEl) + .setName("Position") + .setDesc("Set the position of the heading decorator.") + .addDropdown((dropdown) => { + const options: Record = + settingsType === "outlineSettings" || + settingsType === "quietOutlineSettings" || + settingsType === "fileExplorerSettings" + ? { + before: "Before the heading", + after: "After the heading", + } + : { + before: "Before the heading", + "before-inside": "Before the heading (inside)", + after: "After the heading", + "after-inside": "After the heading (inside)", + }; + + dropdown + .addOptions(options) + .setValue(this.plugin.settings[settingsType].position) + .onChange(async (value) => { + this.plugin.settings[settingsType].position = this.isPositionValue( + value + ) + ? value + : "before"; + await this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl).setName("Ordered").setHeading(); + + //* orderedStyleType + new Setting(containerEl) + .setName("Style type") + .setDesc("Set the style type for ordered list.") + .addDropdown((dropdown) => + dropdown + .addOptions(orderedStyleTypeOptions) + .setValue(this.plugin.settings[settingsType].orderedStyleType) + .onChange(async (value) => { + this.plugin.settings[settingsType].orderedStyleType = + value as OrderedCounterStyleType; + await this.plugin.saveSettings(); + }) + ); + + //* orderedDelimiter + new Setting(containerEl) + .setName("Delimiter") + .setDesc("Set the delimiter for ordered list.") + .addText((text) => + text + .setValue(this.plugin.settings[settingsType].orderedDelimiter) + .onChange(async (value) => { + this.plugin.settings[settingsType].orderedDelimiter = value; + await this.plugin.saveSettings(); + }) + ); + + //* orderedTrailingDelimiter + new Setting(containerEl) + .setName("Trailing delimiter") + .setDesc("Set whether to add a trailing delimiter for ordered list.") + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings[settingsType].orderedTrailingDelimiter) + .onChange(async (value) => { + this.plugin.settings[settingsType].orderedTrailingDelimiter = value; + await this.plugin.saveSettings(); + }) + ); + + //* orderedCustomIdents + new Setting(containerEl) + .setName("Custom list styles") + .setDesc( + 'For the "Custom list styles" option. Set custom list styles for ordered list. Use spaces to separate entries.' + ) + .addText((text) => + text + .setValue(this.plugin.settings[settingsType].orderedCustomIdents) + .onChange(async (value) => { + this.plugin.settings[settingsType].orderedCustomIdents = value; + await this.plugin.saveSettings(); + }) + ); + + //* orderedSpecifiedString + new Setting(containerEl) + .setName("Specified string") + .setDesc( + 'For the "Specified string" option. Set a specified string for ordered list.' + ) + .addText((text) => + text + .setValue(this.plugin.settings[settingsType].orderedSpecifiedString) + .onChange(async (value) => { + this.plugin.settings[settingsType].orderedSpecifiedString = value; + await this.plugin.saveSettings(); + }) + ); + + //* orderedAllowZeroLevel + new Setting(containerEl) + .setName("Allow zero level") + .setDesc( + "If the next heading is more than one level higher, the omitted level is zero instead of one." + ) + .addToggle((toggle) => + toggle + .setValue( + this.plugin.settings[settingsType].orderedAllowZeroLevel ?? false + ) + .onChange(async (value) => { + this.plugin.settings[settingsType].orderedAllowZeroLevel = value; + await this.plugin.saveSettings(); + }) + ); + + //* orderedBasedOnExisting + new Setting(containerEl) + .setName("Based on the existing highest level") + .setDesc( + "Use the highest level of headings in the note as the base for ordered list." + ) + .addToggle((toggle) => + toggle + .setValue( + this.plugin.settings[settingsType].orderedBasedOnExisting ?? false + ) + .onChange(async (value) => { + this.plugin.settings[settingsType].orderedBasedOnExisting = value; + await this.plugin.saveSettings(); + }) + ); + + //* orderedIgnoreSingle + new Setting(containerEl) + .setName("Ignore the single heading at the top-level") + .setDesc( + "If the top-level has only a single heading, exclude it when building an ordered list." + ) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings[settingsType].orderedIgnoreSingle) + .onChange(async (value) => { + this.plugin.settings[settingsType].orderedIgnoreSingle = value; + await this.plugin.saveSettings(); + }) + ); + + //* orderedIgnoreMaximum + new Setting(containerEl) + .setName("The maximum number of ignored") + .setDesc( + "For enabled: Ignore the single heading at the top-level. The maximum number of ignored headings at the top-level." + ) + .addSlider((slider) => + slider + .setLimits(1, 6, 1) + .setValue( + this.plugin.settings[settingsType].orderedIgnoreMaximum ?? 6 + ) + .onChange(async (value) => { + this.plugin.settings[settingsType].orderedIgnoreMaximum = value; + await this.plugin.saveSettings(); + }) + .setDynamicTooltip() + ); + + new Setting(containerEl).setName("Unordered").setHeading(); + + //* unorderedLevelHeadings + new Setting(containerEl) + .setName("Level headings") + .setDesc( + "Set the names for each level. The value is 6 entries separated by spaces." + ) + .addText((text) => + text + .setValue(this.plugin.settings[settingsType].unorderedLevelHeadings) + .onChange(async (value) => { + this.plugin.settings[settingsType].unorderedLevelHeadings = value; + await this.plugin.saveSettings(); + }) + ); + + if (settingsType === "sourceSettings") { + new Setting(containerEl).setName("Other").setHeading(); + + //* hideNumberSigns + new Setting(containerEl) + .setName("Hide number signs on inactive lines") + .setDesc( + "Hide number signs (#) on inactive lines similar to live preview." + ) + .addToggle((toggle) => { + toggle + .setValue( + this.plugin.settings[settingsType].hideNumberSigns ?? false + ) + .onChange(async (value) => { + this.plugin.settings[settingsType].hideNumberSigns = value; + await this.plugin.saveSettings(); + }); + }); + } + + //* Scroll back to the top + containerEl.scrollTo({ top: 0, behavior: "smooth" }); + } + + private manageFolderBlacklist(scrollToTop = false) { + const { containerEl } = this; + + containerEl.empty(); + + new Setting(containerEl) + .setName("Manage folder blocklist") + .setHeading() + .addButton((button) => { + button.setButtonText("Back").onClick(() => { + this.display(); + }); + }); + + this.plugin.settings.folderBlacklist.forEach((folder, index) => { + new Setting(containerEl) + .setName(`Folder blocklist ${index + 1}`) + .addText((text) => { + text.setValue(folder).onChange(async (value) => { + this.plugin.settings.folderBlacklist[index] = value; + await this.plugin.saveSettings(); + }); + + const suggest = new FolderSuggest(this.app, text.inputEl); + suggest.onSelect(async (value) => { + text.setValue(value); + this.plugin.settings.folderBlacklist[index] = value; + suggest.close(); + await this.plugin.saveSettings(); + }); + }) + .addButton((button) => { + button + .setButtonText("Delete") + .setWarning() + .onClick(async () => { + this.plugin.settings.folderBlacklist.splice(index, 1); + await this.plugin.saveSettings(); + this.manageFolderBlacklist(); + }); + }); + }); + + new Setting(containerEl).addButton((button) => { + button + .setButtonText("Add") + .setCta() + .setTooltip("Add a new folder to the blocklist") + .onClick(async () => { + this.plugin.settings.folderBlacklist.push(""); + await this.plugin.saveSettings(); + this.manageFolderBlacklist(); + }); + }); + + //* Scroll back to the top + if (scrollToTop) { + containerEl.scrollTo({ top: 0, behavior: "smooth" }); + } + } + + private manageFileRegexBlacklist(scrollToTop = false) { + const { containerEl } = this; + + containerEl.empty(); + + new Setting(containerEl) + .setName("Manage note name regex blocklist") + .setHeading() + .addButton((button) => { + button.setButtonText("Back").onClick(() => { + this.display(); + }); + }); + + this.plugin.settings.fileRegexBlacklist.forEach((regex, index) => { + new Setting(containerEl) + .setName(`Note name regex blocklist ${index + 1}`) + .addText((text) => + text + .setPlaceholder("e.g., /^daily.*/i") + .setValue(regex) + .onChange(async (value) => { + this.plugin.settings.fileRegexBlacklist[index] = value.trim(); + await this.plugin.saveSettings(); + }) + ) + .addButton((button) => { + button + .setButtonText("Delete") + .setWarning() + .onClick(async () => { + this.plugin.settings.fileRegexBlacklist.splice(index, 1); + await this.plugin.saveSettings(); + this.manageFileRegexBlacklist(); + }); + }); + }); + + new Setting(containerEl).addButton((button) => { + button + .setButtonText("Add") + .setCta() + .setTooltip("Add a new note name regex blocklist") + .onClick(async () => { + this.plugin.settings.fileRegexBlacklist.push(""); + await this.plugin.saveSettings(); + this.manageFileRegexBlacklist(); + }); + }); + + //* Scroll back to the top + if (scrollToTop) { + containerEl.scrollTo({ top: 0, behavior: "smooth" }); + } + } +} diff --git a/main.ts b/main.ts index c0d9459..24df7f4 100644 --- a/main.ts +++ b/main.ts @@ -1,1662 +1,3 @@ -import { - App, - Editor, - FrontMatterCache, - MarkdownView, - Plugin, - PluginSettingTab, - Setting, - TFile, - WorkspaceLeaf, - debounce, -} from "obsidian"; -import { EditorView, ViewPlugin } from "@codemirror/view"; -import type { - HeadingPluginSettings, - OrderedCounterStyleType, - HeadingPluginData, -} from "./common/data"; -import { - headingsSelector, - orderedStyleTypeOptions, - defaultHeadingDecoratorSettings, - defaultSourceHeadingDecoratorSettings, - getUnorderedLevelHeadings, - getOrderedCustomIdents, - diffLevel, - getBoolean, - checkEnabledCSS, - stringToRegex, -} from "./common/data"; -import { Counter, Querier } from "./common/counter"; -import { Heading } from "./common/heading"; -import { - decorateHTMLElement, - decorateOutlineElement, - cancelOutlineDecorator, - decorateFileHeadingElement, - cancelFileHeadingDecorator, - queryHeadingLevelByElement, - getTreeItemLevel, - getTreeItemText, - getFileHeadingItemLevel, - compareHeadingText, -} from "./common/dom"; -import { - HeadingViewPlugin, - headingDecorationsField, - editorModeField, - updateEditorMode, -} from "./components/view"; -import { ViewChildComponent } from "./components/child"; -import { FolderSuggest } from "./components/suggest"; -import { - quietOutlineHandler, - cancelQuietOutlineDecoration, -} from "./components/quiet-outline"; +import { HeadingPlugin } from "./components/plugin"; -interface ObsidianEditor extends Editor { - cm: EditorView; -} - -interface ObsidianWorkspaceLeaf extends WorkspaceLeaf { - id: string; -} - -interface SettingsChangeState { - outline?: boolean; - quietOutline?: boolean; - fileExplorer?: boolean; -} - -const DEFAULT_SETTINGS: HeadingPluginSettings = { - metadataKeyword: "heading", - fileRegexBlacklist: [], - folderBlacklist: [], - enabledInReading: true, - readingSettings: defaultHeadingDecoratorSettings(), - enabledInPreview: true, - previewSettings: defaultHeadingDecoratorSettings(), - enabledInSource: false, - sourceSettings: defaultSourceHeadingDecoratorSettings(), - enabledInOutline: false, - outlineSettings: defaultHeadingDecoratorSettings(), - enabledInQuietOutline: false, - quietOutlineSettings: defaultHeadingDecoratorSettings(), - enabledInFileExplorer: false, - fileExplorerSettings: defaultHeadingDecoratorSettings(), -}; - -export default class HeadingPlugin extends Plugin { - settings: HeadingPluginSettings; - - private outlineIdSet: Set = new Set(); - private outlineComponents: ViewChildComponent[] = []; - - private quietOutlineIdSet: Set = new Set(); - private quietOutlineComponents: ViewChildComponent[] = []; - - private fileExplorerIdSet: Set = new Set(); - private fileExplorerComponents: ViewChildComponent[] = []; - - async onload() { - await this.loadSettings(); - - // Register editor extension - this.registerEditorExtension([ - headingDecorationsField, - editorModeField, - this.craeteHeadingViewPlugin(this.getPluginData.bind(this)), - ]); - - // Register markdown post processor - this.registerMarkdownPostProcessor(async (element, context) => { - if (!this.settings.enabledInReading) { - return; - } - - const metadataEnabled = this.getEnabledFromFrontmatter( - "reading", - context.frontmatter - ); - - if (metadataEnabled == null) { - if ( - this.settings.readingSettings.enabledInEachNote != undefined && - !this.settings.readingSettings.enabledInEachNote - ) { - return; - } - - if (this.getEnabledFromBlacklist(context.sourcePath)) { - return; - } - } else if (!metadataEnabled) { - return; - } - - const headingElements = element.findAll(headingsSelector); - if (headingElements.length === 0) { - return; - } - - const { - readingSettings: { - opacity, - position, - ordered, - orderedDelimiter, - orderedTrailingDelimiter, - orderedStyleType, - orderedSpecifiedString, - orderedCustomIdents, - orderedIgnoreSingle, - orderedIgnoreMaximum = 6, - orderedBasedOnExisting, - orderedAllowZeroLevel, - unorderedLevelHeadings, - }, - } = this.settings; - - if (ordered) { - const file = this.getActiveFile(context.sourcePath); - if (!file) { - return; - } - - const sourceContent = await this.app.vault.cachedRead(file); - const sourceArr = sourceContent.split("\n"); - if (sourceArr.length === 0) { - return; - } - - let ignoreTopLevel = 0; - if (orderedIgnoreSingle || orderedBasedOnExisting) { - const queier = new Querier(orderedAllowZeroLevel); - const heading = new Heading(); - for (let lineIndex = 1; lineIndex <= sourceArr.length; lineIndex++) { - const lineText = sourceArr[lineIndex - 1]; - const nextLineIndex = lineIndex + 1; - const nextLineText = - nextLineIndex <= sourceArr.length - ? sourceArr[nextLineIndex - 1] - : ""; - const level = heading.handler(lineIndex, lineText, nextLineText); - if (level === -1) { - continue; - } - - queier.handler(level); - - ignoreTopLevel = queier.query( - orderedIgnoreSingle, - orderedIgnoreMaximum - ); - if (ignoreTopLevel === 0) { - break; - } - } - } - - const counter = new Counter({ - ordered: true, - delimiter: orderedDelimiter, - trailingDelimiter: orderedTrailingDelimiter, - styleType: orderedStyleType, - customIdents: getOrderedCustomIdents(orderedCustomIdents), - specifiedString: orderedSpecifiedString, - ignoreTopLevel, - allowZeroLevel: orderedAllowZeroLevel, - }); - const heading = new Heading(); - let headingIndex = 1; - - headingElements.forEach((headingElement) => { - const sectionInfo = context.getSectionInfo(headingElement); - if (!sectionInfo) { - return; - } - - const lineStart = sectionInfo.lineStart + 1; - if (lineStart > sourceArr.length) { - return; - } - - for ( - let lineIndex = headingIndex; - lineIndex <= lineStart; - lineIndex++ - ) { - const lineText = sourceArr[lineIndex - 1]; - const nextLineIndex = lineIndex + 1; - const nextLineText = - nextLineIndex <= sourceArr.length - ? sourceArr[nextLineIndex - 1] - : ""; - const level = heading.handler(lineIndex, lineText, nextLineText); - if (lineIndex === lineStart) { - //? 1. When using the page preview feature to only reference fragments, the - //? relative position of rows is incorrect. - //? 2. In the split editing scenario, there may be a delay in the information - //? on the reading tab. - const elementLevel = queryHeadingLevelByElement(headingElement); - if (elementLevel === level) { - const decoratorContent = counter.decorator(level); - decorateHTMLElement( - headingElement, - decoratorContent, - opacity, - position - ); - } - - headingIndex = lineIndex + 1; - } else { - counter.handler(level); - } - } - }); - } else { - const counter = new Counter({ - ordered: false, - levelHeadings: getUnorderedLevelHeadings(unorderedLevelHeadings), - }); - - headingElements.forEach((headingElement) => { - const level = queryHeadingLevelByElement(headingElement); - const decoratorContent = counter.decorator(level); - decorateHTMLElement( - headingElement, - decoratorContent, - opacity, - position - ); - }); - } - }); - - // Listen for metadata changes - this.registerEvent( - this.app.metadataCache.on( - "changed", - debounce( - (file) => { - //! Delay trigger rerender - if (file && file.path === this.getActiveFile()?.path) { - this.rerenderPreviewMarkdown(file); - } - }, - 250, - true - ) - ) - ); - - // Listen for editor mode changes - this.registerEvent( - this.app.workspace.on("active-leaf-change", () => { - this.handleModeChange(); - this.loadOutlineComponents(); - this.loadQuietOutlineComponents(); - this.loadFileExplorerComponents(); - }) - ); - this.registerEvent( - this.app.workspace.on("layout-change", () => { - this.handleModeChange(); - this.loadOutlineComponents(); - this.loadQuietOutlineComponents(); - this.loadFileExplorerComponents(); - }) - ); - - this.loadOutlineComponents(); - this.loadQuietOutlineComponents(); - this.loadFileExplorerComponents(); - - this.addSettingTab(new HeadingSettingTab(this.app, this)); - } - - onunload(): void { - this.unloadOutlineComponents(); - this.unloadQuietOutlineComponents(); - this.unloadFileExplorerComponents(); - } - - async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } - - async saveSettings(state?: SettingsChangeState) { - await this.saveData(this.settings); - - if (state) { - if (state.outline != undefined) { - if (state.outline) { - this.loadOutlineComponents(); - } else { - this.unloadOutlineComponents(); - } - } - - if (state.quietOutline != undefined) { - if (state.quietOutline) { - this.loadQuietOutlineComponents(); - } else { - this.unloadQuietOutlineComponents(); - } - } - - if (state.fileExplorer != undefined) { - if (state.fileExplorer) { - this.loadFileExplorerComponents(); - } else { - this.unloadFileExplorerComponents(); - } - } - } - - this.debouncedSaveSettings(); - } - - private loadOutlineComponents() { - if (this.settings.enabledInOutline) { - this.handleOutline(); - } - } - - private unloadOutlineComponents() { - this.outlineComponents.forEach((child) => child.detach()); - this.outlineComponents = []; - this.outlineIdSet.clear(); - } - - private loadQuietOutlineComponents() { - if (this.settings.enabledInQuietOutline) { - this.handleQuietOutline(); - } - } - - private unloadQuietOutlineComponents() { - this.quietOutlineComponents.forEach((child) => child.detach()); - this.quietOutlineComponents = []; - this.quietOutlineIdSet.clear(); - } - - private loadFileExplorerComponents() { - if (this.settings.enabledInFileExplorer) { - this.handleHeadingsInExplorer(); - } - } - - private unloadFileExplorerComponents() { - this.fileExplorerComponents.forEach((child) => child.detach()); - this.fileExplorerComponents = []; - this.fileExplorerIdSet.clear(); - } - - private craeteHeadingViewPlugin( - getPluginData: () => Promise - ) { - return ViewPlugin.fromClass( - class extends HeadingViewPlugin { - constructor(view: EditorView) { - super(view, getPluginData); - } - } - ); - } - - private handleModeChange() { - const view = this.app.workspace.getActiveViewOfType(MarkdownView); - if (view) { - //? "source" for editing view (Live Preview & Source mode), - //? "preview" for reading view. - const isLivePreview = view.getMode() === "source"; - - //* Get CodeMirror editor instance - const cmEditor = (view.editor as ObsidianEditor).cm; - if (cmEditor) { - cmEditor.dispatch({ - effects: updateEditorMode.of(isLivePreview), - }); - } - } - } - - private handleOutline() { - const leaves = this.app.workspace.getLeavesOfType("outline"); - leaves.forEach((leaf: ObsidianWorkspaceLeaf) => { - const leafId = leaf.id; - if (!leafId || this.outlineIdSet.has(leafId)) { - return; - } - - const view = leaf.view; - const viewContent = view.containerEl.querySelector( - '[data-type="outline"] .view-content' - ); - if (!viewContent) { - return; - } - - this.outlineIdSet.add(leafId); - - const vc = new ViewChildComponent( - leafId, - view, - viewContent, - () => { - const headingElements = - viewContent.querySelectorAll(".tree-item"); - if (headingElements.length === 0) { - return; - } - - const state = view.getState(); - if (typeof state.file !== "string") { - return; - } - - const file = this.app.vault.getFileByPath(state.file); - if (!file) { - return; - } - - const fileCache = this.app.metadataCache.getFileCache(file); - if (!fileCache) { - return; - } - - const cacheHeadings = fileCache.headings || []; - if (cacheHeadings.length === 0) { - return; - } - - const frontmatter = fileCache.frontmatter; - const metadataEnabled = this.getEnabledFromFrontmatter( - "outline", - frontmatter - ); - - const { - enabledInEachNote, - opacity, - position, - ordered, - orderedDelimiter, - orderedTrailingDelimiter, - orderedStyleType, - orderedSpecifiedString, - orderedCustomIdents, - orderedIgnoreSingle, - orderedIgnoreMaximum = 6, - orderedBasedOnExisting, - orderedAllowZeroLevel, - unorderedLevelHeadings, - } = this.settings.outlineSettings; - - if (metadataEnabled == null) { - if (enabledInEachNote != undefined && !enabledInEachNote) { - return; - } - - if (this.getEnabledFromBlacklist(state.file)) { - return; - } - } else if (!metadataEnabled) { - return; - } - - let ignoreTopLevel = 0; - if (ordered && (orderedIgnoreSingle || orderedBasedOnExisting)) { - const queier = new Querier(orderedAllowZeroLevel); - for (const cacheHeading of cacheHeadings) { - queier.handler(cacheHeading.level); - ignoreTopLevel = queier.query( - orderedIgnoreSingle, - orderedIgnoreMaximum - ); - if (ignoreTopLevel === 0) { - break; - } - } - } - - const counter = new Counter({ - ordered, - delimiter: orderedDelimiter, - trailingDelimiter: orderedTrailingDelimiter, - styleType: orderedStyleType, - customIdents: getOrderedCustomIdents(orderedCustomIdents), - specifiedString: orderedSpecifiedString, - ignoreTopLevel, - allowZeroLevel: orderedAllowZeroLevel, - levelHeadings: getUnorderedLevelHeadings(unorderedLevelHeadings), - }); - - let lastCacheLevel = 0; - let lastReadLevel = 0; - for ( - let i = 0, j = 0; - i < headingElements.length && j < cacheHeadings.length; - i++, j++ - ) { - const readLevel = getTreeItemLevel(headingElements[i]); - const readText = getTreeItemText(headingElements[i]); - let cacheLevel = cacheHeadings[j].level; - if (i > 0) { - const diff = diffLevel(readLevel, lastReadLevel); - while ( - j < cacheHeadings.length - 1 && - (diffLevel(cacheLevel, lastCacheLevel) !== diff || - !compareHeadingText(cacheHeadings[j].heading, readText)) - ) { - counter.handler(cacheLevel); - j++; - cacheLevel = cacheHeadings[j].level; - } - } - - const decoratorContent = counter.decorator(cacheLevel); - decorateOutlineElement( - headingElements[i], - decoratorContent, - opacity, - position - ); - - lastCacheLevel = cacheLevel; - lastReadLevel = readLevel; - } - }, - () => { - const headingElements = - viewContent.querySelectorAll(".tree-item"); - headingElements.forEach((ele) => { - cancelOutlineDecorator(ele); - }); - } - ); - - this.outlineComponents.push(vc); - view.addChild(vc); - view.register(() => { - this.outlineComponents = this.outlineComponents.filter( - (item) => !item.equal(leafId) - ); - }); - }); - } - - private handleQuietOutline() { - const leaves = this.app.workspace.getLeavesOfType("quiet-outline"); - leaves.forEach((leaf: ObsidianWorkspaceLeaf) => { - const leafId = leaf.id; - if (!leafId || this.quietOutlineIdSet.has(leafId)) { - return; - } - - const view = leaf.view; - const viewContent = view.containerEl.querySelector( - '[data-type="quiet-outline"] .view-content' - ); - if (!viewContent) { - return; - } - - this.quietOutlineIdSet.add(leafId); - - const vc = new ViewChildComponent( - leafId, - view, - viewContent, - () => { - const containerElement = - viewContent.querySelector(".n-tree"); - if (!containerElement) { - return; - } - - const headingELements = - viewContent.querySelectorAll(".n-tree-node"); - if (headingELements.length === 0) { - return; - } - - const file = this.getActiveFile(); - if (!file) { - return; - } - - const fileCache = this.app.metadataCache.getFileCache(file); - if (!fileCache) { - return; - } - - const frontmatter = fileCache.frontmatter; - const metadataEnabled = this.getEnabledFromFrontmatter( - "quiet-outline", - frontmatter - ); - - const { enabledInEachNote } = this.settings.quietOutlineSettings; - - let enabled = true; - if (metadataEnabled == null) { - if (enabledInEachNote != undefined && !enabledInEachNote) { - enabled = false; - } - - if (this.getEnabledFromBlacklist(file.path)) { - enabled = false; - } - } else if (!metadataEnabled) { - enabled = false; - } - - if (!enabled) { - cancelQuietOutlineDecoration(containerElement); - return; - } - - quietOutlineHandler( - this.settings.quietOutlineSettings, - containerElement, - headingELements - ); - }, - () => { - const containerElement = - viewContent.querySelector(".n-tree"); - if (containerElement) { - cancelQuietOutlineDecoration(containerElement); - } - } - ); - - this.quietOutlineComponents.push(vc); - view.addChild(vc); - view.register(() => { - this.quietOutlineComponents = this.quietOutlineComponents.filter( - (item) => !item.equal(leafId) - ); - }); - }); - } - - private handleHeadingsInExplorer() { - const leaves = this.app.workspace.getLeavesOfType("file-explorer"); - leaves.forEach((leaf: ObsidianWorkspaceLeaf) => { - const leafId = leaf.id; - if (!leafId || this.fileExplorerIdSet.has(leafId)) { - return; - } - - const view = leaf.view; - const navFilesContainer = view.containerEl.querySelector( - '[data-type="file-explorer"] .nav-files-container' - ); - if (!navFilesContainer) { - return; - } - - this.fileExplorerIdSet.add(leafId); - - const vc = new ViewChildComponent( - leafId, - view, - navFilesContainer, - () => { - const navFileTitles = - navFilesContainer.querySelectorAll(".nav-file-title"); - - navFileTitles.forEach((navFile) => { - const headingElements = navFile.querySelectorAll( - ".file-heading-container .clickable-heading" - ); - if (headingElements.length === 0) { - return; - } - - const filePath = navFile.dataset.path; - if (!filePath) { - return; - } - - const file = this.app.vault.getFileByPath(filePath); - if (!file) { - return; - } - - const fileCache = this.app.metadataCache.getFileCache(file); - if (!fileCache) { - return; - } - - const cacheHeadings = fileCache.headings || []; - if (cacheHeadings.length === 0) { - return; - } - - const frontmatter = fileCache.frontmatter; - const metadataEnabled = this.getEnabledFromFrontmatter( - "file-explorer", - frontmatter - ); - - const { - enabledInEachNote, - opacity, - position, - ordered, - orderedDelimiter, - orderedTrailingDelimiter, - orderedStyleType, - orderedSpecifiedString, - orderedCustomIdents, - orderedIgnoreSingle, - orderedIgnoreMaximum = 6, - orderedBasedOnExisting, - orderedAllowZeroLevel, - unorderedLevelHeadings, - } = this.settings.fileExplorerSettings; - - if (metadataEnabled == null) { - if (enabledInEachNote != undefined && !enabledInEachNote) { - return; - } - - if (this.getEnabledFromBlacklist(filePath)) { - return; - } - } else if (!metadataEnabled) { - return; - } - - let ignoreTopLevel = 0; - if (ordered && (orderedIgnoreSingle || orderedBasedOnExisting)) { - const queier = new Querier(orderedAllowZeroLevel); - for (const cacheHeading of cacheHeadings) { - queier.handler(cacheHeading.level); - ignoreTopLevel = queier.query( - orderedIgnoreSingle, - orderedIgnoreMaximum - ); - if (ignoreTopLevel === 0) { - break; - } - } - } - - const counter = new Counter({ - ordered, - delimiter: orderedDelimiter, - trailingDelimiter: orderedTrailingDelimiter, - styleType: orderedStyleType, - customIdents: getOrderedCustomIdents(orderedCustomIdents), - specifiedString: orderedSpecifiedString, - ignoreTopLevel, - allowZeroLevel: orderedAllowZeroLevel, - levelHeadings: getUnorderedLevelHeadings(unorderedLevelHeadings), - }); - - const marginMultiplier = - parseInt( - getComputedStyle(document.body).getPropertyValue( - "--clickable-heading-margin-multiplier" - ) - ) || 10; - - for ( - let i = 0, j = 0; - i < headingElements.length && j < cacheHeadings.length; - i++, j++ - ) { - const readLevel = getFileHeadingItemLevel( - headingElements[i], - marginMultiplier - ); - const readText = headingElements[i].innerText; - let cacheLevel = cacheHeadings[j].level; - - while ( - j < cacheHeadings.length - 1 && - (cacheLevel !== readLevel || - cacheHeadings[j].heading !== readText) - ) { - counter.handler(cacheLevel); - j++; - cacheLevel = cacheHeadings[j].level; - } - - const decoratorContent = counter.decorator(cacheLevel); - decorateFileHeadingElement( - headingElements[i], - decoratorContent, - opacity, - position - ); - } - }); - }, - () => { - const headingElements = - navFilesContainer.querySelectorAll( - ".nav-file-title .file-heading-container .clickable-heading" - ); - headingElements.forEach((ele) => { - cancelFileHeadingDecorator(ele); - }); - } - ); - - this.fileExplorerComponents.push(vc); - view.addChild(vc); - view.register(() => { - this.fileExplorerComponents = this.fileExplorerComponents.filter( - (item) => !item.equal(leafId) - ); - }); - }); - } - - private debouncedSaveSettings = debounce( - this.rerenderPreviewMarkdown.bind(this), - 1000, - true - ); - - /** - * Rerender Preview Markdown. - * @param file - */ - private rerenderPreviewMarkdown(file?: TFile) { - const view = this.app.workspace.getActiveViewOfType(MarkdownView); - if (view) { - if (!file || file === view.file) { - view.previewMode.rerender(true); - } - } - } - - async getPluginData(): Promise { - const { - metadataKeyword, - enabledInPreview: _enabledInPreview, - enabledInSource: _enabledInSource, - previewSettings, - sourceSettings, - } = this.settings; - - let enabledInPreview = _enabledInPreview; - let enabledInSource = _enabledInSource; - - if (metadataKeyword) { - const file = this.getActiveFile(); - if (file) { - const frontmatter = - this.app.metadataCache.getFileCache(file)?.frontmatter; - - if (_enabledInPreview) { - enabledInPreview = - this.getEnabledFromFrontmatter("preview", frontmatter) ?? - previewSettings.enabledInEachNote ?? - !this.getEnabledFromBlacklist(file.path); - } - - if (_enabledInSource) { - enabledInSource = - this.getEnabledFromFrontmatter("source", frontmatter) ?? - sourceSettings.enabledInEachNote ?? - !this.getEnabledFromBlacklist(file.path); - } - } - } - - return { - enabledInPreview, - enabledInSource, - previewSettings, - sourceSettings, - }; - } - - private getActiveFile(sourcePath?: string) { - if (sourcePath) { - return this.app.vault.getFileByPath(sourcePath); - } else { - return this.app.workspace.getActiveFile(); - } - } - - private getEnabledFromFrontmatter( - mode: HeadingMetadataSettingsType, - frontmatter?: FrontMatterCache - ): null | boolean { - const keyword = this.settings.metadataKeyword; - if (keyword && typeof frontmatter === "object" && frontmatter) { - const metadataSettings = frontmatter[keyword]; - if (typeof metadataSettings === "object" && metadataSettings) { - return ( - getBoolean(metadataSettings[mode]) ?? getBoolean(metadataSettings.all) - ); - } - if (metadataSettings != null) { - return getBoolean(metadataSettings); - } else { - const cssclasses = frontmatter.cssclasses; - if (Array.isArray(cssclasses)) { - const cssStatus: CSSEnabledStatus = { - mode: null, - all: null, - }; - - for (const cssItem of cssclasses) { - if (typeof cssItem === "string") { - const { mode: m, all: a } = checkEnabledCSS(cssItem, mode); - if (m != null) { - cssStatus.mode = m; - } - if (a != null) { - cssStatus.all = a; - } - } - } - - return cssStatus.mode ?? cssStatus.all; - } else if (typeof cssclasses === "string") { - const { mode: m, all: a } = checkEnabledCSS(cssclasses, mode); - return m ?? a; - } - } - } - - return null; - } - - private getEnabledFromBlacklist(filepath: string): boolean { - for (const folder of this.settings.folderBlacklist) { - if (filepath.startsWith(`${folder}/`)) { - return true; - } - } - - const filename = filepath.substring( - filepath.lastIndexOf("/") + 1, - filepath.lastIndexOf(".") - ); - for (const regexStr of this.settings.fileRegexBlacklist) { - const regex = stringToRegex(regexStr); - if (regex) { - if (regex.test(filename)) { - return true; - } - } - } - - return false; - } -} - -class HeadingSettingTab extends PluginSettingTab { - plugin: HeadingPlugin; - - constructor(app: App, plugin: HeadingPlugin) { - super(app, plugin); - this.plugin = plugin; - } - - display(): void { - const { containerEl } = this; - - containerEl.empty(); - - //* metadataKeyword - const metadataKeywordSetting = new Setting(containerEl) - .setName("Metadata keyword") - .addText((text) => - text - .setPlaceholder("Enter the keyword") - .setValue(this.plugin.settings.metadataKeyword) - .onChange(async (value) => { - this.plugin.settings.metadataKeyword = value.trim(); - await this.plugin.saveSettings(); - }) - ); - - const metadataKeywordDesc = createFragment(); - metadataKeywordDesc.append( - "The key name that reads the enabled status from the ", - createEl("a", { - href: "https://help.obsidian.md/Editing+and+formatting/Properties", - text: "properties", - }), - "." - ); - metadataKeywordSetting.descEl.appendChild(metadataKeywordDesc); - - new Setting(containerEl).setName("Reading view").setHeading(); - - //* enabledInReading - new Setting(containerEl) - .setName("Enabled in reading view") - .setDesc("Allow to decorate the heading under the reading view.") - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.enabledInReading) - .onChange(async (value) => { - this.plugin.settings.enabledInReading = value; - await this.plugin.saveSettings(); - }) - ); - - new Setting(containerEl) - .setName("Manage reading view heading decorator") - .addButton((button) => { - button.setButtonText("Manage").onClick(() => { - this.manageHeadingDecoratorSettings("readingSettings"); - }); - }); - - new Setting(containerEl).setName("Live preview").setHeading(); - - //* enabledInPreview - new Setting(containerEl) - .setName("Enabled in live preview") - .setDesc("Allow to decorate the heading under the live preview.") - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.enabledInPreview) - .onChange(async (value) => { - this.plugin.settings.enabledInPreview = value; - await this.plugin.saveSettings(); - }) - ); - - new Setting(containerEl) - .setName("Manage live preview heading decorator") - .addButton((button) => { - button.setButtonText("Manage").onClick(() => { - this.manageHeadingDecoratorSettings("previewSettings"); - }); - }); - - new Setting(containerEl).setName("Source mode").setHeading(); - - //* enabledInSource - new Setting(containerEl) - .setName("Enabled in source mode") - .setDesc("Allow to decorate the heading under the source mode.") - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.enabledInSource) - .onChange(async (value) => { - this.plugin.settings.enabledInSource = value; - await this.plugin.saveSettings(); - }) - ); - - new Setting(containerEl) - .setName("Manage source mode heading decorator") - .addButton((button) => { - button.setButtonText("Manage").onClick(() => { - this.manageHeadingDecoratorSettings("sourceSettings"); - }); - }); - - new Setting(containerEl).setName("Outline plugin").setHeading(); - - //* enabledInOutline - new Setting(containerEl) - .setName("Enabled in outline plugin") - .setDesc("Allow to decorate the heading under the outline plugin.") - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.enabledInOutline) - .onChange(async (value) => { - this.plugin.settings.enabledInOutline = value; - await this.plugin.saveSettings({ - outline: this.plugin.settings.enabledInOutline, - }); - }) - ); - - new Setting(containerEl) - .setName("Manage outline plugin heading decorator") - .addButton((button) => { - button.setButtonText("Manage").onClick(() => { - this.manageHeadingDecoratorSettings("outlineSettings"); - }); - }); - - new Setting(containerEl).setName("Quiet Outline plugin").setHeading(); - - //* enabledInQuietOutline - const enabledInQuietOutlineSetting = new Setting(containerEl) - .setName('Enabled in "Quiet Outline" plugin') - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.enabledInQuietOutline) - .onChange(async (value) => { - this.plugin.settings.enabledInQuietOutline = value; - await this.plugin.saveSettings({ - quietOutline: this.plugin.settings.enabledInQuietOutline, - }); - }) - ); - - const enabledInQuietOutlineDesc = createFragment(); - enabledInQuietOutlineDesc.append( - "Allow to decorate the heading under the ", - createEl("a", { - href: "https://github.com/guopenghui/obsidian-quiet-outline", - text: "Quiet Outline", - }), - " plugin." - ); - enabledInQuietOutlineSetting.descEl.appendChild(enabledInQuietOutlineDesc); - - new Setting(containerEl) - .setName('Manage "Quiet Outline" plugin heading decorator') - .addButton((button) => { - button.setButtonText("Manage").onClick(() => { - this.manageHeadingDecoratorSettings("quietOutlineSettings"); - }); - }); - - new Setting(containerEl) - .setName("Headings in Explorer plugin") - .setHeading(); - - //* enabledInFileExplorer - const enabledInFileExplorerSetting = new Setting(containerEl) - .setName('Enabled in "Headings in Explorer" plugin') - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.enabledInFileExplorer) - .onChange(async (value) => { - this.plugin.settings.enabledInFileExplorer = value; - await this.plugin.saveSettings({ - fileExplorer: this.plugin.settings.enabledInFileExplorer, - }); - }) - ); - - const enabledInFileExplorerDesc = createFragment(); - enabledInFileExplorerDesc.append( - "Allow to decorate the heading under the ", - createEl("a", { - href: "https://github.com/patrickchiang/obsidian-headings-in-explorer", - text: "Headings in Explorer", - }), - " plugin." - ); - enabledInFileExplorerSetting.descEl.appendChild(enabledInFileExplorerDesc); - - new Setting(containerEl) - .setName('Manage "Headings in Explorer" plugin heading decorator') - .addButton((button) => { - button.setButtonText("Manage").onClick(() => { - this.manageHeadingDecoratorSettings("fileExplorerSettings"); - }); - }); - - new Setting(containerEl).setName("Blocklist").setHeading(); - - //* folderBlacklist - new Setting(containerEl) - .setName("Manage folder blocklist") - .addButton((button) => { - button.setButtonText("Manage").onClick(() => { - this.manageFolderBlacklist(true); - }); - }); - - //* fileRegexBlacklist - new Setting(containerEl) - .setName("Manage note name regex blocklist") - .addButton((button) => { - button.setButtonText("Manage").onClick(() => { - this.manageFileRegexBlacklist(true); - }); - }); - } - - private isOpacityValue(value: number): value is OpacityOptions { - if (value >= 10 && value <= 100 && value % 10 === 0) { - return true; - } - return false; - } - - private isPositionValue(value: string): value is PostionOptions { - return ["before", "after", "before-inside", "after-inside"].includes(value); - } - - private manageHeadingDecoratorSettings( - settingsType: PluginDecoratorSettingsType - ) { - const { containerEl } = this; - - containerEl.empty(); - - let tabName = ""; - switch (settingsType) { - case "readingSettings": - tabName = "Manage reading view heading decorator"; - break; - case "previewSettings": - tabName = "Manage live preview heading decorator"; - break; - case "sourceSettings": - tabName = "Manage source mode heading decorator"; - break; - case "outlineSettings": - tabName = "Manage outline plugin heading decorator"; - break; - case "quietOutlineSettings": - tabName = 'Manage "Quiet Outline" plugin heading decorator'; - break; - case "fileExplorerSettings": - tabName = 'Manage "Headings in Explorer" plugin heading decorator'; - break; - } - - new Setting(containerEl) - .setName(tabName) - .setHeading() - .addButton((button) => { - button.setButtonText("Back").onClick(() => { - this.display(); - }); - }); - - //* enabledInEachNote - new Setting(containerEl) - .setName("Enabled in each note") - .setDesc( - "Toggle this setting to enable the decoration of headings in each note." - ) - .addToggle((toggle) => - toggle - .setValue( - this.plugin.settings[settingsType].enabledInEachNote ?? true - ) - .onChange(async (value) => { - this.plugin.settings[settingsType].enabledInEachNote = value; - await this.plugin.saveSettings(); - }) - ); - - new Setting(containerEl).setName("Effect").setHeading(); - - //* ordered - new Setting(containerEl) - .setName("Ordered") - .setDesc( - "Toggle this setting to enable the decoration of headings as an ordered or unordered list." - ) - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings[settingsType].ordered) - .onChange(async (value) => { - this.plugin.settings[settingsType].ordered = value; - await this.plugin.saveSettings(); - }) - ); - - //* opacity - new Setting(containerEl) - .setName("Opacity") - .setDesc( - "Set the opacity of the heading decorator. The value is the form of percentage." - ) - .addSlider((slider) => - slider - .setLimits(10, 100, 10) - .setValue(this.plugin.settings[settingsType].opacity) - .onChange(async (value) => { - this.plugin.settings[settingsType].opacity = this.isOpacityValue( - value - ) - ? value - : 20; - await this.plugin.saveSettings(); - }) - .setDynamicTooltip() - ); - - //* position - new Setting(containerEl) - .setName("Position") - .setDesc("Set the position of the heading decorator.") - .addDropdown((dropdown) => { - const options: Record = - settingsType === "outlineSettings" || - settingsType === "quietOutlineSettings" || - settingsType === "fileExplorerSettings" - ? { - before: "Before the heading", - after: "After the heading", - } - : { - before: "Before the heading", - "before-inside": "Before the heading (inside)", - after: "After the heading", - "after-inside": "After the heading (inside)", - }; - - dropdown - .addOptions(options) - .setValue(this.plugin.settings[settingsType].position) - .onChange(async (value) => { - this.plugin.settings[settingsType].position = this.isPositionValue( - value - ) - ? value - : "before"; - await this.plugin.saveSettings(); - }); - }); - - new Setting(containerEl).setName("Ordered").setHeading(); - - //* orderedStyleType - new Setting(containerEl) - .setName("Style type") - .setDesc("Set the style type for ordered list.") - .addDropdown((dropdown) => - dropdown - .addOptions(orderedStyleTypeOptions) - .setValue(this.plugin.settings[settingsType].orderedStyleType) - .onChange(async (value) => { - this.plugin.settings[settingsType].orderedStyleType = - value as OrderedCounterStyleType; - await this.plugin.saveSettings(); - }) - ); - - //* orderedDelimiter - new Setting(containerEl) - .setName("Delimiter") - .setDesc("Set the delimiter for ordered list.") - .addText((text) => - text - .setValue(this.plugin.settings[settingsType].orderedDelimiter) - .onChange(async (value) => { - this.plugin.settings[settingsType].orderedDelimiter = value; - await this.plugin.saveSettings(); - }) - ); - - //* orderedTrailingDelimiter - new Setting(containerEl) - .setName("Trailing delimiter") - .setDesc("Set whether to add a trailing delimiter for ordered list.") - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings[settingsType].orderedTrailingDelimiter) - .onChange(async (value) => { - this.plugin.settings[settingsType].orderedTrailingDelimiter = value; - await this.plugin.saveSettings(); - }) - ); - - //* orderedCustomIdents - new Setting(containerEl) - .setName("Custom list styles") - .setDesc( - 'For the "Custom list styles" option. Set custom list styles for ordered list. Use spaces to separate entries.' - ) - .addText((text) => - text - .setValue(this.plugin.settings[settingsType].orderedCustomIdents) - .onChange(async (value) => { - this.plugin.settings[settingsType].orderedCustomIdents = value; - await this.plugin.saveSettings(); - }) - ); - - //* orderedSpecifiedString - new Setting(containerEl) - .setName("Specified string") - .setDesc( - 'For the "Specified string" option. Set a specified string for ordered list.' - ) - .addText((text) => - text - .setValue(this.plugin.settings[settingsType].orderedSpecifiedString) - .onChange(async (value) => { - this.plugin.settings[settingsType].orderedSpecifiedString = value; - await this.plugin.saveSettings(); - }) - ); - - //* orderedAllowZeroLevel - new Setting(containerEl) - .setName("Allow zero level") - .setDesc( - "If the next heading is more than one level higher, the omitted level is zero instead of one." - ) - .addToggle((toggle) => - toggle - .setValue( - this.plugin.settings[settingsType].orderedAllowZeroLevel ?? false - ) - .onChange(async (value) => { - this.plugin.settings[settingsType].orderedAllowZeroLevel = value; - await this.plugin.saveSettings(); - }) - ); - - //* orderedBasedOnExisting - new Setting(containerEl) - .setName("Based on the existing highest level") - .setDesc( - "Use the highest level of headings in the note as the base for ordered list." - ) - .addToggle((toggle) => - toggle - .setValue( - this.plugin.settings[settingsType].orderedBasedOnExisting ?? false - ) - .onChange(async (value) => { - this.plugin.settings[settingsType].orderedBasedOnExisting = value; - await this.plugin.saveSettings(); - }) - ); - - //* orderedIgnoreSingle - new Setting(containerEl) - .setName("Ignore the single heading at the top-level") - .setDesc( - "If the top-level has only a single heading, exclude it when building an ordered list." - ) - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings[settingsType].orderedIgnoreSingle) - .onChange(async (value) => { - this.plugin.settings[settingsType].orderedIgnoreSingle = value; - await this.plugin.saveSettings(); - }) - ); - - //* orderedIgnoreMaximum - new Setting(containerEl) - .setName("The maximum number of ignored") - .setDesc( - "For enabled: Ignore the single heading at the top-level. The maximum number of ignored headings at the top-level." - ) - .addSlider((slider) => - slider - .setLimits(1, 6, 1) - .setValue( - this.plugin.settings[settingsType].orderedIgnoreMaximum ?? 6 - ) - .onChange(async (value) => { - this.plugin.settings[settingsType].orderedIgnoreMaximum = value; - await this.plugin.saveSettings(); - }) - .setDynamicTooltip() - ); - - new Setting(containerEl).setName("Unordered").setHeading(); - - //* unorderedLevelHeadings - new Setting(containerEl) - .setName("Level headings") - .setDesc( - "Set the names for each level. The value is 6 entries separated by spaces." - ) - .addText((text) => - text - .setValue(this.plugin.settings[settingsType].unorderedLevelHeadings) - .onChange(async (value) => { - this.plugin.settings[settingsType].unorderedLevelHeadings = value; - await this.plugin.saveSettings(); - }) - ); - - if (settingsType === "sourceSettings") { - new Setting(containerEl).setName("Other").setHeading(); - - //* hideNumberSigns - new Setting(containerEl) - .setName("Hide number signs on inactive lines") - .setDesc( - "Hide number signs (#) on inactive lines similar to live preview." - ) - .addToggle((toggle) => { - toggle - .setValue( - this.plugin.settings[settingsType].hideNumberSigns ?? false - ) - .onChange(async (value) => { - this.plugin.settings[settingsType].hideNumberSigns = value; - await this.plugin.saveSettings(); - }); - }); - } - - //* Scroll back to the top - containerEl.scrollTo({ top: 0, behavior: "smooth" }); - } - - private manageFolderBlacklist(scrollToTop = false) { - const { containerEl } = this; - - containerEl.empty(); - - new Setting(containerEl) - .setName("Manage folder blocklist") - .setHeading() - .addButton((button) => { - button.setButtonText("Back").onClick(() => { - this.display(); - }); - }); - - this.plugin.settings.folderBlacklist.forEach((folder, index) => { - new Setting(containerEl) - .setName(`Folder blocklist ${index + 1}`) - .addText((text) => { - text.setValue(folder).onChange(async (value) => { - this.plugin.settings.folderBlacklist[index] = value; - await this.plugin.saveSettings(); - }); - - const suggest = new FolderSuggest(this.app, text.inputEl); - suggest.onSelect(async (value) => { - text.setValue(value); - this.plugin.settings.folderBlacklist[index] = value; - suggest.close(); - await this.plugin.saveSettings(); - }); - }) - .addButton((button) => { - button - .setButtonText("Delete") - .setWarning() - .onClick(async () => { - this.plugin.settings.folderBlacklist.splice(index, 1); - await this.plugin.saveSettings(); - this.manageFolderBlacklist(); - }); - }); - }); - - new Setting(containerEl).addButton((button) => { - button - .setButtonText("Add") - .setCta() - .setTooltip("Add a new folder to the blocklist") - .onClick(async () => { - this.plugin.settings.folderBlacklist.push(""); - await this.plugin.saveSettings(); - this.manageFolderBlacklist(); - }); - }); - - //* Scroll back to the top - if (scrollToTop) { - containerEl.scrollTo({ top: 0, behavior: "smooth" }); - } - } - - private manageFileRegexBlacklist(scrollToTop = false) { - const { containerEl } = this; - - containerEl.empty(); - - new Setting(containerEl) - .setName("Manage note name regex blocklist") - .setHeading() - .addButton((button) => { - button.setButtonText("Back").onClick(() => { - this.display(); - }); - }); - - this.plugin.settings.fileRegexBlacklist.forEach((regex, index) => { - new Setting(containerEl) - .setName(`Note name regex blocklist ${index + 1}`) - .addText((text) => - text - .setPlaceholder("e.g., /^daily.*/i") - .setValue(regex) - .onChange(async (value) => { - this.plugin.settings.fileRegexBlacklist[index] = value.trim(); - await this.plugin.saveSettings(); - }) - ) - .addButton((button) => { - button - .setButtonText("Delete") - .setWarning() - .onClick(async () => { - this.plugin.settings.fileRegexBlacklist.splice(index, 1); - await this.plugin.saveSettings(); - this.manageFileRegexBlacklist(); - }); - }); - }); - - new Setting(containerEl).addButton((button) => { - button - .setButtonText("Add") - .setCta() - .setTooltip("Add a new note name regex blocklist") - .onClick(async () => { - this.plugin.settings.fileRegexBlacklist.push(""); - await this.plugin.saveSettings(); - this.manageFileRegexBlacklist(); - }); - }); - - //* Scroll back to the top - if (scrollToTop) { - containerEl.scrollTo({ top: 0, behavior: "smooth" }); - } - } -} +export default HeadingPlugin;