mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-24 20:14:35 -04:00
Replace @matejmazur/react-katex with own katex component (#2381)
* Replace @matejmazur/react-katex with self-made katex component Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
c57df024f3
commit
a9435e3652
8 changed files with 486 additions and 48 deletions
|
@ -0,0 +1,130 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`katex frame renders a valid latex expression as explicit block 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="katex-block"
|
||||
>
|
||||
<span>
|
||||
This is a mock for lib katex with this parameters:
|
||||
</span>
|
||||
|
||||
|
||||
<ul>
|
||||
|
||||
|
||||
<li>
|
||||
tex: \\int_0^\\infty x^2 dx
|
||||
</li>
|
||||
|
||||
|
||||
<li>
|
||||
block: true
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`katex frame renders a valid latex expression as explicit inline 1`] = `
|
||||
<div>
|
||||
<span
|
||||
data-testid="katex-inline"
|
||||
>
|
||||
<span>
|
||||
This is a mock for lib katex with this parameters:
|
||||
</span>
|
||||
|
||||
|
||||
<ul>
|
||||
|
||||
|
||||
<li>
|
||||
tex: \\int_0^\\infty x^2 dx
|
||||
</li>
|
||||
|
||||
|
||||
<li>
|
||||
block: false
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`katex frame renders a valid latex expression as implicit inline 1`] = `
|
||||
<div>
|
||||
<span
|
||||
data-testid="katex-inline"
|
||||
>
|
||||
<span>
|
||||
This is a mock for lib katex with this parameters:
|
||||
</span>
|
||||
|
||||
|
||||
<ul>
|
||||
|
||||
|
||||
<li>
|
||||
tex: \\int_0^\\infty x^2 dx
|
||||
</li>
|
||||
|
||||
|
||||
<li>
|
||||
block: false
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`katex frame renders an error for an invalid latex expression as explicit block 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="katex-block"
|
||||
>
|
||||
<div
|
||||
class="fade alert alert-danger show"
|
||||
role="alert"
|
||||
>
|
||||
mocked parseerror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`katex frame renders an error for an invalid latex expression as explicit inline 1`] = `
|
||||
<div>
|
||||
<span
|
||||
data-testid="katex-inline"
|
||||
>
|
||||
<div
|
||||
class="fade d-inline-block alert alert-danger show"
|
||||
role="alert"
|
||||
>
|
||||
mocked parseerror
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`katex frame renders an error for an invalid latex expression as implicit inline 1`] = `
|
||||
<div>
|
||||
<span
|
||||
data-testid="katex-inline"
|
||||
>
|
||||
<div
|
||||
class="fade d-inline-block alert alert-danger show"
|
||||
role="alert"
|
||||
>
|
||||
mocked parseerror
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,114 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`KaTeX markdown extensions renders a valid block LaTeX expression in a single line 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="katex-block"
|
||||
>
|
||||
<span>
|
||||
This is a mock for lib katex with this parameters:
|
||||
</span>
|
||||
|
||||
|
||||
<ul>
|
||||
|
||||
|
||||
<li>
|
||||
tex: \\alpha
|
||||
</li>
|
||||
|
||||
|
||||
<li>
|
||||
block: true
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`KaTeX markdown extensions renders a valid block LaTeX expression in multi line 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="katex-block"
|
||||
>
|
||||
<span>
|
||||
This is a mock for lib katex with this parameters:
|
||||
</span>
|
||||
|
||||
|
||||
<ul>
|
||||
|
||||
|
||||
<li>
|
||||
tex:
|
||||
\\alpha
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li>
|
||||
block: true
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`KaTeX markdown extensions renders a valid inline LaTeX expression 1`] = `
|
||||
<div>
|
||||
<p>
|
||||
<span
|
||||
data-testid="katex-inline"
|
||||
>
|
||||
<span>
|
||||
This is a mock for lib katex with this parameters:
|
||||
</span>
|
||||
|
||||
|
||||
<ul>
|
||||
|
||||
|
||||
<li>
|
||||
tex: \\alpha
|
||||
</li>
|
||||
|
||||
|
||||
<li>
|
||||
block: false
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`KaTeX markdown extensions renders an error message for an invalid LaTeX expression 1`] = `
|
||||
<div>
|
||||
<p>
|
||||
<span
|
||||
data-testid="katex-inline"
|
||||
>
|
||||
<div
|
||||
class="fade d-inline-block alert alert-danger show"
|
||||
role="alert"
|
||||
>
|
||||
mocked parseerror
|
||||
</div>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react'
|
||||
import KatexFrame from './katex-frame'
|
||||
import type { KatexOptions } from 'katex'
|
||||
import { default as KatexDefault } from 'katex'
|
||||
|
||||
jest.mock('katex')
|
||||
|
||||
describe('katex frame', () => {
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks()
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(KatexDefault, 'renderToString').mockImplementation(
|
||||
(tex: string, options?: KatexOptions) => `<span>This is a mock for lib katex with this parameters:</span>
|
||||
<ul>
|
||||
<li>tex: ${tex}</li>
|
||||
<li>block: ${String(options?.displayMode)}</li>
|
||||
</ul>`
|
||||
)
|
||||
})
|
||||
|
||||
describe('renders a valid latex expression', () => {
|
||||
it('as implicit inline', () => {
|
||||
const view = render(<KatexFrame expression={'\\int_0^\\infty x^2 dx'}></KatexFrame>)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
it('as explicit inline', () => {
|
||||
const view = render(<KatexFrame expression={'\\int_0^\\infty x^2 dx'} block={false}></KatexFrame>)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
it('as explicit block', () => {
|
||||
const view = render(<KatexFrame expression={'\\int_0^\\infty x^2 dx'} block={true}></KatexFrame>)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('renders an error for an invalid latex expression', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(KatexDefault, 'renderToString').mockImplementation(() => {
|
||||
throw new Error('mocked parseerror')
|
||||
})
|
||||
})
|
||||
|
||||
it('as implicit inline', () => {
|
||||
const view = render(<KatexFrame expression={'\\alf'}></KatexFrame>)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
it('as explicit inline', () => {
|
||||
const view = render(<KatexFrame expression={'\\alf'} block={false}></KatexFrame>)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
it('as explicit block', () => {
|
||||
const view = render(<KatexFrame expression={'\\alf'} block={true}></KatexFrame>)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react'
|
||||
import KaTeX from 'katex'
|
||||
import convertHtmlToReact from '@hedgedoc/html-to-react'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import { testId } from '../../../../utils/test-id'
|
||||
import { sanitize } from 'dompurify'
|
||||
|
||||
interface KatexFrameProps {
|
||||
expression: string
|
||||
block?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a LaTeX expression.
|
||||
*
|
||||
* @param expression The mathematical expression to render
|
||||
* @param block Defines if the output should be a block or inline.
|
||||
*/
|
||||
export const KatexFrame: React.FC<KatexFrameProps> = ({ expression, block = false }) => {
|
||||
const dom = useMemo(() => {
|
||||
try {
|
||||
const katexHtml = KaTeX.renderToString(expression, {
|
||||
displayMode: block === true,
|
||||
throwOnError: true
|
||||
})
|
||||
return convertHtmlToReact(sanitize(katexHtml, { ADD_TAGS: ['semantics', 'annotation'] }))
|
||||
} catch (error) {
|
||||
return (
|
||||
<Alert className={block ? '' : 'd-inline-block'} variant={'danger'}>
|
||||
{(error as Error).message}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
}, [block, expression])
|
||||
|
||||
return block ? <div {...testId('katex-block')}>{dom}</div> : <span {...testId('katex-inline')}>{dom}</span>
|
||||
}
|
||||
|
||||
export default KatexFrame
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { KatexMarkdownExtension } from './katex-markdown-extension'
|
||||
import { TestMarkdownRenderer } from '../../test-utils/test-markdown-renderer'
|
||||
import { Suspense } from 'react'
|
||||
import type { KatexOptions } from 'katex'
|
||||
import { default as KatexDefault } from 'katex'
|
||||
|
||||
jest.mock('katex')
|
||||
|
||||
describe('KaTeX markdown extensions', () => {
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks()
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(KatexDefault, 'renderToString').mockImplementation(
|
||||
(tex: string, options?: KatexOptions) => `<span>This is a mock for lib katex with this parameters:</span>
|
||||
<ul>
|
||||
<li>tex: ${tex}</li>
|
||||
<li>block: ${String(options?.displayMode)}</li>
|
||||
</ul>`
|
||||
)
|
||||
})
|
||||
|
||||
it('renders a valid inline LaTeX expression', async () => {
|
||||
const view = render(
|
||||
<Suspense fallback={null}>
|
||||
<TestMarkdownRenderer extensions={[new KatexMarkdownExtension()]} content={'$\\alpha$'} />
|
||||
</Suspense>
|
||||
)
|
||||
expect(await screen.findByTestId('katex-inline')).toBeInTheDocument()
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a valid block LaTeX expression in a single line', async () => {
|
||||
const view = render(
|
||||
<Suspense fallback={null}>
|
||||
<TestMarkdownRenderer extensions={[new KatexMarkdownExtension()]} content={'$$$\\alpha$$$'} />
|
||||
</Suspense>
|
||||
)
|
||||
expect(await screen.findByTestId('katex-block')).toBeInTheDocument()
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a valid block LaTeX expression in multi line', async () => {
|
||||
const view = render(
|
||||
<Suspense fallback={null}>
|
||||
<TestMarkdownRenderer extensions={[new KatexMarkdownExtension()]} content={'$$$\n\\alpha\n$$$'} />
|
||||
</Suspense>
|
||||
)
|
||||
expect(await screen.findByTestId('katex-block')).toBeInTheDocument()
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders an error message for an invalid LaTeX expression', async () => {
|
||||
jest.spyOn(KatexDefault, 'renderToString').mockImplementation(() => {
|
||||
throw new Error('mocked parseerror')
|
||||
})
|
||||
|
||||
const view = render(
|
||||
<Suspense fallback={null}>
|
||||
<TestMarkdownRenderer extensions={[new KatexMarkdownExtension()]} content={'$\\a$'} />
|
||||
</Suspense>
|
||||
)
|
||||
expect(await screen.findByTestId('katex-inline')).toBeInTheDocument()
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -8,49 +8,60 @@ import type { Element } from 'domhandler'
|
|||
import { isTag } from 'domhandler'
|
||||
import React from 'react'
|
||||
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { KatexMarkdownExtension } from './katex-markdown-extension'
|
||||
import { Optional } from '@mrdrogdrog/optional'
|
||||
|
||||
/**
|
||||
* Checks if the given node is a KaTeX block.
|
||||
*
|
||||
* @param node the node to check
|
||||
* @return The given node if it is a KaTeX block element, {@link undefined} otherwise.
|
||||
*/
|
||||
const containsKatexBlock = (node: Element): Element | undefined => {
|
||||
if (node.name !== 'p' || !node.children || node.children.length === 0) {
|
||||
return
|
||||
}
|
||||
return node.children.filter(isTag).find((subnode) => {
|
||||
return isKatexTag(subnode, false) ? subnode : undefined
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given node is a KaTeX element.
|
||||
*
|
||||
* @param node the node to check
|
||||
* @param expectedInline defines if the found katex element is expected to be an inline or block element.
|
||||
* @return {@link true} if the given node is a katex element.
|
||||
*/
|
||||
const isKatexTag = (node: Element, expectedInline: boolean) => {
|
||||
return (
|
||||
node.name === KatexMarkdownExtension.tagName && (node.attribs?.['data-inline'] !== undefined) === expectedInline
|
||||
)
|
||||
}
|
||||
|
||||
const KaTeX = React.lazy(() => import(/* webpackChunkName: "katex" */ '@matejmazur/react-katex'))
|
||||
const KaTeX = React.lazy(() => import(/* webpackChunkName: "katex" */ './katex-frame'))
|
||||
|
||||
/**
|
||||
* Detects LaTeX syntax and renders it with KaTeX.
|
||||
*/
|
||||
export class KatexReplacer extends ComponentReplacer {
|
||||
public replace(node: Element): React.ReactElement | undefined {
|
||||
if (!(isKatexTag(node, true) || containsKatexBlock(node)) || node.children?.[0] === undefined) {
|
||||
return DO_NOT_REPLACE
|
||||
}
|
||||
const latexContent = ComponentReplacer.extractTextChildContent(node)
|
||||
const isInline = !!node.attribs?.['data-inline']
|
||||
return <KaTeX block={!isInline} math={latexContent} errorColor={'#cc0000'} />
|
||||
return this.extractKatexContent(node)
|
||||
.map((latexContent) => {
|
||||
const isInline = !!node.attribs?.['data-inline']
|
||||
return <KaTeX key={'katex'} block={!isInline} expression={latexContent} />
|
||||
})
|
||||
.orElse(DO_NOT_REPLACE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given node for katex expression tags and extracts the LaTeX code.
|
||||
*
|
||||
* @param node The node to scan for inline or block tags
|
||||
* @return An optional that contains the extracted latex code
|
||||
*/
|
||||
private extractKatexContent(node: Element): Optional<string> {
|
||||
return this.isKatexTag(node, true)
|
||||
? Optional.of(ComponentReplacer.extractTextChildContent(node))
|
||||
: this.extractKatexBlock(node).map((childNode) => ComponentReplacer.extractTextChildContent(childNode))
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given node is a KaTeX element.
|
||||
*
|
||||
* @param node the node to check
|
||||
* @param expectedInline defines if the found katex element is expected to be an inline or block element.
|
||||
* @return {@link true} if the given node is a katex element.
|
||||
*/
|
||||
private isKatexTag(node: Element, expectedInline: boolean) {
|
||||
return (
|
||||
node.name === KatexMarkdownExtension.tagName && (node.attribs?.['data-inline'] !== undefined) === expectedInline
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given node is a KaTeX block.
|
||||
*
|
||||
* @param node the node to check
|
||||
* @return The given node if it is a KaTeX block element, {@link undefined} otherwise.
|
||||
*/
|
||||
private extractKatexBlock(node: Element): Optional<Element> {
|
||||
return Optional.of(node)
|
||||
.filter((node) => node.name === 'p' && node.children?.length > 0)
|
||||
.map((node) =>
|
||||
node.children.filter(isTag).find((subNode) => (this.isKatexTag(subNode, false) ? subNode : undefined))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue