mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-19 17:55:17 -04: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
|
@ -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} />
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue