Repository: platers/obsidian-linter (ts)

Issue:
Bug: Linter fails to recognize aliases containing commas
## Describe the Bug

This is related to the fix for #509

The following YAML array contains two aliases: 

`aliases: Scott, "Scott, Jr."`

Linter parses this into three aliases.

## How to Reproduce

I have attached the [data.json](https://github.com/platers/obsidian-linter/files/10134799/data.json.txt) file.

See the example above.

Linter produces a multiline array of three aliases:

```yaml
aliases:
  - Scott
  - '"Scott'
  - 'Jr."'
```

## Expected Behavior

Linter should parse the tags array exactly how Obsidian parses the tags array.

The example should produce a multiline tags array with two tags. The double quotes should be eliminated, and the tag containing the commas should not be escaped with single quotes.

Expected output:

``` yaml
aliases:
  - Scott
  - Scott, Jr.
```

## Device

- [x] Desktop
- [?] Mobile


Pull request description:
Updated Logic to Allow Multi-Line Array Formatting to Remove Unnecessary Escape Chararcters
Fixes #525 

Changes Made:
- Added tests to verify the removal of unnecessary escape characters is working for formatting yaml arrays
- Added logic around only escaping the array value if needed for multi-line arrays with the designation from the user

Patch:
diff --git a/src/main.ts b/src/main.ts
index 2fb6511..3ae9f65 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -56,6 +56,7 @@ const DEFAULT_SETTINGS: Partial<LinterSettings> = {
     tagArrayStyle: NormalArrayFormats.SingleLine,
     minimumNumberOfDollarSignsToBeAMathBlock: 2,
     escapeCharacter: '"',
+    removeUnnecessaryEscapeCharsForMultiLineArrays: false,
   },
 };
 
diff --git a/src/rules-runner.ts b/src/rules-runner.ts
index 903992b..b65891e 100644
--- a/src/rules-runner.ts
+++ b/src/rules-runner.ts
@@ -64,6 +64,7 @@ export class RulesRunner {
         aliasArrayStyle: runOptions.settings.commonStyles.aliasArrayStyle,
         tagArrayStyle: runOptions.settings.commonStyles.tagArrayStyle,
         defaultEscapeCharacter: runOptions.settings.commonStyles.escapeCharacter,
+        removeUnnecessaryEscapeCharsForMultiLineArrays: runOptions.settings.commonStyles.removeUnnecessaryEscapeCharsForMultiLineArrays,
       });
       timingEnd(rule.alias());
     }
diff --git a/src/rules.ts b/src/rules.ts
index cd3ffa8..6b1b217 100644
--- a/src/rules.ts
+++ b/src/rules.ts
@@ -23,6 +23,7 @@ export type CommonStyles = {
   tagArrayStyle: TagSpecificArrayFormats | NormalArrayFormats | SpecialArrayFormats;
   minimumNumberOfDollarSignsToBeAMathBlock: number;
   escapeCharacter: string;
+  removeUnnecessaryEscapeCharsForMultiLineArrays: boolean;
 }
 
 export type Options = { [optionName: string]: any};
diff --git a/src/rules/format-yaml-arrays.ts b/src/rules/format-yaml-arrays.ts
index d266091..d469e99 100644
--- a/src/rules/format-yaml-arrays.ts
+++ b/src/rules/format-yaml-arrays.ts
@@ -26,6 +26,10 @@ class FormatYamlArrayOptions implements Options {
   formatArrayKeys?: boolean = true;
   forceSingleLineArrayStyle?: string[] = [];
   forceMultiLineArrayStyle?: string[] = [];
+  @RuleBuilder.noSettingControl()
+    defaultEscapeCharacter?: string = '"';
+  @RuleBuilder.noSettingControl()
+    removeUnnecessaryEscapeCharsForMultiLineArrays?: boolean = false;
 }
 
 @RuleBuilder.register
