From 0670cddb0bc00ae5ee5524252a2c25bdbc3fce45 Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Thu, 8 Oct 2020 22:24:42 +0200 Subject: [PATCH] markdown-it-configurator (#626) Signed-off-by: Tilman Vatteroth Co-authored-by: Tilman Vatteroth Co-authored-by: Erik Michelson --- .../editor/app-bar/help-button/cheatsheet.tsx | 17 +- .../basic-markdown-renderer.tsx | 52 +---- .../full-markdown-renderer.tsx | 204 ++---------------- .../hooks/use-extract-first-headline.ts | 24 +++ .../use-post-meta-data-on-change.ts | 0 .../use-post-toc-ast-on-change.ts | 0 .../use-replacer-instance-list-creator.ts | 47 ++++ .../BasicMarkdownItConfigurator.tsx | 33 +++ .../FullMarkdownItConfigurator.tsx | 63 ++++++ .../MarkdownItConfigurator.tsx | 26 +++ .../markdown-it-plugins/alert-container.ts | 10 +- .../markdown-it-plugins/document-toc.ts | 21 ++ .../markdown-it-plugins/frontmatter.ts | 26 +++ .../markdown-it-plugins/headline-anchors.ts | 12 ++ .../markdown-it-plugins/line-number-marker.ts | 76 ------- .../markdown-it-plugins/parser-debugger.ts | 10 +- .../{plantuml-error.ts => plantuml.ts} | 18 +- .../quote-extra.ts} | 30 ++- .../markdown-it-plugins/tasks-lists.ts | 11 + .../markdown-it-plugins/twitter-emojis.ts | 9 + .../replace-legacy-slideshare-short-code.ts | 6 + .../replace-legacy-speakerdeck-short-code.ts | 6 + .../replace-quote-extra-author.ts | 11 - .../regex-plugins/replace-quote-extra-time.ts | 11 - .../replace-components/ComponentReplacer.ts | 5 +- .../asciinema/asciinema-replacer.tsx | 9 +- .../asciinema}/replace-asciinema-link.ts | 2 +- .../replace-components/gist/gist-replacer.tsx | 9 + .../gist}/replace-gist-link.ts | 2 +- .../gist}/replace-legacy-gist-short-code.ts | 2 +- .../katex/katex-replacer.tsx | 15 +- .../linemarker/line-number-marker.ts | 72 +++++++ .../replace-components/pdf/pdf-replacer.tsx | 7 + .../pdf}/replace-pdf-short-code.ts | 2 +- .../vimeo}/replace-legacy-vimeo-short-code.ts | 2 +- .../vimeo}/replace-vimeo-link.ts | 2 +- .../vimeo/vimeo-replacer.tsx | 11 +- .../replace-legacy-youtube-short-code.ts | 2 +- .../youtube}/replace-youtube-link.ts | 2 +- .../youtube/youtube-replacer.tsx | 11 +- .../utils/calculate-line-marker-positions.ts | 2 +- .../markdown-it-front-matter/index.d.ts | 4 +- 42 files changed, 524 insertions(+), 360 deletions(-) create mode 100644 src/components/markdown-renderer/hooks/use-extract-first-headline.ts rename src/components/markdown-renderer/{utils => hooks}/use-post-meta-data-on-change.ts (100%) rename src/components/markdown-renderer/{utils => hooks}/use-post-toc-ast-on-change.ts (100%) create mode 100644 src/components/markdown-renderer/hooks/use-replacer-instance-list-creator.ts create mode 100644 src/components/markdown-renderer/markdown-it-configurator/BasicMarkdownItConfigurator.tsx create mode 100644 src/components/markdown-renderer/markdown-it-configurator/FullMarkdownItConfigurator.tsx create mode 100644 src/components/markdown-renderer/markdown-it-configurator/MarkdownItConfigurator.tsx create mode 100644 src/components/markdown-renderer/markdown-it-plugins/document-toc.ts create mode 100644 src/components/markdown-renderer/markdown-it-plugins/frontmatter.ts create mode 100644 src/components/markdown-renderer/markdown-it-plugins/headline-anchors.ts delete mode 100644 src/components/markdown-renderer/markdown-it-plugins/line-number-marker.ts rename src/components/markdown-renderer/markdown-it-plugins/{plantuml-error.ts => plantuml.ts} (52%) rename src/components/markdown-renderer/{regex-plugins/replace-quote-extra-color.ts => markdown-it-plugins/quote-extra.ts} (64%) create mode 100644 src/components/markdown-renderer/markdown-it-plugins/tasks-lists.ts create mode 100644 src/components/markdown-renderer/markdown-it-plugins/twitter-emojis.ts delete mode 100644 src/components/markdown-renderer/regex-plugins/replace-quote-extra-author.ts delete mode 100644 src/components/markdown-renderer/regex-plugins/replace-quote-extra-time.ts rename src/components/markdown-renderer/{regex-plugins => replace-components/asciinema}/replace-asciinema-link.ts (88%) rename src/components/markdown-renderer/{regex-plugins => replace-components/gist}/replace-gist-link.ts (88%) rename src/components/markdown-renderer/{regex-plugins => replace-components/gist}/replace-legacy-gist-short-code.ts (80%) create mode 100644 src/components/markdown-renderer/replace-components/linemarker/line-number-marker.ts rename src/components/markdown-renderer/{regex-plugins => replace-components/pdf}/replace-pdf-short-code.ts (78%) rename src/components/markdown-renderer/{regex-plugins => replace-components/vimeo}/replace-legacy-vimeo-short-code.ts (79%) rename src/components/markdown-renderer/{regex-plugins => replace-components/vimeo}/replace-vimeo-link.ts (89%) rename src/components/markdown-renderer/{regex-plugins => replace-components/youtube}/replace-legacy-youtube-short-code.ts (80%) rename src/components/markdown-renderer/{regex-plugins => replace-components/youtube}/replace-youtube-link.ts (90%) diff --git a/src/components/editor/app-bar/help-button/cheatsheet.tsx b/src/components/editor/app-bar/help-button/cheatsheet.tsx index da83c0602..02bfb1d31 100644 --- a/src/components/editor/app-bar/help-button/cheatsheet.tsx +++ b/src/components/editor/app-bar/help-button/cheatsheet.tsx @@ -1,10 +1,9 @@ -import MarkdownIt from 'markdown-it' -import markdownItContainer from 'markdown-it-container' -import React, { useCallback } from 'react' +import React, { useMemo } from 'react' import { Table } from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' import { BasicMarkdownRenderer } from '../../../markdown-renderer/basic-markdown-renderer' -import { createRenderContainer, validAlertLevels } from '../../../markdown-renderer/markdown-it-plugins/alert-container' +import { BasicMarkdownItConfigurator } from '../../../markdown-renderer/markdown-it-configurator/BasicMarkdownItConfigurator' +import { alertContainer } from '../../../markdown-renderer/markdown-it-plugins/alert-container' import { HighlightedCode } from '../../../markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code' import './cheatsheet.scss' @@ -31,10 +30,10 @@ export const Cheatsheet: React.FC = () => { `:::info\n${t('editor.help.cheatsheet.exampleAlert')}\n:::` ] - const markdownItPlugins = useCallback((md: MarkdownIt) => { - validAlertLevels.forEach(level => { - md.use(markdownItContainer, level, { render: createRenderContainer(level) }) - }) + const markdownIt = useMemo(() => { + return new BasicMarkdownItConfigurator() + .pushConfig(alertContainer) + .buildConfiguredMarkdownIt() }, []) return ( @@ -53,7 +52,7 @@ export const Cheatsheet: React.FC = () => { diff --git a/src/components/markdown-renderer/basic-markdown-renderer.tsx b/src/components/markdown-renderer/basic-markdown-renderer.tsx index 34c0c1198..c7d11d065 100644 --- a/src/components/markdown-renderer/basic-markdown-renderer.tsx +++ b/src/components/markdown-renderer/basic-markdown-renderer.tsx @@ -1,13 +1,4 @@ import MarkdownIt from 'markdown-it' -import abbreviation from 'markdown-it-abbr' -import definitionList from 'markdown-it-deflist' -import emoji from 'markdown-it-emoji' -import footnote from 'markdown-it-footnote' -import imsize from 'markdown-it-imsize' -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 React, { ReactElement, RefObject, useMemo, useRef } from 'react' import { Alert } from 'react-bootstrap' import ReactHtmlParser from 'react-html-parser' @@ -15,9 +6,6 @@ import { Trans } from 'react-i18next' import { useSelector } from 'react-redux' import { ApplicationState } from '../../redux' import { ShowIf } from '../common/show-if/show-if' -import { combinedEmojiData } from './markdown-it-plugins/emoji/mapping' -import { linkifyExtra } from './markdown-it-plugins/linkify-extra' -import { MarkdownItParserDebugger } from './markdown-it-plugins/parser-debugger' import './markdown-renderer.scss' import { ComponentReplacer } from './replace-components/ComponentReplacer' import { AdditionalMarkdownRendererProps, LineKeys } from './types' @@ -25,8 +13,8 @@ import { buildTransformer } from './utils/html-react-transformer' import { calculateNewLineNumberMapping } from './utils/line-number-mapping' export interface BasicMarkdownRendererProps { - componentReplacers?: ComponentReplacer[], - onConfigureMarkdownIt?: (md: MarkdownIt) => void, + componentReplacers?: () => ComponentReplacer[], + markdownIt: MarkdownIt, documentReference?: RefObject onBeforeRendering?: () => void } @@ -36,44 +24,12 @@ export const BasicMarkdownRenderer: React.FC { const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength) - const markdownIt = useMemo(() => { - const md = new MarkdownIt('default', { - html: true, - breaks: true, - langPrefix: '', - typographer: true - }) - - md.use(emoji, { - defs: combinedEmojiData - }) - md.use(abbreviation) - md.use(definitionList) - md.use(subscript) - md.use(superscript) - md.use(inserted) - md.use(marked) - md.use(footnote) - md.use(imsize) - - if (onConfigureMarkdownIt) { - onConfigureMarkdownIt(md) - } - - md.use(linkifyExtra) - if (process.env.NODE_ENV !== 'production') { - md.use(MarkdownItParserDebugger) - } - - return md - }, [onConfigureMarkdownIt]) - const oldMarkdownLineKeys = useRef() const lastUsedLineId = useRef(0) @@ -87,7 +43,7 @@ export const BasicMarkdownRenderer: React.FC void @@ -79,150 +33,36 @@ export const FullMarkdownRenderer: React.FC { - const allReplacers = useMemo(() => { - return [ - new LinemarkerReplacer(), - new PossibleWiderReplacer(), - new GistReplacer(), - new YoutubeReplacer(), - new VimeoReplacer(), - new AsciinemaReplacer(), - new AbcReplacer(), - new PdfReplacer(), - new ImageReplacer(), - new SequenceDiagramReplacer(), - new CsvReplacer(), - new FlowchartReplacer(), - new MermaidReplacer(), - new GraphvizReplacer(), - new MarkmapReplacer(), - new VegaReplacer(), - new HighlightedCodeReplacer(), - new QuoteOptionsReplacer(), - new KatexReplacer(), - new TaskListReplacer(onTaskCheckedChange) - ] - }, [onTaskCheckedChange]) + const allReplacers = useReplacerInstanceListCreator(onTaskCheckedChange) const [yamlError, setYamlError] = useState(false) - const plantumlServer = useSelector((state: ApplicationState) => state.config.plantumlServer) - const rawMetaRef = useRef() const firstHeadingRef = useRef() const documentElement = useRef(null) const currentLineMarkers = useRef() usePostMetaDataOnChange(rawMetaRef.current, firstHeadingRef.current, onMetaDataChange, onFirstHeadingChange) useCalculateLineMarkerPosition(documentElement, currentLineMarkers.current, onLineMarkerPositionChanged, documentElement.current?.offsetTop ?? 0) + useExtractFirstHeadline(documentElement, content, onFirstHeadingChange) const tocAst = useRef() usePostTocAstOnChange(tocAst, onTocChange) - const extractInnerText = useCallback((node: ChildNode) => { - let innerText = '' - if (node.childNodes && node.childNodes.length > 0) { - node.childNodes.forEach((child) => { innerText += extractInnerText(child) }) - } else if (node.nodeName === 'IMG') { - innerText += (node as HTMLImageElement).getAttribute('alt') - } else { - innerText += node.textContent - } - return innerText - }, []) - - useEffect(() => { - if (onFirstHeadingChange && documentElement.current) { - const firstHeading = documentElement.current.getElementsByTagName('h1').item(0) - if (firstHeading) { - onFirstHeadingChange(extractInnerText(firstHeading)) - } - } - }, [content, extractInnerText, onFirstHeadingChange]) - - const configureMarkdownIt = useCallback((md: MarkdownIt): void => { - if (onMetaDataChange) { - md.use(frontmatter, (rawMeta: string) => { - try { - const meta: RawYAMLMetadata = yaml.safeLoad(rawMeta) as RawYAMLMetadata - setYamlError(false) - rawMetaRef.current = meta - } catch (e) { - console.error(e) - setYamlError(true) - } - }) - } - md.use(markdownItTaskLists, { lineNumber: true }) - if (plantumlServer) { - md.use(plantuml, { - openMarker: '```plantuml', - closeMarker: '```', - server: plantumlServer - }) - } else { - md.use(plantumlError) - } - - if (onMetaDataChange) { - md.use(frontmatter, (rawMeta: string) => { - try { - const meta: RawYAMLMetadata = yaml.safeLoad(rawMeta) as RawYAMLMetadata - setYamlError(false) - rawMetaRef.current = meta - } catch (e) { - console.error(e) - setYamlError(true) - rawMetaRef.current = ({} as RawYAMLMetadata) - } - }) - } - // noinspection CheckTagEmptyBody - md.use(anchor, { - permalink: true, - permalinkBefore: true, - permalinkClass: 'heading-anchor text-dark', - permalinkSymbol: '' - }) - md.use(mathJax({ - beforeMath: '', - afterMath: '', - beforeInlineMath: '', - afterInlineMath: '', - beforeDisplayMath: '', - afterDisplayMath: '' - })) - md.use(markdownItRegex, replaceLegacyYoutubeShortCode) - md.use(markdownItRegex, replaceLegacyVimeoShortCode) - md.use(markdownItRegex, replaceLegacyGistShortCode) - md.use(markdownItRegex, replaceLegacySlideshareShortCode) - md.use(markdownItRegex, replaceLegacySpeakerdeckShortCode) - md.use(markdownItRegex, replacePdfShortCode) - md.use(markdownItRegex, replaceAsciinemaLink) - md.use(markdownItRegex, replaceYouTubeLink) - md.use(markdownItRegex, replaceVimeoLink) - md.use(markdownItRegex, replaceGistLink) - md.use(highlightedCode) - md.use(markdownItRegex, replaceQuoteExtraAuthor) - md.use(markdownItRegex, replaceQuoteExtraColor) - md.use(markdownItRegex, replaceQuoteExtraTime) - md.use(toc, { - placeholder: '(\\[TOC\\]|\\[toc\\])', - listType: 'ul', - level: [1, 2, 3], - callback: (code: string, ast: TocAst): void => { - tocAst.current = ast + const markdownIt = useMemo(() => { + return (new FullMarkdownItConfigurator( + !!onMetaDataChange, + error => setYamlError(error), + rawMeta => { + rawMetaRef.current = rawMeta }, - slugify: slugify - }) - validAlertLevels.forEach(level => { - md.use(markdownItContainer, level, { render: createRenderContainer(level) }) - }) - md.use(lineNumberMarker(), { - postLineMarkers: (lineMarkers) => { + toc => { + tocAst.current = toc + }, + lineMarkers => { currentLineMarkers.current = lineMarkers } - }) - }, [onMetaDataChange, plantumlServer]) + )).buildConfiguredMarkdownIt() + }, [onMetaDataChange]) const clearMetadata = useCallback(() => { rawMetaRef.current = undefined @@ -238,7 +78,7 @@ export const FullMarkdownRenderer: React.FC ) diff --git a/src/components/markdown-renderer/hooks/use-extract-first-headline.ts b/src/components/markdown-renderer/hooks/use-extract-first-headline.ts new file mode 100644 index 000000000..af2799998 --- /dev/null +++ b/src/components/markdown-renderer/hooks/use-extract-first-headline.ts @@ -0,0 +1,24 @@ +import React, { useCallback, useEffect } from 'react' + +export const useExtractFirstHeadline = (documentElement: React.RefObject, content: string, onFirstHeadingChange?: (firstHeading: string | undefined) => void): void => { + const extractInnerText = useCallback((node: ChildNode): string => { + let innerText = '' + if (node.childNodes && node.childNodes.length > 0) { + node.childNodes.forEach((child) => { innerText += extractInnerText(child) }) + } else if (node.nodeName === 'IMG') { + innerText += (node as HTMLImageElement).getAttribute('alt') + } else { + innerText += node.textContent + } + return innerText + }, []) + + useEffect(() => { + if (onFirstHeadingChange && documentElement.current) { + const firstHeading = documentElement.current.getElementsByTagName('h1').item(0) + if (firstHeading) { + onFirstHeadingChange(extractInnerText(firstHeading)) + } + } + }, [documentElement, extractInnerText, onFirstHeadingChange, content]) +} diff --git a/src/components/markdown-renderer/utils/use-post-meta-data-on-change.ts b/src/components/markdown-renderer/hooks/use-post-meta-data-on-change.ts similarity index 100% rename from src/components/markdown-renderer/utils/use-post-meta-data-on-change.ts rename to src/components/markdown-renderer/hooks/use-post-meta-data-on-change.ts diff --git a/src/components/markdown-renderer/utils/use-post-toc-ast-on-change.ts b/src/components/markdown-renderer/hooks/use-post-toc-ast-on-change.ts similarity index 100% rename from src/components/markdown-renderer/utils/use-post-toc-ast-on-change.ts rename to src/components/markdown-renderer/hooks/use-post-toc-ast-on-change.ts diff --git a/src/components/markdown-renderer/hooks/use-replacer-instance-list-creator.ts b/src/components/markdown-renderer/hooks/use-replacer-instance-list-creator.ts new file mode 100644 index 000000000..8e64f0a21 --- /dev/null +++ b/src/components/markdown-renderer/hooks/use-replacer-instance-list-creator.ts @@ -0,0 +1,47 @@ +import { useMemo } from 'react' +import { AbcReplacer } from '../replace-components/abc/abc-replacer' +import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer' +import { ComponentReplacer } from '../replace-components/ComponentReplacer' +import { CsvReplacer } from '../replace-components/csv/csv-replacer' +import { FlowchartReplacer } from '../replace-components/flow/flowchart-replacer' +import { GistReplacer } from '../replace-components/gist/gist-replacer' +import { GraphvizReplacer } from '../replace-components/graphviz/graphviz-replacer' +import { HighlightedCodeReplacer } from '../replace-components/highlighted-fence/highlighted-fence-replacer' +import { ImageReplacer } from '../replace-components/image/image-replacer' +import { KatexReplacer } from '../replace-components/katex/katex-replacer' +import { LinemarkerReplacer } from '../replace-components/linemarker/linemarker-replacer' +import { MarkmapReplacer } from '../replace-components/markmap/markmap-replacer' +import { MermaidReplacer } from '../replace-components/mermaid/mermaid-replacer' +import { PdfReplacer } from '../replace-components/pdf/pdf-replacer' +import { PossibleWiderReplacer } from '../replace-components/possible-wider/possible-wider-replacer' +import { QuoteOptionsReplacer } from '../replace-components/quote-options/quote-options-replacer' +import { SequenceDiagramReplacer } from '../replace-components/sequence-diagram/sequence-diagram-replacer' +import { TaskListReplacer } from '../replace-components/task-list/task-list-replacer' +import { VegaReplacer } from '../replace-components/vega-lite/vega-replacer' +import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer' +import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer' + +export const useReplacerInstanceListCreator = (onTaskCheckedChange: (lineInMarkdown: number, checked: boolean) => void): ()=>ComponentReplacer[] => { + return useMemo(() => () => [ + new LinemarkerReplacer(), + new PossibleWiderReplacer(), + new GistReplacer(), + new YoutubeReplacer(), + new VimeoReplacer(), + new AsciinemaReplacer(), + new AbcReplacer(), + new PdfReplacer(), + new ImageReplacer(), + new SequenceDiagramReplacer(), + new CsvReplacer(), + new FlowchartReplacer(), + new MermaidReplacer(), + new GraphvizReplacer(), + new MarkmapReplacer(), + new VegaReplacer(), + new HighlightedCodeReplacer(), + new QuoteOptionsReplacer(), + new KatexReplacer(), + new TaskListReplacer(onTaskCheckedChange) + ], [onTaskCheckedChange]) +} diff --git a/src/components/markdown-renderer/markdown-it-configurator/BasicMarkdownItConfigurator.tsx b/src/components/markdown-renderer/markdown-it-configurator/BasicMarkdownItConfigurator.tsx new file mode 100644 index 000000000..c018a8d9d --- /dev/null +++ b/src/components/markdown-renderer/markdown-it-configurator/BasicMarkdownItConfigurator.tsx @@ -0,0 +1,33 @@ +import MarkdownIt from 'markdown-it' +import abbreviation from 'markdown-it-abbr' +import definitionList from 'markdown-it-deflist' +import footnote from 'markdown-it-footnote' +import imsize from 'markdown-it-imsize' +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 { linkifyExtra } from '../markdown-it-plugins/linkify-extra' +import { MarkdownItParserDebugger } from '../markdown-it-plugins/parser-debugger' +import { twitterEmojis } from '../markdown-it-plugins/twitter-emojis' +import { MarkdownItConfigurator } from './MarkdownItConfigurator' + +export class BasicMarkdownItConfigurator extends MarkdownItConfigurator { + protected configure (markdownIt: MarkdownIt): void { + this.configurations.push( + twitterEmojis, + abbreviation, + definitionList, + subscript, + superscript, + inserted, + marked, + footnote, + imsize + ) + this.postConfigurations.push( + linkifyExtra, + MarkdownItParserDebugger + ) + } +} diff --git a/src/components/markdown-renderer/markdown-it-configurator/FullMarkdownItConfigurator.tsx b/src/components/markdown-renderer/markdown-it-configurator/FullMarkdownItConfigurator.tsx new file mode 100644 index 000000000..c04c634d3 --- /dev/null +++ b/src/components/markdown-renderer/markdown-it-configurator/FullMarkdownItConfigurator.tsx @@ -0,0 +1,63 @@ +import MarkdownIt from 'markdown-it' +import { TocAst } from '../../../external-types/markdown-it-toc-done-right/interface' +import { RawYAMLMetadata } from '../../editor/yaml-metadata/yaml-metadata' +import { alertContainer } from '../markdown-it-plugins/alert-container' +import { documentToc } from '../markdown-it-plugins/document-toc' +import { frontmatterExtract } from '../markdown-it-plugins/frontmatter' +import { headlineAnchors } from '../markdown-it-plugins/headline-anchors' +import { highlightedCode } from '../markdown-it-plugins/highlighted-code' +import { plantumlWithError } from '../markdown-it-plugins/plantuml' +import { quoteExtra } from '../markdown-it-plugins/quote-extra' +import { tasksLists } from '../markdown-it-plugins/tasks-lists' +import { legacySlideshareShortCode } from '../regex-plugins/replace-legacy-slideshare-short-code' +import { legacySpeakerdeckShortCode } from '../regex-plugins/replace-legacy-speakerdeck-short-code' +import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer' +import { GistReplacer } from '../replace-components/gist/gist-replacer' +import { KatexReplacer } from '../replace-components/katex/katex-replacer' +import { LineMarkers, lineNumberMarker } from '../replace-components/linemarker/line-number-marker' +import { PdfReplacer } from '../replace-components/pdf/pdf-replacer' +import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer' +import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer' +import { BasicMarkdownItConfigurator } from './BasicMarkdownItConfigurator' + +export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator { + constructor ( + private useFrontmatter: boolean, + private onYamlError: (error: boolean) => void, + private onRawMeta: (rawMeta: RawYAMLMetadata) => void, + private onToc: (toc: TocAst) => void, + private onLineMarkers: (lineMarkers: LineMarkers[]) => void + ) { + super() + } + + protected configure (markdownIt: MarkdownIt): void { + super.configure(markdownIt) + + this.configurations.push( + plantumlWithError, + tasksLists, + (markdownIt) => { + frontmatterExtract(markdownIt, + !this.useFrontmatter ? undefined : { + onYamlError: (error: boolean) => this.onYamlError(error), + onRawMeta: (rawMeta: RawYAMLMetadata) => this.onRawMeta(rawMeta) + }) + }, + headlineAnchors, + KatexReplacer.markdownItPlugin, + YoutubeReplacer.markdownItPlugin, + VimeoReplacer.markdownItPlugin, + GistReplacer.markdownItPlugin, + legacySlideshareShortCode, + legacySpeakerdeckShortCode, + PdfReplacer.markdownItPlugin, + AsciinemaReplacer.markdownItPlugin, + highlightedCode, + quoteExtra, + (markdownIt) => documentToc(markdownIt, this.onToc), + alertContainer, + (markdownIt) => lineNumberMarker(markdownIt, (lineMarkers) => this.onLineMarkers(lineMarkers)) + ) + } +} diff --git a/src/components/markdown-renderer/markdown-it-configurator/MarkdownItConfigurator.tsx b/src/components/markdown-renderer/markdown-it-configurator/MarkdownItConfigurator.tsx new file mode 100644 index 000000000..cc7e25c04 --- /dev/null +++ b/src/components/markdown-renderer/markdown-it-configurator/MarkdownItConfigurator.tsx @@ -0,0 +1,26 @@ +import MarkdownIt from 'markdown-it' + +export abstract class MarkdownItConfigurator { + protected configurations:MarkdownIt.PluginSimple[] = []; + protected postConfigurations:MarkdownIt.PluginSimple[] = []; + + protected abstract configure(markdownIt: MarkdownIt): void; + + public pushConfig (plugin: MarkdownIt.PluginSimple): this { + this.configurations.push(plugin) + return this + } + + public buildConfiguredMarkdownIt (): MarkdownIt { + const markdownIt = new MarkdownIt('default', { + html: true, + breaks: true, + langPrefix: '', + typographer: true + }) + this.configure(markdownIt) + this.configurations.forEach((configuration) => markdownIt.use(configuration)) + this.postConfigurations.forEach((postConfiguration) => markdownIt.use(postConfiguration)) + return markdownIt + } +} diff --git a/src/components/markdown-renderer/markdown-it-plugins/alert-container.ts b/src/components/markdown-renderer/markdown-it-plugins/alert-container.ts index de92d4643..800c4ace7 100644 --- a/src/components/markdown-renderer/markdown-it-plugins/alert-container.ts +++ b/src/components/markdown-renderer/markdown-it-plugins/alert-container.ts @@ -1,12 +1,14 @@ import MarkdownIt from 'markdown-it' +import markdownItContainer from 'markdown-it-container' import Renderer from 'markdown-it/lib/renderer' import Token from 'markdown-it/lib/token' +import { MarkdownItPlugin } from '../replace-components/ComponentReplacer' type RenderContainerReturn = (tokens: Token[], index: number, options: MarkdownIt.Options, env: unknown, self: Renderer) => void; type ValidAlertLevels = ('warning' | 'danger' | 'success' | 'info') export const validAlertLevels: ValidAlertLevels[] = ['success', 'danger', 'info', 'warning'] -export const createRenderContainer = (level: ValidAlertLevels): RenderContainerReturn => { +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') @@ -14,3 +16,9 @@ export const createRenderContainer = (level: ValidAlertLevels): RenderContainerR 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-toc.ts b/src/components/markdown-renderer/markdown-it-plugins/document-toc.ts new file mode 100644 index 000000000..0337faa36 --- /dev/null +++ b/src/components/markdown-renderer/markdown-it-plugins/document-toc.ts @@ -0,0 +1,21 @@ +import MarkdownIt from 'markdown-it' +import toc from 'markdown-it-toc-done-right' +import { TocAst } from '../../../external-types/markdown-it-toc-done-right/interface' +import { slugify } from '../../editor/table-of-contents/table-of-contents' + +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: slugify + }) +} diff --git a/src/components/markdown-renderer/markdown-it-plugins/frontmatter.ts b/src/components/markdown-renderer/markdown-it-plugins/frontmatter.ts new file mode 100644 index 000000000..a0d723e03 --- /dev/null +++ b/src/components/markdown-renderer/markdown-it-plugins/frontmatter.ts @@ -0,0 +1,26 @@ +import yaml from 'js-yaml' +import MarkdownIt from 'markdown-it' +import frontmatter from 'markdown-it-front-matter' +import { RawYAMLMetadata } from '../../editor/yaml-metadata/yaml-metadata' + +interface FrontmatterPluginOptions { + onYamlError: (error: boolean) => void, + onRawMeta: (rawMeta: RawYAMLMetadata) => void, +} + +export const frontmatterExtract: MarkdownIt.PluginWithOptions = (markdownIt: MarkdownIt, options) => { + if (!options) { + return + } + frontmatter(markdownIt, (rawMeta: string) => { + try { + const meta: RawYAMLMetadata = yaml.safeLoad(rawMeta) as RawYAMLMetadata + options.onYamlError(false) + options.onRawMeta(meta) + } catch (e) { + console.error(e) + options.onYamlError(true) + options.onRawMeta({} as RawYAMLMetadata) + } + }) +} diff --git a/src/components/markdown-renderer/markdown-it-plugins/headline-anchors.ts b/src/components/markdown-renderer/markdown-it-plugins/headline-anchors.ts new file mode 100644 index 000000000..7e29d8262 --- /dev/null +++ b/src/components/markdown-renderer/markdown-it-plugins/headline-anchors.ts @@ -0,0 +1,12 @@ +import MarkdownIt from 'markdown-it' +import anchor from 'markdown-it-anchor' + +export const headlineAnchors: MarkdownIt.PluginSimple = (markdownIt) => { + // noinspection CheckTagEmptyBody + anchor(markdownIt, { + permalink: true, + permalinkBefore: true, + permalinkClass: 'heading-anchor text-dark', + permalinkSymbol: '' + }) +} diff --git a/src/components/markdown-renderer/markdown-it-plugins/line-number-marker.ts b/src/components/markdown-renderer/markdown-it-plugins/line-number-marker.ts deleted file mode 100644 index d636709dd..000000000 --- a/src/components/markdown-renderer/markdown-it-plugins/line-number-marker.ts +++ /dev/null @@ -1,76 +0,0 @@ -import MarkdownIt from 'markdown-it/lib' -import Token from 'markdown-it/lib/token' - -export interface LineMarkers { - startLine: number - endLine: number -} - -export interface LineNumberMarkerOptions { - postLineMarkers: (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: () => MarkdownIt.PluginWithOptions = () => { - return (md: MarkdownIt, options) => { - // 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) - if (options?.postLineMarkers) { - options.postLineMarkers(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[]) => { - 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, endLine: endLineNumber }) - } - - insertNewLineMarker(startLineNumber, endLineNumber, tokenPosition, token.level, tokens) - tokenPosition += 1 - - if (token.children) { - tagTokens(token.children, lineMarkers) - } - } - } - } -} diff --git a/src/components/markdown-renderer/markdown-it-plugins/parser-debugger.ts b/src/components/markdown-renderer/markdown-it-plugins/parser-debugger.ts index 5a6ba13dc..8518eb746 100644 --- a/src/components/markdown-renderer/markdown-it-plugins/parser-debugger.ts +++ b/src/components/markdown-renderer/markdown-it-plugins/parser-debugger.ts @@ -1,8 +1,10 @@ import MarkdownIt from 'markdown-it/lib' export const MarkdownItParserDebugger: MarkdownIt.PluginSimple = (md: MarkdownIt) => { - md.core.ruler.push('test', (state) => { - console.log(state) - return true - }) + if (process.env.NODE_ENV !== 'production') { + md.core.ruler.push('test', (state) => { + console.log(state) + return true + }) + } } diff --git a/src/components/markdown-renderer/markdown-it-plugins/plantuml-error.ts b/src/components/markdown-renderer/markdown-it-plugins/plantuml.ts similarity index 52% rename from src/components/markdown-renderer/markdown-it-plugins/plantuml-error.ts rename to src/components/markdown-renderer/markdown-it-plugins/plantuml.ts index 355d3d827..9b6e710f6 100644 --- a/src/components/markdown-renderer/markdown-it-plugins/plantuml-error.ts +++ b/src/components/markdown-renderer/markdown-it-plugins/plantuml.ts @@ -1,8 +1,24 @@ +import plantuml from 'markdown-it-plantuml' import MarkdownIt, { Options } from 'markdown-it/lib' import Renderer, { RenderRule } from 'markdown-it/lib/renderer' import Token from 'markdown-it/lib/token' +import { store } from '../../../redux' +import { MarkdownItPlugin } from '../replace-components/ComponentReplacer' -export const plantumlError: MarkdownIt.PluginSimple = (md) => { +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] diff --git a/src/components/markdown-renderer/regex-plugins/replace-quote-extra-color.ts b/src/components/markdown-renderer/markdown-it-plugins/quote-extra.ts similarity index 64% rename from src/components/markdown-renderer/regex-plugins/replace-quote-extra-color.ts rename to src/components/markdown-renderer/markdown-it-plugins/quote-extra.ts index 41c868f01..94db71527 100644 --- a/src/components/markdown-renderer/regex-plugins/replace-quote-extra-color.ts +++ b/src/components/markdown-renderer/markdown-it-plugins/quote-extra.ts @@ -1,8 +1,26 @@ +import MarkdownIt from 'markdown-it' +import markdownItRegex from 'markdown-it-regex' import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' +export const quoteExtra: MarkdownIt.PluginSimple = (markdownIt) => { + markdownItRegex(markdownIt, replaceQuoteExtraAuthor) + markdownItRegex(markdownIt, replaceQuoteExtraColor) + markdownItRegex(markdownIt, replaceQuoteExtraTime) +} + +const replaceQuoteExtraTime: RegexOptions = { + name: 'quote-extra-time', + regex: /\[time=([^\]]+)]/, + replace: (match) => { + // ESLint wants to collapse this tag, but then the tag won't be valid html anymore. + // noinspection CheckTagEmptyBody + return ` ${match}` + } +} + const cssColorRegex = /\[color=(#(?:[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 replaceQuoteExtraColor: RegexOptions = { +const replaceQuoteExtraColor: RegexOptions = { name: 'quote-extra-color', regex: cssColorRegex, replace: (match) => { @@ -11,3 +29,13 @@ export const replaceQuoteExtraColor: RegexOptions = { return `` } } + +const replaceQuoteExtraAuthor: RegexOptions = { + name: 'quote-extra-name', + regex: /\[name=([^\]]+)]/, + replace: (match) => { + // ESLint wants to collapse this tag, but then the tag won't be valid html anymore. + // noinspection CheckTagEmptyBody + return ` ${match}` + } +} diff --git a/src/components/markdown-renderer/markdown-it-plugins/tasks-lists.ts b/src/components/markdown-renderer/markdown-it-plugins/tasks-lists.ts new file mode 100644 index 000000000..8f01f01f3 --- /dev/null +++ b/src/components/markdown-renderer/markdown-it-plugins/tasks-lists.ts @@ -0,0 +1,11 @@ +import markdownItTaskLists from '@hedgedoc/markdown-it-task-lists' +import MarkdownIt from 'markdown-it' + +export const tasksLists: MarkdownIt.PluginSimple = (markdownIt) => { + markdownItTaskLists(markdownIt, { + enabled: true, + label: true, + labelAfter: 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 new file mode 100644 index 000000000..afb75a494 --- /dev/null +++ b/src/components/markdown-renderer/markdown-it-plugins/twitter-emojis.ts @@ -0,0 +1,9 @@ +import MarkdownIt from 'markdown-it' +import emoji from 'markdown-it-emoji' +import { combinedEmojiData } from './emoji/mapping' + +export const twitterEmojis: MarkdownIt.PluginSimple = (markdownIt) => { + emoji(markdownIt, { + defs: combinedEmojiData + }) +} diff --git a/src/components/markdown-renderer/regex-plugins/replace-legacy-slideshare-short-code.ts b/src/components/markdown-renderer/regex-plugins/replace-legacy-slideshare-short-code.ts index ec7282b47..35f814ebf 100644 --- a/src/components/markdown-renderer/regex-plugins/replace-legacy-slideshare-short-code.ts +++ b/src/components/markdown-renderer/regex-plugins/replace-legacy-slideshare-short-code.ts @@ -1,7 +1,13 @@ +import markdownItRegex from 'markdown-it-regex' +import MarkdownIt from 'markdown-it/lib' import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' const finalRegex = /^{%slideshare (\w+\/[\w-]+) ?%}$/ +export const legacySlideshareShortCode: MarkdownIt.PluginSimple = (markdownIt) => { + markdownItRegex(markdownIt, replaceLegacySlideshareShortCode) +} + export const replaceLegacySlideshareShortCode: RegexOptions = { name: 'legacy-slideshare-short-code', regex: finalRegex, diff --git a/src/components/markdown-renderer/regex-plugins/replace-legacy-speakerdeck-short-code.ts b/src/components/markdown-renderer/regex-plugins/replace-legacy-speakerdeck-short-code.ts index f5267aa43..0a20949c0 100644 --- a/src/components/markdown-renderer/regex-plugins/replace-legacy-speakerdeck-short-code.ts +++ b/src/components/markdown-renderer/regex-plugins/replace-legacy-speakerdeck-short-code.ts @@ -1,7 +1,13 @@ +import markdownItRegex from 'markdown-it-regex' +import MarkdownIt from 'markdown-it/lib' import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' const finalRegex = /^{%speakerdeck (\w+\/[\w-]+) ?%}$/ +export const legacySpeakerdeckShortCode: MarkdownIt.PluginSimple = (markdownIt) => { + markdownItRegex(markdownIt, replaceLegacySpeakerdeckShortCode) +} + export const replaceLegacySpeakerdeckShortCode: RegexOptions = { name: 'legacy-speakerdeck-short-code', regex: finalRegex, diff --git a/src/components/markdown-renderer/regex-plugins/replace-quote-extra-author.ts b/src/components/markdown-renderer/regex-plugins/replace-quote-extra-author.ts deleted file mode 100644 index ae97acd89..000000000 --- a/src/components/markdown-renderer/regex-plugins/replace-quote-extra-author.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' - -export const replaceQuoteExtraAuthor: RegexOptions = { - name: 'quote-extra-name', - regex: /\[name=([^\]]+)]/, - replace: (match) => { - // ESLint wants to collapse this tag, but then the tag won't be valid html anymore. - // noinspection CheckTagEmptyBody - return ` ${match}` - } -} diff --git a/src/components/markdown-renderer/regex-plugins/replace-quote-extra-time.ts b/src/components/markdown-renderer/regex-plugins/replace-quote-extra-time.ts deleted file mode 100644 index c8c0ae3ed..000000000 --- a/src/components/markdown-renderer/regex-plugins/replace-quote-extra-time.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' - -export const replaceQuoteExtraTime: RegexOptions = { - name: 'quote-extra-time', - regex: /\[time=([^\]]+)]/, - replace: (match) => { - // ESLint wants to collapse this tag, but then the tag won't be valid html anymore. - // noinspection CheckTagEmptyBody - return ` ${match}` - } -} diff --git a/src/components/markdown-renderer/replace-components/ComponentReplacer.ts b/src/components/markdown-renderer/replace-components/ComponentReplacer.ts index 217eb9db7..0af4943e4 100644 --- a/src/components/markdown-renderer/replace-components/ComponentReplacer.ts +++ b/src/components/markdown-renderer/replace-components/ComponentReplacer.ts @@ -1,10 +1,13 @@ import { DomElement } from 'domhandler' +import MarkdownIt from 'markdown-it' import { ReactElement } from 'react' export type SubNodeTransform = (node: DomElement, subIndex: number) => ReactElement | void | null export type NativeRenderer = (node: DomElement, key: number) => ReactElement +export type MarkdownItPlugin = MarkdownIt.PluginSimple | MarkdownIt.PluginWithOptions | MarkdownIt.PluginWithParams + export abstract class ComponentReplacer { - public abstract getReplacement(node: DomElement, subNodeTransform: SubNodeTransform): (ReactElement | null | undefined); + public abstract getReplacement (node: DomElement, subNodeTransform: SubNodeTransform): (ReactElement | null | undefined); } diff --git a/src/components/markdown-renderer/replace-components/asciinema/asciinema-replacer.tsx b/src/components/markdown-renderer/replace-components/asciinema/asciinema-replacer.tsx index b5f8bc297..ba2b8253c 100644 --- a/src/components/markdown-renderer/replace-components/asciinema/asciinema-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/asciinema/asciinema-replacer.tsx @@ -1,8 +1,11 @@ import { DomElement } from 'domhandler' +import MarkdownIt from 'markdown-it' +import markdownItRegex from 'markdown-it-regex' import React from 'react' -import { getAttributesFromHedgeDocTag } from '../utils' import { ComponentReplacer } from '../ComponentReplacer' +import { getAttributesFromHedgeDocTag } from '../utils' import { AsciinemaFrame } from './asciinema-frame' +import { replaceAsciinemaLink } from './replace-asciinema-link' export class AsciinemaReplacer extends ComponentReplacer { private counterMap: Map = new Map() @@ -18,4 +21,8 @@ export class AsciinemaReplacer extends ComponentReplacer { ) } } + + public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => { + markdownItRegex(markdownIt, replaceAsciinemaLink) + } } diff --git a/src/components/markdown-renderer/regex-plugins/replace-asciinema-link.ts b/src/components/markdown-renderer/replace-components/asciinema/replace-asciinema-link.ts similarity index 88% rename from src/components/markdown-renderer/regex-plugins/replace-asciinema-link.ts rename to src/components/markdown-renderer/replace-components/asciinema/replace-asciinema-link.ts index 5a5ea3ba3..3ebe05d55 100644 --- a/src/components/markdown-renderer/regex-plugins/replace-asciinema-link.ts +++ b/src/components/markdown-renderer/replace-components/asciinema/replace-asciinema-link.ts @@ -1,4 +1,4 @@ -import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' +import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' const protocolRegex = /(?:http(?:s)?:\/\/)?/ const domainRegex = /(?:asciinema\.org\/a\/)/ diff --git a/src/components/markdown-renderer/replace-components/gist/gist-replacer.tsx b/src/components/markdown-renderer/replace-components/gist/gist-replacer.tsx index b958eff08..75f46f669 100644 --- a/src/components/markdown-renderer/replace-components/gist/gist-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/gist/gist-replacer.tsx @@ -1,5 +1,9 @@ import { DomElement } from 'domhandler' +import MarkdownIt from 'markdown-it' +import markdownItRegex from 'markdown-it-regex' import React from 'react' +import { replaceGistLink } from './replace-gist-link' +import { replaceLegacyGistShortCode } from './replace-legacy-gist-short-code' import { getAttributesFromHedgeDocTag } from '../utils' import { ComponentReplacer } from '../ComponentReplacer' import { OneClickEmbedding } from '../one-click-frame/one-click-embedding' @@ -22,4 +26,9 @@ export class GistReplacer extends ComponentReplacer { ) } } + + public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => { + markdownItRegex(markdownIt, replaceGistLink) + markdownItRegex(markdownIt, replaceLegacyGistShortCode) + } } diff --git a/src/components/markdown-renderer/regex-plugins/replace-gist-link.ts b/src/components/markdown-renderer/replace-components/gist/replace-gist-link.ts similarity index 88% rename from src/components/markdown-renderer/regex-plugins/replace-gist-link.ts rename to src/components/markdown-renderer/replace-components/gist/replace-gist-link.ts index 0a1a1e552..4addd34d6 100644 --- a/src/components/markdown-renderer/regex-plugins/replace-gist-link.ts +++ b/src/components/markdown-renderer/replace-components/gist/replace-gist-link.ts @@ -1,4 +1,4 @@ -import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' +import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' const protocolRegex = /(?:http(?:s)?:\/\/)?/ const domainRegex = /(?:gist\.github\.com\/)/ diff --git a/src/components/markdown-renderer/regex-plugins/replace-legacy-gist-short-code.ts b/src/components/markdown-renderer/replace-components/gist/replace-legacy-gist-short-code.ts similarity index 80% rename from src/components/markdown-renderer/regex-plugins/replace-legacy-gist-short-code.ts rename to src/components/markdown-renderer/replace-components/gist/replace-legacy-gist-short-code.ts index c76ec666d..49d6dd320 100644 --- a/src/components/markdown-renderer/regex-plugins/replace-legacy-gist-short-code.ts +++ b/src/components/markdown-renderer/replace-components/gist/replace-legacy-gist-short-code.ts @@ -1,4 +1,4 @@ -import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' +import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' const finalRegex = /^{%gist (\w+\/\w+) ?%}$/ diff --git a/src/components/markdown-renderer/replace-components/katex/katex-replacer.tsx b/src/components/markdown-renderer/replace-components/katex/katex-replacer.tsx index 6dcec4815..3b90ef87a 100644 --- a/src/components/markdown-renderer/replace-components/katex/katex-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/katex/katex-replacer.tsx @@ -1,9 +1,11 @@ import { DomElement } from 'domhandler' +import MarkdownIt from 'markdown-it' +import mathJax from 'markdown-it-mathjax' import React from 'react' import { ComponentReplacer } from '../ComponentReplacer' import './katex.scss' -const getNodeIfKatexBlock = (node: DomElement): (DomElement|undefined) => { +const getNodeIfKatexBlock = (node: DomElement): (DomElement | undefined) => { if (node.name !== 'p' || !node.children || node.children.length === 0) { return } @@ -12,7 +14,7 @@ const getNodeIfKatexBlock = (node: DomElement): (DomElement|undefined) => { }) } -const getNodeIfInlineKatex = (node: DomElement): (DomElement|undefined) => { +const getNodeIfInlineKatex = (node: DomElement): (DomElement | undefined) => { return (node.name === 'app-katex' && node.attribs?.inline !== undefined) ? node : undefined } @@ -27,4 +29,13 @@ export class KatexReplacer extends ComponentReplacer { return } } + + public static readonly markdownItPlugin: MarkdownIt.PluginSimple = mathJax({ + beforeMath: '', + afterMath: '', + beforeInlineMath: '', + afterInlineMath: '', + beforeDisplayMath: '', + afterDisplayMath: '' + }) } 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 new file mode 100644 index 000000000..2f560317e --- /dev/null +++ b/src/components/markdown-renderer/replace-components/linemarker/line-number-marker.ts @@ -0,0 +1,72 @@ +import 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: MarkdownIt.PluginWithOptions = (md: MarkdownIt, options) => { + // 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) + 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[]) => { + 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, endLine: endLineNumber }) + } + + insertNewLineMarker(startLineNumber, endLineNumber, tokenPosition, token.level, tokens) + tokenPosition += 1 + + if (token.children) { + tagTokens(token.children, lineMarkers) + } + } + } +} diff --git a/src/components/markdown-renderer/replace-components/pdf/pdf-replacer.tsx b/src/components/markdown-renderer/replace-components/pdf/pdf-replacer.tsx index d83b79ce5..75f5485c1 100644 --- a/src/components/markdown-renderer/replace-components/pdf/pdf-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/pdf/pdf-replacer.tsx @@ -1,5 +1,8 @@ import { DomElement } from 'domhandler' +import MarkdownIt from 'markdown-it' +import markdownItRegex from 'markdown-it-regex' import React from 'react' +import { replacePdfShortCode } from './replace-pdf-short-code' import { getAttributesFromHedgeDocTag } from '../utils' import { ComponentReplacer } from '../ComponentReplacer' import { PdfFrame } from './pdf-frame' @@ -16,4 +19,8 @@ export class PdfReplacer extends ComponentReplacer { return } } + + public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => { + markdownItRegex(markdownIt, replacePdfShortCode) + } } diff --git a/src/components/markdown-renderer/regex-plugins/replace-pdf-short-code.ts b/src/components/markdown-renderer/replace-components/pdf/replace-pdf-short-code.ts similarity index 78% rename from src/components/markdown-renderer/regex-plugins/replace-pdf-short-code.ts rename to src/components/markdown-renderer/replace-components/pdf/replace-pdf-short-code.ts index a851feb66..4fa010ed2 100644 --- a/src/components/markdown-renderer/regex-plugins/replace-pdf-short-code.ts +++ b/src/components/markdown-renderer/replace-components/pdf/replace-pdf-short-code.ts @@ -1,4 +1,4 @@ -import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' +import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' export const replacePdfShortCode: RegexOptions = { name: 'pdf-short-code', diff --git a/src/components/markdown-renderer/regex-plugins/replace-legacy-vimeo-short-code.ts b/src/components/markdown-renderer/replace-components/vimeo/replace-legacy-vimeo-short-code.ts similarity index 79% rename from src/components/markdown-renderer/regex-plugins/replace-legacy-vimeo-short-code.ts rename to src/components/markdown-renderer/replace-components/vimeo/replace-legacy-vimeo-short-code.ts index 9c069d6b8..9b6aaddd1 100644 --- a/src/components/markdown-renderer/regex-plugins/replace-legacy-vimeo-short-code.ts +++ b/src/components/markdown-renderer/replace-components/vimeo/replace-legacy-vimeo-short-code.ts @@ -1,4 +1,4 @@ -import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' +import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' export const replaceLegacyVimeoShortCode: RegexOptions = { name: 'legacy-vimeo-short-code', diff --git a/src/components/markdown-renderer/regex-plugins/replace-vimeo-link.ts b/src/components/markdown-renderer/replace-components/vimeo/replace-vimeo-link.ts similarity index 89% rename from src/components/markdown-renderer/regex-plugins/replace-vimeo-link.ts rename to src/components/markdown-renderer/replace-components/vimeo/replace-vimeo-link.ts index db363d294..ede009f71 100644 --- a/src/components/markdown-renderer/regex-plugins/replace-vimeo-link.ts +++ b/src/components/markdown-renderer/replace-components/vimeo/replace-vimeo-link.ts @@ -1,4 +1,4 @@ -import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' +import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' const protocolRegex = /(?:http(?:s)?:\/\/)?/ const domainRegex = /(?:player\.)?(?:vimeo\.com\/)(?:(?:channels|album|ondemand|groups)\/\w+\/)?(?:video\/)?/ diff --git a/src/components/markdown-renderer/replace-components/vimeo/vimeo-replacer.tsx b/src/components/markdown-renderer/replace-components/vimeo/vimeo-replacer.tsx index 154d430ce..0734b1d66 100644 --- a/src/components/markdown-renderer/replace-components/vimeo/vimeo-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/vimeo/vimeo-replacer.tsx @@ -1,7 +1,11 @@ import { DomElement } from 'domhandler' +import MarkdownIt from 'markdown-it' +import markdownItRegex from 'markdown-it-regex' import React from 'react' -import { getAttributesFromHedgeDocTag } from '../utils' import { ComponentReplacer } from '../ComponentReplacer' +import { getAttributesFromHedgeDocTag } from '../utils' +import { replaceLegacyVimeoShortCode } from './replace-legacy-vimeo-short-code' +import { replaceVimeoLink } from './replace-vimeo-link' import { VimeoFrame } from './vimeo-frame' export class VimeoReplacer extends ComponentReplacer { @@ -16,4 +20,9 @@ export class VimeoReplacer extends ComponentReplacer { return } } + + public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => { + markdownItRegex(markdownIt, replaceVimeoLink) + markdownItRegex(markdownIt, replaceLegacyVimeoShortCode) + } } diff --git a/src/components/markdown-renderer/regex-plugins/replace-legacy-youtube-short-code.ts b/src/components/markdown-renderer/replace-components/youtube/replace-legacy-youtube-short-code.ts similarity index 80% rename from src/components/markdown-renderer/regex-plugins/replace-legacy-youtube-short-code.ts rename to src/components/markdown-renderer/replace-components/youtube/replace-legacy-youtube-short-code.ts index 87aa345b1..e0f0d1e99 100644 --- a/src/components/markdown-renderer/regex-plugins/replace-legacy-youtube-short-code.ts +++ b/src/components/markdown-renderer/replace-components/youtube/replace-legacy-youtube-short-code.ts @@ -1,4 +1,4 @@ -import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' +import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' export const replaceLegacyYoutubeShortCode: RegexOptions = { name: 'legacy-youtube-short-code', diff --git a/src/components/markdown-renderer/regex-plugins/replace-youtube-link.ts b/src/components/markdown-renderer/replace-components/youtube/replace-youtube-link.ts similarity index 90% rename from src/components/markdown-renderer/regex-plugins/replace-youtube-link.ts rename to src/components/markdown-renderer/replace-components/youtube/replace-youtube-link.ts index 1b509e2da..b80de0953 100644 --- a/src/components/markdown-renderer/regex-plugins/replace-youtube-link.ts +++ b/src/components/markdown-renderer/replace-components/youtube/replace-youtube-link.ts @@ -1,4 +1,4 @@ -import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' +import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' const protocolRegex = /(?:http(?:s)?:\/\/)?/ const subdomainRegex = /(?:www.)?/ diff --git a/src/components/markdown-renderer/replace-components/youtube/youtube-replacer.tsx b/src/components/markdown-renderer/replace-components/youtube/youtube-replacer.tsx index 4528f05e3..16f8d847f 100644 --- a/src/components/markdown-renderer/replace-components/youtube/youtube-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/youtube/youtube-replacer.tsx @@ -1,7 +1,11 @@ import { DomElement } from 'domhandler' +import MarkdownIt from 'markdown-it' +import markdownItRegex from 'markdown-it-regex' import React from 'react' -import { getAttributesFromHedgeDocTag } from '../utils' import { ComponentReplacer } from '../ComponentReplacer' +import { getAttributesFromHedgeDocTag } from '../utils' +import { replaceLegacyYoutubeShortCode } from './replace-legacy-youtube-short-code' +import { replaceYouTubeLink } from './replace-youtube-link' import { YouTubeFrame } from './youtube-frame' export class YoutubeReplacer extends ComponentReplacer { @@ -16,4 +20,9 @@ export class YoutubeReplacer extends ComponentReplacer { return } } + + public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => { + markdownItRegex(markdownIt, replaceYouTubeLink) + markdownItRegex(markdownIt, replaceLegacyYoutubeShortCode) + } } 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 ae77a5d15..50fba00bb 100644 --- a/src/components/markdown-renderer/utils/calculate-line-marker-positions.ts +++ b/src/components/markdown-renderer/utils/calculate-line-marker-positions.ts @@ -2,7 +2,7 @@ import equal from 'fast-deep-equal' import { RefObject, useCallback, useEffect, useRef } from 'react' import useResizeObserver from 'use-resize-observer' import { LineMarkerPosition } from '../types' -import { LineMarkers } from '../markdown-it-plugins/line-number-marker' +import { LineMarkers } from '../replace-components/linemarker/line-number-marker' export const calculateLineMarkerPositions = (documentElement: HTMLDivElement, currentLineMarkers: LineMarkers[], offset?: number): LineMarkerPosition[] => { const lineMarkers = currentLineMarkers diff --git a/src/external-types/markdown-it-front-matter/index.d.ts b/src/external-types/markdown-it-front-matter/index.d.ts index ab9c8f145..e022c97db 100644 --- a/src/external-types/markdown-it-front-matter/index.d.ts +++ b/src/external-types/markdown-it-front-matter/index.d.ts @@ -1,5 +1,7 @@ + declare module 'markdown-it-front-matter' { import MarkdownIt from 'markdown-it/lib' - const markdownItFrontMatter: MarkdownIt.PluginSimple + export type FrontMatterPluginOptions = (rawMeta: string) => void + const markdownItFrontMatter: MarkdownIt.PluginWithOptions export = markdownItFrontMatter }