diff --git a/cypress/integration/diagrams.spec.ts b/cypress/integration/diagrams.spec.ts index 03268e4f9..5c898d188 100644 --- a/cypress/integration/diagrams.spec.ts +++ b/cypress/integration/diagrams.spec.ts @@ -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') - }) }) diff --git a/cypress/integration/emoji.spec.ts b/cypress/integration/emoji.spec.ts deleted file mode 100644 index e087b8b98..000000000 --- a/cypress/integration/emoji.spec.ts +++ /dev/null @@ -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') - }) -}) diff --git a/cypress/integration/highlightedCodeBlock.spec.ts b/cypress/integration/highlightedCodeBlock.spec.ts deleted file mode 100644 index 2353962bf..000000000 --- a/cypress/integration/highlightedCodeBlock.spec.ts +++ /dev/null @@ -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') - }) - }) - }) -}) diff --git a/cypress/integration/linkEmbedder.spec.ts b/cypress/integration/linkEmbedder.spec.ts index 9e0a421b8..651065a82 100644 --- a/cypress/integration/linkEmbedder.spec.ts +++ b/cypress/integration/linkEmbedder.spec.ts @@ -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') - }) }) diff --git a/cypress/integration/shortcodes.spec.ts b/cypress/integration/shortcodes.spec.ts deleted file mode 100644 index deceaced1..000000000 --- a/cypress/integration/shortcodes.spec.ts +++ /dev/null @@ -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') - }) - }) -}) diff --git a/src/components/markdown-renderer/markdown-extension/abcjs/__snapshots__/abc-frame.test.tsx.snap b/src/components/markdown-renderer/markdown-extension/abcjs/__snapshots__/abc-frame.test.tsx.snap index 8e1d74f43..47eafb143 100644 --- a/src/components/markdown-renderer/markdown-extension/abcjs/__snapshots__/abc-frame.test.tsx.snap +++ b/src/components/markdown-renderer/markdown-extension/abcjs/__snapshots__/abc-frame.test.tsx.snap @@ -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`] = `
diff --git a/src/components/markdown-renderer/markdown-extension/abcjs/__snapshots__/abcjs-markdown-extension.test.tsx.snap b/src/components/markdown-renderer/markdown-extension/abcjs/__snapshots__/abcjs-markdown-extension.test.tsx.snap new file mode 100644 index 000000000..786b4b082 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/abcjs/__snapshots__/abcjs-markdown-extension.test.tsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AbcJs Markdown Extension renders an abc codeblock 1`] = ` +
+
+    
+      this is a mock for abc js frame
+      
+        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:|
+
+      
+    
+  
+ + +
+`; diff --git a/src/components/markdown-renderer/markdown-extension/abcjs/abc-frame.tsx b/src/components/markdown-renderer/markdown-extension/abcjs/abc-frame.tsx index fbd1ac78a..b9deb5edc 100644 --- a/src/components/markdown-renderer/markdown-extension/abcjs/abc-frame.tsx +++ b/src/components/markdown-renderer/markdown-extension/abcjs/abc-frame.tsx @@ -28,7 +28,7 @@ export const AbcFrame: React.FC = ({ 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 diff --git a/src/components/markdown-renderer/markdown-extension/abcjs/abcjs-markdown-extension.test.tsx b/src/components/markdown-renderer/markdown-extension/abcjs/abcjs-markdown-extension.test.tsx new file mode 100644 index 000000000..0469e804f --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/abcjs/abcjs-markdown-extension.test.tsx @@ -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 ( + + this is a mock for abc js frame + {code} + + ) + }) as React.FC) + await mockI18n() + }) + + afterAll(() => { + jest.resetModules() + jest.restoreAllMocks() + }) + + it('renders an abc codeblock', () => { + const view = render( + + ) + expect(view.container).toMatchSnapshot() + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-extension.ts index 8822ed868..271be3572 100644 --- a/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-extension.ts +++ b/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-extension.ts @@ -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[] { diff --git a/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-it-plugin.test.ts b/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-it-plugin.test.ts index 3925b763a..149d9983f 100644 --- a/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-it-plugin.test.ts +++ b/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-it-plugin.test.ts @@ -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( + 'markdown' + ) }) 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('[]') }) }) diff --git a/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-it-plugin.ts b/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-it-plugin.ts index 2e3e1234e..d67572bb6 100644 --- a/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-it-plugin.ts +++ b/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-it-plugin.ts @@ -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 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}` } } - 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 { - 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 { + 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 + } + } } } diff --git a/src/components/markdown-renderer/markdown-extension/blockquote/parse-blockquote-extra-tag.ts b/src/components/markdown-renderer/markdown-extension/blockquote/parse-blockquote-extra-tag.ts deleted file mode 100644 index a7c0b0893..000000000 --- a/src/components/markdown-renderer/markdown-extension/blockquote/parse-blockquote-extra-tag.ts +++ /dev/null @@ -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 - } - } -} diff --git a/src/components/markdown-renderer/markdown-extension/csv/__snapshots__/csv-table-markdown-extension.test.tsx.snap b/src/components/markdown-renderer/markdown-extension/csv/__snapshots__/csv-table-markdown-extension.test.tsx.snap new file mode 100644 index 000000000..5761a8740 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/csv/__snapshots__/csv-table-markdown-extension.test.tsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CSV Table Markdown Extension renders a csv codeblock 1`] = ` +
+
+    
+      a;b;c
+d;e;f
+
+    
+  
+ + +
+`; diff --git a/src/components/markdown-renderer/markdown-extension/csv/__snapshots__/csv-table.test.tsx.snap b/src/components/markdown-renderer/markdown-extension/csv/__snapshots__/csv-table.test.tsx.snap new file mode 100644 index 000000000..2f5459b68 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/csv/__snapshots__/csv-table.test.tsx.snap @@ -0,0 +1,79 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CSV Table renders correctly with header 1`] = ` +
+ + + + + + + + + + + + + + + +
+ a + + b + + c +
+ d + + e + + f +
+
+`; + +exports[`CSV Table renders correctly without code 1`] = ` +
+ + +
+
+`; + +exports[`CSV Table renders correctly without header 1`] = ` +
+ + + + + + + + + + + + + +
+ a + + b + + c +
+ d + + e + + f +
+
+`; diff --git a/src/components/markdown-renderer/markdown-extension/csv/csv-parser.ts b/src/components/markdown-renderer/markdown-extension/csv/csv-parser.ts index 87b912311..3eda28ebb 100644 --- a/src/components/markdown-renderer/markdown-extension/csv/csv-parser.ts +++ b/src/components/markdown-renderer/markdown-extension/csv/csv-parser.ts @@ -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 */ diff --git a/src/components/markdown-renderer/markdown-extension/csv/csv-replacer.tsx b/src/components/markdown-renderer/markdown-extension/csv/csv-replacer.tsx index 11dbe1229..12f9bb78a 100644 --- a/src/components/markdown-renderer/markdown-extension/csv/csv-replacer.tsx +++ b/src/components/markdown-renderer/markdown-extension/csv/csv-replacer.tsx @@ -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 } diff --git a/src/components/markdown-renderer/markdown-extension/csv/csv-table-markdown-extension.test.tsx b/src/components/markdown-renderer/markdown-extension/csv/csv-table-markdown-extension.test.tsx new file mode 100644 index 000000000..ffd688d06 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/csv/csv-table-markdown-extension.test.tsx @@ -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 ( + + this is a mock for csv frame + {code} + + ) + }) as React.FC) + await mockI18n() + }) + + afterAll(() => { + jest.resetModules() + jest.restoreAllMocks() + }) + + it('renders a csv codeblock', () => { + const view = render( + + ) + expect(view.container).toMatchSnapshot() + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/csv/csv-table.test.tsx b/src/components/markdown-renderer/markdown-extension/csv/csv-table.test.tsx new file mode 100644 index 000000000..3c0d197b1 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/csv/csv-table.test.tsx @@ -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() + expect(view.container).toMatchSnapshot() + }) + + it('renders correctly without header', () => { + const view = render() + expect(view.container).toMatchSnapshot() + }) + + it('renders correctly without code', () => { + const view = render() + expect(view.container).toMatchSnapshot() + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/csv/csv-table.tsx b/src/components/markdown-renderer/markdown-extension/csv/csv-table.tsx index 1d8d67f53..8993d6c4a 100644 --- a/src/components/markdown-renderer/markdown-extension/csv/csv-table.tsx +++ b/src/components/markdown-renderer/markdown-extension/csv/csv-table.tsx @@ -25,26 +25,21 @@ export const CsvTable: React.FC = ({ }) => { 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 : ( - - - {headerRow.map((column, columnNumber) => ( - {column} - ))} - - - ), - [headerRow] - ) + const renderTableHeader = useMemo(() => { + return headerRow.length === 0 ? undefined : ( + + + {headerRow.map((column, columnNumber) => ( + {column} + ))} + + + ) + }, [headerRow]) const renderTableBody = useMemo( () => ( diff --git a/src/components/markdown-renderer/markdown-extension/emoji/__snapshots__/emoji-markdown-extension.test.tsx.snap b/src/components/markdown-renderer/markdown-extension/emoji/__snapshots__/emoji-markdown-extension.test.tsx.snap new file mode 100644 index 000000000..995759c49 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/emoji/__snapshots__/emoji-markdown-extension.test.tsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Emoji Markdown Extension renders a fork awesome code 1`] = ` +
+

+ +

+ + +
+`; + +exports[`Emoji Markdown Extension renders a skin tone code 1`] = ` +
+

+ šŸ½ +

+ + +
+`; + +exports[`Emoji Markdown Extension renders an emoji code 1`] = ` +
+

+ šŸ˜„ +

+ + +
+`; diff --git a/src/components/markdown-renderer/markdown-extension/emoji/emoji-markdown-extension.test.tsx b/src/components/markdown-renderer/markdown-extension/emoji/emoji-markdown-extension.test.tsx new file mode 100644 index 000000000..9b80f57c7 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/emoji/emoji-markdown-extension.test.tsx @@ -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() + expect(view.container).toMatchSnapshot() + }) + + it('renders a fork awesome code', () => { + const view = render( + + ) + expect(view.container).toMatchSnapshot() + }) + + it('renders a skin tone code', () => { + const view = render() + expect(view.container).toMatchSnapshot() + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/flowchart/__snapshots__/flowchart-markdown-extension.test.tsx.snap b/src/components/markdown-renderer/markdown-extension/flowchart/__snapshots__/flowchart-markdown-extension.test.tsx.snap new file mode 100644 index 000000000..0032ec201 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/flowchart/__snapshots__/flowchart-markdown-extension.test.tsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Flowchart markdown extensions renders a flowchart codeblock 1`] = ` +
+
+    
+      this is a mock for flowchart frame
+      
+        st=>start: Start
+e=>end: End
+st->e
+
+      
+    
+  
+ + +
+`; diff --git a/src/components/markdown-renderer/markdown-extension/flowchart/__snapshots__/flowchart.test.tsx.snap b/src/components/markdown-renderer/markdown-extension/flowchart/__snapshots__/flowchart.test.tsx.snap new file mode 100644 index 000000000..415e64f49 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/flowchart/__snapshots__/flowchart.test.tsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Flowchart handles error if lib loading failed 1`] = ` +
+ +
+`; + +exports[`Flowchart handles error while rendering 1`] = ` +
+ +
+
+`; + +exports[`Flowchart renders correctly 1`] = ` +
+
+ Flowchart rendering succeeded! +
+
+`; diff --git a/src/components/markdown-renderer/markdown-extension/flowchart/flowchart-markdown-extension.test.tsx b/src/components/markdown-renderer/markdown-extension/flowchart/flowchart-markdown-extension.test.tsx new file mode 100644 index 000000000..bd016eaa1 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/flowchart/flowchart-markdown-extension.test.tsx @@ -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 ( + + this is a mock for flowchart frame + {code} + + ) + }) as React.FC) + await mockI18n() + }) + + afterAll(() => { + jest.resetModules() + jest.restoreAllMocks() + }) + + it('renders a flowchart codeblock', () => { + const view = render( + start: Start\ne=>end: End\nst->e\n```'} + /> + ) + expect(view.container).toMatchSnapshot() + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/flowchart/flowchart.test.tsx b/src/components/markdown-renderer/markdown-extension/flowchart/flowchart.test.tsx new file mode 100644 index 000000000..dec918b6f --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/flowchart/flowchart.test.tsx @@ -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> => { + 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( + + + + ) + await screen.findByText(successText) + expect(view.container).toMatchSnapshot() + expect(mockDrawSvg).toBeCalled() + }) + + it('handles error while rendering', async () => { + const mockDrawSvg = mockFlowchartDraw() + + const view = render( + + + + ) + 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( + + + + ) + await screen.findByText('common.errorWhileLoading') + expect(view.container).toMatchSnapshot() + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/flowchart/flowchart.tsx b/src/components/markdown-renderer/markdown-extension/flowchart/flowchart.tsx index 554494e4e..9eb010b5f 100644 --- a/src/components/markdown-renderer/markdown-extension/flowchart/flowchart.tsx +++ b/src/components/markdown-renderer/markdown-extension/flowchart/flowchart.tsx @@ -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,59 +9,69 @@ 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 = ({ code }) => { +export const FlowChart: React.FC = ({ code }) => { const diagramRef = useRef(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 { - parserOutput.drawSVG(currentDiagramRef, { - 'line-width': 2, - fill: 'none', - 'font-size': 16, - 'line-color': darkModeActivated ? '#ffffff' : '#000000', - 'element-color': darkModeActivated ? '#ffffff' : '#000000', - 'font-color': darkModeActivated ? '#ffffff' : '#000000', - 'font-family': fontStyles['font-family-base'] - }) - setError(false) - } catch (error) { - setError(true) - } + try { + const parserOutput = flowchartLib.parse(code) + parserOutput.drawSVG(currentDiagramRef, { + 'line-width': 2, + fill: 'none', + 'font-size': 16, + 'line-color': darkModeActivated ? '#ffffff' : '#000000', + 'element-color': darkModeActivated ? '#ffffff' : '#000000', + 'font-color': darkModeActivated ? '#ffffff' : '#000000', + 'font-family': fontStyles['font-family-base'] }) - .catch((error: Error) => log.error('Error while loading flowchart.js', error)) + setSyntaxError(false) + } catch (error) { + log.error('Error while rendering flowchart', error) + setSyntaxError(true) + } return () => { Array.from(currentDiagramRef.children).forEach((value) => value.remove()) } - }, [code, darkModeActivated]) + }, [code, darkModeActivated, flowchartLib]) - if (error) { - return ( - - - - ) - } else { - return
- } + return ( + + + + + + +
+ + ) } diff --git a/src/components/markdown-renderer/markdown-extension/graphviz/__snapshots__/graphviz-markdown-extension.test.tsx.snap b/src/components/markdown-renderer/markdown-extension/graphviz/__snapshots__/graphviz-markdown-extension.test.tsx.snap new file mode 100644 index 000000000..cc7a39915 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/graphviz/__snapshots__/graphviz-markdown-extension.test.tsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PlantUML markdown extensions renders a plantuml codeblock 1`] = ` +
+
+    
+      this is a mock for graphviz frame
+      
+        graph {
+a -- b
+}
+
+      
+    
+  
+ + +
+`; diff --git a/src/components/markdown-renderer/markdown-extension/graphviz/graphviz-markdown-extension.test.tsx b/src/components/markdown-renderer/markdown-extension/graphviz/graphviz-markdown-extension.test.tsx new file mode 100644 index 000000000..4cb899341 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/graphviz/graphviz-markdown-extension.test.tsx @@ -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 ( + + this is a mock for graphviz frame + {code} + + ) + }) as React.FC) + await mockI18n() + }) + + afterAll(() => { + jest.resetModules() + jest.restoreAllMocks() + }) + + it('renders a plantuml codeblock', () => { + const view = render( + + ) + expect(view.container).toMatchSnapshot() + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/highlighted-fence/__snapshots__/highlighted-code-markdown-extension.test.tsx.snap b/src/components/markdown-renderer/markdown-extension/highlighted-fence/__snapshots__/highlighted-code-markdown-extension.test.tsx.snap new file mode 100644 index 000000000..57b47abb3 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/highlighted-fence/__snapshots__/highlighted-code-markdown-extension.test.tsx.snap @@ -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`] = ` +
+
+    
+      this is a mock for highlighted code
+      
+        let x = 0
+
+      
+      
+        language: 
+        javascript
+      
+      
+        start line number: 
+      
+      
+        wrap line: 
+        true
+      
+    
+  
+ + +
+`; + +exports[`Highlighted code markdown extension renders with just the language doesn't show a gutter 1`] = ` +
+
+    
+      this is a mock for highlighted code
+      
+        let x = 0
+
+      
+      
+        language: 
+        javascript
+      
+      
+        start line number: 
+      
+      
+        wrap line: 
+        false
+      
+    
+  
+ + +
+`; + +exports[`Highlighted code markdown extension renders with the language and show gutter and line wrapping shows the correct line number 1`] = ` +
+
+    
+      this is a mock for highlighted code
+      
+        let x = 0
+
+      
+      
+        language: 
+        javascript
+      
+      
+        start line number: 
+        1
+      
+      
+        wrap line: 
+        true
+      
+    
+  
+ + +
+`; + +exports[`Highlighted code markdown extension renders with the language and show gutter shows the correct line number 1`] = ` +
+
+    
+      this is a mock for highlighted code
+      
+        let x = 0
+
+      
+      
+        language: 
+        javascript
+      
+      
+        start line number: 
+        1
+      
+      
+        wrap line: 
+        false
+      
+    
+  
+ + +
+`; + +exports[`Highlighted code markdown extension renders with the language, show gutter with a start number and line wrapping shows the correct line number 1`] = ` +
+
+    
+      this is a mock for highlighted code
+      
+        let x = 0
+
+      
+      
+        language: 
+        javascript
+      
+      
+        start line number: 
+        100
+      
+      
+        wrap line: 
+        true
+      
+    
+  
+ + +
+`; + +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`] = ` +
+
+    
+      this is a mock for highlighted code
+      
+        let x = 0
+let y = 1
+
+      
+      
+        language: 
+        javascript
+      
+      
+        start line number: 
+        100
+      
+      
+        wrap line: 
+        true
+      
+    
+  
+ + +
+    
+      this is a mock for highlighted code
+      
+        let y = 2
+
+      
+      
+        language: 
+        javascript
+      
+      
+        start line number: 
+        102
+      
+      
+        wrap line: 
+        false
+      
+    
+  
+ + +
+`; + +exports[`Highlighted code markdown extension renders with the language, show gutter with a start number shows the correct line number 1`] = ` +
+
+    
+      this is a mock for highlighted code
+      
+        let x = 0
+
+      
+      
+        language: 
+        javascript
+      
+      
+        start line number: 
+        100
+      
+      
+        wrap line: 
+        false
+      
+    
+  
+ + +
+`; + +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`] = ` +
+
+    
+      this is a mock for highlighted code
+      
+        let x = 0
+let y = 1
+
+      
+      
+        language: 
+        javascript
+      
+      
+        start line number: 
+        100
+      
+      
+        wrap line: 
+        false
+      
+    
+  
+ + +
+    
+      this is a mock for highlighted code
+      
+        let y = 2
+
+      
+      
+        language: 
+        javascript
+      
+      
+        start line number: 
+        102
+      
+      
+        wrap line: 
+        false
+      
+    
+  
+ + +
+`; diff --git a/src/components/markdown-renderer/markdown-extension/highlighted-fence/__snapshots__/highlighted-code.test.tsx.snap b/src/components/markdown-renderer/markdown-extension/highlighted-fence/__snapshots__/highlighted-code.test.tsx.snap new file mode 100644 index 000000000..41dd06018 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/highlighted-fence/__snapshots__/highlighted-code.test.tsx.snap @@ -0,0 +1,133 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Highlighted Code can hide the line numbers 1`] = ` + + + 1 + +
+ + const a = 1 + +
+
+`; + +exports[`Highlighted Code highlights code 1`] = ` + + + 1 + +
+ + const + + a = + + 1 + +
+
+`; + +exports[`Highlighted Code renders plain text 1`] = ` + + + 1 + +
+ + a + +
+ + 2 + +
+ + b + +
+ + 3 + +
+ + c + +
+
+`; + +exports[`Highlighted Code starts with a specific line 1`] = ` + + + 100 + +
+ + const a = 1 + +
+
+`; + +exports[`Highlighted Code wraps code 1`] = ` + + + 1 + +
+ + const a = 1 + +
+
+`; diff --git a/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code-markdown-extension.test.tsx b/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code-markdown-extension.test.tsx new file mode 100644 index 000000000..2fdc45e96 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code-markdown-extension.test.tsx @@ -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 ( + + this is a mock for highlighted code + {code} + language: {language} + start line number: {startLineNumber} + wrap line: {wrapLines ? 'true' : 'false'} + + ) + }) as React.FC) + await mockI18n() + }) + + describe('with just the language', () => { + it("doesn't show a gutter", () => { + const view = render( + + ) + expect(view.container).toMatchSnapshot() + }) + + describe('and line wrapping', () => { + it("doesn't show a gutter", () => { + const view = render( + + ) + expect(view.container).toMatchSnapshot() + }) + }) + }) + + describe('with the language and show gutter', () => { + it('shows the correct line number', () => { + const view = render( + + ) + expect(view.container).toMatchSnapshot() + }) + + describe('and line wrapping', () => { + it('shows the correct line number', () => { + const view = render( + + ) + expect(view.container).toMatchSnapshot() + }) + }) + }) + + describe('with the language, show gutter with a start number', () => { + it('shows the correct line number', () => { + const view = render( + + ) + expect(view.container).toMatchSnapshot() + }) + + it('shows the correct line number and continues in another codeblock', () => { + const view = render( + + ) + expect(view.container).toMatchSnapshot() + }) + + describe('and line wrapping', () => { + it('shows the correct line number', () => { + const view = render( + + ) + expect(view.container).toMatchSnapshot() + }) + + it('shows the correct line number and continues in another codeblock', () => { + const view = render( + + ) + expect(view.container).toMatchSnapshot() + }) + }) + }) + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code.test.tsx b/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code.test.tsx new file mode 100644 index 000000000..1f01b06c7 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code.test.tsx @@ -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() + expect(await screen.findByTestId('code-highlighter')).toMatchSnapshot() + }) + + it('highlights code', async () => { + render( + + ) + expect(await screen.findByTestId('code-highlighter')).toMatchSnapshot() + }) + + it('wraps code', async () => { + render() + expect(await screen.findByTestId('code-highlighter')).toMatchSnapshot() + }) + + it('starts with a specific line', async () => { + render( + + ) + expect(await screen.findByTestId('code-highlighter')).toMatchSnapshot() + }) + + it('can hide the line numbers', async () => { + render( + + ) + expect(await screen.findByTestId('code-highlighter')).toMatchSnapshot() + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code.tsx b/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code.tsx index 992697203..6c0d4f702 100644 --- a/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code.tsx +++ b/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code.tsx @@ -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 = ({ code, language +`; diff --git a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/legacy-shortcodes-markdown-extension.test.tsx b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/legacy-shortcodes-markdown-extension.test.tsx new file mode 100644 index 000000000..15c61c6bd --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/legacy-shortcodes-markdown-extension.test.tsx @@ -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( + + ) + expect(replaceLegacyPdfShortCodeMarkdownItExtensionMock).toHaveBeenCalledWith(expect.any(MarkdownIt)) + expect(replaceLegacySlideshareShortCodeMarkdownItExtensionMock).toHaveBeenCalledWith(expect.any(MarkdownIt)) + expect(replaceLegacySpeakerdeckShortCodeMarkdownItExtensionMock).toHaveBeenCalledWith(expect.any(MarkdownIt)) + expect(view.container).toMatchSnapshot() + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-pdf-short-code.test.ts b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-pdf-short-code.test.ts new file mode 100644 index 000000000..4da87b0e8 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-pdf-short-code.test.ts @@ -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( + `https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf` + ) + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-pdf-short-code.ts b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-pdf-short-code.ts index a34376ee4..7810c16fb 100644 --- a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-pdf-short-code.ts +++ b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-pdf-short-code.ts @@ -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) => `${match}` - }) + replace: (match) => `${match}` + } as RegexOptions) } diff --git a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-slideshare-short-code.test.ts b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-slideshare-short-code.test.ts new file mode 100644 index 000000000..59d8f91e4 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-slideshare-short-code.test.ts @@ -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( + "https://www.slideshare.net/example/123456789" + ) + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-slideshare-short-code.ts b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-slideshare-short-code.ts index 38c2c285b..34aefa0c3 100644 --- a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-slideshare-short-code.ts +++ b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-slideshare-short-code.ts @@ -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) => `https://www.slideshare.net/${match}` - }) + replace: (match) => `https://www.slideshare.net/${match}` + } as RegexOptions) } diff --git a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-speakerdeck-short-code.test.ts b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-speakerdeck-short-code.test.ts new file mode 100644 index 000000000..739791a16 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-speakerdeck-short-code.test.ts @@ -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( + 'https://speakerdeck.com/example/123456789' + ) + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-speakerdeck-short-code.ts b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-speakerdeck-short-code.ts index 42892d1e3..681d0e3f4 100644 --- a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-speakerdeck-short-code.ts +++ b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-speakerdeck-short-code.ts @@ -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) => `https://speakerdeck.com/${match}` - }) + replace: (match) => `https://speakerdeck.com/${match}` + } as RegexOptions) } diff --git a/src/components/markdown-renderer/markdown-extension/plantuml/__snapshots__/plantuml-markdown-extension.test.tsx.snap b/src/components/markdown-renderer/markdown-extension/plantuml/__snapshots__/plantuml-markdown-extension.test.tsx.snap new file mode 100644 index 000000000..727040cc3 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/plantuml/__snapshots__/plantuml-markdown-extension.test.tsx.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PlantUML markdown extensions renders a plantuml codeblock 1`] = ` +
+ uml diagram + + +
+`; + +exports[`PlantUML markdown extensions renders an error if no server is defined 1`] = ` +
+

