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') }) })