@@ -51,7 +55,15 @@ export default class RuleTemplate extends RuleBuilder<FormatYamlArrayOptions> {
 
       for (const aliasKey of OBSIDIAN_ALIASES_KEYS) {
         if (options.formatAliasKey && Object.keys(yaml).includes(aliasKey)) {
-          text = setYamlSection(text, aliasKey, formatYamlArrayValue(convertAliasValueToStringOrStringArray(splitValueIfSingleOrMultilineArray(getYamlSectionValue(text, aliasKey))), options.aliasArrayStyle));
+          text = setYamlSection(text,
+              aliasKey,
+              formatYamlArrayValue(
+                  convertAliasValueToStringOrStringArray(splitValueIfSingleOrMultilineArray(getYamlSectionValue(text, aliasKey))),
+                  options.aliasArrayStyle,
+                  options.defaultEscapeCharacter,
+                  options.removeUnnecessaryEscapeCharsForMultiLineArrays,
+              ),
+          );
 
           break;
         }
@@ -59,7 +71,15 @@ export default class RuleTemplate extends RuleBuilder<FormatYamlArrayOptions> {
 
       for (const tagKey of OBSIDIAN_TAG_KEYS) {
         if (options.formatTagKey && Object.keys(yaml).includes(tagKey)) {
-          text = setYamlSection(text, tagKey, formatYamlArrayValue(convertTagValueToStringOrStringArray(splitValueIfSingleOrMultilineArray(getYamlSectionValue(text, tagKey))), options.tagArrayStyle));
+          text = setYamlSection(text,
+              tagKey,
+              formatYamlArrayValue(
+                  convertTagValueToStringOrStringArray(splitValueIfSingleOrMultilineArray(getYamlSectionValue(text, tagKey))),
+                  options.tagArrayStyle,
+                  options.defaultEscapeCharacter,
+                  options.removeUnnecessaryEscapeCharsForMultiLineArrays,
+              ),
+          );
 
           break;
         }
@@ -74,7 +94,15 @@ export default class RuleTemplate extends RuleBuilder<FormatYamlArrayOptions> {
             continue;
           }
 
-          text = setYamlSection(text, key, formatYamlArrayValue(splitValueIfSingleOrMultilineArray(getYamlSectionValue(text, key)), options.defaultArrayStyle));
+          text = setYamlSection(text,
+              key,
+              formatYamlArrayValue(
+                  splitValueIfSingleOrMultilineArray(getYamlSectionValue(text, key)),
+                  options.defaultArrayStyle,
+                  options.defaultEscapeCharacter,
+                  options.removeUnnecessaryEscapeCharsForMultiLineArrays,
+              ),
+          );
         }
       }
 
@@ -83,7 +111,15 @@ export default class RuleTemplate extends RuleBuilder<FormatYamlArrayOptions> {
           continue;
         }
 
-        text = setYamlSection(text, singleLineArrayKey, formatYamlArrayValue(splitValueIfSingleOrMultilineArray(getYamlSectionValue(text, singleLineArrayKey)), NormalArrayFormats.SingleLine));
+        text = setYamlSection(text,
+            singleLineArrayKey,
+            formatYamlArrayValue(
+                splitValueIfSingleOrMultilineArray(getYamlSectionValue(text, singleLineArrayKey)),
+                NormalArrayFormats.SingleLine,
+                options.defaultEscapeCharacter,
+                options.removeUnnecessaryEscapeCharsForMultiLineArrays,
+            ),
+        );
       }
 
       for (const multiLineArrayKey of options.forceMultiLineArrayStyle) {
@@ -91,7 +127,15 @@ export default class RuleTemplate extends RuleBuilder<FormatYamlArrayOptions> {
           continue;
         }
 
-        text = setYamlSection(text, multiLineArrayKey, formatYamlArrayValue(splitValueIfSingleOrMultilineArray(getYamlSectionValue(text, multiLineArrayKey)), NormalArrayFormats.MultiLine));
+        text = setYamlSection(text,
+            multiLineArrayKey,
+            formatYamlArrayValue(
+                splitValueIfSingleOrMultilineArray(getYamlSectionValue(text, multiLineArrayKey)),
+                NormalArrayFormats.MultiLine,
+                options.defaultEscapeCharacter,
+                options.removeUnnecessaryEscapeCharsForMultiLineArrays,
+            ),
+        );
       }
 
       return text;
diff --git a/src/rules/move-tags-to-yaml.ts b/src/rules/move-tags-to-yaml.ts
index b4eea4a..a3176f5 100644
--- a/src/rules/move-tags-to-yaml.ts
+++ b/src/rules/move-tags-to-yaml.ts
@@ -25,6 +25,10 @@ class MoveTagsToYamlOptions implements Options {
     tagArrayStyle? : TagSpecificArrayFormats | NormalArrayFormats | SpecialArrayFormats = NormalArrayFormats.SingleLine;
   howToHandleExistingTags?: tagOperations = 'Nothing';
   tagsToIgnore?: string[] = [];
+  @RuleBuilder.noSettingControl()
+    defaultEscapeCharacter?: string = '"';
+  @RuleBuilder.noSettingControl()
+    removeUnnecessaryEscapeCharsForMultiLineArrays?: boolean = false;
 }
 
 @RuleBuilder.register
@@ -85,7 +89,7 @@ export default class MoveTagsToYaml extends RuleBuilder<MoveTagsToYamlOptions> {
           }
         }
 
