diff --git a/scripts/issue-body.js b/scripts/issue-body.js index 0777eb7..280e745 100644 --- a/scripts/issue-body.js +++ b/scripts/issue-body.js @@ -338,6 +338,65 @@ export function tickLine(body, raw, section) { return { status: 'ticked', body: lines.join('\n'), line: ticked, count: 1 }; } +/** + * 換掉一個段落的內容,標題與其餘段落一字不動。 + * + * 整併留言裡的決策時用它。不整份重寫的理由跟 upsertLineInSection 一樣,只是代價更大: + * 重寫會把別人在其他段落的編輯一起蓋掉,而議題的編輯紀錄沒有人會去比對。 + * + * 同名標題出現不只一次時交回 `ambiguous`,不賭第一個——理由與 tickLine 相同, + * 而這裡蓋掉的是一整段而不是一行,猜錯的代價更高。 + * + * 段落不存在時交回 `not-found` 讓呼叫端報錯,不補在結尾:「找不到那一段」多半是段落名 + * 打錯,這時把內容塞到議題末尾,比什麼都不做更難收拾。 + * + * @param {string} body 議題 body + * @param {string} section 段落名稱,例如 '目標' + * @param {string} content 新的段落內容(不含 `## 標題` 那一行) + * @returns {{status: 'replaced'|'not-found'|'ambiguous', body?: string, count: number}} + */ +export function replaceSection(body, section, content) { + const rows = [...eachLine(body)]; + const headings = []; + for (let i = 0; i < rows.length; i += 1) { + if (rows[i].inFence) continue; + if (rows[i].line.match(/^##\s+(.+?)\s*$/)?.[1] === section) headings.push(i); + } + + if (headings.length === 0) return { status: 'not-found', count: 0 }; + if (headings.length > 1) return { status: 'ambiguous', count: headings.length }; + + const [start] = headings; + const lines = rows.map((row) => row.line); + // 下一個段落的標題;沒有就是到結尾 + let end = lines.length; + for (let i = start + 1; i < lines.length; i += 1) { + if (!rows[i].inFence && /^##\s+/.test(lines[i])) { + end = i; + break; + } + } + + // 段落與段落之間的空行屬於版面,不屬於內容:換內容時把它留著。 + // 原本就沒有空行(兩個標題緊貼)時補一個,免得新內容黏在下一個標題上。 + let tail = end; + while (tail > start + 1 && lines[tail - 1].trim() === '') tail -= 1; + const spacer = end === lines.length || end > tail ? lines.slice(tail, end) : ['']; + + // 換行沿用 body 原本的那一種:CRLF 的 body 裡混進 LF,會讓抽取契約交出的 raw + // 對不上原文,之後就勾不動那幾行了 + const eol = body.includes('\r\n') ? '\r\n' : '\n'; + const normalized = content.trim().split(/\r?\n/); + + return { + status: 'replaced', + body: [...lines.slice(0, start + 1), '', ...normalized, ...spacer, ...lines.slice(end)] + .map((line) => line.replace(/\r$/, '')) + .join(eol), + count: 1, + }; +} + /** * 在指定段落裡就地更新(或補上)一行「前綴+值」。 * diff --git a/scripts/issue-extract.js b/scripts/issue-extract.js index 8306eac..89e5bc5 100644 --- a/scripts/issue-extract.js +++ b/scripts/issue-extract.js @@ -47,7 +47,7 @@ main(async () => { } const login = resolveLogin({ host: flags.host }); - await preflight(login, repo); + const { user } = await preflight(login, repo); const issue = await fetchIssue(login, repo, index); const sections = parseSections(issue.body); @@ -66,7 +66,7 @@ main(async () => { 驗收標準: listSection(sections, '驗收標準'), 影響範圍: listSection(sections, '影響範圍'), 未決事項: listSection(sections, '未決事項'), - 未處理留言數: await countUnmergedComments(login, repo, index), + 未處理留言數: await countUnmergedComments(login, repo, index, user.login), }; }); diff --git a/scripts/wp-extract.js b/scripts/wp-extract.js index eb31ee1..d96a0fc 100644 --- a/scripts/wp-extract.js +++ b/scripts/wp-extract.js @@ -64,7 +64,7 @@ main(async () => { } const login = resolveLogin({ host: flags.host }); - await preflight(login, repo); + const { user } = await preflight(login, repo); const issue = await fetchIssue(login, repo, index); const sections = parseSections(issue.body); @@ -88,7 +88,7 @@ main(async () => { 相依: { blocks, depends }, assignee: issue.assignee?.login ?? null, 碼錶中: await hasRunningStopwatch(login, repo, index), - 未處理留言數: await countUnmergedComments(login, repo, index), + 未處理留言數: await countUnmergedComments(login, repo, index, user.login), }; });