Developping an Obsidian Plugin

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

  1. Full Document Adjustment: Increase all headers in a document by 2 levels using the modal prompt.
  2. Range Adjustment: Decrease headers between line 5 and line 20 by 1 level using the modal prompt.
  3. 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

  1. 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 = [];
      }
    }
    
  2. 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;
     }
    
  3. 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);
         }
       });
     }
    
  4. 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 });
       });
     }