Repository: platers/obsidian-linter (ts)

Issue:
Bug: Linter breaks callout with table inside
## Describe the Bug

If there is a callout block with the table inside after linting, it will separate table and blockquotes and will indefinitely add new lines between callout and table.

## How to Reproduce

Example to reproduce issue with

``` markdown
> [!warning]- Test
>
> | column1 | column2 |
> | ------ | ------- |
> |   test1     |   test2      |
>
```

<details>
<summary> Linter Logs </summary>
<pre>
Running linter
Running Insert YAML attributes
Running Header Increment
Running Footnote after Punctuation
Running Convert Bullet List Markers
Running Emphasis Style
Running Ordered List Style
Running Remove Empty List Markers
Running Unordered List Style
Running Compact YAML
Running Consecutive blank lines
Running Empty Line Around Code Fences
Running Empty Line Around Tables
Running Heading blank lines
Running Paragraph blank lines
Running Space after list markers
Running Custom Regex
Running Custom Lint Commands
</pre>
</details>

<details>
<summary>Linter Config </summary>
<pre>
{
  "ruleConfigs": {
    "Escape YAML Special Characters": {
      "Escapes colons with a space after them (: ), single quotes ('), and double quotes (\") in YAML.": false,
      "Try to Escape Single Line Arrays": false
    },
    "Format Tags in YAML": {
      "Remove Hashtags from tags in the YAML frontmatter, as they make the tags there invalid.": false
    },
    "Format Yaml Array": {
      "Allows for the formatting of regular yaml arrays as either multi-line or single-line and `tags` and `aliases` are allowed to have some Obsidian specific yaml formats. Note that single string to single-line goes from a single string entry to a single-line array if more than 1 entry is present. The same is true for single string to multi-line except it becomes a multi-line array.": false,
      "Format yaml aliases section": true,
      "Format yaml tags section": true,
      "Default yaml array section style": "single-line",
      "Format yaml array sections": true,
      "Force key values to be single-line arrays": "",
      "Force key values to be multi-line arrays": ""
    },
    "Insert YAML attributes": {
      "Inserts the given YAML attributes into the YAML frontmatter. Put each attribute on a single line.": true,
      "Text to insert": "tags: "
    },
    "Move Tags to Yaml": {
      "Move all tags to Yaml frontmatter of the document.": false,
      "Body tag operation": "Nothing"
    },
    "Remove YAML Keys": {
      "Removes the YAML keys specified": false,
      "YAML Keys to Remove": ""
    },
    "YAML Key Sort": {
      "Sorts the YAML keys based on the order and priority specified. Note: may remove blank lines as well.": false,
      "YAML Key Priority Sort Order": "",
      "Priority Keys at Start of YAML": true,
      "YAML Sort Order for Other Keys": "None",
      "Sorts the YAML keys based on the order and priority specified. Note: removes blank lines as well.": false
    },
    "YAML Timestamp": {
      "Keep track of the date the file was last edited in the YAML front matter. Gets dates from file metadata.": false,
      "Date Created": true,
      "Date Created Key": "date created",
      "Date Modified": true,
      "Date Modified Key": "date modified",
      "Format": "dddd, MMMM Do YYYY, h:mm:ss a"
    },
    "YAML Title": {
      "Inserts the title of the file into the YAML frontmatter. Gets the title from the first H1 or filename if there is no H1.": false,
      "Title Key": "title",
      "Inserts the title of the file into the YAML frontmatter. Gets the title from the first H1 or filename.": false
    },
    "YAML Title Alias": {
      "Inserts the title of the file into the YAML frontmatter's aliases section. Gets the title from the first H1 or filename.": false,
      "Preserve existing aliases section style": true,
      "Keep alias that matches the filename": false,
      "Use the YAML key `linter-yaml-title-alias` to help with filename and heading changes": true
    },
    "Capitalize Headings": {
      "Headings should be formatted with capitalization": false,
      "Style": "First letter",
      "Ignore Cased Words": true,
      "Ignore Words": "macOS, iOS, iPhone, iPad, JavaScript, TypeScript, AppleScript",
      "Lowercase Words": "via, a, an, the, and, or, but, for, nor, so, yet, at, by, in, of, on, to, up, as, is, if, it, for, to, with, without, into, onto, per"
    },
    "File Name Heading": {
      "Inserts the file name as a H1 heading if no H1 heading exists.": false
    },
    "Header Increment": {
      "Heading levels should only increment by one level at a time": true
    },
    "Footnote after Punctuation": {
      "Ensures that footnote references are placed after punctuation, not before.": true
    },
    "Move Footnotes to the bottom": {
      "Move all footnotes to the bottom of the document.": false
    },
    "Re-Index Footnotes": {
      "Re-indexes footnote keys and footnote, based on the order of occurrence (NOTE: This rule deliberately does *not* preserve the relation between key and footnote, to be able to re-index duplicate keys.)": false,
      "Re-indexes footnote keys and footnote, based on the order of occurence (NOTE: This rule deliberately does *not* preserve the relation between key and footnote, to be able to re-index duplicate keys.)": false
    },
    "Convert Bullet List Markers": {
      "Converts common bullet list marker symbols to markdown list markers.": true
    },
    "Emphasis Style": {
      "Makes sure the emphasis style is consistent.": true,
      "Style": "underscore"
    },
    "No Bare URLs": {
      "Encloses bare URLs with angle brackets except when enclosed in back ticks, square braces, or single or double quotes.": false
    },
    "Ordered List Style": {
      "Makes sure that ordered lists follow the style specified.": true,
      "Number Style": "ascending",
      "Ordered List Indicator End Style": ".",
      "Makes sure that ordered lists follow the style specified. Note that 2 spaces or 1 tab is considered to be an indentation level.": true
    },
    "Proper Ellipsis": {
      "Replaces three consecutive dots with an ellipsis.": false
    },
    "Remove Consecutive List Markers": {
      "Removes consecutive list markers. Useful when copy-pasting list items.": false
    },
    "Remove Empty List Markers": {
      "Removes empty list markers, i.e. list items without content.": true
    },
    "Remove Hyphenated Line Breaks": {
      "Removes hyphenated line breaks. Useful when pasting text from textbooks.": false
    },
    "Remove Multiple Spaces": {
      "Removes two or more consecutive spaces. Ignores spaces at the beginning and ending of the line. ": false
    },
    "Strong Style": {
      "Makes sure the strong style is consistent.": false,
      "Style": "consistent"
    },
    "Two Spaces Between Lines with Content": {
      "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": false
    },
    "Compact YAML": {
      "Removes leading and trailing blank lines in the YAML front matter.": true,
      "Inner New Lines": false
    },
    "Consecutive blank lines": {
      "There should be at most one consecutive blank line.": true
    },
    "Convert Spaces to Tabs": {
      "Converts leading spaces to tabs.": false,
      "Tabsize": "4"
    },
    "Empty Line Around Code Fences": {
      "Ensures that there is an empty line around code fences unless they start or end a document.": true
    },
    "Empty Line Around Tables": {
      "Ensures that there is an empty line around github flavored tables unless they start or end a document.": true
    },
    "Heading blank lines": {
      "All headings have a blank line both before and after (except where the heading is at the beginning or end of the document).": true,
      "Bottom": true,
      "Empty Line Between Yaml and Header": true
    },
    "Line Break at Document End": {
      "Ensures that there is exactly one line break at the end of a document.": false
    },
    "Paragraph blank lines": {
      "All paragraphs should have exactly one blank line both before and after.": true
    },
    "Remove Empty Lines Between List Markers and Checklists": {
      "There should not be any empty lines between list markers and checklists.": false
    },
    "Remove link spacing": {
      "Removes spacing around link text.": false
    },
    "Remove Space around Fullwidth Characters": {
      "Ensures that fullwidth characters are not followed by whitespace (either single spaces or a tab). Note that this may causes issues with markdown format in some cases.": false,
      "Ensures that fullwidth characters are not followed by whitespace (either single spaces or a tab)": false
    },
    "Space after list markers": {
      "There should be a single space after list markers and checkboxes.": true
    },
    "Trailing spaces": {
      "Removes extra spaces after every line.": false,
      "Two Space Linebreak": false
    },
    "Space between Chinese Japanese or Korean and English or numbers": {
      "Ensures that Chinese, Japanese, or Korean and English or numbers are separated by a single space. Follows these [guidelines](https://github.com/sparanoid/chinese-copywriting-guidelines)": false
    },
    "Force YAML Escape": {
      "Escapes the values for the specified YAML keys.": false,
      "Force YAML Escape on Keys": ""
    },
    "Headings Start Line": {
      "Headings that do not start a line will have their preceding whitespace removed to make sure they get recognized as headers.": false
    },
    "Remove Trailing Punctuation in Heading": {
      "Removes the specified punctuation from the end of headings making sure to ignore the semicolon at the end of [HTML entity references](https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references).": false,
      "Trailing Punctuation": ".,;:!。，；：！"
    },
    "Unordered List Style": {
      "Makes sure that unordered lists follow the style specified.": true,
      "List item style": "-"
    },
    "Empty Line Around Blockquotes": {
      "Ensures that there is an empty line around blockquotes unless they start or end a document. **Note that an empty line is either one less level of nesting for blockquotes or a newline character.**": false
    },
    "Empty Line Around Math Blocks": {
      "Ensures that there is an empty line around math blocks using `Number of Dollar Signs to Indicate a Math Block` to determine how many dollar signs indicates a math block for single-line math.": false
    },
    "Move Math Block Indicators to Their Own Line": {
      "Move all starting and ending math block indicators to their own lines using `Number of Dollar Signs to Indicate a Math Block` to determine how many dollar signs indicates a math block for single-line math.": false
    },
    "Remove Space around Characters": {
      "Ensures that certain characters are not surrounded by whitespace (either single spaces or a tab). Note that this may causes issues with markdown format in some cases.": false,
      "Include Fullwidth Forms": true,
      "Include CJK Symbols and Punctuation": true,
      "Include Dashes": true,
      "Other symbols": ""
    },
    "Add Blockquote Indentation on Paste": {
      "Adds blockquotes to all but the first line, when the cursor is in a blockquote/callout line during pasting": false
    },
    "Prevent Double Checklist Indicator on Paste": {
      "Removes starting checklist indicator from the text to paste if the line the cursor is on in the file has a checklist indicator": false
    },
    "Prevent Double List Item Indicator on Paste": {
      "Removes starting list indicator from the text to paste if the line the cursor is on in the file has a list indicator": false
    },
    "Proper Ellipsis on Paste": {
      "Replaces three consecutive dots with an ellipsis even if they have a space between them in the text to paste": false
    },
    "Remove Hyphens on Paste": {
      "Removes hyphens from the text to paste": false
    },
    "Remove Leading or Trailing Whitespace on Paste": {
      "Removes any leading non-tab whitespace and all trailing whitespace for the text to paste": false
    },
    "Remove Leftover Footnotes from Quote on Paste": {
      "Removes any leftover footnote references for the text to paste": false
    },
    "Remove Multiple Blank Lines on Paste": {
      "Condenses multiple blank lines down into one blank line for the text to paste": false
    },
    "Auto-correct Common Misspellings": {
      "Uses a dictionary of common misspellings to automatically convert them to their proper spellings. See [auto-correct map](https://github.com/platers/obsidian-linter/tree/master/src/utils/auto-correct-misspellings.ts) for the full list of auto-corrected words.": false,
      "Ignore Words": ""
    }
  },
  "lintOnSave": true,
  "recordLintOnSaveLogs": false,
  "displayChanged": false,
  "foldersToIgnore": [],
  "linterLocale": "system-default",
  "logLevel": 4,
  "lintCommands": [],
  "customRegexes": [],
  "commonStyles": {
    "aliasArrayStyle": "single-line",
    "tagArrayStyle": "single-line",
    "minimumNumberOfDollarSignsToBeAMathBlock": 2,
    "escapeCharacter": "\""
  }
}
</pre>
</details>

