Repository: platers/obsidian-linter (ts)

Issue:
Bug: Move Math Block Indicators to Their Own Line duplicates text before block math delimiter
- [x] I have verified that I am on the latest version of the Linter

## Describe the Bug

The rule `Move Math Block Indicators to Their Own Line` duplicates text before block math delimiter.

## How to Reproduce

Steps to reproduce the behavior:

1. Create a new vault, enable community plugin, install this linter.
2. Enable `Lint on save` rule for simplicity.
3. Enable `Move Math Block Indicators to Their Own Line` Rule
4. Write `a$$b$$` in the editor.
5. Save (or Lint the file)
6. (Additionally) repeatedly linting the file keeps on duplicating the text before the block math delimiter.

## Expected Behavior

Expected output if applicable:

``` markdown
a
$$
b
$$
```

## Screenshots

https://github.com/platers/obsidian-linter/assets/38686321/b75863cf-5654-452d-ba5b-98d953b11fb2

## Device

- [x] Desktop (MacOS)
- [ ] Mobile

## Additional Context

I checked the source code and found that this is actually a feature for math blocks inside blockquotes (`>`).
For blockquotes, 

```
> $$a$$
```

should be linted to

```
> $$
> a
> $$
```

which is probably why the `a` in my example keeps getting duplicated. Is this intended behavior?

Pull request:
Fixed Content on Same Line as Math Block Indicator Getting Put At Start of Each Line Outside of a Blockquote for Inline Math for Moving Math Indicators to Their Own Lines
Fixes #996 

There was an issue where if you had inline math present with other content on the line, if it met the conditions for converting to a math block, it would copy over the content on the same line, but prior to the opening math blocks.

For example, `a $$b$$` would become:
``` markdown
a 
a $$
a b
a $$
```

This was fixed by getting the start of the line as either whitespace or blockquote indicators.

Thus it now becomes
``` markdown
a
$$
b
$$
```

I also addressed the more complex blockquote scenario to properly get the blockquote indicators.
``` markdown
> Here is a math block: $$a$$
```

 now becomes

``` markdown
> Here is a math block:
> $$
> a
> $$
```

The logic also tries to cleanup whitespace that was present between the opening math block indicator and the content on the same line.

For example,
``` markdown
Here is a math block             $$b$$
```

becomes

``` markdown
Here is a math block
$$
b
$$
```

Changes Made:
- Reworked the logic for moving math block indicators to remove whitespace that was between line content and the opening math block indicators
- Reworked the logic to make it handle more blockquote and regular markdown scenarios
- Added UTs for a couple scenarios to make sure it worked
- Moved regex to the regex helper file

Patch:
diff --git a/src/utils/mdast.ts b/src/utils/mdast.ts
index c5a1786..2d5d728 100644
--- a/src/utils/mdast.ts
+++ b/src/utils/mdast.ts
@@ -1,8 +1,8 @@
 import {visit} from 'unist-util-visit';
 import type {Position} from 'unist';
 import type {Root} from 'mdast';
-import {hashString53Bit, makeSureContentHasEmptyLinesAddedBeforeAndAfter, replaceTextBetweenStartAndEndWithNewValue, getStartOfLineIndex, replaceAt} from './strings';
-import {genericLinkRegex, tableRow, tableSeparator, tableStartingPipe, customIgnoreAllStartIndicator, customIgnoreAllEndIndicator, checklistBoxStartsTextRegex, footnoteDefinitionIndicatorAtStartOfLine} from './regex';
+import {hashString53Bit, makeSureContentHasEmptyLinesAddedBeforeAndAfter, replaceTextBetweenStartAndEndWithNewValue, getStartOfLineIndex, replaceAt, getStartOfLineWhitespaceOrBlockquoteLevel} from './strings';
+import {genericLinkRegex, tableRow, tableSeparator, tableStartingPipe, customIgnoreAllStartIndicator, customIgnoreAllEndIndicator, checklistBoxStartsTextRegex, footnoteDefinitionIndicatorAtStartOfLine, emptyLineMathBlockquoteRegex, startsWithBlockquote} from './regex';
 import {gfmFootnote} from 'micromark-extension-gfm-footnote';
 import {gfmTaskListItem} from 'micromark-extension-gfm-task-list-item';
 import {combineExtensions} from 'micromark-util-combine-extensions';
