Repository: platers/obsidian-linter (ts)

Issue:
Bug: YAML from share as gist plugin invalid...
## Describe the Bug

The YAML generated by the "share as a gist" plugin (https://github.com/timrogers/obsidian-share-as-gist) is flagged as invalid by Linter. I'm no YAML expert, but it seems ok to me?

```yaml
gists:
  - id: 4d06d592f0b0ae1771c0012de3562d8d
    url: 'https://gist.github.com/4d06d592f0b0ae1771c0012de3562d8d'
    createdAt: '2022-11-01T16:07:48Z'
    updatedAt: '2022-11-01T16:35:11Z'
    filename: Stochastic P1.md
    isPublic: true
```

## How to Reproduce
Install the plugin, create the github key, share an obsidian page as a gist. YAML header is automatically generated to hold the resulting metadata.

When Linting the file with the above YAML, the following error message is put to the console:
```plain
Lint File Error in File ...:
YAMLException: bad indentation of a sequence entry (3:5)

 1 | gists:
 2 |   - "id: 4d06d592f0b0ae1771c0012de35 ...
 3 |     url: 'https://gist.github.com/4d ...
---------^
 4 |     createdAt: '2022-11-01T16:07:48Z'
 5 |     updatedAt: '2022-11-01T16:35:11Z'
    at du (plugin:obsidian-linter:18:944)
    at q (plugin:obsidian-linter:18:978)
    at Ul (plugin:obsidian-linter:25:696)
    at Kn (plugin:obsidian-linter:25:4861)
    at md (plugin:obsidian-linter:25:2150)
    at Kn (plugin:obsidian-linter:25:4870)
    at gd (plugin:obsidian-linter:25:7247)
    at hu (plugin:obsidian-linter:26:258)
    at bd (plugin:obsidian-linter:26:469)
    at ot (plugin:obsidian-linter:48:223)
```

## Expected Behavior
Should not flag as bad YAML?

## Screenshots
<img width="308" alt="image" src="https://user-images.githubusercontent.com/42705243/199289157-3af0059a-7271-4d0b-b709-de1d6b7d9a20.png">

## Device

- [x] Desktop
- [ ] Mobile

## Additional Context
If it is a problem with the gist plugin please share details so I can communicate that back to him.

Pull request description:
Fix YAML Multiline Array of Dictionaries Getting Escaped
Fixes #467 

Changes Made:
- Check for dictionaries or other more complex multiline arrays by checking the whitespace of the next line to determine if the current line is part of a dictionary if it starts with a dash (note that single-line dictionaries are not supported by this). If the whitespace of the next line is the same or greater than that of the beginning of the potential key, then it will be treated as a dictionary.
- Made sure add a couple of tests to make sure that the escaping did not create any weird issues
- Made sure to update format YAML arrays to disregard arrays of objects as that will potentially cause them to be put into invalid YAML format. Added a test for this scenario.

Patch:
diff --git a/src/rules/escape-yaml-special-characters.ts b/src/rules/escape-yaml-special-characters.ts
index 0589aea..96c1569 100644
--- a/src/rules/escape-yaml-special-characters.ts
+++ b/src/rules/escape-yaml-special-characters.ts
@@ -46,6 +46,22 @@ export default class EscapeYamlSpecialCharacters extends RuleBuilder<EscapeYamlS
         let valueStartIndex = 1;
         if (!startsWithDash) {
           valueStartIndex += firstColonIndex;
+        } else if (firstColonIndex !== -1 && i + 1 < yamlLineCount) { // account for arrays of dictionaries
+          const fullLine = yamlLines[i];
+          let expectedIndentationToBeAnArrayOfDictionaries = fullLine.indexOf('-') + 1;
+          while (expectedIndentationToBeAnArrayOfDictionaries < fullLine.length && fullLine.charAt(expectedIndentationToBeAnArrayOfDictionaries) === ' ') {
+            expectedIndentationToBeAnArrayOfDictionaries++;
+          }
+
+          let actualIndentationOfNextLine = 0;
+          const nextLine = yamlLines[i+1];
+          while (actualIndentationOfNextLine < nextLine.length && nextLine.charAt(actualIndentationOfNextLine) === ' ') {
+            actualIndentationOfNextLine++;
+          }
+
+          if (expectedIndentationToBeAnArrayOfDictionaries <= actualIndentationOfNextLine) {
+            valueStartIndex += firstColonIndex;
+          }
         }
 
         const value = line.substring(valueStartIndex).trim();
diff --git a/src/rules/format-yaml-arrays.ts b/src/rules/format-yaml-arrays.ts
index b74ab92..d266091 100644
--- a/src/rules/format-yaml-arrays.ts
+++ b/src/rules/format-yaml-arrays.ts
@@ -69,8 +69,8 @@ export default class RuleTemplate extends RuleBuilder<FormatYamlArrayOptions> {
         const keysToIgnore = [...OBSIDIAN_ALIASES_KEYS, ...OBSIDIAN_TAG_KEYS, ...options.forceMultiLineArrayStyle, ...options.forceSingleLineArrayStyle];
 
         for (const key of Object.keys(yaml)) {
-          // skip non-arrays and already accounted for keys
-          if (keysToIgnore.includes(key) || !Array.isArray(yaml[key])) {
+          // skip non-arrays, arrays of objects, ignored keys, and already accounted for keys
+          if (keysToIgnore.includes(key) || !Array.isArray(yaml[key]) || (yaml[key].length !== 0 && typeof yaml[key][0] === 'object' && yaml[key][0] !== null)) {
             continue;
           }
 
@@ -159,6 +159,31 @@ export default class RuleTemplate extends RuleBuilder<FormatYamlArrayOptions> {
           tagArrayStyle: TagSpecificArrayFormats.SingleStringSpaceDelimited,
         },
       }),
+      new ExampleBuilder({
+        description: 'Arrays with dictionaries in them are ignored',
+        before: dedent`
+          ---
+          gists:
+            - id: test123
+              url: 'some_url'
+              filename: file.md
+              isPublic: true
+          ---
+        `,
+        after: dedent`
+          ---
+          gists:
+            - id: test123
+              url: 'some_url'
+              filename: file.md
+              isPublic: true
+          ---
+        `,
+        options: {
+          formatArrayKeys: true,
+          defaultArrayStyle: NormalArrayFormats.SingleLine,
+        },
+      }),
     ];
   }
   get optionBuilders(): OptionBuilderBase<FormatYamlArrayOptions>[] {


Test patch:
diff --git a/__tests__/escape-yaml-special-characters.test.ts b/__tests__/escape-yaml-special-characters.test.ts
new file mode 100644
index 0000000..bdd3057
--- /dev/null
+++ b/__tests__/escape-yaml-special-characters.test.ts
@@ -0,0 +1,66 @@
+import dedent from 'ts-dedent';
+import {ruleTest} from './common';
+import EscapeYamlSpecialCharactersOptions from '../src/rules/escape-yaml-special-characters';
+
+ruleTest({
+  RuleBuilderClass: EscapeYamlSpecialCharactersOptions,
+  testCases: [
+    { // accounts for https://github.com/platers/obsidian-linter/issues/467
+      testName: 'Make sure that dictionaries in a multiline array do not accidentally get escaped',
+      before: dedent`
+        ---
+        gists:
+          - id: testing123
+            url: 'https://gist.github.com/testing123'
+            createdAt: '2022-11-01T16:07:48Z'
+            updatedAt: '2022-11-01T16:35:11Z'
+            filename: file1.md
+            isPublic: true
+        ---
+      `,
+      after: dedent`
+        ---
+        gists:
+          - id: testing123
+            url: 'https://gist.github.com/testing123'
+            createdAt: '2022-11-01T16:07:48Z'
+            updatedAt: '2022-11-01T16:35:11Z'
+            filename: file1.md
+            isPublic: true
+        ---
+      `,
+    },
+    {
+      testName: 'Make sure that a multiline array with a key value pair in it gets escaped if there are no subsequent key value pairs at the same indentation level',
+      before: dedent`
+        ---
+        gists:
+          - id: testing123
+        url: 'https://gist.github.com/testing123'
+        ---
+      `,
+      after: dedent`
+        ---
+        gists:
+          - "id: testing123"
+        url: 'https://gist.github.com/testing123'
+        ---
+      `,
+    },
+    {
+      testName: 'Make sure that a multiline array with a key value pair in it gets escaped if and does not throw an error if there are no further lines after it',
+      before: dedent`
+        ---
+        gists:
+          - id: testing123
+        ---
+      `,
+      after: dedent`
+        ---
+        gists:
+          - "id: testing123"
+        ---
+      `,
+    },
+  ],
+});