-        const newYaml = setYamlSection(text, existingTagKey, formatYamlArrayValue(tagValue, options.tagArrayStyle));
+        const newYaml = setYamlSection(text, existingTagKey, formatYamlArrayValue(tagValue, options.tagArrayStyle, options.defaultEscapeCharacter, options.removeUnnecessaryEscapeCharsForMultiLineArrays));
 
         return `---\n${newYaml}---`;
       });
diff --git a/src/rules/yaml-title-alias.ts b/src/rules/yaml-title-alias.ts
index 6a5f83d..a54a904 100644
--- a/src/rules/yaml-title-alias.ts
+++ b/src/rules/yaml-title-alias.ts
@@ -19,6 +19,9 @@ class YamlTitleAliasOptions implements Options {
 
   @RuleBuilder.noSettingControl()
     defaultEscapeCharacter?: string = '"';
+
+  @RuleBuilder.noSettingControl()
+    removeUnnecessaryEscapeCharsForMultiLineArrays?: boolean = false;
 }
 
 @RuleBuilder.register
@@ -129,15 +132,15 @@ export default class YamlTitleAlias extends RuleBuilder<YamlTitleAliasOptions> {
         newYaml = removeYamlSection(newYaml, aliasKeyForFile);
       } else if (options.preserveExistingAliasesSectionStyle) {
         if (!isEmpty && ((isSingleString && title == newAliasValue) || !isSingleString || currentAliasValue == newAliasValue)) {
-          newYaml = setYamlSection(newYaml, aliasKeyForFile, formatYamlArrayValue(newAliasValue, currentAliasStyle));
+          newYaml = setYamlSection(newYaml, aliasKeyForFile, formatYamlArrayValue(newAliasValue, currentAliasStyle, options.defaultEscapeCharacter, options.removeUnnecessaryEscapeCharsForMultiLineArrays));
         } else {
-          newYaml = setYamlSection(newYaml, aliasKeyForFile, formatYamlArrayValue(newAliasValue, options.aliasArrayStyle));
+          newYaml = setYamlSection(newYaml, aliasKeyForFile, formatYamlArrayValue(newAliasValue, options.aliasArrayStyle, options.defaultEscapeCharacter, options.removeUnnecessaryEscapeCharsForMultiLineArrays));
         }
       } else {
-        newYaml = setYamlSection(newYaml, aliasKeyForFile, formatYamlArrayValue(newAliasValue, options.aliasArrayStyle));
+        newYaml = setYamlSection(newYaml, aliasKeyForFile, formatYamlArrayValue(newAliasValue, options.aliasArrayStyle, options.defaultEscapeCharacter, options.removeUnnecessaryEscapeCharsForMultiLineArrays));
       }
     } else if (!shouldRemoveTitleAlias) {
-      newYaml = setYamlSection(newYaml, OBSIDIAN_ALIAS_KEY_PLURAL, formatYamlArrayValue(title, options.aliasArrayStyle));
+      newYaml = setYamlSection(newYaml, OBSIDIAN_ALIAS_KEY_PLURAL, formatYamlArrayValue(title, options.aliasArrayStyle, options.defaultEscapeCharacter, options.removeUnnecessaryEscapeCharsForMultiLineArrays));
     }
 
     if (!options.useYamlKeyToKeepTrackOfOldFilenameOrHeading || shouldRemoveTitleAlias) {
diff --git a/src/ui/linter-components/tab-components/general-tab.ts b/src/ui/linter-components/tab-components/general-tab.ts
index 09137ad..074016a 100644
--- a/src/ui/linter-components/tab-components/general-tab.ts
+++ b/src/ui/linter-components/tab-components/general-tab.ts
@@ -165,6 +165,23 @@ export class GeneralTab extends Tab {
 
     this.addSettingSearchInfo(tempDiv, settingName, settingDesc);
 
+    tempDiv = this.contentEl.createDiv();
+    settingName = 'Remove Unnecessary Escape Characters when in Multi-Line Array Format';
+    settingDesc = 'Escape characters for multi-line YAML arrays don\'t need the same escaping as single-line arrays, so when in multi-line format remove extra escapes that are not necessary';
+    new Setting(tempDiv)
+        .setName(settingName)
+        .setDesc(settingDesc)
+        .addToggle((toggle) => {
+          toggle
+              .setValue(this.plugin.settings.commonStyles.removeUnnecessaryEscapeCharsForMultiLineArrays)
+              .onChange(async (value) => {
+                this.plugin.settings.commonStyles.removeUnnecessaryEscapeCharsForMultiLineArrays = value;
+                await this.plugin.saveSettings();
+              });
+        });
+
+    this.addSettingSearchInfo(tempDiv, settingName, settingDesc);
+
     tempDiv = this.contentEl.createDiv();
     settingName = 'Number of Dollar Signs to Indicate Math Block';
     settingDesc = 'The amount of dollar signs to consider the math content to be a math block instead of inline math';
diff --git a/src/utils/yaml.ts b/src/utils/yaml.ts
index 3fd1d33..b4a98bb 100644
--- a/src/utils/yaml.ts
+++ b/src/utils/yaml.ts
@@ -97,9 +97,11 @@ export enum NormalArrayFormats {
  * Formats the yaml array value passed in with the specified format.
  * @param {string | string[]} value The value(s) that will be used as the parts of the array that is assumed to already be broken down into the appropriate format to be put in the array.
  * @param {NormalArrayFormats | SpecialArrayFormats | TagSpecificArrayFormats} format The format that the array should be converted into.
+ * @param {string} defaultEscapeCharacter The character escape to use around the value if a specific escape character is not needed.
+ * @param {boolean} removeEscapeCharactersIfPossibleWhenGoingToMultiLine Whether or not to remove no longer needed escape values when converting to a multi-line format.
  * @return {string} The formatted array in the specified yaml/obsidian yaml format.
  */
-export function formatYamlArrayValue(value: string | string[], format: NormalArrayFormats | SpecialArrayFormats | TagSpecificArrayFormats): string {
+export function formatYamlArrayValue(value: string | string[], format: NormalArrayFormats | SpecialArrayFormats | TagSpecificArrayFormats, defaultEscapeCharacter: string, removeEscapeCharactersIfPossibleWhenGoingToMultiLine: boolean): string {
   if (typeof value === 'string') {
     value = [value];
   }
@@ -115,6 +117,16 @@ export function formatYamlArrayValue(value: string | string[], format: NormalArr
       if (value == null || value.length === 0) {
         return '\n  - ';
       }
+
+      if (removeEscapeCharactersIfPossibleWhenGoingToMultiLine) {
+        for (let i = 0; i < value.length; i++) {
+          const currentValue = value[i];
+          if (isValueEscapedAlready(currentValue)) {
+            value[i] = escapeStringIfNecessaryAndPossible(currentValue.substring(1, currentValue.length - 1), defaultEscapeCharacter);
+          }
+        }
+      }
+
       return '\n  - ' + value.join('\n  - ');
     case SpecialArrayFormats.SingleStringToSingleLine:
       if (value == null || value.length === 0) {


Test patch:
diff --git a/__tests__/format-yaml-arrays.test.ts b/__tests__/format-yaml-arrays.test.ts
index f1f9c9a..78b6aa6 100644
--- a/__tests__/format-yaml-arrays.test.ts
+++ b/__tests__/format-yaml-arrays.test.ts
@@ -237,6 +237,47 @@ ruleTest({
         tagArrayStyle: NormalArrayFormats.MultiLine,
       },
     },
+    {
+      testName: 'Convert tags from single-line array to multi-line array with no changes removes unnecessary escape values when `removeUnnecessaryEscapeCharsForMultiLineArrays = true`',
+      before: dedent`
+        ---
+        tag: ["tag1", tag2, tag3, tag4]
+        ---
+      `,
+      after: dedent`
+        ---
+        tag:
+          - tag1
+          - tag2
+          - tag3
+          - tag4
+        ---
+      `,
+      options: {
+        tagArrayStyle: NormalArrayFormats.MultiLine,
+        removeUnnecessaryEscapeCharsForMultiLineArrays: true,
+      },
+    },
+    {
+      testName: 'Convert tags from single-line array to multi-line array with no changes doesn\'t remove unnecessary escape values when `removeUnnecessaryEscapeCharsForMultiLineArrays = false`',
+      before: dedent`
+        ---
+        tag: ["tag1", tag2, tag3, tag4]
+        ---
+      `,
+      after: dedent`
+        ---
+        tag:
+          - "tag1"
+          - tag2
+          - tag3
+          - tag4
+        ---
+      `,
+      options: {
+        tagArrayStyle: NormalArrayFormats.MultiLine,
+      },
+    },
 
     // aliases
     {
@@ -387,6 +428,47 @@ ruleTest({
         formatAliasKey: false,
       },
     },
+    {
+      testName: 'Convert aliases from single-line array to multi-line array with no changes removes unnecessary escape values when `removeUnnecessaryEscapeCharsForMultiLineArrays = true`',
+      before: dedent`
+        ---
+        aliases: ["alias1", alias2, alias3, alias4]
+        ---
+      `,
+      after: dedent`
+        ---
+        aliases:
+          - alias1
+          - alias2
+          - alias3
+          - alias4
+        ---
+      `,
+      options: {
+        aliasArrayStyle: NormalArrayFormats.MultiLine,
+        removeUnnecessaryEscapeCharsForMultiLineArrays: true,
+      },
+    },
+    {
+      testName: 'Convert aliases from single-line array to multi-line array with no changes doesn\'t remove unnecessary escape values when `removeUnnecessaryEscapeCharsForMultiLineArrays = false`',
+      before: dedent`
+        ---
+        aliases: ["alias1", alias2, alias3, alias4]
+        ---
+      `,
+      after: dedent`
+        ---
+        aliases:
+          - "alias1"
+          - alias2
+          - alias3
+          - alias4
+        ---
+      `,
+      options: {
+        aliasArrayStyle: NormalArrayFormats.MultiLine,
+      },
+    },
 
     // default array style
     {
@@ -498,6 +580,43 @@ ruleTest({
         defaultArrayStyle: NormalArrayFormats.MultiLine,
       },
     },
+    {
+      testName: 'Convert single-line to multi-line for regular yaml arrays doesn\'t remove unnecessary escape values when `removeUnnecessaryEscapeCharsForMultiLineArrays = false`',
+      before: dedent`
+        ---
+        key: [val1, "other val"]
+        ---
+      `,
+      after: dedent`
+        ---
+        key:
+          - val1
+          - "other val"
+        ---
+      `,
+      options: {
+        defaultArrayStyle: NormalArrayFormats.MultiLine,
+      },
+    },
+    {
+      testName: 'Convert single-line to multi-line for regular yaml arrays removes unnecessary escape values when `removeUnnecessaryEscapeCharsForMultiLineArrays = true`',
+      before: dedent`
+        ---
+        key: [val1, "other val"]
+        ---
+      `,
+      after: dedent`
+        ---
+        key:
+          - val1
+          - other val
+        ---
+      `,
+      options: {
+        defaultArrayStyle: NormalArrayFormats.MultiLine,
+        removeUnnecessaryEscapeCharsForMultiLineArrays: true,
+      },
+    },
 
     // force single-line
     {
@@ -630,6 +749,45 @@ ruleTest({
         forceMultiLineArrayStyle: ['key'],
       },
     },
+    {
+      testName: 'Forcing multi-line on a single-line array results in a multi-line array with existing unnecessary escape values when `removeUnnecessaryEscapeCharsForMultiLineArrays = false`',
+      before: dedent`
+        ---
+        key: [val1, "other val"]
+        ---
+      `,
+      after: dedent`
+        ---
+        key:
+          - val1
+          - "other val"
+        ---
+      `,
+      options: {
+        defaultArrayStyle: NormalArrayFormats.SingleLine,
+        forceMultiLineArrayStyle: ['key'],
+      },
+    },
+    {
+      testName: 'Forcing multi-line on a single-line array results in a multi-line array without existing unnecessary escape values when `removeUnnecessaryEscapeCharsForMultiLineArrays = true`',
+      before: dedent`
+        ---
+        key: [val1, "other val"]
+        ---
+      `,
+      after: dedent`
+        ---
+        key:
+          - val1
+          - other val
+        ---
+      `,
+      options: {
+        defaultArrayStyle: NormalArrayFormats.SingleLine,
+        forceMultiLineArrayStyle: ['key'],
+        removeUnnecessaryEscapeCharsForMultiLineArrays: true,
+      },
+    },
 
     // edge cases
     {