feat/wp-extract-contract/main #33
+102
-8
@@ -102,17 +102,111 @@ export function listSection(sections, name) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 取出兩欄表格型段落。以分隔列(|---|---|)為界,之後才是資料列;
|
||||
* 沒有分隔列就當成沒有資料,避免把表頭當成一筆名詞。
|
||||
* 取出巢狀的待辦清單:上層是待辦,縮排一層是該項自己的驗收。
|
||||
*
|
||||
* 只處理兩欄:多出來的欄會被丟掉。目前唯一的使用者是需求議題的領域名詞表。
|
||||
* 工作包的介面契約是四欄(介面/產出者/消費者/形狀),wp-extract 需要另一個
|
||||
* 保留全部欄位的版本,不能直接沿用這一支。
|
||||
* 只有這一支收 body 而不收切好的段落,因為它要交出 `raw`——下游靠 `raw` 在整份 body 上
|
||||
* 做精確字串替換來勾選 checkbox,那一行必須逐字等於 body 裡的原樣,
|
||||
* 連縮排、行尾空白與 \r 都不能動。段落切分會修掉前後空白,拿不到這種保證。
|
||||
*
|
||||
* 兩種畸形寫法都不丟內容,寧可放在稍微不對的位置也不要靜靜消失:
|
||||
* - 巢狀超過一層 → 攤進所在待辦的驗收
|
||||
* - 還沒有上層待辦就先出現縮排項目 → 升格成待辦
|
||||
*
|
||||
* @param {string} body 議題 body
|
||||
* @param {string} section 段落名稱,例如 '待辦'
|
||||
* @returns {{text: string, done: boolean, raw: string, 驗收: {text: string, done: boolean, raw: string}[]}[]}
|
||||
*/
|
||||
export function checklistInSection(body, section) {
|
||||
const rows = [...eachLine(body)];
|
||||
const { start, end } = sectionBounds(rows, section);
|
||||
if (start === -1) return [];
|
||||
|
||||
const todos = [];
|
||||
let topIndent = null;
|
||||
|
||||
for (let i = start + 1; i < end; i += 1) {
|
||||
if (rows[i].inFence) continue;
|
||||
const item = parseChecklistItem(rows[i].line);
|
||||
if (!item) continue;
|
||||
|
||||
const nested = topIndent !== null && todos.length > 0 && item.indent > topIndent;
|
||||
if (nested) {
|
||||
todos.at(-1).驗收.push(item.value);
|
||||
continue;
|
||||
}
|
||||
// 比目前認定的上層還淺時,把上層改認成更淺的那一層:第一項剛好縮排時,
|
||||
// 後面出現的真正上層才不會被當成它的驗收。
|
||||
topIndent = topIndent === null ? item.indent : Math.min(topIndent, item.indent);
|
||||
todos.push({ ...item.value, 驗收: [] });
|
||||
}
|
||||
return todos;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拆一行清單項。符號清單與編號清單一視同仁,checkbox 可有可無——
|
||||
* 忘了寫 checkbox 的項目仍是一項待辦,只是 done 為 false。
|
||||
* @returns {{indent: number, value: {text: string, done: boolean, raw: string}}|null}
|
||||
*/
|
||||
function parseChecklistItem(line) {
|
||||
// [\s\S] 而非 . 的理由同 listSection:CRLF 的 body 行尾有 \r,. 不吃它。
|
||||
// text 靠 trim 修掉 \r,raw 則原樣留著——它要逐字等於 body 裡的那一行。
|
||||
const item = line.match(/^(\s*)(?:[-*+]|\d+\.)\s+([\s\S]*)$/);
|
||||
if (!item) return null;
|
||||
|
||||
const box = item[2].match(/^\[([ xX])\]\s*([\s\S]*)$/);
|
||||
const text = (box ? box[2] : item[2]).trim();
|
||||
if (text === '') return null;
|
||||
|
||||
return {
|
||||
indent: item[1].length,
|
||||
value: { text, done: box ? box[1].toLowerCase() === 'x' : false, raw: line },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 在段落裡找出「標籤:#編號」那一行的編號,例如關聯段落的 `需求議題:#7`。
|
||||
* 全形與半形冒號都認;找不到回 null——沒填不是解析失敗。
|
||||
* @param {Map<string, string>} sections
|
||||
* @param {string} name 段落名稱
|
||||
* @param {string} label 標籤,例如 '需求議題'
|
||||
* @returns {number|null}
|
||||
*/
|
||||
export function referencedIndex(sections, name, label) {
|
||||
for (const { line, inFence } of eachLine(sections.get(name))) {
|
||||
if (inFence) continue;
|
||||
const at = line.indexOf(label);
|
||||
if (at === -1) continue;
|
||||
|
||||
const value = line.slice(at + label.length).match(/^\s*[::]\s*#?(\d+)/);
|
||||
if (value) return Number(value[1]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取出兩欄表格型段落,欄位固定命名為 term 與 def。
|
||||
* 需求議題的領域名詞表用它;四欄的介面契約請用 tableRows。
|
||||
* @param {Map<string, string>} sections
|
||||
* @param {string} name
|
||||
* @returns {{term: string, def: string}[]}
|
||||
*/
|
||||
export function tableSection(sections, name) {
|
||||
return tableRows(sections, name, ['term', 'def']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取出表格型段落的資料列,欄位依 columns 命名。以分隔列(|---|---|)為界,
|
||||
* 之後才是資料列;沒有分隔列就當成沒有資料,避免把表頭當成一筆資料。
|
||||
*
|
||||
* 資料列比 columns 短時補空字串而不是讓欄位消失——下游拿到的形狀要固定,
|
||||
* 少一欄是內容的問題,不該變成「欄位不存在」讓下游多寫一種分支。
|
||||
*
|
||||
* @param {Map<string, string>} sections
|
||||
* @param {string} name
|
||||
* @param {string[]} columns 由左到右的欄位名稱;多出來的欄會被丟掉
|
||||
* @returns {Record<string, string>[]}
|
||||
*/
|
||||
export function tableRows(sections, name, columns) {
|
||||
const rows = [];
|
||||
|
||||
for (const { line, inFence } of eachLine(sections.get(name))) {
|
||||
@@ -125,10 +219,10 @@ export function tableSection(sections, name) {
|
||||
|
||||
return rows
|
||||
.slice(separator + 1)
|
||||
// 同一段落裡若不慎貼了第二張表,它的分隔列不該變成一筆 {term:'---'}
|
||||
// 同一段落裡若不慎貼了第二張表,它的分隔列不該變成一筆資料
|
||||
.filter((cells) => !isSeparator(cells))
|
||||
.filter((cells) => cells.length >= 2 && cells.some((cell) => cell !== ''))
|
||||
.map(([term, def]) => ({ term, def }));
|
||||
.filter((cells) => cells.some((cell) => cell !== ''))
|
||||
.map((cells) => Object.fromEntries(columns.map((column, i) => [column, cells[i] ?? ''])));
|
||||
}
|
||||
|
||||
/** 以未被逸脫的直線切欄,再把 `\|` 還原成內容裡的直線 */
|
||||
|
||||
Reference in New Issue
Block a user