mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-22 17:56:30 -05:00
Refactor replacers and line id mapping
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
3591c90f9f
commit
ec77e672f6
58 changed files with 899 additions and 750 deletions
|
@ -138,6 +138,7 @@
|
|||
"markmap-view": "0.2.6",
|
||||
"@matejmazur/react-katex": "3.1.3",
|
||||
"mermaid": "8.13.3",
|
||||
"optional-js": "2.3.0",
|
||||
"prettier": "2.4.1",
|
||||
"react": "17.0.2",
|
||||
"react-bootstrap": "1.6.4",
|
||||
|
|
|
@ -16,9 +16,9 @@ import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-po
|
|||
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
|
||||
import type { TocAst } from 'markdown-it-toc-done-right'
|
||||
import { useOnRefChange } from './hooks/use-on-ref-change'
|
||||
import { BasicMarkdownItConfigurator } from './markdown-it-configurator/basic-markdown-it-configurator'
|
||||
import { useTrimmedContent } from './hooks/use-trimmed-content'
|
||||
import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
|
||||
import { DocumentMarkdownItConfigurator } from './markdown-it-configurator/document-markdown-it-configurator'
|
||||
|
||||
export interface DocumentMarkdownRendererProps extends CommonMarkdownRendererProps {
|
||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||
|
@ -44,15 +44,14 @@ export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> =
|
|||
|
||||
const markdownIt = useMemo(
|
||||
() =>
|
||||
new BasicMarkdownItConfigurator({
|
||||
onToc: (toc) => (tocAst.current = toc),
|
||||
new DocumentMarkdownItConfigurator({
|
||||
onTocChange: (toc) => (tocAst.current = toc),
|
||||
onLineMarkers:
|
||||
onLineMarkerPositionChanged === undefined
|
||||
? undefined
|
||||
: (lineMarkers) => (currentLineMarkers.current = lineMarkers),
|
||||
useAlternativeBreaks,
|
||||
lineOffset,
|
||||
headlineAnchors: true
|
||||
lineOffset
|
||||
}).buildConfiguredMarkdownIt(),
|
||||
[onLineMarkerPositionChanged, useAlternativeBreaks, lineOffset]
|
||||
)
|
||||
|
|
|
@ -5,28 +5,30 @@
|
|||
*/
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { AbcReplacer } from '../replace-components/abc/abc-replacer'
|
||||
import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer'
|
||||
import type { ComponentReplacer } from '../replace-components/ComponentReplacer'
|
||||
import type { ComponentReplacer } from '../replace-components/component-replacer'
|
||||
import { CsvReplacer } from '../replace-components/csv/csv-replacer'
|
||||
import { FlowchartReplacer } from '../replace-components/flow/flowchart-replacer'
|
||||
import { GistReplacer } from '../replace-components/gist/gist-replacer'
|
||||
import { GraphvizReplacer } from '../replace-components/graphviz/graphviz-replacer'
|
||||
import { HighlightedCodeReplacer } from '../replace-components/highlighted-fence/highlighted-fence-replacer'
|
||||
import type { ImageClickHandler } from '../replace-components/image/image-replacer'
|
||||
import { ImageReplacer } from '../replace-components/image/image-replacer'
|
||||
import { KatexReplacer } from '../replace-components/katex/katex-replacer'
|
||||
import { LinemarkerReplacer } from '../replace-components/linemarker/linemarker-replacer'
|
||||
import { LinkReplacer } from '../replace-components/link-replacer/link-replacer'
|
||||
import { MarkmapReplacer } from '../replace-components/markmap/markmap-replacer'
|
||||
import { MermaidReplacer } from '../replace-components/mermaid/mermaid-replacer'
|
||||
import { ColoredBlockquoteReplacer } from '../replace-components/colored-blockquote/colored-blockquote-replacer'
|
||||
import { SequenceDiagramReplacer } from '../replace-components/sequence-diagram/sequence-diagram-replacer'
|
||||
import type { TaskCheckedChangeHandler } from '../replace-components/task-list/task-list-replacer'
|
||||
import { TaskListReplacer } from '../replace-components/task-list/task-list-replacer'
|
||||
import { VegaReplacer } from '../replace-components/vega-lite/vega-replacer'
|
||||
import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer'
|
||||
import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
|
||||
import { CodeBlockComponentReplacer } from '../replace-components/code-block-component-replacer'
|
||||
import { GraphvizFrame } from '../replace-components/graphviz/graphviz-frame'
|
||||
import { MarkmapFrame } from '../replace-components/markmap/markmap-frame'
|
||||
import { VegaChart } from '../replace-components/vega-lite/vega-chart'
|
||||
import { MermaidChart } from '../replace-components/mermaid/mermaid-chart'
|
||||
import { FlowChart } from '../replace-components/flow/flowchart'
|
||||
import { SequenceDiagram } from '../replace-components/sequence-diagram/sequence-diagram'
|
||||
import { AbcFrame } from '../replace-components/abc/abc-frame'
|
||||
import { CustomTagWithIdComponentReplacer } from '../replace-components/custom-tag-with-id-component-replacer'
|
||||
import { GistFrame } from '../replace-components/gist/gist-frame'
|
||||
import { YouTubeFrame } from '../replace-components/youtube/youtube-frame'
|
||||
import { VimeoFrame } from '../replace-components/vimeo/vimeo-frame'
|
||||
import { AsciinemaFrame } from '../replace-components/asciinema/asciinema-frame'
|
||||
|
||||
/**
|
||||
* Provides a function that creates a list of {@link ComponentReplacer component replacer} instances.
|
||||
|
@ -47,23 +49,23 @@ export const useComponentReplacers = (
|
|||
useMemo(
|
||||
() => [
|
||||
new LinemarkerReplacer(),
|
||||
new GistReplacer(),
|
||||
new YoutubeReplacer(),
|
||||
new VimeoReplacer(),
|
||||
new AsciinemaReplacer(),
|
||||
new AbcReplacer(),
|
||||
new CustomTagWithIdComponentReplacer(GistFrame, 'gist'),
|
||||
new CustomTagWithIdComponentReplacer(YouTubeFrame, 'youtube'),
|
||||
new CustomTagWithIdComponentReplacer(VimeoFrame, 'vimeo'),
|
||||
new CustomTagWithIdComponentReplacer(AsciinemaFrame, 'asciinema'),
|
||||
new ImageReplacer(onImageClick),
|
||||
new SequenceDiagramReplacer(),
|
||||
new CsvReplacer(),
|
||||
new FlowchartReplacer(),
|
||||
new MermaidReplacer(),
|
||||
new GraphvizReplacer(),
|
||||
new MarkmapReplacer(),
|
||||
new VegaReplacer(),
|
||||
new CodeBlockComponentReplacer(AbcFrame, 'abc'),
|
||||
new CodeBlockComponentReplacer(SequenceDiagram, 'sequence'),
|
||||
new CodeBlockComponentReplacer(FlowChart, 'flow'),
|
||||
new CodeBlockComponentReplacer(MermaidChart, 'mermaid'),
|
||||
new CodeBlockComponentReplacer(GraphvizFrame, 'graphviz'),
|
||||
new CodeBlockComponentReplacer(MarkmapFrame, 'markmap'),
|
||||
new CodeBlockComponentReplacer(VegaChart, 'vega-lite'),
|
||||
new HighlightedCodeReplacer(),
|
||||
new ColoredBlockquoteReplacer(),
|
||||
new KatexReplacer(),
|
||||
new TaskListReplacer(onTaskCheckedChange, frontmatterLinesToSkip),
|
||||
new TaskListReplacer(frontmatterLinesToSkip, onTaskCheckedChange),
|
||||
new LinkReplacer(baseUrl)
|
||||
],
|
||||
[onImageClick, onTaskCheckedChange, baseUrl, frontmatterLinesToSkip]
|
||||
|
|
|
@ -5,13 +5,12 @@
|
|||
*/
|
||||
|
||||
import type MarkdownIt from 'markdown-it/lib'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import type { ComponentReplacer, ValidReactDomElement } from '../replace-components/ComponentReplacer'
|
||||
import type { LineKeys } from '../types'
|
||||
import { buildTransformer } from '../utils/html-react-transformer'
|
||||
import { calculateNewLineNumberMapping } from '../utils/line-number-mapping'
|
||||
import { useMemo } from 'react'
|
||||
import type { ComponentReplacer, ValidReactDomElement } from '../replace-components/component-replacer'
|
||||
import convertHtmlToReact from '@hedgedoc/html-to-react'
|
||||
import type { Document } from 'domhandler'
|
||||
import { NodeToReactTransformer } from '../utils/node-to-react-transformer'
|
||||
import { LineIdMapper } from '../utils/line-id-mapper'
|
||||
|
||||
/**
|
||||
* Renders markdown code into react elements
|
||||
|
@ -28,21 +27,22 @@ export const useConvertMarkdownToReactDom = (
|
|||
replacers: ComponentReplacer[],
|
||||
preprocessNodes?: (nodes: Document) => Document
|
||||
): ValidReactDomElement[] => {
|
||||
const oldMarkdownLineKeys = useRef<LineKeys[]>()
|
||||
const lastUsedLineId = useRef<number>(0)
|
||||
const lineNumberMapper = useMemo(() => new LineIdMapper(), [])
|
||||
const htmlToReactTransformer = useMemo(() => new NodeToReactTransformer(), [])
|
||||
|
||||
useMemo(() => {
|
||||
htmlToReactTransformer.setReplacers(replacers)
|
||||
}, [htmlToReactTransformer, replacers])
|
||||
|
||||
useMemo(() => {
|
||||
htmlToReactTransformer.setLineIds(lineNumberMapper.updateLineMapping(markdownCode))
|
||||
}, [htmlToReactTransformer, lineNumberMapper, markdownCode])
|
||||
|
||||
return useMemo(() => {
|
||||
const html = markdownIt.render(markdownCode)
|
||||
const contentLines = markdownCode.split('\n')
|
||||
const { lines: newLines, lastUsedLineId: newLastUsedLineId } = calculateNewLineNumberMapping(
|
||||
contentLines,
|
||||
oldMarkdownLineKeys.current ?? [],
|
||||
lastUsedLineId.current
|
||||
)
|
||||
oldMarkdownLineKeys.current = newLines
|
||||
lastUsedLineId.current = newLastUsedLineId
|
||||
|
||||
const transformer = replacers.length > 0 ? buildTransformer(newLines, replacers) : undefined
|
||||
return convertHtmlToReact(html, { transform: transformer, preprocessNodes: preprocessNodes })
|
||||
}, [markdownIt, markdownCode, replacers, preprocessNodes])
|
||||
return convertHtmlToReact(html, {
|
||||
transform: (node, index) => htmlToReactTransformer.translateNodeToReactElement(node, index),
|
||||
preprocessNodes: preprocessNodes
|
||||
})
|
||||
}, [htmlToReactTransformer, markdownCode, markdownIt, preprocessNodes])
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Configuration } from './markdown-it-configurator'
|
||||
import { MarkdownItConfigurator } from './markdown-it-configurator'
|
||||
import { headlineAnchors } from '../markdown-it-plugins/headline-anchors'
|
||||
import type { LineMarkers } from '../replace-components/linemarker/line-number-marker'
|
||||
import { lineNumberMarker } from '../replace-components/linemarker/line-number-marker'
|
||||
|
||||
export interface DocumentConfiguration extends Configuration {
|
||||
onLineMarkers?: (lineMarkers: LineMarkers[]) => void
|
||||
}
|
||||
|
||||
export class DocumentMarkdownItConfigurator extends MarkdownItConfigurator<DocumentConfiguration> {
|
||||
protected configure(): void {
|
||||
super.configure()
|
||||
|
||||
this.configurations.push(headlineAnchors)
|
||||
if (this.options.onLineMarkers) {
|
||||
this.configurations.push(lineNumberMarker(this.options.onLineMarkers, this.options.lineOffset ?? 0))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,45 +20,34 @@ import { spoilerContainer } from '../markdown-it-plugins/spoiler-container'
|
|||
import { tasksLists } from '../markdown-it-plugins/tasks-lists'
|
||||
import { twitterEmojis } from '../markdown-it-plugins/twitter-emojis'
|
||||
import type { TocAst } from 'markdown-it-toc-done-right'
|
||||
import type { LineMarkers } from '../replace-components/linemarker/line-number-marker'
|
||||
import { lineNumberMarker } from '../replace-components/linemarker/line-number-marker'
|
||||
import { plantumlWithError } from '../markdown-it-plugins/plantuml'
|
||||
import { headlineAnchors } from '../markdown-it-plugins/headline-anchors'
|
||||
import { KatexReplacer } from '../replace-components/katex/katex-replacer'
|
||||
import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
|
||||
import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer'
|
||||
import { GistReplacer } from '../replace-components/gist/gist-replacer'
|
||||
import { legacyPdfShortCode } from '../regex-plugins/replace-legacy-pdf-short-code'
|
||||
import { legacySlideshareShortCode } from '../regex-plugins/replace-legacy-slideshare-short-code'
|
||||
import { legacySpeakerdeckShortCode } from '../regex-plugins/replace-legacy-speakerdeck-short-code'
|
||||
import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer'
|
||||
import { highlightedCode } from '../markdown-it-plugins/highlighted-code'
|
||||
import { quoteExtraColor } from '../markdown-it-plugins/quote-extra-color'
|
||||
import { quoteExtra } from '../markdown-it-plugins/quote-extra'
|
||||
import { documentTableOfContents } from '../markdown-it-plugins/document-table-of-contents'
|
||||
import { addSlideSectionsMarkdownItPlugin } from '../markdown-it-plugins/reveal-sections'
|
||||
import { youtubeMarkdownItPlugin } from '../replace-components/youtube/youtube-markdown-it-plugin'
|
||||
import { vimeoMarkdownItPlugin } from '../replace-components/vimeo/vimeo-markdown-it-plugin'
|
||||
import { gistMarkdownItPlugin } from '../replace-components/gist/gist-markdown-it-plugin'
|
||||
import { asciinemaMarkdownItPlugin } from '../replace-components/asciinema/replace-asciinema-link'
|
||||
|
||||
export interface ConfiguratorDetails {
|
||||
onToc: (toc: TocAst) => void
|
||||
onLineMarkers?: (lineMarkers: LineMarkers[]) => void
|
||||
export interface Configuration {
|
||||
onTocChange: (toc: TocAst) => void
|
||||
useAlternativeBreaks?: boolean
|
||||
lineOffset?: number
|
||||
headlineAnchors?: boolean
|
||||
slideSections?: boolean
|
||||
}
|
||||
|
||||
export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
|
||||
export abstract class MarkdownItConfigurator<T extends Configuration> {
|
||||
protected readonly options: T
|
||||
protected configurations: MarkdownIt.PluginSimple[] = []
|
||||
protected postConfigurations: MarkdownIt.PluginSimple[] = []
|
||||
|
||||
constructor(options: T) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
public pushConfig(plugin: MarkdownIt.PluginSimple): this {
|
||||
this.configurations.push(plugin)
|
||||
return this
|
||||
this.configure()
|
||||
}
|
||||
|
||||
public buildConfiguredMarkdownIt(): MarkdownIt {
|
||||
|
@ -68,28 +57,27 @@ export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
|
|||
langPrefix: '',
|
||||
typographer: true
|
||||
})
|
||||
this.configure(markdownIt)
|
||||
this.configurations.forEach((configuration) => markdownIt.use(configuration))
|
||||
this.postConfigurations.forEach((postConfiguration) => markdownIt.use(postConfiguration))
|
||||
return markdownIt
|
||||
}
|
||||
|
||||
protected configure(markdownIt: MarkdownIt): void {
|
||||
protected configure(): void {
|
||||
this.configurations.push(
|
||||
plantumlWithError,
|
||||
KatexReplacer.markdownItPlugin,
|
||||
YoutubeReplacer.markdownItPlugin,
|
||||
VimeoReplacer.markdownItPlugin,
|
||||
GistReplacer.markdownItPlugin,
|
||||
youtubeMarkdownItPlugin,
|
||||
vimeoMarkdownItPlugin,
|
||||
gistMarkdownItPlugin,
|
||||
asciinemaMarkdownItPlugin,
|
||||
legacyPdfShortCode,
|
||||
legacySlideshareShortCode,
|
||||
legacySpeakerdeckShortCode,
|
||||
AsciinemaReplacer.markdownItPlugin,
|
||||
highlightedCode,
|
||||
quoteExtraColor,
|
||||
quoteExtra('name', 'user'),
|
||||
quoteExtra('time', 'clock-o'),
|
||||
documentTableOfContents(this.options.onToc),
|
||||
documentTableOfContents(this.options.onTocChange),
|
||||
twitterEmojis,
|
||||
abbreviation,
|
||||
definitionList,
|
||||
|
@ -104,18 +92,6 @@ export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
|
|||
spoilerContainer
|
||||
)
|
||||
|
||||
if (this.options.headlineAnchors) {
|
||||
this.configurations.push(headlineAnchors)
|
||||
}
|
||||
|
||||
if (this.options.slideSections) {
|
||||
this.configurations.push(addSlideSectionsMarkdownItPlugin)
|
||||
}
|
||||
|
||||
if (this.options.onLineMarkers) {
|
||||
this.configurations.push(lineNumberMarker(this.options.onLineMarkers, this.options.lineOffset ?? 0))
|
||||
}
|
||||
|
||||
this.postConfigurations.push(linkifyExtra, MarkdownItParserDebugger)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Configuration } from './markdown-it-configurator'
|
||||
import { MarkdownItConfigurator } from './markdown-it-configurator'
|
||||
import { addSlideSectionsMarkdownItPlugin } from '../markdown-it-plugins/reveal-sections'
|
||||
|
||||
export class SlideshowMarkdownItConfigurator extends MarkdownItConfigurator<Configuration> {
|
||||
protected configure(): void {
|
||||
super.configure()
|
||||
|
||||
this.configurations.push(addSlideSectionsMarkdownItPlugin)
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import type MarkdownIt from 'markdown-it'
|
|||
import markdownItContainer from 'markdown-it-container'
|
||||
import type Renderer from 'markdown-it/lib/renderer'
|
||||
import type Token from 'markdown-it/lib/token'
|
||||
import type { MarkdownItPlugin } from '../replace-components/ComponentReplacer'
|
||||
import type { MarkdownItPlugin } from '../replace-components/component-replacer'
|
||||
|
||||
export type RenderContainerReturn = (
|
||||
tokens: Token[],
|
||||
|
|
|
@ -11,7 +11,7 @@ import type { RenderRule } from 'markdown-it/lib/renderer'
|
|||
import type Renderer from 'markdown-it/lib/renderer'
|
||||
import type Token from 'markdown-it/lib/token'
|
||||
import { store } from '../../../redux'
|
||||
import type { MarkdownItPlugin } from '../replace-components/ComponentReplacer'
|
||||
import type { MarkdownItPlugin } from '../replace-components/component-replacer'
|
||||
|
||||
export const plantumlWithError: MarkdownItPlugin = (markdownIt: MarkdownIt) => {
|
||||
const plantumlServer = store.getState().config.plantumlServer
|
||||
|
|
|
@ -8,7 +8,7 @@ import type MarkdownIt from 'markdown-it'
|
|||
import { escapeHtml } from 'markdown-it/lib/common/utils'
|
||||
import markdownItContainer from 'markdown-it-container'
|
||||
import type Token from 'markdown-it/lib/token'
|
||||
import type { MarkdownItPlugin } from '../replace-components/ComponentReplacer'
|
||||
import type { MarkdownItPlugin } from '../replace-components/component-replacer'
|
||||
import type { RenderContainerReturn } from './alert-container'
|
||||
|
||||
export const spoilerRegEx = /^spoiler\s+(.*)$/
|
||||
|
|
|
@ -7,14 +7,11 @@
|
|||
import React, { useEffect, useRef } from 'react'
|
||||
import './abc.scss'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
import type { CodeProps } from '../code-block-component-replacer'
|
||||
|
||||
const log = new Logger('AbcFrame')
|
||||
|
||||
export interface AbcFrameProps {
|
||||
code: string
|
||||
}
|
||||
|
||||
export const AbcFrame: React.FC<AbcFrameProps> = ({ code }) => {
|
||||
export const AbcFrame: React.FC<CodeProps> = ({ code }) => {
|
||||
const container = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -23,8 +20,8 @@ export const AbcFrame: React.FC<AbcFrameProps> = ({ code }) => {
|
|||
}
|
||||
const actualContainer = container.current
|
||||
import(/* webpackChunkName: "abc.js" */ 'abcjs')
|
||||
.then((imp) => {
|
||||
imp.renderAbc(actualContainer, code, {})
|
||||
.then((importedLibrary) => {
|
||||
importedLibrary.renderAbc(actualContainer, code, {})
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
log.error('Error while loading abcjs', error)
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Element } from 'domhandler'
|
||||
import React from 'react'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { AbcFrame } from './abc-frame'
|
||||
|
||||
/**
|
||||
* Detects code blocks with "abc" as language and renders them as ABC.js
|
||||
*/
|
||||
export class AbcReplacer extends ComponentReplacer {
|
||||
getReplacement(codeNode: Element): React.ReactElement | undefined {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
codeNode.attribs['data-highlight-language'] !== 'abc' ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const code = ComponentReplacer.extractTextChildContent(codeNode)
|
||||
|
||||
return <AbcFrame code={code} />
|
||||
}
|
||||
}
|
|
@ -6,12 +6,9 @@
|
|||
|
||||
import React from 'react'
|
||||
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
|
||||
import type { IdProps } from '../custom-tag-with-id-component-replacer'
|
||||
|
||||
export interface AsciinemaFrameProps {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const AsciinemaFrame: React.FC<AsciinemaFrameProps> = ({ id }) => {
|
||||
export const AsciinemaFrame: React.FC<IdProps> = ({ id }) => {
|
||||
return (
|
||||
<OneClickEmbedding
|
||||
containerClassName={'embed-responsive embed-responsive-16by9'}
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Element } from 'domhandler'
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import React from 'react'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { getAttributesFromHedgeDocTag } from '../utils'
|
||||
import { AsciinemaFrame } from './asciinema-frame'
|
||||
import { replaceAsciinemaLink } from './replace-asciinema-link'
|
||||
|
||||
/**
|
||||
* Detects code blocks with "asciinema" as language and renders them Asciinema frame
|
||||
*/
|
||||
export class AsciinemaReplacer extends ComponentReplacer {
|
||||
public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => {
|
||||
markdownItRegex(markdownIt, replaceAsciinemaLink)
|
||||
}
|
||||
|
||||
public getReplacement(node: Element): React.ReactElement | undefined {
|
||||
const attributes = getAttributesFromHedgeDocTag(node, 'asciinema')
|
||||
if (attributes && attributes.id) {
|
||||
const asciinemaId = attributes.id
|
||||
return <AsciinemaFrame id={asciinemaId} />
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@
|
|||
*/
|
||||
|
||||
import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
||||
import type MarkdownIt from 'markdown-it/lib'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
|
||||
const protocolRegex = /(?:http(?:s)?:\/\/)?/
|
||||
const domainRegex = /(?:asciinema\.org\/a\/)/
|
||||
|
@ -13,6 +15,10 @@ const tailRegex = /(?:[./?#].*)?/
|
|||
const gistUrlRegex = new RegExp(`(?:${protocolRegex.source}${domainRegex.source}${idRegex.source}${tailRegex.source})`)
|
||||
const linkRegex = new RegExp(`^${gistUrlRegex.source}$`, 'i')
|
||||
|
||||
export const asciinemaMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt) => {
|
||||
markdownItRegex(markdownIt, replaceAsciinemaLink)
|
||||
}
|
||||
|
||||
export const replaceAsciinemaLink: RegexOptions = {
|
||||
name: 'asciinema-link',
|
||||
regex: linkRegex,
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { ValidReactDomElement } from './component-replacer'
|
||||
import { ComponentReplacer } from './component-replacer'
|
||||
import type { FunctionComponent } from 'react'
|
||||
import React from 'react'
|
||||
import type { Element } from 'domhandler'
|
||||
|
||||
export interface CodeProps {
|
||||
code: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given checked node is a code block with a specific language attribute and creates an react-element that receives the code.
|
||||
*/
|
||||
export class CodeBlockComponentReplacer extends ComponentReplacer {
|
||||
constructor(private component: FunctionComponent<CodeProps>, private language: string) {
|
||||
super()
|
||||
}
|
||||
|
||||
replace(node: Element): ValidReactDomElement | undefined {
|
||||
const code = CodeBlockComponentReplacer.extractTextFromCodeNode(node, this.language)
|
||||
return code ? React.createElement(this.component, { code: code }) : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the text content if the given {@link Element} is a code block with a specific language.
|
||||
*
|
||||
* @param element The {@link Element} to check.
|
||||
* @param language The language that code block should be assigned to.
|
||||
* @return The text content or undefined if the element isn't a code block or has the wrong language attribute.
|
||||
*/
|
||||
public static extractTextFromCodeNode(element: Element, language: string): string | undefined {
|
||||
return element.name === 'code' && element.attribs['data-highlight-language'] === language && element.children[0]
|
||||
? ComponentReplacer.extractTextChildContent(element)
|
||||
: undefined
|
||||
}
|
||||
}
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import type { Element } from 'domhandler'
|
||||
import { isTag } from 'domhandler'
|
||||
import type { NativeRenderer, SubNodeTransform, ValidReactDomElement } from '../ComponentReplacer'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import type { NativeRenderer, SubNodeTransform, ValidReactDomElement } from '../component-replacer'
|
||||
import { ComponentReplacer } from '../component-replacer'
|
||||
|
||||
/**
|
||||
* Checks if the given node is a blockquote color definition
|
||||
|
@ -42,7 +42,7 @@ const findBlockquoteColorParentElement = (nodes: Element[]): Element | undefined
|
|||
* If a color tag was found then the color will be applied to the node as border.
|
||||
*/
|
||||
export class ColoredBlockquoteReplacer extends ComponentReplacer {
|
||||
public getReplacement(
|
||||
public replace(
|
||||
node: Element,
|
||||
subNodeTransform: SubNodeTransform,
|
||||
nativeRenderer: NativeRenderer
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Element, NodeWithChildren } from 'domhandler'
|
||||
import type { Element } from 'domhandler'
|
||||
import { isText } from 'domhandler'
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import type { ReactElement } from 'react'
|
||||
|
@ -17,6 +17,10 @@ export type NativeRenderer = () => ValidReactDomElement
|
|||
|
||||
export type MarkdownItPlugin = MarkdownIt.PluginSimple | MarkdownIt.PluginWithOptions | MarkdownIt.PluginWithParams
|
||||
|
||||
export const REPLACE_WITH_NOTHING = null
|
||||
export const DO_NOT_REPLACE = undefined
|
||||
export type NodeReplacement = ValidReactDomElement | typeof REPLACE_WITH_NOTHING | typeof DO_NOT_REPLACE
|
||||
|
||||
/**
|
||||
* Base class for all component replacers.
|
||||
* Component replacers detect structures in the HTML DOM from markdown it
|
||||
|
@ -29,7 +33,7 @@ export abstract class ComponentReplacer {
|
|||
* @param node the node with the text node child
|
||||
* @return the string content
|
||||
*/
|
||||
protected static extractTextChildContent(node: NodeWithChildren): string {
|
||||
protected static extractTextChildContent(node: Element): string {
|
||||
const childrenTextNode = node.children[0]
|
||||
return isText(childrenTextNode) ? childrenTextNode.data : ''
|
||||
}
|
||||
|
@ -39,12 +43,12 @@ export abstract class ComponentReplacer {
|
|||
*
|
||||
* @param node The current html dom node
|
||||
* @param subNodeTransform should be used to convert child elements of the current node
|
||||
* @param nativeRenderer renders the current node as it is without any replacement.
|
||||
* @param nativeRenderer renders the current node without any replacement
|
||||
* @return the replacement for the current node or undefined if the current replacer replacer hasn't done anything.
|
||||
*/
|
||||
public abstract getReplacement(
|
||||
public abstract replace(
|
||||
node: Element,
|
||||
subNodeTransform: SubNodeTransform,
|
||||
nativeRenderer: NativeRenderer
|
||||
): ValidReactDomElement | undefined
|
||||
): NodeReplacement
|
||||
}
|
|
@ -6,27 +6,20 @@
|
|||
|
||||
import type { Element } from 'domhandler'
|
||||
import React from 'react'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { ComponentReplacer } from '../component-replacer'
|
||||
import { CsvTable } from './csv-table'
|
||||
import { CodeBlockComponentReplacer } from '../code-block-component-replacer'
|
||||
|
||||
/**
|
||||
* Detects code blocks with "csv" as language and renders them as table.
|
||||
*/
|
||||
export class CsvReplacer extends ComponentReplacer {
|
||||
public getReplacement(codeNode: Element): React.ReactElement | undefined {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
codeNode.attribs['data-highlight-language'] !== 'csv' ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
public replace(codeNode: Element): React.ReactElement | undefined {
|
||||
const code = CodeBlockComponentReplacer.extractTextFromCodeNode(codeNode, 'csv')
|
||||
if (!code) {
|
||||
return
|
||||
}
|
||||
|
||||
const code = ComponentReplacer.extractTextChildContent(codeNode)
|
||||
|
||||
const extraData = codeNode.attribs['data-extra']
|
||||
const extraRegex = /\s*(delimiter=([^\s]*))?\s*(header)?/
|
||||
const extraInfos = extraRegex.exec(extraData)
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { NodeReplacement } from './component-replacer'
|
||||
import { ComponentReplacer } from './component-replacer'
|
||||
import type { FunctionComponent } from 'react'
|
||||
import React from 'react'
|
||||
import type { Element } from 'domhandler'
|
||||
|
||||
export interface IdProps {
|
||||
id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces custom tags that have just an id (<app-something id="something"/>) with react elements.
|
||||
*/
|
||||
export class CustomTagWithIdComponentReplacer extends ComponentReplacer {
|
||||
constructor(private component: FunctionComponent<IdProps>, private tagName: string) {
|
||||
super()
|
||||
}
|
||||
|
||||
public replace(node: Element): NodeReplacement {
|
||||
const id = this.extractId(node)
|
||||
return id ? React.createElement(this.component, { id: id }) : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given {@link Element} is a custom tag and extracts its `id` attribute.
|
||||
*
|
||||
* @param element The element to check.
|
||||
* @return the extracted id or undefined if the element isn't a custom tag or has no id attribute.
|
||||
*/
|
||||
private extractId(element: Element): string | undefined {
|
||||
return element.name === `app-${this.tagName}` && element.attribs && element.attribs.id
|
||||
? element.attribs.id
|
||||
: undefined
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Element } from 'domhandler'
|
||||
import React from 'react'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { FlowChart } from './flowchart/flowchart'
|
||||
|
||||
/**
|
||||
* Detects code blocks with "flow" as language and renders them as flow chart.
|
||||
*/
|
||||
export class FlowchartReplacer extends ComponentReplacer {
|
||||
public getReplacement(codeNode: Element): React.ReactElement | undefined {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
codeNode.attribs['data-highlight-language'] !== 'flow' ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const code = ComponentReplacer.extractTextChildContent(codeNode)
|
||||
|
||||
return <FlowChart code={code} />
|
||||
}
|
||||
}
|
|
@ -7,9 +7,9 @@
|
|||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useIsDarkModeActivated } from '../../../../../hooks/common/use-is-dark-mode-activated'
|
||||
import { Logger } from '../../../../../utils/logger'
|
||||
import { cypressId } from '../../../../../utils/cypress-attribute'
|
||||
import { useIsDarkModeActivated } from '../../../../hooks/common/use-is-dark-mode-activated'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||
|
||||
const log = new Logger('FlowChart')
|
||||
|
||||
|
@ -30,8 +30,8 @@ export const FlowChart: React.FC<FlowChartProps> = ({ code }) => {
|
|||
}
|
||||
const currentDiagramRef = diagramRef.current
|
||||
import(/* webpackChunkName: "flowchart.js" */ 'flowchart.js')
|
||||
.then((imp) => {
|
||||
const parserOutput = imp.parse(code)
|
||||
.then((importedLibrary) => {
|
||||
const parserOutput = importedLibrary.parse(code)
|
||||
try {
|
||||
parserOutput.drawSVG(currentDiagramRef, {
|
||||
'line-width': 2,
|
|
@ -8,17 +8,16 @@ import React, { useCallback } from 'react'
|
|||
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||
import './gist-frame.scss'
|
||||
import { useResizeGistFrame } from './use-resize-gist-frame'
|
||||
|
||||
export interface GistFrameProps {
|
||||
id: string
|
||||
}
|
||||
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
|
||||
import preview from './gist-preview.png'
|
||||
import type { IdProps } from '../custom-tag-with-id-component-replacer'
|
||||
|
||||
/**
|
||||
* This component renders a GitHub Gist by placing the gist URL in an {@link HTMLIFrameElement iframe}.
|
||||
*
|
||||
* @param id The id of the gist
|
||||
*/
|
||||
export const GistFrame: React.FC<GistFrameProps> = ({ id }) => {
|
||||
export const GistFrame: React.FC<IdProps> = ({ id }) => {
|
||||
const [frameHeight, onStartResizing] = useResizeGistFrame(150)
|
||||
|
||||
const onStart = useCallback(
|
||||
|
@ -29,7 +28,7 @@ export const GistFrame: React.FC<GistFrameProps> = ({ id }) => {
|
|||
)
|
||||
|
||||
return (
|
||||
<span>
|
||||
<OneClickEmbedding previewContainerClassName={'gist-frame'} loadingImageUrl={preview} hoverIcon={'github'}>
|
||||
<iframe
|
||||
sandbox=''
|
||||
{...cypressId('gh-gist')}
|
||||
|
@ -42,6 +41,6 @@ export const GistFrame: React.FC<GistFrameProps> = ({ id }) => {
|
|||
<span className={'gist-resizer-row'}>
|
||||
<span className={'gist-resizer'} onMouseDown={onStart} onTouchStart={onStart} />
|
||||
</span>
|
||||
</span>
|
||||
</OneClickEmbedding>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import { replaceGistLink } from './replace-gist-link'
|
||||
import { replaceLegacyGistShortCode } from './replace-legacy-gist-short-code'
|
||||
|
||||
export const gistMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => {
|
||||
markdownItRegex(markdownIt, replaceGistLink)
|
||||
markdownItRegex(markdownIt, replaceLegacyGistShortCode)
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Element } from 'domhandler'
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import React from 'react'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
|
||||
import { getAttributesFromHedgeDocTag } from '../utils'
|
||||
import { GistFrame } from './gist-frame'
|
||||
import preview from './gist-preview.png'
|
||||
import { replaceGistLink } from './replace-gist-link'
|
||||
import { replaceLegacyGistShortCode } from './replace-legacy-gist-short-code'
|
||||
|
||||
/**
|
||||
* Detects "app-gist" tags and renders them as gist frames.
|
||||
*/
|
||||
export class GistReplacer extends ComponentReplacer {
|
||||
public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => {
|
||||
markdownItRegex(markdownIt, replaceGistLink)
|
||||
markdownItRegex(markdownIt, replaceLegacyGistShortCode)
|
||||
}
|
||||
|
||||
public getReplacement(node: Element): React.ReactElement | undefined {
|
||||
const attributes = getAttributesFromHedgeDocTag(node, 'gist')
|
||||
if (attributes && attributes.id) {
|
||||
const gistId = attributes.id
|
||||
return (
|
||||
<OneClickEmbedding
|
||||
previewContainerClassName={'gist-frame'}
|
||||
loadingImageUrl={preview}
|
||||
hoverIcon={'github'}
|
||||
tooltip={'click to load gist'}>
|
||||
<GistFrame id={gistId} />
|
||||
</OneClickEmbedding>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,14 +10,11 @@ import { ShowIf } from '../../../common/show-if/show-if'
|
|||
import { useFrontendBaseUrl } from '../../../../hooks/common/use-frontend-base-url'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||
import type { CodeProps } from '../code-block-component-replacer'
|
||||
|
||||
const log = new Logger('GraphvizFrame')
|
||||
|
||||
export interface GraphvizFrameProps {
|
||||
code: string
|
||||
}
|
||||
|
||||
export const GraphvizFrame: React.FC<GraphvizFrameProps> = ({ code }) => {
|
||||
export const GraphvizFrame: React.FC<CodeProps> = ({ code }) => {
|
||||
const container = useRef<HTMLDivElement>(null)
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Element } from 'domhandler'
|
||||
import React from 'react'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { GraphvizFrame } from './graphviz-frame'
|
||||
|
||||
/**
|
||||
* Detects code blocks with "graphviz" as language and renders them as graphviz graph.
|
||||
*/
|
||||
export class GraphvizReplacer extends ComponentReplacer {
|
||||
getReplacement(codeNode: Element): React.ReactElement | undefined {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
codeNode.attribs['data-highlight-language'] !== 'graphviz' ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const code = ComponentReplacer.extractTextChildContent(codeNode)
|
||||
|
||||
return <GraphvizFrame code={code} />
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import type { Element } from 'domhandler'
|
||||
import React from 'react'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { ComponentReplacer } from '../component-replacer'
|
||||
import { HighlightedCode } from './highlighted-code/highlighted-code'
|
||||
|
||||
/**
|
||||
|
@ -15,14 +15,15 @@ import { HighlightedCode } from './highlighted-code/highlighted-code'
|
|||
export class HighlightedCodeReplacer extends ComponentReplacer {
|
||||
private lastLineNumber = 0
|
||||
|
||||
public getReplacement(codeNode: Element): React.ReactElement | undefined {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
private extractCode(codeNode: Element): string | undefined {
|
||||
return codeNode.name === 'code' && !!codeNode.attribs['data-highlight-language'] && !!codeNode.children[0]
|
||||
? ComponentReplacer.extractTextChildContent(codeNode)
|
||||
: undefined
|
||||
}
|
||||
|
||||
public replace(codeNode: Element): React.ReactElement | undefined {
|
||||
const code = this.extractCode(codeNode)
|
||||
if (!code) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -42,7 +43,6 @@ export class HighlightedCodeReplacer extends ComponentReplacer {
|
|||
|
||||
const startLineNumber =
|
||||
startLineNumberAttribute === '+' ? this.lastLineNumber : parseInt(startLineNumberAttribute) || 1
|
||||
const code = ComponentReplacer.extractTextChildContent(codeNode)
|
||||
|
||||
if (showLineNumbers) {
|
||||
this.lastLineNumber = startLineNumber + code.split('\n').filter((line) => !!line).length
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import type { Element } from 'domhandler'
|
||||
import React from 'react'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { ComponentReplacer } from '../component-replacer'
|
||||
import { ProxyImageFrame } from './proxy-image-frame'
|
||||
|
||||
export type ImageClickHandler = (event: React.MouseEvent<HTMLImageElement, MouseEvent>) => void
|
||||
|
@ -22,8 +22,8 @@ export class ImageReplacer extends ComponentReplacer {
|
|||
this.clickHandler = clickHandler
|
||||
}
|
||||
|
||||
public getReplacement(node: Element): React.ReactElement | undefined {
|
||||
if (node.name === 'img' && node.attribs) {
|
||||
public replace(node: Element): React.ReactElement | undefined {
|
||||
if (node.name === 'img') {
|
||||
return (
|
||||
<ProxyImageFrame
|
||||
id={node.attribs.id}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { isTag } from 'domhandler'
|
|||
import type MarkdownIt from 'markdown-it'
|
||||
import mathJax from 'markdown-it-mathjax'
|
||||
import React from 'react'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { ComponentReplacer } from '../component-replacer'
|
||||
import './katex.scss'
|
||||
|
||||
/**
|
||||
|
@ -52,7 +52,7 @@ export class KatexReplacer extends ComponentReplacer {
|
|||
afterDisplayMath: '</app-katex>'
|
||||
})
|
||||
|
||||
public getReplacement(node: Element): React.ReactElement | undefined {
|
||||
public replace(node: Element): React.ReactElement | undefined {
|
||||
const katex = getNodeIfKatexBlock(node) || getNodeIfInlineKatex(node)
|
||||
if (katex?.children && katex.children[0]) {
|
||||
const mathJaxContent = ComponentReplacer.extractTextChildContent(katex)
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
*/
|
||||
|
||||
import type { Element } from 'domhandler'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { ComponentReplacer } from '../component-replacer'
|
||||
|
||||
/**
|
||||
* Detects line markers and suppresses them in the resulting DOM.
|
||||
*/
|
||||
export class LinemarkerReplacer extends ComponentReplacer {
|
||||
public getReplacement(codeNode: Element): null | undefined {
|
||||
public replace(codeNode: Element): null | undefined {
|
||||
return codeNode.name === 'app-linemarker' ? null : undefined
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
*/
|
||||
import type { Element } from 'domhandler'
|
||||
import React from 'react'
|
||||
import type { NativeRenderer, SubNodeTransform, ValidReactDomElement } from '../ComponentReplacer'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import type { NativeRenderer, SubNodeTransform, ValidReactDomElement } from '../component-replacer'
|
||||
import { ComponentReplacer } from '../component-replacer'
|
||||
|
||||
export const createJumpToMarkClickEventHandler = (id: string) => {
|
||||
return (event: React.MouseEvent<HTMLElement, MouseEvent>): void => {
|
||||
|
@ -25,7 +25,7 @@ export class LinkReplacer extends ComponentReplacer {
|
|||
super()
|
||||
}
|
||||
|
||||
public getReplacement(
|
||||
public replace(
|
||||
node: Element,
|
||||
subNodeTransform: SubNodeTransform,
|
||||
nativeRenderer: NativeRenderer
|
||||
|
|
|
@ -10,18 +10,15 @@ import { LockButton } from '../../../common/lock-button/lock-button'
|
|||
import '../../utils/button-inside.scss'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||
import type { CodeProps } from '../code-block-component-replacer'
|
||||
|
||||
const log = new Logger('MarkmapFrame')
|
||||
|
||||
export interface MarkmapFrameProps {
|
||||
code: string
|
||||
}
|
||||
|
||||
const blockHandler = (event: Event): void => {
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
export const MarkmapFrame: React.FC<MarkmapFrameProps> = ({ code }) => {
|
||||
export const MarkmapFrame: React.FC<CodeProps> = ({ code }) => {
|
||||
const { t } = useTranslation()
|
||||
const diagramContainer = useRef<HTMLDivElement>(null)
|
||||
const [disablePanAndZoom, setDisablePanAndZoom] = useState(true)
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Element } from 'domhandler'
|
||||
import React from 'react'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { MarkmapFrame } from './markmap-frame'
|
||||
|
||||
/**
|
||||
* Detects code blocks with 'markmap' as language and renders them with Markmap.
|
||||
*/
|
||||
export class MarkmapReplacer extends ComponentReplacer {
|
||||
getReplacement(codeNode: Element): React.ReactElement | undefined {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
codeNode.attribs['data-highlight-language'] !== 'markmap' ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const code = ComponentReplacer.extractTextChildContent(codeNode)
|
||||
|
||||
return <MarkmapFrame code={code} />
|
||||
}
|
||||
}
|
|
@ -10,11 +10,9 @@ import { useTranslation } from 'react-i18next'
|
|||
import { ShowIf } from '../../../common/show-if/show-if'
|
||||
import './mermaid.scss'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
import type { CodeProps } from '../code-block-component-replacer'
|
||||
|
||||
const log = new Logger('MermaidChart')
|
||||
export interface MermaidChartProps {
|
||||
code: string
|
||||
}
|
||||
|
||||
interface MermaidParseError {
|
||||
str: string
|
||||
|
@ -22,7 +20,7 @@ interface MermaidParseError {
|
|||
|
||||
let mermaidInitialized = false
|
||||
|
||||
export const MermaidChart: React.FC<MermaidChartProps> = ({ code }) => {
|
||||
export const MermaidChart: React.FC<CodeProps> = ({ code }) => {
|
||||
const diagramContainer = useRef<HTMLDivElement>(null)
|
||||
const [error, setError] = useState<string>()
|
||||
const { t } = useTranslation()
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Element } from 'domhandler'
|
||||
import React from 'react'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { MermaidChart } from './mermaid-chart'
|
||||
|
||||
/**
|
||||
* Detects code blocks with 'mermaid' as language and renders them with mermaid.
|
||||
*/
|
||||
export class MermaidReplacer extends ComponentReplacer {
|
||||
getReplacement(codeNode: Element): React.ReactElement | undefined {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
codeNode.attribs['data-highlight-language'] !== 'mermaid' ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const code = ComponentReplacer.extractTextChildContent(codeNode)
|
||||
|
||||
return <MermaidChart code={code} />
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Element } from 'domhandler'
|
||||
import React, { Fragment } from 'react'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { MermaidChart } from '../mermaid/mermaid-chart'
|
||||
import { DeprecationWarning } from './deprecation-warning'
|
||||
|
||||
/**
|
||||
* Detects code blocks with 'sequence' as language and renders them as
|
||||
* sequence diagram with mermaid.
|
||||
*/
|
||||
export class SequenceDiagramReplacer extends ComponentReplacer {
|
||||
getReplacement(codeNode: Element): React.ReactElement | undefined {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
codeNode.attribs['data-highlight-language'] !== 'sequence' ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const code = ComponentReplacer.extractTextChildContent(codeNode)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<DeprecationWarning />
|
||||
<MermaidChart code={'sequenceDiagram\n' + code} />
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react'
|
||||
import type { CodeProps } from '../code-block-component-replacer'
|
||||
import { MermaidChart } from '../mermaid/mermaid-chart'
|
||||
import { DeprecationWarning } from './deprecation-warning'
|
||||
|
||||
/**
|
||||
* Renders a sequence diagram with a deprecation notice.
|
||||
*
|
||||
* @param code the sequence diagram code
|
||||
*/
|
||||
export const SequenceDiagram: React.FC<CodeProps> = ({ code }) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<DeprecationWarning />
|
||||
<MermaidChart code={'sequenceDiagram\n' + code} />
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
export interface TaskListProps {
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||
checked: boolean
|
||||
lineInMarkdown?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a task list checkbox.
|
||||
*
|
||||
* @param onTaskCheckedChange A callback that is executed if the checkbox was clicked. If this prop is omitted then the checkbox will be disabled.
|
||||
* @param checked Determines if the checkbox should be rendered as checked
|
||||
* @param lineInMarkdown Defines the line in the markdown code this checkbox is mapped to. The information is send with the onTaskCheckedChange callback.
|
||||
*/
|
||||
export const TaskListCheckbox: React.FC<TaskListProps> = ({ onTaskCheckedChange, checked, lineInMarkdown }) => {
|
||||
const onChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
if (onTaskCheckedChange && lineInMarkdown !== undefined) {
|
||||
onTaskCheckedChange(lineInMarkdown, event.currentTarget.checked)
|
||||
}
|
||||
},
|
||||
[lineInMarkdown, onTaskCheckedChange]
|
||||
)
|
||||
|
||||
return (
|
||||
<input
|
||||
disabled={onTaskCheckedChange === undefined}
|
||||
className='task-list-item-checkbox'
|
||||
type='checkbox'
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -7,7 +7,8 @@
|
|||
import type { Element } from 'domhandler'
|
||||
import type { ReactElement } from 'react'
|
||||
import React from 'react'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { ComponentReplacer } from '../component-replacer'
|
||||
import { TaskListCheckbox } from './task-list-checkbox'
|
||||
|
||||
export type TaskCheckedChangeHandler = (lineInMarkdown: number, checked: boolean) => void
|
||||
|
||||
|
@ -16,34 +17,30 @@ export type TaskCheckedChangeHandler = (lineInMarkdown: number, checked: boolean
|
|||
*/
|
||||
export class TaskListReplacer extends ComponentReplacer {
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||
private readonly frontmatterLinesOffset
|
||||
|
||||
constructor(onTaskCheckedChange?: TaskCheckedChangeHandler, frontmatterLinesOffset?: number) {
|
||||
constructor(frontmatterLinesToSkip?: number, onTaskCheckedChange?: TaskCheckedChangeHandler) {
|
||||
super()
|
||||
this.onTaskCheckedChange = onTaskCheckedChange
|
||||
this.frontmatterLinesOffset = frontmatterLinesOffset ?? 0
|
||||
}
|
||||
|
||||
handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const lineNum = Number(event.currentTarget.dataset.line)
|
||||
if (this.onTaskCheckedChange) {
|
||||
this.onTaskCheckedChange(lineNum + this.frontmatterLinesOffset, event.currentTarget.checked)
|
||||
this.onTaskCheckedChange = (lineInMarkdown, checked) => {
|
||||
if (onTaskCheckedChange === undefined || frontmatterLinesToSkip === undefined) {
|
||||
return
|
||||
}
|
||||
onTaskCheckedChange(frontmatterLinesToSkip + lineInMarkdown, checked)
|
||||
}
|
||||
}
|
||||
|
||||
public getReplacement(node: Element): ReactElement | undefined {
|
||||
public replace(node: Element): ReactElement | undefined {
|
||||
if (node.attribs?.class !== 'task-list-item-checkbox') {
|
||||
return
|
||||
}
|
||||
const lineInMarkdown = Number(node.attribs['data-line'])
|
||||
if (isNaN(lineInMarkdown)) {
|
||||
return undefined
|
||||
}
|
||||
return (
|
||||
<input
|
||||
disabled={this.onTaskCheckedChange === undefined}
|
||||
className='task-list-item-checkbox'
|
||||
type='checkbox'
|
||||
<TaskListCheckbox
|
||||
onTaskCheckedChange={this.onTaskCheckedChange}
|
||||
checked={node.attribs.checked !== undefined}
|
||||
onChange={this.handleCheckboxChange}
|
||||
id={node.attribs.id}
|
||||
data-line={node.attribs['data-line']}
|
||||
lineInMarkdown={lineInMarkdown}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Element } from 'domhandler'
|
||||
|
||||
export const getAttributesFromHedgeDocTag = (node: Element, tagName: string): { [s: string]: string } | undefined => {
|
||||
if (node.name !== `app-${tagName}` || !node.attribs) {
|
||||
return
|
||||
}
|
||||
return node.attribs
|
||||
}
|
|
@ -10,14 +10,11 @@ import { useTranslation } from 'react-i18next'
|
|||
import type { VisualizationSpec } from 'vega-embed'
|
||||
import { ShowIf } from '../../../common/show-if/show-if'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
import type { CodeProps } from '../code-block-component-replacer'
|
||||
|
||||
const log = new Logger('VegaChart')
|
||||
|
||||
export interface VegaChartProps {
|
||||
code: string
|
||||
}
|
||||
|
||||
export const VegaChart: React.FC<VegaChartProps> = ({ code }) => {
|
||||
export const VegaChart: React.FC<CodeProps> = ({ code }) => {
|
||||
const diagramContainer = useRef<HTMLDivElement>(null)
|
||||
const [error, setError] = useState<string>()
|
||||
const { t } = useTranslation()
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Element } from 'domhandler'
|
||||
import React from 'react'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { VegaChart } from './vega-chart'
|
||||
|
||||
/**
|
||||
* Detects code blocks with 'vega-lite' as language and renders them with Vega.
|
||||
*/
|
||||
export class VegaReplacer extends ComponentReplacer {
|
||||
getReplacement(codeNode: Element): React.ReactElement | undefined {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
codeNode.attribs['data-highlight-language'] !== 'vega-lite' ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const code = ComponentReplacer.extractTextChildContent(codeNode)
|
||||
|
||||
return <VegaChart code={code} />
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import React, { useCallback } from 'react'
|
||||
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
|
||||
import type { IdProps } from '../custom-tag-with-id-component-replacer'
|
||||
|
||||
interface VimeoApiResponse {
|
||||
// Vimeo uses strange names for their fields. ESLint doesn't like that.
|
||||
|
@ -13,11 +14,7 @@ interface VimeoApiResponse {
|
|||
thumbnail_large?: string
|
||||
}
|
||||
|
||||
export interface VimeoFrameProps {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const VimeoFrame: React.FC<VimeoFrameProps> = ({ id }) => {
|
||||
export const VimeoFrame: React.FC<IdProps> = ({ id }) => {
|
||||
const getPreviewImageLink = useCallback(async () => {
|
||||
const response = await fetch(`https://vimeo.com/api/v2/video/${id}.json`, {
|
||||
credentials: 'omit',
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import { replaceVimeoLink } from './replace-vimeo-link'
|
||||
import { replaceLegacyVimeoShortCode } from './replace-legacy-vimeo-short-code'
|
||||
|
||||
export const vimeoMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => {
|
||||
markdownItRegex(markdownIt, replaceVimeoLink)
|
||||
markdownItRegex(markdownIt, replaceLegacyVimeoShortCode)
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Element } from 'domhandler'
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import React from 'react'
|
||||
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'
|
||||
|
||||
/**
|
||||
* Detects 'app-vimeo' tags and renders them as vimeo embedding.
|
||||
*/
|
||||
export class VimeoReplacer extends ComponentReplacer {
|
||||
public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => {
|
||||
markdownItRegex(markdownIt, replaceVimeoLink)
|
||||
markdownItRegex(markdownIt, replaceLegacyVimeoShortCode)
|
||||
}
|
||||
|
||||
public getReplacement(node: Element): React.ReactElement | undefined {
|
||||
const attributes = getAttributesFromHedgeDocTag(node, 'vimeo')
|
||||
if (attributes && attributes.id) {
|
||||
const videoId = attributes.id
|
||||
return <VimeoFrame id={videoId} />
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,12 +6,9 @@
|
|||
|
||||
import React from 'react'
|
||||
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
|
||||
import type { IdProps } from '../custom-tag-with-id-component-replacer'
|
||||
|
||||
export interface YouTubeFrameProps {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const YouTubeFrame: React.FC<YouTubeFrameProps> = ({ id }) => {
|
||||
export const YouTubeFrame: React.FC<IdProps> = ({ id }) => {
|
||||
return (
|
||||
<OneClickEmbedding
|
||||
containerClassName={'embed-responsive embed-responsive-16by9'}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import { replaceYouTubeLink } from './replace-youtube-link'
|
||||
import { replaceLegacyYoutubeShortCode } from './replace-legacy-youtube-short-code'
|
||||
|
||||
export const youtubeMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => {
|
||||
markdownItRegex(markdownIt, replaceYouTubeLink)
|
||||
markdownItRegex(markdownIt, replaceLegacyYoutubeShortCode)
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Element } from 'domhandler'
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import React from 'react'
|
||||
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'
|
||||
|
||||
/**
|
||||
* Detects 'app-youtube' tags and renders them as youtube embedding.
|
||||
*/
|
||||
export class YoutubeReplacer extends ComponentReplacer {
|
||||
public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => {
|
||||
markdownItRegex(markdownIt, replaceYouTubeLink)
|
||||
markdownItRegex(markdownIt, replaceLegacyYoutubeShortCode)
|
||||
}
|
||||
|
||||
public getReplacement(node: Element): React.ReactElement | undefined {
|
||||
const attributes = getAttributesFromHedgeDocTag(node, 'youtube')
|
||||
if (attributes && attributes.id) {
|
||||
const videoId = attributes.id
|
||||
return <YouTubeFrame id={videoId} />
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,11 +16,11 @@ import { REVEAL_STATUS, useReveal } from './hooks/use-reveal'
|
|||
import './slideshow.scss'
|
||||
import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
||||
import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert'
|
||||
import { BasicMarkdownItConfigurator } from './markdown-it-configurator/basic-markdown-it-configurator'
|
||||
import type { SlideOptions } from '../common/note-frontmatter/types'
|
||||
import { processRevealCommentNodes } from './process-reveal-comment-nodes'
|
||||
import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
|
||||
import { LoadingSlide } from './loading-slide'
|
||||
import { SlideshowMarkdownItConfigurator } from './markdown-it-configurator/slideshow-markdown-it-configurator'
|
||||
|
||||
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps {
|
||||
slideOptions: SlideOptions
|
||||
|
@ -44,12 +44,10 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps
|
|||
|
||||
const markdownIt = useMemo(
|
||||
() =>
|
||||
new BasicMarkdownItConfigurator({
|
||||
onToc: (toc) => (tocAst.current = toc),
|
||||
new SlideshowMarkdownItConfigurator({
|
||||
onTocChange: (toc) => (tocAst.current = toc),
|
||||
useAlternativeBreaks,
|
||||
lineOffset,
|
||||
headlineAnchors: false,
|
||||
slideSections: true
|
||||
lineOffset
|
||||
}).buildConfiguredMarkdownIt(),
|
||||
[lineOffset, useAlternativeBreaks]
|
||||
)
|
||||
|
|
2
src/components/markdown-renderer/types.d.ts
vendored
2
src/components/markdown-renderer/types.d.ts
vendored
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export interface LineKeys {
|
||||
export interface LineWithId {
|
||||
line: string
|
||||
id: number
|
||||
}
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Element } from 'domhandler'
|
||||
import { isTag } from 'domhandler'
|
||||
import React, { Suspense } from 'react'
|
||||
import { convertNodeToReactElement } from '@hedgedoc/html-to-react/dist/convertNodeToReactElement'
|
||||
import type {
|
||||
ComponentReplacer,
|
||||
NativeRenderer,
|
||||
SubNodeTransform,
|
||||
ValidReactDomElement
|
||||
} from '../replace-components/ComponentReplacer'
|
||||
import type { LineKeys } from '../types'
|
||||
import type { NodeToReactElementTransformer } from '@hedgedoc/html-to-react/dist/NodeToReactElementTransformer'
|
||||
|
||||
export interface TextDifferenceResult {
|
||||
lines: LineKeys[]
|
||||
lastUsedLineId: number
|
||||
}
|
||||
|
||||
export const calculateKeyFromLineMarker = (node: Element, lineKeys?: LineKeys[]): string | undefined => {
|
||||
if (!node.attribs || lineKeys === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const key = node.attribs['data-key']
|
||||
if (key) {
|
||||
return key
|
||||
}
|
||||
|
||||
const lineMarker = node.prev
|
||||
if (!lineMarker || !isTag(lineMarker) || !lineMarker.attribs) {
|
||||
return
|
||||
}
|
||||
|
||||
const startLineInMarkdown = lineMarker.attribs['data-start-line']
|
||||
const endLineInMarkdown = lineMarker.attribs['data-end-line']
|
||||
if (startLineInMarkdown === undefined || endLineInMarkdown === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const startLineIndex = Number(startLineInMarkdown)
|
||||
const endLineIndex = Number(endLineInMarkdown)
|
||||
const startLine = lineKeys[startLineIndex - 1]
|
||||
const endLine = lineKeys[endLineIndex - 2]
|
||||
if (startLine === undefined || endLine === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
return `${startLine.id}_${endLine.id}`
|
||||
}
|
||||
|
||||
export const findNodeReplacement = (
|
||||
node: Element,
|
||||
allReplacers: ComponentReplacer[],
|
||||
subNodeTransform: SubNodeTransform,
|
||||
nativeRenderer: NativeRenderer
|
||||
): ValidReactDomElement | undefined => {
|
||||
for (const componentReplacer of allReplacers) {
|
||||
const replacement = componentReplacer.getReplacement(node, subNodeTransform, nativeRenderer)
|
||||
if (replacement !== undefined) {
|
||||
return replacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the given node without any replacement
|
||||
*
|
||||
* @param node The node to render
|
||||
* @param key The unique key for the node
|
||||
* @param transform The transform function that should be applied to the child nodes
|
||||
*/
|
||||
export const renderNativeNode = (
|
||||
node: Element,
|
||||
key: string,
|
||||
transform: NodeToReactElementTransformer
|
||||
): ValidReactDomElement => {
|
||||
if (node.attribs === undefined) {
|
||||
node.attribs = {}
|
||||
}
|
||||
|
||||
delete node.attribs['data-key']
|
||||
return convertNodeToReactElement(node, key, transform)
|
||||
}
|
||||
|
||||
export const buildTransformer = (
|
||||
lineKeys: LineKeys[] | undefined,
|
||||
allReplacers: ComponentReplacer[]
|
||||
): NodeToReactElementTransformer => {
|
||||
const transform: NodeToReactElementTransformer = (node, index) => {
|
||||
if (!isTag(node)) {
|
||||
return convertNodeToReactElement(node, index)
|
||||
}
|
||||
const nativeRenderer: NativeRenderer = () => renderNativeNode(node, key, transform)
|
||||
const subNodeTransform: SubNodeTransform = (subNode, subKey) => transform(subNode, subKey)
|
||||
|
||||
const key = calculateKeyFromLineMarker(node, lineKeys) ?? (-index).toString()
|
||||
const tryReplacement = findNodeReplacement(node, allReplacers, subNodeTransform, nativeRenderer)
|
||||
if (tryReplacement === null) {
|
||||
return null
|
||||
} else if (tryReplacement === undefined) {
|
||||
return nativeRenderer()
|
||||
} else {
|
||||
return (
|
||||
<Suspense key={key} fallback={<span>Loading...</span>}>
|
||||
{tryReplacement}
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
}
|
||||
return transform
|
||||
}
|
110
src/components/markdown-renderer/utils/line-id-mapper.test.ts
Normal file
110
src/components/markdown-renderer/utils/line-id-mapper.test.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { LineIdMapper } from './line-id-mapper'
|
||||
|
||||
describe('line id mapper', () => {
|
||||
let lineIdMapper: LineIdMapper
|
||||
|
||||
beforeEach(() => {
|
||||
lineIdMapper = new LineIdMapper()
|
||||
})
|
||||
|
||||
it('should be case sensitive', () => {
|
||||
lineIdMapper.updateLineMapping('this\nis\ntext')
|
||||
expect(lineIdMapper.updateLineMapping('this\nis\nText')).toEqual([
|
||||
{
|
||||
line: 'this',
|
||||
id: 1
|
||||
},
|
||||
{
|
||||
line: 'is',
|
||||
id: 2
|
||||
},
|
||||
{
|
||||
line: 'Text',
|
||||
id: 4
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('should not update line ids of shifted lines', () => {
|
||||
lineIdMapper.updateLineMapping('this\nis\ntext')
|
||||
expect(lineIdMapper.updateLineMapping('this\nis\nmore\ntext')).toEqual([
|
||||
{
|
||||
line: 'this',
|
||||
id: 1
|
||||
},
|
||||
{
|
||||
line: 'is',
|
||||
id: 2
|
||||
},
|
||||
{
|
||||
line: 'more',
|
||||
id: 4
|
||||
},
|
||||
{
|
||||
line: 'text',
|
||||
id: 3
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('should not update line ids if nothing changes', () => {
|
||||
lineIdMapper.updateLineMapping('this\nis\ntext')
|
||||
expect(lineIdMapper.updateLineMapping('this\nis\ntext')).toEqual([
|
||||
{
|
||||
line: 'this',
|
||||
id: 1
|
||||
},
|
||||
{
|
||||
line: 'is',
|
||||
id: 2
|
||||
},
|
||||
{
|
||||
line: 'text',
|
||||
id: 3
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('should not reuse line ids of removed lines', () => {
|
||||
lineIdMapper.updateLineMapping('this\nis\nold')
|
||||
lineIdMapper.updateLineMapping('this\nis')
|
||||
expect(lineIdMapper.updateLineMapping('this\nis\nnew')).toEqual([
|
||||
{
|
||||
line: 'this',
|
||||
id: 1
|
||||
},
|
||||
{
|
||||
line: 'is',
|
||||
id: 2
|
||||
},
|
||||
{
|
||||
line: 'new',
|
||||
id: 4
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('should update line ids for changed lines', () => {
|
||||
lineIdMapper.updateLineMapping('this\nis\nold')
|
||||
expect(lineIdMapper.updateLineMapping('this\nis\nnew')).toEqual([
|
||||
{
|
||||
line: 'this',
|
||||
id: 1
|
||||
},
|
||||
{
|
||||
line: 'is',
|
||||
id: 2
|
||||
},
|
||||
{
|
||||
line: 'new',
|
||||
id: 4
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
108
src/components/markdown-renderer/utils/line-id-mapper.ts
Normal file
108
src/components/markdown-renderer/utils/line-id-mapper.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { LineWithId } from '../types'
|
||||
import type { ArrayChange } from 'diff'
|
||||
import { diffArrays } from 'diff'
|
||||
|
||||
type NewLine = string
|
||||
type LineChange = ArrayChange<NewLine | LineWithId>
|
||||
|
||||
/**
|
||||
* Calculates ids for every line in a given text and memorized the state of the last given text.
|
||||
* It also assigns ids for new lines on every update.
|
||||
*/
|
||||
export class LineIdMapper {
|
||||
private lastLines: LineWithId[] = []
|
||||
private lastUsedLineId = 0
|
||||
|
||||
/**
|
||||
* Calculates a line id mapping for the given line based text by creating a diff
|
||||
* with the last lines code.
|
||||
*
|
||||
* @param newText The new text for which the line ids should be calculated
|
||||
* @return the calculated {@link LineWithId lines with unique ids}
|
||||
*/
|
||||
public updateLineMapping(newText: string): LineWithId[] {
|
||||
const lines = newText.split('\n')
|
||||
const lineDifferences = this.diffNewLinesWithLastLineKeys(lines)
|
||||
const newLineKeys = this.convertChangesToLinesWithIds(lineDifferences)
|
||||
this.lastLines = newLineKeys
|
||||
return newLineKeys
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a diff between the given {@link string lines} and the existing {@link LineWithId lines with unique ids}.
|
||||
* The diff is based on the line content.
|
||||
*
|
||||
* @param lines The plain lines that describe the new state.
|
||||
* @return {@link LineChange line changes} that describe the difference between the given and the old lines. Because of the way the used diff-lib works, the ADDED lines will be tagged as "removed", because if two lines are the same the lib takes the line from the NEW lines, which results in a loss of the unique id.
|
||||
*/
|
||||
private diffNewLinesWithLastLineKeys(lines: string[]): LineChange[] {
|
||||
return diffArrays<NewLine, LineWithId>(lines, this.lastLines, {
|
||||
comparator: (left: NewLine | LineWithId, right: NewLine | LineWithId) => {
|
||||
const leftLine = (left as LineWithId).line ?? (left as NewLine)
|
||||
const rightLine = (right as LineWithId).line ?? (right as NewLine)
|
||||
return leftLine === rightLine
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given {@link LineChange line changes} to {@link lines with unique ids}.
|
||||
* Only not changed or added lines will be processed.
|
||||
*
|
||||
* @param changes The {@link LineChange changes} whose lines should be converted.
|
||||
* @return The created or reused {@link LineWithId lines with ids}
|
||||
*/
|
||||
private convertChangesToLinesWithIds(changes: LineChange[]): LineWithId[] {
|
||||
return changes
|
||||
.filter((change) => LineIdMapper.changeIsNotChangingLines(change) || LineIdMapper.changeIsAddingLines(change))
|
||||
.reduce(
|
||||
(previousLineKeys, currentChange) => [...previousLineKeys, ...this.convertChangeToLinesWithIds(currentChange)],
|
||||
[] as LineWithId[]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines if the given {@link LineChange change} is neither adding or removing lines.
|
||||
*
|
||||
* @param change The {@link LineChange change} to check.
|
||||
* @return {@code true} if the given change is neither adding or removing lines.
|
||||
*/
|
||||
private static changeIsNotChangingLines(change: LineChange): boolean {
|
||||
return change.added === undefined && change.removed === undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines if the given {@link LineChange change} contains new, not existing lines.
|
||||
*
|
||||
* @param change The {@link LineChange change} to check.
|
||||
* @return {@code true} if the given change contains {@link NewLine new lines}
|
||||
*/
|
||||
private static changeIsAddingLines(change: LineChange): change is ArrayChange<NewLine> {
|
||||
return change.removed === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given {@link LineChange change} into {@link LineWithId lines with unique ids} by inspecting the contained lines.
|
||||
* This is done by either reusing the existing ids (if the line wasn't added),
|
||||
* or by assigning new, unused line ids.
|
||||
*
|
||||
* @param change The {@link LineChange change} whose lines should be converted.
|
||||
* @return The created or reused {@link LineWithId lines with ids}
|
||||
*/
|
||||
private convertChangeToLinesWithIds(change: LineChange): LineWithId[] {
|
||||
if (LineIdMapper.changeIsAddingLines(change)) {
|
||||
return change.value.map((line) => {
|
||||
this.lastUsedLineId += 1
|
||||
return { line: line, id: this.lastUsedLineId }
|
||||
})
|
||||
} else {
|
||||
return change.value as LineWithId[]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { diffArrays } from 'diff'
|
||||
import type { TextDifferenceResult } from './html-react-transformer'
|
||||
import type { LineKeys } from '../types'
|
||||
|
||||
export const calculateNewLineNumberMapping = (
|
||||
newMarkdownLines: string[],
|
||||
oldLineKeys: LineKeys[],
|
||||
lastUsedLineId: number
|
||||
): TextDifferenceResult => {
|
||||
const lineDifferences = diffArrays<string, LineKeys>(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 }
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { NodeToReactTransformer } from './node-to-react-transformer'
|
||||
import { Element } from 'domhandler'
|
||||
import type { ReactElement, ReactHTMLElement } from 'react'
|
||||
import type { NodeReplacement } from '../replace-components/component-replacer'
|
||||
import { DO_NOT_REPLACE, REPLACE_WITH_NOTHING } from '../replace-components/component-replacer'
|
||||
|
||||
describe('node to react transformer', () => {
|
||||
let nodeToReactTransformer: NodeToReactTransformer
|
||||
let defaultTestSpanElement: Element
|
||||
|
||||
beforeEach(() => {
|
||||
defaultTestSpanElement = new Element('span', { 'data-test': 'test' })
|
||||
nodeToReactTransformer = new NodeToReactTransformer()
|
||||
})
|
||||
|
||||
describe('replacement', () => {
|
||||
it('can translate an element without any replacer', () => {
|
||||
const translation = nodeToReactTransformer.translateNodeToReactElement(defaultTestSpanElement, 1) as ReactElement
|
||||
expect(translation.type).toEqual('span')
|
||||
expect(translation.props).toEqual({ children: [], 'data-test': 'test' })
|
||||
})
|
||||
|
||||
it('can replace an element nothing', () => {
|
||||
nodeToReactTransformer.setReplacers([
|
||||
{
|
||||
replace(): NodeReplacement {
|
||||
return REPLACE_WITH_NOTHING
|
||||
}
|
||||
}
|
||||
])
|
||||
const translation = nodeToReactTransformer.translateNodeToReactElement(defaultTestSpanElement, 1) as ReactElement
|
||||
expect(translation).toEqual(null)
|
||||
})
|
||||
|
||||
it('can translate an element with no matching replacer', () => {
|
||||
nodeToReactTransformer.setReplacers([
|
||||
{
|
||||
replace(): NodeReplacement {
|
||||
return DO_NOT_REPLACE
|
||||
}
|
||||
}
|
||||
])
|
||||
const translation = nodeToReactTransformer.translateNodeToReactElement(defaultTestSpanElement, 1) as ReactElement
|
||||
|
||||
expect(translation.type).toEqual('span')
|
||||
expect(translation.props).toEqual({ children: [], 'data-test': 'test' })
|
||||
})
|
||||
|
||||
it('can replace an element', () => {
|
||||
nodeToReactTransformer.setReplacers([
|
||||
{
|
||||
replace(): NodeReplacement {
|
||||
return <div data-test2={'test2'} />
|
||||
}
|
||||
}
|
||||
])
|
||||
const translation = nodeToReactTransformer.translateNodeToReactElement(defaultTestSpanElement, 1) as ReactElement
|
||||
|
||||
expect(translation.type).toEqual('div')
|
||||
expect(translation.props).toEqual({ 'data-test2': 'test2' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('key calculation', () => {
|
||||
beforeEach(() => {
|
||||
nodeToReactTransformer.setLineIds([
|
||||
{
|
||||
id: 1,
|
||||
line: 'test'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('can calculate a fallback key', () => {
|
||||
const result = nodeToReactTransformer.translateNodeToReactElement(
|
||||
defaultTestSpanElement,
|
||||
1
|
||||
) as ReactHTMLElement<HTMLDivElement>
|
||||
|
||||
expect(result.type).toEqual('span')
|
||||
expect(result.key).toEqual('-1')
|
||||
})
|
||||
|
||||
it('can calculate a key based on line markers and line keys', () => {
|
||||
const lineMarker = new Element('app-linemarker', { 'data-start-line': '1', 'data-end-line': '2' })
|
||||
defaultTestSpanElement.prev = lineMarker
|
||||
const rootElement: Element = new Element('div', {}, [lineMarker, defaultTestSpanElement])
|
||||
|
||||
const result = nodeToReactTransformer.translateNodeToReactElement(
|
||||
rootElement,
|
||||
1
|
||||
) as ReactHTMLElement<HTMLDivElement>
|
||||
const resultSpanTag = (result.props.children as ReactElement[])[1]
|
||||
|
||||
expect(result.type).toEqual('div')
|
||||
expect(resultSpanTag.type).toEqual('span')
|
||||
expect(resultSpanTag.key).toEqual('1_1')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Element, Node } from 'domhandler'
|
||||
import { isTag } from 'domhandler'
|
||||
import { convertNodeToReactElement } from '@hedgedoc/html-to-react/dist/convertNodeToReactElement'
|
||||
import type { ComponentReplacer, NodeReplacement, ValidReactDomElement } from '../replace-components/component-replacer'
|
||||
import { DO_NOT_REPLACE, REPLACE_WITH_NOTHING } from '../replace-components/component-replacer'
|
||||
import React from 'react'
|
||||
import type { LineWithId } from '../types'
|
||||
import Optional from 'optional-js'
|
||||
|
||||
type LineIndexPair = [startLineIndex: number, endLineIndex: number]
|
||||
|
||||
/**
|
||||
* Converts {@link Node domhandler nodes} to react elements by using direct translation or {@link ComponentReplacer replacers}.
|
||||
*/
|
||||
export class NodeToReactTransformer {
|
||||
private lineIds: LineWithId[] = []
|
||||
private replacers: ComponentReplacer[] = []
|
||||
|
||||
public setLineIds(lineIds: LineWithId[]): void {
|
||||
this.lineIds = lineIds
|
||||
}
|
||||
|
||||
public setReplacers(replacers: ComponentReplacer[]): void {
|
||||
this.replacers = replacers
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given {@link Node} to a react element.
|
||||
*
|
||||
* @param node The {@link Node DOM node} that should be translated.
|
||||
* @param index The index of the node within its parents child list.
|
||||
* @return the created react element
|
||||
*/
|
||||
public translateNodeToReactElement(node: Node, index: number | string): ValidReactDomElement | undefined {
|
||||
return isTag(node)
|
||||
? this.translateElementToReactElement(node, index)
|
||||
: convertNodeToReactElement(node, index, this.translateNodeToReactElement.bind(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given {@link Element} to a react element.
|
||||
*
|
||||
* @param element The {@link Element DOM element} that should be translated.
|
||||
* @param index The index of the element within its parents child list.
|
||||
* @return the created react element
|
||||
*/
|
||||
private translateElementToReactElement(element: Element, index: number | string): ValidReactDomElement | undefined {
|
||||
const elementKey = this.calculateUniqueKey(element).orElseGet(() => (-index).toString())
|
||||
const replacement = this.findElementReplacement(element, elementKey)
|
||||
if (replacement === REPLACE_WITH_NOTHING) {
|
||||
return null
|
||||
} else if (replacement === DO_NOT_REPLACE) {
|
||||
return this.renderNativeNode(element, elementKey)
|
||||
} else if (typeof replacement === 'string') {
|
||||
return replacement
|
||||
} else {
|
||||
return React.cloneElement(replacement, {
|
||||
...(replacement.props as Record<string, unknown>),
|
||||
key: elementKey
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the unique key for the given {@link Element}.
|
||||
*
|
||||
* @param element The element for which the unique key should be calculated.
|
||||
* @return An {@link Optional} that contains the unique key or is empty if no key could be found.
|
||||
*/
|
||||
private calculateUniqueKey(element: Element): Optional<string> {
|
||||
if (!element.attribs) {
|
||||
return Optional.empty()
|
||||
}
|
||||
|
||||
return Optional.ofNullable(element.prev)
|
||||
.map((lineMarker) => NodeToReactTransformer.extractLineIndexFromLineMarker(lineMarker))
|
||||
.map(([startLineIndex, endLineIndex]) =>
|
||||
NodeToReactTransformer.convertMarkdownItLineIndexesToInternalLineIndexes(startLineIndex, endLineIndex)
|
||||
)
|
||||
.flatMap((adjustedLineIndexes) => this.findLineIdsByIndex(adjustedLineIndexes))
|
||||
.map(([startLine, endLine]) => `${startLine.id}_${endLine.id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks every saved replacer if the given {@link Element element} should be
|
||||
* replaced with another react element or not.
|
||||
*
|
||||
* @param element The {@link Element} that should be checked.
|
||||
* @param elementKey The unique key for the element
|
||||
* @return The replacement or {@link DO_NOT_REPLACE} if the element shouldn't be replaced or {@link REPLACE_WITH_NOTHING} if the node shouldn't be rendered at all.
|
||||
*/
|
||||
private findElementReplacement(element: Element, elementKey: string): NodeReplacement {
|
||||
const transformer = this.translateNodeToReactElement.bind(this)
|
||||
const nativeRenderer = () => this.renderNativeNode(element, elementKey)
|
||||
for (const componentReplacer of this.replacers) {
|
||||
const replacement = componentReplacer.replace(element, transformer, nativeRenderer)
|
||||
if (replacement !== DO_NOT_REPLACE) {
|
||||
return replacement
|
||||
}
|
||||
}
|
||||
return DO_NOT_REPLACE
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the start and end line indexes that are saved in a line marker element
|
||||
* and describe in which line, in the markdown code, the node before the marker ends
|
||||
* and which the node after the marker starts.
|
||||
*
|
||||
* @param lineMarker The line marker that saves a start and end line index.
|
||||
* @return the extracted line indexes
|
||||
*/
|
||||
private static extractLineIndexFromLineMarker(lineMarker: Node): LineIndexPair | undefined {
|
||||
if (!isTag(lineMarker) || lineMarker.tagName !== 'app-linemarker' || !lineMarker.attribs) {
|
||||
return
|
||||
}
|
||||
const startLineInMarkdown = lineMarker.attribs['data-start-line']
|
||||
const endLineInMarkdown = lineMarker.attribs['data-end-line']
|
||||
if (startLineInMarkdown === undefined || endLineInMarkdown === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const startLineIndex = Number(startLineInMarkdown)
|
||||
const endLineIndex = Number(endLineInMarkdown)
|
||||
|
||||
if (isNaN(startLineIndex) || isNaN(endLineIndex)) {
|
||||
return
|
||||
}
|
||||
|
||||
return [startLineIndex, endLineIndex]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts markdown it line indexes to internal line indexes.
|
||||
* The differences are:
|
||||
* - Markdown it starts to count at 1, but we start at 0
|
||||
* - Line indexes in markdown it are start(inclusive) to end(exclusive). But we need start(inclusive) to end(inclusive).
|
||||
*
|
||||
* @param startLineIndex The start line index from markdown it
|
||||
* @param endLineIndex The end line index from markdown it
|
||||
* @return The adjusted start and end line index
|
||||
*/
|
||||
private static convertMarkdownItLineIndexesToInternalLineIndexes(
|
||||
startLineIndex: number,
|
||||
endLineIndex: number
|
||||
): LineIndexPair {
|
||||
return [startLineIndex - 1, endLineIndex - 2]
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the given node without any replacement
|
||||
*
|
||||
* @param node The node to render
|
||||
* @param key The unique key for the node
|
||||
*/
|
||||
private renderNativeNode = (node: Element, key: string): ValidReactDomElement => {
|
||||
if (node.attribs === undefined) {
|
||||
node.attribs = {}
|
||||
}
|
||||
|
||||
return convertNodeToReactElement(node, key, this.translateNodeToReactElement.bind(this))
|
||||
}
|
||||
|
||||
private findLineIdsByIndex([startLineIndex, endLineIndex]: LineIndexPair): Optional<[LineWithId, LineWithId]> {
|
||||
const startLine = this.lineIds[startLineIndex]
|
||||
const endLine = this.lineIds[endLineIndex]
|
||||
return startLine === undefined || endLine === undefined ? Optional.empty() : Optional.of([startLine, endLine])
|
||||
}
|
||||
}
|
|
@ -10444,6 +10444,11 @@ optimize-css-assets-webpack-plugin@5.0.4:
|
|||
cssnano "^4.1.10"
|
||||
last-call-webpack-plugin "^3.0.0"
|
||||
|
||||
optional-js@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/optional-js/-/optional-js-2.3.0.tgz#81d54c4719afa8845b988143643a5148f9d89490"
|
||||
integrity sha512-B0LLi+Vg+eko++0z/b8zIv57kp7HKEzaPJo7LowJXMUKYdf+3XJGu/cw03h/JhIOsLnP+cG5QnTHAuicjA5fMw==
|
||||
|
||||
optionator@^0.8.1:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
|
||||
|
|
Loading…
Reference in a new issue