## Expected Behavior

The linter should not break the callout block

## Screenshots

![demonstration](https://user-images.githubusercontent.com/8974488/214655569-c65fefd1-3357-4158-a999-6ba1086f3219.gif)


## Device

- [x] Desktop
- [ ] Mobile

Bug: Malformed Table Hangs Linter
## Describe the Bug

Malformed tables (e.g. incorrect header syntax) cause linter to hang indefinitely. 

## How to Reproduce

Lint the attached example markdown file which contains a malformed table: "Malformed Table Hangs Linter.md"

Please make sure to include logs and the rule(s) that cause the issue if applicable. You can include logs by going to the debug tab of the Linter and setting the log level to debug. Then enable the capturing of logs and either lint on save or lint the current file. Then go back to the debug tab and you can grab Linter logs from the last run. > Logs are not produced so unable to provide them.

If you are not sure which rule(s) may cause the issue, please attach the `data.json` file located at `<vault>/.obsidian/plugins/obsidian-linter/` or copy the config from the debug tab.

data.json attached. I am unclear which rule causes the issue.

## Expected Behavior

Linting is not expected to transform malformed tables, so it should just ignore them in that case. 

## Screenshots

NA

## Device

- [x] Desktop
- [ ] Mobile

## Additional Context

This bug makes it very difficult to bulk lint large folders since only one malformed table causes linter to hang and very difficult to find the troublesome file.

[data.json.zip](https://github.com/platers/obsidian-linter/files/10990321/data.json.zip)
[Malformed Table Hangs Linter.md](https://github.com/platers/obsidian-linter/files/10990337/Malformed.Table.Hangs.Linter.md)


Pull request:
Better Table Parsing
Fixes #652 
Fixes #597 

The regex table parsing logic was a little too complex and could cause performance issues. Thus it was replaced with a different parser that starts by identifying the table separator row and validates that it has no text earlier on the line, that there is an actual line before it, and that all cells in the separator row have at least one dash in it. Then it verifies that the separator row is not three dashes that has a line of text since that is either YAML or a header. Once that is done, it grabs any rows that may exist after the table separator row.

Changes Made:
- Updated the table parsing logic
- Added UTs for the table parsing
- Added a UT for the paragraph blank lines issue

Patch:
diff --git a/src/utils/ignore-types.ts b/src/utils/ignore-types.ts
index 385e400..66cfb69 100644
--- a/src/utils/ignore-types.ts
+++ b/src/utils/ignore-types.ts
@@ -1,5 +1,5 @@
-import {obsidianMultilineCommentRegex, tagWithLeadingWhitespaceRegex, wikiLinkRegex, yamlRegex, escapeDollarSigns, genericLinkRegex, tableRegex, urlRegex, anchorTagRegex} from './regex';
-import {getPositions, MDAstTypes} from './mdast';
+import {obsidianMultilineCommentRegex, tagWithLeadingWhitespaceRegex, wikiLinkRegex, yamlRegex, escapeDollarSigns, genericLinkRegex, urlRegex, anchorTagRegex} from './regex';
+import {getAllTablesInText, getPositions, MDAstTypes} from './mdast';
 import type {Position} from 'unist';
 import {replaceTextBetweenStartAndEndWithNewValue} from './strings';
 
@@ -23,15 +23,15 @@ export const IgnoreTypes: Record<string, IgnoreType> = {
   // RegExp
   yaml: {replaceAction: yamlRegex, placeholder: escapeDollarSigns('---\n---')},
   wikiLink: {replaceAction: wikiLinkRegex, placeholder: '{WIKI_LINK_PLACEHOLDER}'},
-  tag: {replaceAction: replaceTags, placeholder: '#tag-placeholder'},
   obsidianMultiLineComments: {replaceAction: obsidianMultilineCommentRegex, placeholder: '{OBSIDIAN_COMMENT_PLACEHOLDER}'},
-  table: {replaceAction: tableRegex, placeholder: '{TABLE_PLACEHOLDER}'},
   footnoteAtStartOfLine: {replaceAction: /^(\[\^\w+\]) ?([,.;!:?])/gm, placeholder: '{FOOTNOTE_AT_START_OF_LINE_PLACEHOLDER}'},
   footnoteAfterATask: {replaceAction: /- \[.] (\[\^\w+\]) ?([,.;!:?])/gm, placeholder: '{FOOTNOTE_AFTER_A_TASK_PLACEHOLDER}'},
   url: {replaceAction: urlRegex, placeholder: '{URL_PLACEHOLDER}'},
   anchorTag: {replaceAction: anchorTagRegex, placeholder: '{ANCHOR_PLACEHOLDER}'},
   // custom functions
   link: {replaceAction: replaceMarkdownLinks, placeholder: '{REGULAR_LINK_PLACEHOLDER}'},
+  tag: {replaceAction: replaceTags, placeholder: '#tag-placeholder'},
+  table: {replaceAction: replaceTables, placeholder: '{TABLE_PLACEHOLDER}'},
 } as const;
 
 export function ignoreListOfTypes(ignoreTypes: IgnoreType[], text: string, func: ((text: string) => string)): string {
@@ -166,3 +166,17 @@ function replaceTags(text: string, placeholder: string): IgnoreResults {
 
   return {newText: text, replacedValues: replacedValues};
 }
+
+function replaceTables(text: string, tablePlaceholder: string): IgnoreResults {
+  const tablePositions = getAllTablesInText(text);
+
+  const replacedTables: string[] = new Array(tablePositions.length);
+  let index = 0;
+  const length = replacedTables.length;
+  for (const tablePosition of tablePositions) {
+    replacedTables[length - 1 - index++] = text.substring(tablePosition.startIndex, tablePosition.endIndex);
+    text = replaceTextBetweenStartAndEndWithNewValue(text, tablePosition.startIndex, tablePosition.endIndex, tablePlaceholder);
+  }
+
+  return {newText: text, replacedValues: replacedTables};
+}
diff --git a/src/utils/mdast.ts b/src/utils/mdast.ts
index c09e948..6ae82ad 100644
--- a/src/utils/mdast.ts
+++ b/src/utils/mdast.ts
@@ -2,7 +2,7 @@ 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} from './regex';
+import {genericLinkRegex, tableRow, tableSeparator, tableStartingPipe} 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';
@@ -743,3 +743,105 @@ function addBlankLinesAroundStartAndStopMathIndicators(text: string, mathBlockSt
 
   return replaceTextBetweenStartAndEndWithNewValue(text, mathBlockStartIndex, mathBlockEndIndex, mathBlock);
 }
+
+/**
+ * Gets a list of all tables in the provided text and returns a list of starting and ending positions from the
+ * last to first found based on index.
+ * @param {string} text - The text to get the list of table locations from.
+ * @return {{startIndex: number, endIndex: number}[]} An array of start and end indexes of each table found from last to earliest.
+ */
+export function getAllTablesInText(text: string): {startIndex: number, endIndex: number}[] {
+  const regexMatches = [...text.matchAll(tableSeparator)];
+  const positions: {startIndex: number, endIndex: number}[] = [];
+  for (const match of regexMatches) {
+    const startOfCurrentLine = getStartOfLineIndex(text, match.index);
+    if (startOfCurrentLine === 0) {
+      continue;
+    }
+
+    const startOfPreviousLine = getStartOfLineIndex(text, startOfCurrentLine - 1);
+
+    const separatorRowMatch = match[0];
+    const tableRowSeparator = text.substring(startOfCurrentLine, match.index + separatorRowMatch.length);
+    if (isInvalidTableSeparatorRow(tableRowSeparator, separatorRowMatch)) {
+      continue;
+    }
+
+    let start = startOfPreviousLine;
+    let firstLine = text.substring(startOfPreviousLine, startOfCurrentLine - 1);
+    // if the separator row matches a yaml block indicator and the first line is missing
+    // a pipe, then we must assume that we are dealing with a header or yaml instead of a table
+    if (separatorRowMatch === '---' && !firstLine.includes('|')) {
+      continue;
+    }
+
+    firstLine = firstLine.replace(tableStartingPipe, (match: string)=> {
+      // do nothing if the table only has whitespace or a pipe before it
+      const trimmedMatch = match.trim();
+      if (trimmedMatch === '' || trimmedMatch === '|') {
+        return '';
+      }
+
+      start += match.length - 1;
+
+      return '';
+    });
+    let delimiterLine = separatorRowMatch.replace(tableStartingPipe, '');
+    if (firstLine.endsWith('|')) {
+      firstLine = firstLine.slice(0, -1);
+    }
+
+    if (delimiterLine.endsWith('|')) {
+      delimiterLine = delimiterLine.slice(0, -1);
+    }
+
+    // if the delimiter row and the first row do not have the same amount of cells,
+    // we are not dealing with a table
+    if (firstLine.split('|').length !== delimiterLine.split('|').length) {
+      continue;
+    }
+
+    let end = match.index + match[0].length;
+
+    if (end >= text.length - 1) {
+      positions.push({
+        startIndex: start,
+        endIndex: text.length,
+      });
+
+      continue;
+    }
+
+    const remainingLines = text.substring(end + 1).split('\n');
+    let index = 0;
+    // grab rows as part of the table until empty line or it no longer matches row content
+    while (index < remainingLines.length && tableRow.test(remainingLines[index])) {
+      end += remainingLines[index].length + 1;
+      index++;
+    }
+
+    positions.push({
+      startIndex: start,
+      endIndex: end,
+    });
+  }
+
+  return positions.reverse();
+}
+
+function isInvalidTableSeparatorRow(fullRow: string, separatorMatch: string): boolean {
+  if (fullRow.trim() === '') {
+    return true;
+  }
+
+  // The regex for the separator allows for two back to back pipes in the middle of the row, so we need to filter those results out
+  // since they are not valid
+  if (separatorMatch.includes('||')) {
+    return true;
+  }
+
+  // handle a scenario where the regex fails to work as intended and matches the ending of an invalid table separator
+  // it could contain text or an invalid table cell for the separator
+  const nonSeparatorContent = fullRow.replace(separatorMatch, '');
+  return /[^\s>]/.test(nonSeparatorContent);
+}
diff --git a/src/utils/regex.ts b/src/utils/regex.ts
index 97c7510..c3b8d3e 100644
--- a/src/utils/regex.ts
+++ b/src/utils/regex.ts
@@ -1,4 +1,5 @@
 
+import {getAllTablesInText} from './mdast';
 import {makeSureContentHasEmptyLinesAddedBeforeAndAfter} from './strings';
 
 // Useful regexes
@@ -18,9 +19,9 @@ export const obsidianMultilineCommentRegex = /^%%\n[^%]*\n%%/gm;
 export const wordSplitterRegex = /[,\s]+/;
 export const ellipsisRegex = /(\. ?){2}\./g;
 export const lineStartingWithWhitespaceOrBlockquoteTemplate = `\\s*(>\\s*)*`;
-// Note that the following regex has an issue where if the table is followed by another table with only 1 blank line between them, it considers them to be one table
-// if this becomes an issue, we can address it then
-export const tableRegex = /^((((>[ ]?)*)|([ ]{0,3}))\[.*?\][ \t]*\n)?((((>[ ]?)*)|([ ]{0,3}))\S+.*?\|.*?\n([^\n]*?\|[^\n]*?\n)*?)?(((>[ ]?)*)|([ ]{0,3}))[|\-+:.][ \-+|:.]*?\|[ \-+|:.]*(?:\n?[^\n]*?\|([^\n]*?)*)+/gm;
+export const tableSeparator = /(\|? *:?-{1,}:? *\|?)(\| *:?-{1,}:? *\|?)*( |\t)*$/gm;
+export const tableStartingPipe = /^(((>[ ]?)*)|([ ]{0,3}))\|/m;
+export const tableRow = /[^\n]*?\|[^\n]*?(\n|$)/m;
 export const urlRegex = /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s`\]'"‘’“”>]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s`\]'"‘’“”>]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s`\]'"‘’“”>]{2,}|www\.[a-zA-Z0-9]+\.[^\s`\]'"‘’“”>]{2,})/gi;
 export const anchorTagRegex = /<a[\s]+([^>]+)>((?:.(?!<\/a>))*.)<\/a>/g;
 
