mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-14 07:04:45 -04:00
Merge basic and full markdown renderer (#1040)
The original idea of the basic-markdown-renderer and the full-markdown-renderer was to reduce the complexity. The basic markdown renderer should just render markdown code and the full markdown renderer should implement all the special hedgedoc stuff like the embeddings. While developing other aspects of the software I noticed, that it makes more sense to split the markdown-renderer by the view and not by the features. E.g.: The slide markdown renderer must translate <hr> into <sections> for the slides and the document markdown renderer must provide precise scroll positions. But both need e.g. the ability to show a youtube video. Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
364aec1318
commit
d9292e4db0
51 changed files with 777 additions and 979 deletions
|
@ -4,45 +4,112 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import React, { RefObject, useMemo } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { ApplicationState } from '../../redux'
|
||||
import React, { Ref, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert'
|
||||
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
|
||||
import './markdown-renderer.scss'
|
||||
import { ComponentReplacer } from './replace-components/ComponentReplacer'
|
||||
import { AdditionalMarkdownRendererProps } from './types'
|
||||
import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
|
||||
import { useComponentReplacers } from './hooks/use-component-replacers'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { NoteFrontmatter, RawNoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter'
|
||||
import { LineMarkers } from './replace-components/linemarker/line-number-marker'
|
||||
import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
|
||||
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
|
||||
import { TocAst } from 'markdown-it-toc-done-right'
|
||||
import { useOnRefChange } from './hooks/use-on-ref-change'
|
||||
import { BasicMarkdownItConfigurator } from './markdown-it-configurator/BasicMarkdownItConfigurator'
|
||||
import { ImageClickHandler } from './replace-components/image/image-replacer'
|
||||
import { InvalidYamlAlert } from './invalid-yaml-alert'
|
||||
import { useTrimmedContent } from './hooks/use-trimmed-content'
|
||||
|
||||
export interface BasicMarkdownRendererProps {
|
||||
componentReplacers?: () => ComponentReplacer[],
|
||||
markdownIt: MarkdownIt,
|
||||
documentReference?: RefObject<HTMLDivElement>
|
||||
additionalReplacers?: () => ComponentReplacer[],
|
||||
onBeforeRendering?: () => void
|
||||
onAfterRendering?: () => void
|
||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||
onTocChange?: (ast?: TocAst) => void
|
||||
baseUrl?: string
|
||||
onImageClick?: ImageClickHandler
|
||||
outerContainerRef?: Ref<HTMLDivElement>
|
||||
useAlternativeBreaks?: boolean
|
||||
}
|
||||
|
||||
export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & AdditionalMarkdownRendererProps> = (
|
||||
{
|
||||
className,
|
||||
content,
|
||||
componentReplacers,
|
||||
markdownIt,
|
||||
documentReference,
|
||||
additionalReplacers,
|
||||
onBeforeRendering,
|
||||
onAfterRendering
|
||||
onAfterRendering,
|
||||
onFirstHeadingChange,
|
||||
onLineMarkerPositionChanged,
|
||||
onFrontmatterChange,
|
||||
onTaskCheckedChange,
|
||||
onTocChange,
|
||||
baseUrl,
|
||||
onImageClick,
|
||||
outerContainerRef,
|
||||
useAlternativeBreaks
|
||||
}) => {
|
||||
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
||||
const trimmedContent = useMemo(() => content.length > maxLength ? content.substr(0, maxLength) : content, [content,
|
||||
maxLength])
|
||||
const markdownReactDom = useConvertMarkdownToReactDom(trimmedContent, markdownIt, componentReplacers, onBeforeRendering, onAfterRendering)
|
||||
const rawMetaRef = useRef<RawNoteFrontmatter>()
|
||||
const markdownBodyRef = useRef<HTMLDivElement>(null)
|
||||
const currentLineMarkers = useRef<LineMarkers[]>()
|
||||
const hasNewYamlError = useRef(false)
|
||||
const tocAst = useRef<TocAst>()
|
||||
const [showYamlError, setShowYamlError] = useState(false)
|
||||
const [trimmedContent, contentExceedsLimit] = useTrimmedContent(content)
|
||||
|
||||
const markdownIt = useMemo(() =>
|
||||
new BasicMarkdownItConfigurator({
|
||||
useFrontmatter: !!onFrontmatterChange,
|
||||
onParseError: errorState => hasNewYamlError.current = errorState,
|
||||
onRawMetaChange: rawMeta => rawMetaRef.current = rawMeta,
|
||||
onToc: toc => tocAst.current = toc,
|
||||
onLineMarkers: onLineMarkerPositionChanged === undefined ? undefined
|
||||
: lineMarkers => currentLineMarkers.current = lineMarkers,
|
||||
useAlternativeBreaks
|
||||
}).buildConfiguredMarkdownIt(), [onFrontmatterChange, onLineMarkerPositionChanged, useAlternativeBreaks])
|
||||
|
||||
const clearFrontmatter = useCallback(() => {
|
||||
hasNewYamlError.current = false
|
||||
rawMetaRef.current = undefined
|
||||
onBeforeRendering?.()
|
||||
}, [onBeforeRendering])
|
||||
|
||||
const checkYamlErrorState = useCallback(() => {
|
||||
setShowYamlError(hasNewYamlError.current)
|
||||
onAfterRendering?.()
|
||||
}, [onAfterRendering])
|
||||
|
||||
const baseReplacers = useComponentReplacers(onTaskCheckedChange, onImageClick, baseUrl)
|
||||
const markdownReactDom = useConvertMarkdownToReactDom(trimmedContent, markdownIt, baseReplacers, additionalReplacers, clearFrontmatter, checkYamlErrorState)
|
||||
|
||||
useTranslation()
|
||||
useCalculateLineMarkerPosition(markdownBodyRef, currentLineMarkers.current, onLineMarkerPositionChanged, markdownBodyRef.current?.offsetTop ?? 0)
|
||||
useExtractFirstHeadline(markdownBodyRef, content, onFirstHeadingChange)
|
||||
useOnRefChange(tocAst, onTocChange)
|
||||
useOnRefChange(rawMetaRef, (newValue) => {
|
||||
if (!newValue) {
|
||||
onFrontmatterChange?.(undefined)
|
||||
} else {
|
||||
onFrontmatterChange?.(new NoteFrontmatter(newValue))
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={ `${ className ?? '' } d-flex flex-column align-items-center` }>
|
||||
<DocumentLengthLimitReachedAlert contentLength={ content.length }/>
|
||||
<div ref={ documentReference } className={ 'markdown-body w-100 d-flex flex-column align-items-center' }>
|
||||
<div ref={ outerContainerRef } className={ 'position-relative' }>
|
||||
<InvalidYamlAlert show={ showYamlError }/>
|
||||
<DocumentLengthLimitReachedAlert show={ contentExceedsLimit }/>
|
||||
<div ref={ markdownBodyRef }
|
||||
className={ `${ className ?? '' } markdown-body w-100 d-flex flex-column align-items-center` }>
|
||||
{ markdownReactDom }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BasicMarkdownRenderer
|
||||
|
|
|
@ -10,17 +10,15 @@ import { Trans, useTranslation } from 'react-i18next'
|
|||
import { useSelector } from 'react-redux'
|
||||
import { ApplicationState } from '../../redux'
|
||||
import { ShowIf } from '../common/show-if/show-if'
|
||||
import { SimpleAlertProps } from '../common/simple-alert/simple-alert-props'
|
||||
|
||||
export interface DocumentLengthLimitReachedAlertProps {
|
||||
contentLength: number
|
||||
}
|
||||
|
||||
export const DocumentLengthLimitReachedAlert: React.FC<DocumentLengthLimitReachedAlertProps> = ({ contentLength }) => {
|
||||
export const DocumentLengthLimitReachedAlert: React.FC<SimpleAlertProps> = ({ show }) => {
|
||||
useTranslation()
|
||||
|
||||
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
||||
|
||||
return (
|
||||
<ShowIf condition={ contentLength > maxLength }>
|
||||
<ShowIf condition={ show }>
|
||||
<Alert variant='danger' dir={ 'auto' } data-cy={ 'limitReachedMessage' }>
|
||||
<Trans i18nKey={ 'editor.error.limitReached.description' } values={ { maxLength } }/>
|
||||
</Alert>
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { TocAst } from 'markdown-it-toc-done-right'
|
||||
import React, { Ref, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { NoteFrontmatter, RawNoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter'
|
||||
import { BasicMarkdownRenderer } from './basic-markdown-renderer'
|
||||
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
|
||||
import { usePostFrontmatterOnChange } from './hooks/use-post-frontmatter-on-change'
|
||||
import { usePostTocAstOnChange } from './hooks/use-post-toc-ast-on-change'
|
||||
import { useReplacerInstanceListCreator } from './hooks/use-replacer-instance-list-creator'
|
||||
import { InvalidYamlAlert } from './invalid-yaml-alert'
|
||||
import { FullMarkdownItConfigurator } from './markdown-it-configurator/FullMarkdownItConfigurator'
|
||||
import { ImageClickHandler } from './replace-components/image/image-replacer'
|
||||
import { LineMarkers } from './replace-components/linemarker/line-number-marker'
|
||||
import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
|
||||
import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
|
||||
|
||||
export interface FullMarkdownRendererProps {
|
||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||
onTocChange?: (ast: TocAst) => void
|
||||
rendererRef?: Ref<HTMLDivElement>
|
||||
baseUrl?: string
|
||||
onImageClick?: ImageClickHandler
|
||||
}
|
||||
|
||||
export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & AdditionalMarkdownRendererProps> = (
|
||||
{
|
||||
onFirstHeadingChange,
|
||||
onLineMarkerPositionChanged,
|
||||
onFrontmatterChange,
|
||||
onTaskCheckedChange,
|
||||
onTocChange,
|
||||
content,
|
||||
className,
|
||||
rendererRef,
|
||||
baseUrl,
|
||||
onImageClick
|
||||
}) => {
|
||||
const allReplacers = useReplacerInstanceListCreator(onTaskCheckedChange, onImageClick, baseUrl)
|
||||
useTranslation()
|
||||
|
||||
const [showYamlError, setShowYamlError] = useState(false)
|
||||
const hasNewYamlError = useRef(false)
|
||||
|
||||
const rawMetaRef = useRef<RawNoteFrontmatter>()
|
||||
const firstHeadingRef = useRef<string>()
|
||||
const documentElement = useRef<HTMLDivElement>(null)
|
||||
const currentLineMarkers = useRef<LineMarkers[]>()
|
||||
usePostFrontmatterOnChange(rawMetaRef.current, firstHeadingRef.current, onFrontmatterChange, onFirstHeadingChange)
|
||||
useCalculateLineMarkerPosition(documentElement, currentLineMarkers.current, onLineMarkerPositionChanged, documentElement.current?.offsetTop ?? 0)
|
||||
useExtractFirstHeadline(documentElement, content, onFirstHeadingChange)
|
||||
|
||||
const tocAst = useRef<TocAst>()
|
||||
usePostTocAstOnChange(tocAst, onTocChange)
|
||||
|
||||
const markdownIt = useMemo(() => {
|
||||
return (new FullMarkdownItConfigurator(
|
||||
!!onFrontmatterChange,
|
||||
errorState => hasNewYamlError.current = errorState,
|
||||
rawMeta => {
|
||||
rawMetaRef.current = rawMeta
|
||||
},
|
||||
toc => {
|
||||
tocAst.current = toc
|
||||
},
|
||||
onLineMarkerPositionChanged === undefined
|
||||
? undefined
|
||||
: lineMarkers => {
|
||||
currentLineMarkers.current = lineMarkers
|
||||
}
|
||||
)).buildConfiguredMarkdownIt()
|
||||
}, [onLineMarkerPositionChanged, onFrontmatterChange])
|
||||
|
||||
const clearFrontmatter = useCallback(() => {
|
||||
hasNewYamlError.current = false
|
||||
rawMetaRef.current = undefined
|
||||
}, [])
|
||||
|
||||
const checkYamlErrorState = useCallback(() => {
|
||||
if (hasNewYamlError.current !== showYamlError) {
|
||||
setShowYamlError(hasNewYamlError.current)
|
||||
}
|
||||
}, [setShowYamlError, showYamlError])
|
||||
|
||||
return (
|
||||
<div ref={ rendererRef } className={ 'position-relative' }>
|
||||
<InvalidYamlAlert showYamlError={ showYamlError }/>
|
||||
<BasicMarkdownRenderer
|
||||
className={ className }
|
||||
content={ content }
|
||||
componentReplacers={ allReplacers }
|
||||
markdownIt={ markdownIt }
|
||||
documentReference={ documentElement }
|
||||
onBeforeRendering={ clearFrontmatter }
|
||||
onAfterRendering={ checkYamlErrorState }/>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { AbcReplacer } from '../replace-components/abc/abc-replacer'
|
||||
import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer'
|
||||
import { ComponentReplacer } from '../replace-components/ComponentReplacer'
|
||||
|
@ -21,14 +21,13 @@ import { MarkmapReplacer } from '../replace-components/markmap/markmap-replacer'
|
|||
import { MermaidReplacer } from '../replace-components/mermaid/mermaid-replacer'
|
||||
import { ColoredBlockquoteReplacer } from '../replace-components/colored-blockquote/colored-blockquote-replacer'
|
||||
import { SequenceDiagramReplacer } from '../replace-components/sequence-diagram/sequence-diagram-replacer'
|
||||
import { TaskListReplacer } from '../replace-components/task-list/task-list-replacer'
|
||||
import { TaskCheckedChangeHandler, 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,
|
||||
onImageClick?: ImageClickHandler, baseUrl?: string): () => ComponentReplacer[] => useMemo(() =>
|
||||
() => [
|
||||
export const useComponentReplacers = (onTaskCheckedChange?: TaskCheckedChangeHandler, onImageClick?: ImageClickHandler, baseUrl?: string): () => ComponentReplacer[] =>
|
||||
useCallback(() => [
|
||||
new LinemarkerReplacer(),
|
||||
new GistReplacer(),
|
||||
new YoutubeReplacer(),
|
|
@ -15,7 +15,8 @@ import { calculateNewLineNumberMapping } from '../utils/line-number-mapping'
|
|||
export const useConvertMarkdownToReactDom = (
|
||||
markdownCode: string,
|
||||
markdownIt: MarkdownIt,
|
||||
componentReplacers?: () => ComponentReplacer[],
|
||||
baseReplacers: () => ComponentReplacer[],
|
||||
additionalReplacers?: () => ComponentReplacer[],
|
||||
onBeforeRendering?: () => void,
|
||||
onAfterRendering?: () => void): ReactElement[] => {
|
||||
const oldMarkdownLineKeys = useRef<LineKeys[]>()
|
||||
|
@ -33,11 +34,14 @@ export const useConvertMarkdownToReactDom = (
|
|||
} = calculateNewLineNumberMapping(contentLines, oldMarkdownLineKeys.current ?? [], lastUsedLineId.current)
|
||||
oldMarkdownLineKeys.current = newLines
|
||||
lastUsedLineId.current = newLastUsedLineId
|
||||
const transformer = componentReplacers ? buildTransformer(newLines, componentReplacers()) : undefined
|
||||
|
||||
const replacers = baseReplacers()
|
||||
.concat(additionalReplacers ? additionalReplacers() : [])
|
||||
const transformer = replacers.length > 0 ? buildTransformer(newLines, replacers) : undefined
|
||||
const rendering = ReactHtmlParser(html, { transform: transformer })
|
||||
if (onAfterRendering) {
|
||||
onAfterRendering()
|
||||
}
|
||||
return rendering
|
||||
}, [onBeforeRendering, onAfterRendering, markdownCode, markdownIt, componentReplacers])
|
||||
}, [onBeforeRendering, markdownIt, markdownCode, baseReplacers, additionalReplacers, onAfterRendering])
|
||||
}
|
||||
|
|
|
@ -12,12 +12,12 @@ export const useExtractFirstHeadline = (documentElement: React.RefObject<HTMLDiv
|
|||
return ''
|
||||
}
|
||||
|
||||
let innerText = ''
|
||||
|
||||
if ((node as HTMLElement).classList?.contains('katex-mathml')) {
|
||||
return ''
|
||||
}
|
||||
|
||||
let innerText = ''
|
||||
|
||||
if (node.childNodes && node.childNodes.length > 0) {
|
||||
node.childNodes.forEach((child) => {
|
||||
innerText += extractInnerText(child)
|
||||
|
@ -37,11 +37,11 @@ export const useExtractFirstHeadline = (documentElement: React.RefObject<HTMLDiv
|
|||
const firstHeading = documentElement.current.getElementsByTagName('h1')
|
||||
.item(0)
|
||||
const headingText = extractInnerText(firstHeading)
|
||||
if (headingText === lastFirstHeading.current) {
|
||||
return
|
||||
.trim()
|
||||
if (headingText !== lastFirstHeading.current) {
|
||||
lastFirstHeading.current = headingText
|
||||
onFirstHeadingChange(headingText)
|
||||
}
|
||||
lastFirstHeading.current = headingText
|
||||
onFirstHeadingChange(headingText)
|
||||
}
|
||||
}, [documentElement, extractInnerText, onFirstHeadingChange, content])
|
||||
}
|
||||
|
|
18
src/components/markdown-renderer/hooks/use-on-ref-change.ts
Normal file
18
src/components/markdown-renderer/hooks/use-on-ref-change.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import equal from 'fast-deep-equal'
|
||||
import { MutableRefObject, useEffect, useRef } from 'react'
|
||||
|
||||
export const useOnRefChange = <T>(reference: MutableRefObject<T | undefined>, onChange?: (newValue?: T) => void): void => {
|
||||
const lastValue = useRef<T | undefined>()
|
||||
useEffect(() => {
|
||||
if (onChange && !equal(reference, lastValue.current)) {
|
||||
lastValue.current = reference.current
|
||||
onChange(reference.current)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import equal from 'fast-deep-equal'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { NoteFrontmatter, RawNoteFrontmatter } from '../../editor-page/note-frontmatter/note-frontmatter'
|
||||
|
||||
export const usePostFrontmatterOnChange = (
|
||||
rawFrontmatter: RawNoteFrontmatter | undefined,
|
||||
firstHeadingRef: string | undefined,
|
||||
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void,
|
||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
): void => {
|
||||
const oldMetaRef = useRef<RawNoteFrontmatter>()
|
||||
const oldFirstHeadingRef = useRef<string>()
|
||||
|
||||
useEffect(() => {
|
||||
if (onFrontmatterChange && !equal(oldMetaRef.current, rawFrontmatter)) {
|
||||
if (rawFrontmatter) {
|
||||
const newFrontmatter = new NoteFrontmatter(rawFrontmatter)
|
||||
onFrontmatterChange(newFrontmatter)
|
||||
} else {
|
||||
onFrontmatterChange(undefined)
|
||||
}
|
||||
oldMetaRef.current = rawFrontmatter
|
||||
}
|
||||
if (onFirstHeadingChange && !equal(firstHeadingRef, oldFirstHeadingRef.current)) {
|
||||
onFirstHeadingChange(firstHeadingRef || undefined)
|
||||
oldFirstHeadingRef.current = firstHeadingRef
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import equal from 'fast-deep-equal'
|
||||
import { TocAst } from 'markdown-it-toc-done-right'
|
||||
import { RefObject, useEffect, useRef } from 'react'
|
||||
|
||||
export const usePostTocAstOnChange = (tocAst: RefObject<TocAst | undefined>, onTocChange?: (ast: TocAst) => void): void => {
|
||||
const lastTocAst = useRef<TocAst>()
|
||||
useEffect(() => {
|
||||
if (onTocChange && tocAst.current && !equal(tocAst, lastTocAst.current)) {
|
||||
lastTocAst.current = tocAst.current
|
||||
onTocChange(tocAst.current)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useSelector } from 'react-redux'
|
||||
import { ApplicationState } from '../../../redux'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
export const useTrimmedContent = (content: string): [trimmedContent: string, contentExceedsLimit: boolean] => {
|
||||
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
||||
const contentExceedsLimit = content.length > maxLength
|
||||
|
||||
const trimmedContent = useMemo(() => contentExceedsLimit ? content.substr(0, maxLength) : content, [content,
|
||||
contentExceedsLimit,
|
||||
maxLength])
|
||||
return [trimmedContent, contentExceedsLimit]
|
||||
}
|
|
@ -9,16 +9,13 @@ import { Alert } from 'react-bootstrap'
|
|||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { InternalLink } from '../common/links/internal-link'
|
||||
import { ShowIf } from '../common/show-if/show-if'
|
||||
import { SimpleAlertProps } from '../common/simple-alert/simple-alert-props'
|
||||
|
||||
export interface InvalidYamlAlertProps {
|
||||
showYamlError: boolean
|
||||
}
|
||||
|
||||
export const InvalidYamlAlert: React.FC<InvalidYamlAlertProps> = ({ showYamlError }) => {
|
||||
export const InvalidYamlAlert: React.FC<SimpleAlertProps> = ({ show }) => {
|
||||
useTranslation()
|
||||
|
||||
return (
|
||||
<ShowIf condition={ showYamlError }>
|
||||
<ShowIf condition={ show }>
|
||||
<Alert variant='warning' dir='auto'>
|
||||
<Trans i18nKey='editor.invalidYaml'>
|
||||
<InternalLink text='yaml-metadata' href='/n/yaml-metadata' className='text-primary'/>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
|
@ -19,11 +19,78 @@ import { MarkdownItParserDebugger } from '../markdown-it-plugins/parser-debugger
|
|||
import { spoilerContainer } from '../markdown-it-plugins/spoiler-container'
|
||||
import { tasksLists } from '../markdown-it-plugins/tasks-lists'
|
||||
import { twitterEmojis } from '../markdown-it-plugins/twitter-emojis'
|
||||
import { MarkdownItConfigurator } from './MarkdownItConfigurator'
|
||||
import { RawNoteFrontmatter } from '../../editor-page/note-frontmatter/note-frontmatter'
|
||||
import { TocAst } from 'markdown-it-toc-done-right'
|
||||
import { LineMarkers, lineNumberMarker } from '../replace-components/linemarker/line-number-marker'
|
||||
import { plantumlWithError } from '../markdown-it-plugins/plantuml'
|
||||
import { headlineAnchors } from '../markdown-it-plugins/headline-anchors'
|
||||
import { KatexReplacer } from '../replace-components/katex/katex-replacer'
|
||||
import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
|
||||
import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer'
|
||||
import { GistReplacer } from '../replace-components/gist/gist-replacer'
|
||||
import { legacyPdfShortCode } from '../regex-plugins/replace-legacy-pdf-short-code'
|
||||
import { legacySlideshareShortCode } from '../regex-plugins/replace-legacy-slideshare-short-code'
|
||||
import { legacySpeakerdeckShortCode } from '../regex-plugins/replace-legacy-speakerdeck-short-code'
|
||||
import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer'
|
||||
import { highlightedCode } from '../markdown-it-plugins/highlighted-code'
|
||||
import { quoteExtraColor } from '../markdown-it-plugins/quote-extra-color'
|
||||
import { quoteExtra } from '../markdown-it-plugins/quote-extra'
|
||||
import { documentTableOfContents } from '../markdown-it-plugins/document-table-of-contents'
|
||||
import { frontmatterExtract } from '../markdown-it-plugins/frontmatter'
|
||||
|
||||
export interface ConfiguratorDetails {
|
||||
useFrontmatter: boolean,
|
||||
onParseError: (error: boolean) => void,
|
||||
onRawMetaChange: (rawMeta: RawNoteFrontmatter) => void,
|
||||
onToc: (toc: TocAst) => void,
|
||||
onLineMarkers?: (lineMarkers: LineMarkers[]) => void
|
||||
useAlternativeBreaks?: boolean
|
||||
}
|
||||
|
||||
export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
|
||||
protected readonly options: T
|
||||
protected configurations: MarkdownIt.PluginSimple[] = []
|
||||
protected postConfigurations: MarkdownIt.PluginSimple[] = []
|
||||
|
||||
constructor(options: T) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
public pushConfig(plugin: MarkdownIt.PluginSimple): this {
|
||||
this.configurations.push(plugin)
|
||||
return this
|
||||
}
|
||||
|
||||
public buildConfiguredMarkdownIt(): MarkdownIt {
|
||||
const markdownIt = new MarkdownIt('default', {
|
||||
html: true,
|
||||
breaks: this.options.useAlternativeBreaks ?? true,
|
||||
langPrefix: '',
|
||||
typographer: true
|
||||
})
|
||||
this.configure(markdownIt)
|
||||
this.configurations.forEach((configuration) => markdownIt.use(configuration))
|
||||
this.postConfigurations.forEach((postConfiguration) => markdownIt.use(postConfiguration))
|
||||
return markdownIt
|
||||
}
|
||||
|
||||
export class BasicMarkdownItConfigurator extends MarkdownItConfigurator {
|
||||
protected configure(markdownIt: MarkdownIt): void {
|
||||
this.configurations.push(
|
||||
plantumlWithError,
|
||||
headlineAnchors,
|
||||
KatexReplacer.markdownItPlugin,
|
||||
YoutubeReplacer.markdownItPlugin,
|
||||
VimeoReplacer.markdownItPlugin,
|
||||
GistReplacer.markdownItPlugin,
|
||||
legacyPdfShortCode,
|
||||
legacySlideshareShortCode,
|
||||
legacySpeakerdeckShortCode,
|
||||
AsciinemaReplacer.markdownItPlugin,
|
||||
highlightedCode,
|
||||
quoteExtraColor,
|
||||
quoteExtra('name', 'user'),
|
||||
quoteExtra('time', 'clock-o'),
|
||||
documentTableOfContents(this.options.onToc),
|
||||
twitterEmojis,
|
||||
abbreviation,
|
||||
definitionList,
|
||||
|
@ -35,8 +102,19 @@ export class BasicMarkdownItConfigurator extends MarkdownItConfigurator {
|
|||
imsize,
|
||||
tasksLists,
|
||||
alertContainer,
|
||||
spoilerContainer
|
||||
)
|
||||
spoilerContainer)
|
||||
|
||||
if (this.options.useFrontmatter) {
|
||||
this.configurations.push(frontmatterExtract({
|
||||
onParseError: this.options.onParseError,
|
||||
onRawMetaChange: this.options.onRawMetaChange
|
||||
}))
|
||||
}
|
||||
|
||||
if (this.options.onLineMarkers) {
|
||||
this.configurations.push(lineNumberMarker(this.options.onLineMarkers))
|
||||
}
|
||||
|
||||
this.postConfigurations.push(
|
||||
linkifyExtra,
|
||||
MarkdownItParserDebugger
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { TocAst } from 'markdown-it-toc-done-right'
|
||||
import { RawNoteFrontmatter } from '../../editor-page/note-frontmatter/note-frontmatter'
|
||||
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 { 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 { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer'
|
||||
import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
|
||||
import { BasicMarkdownItConfigurator } from './BasicMarkdownItConfigurator'
|
||||
import { quoteExtraColor } from '../markdown-it-plugins/quote-extra-color'
|
||||
import { legacyPdfShortCode } from '../regex-plugins/replace-legacy-pdf-short-code'
|
||||
|
||||
export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
|
||||
constructor(
|
||||
private useFrontmatter: boolean,
|
||||
private passYamlErrorState: (error: boolean) => void,
|
||||
private onRawMeta: (rawMeta: RawNoteFrontmatter) => void,
|
||||
private onToc: (toc: TocAst) => void,
|
||||
private onLineMarkers?: (lineMarkers: LineMarkers[]) => void
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
protected configure(markdownIt: MarkdownIt): void {
|
||||
super.configure(markdownIt)
|
||||
|
||||
this.configurations.push(
|
||||
plantumlWithError,
|
||||
(markdownIt) => {
|
||||
frontmatterExtract(markdownIt,
|
||||
!this.useFrontmatter
|
||||
? undefined
|
||||
: {
|
||||
onParseError: (hasError: boolean) => this.passYamlErrorState(hasError),
|
||||
onRawMeta: (rawMeta: RawNoteFrontmatter) => this.onRawMeta(rawMeta)
|
||||
})
|
||||
},
|
||||
headlineAnchors,
|
||||
KatexReplacer.markdownItPlugin,
|
||||
YoutubeReplacer.markdownItPlugin,
|
||||
VimeoReplacer.markdownItPlugin,
|
||||
GistReplacer.markdownItPlugin,
|
||||
legacyPdfShortCode,
|
||||
legacySlideshareShortCode,
|
||||
legacySpeakerdeckShortCode,
|
||||
AsciinemaReplacer.markdownItPlugin,
|
||||
highlightedCode,
|
||||
quoteExtraColor,
|
||||
quoteExtra({
|
||||
quoteLabel: 'name',
|
||||
icon: 'user'
|
||||
}),
|
||||
quoteExtra({
|
||||
quoteLabel: 'time',
|
||||
icon: 'clock-o'
|
||||
}),
|
||||
(markdownIt) => documentToc(markdownIt, this.onToc))
|
||||
if (this.onLineMarkers) {
|
||||
const callback = this.onLineMarkers
|
||||
this.configurations.push(
|
||||
(markdownIt) => lineNumberMarker(markdownIt, (lineMarkers) => callback(lineMarkers))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
|
||||
export abstract class MarkdownItConfigurator {
|
||||
protected configurations: MarkdownIt.PluginSimple[] = []
|
||||
protected postConfigurations: MarkdownIt.PluginSimple[] = []
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
protected abstract configure(markdownIt: MarkdownIt): void;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it/lib'
|
||||
import { TocAst } from 'markdown-it-toc-done-right'
|
||||
import { documentToc } from './document-toc'
|
||||
|
||||
export const documentTableOfContents = (onTocChange: ((toc: TocAst) => void)): MarkdownIt.PluginSimple => {
|
||||
return (markdownIt) => documentToc(markdownIt, onTocChange)
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import toc, { TocAst } from 'markdown-it-toc-done-right'
|
||||
import { slugify } from '../../editor-page/table-of-contents/table-of-contents'
|
||||
import { tocSlugify } from '../../editor-page/table-of-contents/toc-slugify'
|
||||
|
||||
export type DocumentTocPluginOptions = (ast: TocAst) => void
|
||||
|
||||
|
@ -21,6 +21,6 @@ export const documentToc: MarkdownIt.PluginWithOptions<DocumentTocPluginOptions>
|
|||
callback: (code: string, ast: TocAst): void => {
|
||||
onToc(ast)
|
||||
},
|
||||
slugify: slugify
|
||||
slugify: tocSlugify
|
||||
})
|
||||
}
|
||||
|
|
|
@ -11,22 +11,20 @@ import { RawNoteFrontmatter } from '../../editor-page/note-frontmatter/note-fron
|
|||
|
||||
interface FrontmatterPluginOptions {
|
||||
onParseError: (error: boolean) => void,
|
||||
onRawMeta: (rawMeta: RawNoteFrontmatter) => void,
|
||||
onRawMetaChange: (rawMeta: RawNoteFrontmatter) => void,
|
||||
}
|
||||
|
||||
export const frontmatterExtract: MarkdownIt.PluginWithOptions<FrontmatterPluginOptions> = (markdownIt: MarkdownIt, options) => {
|
||||
if (!options) {
|
||||
return
|
||||
export const frontmatterExtract: (options: FrontmatterPluginOptions) => MarkdownIt.PluginSimple = (options) =>
|
||||
(markdownIt) => {
|
||||
frontmatter(markdownIt, (rawMeta: string) => {
|
||||
try {
|
||||
const meta: RawNoteFrontmatter = yaml.load(rawMeta) as RawNoteFrontmatter
|
||||
options.onParseError(false)
|
||||
options.onRawMetaChange(meta)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
options.onParseError(true)
|
||||
options.onRawMetaChange({} as RawNoteFrontmatter)
|
||||
}
|
||||
})
|
||||
}
|
||||
frontmatter(markdownIt, (rawMeta: string) => {
|
||||
try {
|
||||
const meta: RawNoteFrontmatter = yaml.load(rawMeta) as RawNoteFrontmatter
|
||||
options.onParseError(false)
|
||||
options.onRawMeta(meta)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
options.onParseError(true)
|
||||
options.onRawMeta({} as RawNoteFrontmatter)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,17 +8,12 @@ import MarkdownIt from 'markdown-it/lib'
|
|||
import Token from 'markdown-it/lib/token'
|
||||
import { IconName } from '../../common/fork-awesome/types'
|
||||
|
||||
export interface QuoteExtraOptions {
|
||||
quoteLabel: string
|
||||
icon: IconName
|
||||
}
|
||||
|
||||
export const quoteExtra: (pluginOptions: QuoteExtraOptions) => MarkdownIt.PluginSimple =
|
||||
(pluginOptions) => (md) => {
|
||||
md.inline.ruler.push(`extraQuote_${ pluginOptions.quoteLabel }`, (state) => {
|
||||
export const quoteExtra: (quoteLabel: string, icon: IconName) => MarkdownIt.PluginSimple =
|
||||
(quoteLabel: string, icon: IconName) => (md) => {
|
||||
md.inline.ruler.push(`extraQuote_${ quoteLabel }`, (state) => {
|
||||
const quoteExtraTagValues = parseQuoteExtraTag(state.src, state.pos, state.posMax)
|
||||
|
||||
if (!quoteExtraTagValues || quoteExtraTagValues.label !== pluginOptions.quoteLabel) {
|
||||
if (!quoteExtraTagValues || quoteExtraTagValues.label !== quoteLabel) {
|
||||
return false
|
||||
}
|
||||
state.pos = quoteExtraTagValues.valueEndIndex + 1
|
||||
|
@ -32,7 +27,7 @@ export const quoteExtra: (pluginOptions: QuoteExtraOptions) => MarkdownIt.Plugin
|
|||
)
|
||||
|
||||
const token = state.push('quote-extra', '', 0)
|
||||
token.attrSet('icon', pluginOptions.icon)
|
||||
token.attrSet('icon', icon)
|
||||
token.children = tokens
|
||||
|
||||
return true
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
|
@ -25,26 +25,25 @@ export const FlowChart: React.FC<FlowChartProps> = ({ code }) => {
|
|||
return
|
||||
}
|
||||
const currentDiagramRef = diagramRef.current
|
||||
import(/* webpackChunkName: "flowchart.js" */ 'flowchart.js').then((imp) => {
|
||||
const parserOutput = imp.parse(code)
|
||||
try {
|
||||
parserOutput.drawSVG(currentDiagramRef, {
|
||||
'line-width': 2,
|
||||
fill: 'none',
|
||||
'font-size': 16,
|
||||
'line-color': darkModeActivated ? '#ffffff' : '#000000',
|
||||
'element-color': darkModeActivated ? '#ffffff' : '#000000',
|
||||
'font-color': darkModeActivated ? '#ffffff' : '#000000',
|
||||
'font-family': 'Source Sans Pro, "Twemoji Mozilla", monospace'
|
||||
})
|
||||
setError(false)
|
||||
} catch (error) {
|
||||
setError(true)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('error while loading flowchart.js')
|
||||
})
|
||||
import(/* webpackChunkName: "flowchart.js" */ 'flowchart.js')
|
||||
.then((imp) => {
|
||||
const parserOutput = imp.parse(code)
|
||||
try {
|
||||
parserOutput.drawSVG(currentDiagramRef, {
|
||||
'line-width': 2,
|
||||
fill: 'none',
|
||||
'font-size': 16,
|
||||
'line-color': darkModeActivated ? '#ffffff' : '#000000',
|
||||
'element-color': darkModeActivated ? '#ffffff' : '#000000',
|
||||
'font-color': darkModeActivated ? '#ffffff' : '#000000',
|
||||
'font-family': 'Source Sans Pro, "Twemoji Mozilla", monospace'
|
||||
})
|
||||
setError(false)
|
||||
} catch (error) {
|
||||
setError(true)
|
||||
}
|
||||
})
|
||||
.catch(() => console.error('error while loading flowchart.js'))
|
||||
|
||||
return () => {
|
||||
Array.from(currentDiagramRef.children)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
@ -32,7 +32,7 @@ export const GraphvizFrame: React.FC<GraphvizFrameProps> = ({ code }) => {
|
|||
}
|
||||
const actualContainer = container.current
|
||||
|
||||
import('@hpcc-js/wasm')
|
||||
import(/* webpackChunkName: "d3-graphviz" */'@hpcc-js/wasm')
|
||||
.then((wasmPlugin) => {
|
||||
wasmPlugin.wasmFolder('/static/js')
|
||||
})
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
/*
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
.markdown-body {
|
||||
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
@import '../../../../../../node_modules/highlight.js/styles/github';
|
||||
|
||||
|
|
|
@ -17,13 +17,16 @@ export interface HighlightedCodeProps {
|
|||
wrapLines: boolean
|
||||
}
|
||||
|
||||
export const escapeHtml = (unsafe: string): string => {
|
||||
/*
|
||||
TODO: Test method or rewrite code so this is not necessary anymore
|
||||
*/
|
||||
const escapeHtml = (unsafe: string): string => {
|
||||
return unsafe
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replaceAll(/&/g, '&')
|
||||
.replaceAll(/</g, '<')
|
||||
.replaceAll(/>/g, '>')
|
||||
.replaceAll(/"/g, '"')
|
||||
.replaceAll(/'/g, ''')
|
||||
}
|
||||
|
||||
const replaceCode = (code: string): ReactElement[][] => {
|
||||
|
@ -69,3 +72,5 @@ export const HighlightedCode: React.FC<HighlightedCodeProps> = ({ code, language
|
|||
</div>
|
||||
</Fragment>)
|
||||
}
|
||||
|
||||
export default HighlightedCode
|
||||
|
|
|
@ -18,7 +18,7 @@ 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<LineNumberMarkerOptions> = (md: MarkdownIt, options) => {
|
||||
export const lineNumberMarker: (options: LineNumberMarkerOptions) => MarkdownIt.PluginSimple = (options) => (md: MarkdownIt) => {
|
||||
// add app_linemarker token before each opening or self-closing level-0 tag
|
||||
md.core.ruler.push('line_number_marker', (state) => {
|
||||
const lineMarkers: LineMarkers[] = []
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useEffect, useRef, useState } from 'react'
|
||||
|
@ -45,21 +45,22 @@ export const MarkmapFrame: React.FC<MarkmapFrameProps> = ({ code }) => {
|
|||
return
|
||||
}
|
||||
const actualContainer = diagramContainer.current
|
||||
import('./markmap-loader').then(({ markmapLoader }) => {
|
||||
try {
|
||||
const svg: SVGSVGElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
svg.setAttribute('width', '100%')
|
||||
actualContainer.querySelectorAll('svg')
|
||||
.forEach(child => child.remove())
|
||||
actualContainer.appendChild(svg)
|
||||
markmapLoader(svg, code)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('error while loading markmap')
|
||||
})
|
||||
import(/* webpackChunkName: "markmap" */'./markmap-loader')
|
||||
.then(({ markmapLoader }) => {
|
||||
try {
|
||||
const svg: SVGSVGElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
svg.setAttribute('width', '100%')
|
||||
actualContainer.querySelectorAll('svg')
|
||||
.forEach(child => child.remove())
|
||||
actualContainer.appendChild(svg)
|
||||
markmapLoader(svg, code)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('error while loading markmap')
|
||||
})
|
||||
}, [code])
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
@ -27,13 +27,14 @@ export const MermaidChart: React.FC<MermaidChartProps> = ({ code }) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (!mermaidInitialized) {
|
||||
import('mermaid').then((mermaid) => {
|
||||
mermaid.default.initialize({ startOnLoad: false })
|
||||
mermaidInitialized = true
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('error while loading mermaid')
|
||||
})
|
||||
import(/* webpackChunkName: "mermaid" */'mermaid')
|
||||
.then((mermaid) => {
|
||||
mermaid.default.initialize({ startOnLoad: false })
|
||||
mermaidInitialized = true
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('error while loading mermaid')
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
@ -51,22 +52,23 @@ export const MermaidChart: React.FC<MermaidChartProps> = ({ code }) => {
|
|||
if (!diagramContainer.current) {
|
||||
return
|
||||
}
|
||||
import('mermaid').then((mermaid) => {
|
||||
try {
|
||||
if (!diagramContainer.current) {
|
||||
return
|
||||
import(/* webpackChunkName: "mermaid" */'mermaid')
|
||||
.then((mermaid) => {
|
||||
try {
|
||||
if (!diagramContainer.current) {
|
||||
return
|
||||
}
|
||||
mermaid.default.parse(code)
|
||||
delete diagramContainer.current.dataset.processed
|
||||
diagramContainer.current.textContent = code
|
||||
mermaid.default.init(diagramContainer.current)
|
||||
setError(undefined)
|
||||
} catch (error) {
|
||||
const message = (error as MermaidParseError).str
|
||||
showError(message || t('renderer.mermaid.unknownError'))
|
||||
}
|
||||
mermaid.default.parse(code)
|
||||
delete diagramContainer.current.dataset.processed
|
||||
diagramContainer.current.textContent = code
|
||||
mermaid.default.init(diagramContainer.current)
|
||||
setError(undefined)
|
||||
} catch (error) {
|
||||
const message = (error as MermaidParseError).str
|
||||
showError(message || t('renderer.mermaid.unknownError'))
|
||||
}
|
||||
})
|
||||
.catch(() => showError('Error while loading mermaid'))
|
||||
})
|
||||
.catch(() => showError('Error while loading mermaid'))
|
||||
}, [code, showError, t])
|
||||
|
||||
return <Fragment>
|
||||
|
|
|
@ -8,10 +8,12 @@ import { DomElement } from 'domhandler'
|
|||
import React, { ReactElement } from 'react'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
|
||||
export type TaskCheckedChangeHandler = (lineInMarkdown: number, checked: boolean) => void
|
||||
|
||||
export class TaskListReplacer extends ComponentReplacer {
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||
|
||||
constructor(onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void) {
|
||||
constructor(onTaskCheckedChange?: TaskCheckedChangeHandler) {
|
||||
super()
|
||||
this.onTaskCheckedChange = onTaskCheckedChange
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
@ -31,34 +31,35 @@ export const VegaChart: React.FC<VegaChartProps> = ({ code }) => {
|
|||
if (!diagramContainer.current) {
|
||||
return
|
||||
}
|
||||
import(/* webpackChunkName: "vega" */ 'vega-embed').then((embed) => {
|
||||
try {
|
||||
if (!diagramContainer.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const spec = JSON.parse(code) as VisualizationSpec
|
||||
embed.default(diagramContainer.current, spec, {
|
||||
actions: {
|
||||
export: true,
|
||||
source: false,
|
||||
compiled: false,
|
||||
editor: false
|
||||
},
|
||||
i18n: {
|
||||
PNG_ACTION: t('renderer.vega-lite.png'),
|
||||
SVG_ACTION: t('renderer.vega-lite.svg')
|
||||
import(/* webpackChunkName: "vega" */ 'vega-embed')
|
||||
.then((embed) => {
|
||||
try {
|
||||
if (!diagramContainer.current) {
|
||||
return
|
||||
}
|
||||
})
|
||||
.then(() => setError(undefined))
|
||||
.catch(err => showError(err))
|
||||
} catch (err) {
|
||||
showError(t('renderer.vega-lite.errorJson'))
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('error while loading vega-light')
|
||||
})
|
||||
|
||||
const spec = JSON.parse(code) as VisualizationSpec
|
||||
embed.default(diagramContainer.current, spec, {
|
||||
actions: {
|
||||
export: true,
|
||||
source: false,
|
||||
compiled: false,
|
||||
editor: false
|
||||
},
|
||||
i18n: {
|
||||
PNG_ACTION: t('renderer.vega-lite.png'),
|
||||
SVG_ACTION: t('renderer.vega-lite.svg')
|
||||
}
|
||||
})
|
||||
.then(() => setError(undefined))
|
||||
.catch(err => showError(err))
|
||||
} catch (err) {
|
||||
showError(t('renderer.vega-lite.errorJson'))
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('error while loading vega-light')
|
||||
})
|
||||
}, [code, showError, t])
|
||||
|
||||
return <Fragment>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue