Repository: platers/obsidian-linter
Pull request #532 (2022-12-03 01:30:30)

Add More Robust YAML String to Array Parsing
Relates to #525 

Changs Made:
- Instead of Assuming all delimiters were valid, the logic was updated to check for only unescaped delimiters. This means delimiters not surrounded by a single or double quote. _Note that double nesting is not supported._
- Added tests for the new parser and to verify that the YAML array format rule is working well with it



Patch:
diff --git a/src/utils/yaml.ts b/src/utils/yaml.ts
index d4290eb..3fd1d33 100644
--- a/src/utils/yaml.ts
+++ b/src/utils/yaml.ts
@@ -190,7 +190,7 @@ export function splitValueIfSingleOrMultilineArray(value: string): string | stri
       return null;
     }
 
-    const arrayItems = value.split(',').map((val: string) => val.trim());
+    const arrayItems = convertYAMLStringToArray(value, ',');
 
     return arrayItems.filter((el: string) => {
       return el != '';
@@ -230,9 +230,9 @@ export function convertTagValueToStringOrStringArray(value: string | string[]):
   if (Array.isArray(value)) {
     originalTagValues = value;
   } else if (value.includes(',')) {
-    originalTagValues = value.split(',').map((val: string) => val.trim());
+    originalTagValues = convertYAMLStringToArray(value, ',');
   } else {
-    originalTagValues = value.split(' ');
+    originalTagValues = convertYAMLStringToArray(value, ' ');
   }
 
   for (const tagValue of originalTagValues) {
@@ -249,12 +249,54 @@ export function convertTagValueToStringOrStringArray(value: string | string[]):
  */
 export function convertAliasValueToStringOrStringArray(value: string | string[]): string[] {
   if (typeof value === 'string') {
-    return value.split(', ');
+    return convertYAMLStringToArray(value, ',');
   }
 
   return value;
 }
 
+export function convertYAMLStringToArray(value: string, delimiter: string = ','): string[] {
+  if (value == '' || value == null) {
+    return null;
+  }
+
+  if (delimiter.length > 1) {
+    throw new Error('delimiter is only allowed to be a single character');
+  }
+
+  const arrayItems: string[] = [];
+  let currentItem = '';
+  let index = 0;
+  while (index < value.length) {
+    const currentChar = value.charAt(index);
+
+    if (currentChar === delimiter) {
+      // case where you find a delimiter
+      arrayItems.push(currentItem.trim());
+      currentItem = '';
+    } else if (currentChar === '"' || currentChar === '\'') {
+      // if there is an escape character check to see if there is a closing escape character and if so, skip to it as the next part of the value
+      const endOfEscapedValue = value.indexOf(currentChar, index+1);
+      if (endOfEscapedValue != -1) {
+        currentItem += value.substring(index, endOfEscapedValue + 1);
+        index = endOfEscapedValue;
+      } else {
+        currentItem += currentChar;
+      }
+    } else {
+      currentItem += currentChar;
+    }
+
+    index++;
+  }
+
+  if (currentItem.trim() != '') {
+    arrayItems.push(currentItem.trim());
+  }
+
+  return arrayItems;
+}
+
 /**
  * Returns whether or not the YAML string value is already escaped
  * @param {string} value The YAML string to check if it is already escaped


Test patch:
diff --git a/__tests__/format-yaml-arrays.test.ts b/__tests__/format-yaml-arrays.test.ts
index 82daab6..f1f9c9a 100644
--- a/__tests__/format-yaml-arrays.test.ts
+++ b/__tests__/format-yaml-arrays.test.ts
@@ -868,5 +868,24 @@ ruleTest({
         defaultArrayStyle: NormalArrayFormats.MultiLine,
       },
     },
+    {
+      // accounts for https://github.com/platers/obsidian-linter/issues/525
+      testName: 'Converting a single string comma separated array to a multi-line array should respect escaped entries',
+      before: dedent`
+        ---
+        aliases: Scott, "Scott, Jr."
+        ---
+      `,
+      after: dedent`
+        ---
+        aliases:
+          - Scott
+          - "Scott, Jr."
+        ---
+      `,
+      options: {
+        aliasArrayStyle: NormalArrayFormats.MultiLine,
+      },
+    },
   ],
 });
diff --git a/__tests__/yaml-string-to-array.test.ts b/__tests__/yaml-string-to-array.test.ts
new file mode 100644
index 0000000..8bbef82
--- /dev/null
+++ b/__tests__/yaml-string-to-array.test.ts
@@ -0,0 +1,62 @@
+import {convertYAMLStringToArray} from '../src/utils/yaml';
+
+type YAMLStringTestCase = {
+  testName: string,
+  delimiter: string,
+  before: string,
+  after: string[],
+}
+
+const testCases: YAMLStringTestCase[] = [
+  {
+    testName: 'Two entry string with empty last value',
+    delimiter: ',',
+    before: 'value1, ',
+    after: ['value1'],
+  },
+  {
+    testName: 'A delimiter escaped with a double quote should not be counted as a delimiter',
+    delimiter: ',',
+    before: 'value1, "Scott, Jr.", Bob',
+    after: ['value1', '"Scott, Jr."', 'Bob'],
+  },
+  {
+    testName: 'A delimiter escaped with a single quote should not be counted as a delimiter',
+    delimiter: ',',
+    before: 'value1, \'Scott, Jr.\', Bob',
+    after: ['value1', '\'Scott, Jr.\'', 'Bob'],
+  },
+  {
+    testName: 'An empty string should return null',
+    delimiter: ',',
+    before: '',
+    after: null,
+  },
+  {
+    testName: 'A null value should return null',
+    delimiter: ',',
+    before: null,
+    after: null,
+  },
+  {
+    testName: 'A single single quote in the string should not result in an escaped value and it should parse it as normal',
+    delimiter: ',',
+    before: 'Bob, John\'s,Doug',
+    after: ['Bob', 'John\'s', 'Doug'],
+  },
+  {
+    testName: 'A single double quote in the string should not result in an escaped value and it should parse it as normal',
+    delimiter: ',',
+    before: 'Bob, "John,Doug',
+    after: ['Bob', '"John', 'Doug'],
+  },
+];
+
+
+describe('Parse YAML Strings to Arrays', () => {
+  for (const testCase of testCases) {
+    it(testCase.testName, () => {
+      expect(convertYAMLStringToArray(testCase.before, testCase.delimiter)).toStrictEqual(testCase.after);
+    });
+  }
+});