// app/components/base/markdown/preprocess.spec.ts import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' /** * Helper to (re)load the module with a mocked config value. * We need to reset modules because the tested module imports * ALLOW_UNSAFE_DATA_SCHEME at top-level. */ const loadModuleWithConfig = async (allowDataScheme: boolean) => { vi.resetModules() vi.doMock('@/config', () => ({ ALLOW_UNSAFE_DATA_SCHEME: allowDataScheme })) return await import('./markdown-utils') } describe('preprocessLaTeX', () => { let mod: typeof import('./markdown-utils') beforeEach(async () => { // config value doesn't matter for LaTeX preprocessing, mock it false mod = await loadModuleWithConfig(false) }) it('returns non-string input unchanged', () => { // call with a non-string (bypass TS type system) // @ts-expect-error test const out = mod.preprocessLaTeX(123) expect(out).toBe(123) }) it('converts \\[ ... \\] into $$ ... $$', () => { const input = 'This is math: \\[x^2 + 1\\]' const out = mod.preprocessLaTeX(input) expect(out).toContain('$$x^2 + 1$$') }) it('converts \\( ... \\) into $$ ... $$', () => { const input = 'Inline: \\(a+b\\)' const out = mod.preprocessLaTeX(input) expect(out).toContain('$$a+b$$') }) it('preserves code blocks (does not transform $ inside them)', () => { const input = [ 'Some text before', '```js', 'const s = \'$insideCode$\'', '```', 'And outside $math$', ].join('\n') const out = mod.preprocessLaTeX(input) // code block should be preserved exactly (including $ inside) expect(out).toContain('```js\nconst s = \'$insideCode$\'\n```') // outside inline $math$ should remain intact (function keeps inline $...$) expect(out).toContain('$math$') }) it('does not treat escaped dollar \\$ as math delimiter', () => { const input = 'Price: \\$5 and math $x$' const out = mod.preprocessLaTeX(input) // escaped dollar should remain escaped expect(out).toContain('\\$5') // math should still be present expect(out).toContain('$x$') }) }) describe('preprocessThinkTag', () => { let mod: typeof import('./markdown-utils') beforeEach(async () => { mod = await loadModuleWithConfig(false) }) it('transforms single ... into details with data-think and ENDTHINKFLAG', () => { const input = 'this is a thought' const out = mod.preprocessThinkTag(input) expect(out).toContain('
') expect(out).toContain('this is a thought') expect(out).toContain('[ENDTHINKFLAG]
') }) it('handles multiple tags and inserts newline after closing ', () => { const input = 'one\ntwo' const out = mod.preprocessThinkTag(input) // both thoughts become details blocks const occurrences = (out.match(/
/g) || []).length expect(occurrences).toBe(2) // ensure ENDTHINKFLAG is present twice const endCount = (out.match(/\[ENDTHINKFLAG\]<\/details>/g) || []).length expect(endCount).toBe(2) }) }) describe('customUrlTransform', () => { afterEach(() => { vi.resetAllMocks() vi.resetModules() }) it('allows fragments (#foo) and protocol-relative (//host) and relative paths', async () => { const mod = await loadModuleWithConfig(false) const t = mod.customUrlTransform expect(t('#some-id')).toBe('#some-id') expect(t('//example.com/path')).toBe('//example.com/path') expect(t('relative/path/to/file')).toBe('relative/path/to/file') expect(t('/absolute/path')).toBe('/absolute/path') }) it('allows permitted schemes (http, https, mailto, xmpp, irc/ircs, abbr) case-insensitively', async () => { const mod = await loadModuleWithConfig(false) const t = mod.customUrlTransform expect(t('http://example.com')).toBe('http://example.com') expect(t('HTTPS://example.com')).toBe('HTTPS://example.com') expect(t('mailto:user@example.com')).toBe('mailto:user@example.com') expect(t('xmpp:user@example.com')).toBe('xmpp:user@example.com') expect(t('irc:somewhere')).toBe('irc:somewhere') expect(t('ircs:secure')).toBe('ircs:secure') expect(t('abbr:some-ref')).toBe('abbr:some-ref') }) it('rejects unknown/unsafe schemes (javascript:, ftp:) and returns undefined', async () => { const mod = await loadModuleWithConfig(false) const t = mod.customUrlTransform expect(t('javascript:alert(1)')).toBeUndefined() expect(t('ftp://example.com/file')).toBeUndefined() }) it('treats colons inside path/query/fragment as NOT a scheme and returns the original URI', async () => { const mod = await loadModuleWithConfig(false) const t = mod.customUrlTransform // colon after a slash -> part of path expect(t('folder/name:withcolon')).toBe('folder/name:withcolon') // colon after question mark -> part of query expect(t('page?param:http')).toBe('page?param:http') // colon after hash -> part of fragment expect(t('page#frag:with:colon')).toBe('page#frag:with:colon') }) it('respects ALLOW_UNSAFE_DATA_SCHEME: false blocks data:, true allows data:', async () => { const modFalse = await loadModuleWithConfig(false) expect(modFalse.customUrlTransform('data:text/plain;base64,SGVsbG8=')).toBeUndefined() const modTrue = await loadModuleWithConfig(true) expect(modTrue.customUrlTransform('data:text/plain;base64,SGVsbG8=')).toBe('data:text/plain;base64,SGVsbG8=') }) })