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}${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 {
- 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`] = `
+
+
+ common.errorWhileLoading
+
+
+`;
+
+exports[`Flowchart handles error while rendering 1`] = `
+
+
+ renderer.flowchart.invalidSyntax
+
+
+
+`;
+
+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`] = `
+
+

+
+
+
+`;
+
+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
+
+
+
+
+
+`;
diff --git a/src/components/markdown-renderer/markdown-extension/vimeo/replace-legacy-vimeo-short-code.test.ts b/src/components/markdown-renderer/markdown-extension/vimeo/replace-legacy-vimeo-short-code.test.ts
new file mode 100644
index 000000000..cd2db2a26
--- /dev/null
+++ b/src/components/markdown-renderer/markdown-extension/vimeo/replace-legacy-vimeo-short-code.test.ts
@@ -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('')
+ })
+
+ 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)
+ })
+})
diff --git a/src/components/markdown-renderer/markdown-extension/vimeo/replace-legacy-vimeo-short-code.ts b/src/components/markdown-renderer/markdown-extension/vimeo/replace-legacy-vimeo-short-code.ts
index c9e17a3d0..516c0649a 100644
--- a/src/components/markdown-renderer/markdown-extension/vimeo/replace-legacy-vimeo-short-code.ts
+++ b/src/components/markdown-renderer/markdown-extension/vimeo/replace-legacy-vimeo-short-code.ts
@@ -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)
diff --git a/src/components/markdown-renderer/markdown-extension/vimeo/replace-vimeo-link.test.ts b/src/components/markdown-renderer/markdown-extension/vimeo/replace-vimeo-link.test.ts
new file mode 100644
index 000000000..0b4eae38d
--- /dev/null
+++ b/src/components/markdown-renderer/markdown-extension/vimeo/replace-vimeo-link.test.ts
@@ -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("")
+ })
+
+ it("won't detect an URL without video id", () => {
+ expect(markdownIt.renderInline(origin)).toBe(origin)
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/src/components/markdown-renderer/markdown-extension/vimeo/replace-vimeo-link.ts b/src/components/markdown-renderer/markdown-extension/vimeo/replace-vimeo-link.ts
index 7b097fb6d..5d498d0de 100644
--- a/src/components/markdown-renderer/markdown-extension/vimeo/replace-vimeo-link.ts
+++ b/src/components/markdown-renderer/markdown-extension/vimeo/replace-vimeo-link.ts
@@ -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)
diff --git a/src/components/markdown-renderer/markdown-extension/vimeo/vimeo-frame.test.tsx b/src/components/markdown-renderer/markdown-extension/vimeo/vimeo-frame.test.tsx
new file mode 100644
index 000000000..1eaa0bfcb
--- /dev/null
+++ b/src/components/markdown-renderer/markdown-extension/vimeo/vimeo-frame.test.tsx
@@ -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 This is a click shield for {children}
+ }) as React.FC)
+ })
+
+ it('renders a click shield', () => {
+ const view = render()
+ expect(view.container).toMatchSnapshot()
+ })
+})
diff --git a/src/components/markdown-renderer/markdown-extension/vimeo/vimeo-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/vimeo/vimeo-markdown-extension.ts
index 4ccbca9ba..90244bae8 100644
--- a/src/components/markdown-renderer/markdown-extension/vimeo/vimeo-markdown-extension.ts
+++ b/src/components/markdown-renderer/markdown-extension/vimeo/vimeo-markdown-extension.ts
@@ -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[] {
diff --git a/src/components/markdown-renderer/markdown-extension/youtube/__snapshots__/youtube-frame.test.tsx.snap b/src/components/markdown-renderer/markdown-extension/youtube/__snapshots__/youtube-frame.test.tsx.snap
new file mode 100644
index 000000000..8a1b518b4
--- /dev/null
+++ b/src/components/markdown-renderer/markdown-extension/youtube/__snapshots__/youtube-frame.test.tsx.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`YoutubeFrame renders a click shield 1`] = `
+
+
+ This is a click shield for
+
+
+
+
+
+`;
diff --git a/src/components/markdown-renderer/markdown-extension/youtube/__snapshots__/youtube-markdown-extension.test.tsx.snap b/src/components/markdown-renderer/markdown-extension/youtube/__snapshots__/youtube-markdown-extension.test.tsx.snap
new file mode 100644
index 000000000..ef51b2220
--- /dev/null
+++ b/src/components/markdown-renderer/markdown-extension/youtube/__snapshots__/youtube-markdown-extension.test.tsx.snap
@@ -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`] = `
+
+
+ {%youtube a %}
+
+
+
+
+`;
+
+exports[`youtube markdown extension renders legacy youtube syntax 1`] = `
+
+
+
+ this is a mock for the youtube frame with id
+ XDnhKh5V5XQ
+
+
+
+
+
+`;
+
+exports[`youtube markdown extension renders plain youtube URLs 1`] = `
+
+
+
+ this is a mock for the youtube frame with id
+ XDnhKh5V5XQ
+
+
+
+
+
+`;
diff --git a/src/components/markdown-renderer/markdown-extension/youtube/replace-legacy-youtube-short-code.test.ts b/src/components/markdown-renderer/markdown-extension/youtube/replace-legacy-youtube-short-code.test.ts
new file mode 100644
index 000000000..1f39e3766
--- /dev/null
+++ b/src/components/markdown-renderer/markdown-extension/youtube/replace-legacy-youtube-short-code.test.ts
@@ -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('')
+ })
+
+ 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)
+ })
+})
diff --git a/src/components/markdown-renderer/markdown-extension/youtube/replace-legacy-youtube-short-code.ts b/src/components/markdown-renderer/markdown-extension/youtube/replace-legacy-youtube-short-code.ts
index dd824d8de..d7e64109a 100644
--- a/src/components/markdown-renderer/markdown-extension/youtube/replace-legacy-youtube-short-code.ts
+++ b/src/components/markdown-renderer/markdown-extension/youtube/replace-legacy-youtube-short-code.ts
@@ -6,13 +6,16 @@
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 = {
- name: 'legacy-youtube-short-code',
- regex: /^{%youtube ([^"&?\\/\s]{11}) ?%}$/,
- replace: (match) => {
- // ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
- // noinspection CheckTagEmptyBody
- return `<${YoutubeMarkdownExtension.tagName} id="${match}">${YoutubeMarkdownExtension.tagName}>`
- }
-}
+export const replaceLegacyYoutubeShortCodeMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt): void =>
+ markdownItRegex(markdownIt, {
+ name: 'legacy-youtube-short-code',
+ regex: /^{%youtube ([^"&?\\/\s]{11}) ?%}$/,
+ replace: (match) => {
+ // ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
+ // noinspection CheckTagEmptyBody
+ return `<${YoutubeMarkdownExtension.tagName} id="${match}">${YoutubeMarkdownExtension.tagName}>`
+ }
+ } as RegexOptions)
diff --git a/src/components/markdown-renderer/markdown-extension/youtube/replace-youtube-link.test.ts b/src/components/markdown-renderer/markdown-extension/youtube/replace-youtube-link.test.ts
new file mode 100644
index 000000000..d1a053782
--- /dev/null
+++ b/src/components/markdown-renderer/markdown-extension/youtube/replace-youtube-link.test.ts
@@ -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('')
+ })
+
+ it("won't detect an URL without video id", () => {
+ expect(markdownIt.renderInline(origin)).toBe(origin)
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/src/components/markdown-renderer/markdown-extension/youtube/replace-youtube-link.ts b/src/components/markdown-renderer/markdown-extension/youtube/replace-youtube-link.ts
index cd7b23dc8..f1c819929 100644
--- a/src/components/markdown-renderer/markdown-extension/youtube/replace-youtube-link.ts
+++ b/src/components/markdown-renderer/markdown-extension/youtube/replace-youtube-link.ts
@@ -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,12 +19,13 @@ const youtubeVideoUrlRegex = new RegExp(
)
const linkRegex = new RegExp(`^${youtubeVideoUrlRegex.source}$`, 'i')
-export const replaceYouTubeLink: RegexOptions = {
- name: 'youtube-link',
- regex: linkRegex,
- replace: (match) => {
- // ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
- // noinspection CheckTagEmptyBody
- return `<${YoutubeMarkdownExtension.tagName} id="${match}">${YoutubeMarkdownExtension.tagName}>`
- }
-}
+export const replaceYouTubeLinkMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt) =>
+ markdownItRegex(markdownIt, {
+ name: 'youtube-link',
+ regex: linkRegex,
+ replace: (match) => {
+ // ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
+ // noinspection CheckTagEmptyBody
+ return `<${YoutubeMarkdownExtension.tagName} id="${match}">${YoutubeMarkdownExtension.tagName}>`
+ }
+ } as RegexOptions)
diff --git a/src/components/markdown-renderer/markdown-extension/youtube/youtube-frame.test.tsx b/src/components/markdown-renderer/markdown-extension/youtube/youtube-frame.test.tsx
new file mode 100644
index 000000000..d560f0f41
--- /dev/null
+++ b/src/components/markdown-renderer/markdown-extension/youtube/youtube-frame.test.tsx
@@ -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 This is a click shield for {children}
+ }) as React.FC)
+ })
+
+ it('renders a click shield', () => {
+ const view = render()
+ expect(view.container).toMatchSnapshot()
+ })
+})
diff --git a/src/components/markdown-renderer/markdown-extension/youtube/youtube-markdown-extension.test.tsx b/src/components/markdown-renderer/markdown-extension/youtube/youtube-markdown-extension.test.tsx
new file mode 100644
index 000000000..58787633b
--- /dev/null
+++ b/src/components/markdown-renderer/markdown-extension/youtube/youtube-markdown-extension.test.tsx
@@ -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 }) => (
+ this is a mock for the youtube frame with id {id}
+ )) as React.FC)
+ await mockI18n()
+ })
+
+ afterAll(() => {
+ jest.resetAllMocks()
+ jest.resetModules()
+ })
+
+ it('renders plain youtube URLs', () => {
+ const view = render(
+
+ )
+ expect(replaceYouTubeLinkMarkdownItPluginSpy).toHaveBeenCalledWith(expect.any(MarkdownIt))
+ expect(replaceLegacyYoutubeShortCodeMarkdownItPluginSpy).toHaveBeenCalledWith(expect.any(MarkdownIt))
+ expect(view.container).toMatchSnapshot()
+ })
+
+ it('renders legacy youtube syntax', () => {
+ const view = render(
+
+ )
+ 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(
+
+ )
+ expect(replaceYouTubeLinkMarkdownItPluginSpy).toHaveBeenCalledWith(expect.any(MarkdownIt))
+ expect(replaceLegacyYoutubeShortCodeMarkdownItPluginSpy).toHaveBeenCalledWith(expect.any(MarkdownIt))
+ expect(view.container).toMatchSnapshot()
+ })
+})
diff --git a/src/components/markdown-renderer/markdown-extension/youtube/youtube-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/youtube/youtube-markdown-extension.ts
index 4259a2c7a..09b77db47 100644
--- a/src/components/markdown-renderer/markdown-extension/youtube/youtube-markdown-extension.ts
+++ b/src/components/markdown-renderer/markdown-extension/youtube/youtube-markdown-extension.ts
@@ -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[] {
diff --git a/src/components/markdown-renderer/replace-components/click-shield/click-shield.tsx b/src/components/markdown-renderer/replace-components/click-shield/click-shield.tsx
index c1b2d76cd..f019be895 100644
--- a/src/components/markdown-renderer/replace-components/click-shield/click-shield.tsx
+++ b/src/components/markdown-renderer/replace-components/click-shield/click-shield.tsx
@@ -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 {
onImageFetch?: () => Promise
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> = ({
+export const ClickShield: React.FC = ({
containerClassName,
onImageFetch,
fallbackPreviewImageUrl,
diff --git a/src/components/markdown-renderer/test-utils/test-markdown-renderer.tsx b/src/components/markdown-renderer/test-utils/test-markdown-renderer.tsx
new file mode 100644
index 000000000..688cd3955
--- /dev/null
+++ b/src/components/markdown-renderer/test-utils/test-markdown-renderer.tsx
@@ -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 = ({ content, extensions }) => {
+ const lines = useMemo(() => content.split('\n'), [content])
+ const dom = useConvertMarkdownToReactDom(lines, extensions, true, false)
+
+ return {dom}
+}
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 8ec38a1a3..884a9191d 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -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 = ({ Component, pageProps }: AppProps) => {
return (
-
+
-
+
)
}
diff --git a/src/redux/store-provider.tsx b/src/redux/store-provider.tsx
new file mode 100644
index 000000000..5541d6d99
--- /dev/null
+++ b/src/redux/store-provider.tsx
@@ -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> = ({ children }) => {
+ return {children}
+}