Developping an Obsidian Plugin
1- Motivation
The Header Adjuster Plugin was developed to address one of my key needs in managing Markdown documents within Obsidian. When integrating text from various sources into my notes, I often encountered the challenge of mismatched header levels. To streamline this process, I initially developed a Python script to adjust header levels efficiently. However, to make the solution more convenient and directly usable within Obsidian, I decided to integrate this functionality into a plugin. This way, I can adjust header levels seamlessly without leaving the Obsidian environment. You can find the Obsidian plugin on GitHub.
2- Key Features
- Increase Header Levels: Effortlessly increase the levels of headers by a specified number.
- Decrease Header Levels: Effortlessly decrease the levels of headers by a specified number.
- Range Adjustment: Specify start and end lines to adjust headers within a specific section of the document.
- Default Settings: Utilize default increase/decrease levels set in the plugin settings for quick adjustments.
- Commands: Access commands from the command palette to increase or decrease header levels with or without prompts.
- Ribbon Icon: Convenient ribbon icon with context menu for quick header level adjustments.
3- Usage Examples
- Full Document Adjustment: Increase all headers in a document by 2 levels using the modal prompt.
- Range Adjustment: Decrease headers between line 5 and line 20 by 1 level using the modal prompt.
- Default Settings: Quickly increase or decrease header levels using the default settings via the command palette.
4- Code Logic
The core functionality of the plugin revolves around maintaining the hierarchical structure of headers while adjusting their levels. To achieve this, we implemented a system that tracks both parent and child relationships for each header. This ensures that adjustments respect the document's hierarchy, preventing situations where a parent header would have a lower level than its child or vice versa.
4.1- Detailed Code Logic
HeaderObject Class: This class represents a header in the document. Each instance keeps track of its level, line number, parent, and children.
class HeaderObject { level: number; lineNumber: number; parent: HeaderObject | null; children: HeaderObject[]; constructor(level: number, lineNumber: number, parent: HeaderObject | null) { this.level = level; this.lineNumber = lineNumber; this.parent = parent; this.children = []; } }
Creating Header Objects: The
createHeaderObjects
method parses the document, identifies headers, and establishes parent-child relationships.createHeaderObjects(editor: Editor, startLine: number | null, endLine: number | null): HeaderObject[] { const headerPattern = /^(#{1,6})\s(.*)$/; const headerObjects: HeaderObject[] = []; let lastHeader: HeaderObject | null = null; for (let i = 0; i < editor.lineCount(); i++) { if ((startLine !== null && i + 1 < startLine) || (endLine !== null && i + 1 > endLine)) { continue; } const line = editor.getLine(i); const match = line.match(headerPattern); if (match) { const currentLevel = match[1].length; const newHeader = new HeaderObject(currentLevel, i + 1, null); if (lastHeader && currentLevel > lastHeader.level) { newHeader.parent = lastHeader; lastHeader.children.push(newHeader); } else if (lastHeader) { let parent = lastHeader.parent; while (parent && parent.level >= currentLevel) { parent = parent.parent; } newHeader.parent = parent; if (parent) { parent.children.push(newHeader); } } headerObjects.push(newHeader); lastHeader = newHeader; } } return headerObjects; }
Updating Header Levels: The
updateHeaderLevels
method adjusts the levels while ensuring hierarchical integrity.updateHeaderLevels(headerObjects: HeaderObject[], operation: 'increase' | 'decrease', levels: number) { headerObjects.forEach(header => { if (operation === 'decrease') { let newLevel = header.level - levels; if (header.parent && newLevel <= header.parent.level) { newLevel = header.parent.level + 1; } header.level = Math.max(newLevel, 1); } else if (operation === 'increase') { let newLevel = header.level + levels; header.children.forEach(child => { if (newLevel >= child.level) { newLevel = child.level - 1; } }); header.level = Math.min(newLevel, 6); } }); }
Applying Changes: The
applyHeaderChanges
method updates the document based on the new header levels.applyHeaderChanges(editor: Editor, headerObjects: HeaderObject[]) { headerObjects.forEach(header => { const line = editor.getLine(header.lineNumber - 1); const newHeader = '#'.repeat(header.level) + ' ' + line.replace(/^(#{1,6})\s/, ''); editor.replaceRange(newHeader, { line: header.lineNumber - 1, ch: 0 }, { line: header.lineNumber - 1, ch: line.length }); }); }