From ec77e672f61d7c1f941b50d27a80e9bf872c4624 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Mon, 25 Oct 2021 00:13:40 +0200 Subject: [PATCH] Refactor replacers and line id mapping Signed-off-by: Tilman Vatteroth --- package.json | 1 + .../document-markdown-renderer.tsx | 9 +- .../hooks/use-component-replacers.ts | 50 ++--- .../use-convert-markdown-to-react-dom.ts | 38 ++-- .../document-markdown-it-configurator.ts | 26 +++ ...gurator.ts => markdown-it-configurator.ts} | 52 ++---- .../slideshow-markdown-it-configurator.ts | 17 ++ .../markdown-it-plugins/alert-container.ts | 2 +- .../markdown-it-plugins/plantuml.ts | 2 +- .../markdown-it-plugins/spoiler-container.ts | 2 +- .../replace-components/abc/abc-frame.tsx | 11 +- .../replace-components/abc/abc-replacer.tsx | 32 ---- .../asciinema/asciinema-frame.tsx | 7 +- .../asciinema/asciinema-replacer.tsx | 31 ---- .../asciinema/replace-asciinema-link.ts | 6 + .../code-block-component-replacer.ts | 42 +++++ .../colored-blockquote-replacer.tsx | 6 +- ...onentReplacer.ts => component-replacer.ts} | 14 +- .../replace-components/csv/csv-replacer.tsx | 17 +- .../custom-tag-with-id-component-replacer.ts | 41 +++++ .../flow/flowchart-replacer.tsx | 32 ---- .../flow/{flowchart => }/flowchart.tsx | 10 +- .../replace-components/gist/gist-frame.tsx | 13 +- .../gist/gist-markdown-it-plugin.ts | 15 ++ .../replace-components/gist/gist-replacer.tsx | 43 ----- .../graphviz/graphviz-frame.tsx | 7 +- .../graphviz/graphviz-replacer.tsx | 32 ---- .../highlighted-fence-replacer.tsx | 20 +- .../image/image-replacer.tsx | 6 +- .../katex/katex-replacer.tsx | 4 +- .../linemarker/linemarker-replacer.tsx | 4 +- .../link-replacer/link-replacer.tsx | 6 +- .../markmap/markmap-frame.tsx | 7 +- .../markmap/markmap-replacer.tsx | 32 ---- .../mermaid/mermaid-chart.tsx | 6 +- .../mermaid/mermaid-replacer.tsx | 32 ---- .../sequence-diagram-replacer.tsx | 39 ---- .../sequence-diagram/sequence-diagram.tsx | 24 +++ .../task-list/task-list-checkbox.tsx | 41 +++++ .../task-list/task-list-replacer.tsx | 35 ++-- .../replace-components/utils.ts | 14 -- .../vega-lite/vega-chart.tsx | 7 +- .../vega-lite/vega-replacer.tsx | 32 ---- .../replace-components/vimeo/vimeo-frame.tsx | 7 +- .../vimeo/vimeo-markdown-it-plugin.ts | 15 ++ .../vimeo/vimeo-replacer.tsx | 33 ---- .../youtube/youtube-frame.tsx | 7 +- .../youtube/youtube-markdown-it-plugin.ts | 15 ++ .../youtube/youtube-replacer.tsx | 33 ---- .../slideshow-markdown-renderer.tsx | 10 +- src/components/markdown-renderer/types.d.ts | 2 +- .../utils/html-react-transformer.tsx | 117 ------------ .../utils/line-id-mapper.test.ts | 110 +++++++++++ .../markdown-renderer/utils/line-id-mapper.ts | 108 +++++++++++ .../utils/line-number-mapping.ts | 40 ---- .../utils/node-to-react-transformer.test.tsx | 106 +++++++++++ .../utils/node-to-react-transformer.tsx | 174 ++++++++++++++++++ yarn.lock | 5 + 58 files changed, 899 insertions(+), 750 deletions(-) create mode 100644 src/components/markdown-renderer/markdown-it-configurator/document-markdown-it-configurator.ts rename src/components/markdown-renderer/markdown-it-configurator/{basic-markdown-it-configurator.ts => markdown-it-configurator.ts} (64%) create mode 100644 src/components/markdown-renderer/markdown-it-configurator/slideshow-markdown-it-configurator.ts delete mode 100644 src/components/markdown-renderer/replace-components/abc/abc-replacer.tsx delete mode 100644 src/components/markdown-renderer/replace-components/asciinema/asciinema-replacer.tsx create mode 100644 src/components/markdown-renderer/replace-components/code-block-component-replacer.ts rename src/components/markdown-renderer/replace-components/{ComponentReplacer.ts => component-replacer.ts} (78%) create mode 100644 src/components/markdown-renderer/replace-components/custom-tag-with-id-component-replacer.ts delete mode 100644 src/components/markdown-renderer/replace-components/flow/flowchart-replacer.tsx rename src/components/markdown-renderer/replace-components/flow/{flowchart => }/flowchart.tsx (85%) create mode 100644 src/components/markdown-renderer/replace-components/gist/gist-markdown-it-plugin.ts delete mode 100644 src/components/markdown-renderer/replace-components/gist/gist-replacer.tsx delete mode 100644 src/components/markdown-renderer/replace-components/graphviz/graphviz-replacer.tsx delete mode 100644 src/components/markdown-renderer/replace-components/markmap/markmap-replacer.tsx delete mode 100644 src/components/markdown-renderer/replace-components/mermaid/mermaid-replacer.tsx delete mode 100644 src/components/markdown-renderer/replace-components/sequence-diagram/sequence-diagram-replacer.tsx create mode 100644 src/components/markdown-renderer/replace-components/sequence-diagram/sequence-diagram.tsx create mode 100644 src/components/markdown-renderer/replace-components/task-list/task-list-checkbox.tsx delete mode 100644 src/components/markdown-renderer/replace-components/utils.ts delete mode 100644 src/components/markdown-renderer/replace-components/vega-lite/vega-replacer.tsx create mode 100644 src/components/markdown-renderer/replace-components/vimeo/vimeo-markdown-it-plugin.ts delete mode 100644 src/components/markdown-renderer/replace-components/vimeo/vimeo-replacer.tsx create mode 100644 src/components/markdown-renderer/replace-components/youtube/youtube-markdown-it-plugin.ts delete mode 100644 src/components/markdown-renderer/replace-components/youtube/youtube-replacer.tsx delete mode 100644 src/components/markdown-renderer/utils/html-react-transformer.tsx create mode 100644 src/components/markdown-renderer/utils/line-id-mapper.test.ts create mode 100644 src/components/markdown-renderer/utils/line-id-mapper.ts delete mode 100644 src/components/markdown-renderer/utils/line-number-mapping.ts create mode 100644 src/components/markdown-renderer/utils/node-to-react-transformer.test.tsx create mode 100644 src/components/markdown-renderer/utils/node-to-react-transformer.tsx diff --git a/package.json b/package.json index fde116822..147e6259a 100644 --- a/package.json +++ b/package.json @@ -138,6 +138,7 @@ "markmap-view": "0.2.6", "@matejmazur/react-katex": "3.1.3", "mermaid": "8.13.3", + "optional-js": "2.3.0", "prettier": "2.4.1", "react": "17.0.2", "react-bootstrap": "1.6.4", diff --git a/src/components/markdown-renderer/document-markdown-renderer.tsx b/src/components/markdown-renderer/document-markdown-renderer.tsx index b49df42fe..3fa2b7647 100644 --- a/src/components/markdown-renderer/document-markdown-renderer.tsx +++ b/src/components/markdown-renderer/document-markdown-renderer.tsx @@ -16,9 +16,9 @@ import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-po import { useExtractFirstHeadline } from './hooks/use-extract-first-headline' import type { TocAst } from 'markdown-it-toc-done-right' import { useOnRefChange } from './hooks/use-on-ref-change' -import { BasicMarkdownItConfigurator } from './markdown-it-configurator/basic-markdown-it-configurator' import { useTrimmedContent } from './hooks/use-trimmed-content' import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props' +import { DocumentMarkdownItConfigurator } from './markdown-it-configurator/document-markdown-it-configurator' export interface DocumentMarkdownRendererProps extends CommonMarkdownRendererProps { onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void @@ -44,15 +44,14 @@ export const DocumentMarkdownRenderer: React.FC = const markdownIt = useMemo( () => - new BasicMarkdownItConfigurator({ - onToc: (toc) => (tocAst.current = toc), + new DocumentMarkdownItConfigurator({ + onTocChange: (toc) => (tocAst.current = toc), onLineMarkers: onLineMarkerPositionChanged === undefined ? undefined : (lineMarkers) => (currentLineMarkers.current = lineMarkers), useAlternativeBreaks, - lineOffset, - headlineAnchors: true + lineOffset }).buildConfiguredMarkdownIt(), [onLineMarkerPositionChanged, useAlternativeBreaks, lineOffset] ) diff --git a/src/components/markdown-renderer/hooks/use-component-replacers.ts b/src/components/markdown-renderer/hooks/use-component-replacers.ts index 78a94faeb..a654b90bf 100644 --- a/src/components/markdown-renderer/hooks/use-component-replacers.ts +++ b/src/components/markdown-renderer/hooks/use-component-replacers.ts @@ -5,28 +5,30 @@ */ import { useMemo } from 'react' -import { AbcReplacer } from '../replace-components/abc/abc-replacer' -import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer' -import type { ComponentReplacer } from '../replace-components/ComponentReplacer' +import type { ComponentReplacer } from '../replace-components/component-replacer' import { CsvReplacer } from '../replace-components/csv/csv-replacer' -import { FlowchartReplacer } from '../replace-components/flow/flowchart-replacer' -import { GistReplacer } from '../replace-components/gist/gist-replacer' -import { GraphvizReplacer } from '../replace-components/graphviz/graphviz-replacer' import { HighlightedCodeReplacer } from '../replace-components/highlighted-fence/highlighted-fence-replacer' import type { ImageClickHandler } from '../replace-components/image/image-replacer' import { ImageReplacer } from '../replace-components/image/image-replacer' import { KatexReplacer } from '../replace-components/katex/katex-replacer' import { LinemarkerReplacer } from '../replace-components/linemarker/linemarker-replacer' import { LinkReplacer } from '../replace-components/link-replacer/link-replacer' -import { MarkmapReplacer } from '../replace-components/markmap/markmap-replacer' -import { MermaidReplacer } from '../replace-components/mermaid/mermaid-replacer' import { ColoredBlockquoteReplacer } from '../replace-components/colored-blockquote/colored-blockquote-replacer' -import { SequenceDiagramReplacer } from '../replace-components/sequence-diagram/sequence-diagram-replacer' import type { TaskCheckedChangeHandler } from '../replace-components/task-list/task-list-replacer' import { TaskListReplacer } from '../replace-components/task-list/task-list-replacer' -import { VegaReplacer } from '../replace-components/vega-lite/vega-replacer' -import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer' -import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer' +import { CodeBlockComponentReplacer } from '../replace-components/code-block-component-replacer' +import { GraphvizFrame } from '../replace-components/graphviz/graphviz-frame' +import { MarkmapFrame } from '../replace-components/markmap/markmap-frame' +import { VegaChart } from '../replace-components/vega-lite/vega-chart' +import { MermaidChart } from '../replace-components/mermaid/mermaid-chart' +import { FlowChart } from '../replace-components/flow/flowchart' +import { SequenceDiagram } from '../replace-components/sequence-diagram/sequence-diagram' +import { AbcFrame } from '../replace-components/abc/abc-frame' +import { CustomTagWithIdComponentReplacer } from '../replace-components/custom-tag-with-id-component-replacer' +import { GistFrame } from '../replace-components/gist/gist-frame' +import { YouTubeFrame } from '../replace-components/youtube/youtube-frame' +import { VimeoFrame } from '../replace-components/vimeo/vimeo-frame' +import { AsciinemaFrame } from '../replace-components/asciinema/asciinema-frame' /** * Provides a function that creates a list of {@link ComponentReplacer component replacer} instances. @@ -47,23 +49,23 @@ export const useComponentReplacers = ( useMemo( () => [ new LinemarkerReplacer(), - new GistReplacer(), - new YoutubeReplacer(), - new VimeoReplacer(), - new AsciinemaReplacer(), - new AbcReplacer(), + new CustomTagWithIdComponentReplacer(GistFrame, 'gist'), + new CustomTagWithIdComponentReplacer(YouTubeFrame, 'youtube'), + new CustomTagWithIdComponentReplacer(VimeoFrame, 'vimeo'), + new CustomTagWithIdComponentReplacer(AsciinemaFrame, 'asciinema'), new ImageReplacer(onImageClick), - new SequenceDiagramReplacer(), new CsvReplacer(), - new FlowchartReplacer(), - new MermaidReplacer(), - new GraphvizReplacer(), - new MarkmapReplacer(), - new VegaReplacer(), + new CodeBlockComponentReplacer(AbcFrame, 'abc'), + new CodeBlockComponentReplacer(SequenceDiagram, 'sequence'), + new CodeBlockComponentReplacer(FlowChart, 'flow'), + new CodeBlockComponentReplacer(MermaidChart, 'mermaid'), + new CodeBlockComponentReplacer(GraphvizFrame, 'graphviz'), + new CodeBlockComponentReplacer(MarkmapFrame, 'markmap'), + new CodeBlockComponentReplacer(VegaChart, 'vega-lite'), new HighlightedCodeReplacer(), new ColoredBlockquoteReplacer(), new KatexReplacer(), - new TaskListReplacer(onTaskCheckedChange, frontmatterLinesToSkip), + new TaskListReplacer(frontmatterLinesToSkip, onTaskCheckedChange), new LinkReplacer(baseUrl) ], [onImageClick, onTaskCheckedChange, baseUrl, frontmatterLinesToSkip] diff --git a/src/components/markdown-renderer/hooks/use-convert-markdown-to-react-dom.ts b/src/components/markdown-renderer/hooks/use-convert-markdown-to-react-dom.ts index c0a67ca0b..0803e3fa7 100644 --- a/src/components/markdown-renderer/hooks/use-convert-markdown-to-react-dom.ts +++ b/src/components/markdown-renderer/hooks/use-convert-markdown-to-react-dom.ts @@ -5,13 +5,12 @@ */ import type MarkdownIt from 'markdown-it/lib' -import { useMemo, useRef } from 'react' -import type { ComponentReplacer, ValidReactDomElement } from '../replace-components/ComponentReplacer' -import type { LineKeys } from '../types' -import { buildTransformer } from '../utils/html-react-transformer' -import { calculateNewLineNumberMapping } from '../utils/line-number-mapping' +import { useMemo } from 'react' +import type { ComponentReplacer, ValidReactDomElement } from '../replace-components/component-replacer' import convertHtmlToReact from '@hedgedoc/html-to-react' import type { Document } from 'domhandler' +import { NodeToReactTransformer } from '../utils/node-to-react-transformer' +import { LineIdMapper } from '../utils/line-id-mapper' /** * Renders markdown code into react elements @@ -28,21 +27,22 @@ export const useConvertMarkdownToReactDom = ( replacers: ComponentReplacer[], preprocessNodes?: (nodes: Document) => Document ): ValidReactDomElement[] => { - const oldMarkdownLineKeys = useRef() - const lastUsedLineId = useRef(0) + const lineNumberMapper = useMemo(() => new LineIdMapper(), []) + const htmlToReactTransformer = useMemo(() => new NodeToReactTransformer(), []) + + useMemo(() => { + htmlToReactTransformer.setReplacers(replacers) + }, [htmlToReactTransformer, replacers]) + + useMemo(() => { + htmlToReactTransformer.setLineIds(lineNumberMapper.updateLineMapping(markdownCode)) + }, [htmlToReactTransformer, lineNumberMapper, markdownCode]) return useMemo(() => { const html = markdownIt.render(markdownCode) - const contentLines = markdownCode.split('\n') - const { lines: newLines, lastUsedLineId: newLastUsedLineId } = calculateNewLineNumberMapping( - contentLines, - oldMarkdownLineKeys.current ?? [], - lastUsedLineId.current - ) - oldMarkdownLineKeys.current = newLines - lastUsedLineId.current = newLastUsedLineId - - const transformer = replacers.length > 0 ? buildTransformer(newLines, replacers) : undefined - return convertHtmlToReact(html, { transform: transformer, preprocessNodes: preprocessNodes }) - }, [markdownIt, markdownCode, replacers, preprocessNodes]) + return convertHtmlToReact(html, { + transform: (node, index) => htmlToReactTransformer.translateNodeToReactElement(node, index), + preprocessNodes: preprocessNodes + }) + }, [htmlToReactTransformer, markdownCode, markdownIt, preprocessNodes]) } diff --git a/src/components/markdown-renderer/markdown-it-configurator/document-markdown-it-configurator.ts b/src/components/markdown-renderer/markdown-it-configurator/document-markdown-it-configurator.ts new file mode 100644 index 000000000..79a689a47 --- /dev/null +++ b/src/components/markdown-renderer/markdown-it-configurator/document-markdown-it-configurator.ts @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { Configuration } from './markdown-it-configurator' +import { MarkdownItConfigurator } from './markdown-it-configurator' +import { headlineAnchors } from '../markdown-it-plugins/headline-anchors' +import type { LineMarkers } from '../replace-components/linemarker/line-number-marker' +import { lineNumberMarker } from '../replace-components/linemarker/line-number-marker' + +export interface DocumentConfiguration extends Configuration { + onLineMarkers?: (lineMarkers: LineMarkers[]) => void +} + +export class DocumentMarkdownItConfigurator extends MarkdownItConfigurator { + protected configure(): void { + super.configure() + + this.configurations.push(headlineAnchors) + if (this.options.onLineMarkers) { + this.configurations.push(lineNumberMarker(this.options.onLineMarkers, this.options.lineOffset ?? 0)) + } + } +} diff --git a/src/components/markdown-renderer/markdown-it-configurator/basic-markdown-it-configurator.ts b/src/components/markdown-renderer/markdown-it-configurator/markdown-it-configurator.ts similarity index 64% rename from src/components/markdown-renderer/markdown-it-configurator/basic-markdown-it-configurator.ts rename to src/components/markdown-renderer/markdown-it-configurator/markdown-it-configurator.ts index eba2df7ce..662068986 100644 --- a/src/components/markdown-renderer/markdown-it-configurator/basic-markdown-it-configurator.ts +++ b/src/components/markdown-renderer/markdown-it-configurator/markdown-it-configurator.ts @@ -20,45 +20,34 @@ import { spoilerContainer } from '../markdown-it-plugins/spoiler-container' import { tasksLists } from '../markdown-it-plugins/tasks-lists' import { twitterEmojis } from '../markdown-it-plugins/twitter-emojis' import type { TocAst } from 'markdown-it-toc-done-right' -import type { LineMarkers } from '../replace-components/linemarker/line-number-marker' -import { lineNumberMarker } from '../replace-components/linemarker/line-number-marker' import { plantumlWithError } from '../markdown-it-plugins/plantuml' -import { headlineAnchors } from '../markdown-it-plugins/headline-anchors' import { KatexReplacer } from '../replace-components/katex/katex-replacer' -import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer' -import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer' -import { GistReplacer } from '../replace-components/gist/gist-replacer' import { legacyPdfShortCode } from '../regex-plugins/replace-legacy-pdf-short-code' import { legacySlideshareShortCode } from '../regex-plugins/replace-legacy-slideshare-short-code' import { legacySpeakerdeckShortCode } from '../regex-plugins/replace-legacy-speakerdeck-short-code' -import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer' import { highlightedCode } from '../markdown-it-plugins/highlighted-code' import { quoteExtraColor } from '../markdown-it-plugins/quote-extra-color' import { quoteExtra } from '../markdown-it-plugins/quote-extra' import { documentTableOfContents } from '../markdown-it-plugins/document-table-of-contents' -import { addSlideSectionsMarkdownItPlugin } from '../markdown-it-plugins/reveal-sections' +import { youtubeMarkdownItPlugin } from '../replace-components/youtube/youtube-markdown-it-plugin' +import { vimeoMarkdownItPlugin } from '../replace-components/vimeo/vimeo-markdown-it-plugin' +import { gistMarkdownItPlugin } from '../replace-components/gist/gist-markdown-it-plugin' +import { asciinemaMarkdownItPlugin } from '../replace-components/asciinema/replace-asciinema-link' -export interface ConfiguratorDetails { - onToc: (toc: TocAst) => void - onLineMarkers?: (lineMarkers: LineMarkers[]) => void +export interface Configuration { + onTocChange: (toc: TocAst) => void useAlternativeBreaks?: boolean lineOffset?: number - headlineAnchors?: boolean - slideSections?: boolean } -export class BasicMarkdownItConfigurator { +export abstract class MarkdownItConfigurator { protected readonly options: T protected configurations: MarkdownIt.PluginSimple[] = [] protected postConfigurations: MarkdownIt.PluginSimple[] = [] constructor(options: T) { this.options = options - } - - public pushConfig(plugin: MarkdownIt.PluginSimple): this { - this.configurations.push(plugin) - return this + this.configure() } public buildConfiguredMarkdownIt(): MarkdownIt { @@ -68,28 +57,27 @@ export class BasicMarkdownItConfigurator { langPrefix: '', typographer: true }) - this.configure(markdownIt) this.configurations.forEach((configuration) => markdownIt.use(configuration)) this.postConfigurations.forEach((postConfiguration) => markdownIt.use(postConfiguration)) return markdownIt } - protected configure(markdownIt: MarkdownIt): void { + protected configure(): void { this.configurations.push( plantumlWithError, KatexReplacer.markdownItPlugin, - YoutubeReplacer.markdownItPlugin, - VimeoReplacer.markdownItPlugin, - GistReplacer.markdownItPlugin, + youtubeMarkdownItPlugin, + vimeoMarkdownItPlugin, + gistMarkdownItPlugin, + asciinemaMarkdownItPlugin, legacyPdfShortCode, legacySlideshareShortCode, legacySpeakerdeckShortCode, - AsciinemaReplacer.markdownItPlugin, highlightedCode, quoteExtraColor, quoteExtra('name', 'user'), quoteExtra('time', 'clock-o'), - documentTableOfContents(this.options.onToc), + documentTableOfContents(this.options.onTocChange), twitterEmojis, abbreviation, definitionList, @@ -104,18 +92,6 @@ export class BasicMarkdownItConfigurator { spoilerContainer ) - if (this.options.headlineAnchors) { - this.configurations.push(headlineAnchors) - } - - if (this.options.slideSections) { - this.configurations.push(addSlideSectionsMarkdownItPlugin) - } - - if (this.options.onLineMarkers) { - this.configurations.push(lineNumberMarker(this.options.onLineMarkers, this.options.lineOffset ?? 0)) - } - this.postConfigurations.push(linkifyExtra, MarkdownItParserDebugger) } } diff --git a/src/components/markdown-renderer/markdown-it-configurator/slideshow-markdown-it-configurator.ts b/src/components/markdown-renderer/markdown-it-configurator/slideshow-markdown-it-configurator.ts new file mode 100644 index 000000000..5d5369329 --- /dev/null +++ b/src/components/markdown-renderer/markdown-it-configurator/slideshow-markdown-it-configurator.ts @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { Configuration } from './markdown-it-configurator' +import { MarkdownItConfigurator } from './markdown-it-configurator' +import { addSlideSectionsMarkdownItPlugin } from '../markdown-it-plugins/reveal-sections' + +export class SlideshowMarkdownItConfigurator extends MarkdownItConfigurator { + protected configure(): void { + super.configure() + + this.configurations.push(addSlideSectionsMarkdownItPlugin) + } +} diff --git a/src/components/markdown-renderer/markdown-it-plugins/alert-container.ts b/src/components/markdown-renderer/markdown-it-plugins/alert-container.ts index 7f0f66d3d..22a99af5b 100644 --- a/src/components/markdown-renderer/markdown-it-plugins/alert-container.ts +++ b/src/components/markdown-renderer/markdown-it-plugins/alert-container.ts @@ -8,7 +8,7 @@ import type MarkdownIt from 'markdown-it' import markdownItContainer from 'markdown-it-container' import type Renderer from 'markdown-it/lib/renderer' import type Token from 'markdown-it/lib/token' -import type { MarkdownItPlugin } from '../replace-components/ComponentReplacer' +import type { MarkdownItPlugin } from '../replace-components/component-replacer' export type RenderContainerReturn = ( tokens: Token[], diff --git a/src/components/markdown-renderer/markdown-it-plugins/plantuml.ts b/src/components/markdown-renderer/markdown-it-plugins/plantuml.ts index 0710e2733..0df1d36b3 100644 --- a/src/components/markdown-renderer/markdown-it-plugins/plantuml.ts +++ b/src/components/markdown-renderer/markdown-it-plugins/plantuml.ts @@ -11,7 +11,7 @@ import type { RenderRule } from 'markdown-it/lib/renderer' import type Renderer from 'markdown-it/lib/renderer' import type Token from 'markdown-it/lib/token' import { store } from '../../../redux' -import type { MarkdownItPlugin } from '../replace-components/ComponentReplacer' +import type { MarkdownItPlugin } from '../replace-components/component-replacer' export const plantumlWithError: MarkdownItPlugin = (markdownIt: MarkdownIt) => { const plantumlServer = store.getState().config.plantumlServer diff --git a/src/components/markdown-renderer/markdown-it-plugins/spoiler-container.ts b/src/components/markdown-renderer/markdown-it-plugins/spoiler-container.ts index 10a36f13f..6ac62fc29 100644 --- a/src/components/markdown-renderer/markdown-it-plugins/spoiler-container.ts +++ b/src/components/markdown-renderer/markdown-it-plugins/spoiler-container.ts @@ -8,7 +8,7 @@ import type MarkdownIt from 'markdown-it' import { escapeHtml } from 'markdown-it/lib/common/utils' import markdownItContainer from 'markdown-it-container' import type Token from 'markdown-it/lib/token' -import type { MarkdownItPlugin } from '../replace-components/ComponentReplacer' +import type { MarkdownItPlugin } from '../replace-components/component-replacer' import type { RenderContainerReturn } from './alert-container' export const spoilerRegEx = /^spoiler\s+(.*)$/ diff --git a/src/components/markdown-renderer/replace-components/abc/abc-frame.tsx b/src/components/markdown-renderer/replace-components/abc/abc-frame.tsx index 1305023a1..1c92a6a75 100644 --- a/src/components/markdown-renderer/replace-components/abc/abc-frame.tsx +++ b/src/components/markdown-renderer/replace-components/abc/abc-frame.tsx @@ -7,14 +7,11 @@ import React, { useEffect, useRef } from 'react' import './abc.scss' import { Logger } from '../../../../utils/logger' +import type { CodeProps } from '../code-block-component-replacer' const log = new Logger('AbcFrame') -export interface AbcFrameProps { - code: string -} - -export const AbcFrame: React.FC = ({ code }) => { +export const AbcFrame: React.FC = ({ code }) => { const container = useRef(null) useEffect(() => { @@ -23,8 +20,8 @@ export const AbcFrame: React.FC = ({ code }) => { } const actualContainer = container.current import(/* webpackChunkName: "abc.js" */ 'abcjs') - .then((imp) => { - imp.renderAbc(actualContainer, code, {}) + .then((importedLibrary) => { + importedLibrary.renderAbc(actualContainer, code, {}) }) .catch((error: Error) => { log.error('Error while loading abcjs', error) diff --git a/src/components/markdown-renderer/replace-components/abc/abc-replacer.tsx b/src/components/markdown-renderer/replace-components/abc/abc-replacer.tsx deleted file mode 100644 index de963ccdc..000000000 --- a/src/components/markdown-renderer/replace-components/abc/abc-replacer.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { Element } from 'domhandler' -import React from 'react' -import { ComponentReplacer } from '../ComponentReplacer' -import { AbcFrame } from './abc-frame' - -/** - * Detects code blocks with "abc" as language and renders them as ABC.js - */ -export class AbcReplacer extends ComponentReplacer { - getReplacement(codeNode: Element): React.ReactElement | undefined { - if ( - codeNode.name !== 'code' || - !codeNode.attribs || - !codeNode.attribs['data-highlight-language'] || - codeNode.attribs['data-highlight-language'] !== 'abc' || - !codeNode.children || - !codeNode.children[0] - ) { - return - } - - const code = ComponentReplacer.extractTextChildContent(codeNode) - - return - } -} diff --git a/src/components/markdown-renderer/replace-components/asciinema/asciinema-frame.tsx b/src/components/markdown-renderer/replace-components/asciinema/asciinema-frame.tsx index ddf69ed41..bf6363df8 100644 --- a/src/components/markdown-renderer/replace-components/asciinema/asciinema-frame.tsx +++ b/src/components/markdown-renderer/replace-components/asciinema/asciinema-frame.tsx @@ -6,12 +6,9 @@ import React from 'react' import { OneClickEmbedding } from '../one-click-frame/one-click-embedding' +import type { IdProps } from '../custom-tag-with-id-component-replacer' -export interface AsciinemaFrameProps { - id: string -} - -export const AsciinemaFrame: React.FC = ({ id }) => { +export const AsciinemaFrame: React.FC = ({ id }) => { return ( { - markdownItRegex(markdownIt, replaceAsciinemaLink) - } - - public getReplacement(node: Element): React.ReactElement | undefined { - const attributes = getAttributesFromHedgeDocTag(node, 'asciinema') - if (attributes && attributes.id) { - const asciinemaId = attributes.id - return - } - } -} diff --git a/src/components/markdown-renderer/replace-components/asciinema/replace-asciinema-link.ts b/src/components/markdown-renderer/replace-components/asciinema/replace-asciinema-link.ts index afa2c74a2..d5b1a75c1 100644 --- a/src/components/markdown-renderer/replace-components/asciinema/replace-asciinema-link.ts +++ b/src/components/markdown-renderer/replace-components/asciinema/replace-asciinema-link.ts @@ -5,6 +5,8 @@ */ import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' +import type MarkdownIt from 'markdown-it/lib' +import markdownItRegex from 'markdown-it-regex' const protocolRegex = /(?:http(?:s)?:\/\/)?/ const domainRegex = /(?:asciinema\.org\/a\/)/ @@ -13,6 +15,10 @@ const tailRegex = /(?:[./?#].*)?/ const gistUrlRegex = new RegExp(`(?:${protocolRegex.source}${domainRegex.source}${idRegex.source}${tailRegex.source})`) const linkRegex = new RegExp(`^${gistUrlRegex.source}$`, 'i') +export const asciinemaMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt) => { + markdownItRegex(markdownIt, replaceAsciinemaLink) +} + export const replaceAsciinemaLink: RegexOptions = { name: 'asciinema-link', regex: linkRegex, diff --git a/src/components/markdown-renderer/replace-components/code-block-component-replacer.ts b/src/components/markdown-renderer/replace-components/code-block-component-replacer.ts new file mode 100644 index 000000000..bd294a710 --- /dev/null +++ b/src/components/markdown-renderer/replace-components/code-block-component-replacer.ts @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { ValidReactDomElement } from './component-replacer' +import { ComponentReplacer } from './component-replacer' +import type { FunctionComponent } from 'react' +import React from 'react' +import type { Element } from 'domhandler' + +export interface CodeProps { + code: string +} + +/** + * Checks if the given checked node is a code block with a specific language attribute and creates an react-element that receives the code. + */ +export class CodeBlockComponentReplacer extends ComponentReplacer { + constructor(private component: FunctionComponent, private language: string) { + super() + } + + replace(node: Element): ValidReactDomElement | undefined { + const code = CodeBlockComponentReplacer.extractTextFromCodeNode(node, this.language) + return code ? React.createElement(this.component, { code: code }) : undefined + } + + /** + * Extracts the text content if the given {@link Element} is a code block with a specific language. + * + * @param element The {@link Element} to check. + * @param language The language that code block should be assigned to. + * @return The text content or undefined if the element isn't a code block or has the wrong language attribute. + */ + public static extractTextFromCodeNode(element: Element, language: string): string | undefined { + return element.name === 'code' && element.attribs['data-highlight-language'] === language && element.children[0] + ? ComponentReplacer.extractTextChildContent(element) + : undefined + } +} diff --git a/src/components/markdown-renderer/replace-components/colored-blockquote/colored-blockquote-replacer.tsx b/src/components/markdown-renderer/replace-components/colored-blockquote/colored-blockquote-replacer.tsx index 7440c4e92..16e2e26f5 100644 --- a/src/components/markdown-renderer/replace-components/colored-blockquote/colored-blockquote-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/colored-blockquote/colored-blockquote-replacer.tsx @@ -6,8 +6,8 @@ import type { Element } from 'domhandler' import { isTag } from 'domhandler' -import type { NativeRenderer, SubNodeTransform, ValidReactDomElement } from '../ComponentReplacer' -import { ComponentReplacer } from '../ComponentReplacer' +import type { NativeRenderer, SubNodeTransform, ValidReactDomElement } from '../component-replacer' +import { ComponentReplacer } from '../component-replacer' /** * Checks if the given node is a blockquote color definition @@ -42,7 +42,7 @@ const findBlockquoteColorParentElement = (nodes: Element[]): Element | undefined * If a color tag was found then the color will be applied to the node as border. */ export class ColoredBlockquoteReplacer extends ComponentReplacer { - public getReplacement( + public replace( node: Element, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer diff --git a/src/components/markdown-renderer/replace-components/ComponentReplacer.ts b/src/components/markdown-renderer/replace-components/component-replacer.ts similarity index 78% rename from src/components/markdown-renderer/replace-components/ComponentReplacer.ts rename to src/components/markdown-renderer/replace-components/component-replacer.ts index 231a5f502..400a5bc9a 100644 --- a/src/components/markdown-renderer/replace-components/ComponentReplacer.ts +++ b/src/components/markdown-renderer/replace-components/component-replacer.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { Element, NodeWithChildren } from 'domhandler' +import type { Element } from 'domhandler' import { isText } from 'domhandler' import type MarkdownIt from 'markdown-it' import type { ReactElement } from 'react' @@ -17,6 +17,10 @@ export type NativeRenderer = () => ValidReactDomElement export type MarkdownItPlugin = MarkdownIt.PluginSimple | MarkdownIt.PluginWithOptions | MarkdownIt.PluginWithParams +export const REPLACE_WITH_NOTHING = null +export const DO_NOT_REPLACE = undefined +export type NodeReplacement = ValidReactDomElement | typeof REPLACE_WITH_NOTHING | typeof DO_NOT_REPLACE + /** * Base class for all component replacers. * Component replacers detect structures in the HTML DOM from markdown it @@ -29,7 +33,7 @@ export abstract class ComponentReplacer { * @param node the node with the text node child * @return the string content */ - protected static extractTextChildContent(node: NodeWithChildren): string { + protected static extractTextChildContent(node: Element): string { const childrenTextNode = node.children[0] return isText(childrenTextNode) ? childrenTextNode.data : '' } @@ -39,12 +43,12 @@ export abstract class ComponentReplacer { * * @param node The current html dom node * @param subNodeTransform should be used to convert child elements of the current node - * @param nativeRenderer renders the current node as it is without any replacement. + * @param nativeRenderer renders the current node without any replacement * @return the replacement for the current node or undefined if the current replacer replacer hasn't done anything. */ - public abstract getReplacement( + public abstract replace( node: Element, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer - ): ValidReactDomElement | undefined + ): NodeReplacement } diff --git a/src/components/markdown-renderer/replace-components/csv/csv-replacer.tsx b/src/components/markdown-renderer/replace-components/csv/csv-replacer.tsx index fdafb1668..80ef3596a 100644 --- a/src/components/markdown-renderer/replace-components/csv/csv-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/csv/csv-replacer.tsx @@ -6,27 +6,20 @@ import type { Element } from 'domhandler' import React from 'react' -import { ComponentReplacer } from '../ComponentReplacer' +import { ComponentReplacer } from '../component-replacer' import { CsvTable } from './csv-table' +import { CodeBlockComponentReplacer } from '../code-block-component-replacer' /** * Detects code blocks with "csv" as language and renders them as table. */ export class CsvReplacer extends ComponentReplacer { - public getReplacement(codeNode: Element): React.ReactElement | undefined { - if ( - codeNode.name !== 'code' || - !codeNode.attribs || - !codeNode.attribs['data-highlight-language'] || - codeNode.attribs['data-highlight-language'] !== 'csv' || - !codeNode.children || - !codeNode.children[0] - ) { + public replace(codeNode: Element): React.ReactElement | undefined { + const code = CodeBlockComponentReplacer.extractTextFromCodeNode(codeNode, 'csv') + if (!code) { return } - const code = ComponentReplacer.extractTextChildContent(codeNode) - const extraData = codeNode.attribs['data-extra'] const extraRegex = /\s*(delimiter=([^\s]*))?\s*(header)?/ const extraInfos = extraRegex.exec(extraData) diff --git a/src/components/markdown-renderer/replace-components/custom-tag-with-id-component-replacer.ts b/src/components/markdown-renderer/replace-components/custom-tag-with-id-component-replacer.ts new file mode 100644 index 000000000..c03e99457 --- /dev/null +++ b/src/components/markdown-renderer/replace-components/custom-tag-with-id-component-replacer.ts @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { NodeReplacement } from './component-replacer' +import { ComponentReplacer } from './component-replacer' +import type { FunctionComponent } from 'react' +import React from 'react' +import type { Element } from 'domhandler' + +export interface IdProps { + id: string +} + +/** + * Replaces custom tags that have just an id () with react elements. + */ +export class CustomTagWithIdComponentReplacer extends ComponentReplacer { + constructor(private component: FunctionComponent, private tagName: string) { + super() + } + + public replace(node: Element): NodeReplacement { + const id = this.extractId(node) + return id ? React.createElement(this.component, { id: id }) : undefined + } + + /** + * Checks if the given {@link Element} is a custom tag and extracts its `id` attribute. + * + * @param element The element to check. + * @return the extracted id or undefined if the element isn't a custom tag or has no id attribute. + */ + private extractId(element: Element): string | undefined { + return element.name === `app-${this.tagName}` && element.attribs && element.attribs.id + ? element.attribs.id + : undefined + } +} diff --git a/src/components/markdown-renderer/replace-components/flow/flowchart-replacer.tsx b/src/components/markdown-renderer/replace-components/flow/flowchart-replacer.tsx deleted file mode 100644 index c5f022882..000000000 --- a/src/components/markdown-renderer/replace-components/flow/flowchart-replacer.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { Element } from 'domhandler' -import React from 'react' -import { ComponentReplacer } from '../ComponentReplacer' -import { FlowChart } from './flowchart/flowchart' - -/** - * Detects code blocks with "flow" as language and renders them as flow chart. - */ -export class FlowchartReplacer extends ComponentReplacer { - public getReplacement(codeNode: Element): React.ReactElement | undefined { - if ( - codeNode.name !== 'code' || - !codeNode.attribs || - !codeNode.attribs['data-highlight-language'] || - codeNode.attribs['data-highlight-language'] !== 'flow' || - !codeNode.children || - !codeNode.children[0] - ) { - return - } - - const code = ComponentReplacer.extractTextChildContent(codeNode) - - return - } -} diff --git a/src/components/markdown-renderer/replace-components/flow/flowchart/flowchart.tsx b/src/components/markdown-renderer/replace-components/flow/flowchart.tsx similarity index 85% rename from src/components/markdown-renderer/replace-components/flow/flowchart/flowchart.tsx rename to src/components/markdown-renderer/replace-components/flow/flowchart.tsx index 84ae2cabd..39722ad7d 100644 --- a/src/components/markdown-renderer/replace-components/flow/flowchart/flowchart.tsx +++ b/src/components/markdown-renderer/replace-components/flow/flowchart.tsx @@ -7,9 +7,9 @@ import React, { useEffect, useRef, useState } from 'react' 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 { useIsDarkModeActivated } from '../../../../hooks/common/use-is-dark-mode-activated' +import { Logger } from '../../../../utils/logger' +import { cypressId } from '../../../../utils/cypress-attribute' const log = new Logger('FlowChart') @@ -30,8 +30,8 @@ export const FlowChart: React.FC = ({ code }) => { } const currentDiagramRef = diagramRef.current import(/* webpackChunkName: "flowchart.js" */ 'flowchart.js') - .then((imp) => { - const parserOutput = imp.parse(code) + .then((importedLibrary) => { + const parserOutput = importedLibrary.parse(code) try { parserOutput.drawSVG(currentDiagramRef, { 'line-width': 2, diff --git a/src/components/markdown-renderer/replace-components/gist/gist-frame.tsx b/src/components/markdown-renderer/replace-components/gist/gist-frame.tsx index f2dc6281e..8c62cf103 100644 --- a/src/components/markdown-renderer/replace-components/gist/gist-frame.tsx +++ b/src/components/markdown-renderer/replace-components/gist/gist-frame.tsx @@ -8,17 +8,16 @@ import React, { useCallback } from 'react' import { cypressId } from '../../../../utils/cypress-attribute' import './gist-frame.scss' import { useResizeGistFrame } from './use-resize-gist-frame' - -export interface GistFrameProps { - id: string -} +import { OneClickEmbedding } from '../one-click-frame/one-click-embedding' +import preview from './gist-preview.png' +import type { IdProps } from '../custom-tag-with-id-component-replacer' /** * This component renders a GitHub Gist by placing the gist URL in an {@link HTMLIFrameElement iframe}. * * @param id The id of the gist */ -export const GistFrame: React.FC = ({ id }) => { +export const GistFrame: React.FC = ({ id }) => { const [frameHeight, onStartResizing] = useResizeGistFrame(150) const onStart = useCallback( @@ -29,7 +28,7 @@ export const GistFrame: React.FC = ({ id }) => { ) return ( - +