|
1 | 1 | import { describe, expect, it } from 'vitest' |
| 2 | +import fc from 'fast-check' |
2 | 3 | import { parseBasicFrontmatter } from '../../../../shared/utils/parse-basic-frontmatter' |
3 | 4 |
|
4 | 5 | describe('parseBasicFrontmatter', () => { |
@@ -105,4 +106,109 @@ describe('parseBasicFrontmatter', () => { |
105 | 106 | tags: ['a', 'b'], |
106 | 107 | }) |
107 | 108 | }) |
| 109 | + |
| 110 | + it.fails('handles string numerics as strings', () => { |
| 111 | + const input = "---\nid: '42'\n---" |
| 112 | + expect(parseBasicFrontmatter(input)).toEqual({ id: '42' }) |
| 113 | + }) |
| 114 | + |
| 115 | + it.fails('handles numbers using scientific notation', () => { |
| 116 | + const input = '---\nprice: 1e+50\n---' |
| 117 | + expect(parseBasicFrontmatter(input)).toEqual({ price: 1e50 }) |
| 118 | + }) |
| 119 | + |
| 120 | + it.fails('handles escaped double quote', () => { |
| 121 | + const input = '---\nquote: "He said, \\"Hello\\""\n---' |
| 122 | + expect(parseBasicFrontmatter(input)).toEqual({ quote: 'He said, "Hello"' }) |
| 123 | + }) |
| 124 | + |
| 125 | + it('strips leading and trailing whitespace', () => { |
| 126 | + const input = '---\ntext: Something to say \n---' |
| 127 | + expect(parseBasicFrontmatter(input)).toEqual({ text: 'Something to say' }) |
| 128 | + }) |
| 129 | + |
| 130 | + it.fails('handles numeric in arrays', () => { |
| 131 | + const input = '---\ntags: [1, 2]\n---' |
| 132 | + expect(parseBasicFrontmatter(input)).toEqual({ tags: [1, 2] }) |
| 133 | + }) |
| 134 | + |
| 135 | + it('handles strings with array like content', () => { |
| 136 | + const input = '---\ntext: Read the content of this array [1, 2, 3]\n---' |
| 137 | + expect(parseBasicFrontmatter(input)).toEqual({ |
| 138 | + text: 'Read the content of this array [1, 2, 3]', |
| 139 | + }) |
| 140 | + }) |
| 141 | + |
| 142 | + it.fails('handles quoted strings with array as content', () => { |
| 143 | + const input = '---\nvalue: "[123]"\n---' |
| 144 | + expect(parseBasicFrontmatter(input)).toEqual({ value: '[123]' }) |
| 145 | + }) |
| 146 | + |
| 147 | + it.fails('handles string with commas when quoted in arrays', () => { |
| 148 | + const input = '---\ntags: ["foo, bar", "baz"]\n---' |
| 149 | + expect(parseBasicFrontmatter(input)).toEqual({ tags: ['foo, bar', 'baz'] }) |
| 150 | + }) |
| 151 | + |
| 152 | + it('should parse back every key-value pair from generated frontmatter', () => { |
| 153 | + const keyArb = fc.stringMatching(/^[a-zA-Z][a-zA-Z0-9_]*$/) |
| 154 | + const singleValueArbs = (inArray: boolean) => |
| 155 | + // for arrays: all values get parsed as strings and there should not be any comma in the value even for quoted strings |
| 156 | + [ |
| 157 | + // For boolean values |
| 158 | + fc.boolean().map(b => ({ raw: `${b}`, expected: inArray ? `${b}` : b })), |
| 159 | + // For number values |
| 160 | + fc |
| 161 | + .oneof( |
| 162 | + // no support for -0 |
| 163 | + fc.constant(0), |
| 164 | + // no support for scientific notation |
| 165 | + // anything <0.000001 will be displayed in scientific notation, and anything >999999999999999900000 too |
| 166 | + fc.double({ min: 0.000001, max: 999999999999999900000, noNaN: true }), |
| 167 | + fc.double({ min: -999999999999999900000, max: -0.000001, noNaN: true }), |
| 168 | + ) |
| 169 | + .map(n => ({ raw: `${n}`, expected: inArray ? `${n}` : n })), |
| 170 | + // For string values |
| 171 | + fc.oneof( |
| 172 | + fc |
| 173 | + .stringMatching(inArray ? /^[^',]*$/ : /^[^']*$/) // single-quoted string |
| 174 | + .filter(v => Number.isNaN(Number(v))) // numbers, even quoted ones, get parsed as numbers |
| 175 | + .map(v => ({ raw: `'${v}'`, expected: v })), |
| 176 | + fc |
| 177 | + .stringMatching(inArray ? /^[^",]*$/ : /^[^"]*$/) // double-quoted string |
| 178 | + .filter(v => Number.isNaN(Number(v))) |
| 179 | + .map(v => ({ raw: `"${v}"`, expected: v })), |
| 180 | + fc // need to forbid [, ', " or space as first character |
| 181 | + .stringMatching(inArray ? /^[^,]+$/ : /^.+$/) // leading and trailing whitespace are ignored for unquoted strings |
| 182 | + .filter(v => Number.isNaN(Number(v))) |
| 183 | + .map(v => v.trim()) |
| 184 | + .map(v => ({ raw: `'${v}'`, expected: v })), |
| 185 | + ), |
| 186 | + ] |
| 187 | + const valueArb = fc.oneof( |
| 188 | + ...singleValueArbs(false), |
| 189 | + fc |
| 190 | + .array(fc.oneof(...singleValueArbs(true)), { minLength: 1 }) // all values get read as strings, no support to empty arrays |
| 191 | + .map(arr => ({ |
| 192 | + raw: `[${arr.map(v => v.raw).join(', ')}]`, |
| 193 | + expected: arr.map(v => v.expected), |
| 194 | + })), |
| 195 | + ) |
| 196 | + const frontmatterContentArb = fc.dictionary(keyArb, valueArb).map(dict => { |
| 197 | + const entries = Object.entries(dict).map(([key, { raw, expected }]) => ({ |
| 198 | + key, |
| 199 | + raw: `${key}: ${raw}`, |
| 200 | + expected, |
| 201 | + })) |
| 202 | + return { |
| 203 | + raw: `---\n${entries.map(e => e.raw).join('\n')}\n---\n`, |
| 204 | + expected: Object.fromEntries(entries.map(e => [e.key, e.expected])), |
| 205 | + } |
| 206 | + }) |
| 207 | + fc.assert( |
| 208 | + fc.property(frontmatterContentArb, ({ raw, expected }) => { |
| 209 | + expect(parseBasicFrontmatter(raw)).toEqual(expected) |
| 210 | + }), |
| 211 | + { numRuns: 10000, endOnFailure: true }, |
| 212 | + ) |
| 213 | + }) |
108 | 214 | }) |
0 commit comments