- null}
- onTocChange={() => false}
- onMetaDataChange={() => false}
- onFirstHeadingChange={() => false}
- onLineMarkerPositionChanged={() => false}
+ onConfigureMarkdownIt={markdownItPlugins}
/>
|
diff --git a/src/components/editor/app-bar/help-button/help-button.tsx b/src/components/editor/app-bar/help-button/help-button.tsx
index 148280d99..fed9ef53c 100644
--- a/src/components/editor/app-bar/help-button/help-button.tsx
+++ b/src/components/editor/app-bar/help-button/help-button.tsx
@@ -6,7 +6,7 @@ import { Cheatsheet } from './cheatsheet'
import { Links } from './links'
import { Shortcut } from './shortcuts'
-enum HelpTabStatus {
+export enum HelpTabStatus {
Cheatsheet='cheatsheet.title',
Shortcuts='shortcuts.title',
Links='links.title'
diff --git a/src/components/editor/document-renderer-pane/document-render-pane.tsx b/src/components/editor/document-renderer-pane/document-render-pane.tsx
index 71557a74e..6fdd778ca 100644
--- a/src/components/editor/document-renderer-pane/document-render-pane.tsx
+++ b/src/components/editor/document-renderer-pane/document-render-pane.tsx
@@ -4,7 +4,8 @@ import useResizeObserver from 'use-resize-observer'
import { TocAst } from '../../../external-types/markdown-it-toc-done-right/interface'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { ShowIf } from '../../common/show-if/show-if'
-import { LineMarkerPosition, MarkdownRenderer } from '../../markdown-renderer/markdown-renderer'
+import { LineMarkerPosition } from '../../markdown-renderer/types'
+import { FullMarkdownRenderer } from '../../markdown-renderer/full-markdown-renderer'
import { ScrollProps, ScrollState } from '../scroll/scroll-props'
import { findLineMarks } from '../scroll/utils'
import { TableOfContents } from '../table-of-contents/table-of-contents'
@@ -108,16 +109,18 @@ export const DocumentRenderPane: React.FC
- setTocAst(tocAst)}
- wide={wide}
- />
+
+ setTocAst(tocAst)}
+ wide={wide}
+ />
+
= 1280 && !!tocAst}>
diff --git a/src/components/editor/scroll/utils.ts b/src/components/editor/scroll/utils.ts
index 6fa271998..4f2a039e4 100644
--- a/src/components/editor/scroll/utils.ts
+++ b/src/components/editor/scroll/utils.ts
@@ -1,4 +1,5 @@
-import { LineMarkerPosition } from '../../markdown-renderer/markdown-renderer'
+import { LineMarkerPosition } from '../../markdown-renderer/types'
+
export const findLineMarks = (lineMarks: LineMarkerPosition[], lineNumber: number): { lastMarkBefore: LineMarkerPosition | undefined, firstMarkAfter: LineMarkerPosition | undefined } => {
let lastMarkBefore
let firstMarkAfter
diff --git a/src/components/markdown-renderer/basic-markdown-renderer.tsx b/src/components/markdown-renderer/basic-markdown-renderer.tsx
new file mode 100644
index 000000000..34c0c1198
--- /dev/null
+++ b/src/components/markdown-renderer/basic-markdown-renderer.tsx
@@ -0,0 +1,106 @@
+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'
+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'
+import { buildTransformer } from './utils/html-react-transformer'
+import { calculateNewLineNumberMapping } from './utils/line-number-mapping'
+
+export interface BasicMarkdownRendererProps {
+ componentReplacers?: ComponentReplacer[],
+ onConfigureMarkdownIt?: (md: MarkdownIt) => void,
+ documentReference?: RefObject
+ onBeforeRendering?: () => void
+}
+
+export const BasicMarkdownRenderer: React.FC = ({
+ className,
+ content,
+ wide,
+ componentReplacers,
+ onConfigureMarkdownIt,
+ documentReference,
+ onBeforeRendering
+}) => {
+ 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)
+
+ const markdownReactDom: ReactElement[] = useMemo(() => {
+ if (onBeforeRendering) {
+ onBeforeRendering()
+ }
+ const trimmedContent = content.substr(0, maxLength)
+ const html: string = markdownIt.render(trimmedContent)
+ const contentLines = trimmedContent.split('\n')
+ const { lines: newLines, lastUsedLineId: newLastUsedLineId } = calculateNewLineNumberMapping(contentLines, oldMarkdownLineKeys.current ?? [], lastUsedLineId.current)
+ oldMarkdownLineKeys.current = newLines
+ lastUsedLineId.current = newLastUsedLineId
+ const transformer = componentReplacers ? buildTransformer(newLines, componentReplacers) : undefined
+ return ReactHtmlParser(html, { transform: transformer })
+ }, [onBeforeRendering, content, maxLength, markdownIt, componentReplacers])
+
+ return (
+
+ maxLength}>
+
+
+
+
+
+ {markdownReactDom}
+
+
+ )
+}
diff --git a/src/components/markdown-renderer/full-markdown-renderer.tsx b/src/components/markdown-renderer/full-markdown-renderer.tsx
new file mode 100644
index 000000000..09583cafe
--- /dev/null
+++ b/src/components/markdown-renderer/full-markdown-renderer.tsx
@@ -0,0 +1,227 @@
+import markdownItTaskLists from '@hedgedoc/markdown-it-task-lists'
+import yaml from 'js-yaml'
+import MarkdownIt from 'markdown-it'
+import anchor from 'markdown-it-anchor'
+import markdownItContainer from 'markdown-it-container'
+import frontmatter from 'markdown-it-front-matter'
+import mathJax from 'markdown-it-mathjax'
+import plantuml from 'markdown-it-plantuml'
+import markdownItRegex from 'markdown-it-regex'
+import toc from 'markdown-it-toc-done-right'
+import React, { useCallback, useMemo, useRef, useState } from 'react'
+import { Alert } from 'react-bootstrap'
+import { Trans } from 'react-i18next'
+import { useSelector } from 'react-redux'
+import { TocAst } from '../../external-types/markdown-it-toc-done-right/interface'
+import { ApplicationState } from '../../redux'
+import { InternalLink } from '../common/links/internal-link'
+import { ShowIf } from '../common/show-if/show-if'
+import { slugify } from '../editor/table-of-contents/table-of-contents'
+import { RawYAMLMetadata, YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
+import { BasicMarkdownRenderer } from './basic-markdown-renderer'
+import { createRenderContainer, validAlertLevels } from './markdown-it-plugins/alert-container'
+import { firstHeaderExtractor } from './markdown-it-plugins/first-header-extractor'
+import { highlightedCode } from './markdown-it-plugins/highlighted-code'
+import { LineMarkers, lineNumberMarker } from './markdown-it-plugins/line-number-marker'
+import { plantumlError } from './markdown-it-plugins/plantuml-error'
+import { replaceAsciinemaLink } from './regex-plugins/replace-asciinema-link'
+import { replaceGistLink } from './regex-plugins/replace-gist-link'
+import { replaceLegacyGistShortCode } from './regex-plugins/replace-legacy-gist-short-code'
+import { replaceLegacySlideshareShortCode } from './regex-plugins/replace-legacy-slideshare-short-code'
+import { replaceLegacySpeakerdeckShortCode } from './regex-plugins/replace-legacy-speakerdeck-short-code'
+import { replaceLegacyVimeoShortCode } from './regex-plugins/replace-legacy-vimeo-short-code'
+import { replaceLegacyYoutubeShortCode } from './regex-plugins/replace-legacy-youtube-short-code'
+import { replacePdfShortCode } from './regex-plugins/replace-pdf-short-code'
+import { replaceQuoteExtraAuthor } from './regex-plugins/replace-quote-extra-author'
+import { replaceQuoteExtraColor } from './regex-plugins/replace-quote-extra-color'
+import { replaceQuoteExtraTime } from './regex-plugins/replace-quote-extra-time'
+import { replaceVimeoLink } from './regex-plugins/replace-vimeo-link'
+import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
+import { AbcReplacer } from './replace-components/abc/abc-replacer'
+import { AsciinemaReplacer } from './replace-components/asciinema/asciinema-replacer'
+import { CsvReplacer } from './replace-components/csv/csv-replacer'
+import { FlowchartReplacer } from './replace-components/flow/flowchart-replacer'
+import { GistReplacer } from './replace-components/gist/gist-replacer'
+import { 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 { 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 { VimeoReplacer } from './replace-components/vimeo/vimeo-replacer'
+import { YoutubeReplacer } from './replace-components/youtube/youtube-replacer'
+import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
+import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
+import { usePostMetaDataOnChange } from './utils/use-post-meta-data-on-change'
+import { usePostTocAstOnChange } from './utils/use-post-toc-ast-on-change'
+
+export interface FullMarkdownRendererProps {
+ onFirstHeadingChange?: (firstHeading: string | undefined) => void
+ onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
+ onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void
+ onTaskCheckedChange: (lineInMarkdown: number, checked: boolean) => void
+ onTocChange?: (ast: TocAst) => void
+}
+
+export const FullMarkdownRenderer: React.FC = ({
+ onFirstHeadingChange,
+ onLineMarkerPositionChanged,
+ onMetaDataChange,
+ onTaskCheckedChange,
+ onTocChange,
+ content,
+ className,
+ wide
+}) => {
+ 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 HighlightedCodeReplacer(),
+ new QuoteOptionsReplacer(),
+ new KatexReplacer(),
+ new TaskListReplacer(onTaskCheckedChange)
+ ]
+ }, [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)
+
+ const tocAst = useRef()
+ usePostTocAstOnChange(tocAst, onTocChange)
+
+ const configureMarkdownIt = useCallback((md: MarkdownIt): void => {
+ if (onFirstHeadingChange) {
+ md.use(firstHeaderExtractor(), {
+ firstHeaderFound: (firstHeader: string | undefined) => {
+ firstHeadingRef.current = firstHeader
+ }
+ })
+ }
+
+ 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
+ },
+ slugify: slugify
+ })
+ validAlertLevels.forEach(level => {
+ md.use(markdownItContainer, level, { render: createRenderContainer(level) })
+ })
+ md.use(lineNumberMarker(), {
+ postLineMarkers: (lineMarkers) => {
+ currentLineMarkers.current = lineMarkers
+ }
+ })
+ }, [onFirstHeadingChange, onMetaDataChange, plantumlServer])
+
+ const clearMetadata = useCallback(() => {
+ rawMetaRef.current = undefined
+ }, [])
+
+ return (
+
+ )
+}
diff --git a/src/components/markdown-renderer/markdown-it-plugins/emoji/mapping.ts b/src/components/markdown-renderer/markdown-it-plugins/emoji/mapping.ts
new file mode 100644
index 000000000..79285a6c7
--- /dev/null
+++ b/src/components/markdown-renderer/markdown-it-plugins/emoji/mapping.ts
@@ -0,0 +1,36 @@
+import emojiData from 'emoji-mart/data/twitter.json'
+import { Data } from 'emoji-mart/dist-es/utils/data'
+import { ForkAwesomeIcons } from '../../../editor/editor-pane/tool-bar/emoji-picker/icon-names'
+
+export const markdownItTwitterEmojis = Object.keys((emojiData as unknown as Data).emojis)
+ .reduce((reduceObject, emojiIdentifier) => {
+ const emoji = (emojiData as unknown as Data).emojis[emojiIdentifier]
+ const emojiCodes = emoji.unified ?? emoji.b
+ if (emojiCodes) {
+ reduceObject[emojiIdentifier] = emojiCodes.split('-').map(char => `${char};`).join('')
+ }
+ return reduceObject
+ }, {} as { [key: string]: string })
+
+export const emojiSkinToneModifierMap = [2, 3, 4, 5, 6]
+ .reduce((reduceObject, modifierValue) => {
+ const lightSkinCode = 127995
+ const codepoint = lightSkinCode + (modifierValue - 2)
+ const shortcode = `skin-tone-${modifierValue}`
+ reduceObject[shortcode] = `${codepoint};`
+ return reduceObject
+ }, {} as { [key: string]: string })
+
+export const forkAwesomeIconMap = Object.keys(ForkAwesomeIcons)
+ .reduce((reduceObject, icon) => {
+ const shortcode = `fa-${icon}`
+ // noinspection CheckTagEmptyBody
+ reduceObject[shortcode] = ``
+ return reduceObject
+ }, {} as { [key: string]: string })
+
+export const combinedEmojiData = {
+ ...markdownItTwitterEmojis,
+ ...emojiSkinToneModifierMap,
+ ...forkAwesomeIconMap
+}
diff --git a/src/components/markdown-renderer/markdown-it-plugins/first-header-extractor.ts b/src/components/markdown-renderer/markdown-it-plugins/first-header-extractor.ts
new file mode 100644
index 000000000..96d183f87
--- /dev/null
+++ b/src/components/markdown-renderer/markdown-it-plugins/first-header-extractor.ts
@@ -0,0 +1,26 @@
+import MarkdownIt from 'markdown-it/lib'
+
+export interface FirstHeaderExtractorOptions {
+ firstHeaderFound: (firstHeader: string|undefined) => void
+}
+
+export const firstHeaderExtractor: () => MarkdownIt.PluginWithOptions = () => {
+ return (md, options) => {
+ if (!options?.firstHeaderFound) {
+ return
+ }
+ md.core.ruler.after('normalize', 'extract first L1 heading', (state) => {
+ const lines = state.src.split('\n')
+ const linkAltTextRegex = /!?\[([^\]]*)]\([^)]*\)/
+ for (const line of lines) {
+ if (line.startsWith('# ')) {
+ const headerLine = line.replace('# ', '').replace(linkAltTextRegex, '$1')
+ options.firstHeaderFound(headerLine)
+ return true
+ }
+ }
+ options.firstHeaderFound(undefined)
+ return true
+ })
+ }
+}
diff --git a/src/components/markdown-renderer/markdown-renderer.tsx b/src/components/markdown-renderer/markdown-renderer.tsx
deleted file mode 100644
index 0ebba77c6..000000000
--- a/src/components/markdown-renderer/markdown-renderer.tsx
+++ /dev/null
@@ -1,398 +0,0 @@
-import emojiData from 'emoji-mart/data/twitter.json'
-import { Data } from 'emoji-mart/dist-es/utils/data'
-import equal from 'fast-deep-equal'
-import yaml from 'js-yaml'
-import MarkdownIt from 'markdown-it'
-import abbreviation from 'markdown-it-abbr'
-import anchor from 'markdown-it-anchor'
-import markdownItContainer from 'markdown-it-container'
-import definitionList from 'markdown-it-deflist'
-import emoji from 'markdown-it-emoji'
-import footnote from 'markdown-it-footnote'
-import frontmatter from 'markdown-it-front-matter'
-import imsize from 'markdown-it-imsize'
-import inserted from 'markdown-it-ins'
-import marked from 'markdown-it-mark'
-import mathJax from 'markdown-it-mathjax'
-import plantuml from 'markdown-it-plantuml'
-import markdownItRegex from 'markdown-it-regex'
-import subscript from 'markdown-it-sub'
-import superscript from 'markdown-it-sup'
-import toc from 'markdown-it-toc-done-right'
-import React, { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'
-import markdownItTaskLists from '@hedgedoc/markdown-it-task-lists'
-import { Alert } from 'react-bootstrap'
-import ReactHtmlParser from 'react-html-parser'
-import { Trans } from 'react-i18next'
-import { useSelector } from 'react-redux'
-import useResizeObserver from 'use-resize-observer'
-import { TocAst } from '../../external-types/markdown-it-toc-done-right/interface'
-import { ApplicationState } from '../../redux'
-import { InternalLink } from '../common/links/internal-link'
-import { ShowIf } from '../common/show-if/show-if'
-import { ForkAwesomeIcons } from '../editor/editor-pane/tool-bar/emoji-picker/icon-names'
-import { slugify } from '../editor/table-of-contents/table-of-contents'
-import { RawYAMLMetadata, YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
-import { createRenderContainer, validAlertLevels } from './markdown-it-plugins/alert-container'
-import { highlightedCode } from './markdown-it-plugins/highlighted-code'
-import { LineMarkers, lineNumberMarker } from './markdown-it-plugins/line-number-marker'
-import { linkifyExtra } from './markdown-it-plugins/linkify-extra'
-import { MarkdownItParserDebugger } from './markdown-it-plugins/parser-debugger'
-import { plantumlError } from './markdown-it-plugins/plantuml-error'
-import { replaceAsciinemaLink } from './regex-plugins/replace-asciinema-link'
-import { replaceGistLink } from './regex-plugins/replace-gist-link'
-import { replaceLegacyGistShortCode } from './regex-plugins/replace-legacy-gist-short-code'
-import { replaceLegacySlideshareShortCode } from './regex-plugins/replace-legacy-slideshare-short-code'
-import { replaceLegacySpeakerdeckShortCode } from './regex-plugins/replace-legacy-speakerdeck-short-code'
-import { replaceLegacyVimeoShortCode } from './regex-plugins/replace-legacy-vimeo-short-code'
-import { replaceLegacyYoutubeShortCode } from './regex-plugins/replace-legacy-youtube-short-code'
-import { replacePdfShortCode } from './regex-plugins/replace-pdf-short-code'
-import { replaceQuoteExtraAuthor } from './regex-plugins/replace-quote-extra-author'
-import { replaceQuoteExtraColor } from './regex-plugins/replace-quote-extra-color'
-import { replaceQuoteExtraTime } from './regex-plugins/replace-quote-extra-time'
-import { replaceVimeoLink } from './regex-plugins/replace-vimeo-link'
-import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
-import { buildTransformer, calculateNewLineNumberMapping, LineKeys } from './renderer-utils'
-import { AbcReplacer } from './replace-components/abc/abc-replacer'
-import { AsciinemaReplacer } from './replace-components/asciinema/asciinema-replacer'
-import { LinemarkerReplacer } from './replace-components/linemarker/linemarker-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 { 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 { 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 { VimeoReplacer } from './replace-components/vimeo/vimeo-replacer'
-import { YoutubeReplacer } from './replace-components/youtube/youtube-replacer'
-import './markdown-renderer.scss'
-
-export interface LineMarkerPosition {
- line: number
- position: number
-}
-
-export interface MarkdownRendererProps {
- className?: string
- content: string
- onFirstHeadingChange?: (firstHeading: string | undefined) => void
- onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
- onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void
- onTaskCheckedChange: (lineInMarkdown: number, checked: boolean) => void
- onTocChange?: (ast: TocAst) => void
- wide?: boolean
-}
-
-const markdownItTwitterEmojis = Object.keys((emojiData as unknown as Data).emojis)
- .reduce((reduceObject, emojiIdentifier) => {
- const emoji = (emojiData as unknown as Data).emojis[emojiIdentifier]
- const emojiCodes = emoji.unified ?? emoji.b
- if (emojiCodes) {
- reduceObject[emojiIdentifier] = emojiCodes.split('-').map(char => `${char};`).join('')
- }
- return reduceObject
- }, {} as { [key: string]: string })
-
-const emojiSkinToneModifierMap = [2, 3, 4, 5, 6]
- .reduce((reduceObject, modifierValue) => {
- const lightSkinCode = 127995
- const codepoint = lightSkinCode + (modifierValue - 2)
- const shortcode = `skin-tone-${modifierValue}`
- reduceObject[shortcode] = `${codepoint};`
- return reduceObject
- }, {} as { [key: string]: string })
-
-const forkAwesomeIconMap = Object.keys(ForkAwesomeIcons)
- .reduce((reduceObject, icon) => {
- const shortcode = `fa-${icon}`
- // noinspection CheckTagEmptyBody
- reduceObject[shortcode] = ``
- return reduceObject
- }, {} as { [key: string]: string })
-
-export const MarkdownRenderer: React.FC = ({
- className,
- content,
- onFirstHeadingChange,
- onLineMarkerPositionChanged,
- onMetaDataChange,
- onTaskCheckedChange,
- onTocChange,
- wide
-}) => {
- const [tocAst, setTocAst] = useState()
- const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
- const lastTocAst = useRef()
- const [yamlError, setYamlError] = useState(false)
- const rawMetaRef = useRef()
- const oldMetaRef = useRef()
- const firstHeadingRef = useRef()
- const oldFirstHeadingRef = useRef()
- const documentElement = useRef(null)
- const lastLineMarkerPositions = useRef()
- const currentLineMarkers = useRef()
-
- const calculateLineMarkerPositions = useCallback(() => {
- if (!(documentElement.current && onLineMarkerPositionChanged)) {
- return
- }
- if (currentLineMarkers.current === undefined) {
- return
- }
- const lineMarkers = currentLineMarkers.current
- const children: HTMLCollection = documentElement.current.children
- const lineMarkerPositions:LineMarkerPosition[] = []
-
- Array.from(children).forEach((child, childIndex) => {
- const htmlChild = (child as HTMLElement)
- if (htmlChild.offsetTop === undefined) {
- return
- }
- const currentLineMarker = lineMarkers[childIndex]
- if (currentLineMarker === undefined) {
- return
- }
-
- const lastPosition = lineMarkerPositions[lineMarkerPositions.length - 1]
- if (!lastPosition || lastPosition.line !== currentLineMarker.startLine) {
- lineMarkerPositions.push({
- line: currentLineMarker.startLine,
- position: htmlChild.offsetTop
- })
- }
-
- lineMarkerPositions.push({
- line: currentLineMarker.endLine,
- position: htmlChild.offsetTop + htmlChild.offsetHeight
- })
- })
-
- if (!equal(lineMarkerPositions, lastLineMarkerPositions.current)) {
- lastLineMarkerPositions.current = lineMarkerPositions
- onLineMarkerPositionChanged(lineMarkerPositions)
- }
- }, [onLineMarkerPositionChanged])
-
- useEffect(() => {
- calculateLineMarkerPositions()
- }, [calculateLineMarkerPositions, content])
-
- useResizeObserver({
- ref: documentElement,
- onResize: () => calculateLineMarkerPositions()
- })
-
- useEffect(() => {
- if (onMetaDataChange && !equal(oldMetaRef.current, rawMetaRef.current)) {
- if (rawMetaRef.current) {
- const newMetaData = new YAMLMetaData(rawMetaRef.current)
- onMetaDataChange(newMetaData)
- } else {
- onMetaDataChange(undefined)
- }
- oldMetaRef.current = rawMetaRef.current
- }
- if (onFirstHeadingChange && !equal(firstHeadingRef.current, oldFirstHeadingRef.current)) {
- onFirstHeadingChange(firstHeadingRef.current || undefined)
- oldFirstHeadingRef.current = firstHeadingRef.current
- }
- })
-
- const plantumlServer = useSelector((state: ApplicationState) => state.config.plantumlServer)
-
- const markdownIt = useMemo(() => {
- const md = new MarkdownIt('default', {
- html: true,
- breaks: true,
- langPrefix: '',
- typographer: true
- })
- if (onFirstHeadingChange) {
- md.core.ruler.after('normalize', 'extract first L1 heading', (state) => {
- const lines = state.src.split('\n')
- const linkAltTextRegex = /!?\[([^\]]*)]\([^)]*\)/
- for (const line of lines) {
- if (line.startsWith('# ')) {
- firstHeadingRef.current = line.replace('# ', '').replace(linkAltTextRegex, '$1')
- return true
- }
- }
- firstHeadingRef.current = undefined
- return true
- })
- }
- 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)
- }
- md.use(emoji, {
- defs: {
- ...markdownItTwitterEmojis,
- ...emojiSkinToneModifierMap,
- ...forkAwesomeIconMap
- }
- })
- md.use(abbreviation)
- md.use(definitionList)
- md.use(subscript)
- md.use(superscript)
- md.use(inserted)
- md.use(marked)
- md.use(footnote)
- 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)
- }
- })
- }
- md.use(imsize)
- // 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 => {
- setTocAst(ast)
- },
- slugify: slugify
- })
- md.use(linkifyExtra)
- validAlertLevels.forEach(level => {
- md.use(markdownItContainer, level, { render: createRenderContainer(level) })
- })
- md.use(lineNumberMarker(), {
- postLineMarkers: (lineMarkers) => {
- currentLineMarkers.current = lineMarkers
- }
- })
- if (process.env.NODE_ENV !== 'production') {
- md.use(MarkdownItParserDebugger)
- }
-
- return md
- }, [onMetaDataChange, onFirstHeadingChange, plantumlServer])
-
- useEffect(() => {
- if (onTocChange && tocAst && !equal(tocAst, lastTocAst.current)) {
- lastTocAst.current = tocAst
- onTocChange(tocAst)
- }
- }, [tocAst, onTocChange])
-
- const oldMarkdownLineKeys = useRef()
- const lastUsedLineId = useRef(0)
-
- const markdownReactDom: ReactElement[] = useMemo(() => {
- const allReplacers: ComponentReplacer[] = [
- 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 HighlightedCodeReplacer(),
- new QuoteOptionsReplacer(),
- new KatexReplacer(),
- new TaskListReplacer(onTaskCheckedChange)
- ]
- if (onMetaDataChange) {
- // This is used if the front-matter callback is never called, because the user deleted everything regarding metadata from the document
- rawMetaRef.current = undefined
- }
- const trimmedContent = content.substr(0, maxLength)
- const html: string = markdownIt.render(trimmedContent)
- const contentLines = trimmedContent.split('\n')
- const { lines: newLines, lastUsedLineId: newLastUsedLineId } = calculateNewLineNumberMapping(contentLines, oldMarkdownLineKeys.current ?? [], lastUsedLineId.current)
- oldMarkdownLineKeys.current = newLines
- lastUsedLineId.current = newLastUsedLineId
- return ReactHtmlParser(html, { transform: buildTransformer(newLines, allReplacers) })
- }, [content, markdownIt, onMetaDataChange, onTaskCheckedChange, maxLength])
-
- return (
-
-
-
-
-
-
-
-
-
- maxLength}>
-
-
-
-
-
- {markdownReactDom}
-
-
-
- )
-}
diff --git a/src/components/markdown-renderer/types.d.ts b/src/components/markdown-renderer/types.d.ts
new file mode 100644
index 000000000..1849263cc
--- /dev/null
+++ b/src/components/markdown-renderer/types.d.ts
@@ -0,0 +1,15 @@
+export interface LineKeys {
+ line: string,
+ id: number
+}
+
+export interface LineMarkerPosition {
+ line: number
+ position: number
+}
+
+export interface AdditionalMarkdownRendererProps {
+ className?: string,
+ content: string,
+ wide?: boolean,
+}
diff --git a/src/components/markdown-renderer/utils/calculate-line-marker-positions.ts b/src/components/markdown-renderer/utils/calculate-line-marker-positions.ts
new file mode 100644
index 000000000..ae77a5d15
--- /dev/null
+++ b/src/components/markdown-renderer/utils/calculate-line-marker-positions.ts
@@ -0,0 +1,63 @@
+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'
+
+export const calculateLineMarkerPositions = (documentElement: HTMLDivElement, currentLineMarkers: LineMarkers[], offset?: number): LineMarkerPosition[] => {
+ const lineMarkers = currentLineMarkers
+ const children: HTMLCollection = documentElement.children
+ const lineMarkerPositions:LineMarkerPosition[] = []
+
+ Array.from(children).forEach((child, childIndex) => {
+ const htmlChild = (child as HTMLElement)
+ if (htmlChild.offsetTop === undefined) {
+ return
+ }
+ const currentLineMarker = lineMarkers[childIndex]
+ if (currentLineMarker === undefined) {
+ return
+ }
+
+ const lastPosition = lineMarkerPositions[lineMarkerPositions.length - 1]
+ if (!lastPosition || lastPosition.line !== currentLineMarker.startLine) {
+ lineMarkerPositions.push({
+ line: currentLineMarker.startLine,
+ position: htmlChild.offsetTop + (offset ?? 0)
+ })
+ }
+
+ lineMarkerPositions.push({
+ line: currentLineMarker.endLine,
+ position: htmlChild.offsetTop + htmlChild.offsetHeight + (offset ?? 0)
+ })
+ })
+
+ return lineMarkerPositions
+}
+
+export const useCalculateLineMarkerPosition = (documentElement: RefObject, lineMarkers?: LineMarkers[], onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void, offset?: number) : void => {
+ const lastLineMarkerPositions = useRef()
+
+ const calculateNewLineMarkerPositions = useCallback(() => {
+ if (!documentElement.current || !onLineMarkerPositionChanged || !lineMarkers) {
+ return
+ }
+
+ const newLines = calculateLineMarkerPositions(documentElement.current, lineMarkers, offset)
+
+ if (!equal(newLines, lastLineMarkerPositions)) {
+ lastLineMarkerPositions.current = newLines
+ onLineMarkerPositionChanged(newLines)
+ }
+ }, [documentElement, lineMarkers, offset, onLineMarkerPositionChanged])
+
+ useEffect(() => {
+ calculateNewLineMarkerPositions()
+ }, [calculateNewLineMarkerPositions])
+
+ useResizeObserver({
+ ref: documentElement,
+ onResize: () => calculateNewLineMarkerPositions()
+ })
+}
diff --git a/src/components/markdown-renderer/renderer-utils.tsx b/src/components/markdown-renderer/utils/html-react-transformer.tsx
similarity index 66%
rename from src/components/markdown-renderer/renderer-utils.tsx
rename to src/components/markdown-renderer/utils/html-react-transformer.tsx
index 99ea0cdb2..225619557 100644
--- a/src/components/markdown-renderer/renderer-utils.tsx
+++ b/src/components/markdown-renderer/utils/html-react-transformer.tsx
@@ -1,51 +1,17 @@
-import { diffArrays } from 'diff'
import { DomElement } from 'domhandler'
import React, { Fragment, ReactElement } from 'react'
import { convertNodeToElement, Transform } from 'react-html-parser'
import {
ComponentReplacer,
SubNodeTransform
-} from './replace-components/ComponentReplacer'
+} from '../replace-components/ComponentReplacer'
+import { LineKeys } from '../types'
export interface TextDifferenceResult {
lines: LineKeys[],
lastUsedLineId: number
}
-export interface LineKeys {
- line: string,
- id: number
-}
-
-export const calculateNewLineNumberMapping = (newMarkdownLines: string[], oldLineKeys: LineKeys[], lastUsedLineId: number): TextDifferenceResult => {
- const lineDifferences = diffArrays(newMarkdownLines, oldLineKeys, {
- comparator: (left:string|LineKeys, right:string|LineKeys) => {
- const leftLine = (left as LineKeys).line ?? (left as string)
- const rightLine = (right as LineKeys).line ?? (right as string)
- return leftLine === rightLine
- }
- })
-
- const newLines: LineKeys[] = []
-
- lineDifferences
- .filter((change) => change.added === undefined || !change.added)
- .forEach((value) => {
- if (value.removed) {
- (value.value as string[])
- .forEach(line => {
- lastUsedLineId += 1
- newLines.push({ line: line, id: lastUsedLineId })
- })
- } else {
- (value.value as LineKeys[])
- .forEach((line) => newLines.push(line))
- }
- })
-
- return { lines: newLines, lastUsedLineId: lastUsedLineId }
-}
-
export const calculateKeyFromLineMarker = (node: DomElement, lineKeys?: LineKeys[]): number|undefined => {
if (!node.attribs || lineKeys === undefined) {
return
diff --git a/src/components/markdown-renderer/utils/line-number-mapping.ts b/src/components/markdown-renderer/utils/line-number-mapping.ts
new file mode 100644
index 000000000..6a7908835
--- /dev/null
+++ b/src/components/markdown-renderer/utils/line-number-mapping.ts
@@ -0,0 +1,32 @@
+import { diffArrays } from 'diff'
+import { TextDifferenceResult } from './html-react-transformer'
+import { LineKeys } from '../types'
+
+export const calculateNewLineNumberMapping = (newMarkdownLines: string[], oldLineKeys: LineKeys[], lastUsedLineId: number): TextDifferenceResult => {
+ const lineDifferences = diffArrays(newMarkdownLines, oldLineKeys, {
+ comparator: (left:string|LineKeys, right:string|LineKeys) => {
+ const leftLine = (left as LineKeys).line ?? (left as string)
+ const rightLine = (right as LineKeys).line ?? (right as string)
+ return leftLine === rightLine
+ }
+ })
+
+ const newLines: LineKeys[] = []
+
+ lineDifferences
+ .filter((change) => change.added === undefined || !change.added)
+ .forEach((value) => {
+ if (value.removed) {
+ (value.value as string[])
+ .forEach(line => {
+ lastUsedLineId += 1
+ newLines.push({ line: line, id: lastUsedLineId })
+ })
+ } else {
+ (value.value as LineKeys[])
+ .forEach((line) => newLines.push(line))
+ }
+ })
+
+ return { lines: newLines, lastUsedLineId: lastUsedLineId }
+}
diff --git a/src/components/markdown-renderer/utils/use-post-meta-data-on-change.ts b/src/components/markdown-renderer/utils/use-post-meta-data-on-change.ts
new file mode 100644
index 000000000..b7ad66f1d
--- /dev/null
+++ b/src/components/markdown-renderer/utils/use-post-meta-data-on-change.ts
@@ -0,0 +1,29 @@
+import equal from 'fast-deep-equal'
+import { useEffect, useRef } from 'react'
+import { RawYAMLMetadata, YAMLMetaData } from '../../editor/yaml-metadata/yaml-metadata'
+
+export const usePostMetaDataOnChange = (
+ rawMetaRef: RawYAMLMetadata|undefined,
+ firstHeadingRef: string|undefined,
+ onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void,
+ onFirstHeadingChange?: (firstHeading: string | undefined) => void
+): void => {
+ const oldMetaRef = useRef()
+ const oldFirstHeadingRef = useRef()
+
+ useEffect(() => {
+ if (onMetaDataChange && !equal(oldMetaRef.current, rawMetaRef)) {
+ if (rawMetaRef) {
+ const newMetaData = new YAMLMetaData(rawMetaRef)
+ onMetaDataChange(newMetaData)
+ } else {
+ onMetaDataChange(undefined)
+ }
+ oldMetaRef.current = rawMetaRef
+ }
+ if (onFirstHeadingChange && !equal(firstHeadingRef, oldFirstHeadingRef.current)) {
+ onFirstHeadingChange(firstHeadingRef || undefined)
+ oldFirstHeadingRef.current = firstHeadingRef
+ }
+ })
+}
diff --git a/src/components/markdown-renderer/utils/use-post-toc-ast-on-change.ts b/src/components/markdown-renderer/utils/use-post-toc-ast-on-change.ts
new file mode 100644
index 000000000..2fd494802
--- /dev/null
+++ b/src/components/markdown-renderer/utils/use-post-toc-ast-on-change.ts
@@ -0,0 +1,13 @@
+import equal from 'fast-deep-equal'
+import { RefObject, useEffect, useRef } from 'react'
+import { TocAst } from '../../../external-types/markdown-it-toc-done-right/interface'
+
+export const usePostTocAstOnChange = (tocAst: RefObject, onTocChange?: (ast: TocAst) => void): void => {
+ const lastTocAst = useRef()
+ useEffect(() => {
+ if (onTocChange && tocAst.current && !equal(tocAst, lastTocAst.current)) {
+ lastTocAst.current = tocAst.current
+ onTocChange(tocAst.current)
+ }
+ }, [onTocChange, tocAst])
+}
|