/** * 議題 body 的 markdown 解析。 * * 純函式,不碰網路也不碰檔案系統:抽取類腳本的解析全部走這裡, * 解析規則只寫一次,需求議題與工作包議題共用同一套。 */ /** * 依 `## 標題` 切出各段落。 * 圍欄區塊(```)內的內容一律不當成標題,否則 mermaid 或程式碼裡的井字號會把段落切碎。 * @param {string} body 議題 body * @returns {Map} 段落名稱 → 該段內容(前後空白已修掉) */ export function parseSections(body) { const sections = new Map(); const buffer = []; let current = null; let inFence = false; const flush = () => { if (current !== null) sections.set(current, buffer.join('\n').trim()); buffer.length = 0; }; for (const line of (body ?? '').split('\n')) { if (/^\s*```/.test(line)) inFence = !inFence; const heading = inFence ? null : line.match(/^##\s+(.+?)\s*$/); if (heading) { flush(); current = heading[1]; continue; } if (current !== null) buffer.push(line); } flush(); return sections; } /** * 取出文字型段落的整段內容。段落不存在時回空字串,不報錯 —— * 缺段落是模板的正常變體,不是解析失敗。 * @param {Map} sections * @param {string} name * @returns {string} */ export function textSection(sections, name) { return sections.get(name) ?? ''; } /** * 取出列表型段落的每一項。符號清單與編號清單一視同仁, * checkbox 只留文字不留標記 —— 需求議題的驗收標準不追蹤勾選狀態。 * @param {Map} sections * @param {string} name * @returns {string[]} */ export function listSection(sections, name) { const content = sections.get(name); if (!content) return []; return content .split('\n') .map((line) => line.match(/^\s*(?:[-*+]|\d+\.)\s+(.*)$/)) .filter((match) => match !== null) .map((match) => match[1].replace(/^\[[ xX]\]\s*/, '').trim()) .filter((text) => text !== ''); } /** * 取出兩欄表格型段落。以分隔列(|---|---|)為界,之後才是資料列; * 沒有分隔列就當成沒有資料,避免把表頭當成一筆名詞。 * @param {Map} sections * @param {string} name * @returns {{term: string, def: string}[]} */ export function tableSection(sections, name) { const content = sections.get(name); if (!content) return []; const rows = content .split('\n') .filter((line) => line.trim().startsWith('|')) .map((line) => line.trim().replace(/^\||\|$/g, '').split('|').map((cell) => cell.trim())); const separator = rows.findIndex((cells) => cells.every((cell) => /^:?-+:?$/.test(cell))); if (separator === -1) return []; return rows .slice(separator + 1) .filter((cells) => cells.length >= 2 && cells.some((cell) => cell !== '')) .map(([term, def]) => ({ term, def })); }