Repository: platers/obsidian-linter (ts)

Issue:
Bug: `YAML Title` rule incorrectly considers `#tag` after header as a part of the title
## Describe the Bug

`YAML Title` rule incorrectly considers `#tag` after header as a part of the title

## How to Reproduce

Markup

``` markdown
# title
#tag
```

`YAML Title` rule.

## Expected Behavior

``` markdown
---
title: title
---
# title
#tag
```

## Actual Behavior

``` markdown
---
title: title
#tag
---
# title
#tag
```



Pull request description:
Don't include whitespace when parsing tags
Fixes #551

Patch:
diff --git a/src/rules/move-tags-to-yaml.ts b/src/rules/move-tags-to-yaml.ts
index b909600..9905b26 100644
--- a/src/rules/move-tags-to-yaml.ts
+++ b/src/rules/move-tags-to-yaml.ts
@@ -2,7 +2,7 @@ import {Options, RuleType} from '../rules';
 import RuleBuilder, {DropdownOptionBuilder, ExampleBuilder, OptionBuilderBase, TextAreaOptionBuilder} from './rule-builder';
 import dedent from 'ts-dedent';
 import {ignoreListOfTypes, IgnoreTypes} from '../utils/ignore-types';
-import {tagRegex} from '../utils/regex';
+import {matchTagRegex, tagWithLeadingWhitespaceRegex} from '../utils/regex';
 import {
   convertTagValueToStringOrStringArray,
   getYamlSectionValue,
@@ -43,8 +43,8 @@ export default class MoveTagsToYaml extends RuleBuilder<MoveTagsToYamlOptions> {
   }
   apply(text: string, options: MoveTagsToYamlOptions): string {
     return ignoreListOfTypes([IgnoreTypes.code, IgnoreTypes.inlineCode, IgnoreTypes.math, IgnoreTypes.html], text, (text) => {
-      const tags = text.match(tagRegex);
-      if (!tags) {
+      const tags = matchTagRegex(text);
+      if (tags.length === 0) {
         return text;
       }
 
@@ -91,7 +91,7 @@ export default class MoveTagsToYaml extends RuleBuilder<MoveTagsToYamlOptions> {
       });
 
       if (options.howToHandleExistingTags !== 'Nothing') {
-        text = text.replace(tagRegex, (tag: string) => {
+        text = text.replace(tagWithLeadingWhitespaceRegex, (tag: string) => {
           const hashtagIndex = tag.indexOf('#');
 
           const tagContents = tag.substring(hashtagIndex+1);
diff --git a/src/utils/ignore-types.ts b/src/utils/ignore-types.ts
index 296c623..7ed9945 100644
--- a/src/utils/ignore-types.ts
+++ b/src/utils/ignore-types.ts
@@ -1,4 +1,4 @@
-import {obsidianMultilineCommentRegex, tagRegex, wikiLinkRegex, yamlRegex, escapeDollarSigns, genericLinkRegex, tableRegex, urlRegex} from './regex';
+import {obsidianMultilineCommentRegex, tagWithLeadingWhitespaceRegex, wikiLinkRegex, yamlRegex, escapeDollarSigns, genericLinkRegex, tableRegex, urlRegex} from './regex';
 import {getPositions, MDAstTypes} from './mdast';
 import type {Position} from 'unist';
 import {replaceTextBetweenStartAndEndWithNewValue} from './strings';
@@ -24,7 +24,7 @@ export const IgnoreTypes: Record<string, IgnoreType> = {
   // RegExp
   yaml: {replaceAction: yamlRegex, placeholder: escapeDollarSigns('---\n---')},
   wikiLink: {replaceAction: wikiLinkRegex, placeholder: '{WIKI_LINK_PLACEHOLDER}'},
-  tag: {replaceAction: tagRegex, placeholder: '#tag-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}'},
@@ -155,3 +155,14 @@ function replaceMarkdownLinks(text: string, regularLinkPlaceholder: string): Ign
 
   return {newText: text, replacedValues: replacedRegularLinks};
 }
+
+function replaceTags(text: string, placeholder: string): IgnoreResults {
+  const replacedValues: string[] = [];
+
+  text = text.replace(tagWithLeadingWhitespaceRegex, (match, whitespace, tag) => {
+    replacedValues.push(tag);
+    return whitespace + placeholder;
+  });
+
+  return {newText: text, replacedValues: replacedValues};
+}
diff --git a/src/utils/regex.ts b/src/utils/regex.ts
index 3873035..e35900f 100644
--- a/src/utils/regex.ts
+++ b/src/utils/regex.ts
@@ -13,7 +13,7 @@ export const codeBlockRegex = new RegExp(`${backtickBlockRegexTemplate}|${tildeB
 export const wikiLinkRegex = /(!?)\[{2}([^\][\n|]+)(\|([^\][\n|]+))?\]{2}/g;
 // based on https://davidwells.io/snippets/regex-match-markdown-links
 export const genericLinkRegex = /(!?)\[([^[]*)\](\(.*\))/g;
-export const tagRegex = /(?:\s|^)#[^\s#;.,><?!=+]+/g;
+export const tagWithLeadingWhitespaceRegex = /(\s|^)(#[^\s#;.,><?!=+]+)/g;
 export const obsidianMultilineCommentRegex = /^%%\n[^%]*\n%%/gm;
 export const wordSplitterRegex = /[,\s]+/;
 export const ellipsisRegex = /(\. ?){2}\./g;
@@ -100,3 +100,7 @@ export function getFirstHeaderOneText(text: string) {
 
   return '';
 }
+
+export function matchTagRegex(text: string): string[] {
+  return [...text.matchAll(tagWithLeadingWhitespaceRegex)].map((match) => match[2]);
+}


Test patch:
diff --git a/__tests__/yaml-title.test.ts b/__tests__/yaml-title.test.ts
index 09c9beb..a6d3818 100644
--- a/__tests__/yaml-title.test.ts
+++ b/__tests__/yaml-title.test.ts
@@ -127,5 +127,19 @@ ruleTest({
         # Title
       `,
     },
+    {
+      testName: 'Tag after first header is not considered as a part of the title',
+      before: dedent`
+        # title
+        #tag
+      `,
+      after: dedent`
+        ---
+        title: title
+        ---
+        # title
+        #tag
+      `,
+    },
   ],
 });