mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-17 08:34:54 -04:00
Test multiple markdown extensions (#1886)
This commit adds multiple unit jest tests for components and removes e2e tests. Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de> Signed-off-by: Philip Molares <philip.molares@udo.edu> Co-authored-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
parent
dca541ea1a
commit
32c6bbb8e3
67 changed files with 2123 additions and 553 deletions
|
@ -11,50 +11,14 @@ describe('Diagram codeblock ', () => {
|
|||
|
||||
/*
|
||||
TODO: Readd test after fixing https://github.com/hedgedoc/react-client/issues/1709
|
||||
|
||||
it('renders markmap', () => {
|
||||
cy.setCodemirrorContent('```markmap\n- pro\n- contra\n```')
|
||||
cy.getMarkdownBody().findByCypressId('markmap').children().should('be.visible')
|
||||
})
|
||||
*/
|
||||
|
||||
it('renders vega-lite', () => {
|
||||
cy.setCodemirrorContent(
|
||||
'```vega-lite\n{"$schema":"https://vega.github.io/schema/vega-lite/v4.json","data":{"values":[{"a":"","b":28}]},"mark":"bar","encoding":{"x":{"field":"a"},"y":{"field":"b"}}}\n```'
|
||||
)
|
||||
cy.getMarkdownBody().find('.vega-embed').children().should('be.visible')
|
||||
})
|
||||
|
||||
it('renders graphviz', () => {
|
||||
cy.setCodemirrorContent('```graphviz\ngraph {\na -- b\n}\n```')
|
||||
cy.getMarkdownBody().findByCypressId('graphviz').children().should('be.visible')
|
||||
})
|
||||
|
||||
it('renders mermaid', () => {
|
||||
cy.setCodemirrorContent('```mermaid\ngraph TD;\n A-->B;\n```')
|
||||
cy.getMarkdownBody().findByCypressId('mermaid-frame').children().should('be.visible')
|
||||
})
|
||||
|
||||
it('renders flowcharts', () => {
|
||||
cy.setCodemirrorContent('```flow\nst=>start: Start\ne=>end: End\nst->e\n```')
|
||||
cy.getMarkdownBody().findByCypressId('flowchart').children().should('be.visible')
|
||||
})
|
||||
|
||||
it('renders abc scores', () => {
|
||||
cy.setCodemirrorContent('```abc\nM:4/4\nK:G\n|:GABc dedB:|\n```')
|
||||
cy.getMarkdownBody().findByCypressId('abcjs').children().should('be.visible')
|
||||
})
|
||||
|
||||
it('renders csv as table', () => {
|
||||
cy.setCodemirrorContent('```csv delimiter=; header\na;b;c;d\n1;2;3;4\n```')
|
||||
cy.getMarkdownBody().findByCypressId('csv-html-table').first().should('be.visible')
|
||||
})
|
||||
|
||||
it('renders plantuml', () => {
|
||||
cy.setCodemirrorContent('```plantuml\nclass Example\n```')
|
||||
cy.getMarkdownBody()
|
||||
.find('img')
|
||||
// PlantUML uses base64 encoded version of zip-deflated PlantUML code in the request URL.
|
||||
.should('have.attr', 'src', 'http://mock-plantuml.local/svg/SoWkIImgAStDuKhEIImkLd2jICmjo4dbSaZDIm6A0W00')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
describe('emojis', () => {
|
||||
const HEDGEHOG_UNICODE_CHARACTER = '\n🦔\n'
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visitTestNote()
|
||||
})
|
||||
|
||||
it('renders an emoji shortcode', () => {
|
||||
cy.setCodemirrorContent(':hedgehog:')
|
||||
cy.getMarkdownBody().should('have.text', HEDGEHOG_UNICODE_CHARACTER)
|
||||
})
|
||||
|
||||
it('renders an emoji unicode character', () => {
|
||||
cy.setCodemirrorContent(HEDGEHOG_UNICODE_CHARACTER)
|
||||
cy.getMarkdownBody().should('have.text', HEDGEHOG_UNICODE_CHARACTER)
|
||||
})
|
||||
|
||||
it('renders an fork awesome icon', () => {
|
||||
cy.setCodemirrorContent(':fa-matrix-org:')
|
||||
cy.getMarkdownBody().find('i.fa.fa-matrix-org').should('be.visible')
|
||||
})
|
||||
})
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
const findHljsCodeBlock = () => {
|
||||
return cy.getMarkdownBody().findByCypressId('code-highlighter').should('be.visible')
|
||||
}
|
||||
|
||||
describe('Code', () => {
|
||||
beforeEach(() => {
|
||||
cy.visitTestNote()
|
||||
})
|
||||
|
||||
describe('with just the language', () => {
|
||||
it("doesn't show a gutter", () => {
|
||||
cy.setCodemirrorContent('```javascript \nlet x = 0\n```')
|
||||
findHljsCodeBlock().should('have.attr', 'data-cypress-showgutter', 'false')
|
||||
|
||||
findHljsCodeBlock().findByCypressId('linenumber').should('not.be.visible')
|
||||
})
|
||||
|
||||
describe('and line wrapping', () => {
|
||||
it("doesn't show a gutter", () => {
|
||||
cy.setCodemirrorContent('```javascript! \nlet x = 0\n```')
|
||||
findHljsCodeBlock().should('have.attr', 'data-cypress-showgutter', 'false')
|
||||
findHljsCodeBlock().should('have.attr', 'data-cypress-wrapLines', 'true')
|
||||
|
||||
findHljsCodeBlock().findByCypressId('linenumber').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with the language and show gutter', () => {
|
||||
it('shows the correct line number', () => {
|
||||
cy.setCodemirrorContent('```javascript= \nlet x = 0\n```')
|
||||
findHljsCodeBlock().should('have.attr', 'data-cypress-showgutter', 'true')
|
||||
|
||||
findHljsCodeBlock().findByCypressId('linenumber').should('be.visible').text().should('eq', '1')
|
||||
})
|
||||
|
||||
describe('and line wrapping', () => {
|
||||
it('shows the correct line number', () => {
|
||||
cy.setCodemirrorContent('```javascript=! \nlet x = 0\n```')
|
||||
findHljsCodeBlock().should('have.attr', 'data-cypress-showgutter', 'true')
|
||||
findHljsCodeBlock().should('have.attr', 'data-cypress-wrapLines', 'true')
|
||||
|
||||
findHljsCodeBlock().findByCypressId('linenumber').should('be.visible').text().should('eq', '1')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with the language, show gutter with a start number', () => {
|
||||
it('shows the correct line number', () => {
|
||||
cy.setCodemirrorContent('```javascript=100 \nlet x = 0\n```')
|
||||
findHljsCodeBlock().should('have.attr', 'data-cypress-showgutter', 'true')
|
||||
|
||||
findHljsCodeBlock().findByCypressId('linenumber').should('be.visible').text().should('eq', '100')
|
||||
})
|
||||
|
||||
it('shows the correct line number and continues in another codeblock', () => {
|
||||
cy.setCodemirrorContent('```javascript=100 \nlet x = 0\nlet y = 1\n```\n\n```javascript=+\nlet y = 2\n```\n')
|
||||
findHljsCodeBlock().should('have.attr', 'data-cypress-showgutter', 'true')
|
||||
findHljsCodeBlock().first().findByCypressId('linenumber').first().should('be.visible').text().should('eq', '100')
|
||||
findHljsCodeBlock().first().findByCypressId('linenumber').last().should('be.visible').text().should('eq', '101')
|
||||
findHljsCodeBlock().last().findByCypressId('linenumber').first().should('be.visible').text().should('eq', '102')
|
||||
})
|
||||
|
||||
describe('and line wrapping', () => {
|
||||
it('shows the correct line number', () => {
|
||||
cy.setCodemirrorContent('```javascript=100! \nlet x = 0\n```')
|
||||
findHljsCodeBlock().should('have.attr', 'data-cypress-showgutter', 'true')
|
||||
findHljsCodeBlock().should('have.attr', 'data-cypress-wrapLines', 'true')
|
||||
findHljsCodeBlock().findByCypressId('linenumber').should('be.visible').text().should('eq', '100')
|
||||
})
|
||||
|
||||
it('shows the correct line number and continues in another codeblock', () => {
|
||||
cy.setCodemirrorContent('```javascript=100! \nlet x = 0\nlet y = 1\n```\n\n```javascript=+\nlet y = 2\n```\n')
|
||||
findHljsCodeBlock().should('have.attr', 'data-cypress-showgutter', 'true')
|
||||
findHljsCodeBlock().should('have.attr', 'data-cypress-wrapLines', 'true')
|
||||
findHljsCodeBlock()
|
||||
.first()
|
||||
.findByCypressId('linenumber')
|
||||
.first()
|
||||
.should('be.visible')
|
||||
.text()
|
||||
.should('eq', '100')
|
||||
findHljsCodeBlock().first().findByCypressId('linenumber').last().should('be.visible').text().should('eq', '101')
|
||||
findHljsCodeBlock().last().findByCypressId('linenumber').first().should('be.visible').text().should('eq', '102')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -16,41 +16,4 @@ describe('Link gets replaced with embedding: ', () => {
|
|||
cy.getMarkdownBody().findByCypressId('click-shield-gist').find('.preview-background').parent().click()
|
||||
cy.getMarkdownBody().findByCypressId('gh-gist').should('be.visible')
|
||||
})
|
||||
|
||||
it('YouTube', () => {
|
||||
cy.setCodemirrorContent('https://www.youtube.com/watch?v=YE7VzlLtp-4')
|
||||
cy.getMarkdownBody()
|
||||
.findByCypressId('click-shield-youtube')
|
||||
.find('.preview-background')
|
||||
.should('have.attr', 'src', 'https://i.ytimg.com/vi/YE7VzlLtp-4/maxresdefault.jpg')
|
||||
.parent()
|
||||
.click()
|
||||
cy.getMarkdownBody()
|
||||
.find('iframe')
|
||||
.should('have.attr', 'src', 'https://www.youtube-nocookie.com/embed/YE7VzlLtp-4?autoplay=1')
|
||||
})
|
||||
|
||||
it('Vimeo', () => {
|
||||
cy.intercept(
|
||||
{
|
||||
method: 'GET',
|
||||
url: 'https://vimeo.com/api/v2/video/23237102.json'
|
||||
},
|
||||
{
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: '[{"thumbnail_large": "https://i.vimeocdn.com/video/503631401_640.jpg"}]'
|
||||
}
|
||||
)
|
||||
cy.setCodemirrorContent('https://vimeo.com/23237102')
|
||||
cy.getMarkdownBody()
|
||||
.findByCypressId('click-shield-vimeo')
|
||||
.find('.preview-background')
|
||||
.should('have.attr', 'src', 'https://i.vimeocdn.com/video/503631401_640.jpg')
|
||||
.parent()
|
||||
.click()
|
||||
cy.getMarkdownBody().find('iframe').should('have.attr', 'src', 'https://player.vimeo.com/video/23237102?autoplay=1')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
describe('Short code gets replaced or rendered: ', () => {
|
||||
beforeEach(() => {
|
||||
cy.visitTestNote()
|
||||
})
|
||||
|
||||
describe('pdf', () => {
|
||||
it('renders a plain link', () => {
|
||||
cy.setCodemirrorContent(`{%pdf https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf %}`)
|
||||
cy.getMarkdownBody()
|
||||
.find('a')
|
||||
.should('have.attr', 'href', 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf')
|
||||
})
|
||||
})
|
||||
|
||||
describe('slideshare', () => {
|
||||
it('renders a plain link', () => {
|
||||
cy.setCodemirrorContent(`{%slideshare example/123456789 %}`)
|
||||
cy.getMarkdownBody().find('a').should('have.attr', 'href', 'https://www.slideshare.net/example/123456789')
|
||||
})
|
||||
})
|
||||
|
||||
describe('speakerdeck', () => {
|
||||
it('renders a plain link', () => {
|
||||
cy.setCodemirrorContent(`{%speakerdeck example/123456789 %}`)
|
||||
cy.getMarkdownBody().find('a').should('have.attr', 'href', 'https://speakerdeck.com/example/123456789')
|
||||
})
|
||||
})
|
||||
|
||||
describe('youtube', () => {
|
||||
it('renders click-shield', () => {
|
||||
cy.setCodemirrorContent(`{%youtube YE7VzlLtp-4 %}`)
|
||||
cy.getMarkdownBody().findByCypressId('click-shield-youtube')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,9 +1,4 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
exports[`AbcFrame renders a music sheet 1`] = `
|
||||
<div>
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AbcJs Markdown Extension renders an abc codeblock 1`] = `
|
||||
<div>
|
||||
<pre>
|
||||
<span>
|
||||
this is a mock for abc js frame
|
||||
<code>
|
||||
X:1\\nT:Speed the Plough\\nM:4/4\\nC:Trad.\\nK:G\\n|:GABc dedB|dedB dedB|c2ec B2dB|c2A2 A2BA|\\nGABc dedB|dedB dedB|c2ec B2dB|A2F2 G4:|\\n|:g2gf gdBd|g2f2 e2d2|c2ec B2dB|c2A2 A2df|\\ng2gf g2Bd|g2f2 e2d2|c2ec B2dB|A2F2 G4:|
|
||||
|
||||
</code>
|
||||
</span>
|
||||
</pre>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
|
@ -28,7 +28,7 @@ export const AbcFrame: React.FC<CodeProps> = ({ code }) => {
|
|||
value: abcLib
|
||||
} = useAsync(async () => {
|
||||
try {
|
||||
return await import(/* webpackChunkName: "abc.js" */ 'abcjs')
|
||||
return import(/* webpackChunkName: "abc.js" */ 'abcjs')
|
||||
} catch (error) {
|
||||
log.error('Error while loading abcjs', error)
|
||||
throw error
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import { TestMarkdownRenderer } from '../../test-utils/test-markdown-renderer'
|
||||
import * as AbcFrameModule from './abc-frame'
|
||||
import type { CodeProps } from '../../replace-components/code-block-component-replacer'
|
||||
import { AbcjsMarkdownExtension } from './abcjs-markdown-extension'
|
||||
import { mockI18n } from '../../test-utils/mock-i18n'
|
||||
|
||||
describe('AbcJs Markdown Extension', () => {
|
||||
beforeAll(async () => {
|
||||
jest.spyOn(AbcFrameModule, 'AbcFrame').mockImplementation((({ code }) => {
|
||||
return (
|
||||
<span>
|
||||
this is a mock for abc js frame
|
||||
<code>{code}</code>
|
||||
</span>
|
||||
)
|
||||
}) as React.FC<CodeProps>)
|
||||
await mockI18n()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetModules()
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('renders an abc codeblock', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new AbcjsMarkdownExtension()]}
|
||||
content={
|
||||
'```abc\nX:1\\nT:Speed the Plough\\nM:4/4\\nC:Trad.\\nK:G\\n|:GABc dedB|dedB dedB|c2ec B2dB|c2A2 A2BA|\\nGABc dedB|dedB dedB|c2ec B2dB|A2F2 G4:|\\n|:g2gf gdBd|g2f2 e2d2|c2ec B2dB|c2A2 A2df|\\ng2gf g2Bd|g2f2 e2d2|c2ec B2dB|A2F2 G4:|\n```'
|
||||
}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -8,10 +8,10 @@ import { MarkdownExtension } from '../markdown-extension'
|
|||
import { BlockquoteColorExtraTagReplacer } from './blockquote-color-extra-tag-replacer'
|
||||
import { BlockquoteExtraTagReplacer } from './blockquote-extra-tag-replacer'
|
||||
import type { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||
import { BlockquoteExtraTagMarkdownItPlugin } from './blockquote-extra-tag-markdown-it-plugin'
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import type { NodeProcessor } from '../../node-preprocessors/node-processor'
|
||||
import { BlockquoteBorderColorNodePreprocessor } from './blockquote-border-color-node-preprocessor'
|
||||
import { BlockquoteExtraTagMarkdownItPlugin } from './blockquote-extra-tag-markdown-it-plugin'
|
||||
|
||||
/**
|
||||
* Adds support for generic blockquote extra tags and blockquote color extra tags.
|
||||
|
@ -20,10 +20,9 @@ export class BlockquoteExtraTagMarkdownExtension extends MarkdownExtension {
|
|||
public static readonly tagName = 'app-blockquote-tag'
|
||||
|
||||
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
||||
new BlockquoteExtraTagMarkdownItPlugin('color', 'tag').registerInlineRule(markdownIt)
|
||||
new BlockquoteExtraTagMarkdownItPlugin('name', 'user').registerInlineRule(markdownIt)
|
||||
new BlockquoteExtraTagMarkdownItPlugin('time', 'clock-o').registerInlineRule(markdownIt)
|
||||
BlockquoteExtraTagMarkdownItPlugin.registerRenderer(markdownIt)
|
||||
new BlockquoteExtraTagMarkdownItPlugin('color', 'tag').registerRule(markdownIt)
|
||||
new BlockquoteExtraTagMarkdownItPlugin('name', 'user').registerRule(markdownIt)
|
||||
new BlockquoteExtraTagMarkdownItPlugin('time', 'clock-o').registerRule(markdownIt)
|
||||
}
|
||||
|
||||
public buildReplacers(): ComponentReplacer[] {
|
||||
|
|
|
@ -4,59 +4,49 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { QuoteExtraTagValues } from './parse-blockquote-extra-tag'
|
||||
import { parseBlockquoteExtraTag } from './parse-blockquote-extra-tag'
|
||||
import { BlockquoteExtraTagMarkdownItPlugin } from './blockquote-extra-tag-markdown-it-plugin'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
|
||||
describe('Quote extra syntax parser', () => {
|
||||
let markdownIt: MarkdownIt
|
||||
|
||||
beforeEach(() => {
|
||||
markdownIt = new MarkdownIt('default', {
|
||||
html: false,
|
||||
breaks: true,
|
||||
langPrefix: '',
|
||||
typographer: true
|
||||
})
|
||||
new BlockquoteExtraTagMarkdownItPlugin('abc', 'markdown').registerRule(markdownIt)
|
||||
})
|
||||
|
||||
it('should parse a valid tag', () => {
|
||||
const expected: QuoteExtraTagValues = {
|
||||
labelStartIndex: 1,
|
||||
labelEndIndex: 4,
|
||||
valueStartIndex: 5,
|
||||
valueEndIndex: 8,
|
||||
label: 'abc',
|
||||
value: 'def'
|
||||
}
|
||||
expect(parseBlockquoteExtraTag('[abc=def]', 0, 1000)).toEqual(expected)
|
||||
expect(markdownIt.renderInline('[abc=markdown]')).toEqual(
|
||||
'<app-blockquote-tag data-label=\'abc\' data-icon="markdown">markdown</app-blockquote-tag>'
|
||||
)
|
||||
})
|
||||
|
||||
it("shouldn't parse a tag with no opener bracket", () => {
|
||||
expect(parseBlockquoteExtraTag('abc=def]', 0, 1000)).toEqual(undefined)
|
||||
expect(markdownIt.renderInline('abc=def]')).toEqual('abc=def]')
|
||||
})
|
||||
|
||||
it("shouldn't parse a tag with no closing bracket", () => {
|
||||
expect(parseBlockquoteExtraTag('[abc=def', 0, 1000)).toEqual(undefined)
|
||||
expect(markdownIt.renderInline('[abc=def')).toEqual('[abc=def')
|
||||
})
|
||||
|
||||
it("shouldn't parse a tag with no separation character", () => {
|
||||
expect(parseBlockquoteExtraTag('[abcdef]', 0, 1000)).toEqual(undefined)
|
||||
expect(markdownIt.renderInline('[abcdef]')).toEqual('[abcdef]')
|
||||
})
|
||||
|
||||
it("shouldn't parse a tag with an empty label", () => {
|
||||
expect(parseBlockquoteExtraTag('[=def]', 0, 1000)).toEqual(undefined)
|
||||
expect(markdownIt.renderInline('[=def]')).toEqual('[=def]')
|
||||
})
|
||||
|
||||
it("shouldn't parse a tag with an empty value", () => {
|
||||
expect(parseBlockquoteExtraTag('[abc=]', 0, 1000)).toEqual(undefined)
|
||||
expect(markdownIt.renderInline('[abc=]')).toEqual('[abc=]')
|
||||
})
|
||||
|
||||
it("shouldn't parse a tag with an empty body", () => {
|
||||
expect(parseBlockquoteExtraTag('[]', 0, 1000)).toEqual(undefined)
|
||||
})
|
||||
|
||||
it("shouldn't parse a tag with an empty body", () => {
|
||||
expect(parseBlockquoteExtraTag('[]', 0, 1000)).toEqual(undefined)
|
||||
})
|
||||
|
||||
it("shouldn't parse a correct tag if start index isn't at the opening bracket", () => {
|
||||
expect(parseBlockquoteExtraTag('[abc=def]', 1, 1000)).toEqual(undefined)
|
||||
})
|
||||
|
||||
it("shouldn't parse a correct tag if maxPos ends before tag end", () => {
|
||||
expect(parseBlockquoteExtraTag('[abc=def]', 0, 1)).toEqual(undefined)
|
||||
})
|
||||
|
||||
it("shouldn't parse a correct tag if start index is after maxPos", () => {
|
||||
expect(parseBlockquoteExtraTag(' [abc=def]', 3, 2)).toEqual(undefined)
|
||||
expect(markdownIt.renderInline('[]')).toEqual('[]')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -6,12 +6,11 @@
|
|||
|
||||
import type MarkdownIt from 'markdown-it/lib'
|
||||
import type Token from 'markdown-it/lib/token'
|
||||
import type { QuoteExtraTagValues } from './parse-blockquote-extra-tag'
|
||||
import { parseBlockquoteExtraTag } from './parse-blockquote-extra-tag'
|
||||
import type { IconName } from '../../../common/fork-awesome/types'
|
||||
import Optional from 'optional-js'
|
||||
import type StateInline from 'markdown-it/lib/rules_inline/state_inline'
|
||||
import { BlockquoteExtraTagMarkdownExtension } from './blockquote-extra-tag-markdown-extension'
|
||||
import type { RuleInline } from 'markdown-it/lib/parser_inline'
|
||||
|
||||
export interface BlockquoteTagOptions {
|
||||
parseSubTags?: boolean
|
||||
|
@ -19,60 +18,181 @@ export interface BlockquoteTagOptions {
|
|||
icon?: IconName
|
||||
}
|
||||
|
||||
export interface QuoteExtraTagValues {
|
||||
labelStartIndex: number
|
||||
labelEndIndex: number
|
||||
valueStartIndex: number
|
||||
valueEndIndex: number
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the blockquote extra tag syntax `[label=value]` and creates <blockquote-tag> elements.
|
||||
*/
|
||||
export class BlockquoteExtraTagMarkdownItPlugin {
|
||||
private static readonly BlockquoteExtraTagRuleName = 'blockquote_extra_tag'
|
||||
|
||||
constructor(private tagName: string, private icon: IconName) {}
|
||||
|
||||
public static registerRenderer(markdownIt: MarkdownIt): void {
|
||||
if (markdownIt.renderer.rules['blockquote_tag']) {
|
||||
/**
|
||||
* Registers an inline rule that detects blockquote extra tags and replaces them with blockquote tokens.
|
||||
*
|
||||
* @param markdownIt The {@link MarkdownIt markdown-it} in which the inline rule should be registered.
|
||||
*/
|
||||
public registerRule(markdownIt: MarkdownIt): void {
|
||||
markdownIt.inline.ruler.before('link', `blockquote_extra_tag_${this.tagName}`, this.createInlineRuler())
|
||||
BlockquoteExtraTagMarkdownItPlugin.registerRenderer(markdownIt)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link RuleInline markdown-it inline rule} that detects the configured blockquote extra tag.
|
||||
*
|
||||
* @return The created inline rule
|
||||
*/
|
||||
private createInlineRuler(): RuleInline {
|
||||
return (state) =>
|
||||
this.parseBlockquoteExtraTag(state.src, state.pos, state.posMax)
|
||||
.map((parseResults) => {
|
||||
this.createNewBlockquoteExtraTagToken(state, parseResults.label, parseResults.value)
|
||||
state.pos = parseResults.valueEndIndex + 1
|
||||
return true
|
||||
})
|
||||
.orElse(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the markdown-it renderer that translated `blockquote_tag` tokens into HTML
|
||||
*
|
||||
* @param markdownIt The {@link MarkdownIt markdown-it} in which the render should be registered.
|
||||
*/
|
||||
private static registerRenderer(markdownIt: MarkdownIt): void {
|
||||
if (markdownIt.renderer.rules[BlockquoteExtraTagMarkdownItPlugin.BlockquoteExtraTagRuleName]) {
|
||||
return
|
||||
}
|
||||
markdownIt.renderer.rules['blockquote_tag'] = (tokens, idx, options: MarkdownIt.Options, env: unknown) => {
|
||||
markdownIt.renderer.rules[BlockquoteExtraTagMarkdownItPlugin.BlockquoteExtraTagRuleName] = (
|
||||
tokens,
|
||||
idx,
|
||||
options: MarkdownIt.Options,
|
||||
env: unknown
|
||||
) => {
|
||||
const token = tokens[idx]
|
||||
const innerTokens = token.children
|
||||
const label = token.attrGet('label') ?? ''
|
||||
const icon = token.attrGet('icon')
|
||||
|
||||
const iconAttribute = icon === null ? '' : ` data-icon="${icon}"`
|
||||
|
||||
const innerHtml = innerTokens === null ? '' : markdownIt.renderer.renderInline(innerTokens, options, env)
|
||||
return `<${BlockquoteExtraTagMarkdownExtension.tagName} data-label='${label}'${iconAttribute}>${innerHtml}</${BlockquoteExtraTagMarkdownExtension.tagName}>`
|
||||
}
|
||||
}
|
||||
|
||||
public registerInlineRule(markdownIt: MarkdownIt): void {
|
||||
markdownIt.inline.ruler.before('link', `blockquote_${this.tagName}`, (state) =>
|
||||
this.parseSpecificBlockquoteTag(state)
|
||||
.map((parseResults) => {
|
||||
const token = this.createBlockquoteTagToken(state, parseResults)
|
||||
this.processTagValue(token, state, parseResults)
|
||||
return true
|
||||
})
|
||||
.orElse(false)
|
||||
)
|
||||
}
|
||||
|
||||
private parseSpecificBlockquoteTag(state: StateInline): Optional<QuoteExtraTagValues> {
|
||||
return Optional.ofNullable(parseBlockquoteExtraTag(state.src, state.pos, state.posMax))
|
||||
.filter((results) => results.label === this.tagName)
|
||||
.map((parseResults) => {
|
||||
state.pos = parseResults.valueEndIndex + 1
|
||||
return parseResults
|
||||
})
|
||||
}
|
||||
|
||||
private createBlockquoteTagToken(state: StateInline, parseResults: QuoteExtraTagValues): Token {
|
||||
const token = state.push('blockquote_tag', '', 0)
|
||||
token.attrSet('label', parseResults.label)
|
||||
/**
|
||||
* Creates a new blockquote extra {@link Token token} using the given values.
|
||||
*
|
||||
* @param state The state in which the token should be inserted
|
||||
* @param label The label for the extra token
|
||||
* @param value The value for the extra token that will be inline parsed
|
||||
* @return The generated token
|
||||
*/
|
||||
private createNewBlockquoteExtraTagToken(state: StateInline, label: string, value: string): Token {
|
||||
const token = state.push(BlockquoteExtraTagMarkdownItPlugin.BlockquoteExtraTagRuleName, '', 0)
|
||||
token.attrSet('label', label)
|
||||
token.attrSet('icon', this.icon)
|
||||
token.children = BlockquoteExtraTagMarkdownItPlugin.parseInlineContent(state, value)
|
||||
return token
|
||||
}
|
||||
|
||||
protected processTagValue(token: Token, state: StateInline, parseResults: QuoteExtraTagValues): void {
|
||||
/**
|
||||
* Parses the given content using the markdown-it instance of the given state.
|
||||
*
|
||||
* @param state The state whose inline parser should be used to parse the given content
|
||||
* @param content The content to parse
|
||||
* @return The generated tokens
|
||||
*/
|
||||
private static parseInlineContent(state: StateInline, content: string): Token[] {
|
||||
const childTokens: Token[] = []
|
||||
state.md.inline.parse(parseResults.value, state.md, state.env, childTokens)
|
||||
token.children = childTokens
|
||||
state.md.inline.parse(content, state.md, state.env, childTokens)
|
||||
return childTokens
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a blockquote tag. The syntax is [label=value].
|
||||
*
|
||||
* @param line The line in which the tag should be looked for.
|
||||
* @param startIndex The start index for the search.
|
||||
* @param dontSearchAfterIndex The maximal position for the search.
|
||||
*/
|
||||
private parseBlockquoteExtraTag(
|
||||
line: string,
|
||||
startIndex: number,
|
||||
dontSearchAfterIndex: number
|
||||
): Optional<QuoteExtraTagValues> {
|
||||
if (line[startIndex] !== '[') {
|
||||
return Optional.empty()
|
||||
}
|
||||
|
||||
const labelStartIndex = startIndex + 1
|
||||
const labelEndIndex = BlockquoteExtraTagMarkdownItPlugin.parseLabel(line, labelStartIndex, dontSearchAfterIndex)
|
||||
if (!labelEndIndex || labelStartIndex === labelEndIndex) {
|
||||
return Optional.empty()
|
||||
}
|
||||
|
||||
const label = line.slice(labelStartIndex, labelEndIndex)
|
||||
if (label !== this.tagName) {
|
||||
return Optional.empty()
|
||||
}
|
||||
|
||||
const valueStartIndex = labelEndIndex + 1
|
||||
const valueEndIndex = BlockquoteExtraTagMarkdownItPlugin.parseValue(line, valueStartIndex, dontSearchAfterIndex)
|
||||
if (!valueEndIndex || valueStartIndex === valueEndIndex) {
|
||||
return Optional.empty()
|
||||
}
|
||||
const value = line.slice(valueStartIndex, valueEndIndex)
|
||||
|
||||
return Optional.of({
|
||||
labelStartIndex,
|
||||
labelEndIndex,
|
||||
valueStartIndex,
|
||||
valueEndIndex,
|
||||
label,
|
||||
value
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the value part of a blockquote tag. That is [notthis=THIS] part. It also detects nested [] blocks.
|
||||
*
|
||||
* @param line The line in which the tag is.
|
||||
* @param startIndex The start index of the tag.
|
||||
* @param dontSearchAfterIndex The maximal position for the search.
|
||||
*/
|
||||
private static parseValue(line: string, startIndex: number, dontSearchAfterIndex: number): number | undefined {
|
||||
let level = 0
|
||||
for (let position = startIndex; position <= dontSearchAfterIndex; position += 1) {
|
||||
const currentCharacter = line[position]
|
||||
if (currentCharacter === ']') {
|
||||
if (level === 0) {
|
||||
return position
|
||||
}
|
||||
level -= 1
|
||||
} else if (currentCharacter === '[') {
|
||||
level += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the label part of a blockquote tag. That is [THIS=notthis] part.
|
||||
*
|
||||
* @param line The line in which the tag is.
|
||||
* @param startIndex The start index of the tag.
|
||||
* @param dontSearchAfterIndex The maximal position for the search.
|
||||
*/
|
||||
private static parseLabel(line: string, startIndex: number, dontSearchAfterIndex: number): number | undefined {
|
||||
for (let pos = startIndex; pos <= dontSearchAfterIndex; pos += 1) {
|
||||
if (line[pos] === '=') {
|
||||
return pos
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export interface QuoteExtraTagValues {
|
||||
labelStartIndex: number
|
||||
labelEndIndex: number
|
||||
valueStartIndex: number
|
||||
valueEndIndex: number
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a blockquote tag. The syntax is [label=value].
|
||||
*
|
||||
* @param line The line in which the tag should be looked for.
|
||||
* @param startIndex The start index for the search.
|
||||
* @param dontSearchAfterIndex The maximal position for the search.
|
||||
*/
|
||||
export const parseBlockquoteExtraTag = (
|
||||
line: string,
|
||||
startIndex: number,
|
||||
dontSearchAfterIndex: number
|
||||
): QuoteExtraTagValues | undefined => {
|
||||
if (line[startIndex] !== '[') {
|
||||
return
|
||||
}
|
||||
|
||||
const labelStartIndex = startIndex + 1
|
||||
const labelEndIndex = parseLabel(line, labelStartIndex, dontSearchAfterIndex)
|
||||
if (!labelEndIndex || labelStartIndex === labelEndIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
const valueStartIndex = labelEndIndex + 1
|
||||
const valueEndIndex = parseValue(line, valueStartIndex, dontSearchAfterIndex)
|
||||
if (!valueEndIndex || valueStartIndex === valueEndIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
labelStartIndex,
|
||||
labelEndIndex,
|
||||
valueStartIndex,
|
||||
valueEndIndex,
|
||||
label: line.slice(labelStartIndex, labelEndIndex),
|
||||
value: line.slice(valueStartIndex, valueEndIndex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the value part of a blockquote tag. That is [notthis=THIS] part. It also detects nested [] blocks.
|
||||
*
|
||||
* @param line The line in which the tag is.
|
||||
* @param startIndex The start index of the tag.
|
||||
* @param dontSearchAfterIndex The maximal position for the search.
|
||||
*/
|
||||
const parseValue = (line: string, startIndex: number, dontSearchAfterIndex: number): number | undefined => {
|
||||
let level = 0
|
||||
for (let position = startIndex; position <= dontSearchAfterIndex; position += 1) {
|
||||
const currentCharacter = line[position]
|
||||
if (currentCharacter === ']') {
|
||||
if (level === 0) {
|
||||
return position
|
||||
}
|
||||
level -= 1
|
||||
} else if (currentCharacter === '[') {
|
||||
level += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the label part of a blockquote tag. That is [THIS=notthis] part.
|
||||
*
|
||||
* @param line The line in which the tag is.
|
||||
* @param startIndex The start index of the tag.
|
||||
* @param dontSearchAfterIndex The maximal position for the search.
|
||||
*/
|
||||
const parseLabel = (line: string, startIndex: number, dontSearchAfterIndex: number): number | undefined => {
|
||||
for (let pos = startIndex; pos <= dontSearchAfterIndex; pos += 1) {
|
||||
if (line[pos] === '=') {
|
||||
return pos
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CSV Table Markdown Extension renders a csv codeblock 1`] = `
|
||||
<div>
|
||||
<pre>
|
||||
<code
|
||||
class="csv"
|
||||
>
|
||||
a;b;c
|
||||
d;e;f
|
||||
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,79 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CSV Table renders correctly with header 1`] = `
|
||||
<div>
|
||||
<table
|
||||
class="csv-html-table table-striped"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
a
|
||||
</th>
|
||||
<th>
|
||||
b
|
||||
</th>
|
||||
<th>
|
||||
c
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
d
|
||||
</td>
|
||||
<td>
|
||||
e
|
||||
</td>
|
||||
<td>
|
||||
f
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CSV Table renders correctly without code 1`] = `
|
||||
<div>
|
||||
<table
|
||||
class="csv-html-table table-striped"
|
||||
>
|
||||
<tbody />
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CSV Table renders correctly without header 1`] = `
|
||||
<div>
|
||||
<table
|
||||
class="csv-html-table table-striped"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
a
|
||||
</td>
|
||||
<td>
|
||||
b
|
||||
</td>
|
||||
<td>
|
||||
c
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
d
|
||||
</td>
|
||||
<td>
|
||||
e
|
||||
</td>
|
||||
<td>
|
||||
f
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
|
@ -21,7 +21,7 @@ export const parseCsv = (csvText: string, csvColumnDelimiter: string): string[][
|
|||
}
|
||||
|
||||
/**
|
||||
* Escapes regex characters in the given string so it can be used as literal string in another regex.
|
||||
* Escapes regex characters in the given string, so it can be used as literal string in another regex.
|
||||
* @param unsafe The unescaped string
|
||||
* @return The escaped string
|
||||
*/
|
||||
|
|
|
@ -24,13 +24,8 @@ export class CsvReplacer extends ComponentReplacer {
|
|||
const extraRegex = /\s*(delimiter=([^\s]*))?\s*(header)?/
|
||||
const extraInfos = extraRegex.exec(extraData)
|
||||
|
||||
let delimiter = ','
|
||||
let showHeader = false
|
||||
|
||||
if (extraInfos) {
|
||||
delimiter = extraInfos[2] || delimiter
|
||||
showHeader = extraInfos[3] !== undefined
|
||||
}
|
||||
const delimiter = extraInfos?.[2] ?? ','
|
||||
const showHeader = extraInfos?.[3] !== undefined
|
||||
|
||||
return <CsvTable code={code} delimiter={delimiter} showHeader={showHeader} />
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as CsvTableModule from '../csv/csv-table'
|
||||
import React from 'react'
|
||||
import type { CodeProps } from '../../replace-components/code-block-component-replacer'
|
||||
import { mockI18n } from '../../test-utils/mock-i18n'
|
||||
import { render } from '@testing-library/react'
|
||||
import { TestMarkdownRenderer } from '../../test-utils/test-markdown-renderer'
|
||||
import { CsvTableMarkdownExtension } from './csv-table-markdown-extension'
|
||||
|
||||
describe('CSV Table Markdown Extension', () => {
|
||||
beforeAll(async () => {
|
||||
jest.spyOn(CsvTableModule, 'CsvTable').mockImplementation((({ code }) => {
|
||||
return (
|
||||
<span>
|
||||
this is a mock for csv frame
|
||||
<code>{code}</code>
|
||||
</span>
|
||||
)
|
||||
}) as React.FC<CodeProps>)
|
||||
await mockI18n()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetModules()
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('renders a csv codeblock', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer extensions={[new CsvTableMarkdownExtension()]} content={'```csv\na;b;c\nd;e;f\n```'} />
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react'
|
||||
import { CsvTable } from './csv-table'
|
||||
|
||||
describe('CSV Table', () => {
|
||||
it('renders correctly with header', () => {
|
||||
const view = render(<CsvTable code={'a;b;c\nd;e;f'} delimiter={';'} showHeader={true} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders correctly without header', () => {
|
||||
const view = render(<CsvTable code={'a;b;c\nd;e;f'} delimiter={';'} showHeader={false} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders correctly without code', () => {
|
||||
const view = render(<CsvTable code={''} delimiter={';'} showHeader={false} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -25,16 +25,12 @@ export const CsvTable: React.FC<CsvTableProps> = ({
|
|||
}) => {
|
||||
const { rowsWithColumns, headerRow } = useMemo(() => {
|
||||
const rowsWithColumns = parseCsv(code.trim(), delimiter)
|
||||
let headerRow: string[] = []
|
||||
if (showHeader) {
|
||||
headerRow = rowsWithColumns.splice(0, 1)[0]
|
||||
}
|
||||
const headerRow = showHeader ? rowsWithColumns.splice(0, 1)[0] : []
|
||||
return { rowsWithColumns, headerRow }
|
||||
}, [code, delimiter, showHeader])
|
||||
|
||||
const renderTableHeader = useMemo(
|
||||
() =>
|
||||
headerRow === [] ? undefined : (
|
||||
const renderTableHeader = useMemo(() => {
|
||||
return headerRow.length === 0 ? undefined : (
|
||||
<thead>
|
||||
<tr>
|
||||
{headerRow.map((column, columnNumber) => (
|
||||
|
@ -42,9 +38,8 @@ export const CsvTable: React.FC<CsvTableProps> = ({
|
|||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
),
|
||||
[headerRow]
|
||||
)
|
||||
}, [headerRow])
|
||||
|
||||
const renderTableBody = useMemo(
|
||||
() => (
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Emoji Markdown Extension renders a fork awesome code 1`] = `
|
||||
<div>
|
||||
<p>
|
||||
<i
|
||||
class="fa fa-circle-thin"
|
||||
/>
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Emoji Markdown Extension renders a skin tone code 1`] = `
|
||||
<div>
|
||||
<p>
|
||||
🏽
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Emoji Markdown Extension renders an emoji code 1`] = `
|
||||
<div>
|
||||
<p>
|
||||
😄
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { mockI18n } from '../../test-utils/mock-i18n'
|
||||
import { render } from '@testing-library/react'
|
||||
import { TestMarkdownRenderer } from '../../test-utils/test-markdown-renderer'
|
||||
import { EmojiMarkdownExtension } from './emoji-markdown-extension'
|
||||
|
||||
describe('Emoji Markdown Extension', () => {
|
||||
beforeAll(async () => {
|
||||
await mockI18n()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetModules()
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('renders an emoji code', () => {
|
||||
const view = render(<TestMarkdownRenderer extensions={[new EmojiMarkdownExtension()]} content={':smile:'} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a fork awesome code', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer extensions={[new EmojiMarkdownExtension()]} content={':fa-circle-thin:'} />
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a skin tone code', () => {
|
||||
const view = render(<TestMarkdownRenderer extensions={[new EmojiMarkdownExtension()]} content={':skin-tone-3:'} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,19 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Flowchart markdown extensions renders a flowchart codeblock 1`] = `
|
||||
<div>
|
||||
<pre>
|
||||
<span>
|
||||
this is a mock for flowchart frame
|
||||
<code>
|
||||
st=>start: Start
|
||||
e=>end: End
|
||||
st->e
|
||||
|
||||
</code>
|
||||
</span>
|
||||
</pre>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,38 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Flowchart handles error if lib loading failed 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="fade alert alert-danger show"
|
||||
role="alert"
|
||||
>
|
||||
common.errorWhileLoading
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Flowchart handles error while rendering 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="fade alert alert-danger show"
|
||||
role="alert"
|
||||
>
|
||||
renderer.flowchart.invalidSyntax
|
||||
</div>
|
||||
<div
|
||||
class="text-center"
|
||||
data-testid="flowchart"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Flowchart renders correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="text-center"
|
||||
data-testid="flowchart"
|
||||
>
|
||||
Flowchart rendering succeeded!
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react'
|
||||
import { TestMarkdownRenderer } from '../../test-utils/test-markdown-renderer'
|
||||
import React from 'react'
|
||||
import { mockI18n } from '../../test-utils/mock-i18n'
|
||||
import { FlowchartMarkdownExtension } from './flowchart-markdown-extension'
|
||||
import * as Flowchart from '../flowchart/flowchart'
|
||||
import type { CodeProps } from '../../replace-components/code-block-component-replacer'
|
||||
|
||||
describe('Flowchart markdown extensions', () => {
|
||||
beforeAll(async () => {
|
||||
jest.spyOn(Flowchart, 'FlowChart').mockImplementation((({ code }) => {
|
||||
return (
|
||||
<span>
|
||||
this is a mock for flowchart frame
|
||||
<code>{code}</code>
|
||||
</span>
|
||||
)
|
||||
}) as React.FC<CodeProps>)
|
||||
await mockI18n()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetModules()
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('renders a flowchart codeblock', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new FlowchartMarkdownExtension()]}
|
||||
content={'```flow\nst=>start: Start\ne=>end: End\nst->e\n```'}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { mockI18n } from '../../test-utils/mock-i18n'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { FlowChart } from './flowchart'
|
||||
import type * as flowchartJsModule from 'flowchart.js'
|
||||
import { StoreProvider } from '../../../../redux/store-provider'
|
||||
|
||||
describe('Flowchart', () => {
|
||||
const successText = 'Flowchart rendering succeeded!'
|
||||
const expectedValidFlowchartCode = 'test code'
|
||||
|
||||
beforeAll(async () => {
|
||||
await mockI18n()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
const mockFlowchartDraw = (): jest.Mock<void, Parameters<flowchartJsModule.Instance['drawSVG']>> => {
|
||||
const drawSvg = jest.fn((container: HTMLElement | string) => {
|
||||
if (typeof container === 'string') {
|
||||
throw new Error('HTMLElement expected')
|
||||
} else {
|
||||
container.innerHTML = successText
|
||||
}
|
||||
})
|
||||
jest.mock('flowchart.js', () => ({
|
||||
parse: jest.fn((code) => {
|
||||
if (code !== expectedValidFlowchartCode) {
|
||||
throw new Error('invalid flowchart code')
|
||||
}
|
||||
return { drawSVG: drawSvg, clean: jest.fn() }
|
||||
})
|
||||
}))
|
||||
return drawSvg
|
||||
}
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const successText = 'Flowchart rendering succeeded!'
|
||||
const validFlowchartCode = 'test code'
|
||||
const mockDrawSvg = mockFlowchartDraw()
|
||||
|
||||
const view = render(
|
||||
<StoreProvider>
|
||||
<FlowChart code={validFlowchartCode} />
|
||||
</StoreProvider>
|
||||
)
|
||||
await screen.findByText(successText)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
expect(mockDrawSvg).toBeCalled()
|
||||
})
|
||||
|
||||
it('handles error while rendering', async () => {
|
||||
const mockDrawSvg = mockFlowchartDraw()
|
||||
|
||||
const view = render(
|
||||
<StoreProvider>
|
||||
<FlowChart code={'Invalid!'} />
|
||||
</StoreProvider>
|
||||
)
|
||||
await screen.findByText('renderer.flowchart.invalidSyntax')
|
||||
expect(view.container).toMatchSnapshot()
|
||||
expect(mockDrawSvg).not.toBeCalled()
|
||||
})
|
||||
|
||||
it('handles error if lib loading failed', async () => {
|
||||
jest.mock('flowchart.js', () => {
|
||||
throw new Error('flowchart.js import is exploded!')
|
||||
})
|
||||
|
||||
const view = render(
|
||||
<StoreProvider>
|
||||
<FlowChart code={'Invalid!'} />
|
||||
</StoreProvider>
|
||||
)
|
||||
await screen.findByText('common.errorWhileLoading')
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -9,31 +9,41 @@ import { Alert } from 'react-bootstrap'
|
|||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useIsDarkModeActivated } from '../../../../hooks/common/use-is-dark-mode-activated'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||
import fontStyles from '../../../../../global-styles/variables.module.scss'
|
||||
import { useAsync } from 'react-use'
|
||||
import { AsyncLoadingBoundary } from '../../../common/async-loading-boundary'
|
||||
import { ShowIf } from '../../../common/show-if/show-if'
|
||||
import type { CodeProps } from '../../replace-components/code-block-component-replacer'
|
||||
import { testId } from '../../../../utils/test-id'
|
||||
|
||||
const log = new Logger('FlowChart')
|
||||
|
||||
export interface FlowChartProps {
|
||||
code: string
|
||||
}
|
||||
|
||||
export const FlowChart: React.FC<FlowChartProps> = ({ code }) => {
|
||||
export const FlowChart: React.FC<CodeProps> = ({ code }) => {
|
||||
const diagramRef = useRef<HTMLDivElement>(null)
|
||||
const [error, setError] = useState(false)
|
||||
const [syntaxError, setSyntaxError] = useState(false)
|
||||
const darkModeActivated = useIsDarkModeActivated()
|
||||
|
||||
useTranslation()
|
||||
|
||||
const {
|
||||
value: flowchartLib,
|
||||
loading,
|
||||
error: libLoadingError
|
||||
} = useAsync(async () => import(/* webpackChunkName: "flowchart.js" */ 'flowchart.js'), [])
|
||||
|
||||
useEffect(() => {
|
||||
if (diagramRef.current === null) {
|
||||
if (libLoadingError) {
|
||||
log.error('Error while loading flowchart.js', libLoadingError)
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (diagramRef.current === null || flowchartLib === undefined) {
|
||||
return
|
||||
}
|
||||
const currentDiagramRef = diagramRef.current
|
||||
import(/* webpackChunkName: "flowchart.js" */ 'flowchart.js')
|
||||
.then((importedLibrary) => {
|
||||
const parserOutput = importedLibrary.parse(code)
|
||||
try {
|
||||
const parserOutput = flowchartLib.parse(code)
|
||||
parserOutput.drawSVG(currentDiagramRef, {
|
||||
'line-width': 2,
|
||||
fill: 'none',
|
||||
|
@ -43,25 +53,25 @@ export const FlowChart: React.FC<FlowChartProps> = ({ code }) => {
|
|||
'font-color': darkModeActivated ? '#ffffff' : '#000000',
|
||||
'font-family': fontStyles['font-family-base']
|
||||
})
|
||||
setError(false)
|
||||
setSyntaxError(false)
|
||||
} catch (error) {
|
||||
setError(true)
|
||||
log.error('Error while rendering flowchart', error)
|
||||
setSyntaxError(true)
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => log.error('Error while loading flowchart.js', error))
|
||||
|
||||
return () => {
|
||||
Array.from(currentDiagramRef.children).forEach((value) => value.remove())
|
||||
}
|
||||
}, [code, darkModeActivated])
|
||||
}, [code, darkModeActivated, flowchartLib])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<AsyncLoadingBoundary loading={loading} componentName={'flowchart.js'} error={!!libLoadingError}>
|
||||
<ShowIf condition={syntaxError}>
|
||||
<Alert variant={'danger'}>
|
||||
<Trans i18nKey={'renderer.flowchart.invalidSyntax'} />
|
||||
</Alert>
|
||||
</ShowIf>
|
||||
<div ref={diagramRef} {...testId('flowchart')} className={'text-center'} />
|
||||
</AsyncLoadingBoundary>
|
||||
)
|
||||
} else {
|
||||
return <div ref={diagramRef} {...cypressId('flowchart')} className={'text-center'} />
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PlantUML markdown extensions renders a plantuml codeblock 1`] = `
|
||||
<div>
|
||||
<pre>
|
||||
<span>
|
||||
this is a mock for graphviz frame
|
||||
<code>
|
||||
graph {
|
||||
a -- b
|
||||
}
|
||||
|
||||
</code>
|
||||
</span>
|
||||
</pre>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react'
|
||||
import { TestMarkdownRenderer } from '../../test-utils/test-markdown-renderer'
|
||||
import React from 'react'
|
||||
import { mockI18n } from '../../test-utils/mock-i18n'
|
||||
import { GraphvizMarkdownExtension } from './graphviz-markdown-extension'
|
||||
import * as GraphvizFrameModule from '../graphviz/graphviz-frame'
|
||||
import type { CodeProps } from '../../replace-components/code-block-component-replacer'
|
||||
|
||||
describe('PlantUML markdown extensions', () => {
|
||||
beforeAll(async () => {
|
||||
jest.spyOn(GraphvizFrameModule, 'GraphvizFrame').mockImplementation((({ code }) => {
|
||||
return (
|
||||
<span>
|
||||
this is a mock for graphviz frame
|
||||
<code>{code}</code>
|
||||
</span>
|
||||
)
|
||||
}) as React.FC<CodeProps>)
|
||||
await mockI18n()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetModules()
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('renders a plantuml codeblock', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new GraphvizMarkdownExtension()]}
|
||||
content={'```graphviz\ngraph {\na -- b\n}\n```'}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,271 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Highlighted code markdown extension renders with just the language and line wrapping doesn't show a gutter 1`] = `
|
||||
<div>
|
||||
<pre>
|
||||
<span>
|
||||
this is a mock for highlighted code
|
||||
<code>
|
||||
let x = 0
|
||||
|
||||
</code>
|
||||
<span>
|
||||
language:
|
||||
javascript
|
||||
</span>
|
||||
<span>
|
||||
start line number:
|
||||
</span>
|
||||
<span>
|
||||
wrap line:
|
||||
true
|
||||
</span>
|
||||
</span>
|
||||
</pre>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Highlighted code markdown extension renders with just the language doesn't show a gutter 1`] = `
|
||||
<div>
|
||||
<pre>
|
||||
<span>
|
||||
this is a mock for highlighted code
|
||||
<code>
|
||||
let x = 0
|
||||
|
||||
</code>
|
||||
<span>
|
||||
language:
|
||||
javascript
|
||||
</span>
|
||||
<span>
|
||||
start line number:
|
||||
</span>
|
||||
<span>
|
||||
wrap line:
|
||||
false
|
||||
</span>
|
||||
</span>
|
||||
</pre>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Highlighted code markdown extension renders with the language and show gutter and line wrapping shows the correct line number 1`] = `
|
||||
<div>
|
||||
<pre>
|
||||
<span>
|
||||
this is a mock for highlighted code
|
||||
<code>
|
||||
let x = 0
|
||||
|
||||
</code>
|
||||
<span>
|
||||
language:
|
||||
javascript
|
||||
</span>
|
||||
<span>
|
||||
start line number:
|
||||
1
|
||||
</span>
|
||||
<span>
|
||||
wrap line:
|
||||
true
|
||||
</span>
|
||||
</span>
|
||||
</pre>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Highlighted code markdown extension renders with the language and show gutter shows the correct line number 1`] = `
|
||||
<div>
|
||||
<pre>
|
||||
<span>
|
||||
this is a mock for highlighted code
|
||||
<code>
|
||||
let x = 0
|
||||
|
||||
</code>
|
||||
<span>
|
||||
language:
|
||||
javascript
|
||||
</span>
|
||||
<span>
|
||||
start line number:
|
||||
1
|
||||
</span>
|
||||
<span>
|
||||
wrap line:
|
||||
false
|
||||
</span>
|
||||
</span>
|
||||
</pre>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Highlighted code markdown extension renders with the language, show gutter with a start number and line wrapping shows the correct line number 1`] = `
|
||||
<div>
|
||||
<pre>
|
||||
<span>
|
||||
this is a mock for highlighted code
|
||||
<code>
|
||||
let x = 0
|
||||
|
||||
</code>
|
||||
<span>
|
||||
language:
|
||||
javascript
|
||||
</span>
|
||||
<span>
|
||||
start line number:
|
||||
100
|
||||
</span>
|
||||
<span>
|
||||
wrap line:
|
||||
true
|
||||
</span>
|
||||
</span>
|
||||
</pre>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Highlighted code markdown extension renders with the language, show gutter with a start number and line wrapping shows the correct line number and continues in another codeblock 1`] = `
|
||||
<div>
|
||||
<pre>
|
||||
<span>
|
||||
this is a mock for highlighted code
|
||||
<code>
|
||||
let x = 0
|
||||
let y = 1
|
||||
|
||||
</code>
|
||||
<span>
|
||||
language:
|
||||
javascript
|
||||
</span>
|
||||
<span>
|
||||
start line number:
|
||||
100
|
||||
</span>
|
||||
<span>
|
||||
wrap line:
|
||||
true
|
||||
</span>
|
||||
</span>
|
||||
</pre>
|
||||
|
||||
|
||||
<pre>
|
||||
<span>
|
||||
this is a mock for highlighted code
|
||||
<code>
|
||||
let y = 2
|
||||
|
||||
</code>
|
||||
<span>
|
||||
language:
|
||||
javascript
|
||||
</span>
|
||||
<span>
|
||||
start line number:
|
||||
102
|
||||
</span>
|
||||
<span>
|
||||
wrap line:
|
||||
false
|
||||
</span>
|
||||
</span>
|
||||
</pre>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Highlighted code markdown extension renders with the language, show gutter with a start number shows the correct line number 1`] = `
|
||||
<div>
|
||||
<pre>
|
||||
<span>
|
||||
this is a mock for highlighted code
|
||||
<code>
|
||||
let x = 0
|
||||
|
||||
</code>
|
||||
<span>
|
||||
language:
|
||||
javascript
|
||||
</span>
|
||||
<span>
|
||||
start line number:
|
||||
100
|
||||
</span>
|
||||
<span>
|
||||
wrap line:
|
||||
false
|
||||
</span>
|
||||
</span>
|
||||
</pre>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Highlighted code markdown extension renders with the language, show gutter with a start number shows the correct line number and continues in another codeblock 1`] = `
|
||||
<div>
|
||||
<pre>
|
||||
<span>
|
||||
this is a mock for highlighted code
|
||||
<code>
|
||||
let x = 0
|
||||
let y = 1
|
||||
|
||||
</code>
|
||||
<span>
|
||||
language:
|
||||
javascript
|
||||
</span>
|
||||
<span>
|
||||
start line number:
|
||||
100
|
||||
</span>
|
||||
<span>
|
||||
wrap line:
|
||||
false
|
||||
</span>
|
||||
</span>
|
||||
</pre>
|
||||
|
||||
|
||||
<pre>
|
||||
<span>
|
||||
this is a mock for highlighted code
|
||||
<code>
|
||||
let y = 2
|
||||
|
||||
</code>
|
||||
<span>
|
||||
language:
|
||||
javascript
|
||||
</span>
|
||||
<span>
|
||||
start line number:
|
||||
102
|
||||
</span>
|
||||
<span>
|
||||
wrap line:
|
||||
false
|
||||
</span>
|
||||
</span>
|
||||
</pre>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,133 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Highlighted Code can hide the line numbers 1`] = `
|
||||
<code
|
||||
class="hljs wrapLines"
|
||||
data-testid="code-highlighter"
|
||||
>
|
||||
<span
|
||||
class="linenumber"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
<div
|
||||
class="codeline"
|
||||
>
|
||||
<span>
|
||||
const a = 1
|
||||
</span>
|
||||
</div>
|
||||
</code>
|
||||
`;
|
||||
|
||||
exports[`Highlighted Code highlights code 1`] = `
|
||||
<code
|
||||
class="hljs showGutter "
|
||||
data-testid="code-highlighter"
|
||||
>
|
||||
<span
|
||||
class="linenumber"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
<div
|
||||
class="codeline"
|
||||
>
|
||||
<span
|
||||
class="hljs-keyword"
|
||||
>
|
||||
const
|
||||
</span>
|
||||
a =
|
||||
<span
|
||||
class="hljs-number"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
</div>
|
||||
</code>
|
||||
`;
|
||||
|
||||
exports[`Highlighted Code renders plain text 1`] = `
|
||||
<code
|
||||
class="hljs showGutter "
|
||||
data-testid="code-highlighter"
|
||||
>
|
||||
<span
|
||||
class="linenumber"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
<div
|
||||
class="codeline"
|
||||
>
|
||||
<span>
|
||||
a
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="linenumber"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
<div
|
||||
class="codeline"
|
||||
>
|
||||
<span>
|
||||
b
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="linenumber"
|
||||
>
|
||||
3
|
||||
</span>
|
||||
<div
|
||||
class="codeline"
|
||||
>
|
||||
<span>
|
||||
c
|
||||
</span>
|
||||
</div>
|
||||
</code>
|
||||
`;
|
||||
|
||||
exports[`Highlighted Code starts with a specific line 1`] = `
|
||||
<code
|
||||
class="hljs showGutter wrapLines"
|
||||
data-testid="code-highlighter"
|
||||
>
|
||||
<span
|
||||
class="linenumber"
|
||||
>
|
||||
100
|
||||
</span>
|
||||
<div
|
||||
class="codeline"
|
||||
>
|
||||
<span>
|
||||
const a = 1
|
||||
</span>
|
||||
</div>
|
||||
</code>
|
||||
`;
|
||||
|
||||
exports[`Highlighted Code wraps code 1`] = `
|
||||
<code
|
||||
class="hljs showGutter wrapLines"
|
||||
data-testid="code-highlighter"
|
||||
>
|
||||
<span
|
||||
class="linenumber"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
<div
|
||||
class="codeline"
|
||||
>
|
||||
<span>
|
||||
const a = 1
|
||||
</span>
|
||||
</div>
|
||||
</code>
|
||||
`;
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react'
|
||||
import { TestMarkdownRenderer } from '../../test-utils/test-markdown-renderer'
|
||||
import React from 'react'
|
||||
import type { HighlightedCodeProps } from './highlighted-code'
|
||||
import * as HighlightedCodeModule from './highlighted-code'
|
||||
import { mockI18n } from '../../test-utils/mock-i18n'
|
||||
import { HighlightedCodeMarkdownExtension } from './highlighted-code-markdown-extension'
|
||||
|
||||
describe('Highlighted code markdown extension', () => {
|
||||
describe('renders', () => {
|
||||
beforeAll(async () => {
|
||||
jest.spyOn(HighlightedCodeModule, 'HighlightedCode').mockImplementation((({
|
||||
code,
|
||||
language,
|
||||
startLineNumber,
|
||||
wrapLines
|
||||
}) => {
|
||||
return (
|
||||
<span>
|
||||
this is a mock for highlighted code
|
||||
<code>{code}</code>
|
||||
<span>language: {language}</span>
|
||||
<span>start line number: {startLineNumber}</span>
|
||||
<span>wrap line: {wrapLines ? 'true' : 'false'}</span>
|
||||
</span>
|
||||
)
|
||||
}) as React.FC<HighlightedCodeProps>)
|
||||
await mockI18n()
|
||||
})
|
||||
|
||||
describe('with just the language', () => {
|
||||
it("doesn't show a gutter", () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new HighlightedCodeMarkdownExtension()]}
|
||||
content={'```javascript \nlet x = 0\n```'}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
describe('and line wrapping', () => {
|
||||
it("doesn't show a gutter", () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new HighlightedCodeMarkdownExtension()]}
|
||||
content={'```javascript! \nlet x = 0\n```'}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with the language and show gutter', () => {
|
||||
it('shows the correct line number', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new HighlightedCodeMarkdownExtension()]}
|
||||
content={'```javascript= \nlet x = 0\n```'}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
describe('and line wrapping', () => {
|
||||
it('shows the correct line number', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new HighlightedCodeMarkdownExtension()]}
|
||||
content={'```javascript=! \nlet x = 0\n```'}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with the language, show gutter with a start number', () => {
|
||||
it('shows the correct line number', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new HighlightedCodeMarkdownExtension()]}
|
||||
content={'```javascript=100 \nlet x = 0\n```'}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('shows the correct line number and continues in another codeblock', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new HighlightedCodeMarkdownExtension()]}
|
||||
content={'```javascript=100 \nlet x = 0\nlet y = 1\n```\n\n```javascript=+\nlet y = 2\n```\n'}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
describe('and line wrapping', () => {
|
||||
it('shows the correct line number', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new HighlightedCodeMarkdownExtension()]}
|
||||
content={'```javascript=100! \nlet x = 0\n```'}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('shows the correct line number and continues in another codeblock', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new HighlightedCodeMarkdownExtension()]}
|
||||
content={'```javascript=100! \nlet x = 0\nlet y = 1\n```\n\n```javascript=+\nlet y = 2\n```\n'}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import HighlightedCode from './highlighted-code'
|
||||
import { mockI18n } from '../../test-utils/mock-i18n'
|
||||
|
||||
describe('Highlighted Code', () => {
|
||||
beforeAll(() => mockI18n())
|
||||
|
||||
it('renders plain text', async () => {
|
||||
render(<HighlightedCode code={'a\nb\nc'} startLineNumber={1} language={''} wrapLines={false}></HighlightedCode>)
|
||||
expect(await screen.findByTestId('code-highlighter')).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('highlights code', async () => {
|
||||
render(
|
||||
<HighlightedCode
|
||||
code={'const a = 1'}
|
||||
language={'typescript'}
|
||||
startLineNumber={1}
|
||||
wrapLines={false}></HighlightedCode>
|
||||
)
|
||||
expect(await screen.findByTestId('code-highlighter')).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('wraps code', async () => {
|
||||
render(<HighlightedCode code={'const a = 1'} wrapLines={true} startLineNumber={1} language={''}></HighlightedCode>)
|
||||
expect(await screen.findByTestId('code-highlighter')).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('starts with a specific line', async () => {
|
||||
render(
|
||||
<HighlightedCode code={'const a = 1'} startLineNumber={100} language={''} wrapLines={true}></HighlightedCode>
|
||||
)
|
||||
expect(await screen.findByTestId('code-highlighter')).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('can hide the line numbers', async () => {
|
||||
render(
|
||||
<HighlightedCode
|
||||
code={'const a = 1'}
|
||||
startLineNumber={undefined}
|
||||
language={''}
|
||||
wrapLines={true}></HighlightedCode>
|
||||
)
|
||||
expect(await screen.findByTestId('code-highlighter')).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -11,6 +11,7 @@ import { cypressAttribute, cypressId } from '../../../../utils/cypress-attribute
|
|||
import { AsyncLoadingBoundary } from '../../../common/async-loading-boundary'
|
||||
import { useAsyncHighlightedCodeDom } from './hooks/use-async-highlighted-code-dom'
|
||||
import { useAttachLineNumbers } from './hooks/use-attach-line-numbers'
|
||||
import { testId } from '../../../../utils/test-id'
|
||||
|
||||
export interface HighlightedCodeProps {
|
||||
code: string
|
||||
|
@ -36,6 +37,7 @@ export const HighlightedCode: React.FC<HighlightedCodeProps> = ({ code, language
|
|||
<AsyncLoadingBoundary loading={loading} error={!!error} componentName={'highlight.js'}>
|
||||
<div className={styles['code-highlighter']} {...cypressId('highlighted-code-block')}>
|
||||
<code
|
||||
{...testId('code-highlighter')}
|
||||
{...cypressId('code-highlighter')}
|
||||
{...cypressAttribute('showgutter', showGutter ? 'true' : 'false')}
|
||||
{...cypressAttribute('wraplines', wrapLines ? 'true' : 'false')}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Legacy shortcodes markdown extension renders correctly 1`] = `
|
||||
<div>
|
||||
<p>
|
||||
<a
|
||||
href="https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
|
||||
>
|
||||
https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf
|
||||
</a>
|
||||
<br />
|
||||
|
||||
|
||||
<a
|
||||
href="https://www.slideshare.net/example/123456789"
|
||||
>
|
||||
https://www.slideshare.net/example/123456789
|
||||
</a>
|
||||
<br />
|
||||
|
||||
|
||||
<a
|
||||
href="https://speakerdeck.com/example/123456789"
|
||||
>
|
||||
https://speakerdeck.com/example/123456789
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import * as replaceLegacyPdfShortCodeModule from './replace-legacy-pdf-short-code'
|
||||
import * as replaceLegacySlideshareShortCodeModule from './replace-legacy-slideshare-short-code'
|
||||
import * as replaceLegacySpeakerdeckShortCodeModule from './replace-legacy-speakerdeck-short-code'
|
||||
import { render } from '@testing-library/react'
|
||||
import { TestMarkdownRenderer } from '../../test-utils/test-markdown-renderer'
|
||||
import React from 'react'
|
||||
import { LegacyShortcodesMarkdownExtension } from './legacy-shortcodes-markdown-extension'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
|
||||
describe('Legacy shortcodes markdown extension', () => {
|
||||
const replaceLegacyPdfShortCodeMarkdownItExtensionMock = jest.spyOn(
|
||||
replaceLegacyPdfShortCodeModule,
|
||||
'legacyPdfShortCode'
|
||||
)
|
||||
const replaceLegacySlideshareShortCodeMarkdownItExtensionMock = jest.spyOn(
|
||||
replaceLegacySlideshareShortCodeModule,
|
||||
'legacySlideshareShortCode'
|
||||
)
|
||||
const replaceLegacySpeakerdeckShortCodeMarkdownItExtensionMock = jest.spyOn(
|
||||
replaceLegacySpeakerdeckShortCodeModule,
|
||||
'legacySpeakerdeckShortCode'
|
||||
)
|
||||
|
||||
it('renders correctly', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new LegacyShortcodesMarkdownExtension()]}
|
||||
content={
|
||||
'{%pdf https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf %}\n{%slideshare example/123456789 %}\n{%speakerdeck example/123456789 %}'
|
||||
}
|
||||
/>
|
||||
)
|
||||
expect(replaceLegacyPdfShortCodeMarkdownItExtensionMock).toHaveBeenCalledWith(expect.any(MarkdownIt))
|
||||
expect(replaceLegacySlideshareShortCodeMarkdownItExtensionMock).toHaveBeenCalledWith(expect.any(MarkdownIt))
|
||||
expect(replaceLegacySpeakerdeckShortCodeMarkdownItExtensionMock).toHaveBeenCalledWith(expect.any(MarkdownIt))
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { legacyPdfShortCode } from './replace-legacy-pdf-short-code'
|
||||
|
||||
describe('Legacy pdf short code', () => {
|
||||
it('replaces with link', () => {
|
||||
const markdownIt = new MarkdownIt('default', {
|
||||
html: false,
|
||||
breaks: true,
|
||||
langPrefix: '',
|
||||
typographer: true
|
||||
})
|
||||
markdownIt.use(legacyPdfShortCode)
|
||||
expect(
|
||||
markdownIt.renderInline('{%pdf https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf %}')
|
||||
).toEqual(
|
||||
`<a href="https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf">https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf</a>`
|
||||
)
|
||||
})
|
||||
})
|
|
@ -6,13 +6,14 @@
|
|||
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import type MarkdownIt from 'markdown-it/lib'
|
||||
import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
||||
|
||||
const finalRegex = /^{%pdf (.*) ?%}$/
|
||||
const finalRegex = /^{%pdf (\S*) *%}$/
|
||||
|
||||
export const legacyPdfShortCode: MarkdownIt.PluginSimple = (markdownIt) => {
|
||||
markdownItRegex(markdownIt, {
|
||||
name: 'legacy-pdf-short-code',
|
||||
regex: finalRegex,
|
||||
replace: (match: string) => `<a href="${match}">${match}</a>`
|
||||
})
|
||||
replace: (match) => `<a href="${match}">${match}</a>`
|
||||
} as RegexOptions)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { legacySlideshareShortCode } from './replace-legacy-slideshare-short-code'
|
||||
|
||||
describe('Legacy slideshare short code', () => {
|
||||
it('replaces with link', () => {
|
||||
const markdownIt = new MarkdownIt('default', {
|
||||
html: false,
|
||||
breaks: true,
|
||||
langPrefix: '',
|
||||
typographer: true
|
||||
})
|
||||
markdownIt.use(legacySlideshareShortCode)
|
||||
expect(markdownIt.renderInline('{%slideshare example/123456789 %}')).toEqual(
|
||||
"<a href='https://www.slideshare.net/example/123456789'>https://www.slideshare.net/example/123456789</a>"
|
||||
)
|
||||
})
|
||||
})
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import type MarkdownIt from 'markdown-it/lib'
|
||||
import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
||||
|
||||
const finalRegex = /^{%slideshare (\w+\/[\w-]+) ?%}$/
|
||||
|
||||
|
@ -13,6 +14,6 @@ export const legacySlideshareShortCode: MarkdownIt.PluginSimple = (markdownIt) =
|
|||
markdownItRegex(markdownIt, {
|
||||
name: 'legacy-slideshare-short-code',
|
||||
regex: finalRegex,
|
||||
replace: (match: string) => `<a href='https://www.slideshare.net/${match}'>https://www.slideshare.net/${match}</a>`
|
||||
})
|
||||
replace: (match) => `<a href='https://www.slideshare.net/${match}'>https://www.slideshare.net/${match}</a>`
|
||||
} as RegexOptions)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { legacySpeakerdeckShortCode } from './replace-legacy-speakerdeck-short-code'
|
||||
|
||||
describe('Legacy speakerdeck short code', () => {
|
||||
it('replaces with link', () => {
|
||||
const markdownIt = new MarkdownIt('default', {
|
||||
html: false,
|
||||
breaks: true,
|
||||
langPrefix: '',
|
||||
typographer: true
|
||||
})
|
||||
markdownIt.use(legacySpeakerdeckShortCode)
|
||||
expect(markdownIt.renderInline('{%speakerdeck example/123456789 %}')).toEqual(
|
||||
'<a href="https://speakerdeck.com/example/123456789">https://speakerdeck.com/example/123456789</a>'
|
||||
)
|
||||
})
|
||||
})
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import type MarkdownIt from 'markdown-it/lib'
|
||||
import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
||||
|
||||
const finalRegex = /^{%speakerdeck (\w+\/[\w-]+) ?%}$/
|
||||
|
||||
|
@ -13,6 +14,6 @@ export const legacySpeakerdeckShortCode: MarkdownIt.PluginSimple = (markdownIt)
|
|||
markdownItRegex(markdownIt, {
|
||||
name: 'legacy-speakerdeck-short-code',
|
||||
regex: finalRegex,
|
||||
replace: (match: string) => `<a href="https://speakerdeck.com/${match}">https://speakerdeck.com/${match}</a>`
|
||||
})
|
||||
replace: (match) => `<a href="https://speakerdeck.com/${match}">https://speakerdeck.com/${match}</a>`
|
||||
} as RegexOptions)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PlantUML markdown extensions renders a plantuml codeblock 1`] = `
|
||||
<div>
|
||||
<img
|
||||
alt="uml diagram"
|
||||
src="http://example.org/svg/SoWkIImgAStDuKhEIImkLd2jICmjo4dbSaZDIm6A0W00"
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PlantUML markdown extensions renders an error if no server is defined 1`] = `
|
||||
<div>
|
||||
<p
|
||||
class="alert alert-danger"
|
||||
>
|
||||
renderer.plantuml.notConfigured
|
||||
</p>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react'
|
||||
import { TestMarkdownRenderer } from '../../test-utils/test-markdown-renderer'
|
||||
import React from 'react'
|
||||
import { PlantumlMarkdownExtension } from './plantuml-markdown-extension'
|
||||
import { mockI18n } from '../../test-utils/mock-i18n'
|
||||
|
||||
describe('PlantUML markdown extensions', () => {
|
||||
beforeAll(async () => {
|
||||
await mockI18n()
|
||||
})
|
||||
|
||||
it('renders a plantuml codeblock', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new PlantumlMarkdownExtension('http://example.org')]}
|
||||
content={'```plantuml\nclass Example\n```'}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders an error if no server is defined', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new PlantumlMarkdownExtension(null)]}
|
||||
content={'```plantuml\nclass Example\n```'}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Vega-Lite markdown extensions renders a vega-lite codeblock 1`] = `
|
||||
<div>
|
||||
<pre>
|
||||
<span>
|
||||
this is a mock for vega lite
|
||||
<code>
|
||||
{"$schema":"https://vega.github.io/schema/vega-lite/v4.json","data":{"values":[{"a":"","b":28}]},"mark":"bar","encoding":{"x":{"field":"a"},"y":{"field":"b"}}}
|
||||
|
||||
</code>
|
||||
</span>
|
||||
</pre>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
|
@ -4,43 +4,35 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import type { VisualizationSpec } from 'vega-embed'
|
||||
import { ShowIf } from '../../../common/show-if/show-if'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
import type { CodeProps } from '../../replace-components/code-block-component-replacer'
|
||||
import { useAsync } from 'react-use'
|
||||
import { AsyncLoadingBoundary } from '../../../common/async-loading-boundary'
|
||||
|
||||
const log = new Logger('VegaChart')
|
||||
|
||||
export const VegaLiteChart: React.FC<CodeProps> = ({ code }) => {
|
||||
const diagramContainer = useRef<HTMLDivElement>(null)
|
||||
const [error, setError] = useState<string>()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const showError = useCallback((error: string) => {
|
||||
if (!diagramContainer.current) {
|
||||
return
|
||||
}
|
||||
log.error(error)
|
||||
setError(error)
|
||||
}, [])
|
||||
const {
|
||||
value: vegaEmbed,
|
||||
error: libLoadingError,
|
||||
loading: libLoading
|
||||
} = useAsync(async () => (await import(/* webpackChunkName: "vega" */ 'vega-embed')).default, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!diagramContainer.current) {
|
||||
const { error: renderingError } = useAsync(async () => {
|
||||
const container = diagramContainer.current
|
||||
if (!container || !vegaEmbed) {
|
||||
return
|
||||
}
|
||||
import(/* webpackChunkName: "vega" */ 'vega-embed')
|
||||
.then((embed) => {
|
||||
try {
|
||||
if (!diagramContainer.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const spec = JSON.parse(code) as VisualizationSpec
|
||||
embed
|
||||
.default(diagramContainer.current, spec, {
|
||||
await vegaEmbed(container, spec, {
|
||||
actions: {
|
||||
export: true,
|
||||
source: false,
|
||||
|
@ -52,25 +44,24 @@ export const VegaLiteChart: React.FC<CodeProps> = ({ code }) => {
|
|||
SVG_ACTION: t('renderer.vega-lite.svg')
|
||||
}
|
||||
})
|
||||
.then(() => setError(undefined))
|
||||
.catch((error: Error) => showError(error.message))
|
||||
} catch (error) {
|
||||
showError(t('renderer.vega-lite.errorJson'))
|
||||
}, [code, vegaEmbed])
|
||||
|
||||
useEffect(() => {
|
||||
if (renderingError) {
|
||||
log.error('Error while rendering vega lite diagram', renderingError)
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
log.error('Error while loading vega-light', error)
|
||||
})
|
||||
}, [code, showError, t])
|
||||
}, [renderingError])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ShowIf condition={!!error}>
|
||||
<Alert variant={'danger'}>{error}</Alert>
|
||||
<AsyncLoadingBoundary loading={libLoading} error={libLoadingError} componentName={'Vega Lite'}>
|
||||
<ShowIf condition={!!renderingError}>
|
||||
<Alert variant={'danger'}>
|
||||
<Trans i18nKey={'renderer.vega-lite.errorJson'} />
|
||||
</Alert>
|
||||
</ShowIf>
|
||||
<div className={'text-center'}>
|
||||
<div ref={diagramContainer} />
|
||||
</div>
|
||||
</Fragment>
|
||||
</AsyncLoadingBoundary>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react'
|
||||
import { TestMarkdownRenderer } from '../../test-utils/test-markdown-renderer'
|
||||
import React from 'react'
|
||||
import { mockI18n } from '../../test-utils/mock-i18n'
|
||||
import { VegaLiteMarkdownExtension } from './vega-lite-markdown-extension'
|
||||
import * as VegaLiteChartModule from '../vega-lite/vega-lite-chart'
|
||||
import type { CodeProps } from '../../replace-components/code-block-component-replacer'
|
||||
|
||||
describe('Vega-Lite markdown extensions', () => {
|
||||
beforeAll(async () => {
|
||||
jest.spyOn(VegaLiteChartModule, 'VegaLiteChart').mockImplementation((({ code }) => {
|
||||
return (
|
||||
<span>
|
||||
this is a mock for vega lite
|
||||
<code>{code}</code>
|
||||
</span>
|
||||
)
|
||||
}) as React.FC<CodeProps>)
|
||||
await mockI18n()
|
||||
})
|
||||
|
||||
it('renders a vega-lite codeblock', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new VegaLiteMarkdownExtension()]}
|
||||
content={
|
||||
'```vega-lite\n{"$schema":"https://vega.github.io/schema/vega-lite/v4.json","data":{"values":[{"a":"","b":28}]},"mark":"bar","encoding":{"x":{"field":"a"},"y":{"field":"b"}}}\n```'
|
||||
}
|
||||
/>
|
||||
)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,19 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VimeoFrame renders a click shield 1`] = `
|
||||
<div>
|
||||
<span>
|
||||
This is a click shield for
|
||||
<span
|
||||
class="embed-responsive embed-responsive-16by9"
|
||||
>
|
||||
<iframe
|
||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
||||
class="embed-responsive-item"
|
||||
src="https://player.vimeo.com/video/valid vimeo id?autoplay=1"
|
||||
title="vimeo video of valid vimeo id"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { replaceLegacyVimeoShortCodeMarkdownItPlugin } from './replace-legacy-vimeo-short-code'
|
||||
|
||||
describe('Replace legacy youtube short codes', () => {
|
||||
let markdownIt: MarkdownIt
|
||||
|
||||
beforeEach(() => {
|
||||
markdownIt = new MarkdownIt('default', {
|
||||
html: false,
|
||||
breaks: true,
|
||||
langPrefix: '',
|
||||
typographer: true
|
||||
})
|
||||
markdownIt.use(replaceLegacyVimeoShortCodeMarkdownItPlugin)
|
||||
})
|
||||
|
||||
it('detects a valid legacy youtube short code', () => {
|
||||
expect(markdownIt.renderInline('{%vimeo 12312312312 %}')).toBe('<app-vimeo id="12312312312"></app-vimeo>')
|
||||
})
|
||||
|
||||
it("won't detect an empty string", () => {
|
||||
const code = '{%vimeo %}'
|
||||
expect(markdownIt.renderInline(code)).toBe(code)
|
||||
})
|
||||
|
||||
it("won't detect letters", () => {
|
||||
const code = '{%vimeo 123123a2311 %}'
|
||||
expect(markdownIt.renderInline(code)).toBe(code)
|
||||
})
|
||||
|
||||
it("won't detect an invalid(to short) youtube id", () => {
|
||||
const code = '{%vimeo 1 %}'
|
||||
expect(markdownIt.renderInline(code)).toBe(code)
|
||||
})
|
||||
|
||||
it("won't detect an invalid(to long) youtube id", () => {
|
||||
const code = '{%vimeo 111111111111111111111111111111111 %}'
|
||||
expect(markdownIt.renderInline(code)).toBe(code)
|
||||
})
|
||||
})
|
|
@ -6,8 +6,10 @@
|
|||
|
||||
import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
||||
import { VimeoMarkdownExtension } from './vimeo-markdown-extension'
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
|
||||
export const replaceLegacyVimeoShortCode: RegexOptions = {
|
||||
const replaceLegacyVimeoShortCode: RegexOptions = {
|
||||
name: 'legacy-vimeo-short-code',
|
||||
regex: /^{%vimeo ([\d]{6,11}) ?%}$/,
|
||||
replace: (match) => {
|
||||
|
@ -16,3 +18,6 @@ export const replaceLegacyVimeoShortCode: RegexOptions = {
|
|||
return `<${VimeoMarkdownExtension.tagName} id="${match}"></${VimeoMarkdownExtension.tagName}>`
|
||||
}
|
||||
}
|
||||
|
||||
export const replaceLegacyVimeoShortCodeMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt) =>
|
||||
markdownItRegex(markdownIt, replaceLegacyVimeoShortCode)
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { replaceVimeoLinkMarkdownItPlugin } from './replace-vimeo-link'
|
||||
|
||||
describe('Replace youtube link', () => {
|
||||
let markdownIt: MarkdownIt
|
||||
|
||||
beforeEach(() => {
|
||||
markdownIt = new MarkdownIt('default', {
|
||||
html: false,
|
||||
breaks: true,
|
||||
langPrefix: '',
|
||||
typographer: true
|
||||
})
|
||||
markdownIt.use(replaceVimeoLinkMarkdownItPlugin)
|
||||
})
|
||||
;['http://', 'https://', ''].forEach((protocol) => {
|
||||
;['player.', ''].forEach((subdomain) => {
|
||||
;['vimeo.com'].forEach((domain) => {
|
||||
const origin = `${protocol}${subdomain}${domain}/`
|
||||
describe(origin, () => {
|
||||
const validUrl = `${origin}23237102`
|
||||
it(`can detect a correct vimeo video url`, () => {
|
||||
expect(markdownIt.renderInline(validUrl)).toBe("<app-vimeo id='23237102'></app-vimeo>")
|
||||
})
|
||||
|
||||
it("won't detect an URL without video id", () => {
|
||||
expect(markdownIt.renderInline(origin)).toBe(origin)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
||||
import { VimeoMarkdownExtension } from './vimeo-markdown-extension'
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
|
||||
const protocolRegex = /(?:http(?:s)?:\/\/)?/
|
||||
const domainRegex = /(?:player\.)?(?:vimeo\.com\/)(?:(?:channels|album|ondemand|groups)\/\w+\/)?(?:video\/)?/
|
||||
|
@ -16,7 +18,7 @@ const vimeoVideoUrlRegex = new RegExp(
|
|||
)
|
||||
const linkRegex = new RegExp(`^${vimeoVideoUrlRegex.source}$`, 'i')
|
||||
|
||||
export const replaceVimeoLink: RegexOptions = {
|
||||
const replaceVimeoLink: RegexOptions = {
|
||||
name: 'vimeo-link',
|
||||
regex: linkRegex,
|
||||
replace: (match) => {
|
||||
|
@ -25,3 +27,6 @@ export const replaceVimeoLink: RegexOptions = {
|
|||
return `<${VimeoMarkdownExtension.tagName} id='${match}'></${VimeoMarkdownExtension.tagName}>`
|
||||
}
|
||||
}
|
||||
|
||||
export const replaceVimeoLinkMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt) =>
|
||||
markdownItRegex(markdownIt, replaceVimeoLink)
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { render } from '@testing-library/react'
|
||||
import { VimeoFrame } from './vimeo-frame'
|
||||
import React from 'react'
|
||||
import type { ClickShieldProps } from '../../replace-components/click-shield/click-shield'
|
||||
import * as ClickShieldModule from '../../replace-components/click-shield/click-shield'
|
||||
|
||||
describe('VimeoFrame', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(ClickShieldModule, 'ClickShield').mockImplementation((({ children }) => {
|
||||
return <span>This is a click shield for {children}</span>
|
||||
}) as React.FC<ClickShieldProps>)
|
||||
})
|
||||
|
||||
it('renders a click shield', () => {
|
||||
const view = render(<VimeoFrame id={'valid vimeo id'} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -5,13 +5,12 @@
|
|||
*/
|
||||
|
||||
import { MarkdownExtension } from '../markdown-extension'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import type { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||
import { CustomTagWithIdComponentReplacer } from '../../replace-components/custom-tag-with-id-component-replacer'
|
||||
import { replaceVimeoLink } from './replace-vimeo-link'
|
||||
import { replaceVimeoLinkMarkdownItPlugin } from './replace-vimeo-link'
|
||||
import { VimeoFrame } from './vimeo-frame'
|
||||
import { replaceLegacyVimeoShortCode } from './replace-legacy-vimeo-short-code'
|
||||
import { replaceLegacyVimeoShortCodeMarkdownItPlugin } from './replace-legacy-vimeo-short-code'
|
||||
|
||||
/**
|
||||
* Adds vimeo video embeddings using link detection and the legacy vimeo short code syntax.
|
||||
|
@ -20,8 +19,8 @@ export class VimeoMarkdownExtension extends MarkdownExtension {
|
|||
public static readonly tagName = 'app-vimeo'
|
||||
|
||||
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
||||
markdownItRegex(markdownIt, replaceVimeoLink)
|
||||
markdownItRegex(markdownIt, replaceLegacyVimeoShortCode)
|
||||
replaceLegacyVimeoShortCodeMarkdownItPlugin(markdownIt)
|
||||
replaceVimeoLinkMarkdownItPlugin(markdownIt)
|
||||
}
|
||||
|
||||
public buildReplacers(): ComponentReplacer[] {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`YoutubeFrame renders a click shield 1`] = `
|
||||
<div>
|
||||
<span>
|
||||
This is a click shield for
|
||||
<span
|
||||
class="embed-responsive embed-responsive-16by9"
|
||||
>
|
||||
<iframe
|
||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
||||
class="embed-responsive-item"
|
||||
src="https://www.youtube-nocookie.com/embed/valid youtube id?autoplay=1"
|
||||
title="youtube video of valid youtube id"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,37 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`youtube markdown extension doesn't render invalid youtube ids in short code syntax 1`] = `
|
||||
<div>
|
||||
<p>
|
||||
{%youtube a %}
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`youtube markdown extension renders legacy youtube syntax 1`] = `
|
||||
<div>
|
||||
<p>
|
||||
<span>
|
||||
this is a mock for the youtube frame with id
|
||||
XDnhKh5V5XQ
|
||||
</span>
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`youtube markdown extension renders plain youtube URLs 1`] = `
|
||||
<div>
|
||||
<p>
|
||||
<span>
|
||||
this is a mock for the youtube frame with id
|
||||
XDnhKh5V5XQ
|
||||
</span>
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { replaceLegacyYoutubeShortCodeMarkdownItPlugin } from './replace-legacy-youtube-short-code'
|
||||
|
||||
describe('Replace legacy youtube short codes', () => {
|
||||
let markdownIt: MarkdownIt
|
||||
|
||||
beforeEach(() => {
|
||||
markdownIt = new MarkdownIt('default', {
|
||||
html: false,
|
||||
breaks: true,
|
||||
langPrefix: '',
|
||||
typographer: true
|
||||
})
|
||||
markdownIt.use(replaceLegacyYoutubeShortCodeMarkdownItPlugin)
|
||||
})
|
||||
|
||||
it('detects a valid legacy youtube short code', () => {
|
||||
expect(markdownIt.renderInline('{%youtube 12312312312 %}')).toBe('<app-youtube id="12312312312"></app-youtube>')
|
||||
})
|
||||
|
||||
it("won't detect an empty string", () => {
|
||||
const code = '{%youtube %}'
|
||||
expect(markdownIt.renderInline(code)).toBe(code)
|
||||
})
|
||||
|
||||
it("won't detect an invalid(to short) youtube id", () => {
|
||||
const code = '{%youtube 1 %}'
|
||||
expect(markdownIt.renderInline(code)).toBe(code)
|
||||
})
|
||||
|
||||
it("won't detect an invalid(to long) youtube id", () => {
|
||||
const code = '{%youtube 111111111111111111111111111111111 %}'
|
||||
expect(markdownIt.renderInline(code)).toBe(code)
|
||||
})
|
||||
})
|
|
@ -6,8 +6,11 @@
|
|||
|
||||
import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
||||
import { YoutubeMarkdownExtension } from './youtube-markdown-extension'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
|
||||
export const replaceLegacyYoutubeShortCode: RegexOptions = {
|
||||
export const replaceLegacyYoutubeShortCodeMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt): void =>
|
||||
markdownItRegex(markdownIt, {
|
||||
name: 'legacy-youtube-short-code',
|
||||
regex: /^{%youtube ([^"&?\\/\s]{11}) ?%}$/,
|
||||
replace: (match) => {
|
||||
|
@ -15,4 +18,4 @@ export const replaceLegacyYoutubeShortCode: RegexOptions = {
|
|||
// noinspection CheckTagEmptyBody
|
||||
return `<${YoutubeMarkdownExtension.tagName} id="${match}"></${YoutubeMarkdownExtension.tagName}>`
|
||||
}
|
||||
}
|
||||
} as RegexOptions)
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { replaceYouTubeLinkMarkdownItPlugin } from './replace-youtube-link'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
|
||||
describe('Replace youtube link', () => {
|
||||
let markdownIt: MarkdownIt
|
||||
|
||||
beforeEach(() => {
|
||||
markdownIt = new MarkdownIt('default', {
|
||||
html: false,
|
||||
breaks: true,
|
||||
langPrefix: '',
|
||||
typographer: true
|
||||
})
|
||||
markdownIt.use(replaceYouTubeLinkMarkdownItPlugin)
|
||||
})
|
||||
;['http://', 'https://', ''].forEach((protocol) => {
|
||||
;['www.', ''].forEach((subdomain) => {
|
||||
;['youtube.com', 'youtube-nocookie.com'].forEach((domain) => {
|
||||
const origin = `${protocol}${subdomain}${domain}/`
|
||||
describe(origin, () => {
|
||||
const validUrl = `${origin}?v=12312312312`
|
||||
it(`can detect a correct youtube video url`, () => {
|
||||
expect(markdownIt.renderInline(validUrl)).toBe('<app-youtube id="12312312312"></app-youtube>')
|
||||
})
|
||||
|
||||
it("won't detect an URL without video id", () => {
|
||||
expect(markdownIt.renderInline(origin)).toBe(origin)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
||||
import { YoutubeMarkdownExtension } from './youtube-markdown-extension'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
|
||||
const protocolRegex = /(?:http(?:s)?:\/\/)?/
|
||||
const subdomainRegex = /(?:www.)?/
|
||||
|
@ -17,7 +19,8 @@ const youtubeVideoUrlRegex = new RegExp(
|
|||
)
|
||||
const linkRegex = new RegExp(`^${youtubeVideoUrlRegex.source}$`, 'i')
|
||||
|
||||
export const replaceYouTubeLink: RegexOptions = {
|
||||
export const replaceYouTubeLinkMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt) =>
|
||||
markdownItRegex(markdownIt, {
|
||||
name: 'youtube-link',
|
||||
regex: linkRegex,
|
||||
replace: (match) => {
|
||||
|
@ -25,4 +28,4 @@ export const replaceYouTubeLink: RegexOptions = {
|
|||
// noinspection CheckTagEmptyBody
|
||||
return `<${YoutubeMarkdownExtension.tagName} id="${match}"></${YoutubeMarkdownExtension.tagName}>`
|
||||
}
|
||||
}
|
||||
} as RegexOptions)
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { render } from '@testing-library/react'
|
||||
import { YouTubeFrame } from './youtube-frame'
|
||||
import React from 'react'
|
||||
import type { ClickShieldProps } from '../../replace-components/click-shield/click-shield'
|
||||
import * as ClickShieldModule from '../../replace-components/click-shield/click-shield'
|
||||
|
||||
describe('YoutubeFrame', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(ClickShieldModule, 'ClickShield').mockImplementation((({ children }) => {
|
||||
return <span>This is a click shield for {children}</span>
|
||||
}) as React.FC<ClickShieldProps>)
|
||||
})
|
||||
|
||||
it('renders a click shield', () => {
|
||||
const view = render(<YouTubeFrame id={'valid youtube id'} />)
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { YoutubeMarkdownExtension } from './youtube-markdown-extension'
|
||||
import { render } from '@testing-library/react'
|
||||
import { TestMarkdownRenderer } from '../../test-utils/test-markdown-renderer'
|
||||
import * as YouTubeFrameModule from './youtube-frame'
|
||||
import React from 'react'
|
||||
import type { IdProps } from '../../replace-components/custom-tag-with-id-component-replacer'
|
||||
import { mockI18n } from '../../test-utils/mock-i18n'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import * as replaceLegacyYoutubeShortCodeMarkdownItPluginModule from './replace-legacy-youtube-short-code'
|
||||
import * as replaceYouTubeLinkMarkdownItPluginModule from './replace-youtube-link'
|
||||
|
||||
describe('youtube markdown extension', () => {
|
||||
const replaceYouTubeLinkMarkdownItPluginSpy = jest.spyOn(
|
||||
replaceYouTubeLinkMarkdownItPluginModule,
|
||||
'replaceYouTubeLinkMarkdownItPlugin'
|
||||
)
|
||||
const replaceLegacyYoutubeShortCodeMarkdownItPluginSpy = jest.spyOn(
|
||||
replaceLegacyYoutubeShortCodeMarkdownItPluginModule,
|
||||
'replaceLegacyYoutubeShortCodeMarkdownItPlugin'
|
||||
)
|
||||
|
||||
beforeAll(async () => {
|
||||
jest
|
||||
.spyOn(YouTubeFrameModule, 'YouTubeFrame')
|
||||
.mockImplementation((({ id }) => (
|
||||
<span>this is a mock for the youtube frame with id {id}</span>
|
||||
)) as React.FC<IdProps>)
|
||||
await mockI18n()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks()
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
it('renders plain youtube URLs', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer
|
||||
extensions={[new YoutubeMarkdownExtension()]}
|
||||
content={'https://www.youtube.com/watch?v=XDnhKh5V5XQ'}
|
||||
/>
|
||||
)
|
||||
expect(replaceYouTubeLinkMarkdownItPluginSpy).toHaveBeenCalledWith(expect.any(MarkdownIt))
|
||||
expect(replaceLegacyYoutubeShortCodeMarkdownItPluginSpy).toHaveBeenCalledWith(expect.any(MarkdownIt))
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders legacy youtube syntax', () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer extensions={[new YoutubeMarkdownExtension()]} content={'{%youtube XDnhKh5V5XQ %}'} />
|
||||
)
|
||||
expect(replaceYouTubeLinkMarkdownItPluginSpy).toHaveBeenCalledWith(expect.any(MarkdownIt))
|
||||
expect(replaceLegacyYoutubeShortCodeMarkdownItPluginSpy).toHaveBeenCalledWith(expect.any(MarkdownIt))
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("doesn't render invalid youtube ids in short code syntax", () => {
|
||||
const view = render(
|
||||
<TestMarkdownRenderer extensions={[new YoutubeMarkdownExtension()]} content={'{%youtube a %}'} />
|
||||
)
|
||||
expect(replaceYouTubeLinkMarkdownItPluginSpy).toHaveBeenCalledWith(expect.any(MarkdownIt))
|
||||
expect(replaceLegacyYoutubeShortCodeMarkdownItPluginSpy).toHaveBeenCalledWith(expect.any(MarkdownIt))
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -5,9 +5,8 @@
|
|||
*/
|
||||
|
||||
import { MarkdownExtension } from '../markdown-extension'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import { replaceYouTubeLink } from './replace-youtube-link'
|
||||
import { replaceLegacyYoutubeShortCode } from './replace-legacy-youtube-short-code'
|
||||
import { replaceYouTubeLinkMarkdownItPlugin } from './replace-youtube-link'
|
||||
import { replaceLegacyYoutubeShortCodeMarkdownItPlugin } from './replace-legacy-youtube-short-code'
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import type { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||
import { CustomTagWithIdComponentReplacer } from '../../replace-components/custom-tag-with-id-component-replacer'
|
||||
|
@ -20,8 +19,8 @@ export class YoutubeMarkdownExtension extends MarkdownExtension {
|
|||
public static readonly tagName = 'app-youtube'
|
||||
|
||||
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
||||
markdownItRegex(markdownIt, replaceYouTubeLink)
|
||||
markdownItRegex(markdownIt, replaceLegacyYoutubeShortCode)
|
||||
replaceYouTubeLinkMarkdownItPlugin(markdownIt)
|
||||
replaceLegacyYoutubeShortCodeMarkdownItPlugin(markdownIt)
|
||||
}
|
||||
|
||||
public buildReplacers(): ComponentReplacer[] {
|
||||
|
|
|
@ -19,7 +19,7 @@ import { ProxyImageFrame } from '../../markdown-extension/image/proxy-image-fram
|
|||
|
||||
const log = new Logger('OneClickEmbedding')
|
||||
|
||||
interface ClickShieldProps extends PropsWithDataCypressId {
|
||||
export interface ClickShieldProps extends PropsWithChildren<PropsWithDataCypressId> {
|
||||
onImageFetch?: () => Promise<string>
|
||||
fallbackPreviewImageUrl?: string
|
||||
hoverIcon: IconName
|
||||
|
@ -39,7 +39,7 @@ interface ClickShieldProps extends PropsWithDataCypressId {
|
|||
* @param fallbackBackgroundColor A color that should be used if no background image was provided or could be loaded.
|
||||
* @param children The children element that should be shielded.
|
||||
*/
|
||||
export const ClickShield: React.FC<PropsWithChildren<ClickShieldProps>> = ({
|
||||
export const ClickShield: React.FC<ClickShieldProps> = ({
|
||||
containerClassName,
|
||||
onImageFetch,
|
||||
fallbackPreviewImageUrl,
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react'
|
||||
import { useConvertMarkdownToReactDom } from '../hooks/use-convert-markdown-to-react-dom'
|
||||
import type { MarkdownExtension } from '../markdown-extension/markdown-extension'
|
||||
import { StoreProvider } from '../../../redux/store-provider'
|
||||
|
||||
export interface SimpleMarkdownRendererProps {
|
||||
content: string
|
||||
extensions: MarkdownExtension[]
|
||||
}
|
||||
|
||||
export const TestMarkdownRenderer: React.FC<SimpleMarkdownRendererProps> = ({ content, extensions }) => {
|
||||
const lines = useMemo(() => content.split('\n'), [content])
|
||||
const dom = useConvertMarkdownToReactDom(lines, extensions, true, false)
|
||||
|
||||
return <StoreProvider>{dom}</StoreProvider>
|
||||
}
|
|
@ -4,14 +4,13 @@
|
|||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { AppProps } from 'next/app'
|
||||
import { store } from '../redux'
|
||||
import { Provider } from 'react-redux'
|
||||
import { ErrorBoundary } from '../components/error-boundary/error-boundary'
|
||||
import { ApplicationLoader } from '../components/application-loader/application-loader'
|
||||
import '../../global-styles/dark.scss'
|
||||
import '../../global-styles/index.scss'
|
||||
import type { NextPage } from 'next'
|
||||
import { BaseHead } from '../components/layout/base-head'
|
||||
import { StoreProvider } from '../redux/store-provider'
|
||||
|
||||
/**
|
||||
* The actual hedgedoc next js app.
|
||||
|
@ -19,14 +18,14 @@ import { BaseHead } from '../components/layout/base-head'
|
|||
*/
|
||||
const HedgeDocApp: NextPage<AppProps> = ({ Component, pageProps }: AppProps) => {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<StoreProvider>
|
||||
<BaseHead />
|
||||
<ApplicationLoader>
|
||||
<ErrorBoundary>
|
||||
<Component {...pageProps} />
|
||||
</ErrorBoundary>
|
||||
</ApplicationLoader>
|
||||
</Provider>
|
||||
</StoreProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
19
src/redux/store-provider.tsx
Normal file
19
src/redux/store-provider.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import React from 'react'
|
||||
import { Provider } from 'react-redux'
|
||||
import { store } from './index'
|
||||
|
||||
/**
|
||||
* Sets the redux store for the children components.
|
||||
*
|
||||
* @param children The child components that should access the redux store
|
||||
*/
|
||||
export const StoreProvider: React.FC<PropsWithChildren<unknown>> = ({ children }) => {
|
||||
return <Provider store={store}>{children}</Provider>
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue