mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 11:16:31 -05: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
|
@ -50,12 +50,12 @@
|
|||
"@hedgedoc/markdown-it-task-lists": "1.0.5",
|
||||
"@hedgedoc/realtime": "0.3.0",
|
||||
"@hpcc-js/wasm": "1.16.1",
|
||||
"@matejmazur/react-katex": "3.1.3",
|
||||
"@mrdrogdrog/optional": "0.2.0",
|
||||
"@react-hook/resize-observer": "1.2.6",
|
||||
"@redux-devtools/core": "3.13.1",
|
||||
"@reduxjs/toolkit": "1.8.5",
|
||||
"@svgr/webpack": "6.3.1",
|
||||
"@types/katex": "0.14.0",
|
||||
"@uiw/react-codemirror": "4.12.3",
|
||||
"abcjs": "6.1.3",
|
||||
"bootstrap": "4.6.2",
|
||||
|
|
|
@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
19
yarn.lock
19
yarn.lock
|
@ -1946,7 +1946,6 @@ __metadata:
|
|||
"@hedgedoc/markdown-it-task-lists": 1.0.5
|
||||
"@hedgedoc/realtime": 0.3.0
|
||||
"@hpcc-js/wasm": 1.16.1
|
||||
"@matejmazur/react-katex": 3.1.3
|
||||
"@mrdrogdrog/optional": 0.2.0
|
||||
"@next/bundle-analyzer": 12.3.1
|
||||
"@react-hook/resize-observer": 1.2.6
|
||||
|
@ -1963,6 +1962,7 @@ __metadata:
|
|||
"@types/dompurify": 2.3.4
|
||||
"@types/jest": 29.0.3
|
||||
"@types/js-yaml": 4.0.5
|
||||
"@types/katex": 0.14.0
|
||||
"@types/luxon": 3.0.1
|
||||
"@types/markdown-it": 12.2.3
|
||||
"@types/markdown-it-container": 2.0.5
|
||||
|
@ -2582,16 +2582,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@matejmazur/react-katex@npm:3.1.3":
|
||||
version: 3.1.3
|
||||
resolution: "@matejmazur/react-katex@npm:3.1.3"
|
||||
peerDependencies:
|
||||
katex: ">=0.9"
|
||||
react: ">=16"
|
||||
checksum: db9e9aa03d3b094fcb9854abeab3676732b04dbe4fbfe2dbf26782a6d50c197351d06ad833c78b90f7e418d769b77c30460a60906e80e800d84e16c938f3d6d1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mrdrogdrog/optional@npm:0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "@mrdrogdrog/optional@npm:0.2.0"
|
||||
|
@ -3428,6 +3418,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/katex@npm:0.14.0":
|
||||
version: 0.14.0
|
||||
resolution: "@types/katex@npm:0.14.0"
|
||||
checksum: 330e0d0337ba48c87f5b793965fbad673653789bf6e50dfe8d726a7b0cbefd37195055e31503aae629814aa79447e4f23a4b87ad1ac565c0d9a9d9978836f39b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/linkify-it@npm:*":
|
||||
version: 3.0.2
|
||||
resolution: "@types/linkify-it@npm:3.0.2"
|
||||
|
|
Loading…
Reference in a new issue