/** * 写回 YAML 之后的格式修复。 * * 背景:`yaml` 库把行尾注释的文字存在节点上,但不存注释前面那几个对齐空格。 * 于是即便某一段落根本没被改动,重新输出时 `interval: 86400 # 每 24 小时…` * 也会变成 `interval: 86400 # 每 24 小时…`。信息没丢,但 diff 里会多出十几行噪音, * 而「改一处只动一处」正是这个工具赖以取信于人的地方。 * * 做法:逐行比对。如果输出的某一行与原文某一行「注释前的内容」和「注释文字」都相同, * 只是中间的空格数不同,就把原文那一行原样放回去。 */ /** 把一行拆成注释前的内容和注释文字。没有行尾注释则返回 null。 */ function splitInlineComment(line: string): { content: string; comment: string } | null { const hashIndex = line.indexOf('#') // 整行注释(# 前面只有空白)不在处理范围内——那种注释的缩进本来就会被保留。 if (hashIndex <= 0) return null if (line.slice(0, hashIndex).trim() === '') return null // 注释符号前必须有空白,否则 `a#b` 这种不是注释。 if (!/\s$/.test(line.slice(0, hashIndex))) return null return { content: line.slice(0, hashIndex).trimEnd(), comment: line.slice(hashIndex), } } const keyOf = (content: string, comment: string) => `${content}\u0000${comment}` /** * 把 `output` 里因为重新输出而丢失对齐的行,还原成 `source` 里对应的那一行。 * * 只在「注释前内容」和「注释文字」都完全一致时才还原,所以不会改变任何语义; * 匹配不上的行原样保留。同样的内容在原文里出现多次时按出现顺序依次配对。 */ export function restoreInlineCommentAlignment(source: string, output: string): string { const pending = new Map() for (const line of source.split('\n')) { const parts = splitInlineComment(line) if (!parts) continue const key = keyOf(parts.content, parts.comment) const bucket = pending.get(key) if (bucket) bucket.push(line) else pending.set(key, [line]) } if (pending.size === 0) return output return output .split('\n') .map((line) => { const parts = splitInlineComment(line) if (!parts) return line const bucket = pending.get(keyOf(parts.content, parts.comment)) if (!bucket || bucket.length === 0) return line return bucket.shift()! }) .join('\n') }