fix(議題解析): upsertLineInSection 限定在目標段落內
三個實際重現過的污染情境: - 圍欄裡的 `## 關聯` 被當成真標題,估算插進圍欄後面。本檔的共同前提是「圍欄裡的 東西不是內容」,這支卻自己用 indexOf 找標題,繞過了那個判斷。 - `## 關聯度說明` 被 indexOf 當成 `## 關聯` 命中,改到別人的段落。 - 「這一行是否已存在」用整份 body 比對,於是別的段落剛好有 `估算人天:` 時被改掉, 真正的關聯段落反而一直拿不到值。 改成沿用同檔的 eachLine 走行、標題要完全相同、既有那一行只在段落範圍內找。 弄錯的代價是靜靜改壞別人的內容,所以三道判斷都收緊。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+37
-15
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user