Repository: platers/obsidian-linter (ts)

Issue:
Bug: "Move Math Block Indicators to Their Own Line" Not Working
- [x] I have verified that I am on the latest version of the Linter

## Describe the Bug

The `Move Math Block Indicators to Their Own Line` rule did not work for simple cases and it seems related to incorrect `parseTextToAST` result.

## How to Reproduce

Example to reproduce issue with
```markdown
$$a$$

$$
b$$

$$c
$$
```

rule: `move-math-block-indicators-to-their-own-line`

## Expected Behavior

A clear and concise description of what you expected to happen.

Expected output if applicable:

``` markdown
$$
a
$$

$$
b
$$

$$
c
$$
```

## ~~Screenshots~~ Current Output

``` markdown
$$
a
$$

$$
b$$

$$c
$$
```

## Device

- [x] Desktop

## Additional Context

In the function `makeSureMathBlockIndicatorsAreOnTheirOwnLines` in `mdast.ts`, there are `getPositions(MDAstTypes.Math, text)` and `getPositions(MDAstTypes.InlineMath, text)`. I printed the results, and it shows each has 1 element: it recognizes `$$a$$` as an **inline math**, which makes the rule accidentally worked, and it takes line 3-7, that is, the
```markdown
$$
b$$

$$c
$$
```
part as **one** math block, which makes the rule failed. See the console output below (the first array is by `MDAstTypes.Math` and the second one is by `MDAstTypes.InlineMath`).

<img width="408" alt="image" src="https://github.com/platers/obsidian-linter/assets/13874231/a76ba8b3-9d35-4f0a-a004-8f56da87cb25">

Pull request description:
Try to Handle Math Blocks Getting Combined when 1 or More Are Malformed
Fixes #783 

Changes Made:
- Added some documentation around handling math blocks and what is done to special exceptions for moving math blocks to their own lines
- Added a test for the scenario referenced
- Added logic to split apart math blocks into pairs of "sub" math blocks in the event that multiple math blocks got combined

