From e494bd534e43921fa9d188dbd2c22e909ec2e5df Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 06:06:09 +0000 Subject: [PATCH] =?UTF-8?q?fix(=E8=AD=B0=E9=A1=8C=E8=A7=A3=E6=9E=90):=20up?= =?UTF-8?q?sertLineInSection=20=E9=99=90=E5=AE=9A=E5=9C=A8=E7=9B=AE?= =?UTF-8?q?=E6=A8=99=E6=AE=B5=E8=90=BD=E5=85=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三個實際重現過的污染情境: - 圍欄裡的 `## 關聯` 被當成真標題,估算插進圍欄後面。本檔的共同前提是「圍欄裡的 東西不是內容」,這支卻自己用 indexOf 找標題,繞過了那個判斷。 - `## 關聯度說明` 被 indexOf 當成 `## 關聯` 命中,改到別人的段落。 - 「這一行是否已存在」用整份 body 比對,於是別的段落剛好有 `估算人天:` 時被改掉, 真正的關聯段落反而一直拿不到值。 改成沿用同檔的 eachLine 走行、標題要完全相同、既有那一行只在段落範圍內找。 弄錯的代價是靜靜改壞別人的內容,所以三道判斷都收緊。 Co-Authored-By: Claude Opus 5 (1M context) --- scripts/issue-body.js | 52 ++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/scripts/issue-body.js b/scripts/issue-body.js index 972a282..bbff973 100644 --- a/scripts/issue-body.js +++ b/scripts/issue-body.js @@ -146,6 +146,12 @@ function isSeparator(cells) { * 在指定段落裡就地更新(或補上)一行「前綴+值」。 * * 用於人天估算這種「議題上只該有一行、重跑要覆蓋而不是累積」的欄位。 + * + * 三件事都限定在目標段落之內,因為弄錯的代價是靜靜改壞別人的內容: + * - 標題要完全相同才算數,`## 關聯度說明` 不是 `## 關聯`。 + * - 圍欄裡的假標題不算標題,沿用本檔共同的圍欄判斷。 + * - 找既有那一行只在段落範圍內找,別的段落剛好有同前綴時不會被改掉。 + * * 段落不存在時補在 body 結尾——寧可放錯位置,也不要讓值靜靜消失。 * * @param {string} body 議題 body @@ -155,26 +161,42 @@ function isSeparator(cells) { */ export function upsertLineInSection(body, section, line) { const prefix = line.slice(0, line.indexOf(':') + 1); - const existing = new RegExp(`^${escapeRegExp(prefix)}.*$`, 'm'); + const rows = [...eachLine(body)]; + const { start, end } = sectionBounds(rows, section); - if (existing.test(body)) { - const updated = body.replace(existing, line); - return updated; - } - - const heading = `## ${section}`; - const start = body.indexOf(heading); if (start === -1) { return `${body.replace(/\n*$/, '')}\n\n${line}\n`; } - // 段落的結尾是下一個標題之前,或整份 body 的結尾 - const after = body.indexOf('\n## ', start + heading.length); - const end = after === -1 ? body.length : after; - const head = body.slice(0, end).replace(/\n*$/, ''); - return `${head}\n${line}\n${body.slice(end).replace(/^\n+/, '\n')}`; + const text = rows.map((row) => row.line); + for (let i = start + 1; i < end; i += 1) { + if (rows[i].inFence || !text[i].startsWith(prefix)) continue; + if (text[i] === line) return body; + text[i] = line; + return text.join('\n'); + } + + // 插在段落內容的結尾,跳過段落與段落之間的空行 + let insertAt = end; + while (insertAt > start + 1 && text[insertAt - 1].trim() === '') insertAt -= 1; + text.splice(insertAt, 0, line); + return text.join('\n'); } -function escapeRegExp(text) { - return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +/** 找出段落的起訖行號;start 為標題那一行,end 為下一個標題(或結尾) */ +function sectionBounds(rows, section) { + let start = -1; + + for (let i = 0; i < rows.length; i += 1) { + if (rows[i].inFence) continue; + const heading = rows[i].line.match(/^##\s+(.+?)\s*$/); + if (!heading) continue; + + if (start === -1) { + if (heading[1] === section) start = i; + continue; + } + return { start, end: i }; + } + return { start, end: rows.length }; }