diff --git a/cypress/integration/quote-extra.spec.ts b/cypress/integration/quote-extra.spec.ts index 8e81adb8e..fdc1b8750 100644 --- a/cypress/integration/quote-extra.spec.ts +++ b/cypress/integration/quote-extra.spec.ts @@ -14,13 +14,13 @@ describe('Quote extra tags', function () { cy.setCodemirrorContent('[name=testy mctestface]') cy.getMarkdownBody() - .find('.quote-extra') + .find('.blockquote-extra') .should('be.visible') .find('.fa-user') .should('be.visible') cy.getMarkdownBody() - .find('.quote-extra') + .find('.blockquote-extra') .should('be.visible') .contains('testy mctestface') }) @@ -31,13 +31,13 @@ describe('Quote extra tags', function () { cy.setCodemirrorContent(`[time=always]`) cy.getMarkdownBody() - .find('.quote-extra') + .find('.blockquote-extra') .should('be.visible') .find('.fa-clock-o') .should('be.visible') cy.getMarkdownBody() - .find('.quote-extra') + .find('.blockquote-extra') .should('be.visible') .contains('always') }) @@ -48,13 +48,13 @@ describe('Quote extra tags', function () { cy.setCodemirrorContent(`[color=#b51f08]`) cy.getMarkdownBody() - .find('.quote-extra') + .find('.blockquote-extra') .should('be.visible') .find('.fa-tag') .should('be.visible') cy.getMarkdownBody() - .find('.quote-extra') + .find('.blockquote-extra') .should('be.visible') .should('have.css', 'color', 'rgb(181, 31, 8)') }) @@ -63,7 +63,7 @@ describe('Quote extra tags', function () { cy.setCodemirrorContent(`> [color=#b51f08] HedgeDoc`) cy.getMarkdownBody() - .find('.quote-extra') + .find('.blockquote-extra') .should('not.exist') cy.getMarkdownBody() diff --git a/locales/en.json b/locales/en.json index 477719aa0..1cf08daa7 100644 --- a/locales/en.json +++ b/locales/en.json @@ -15,6 +15,9 @@ "locked": "Mouse input locked", "unlocked": "Mouse input unlocked" }, + "plantuml": { + "notConfigured": "PlantUML plugin is enabled but not properly configured." + }, "flowchart": { "invalidSyntax": "Invalid flowchart.js syntax!" }, diff --git a/src/api/media/index.ts b/src/api/media/index.ts index 696b73ae7..0d73c43c0 100644 --- a/src/api/media/index.ts +++ b/src/api/media/index.ts @@ -4,10 +4,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { ImageProxyResponse } from '../../components/markdown-renderer/replace-components/image/types' import { isMockMode } from '../../utils/test-modes' import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils' +export interface ImageProxyResponse { + src: string +} + export const getProxiedUrl = async (imageUrl: string): Promise => { const response = await fetch(getApiUrl() + 'media/proxy', { ...defaultFetchConfig, diff --git a/src/components/common/note-frontmatter/note-frontmatter.test.ts b/src/components/common/note-frontmatter/note-frontmatter.test.ts index 2073889b9..2afe72e5b 100644 --- a/src/components/common/note-frontmatter/note-frontmatter.test.ts +++ b/src/components/common/note-frontmatter/note-frontmatter.test.ts @@ -40,7 +40,7 @@ describe('yaml frontmatter', () => { it('should parse "breaks"', () => { const noteFrontmatter = createNoteFrontmatterFromYaml('breaks: false') - expect(noteFrontmatter.breaks).toEqual(false) + expect(noteFrontmatter.newlinesAreBreaks).toEqual(false) }) it('should parse an empty opengraph object', () => { diff --git a/src/components/common/note-frontmatter/note-frontmatter.ts b/src/components/common/note-frontmatter/note-frontmatter.ts index 128785d0f..65560dbb3 100644 --- a/src/components/common/note-frontmatter/note-frontmatter.ts +++ b/src/components/common/note-frontmatter/note-frontmatter.ts @@ -21,7 +21,7 @@ export interface NoteFrontmatter { robots: string lang: typeof ISO6391[number] dir: NoteTextDirection - breaks: boolean + newlinesAreBreaks: boolean GA: string disqus: string type: NoteType @@ -51,7 +51,7 @@ export const parseRawNoteFrontmatter = (rawData: RawNoteFrontmatter): NoteFrontm title: rawData.title ?? '', description: rawData.description ?? '', robots: rawData.robots ?? '', - breaks: rawData.breaks ?? true, + newlinesAreBreaks: rawData.breaks ?? true, GA: rawData.GA ?? '', disqus: rawData.disqus ?? '', lang: (rawData.lang ? ISO6391.find((lang) => lang === rawData.lang) : undefined) ?? 'en', diff --git a/src/components/editor-page/app-bar/help-button/cheatsheet-line.tsx b/src/components/editor-page/app-bar/help-button/cheatsheet-line.tsx index e1735f709..1be416c76 100644 --- a/src/components/editor-page/app-bar/help-button/cheatsheet-line.tsx +++ b/src/components/editor-page/app-bar/help-button/cheatsheet-line.tsx @@ -13,7 +13,7 @@ export interface CheatsheetLineProps { } const HighlightedCode = React.lazy( - () => import('../../../markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code') + () => import('../../../markdown-renderer/markdown-extension/highlighted-fence/highlighted-code') ) const DocumentMarkdownRenderer = React.lazy(() => import('../../../markdown-renderer/document-markdown-renderer')) diff --git a/src/components/editor-page/editor-pane/autocompletion/container.ts b/src/components/editor-page/editor-pane/autocompletion/container.ts index ea2a435a2..ab2638b57 100644 --- a/src/components/editor-page/editor-pane/autocompletion/container.ts +++ b/src/components/editor-page/editor-pane/autocompletion/container.ts @@ -6,16 +6,16 @@ import type { Editor, Hint, Hints } from 'codemirror' import { Pos } from 'codemirror' -import { validAlertLevels } from '../../../markdown-renderer/markdown-it-plugins/alert-container' import type { Hinter } from './index' import { findWordAtCursor } from './index' +import { alertLevels } from '../../../markdown-renderer/markdown-extension/alert-markdown-extension' const wordRegExp = /^:::((?:\w|-|\+)*)$/ const spoilerSuggestion: Hint = { text: ':::spoiler Toggle label\nToggled content\n::: \n', displayText: 'spoiler' } -const suggestions = validAlertLevels +const suggestions = alertLevels .map( (suggestion: string): Hint => ({ text: ':::' + suggestion + '\n\n::: \n', diff --git a/src/components/editor-page/renderer-pane/communicator-image-lightbox.tsx b/src/components/editor-page/renderer-pane/communicator-image-lightbox.tsx index 2844fd2ac..d0ec8a2e3 100644 --- a/src/components/editor-page/renderer-pane/communicator-image-lightbox.tsx +++ b/src/components/editor-page/renderer-pane/communicator-image-lightbox.tsx @@ -5,7 +5,7 @@ */ import React, { useCallback, useState } from 'react' -import { ImageLightboxModal } from '../../markdown-renderer/replace-components/image/image-lightbox-modal' +import { ImageLightboxModal } from '../../markdown-renderer/markdown-extension/image/image-lightbox-modal' import type { ImageClickedMessage, ImageDetails diff --git a/src/components/editor-page/synced-scroll/utils.ts b/src/components/editor-page/synced-scroll/utils.ts index 0da7bafec..7fbc997e8 100644 --- a/src/components/editor-page/synced-scroll/utils.ts +++ b/src/components/editor-page/synced-scroll/utils.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { LineMarkerPosition } from '../../markdown-renderer/types' +import type { LineMarkerPosition } from '../../markdown-renderer/markdown-extension/linemarker/types' export const findLineMarks = ( lineMarks: LineMarkerPosition[], diff --git a/src/components/editor-page/table-of-contents/build-react-dom-from-toc-ast.tsx b/src/components/editor-page/table-of-contents/build-react-dom-from-toc-ast.tsx index fe681606f..9f0c00de1 100644 --- a/src/components/editor-page/table-of-contents/build-react-dom-from-toc-ast.tsx +++ b/src/components/editor-page/table-of-contents/build-react-dom-from-toc-ast.tsx @@ -8,8 +8,8 @@ import type { TocAst } from 'markdown-it-toc-done-right' import type { ReactElement } from 'react' import React, { Fragment } from 'react' import { ShowIf } from '../../common/show-if/show-if' -import { createJumpToMarkClickEventHandler } from '../../markdown-renderer/replace-components/link-replacer/link-replacer' import { tocSlugify } from './toc-slugify' +import { JumpAnchor } from '../../markdown-renderer/markdown-extension/link-replacer/jump-anchor' export const buildReactDomFromTocAst = ( toc: TocAst, @@ -32,9 +32,9 @@ export const buildReactDomFromTocAst = ( const content = ( 0}> - + {rawName} - + 0}>
    diff --git a/src/components/markdown-renderer/common-markdown-renderer-props.ts b/src/components/markdown-renderer/common-markdown-renderer-props.ts index a1e1b2046..24e287d0d 100644 --- a/src/components/markdown-renderer/common-markdown-renderer-props.ts +++ b/src/components/markdown-renderer/common-markdown-renderer-props.ts @@ -5,17 +5,17 @@ */ import type { TocAst } from 'markdown-it-toc-done-right' -import type { ImageClickHandler } from './replace-components/image/image-replacer' +import type { ImageClickHandler } from './markdown-extension/image/proxy-image-replacer' import type { Ref } from 'react' export interface CommonMarkdownRendererProps { onFirstHeadingChange?: (firstHeading: string | undefined) => void onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void onTocChange?: (ast?: TocAst) => void - baseUrl?: string + baseUrl: string onImageClick?: ImageClickHandler outerContainerRef?: Ref - useAlternativeBreaks?: boolean + newlinesAreBreaks?: boolean lineOffset?: number className?: string content: string diff --git a/src/components/markdown-renderer/document-markdown-renderer.tsx b/src/components/markdown-renderer/document-markdown-renderer.tsx index 3fa2b7647..3eafbe0a3 100644 --- a/src/components/markdown-renderer/document-markdown-renderer.tsx +++ b/src/components/markdown-renderer/document-markdown-renderer.tsx @@ -8,17 +8,17 @@ import React, { useMemo, useRef } from 'react' import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert' import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom' import './markdown-renderer.scss' -import type { LineMarkerPosition } from './types' -import { useComponentReplacers } from './hooks/use-component-replacers' +import type { LineMarkerPosition } from './markdown-extension/linemarker/types' import { useTranslation } from 'react-i18next' -import type { LineMarkers } from './replace-components/linemarker/line-number-marker' +import type { LineMarkers } from './markdown-extension/linemarker/add-line-marker-markdown-it-plugin' import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions' 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 { useTrimmedContent } from './hooks/use-trimmed-content' import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props' -import { DocumentMarkdownItConfigurator } from './markdown-it-configurator/document-markdown-it-configurator' +import { useMarkdownExtensions } from './hooks/use-markdown-extensions' +import { HeadlineAnchorsMarkdownExtension } from './markdown-extension/headline-anchors-markdown-extension' export interface DocumentMarkdownRendererProps extends CommonMarkdownRendererProps { onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void @@ -34,7 +34,7 @@ export const DocumentMarkdownRenderer: React.FC = baseUrl, onImageClick, outerContainerRef, - useAlternativeBreaks, + newlinesAreBreaks, lineOffset }) => { const markdownBodyRef = useRef(null) @@ -42,21 +42,16 @@ export const DocumentMarkdownRenderer: React.FC = const tocAst = useRef() const [trimmedContent, contentExceedsLimit] = useTrimmedContent(content) - const markdownIt = useMemo( - () => - new DocumentMarkdownItConfigurator({ - onTocChange: (toc) => (tocAst.current = toc), - onLineMarkers: - onLineMarkerPositionChanged === undefined - ? undefined - : (lineMarkers) => (currentLineMarkers.current = lineMarkers), - useAlternativeBreaks, - lineOffset - }).buildConfiguredMarkdownIt(), - [onLineMarkerPositionChanged, useAlternativeBreaks, lineOffset] + const extensions = useMarkdownExtensions( + baseUrl, + currentLineMarkers, + useMemo(() => [new HeadlineAnchorsMarkdownExtension()], []), + lineOffset, + onTaskCheckedChange, + onImageClick, + onTocChange ) - const replacers = useComponentReplacers(onTaskCheckedChange, onImageClick, baseUrl, lineOffset) - const markdownReactDom = useConvertMarkdownToReactDom(trimmedContent, markdownIt, replacers) + const markdownReactDom = useConvertMarkdownToReactDom(trimmedContent, extensions, newlinesAreBreaks) useTranslation() useCalculateLineMarkerPosition( diff --git a/src/components/markdown-renderer/hooks/dom-purifier-node-preprocessor.ts b/src/components/markdown-renderer/hooks/dom-purifier-node-preprocessor.ts deleted file mode 100644 index 2c5746bc2..000000000 --- a/src/components/markdown-renderer/hooks/dom-purifier-node-preprocessor.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { Document } from 'domhandler' -import render from 'dom-serializer' -import DOMPurify from 'dompurify' -import { parseDocument } from 'htmlparser2' - -const customTags = ['app-linemarker', 'app-katex', 'app-gist', 'app-youtube', 'app-vimeo', 'app-asciinema'] - -/** - * Sanitizes the given {@link Document document}. - * - * @param document The dirty document - * @return the sanitized Document - */ -export const domPurifierNodePreprocessor = (document: Document): Document => { - const sanitizedHtml = DOMPurify.sanitize(render(document), { - ADD_TAGS: customTags - }) - return parseDocument(sanitizedHtml) -} diff --git a/src/components/markdown-renderer/hooks/use-component-replacers.ts b/src/components/markdown-renderer/hooks/use-component-replacers.ts deleted file mode 100644 index a654b90bf..000000000 --- a/src/components/markdown-renderer/hooks/use-component-replacers.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { useMemo } from 'react' -import type { ComponentReplacer } from '../replace-components/component-replacer' -import { CsvReplacer } from '../replace-components/csv/csv-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 { ColoredBlockquoteReplacer } from '../replace-components/colored-blockquote/colored-blockquote-replacer' -import type { TaskCheckedChangeHandler } from '../replace-components/task-list/task-list-replacer' -import { TaskListReplacer } from '../replace-components/task-list/task-list-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. - * - * @param onTaskCheckedChange A callback that gets executed if a task checkbox gets clicked - * @param onImageClick A callback that should be executed if an image gets clicked - * @param baseUrl The base url for relative links - * @param frontmatterLinesToSkip The number of lines of the frontmatter part to add this as offset to line-numbers. - * - * @return the created list - */ -export const useComponentReplacers = ( - onTaskCheckedChange?: TaskCheckedChangeHandler, - onImageClick?: ImageClickHandler, - baseUrl?: string, - frontmatterLinesToSkip?: number -): ComponentReplacer[] => - useMemo( - () => [ - new LinemarkerReplacer(), - new CustomTagWithIdComponentReplacer(GistFrame, 'gist'), - new CustomTagWithIdComponentReplacer(YouTubeFrame, 'youtube'), - new CustomTagWithIdComponentReplacer(VimeoFrame, 'vimeo'), - new CustomTagWithIdComponentReplacer(AsciinemaFrame, 'asciinema'), - new ImageReplacer(onImageClick), - new CsvReplacer(), - 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(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 f47229e98..439cc5439 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 @@ -4,50 +4,83 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type MarkdownIt from 'markdown-it/lib' +import MarkdownIt from 'markdown-it/lib' 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' -import { domPurifierNodePreprocessor } from './dom-purifier-node-preprocessor' +import type { MarkdownExtension } from '../markdown-extension/markdown-extension' +import type { NodeProcessor } from '../node-preprocessors/node-processor' +import type { Document } from 'domhandler' +import { SanitizerMarkdownExtension } from '../markdown-extension/sanitizer/sanitizer-markdown-extension' /** * Renders markdown code into react elements * * @param markdownCode The markdown code that should be rendered - * @param markdownIt The configured {@link MarkdownIt markdown it} instance that should render the code - * @param replacers A function that provides a list of {@link ComponentReplacer component replacers} - * @param preprocessNodes A function that processes nodes after parsing the html code that is generated by markdown it. + * @param additionalMarkdownExtensions A list of {@link MarkdownExtension markdown extensions} that should be used + * @param newlinesAreBreaks Defines if the alternative break mode of markdown it should be used * @return The React DOM that represents the rendered markdown code */ export const useConvertMarkdownToReactDom = ( markdownCode: string, - markdownIt: MarkdownIt, - replacers: ComponentReplacer[], - preprocessNodes?: (nodes: Document) => Document + additionalMarkdownExtensions: MarkdownExtension[], + newlinesAreBreaks?: boolean ): ValidReactDomElement[] => { const lineNumberMapper = useMemo(() => new LineIdMapper(), []) const htmlToReactTransformer = useMemo(() => new NodeToReactTransformer(), []) + const markdownExtensions = useMemo(() => { + const tagNameWhiteList = additionalMarkdownExtensions.reduce( + (state, extension) => [...state, ...extension.buildTagNameWhitelist()], + [] as string[] + ) + return [...additionalMarkdownExtensions, new SanitizerMarkdownExtension(tagNameWhiteList)] + }, [additionalMarkdownExtensions]) + + const markdownIt = useMemo(() => { + const newMarkdownIt = new MarkdownIt('default', { + html: true, + breaks: newlinesAreBreaks ?? true, + langPrefix: '', + typographer: true + }) + markdownExtensions.forEach((extension) => + newMarkdownIt.use((markdownIt) => extension.configureMarkdownIt(markdownIt)) + ) + markdownExtensions.forEach((extension) => + newMarkdownIt.use((markdownIt) => extension.configureMarkdownItPost(markdownIt)) + ) + return newMarkdownIt + }, [markdownExtensions, newlinesAreBreaks]) useMemo(() => { + const replacers = markdownExtensions.reduce( + (state, extension) => [...state, ...extension.buildReplacers()], + [] as ComponentReplacer[] + ) htmlToReactTransformer.setReplacers(replacers) - }, [htmlToReactTransformer, replacers]) + }, [htmlToReactTransformer, markdownExtensions]) useMemo(() => { htmlToReactTransformer.setLineIds(lineNumberMapper.updateLineMapping(markdownCode)) }, [htmlToReactTransformer, lineNumberMapper, markdownCode]) + const nodePreProcessor = useMemo(() => { + return markdownExtensions + .reduce((state, extension) => [...state, ...extension.buildNodeProcessors()], [] as NodeProcessor[]) + .reduce( + (state, processor) => (document: Document) => state(processor.process(document)), + (document: Document) => document + ) + }, [markdownExtensions]) + return useMemo(() => { const html = markdownIt.render(markdownCode) return convertHtmlToReact(html, { transform: (node, index) => htmlToReactTransformer.translateNodeToReactElement(node, index), - preprocessNodes: (document: Document): Document => { - const processedDocument = preprocessNodes ? preprocessNodes(document) : document - return domPurifierNodePreprocessor(processedDocument) - } + preprocessNodes: (document) => nodePreProcessor(document) }) - }, [htmlToReactTransformer, markdownCode, markdownIt, preprocessNodes]) + }, [htmlToReactTransformer, markdownCode, markdownIt, nodePreProcessor]) } diff --git a/src/components/markdown-renderer/hooks/use-markdown-extensions.ts b/src/components/markdown-renderer/hooks/use-markdown-extensions.ts new file mode 100644 index 000000000..b74033a7a --- /dev/null +++ b/src/components/markdown-renderer/hooks/use-markdown-extensions.ts @@ -0,0 +1,111 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { MutableRefObject } from 'react' +import { useMemo } from 'react' +import { TableOfContentsMarkdownExtension } from '../markdown-extension/table-of-contents-markdown-extension' +import { VegaLiteMarkdownExtension } from '../markdown-extension/vega-lite/vega-lite-markdown-extension' +import { MarkmapMarkdownExtension } from '../markdown-extension/markmap/markmap-markdown-extension' +import { LinemarkerMarkdownExtension } from '../markdown-extension/linemarker/linemarker-markdown-extension' +import { GistMarkdownExtension } from '../markdown-extension/gist/gist-markdown-extension' +import { YoutubeMarkdownExtension } from '../markdown-extension/youtube/youtube-markdown-extension' +import { VimeoMarkdownExtension } from '../markdown-extension/vimeo/vimeo-markdown-extension' +import { AsciinemaMarkdownExtension } from '../markdown-extension/asciinema/asciinema-markdown-extension' +import { ProxyImageMarkdownExtension } from '../markdown-extension/image/proxy-image-markdown-extension' +import { CsvTableMarkdownExtension } from '../markdown-extension/csv/csv-table-markdown-extension' +import { AbcjsMarkdownExtension } from '../markdown-extension/abcjs/abcjs-markdown-extension' +import { SequenceDiagramMarkdownExtension } from '../markdown-extension/sequence-diagram/sequence-diagram-markdown-extension' +import { FlowchartMarkdownExtension } from '../markdown-extension/flowchart/flowchart-markdown-extension' +import { MermaidMarkdownExtension } from '../markdown-extension/mermaid/mermaid-markdown-extension' +import { GraphvizMarkdownExtension } from '../markdown-extension/graphviz/graphviz-markdown-extension' +import { BlockquoteExtraTagMarkdownExtension } from '../markdown-extension/blockquote/blockquote-extra-tag-markdown-extension' +import { LinkAdjustmentMarkdownExtension } from '../markdown-extension/link-replacer/link-adjustment-markdown-extension' +import { KatexMarkdownExtension } from '../markdown-extension/katex/katex-markdown-extension' +import { TaskListMarkdownExtension } from '../markdown-extension/task-list/task-list-markdown-extension' +import { PlantumlMarkdownExtension } from '../markdown-extension/plantuml/plantuml-markdown-extension' +import { LegacyShortcodesMarkdownExtension } from '../markdown-extension/legacy-short-codes/legacy-shortcodes-markdown-extension' +import { EmojiMarkdownExtension } from '../markdown-extension/emoji/emoji-markdown-extension' +import { GenericSyntaxMarkdownExtension } from '../markdown-extension/generic-syntax-markdown-extension' +import { AlertMarkdownExtension } from '../markdown-extension/alert-markdown-extension' +import { SpoilerMarkdownExtension } from '../markdown-extension/spoiler-markdown-extension' +import { LinkifyFixMarkdownExtension } from '../markdown-extension/linkify-fix-markdown-extension' +import { HighlightedCodeMarkdownExtension } from '../markdown-extension/highlighted-fence/highlighted-code-markdown-extension' +import { DebuggerMarkdownExtension } from '../markdown-extension/debugger-markdown-extension' +import { useApplicationState } from '../../../hooks/common/use-application-state' +import type { LineMarkers } from '../markdown-extension/linemarker/add-line-marker-markdown-it-plugin' +import type { ImageClickHandler } from '../markdown-extension/image/proxy-image-replacer' +import type { TocAst } from 'markdown-it-toc-done-right' +import type { MarkdownExtension } from '../markdown-extension/markdown-extension' + +/** + * Provides a list of {@link MarkdownExtension markdown extensions} that is a combination of the common extensions and the given additional. + * + * @param baseUrl The base url for the {@link LinkAdjustmentMarkdownExtension} + * @param currentLineMarkers A {@link MutableRefObject reference} to {@link LineMarkers} for the {@link LinemarkerMarkdownExtension} + * @param additionalExtensions The additional extensions that should be included in the list + * @param lineOffset The line offset for the {@link LinemarkerMarkdownExtension} and {@link TaskListMarkdownExtension} + * @param onTaskCheckedChange The checkbox click callback for the {@link TaskListMarkdownExtension} + * @param onImageClick The image click callback for the {@link ProxyImageMarkdownExtension} + * @param onTocChange The toc-changed callback for the {@link TableOfContentsMarkdownExtension} + * @return The created list of markdown extensions + */ +export const useMarkdownExtensions = ( + baseUrl: string, + currentLineMarkers: MutableRefObject | undefined, + additionalExtensions: MarkdownExtension[], + lineOffset?: number, + onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void, + onImageClick?: ImageClickHandler, + onTocChange?: (ast?: TocAst) => void +): MarkdownExtension[] => { + const plantumlServer = useApplicationState((state) => state.config.plantumlServer) + + return useMemo(() => { + return [ + new TableOfContentsMarkdownExtension(onTocChange), + ...additionalExtensions, + new VegaLiteMarkdownExtension(), + new MarkmapMarkdownExtension(), + new LinemarkerMarkdownExtension( + currentLineMarkers ? (lineMarkers) => (currentLineMarkers.current = lineMarkers) : undefined, + lineOffset + ), + new GistMarkdownExtension(), + new YoutubeMarkdownExtension(), + new VimeoMarkdownExtension(), + new AsciinemaMarkdownExtension(), + new ProxyImageMarkdownExtension(onImageClick), + new CsvTableMarkdownExtension(), + new AbcjsMarkdownExtension(), + new SequenceDiagramMarkdownExtension(), + new FlowchartMarkdownExtension(), + new MermaidMarkdownExtension(), + new GraphvizMarkdownExtension(), + new BlockquoteExtraTagMarkdownExtension(), + new LinkAdjustmentMarkdownExtension(baseUrl), + new KatexMarkdownExtension(), + new TaskListMarkdownExtension(lineOffset, onTaskCheckedChange), + new PlantumlMarkdownExtension(plantumlServer), + new LegacyShortcodesMarkdownExtension(), + new EmojiMarkdownExtension(), + new GenericSyntaxMarkdownExtension(), + new AlertMarkdownExtension(), + new SpoilerMarkdownExtension(), + new LinkifyFixMarkdownExtension(), + new HighlightedCodeMarkdownExtension(), + new DebuggerMarkdownExtension() + ] + }, [ + additionalExtensions, + baseUrl, + currentLineMarkers, + lineOffset, + onImageClick, + onTaskCheckedChange, + onTocChange, + plantumlServer + ]) +} diff --git a/src/components/markdown-renderer/replace-components/abc/abc-frame.tsx b/src/components/markdown-renderer/markdown-extension/abcjs/abc-frame.tsx similarity index 90% rename from src/components/markdown-renderer/replace-components/abc/abc-frame.tsx rename to src/components/markdown-renderer/markdown-extension/abcjs/abc-frame.tsx index 1c92a6a75..edcc0aa6c 100644 --- a/src/components/markdown-renderer/replace-components/abc/abc-frame.tsx +++ b/src/components/markdown-renderer/markdown-extension/abcjs/abc-frame.tsx @@ -7,7 +7,7 @@ import React, { useEffect, useRef } from 'react' import './abc.scss' import { Logger } from '../../../../utils/logger' -import type { CodeProps } from '../code-block-component-replacer' +import type { CodeProps } from '../../replace-components/code-block-component-replacer' const log = new Logger('AbcFrame') diff --git a/src/components/markdown-renderer/replace-components/abc/abc.scss b/src/components/markdown-renderer/markdown-extension/abcjs/abc.scss similarity index 87% rename from src/components/markdown-renderer/replace-components/abc/abc.scss rename to src/components/markdown-renderer/markdown-extension/abcjs/abc.scss index 4068605f2..53ca1e956 100644 --- a/src/components/markdown-renderer/replace-components/abc/abc.scss +++ b/src/components/markdown-renderer/markdown-extension/abcjs/abc.scss @@ -6,7 +6,7 @@ .abcjs-score { - @import "../../../../style/variables.scss"; + @import "../../../../style/variables"; .markdown-body & { overflow-x: auto !important; diff --git a/src/components/markdown-renderer/markdown-extension/abcjs/abcjs-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/abcjs/abcjs-markdown-extension.ts new file mode 100644 index 000000000..89bedd5d8 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/abcjs/abcjs-markdown-extension.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import { CodeBlockComponentReplacer } from '../../replace-components/code-block-component-replacer' +import { AbcFrame } from './abc-frame' +import type { ComponentReplacer } from '../../replace-components/component-replacer' + +/** + * Adds support for abc.js to the markdown rendering using code fences with "abc" as language. + */ +export class AbcjsMarkdownExtension extends MarkdownExtension { + public buildReplacers(): ComponentReplacer[] { + return [new CodeBlockComponentReplacer(AbcFrame, 'abc')] + } +} diff --git a/src/components/markdown-renderer/markdown-extension/alert-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/alert-markdown-extension.ts new file mode 100644 index 000000000..6c26cade1 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/alert-markdown-extension.ts @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from './markdown-extension' +import type MarkdownIt from 'markdown-it' +import markdownItContainer from 'markdown-it-container' +import type Token from 'markdown-it/lib/token' +import type Renderer from 'markdown-it/lib/renderer' + +export const alertLevels = ['success', 'danger', 'info', 'warning'] + +export class AlertMarkdownExtension extends MarkdownExtension { + public configureMarkdownIt(markdownIt: MarkdownIt): void { + alertLevels.forEach((level) => { + markdownItContainer(markdownIt, level, { + render: (tokens: Token[], index: number, options: MarkdownIt.Options, env: unknown, self: Renderer) => { + tokens[index].attrJoin('role', 'alert') + tokens[index].attrJoin('class', 'alert') + tokens[index].attrJoin('class', `alert-${level}`) + return self.renderToken(tokens, index, options) + } + }) + }) + } +} diff --git a/src/components/markdown-renderer/replace-components/asciinema/asciinema-frame.tsx b/src/components/markdown-renderer/markdown-extension/asciinema/asciinema-frame.tsx similarity index 82% rename from src/components/markdown-renderer/replace-components/asciinema/asciinema-frame.tsx rename to src/components/markdown-renderer/markdown-extension/asciinema/asciinema-frame.tsx index e22613fe8..36f9fe4ae 100644 --- a/src/components/markdown-renderer/replace-components/asciinema/asciinema-frame.tsx +++ b/src/components/markdown-renderer/markdown-extension/asciinema/asciinema-frame.tsx @@ -5,8 +5,8 @@ */ import React from 'react' -import { ClickShield } from '../click-shield/click-shield' -import type { IdProps } from '../custom-tag-with-id-component-replacer' +import type { IdProps } from '../../replace-components/custom-tag-with-id-component-replacer' +import { ClickShield } from '../../replace-components/click-shield/click-shield' /** * Renders an embedding for https://asciinema.org diff --git a/src/components/markdown-renderer/markdown-extension/asciinema/asciinema-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/asciinema/asciinema-markdown-extension.ts new file mode 100644 index 000000000..9d6b0f27e --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/asciinema/asciinema-markdown-extension.ts @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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 { AsciinemaFrame } from './asciinema-frame' +import { replaceAsciinemaLink } from './replace-asciinema-link' + +/** + * Adds asciinema embeddings to the markdown rendering by detecting asciinema.org links. + */ +export class AsciinemaMarkdownExtension extends MarkdownExtension { + public static readonly tagName = 'app-asciinema' + + public configureMarkdownIt(markdownIt: MarkdownIt): void { + markdownItRegex(markdownIt, replaceAsciinemaLink) + } + + public buildReplacers(): ComponentReplacer[] { + return [new CustomTagWithIdComponentReplacer(AsciinemaFrame, AsciinemaMarkdownExtension.tagName)] + } + + public buildTagNameWhitelist(): string[] { + return [AsciinemaMarkdownExtension.tagName] + } +} diff --git a/src/components/markdown-renderer/replace-components/asciinema/replace-asciinema-link.ts b/src/components/markdown-renderer/markdown-extension/asciinema/replace-asciinema-link.ts similarity index 54% rename from src/components/markdown-renderer/replace-components/asciinema/replace-asciinema-link.ts rename to src/components/markdown-renderer/markdown-extension/asciinema/replace-asciinema-link.ts index d5b1a75c1..c2c62b0e6 100644 --- a/src/components/markdown-renderer/replace-components/asciinema/replace-asciinema-link.ts +++ b/src/components/markdown-renderer/markdown-extension/asciinema/replace-asciinema-link.ts @@ -5,26 +5,23 @@ */ import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' -import type MarkdownIt from 'markdown-it/lib' -import markdownItRegex from 'markdown-it-regex' +import { AsciinemaMarkdownExtension } from './asciinema-markdown-extension' const protocolRegex = /(?:http(?:s)?:\/\/)?/ const domainRegex = /(?:asciinema\.org\/a\/)/ const idRegex = /(\d+)/ 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) -} +const asciinemaUrlRegex = new RegExp( + `^(?:${protocolRegex.source}${domainRegex.source}${idRegex.source}${tailRegex.source})$`, + 'i' +) export const replaceAsciinemaLink: RegexOptions = { name: 'asciinema-link', - regex: linkRegex, + regex: asciinemaUrlRegex, replace: (match) => { // ESLint wants to collapse this tag, but then the tag won't be valid html anymore. // noinspection CheckTagEmptyBody - return `` + return `<${AsciinemaMarkdownExtension.tagName} id='${match}'>` } } diff --git a/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-border-color-node-preprocessor.ts b/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-border-color-node-preprocessor.ts new file mode 100644 index 000000000..422c47b55 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-border-color-node-preprocessor.ts @@ -0,0 +1,104 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { Element, Node } from 'domhandler' +import { isTag, isText } from 'domhandler' +import { TravelerNodeProcessor } from '../../node-preprocessors/traveler-node-processor' +import Optional from 'optional-js' +import { BlockquoteExtraTagMarkdownExtension } from './blockquote-extra-tag-markdown-extension' + +/** + * Detects blockquotes with blockquote color tags and uses them to color the blockquote border. + */ +export class BlockquoteBorderColorNodePreprocessor extends TravelerNodeProcessor { + protected processNode(node: Node): void { + if (!isTag(node) || isBlockquoteWithChildren(node)) { + return + } + + Optional.ofNullable(findBlockquoteColorDefinitionAndParent(node.children)).ifPresent(([color, parentParagraph]) => { + removeColorDefinitionsFromParagraph(parentParagraph) + if (!cssColor.test(color)) { + return + } + setLeftBorderColor(node, color) + }) + } +} + +export const cssColor = + /^(#(?:[0-9a-f]{2}){2,4}|#[0-9a-f]{3}|black|silver|gray|whitesmoke|maroon|red|purple|fuchsia|green|lime|olivedrab|yellow|navy|blue|teal|aquamarine|orange|aliceblue|antiquewhite|aqua|azure|beige|bisque|blanchedalmond|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|currentcolor|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|gainsboro|ghostwhite|goldenrod|gold|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavenderblush|lavender|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|limegreen|linen|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|oldlace|olive|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|rebeccapurple|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|thistle|tomato|transparent|turquoise|violet|wheat|white|yellowgreen)$/i + +/** + * Checks if the given {@link Element} is a blockquote with children. + * @param element The {@link Element} to check + * @return {@code true} if the element is a blockquote with children. + */ +const isBlockquoteWithChildren = (element: Element): boolean => { + return element.name !== 'blockquote' || !element.children || element.children.length < 1 +} + +/** + * Searches for a blockquote color definition tag + * @param elements The {@link Element} elements that should be searched through. + * @return The parent paragraph and the extracted color if a color definition was found. {@code undefined} otherwise. + */ +const findBlockquoteColorDefinitionAndParent = ( + elements: Node[] +): [color: string, parentParagraph: Element] | undefined => { + for (const paragraph of elements) { + if (!isTag(paragraph) || paragraph.name !== 'p' || paragraph.children.length === 0) { + continue + } + + for (const colorDefinition of paragraph.children) { + if (!isTag(colorDefinition)) { + continue + } + + const content = extractBlockquoteColorDefinition(colorDefinition) + if (content !== undefined) { + return [content, paragraph] + } + } + } +} + +/** + * Checks if the given node is a blockquote color definition + * + * @param element The {@link Element} to check + * @return true if the checked node is a blockquote color definition + */ +const extractBlockquoteColorDefinition = (element: Element): string | undefined => { + if ( + element.name === BlockquoteExtraTagMarkdownExtension.tagName && + element.attribs['data-label'] === 'color' && + element.children.length === 1 && + isText(element.children[0]) + ) { + return element.children[0].data + } +} + +/** + * Removes all color definition elements from the given paragraph {@link Element} + * @param paragraph The {@link Element} whose children should be filtered + */ +const removeColorDefinitionsFromParagraph = (paragraph: Element): void => { + const childElements = paragraph.children + paragraph.children = childElements.filter((elem) => !isTag(elem) || !extractBlockquoteColorDefinition(elem)) +} + +/** + * Sets the left border color of the given {@link Element}. + * + * @param element The {@link Element} to change + * @param color The border color + */ +const setLeftBorderColor = (element: Element, color: string): void => { + element.attribs = Object.assign(element.attribs || {}, { style: `border-left-color: ${color};` }) +} diff --git a/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-color-extra-tag-replacer.tsx b/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-color-extra-tag-replacer.tsx new file mode 100644 index 000000000..057dc12ea --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-color-extra-tag-replacer.tsx @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { NativeRenderer, NodeReplacement, SubNodeTransform } from '../../replace-components/component-replacer' +import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer' +import type { Element } from 'domhandler' +import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon' +import { isText } from 'domhandler' +import { cssColor } from './blockquote-border-color-node-preprocessor' +import Optional from 'optional-js' +import type { Text } from 'domhandler/lib/node' +import { BlockquoteExtraTagMarkdownExtension } from './blockquote-extra-tag-markdown-extension' + +/** + * Replaces elements with "color" as label and a valid color as content + * with an colored label icon. + * + * @see BlockquoteTagMarkdownItPlugin + */ +export class BlockquoteColorExtraTagReplacer extends ComponentReplacer { + replace(element: Element, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): NodeReplacement { + if ( + element.tagName === BlockquoteExtraTagMarkdownExtension.tagName && + element.attribs?.['data-label'] === 'color' && + element.children !== undefined + ) { + return Optional.ofNullable(element.children[0]) + .filter(isText) + .map((child) => (child as Text).data) + .filter((content) => cssColor.test(content)) + .map((color) => ( + + + + )) + .orElse(DO_NOT_REPLACE) + } + } +} 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 new file mode 100644 index 000000000..8822ed868 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-extension.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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' + +/** + * Adds support for generic blockquote extra tags and blockquote color extra tags. + */ +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) + } + + public buildReplacers(): ComponentReplacer[] { + return [new BlockquoteColorExtraTagReplacer(), new BlockquoteExtraTagReplacer()] + } + + public buildNodeProcessors(): NodeProcessor[] { + return [new BlockquoteBorderColorNodePreprocessor()] + } + + public buildTagNameWhitelist(): string[] { + return [BlockquoteExtraTagMarkdownExtension.tagName] + } +} diff --git a/src/components/markdown-renderer/markdown-it-plugins/quote-extra.test.ts b/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-it-plugin.test.ts similarity index 54% rename from src/components/markdown-renderer/markdown-it-plugins/quote-extra.test.ts rename to src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-it-plugin.test.ts index 5069471bf..3925b763a 100644 --- a/src/components/markdown-renderer/markdown-it-plugins/quote-extra.test.ts +++ b/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-it-plugin.test.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { QuoteExtraTagValues } from './quote-extra' -import { parseQuoteExtraTag } from './quote-extra' +import type { QuoteExtraTagValues } from './parse-blockquote-extra-tag' +import { parseBlockquoteExtraTag } from './parse-blockquote-extra-tag' describe('Quote extra syntax parser', () => { it('should parse a valid tag', () => { @@ -17,46 +17,46 @@ describe('Quote extra syntax parser', () => { label: 'abc', value: 'def' } - expect(parseQuoteExtraTag('[abc=def]', 0, 1000)).toEqual(expected) + expect(parseBlockquoteExtraTag('[abc=def]', 0, 1000)).toEqual(expected) }) it("shouldn't parse a tag with no opener bracket", () => { - expect(parseQuoteExtraTag('abc=def]', 0, 1000)).toEqual(undefined) + expect(parseBlockquoteExtraTag('abc=def]', 0, 1000)).toEqual(undefined) }) it("shouldn't parse a tag with no closing bracket", () => { - expect(parseQuoteExtraTag('[abc=def', 0, 1000)).toEqual(undefined) + expect(parseBlockquoteExtraTag('[abc=def', 0, 1000)).toEqual(undefined) }) it("shouldn't parse a tag with no separation character", () => { - expect(parseQuoteExtraTag('[abcdef]', 0, 1000)).toEqual(undefined) + expect(parseBlockquoteExtraTag('[abcdef]', 0, 1000)).toEqual(undefined) }) it("shouldn't parse a tag with an empty label", () => { - expect(parseQuoteExtraTag('[=def]', 0, 1000)).toEqual(undefined) + expect(parseBlockquoteExtraTag('[=def]', 0, 1000)).toEqual(undefined) }) it("shouldn't parse a tag with an empty value", () => { - expect(parseQuoteExtraTag('[abc=]', 0, 1000)).toEqual(undefined) + expect(parseBlockquoteExtraTag('[abc=]', 0, 1000)).toEqual(undefined) }) it("shouldn't parse a tag with an empty body", () => { - expect(parseQuoteExtraTag('[]', 0, 1000)).toEqual(undefined) + expect(parseBlockquoteExtraTag('[]', 0, 1000)).toEqual(undefined) }) it("shouldn't parse a tag with an empty body", () => { - expect(parseQuoteExtraTag('[]', 0, 1000)).toEqual(undefined) + expect(parseBlockquoteExtraTag('[]', 0, 1000)).toEqual(undefined) }) it("shouldn't parse a correct tag if start index isn't at the opening bracket", () => { - expect(parseQuoteExtraTag('[abc=def]', 1, 1000)).toEqual(undefined) + expect(parseBlockquoteExtraTag('[abc=def]', 1, 1000)).toEqual(undefined) }) it("shouldn't parse a correct tag if maxPos ends before tag end", () => { - expect(parseQuoteExtraTag('[abc=def]', 0, 1)).toEqual(undefined) + expect(parseBlockquoteExtraTag('[abc=def]', 0, 1)).toEqual(undefined) }) it("shouldn't parse a correct tag if start index is after maxPos", () => { - expect(parseQuoteExtraTag(' [abc=def]', 3, 2)).toEqual(undefined) + expect(parseBlockquoteExtraTag(' [abc=def]', 3, 2)).toEqual(undefined) }) }) 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 new file mode 100644 index 000000000..2e3e1234e --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-markdown-it-plugin.ts @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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' + +export interface BlockquoteTagOptions { + parseSubTags?: boolean + valueRegex?: RegExp + icon?: IconName +} + +/** + * Detects the blockquote extra tag syntax `[label=value]` and creates elements. + */ +export class BlockquoteExtraTagMarkdownItPlugin { + constructor(private tagName: string, private icon: IconName) {} + + public static registerRenderer(markdownIt: MarkdownIt): void { + if (markdownIt.renderer.rules['blockquote_tag']) { + return + } + markdownIt.renderer.rules['blockquote_tag'] = (tokens, idx, options: MarkdownIt.Options, env: unknown) => { + const token = tokens[idx] + const innerTokens = token.children + const label = token.attrGet('label') ?? '' + const icon = token.attrGet('icon') + + const iconAttribute = icon === null ? '' : ` data-icon="${icon}"` + + const innerHtml = innerTokens === null ? '' : markdownIt.renderer.renderInline(innerTokens, options, env) + return `<${BlockquoteExtraTagMarkdownExtension.tagName} data-label='${label}'${iconAttribute}>${innerHtml}` + } + } + + public registerInlineRule(markdownIt: MarkdownIt): void { + markdownIt.inline.ruler.before('link', `blockquote_${this.tagName}`, (state) => + this.parseSpecificBlockquoteTag(state) + .map((parseResults) => { + const token = this.createBlockquoteTagToken(state, parseResults) + this.processTagValue(token, state, parseResults) + return true + }) + .orElse(false) + ) + } + + private parseSpecificBlockquoteTag(state: StateInline): Optional { + return Optional.ofNullable(parseBlockquoteExtraTag(state.src, state.pos, state.posMax)) + .filter((results) => results.label === this.tagName) + .map((parseResults) => { + state.pos = parseResults.valueEndIndex + 1 + return parseResults + }) + } + + private createBlockquoteTagToken(state: StateInline, parseResults: QuoteExtraTagValues): Token { + const token = state.push('blockquote_tag', '', 0) + token.attrSet('label', parseResults.label) + token.attrSet('icon', this.icon) + return token + } + + protected processTagValue(token: Token, state: StateInline, parseResults: QuoteExtraTagValues): void { + const childTokens: Token[] = [] + state.md.inline.parse(parseResults.value, state.md, state.env, childTokens) + token.children = childTokens + } +} diff --git a/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-replacer.tsx b/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-replacer.tsx new file mode 100644 index 000000000..0eaa05c0b --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/blockquote/blockquote-extra-tag-replacer.tsx @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { NativeRenderer, NodeReplacement, SubNodeTransform } from '../../replace-components/component-replacer' +import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer' +import type { Element } from 'domhandler' +import type { ForkAwesomeIconProps } from '../../../common/fork-awesome/fork-awesome-icon' +import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon' +import type { IconName } from '../../../common/fork-awesome/types' +import { ForkAwesomeIcons } from '../../../common/fork-awesome/fork-awesome-icons' +import Optional from 'optional-js' +import type { ReactElement } from 'react' +import { BlockquoteExtraTagMarkdownExtension } from './blockquote-extra-tag-markdown-extension' + +/** + * Replaces elements with an icon and a small text. + * + * @see BlockquoteTagMarkdownItPlugin + * @see ColoredBlockquoteNodePreprocessor + */ +export class BlockquoteExtraTagReplacer extends ComponentReplacer { + replace(element: Element, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): NodeReplacement { + if (element.tagName !== BlockquoteExtraTagMarkdownExtension.tagName || !element.attribs) { + return DO_NOT_REPLACE + } + + return ( + + {this.buildIconElement(element)} + {BlockquoteExtraTagReplacer.transformChildren(element, subNodeTransform)} + + ) + } + + /** + * Extracts a fork awesome icon name from the node and builds a {@link ForkAwesomeIcon fork awesome icon react element}. + * + * @param node The node that holds the "data-icon" attribute. + * @return the {@link ForkAwesomeIcon fork awesome icon react element} or {@code undefined} if no icon name was found. + */ + private buildIconElement(node: Element): ReactElement | undefined { + return Optional.ofNullable(node.attribs['data-icon'] as IconName) + .filter((iconName) => ForkAwesomeIcons.includes(iconName)) + .map | undefined>((iconName) => ( + + )) + .orElse(undefined) + } +} 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 new file mode 100644 index 000000000..d3c1ee0c2 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/blockquote/parse-blockquote-extra-tag.ts @@ -0,0 +1,89 @@ +/* + * 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.substr(labelStartIndex, labelEndIndex - labelStartIndex), + value: line.substr(valueStartIndex, valueEndIndex - valueStartIndex) + } +} + +/** + * 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/replace-components/csv/csv-parser.test.ts b/src/components/markdown-renderer/markdown-extension/csv/csv-parser.test.ts similarity index 100% rename from src/components/markdown-renderer/replace-components/csv/csv-parser.test.ts rename to src/components/markdown-renderer/markdown-extension/csv/csv-parser.test.ts diff --git a/src/components/markdown-renderer/replace-components/csv/csv-parser.ts b/src/components/markdown-renderer/markdown-extension/csv/csv-parser.ts similarity index 100% rename from src/components/markdown-renderer/replace-components/csv/csv-parser.ts rename to src/components/markdown-renderer/markdown-extension/csv/csv-parser.ts diff --git a/src/components/markdown-renderer/replace-components/csv/csv-replacer.tsx b/src/components/markdown-renderer/markdown-extension/csv/csv-replacer.tsx similarity index 84% rename from src/components/markdown-renderer/replace-components/csv/csv-replacer.tsx rename to src/components/markdown-renderer/markdown-extension/csv/csv-replacer.tsx index 80ef3596a..11dbe1229 100644 --- a/src/components/markdown-renderer/replace-components/csv/csv-replacer.tsx +++ b/src/components/markdown-renderer/markdown-extension/csv/csv-replacer.tsx @@ -6,9 +6,9 @@ import type { Element } from 'domhandler' import React from 'react' -import { ComponentReplacer } from '../component-replacer' +import { ComponentReplacer } from '../../replace-components/component-replacer' import { CsvTable } from './csv-table' -import { CodeBlockComponentReplacer } from '../code-block-component-replacer' +import { CodeBlockComponentReplacer } from '../../replace-components/code-block-component-replacer' /** * Detects code blocks with "csv" as language and renders them as table. diff --git a/src/components/markdown-renderer/markdown-extension/csv/csv-table-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/csv/csv-table-markdown-extension.ts new file mode 100644 index 000000000..2c43a12d2 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/csv/csv-table-markdown-extension.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import { CsvReplacer } from './csv-replacer' +import type { ComponentReplacer } from '../../replace-components/component-replacer' + +/** + * Adds support for csv tables to the markdown rendering using code fences with "csv" as language. + */ +export class CsvTableMarkdownExtension extends MarkdownExtension { + public buildReplacers(): ComponentReplacer[] { + return [new CsvReplacer()] + } +} diff --git a/src/components/markdown-renderer/replace-components/csv/csv-table.tsx b/src/components/markdown-renderer/markdown-extension/csv/csv-table.tsx similarity index 77% rename from src/components/markdown-renderer/replace-components/csv/csv-table.tsx rename to src/components/markdown-renderer/markdown-extension/csv/csv-table.tsx index dc8ad4cf0..fc5acbcd1 100644 --- a/src/components/markdown-renderer/replace-components/csv/csv-table.tsx +++ b/src/components/markdown-renderer/markdown-extension/csv/csv-table.tsx @@ -31,24 +31,24 @@ export const CsvTable: React.FC = ({ return { rowsWithColumns, headerRow } }, [code, delimiter, showHeader]) - const renderTableHeader = (row: string[]) => { - if (row !== []) { - return ( + const renderTableHeader = useMemo( + () => + headerRow === [] ? undefined : ( - {row.map((column, columnNumber) => ( + {headerRow.map((column, columnNumber) => ( {column} ))} - ) - } - } + ), + [headerRow] + ) - const renderTableBody = (rows: string[][]) => { - return ( + const renderTableBody = useMemo( + () => ( - {rows.map((row, rowNumber) => ( + {rowsWithColumns.map((row, rowNumber) => ( {row.map((column, columnIndex) => ( @@ -58,13 +58,14 @@ export const CsvTable: React.FC = ({ ))} - ) - } + ), + [rowsWithColumns, tableColumnClassName, tableRowClassName] + ) return ( - {renderTableHeader(headerRow)} - {renderTableBody(rowsWithColumns)} + {renderTableHeader} + {renderTableBody}
    ) } diff --git a/src/components/markdown-renderer/markdown-extension/debugger-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/debugger-markdown-extension.ts new file mode 100644 index 000000000..422b28583 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/debugger-markdown-extension.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from './markdown-extension' +import type MarkdownIt from 'markdown-it' +import { Logger } from '../../../utils/logger' + +const log = new Logger('DebuggerMarkdownExtension') + +export class DebuggerMarkdownExtension extends MarkdownExtension { + public configureMarkdownItPost(markdownIt: MarkdownIt): void { + if (process.env.NODE_ENV !== 'production') { + markdownIt.core.ruler.push('printStateToConsole', (state) => { + log.debug('Current state', state) + return false + }) + } + } +} diff --git a/src/components/markdown-renderer/markdown-extension/emoji/emoji-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/emoji/emoji-markdown-extension.ts new file mode 100644 index 000000000..ae59a02d2 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/emoji/emoji-markdown-extension.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import type MarkdownIt from 'markdown-it' +import emoji from 'markdown-it-emoji/bare' +import { combinedEmojiData } from './mapping' + +/** + * Adds support for utf-8 emojis. + */ +export class EmojiMarkdownExtension extends MarkdownExtension { + public configureMarkdownIt(markdownIt: MarkdownIt): void { + markdownIt.use(emoji, { + defs: combinedEmojiData + }) + } +} diff --git a/src/components/markdown-renderer/markdown-it-plugins/emoji/mapping.ts b/src/components/markdown-renderer/markdown-extension/emoji/mapping.ts similarity index 100% rename from src/components/markdown-renderer/markdown-it-plugins/emoji/mapping.ts rename to src/components/markdown-renderer/markdown-extension/emoji/mapping.ts diff --git a/src/components/markdown-renderer/markdown-extension/flowchart/flowchart-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/flowchart/flowchart-markdown-extension.ts new file mode 100644 index 000000000..d2b3fa611 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/flowchart/flowchart-markdown-extension.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import { CodeBlockComponentReplacer } from '../../replace-components/code-block-component-replacer' +import type { ComponentReplacer } from '../../replace-components/component-replacer' +import { FlowChart } from './flowchart' + +/** + * Adds support for flow charts to the markdown rendering using code fences with "flow" as language. + */ +export class FlowchartMarkdownExtension extends MarkdownExtension { + public buildReplacers(): ComponentReplacer[] { + return [new CodeBlockComponentReplacer(FlowChart, 'flow')] + } +} diff --git a/src/components/markdown-renderer/replace-components/flow/flowchart.tsx b/src/components/markdown-renderer/markdown-extension/flowchart/flowchart.tsx similarity index 100% rename from src/components/markdown-renderer/replace-components/flow/flowchart.tsx rename to src/components/markdown-renderer/markdown-extension/flowchart/flowchart.tsx diff --git a/src/components/markdown-renderer/markdown-extension/generic-syntax-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/generic-syntax-markdown-extension.ts new file mode 100644 index 000000000..409e0899f --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/generic-syntax-markdown-extension.ts @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from './markdown-extension' +import type MarkdownIt from 'markdown-it' +import abbreviation from 'markdown-it-abbr' +import definitionList from 'markdown-it-deflist' +import subscript from 'markdown-it-sub' +import superscript from 'markdown-it-sup' +import inserted from 'markdown-it-ins' +import marked from 'markdown-it-mark' +import footnote from 'markdown-it-footnote' +import { imageSize } from '@hedgedoc/markdown-it-image-size' + +export class GenericSyntaxMarkdownExtension extends MarkdownExtension { + public configureMarkdownIt(markdownIt: MarkdownIt): void { + abbreviation(markdownIt) + definitionList(markdownIt) + subscript(markdownIt) + superscript(markdownIt) + inserted(markdownIt) + marked(markdownIt) + footnote(markdownIt) + imageSize(markdownIt) + } +} diff --git a/src/components/markdown-renderer/replace-components/gist/gist-frame.scss b/src/components/markdown-renderer/markdown-extension/gist/gist-frame.scss similarity index 100% rename from src/components/markdown-renderer/replace-components/gist/gist-frame.scss rename to src/components/markdown-renderer/markdown-extension/gist/gist-frame.scss diff --git a/src/components/markdown-renderer/replace-components/gist/gist-frame.tsx b/src/components/markdown-renderer/markdown-extension/gist/gist-frame.tsx similarity index 88% rename from src/components/markdown-renderer/replace-components/gist/gist-frame.tsx rename to src/components/markdown-renderer/markdown-extension/gist/gist-frame.tsx index aab6e4f82..71d941acb 100644 --- a/src/components/markdown-renderer/replace-components/gist/gist-frame.tsx +++ b/src/components/markdown-renderer/markdown-extension/gist/gist-frame.tsx @@ -8,8 +8,8 @@ import React, { useCallback } from 'react' import { cypressId } from '../../../../utils/cypress-attribute' import './gist-frame.scss' import { useResizeGistFrame } from './use-resize-gist-frame' -import { ClickShield } from '../click-shield/click-shield' -import type { IdProps } from '../custom-tag-with-id-component-replacer' +import type { IdProps } from '../../replace-components/custom-tag-with-id-component-replacer' +import { ClickShield } from '../../replace-components/click-shield/click-shield' /** * This component renders a GitHub Gist by placing the gist URL in an {@link HTMLIFrameElement iframe}. diff --git a/src/components/markdown-renderer/markdown-extension/gist/gist-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/gist/gist-markdown-extension.ts new file mode 100644 index 000000000..0a7cbacf9 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/gist/gist-markdown-extension.ts @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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 { replaceGistLink } from './replace-gist-link' +import { replaceLegacyGistShortCode } from './replace-legacy-gist-short-code' +import { GistFrame } from './gist-frame' + +/** + * Adds support for embeddings of GitHub Gists by detecting gist links and the legacy gist shortcode. + */ +export class GistMarkdownExtension extends MarkdownExtension { + public static readonly tagName = 'app-gist' + + public configureMarkdownIt(markdownIt: MarkdownIt): void { + markdownItRegex(markdownIt, replaceGistLink) + markdownItRegex(markdownIt, replaceLegacyGistShortCode) + } + + public buildReplacers(): ComponentReplacer[] { + return [new CustomTagWithIdComponentReplacer(GistFrame, GistMarkdownExtension.tagName)] + } + + public buildTagNameWhitelist(): string[] { + return [GistMarkdownExtension.tagName] + } +} diff --git a/src/components/markdown-renderer/replace-components/gist/replace-gist-link.ts b/src/components/markdown-renderer/markdown-extension/gist/replace-gist-link.ts similarity index 83% rename from src/components/markdown-renderer/replace-components/gist/replace-gist-link.ts rename to src/components/markdown-renderer/markdown-extension/gist/replace-gist-link.ts index d4a647d40..8c2f12d59 100644 --- a/src/components/markdown-renderer/replace-components/gist/replace-gist-link.ts +++ b/src/components/markdown-renderer/markdown-extension/gist/replace-gist-link.ts @@ -5,6 +5,7 @@ */ import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' +import { GistMarkdownExtension } from './gist-markdown-extension' const protocolRegex = /(?:http(?:s)?:\/\/)?/ const domainRegex = /(?:gist\.github\.com\/)/ @@ -19,6 +20,6 @@ export const replaceGistLink: RegexOptions = { replace: (match) => { // ESLint wants to collapse this tag, but then the tag won't be valid html anymore. // noinspection CheckTagEmptyBody - return `` + return `<${GistMarkdownExtension.tagName} id='${match}'>` } } diff --git a/src/components/markdown-renderer/replace-components/gist/replace-legacy-gist-short-code.ts b/src/components/markdown-renderer/markdown-extension/gist/replace-legacy-gist-short-code.ts similarity index 76% rename from src/components/markdown-renderer/replace-components/gist/replace-legacy-gist-short-code.ts rename to src/components/markdown-renderer/markdown-extension/gist/replace-legacy-gist-short-code.ts index ab4b81b20..b5286a6c9 100644 --- a/src/components/markdown-renderer/replace-components/gist/replace-legacy-gist-short-code.ts +++ b/src/components/markdown-renderer/markdown-extension/gist/replace-legacy-gist-short-code.ts @@ -5,6 +5,7 @@ */ import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' +import { GistMarkdownExtension } from './gist-markdown-extension' const finalRegex = /^{%gist (\w+\/\w+) ?%}$/ @@ -14,6 +15,6 @@ export const replaceLegacyGistShortCode: RegexOptions = { replace: (match) => { // ESLint wants to collapse this tag, but then the tag won't be valid html anymore. // noinspection CheckTagEmptyBody - return `` + return `<${GistMarkdownExtension.tagName} id="${match}">` } } diff --git a/src/components/markdown-renderer/replace-components/gist/use-resize-gist-frame.ts b/src/components/markdown-renderer/markdown-extension/gist/use-resize-gist-frame.ts similarity index 100% rename from src/components/markdown-renderer/replace-components/gist/use-resize-gist-frame.ts rename to src/components/markdown-renderer/markdown-extension/gist/use-resize-gist-frame.ts diff --git a/src/components/markdown-renderer/replace-components/graphviz/graphviz-frame.tsx b/src/components/markdown-renderer/markdown-extension/graphviz/graphviz-frame.tsx similarity index 96% rename from src/components/markdown-renderer/replace-components/graphviz/graphviz-frame.tsx rename to src/components/markdown-renderer/markdown-extension/graphviz/graphviz-frame.tsx index 3edcff543..d16b30e26 100644 --- a/src/components/markdown-renderer/replace-components/graphviz/graphviz-frame.tsx +++ b/src/components/markdown-renderer/markdown-extension/graphviz/graphviz-frame.tsx @@ -10,7 +10,7 @@ import { ShowIf } from '../../../common/show-if/show-if' import { useFrontendBaseUrl } from '../../../../hooks/common/use-frontend-base-url' import { Logger } from '../../../../utils/logger' import { cypressId } from '../../../../utils/cypress-attribute' -import type { CodeProps } from '../code-block-component-replacer' +import type { CodeProps } from '../../replace-components/code-block-component-replacer' const log = new Logger('GraphvizFrame') diff --git a/src/components/markdown-renderer/markdown-extension/graphviz/graphviz-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/graphviz/graphviz-markdown-extension.ts new file mode 100644 index 000000000..177f65984 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/graphviz/graphviz-markdown-extension.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import { CodeBlockComponentReplacer } from '../../replace-components/code-block-component-replacer' +import type { ComponentReplacer } from '../../replace-components/component-replacer' +import { GraphvizFrame } from './graphviz-frame' + +/** + * Adds support for graphviz to the markdown rendering using code fences with "graphviz" as language. + */ +export class GraphvizMarkdownExtension extends MarkdownExtension { + public buildReplacers(): ComponentReplacer[] { + return [new CodeBlockComponentReplacer(GraphvizFrame, 'graphviz')] + } +} diff --git a/src/components/markdown-renderer/markdown-extension/headline-anchors-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/headline-anchors-markdown-extension.ts new file mode 100644 index 000000000..9bca7727d --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/headline-anchors-markdown-extension.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from './markdown-extension' +import type MarkdownIt from 'markdown-it' +import anchor from 'markdown-it-anchor' + +export class HeadlineAnchorsMarkdownExtension extends MarkdownExtension { + public configureMarkdownIt(markdownIt: MarkdownIt): void { + anchor(markdownIt, { + permalink: anchor.permalink.ariaHidden({ + symbol: '', + class: 'heading-anchor text-dark', + renderHref: (slug: string): string => `#${slug}`, + placement: 'before' + }) + }) + } +} diff --git a/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code-markdown-extension.ts new file mode 100644 index 000000000..3f473e261 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code-markdown-extension.ts @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import type MarkdownIt from 'markdown-it' +import { HighlightedCodeReplacer } from './highlighted-code-replacer' +import type { ComponentReplacer } from '../../replace-components/component-replacer' + +/** + * Adds support code highlighting to the markdown rendering. + * Every code fence that is not replaced by another replacer is highlighted using highlightjs. + */ +export class HighlightedCodeMarkdownExtension extends MarkdownExtension { + private static readonly highlightRegex = /^ *([\w-]*)(.*)$/ + + public configureMarkdownIt(markdownIt: MarkdownIt): void { + markdownIt.core.ruler.push('highlighted-code', (state) => { + state.tokens.forEach((token) => { + if (token.type === 'fence') { + const highlightInfos = HighlightedCodeMarkdownExtension.highlightRegex.exec(token.info) + if (!highlightInfos) { + return + } + if (highlightInfos[1]) { + token.attrJoin('data-highlight-language', highlightInfos[1]) + } + if (highlightInfos[2]) { + token.attrJoin('data-extra', highlightInfos[2]) + } + } + }) + return true + }) + } + + public buildReplacers(): ComponentReplacer[] { + return [new HighlightedCodeReplacer()] + } +} diff --git a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-fence-replacer.tsx b/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code-replacer.tsx similarity index 92% rename from src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-fence-replacer.tsx rename to src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code-replacer.tsx index e7ef46276..b514f6dd0 100644 --- a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-fence-replacer.tsx +++ b/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code-replacer.tsx @@ -6,8 +6,8 @@ import type { Element } from 'domhandler' import React from 'react' -import { ComponentReplacer } from '../component-replacer' -import { HighlightedCode } from './highlighted-code/highlighted-code' +import { ComponentReplacer } from '../../replace-components/component-replacer' +import { HighlightedCode } from './highlighted-code' /** * Detects code blocks and renders them as highlighted code blocks diff --git a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.scss b/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code.scss similarity index 88% rename from src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.scss rename to src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code.scss index dcc8745ef..14f2c90c7 100644 --- a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.scss +++ b/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code.scss @@ -5,10 +5,10 @@ */ .code-highlighter { - @import '../../../../../../node_modules/highlight.js/styles/github'; + @import '../../../../../node_modules/highlight.js/styles/github'; body.dark & { - @import '../../../../../../node_modules/highlight.js/styles/github-dark'; + @import '../../../../../node_modules/highlight.js/styles/github-dark'; } position: relative; diff --git a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx b/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code.tsx similarity index 86% rename from src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx rename to src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code.tsx index b12cf67ff..56620931e 100644 --- a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx +++ b/src/components/markdown-renderer/markdown-extension/highlighted-fence/highlighted-code.tsx @@ -7,11 +7,11 @@ import type { ReactElement } from 'react' import React, { Fragment, useEffect, useState } from 'react' import convertHtmlToReact from '@hedgedoc/html-to-react' -import { CopyToClipboardButton } from '../../../../common/copyable/copy-to-clipboard-button/copy-to-clipboard-button' -import '../../../utils/button-inside.scss' +import { CopyToClipboardButton } from '../../../common/copyable/copy-to-clipboard-button/copy-to-clipboard-button' +import '../../utils/button-inside.scss' import './highlighted-code.scss' -import { Logger } from '../../../../../utils/logger' -import { cypressId } from '../../../../../utils/cypress-attribute' +import { Logger } from '../../../../utils/logger' +import { cypressId } from '../../../../utils/cypress-attribute' const log = new Logger('HighlightedCode') @@ -45,7 +45,7 @@ export const HighlightedCode: React.FC = ({ code, language const [dom, setDom] = useState() useEffect(() => { - import(/* webpackChunkName: "highlight.js" */ '../../../../common/hljs/hljs') + import(/* webpackChunkName: "highlight.js" */ '../../../common/hljs/hljs') .then((hljs) => { const languageSupported = (lang: string) => hljs.default.listLanguages().includes(lang) const unreplacedCode = diff --git a/src/components/markdown-renderer/replace-components/image/image-lightbox-modal.tsx b/src/components/markdown-renderer/markdown-extension/image/image-lightbox-modal.tsx similarity index 100% rename from src/components/markdown-renderer/replace-components/image/image-lightbox-modal.tsx rename to src/components/markdown-renderer/markdown-extension/image/image-lightbox-modal.tsx diff --git a/src/components/markdown-renderer/replace-components/image/lightbox.scss b/src/components/markdown-renderer/markdown-extension/image/lightbox.scss similarity index 100% rename from src/components/markdown-renderer/replace-components/image/lightbox.scss rename to src/components/markdown-renderer/markdown-extension/image/lightbox.scss diff --git a/src/components/markdown-renderer/replace-components/image/proxy-image-frame.tsx b/src/components/markdown-renderer/markdown-extension/image/proxy-image-frame.tsx similarity index 100% rename from src/components/markdown-renderer/replace-components/image/proxy-image-frame.tsx rename to src/components/markdown-renderer/markdown-extension/image/proxy-image-frame.tsx diff --git a/src/components/markdown-renderer/markdown-extension/image/proxy-image-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/image/proxy-image-markdown-extension.ts new file mode 100644 index 000000000..7cafe1f5d --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/image/proxy-image-markdown-extension.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import type { ComponentReplacer } from '../../replace-components/component-replacer' +import type { ImageClickHandler } from './proxy-image-replacer' +import { ProxyImageReplacer } from './proxy-image-replacer' + +/** + * Adds support for image lightbox and image proxy redirection. + */ +export class ProxyImageMarkdownExtension extends MarkdownExtension { + constructor(private onImageClick?: ImageClickHandler) { + super() + } + + buildReplacers(): ComponentReplacer[] { + return [new ProxyImageReplacer(this.onImageClick)] + } +} diff --git a/src/components/markdown-renderer/replace-components/image/image-replacer.tsx b/src/components/markdown-renderer/markdown-extension/image/proxy-image-replacer.tsx similarity index 88% rename from src/components/markdown-renderer/replace-components/image/image-replacer.tsx rename to src/components/markdown-renderer/markdown-extension/image/proxy-image-replacer.tsx index 693d76f1d..7d155fb1f 100644 --- a/src/components/markdown-renderer/replace-components/image/image-replacer.tsx +++ b/src/components/markdown-renderer/markdown-extension/image/proxy-image-replacer.tsx @@ -6,7 +6,7 @@ import type { Element } from 'domhandler' import React from 'react' -import { ComponentReplacer } from '../component-replacer' +import { ComponentReplacer } from '../../replace-components/component-replacer' import { ProxyImageFrame } from './proxy-image-frame' export type ImageClickHandler = (event: React.MouseEvent) => void @@ -14,7 +14,7 @@ export type ImageClickHandler = (event: React.MouseEvent`, + afterMath: ``, + beforeInlineMath: `<${KatexMarkdownExtension.tagName} data-inline="true">`, + afterInlineMath: ``, + beforeDisplayMath: `<${KatexMarkdownExtension.tagName}>`, + afterDisplayMath: `` + })(markdownIt) + } + + public buildReplacers(): ComponentReplacer[] { + return [new KatexReplacer()] + } + + public buildTagNameWhitelist(): string[] { + return [KatexMarkdownExtension.tagName] + } +} diff --git a/src/components/markdown-renderer/replace-components/katex/katex-replacer.tsx b/src/components/markdown-renderer/markdown-extension/katex/katex-replacer.tsx similarity index 74% rename from src/components/markdown-renderer/replace-components/katex/katex-replacer.tsx rename to src/components/markdown-renderer/markdown-extension/katex/katex-replacer.tsx index 948b69e71..b69f7c589 100644 --- a/src/components/markdown-renderer/replace-components/katex/katex-replacer.tsx +++ b/src/components/markdown-renderer/markdown-extension/katex/katex-replacer.tsx @@ -6,11 +6,10 @@ import type { Element } from 'domhandler' import { isTag } from 'domhandler' -import type MarkdownIt from 'markdown-it' -import mathJax from 'markdown-it-mathjax' import React from 'react' -import { ComponentReplacer, DO_NOT_REPLACE } from '../component-replacer' +import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer' import './katex.scss' +import { KatexMarkdownExtension } from './katex-markdown-extension' /** * Checks if the given node is a KaTeX block. @@ -35,7 +34,9 @@ const containsKatexBlock = (node: Element): Element | undefined => { * @return {@code true} if the given node is a katex element. */ const isKatexTag = (node: Element, expectedInline: boolean) => { - return node.name === 'app-katex' && (node.attribs?.['data-inline'] !== undefined) === expectedInline + return ( + node.name === KatexMarkdownExtension.tagName && (node.attribs?.['data-inline'] !== undefined) === expectedInline + ) } const KaTeX = React.lazy(() => import(/* webpackChunkName: "katex" */ '@matejmazur/react-katex')) @@ -44,15 +45,6 @@ const KaTeX = React.lazy(() => import(/* webpackChunkName: "katex" */ '@matejmaz * Detects LaTeX syntax and renders it with KaTeX. */ export class KatexReplacer extends ComponentReplacer { - public static readonly markdownItPlugin: MarkdownIt.PluginSimple = mathJax({ - beforeMath: '', - afterMath: '', - beforeInlineMath: '', - afterInlineMath: '', - beforeDisplayMath: '', - afterDisplayMath: '' - }) - public replace(node: Element): React.ReactElement | undefined { if (!(isKatexTag(node, true) || containsKatexBlock(node)) || node.children?.[0] === undefined) { return DO_NOT_REPLACE diff --git a/src/components/markdown-renderer/replace-components/katex/katex.scss b/src/components/markdown-renderer/markdown-extension/katex/katex.scss similarity index 66% rename from src/components/markdown-renderer/replace-components/katex/katex.scss rename to src/components/markdown-renderer/markdown-extension/katex/katex.scss index 1cddac9db..e92d27476 100644 --- a/src/components/markdown-renderer/replace-components/katex/katex.scss +++ b/src/components/markdown-renderer/markdown-extension/katex/katex.scss @@ -4,4 +4,4 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -@import '../../../../../node_modules/katex/dist/katex.min'; +@import '../../../../../node_modules/katex/dist/katex.min.css'; diff --git a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/legacy-shortcodes-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/legacy-shortcodes-markdown-extension.ts new file mode 100644 index 000000000..7a5377c6f --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/legacy-shortcodes-markdown-extension.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import type MarkdownIt from 'markdown-it' +import { legacyPdfShortCode } from './replace-legacy-pdf-short-code' +import { legacySlideshareShortCode } from './replace-legacy-slideshare-short-code' +import { legacySpeakerdeckShortCode } from './replace-legacy-speakerdeck-short-code' + +/** + * Adds support for legacy shortcodes (pdf, slideshare and speakerdeck) by replacing them with anchor elements. + */ +export class LegacyShortcodesMarkdownExtension extends MarkdownExtension { + public configureMarkdownIt(markdownIt: MarkdownIt): void { + legacyPdfShortCode(markdownIt) + legacySlideshareShortCode(markdownIt) + legacySpeakerdeckShortCode(markdownIt) + } +} diff --git a/src/components/markdown-renderer/regex-plugins/replace-legacy-pdf-short-code.ts b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-pdf-short-code.ts similarity index 77% rename from src/components/markdown-renderer/regex-plugins/replace-legacy-pdf-short-code.ts rename to src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-pdf-short-code.ts index 5643f5874..a34376ee4 100644 --- a/src/components/markdown-renderer/regex-plugins/replace-legacy-pdf-short-code.ts +++ b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-pdf-short-code.ts @@ -13,8 +13,6 @@ export const legacyPdfShortCode: MarkdownIt.PluginSimple = (markdownIt) => { markdownItRegex(markdownIt, { name: 'legacy-pdf-short-code', regex: finalRegex, - replace: (match: string) => { - return `${match}` - } + replace: (match: string) => `${match}` }) } diff --git a/src/components/markdown-renderer/regex-plugins/replace-legacy-slideshare-short-code.ts b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-slideshare-short-code.ts similarity index 76% rename from src/components/markdown-renderer/regex-plugins/replace-legacy-slideshare-short-code.ts rename to src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-slideshare-short-code.ts index cfafa860b..38c2c285b 100644 --- a/src/components/markdown-renderer/regex-plugins/replace-legacy-slideshare-short-code.ts +++ b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-slideshare-short-code.ts @@ -13,8 +13,6 @@ export const legacySlideshareShortCode: MarkdownIt.PluginSimple = (markdownIt) = markdownItRegex(markdownIt, { name: 'legacy-slideshare-short-code', regex: finalRegex, - replace: (match: string) => { - return `https://www.slideshare.net/${match}` - } + replace: (match: string) => `https://www.slideshare.net/${match}` }) } diff --git a/src/components/markdown-renderer/regex-plugins/replace-legacy-speakerdeck-short-code.ts b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-speakerdeck-short-code.ts similarity index 72% rename from src/components/markdown-renderer/regex-plugins/replace-legacy-speakerdeck-short-code.ts rename to src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-speakerdeck-short-code.ts index 2fcf97a9f..42892d1e3 100644 --- a/src/components/markdown-renderer/regex-plugins/replace-legacy-speakerdeck-short-code.ts +++ b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-speakerdeck-short-code.ts @@ -13,8 +13,6 @@ export const legacySpeakerdeckShortCode: MarkdownIt.PluginSimple = (markdownIt) markdownItRegex(markdownIt, { name: 'legacy-speakerdeck-short-code', regex: finalRegex, - replace: (match: string) => { - return `https://speakerdeck.com/${match}` - } + replace: (match: string) => `https://speakerdeck.com/${match}` }) } diff --git a/src/components/markdown-renderer/markdown-extension/linemarker/add-line-marker-markdown-it-plugin.ts b/src/components/markdown-renderer/markdown-extension/linemarker/add-line-marker-markdown-it-plugin.ts new file mode 100644 index 000000000..d7a3ae533 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/linemarker/add-line-marker-markdown-it-plugin.ts @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type MarkdownIt from 'markdown-it/lib' +import Token from 'markdown-it/lib/token' +import { LinemarkerMarkdownExtension } from './linemarker-markdown-extension' + +export interface LineMarkers { + startLine: number + endLine: number +} + +/** + * This plugin adds markers to the dom, that are used to map line numbers to dom elements. + * It also provides a list of line numbers for the top level dom elements. + */ +export const addLineMarkerMarkdownItPlugin: ( + markdownIt: MarkdownIt, + lineOffset: number, + onLineMarkerChange?: (lineMarkers: LineMarkers[]) => void +) => void = (md: MarkdownIt, lineOffset, onLineMarkerChange) => { + // add app_linemarker token before each opening or self-closing level-0 tag + md.core.ruler.push('line_number_marker', (state) => { + const lineMarkers: LineMarkers[] = [] + tagTokens(state.tokens, lineMarkers, lineOffset) + if (onLineMarkerChange) { + onLineMarkerChange(lineMarkers) + } + return true + }) + + md.renderer.rules.app_linemarker = (tokens: Token[], index: number): string => { + const startLineNumber = tokens[index].attrGet('data-start-line') + const endLineNumber = tokens[index].attrGet('data-end-line') + + if (!startLineNumber || !endLineNumber) { + // don't render broken linemarkers without a linenumber + return '' + } + // noinspection CheckTagEmptyBody + return `<${LinemarkerMarkdownExtension.tagName} data-start-line='${startLineNumber}' data-end-line='${endLineNumber}'>` + } + + const insertNewLineMarker = ( + startLineNumber: number, + endLineNumber: number, + tokenPosition: number, + level: number, + tokens: Token[] + ) => { + const startToken = new Token('app_linemarker', LinemarkerMarkdownExtension.tagName, 0) + startToken.level = level + startToken.attrPush(['data-start-line', `${startLineNumber}`]) + startToken.attrPush(['data-end-line', `${endLineNumber}`]) + tokens.splice(tokenPosition, 0, startToken) + } + + const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[], lineOffset: number) => { + for (let tokenPosition = 0; tokenPosition < tokens.length; tokenPosition++) { + const token = tokens[tokenPosition] + if (token.hidden) { + continue + } + + if (!token.map) { + continue + } + + const startLineNumber = token.map[0] + 1 + const endLineNumber = token.map[1] + 1 + + if (token.level === 0) { + lineMarkers.push({ startLine: startLineNumber + lineOffset, endLine: endLineNumber + lineOffset }) + } + + insertNewLineMarker(startLineNumber, endLineNumber, tokenPosition, token.level, tokens) + tokenPosition += 1 + + if (token.children) { + tagTokens(token.children, lineMarkers, lineOffset) + } + } + } +} diff --git a/src/components/markdown-renderer/markdown-extension/linemarker/linemarker-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/linemarker/linemarker-markdown-extension.ts new file mode 100644 index 000000000..d03b97b3b --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/linemarker/linemarker-markdown-extension.ts @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import { LinemarkerReplacer } from './linemarker-replacer' +import type { ComponentReplacer } from '../../replace-components/component-replacer' +import type { LineMarkers } from './add-line-marker-markdown-it-plugin' +import { addLineMarkerMarkdownItPlugin } from './add-line-marker-markdown-it-plugin' +import type MarkdownIt from 'markdown-it' + +/** + * Adds support for the generation of line marker elements which are needed for synced scrolling. + */ +export class LinemarkerMarkdownExtension extends MarkdownExtension { + public static readonly tagName = 'app-linemarker' + + constructor(private onLineMarkers?: (lineMarkers: LineMarkers[]) => void, private lineOffset?: number) { + super() + } + + public configureMarkdownIt(markdownIt: MarkdownIt): void { + addLineMarkerMarkdownItPlugin(markdownIt, this.lineOffset ?? 0, this.onLineMarkers) + } + + public buildReplacers(): ComponentReplacer[] { + return [new LinemarkerReplacer()] + } + + public buildTagNameWhitelist(): string[] { + return [LinemarkerMarkdownExtension.tagName] + } +} diff --git a/src/components/markdown-renderer/replace-components/linemarker/linemarker-replacer.tsx b/src/components/markdown-renderer/markdown-extension/linemarker/linemarker-replacer.tsx similarity index 59% rename from src/components/markdown-renderer/replace-components/linemarker/linemarker-replacer.tsx rename to src/components/markdown-renderer/markdown-extension/linemarker/linemarker-replacer.tsx index 55f621fad..dfda7ee62 100644 --- a/src/components/markdown-renderer/replace-components/linemarker/linemarker-replacer.tsx +++ b/src/components/markdown-renderer/markdown-extension/linemarker/linemarker-replacer.tsx @@ -5,13 +5,14 @@ */ import type { Element } from 'domhandler' -import { ComponentReplacer } from '../component-replacer' +import { ComponentReplacer } from '../../replace-components/component-replacer' +import { LinemarkerMarkdownExtension } from './linemarker-markdown-extension' /** * Detects line markers and suppresses them in the resulting DOM. */ export class LinemarkerReplacer extends ComponentReplacer { public replace(codeNode: Element): null | undefined { - return codeNode.name === 'app-linemarker' ? null : undefined + return codeNode.name === LinemarkerMarkdownExtension.tagName ? null : undefined } } diff --git a/src/components/markdown-renderer/types.d.ts b/src/components/markdown-renderer/markdown-extension/linemarker/types.d.ts similarity index 100% rename from src/components/markdown-renderer/types.d.ts rename to src/components/markdown-renderer/markdown-extension/linemarker/types.d.ts diff --git a/src/components/markdown-renderer/markdown-extension/link-replacer/anchor-node-preprocessor.ts b/src/components/markdown-renderer/markdown-extension/link-replacer/anchor-node-preprocessor.ts new file mode 100644 index 000000000..663ab2102 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/link-replacer/anchor-node-preprocessor.ts @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { TravelerNodeProcessor } from '../../node-preprocessors/traveler-node-processor' +import type { Node } from 'domhandler' +import { isTag } from 'domhandler' + +export class AnchorNodePreprocessor extends TravelerNodeProcessor { + constructor(private baseUrl: string) { + super() + } + + protected processNode(node: Node): void { + if (!isTag(node) || node.name !== 'a' || !node.attribs || !node.attribs.href) { + return + } + + const url = node.attribs.href.trim() + + // eslint-disable-next-line no-script-url + if (url.startsWith('data:') || url.startsWith('javascript:') || url.startsWith('vbscript:')) { + delete node.attribs.href + return + } + + const isJumpMark = url.substr(0, 1) === '#' + + if (isJumpMark) { + node.attribs['data-jump-target-id'] = url.substr(1) + } else { + node.attribs.rel = 'noreferer noopener' + node.attribs.target = '_blank' + } + + try { + node.attribs.href = new URL(url, this.baseUrl).toString() + } catch (e) { + node.attribs.href = url + } + } +} diff --git a/src/components/markdown-renderer/markdown-extension/link-replacer/jump-anchor-replacer.tsx b/src/components/markdown-renderer/markdown-extension/link-replacer/jump-anchor-replacer.tsx new file mode 100644 index 000000000..23cd31058 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/link-replacer/jump-anchor-replacer.tsx @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import type { Element } from 'domhandler' +import type { AllHTMLAttributes } from 'react' +import React from 'react' +import type { NativeRenderer, NodeReplacement, SubNodeTransform } from '../../replace-components/component-replacer' +import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer' +import { JumpAnchor } from './jump-anchor' + +/** + * Detects anchors that should jump to scroll to another element. + */ +export class JumpAnchorReplacer extends ComponentReplacer { + public replace(node: Element, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): NodeReplacement { + if (node.name !== 'a' || !node.attribs || !node.attribs['data-jump-target-id']) { + return DO_NOT_REPLACE + } + + const jumpId = node.attribs['data-jump-target-id'] + delete node.attribs['data-jump-target-id'] + const replacement = nativeRenderer() + if (replacement === null || typeof replacement === 'string') { + return replacement + } else { + return )} jumpTargetId={jumpId} /> + } + } +} diff --git a/src/components/markdown-renderer/markdown-extension/link-replacer/jump-anchor.tsx b/src/components/markdown-renderer/markdown-extension/link-replacer/jump-anchor.tsx new file mode 100644 index 000000000..e90b29340 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/link-replacer/jump-anchor.tsx @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { AllHTMLAttributes } from 'react' +import React, { useCallback } from 'react' + +export interface JumpAnchorProps extends AllHTMLAttributes { + jumpTargetId: string +} + +export const JumpAnchor: React.FC = ({ jumpTargetId, children, ...props }) => { + const jumpToTargetId = useCallback( + (event: React.MouseEvent): void => { + document.getElementById(jumpTargetId)?.scrollIntoView({ behavior: 'smooth' }) + event.preventDefault() + }, + [jumpTargetId] + ) + + return ( + + {children} + + ) +} diff --git a/src/components/markdown-renderer/markdown-extension/link-replacer/link-adjustment-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/link-replacer/link-adjustment-markdown-extension.ts new file mode 100644 index 000000000..ef9956bd1 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/link-replacer/link-adjustment-markdown-extension.ts @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import { JumpAnchorReplacer } from './jump-anchor-replacer' +import type { ComponentReplacer } from '../../replace-components/component-replacer' +import type { NodeProcessor } from '../../node-preprocessors/node-processor' +import { AnchorNodePreprocessor } from './anchor-node-preprocessor' + +/** + * Adds tweaks for anchor tags which are needed for the use in the secured iframe. + */ +export class LinkAdjustmentMarkdownExtension extends MarkdownExtension { + constructor(private baseUrl: string) { + super() + } + + public buildNodeProcessors(): NodeProcessor[] { + return [new AnchorNodePreprocessor(this.baseUrl)] + } + + public buildReplacers(): ComponentReplacer[] { + return [new JumpAnchorReplacer()] + } +} diff --git a/src/components/markdown-renderer/markdown-extension/linkify-fix-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/linkify-fix-markdown-extension.ts new file mode 100644 index 000000000..d74099c55 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/linkify-fix-markdown-extension.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from './markdown-extension' +import linkify from 'markdown-it/lib/rules_core/linkify' +import type MarkdownIt from 'markdown-it' + +export class LinkifyFixMarkdownExtension extends MarkdownExtension { + public configureMarkdownItPost(markdownIt: MarkdownIt): void { + markdownIt.core.ruler.push('linkify', (state) => { + try { + state.md.options.linkify = true + return linkify(state) + } finally { + state.md.options.linkify = false + } + }) + } +} diff --git a/src/components/markdown-renderer/markdown-extension/markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/markdown-extension.ts new file mode 100644 index 000000000..c2c8d9bf6 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/markdown-extension.ts @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type MarkdownIt from 'markdown-it' +import type { NodeProcessor } from '../node-preprocessors/node-processor' +import type { ComponentReplacer } from '../replace-components/component-replacer' + +export abstract class MarkdownExtension { + public configureMarkdownIt(markdownIt: MarkdownIt): void { + return + } + + public configureMarkdownItPost(markdownIt: MarkdownIt): void { + return + } + + public buildNodeProcessors(): NodeProcessor[] { + return [] + } + + public buildReplacers(): ComponentReplacer[] { + return [] + } + + public buildTagNameWhitelist(): string[] { + return [] + } +} diff --git a/src/components/markdown-renderer/replace-components/markmap/markmap-frame.tsx b/src/components/markdown-renderer/markdown-extension/markmap/markmap-frame.tsx similarity index 97% rename from src/components/markdown-renderer/replace-components/markmap/markmap-frame.tsx rename to src/components/markdown-renderer/markdown-extension/markmap/markmap-frame.tsx index bd730de51..eee14e901 100644 --- a/src/components/markdown-renderer/replace-components/markmap/markmap-frame.tsx +++ b/src/components/markdown-renderer/markdown-extension/markmap/markmap-frame.tsx @@ -10,7 +10,7 @@ import { LockButton } from '../../../common/lock-button/lock-button' import '../../utils/button-inside.scss' import { Logger } from '../../../../utils/logger' import { cypressId } from '../../../../utils/cypress-attribute' -import type { CodeProps } from '../code-block-component-replacer' +import type { CodeProps } from '../../replace-components/code-block-component-replacer' const log = new Logger('MarkmapFrame') diff --git a/src/components/markdown-renderer/replace-components/markmap/markmap-loader.ts b/src/components/markdown-renderer/markdown-extension/markmap/markmap-loader.ts similarity index 100% rename from src/components/markdown-renderer/replace-components/markmap/markmap-loader.ts rename to src/components/markdown-renderer/markdown-extension/markmap/markmap-loader.ts diff --git a/src/components/markdown-renderer/markdown-extension/markmap/markmap-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/markmap/markmap-markdown-extension.ts new file mode 100644 index 000000000..692594f1e --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/markmap/markmap-markdown-extension.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import { CodeBlockComponentReplacer } from '../../replace-components/code-block-component-replacer' +import type { ComponentReplacer } from '../../replace-components/component-replacer' +import { MarkmapFrame } from './markmap-frame' + +/** + * Adds support for markmap to the markdown rendering using code fences with "markmap" as language. + */ +export class MarkmapMarkdownExtension extends MarkdownExtension { + public buildReplacers(): ComponentReplacer[] { + return [new CodeBlockComponentReplacer(MarkmapFrame, 'markmap')] + } +} diff --git a/src/components/markdown-renderer/replace-components/mermaid/mermaid-chart.tsx b/src/components/markdown-renderer/markdown-extension/mermaid/mermaid-chart.tsx similarity index 96% rename from src/components/markdown-renderer/replace-components/mermaid/mermaid-chart.tsx rename to src/components/markdown-renderer/markdown-extension/mermaid/mermaid-chart.tsx index 5950ec792..fd2cbec33 100644 --- a/src/components/markdown-renderer/replace-components/mermaid/mermaid-chart.tsx +++ b/src/components/markdown-renderer/markdown-extension/mermaid/mermaid-chart.tsx @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next' import { ShowIf } from '../../../common/show-if/show-if' import './mermaid.scss' import { Logger } from '../../../../utils/logger' -import type { CodeProps } from '../code-block-component-replacer' +import type { CodeProps } from '../../replace-components/code-block-component-replacer' const log = new Logger('MermaidChart') diff --git a/src/components/markdown-renderer/markdown-extension/mermaid/mermaid-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/mermaid/mermaid-markdown-extension.ts new file mode 100644 index 000000000..efa936d62 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/mermaid/mermaid-markdown-extension.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import { CodeBlockComponentReplacer } from '../../replace-components/code-block-component-replacer' +import type { ComponentReplacer } from '../../replace-components/component-replacer' +import { MermaidChart } from './mermaid-chart' + +/** + * Adds support for chart rendering using mermaid to the markdown rendering using code fences with "mermaid" as language. + */ +export class MermaidMarkdownExtension extends MarkdownExtension { + public buildReplacers(): ComponentReplacer[] { + return [new CodeBlockComponentReplacer(MermaidChart, 'mermaid')] + } +} diff --git a/src/components/markdown-renderer/replace-components/mermaid/mermaid.scss b/src/components/markdown-renderer/markdown-extension/mermaid/mermaid.scss similarity index 100% rename from src/components/markdown-renderer/replace-components/mermaid/mermaid.scss rename to src/components/markdown-renderer/markdown-extension/mermaid/mermaid.scss diff --git a/src/components/markdown-renderer/markdown-extension/plantuml/plantuml-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/plantuml/plantuml-markdown-extension.ts new file mode 100644 index 000000000..cd5b55beb --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/plantuml/plantuml-markdown-extension.ts @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import type MarkdownIt from 'markdown-it' +import plantuml from 'markdown-it-plantuml' +import type Renderer from 'markdown-it/lib/renderer' +import type { RenderRule } from 'markdown-it/lib/renderer' +import type Token from 'markdown-it/lib/token' +import type { Options } from 'markdown-it/lib' +import type { ComponentReplacer } from '../../replace-components/component-replacer' +import { PlantumlNotConfiguredComponentReplacer } from './plantuml-not-configured-component-replacer' + +export class PlantumlMarkdownExtension extends MarkdownExtension { + constructor(private plantumlServer: string | null) { + super() + } + + private plantumlError(markdownIt: MarkdownIt): void { + const defaultRenderer: RenderRule = markdownIt.renderer.rules.fence || (() => '') + markdownIt.renderer.rules.fence = (tokens: Token[], idx: number, options: Options, env, slf: Renderer) => { + return tokens[idx].info === 'plantuml' + ? '' + : defaultRenderer(tokens, idx, options, env, slf) + } + } + + public configureMarkdownIt(markdownIt: MarkdownIt): void { + if (this.plantumlServer) { + plantuml(markdownIt, { + openMarker: '```plantuml', + closeMarker: '```', + server: this.plantumlServer + }) + } else { + this.plantumlError(markdownIt) + } + } + + buildReplacers(): ComponentReplacer[] { + return [new PlantumlNotConfiguredComponentReplacer()] + } +} diff --git a/src/components/markdown-renderer/markdown-extension/plantuml/plantuml-not-configured-alert.tsx b/src/components/markdown-renderer/markdown-extension/plantuml/plantuml-not-configured-alert.tsx new file mode 100644 index 000000000..1025eb112 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/plantuml/plantuml-not-configured-alert.tsx @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React from 'react' +import { Trans, useTranslation } from 'react-i18next' + +export const PlantumlNotConfiguredAlert: React.FC = () => { + useTranslation() + + return ( +

    + +

    + ) +} diff --git a/src/components/markdown-renderer/markdown-extension/plantuml/plantuml-not-configured-component-replacer.tsx b/src/components/markdown-renderer/markdown-extension/plantuml/plantuml-not-configured-component-replacer.tsx new file mode 100644 index 000000000..0c9651dd4 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/plantuml/plantuml-not-configured-component-replacer.tsx @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { NativeRenderer, NodeReplacement, SubNodeTransform } from '../../replace-components/component-replacer' +import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer' +import { PlantumlNotConfiguredAlert } from './plantuml-not-configured-alert' +import type { Element } from 'domhandler' + +export class PlantumlNotConfiguredComponentReplacer extends ComponentReplacer { + replace(node: Element, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): NodeReplacement { + return node.tagName === 'plantuml-not-configured' ? : DO_NOT_REPLACE + } +} diff --git a/src/components/markdown-renderer/process-reveal-comment-nodes.ts b/src/components/markdown-renderer/markdown-extension/reveal/process-reveal-comment-nodes.ts similarity index 81% rename from src/components/markdown-renderer/process-reveal-comment-nodes.ts rename to src/components/markdown-renderer/markdown-extension/reveal/process-reveal-comment-nodes.ts index 757f377a3..30a59b0fb 100644 --- a/src/components/markdown-renderer/process-reveal-comment-nodes.ts +++ b/src/components/markdown-renderer/markdown-extension/reveal/process-reveal-comment-nodes.ts @@ -3,9 +3,10 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import type { DataNode, Document, Element, Node } from 'domhandler' -import { hasChildren, isComment, isTag } from 'domhandler' -import { Logger } from '../../utils/logger' +import type { DataNode, Element, Node } from 'domhandler' +import { isComment, isTag } from 'domhandler' +import { Logger } from '../../../../utils/logger' +import { TravelerNodeProcessor } from '../../node-preprocessors/traveler-node-processor' const log = new Logger('reveal.js > Comment Node Preprocessor') const revealCommandSyntax = /^\s*\.(\w*):(.*)$/g @@ -17,20 +18,11 @@ const dataAttributesSyntax = /\s*(data-[\w-]*|class)=(?:"((?:[^"\\]|\\"|\\)*)"|' * @param doc The document that should be changed * @return The edited document */ -export const processRevealCommentNodes = (doc: Document): Document => { - visitNode(doc) - return doc -} - -/** - * Processes the given {@link Node} if it is a comment node. If the node has children then all child nodes will be processed. - * @param node The node to process. - */ -const visitNode = (node: Node): void => { - if (isComment(node)) { - processCommentNode(node) - } else if (hasChildren(node)) { - node.childNodes.forEach((childNode) => visitNode(childNode)) +export class RevealCommentCommandNodePreprocessor extends TravelerNodeProcessor { + protected processNode(node: Node): void { + if (isComment(node)) { + processCommentNode(node) + } } } diff --git a/src/components/markdown-renderer/markdown-extension/reveal/reveal-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/reveal/reveal-markdown-extension.ts new file mode 100644 index 000000000..9c3e525fd --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/reveal/reveal-markdown-extension.ts @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import type MarkdownIt from 'markdown-it' +import { addSlideSectionsMarkdownItPlugin } from './reveal-sections' +import { RevealCommentCommandNodePreprocessor } from './process-reveal-comment-nodes' +import type { NodeProcessor } from '../../node-preprocessors/node-processor' + +/** + * Adds support for reveal.js to the markdown rendering. + * This includes the generation of sections and the manipulation of elements using reveal comments. + */ +export class RevealMarkdownExtension extends MarkdownExtension { + public configureMarkdownIt(markdownIt: MarkdownIt): void { + addSlideSectionsMarkdownItPlugin(markdownIt) + } + + public buildNodeProcessors(): NodeProcessor[] { + return [new RevealCommentCommandNodePreprocessor()] + } +} diff --git a/src/components/markdown-renderer/markdown-it-plugins/reveal-sections.ts b/src/components/markdown-renderer/markdown-extension/reveal/reveal-sections.ts similarity index 100% rename from src/components/markdown-renderer/markdown-it-plugins/reveal-sections.ts rename to src/components/markdown-renderer/markdown-extension/reveal/reveal-sections.ts diff --git a/src/components/markdown-renderer/markdown-extension/sanitizer/dom-purifier-node-preprocessor.ts b/src/components/markdown-renderer/markdown-extension/sanitizer/dom-purifier-node-preprocessor.ts new file mode 100644 index 000000000..7f787145e --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/sanitizer/dom-purifier-node-preprocessor.ts @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { Document } from 'domhandler' +import render from 'dom-serializer' +import DOMPurify from 'dompurify' +import { parseDocument } from 'htmlparser2' +import { NodeProcessor } from '../../node-preprocessors/node-processor' + +/** + * Sanitizes the given {@link Document document}. + */ +export class SanitizerNodePreprocessor extends NodeProcessor { + constructor(private tagNameWhiteList: string[]) { + super() + } + + process(nodes: Document): Document { + const sanitizedHtml = DOMPurify.sanitize(render(nodes), { + ADD_TAGS: this.tagNameWhiteList + }) + return parseDocument(sanitizedHtml) + } +} diff --git a/src/components/markdown-renderer/markdown-extension/sanitizer/sanitizer-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/sanitizer/sanitizer-markdown-extension.ts new file mode 100644 index 000000000..545bdbc0e --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/sanitizer/sanitizer-markdown-extension.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import { SanitizerNodePreprocessor } from './dom-purifier-node-preprocessor' +import type { NodeProcessor } from '../../node-preprocessors/node-processor' + +/** + * Adds support for html sanitizing using dompurify to the markdown rendering. + */ +export class SanitizerMarkdownExtension extends MarkdownExtension { + constructor(private tagNameWhiteList: string[]) { + super() + } + + public buildNodeProcessors(): NodeProcessor[] { + return [new SanitizerNodePreprocessor(this.tagNameWhiteList)] + } +} diff --git a/src/components/markdown-renderer/replace-components/sequence-diagram/deprecation-warning.tsx b/src/components/markdown-renderer/markdown-extension/sequence-diagram/deprecation-warning.tsx similarity index 100% rename from src/components/markdown-renderer/replace-components/sequence-diagram/deprecation-warning.tsx rename to src/components/markdown-renderer/markdown-extension/sequence-diagram/deprecation-warning.tsx diff --git a/src/components/markdown-renderer/markdown-extension/sequence-diagram/sequence-diagram-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/sequence-diagram/sequence-diagram-markdown-extension.ts new file mode 100644 index 000000000..c470f5afb --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/sequence-diagram/sequence-diagram-markdown-extension.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import { CodeBlockComponentReplacer } from '../../replace-components/code-block-component-replacer' +import type { ComponentReplacer } from '../../replace-components/component-replacer' +import { SequenceDiagram } from './sequence-diagram' + +/** + * Adds legacy support for sequence diagram to the markdown rendering using code fences with "sequence" as language. + */ +export class SequenceDiagramMarkdownExtension extends MarkdownExtension { + public buildReplacers(): ComponentReplacer[] { + return [new CodeBlockComponentReplacer(SequenceDiagram, 'sequence')] + } +} diff --git a/src/components/markdown-renderer/replace-components/sequence-diagram/sequence-diagram.tsx b/src/components/markdown-renderer/markdown-extension/sequence-diagram/sequence-diagram.tsx similarity index 87% rename from src/components/markdown-renderer/replace-components/sequence-diagram/sequence-diagram.tsx rename to src/components/markdown-renderer/markdown-extension/sequence-diagram/sequence-diagram.tsx index 9271da3a2..08dffd1f0 100644 --- a/src/components/markdown-renderer/replace-components/sequence-diagram/sequence-diagram.tsx +++ b/src/components/markdown-renderer/markdown-extension/sequence-diagram/sequence-diagram.tsx @@ -5,7 +5,7 @@ */ import React, { Fragment } from 'react' -import type { CodeProps } from '../code-block-component-replacer' +import type { CodeProps } from '../../replace-components/code-block-component-replacer' import { MermaidChart } from '../mermaid/mermaid-chart' import { DeprecationWarning } from './deprecation-warning' diff --git a/src/components/markdown-renderer/markdown-extension/spoiler-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/spoiler-markdown-extension.ts new file mode 100644 index 000000000..cd05dd78e --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/spoiler-markdown-extension.ts @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from './markdown-extension' +import type MarkdownIt from 'markdown-it' +import markdownItContainer from 'markdown-it-container' +import type Token from 'markdown-it/lib/token' +import { escapeHtml } from 'markdown-it/lib/common/utils' + +export class SpoilerMarkdownExtension extends MarkdownExtension { + private static readonly spoilerRegEx = /^spoiler\s+(.*)$/ + + private createSpoilerContainer(): (tokens: Token[], index: number) => void { + return (tokens: Token[], index: number) => { + const matches = SpoilerMarkdownExtension.spoilerRegEx.exec(tokens[index].info.trim()) + + if (tokens[index].nesting === 1 && matches && matches[1]) { + // opening tag + return `
    ${escapeHtml(matches[1])}` + } else { + // closing tag + return '
    \n' + } + } + } + + public configureMarkdownIt(markdownIt: MarkdownIt): void { + markdownItContainer(markdownIt, 'spoiler', { + validate: (params: string) => SpoilerMarkdownExtension.spoilerRegEx.test(params), + render: () => this.createSpoilerContainer() + }) + } +} diff --git a/src/components/markdown-renderer/markdown-extension/table-of-contents-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/table-of-contents-markdown-extension.ts new file mode 100644 index 000000000..a1aa0c1f2 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/table-of-contents-markdown-extension.ts @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from './markdown-extension' +import type MarkdownIt from 'markdown-it' +import type { TocAst } from 'markdown-it-toc-done-right' +import toc from 'markdown-it-toc-done-right' +import { tocSlugify } from '../../editor-page/table-of-contents/toc-slugify' + +export class TableOfContentsMarkdownExtension extends MarkdownExtension { + constructor(private onTocChange?: (ast: TocAst) => void) { + super() + } + + public configureMarkdownIt(markdownIt: MarkdownIt): void { + if (!this.onTocChange) { + return + } + toc(markdownIt, { + placeholder: '(\\[TOC\\]|\\[toc\\])', + listType: 'ul', + level: [1, 2, 3], + callback: (code: string, ast: TocAst): void => { + this.onTocChange?.(ast) + }, + slugify: tocSlugify + }) + } +} diff --git a/src/components/markdown-renderer/replace-components/task-list/task-list-checkbox.tsx b/src/components/markdown-renderer/markdown-extension/task-list/task-list-checkbox.tsx similarity index 100% rename from src/components/markdown-renderer/replace-components/task-list/task-list-checkbox.tsx rename to src/components/markdown-renderer/markdown-extension/task-list/task-list-checkbox.tsx diff --git a/src/components/markdown-renderer/markdown-extension/task-list/task-list-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/task-list/task-list-markdown-extension.ts new file mode 100644 index 000000000..3d2d1fc41 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/task-list/task-list-markdown-extension.ts @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import type MarkdownIt from 'markdown-it' +import type { ComponentReplacer } from '../../replace-components/component-replacer' +import type { TaskCheckedChangeHandler } from './task-list-replacer' +import { TaskListReplacer } from './task-list-replacer' +import markdownItTaskLists from '@hedgedoc/markdown-it-task-lists' + +/** + * Adds support for interactive checkbox lists to the markdown rendering using the github checklist syntax. + */ +export class TaskListMarkdownExtension extends MarkdownExtension { + constructor(private frontmatterLinesToSkip?: number, private onTaskCheckedChange?: TaskCheckedChangeHandler) { + super() + } + + public configureMarkdownIt(markdownIt: MarkdownIt): void { + markdownItTaskLists(markdownIt, { + enabled: true, + label: true, + lineNumber: true + }) + } + + public buildReplacers(): ComponentReplacer[] { + return [new TaskListReplacer(this.frontmatterLinesToSkip, this.onTaskCheckedChange)] + } +} diff --git a/src/components/markdown-renderer/replace-components/task-list/task-list-replacer.tsx b/src/components/markdown-renderer/markdown-extension/task-list/task-list-replacer.tsx similarity index 94% rename from src/components/markdown-renderer/replace-components/task-list/task-list-replacer.tsx rename to src/components/markdown-renderer/markdown-extension/task-list/task-list-replacer.tsx index 92bdc5c2d..4f00f4070 100644 --- a/src/components/markdown-renderer/replace-components/task-list/task-list-replacer.tsx +++ b/src/components/markdown-renderer/markdown-extension/task-list/task-list-replacer.tsx @@ -7,7 +7,7 @@ import type { Element } from 'domhandler' import type { ReactElement } from 'react' import React from 'react' -import { ComponentReplacer } from '../component-replacer' +import { ComponentReplacer } from '../../replace-components/component-replacer' import { TaskListCheckbox } from './task-list-checkbox' export type TaskCheckedChangeHandler = (lineInMarkdown: number, checked: boolean) => void diff --git a/src/components/markdown-renderer/replace-components/vega-lite/vega-chart.tsx b/src/components/markdown-renderer/markdown-extension/vega-lite/vega-lite-chart.tsx similarity index 93% rename from src/components/markdown-renderer/replace-components/vega-lite/vega-chart.tsx rename to src/components/markdown-renderer/markdown-extension/vega-lite/vega-lite-chart.tsx index 5ef185cd8..49a2d7cda 100644 --- a/src/components/markdown-renderer/replace-components/vega-lite/vega-chart.tsx +++ b/src/components/markdown-renderer/markdown-extension/vega-lite/vega-lite-chart.tsx @@ -10,11 +10,11 @@ import { 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 '../code-block-component-replacer' +import type { CodeProps } from '../../replace-components/code-block-component-replacer' const log = new Logger('VegaChart') -export const VegaChart: React.FC = ({ code }) => { +export const VegaLiteChart: React.FC = ({ code }) => { const diagramContainer = useRef(null) const [error, setError] = useState() const { t } = useTranslation() diff --git a/src/components/markdown-renderer/markdown-extension/vega-lite/vega-lite-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/vega-lite/vega-lite-markdown-extension.ts new file mode 100644 index 000000000..2c3da7c5c --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/vega-lite/vega-lite-markdown-extension.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { MarkdownExtension } from '../markdown-extension' +import { CodeBlockComponentReplacer } from '../../replace-components/code-block-component-replacer' +import type { ComponentReplacer } from '../../replace-components/component-replacer' +import { VegaLiteChart } from './vega-lite-chart' + +/** + * Adds support for chart rendering using vega lite to the markdown rendering using code fences with "vega-lite" as language. + */ +export class VegaLiteMarkdownExtension extends MarkdownExtension { + public buildReplacers(): ComponentReplacer[] { + return [new CodeBlockComponentReplacer(VegaLiteChart, 'vega-lite')] + } +} diff --git a/src/components/markdown-renderer/replace-components/vimeo/replace-legacy-vimeo-short-code.ts b/src/components/markdown-renderer/markdown-extension/vimeo/replace-legacy-vimeo-short-code.ts similarity index 75% rename from src/components/markdown-renderer/replace-components/vimeo/replace-legacy-vimeo-short-code.ts rename to src/components/markdown-renderer/markdown-extension/vimeo/replace-legacy-vimeo-short-code.ts index 6f96ff1b5..c9e17a3d0 100644 --- a/src/components/markdown-renderer/replace-components/vimeo/replace-legacy-vimeo-short-code.ts +++ b/src/components/markdown-renderer/markdown-extension/vimeo/replace-legacy-vimeo-short-code.ts @@ -5,6 +5,7 @@ */ import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' +import { VimeoMarkdownExtension } from './vimeo-markdown-extension' export const replaceLegacyVimeoShortCode: RegexOptions = { name: 'legacy-vimeo-short-code', @@ -12,6 +13,6 @@ export const replaceLegacyVimeoShortCode: RegexOptions = { replace: (match) => { // ESLint wants to collapse this tag, but then the tag won't be valid html anymore. // noinspection CheckTagEmptyBody - return `` + return `<${VimeoMarkdownExtension.tagName} id="${match}">` } } diff --git a/src/components/markdown-renderer/replace-components/vimeo/replace-vimeo-link.ts b/src/components/markdown-renderer/markdown-extension/vimeo/replace-vimeo-link.ts similarity index 84% rename from src/components/markdown-renderer/replace-components/vimeo/replace-vimeo-link.ts rename to src/components/markdown-renderer/markdown-extension/vimeo/replace-vimeo-link.ts index 3c988df63..7b097fb6d 100644 --- a/src/components/markdown-renderer/replace-components/vimeo/replace-vimeo-link.ts +++ b/src/components/markdown-renderer/markdown-extension/vimeo/replace-vimeo-link.ts @@ -5,6 +5,7 @@ */ import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' +import { VimeoMarkdownExtension } from './vimeo-markdown-extension' const protocolRegex = /(?:http(?:s)?:\/\/)?/ const domainRegex = /(?:player\.)?(?:vimeo\.com\/)(?:(?:channels|album|ondemand|groups)\/\w+\/)?(?:video\/)?/ @@ -21,6 +22,6 @@ export const replaceVimeoLink: RegexOptions = { replace: (match) => { // ESLint wants to collapse this tag, but then the tag won't be valid html anymore. // noinspection CheckTagEmptyBody - return `` + return `<${VimeoMarkdownExtension.tagName} id='${match}'>` } } diff --git a/src/components/markdown-renderer/replace-components/vimeo/vimeo-frame.tsx b/src/components/markdown-renderer/markdown-extension/vimeo/vimeo-frame.tsx similarity index 90% rename from src/components/markdown-renderer/replace-components/vimeo/vimeo-frame.tsx rename to src/components/markdown-renderer/markdown-extension/vimeo/vimeo-frame.tsx index f2c65b769..8328d721f 100644 --- a/src/components/markdown-renderer/replace-components/vimeo/vimeo-frame.tsx +++ b/src/components/markdown-renderer/markdown-extension/vimeo/vimeo-frame.tsx @@ -5,8 +5,8 @@ */ import React, { useCallback } from 'react' -import { ClickShield } from '../click-shield/click-shield' -import type { IdProps } from '../custom-tag-with-id-component-replacer' +import type { IdProps } from '../../replace-components/custom-tag-with-id-component-replacer' +import { ClickShield } from '../../replace-components/click-shield/click-shield' interface VimeoApiResponse { // Vimeo uses strange names for their fields. ESLint doesn't like that. 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 new file mode 100644 index 000000000..4ccbca9ba --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/vimeo/vimeo-markdown-extension.ts @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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 { VimeoFrame } from './vimeo-frame' +import { replaceLegacyVimeoShortCode } from './replace-legacy-vimeo-short-code' + +/** + * Adds vimeo video embeddings using link detection and the legacy vimeo short code syntax. + */ +export class VimeoMarkdownExtension extends MarkdownExtension { + public static readonly tagName = 'app-vimeo' + + public configureMarkdownIt(markdownIt: MarkdownIt): void { + markdownItRegex(markdownIt, replaceVimeoLink) + markdownItRegex(markdownIt, replaceLegacyVimeoShortCode) + } + + public buildReplacers(): ComponentReplacer[] { + return [new CustomTagWithIdComponentReplacer(VimeoFrame, VimeoMarkdownExtension.tagName)] + } + + public buildTagNameWhitelist(): string[] { + return [VimeoMarkdownExtension.tagName] + } +} diff --git a/src/components/markdown-renderer/replace-components/youtube/replace-legacy-youtube-short-code.ts b/src/components/markdown-renderer/markdown-extension/youtube/replace-legacy-youtube-short-code.ts similarity index 74% rename from src/components/markdown-renderer/replace-components/youtube/replace-legacy-youtube-short-code.ts rename to src/components/markdown-renderer/markdown-extension/youtube/replace-legacy-youtube-short-code.ts index c9b64f4f4..dd824d8de 100644 --- a/src/components/markdown-renderer/replace-components/youtube/replace-legacy-youtube-short-code.ts +++ b/src/components/markdown-renderer/markdown-extension/youtube/replace-legacy-youtube-short-code.ts @@ -5,6 +5,7 @@ */ import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' +import { YoutubeMarkdownExtension } from './youtube-markdown-extension' export const replaceLegacyYoutubeShortCode: RegexOptions = { name: 'legacy-youtube-short-code', @@ -12,6 +13,6 @@ export const replaceLegacyYoutubeShortCode: RegexOptions = { replace: (match) => { // ESLint wants to collapse this tag, but then the tag won't be valid html anymore. // noinspection CheckTagEmptyBody - return `` + return `<${YoutubeMarkdownExtension.tagName} id="${match}">` } } diff --git a/src/components/markdown-renderer/replace-components/youtube/replace-youtube-link.ts b/src/components/markdown-renderer/markdown-extension/youtube/replace-youtube-link.ts similarity index 84% rename from src/components/markdown-renderer/replace-components/youtube/replace-youtube-link.ts rename to src/components/markdown-renderer/markdown-extension/youtube/replace-youtube-link.ts index bb1a726c2..cd7b23dc8 100644 --- a/src/components/markdown-renderer/replace-components/youtube/replace-youtube-link.ts +++ b/src/components/markdown-renderer/markdown-extension/youtube/replace-youtube-link.ts @@ -5,6 +5,7 @@ */ import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' +import { YoutubeMarkdownExtension } from './youtube-markdown-extension' const protocolRegex = /(?:http(?:s)?:\/\/)?/ const subdomainRegex = /(?:www.)?/ @@ -22,6 +23,6 @@ export const replaceYouTubeLink: RegexOptions = { replace: (match) => { // ESLint wants to collapse this tag, but then the tag won't be valid html anymore. // noinspection CheckTagEmptyBody - return `` + return `<${YoutubeMarkdownExtension.tagName} id="${match}">` } } diff --git a/src/components/markdown-renderer/replace-components/youtube/youtube-frame.tsx b/src/components/markdown-renderer/markdown-extension/youtube/youtube-frame.tsx similarity index 84% rename from src/components/markdown-renderer/replace-components/youtube/youtube-frame.tsx rename to src/components/markdown-renderer/markdown-extension/youtube/youtube-frame.tsx index 79e58c82c..affb5dc3c 100644 --- a/src/components/markdown-renderer/replace-components/youtube/youtube-frame.tsx +++ b/src/components/markdown-renderer/markdown-extension/youtube/youtube-frame.tsx @@ -5,8 +5,8 @@ */ import React from 'react' -import { ClickShield } from '../click-shield/click-shield' -import type { IdProps } from '../custom-tag-with-id-component-replacer' +import type { IdProps } from '../../replace-components/custom-tag-with-id-component-replacer' +import { ClickShield } from '../../replace-components/click-shield/click-shield' /** * Renders a video player embedding for https://youtube.com 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 new file mode 100644 index 000000000..39486abf5 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/youtube/youtube-markdown-extension.ts @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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 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 { YouTubeFrame } from './youtube-frame' + +/** + * Adds youtube video embeddings using link detection and the legacy youtube short code syntax. + */ +export class YoutubeMarkdownExtension extends MarkdownExtension { + public static readonly tagName = 'app-youtube' + + public configureMarkdownIt(markdownIt: MarkdownIt): void { + markdownItRegex(markdownIt, replaceYouTubeLink) + markdownItRegex(markdownIt, replaceLegacyYoutubeShortCode) + } + + public buildReplacers(): ComponentReplacer[] { + return [new CustomTagWithIdComponentReplacer(YouTubeFrame, YoutubeMarkdownExtension.tagName)] + } + + public buildTagNameWhitelist(): string[] { + return [YoutubeMarkdownExtension.tagName] + } +} 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 deleted file mode 100644 index 79a689a47..000000000 --- a/src/components/markdown-renderer/markdown-it-configurator/document-markdown-it-configurator.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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/markdown-it-configurator.ts b/src/components/markdown-renderer/markdown-it-configurator/markdown-it-configurator.ts deleted file mode 100644 index 662068986..000000000 --- a/src/components/markdown-renderer/markdown-it-configurator/markdown-it-configurator.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import MarkdownIt from 'markdown-it' -import abbreviation from 'markdown-it-abbr' -import definitionList from 'markdown-it-deflist' -import footnote from 'markdown-it-footnote' -import { imageSize } from '@hedgedoc/markdown-it-image-size' -import inserted from 'markdown-it-ins' -import marked from 'markdown-it-mark' -import subscript from 'markdown-it-sub' -import superscript from 'markdown-it-sup' -import { alertContainer } from '../markdown-it-plugins/alert-container' -import { linkifyExtra } from '../markdown-it-plugins/linkify-extra' -import { MarkdownItParserDebugger } from '../markdown-it-plugins/parser-debugger' -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 { plantumlWithError } from '../markdown-it-plugins/plantuml' -import { KatexReplacer } from '../replace-components/katex/katex-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 { 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 { 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 Configuration { - onTocChange: (toc: TocAst) => void - useAlternativeBreaks?: boolean - lineOffset?: number -} - -export abstract class MarkdownItConfigurator { - protected readonly options: T - protected configurations: MarkdownIt.PluginSimple[] = [] - protected postConfigurations: MarkdownIt.PluginSimple[] = [] - - constructor(options: T) { - this.options = options - this.configure() - } - - public buildConfiguredMarkdownIt(): MarkdownIt { - const markdownIt = new MarkdownIt('default', { - html: true, - breaks: this.options.useAlternativeBreaks ?? true, - langPrefix: '', - typographer: true - }) - this.configurations.forEach((configuration) => markdownIt.use(configuration)) - this.postConfigurations.forEach((postConfiguration) => markdownIt.use(postConfiguration)) - return markdownIt - } - - protected configure(): void { - this.configurations.push( - plantumlWithError, - KatexReplacer.markdownItPlugin, - youtubeMarkdownItPlugin, - vimeoMarkdownItPlugin, - gistMarkdownItPlugin, - asciinemaMarkdownItPlugin, - legacyPdfShortCode, - legacySlideshareShortCode, - legacySpeakerdeckShortCode, - highlightedCode, - quoteExtraColor, - quoteExtra('name', 'user'), - quoteExtra('time', 'clock-o'), - documentTableOfContents(this.options.onTocChange), - twitterEmojis, - abbreviation, - definitionList, - subscript, - superscript, - inserted, - marked, - footnote, - imageSize, - tasksLists, - alertContainer, - spoilerContainer - ) - - 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 deleted file mode 100644 index 5d5369329..000000000 --- a/src/components/markdown-renderer/markdown-it-configurator/slideshow-markdown-it-configurator.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 deleted file mode 100644 index 22a99af5b..000000000 --- a/src/components/markdown-renderer/markdown-it-plugins/alert-container.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -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/component-replacer' - -export type RenderContainerReturn = ( - tokens: Token[], - index: number, - options: MarkdownIt.Options, - env: unknown, - self: Renderer -) => string -type ValidAlertLevels = 'warning' | 'danger' | 'success' | 'info' -export const validAlertLevels: ValidAlertLevels[] = ['success', 'danger', 'info', 'warning'] - -const createRenderContainer = (level: ValidAlertLevels): RenderContainerReturn => { - return (tokens: Token[], index: number, options: MarkdownIt.Options, env: unknown, self: Renderer) => { - tokens[index].attrJoin('role', 'alert') - tokens[index].attrJoin('class', 'alert') - tokens[index].attrJoin('class', `alert-${level}`) - return self.renderToken(tokens, index, options) - } -} - -export const alertContainer: MarkdownItPlugin = (markdownIt: MarkdownIt) => { - validAlertLevels.forEach((level) => { - markdownItContainer(markdownIt, level, { render: createRenderContainer(level) }) - }) -} diff --git a/src/components/markdown-renderer/markdown-it-plugins/document-table-of-contents.ts b/src/components/markdown-renderer/markdown-it-plugins/document-table-of-contents.ts deleted file mode 100644 index 21114de49..000000000 --- a/src/components/markdown-renderer/markdown-it-plugins/document-table-of-contents.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type MarkdownIt from 'markdown-it/lib' -import type { TocAst } from 'markdown-it-toc-done-right' -import { documentToc } from './document-toc' - -export const documentTableOfContents = (onTocChange: (toc: TocAst) => void): MarkdownIt.PluginSimple => { - return (markdownIt) => documentToc(markdownIt, onTocChange) -} diff --git a/src/components/markdown-renderer/markdown-it-plugins/document-toc.ts b/src/components/markdown-renderer/markdown-it-plugins/document-toc.ts deleted file mode 100644 index 7b276392a..000000000 --- a/src/components/markdown-renderer/markdown-it-plugins/document-toc.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type MarkdownIt from 'markdown-it' -import type { TocAst } from 'markdown-it-toc-done-right' -import toc from 'markdown-it-toc-done-right' -import { tocSlugify } from '../../editor-page/table-of-contents/toc-slugify' - -export type DocumentTocPluginOptions = (ast: TocAst) => void - -export const documentToc: MarkdownIt.PluginWithOptions = (markdownIt, onToc) => { - if (!onToc) { - return - } - toc(markdownIt, { - placeholder: '(\\[TOC\\]|\\[toc\\])', - listType: 'ul', - level: [1, 2, 3], - callback: (code: string, ast: TocAst): void => { - onToc(ast) - }, - slugify: tocSlugify - }) -} diff --git a/src/components/markdown-renderer/markdown-it-plugins/headline-anchors.ts b/src/components/markdown-renderer/markdown-it-plugins/headline-anchors.ts deleted file mode 100644 index 0c850a06f..000000000 --- a/src/components/markdown-renderer/markdown-it-plugins/headline-anchors.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type MarkdownIt from 'markdown-it' -import anchor from 'markdown-it-anchor' - -export const headlineAnchors: MarkdownIt.PluginSimple = (markdownIt) => { - anchor(markdownIt, { - permalink: anchor.permalink.ariaHidden({ - symbol: '', - class: 'heading-anchor text-dark', - renderHref: (slug: string): string => `#${slug}`, - placement: 'before' - }) - }) -} diff --git a/src/components/markdown-renderer/markdown-it-plugins/highlighted-code.ts b/src/components/markdown-renderer/markdown-it-plugins/highlighted-code.ts deleted file mode 100644 index f78f9044c..000000000 --- a/src/components/markdown-renderer/markdown-it-plugins/highlighted-code.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type MarkdownIt from 'markdown-it/lib' - -const highlightRegex = /^ *([\w-]*)(.*)$/ - -export const highlightedCode: MarkdownIt.PluginSimple = (md: MarkdownIt) => { - md.core.ruler.push('highlighted-code', (state) => { - state.tokens.forEach((token) => { - if (token.type === 'fence') { - const highlightInfos = highlightRegex.exec(token.info) - if (!highlightInfos) { - return - } - if (highlightInfos[1]) { - token.attrJoin('data-highlight-language', highlightInfos[1]) - } - if (highlightInfos[2]) { - token.attrJoin('data-extra', highlightInfos[2]) - } - } - }) - return true - }) -} diff --git a/src/components/markdown-renderer/markdown-it-plugins/linkify-extra.ts b/src/components/markdown-renderer/markdown-it-plugins/linkify-extra.ts deleted file mode 100644 index 9583f2edd..000000000 --- a/src/components/markdown-renderer/markdown-it-plugins/linkify-extra.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type MarkdownIt from 'markdown-it/lib' -import linkify from 'markdown-it/lib/rules_core/linkify' - -export const linkifyExtra: MarkdownIt.PluginSimple = (md) => { - md.core.ruler.push('linkify', (state) => { - try { - state.md.options.linkify = true - return linkify(state) - } finally { - state.md.options.linkify = false - } - }) -} diff --git a/src/components/markdown-renderer/markdown-it-plugins/parser-debugger.ts b/src/components/markdown-renderer/markdown-it-plugins/parser-debugger.ts deleted file mode 100644 index 646414d43..000000000 --- a/src/components/markdown-renderer/markdown-it-plugins/parser-debugger.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type MarkdownIt from 'markdown-it/lib' -import { Logger } from '../../../utils/logger' - -const log = new Logger('MarkdownItParserDebugger') - -export const MarkdownItParserDebugger: MarkdownIt.PluginSimple = (md: MarkdownIt) => { - if (process.env.NODE_ENV !== 'production') { - md.core.ruler.push('test', (state) => { - log.debug('Current state', state) - return false - }) - } -} diff --git a/src/components/markdown-renderer/markdown-it-plugins/plantuml.ts b/src/components/markdown-renderer/markdown-it-plugins/plantuml.ts deleted file mode 100644 index 0df1d36b3..000000000 --- a/src/components/markdown-renderer/markdown-it-plugins/plantuml.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import plantuml from 'markdown-it-plantuml' -import type { Options } from 'markdown-it/lib' -import type MarkdownIt from 'markdown-it/lib' -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/component-replacer' - -export const plantumlWithError: MarkdownItPlugin = (markdownIt: MarkdownIt) => { - const plantumlServer = store.getState().config.plantumlServer - if (plantumlServer) { - plantuml(markdownIt, { - openMarker: '```plantuml', - closeMarker: '```', - server: plantumlServer - }) - } else { - plantumlError(markdownIt) - } -} - -const plantumlError: MarkdownIt.PluginSimple = (md) => { - const defaultRenderer: RenderRule = md.renderer.rules.fence || (() => '') - md.renderer.rules.fence = (tokens: Token[], idx: number, options: Options, env, slf: Renderer) => { - const token = tokens[idx] - if (token.info === 'plantuml') { - return ` -

    - PlantUML plugin is enabled but not properly configured. -

    - ` - } - return defaultRenderer(tokens, idx, options, env, slf) - } -} diff --git a/src/components/markdown-renderer/markdown-it-plugins/quote-extra-color.ts b/src/components/markdown-renderer/markdown-it-plugins/quote-extra-color.ts deleted file mode 100644 index 777f863a7..000000000 --- a/src/components/markdown-renderer/markdown-it-plugins/quote-extra-color.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type MarkdownIt from 'markdown-it/lib' -import { parseQuoteExtraTag } from './quote-extra' - -const cssColorRegex = - /(#(?:[0-9a-f]{2}){2,4}|(?:#[0-9a-f]{3})|black|silver|gray|whitesmoke|maroon|red|purple|fuchsia|green|lime|olivedrab|yellow|navy|blue|teal|aquamarine|orange|aliceblue|antiquewhite|aqua|azure|beige|bisque|blanchedalmond|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|currentcolor|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|gainsboro|ghostwhite|goldenrod|gold|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavenderblush|lavender|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|limegreen|linen|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|oldlace|olive|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|rebeccapurple|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|thistle|tomato|transparent|turquoise|violet|wheat|white|yellowgreen)/i - -export const quoteExtraColor: MarkdownIt.PluginSimple = (md) => { - md.inline.ruler.push(`extraQuote_color`, (state) => { - const quoteExtraTagValues = parseQuoteExtraTag(state.src, state.pos, state.posMax) - - if (!quoteExtraTagValues || quoteExtraTagValues.label !== 'color') { - return false - } - state.pos = quoteExtraTagValues.valueEndIndex + 1 - - if (!cssColorRegex.exec(quoteExtraTagValues.value)) { - return false - } - - state.pos = quoteExtraTagValues.valueEndIndex + 1 - - const token = state.push('quote-extra-color', '', 0) - token.attrSet('color', quoteExtraTagValues.value) - - return true - }) - - md.renderer.rules['quote-extra-color'] = (tokens, idx) => { - const token = tokens[idx] - const color = token.attrGet('color') ?? '' - - return `` - } -} diff --git a/src/components/markdown-renderer/markdown-it-plugins/quote-extra.ts b/src/components/markdown-renderer/markdown-it-plugins/quote-extra.ts deleted file mode 100644 index 075c2a430..000000000 --- a/src/components/markdown-renderer/markdown-it-plugins/quote-extra.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type MarkdownIt from 'markdown-it/lib' -import type Token from 'markdown-it/lib/token' -import type { IconName } from '../../common/fork-awesome/types' - -export const quoteExtra: (quoteLabel: string, icon: IconName) => MarkdownIt.PluginSimple = - (quoteLabel: string, icon: IconName) => (md) => { - md.inline.ruler.push(`extraQuote_${quoteLabel}`, (state) => { - const quoteExtraTagValues = parseQuoteExtraTag(state.src, state.pos, state.posMax) - - if (!quoteExtraTagValues || quoteExtraTagValues.label !== quoteLabel) { - return false - } - state.pos = quoteExtraTagValues.valueEndIndex + 1 - - const tokens: Token[] = [] - state.md.inline.parse(quoteExtraTagValues.value, state.md, state.env, tokens) - - const token = state.push('quote-extra', '', 0) - token.attrSet('icon', icon) - token.children = tokens - - return true - }) - - if (md.renderer.rules['quote-extra']) { - return - } - - md.renderer.rules['quote-extra'] = (tokens, idx, options: MarkdownIt.Options, env: unknown) => { - const token = tokens[idx] - const innerTokens = token.children - - if (!innerTokens) { - return '' - } - - const innerHtml = md.renderer.renderInline(innerTokens, options, env) - return `${innerHtml}` - } - } - -export interface QuoteExtraTagValues { - labelStartIndex: number - labelEndIndex: number - valueStartIndex: number - valueEndIndex: number - label: string - value: string -} - -export const parseQuoteExtraTag = (line: string, start: number, maxPos: number): QuoteExtraTagValues | undefined => { - if (line[start] !== '[') { - return - } - - const labelStartIndex = start + 1 - const labelEndIndex = parseLabel(line, labelStartIndex, maxPos) - if (!labelEndIndex || labelStartIndex === labelEndIndex) { - return - } - - const valueStartIndex = labelEndIndex + 1 - const valueEndIndex = parseValue(line, valueStartIndex, maxPos) - if (!valueEndIndex || valueStartIndex === valueEndIndex) { - return - } - - return { - labelStartIndex, - labelEndIndex, - valueStartIndex, - valueEndIndex, - label: line.substr(labelStartIndex, labelEndIndex - labelStartIndex), - value: line.substr(valueStartIndex, valueEndIndex - valueStartIndex) - } -} - -const parseValue = (line: string, start: number, maxPos: number): number | undefined => { - let level = 1 - for (let pos = start; pos <= maxPos; pos += 1) { - const currentCharacter = line[pos] - if (currentCharacter === ']') { - level -= 1 - if (level === 0) { - return pos - } - } else if (currentCharacter === '[') { - level += 1 - } - } -} - -const parseLabel = (line: string, start: number, maxPos: number): number | undefined => { - for (let pos = start; pos <= maxPos; pos += 1) { - if (line[pos] === '=') { - return pos - } - } -} diff --git a/src/components/markdown-renderer/markdown-it-plugins/spoiler-container.ts b/src/components/markdown-renderer/markdown-it-plugins/spoiler-container.ts deleted file mode 100644 index 6ac62fc29..000000000 --- a/src/components/markdown-renderer/markdown-it-plugins/spoiler-container.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -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/component-replacer' -import type { RenderContainerReturn } from './alert-container' - -export const spoilerRegEx = /^spoiler\s+(.*)$/ - -const createSpoilerContainer = (): RenderContainerReturn => { - return (tokens: Token[], index: number) => { - const matches = spoilerRegEx.exec(tokens[index].info.trim()) - - if (tokens[index].nesting === 1 && matches && matches[1]) { - // opening tag - return `
    ${escapeHtml(matches[1])}` - } else { - // closing tag - return '
    \n' - } - } -} - -export const spoilerContainer: MarkdownItPlugin = (markdownIt: MarkdownIt) => { - markdownItContainer(markdownIt, 'spoiler', { - validate: (params: string) => spoilerRegEx.test(params), - render: createSpoilerContainer() - }) -} diff --git a/src/components/markdown-renderer/markdown-it-plugins/tasks-lists.ts b/src/components/markdown-renderer/markdown-it-plugins/tasks-lists.ts deleted file mode 100644 index db44651e0..000000000 --- a/src/components/markdown-renderer/markdown-it-plugins/tasks-lists.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import markdownItTaskLists from '@hedgedoc/markdown-it-task-lists' -import type MarkdownIt from 'markdown-it' - -export const tasksLists: MarkdownIt.PluginSimple = (markdownIt) => { - markdownItTaskLists(markdownIt, { - enabled: true, - label: true, - lineNumber: true - }) -} diff --git a/src/components/markdown-renderer/markdown-it-plugins/twitter-emojis.ts b/src/components/markdown-renderer/markdown-it-plugins/twitter-emojis.ts deleted file mode 100644 index 8e44804f5..000000000 --- a/src/components/markdown-renderer/markdown-it-plugins/twitter-emojis.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type MarkdownIt from 'markdown-it' -import emoji from 'markdown-it-emoji/bare' -import { combinedEmojiData } from './emoji/mapping' - -export const twitterEmojis: MarkdownIt.PluginSimple = (markdownIt) => { - emoji(markdownIt, { - defs: combinedEmojiData - }) -} diff --git a/src/components/markdown-renderer/markdown-renderer.scss b/src/components/markdown-renderer/markdown-renderer.scss index df8df79c7..d56f0853f 100644 --- a/src/components/markdown-renderer/markdown-renderer.scss +++ b/src/components/markdown-renderer/markdown-renderer.scss @@ -45,7 +45,7 @@ } } - blockquote .quote-extra { + blockquote .blockquote-extra { font-size: 0.85em; margin-inline-start: 0.5em; diff --git a/src/components/markdown-renderer/node-preprocessors/node-processor.ts b/src/components/markdown-renderer/node-preprocessors/node-processor.ts new file mode 100644 index 000000000..8c631ac6c --- /dev/null +++ b/src/components/markdown-renderer/node-preprocessors/node-processor.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { Document } from 'domhandler' + +export abstract class NodeProcessor { + public abstract process(nodes: Document): Document +} diff --git a/src/components/markdown-renderer/node-preprocessors/traveler-node-processor.ts b/src/components/markdown-renderer/node-preprocessors/traveler-node-processor.ts new file mode 100644 index 000000000..5ea6aacd5 --- /dev/null +++ b/src/components/markdown-renderer/node-preprocessors/traveler-node-processor.ts @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { NodeProcessor } from './node-processor' +import type { Document, Node } from 'domhandler' +import { hasChildren } from 'domhandler' + +export abstract class TravelerNodeProcessor extends NodeProcessor { + process(nodes: Document): Document { + this.processNodes(nodes.children) + return nodes + } + + private processNodes(nodes: Node[]): void { + nodes.forEach((node) => { + this.processNode(node) + if (hasChildren(node)) { + this.processNodes(node.children) + } + }) + } + + protected abstract processNode(node: Node): void +} 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 3764c79fd..13fe3b03b 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 @@ -9,12 +9,12 @@ import { Trans, useTranslation } from 'react-i18next' import type { IconName } from '../../../common/fork-awesome/types' import { ShowIf } from '../../../common/show-if/show-if' import './click-shield.scss' -import { ProxyImageFrame } from '../image/proxy-image-frame' import { Logger } from '../../../../utils/logger' import type { Property } from 'csstype' import type { PropsWithDataCypressId } from '../../../../utils/cypress-attribute' import { cypressId } from '../../../../utils/cypress-attribute' import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon' +import { ProxyImageFrame } from '../../markdown-extension/image/proxy-image-frame' const log = new Logger('OneClickEmbedding') 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 deleted file mode 100644 index 16e2e26f5..000000000 --- a/src/components/markdown-renderer/replace-components/colored-blockquote/colored-blockquote-replacer.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { Element } from 'domhandler' -import { isTag } from 'domhandler' -import type { NativeRenderer, SubNodeTransform, ValidReactDomElement } from '../component-replacer' -import { ComponentReplacer } from '../component-replacer' - -/** - * Checks if the given node is a blockquote color definition - * - * @param node The node to check - * @return true if the checked node is a blockquote color definition - */ -const isBlockquoteColorDefinition = (node: Element | undefined): boolean => { - if (!node || !node.attribs || !node.attribs.class || !node.attribs['data-color']) { - return false - } - return node.name === 'span' && node.attribs.class === 'quote-extra' -} - -/** - * Checks if any of the given nodes is the parent element of a color extra element. - * - * @param nodes The array of nodes to check - * @return the found element or undefined if no element was found - */ -const findBlockquoteColorParentElement = (nodes: Element[]): Element | undefined => { - return nodes.find((child) => { - if (child.name !== 'p' || !child.children || child.children.length < 1) { - return false - } - return child.children.filter(isTag).find(isBlockquoteColorDefinition) !== undefined - }) -} - -/** - * Detects blockquotes and checks if they contain a color tag. - * If a color tag was found then the color will be applied to the node as border. - */ -export class ColoredBlockquoteReplacer extends ComponentReplacer { - public replace( - node: Element, - subNodeTransform: SubNodeTransform, - nativeRenderer: NativeRenderer - ): ValidReactDomElement | undefined { - if (node.name !== 'blockquote' || !node.children || node.children.length < 1) { - return - } - const paragraph = findBlockquoteColorParentElement(node.children.filter(isTag)) - if (!paragraph) { - return - } - const childElements = paragraph.children || [] - const optionsTag = childElements.filter(isTag).find(isBlockquoteColorDefinition) - if (!optionsTag) { - return - } - paragraph.children = childElements.filter((elem) => !isTag(elem) || !isBlockquoteColorDefinition(elem)) - const attributes = optionsTag.attribs - if (!attributes || !attributes['data-color']) { - return - } - node.attribs = Object.assign(node.attribs || {}, { style: `border-left-color: ${attributes['data-color']};` }) - return nativeRenderer() - } -} diff --git a/src/components/markdown-renderer/replace-components/component-replacer.ts b/src/components/markdown-renderer/replace-components/component-replacer.ts index 400a5bc9a..631bd7181 100644 --- a/src/components/markdown-renderer/replace-components/component-replacer.ts +++ b/src/components/markdown-renderer/replace-components/component-replacer.ts @@ -4,14 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { Element } from 'domhandler' +import type { Element, Node } from 'domhandler' import { isText } from 'domhandler' import type MarkdownIt from 'markdown-it' import type { ReactElement } from 'react' export type ValidReactDomElement = ReactElement | string | null -export type SubNodeTransform = (node: Element, subKey: number | string) => ValidReactDomElement | void +export type SubNodeTransform = (node: Node, subKey: number | string) => NodeReplacement export type NativeRenderer = () => ValidReactDomElement @@ -38,6 +38,17 @@ export abstract class ComponentReplacer { return isText(childrenTextNode) ? childrenTextNode.data : '' } + /** + * Applies the given {@link SubNodeTransform sub node transformer} to every children of the given {@link Node} + * + * @param node The node whose children should be transformed + * @param subNodeTransform The transformer that should be used. + * @return The children as react elements. + */ + protected static transformChildren(node: Element, subNodeTransform: SubNodeTransform): NodeReplacement[] { + return node.children.map((value, index) => subNodeTransform(value, index)) + } + /** * Checks if the current node should be altered or replaced and does if needed. * 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 index c03e99457..ed18bb998 100644 --- 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 @@ -34,8 +34,6 @@ export class CustomTagWithIdComponentReplacer extends ComponentReplacer { * @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 + return element.name === this.tagName && element.attribs && element.attribs.id ? element.attribs.id : undefined } } diff --git a/src/components/markdown-renderer/replace-components/gist/gist-markdown-it-plugin.ts b/src/components/markdown-renderer/replace-components/gist/gist-markdown-it-plugin.ts deleted file mode 100644 index aed63e9d8..000000000 --- a/src/components/markdown-renderer/replace-components/gist/gist-markdown-it-plugin.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type MarkdownIt from 'markdown-it' -import markdownItRegex from 'markdown-it-regex' -import { replaceGistLink } from './replace-gist-link' -import { replaceLegacyGistShortCode } from './replace-legacy-gist-short-code' - -export const gistMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => { - markdownItRegex(markdownIt, replaceGistLink) - markdownItRegex(markdownIt, replaceLegacyGistShortCode) -} diff --git a/src/components/markdown-renderer/replace-components/image/types.ts b/src/components/markdown-renderer/replace-components/image/types.ts deleted file mode 100644 index 42cf652f7..000000000 --- a/src/components/markdown-renderer/replace-components/image/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export interface ImageProxyResponse { - src: string -} diff --git a/src/components/markdown-renderer/replace-components/linemarker/line-number-marker.ts b/src/components/markdown-renderer/replace-components/linemarker/line-number-marker.ts deleted file mode 100644 index 498d6f5ae..000000000 --- a/src/components/markdown-renderer/replace-components/linemarker/line-number-marker.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type MarkdownIt from 'markdown-it/lib' -import Token from 'markdown-it/lib/token' - -export interface LineMarkers { - startLine: number - endLine: number -} - -export type LineNumberMarkerOptions = (lineMarkers: LineMarkers[]) => void - -/** - * This plugin adds markers to the dom, that are used to map line numbers to dom elements. - * It also provides a list of line numbers for the top level dom elements. - */ -export const lineNumberMarker: (options: LineNumberMarkerOptions, lineOffset: number) => MarkdownIt.PluginSimple = - (options, lineOffset = 0) => - (md: MarkdownIt) => { - // add app_linemarker token before each opening or self-closing level-0 tag - md.core.ruler.push('line_number_marker', (state) => { - const lineMarkers: LineMarkers[] = [] - tagTokens(state.tokens, lineMarkers, lineOffset) - if (options) { - options(lineMarkers) - } - return true - }) - - md.renderer.rules.app_linemarker = (tokens: Token[], index: number): string => { - const startLineNumber = tokens[index].attrGet('data-start-line') - const endLineNumber = tokens[index].attrGet('data-end-line') - - if (!startLineNumber || !endLineNumber) { - // don't render broken linemarkers without a linenumber - return '' - } - // noinspection CheckTagEmptyBody - return `` - } - - const insertNewLineMarker = ( - startLineNumber: number, - endLineNumber: number, - tokenPosition: number, - level: number, - tokens: Token[] - ) => { - const startToken = new Token('app_linemarker', 'app-linemarker', 0) - startToken.level = level - startToken.attrPush(['data-start-line', `${startLineNumber}`]) - startToken.attrPush(['data-end-line', `${endLineNumber}`]) - tokens.splice(tokenPosition, 0, startToken) - } - - const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[], lineOffset: number) => { - for (let tokenPosition = 0; tokenPosition < tokens.length; tokenPosition++) { - const token = tokens[tokenPosition] - if (token.hidden) { - continue - } - - if (!token.map) { - continue - } - - const startLineNumber = token.map[0] + 1 - const endLineNumber = token.map[1] + 1 - - if (token.level === 0) { - lineMarkers.push({ startLine: startLineNumber + lineOffset, endLine: endLineNumber + lineOffset }) - } - - insertNewLineMarker(startLineNumber, endLineNumber, tokenPosition, token.level, tokens) - tokenPosition += 1 - - if (token.children) { - tagTokens(token.children, lineMarkers, lineOffset) - } - } - } - } diff --git a/src/components/markdown-renderer/replace-components/link-replacer/link-replacer.tsx b/src/components/markdown-renderer/replace-components/link-replacer/link-replacer.tsx deleted file mode 100644 index b12b0231d..000000000 --- a/src/components/markdown-renderer/replace-components/link-replacer/link-replacer.tsx +++ /dev/null @@ -1,55 +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 type { NativeRenderer, SubNodeTransform, ValidReactDomElement } from '../component-replacer' -import { ComponentReplacer } from '../component-replacer' - -export const createJumpToMarkClickEventHandler = (id: string) => { - return (event: React.MouseEvent): void => { - document.getElementById(id)?.scrollIntoView() - event.preventDefault() - } -} - -/** - * Detects link tags and polishs them. - * This replacer prevents data and javascript links, - * extends relative links with the base url and creates working jump links. - */ -export class LinkReplacer extends ComponentReplacer { - constructor(private baseUrl?: string) { - super() - } - - public replace( - node: Element, - subNodeTransform: SubNodeTransform, - nativeRenderer: NativeRenderer - ): ValidReactDomElement | undefined { - if (node.name !== 'a' || !node.attribs || !node.attribs.href) { - return undefined - } - - const url = node.attribs.href.trim() - const isJumpMark = url.substr(0, 1) === '#' - const id = url.substr(1) - - try { - node.attribs.href = new URL(url, this.baseUrl).toString() - } catch (e) { - node.attribs.href = url - } - - if (isJumpMark) { - return {nativeRenderer()} - } else { - node.attribs.rel = 'noreferer noopener' - node.attribs.target = '_blank' - return nativeRenderer() - } - } -} diff --git a/src/components/markdown-renderer/replace-components/vimeo/vimeo-markdown-it-plugin.ts b/src/components/markdown-renderer/replace-components/vimeo/vimeo-markdown-it-plugin.ts deleted file mode 100644 index 7cd1acd6e..000000000 --- a/src/components/markdown-renderer/replace-components/vimeo/vimeo-markdown-it-plugin.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type MarkdownIt from 'markdown-it' -import markdownItRegex from 'markdown-it-regex' -import { replaceVimeoLink } from './replace-vimeo-link' -import { replaceLegacyVimeoShortCode } from './replace-legacy-vimeo-short-code' - -export const vimeoMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => { - markdownItRegex(markdownIt, replaceVimeoLink) - markdownItRegex(markdownIt, replaceLegacyVimeoShortCode) -} diff --git a/src/components/markdown-renderer/replace-components/youtube/youtube-markdown-it-plugin.ts b/src/components/markdown-renderer/replace-components/youtube/youtube-markdown-it-plugin.ts deleted file mode 100644 index 23d7beb67..000000000 --- a/src/components/markdown-renderer/replace-components/youtube/youtube-markdown-it-plugin.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type MarkdownIt from 'markdown-it' -import markdownItRegex from 'markdown-it-regex' -import { replaceYouTubeLink } from './replace-youtube-link' -import { replaceLegacyYoutubeShortCode } from './replace-legacy-youtube-short-code' - -export const youtubeMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => { - markdownItRegex(markdownIt, replaceYouTubeLink) - markdownItRegex(markdownIt, replaceLegacyYoutubeShortCode) -} diff --git a/src/components/markdown-renderer/slideshow-markdown-renderer.tsx b/src/components/markdown-renderer/slideshow-markdown-renderer.tsx index 65552e0f8..cf6961fbd 100644 --- a/src/components/markdown-renderer/slideshow-markdown-renderer.tsx +++ b/src/components/markdown-renderer/slideshow-markdown-renderer.tsx @@ -7,7 +7,6 @@ import React, { Fragment, useMemo, useRef } from 'react' import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom' import './markdown-renderer.scss' -import { useComponentReplacers } from './hooks/use-component-replacers' 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' @@ -17,10 +16,10 @@ import './slideshow.scss' import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props' import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert' import type { SlideOptions } from '../common/note-frontmatter/types' -import { processRevealCommentNodes } from './process-reveal-comment-nodes' import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props' import { LoadingSlide } from './loading-slide' -import { SlideshowMarkdownItConfigurator } from './markdown-it-configurator/slideshow-markdown-it-configurator' +import { RevealMarkdownExtension } from './markdown-extension/reveal/reveal-markdown-extension' +import { useMarkdownExtensions } from './hooks/use-markdown-extensions' export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps { slideOptions: SlideOptions @@ -34,7 +33,7 @@ export const SlideshowMarkdownRenderer: React.FC { @@ -42,22 +41,17 @@ export const SlideshowMarkdownRenderer: React.FC() const [trimmedContent, contentExceedsLimit] = useTrimmedContent(content) - const markdownIt = useMemo( - () => - new SlideshowMarkdownItConfigurator({ - onTocChange: (toc) => (tocAst.current = toc), - useAlternativeBreaks, - lineOffset - }).buildConfiguredMarkdownIt(), - [lineOffset, useAlternativeBreaks] - ) - const replacers = useComponentReplacers(onTaskCheckedChange, onImageClick, baseUrl, lineOffset) - const markdownReactDom = useConvertMarkdownToReactDom( - trimmedContent, - markdownIt, - replacers, - processRevealCommentNodes + const extensions = useMarkdownExtensions( + baseUrl, + undefined, + useMemo(() => [new RevealMarkdownExtension()], []), + lineOffset, + onTaskCheckedChange, + onImageClick, + onTocChange ) + + const markdownReactDom = useConvertMarkdownToReactDom(trimmedContent, extensions, newlinesAreBreaks) const revealStatus = useReveal(content, slideOptions) useExtractFirstHeadline( diff --git a/src/components/markdown-renderer/utils/calculate-line-marker-positions.ts b/src/components/markdown-renderer/utils/calculate-line-marker-positions.ts index d8b697c44..8fc09d2f8 100644 --- a/src/components/markdown-renderer/utils/calculate-line-marker-positions.ts +++ b/src/components/markdown-renderer/utils/calculate-line-marker-positions.ts @@ -8,8 +8,8 @@ import equal from 'fast-deep-equal' import type { RefObject } from 'react' import { useCallback, useEffect, useRef } from 'react' import useResizeObserver from 'use-resize-observer' -import type { LineMarkerPosition } from '../types' -import type { LineMarkers } from '../replace-components/linemarker/line-number-marker' +import type { LineMarkerPosition } from '../markdown-extension/linemarker/types' +import type { LineMarkers } from '../markdown-extension/linemarker/add-line-marker-markdown-it-plugin' export const calculateLineMarkerPositions = ( documentElement: HTMLDivElement, diff --git a/src/components/markdown-renderer/utils/line-id-mapper.ts b/src/components/markdown-renderer/utils/line-id-mapper.ts index 7b689b61a..79b9ae1ca 100644 --- a/src/components/markdown-renderer/utils/line-id-mapper.ts +++ b/src/components/markdown-renderer/utils/line-id-mapper.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { LineWithId } from '../types' +import type { LineWithId } from '../markdown-extension/linemarker/types' import type { ArrayChange } from 'diff' import { diffArrays } from 'diff' diff --git a/src/components/markdown-renderer/utils/node-to-react-transformer.tsx b/src/components/markdown-renderer/utils/node-to-react-transformer.tsx index 6d37ffa2d..86031bdba 100644 --- a/src/components/markdown-renderer/utils/node-to-react-transformer.tsx +++ b/src/components/markdown-renderer/utils/node-to-react-transformer.tsx @@ -10,8 +10,9 @@ import { convertNodeToReactElement } from '@hedgedoc/html-to-react/dist/convertN import type { ComponentReplacer, NodeReplacement, ValidReactDomElement } from '../replace-components/component-replacer' import { DO_NOT_REPLACE, REPLACE_WITH_NOTHING } from '../replace-components/component-replacer' import React from 'react' -import type { LineWithId } from '../types' +import type { LineWithId } from '../markdown-extension/linemarker/types' import Optional from 'optional-js' +import { LinemarkerMarkdownExtension } from '../markdown-extension/linemarker/linemarker-markdown-extension' type LineIndexPair = [startLineIndex: number, endLineIndex: number] @@ -116,7 +117,7 @@ export class NodeToReactTransformer { * @return the extracted line indexes */ private static extractLineIndexFromLineMarker(lineMarker: Node): LineIndexPair | undefined { - if (!isTag(lineMarker) || lineMarker.tagName !== 'app-linemarker' || !lineMarker.attribs) { + if (!isTag(lineMarker) || lineMarker.tagName !== LinemarkerMarkdownExtension.tagName || !lineMarker.attribs) { return } const startLineInMarkdown = lineMarker.attribs['data-start-line'] diff --git a/src/components/render-page/hooks/sync-scroll/use-document-sync-scrolling.ts b/src/components/render-page/hooks/sync-scroll/use-document-sync-scrolling.ts index 8d4ede61b..d06584860 100644 --- a/src/components/render-page/hooks/sync-scroll/use-document-sync-scrolling.ts +++ b/src/components/render-page/hooks/sync-scroll/use-document-sync-scrolling.ts @@ -6,7 +6,7 @@ import type React from 'react' import { useCallback, useState } from 'react' -import type { LineMarkerPosition } from '../../../markdown-renderer/types' +import type { LineMarkerPosition } from '../../../markdown-renderer/markdown-extension/linemarker/types' import type { ScrollState } from '../../../editor-page/synced-scroll/scroll-props' import { useOnUserScroll } from './use-on-user-scroll' import { useScrollToLineMark } from './use-scroll-to-line-mark' diff --git a/src/components/render-page/hooks/sync-scroll/use-on-user-scroll.ts b/src/components/render-page/hooks/sync-scroll/use-on-user-scroll.ts index a11391bbe..c868aa6f6 100644 --- a/src/components/render-page/hooks/sync-scroll/use-on-user-scroll.ts +++ b/src/components/render-page/hooks/sync-scroll/use-on-user-scroll.ts @@ -6,7 +6,7 @@ import type { RefObject } from 'react' import { useCallback } from 'react' -import type { LineMarkerPosition } from '../../../markdown-renderer/types' +import type { LineMarkerPosition } from '../../../markdown-renderer/markdown-extension/linemarker/types' import type { ScrollState } from '../../../editor-page/synced-scroll/scroll-props' export const useOnUserScroll = ( diff --git a/src/components/render-page/hooks/sync-scroll/use-scroll-to-line-mark.ts b/src/components/render-page/hooks/sync-scroll/use-scroll-to-line-mark.ts index 3d51038d4..43af69f2d 100644 --- a/src/components/render-page/hooks/sync-scroll/use-scroll-to-line-mark.ts +++ b/src/components/render-page/hooks/sync-scroll/use-scroll-to-line-mark.ts @@ -6,7 +6,7 @@ import type { RefObject } from 'react' import { useCallback, useEffect, useRef } from 'react' -import type { LineMarkerPosition } from '../../../markdown-renderer/types' +import type { LineMarkerPosition } from '../../../markdown-renderer/markdown-extension/linemarker/types' import type { ScrollState } from '../../../editor-page/synced-scroll/scroll-props' import { findLineMarks } from '../../../editor-page/synced-scroll/utils' diff --git a/src/components/render-page/hooks/use-image-click-handler.ts b/src/components/render-page/hooks/use-image-click-handler.ts index 867c19ee4..25ab86235 100644 --- a/src/components/render-page/hooks/use-image-click-handler.ts +++ b/src/components/render-page/hooks/use-image-click-handler.ts @@ -6,7 +6,7 @@ import type React from 'react' import { useCallback } from 'react' -import type { ImageClickHandler } from '../../markdown-renderer/replace-components/image/image-replacer' +import type { ImageClickHandler } from '../../markdown-renderer/markdown-extension/image/proxy-image-replacer' import type { RendererToEditorCommunicator } from '../window-post-message-communicator/renderer-to-editor-communicator' import { CommunicationMessageType } from '../window-post-message-communicator/rendering-message' diff --git a/src/components/render-page/iframe-markdown-renderer.tsx b/src/components/render-page/iframe-markdown-renderer.tsx index 2a477404b..50a6e91e1 100644 --- a/src/components/render-page/iframe-markdown-renderer.tsx +++ b/src/components/render-page/iframe-markdown-renderer.tsx @@ -9,7 +9,7 @@ import type { ScrollState } from '../editor-page/synced-scroll/scroll-props' import type { BaseConfiguration } from './window-post-message-communicator/rendering-message' import { CommunicationMessageType, RendererType } from './window-post-message-communicator/rendering-message' import { setDarkMode } from '../../redux/dark-mode/methods' -import type { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer' +import type { ImageClickHandler } from '../markdown-renderer/markdown-extension/image/proxy-image-replacer' import { useImageClickHandler } from './hooks/use-image-click-handler' import { MarkdownDocument } from './markdown-document' import { countWords } from './word-counter' diff --git a/src/components/render-page/markdown-document.tsx b/src/components/render-page/markdown-document.tsx index 3c96d562c..ba68b354f 100644 --- a/src/components/render-page/markdown-document.tsx +++ b/src/components/render-page/markdown-document.tsx @@ -12,7 +12,7 @@ import { YamlArrayDeprecationAlert } from '../editor-page/renderer-pane/yaml-arr import { useDocumentSyncScrolling } from './hooks/sync-scroll/use-document-sync-scrolling' import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props' import { DocumentMarkdownRenderer } from '../markdown-renderer/document-markdown-renderer' -import type { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer' +import type { ImageClickHandler } from '../markdown-renderer/markdown-extension/image/proxy-image-replacer' import './markdown-document.scss' import { WidthBasedTableOfContents } from './width-based-table-of-contents' import { ShowIf } from '../common/show-if/show-if' @@ -61,7 +61,7 @@ export const MarkdownDocument: React.FC = ({ const [tocAst, setTocAst] = useState() - const useAlternativeBreaks = useApplicationState((state) => state.noteDetails.frontmatter.breaks) + const newlinesAreBreaks = useApplicationState((state) => state.noteDetails.frontmatter.newlinesAreBreaks) useEffect(() => { if (!onHeightChange) { @@ -99,7 +99,7 @@ export const MarkdownDocument: React.FC = ({ onTocChange={setTocAst} baseUrl={baseUrl} onImageClick={onImageClick} - useAlternativeBreaks={useAlternativeBreaks} + newlinesAreBreaks={newlinesAreBreaks} lineOffset={frontmatterInfo?.lineOffset} /> diff --git a/src/redux/note-details/initial-state.ts b/src/redux/note-details/initial-state.ts index 36b1ef386..71337a53c 100644 --- a/src/redux/note-details/initial-state.ts +++ b/src/redux/note-details/initial-state.ts @@ -45,7 +45,7 @@ export const initialState: NoteDetails = { robots: '', lang: 'en', dir: NoteTextDirection.LTR, - breaks: true, + newlinesAreBreaks: true, GA: '', disqus: '', type: NoteType.DOCUMENT,