@@ -830,30 +830,56 @@ function breakMathBlockIntoMultipleBlocksIfNeedBe(mathBlock: string, numberOfDol
 
 function addBlankLinesAroundStartAndStopMathIndicators(text: string, mathBlockStartIndex: number, mathBlockEndIndex: number, mathOpeningIndicatorRegex: RegExp, mathEndingIndicatorRegex: RegExp): string {
   const startOfLine = text.substring(getStartOfLineIndex(text, mathBlockStartIndex), mathBlockStartIndex) ?? '';
+  const [lineStart] = getStartOfLineWhitespaceOrBlockquoteLevel(startOfLine, startOfLine.length);
   const startOfEndingLine = text.substring(getStartOfLineIndex(text, mathBlockEndIndex), mathBlockEndIndex) ?? '';
-  const emptyLineBlockquoteRegex = /^(>( |\t)*)+\$+$/m;
   let mathBlock = text.substring(mathBlockStartIndex, mathBlockEndIndex);
+  const isBlockquote = startsWithBlockquote.test(startOfLine.trim());
+  let startingNewLineAdded = false;
+
   mathBlock = mathBlock.replace(mathOpeningIndicatorRegex, (_: string, $1: string, $2: string = '') => {
+    let newOpening = '';
+    if (!isBlockquote && startOfLine.trim() != '') {
+      newOpening += '\n';
+      startingNewLineAdded = true;
+    } else if (isBlockquote && !emptyLineMathBlockquoteRegex.test(startOfLine)) {
+      newOpening += '\n' + lineStart;
+      startingNewLineAdded = true;
+    }
+
+    newOpening += $1 + '\n';
+
     // a new line is being added
-    if ($2 === '') {
-      return $1 + '\n' + startOfLine;
+    if ($2 === '' && isBlockquote) {
+      newOpening += lineStart;
     }
 
-    return $1 + '\n';
+    return newOpening;
   });
-  mathBlock= mathBlock.replace(mathEndingIndicatorRegex, (match: string, $1: string = '', $2: string, $3: string) => {
+  mathBlock = mathBlock.replace(mathEndingIndicatorRegex, (match: string, $1: string = '', $2: string, $3: string) => {
     const groupOneIsEmpty = $1 === '';
 
     // make sure that a blank blockquote line is checked for in order to determine if a change needs to happen just for blockquotes
-    if (groupOneIsEmpty && emptyLineBlockquoteRegex.test(startOfEndingLine.trim())) {
+    if (groupOneIsEmpty && isBlockquote && emptyLineMathBlockquoteRegex.test(startOfEndingLine.trim())) {
       return match;
-    } else if (groupOneIsEmpty) { // a new line is being added
-      return '\n' + startOfLine + $2 + $3;
+    } else if (groupOneIsEmpty && isBlockquote) { // a new line is being added
+      return '\n' + lineStart + $2 + $3;
     }
 
     return '\n' + $2 + $3;
   });
 
+  // try to cleanup whitespace that may get left behind by this logic when moving the opening
+  // math block indicators to its own line
+  // eslint-disable-next-line no-unmodified-loop-condition
+  while (startingNewLineAdded && mathBlockStartIndex > 0) {
+    const previousChar = text[mathBlockStartIndex-1];
+    if (previousChar !== ' ' && previousChar !== '\t') {
+      break;
+    }
+
+    mathBlockStartIndex--;
+  }
+
   return replaceTextBetweenStartAndEndWithNewValue(text, mathBlockStartIndex, mathBlockEndIndex, mathBlock);
 }
 
diff --git a/src/utils/regex.ts b/src/utils/regex.ts
index 44baa9a..ac16a45 100644
--- a/src/utils/regex.ts
+++ b/src/utils/regex.ts
@@ -18,6 +18,8 @@ export const obsidianMultilineCommentRegex = /^%%\n[^%]*\n%%/gm;
 export const wordSplitterRegex = /[,\s]+/;
 export const ellipsisRegex = /(\. ?){2}\./g;
 export const lineStartingWithWhitespaceOrBlockquoteTemplate = `\\s*(>\\s*)*`;
+export const emptyLineMathBlockquoteRegex = /^(>( |\t)*)+\$*?$/m;
+export const startsWithBlockquote = /^\s*(>\s*)+/m;
 export const tableSeparator = /(\|? *:?-{1,}:? *\|?)(\| *:?-{1,}:? *\|?)*( |\t)*$/gm;
 export const tableStartingPipe = /^(((>[ ]?)*)|([ ]{0,3}))\|/m;
 export const tableRow = /[^\n]*?\|[^\n]*?(\n|$)/m;


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 42ff54b..113c7f2 100644
--- a/__tests__/move-math-block-indicators-to-own-line.test.ts
+++ b/__tests__/move-math-block-indicators-to-own-line.test.ts
@@ -110,5 +110,41 @@ ruleTest({
         text
       `,
     },
+    { // accounts for https://github.com/platers/obsidian-linter/issues/996
+      testName: 'Math indicators with content before them should not have that content duplicated unless it is a blockquote',
+      before: dedent`
+        a $$b$$
+      `,
+      after: dedent`
+        a
+        $$
+        b
+        $$
+      `,
+    },
+    { // relates to https://github.com/platers/obsidian-linter/issues/996
+      testName: 'Math indicators with content before them should have the opening indicator moved to a new line and have any tabs and spaces that were between the content and the opening indicators removed.',
+      before: dedent`
+        a \t    $$b$$
+      `,
+      after: dedent`
+        a
+        $$
+        b
+        $$
+      `,
+    },
+    { // relates to https://github.com/platers/obsidian-linter/issues/996
+      testName: 'Math indicators in a blockquote/callout with content before them should have the opening indicator moved to a new line and have any tabs and spaces that were between the content and the opening indicators removed.',
+      before: dedent`
+        > a \t    $$b$$
+      `,
+      after: dedent`
+        > a
+        > $$
+        > b
+        > $$
+      `,
+    },
   ],
 });