Repository navigation
source map: expose code block structure info (fenced vs indented, openingFence, infoInsertPoint) #141
Description
背景
@lint-md/core 的 no-empty-code-lang 需要回答一个问题:一个 code 节点是不是 fenced code。
mdast 的 code 节点不带这个信息:
export interface Code extends Literal {
type: 'code'
lang?: string | null | undefined
meta?: string | null | undefined
}于是 core 只能回到原始 Markdown 自己推断。lint-md/lint-md#310 里的实现:
const isFencedCodeStart = (text: string, offset: number): boolean => {
const character = text[offset];
return character === '`' || character === '~';
};它依赖的事实是:parser 把 fenced code 的 position.start 定位到围栏字符,把 indented code 的 position.start 定位到行首缩进。
这段代码能工作,但它把一个 parser 已经知道的事实复制到了 consumer 一侧。
parser 已经知道这个事实
src/source-map/recording-extension.ts 里两条路径是完全分开的:
onentercodefenced / onexitcodefenced -> FencedCodeRecording
onentercodeindented / onexitcodeindented -> CodeValueRecording
两条路径都构造同一个节点:
this.enter({ type: 'code', lang: null, meta: null, value: '' }, token);结构信息在节点上被抹平,之后只有 sidecar 的 recording 还留着它。
而且 FencedCodeRecording 已经记录了 core 需要的大部分东西:
interface FencedCodeRecording extends CodeValueRecording {
emptyOffset: number
sawOpeningLineEnding: boolean
sawContentAfterOpening: boolean
sawClosingFence: boolean
openingIndent: number
closingFenceStart?: number
}emptyOffset 在 opening 行结尾处算出,已经很接近「没有 lang 时应该插在哪里」。它现在写进 state.emptyCodeOffsets(WeakMap<object, number>),但只在 src/source-map/build-source-map.ts 内部用于空范围的映射查询,MarkdownSourceMap 没有暴露。而且只有 onexitcodefenced 会写它,indented 路径不写。
公开 API 目前有 getValueSourceIndex / getRaw / getSourceRange / getFieldSourceRange,全部是「已有的 value 或 field 对应源码哪里」。没有「这个节点的源码结构是什么」这一类查询。
建议方向
不要往 mdast AST 上加字段(例如 node.fenced = true)。
parser 现在保持 parseMd() 和 parseMdWithSourceMap().ast 的 AST parity,MarkdownCodeNode 又直接来自 mdast 的 Code。往 AST 上塞 parser 私有字段,会让标准 mdast AST 承担 lint-md 的私有语义。放在 sidecar MarkdownSourceMap 上更符合现有架构。
建议一次做成一个小的 discriminated union,而不是一个布尔标志:
interface FencedCodeSourceInfo {
kind: 'fenced'
openingFence: ParsedPosition
infoInsertPoint: ParsedPoint
}
interface IndentedCodeSourceInfo {
kind: 'indented'
}
type CodeSourceInfo = FencedCodeSourceInfo | IndentedCodeSourceInfo
interface MarkdownSourceMap {
getCodeSourceInfo(node: MarkdownCodeNode): CodeSourceInfo
}core 侧就能变成:
const info = sourceMap.getCodeSourceInfo(node);
if (node.lang || info.kind !== 'fenced') {
return;
}
fixer.insertTextAt(info.infoInsertPoint.offset, 'plain');比 isFencedCodeStart(text, start) 加 findOpeningLineEnd(text, start) 高一层语义,并且能删掉 core 侧这两个 helper。
为什么不是只加 fenced: boolean
加了布尔值之后,下一条规则会立刻需要更多信息:
- opening fence 在哪里
- lang 在源码哪里
- meta 在源码哪里
- 没有 lang 时应该插在哪里
于是还要继续加 API。一次给出一个小的结构信息 union,比逐个补布尔标志更省事。
lang / meta mapping 属于第二阶段
getFieldSourceRange(node, 'lang' | 'meta', ...) 也值得做,但语义不同:
getFieldSourceRange回答:已经存在的 lang / meta 对应源码哪里- 本 issue 要回答:lang 不存在时应该插在哪里
所以只加 field range 不够。infoInsertPoint(或等价的 opening fence / opening line metadata)是这个 issue 的核心价值。
范围控制
第一阶段只做 getCodeSourceInfo,包含 kind、openingFence、infoInsertPoint。
不要一次公开 opening fence、closing fence、lang、meta、marker character、marker length 全部内容,那样 API 很容易过度设计。等真的有规则需要精确修改已有的 lang 或 meta,再做第二阶段。
相关
- refactor(no-long-code): map code lines through the parser source map, with correctness fixes lint-md#309 — core 不再自己理解 code content 如何映射
- fix(no-empty-code-lang): only autofix fenced code, at the end of the opening line lint-md#310 — 本 issue 的直接动机
- refactor: record inline code mappings during parsing #135 / refactor: record fenced code mappings during parsing #136 / refactor: record indented code mappings during parsing #137 — 分别把 inlineCode、fenced code、indented code 的映射搬到 parse 阶段
- source map: map code.value for fenced and indented blocks #67 —
code.value的 fenced / indented 映射(已完成,映射的是 value;本 issue 要的是源码结构,两者不重叠)
同一条原则:一个事实只有 parser 能可靠知道,就不要让 downstream 从原始 Markdown 再推断一次。
Activity
feat: expose code block structure info from the source map (#142)