Skip to content

Commit 9f3f26a

Browse files
authored
Fix markdown: my_snake_case should not be italic (#233)
1 parent 0d67c6e commit 9f3f26a

File tree

2 files changed

+54
-11
lines changed

2 files changed

+54
-11
lines changed

src/components/Markdown/Markdown.test.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,21 @@ describe('Markdown', () => {
2929
expect(container.querySelector('em')).toBeNull()
3030
})
3131

32+
it('does not italicize snake case', () => {
33+
const text = 'Variables snake_post_ and mid_snake_case and _init_snake should not be italicized.'
34+
const { container, getByText } = render(<Markdown text={text} />)
35+
expect(container.innerHTML).not.toContain('<em>')
36+
expect(container.innerHTML).toContain('mid_snake_case')
37+
expect(getByText(text)).toBeDefined()
38+
})
39+
40+
it('does italicize surrounding underscores', () => {
41+
const text = '_this_one_tho_'
42+
const { container, getByText } = render(<Markdown text={text} />)
43+
expect(getByText('this_one_tho')).toBeDefined()
44+
expect(container.querySelector('em')).toBeDefined()
45+
})
46+
3247
it('renders headers', () => {
3348
const text = '# Heading 1\n## Heading 2\n### Heading 3'
3449
const { getByText } = render(<Markdown text={text} />)

src/components/Markdown/Markdown.tsx

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,10 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
235235

236236
while (i < text.length) {
237237
if (stop && text.startsWith(stop, i)) {
238-
return [tokens, i]
238+
// if we're closing "_", only do so when it's a real closing‐underscore
239+
if (stop !== '_' || isClosingUnderscore(text, i)) {
240+
return [tokens, i]
241+
}
239242
}
240243

241244
// Image: ![alt](src)
@@ -336,8 +339,8 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
336339
continue
337340
}
338341

339-
// Italic (* or _)
340-
if (text[i] === '*' || text[i] === '_') {
342+
// Italic opener: "*" always, "_" only when isOpeningUnderscore
343+
if (text[i] === '*' || text[i] === '_' && isOpeningUnderscore(text, i)) {
341344
const delimiter = text[i]
342345
// For '*' only: if surrounding non-space chars are digits, treat as literal
343346
if (delimiter === '*') {
@@ -361,11 +364,21 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
361364
continue
362365
}
363366
}
364-
i++
365-
const [innerTokens, consumed] = parseInlineRecursive(text.slice(i), delimiter)
366-
i += consumed
367-
i++ // skip closing delimiter
368-
tokens.push({ type: 'italic', children: innerTokens })
367+
368+
// look ahead for the rest of the text
369+
const rest = text.slice(i + 1)
370+
const [innerTokens, consumed] = parseInlineRecursive(rest, delimiter)
371+
372+
if (consumed < rest.length) {
373+
// we found a real closing delimiter
374+
tokens.push({ type: 'italic', children: innerTokens })
375+
// skip open, inner content, and closing
376+
i += 1 + consumed + 1
377+
} else if (delimiter) {
378+
// no closing delimiter — just emit a literal underscore/star
379+
tokens.push({ type: 'text', content: delimiter })
380+
i += 1
381+
}
369382
continue
370383
}
371384

@@ -376,10 +389,12 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
376389
&& text[j] !== '`'
377390
&& !(text.startsWith('**', j) || text.startsWith('__', j))
378391
&& text[j] !== '*'
379-
&& text[j] !== '_'
392+
&& !(text[j] === '_' && isOpeningUnderscore(text, j))
380393
&& text[j] !== '['
381-
// && text[j] !== '!'
382-
&& !(stop && text.startsWith(stop, j))
394+
// only break on stop when it's a real delimiter
395+
&& !(stop
396+
&& text.startsWith(stop, j)
397+
&& (stop !== '_' || isClosingUnderscore(text, j)))
383398
// handle ![alt](src) for images but not for `text!`
384399
&& !(text[j] === '!' && j + 1 < text.length && text[j + 1] === '[')
385400
) {
@@ -392,6 +407,19 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
392407
return [tokens, i]
393408
}
394409

410+
function isOpeningUnderscore(text: string, pos: number): boolean {
411+
const prev = text[pos - 1] ?? '\n'
412+
const next = text[pos + 1] ?? '\n'
413+
// can open only if next isn't whitespace, and prev isn't alnum
414+
return !/\s/.test(next) && !/\w/.test(prev)
415+
}
416+
function isClosingUnderscore(text: string, pos: number): boolean {
417+
const prev = text[pos - 1] ?? '\n'
418+
const next = text[pos + 1] ?? '\n'
419+
// can close only if prev isn't whitespace, and next isn't alnum
420+
return !/\s/.test(prev) && !/\w/.test(next)
421+
}
422+
395423
function renderTokens(tokens: Token[], keyPrefix = ''): ReactNode[] {
396424
return tokens.map((token, index) => {
397425
const key = `${keyPrefix}${index}`

0 commit comments

Comments
 (0)