@@ -63,21 +64,13 @@ export function removeSpacesInWikiLinkText(text: string): string {
  * @return {string} The text with an empty line before and after tables unless the table starts off the file
  */
 export function ensureEmptyLinesAroundTables(text: string): string {
-  const tableMatches = text.match(tableRegex);
-  if (tableMatches == null) {
+  const tablePositions = getAllTablesInText(text);
+  if (tablePositions.length === 0) {
     return text;
   }
 
-  for (const table of tableMatches) {
-    let start = text.indexOf(table.trimStart());
-    const end = start + table.length;
-    if (table.trim().startsWith('>')) {
-      while (text.charAt(start).trim() === '' || text.charAt(start) === '>') {
-        start++;
-      }
-    }
-
-    text = makeSureContentHasEmptyLinesAddedBeforeAndAfter(text, start, end);
+  for (const tablePosition of tablePositions) {
+    text = makeSureContentHasEmptyLinesAddedBeforeAndAfter(text, tablePosition.startIndex, tablePosition.endIndex);
   }
 
   return text;
diff --git a/src/utils/strings.ts b/src/utils/strings.ts
index ea758fa..939d0ef 100644
--- a/src/utils/strings.ts
+++ b/src/utils/strings.ts
@@ -272,7 +272,7 @@ export function convertStringVersionOfEscapeCharactersToEscapeCharacters(val: st
 }
 
 export function getStartOfLineIndex(text: string, indexToStartFrom: number): number {
-  if (indexToStartFrom < 2) {
+  if (indexToStartFrom == 0) {
     return indexToStartFrom;
   }
 


Test patch:
diff --git a/__tests__/get-all-tables-in-text.test.ts b/__tests__/get-all-tables-in-text.test.ts
new file mode 100644
index 0000000..35d7a48
--- /dev/null
+++ b/__tests__/get-all-tables-in-text.test.ts
@@ -0,0 +1,220 @@
+import {getAllTablesInText} from '../src/utils/mdast';
+import dedent from 'ts-dedent';
+
+type tablesInTextTestCase = {
+  name: string,
+  text: string,
+  expectedTablesInText: number,
+  expectedPositions: {startIndex:number, endIndex: number}[]
+};
+
+const getTablesInTextTestCases: tablesInTextTestCase[] = [
+  {
+    name: 'matches simple table',
+    text: dedent`
+      Here is some text
+      | column1 | column2 |
+      | ------- | ------- |
+      | data1   | data2   |
+    `,
+    expectedTablesInText: 1,
+    expectedPositions: [{startIndex: 18, endIndex: 83}],
+  },
+  {
+    name: 'matches empty table as a single table',
+    text: dedent`
+      Here is some text
+      |||
+      |-|-|
+    `,
+    expectedTablesInText: 1,
+    expectedPositions: [{startIndex: 18, endIndex: 27}],
+  },
+  {
+    name: 'does not get a table where the header and delimiter have a different cell count',
+    text: dedent`
+      Here is some text
+      |||
+      |-|-|-|
+    `,
+    expectedTablesInText: 0,
+    expectedPositions: [],
+  },
+  {
+    name: 'matches table where delimiter has all pipes while the header is missing optional pipes',
+    text: dedent`
+      Here is some text
+      heading1|heading2
+      |-|-|
+    `,
+    expectedTablesInText: 1,
+    expectedPositions: [{startIndex: 18, endIndex: 41}],
+  },
+  {
+    name: 'matches table where delimiter has some pipes while the header is missing optional pipes',
+    text: dedent`
+      Here is some text
+      heading1|heading2
+      |-|-
+    `,
+    expectedTablesInText: 1,
+    expectedPositions: [{startIndex: 18, endIndex: 40}],
+  },
+  {
+    name: 'matches table where there is trailing space at the end of the table header and delimiter',
+    text: dedent`
+      Here is some text
+      |heading1|heading2|${'  '}
+      |-|-|${'  '}
+    `,
+    expectedTablesInText: 1,
+    expectedPositions: [{startIndex: 18, endIndex: 47}],
+  },
+  {
+    name: 'matches table that uses the alignment operators (:) in table delimiter',
+    text: dedent`
+      Here is some text
+      |heading1|heading2|heading3|heading4|heading5
+      |:-|-:|-| :----:  |   -${'      '}
+    `,
+    expectedTablesInText: 1,
+    expectedPositions: [{startIndex: 18, endIndex: 93}],
+  },
+  {
+    name: 'does not match table with a delimiter row that has an empty cell devoid of any characters',
+    text: dedent`
+      Here is some text
+      heading1|heading2|heading3
+      |:-||-|
+    `,
+    expectedTablesInText: 0,
+    expectedPositions: [],
+  },
+  {
+    name: 'does not match table with a delimiter row that has a cell at the start or middle that is missing a dash',
+    text: dedent`
+      Here is some text
+      heading1|heading2|heading3
+      |:-| |-|
+    `,
+    expectedTablesInText: 0,
+    expectedPositions: [],
+  },
+  {
+    name: 'does not match a table without a table header',
+    text: dedent`
+      Here is some text
+      |-|-|-|
+    `,
+    expectedTablesInText: 0,
+    expectedPositions: [],
+  },
+  {
+    name: 'matches text that precedes a table header when the table only has one column',
+    text: dedent`
+      Here is some text
+      |-|
+    `,
+    expectedTablesInText: 1,
+    expectedPositions: [{startIndex: 0, endIndex: 21}],
+  },
+  {
+    name: 'does not match table in blockquote that is missing table separator row',
+    text: dedent`
+      > Here is some text
+      > | Looks like a table | Column2 |
+      > | Another | Row |
+      > | One | More |
+    `,
+    expectedTablesInText: 0,
+    expectedPositions: [],
+  },
+  {
+    name: 'matches a table in blockquote when it has a separator row',
+    text: dedent`
+      > Here is some text
+      > | Looks like a table | Column2 |
+      > | :--- | ----: |
+      > | One | More |
+    `,
+    expectedTablesInText: 1,
+    expectedPositions: [{startIndex: 22, endIndex: 90}],
+  },
+  {
+    name: 'matches recognizes two separate tables in a blockquote/callout when there is a blank line between them',
+    text: dedent`
+      > Here is some text
+      > | Looks like a table | Column2 |
+      > | :--- | ----: |
+      > | One | More |
+      >
+      > | Header 1 | Header 2 |
+      > | -------- | -------- |
+      > | Text | Text 2 |
+      ${''}
+      Some more text here...
+    `,
+    expectedTablesInText: 2,
+    expectedPositions: [{startIndex: 95, endIndex: 164}, {startIndex: 22, endIndex: 90}],
+  },
+  {
+    name: 'matches two tables with a blank line between them',
+    text: dedent`
+      Here is some text
+      | column1 | column2 |
+      | ------- | ------- |
+      | data1   | data2   |
+      ${''}
+      | column1 | column2 |
+      | ------- | ------- |
+      | data3   | data4   |
+    `,
+    expectedTablesInText: 2,
+    expectedPositions: [{startIndex: 85, endIndex: 150}, {startIndex: 18, endIndex: 83}],
+  },
+  {
+    name: 'matches indented table',
+    text: dedent`
+      Here is some text
+      ${'   '}| column1 | column2 |
+      ${'   '}| ------- | ------- |
+      ${'   '}| data1   | data2   |
+    `,
+    expectedTablesInText: 1,
+    expectedPositions: [{startIndex: 18, endIndex: 92}],
+  },
+  {
+    name: 'does not match yaml indicators',
+    text: dedent`
+      ---
+      ---
+    `,
+    expectedTablesInText: 0,
+    expectedPositions: [],
+  },
+  {
+    name: 'does not match yaml frontmatter',
+    text: dedent`
+      ---
+      date: 06/16/2022
+      type: topic
+      tags: 
+      keywords: []
+      status: WIP
+      ---
+    `,
+    expectedTablesInText: 0,
+    expectedPositions: [],
+  },
+];
+
+describe('Get All Tables in Text', () => {
+  for (const testCase of getTablesInTextTestCases) {
+    it(testCase.name, () => {
+      const tablePositions = getAllTablesInText(testCase.text);
+
+      expect(tablePositions.length).toEqual(testCase.expectedTablesInText);
+      expect(tablePositions).toEqual(testCase.expectedPositions);
+    });
+  }
+});