(markdown)
| 17 | } |
| 18 | |
| 19 | export function parseFrontmatter(markdown) { |
| 20 | const match = markdown.match(/^---\n([\s\S]*?)\n---(?:\n|$)/); |
| 21 | if (!match) { |
| 22 | throw new Error('Spec must start with Markdown frontmatter'); |
| 23 | } |
| 24 | |
| 25 | const data = {}; |
| 26 | let currentKey = null; |
| 27 | let nestedKey = null; |
| 28 | |
| 29 | for (const rawLine of match[1].split('\n')) { |
| 30 | if (!rawLine.trim() || rawLine.trimStart().startsWith('#')) continue; |
| 31 | const indent = rawLine.match(/^ */)?.[0].length ?? 0; |
| 32 | const line = rawLine.trim(); |
| 33 | |
| 34 | if (indent === 0) { |
| 35 | const keyMatch = line.match(/^([A-Za-z0-9_-]+):(.*)$/); |
| 36 | if (!keyMatch) continue; |
| 37 | const [, key, rawValue] = keyMatch; |
| 38 | currentKey = key; |
| 39 | nestedKey = null; |
| 40 | data[key] = rawValue.trim() ? parseScalar(rawValue) : []; |
| 41 | continue; |
| 42 | } |
| 43 | |
| 44 | if (indent === 2 && line.startsWith('- ')) { |
| 45 | if (!currentKey) continue; |
| 46 | if (!Array.isArray(data[currentKey])) data[currentKey] = []; |
| 47 | data[currentKey].push(parseScalar(line.slice(2))); |
| 48 | continue; |
| 49 | } |
| 50 | |
| 51 | if (indent === 2) { |
| 52 | const keyMatch = line.match(/^([A-Za-z0-9_-]+):(.*)$/); |
| 53 | if (!keyMatch || !currentKey) continue; |
| 54 | const [, key, rawValue] = keyMatch; |
| 55 | if (!data[currentKey] || Array.isArray(data[currentKey])) data[currentKey] = {}; |
| 56 | nestedKey = key; |
| 57 | data[currentKey][key] = rawValue.trim() ? parseScalar(rawValue) : []; |
| 58 | continue; |
| 59 | } |
| 60 | |
| 61 | if (indent === 4 && line.startsWith('- ') && currentKey && nestedKey) { |
| 62 | if (!data[currentKey] || Array.isArray(data[currentKey])) data[currentKey] = {}; |
| 63 | if (!Array.isArray(data[currentKey][nestedKey])) data[currentKey][nestedKey] = []; |
| 64 | data[currentKey][nestedKey].push(parseScalar(line.slice(2))); |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | return { |
| 69 | data, |
| 70 | body: markdown.slice(match[0].length).trim(), |
| 71 | }; |
| 72 | } |
| 73 | |
| 74 | export async function loadSpec(specPath) { |
| 75 | const fullPath = path.resolve(ROOT, specPath); |
no test coverage detected