mirror of
https://github.com/langgenius/dify.git
synced 2026-03-14 11:28:34 +08:00
Signed-off-by: edvatar <88481784+toroleapinc@users.noreply.github.com> Signed-off-by: -LAN- <laipz8200@outlook.com> Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: majiayu000 <1835304752@qq.com> Co-authored-by: Poojan <poojan@infocusp.com> Co-authored-by: sahil-infocusp <73810410+sahil-infocusp@users.noreply.github.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: Pandaaaa906 <ye.pandaaaa906@gmail.com> Co-authored-by: Asuka Minato <i@asukaminato.eu.org> Co-authored-by: heyszt <270985384@qq.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Ijas <ijas.ahmd.ap@gmail.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: 木之本澪 <kinomotomiovo@gmail.com> Co-authored-by: KinomotoMio <200703522+KinomotoMio@users.noreply.github.com> Co-authored-by: 不做了睡大觉 <64798754+stakeswky@users.noreply.github.com> Co-authored-by: User <user@example.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: edvatar <88481784+toroleapinc@users.noreply.github.com> Co-authored-by: -LAN- <laipz8200@outlook.com> Co-authored-by: Leilei <138381132+Inlei@users.noreply.github.com> Co-authored-by: HaKu <104669497+haku-ink@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: wangxiaolei <fatelei@gmail.com> Co-authored-by: Varun Chawla <34209028+veeceey@users.noreply.github.com> Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: tda <95275462+tda1017@users.noreply.github.com> Co-authored-by: root <root@DESKTOP-KQLO90N> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> Co-authored-by: Niels Kaspers <153818647+nielskaspers@users.noreply.github.com> Co-authored-by: hj24 <mambahj24@gmail.com> Co-authored-by: Tyson Cung <45380903+tysoncung@users.noreply.github.com> Co-authored-by: Stephen Zhou <hi@hyoban.cc> Co-authored-by: FFXN <31929997+FFXN@users.noreply.github.com> Co-authored-by: slegarraga <64795732+slegarraga@users.noreply.github.com> Co-authored-by: 99 <wh2099@pm.me> Co-authored-by: Br1an <932039080@qq.com> Co-authored-by: L1nSn0w <l1nsn0w@qq.com> Co-authored-by: Yunlu Wen <yunlu.wen@dify.ai> Co-authored-by: akkoaya <151345394+akkoaya@users.noreply.github.com> Co-authored-by: 盐粒 Yanli <yanli@dify.ai> Co-authored-by: lif <1835304752@qq.com> Co-authored-by: weiguang li <codingpunk@gmail.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: HanWenbo <124024253+hwb96@users.noreply.github.com> Co-authored-by: Coding On Star <447357187@qq.com> Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: Stable Genius <stablegenius043@gmail.com> Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com> Co-authored-by: ふるい <46769295+Echo0ff@users.noreply.github.com> Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
155 lines
5.3 KiB
TypeScript
155 lines
5.3 KiB
TypeScript
/**
|
|
* 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 <think>...</think> into details with data-think and ENDTHINKFLAG', () => {
|
|
const input = '<think>this is a thought</think>'
|
|
const out = mod.preprocessThinkTag(input)
|
|
|
|
expect(out).toContain('<details data-think=true>')
|
|
expect(out).toContain('this is a thought')
|
|
expect(out).toContain('[ENDTHINKFLAG]</details>')
|
|
})
|
|
|
|
it('handles multiple <think> tags and inserts newline after closing </details>', () => {
|
|
const input = '<think>one</think>\n<think>two</think>'
|
|
const out = mod.preprocessThinkTag(input)
|
|
|
|
// both thoughts become details blocks
|
|
const occurrences = (out.match(/<details data-think=true>/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=')
|
|
})
|
|
})
|