import { markdownLanguage } from '@codemirror/lang-markdown'
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark'
import { Parser } from '@lezer/common'
import { tagHighlighter, tags } from '@lezer/highlight'
import { render, screen } from '@testing-library/react'
// @vitest-environment jsdom
import { describe, expect, it } from 'vitest'
import { getCodeParser, highlightCode, Highlighter } from '../src'
const sleep = (msec: number) => new Promise(resolve => setTimeout(resolve, msec))
describe('getCodeParser', () => {
it('loads a JavaScript parser', async () => {
const parser = await getCodeParser('', 'js')
expect(parser).toBeInstanceOf(Parser)
})
it('loads a Python parser', async () => {
const parser = await getCodeParser('', 'python')
expect(parser).toBeInstanceOf(Parser)
})
it('returns null for non-existing languages', async () => {
const parser = await getCodeParser('', 'foobar123')
expect(parser).toBeNull()
})
})
describe('highlightCode', () => {
it('highlights JavaScript code', async () => {
const highlighted = await highlightCode(
'js',
'const x = 123',
oneDarkHighlightStyle,
undefined,
undefined,
(text, style, from, to) => ({ text, style, from, to })
)
expect(highlighted).toHaveLength(7)
expect(highlighted[0]).toEqual({
text: 'const',
style: 'ͼp',
from: 0,
to: 5
})
expect(highlighted[1]).toEqual({
text: ' ',
style: null,
from: 5,
to: 6
})
expect(highlighted[2]).toEqual({
text: 'x',
style: 'ͼt',
from: 6,
to: 7
})
expect(highlighted[4]).toEqual({
text: '=',
style: 'ͼv',
from: 8,
to: 9
})
expect(highlighted[6]).toEqual({
text: '123',
style: 'ͼu',
from: 10,
to: 13
})
})
})
describe('Highlight codeblocks', () => {
it('highlights codeblocks in markdown', async () => {
const mdCode = '```js\nconst x = 123\n```'
const highlighted = await highlightCode(
'markdown',
mdCode,
oneDarkHighlightStyle,
undefined,
undefined,
(text, style, from, to) => ({ text, style, from, to })
)
// Should have tokens for the markdown code block structure and the JS code inside
expect(highlighted.length).toBeGreaterThan(0)
// Find the code fence markers
const fenceStart = highlighted.find(t => t.text === '```')
expect(fenceStart).toBeDefined()
// Find the language info token
const langInfo = highlighted.find(t => t.text === 'js')
expect(langInfo).toBeDefined()
// Find the JS keyword 'const' inside the code block
const constToken = highlighted.find(t => t.text === 'const')
expect(constToken).toBeDefined()
expect(constToken?.style).not.toBeNull()
// Find the number '123'
const numberToken = highlighted.find(t => t.text === '123')
expect(numberToken).toBeDefined()
expect(numberToken?.style).not.toBeNull()
})
it('highlights typescript codeblocks in markdown', async () => {
const mdCode = '```typescript\nconst x: number = 123\n```'
const highlighted = await highlightCode(
'markdown',
mdCode,
oneDarkHighlightStyle,
undefined,
undefined,
(text, style, from, to) => ({ text, style, from, to })
)
expect(highlighted.length).toBeGreaterThan(0)
// Find the language info token
const langInfo = highlighted.find(t => t.text === 'typescript')
expect(langInfo).toBeDefined()
// Find the TS keyword 'const'
const constToken = highlighted.find(t => t.text === 'const')
expect(constToken).toBeDefined()
expect(constToken?.style).not.toBeNull()
// Find the type annotation 'number'
const typeToken = highlighted.find(t => t.text === 'number')
expect(typeToken).toBeDefined()
expect(typeToken?.style).not.toBeNull()
// Find the number '123'
const numberToken = highlighted.find(t => t.text === '123')
expect(numberToken).toBeDefined()
expect(numberToken?.style).not.toBeNull()
})
})
describe('markdownConfig (GFM)', () => {
const strikeHighlighter = tagHighlighter([{ tag: tags.strikethrough, class: 'strike' }])
it('does not tokenize GFM strikethrough with the default CommonMark base', async () => {
const highlighted = await highlightCode(
'markdown',
'~~gone~~',
strikeHighlighter,
undefined,
undefined,
(text, style) => ({ text, style })
)
expect(highlighted.some(t => t.style === 'strike')).toBe(false)
})
it('tokenizes GFM strikethrough when base is markdownLanguage', async () => {
const highlighted = await highlightCode(
'markdown',
'~~gone~~',
strikeHighlighter,
undefined,
undefined,
(text, style) => ({ text, style }),
{ base: markdownLanguage }
)
const struck = highlighted.find(t => t.style === 'strike')
expect(struck).toBeDefined()
expect(struck?.text).toContain('gone')
})
})
describe('multiple highlighters', () => {
const highlighterA = tagHighlighter([{ tag: tags.strikethrough, class: 'a-strike' }])
const highlighterB = tagHighlighter([{ tag: tags.strikethrough, class: 'b-strike' }])
it('merges classes emitted by an array of highlighters', async () => {
const highlighted = await highlightCode(
'markdown',
'~~gone~~',
[highlighterA, highlighterB],
undefined,
undefined,
(text, style) => ({ text, style }),
{ base: markdownLanguage }
)
const struck = highlighted.find(t => t.style?.includes('a-strike'))
expect(struck).toBeDefined()
expect(struck?.style).toContain('a-strike')
expect(struck?.style).toContain('b-strike')
})
})
describe('React Highlighter', () => {
it('renders highlighted code', async () => {
const code = 'const x = 123'
render(
{code}
)
await sleep(100)
const element = screen.getByText('const')
expect(element).toBeDefined()
expect(element).toHaveProperty('className', 'ͼp')
expect(screen.getByText('123')).toHaveProperty('className', 'ͼu')
})
it('swaps in a freshly created wrapper once highlighting resolves', async () => {
const { container } = render(
{'const x = 123'}
)
const pendingWrapper = container.querySelector('code > span')
expect(pendingWrapper).not.toBeNull()
await sleep(100)
// Reusing the pending element would make React insert every highlighted span
// into the already-attached DOM one at a time, which is ~25x slower on large
// input. The wrapper must be a genuinely new element.
const resolvedWrapper = container.querySelector('code > span')
expect(resolvedWrapper).not.toBe(pendingWrapper)
expect(resolvedWrapper?.getAttribute('style')).toContain('display: contents')
expect(container.textContent).toBe('const x = 123')
})
})