Repository: platers/obsidian-linter (ts)

Issue:
Bug: Header Increment Doesn't Properly Decrement when Decremented Heading Is One Less than Next Heading
When I open  the button “Start Header Increment at Heading Level 2”

Original display：
```
## HHELLO
#### HHELLO
## HHELLO
## HHELLO
## HHELLO
```
Expected display content：
```
## HHELLO
### HHELLO
## HHELLO
## HHELLO
## HHELLO
```

Actual display content：
```
## HHELLO
### HHELLO
## HHELLO
### HHELLO
### HHELLO
```
I don't know if it's a problem with my setup, or a bug. There have been attempts to disable and reset other plugins, but this is still the case. The obsidian version used is 1.1.9. Looking forward to guidance, thanks!



Pull request description:
Rewrite Header Increment to Better Handle Heading Level Groupings
Fixes #576 

Changes Made:
- Added a couple of UTs
- Fixed several UTs and examples where it was not written to be consistent
- Added a way to track which headings are a part of which heading level group and assign heading levels that way

Patch:
diff --git a/src/rules/header-increment.ts b/src/rules/header-increment.ts
index 62bd027..5d0d758 100644
--- a/src/rules/header-increment.ts
+++ b/src/rules/header-increment.ts
@@ -24,29 +24,52 @@ export default class HeaderIncrement extends RuleBuilder<HeaderIncrementOptions>
   }
   apply(text: string, options: HeaderIncrementOptions): string {
     return ignoreListOfTypes([IgnoreTypes.code, IgnoreTypes.math, IgnoreTypes.yaml, IgnoreTypes.link, IgnoreTypes.wikiLink, IgnoreTypes.tag], text, (text) => {
-      let lastLevel = options.startAtH2 ? 1 : 0; // level of last header processed
-      let decrement = 0; // number of levels to decrement following headers
+      let lastLevel = 0; // level of last header processed
       const minimumLevel = options.startAtH2 ? 2: 1;
-      const levelOffSet = options.startAtH2 ? 1: 0;
+      const headingLevelStartNumbers: Array<number> = [];
+      // These are the heading level mappings for each heading level where the index + 1 is the heading level in the file
+      // the value represents the new heading level to use with 0 meaning not mapped
+      const headingLevels = [0, 0, 0, 0, 0, 0];
+      const highestHeadingLevel = headingLevels.length;
 
-      return text.replace(allHeadersRegex, (headerText: string, $1: string = '', $2: string = '', $3: string = '', $4: string = '', $5: string = '') => {
-        let level = $2.length + levelOffSet - decrement;
-        if (level > lastLevel + 1) {
-          decrement += level - (lastLevel + 1);
-          level = lastLevel + 1;
-        } else if (level < lastLevel) {
-          decrement -= lastLevel + decrement - $2.length;
+      return text.replace(allHeadersRegex, (_: string, $1: string = '', $2: string = '', $3: string = '', $4: string = '', $5: string = '') => {
+        let level = $2.length;
+        level = level <= highestHeadingLevel ? level : highestHeadingLevel;
 
-          if (decrement <= 0) {
-            level = $2.length;
-            decrement = 0;
+        if (headingLevels[level - 1] >= 0 && level < lastLevel) {
+          let removeLevelTo = headingLevels.length;
+          while (headingLevelStartNumbers.length !== 0 && level <= headingLevelStartNumbers[headingLevelStartNumbers.length - 1]) {
+            removeLevelTo = headingLevelStartNumbers.pop();
           }
+
+          // in the rare case that a heading is lower than the first header in the file, make sure to reset all values for headers
+          if (headingLevelStartNumbers.length === 0) {
+            removeLevelTo = 0;
+          } else {
+            removeLevelTo--;
+          }
+
+          for (let i = headingLevels.length - 1; i >= removeLevelTo; i--) {
+            headingLevels[i] = 0;
+          }
+        }
+
+        if (headingLevels[level - 1] <= 0) {
+          const startingLevelToFillIn = lastLevel;
+          let newHeadingLevel = headingLevelStartNumbers.length + minimumLevel;
+          newHeadingLevel = newHeadingLevel <= highestHeadingLevel ? newHeadingLevel : highestHeadingLevel;
+
+          for (let i = startingLevelToFillIn; i < level - 1; i++) {
+            headingLevels[i] = newHeadingLevel - 1;
+          }
+
+          headingLevelStartNumbers.push(level);
+          headingLevels[level - 1] = newHeadingLevel;
         }
 
-        level = level < minimumLevel ? minimumLevel : level;
         lastLevel = level;
 
-        return $1 + '#'.repeat(level) + $3 + $4 + $5;
+        return $1 + '#'.repeat(headingLevels[level - 1]) + $3 + $4 + $5;
       });
     });
   }
@@ -81,13 +104,13 @@ export default class HeaderIncrement extends RuleBuilder<HeaderIncrementOptions>
           ${''}
           We skip from 1 to 3
           ${''}
-          ####### H7
+          ###### H6
           ${''}
-          We skip from 3 to 7 leaving out 4, 5, and 6. Thus headings level 4, 5, and 6 will be treated like H3 above until another H2 or H1 is encountered
+          We skip from 3 to 6 leaving out 4, 5, and 6. Thus headings level 4 and 5 will be treated like H3 above until another H2 or H1 is encountered
           ${''}
-          ###### H6
+          ##### H5
           ${''}
-          We skipped 6 previously so it will be treated the same as the H3 above since it was the next lowest header that was to be decremented
+          We skipped 5 previously so it will be treated the same as the H3 above since it was the next lowest header that was to be decremented
           ${''}
           ## H2
           ${''}
@@ -101,19 +124,19 @@ export default class HeaderIncrement extends RuleBuilder<HeaderIncrementOptions>
           ${''}
           We skip from 1 to 3
           ${''}
-          ### H7
+          ### H6
           ${''}
-          We skip from 3 to 7 leaving out 4, 5, and 6. Thus headings level 4, 5, and 6 will be treated like H3 above until another H2 or H1 is encountered
+          We skip from 3 to 6 leaving out 4, 5, and 6. Thus headings level 4 and 5 will be treated like H3 above until another H2 or H1 is encountered
           ${''}
-          ## H6
+          ## H5
           ${''}
-          We skipped 6 previously so it will be treated the same as the H3 above since it was the next lowest header that was to be decremented
+          We skipped 5 previously so it will be treated the same as the H3 above since it was the next lowest header that was to be decremented
           ${''}
-          ## H2
+          # H2
           ${''}
           This resets the decrement section so the H6 below is decremented to an H3
           ${''}
-          ### H6
+          ## H6
         `,
       }),
       new ExampleBuilder({
@@ -121,7 +144,6 @@ export default class HeaderIncrement extends RuleBuilder<HeaderIncrementOptions>
         before: dedent`
           # H1 becomes H2
           #### H4 becomes H3
-          ####### H7
           ###### H6
           ## H2
           ###### H6
@@ -131,8 +153,7 @@ export default class HeaderIncrement extends RuleBuilder<HeaderIncrementOptions>
         after: dedent`
           ## H1 becomes H2
           ### H4 becomes H3
-          #### H7
-          ### H6
+          #### H6
           ## H2
           ### H6
           ## H1


Test patch:
diff --git a/__tests__/header-increment.test.ts b/__tests__/header-increment.test.ts
index 6109a70..14e2849 100644
--- a/__tests__/header-increment.test.ts
+++ b/__tests__/header-increment.test.ts
@@ -10,29 +10,29 @@ ruleTest({
       before: dedent`
         # H1
         #### H4
-        ####### H7
+        ###### H6
       `,
       after: dedent`
         # H1
         ## H4
-        ### H7
+        ### H6
       `,
     },
     {
       testName: 'Handles change from decrement to regular increment',
       before: dedent`
         # H1
-        ##### H5
-        ####### H7
+        #### H4
         ###### H6
+        ##### H5
         ## H2
       `,
       after: dedent`
         # H1
+        ## H4
+        ### H6
         ## H5
-        ### H7
-        ## H6
-        ## H2
+        # H2
       `,
     },
     {
@@ -56,13 +56,13 @@ ruleTest({
         ## H3
         ### H4
         #### H6
-        ## H2
+        # H2
         # H1
         ## H2
         ### H3
         #### H4
         ##### H6
-        ##### H5
+        #### H5
         ### H3
       `,
     },
@@ -132,5 +132,62 @@ ruleTest({
         startAtH2: true,
       },
     },
+    { // accounts for https://github.com/platers/obsidian-linter/issues/576
+      testName: 'When decreasing the heading level, when the previous value was decreased and the next value is 1 more than the decreased value, but the same as the value just decremented, make sure the header is decremented with `startAtH2 = true`',
+      before: dedent`
+        ## HHELLO
+        #### HHELLO
+        ## HHELLO
+        ## HHELLO
+        ## HHELLO
+      `,
+      after: dedent`
+        ## HHELLO
+        ### HHELLO
+        ## HHELLO
+        ## HHELLO
+        ## HHELLO
+      `,
+      options: {
+        startAtH2: true,
+      },
+    },
+    { // accounts for https://github.com/platers/obsidian-linter/issues/589
+      testName: 'When decreasing the heading level, when the previous value was decreased and the next value is 1 more than the decreased value, but the same as the value just decremented, make sure the header is decremented',
+      before: dedent`
+        ## Second
+        ### Third
+        ## Second again
+        ### Third again
+      `,
+      after: dedent`
+        # Second
+        ## Third
+        # Second again
+        ## Third again
+      `,
+    },
+    { // accounts for https://github.com/platers/obsidian-linter/issues/589
+      testName: 'Make sure that when all header levels are present and we need to start at H2, that the H6 is left as an H6 since H7 is not valid',
+      before: dedent`
+        # H1
+        ## H2
+        ### H3
+        #### H4
+        ##### H5
+        ###### H6
+      `,
+      after: dedent`
+        ## H1
+        ### H2
+        #### H3
+        ##### H4
+        ###### H5
+        ###### H6
+      `,
+      options: {
+        startAtH2: true,
+      },
+    },
   ],
 });