import { App, Editor, EditorChange, EditorPosition, EditorTransaction, MarkdownFileInfo, MarkdownView, } from 'obsidian' import BindThemPlugin from './main' import { Heading } from './Entities' import { getMainSelection, selectionToRange } from './Utils' import { HEADING_REGEX } from './Constants' /** * ToggleHeading provides commands to toggle markdown heading levels */ export class ToggleHeading { public app: App private plugin: BindThemPlugin constructor(app: App, plugin: BindThemPlugin) { this.app = app this.plugin = plugin } /** * Create a change to set the heading level for a specific line */ private setHeading( editor: Editor, heading: Heading, line: number ): EditorChange { const headingStr = '#'.repeat(heading) const text = editor.getLine(line) const matches = HEADING_REGEX.exec(text)! const from: EditorPosition = { line: line, ch: 0, } const to: EditorPosition = { line: line, ch: matches[1].length + matches[2].length, } const replacementStr = heading === Heading.NORMAL ? '' : headingStr + ' ' return { from: from, to: to, text: replacementStr, } } /** * Get the heading level of a specific line */ private getHeadingOfLine(editor: Editor, line: number): Heading { const text = editor.getLine(line) const matches = HEADING_REGEX.exec(text)! return matches[1].length as Heading } /** * Toggle heading for the current selection(s) * If the line already has the specified heading, remove it (set to NORMAL) */ public toggleHeading( editor: Editor, view: MarkdownView | MarkdownFileInfo, heading: Heading ): void { const selections = editor.listSelections() const mainSelection = getMainSelection(editor) const mainRange = selectionToRange(mainSelection) const mainHeading = this.getHeadingOfLine(editor, mainRange.from.line) const targetHeading = heading === mainHeading ? Heading.NORMAL : heading const changes: Array = [] // Collect unique lines from all selections const linesToSet: Set = new Set() for (const selection of selections) { const range = selectionToRange(selection) for (let line = range.from.line; line <= range.to.line; line++) { linesToSet.add(line) } } // Create changes for each line for (const line of linesToSet) { if (editor.getLine(line) === '') { continue } changes.push(this.setHeading(editor, targetHeading, line)) } const transaction: EditorTransaction = { changes: changes, } editor.transaction(transaction, 'ToggleHeading' + heading.toString()) } /** * Toggle heading with stripping of inline formatting * This removes bold/italic formatting when toggling headings */ public toggleHeadingWithStrip( editor: Editor, view: MarkdownView | MarkdownFileInfo, level: number ): void { const cursor = editor.getCursor() const line = editor.getLine(cursor.line) const match = line.match(/^(#+)\s(.*)/) let content = line let currentLevel = 0 if (match) { currentLevel = match[1].length content = match[2] } // Strip bold and italic formatting content = content .replace(/(\*\*|__)(.*?)\1/g, '$2') .replace(/(\*|_)(.*?)\1/g, '$2') editor.setLine( cursor.line, currentLevel === level ? content : '#'.repeat(level) + ' ' + content ) } }