Repository: platers/obsidian-linter (ts)

Issue:
Rule: YAML alias from title
The rule should transform

```
# My title
```

into

```
---
aliases:
  - My title
---

# My title
```

This would be helpful as `Quick Switcher` (`Ctrl + O`) supports aliases, so notes could be found not only by filenames but also with titles

***

However, I am not sure how to handle edits

```
---
aliases:
  - Old title
---

# Old title
```

edited to

```
---
aliases:
  - Old title
---

# New title
```

How can we possibly detect that `Old title` was the previous title on `modify` event?

Pull request:
Add rule YAML Title Alias
Fixes #254 

Patch:
diff --git a/README.md b/README.md
index 56ef805..0b41271 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,7 @@ Documentation for all rules can be found in the [rules docs](https://github.com/
 - [insert-yaml-attributes](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#insert-yaml-attributes)
 - [yaml-timestamp](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#yaml-timestamp)
 - [yaml-title](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#yaml-title)
+- [yaml-title-alias](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#yaml-title-alias)
 - [escape-yaml-special-characters](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#escape-yaml-special-characters)
 
 ### Heading rules
diff --git a/docs/rules.md b/docs/rules.md
index 981ea00..c0a07c9 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -229,6 +229,61 @@ title: Filename
 
 ```
 
+### YAML Title Alias
+
+Alias: `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.
+
+Options:
+- YAML aliases section style: The style of the aliases YAML section
+	- Default: `Multi-line array`
+	- `Multi-line array`: ```aliases:\n  - Title```
+	- `Single-line array`: ```aliases: [Title]```
+	- `Single string that expands to multi-line array if needed`: ```aliases: Title```
+	- `Single string that expands to single-line array if needed`: ```aliases: Title```
+- Preserve existing aliases section style: If set, the `YAML aliases section style` setting applies only to the newly created sections
+	- Default: `true`
+- Keep alias that matches the filename: Such aliases are usually redundant
+	- Default: `false`
+
+Example: Adds a header with the title from heading.
+
+Before:
+
+```markdown
+# Obsidian
+```
+
+After:
+
+```markdown
+---
+aliases:
+  - Obsidian
+linter-yaml-title-alias: Obsidian
+---
+# Obsidian
+```
+Example: Adds a header with the title.
+
+Before:
+
+```markdown
+
+```
+
+After:
+
+```markdown
+---
+aliases:
+  - Filename
+linter-yaml-title-alias: Filename
+---
+
+```
+
 ### Escape YAML Special Characters
 
 Alias: `escape-yaml-special-characters`
diff --git a/src/rules.ts b/src/rules.ts
index a781fbd..ecf5703 100644
--- a/src/rules.ts
+++ b/src/rules.ts
@@ -10,11 +10,15 @@ import {
   loadYAML,
   moveFootnotesToEnd,
   yamlRegex,
-  escapeYamlString,
+  toYamlString,
   makeEmphasisOrBoldConsistent,
   addTwoSpacesAtEndOfLinesFollowedByAnotherLineOfTextContent,
   makeSureThereIsOnlyOneBlankLineBeforeAndAfterParagraphs,
   removeSpacesInLinkText,
+  toSingleLineArrayYamlString,
+  setYamlSection,
+  getYamlSectionValue,
+  removeYamlSection,
 } from './utils';
 import {
   Option,
@@ -1606,7 +1610,7 @@ export const rules: Rule[] = [
         });
         title = title || options['metadata: file name'];
 
-        title = escapeYamlString(title);
+        title = toYamlString(title);
 
         return formatYAML(text, (text) => {
           const title_match_str = `\n${options['Title Key']}.*\n`;
@@ -1658,6 +1662,292 @@ export const rules: Rule[] = [
       [new TextOption('Title Key', 'Which YAML key to use for title', 'title')],
   ),
 
+  new Rule(
+      '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.',
+      RuleType.YAML,
+      (text: string, options = {}) => {
+        const ALIASES_YAML_SECTION_NAME = 'aliases';
+        const LINTER_ALIASES_HELPER_NAME = 'linter-yaml-title-alias';
+
+        const optionsObj = {
+          yamlAliasesSectionStyle: options['YAML aliases section style'] as string,
+          preserveExistingAliasesSectionStyle: options['Preserve existing aliases section style'] as boolean ?? true,
+          keepAliasThatMatchesTheFilename: options['Keep alias that matches the filename'] as boolean,
+          fileName: options['metadata: file name'] as string,
+        };
+
+        text = initYAML(text);
+        let title = ignoreCodeBlocksYAMLTagsAndLinks(text, (text) => {
+          const result = text.match(/^#\s+(.*)/m);
+          if (result) {
+            return result[1];
+          }
+          return '';
+        });
+        title = title || optionsObj.fileName;
+
+        const shouldRemoveTitleAlias = !optionsObj.keepAliasThatMatchesTheFilename && title === optionsObj.fileName;
+
+        let yaml = text.match(yamlRegex)[1];
+
+        let previousTitle = loadYAML(getYamlSectionValue(yaml, LINTER_ALIASES_HELPER_NAME));
+
+
+        let requiresChanges = true;
+
+        if (previousTitle === title && !shouldRemoveTitleAlias) {
+          requiresChanges = false;
+        }
+
+        if (previousTitle === null && shouldRemoveTitleAlias) {
+          requiresChanges = false;
+        }
+
+        if (!requiresChanges && optionsObj.preserveExistingAliasesSectionStyle) {
+          return text;
+        }
+
+        let aliasesValue = getYamlSectionValue(yaml, ALIASES_YAML_SECTION_NAME);
+
+        if (!aliasesValue) {
+          if (shouldRemoveTitleAlias) {
+            return text;
+          }
+
+          let emptyValue;
+          switch (optionsObj.yamlAliasesSectionStyle) {
+            case 'Multi-line array':
+              emptyValue = '\n  - \'\'';
+              break;
+            case 'Single-line array':
+              emptyValue = ' [\'\']';
+              break;
+            case 'Single string that expands to multi-line array if needed':
+            case 'Single string that expands to single-line array if needed':
+              emptyValue = ' \'\'';
+              break;
+            default:
+              throw new Error(`Unsupported setting 'YAML aliases section style': ${optionsObj.yamlAliasesSectionStyle}`);
+          }
+
+          let newYaml = yaml;
+          newYaml = setYamlSection(newYaml, ALIASES_YAML_SECTION_NAME, emptyValue);
+          newYaml = setYamlSection(newYaml, LINTER_ALIASES_HELPER_NAME, ' \'\'');
+
+          text = text.replace(`---\n${yaml}---`, `---\n${newYaml}---`);
+          yaml = newYaml;
+          aliasesValue = getYamlSectionValue(yaml, ALIASES_YAML_SECTION_NAME);
+          previousTitle = '';
+        }
+
+        const isMultiline = aliasesValue.includes('\n');
+        const parsedAliases = loadYAML(aliasesValue);
+
+        const isSingleString = !isMultiline && aliasesValue.match(/^\[.*\]/) === null;
+
+        let resultAliasesArray = isSingleString ? [parsedAliases] : [...parsedAliases];
+
+        const previousTitleIndex = resultAliasesArray.indexOf(previousTitle);
+        if (previousTitleIndex !== -1) {
+          if (shouldRemoveTitleAlias) {
+            resultAliasesArray.splice(previousTitleIndex, 1);
+          } else {
+            resultAliasesArray[previousTitleIndex] = title;
+          }
+        } else if (!shouldRemoveTitleAlias) {
+          resultAliasesArray = [title, ...resultAliasesArray];
+        }
+
+        if (!requiresChanges) {
+          switch (optionsObj.yamlAliasesSectionStyle) {
+            case 'Multi-line array':
+              if (isMultiline) {
+                return text;
+              }
+              break;
+            case 'Single-line array':
+              if (!isMultiline && !isSingleString) {
+                return text;
+              }
+              break;
+            case 'Single string that expands to multi-line array if needed':
+              if (isSingleString) {
+                return text;
+              }
+              if (isMultiline && resultAliasesArray.length > 1) {
+                return text;
+              }
+              break;
+            case 'Single string that expands to single-line array if needed':
+              if (isSingleString) {
+                return text;
+              }
+              if (!isMultiline && resultAliasesArray.length > 1) {
+                return text;
+              }
+              break;
+          }
+        }
+
+        let resultStyle;
+
+        if (resultAliasesArray.length === 0) {
+          resultStyle = 'Remove';
+        } else if (!optionsObj.preserveExistingAliasesSectionStyle) {
+          switch (optionsObj.yamlAliasesSectionStyle) {
+            case 'Multi-line array':
+              resultStyle = 'Multi-line array';
+              break;
+            case 'Single-line array':
+              resultStyle = 'Single-line array';
+              break;
+            case 'Single string that expands to multi-line array if needed':
+              if (resultAliasesArray.length === 1) {
+                resultStyle = 'Single string';
+              } else {
+                resultStyle = 'Multi-line array';
+              }
+              break;
+            case 'Single string that expands to single-line array if needed':
+              if (resultAliasesArray.length === 1) {
+                resultStyle = 'Single string';
+              } else {
+                resultStyle = 'Single-line array';
+              }
+              break;
+          }
+        } else if (isSingleString) {
+          if (resultAliasesArray.length === 1) {
+            resultStyle = 'Single string';
+          } else {
+            switch (optionsObj.yamlAliasesSectionStyle) {
+              case 'Multi-line array':
+              case 'Single string that expands to multi-line array if needed':
+                resultStyle = 'Multi-line array';
+                break;
+              case 'Single-line array':
+              case 'Single string that expands to single-line array if needed':
+                resultStyle = 'Single-line array';
+                break;
+            }
+          }
+        } else if (isMultiline) {
+          resultStyle = 'Multi-line array';
+        } else {
+          resultStyle = 'Single-line array';
+        }
+
+        let newAliasesYaml;
+
+        switch (resultStyle) {
+          case 'Remove':
+            break;
+          case 'Multi-line array':
+            newAliasesYaml = `\n${toYamlString(resultAliasesArray)}`.replace(/\n-/g, '\n  -');
+            break;
+          case 'Single-line array':
+            newAliasesYaml = ` ${toSingleLineArrayYamlString(resultAliasesArray)}`;
+            break;
+          case 'Single string':
+            newAliasesYaml = resultAliasesArray.length === 0 ? '' : ` ${toYamlString(resultAliasesArray[0])}`;
+            break;
+          default:
+            throw new Error(`Unsupported resultStyle: ${resultStyle}`);
+        }
+
+        let newYaml = yaml;
+
+        if (resultStyle === 'Remove') {
+          newYaml = removeYamlSection(newYaml, ALIASES_YAML_SECTION_NAME);
+        } else {
+          newYaml = setYamlSection(newYaml, ALIASES_YAML_SECTION_NAME, newAliasesYaml);
+        }
+
+        if (shouldRemoveTitleAlias) {
+          newYaml = removeYamlSection(newYaml, LINTER_ALIASES_HELPER_NAME);
+        } else {
+          newYaml = setYamlSection(newYaml, LINTER_ALIASES_HELPER_NAME, ` ${toYamlString(title)}`);
+        }
+
+        text = text.replace(yaml, newYaml);
+
+        return text;
+      },
+      [
+        new Example(
+            'Adds a header with the title from heading.',
+            dedent`
+      # Obsidian
+      `,
+            dedent`
+      ---
+      aliases:
+        - Obsidian
+      linter-yaml-title-alias: Obsidian
+      ---
+      # Obsidian
+      `,
+            {
+              'YAML aliases section style': 'Multi-line array',
+            },
+        ),
+        new Example(
+            'Adds a header with the title.',
+            dedent`
+      `,
+            dedent`
+      ---
+      aliases:
+        - Filename
+      linter-yaml-title-alias: Filename
+      ---
+
+      `,
+            {
+              'metadata: file name': 'Filename',
+              'YAML aliases section style': 'Multi-line array',
+              'Keep alias that matches the filename': true,
+            },
+        ),
+      ],
+      [
+        new DropdownOption(
+            'YAML aliases section style',
+            'The style of the aliases YAML section',
+            'Multi-line array',
+            [
+              new DropdownRecord(
+                  'Multi-line array',
+                  '```aliases:\\n  - Title```',
+              ),
+              new DropdownRecord(
+                  'Single-line array',
+                  '```aliases: [Title]```',
+              ),
+              new DropdownRecord(
+                  'Single string that expands to multi-line array if needed',
+                  '```aliases: Title```',
+              ),
+              new DropdownRecord(
+                  'Single string that expands to single-line array if needed',
+                  '```aliases: Title```',
+              ),
+            ],
+        ),
+        new BooleanOption(
+            'Preserve existing aliases section style',
+            'If set, the `YAML aliases section style` setting applies only to the newly created sections',
+            true,
+        ),
+        new BooleanOption(
+            'Keep alias that matches the filename',
+            'Such aliases are usually redundant',
+            false,
+        ),
+      ],
+  ),
+
   new Rule(
       'Escape YAML Special Characters',
       'Escapes colons with a space after them (: ), single quotes (\'), and double quotes (") in YAML.',
diff --git a/src/utils.ts b/src/utils.ts
index aebabe8..746d4aa 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -480,16 +480,52 @@ export function stripCr(text: string): string {
 }
 
 export function loadYAML(yaml_text: string): any {
+  if (yaml_text == null) {
+    return null;
+  }
+
   // replacing tabs at the beginning of new lines with 2 spaces fixes loading yaml that has tabs at the start of a line
   // https://github.com/platers/obsidian-linter/issues/157
   const parsed_yaml = load(yaml_text.replace(/\n(\t)+/g, '\n  ')) as {};
-  if (!parsed_yaml) {
+  if (parsed_yaml == null) {
     return {};
   }
 
   return parsed_yaml;
 }
 
-export function escapeYamlString(str: string): string {
-  return dump(str, {lineWidth: -1}).slice(0, -1);
+export function toYamlString(obj: any): string {
+  return dump(obj, {lineWidth: -1}).slice(0, -1);
+}
+
+export function toSingleLineArrayYamlString<T>(arr: T[]): string {
+  return dump(arr, {flowLevel: 0}).slice(0, -1);
+}
+
+function getYamlSectionRegExp(rawKey: string): RegExp {
+  return new RegExp(`(?<=^|\\n)${rawKey}:[ \\t]*(\\S.*|(?:\\n {2}\\S.*)*)\\n(?=$|\\S)`);
+}
+
+export function setYamlSection(yaml: string, rawKey: string, rawValue: string): string {
+  const yamlSectionEscaped = `${rawKey}:${rawValue}\n`;
+  let isReplaced = false;
+  let result = yaml.replace(getYamlSectionRegExp(rawKey), () => {
+    isReplaced = true;
+    return yamlSectionEscaped;
+  });
+  if (!isReplaced) {
+    result = `${yaml}${yamlSectionEscaped}`;
+  }
+  return result;
+}
+
+export function getYamlSectionValue(yaml: string, rawKey: string): string | null {
+  const match = yaml.match(getYamlSectionRegExp(rawKey));
+  const result = match == null ? null : match[1];
+  return result;
+}
+
+export function removeYamlSection(yaml: string, rawKey: string): string {
+  const result = yaml.replace(getYamlSectionRegExp(rawKey), '');
+  return result;
 }


Test patch:
diff --git a/src/test.ts b/src/test.ts
index 7269283..370a10c 100644
--- a/src/test.ts
+++ b/src/test.ts
@@ -1205,6 +1205,767 @@ describe('YAML Title', () => {
   });
 });
 
+describe('YAML Title Alias', () => {
+  it('Creates multi-line array aliases when missing', () => {
+    const before = dedent`
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases:
+      - Title
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Multi-line array'})).toBe(after);
+  });
+
+  it('Creates single-line array aliases when missing', () => {
+    const before = dedent`
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases: [Title]
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Single-line array'})).toBe(after);
+  });
+
+  it('Creates single string alias when missing', () => {
+    const before = dedent`
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases: Title
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Single string that expands to multi-line array if needed'})).toBe(after);
+  });
+
+  it('Creates multi-line array aliases when empty', () => {
+    const before = dedent`
+    ---
+    aliases: 
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases:
+      - Title
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Multi-line array'})).toBe(after);
+  });
+
+  it('Creates single-line array aliases when empty', () => {
+    const before = dedent`
+    ---
+    aliases: 
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases: [Title]
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Single-line array'})).toBe(after);
+  });
+
+  it('Creates single string alias when empty', () => {
+    const before = dedent`
+    ---
+    aliases: 
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases: Title
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Single string that expands to multi-line array if needed'})).toBe(after);
+  });
+
+  it('Updates first alias in multi-line array', () => {
+    const before = dedent`
+    ---
+    aliases:
+      - alias1
+      - alias2
+      - alias3
+    linter-yaml-title-alias: alias1
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases:
+      - Title
+      - alias2
+      - alias3
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before)).toBe(after);
+  });
+
+  it('Adds before first alias in multi-line array', () => {
+    const before = dedent`
+    ---
+    aliases:
+      - alias1
+      - alias2
+      - alias3
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases:
+      - Title
+      - alias1
+      - alias2
+      - alias3
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before)).toBe(after);
+  });
+
+  it('Updates first alias in single-line array', () => {
+    const before = dedent`
+    ---
+    aliases: [alias1, alias2, alias3]
+    linter-yaml-title-alias: alias1
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases: [Title, alias2, alias3]
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before)).toBe(after);
+  });
+
+  it('Adds before first alias in single-line array', () => {
+    const before = dedent`
+    ---
+    aliases: [alias1, alias2, alias3]
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases: [Title, alias1, alias2, alias3]
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before)).toBe(after);
+  });
+
+  it('Updates single string alias', () => {
+    const before = dedent`
+    ---
+    aliases: other alias
+    linter-yaml-title-alias: other alias
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases: Title
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before)).toBe(after);
+  });
+
+  it('Changes single string aliases to multi-line array when adding', () => {
+    const before = dedent`
+    ---
+    aliases: other alias
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases:
+      - Title
+      - other alias
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Single string that expands to multi-line array if needed'})).toBe(after);
+  });
+
+  it('Changes single string aliases to single-line array when adding', () => {
+    const before = dedent`
+    ---
+    aliases: other alias
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases: [Title, other alias]
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Single string that expands to single-line array if needed'})).toBe(after);
+  });
+
+  it('Titles with special characters are escaped', () => {
+    const before = dedent`
+    # Title with: colon, 'quote', "single quote"
+    `;
+
+    const after = dedent`
+    ---
+    aliases:
+      - 'Title with: colon, ''quote'', "single quote"'
+    linter-yaml-title-alias: 'Title with: colon, ''quote'', "single quote"'
+    ---
+    # Title with: colon, 'quote', "single quote"
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Multi-line array'})).toBe(after);
+  });
+
+  it('Position of existing non-empty aliases section is preserved', () => {
+    const before = dedent`
+    ---
+    key1: value1
+    key2: value2
+    aliases:
+      - alias1
+      - alias2
+      - alias3
+    key3: value3
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    key1: value1
+    key2: value2
+    aliases:
+      - Title
+      - alias1
+      - alias2
+      - alias3
+    key3: value3
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before)).toBe(after);
+  });
+
+  it('Position of existing empty aliases section is preserved', () => {
+    const before = dedent`
+    ---
+    key1: value1
+    key2: value2
+    aliases: 
+    key3: value3
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    key1: value1
+    key2: value2
+    aliases:
+      - Title
+    key3: value3
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Multi-line array'})).toBe(after);
+  });
+
+  it('Does not add alias that matches the filename for multi-line array style aliases section', () => {
+    const before = dedent`
+    ---
+    aliases:
+      - alias1
+    ---
+    # Filename
+    `;
+
+    const after = dedent`
+    ---
+    aliases:
+      - alias1
+    ---
+    # Filename
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'Keep alias that matches the filename': false, 'metadata: file name': 'Filename'})).toBe(after);
+  });
+
+  it('Does not add alias that matches the filename for single-line array style aliases section', () => {
+    const before = dedent`
+    ---
+    aliases: [alias1]
+    ---
+    # Filename
+    `;
+
+    const after = dedent`
+    ---
+    aliases: [alias1]
+    ---
+    # Filename
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'Keep alias that matches the filename': false, 'metadata: file name': 'Filename'})).toBe(after);
+  });
+
+  it('Does not add alias that matches the filename for single string style aliases section', () => {
+    const before = dedent`
+    ---
+    aliases: alias1
+    ---
+    # Filename
+    `;
+
+    const after = dedent`
+    ---
+    aliases: alias1
+    ---
+    # Filename
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'Keep alias that matches the filename': false, 'metadata: file name': 'Filename'})).toBe(after);
+  });
+
+  it('Does not add alias that matches the filename for multi-line array style aliases section, removes previous alias', () => {
+    const before = dedent`
+    ---
+    aliases:
+      - alias1
+      - alias2
+    linter-yaml-title-alias: alias1
+    ---
+    # Filename
+    `;
+
+    const after = dedent`
+    ---
+    aliases:
+      - alias2
+    ---
+    # Filename
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'Keep alias that matches the filename': false, 'metadata: file name': 'Filename'})).toBe(after);
+  });
+
+  it('Does not add alias that matches the filename for single-line array style aliases section, removes previous alias', () => {
+    const before = dedent`
+    ---
+    aliases: [alias1, alias2]
+    linter-yaml-title-alias: alias1
+    ---
+    # Filename
+    `;
+
+    const after = dedent`
+    ---
+    aliases: [alias2]
+    ---
+    # Filename
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'Keep alias that matches the filename': false, 'metadata: file name': 'Filename'})).toBe(after);
+  });
+
+  it('Does not add alias that matches the filename for single string style aliases section, removes previous alias', () => {
+    const before = dedent`
+    ---
+    aliases: alias1
+    linter-yaml-title-alias: alias1
+    ---
+    # Filename
+    `;
+
+    const after = dedent`
+    ---
+    ---
+    # Filename
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'Keep alias that matches the filename': false, 'metadata: file name': 'Filename'})).toBe(after);
+  });
+
+  it('Adds alias that matches the filename for multi-line array style aliases section', () => {
+    const before = dedent`
+    ---
+    aliases:
+      - alias1
+    ---
+    # Filename
+    `;
+
+    const after = dedent`
+    ---
+    aliases:
+      - Filename
+      - alias1
+    linter-yaml-title-alias: Filename
+    ---
+    # Filename
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'Keep alias that matches the filename': true, 'metadata: file name': 'Filename'})).toBe(after);
+  });
+
+  it('Adds alias that matches the filename for single-line array style aliases section', () => {
+    const before = dedent`
+    ---
+    aliases: [alias1]
+    ---
+    # Filename
+    `;
+
+    const after = dedent`
+    ---
+    aliases: [Filename, alias1]
+    linter-yaml-title-alias: Filename
+    ---
+    # Filename
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'Keep alias that matches the filename': true, 'metadata: file name': 'Filename'})).toBe(after);
+  });
+
+  it('Adds alias that matches the filename for single string style aliases section', () => {
+    const before = dedent`
+    ---
+    aliases: alias1
+    ---
+    # Filename
+    `;
+
+    const after = dedent`
+    ---
+    aliases:
+      - Filename
+      - alias1
+    linter-yaml-title-alias: Filename
+    ---
+    # Filename
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'Keep alias that matches the filename': true, 'metadata: file name': 'Filename', 'YAML aliases section style': 'Single string that expands to multi-line array if needed'})).toBe(after);
+  });
+
+  it('Replaces alias that matches the filename for multi-line array style aliases section', () => {
+    const before = dedent`
+    ---
+    aliases:
+      - alias1
+      - alias2
+    linter-yaml-title-alias: alias1
+    ---
+    # Filename
+    `;
+
+    const after = dedent`
+    ---
+    aliases:
+      - Filename
+      - alias2
+    linter-yaml-title-alias: Filename
+    ---
+    # Filename
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'Keep alias that matches the filename': true, 'metadata: file name': 'Filename'})).toBe(after);
+  });
+
+  it('Replaces alias that matches the filename for single-line array style aliases section', () => {
+    const before = dedent`
+    ---
+    aliases: [alias1, alias2]
+    linter-yaml-title-alias: alias1
+    ---
+    # Filename
+    `;
+
+    const after = dedent`
+    ---
+    aliases: [Filename, alias2]
+    linter-yaml-title-alias: Filename
+    ---
+    # Filename
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'Keep alias that matches the filename': true, 'metadata: file name': 'Filename'})).toBe(after);
+  });
+
+  it('Replaces alias that matches the filename for single string style aliases section, removes previous alias', () => {
+    const before = dedent`
+    ---
+    aliases: alias1
+    linter-yaml-title-alias: alias1
+    ---
+    # Filename
+    `;
+
+    const after = dedent`
+    ---
+    aliases: Filename
+    linter-yaml-title-alias: Filename
+    ---
+    # Filename
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'Keep alias that matches the filename': true, 'metadata: file name': 'Filename'})).toBe(after);
+  });
+
+  it('Dollar sign $ is handled properly', () => {
+    const before = dedent`
+    # Dollar $
+    `;
+
+    const after = dedent`
+    ---
+    aliases:
+      - Dollar $
+    linter-yaml-title-alias: Dollar $
+    ---
+    # Dollar $
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Multi-line array'})).toBe(after);
+  });
+
+  it('Converts from single-line array to multi-line array', () => {
+    const before = dedent`
+    ---
+    aliases: [Title]
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases:
+      - Title
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Multi-line array', 'Preserve existing aliases section style': false})).toBe(after);
+  });
+
+  it('Converts from single-line array to multi-line array for single string style setting', () => {
+    const before = dedent`
+    ---
+    aliases: [Title, alias2]
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases:
+      - Title
+      - alias2
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Single string that expands to multi-line array if needed', 'Preserve existing aliases section style': false})).toBe(after);
+  });
+
+  it('Converts from single string to multi-line array', () => {
+    const before = dedent`
+    ---
+    aliases: Title
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases:
+      - Title
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Multi-line array', 'Preserve existing aliases section style': false})).toBe(after);
+  });
+
+  it('Converts from multi-line array to single-line array', () => {
+    const before = dedent`
+    ---
+    aliases:
+      - Title
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases: [Title]
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Single-line array', 'Preserve existing aliases section style': false})).toBe(after);
+  });
+
+  it('Converts from multi-line array to single-line array for single string style setting', () => {
+    const before = dedent`
+    ---
+    aliases:
+      - Title
+      - alias2
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases: [Title, alias2]
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Single string that expands to single-line array if needed', 'Preserve existing aliases section style': false})).toBe(after);
+  });
+
+  it('Converts from single string to single-line array', () => {
+    const before = dedent`
+    ---
+    aliases: Title
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases: [Title]
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Single-line array', 'Preserve existing aliases section style': false})).toBe(after);
+  });
+
+  it('Converts from multi-line array to single string', () => {
+    const before = dedent`
+    ---
+    aliases:
+      - Title
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases: Title
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Single string that expands to multi-line array if needed', 'Preserve existing aliases section style': false})).toBe(after);
+  });
+
+  it('Converts from single-line array to single string', () => {
+    const before = dedent`
+    ---
+    aliases: [Title]
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    const after = dedent`
+    ---
+    aliases: Title
+    linter-yaml-title-alias: Title
+    ---
+    # Title
+    `;
+
+    expect(rulesDict['yaml-title-alias'].apply(before, {'YAML aliases section style': 'Single string that expands to multi-line array if needed', 'Preserve existing aliases section style': false})).toBe(after);
+  });
+});
+
 describe('Links', () => {
   it('Regular link with spaces stays the same', () => {
     const before = dedent`