Skip to content

Commit f64ce5b

Browse files
authored
fix(parse): map sub/sup tokens to correct tags (#202)
1 parent 181301c commit f64ce5b

2 files changed

Lines changed: 68 additions & 1 deletion

File tree

packages/comark/src/internal/parse/token-processor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ const INLINE_TAG_MAP: Record<string, string> = {
2020
strong_open: 'strong',
2121
em_open: 'em',
2222
s_open: 'del',
23-
sub_open: 'del',
23+
sub_open: 'sub',
24+
sup_open: 'sup',
2425
}
2526

2627
interface ProcessState {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, expect, it } from 'vitest'
2+
import type { MarkdownItPlugin } from '../../src/types'
3+
import { defineComarkPlugin, parse } from '../../src/parse'
4+
5+
// Minimal inline rules that mirror what `markdown-it-sub` / `markdown-it-sup`
6+
// emit: `~text~` → sub_open/text/sub_close, `^text^` → sup_open/text/sup_close.
7+
// This lets the test pin the bug from comarkdown/comark#201 without pulling in
8+
// the upstream packages.
9+
function makeDelimiterRule(tag: 'sub' | 'sup', char: number) {
10+
return (state: any, silent: boolean) => {
11+
if (silent) return false
12+
if (state.src.charCodeAt(state.pos) !== char) return false
13+
14+
const start = state.pos + 1
15+
const max = state.posMax
16+
let pos = start
17+
while (pos < max && state.src.charCodeAt(pos) !== char) {
18+
if (state.src.charCodeAt(pos) === 0x20 /* space */) return false
19+
pos++
20+
}
21+
if (pos === start || pos >= max) return false
22+
23+
state.push(`${tag}_open`, tag, 1)
24+
const text = state.push('text', '', 0)
25+
text.content = state.src.slice(start, pos)
26+
state.push(`${tag}_close`, tag, -1)
27+
28+
state.pos = pos + 1
29+
return true
30+
}
31+
}
32+
33+
const markdownItSub: MarkdownItPlugin = (md) => {
34+
md.inline.ruler.after('emphasis', 'sub', makeDelimiterRule('sub', 0x7e /* ~ */))
35+
}
36+
37+
const markdownItSup: MarkdownItPlugin = (md) => {
38+
md.inline.ruler.after('emphasis', 'sup', makeDelimiterRule('sup', 0x5e /* ^ */))
39+
}
40+
41+
const subscript = defineComarkPlugin(() => ({
42+
name: 'subscript',
43+
markdownItPlugins: [markdownItSub],
44+
}))
45+
46+
const superscript = defineComarkPlugin(() => ({
47+
name: 'superscript',
48+
markdownItPlugins: [markdownItSup],
49+
}))
50+
51+
describe('sub/sup token mapping (regression for #201)', () => {
52+
it('maps sub_open tokens to a <sub> element (not <del>)', async () => {
53+
const tree = await parse('H~2~O', { plugins: [subscript()] })
54+
expect(tree.nodes).toEqual([['p', {}, 'H', ['sub', {}, '2'], 'O']])
55+
})
56+
57+
it('maps sup_open tokens to a <sup> element (not dropped)', async () => {
58+
const tree = await parse('x^2^', { plugins: [superscript()] })
59+
expect(tree.nodes).toEqual([['p', {}, 'x', ['sup', {}, '2']]])
60+
})
61+
62+
it('preserves both sub and sup in the same paragraph', async () => {
63+
const tree = await parse('H~2~O and x^2^', { plugins: [subscript(), superscript()] })
64+
expect(tree.nodes).toEqual([['p', {}, 'H', ['sub', {}, '2'], 'O and x', ['sup', {}, '2']]])
65+
})
66+
})

0 commit comments

Comments
 (0)