Repository: platers/obsidian-linter (ts)

Issue:
FR: Support baskslash `\` at line ending to insert line break
## Is Your Feature Request Related to a Problem? Please Describe.

Apparently CommonMark supports this (although I only read this from a comment online), but using a backslash is something I've previously used to insert line breaks using some Markdown tools, like VS Code (I believe it supports it by default rather it than being some extension I have). I'm wondering why backslash line breaks are not supported by the Linter.

## Describe the Solution You'd Like

`foo \` -> `foo<br>`
`bar\` -> `bar<br>`

**Please include an example where applicable:**

```Markdown
Hello \
World
```

Lints as:

```Markdown
Hello \

World
```

Ideally would lint as:

```markdown
Hello \
World
```

## Describe Alternatives You've Considered

Just using two spaces instead of a backslash.

## Additional Context

I would prefer to use a backslash due to it being visually obviously that an explicit line break is present. Otherwise it's hard to detect in Obsidian.

Thanks.
FR: <br> as option for Two Spaces Between Lines with Content
## Is Your Feature Request Related to a Problem? Please Describe.

It is not a problem, but `<br>` can be used for better readability.

## Describe the Solution You'd Like

A option to select two spaces or `<br>`

``` markdown
line1  
line2  
```

``` markdown
line1<br>
line2<br>
```

Pull request description:
Feat: Add Ability for User to Select from Multiple Different Line Break Indicators
Fixes #974 
Fixes #933 

There have been a couple of requests to allow line endings other than just `  ` for the two spaces rule. So `<br>`, `<br/>`, and `\` have been added as options since they seem to be valid and respected. This will give users the ability to specify which line break to use. It will replace the other line endings that already exist if they do not match the current one for a paragraph.

Changes Made:
- Added `<br>`, `<br/>`, and `\` as valid line break indicators
- Updated and added a couple of tests and examples
- Updated some documentation for paragraph blank lines
- Updated UI wording for `two-spaces-between-lines-with-content`

Patch:
diff --git a/docs/additional-info/rules/paragraph-blank-lines.md b/docs/additional-info/rules/paragraph-blank-lines.md
index 62faedb..4c33660 100644
--- a/docs/additional-info/rules/paragraph-blank-lines.md
+++ b/docs/additional-info/rules/paragraph-blank-lines.md
@@ -3,4 +3,4 @@
 
 #### When Is a Blank Line Added?
 
-When a paragraph has another line after the current one and it does not end in 2 or more spaces or `<br>` or `<br/>`.
+When a paragraph has another line after the current one and it does not end in 2 or more spaces, `<br>`, `<br/>`, or `\`.
diff --git a/src/lang/locale/de.ts b/src/lang/locale/de.ts
index d99e784..2da616f 100644
--- a/src/lang/locale/de.ts
+++ b/src/lang/locale/de.ts
@@ -785,5 +785,10 @@ export default {
     '‘’': '‘’', // leave as is
     '""': '""', // leave as is
     '“”': '“”', // leave as is
+    // yaml.ts
+    '\\': '\\', // leave as is
+    '<br>': '<br>', // leave as is
+    '  ': '  ', // leave as is
+    '<br/>': '<br/>', // leave as is
   },
 };
diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts
index 52cd046..9d0d6c2 100644
--- a/src/lang/locale/en.ts
+++ b/src/lang/locale/en.ts
@@ -717,8 +717,12 @@ export default {
     },
     // two-spaces-between-lines-with-content.ts
     'two-spaces-between-lines-with-content': {
-      'name': 'Two Spaces Between Lines with Content',
-      'description': 'Makes sure that two spaces are added to the ends of lines with content continued on the next line for paragraphs, blockquotes, and list items',
+      'name': 'Line Break Between Lines with Content',
+      'description': 'Makes sure that the specified line break is added to the ends of lines with content continued on the next line for paragraphs, blockquotes, and list items',
+      'line-break-indicator': {
+        'name': 'Line Break Indicator',
+        'description': 'The line break indicator to use.',
+      },
     },
     // unordered-list-style.ts
     'unordered-list-style': {
@@ -857,5 +861,10 @@ export default {
     '‘’': '‘’', // leave as is
     '""': '""', // leave as is
     '“”': '“”', // leave as is
+    // yaml.ts
+    '\\': '\\', // leave as is
+    '<br>': '<br>', // leave as is
+    '  ': '  ', // leave as is
+    '<br/>': '<br/>', // leave as is
   },
 };
diff --git a/src/lang/locale/es.ts b/src/lang/locale/es.ts
index 98dbf91..4dd3e96 100644
--- a/src/lang/locale/es.ts
+++ b/src/lang/locale/es.ts
@@ -565,7 +565,11 @@ export default {
     },
     'two-spaces-between-lines-with-content': {
       'name': 'Dos espacios entre líneas con contenido',
-      'description': 'Se asegura de que se agreguen dos espacios al final de las líneas con contenido que continúa en la siguiente línea para párrafos, comillas y elementos de lista',
+      'description': 'Se asegura de que el salto de línea especificado se agregue al final de las líneas y el contenido continúe en la línea siguiente para párrafos, citas en bloque y elementos de lista.',
+      'line-break-indicator': {
+        'name': 'Indicador de salto de línea',
+        'description': 'El indicador de salto de línea a utilizar.',
+      },
     },
     'unordered-list-style': {
       'name': 'Estilo de lista desordenada',
@@ -694,5 +698,10 @@ export default {
     '‘’': '‘’', // leave as is
     '""': '""', // leave as is
     '“”': '“”', // leave as is
+    // yaml.ts
+    '\\': '\\', // leave as is
+    '<br>': '<br>', // leave as is
+    '  ': '  ', // leave as is
+    '<br/>': '<br/>', // leave as is
   },
 };
diff --git a/src/lang/locale/tr.ts b/src/lang/locale/tr.ts
index 86efbb6..5429d11 100644
--- a/src/lang/locale/tr.ts
+++ b/src/lang/locale/tr.ts
@@ -781,5 +781,10 @@ export default {
     '‘’': '‘’', // leave as is
     '""': '""', // leave as is
     '“”': '“”', // leave as is
+    // yaml.ts
+    '\\': '\\', // leave as is
+    '<br>': '<br>', // leave as is
+    '  ': '  ', // leave as is
+    '<br/>': '<br/>', // leave as is
   },
 };
diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts
index 27a3cad..5266ec2 100644
--- a/src/lang/locale/zh-cn.ts
+++ b/src/lang/locale/zh-cn.ts
@@ -789,5 +789,10 @@ export default {
     '‘’': '‘’', // leave as is
     '""': '""', // leave as is
     '“”': '“”', // leave as is
+    // yaml.ts
+    '\\': '\\', // leave as is
+    '<br>': '<br>', // leave as is
+    '  ': '  ', // leave as is
+    '<br/>': '<br/>', // leave as is
   },
 };
diff --git a/src/rules/paragraph-blank-lines.ts b/src/rules/paragraph-blank-lines.ts
index 90fd3be..d5acc90 100644
--- a/src/rules/paragraph-blank-lines.ts
+++ b/src/rules/paragraph-blank-lines.ts
@@ -40,12 +40,13 @@ export default class ParagraphBlankLines extends RuleBuilder<ParagraphBlankLines
         `,
       }),
       new ExampleBuilder({
-        description: 'Paragraphs can be extended via the use of 2 or more spaces at the end of a line or line break html',
+        description: 'Paragraphs can be extended via the use of 2 or more spaces at the end of a line, a line break html or xml, or a backslash (\\)',
         before: dedent`
           # H1
           Content${'  '}
           Paragraph content continued <br>
           Paragraph content continued once more <br/>
+          Paragraph content yet again\\
           Last line of paragraph
           A new paragraph
           # H2
@@ -56,6 +57,7 @@ export default class ParagraphBlankLines extends RuleBuilder<ParagraphBlankLines
           Content${'  '}
           Paragraph content continued <br>
           Paragraph content continued once more <br/>
+          Paragraph content yet again\\
           Last line of paragraph
           ${''}
           A new paragraph
diff --git a/src/rules/two-spaces-between-lines-with-content.ts b/src/rules/two-spaces-between-lines-with-content.ts
index b240d8f..c46df11 100644
--- a/src/rules/two-spaces-between-lines-with-content.ts
+++ b/src/rules/two-spaces-between-lines-with-content.ts
@@ -1,10 +1,12 @@
 import {Options, RuleType} from '../rules';
-import RuleBuilder, {ExampleBuilder, OptionBuilderBase} from './rule-builder';
+import RuleBuilder, {DropdownOptionBuilder, ExampleBuilder, OptionBuilderBase} from './rule-builder';
 import dedent from 'ts-dedent';
 import {IgnoreTypes} from '../utils/ignore-types';
-import {addTwoSpacesAtEndOfLinesFollowedByAnotherLineOfTextContent} from '../utils/mdast';
+import {LineBreakIndicators, addTwoSpacesAtEndOfLinesFollowedByAnotherLineOfTextContent} from '../utils/mdast';
 
-class TwoSpacesBetweenLinesWithContentOptions implements Options {}
+class TwoSpacesBetweenLinesWithContentOptions implements Options {
+  lineBreakIndicator?: LineBreakIndicators = LineBreakIndicators.TwoSpaces;
+}
 
 @RuleBuilder.register
 export default class TwoSpacesBetweenLinesWithContent extends RuleBuilder<TwoSpacesBetweenLinesWithContentOptions> {
@@ -20,12 +22,12 @@ export default class TwoSpacesBetweenLinesWithContent extends RuleBuilder<TwoSpa
     return TwoSpacesBetweenLinesWithContentOptions;
   }
   apply(text: string, options: TwoSpacesBetweenLinesWithContentOptions): string {
-    return addTwoSpacesAtEndOfLinesFollowedByAnotherLineOfTextContent(text);
+    return addTwoSpacesAtEndOfLinesFollowedByAnotherLineOfTextContent(text, options.lineBreakIndicator);
   }
   get exampleBuilders(): ExampleBuilder<TwoSpacesBetweenLinesWithContentOptions>[] {
     return [
       new ExampleBuilder({
-        description: 'Make sure two spaces are added to the ends of lines that have content on it and the next line for lists, blockquotes, and paragraphs',
+        description: 'Make sure two spaces are added to the ends of lines that have content on it and the next line for lists, blockquotes, and paragraphs when the line break indicator is `  `',
         before: dedent`
           # Heading 1
           First paragraph stays as the first paragraph
@@ -46,7 +48,7 @@ export default class TwoSpacesBetweenLinesWithContent extends RuleBuilder<TwoSpa
           ${''}
           Paragraph lines that end in <br/>
           Or lines that end in <br>
-          Are left alone
+          Are left swapped
           Since they mean the same thing
           ${''}
           \`\`\` text
@@ -84,9 +86,9 @@ export default class TwoSpacesBetweenLinesWithContent extends RuleBuilder<TwoSpa
           Continuation *of* the paragraph has \`inline code block\` __in it__.  ${''}
           Even more continuation
           ${''}
-          Paragraph lines that end in <br/>
-          Or lines that end in <br>
-          Are left alone  ${''}
+          Paragraph lines that end in  ${''}
+          Or lines that end in  ${''}
+          Are left swapped  ${''}
           Since they mean the same thing
           ${''}
           \`\`\` text
@@ -106,10 +108,38 @@ export default class TwoSpacesBetweenLinesWithContent extends RuleBuilder<TwoSpa
           Even more content here
           ${''}
         `,
+        options: {
+          lineBreakIndicator: LineBreakIndicators.TwoSpaces,
+        },
       }),
     ];
   }
   get optionBuilders(): OptionBuilderBase<TwoSpacesBetweenLinesWithContentOptions>[] {
-    return [];
+    return [
+      new DropdownOptionBuilder<TwoSpacesBetweenLinesWithContentOptions, LineBreakIndicators>({
+        OptionsClass: TwoSpacesBetweenLinesWithContentOptions,
+        nameKey: 'rules.two-spaces-between-lines-with-content.line-break-indicator.name',
+        descriptionKey: 'rules.two-spaces-between-lines-with-content.line-break-indicator.description',
+        optionsKey: 'lineBreakIndicator',
+        records: [
+          {
+            value: LineBreakIndicators.TwoSpaces,
+            description: LineBreakIndicators.TwoSpaces,
+          },
+          {
+            value: LineBreakIndicators.LineBreakHtml,
+            description: LineBreakIndicators.LineBreakHtml,
+          },
+          {
+            value: LineBreakIndicators.LineBreakHtmlNotXml,
+            description: LineBreakIndicators.LineBreakHtmlNotXml,
+          },
+          {
+            value: LineBreakIndicators.Backslash,
+            description: LineBreakIndicators.Backslash,
+          },
+        ],
+      }),
+    ];
   }
 }
diff --git a/src/utils/mdast.ts b/src/utils/mdast.ts
index 0b3e216..fe742e4 100644
--- a/src/utils/mdast.ts
+++ b/src/utils/mdast.ts
@@ -53,6 +53,13 @@ export enum UnorderedListItemStyles {
   Consistent = 'consistent',
 }
 
+export enum LineBreakIndicators {
+  TwoSpaces = '  ',
+  LineBreakHtmlNotXml = '<br>',
+  LineBreakHtml = '<br/>',
+  Backslash = '\\',
+}
+
 function parseTextToAST(text: string): Root {
   const textHash = hashString53Bit(text);
   if (LRU.has(textHash)) {
@@ -360,9 +367,10 @@ export function makeEmphasisOrBoldConsistent(text: string, style: string, type:
 /**
    * Makes sure that blockquotes, paragraphs, and list items have two spaces at the end of them if the following line continues its content.
    * @param {string} text The text to make sure that the two spaces are added to if there are consecutive lines of content
+   * @param {LineBreakIndicators} indicator The indicator to use for the lines that do not already use a blank line indicator
    * @return {string} The text with two spaces at the end of lines of paragraphs, list items, and blockquotes where there were consecutive lines of content.
    */
-export function addTwoSpacesAtEndOfLinesFollowedByAnotherLineOfTextContent(text: string): string {
+export function addTwoSpacesAtEndOfLinesFollowedByAnotherLineOfTextContent(text: string, indicator: LineBreakIndicators): string {
   const positions: Position[] = getPositions(MDAstTypes.Paragraph, text);
   if (positions.length === 0) {
     return text;
@@ -377,13 +385,12 @@ export function addTwoSpacesAtEndOfLinesFollowedByAnotherLineOfTextContent(text:
     }
 
     for (let i = 0; i < lastLineIndex; i++) {
-      const paragraphLine = paragraphLines[i].trimEnd();
+      const paragraphLine = paragraphLines[i];
 
-      // skip lines that end in <br> or <br/> as it is the same as two spaces in Markdown
-      if (paragraphLine.endsWith('<br>') || paragraphLine.endsWith('<br/>')) {
+      if (lineEndsInLineBreak(paragraphLine, indicator)) {
         continue;
       }
-      paragraphLines[i] = paragraphLine + '  ';
+      paragraphLines[i] = addOrReplaceLineEnding(paragraphLine, indicator);
     }
 
     text = replaceTextBetweenStartAndEndWithNewValue(text, position.start.offset, position.end.offset, paragraphLines.join('\n'));
@@ -392,6 +399,48 @@ export function addTwoSpacesAtEndOfLinesFollowedByAnotherLineOfTextContent(text:
   return text;
 }
 
+function lineEndsInLineBreak(paragraphLine: string, indicator: LineBreakIndicators): boolean {
+  if (paragraphLine.endsWith('<br>') && indicator == LineBreakIndicators.LineBreakHtmlNotXml) {
+    return true;
+  }
+
+  if (paragraphLine.endsWith('<br/>') && indicator == LineBreakIndicators.LineBreakHtml) {
+    return true;
+  }
+
+  if (paragraphLine.endsWith('  ') && indicator == LineBreakIndicators.TwoSpaces) {
+    return true;
+  }
+
+  if (!paragraphLine.endsWith('\\\\') && paragraphLine.endsWith('\\') && indicator == LineBreakIndicators.Backslash) {
+    return true;
+  }
+
+  return false;
+}
+
+function addOrReplaceLineEnding(paragraphLine: string, indicator: LineBreakIndicators): string {
+  paragraphLine = paragraphLine.trimEnd();
+  let numCharsToRemove = 0;
+  if (paragraphLine.endsWith('<br>')) {
+    numCharsToRemove = 4;
+  }
+
+  if (paragraphLine.endsWith('<br/>')) {
+    numCharsToRemove = 5;
+  }
+
+  if (!paragraphLine.endsWith('\\\\') && paragraphLine.endsWith('\\')) {
+    numCharsToRemove = 1;
+  }
+
+  if (numCharsToRemove) {
+    paragraphLine = paragraphLine.substring(0, paragraphLine.length - numCharsToRemove);
+  }
+
+  return paragraphLine.trimEnd() + indicator;
+}
+
 /**
  * Makes sure that paragraphs have a single new line before and after them.
  * @param {string} text The text to make sure that paragraphs have only 1 new line before and after them
@@ -437,8 +486,8 @@ export function makeSureThereIsOnlyOneBlankLineBeforeAndAfterParagraphs(text: st
         newParagraphLines.push(paragraphLine);
       }
 
-      // make sure that lines that end in <br>, <br/>, or two or more spaces are in the same paragraph
-      nextLineIsSameParagraph = paragraphLine.endsWith('<br>') || paragraphLine.endsWith('<br/>') || paragraphLine.endsWith('  ');
+      // make sure that lines that end in \, <br>, <br/>, or two or more spaces are in the same paragraph
+      nextLineIsSameParagraph = paragraphLine.endsWith(LineBreakIndicators.LineBreakHtmlNotXml) || paragraphLine.endsWith(LineBreakIndicators.LineBreakHtml) || paragraphLine.endsWith(LineBreakIndicators.TwoSpaces) || (!paragraphLine.endsWith('\\\\') && paragraphLine.endsWith(LineBreakIndicators.Backslash));
     }
 
     // remove new lines prior to paragraph


Test patch:
diff --git a/__tests__/paragraph-blank-lines.test.ts b/__tests__/paragraph-blank-lines.test.ts
index 26cf16f..c95f25d 100644
--- a/__tests__/paragraph-blank-lines.test.ts
+++ b/__tests__/paragraph-blank-lines.test.ts
@@ -164,6 +164,7 @@ ruleTest({
         paragraph line 1 <br>
         paragraph line 2  ${''}
         paragraph line 3 <br/>
+        paragraph line 4 \\
         paragraph final line
         ${''}
         ${''}
@@ -174,6 +175,7 @@ ruleTest({
         paragraph line 1 <br>
         paragraph line 2  ${''}
         paragraph line 3 <br/>
+        paragraph line 4 \\
         paragraph final line
         ${''}
       `,
diff --git a/__tests__/two-spaces-between-lines-with-content.test.ts b/__tests__/two-spaces-between-lines-with-content.test.ts
index ab79f47..02f23fb 100644
--- a/__tests__/two-spaces-between-lines-with-content.test.ts
+++ b/__tests__/two-spaces-between-lines-with-content.test.ts
@@ -1,6 +1,7 @@
 import TwoSpacesBetweenLinesWithContent from '../src/rules/two-spaces-between-lines-with-content';
 import dedent from 'ts-dedent';
 import {ruleTest} from './common';
+import {LineBreakIndicators} from '../src/utils/mdast';
 
 ruleTest({
   RuleBuilderClass: TwoSpacesBetweenLinesWithContent,
@@ -86,5 +87,85 @@ ruleTest({
         %%
       `,
     },
+    {
+      testName: 'Make sure that using a line break indicator of `<br>` replaces the other line break endings properly',
+      before: dedent`
+        Here is some text${'  '}
+        Here is some more text\\
+        Here is yet some more text<br>
+        Even more text<br/>
+        Once more...
+      `,
+      after: dedent`
+        Here is some text<br>
+        Here is some more text<br>
+        Here is yet some more text<br>
+        Even more text<br>
+        Once more...
+      `,
+      options: {
+        lineBreakIndicator: LineBreakIndicators.LineBreakHtmlNotXml,
+      },
+    },
+    {
+      testName: 'Make sure that using a line break indicator of `<br/>` replaces the other line break endings properly',
+      before: dedent`
+        Here is some text${'  '}
+        Here is some more text\\
+        Here is yet some more text<br>
+        Even more text<br/>
+        Once more...
+      `,
+      after: dedent`
+        Here is some text<br/>
+        Here is some more text<br/>
+        Here is yet some more text<br/>
+        Even more text<br/>
+        Once more...
+      `,
+      options: {
+        lineBreakIndicator: LineBreakIndicators.LineBreakHtml,
+      },
+    },
+    {
+      testName: 'Make sure that using a line break indicator of `\\` replaces the other line break endings properly',
+      before: dedent`
+        Here is some text${'  '}
+        Here is some more text\\
+        Here is yet some more text<br>
+        Even more text<br/>
+        Once more...
+      `,
+      after: dedent`
+        Here is some text\\
+        Here is some more text\\
+        Here is yet some more text\\
+        Even more text\\
+        Once more...
+      `,
+      options: {
+        lineBreakIndicator: LineBreakIndicators.Backslash,
+      },
+    },
+    {
+      testName: 'Make sure that using a line break indicator of `  ` replaces the other line break endings properly',
+      before: dedent`
+        Here is some text${'  '}
+        Here is some more text\\
+        Here is yet some more text<br>
+        Even more text<br/>
+        Once more...
+      `,
+      after: dedent`
+        Here is some text${'  '}
+        Here is some more text${'  '}
+        Here is yet some more text${'  '}
+        Even more text${'  '}
+        Once more...
+      `,
+      options: {
+        lineBreakIndicator: LineBreakIndicators.TwoSpaces,
+      },
+    },
   ],
 });