Patch:
diff --git a/docs/additional-info/rules/move-math-block-indicators-to-own-line.md b/docs/additional-info/rules/move-math-block-indicators-to-own-line.md
new file mode 100644
index 0000000..da942ec
--- /dev/null
+++ b/docs/additional-info/rules/move-math-block-indicators-to-own-line.md
@@ -0,0 +1,54 @@
+#### What Math Formats Does the Linter Support?
+
+The Linter uses [remark-math](https://github.com/remarkjs/remark-math) as its math block parser. This is not the one used by Obsidian.
+So it may be that Obsidian and the Linter consider different things to be math blocks or inline math.
+
+The expected format for inline math is `$MATH_HERE$`. The amount of dollar signs does not matter so long as it is less than the value being
+used to indicate a math block. Math blocks should look something like the following:
+
+``` markdown
+$$
+MATH_HERE
+$$
+```
+
+The Linter will try to break inline math blocks into math blocks when the inline math block has the minimum number of `$` at the start of
+the inline math.
+
+##### Exceptions
+
+The Linter tries to correct a couple of exceptions that seem to be weird results from the parser when the desired format is not followed for inline and math blocks.
+
+###### Splitting Simple Combined Math Blocks
+
+The Linter will try to fix some combined math blocks that the parser returns. For example the following is considered 2 math blocks even
+though it is really 3 different math blocks:
+
+``` markdown
+$$a$$
+
+$$
+b$$
+
+$$c
+$$
+```
+
+The Linter does its best to break that apart under the hood into 3 separate math blocks which can then be handled appropriately.
+!!! Warning
+    This may not be reliable to use to fix more complex math block parsing issues, so please try to conform with the block and inline
+    patterns described above for math blocks.
+
+###### Moving Text after Closing Math Block Indicator to Its Own Line
+
+The parser does not handle math block closing indicators on the same line as text as it will consider that text to be part of the math
+block even if it comes after the ending indicator. For example, the following is considered to be an entire math block according to 
+the underlying parser:
+
+``` markdown
+$$
+a
+$$b
+```
+
+The Linter does its best to make sure that the `b` is treated as not being part of the math block.
diff --git a/src/utils/mdast.ts b/src/utils/mdast.ts
index cc6ae9f..9466358 100644
--- a/src/utils/mdast.ts
+++ b/src/utils/mdast.ts
@@ -12,6 +12,7 @@ import {fromMarkdown} from 'mdast-util-from-markdown';
 import {gfmFootnoteFromMarkdown} from 'mdast-util-gfm-footnote';
 import {gfmTaskListItemFromMarkdown} from 'mdast-util-gfm-task-list-item';
 import QuickLRU from 'quick-lru';
+import {countInstances} from './strings';
 import {getTextInLanguage} from '../lang/helpers';
 
 const LRU = new QuickLRU({maxSize: 200});
@@ -745,7 +746,12 @@ export function makeSureMathBlockIndicatorsAreOnTheirOwnLines(text: string, numb
   const mathOpeningIndicatorRegex = new RegExp('^(\\${' + numberOfDollarSignsForMathBlock + ',})(\\n*)');
   const mathEndingIndicatorRegex = new RegExp('(\\n*)(\\${' + numberOfDollarSignsForMathBlock + ',})([^\\$]*)$');
   for (const position of positions) {
-    text = addBlankLinesAroundStartAndStopMathIndicators(text, position.start.offset, position.end.offset, mathOpeningIndicatorRegex, mathEndingIndicatorRegex);
+    const mathBlock = text.substring(position.start.offset, position.end.offset);
+    const mathBlockIndexes = breakMathBlockIntoMultipleBlocksIfNeedBe(mathBlock, numberOfDollarSignsForMathBlock, position.start.offset);
+
+    for (const blockIndexes of mathBlockIndexes) {
+      text = addBlankLinesAroundStartAndStopMathIndicators(text, blockIndexes.startIndex, blockIndexes.endIndex, mathOpeningIndicatorRegex, mathEndingIndicatorRegex);
+    }
   }
 
   positions = getPositions(MDAstTypes.InlineMath, text);
@@ -760,6 +766,54 @@ export function makeSureMathBlockIndicatorsAreOnTheirOwnLines(text: string, numb
   return text;
 }
 
+function breakMathBlockIntoMultipleBlocksIfNeedBe(mathBlock: string, numberOfDollarSignsForMathBlock: number, startIndexOfMathBlock: number): {startIndex: number, endIndex: number}[] {
+  let mathBlockIndicator = '$'.repeat(numberOfDollarSignsForMathBlock);
+  let endOfOpeningIndicator = numberOfDollarSignsForMathBlock;
+  while (mathBlock.charAt(endOfOpeningIndicator) === '$') {
+    mathBlockIndicator += '$';
+    endOfOpeningIndicator++;
+  }
+
+  const mathBlockIndexes = [] as {startIndex: number, endIndex: number}[];
+
+  let matchCount = countInstances(mathBlock, mathBlockIndicator);
+  if (matchCount <= 3) {
+    mathBlockIndexes.unshift({
+      startIndex: startIndexOfMathBlock,
+      endIndex: startIndexOfMathBlock + mathBlock.length,
+    });
+
+    return mathBlockIndexes;
+  }
+
+  // if there is an odd amount of matches, remove one from the list so it is even
+  if (matchCount % 2 === 1) {
+    matchCount--;
+  }
+
+  // pair the earliest matches together until there are no more pairs
+  let startIndex = startIndexOfMathBlock;
+  let startSearch = mathBlockIndicator.length;
+  while (matchCount > 2 ) {
+    const endOfIndex = mathBlock.indexOf(mathBlockIndicator, startSearch) + mathBlockIndicator.length;
+    mathBlockIndexes.unshift({
+      startIndex: startIndex,
+      endIndex: startIndexOfMathBlock + endOfIndex,
+    });
+
+    startIndex = startIndexOfMathBlock + endOfIndex + 1;
+    startSearch = endOfIndex + 1;
+    matchCount -= 2;
+  }
+
+  mathBlockIndexes.unshift({
+    startIndex: startIndexOfMathBlock + mathBlock.indexOf(mathBlockIndicator, startSearch),
+    endIndex: startIndexOfMathBlock + mathBlock.length,
+  });
+
+  return mathBlockIndexes;
+}
+
 function addBlankLinesAroundStartAndStopMathIndicators(text: string, mathBlockStartIndex: number, mathBlockEndIndex: number, mathOpeningIndicatorRegex: RegExp, mathEndingIndicatorRegex: RegExp): string {
   const startOfLine = text.substring(getStartOfLineIndex(text, mathBlockStartIndex), mathBlockStartIndex) ?? '';
   const startOfEndingLine = text.substring(getStartOfLineIndex(text, mathBlockEndIndex), mathBlockEndIndex) ?? '';


Test patch:
diff --git a/__tests__/move-math-block-indicators-to-own-line.test.ts b/__tests__/move-math-block-indicators-to-own-line.test.ts
index 71bc8fe..9ffb575 100644
--- a/__tests__/move-math-block-indicators-to-own-line.test.ts
+++ b/__tests__/move-math-block-indicators-to-own-line.test.ts
@@ -59,5 +59,30 @@ ruleTest({
         > $$
       `,
     },
+    { // accounts for https://github.com/platers/obsidian-linter/issues/783
+      testName: 'Math indicators greater than or equal to the the starting indicator lead to a splitting of the math block if the number of qualifying math blocks is greater than 3',
+      before: dedent`
+        $$a$$
+        ${''}
+        $$
+        b$$
+        ${''}
+        $$c
+        $$
+      `,
+      after: dedent`
+        $$
+        a
+        $$
+        ${''}
+        $$
+        b
+        $$
+        ${''}
+        $$
+        c
+        $$
+      `,
+    },
   ],
 });