Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| `<https://example.com>` | Link |
| `<contact@example.com>` | 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,但 **不保证恢复原始文本**。
Expand Down
33 changes: 33 additions & 0 deletions __tests__/plugins/gfm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
});
});
18 changes: 18 additions & 0 deletions __tests__/unit/source-map.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading