diff --git a/README.md b/README.md index 51fdafa..01472a5 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,24 @@ JSX attribute nodes have no source position in the MDX AST. Use the JSX element position to read its full source with `sourceMap.getRaw(element)`. Apply fixes only to mapped Markdown fields. Keep ESM, JSX attributes, and expressions unchanged. +### GFM autolink behavior + +The parser uses `remark-gfm` to tokenize GFM autolinks. +It disables the autolink-literal transform after parsing. +That transform can create nodes without source positions ([remark-gfm issue #16](https://github.com/remarkjs/remark-gfm/issues/16)). +Markdown AST nodes must keep their source positions for source maps and fixes. + +| Input | AST behavior | +| --- | --- | +| `www.example.com` | Link | +| `` | Link | +| `` | Link | +| `visit https://example.com` | Text followed by a link | +| `"www.google.com"` | Text | + +The quoted URL remains text because the tokenizer does not recognize it. +Other URLs that require the disabled transform also remain text. + ### 序列化说明 `revertMdAstNode` 会将 AST 序列化为 Markdown,但 **不保证恢复原始文本**。 diff --git a/__tests__/plugins/gfm.spec.ts b/__tests__/plugins/gfm.spec.ts index 77f47a8..d1f6ac9 100644 --- a/__tests__/plugins/gfm.spec.ts +++ b/__tests__/plugins/gfm.spec.ts @@ -79,4 +79,37 @@ describe('remark-gfm plugin', () => { } } }); + + test('autolink www-prefixed URL', () => { + const root = parseMd('www.example.com'); + const paragraph = root.children[0]; + expect(paragraph.type).toBe('paragraph'); + if (paragraph.type === 'paragraph') { + expect(paragraph.children).toHaveLength(1); + const link = paragraph.children[0]; + expect(link.type).toBe('link'); + if (link.type === 'link') { + expect(link.url).toContain('www.example.com'); + expect(link.position.start.offset).toBe(0); + expect(link.position.end.offset).toBe('www.example.com'.length); + } + } + }); + + test('quoted www URL remains positioned text', () => { + const source = '"www.google.com"'; + const root = parseMd(source); + const paragraph = root.children[0]; + expect(paragraph.type).toBe('paragraph'); + if (paragraph.type === 'paragraph') { + expect(paragraph.children).toHaveLength(1); + const text = paragraph.children[0]; + expect(text.type).toBe('text'); + if (text.type === 'text') { + expect(text.value).toBe(source); + expect(text.position.start.offset).toBe(0); + expect(text.position.end.offset).toBe(source.length); + } + } + }); }); diff --git a/__tests__/unit/source-map.spec.ts b/__tests__/unit/source-map.spec.ts index af80c0b..e1f6f9a 100644 --- a/__tests__/unit/source-map.spec.ts +++ b/__tests__/unit/source-map.spec.ts @@ -30,6 +30,24 @@ describe('TypeScript source entry points', () => { expect(ast.children[0].type).toBe('heading'); }); + test('preserves positions and AST parity for quoted www URLs', () => { + const source = '"www.google.com"'; + const ast = parseMd(source); + const mapped = parseMdWithSourceMap(source); + const text = firstNode(ast, 'text'); + const mappedText = firstNode(mapped.ast, 'text'); + + expect(text.value).toBe(source); + expect(text.position.start.offset).toBe(0); + expect(text.position.end.offset).toBe(source.length); + expect(mappedText.value).toBe(source); + expect(mapped.sourceMap.getRaw(mappedText)).toBe(source); + expect(mappedText.position.start.offset).toBe(0); + expect(mappedText.position.end.offset).toBe(source.length); + expect(JSON.parse(JSON.stringify(mapped.ast))) + .toEqual(JSON.parse(JSON.stringify(ast))); + }); + test('maps a plain text value', () => { const source = 'Hello world'; const { ast, sourceMap } = parseMdWithSourceMap(source);