|
17 | 17 | * defeat the loader's "respect what's there" stance. |
18 | 18 | */ |
19 | 19 |
|
| 20 | +/** |
| 21 | + * YAML block scalar header: `|`, `>`, with optional chomping (`-`/`+`) and an |
| 22 | + * optional explicit indent digit — e.g. `>-`, `|2`, `>2-`. |
| 23 | + */ |
| 24 | +const BLOCK_SCALAR_RE = /^([|>])([0-9]*)([-+]?)$/; |
| 25 | + |
| 26 | +/** |
| 27 | + * Read an indented text block beneath a block-scalar header and fold it per the |
| 28 | + * YAML rules we actually use. |
| 29 | + * |
| 30 | + * `|` literal — newlines preserved |
| 31 | + * `>` folded — lines joined with a single space |
| 32 | + * `-` strip — no trailing newline (what every lede/summary in this tree uses) |
| 33 | + * |
| 34 | + * Returns null when there is no indented body, so the caller can fall through. |
| 35 | + */ |
| 36 | +function readIndentedBlock( |
| 37 | + lines: string[], |
| 38 | + start: number, |
| 39 | + style: string, |
| 40 | + chomp: string, |
| 41 | +): { text: string | null; consumed: number } { |
| 42 | + const collected: string[] = []; |
| 43 | + let i = start; |
| 44 | + while (i < lines.length) { |
| 45 | + const line = lines[i]; |
| 46 | + if (line.trim() === '') { collected.push(''); i++; continue; } |
| 47 | + if (!/^[ \t]/.test(line)) break; // dedent ends the block |
| 48 | + collected.push(line.replace(/^[ \t]+/, '')); |
| 49 | + i++; |
| 50 | + } |
| 51 | + while (collected.length && collected[collected.length - 1] === '') collected.pop(); |
| 52 | + if (collected.length === 0) return { text: null, consumed: 0 }; |
| 53 | + let text = style === '|' ? collected.join('\n') : collected.join(' ').replace(/\s+/g, ' ').trim(); |
| 54 | + if (chomp === '+') text += '\n'; |
| 55 | + return { text, consumed: i - start }; |
| 56 | +} |
| 57 | + |
20 | 58 | const FENCE_RE = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n?([\s\S]*)$/; |
21 | 59 |
|
22 | 60 | export interface ParsedFrontmatter { |
@@ -47,13 +85,24 @@ function parseYamlSubset(text: string): Record<string, unknown> { |
47 | 85 | const key = line.slice(0, colonIdx).trim(); |
48 | 86 | const rest = line.slice(colonIdx + 1).trim(); |
49 | 87 |
|
50 | | - if (rest === '' || rest === '|' || rest === '>') { |
| 88 | + if (rest === '' || BLOCK_SCALAR_RE.test(rest)) { |
51 | 89 | const { items, consumed } = readIndentedArray(lines, i + 1); |
52 | 90 | if (items !== null) { |
53 | 91 | data[key] = items; |
54 | 92 | i += 1 + consumed; |
55 | 93 | continue; |
56 | 94 | } |
| 95 | + // Not a list. If a block-scalar header was given, the indented body is |
| 96 | + // text — fold it rather than dropping the value on the floor. |
| 97 | + const bs = rest.match(BLOCK_SCALAR_RE); |
| 98 | + if (bs) { |
| 99 | + const { text, consumed: used } = readIndentedBlock(lines, i + 1, bs[1], bs[3]); |
| 100 | + if (text !== null) { |
| 101 | + data[key] = text; |
| 102 | + i += 1 + used; |
| 103 | + continue; |
| 104 | + } |
| 105 | + } |
57 | 106 | data[key] = null; |
58 | 107 | i++; |
59 | 108 | continue; |
|
0 commit comments