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
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@
"watch:url": "NODE_ENV=development nodemon bin/cli.js https://hyperparam.blob.core.windows.net/hyperparam/starcoderdata-js-00000-of-00065.parquet"
},
"dependencies": {
"hightable": "0.15.1",
"hyparquet": "1.12.1",
"hightable": "0.15.2",
"hyparquet": "1.13.0",
"hyparquet-compressors": "1.1.1",
"icebird": "0.2.0",
"react": "18.3.1",
Expand All @@ -71,7 +71,7 @@
"@testing-library/react": "16.3.0",
"@types/node": "22.14.1",
"@types/react": "19.1.2",
"@types/react-dom": "19.1.2",
"@types/react-dom": "19.1.3",
"@vitejs/plugin-react": "4.4.1",
"@vitest/coverage-v8": "3.1.2",
"eslint": "9.25.1",
Expand All @@ -85,8 +85,8 @@
"npm-run-all": "4.1.5",
"storybook": "8.6.12",
"typescript": "5.8.3",
"typescript-eslint": "8.31.0",
"vite": "6.3.3",
"typescript-eslint": "8.31.1",
"vite": "6.3.4",
"vitest": "3.1.2"
},
"eslintConfig": {
Expand Down
15 changes: 15 additions & 0 deletions src/components/Markdown/Markdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ describe('Markdown', () => {
expect(container.querySelector('em')).toBeNull()
})

it('does italicize numbers without spaces', () => {
const text = 'Four should be italic: 3*4*5.'
const { container, getByText } = render(<Markdown text={text} />)
expect(getByText('4')).toBeDefined()
expect(container.querySelector('em')).toBeDefined()
})

it('does not italicize snake case', () => {
const text = 'Variables snake_post_ and mid_snake_case and _init_snake should not be italicized.'
const { container, getByText } = render(<Markdown text={text} />)
Expand All @@ -44,6 +51,14 @@ describe('Markdown', () => {
expect(container.querySelector('em')).toBeDefined()
})

it('renders single asterisks for italic', () => {
const text = '*single asterisks*'
const { getByText } = render(<Markdown text={text} />)
const italicText = getByText('single asterisks')
expect(italicText).toBeDefined()
expect(italicText.tagName).toBe('EM')
})

it('renders headers', () => {
const text = '# Heading 1\n## Heading 2\n### Heading 3'
const { getByText } = render(<Markdown text={text} />)
Expand Down
80 changes: 40 additions & 40 deletions src/components/Markdown/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,12 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {

while (i < text.length) {
if (stop && text.startsWith(stop, i)) {
// if we're closing "_", only do so when it's a real closing‐underscore
if (stop !== '_' || isClosingUnderscore(text, i)) {
// validate closing delimiter
const validClosing =
(stop !== '_' || isClosingUnderscore(text, i)) &&
(stop !== '*' || isClosingAsterisk(text, i))

if (validClosing) {
return [tokens, i]
}
}
Expand Down Expand Up @@ -339,31 +343,12 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
continue
}

// Italic opener: "*" always, "_" only when isOpeningUnderscore
if (text[i] === '*' || text[i] === '_' && isOpeningUnderscore(text, i)) {
// Italic opener: "*" when left-flanking, "_" only when isOpeningUnderscore
if (
text[i] === '*' && isOpeningAsterisk(text, i) ||
text[i] === '_' && isOpeningUnderscore(text, i)
) {
const delimiter = text[i]
// For '*' only: if surrounding non-space chars are digits, treat as literal
if (delimiter === '*') {
let j = i - 1
while (j >= 0 && text[j] === ' ') j--
const characterAtJ = text[j]
if (characterAtJ === undefined) {
throw new Error(`Character at index ${j} is undefined`)
}
const prevIsDigit = j >= 0 && /\d/.test(characterAtJ)
let k = i + 1
while (k < text.length && text[k] === ' ') k++
const characterAtK = text[k]
if (characterAtK === undefined) {
throw new Error(`Character at index ${j} is undefined`)
}
const nextIsDigit = k < text.length && /\d/.test(characterAtK)
if (prevIsDigit && nextIsDigit) {
tokens.push({ type: 'text', content: delimiter })
i++
continue
}
}

// look ahead for the rest of the text
const rest = text.slice(i + 1)
Expand All @@ -385,23 +370,29 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
// Otherwise, consume plain text until next special character or end
let j = i
while (
j < text.length
&& text[j] !== '`'
&& !(text.startsWith('**', j) || text.startsWith('__', j))
&& text[j] !== '*'
&& !(text[j] === '_' && isOpeningUnderscore(text, j))
&& text[j] !== '['
// only break on stop when it's a real delimiter
&& !(stop
&& text.startsWith(stop, j)
&& (stop !== '_' || isClosingUnderscore(text, j)))
// handle ![alt](src) for images but not for `text!`
&& !(text[j] === '!' && j + 1 < text.length && text[j + 1] === '[')
j < text.length &&
text[j] !== '`' &&
!(text.startsWith('**', j) || text.startsWith('__', j)) &&
text[j] !== '*' &&
!(text[j] === '_' && isOpeningUnderscore(text, j)) &&
text[j] !== '[' &&
!(stop &&
text.startsWith(stop, j) &&
(stop !== '_' || isClosingUnderscore(text, j)) &&
(stop !== '*' || isClosingAsterisk(text, j))) &&
!(text[j] === '!' && j + 1 < text.length && text[j + 1] === '[')
) {
j++
}
tokens.push({ type: 'text', content: text.slice(i, j) })
i = j

if (j === i) {
// didn't consume anything – treat the single char literally
tokens.push({ type: 'text', content: text[i] ?? '' })
i++
} else {
tokens.push({ type: 'text', content: text.slice(i, j) })
i = j
}
}

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

function isOpeningAsterisk(text: string, pos: number): boolean {
const next = text[pos + 1] ?? '\n'
return !/\s/.test(next) // next char is not whitespace
}
function isClosingAsterisk(text: string, pos: number): boolean {
const prev = text[pos - 1] ?? '\n'
return !/\s/.test(prev) // prev char is not whitespace
}

function renderTokens(tokens: Token[], keyPrefix = ''): ReactNode[] {
return tokens.map((token, index) => {
const key = `${keyPrefix}${index}`
Expand Down