(text: string)
| 40 | |
| 41 | /** Split a message into alternating prose and fenced-code segments. */ |
| 42 | export function parseSegments(text: string): Segment[] { |
| 43 | const segments: Segment[] = []; |
| 44 | const fence = /```([^\n`]*)\n?([\s\S]*?)```/g; |
| 45 | let last = 0; |
| 46 | let m: RegExpExecArray | null; |
| 47 | while ((m = fence.exec(text)) !== null) { |
| 48 | if (m.index > last) { |
| 49 | const prose = text.slice(last, m.index); |
| 50 | if (prose.trim()) segments.push({ kind: 'text', body: normalizeProse(prose) }); |
| 51 | } |
| 52 | segments.push({ kind: 'code', lang: (m[1] ?? '').trim(), body: (m[2] ?? '').replace(/\n$/, '') }); |
| 53 | last = fence.lastIndex; |
| 54 | } |
| 55 | if (last < text.length) { |
| 56 | const tail = text.slice(last); |
| 57 | // A trailing, still-open fence (mid-stream): render what follows it as a code |
| 58 | // card flagged incomplete, so streaming code is boxed as it arrives rather |
| 59 | // than dumped raw until the closing ``` finally lands. |
| 60 | const open = tail.match(/```([^\n`]*)\n?/); |
| 61 | if (open && open.index !== undefined) { |
| 62 | const before = tail.slice(0, open.index); |
| 63 | if (before.trim()) segments.push({ kind: 'text', body: normalizeProse(before) }); |
| 64 | const codeBody = tail.slice(open.index + open[0].length); |
| 65 | segments.push({ kind: 'code', lang: (open[1] ?? '').trim(), body: codeBody.replace(/\n$/, ''), incomplete: true }); |
| 66 | } else if (tail.trim()) { |
| 67 | segments.push({ kind: 'text', body: normalizeProse(tail) }); |
| 68 | } |
| 69 | } |
| 70 | return segments.length ? segments : [{ kind: 'text', body: normalizeProse(text) }]; |
| 71 | } |
| 72 | |
| 73 | // --------------------------------------------------------------------------- |
| 74 | // Inline emphasis: render **bold** and `code` instead of stripping the markers. |
no test coverage detected