+ renderer.plantuml.notConfigured +

+
+`; diff --git a/src/components/markdown-renderer/markdown-extension/plantuml/plantuml-markdown-extension.test.tsx b/src/components/markdown-renderer/markdown-extension/plantuml/plantuml-markdown-extension.test.tsx new file mode 100644 index 000000000..4414cbf1f --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/plantuml/plantuml-markdown-extension.test.tsx @@ -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( + + ) + expect(view.container).toMatchSnapshot() + }) + + it('renders an error if no server is defined', () => { + const view = render( + + ) + expect(view.container).toMatchSnapshot() + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/vega-lite/__snapshots__/vega-lite-markdown-extension.test.tsx.snap b/src/components/markdown-renderer/markdown-extension/vega-lite/__snapshots__/vega-lite-markdown-extension.test.tsx.snap new file mode 100644 index 000000000..5c0dadf7d --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/vega-lite/__snapshots__/vega-lite-markdown-extension.test.tsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Vega-Lite markdown extensions renders a vega-lite codeblock 1`] = ` +
+
+    
+      this is a mock for vega lite
+      
+        {"$schema":"https://vega.github.io/schema/vega-lite/v4.json","data":{"values":[{"a":"","b":28}]},"mark":"bar","encoding":{"x":{"field":"a"},"y":{"field":"b"}}}
+
+      
+    
+  
+ + +
+`; diff --git a/src/components/markdown-renderer/markdown-extension/vega-lite/vega-lite-chart.tsx b/src/components/markdown-renderer/markdown-extension/vega-lite/vega-lite-chart.tsx index 49a2d7cda..772bde4c9 100644 --- a/src/components/markdown-renderer/markdown-extension/vega-lite/vega-lite-chart.tsx +++ b/src/components/markdown-renderer/markdown-extension/vega-lite/vega-lite-chart.tsx @@ -4,73 +4,64 @@ * 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 = ({ code }) => { const diagramContainer = useRef(null) - const [error, setError] = useState() const { t } = useTranslation() - const showError = useCallback((error: string) => { - if (!diagramContainer.current) { + const { + value: vegaEmbed, + error: libLoadingError, + loading: libLoading + } = useAsync(async () => (await import(/* webpackChunkName: "vega" */ 'vega-embed')).default, []) + + const { error: renderingError } = useAsync(async () => { + const container = diagramContainer.current + if (!container || !vegaEmbed) { return } - log.error(error) - setError(error) - }, []) + const spec = JSON.parse(code) as VisualizationSpec + await vegaEmbed(container, spec, { + actions: { + export: true, + source: false, + compiled: false, + editor: false + }, + i18n: { + PNG_ACTION: t('renderer.vega-lite.png'), + SVG_ACTION: t('renderer.vega-lite.svg') + } + }) + }, [code, vegaEmbed]) useEffect(() => { - if (!diagramContainer.current) { - return + if (renderingError) { + log.error('Error while rendering vega lite diagram', renderingError) } - import(/* webpackChunkName: "vega" */ 'vega-embed') - .then((embed) => { - try { - if (!diagramContainer.current) { - return - } - - const spec = JSON.parse(code) as VisualizationSpec - embed - .default(diagramContainer.current, spec, { - actions: { - export: true, - source: false, - compiled: false, - editor: false - }, - i18n: { - PNG_ACTION: t('renderer.vega-lite.png'), - SVG_ACTION: t('renderer.vega-lite.svg') - } - }) - .then(() => setError(undefined)) - .catch((error: Error) => showError(error.message)) - } catch (error) { - showError(t('renderer.vega-lite.errorJson')) - } - }) - .catch((error: Error) => { - log.error('Error while loading vega-light', error) - }) - }, [code, showError, t]) + }, [renderingError]) return ( - - - {error} + + + + +
- + ) } diff --git a/src/components/markdown-renderer/markdown-extension/vega-lite/vega-lite-markdown-extension.test.tsx b/src/components/markdown-renderer/markdown-extension/vega-lite/vega-lite-markdown-extension.test.tsx new file mode 100644 index 000000000..1d5f973c6 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/vega-lite/vega-lite-markdown-extension.test.tsx @@ -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 ( + + this is a mock for vega lite + {code} + + ) + }) as React.FC) + await mockI18n() + }) + + it('renders a vega-lite codeblock', () => { + const view = render( + + ) + expect(view.container).toMatchSnapshot() + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/vimeo/__snapshots__/vimeo-frame.test.tsx.snap b/src/components/markdown-renderer/markdown-extension/vimeo/__snapshots__/vimeo-frame.test.tsx.snap new file mode 100644 index 000000000..4557bd224 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/vimeo/__snapshots__/vimeo-frame.test.tsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VimeoFrame renders a click shield 1`] = ` +
+ + This is a click shield for + +