Skip to content

Commit 4bfc2b3

Browse files
authored
Fix markdown index -1 error (#237)
1 parent 288a53c commit 4bfc2b3

File tree

3 files changed

+60
-45
lines changed

3 files changed

+60
-45
lines changed

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@
5353
"watch:url": "NODE_ENV=development nodemon bin/cli.js https://hyperparam.blob.core.windows.net/hyperparam/starcoderdata-js-00000-of-00065.parquet"
5454
},
5555
"dependencies": {
56-
"hightable": "0.15.1",
57-
"hyparquet": "1.12.1",
56+
"hightable": "0.15.2",
57+
"hyparquet": "1.13.0",
5858
"hyparquet-compressors": "1.1.1",
5959
"icebird": "0.2.0",
6060
"react": "18.3.1",
@@ -71,7 +71,7 @@
7171
"@testing-library/react": "16.3.0",
7272
"@types/node": "22.14.1",
7373
"@types/react": "19.1.2",
74-
"@types/react-dom": "19.1.2",
74+
"@types/react-dom": "19.1.3",
7575
"@vitejs/plugin-react": "4.4.1",
7676
"@vitest/coverage-v8": "3.1.2",
7777
"eslint": "9.25.1",
@@ -85,8 +85,8 @@
8585
"npm-run-all": "4.1.5",
8686
"storybook": "8.6.12",
8787
"typescript": "5.8.3",
88-
"typescript-eslint": "8.31.0",
89-
"vite": "6.3.3",
88+
"typescript-eslint": "8.31.1",
89+
"vite": "6.3.4",
9090
"vitest": "3.1.2"
9191
},
9292
"eslintConfig": {

src/components/Markdown/Markdown.test.tsx

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

32+
it('does italicize numbers without spaces', () => {
33+
const text = 'Four should be italic: 3*4*5.'
34+
const { container, getByText } = render(<Markdown text={text} />)
35+
expect(getByText('4')).toBeDefined()
36+
expect(container.querySelector('em')).toBeDefined()
37+
})
38+
3239
it('does not italicize snake case', () => {
3340
const text = 'Variables snake_post_ and mid_snake_case and _init_snake should not be italicized.'
3441
const { container, getByText } = render(<Markdown text={text} />)
@@ -44,6 +51,14 @@ describe('Markdown', () => {
4451
expect(container.querySelector('em')).toBeDefined()
4552
})
4653

54+
it('renders single asterisks for italic', () => {
55+
const text = '*single asterisks*'
56+
const { getByText } = render(<Markdown text={text} />)
57+
const italicText = getByText('single asterisks')
58+
expect(italicText).toBeDefined()
59+
expect(italicText.tagName).toBe('EM')
60+
})
61+
4762
it('renders headers', () => {
4863
const text = '# Heading 1\n## Heading 2\n### Heading 3'
4964
const { getByText } = render(<Markdown text={text} />)

src/components/Markdown/Markdown.tsx

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

236236
while (i < text.length) {
237237
if (stop && text.startsWith(stop, i)) {
238-
// if we're closing "_", only do so when it's a real closing‐underscore
239-
if (stop !== '_' || isClosingUnderscore(text, i)) {
238+
// validate closing delimiter
239+
const validClosing =
240+
(stop !== '_' || isClosingUnderscore(text, i)) &&
241+
(stop !== '*' || isClosingAsterisk(text, i))
242+
243+
if (validClosing) {
240244
return [tokens, i]
241245
}
242246
}
@@ -339,31 +343,12 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
339343
continue
340344
}
341345

342-
// Italic opener: "*" always, "_" only when isOpeningUnderscore
343-
if (text[i] === '*' || text[i] === '_' && isOpeningUnderscore(text, i)) {
346+
// Italic opener: "*" when left-flanking, "_" only when isOpeningUnderscore
347+
if (
348+
text[i] === '*' && isOpeningAsterisk(text, i) ||
349+
text[i] === '_' && isOpeningUnderscore(text, i)
350+
) {
344351
const delimiter = text[i]
345-
// For '*' only: if surrounding non-space chars are digits, treat as literal
346-
if (delimiter === '*') {
347-
let j = i - 1
348-
while (j >= 0 && text[j] === ' ') j--
349-
const characterAtJ = text[j]
350-
if (characterAtJ === undefined) {
351-
throw new Error(`Character at index ${j} is undefined`)
352-
}
353-
const prevIsDigit = j >= 0 && /\d/.test(characterAtJ)
354-
let k = i + 1
355-
while (k < text.length && text[k] === ' ') k++
356-
const characterAtK = text[k]
357-
if (characterAtK === undefined) {
358-
throw new Error(`Character at index ${j} is undefined`)
359-
}
360-
const nextIsDigit = k < text.length && /\d/.test(characterAtK)
361-
if (prevIsDigit && nextIsDigit) {
362-
tokens.push({ type: 'text', content: delimiter })
363-
i++
364-
continue
365-
}
366-
}
367352

368353
// look ahead for the rest of the text
369354
const rest = text.slice(i + 1)
@@ -385,23 +370,29 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
385370
// Otherwise, consume plain text until next special character or end
386371
let j = i
387372
while (
388-
j < text.length
389-
&& text[j] !== '`'
390-
&& !(text.startsWith('**', j) || text.startsWith('__', j))
391-
&& text[j] !== '*'
392-
&& !(text[j] === '_' && isOpeningUnderscore(text, j))
393-
&& text[j] !== '['
394-
// only break on stop when it's a real delimiter
395-
&& !(stop
396-
&& text.startsWith(stop, j)
397-
&& (stop !== '_' || isClosingUnderscore(text, j)))
398-
// handle ![alt](src) for images but not for `text!`
399-
&& !(text[j] === '!' && j + 1 < text.length && text[j + 1] === '[')
373+
j < text.length &&
374+
text[j] !== '`' &&
375+
!(text.startsWith('**', j) || text.startsWith('__', j)) &&
376+
text[j] !== '*' &&
377+
!(text[j] === '_' && isOpeningUnderscore(text, j)) &&
378+
text[j] !== '[' &&
379+
!(stop &&
380+
text.startsWith(stop, j) &&
381+
(stop !== '_' || isClosingUnderscore(text, j)) &&
382+
(stop !== '*' || isClosingAsterisk(text, j))) &&
383+
!(text[j] === '!' && j + 1 < text.length && text[j + 1] === '[')
400384
) {
401385
j++
402386
}
403-
tokens.push({ type: 'text', content: text.slice(i, j) })
404-
i = j
387+
388+
if (j === i) {
389+
// didn't consume anything – treat the single char literally
390+
tokens.push({ type: 'text', content: text[i] ?? '' })
391+
i++
392+
} else {
393+
tokens.push({ type: 'text', content: text.slice(i, j) })
394+
i = j
395+
}
405396
}
406397

407398
return [tokens, i]
@@ -420,6 +411,15 @@ function isClosingUnderscore(text: string, pos: number): boolean {
420411
return !/\s/.test(prev) && !/\w/.test(next)
421412
}
422413

414+
function isOpeningAsterisk(text: string, pos: number): boolean {
415+
const next = text[pos + 1] ?? '\n'
416+
return !/\s/.test(next) // next char is not whitespace
417+
}
418+
function isClosingAsterisk(text: string, pos: number): boolean {
419+
const prev = text[pos - 1] ?? '\n'
420+
return !/\s/.test(prev) // prev char is not whitespace
421+
}
422+
423423
function renderTokens(tokens: Token[], keyPrefix = ''): ReactNode[] {
424424
return tokens.map((token, index) => {
425425
const key = `${keyPrefix}${index}`

0 commit comments

Comments
 (0)