markdown-it-configurator (#626)

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
Co-authored-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
Co-authored-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Philip Molares 2020-10-08 22:24:42 +02:00 committed by GitHub
parent 89968387c2
commit 0670cddb0b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 524 additions and 360 deletions

View file

@ -1,10 +1,9 @@
import MarkdownIt from 'markdown-it' import React, { useMemo } from 'react'
import markdownItContainer from 'markdown-it-container'
import React, { useCallback } from 'react'
import { Table } from 'react-bootstrap' import { Table } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { BasicMarkdownRenderer } from '../../../markdown-renderer/basic-markdown-renderer' import { BasicMarkdownRenderer } from '../../../markdown-renderer/basic-markdown-renderer'
import { createRenderContainer, validAlertLevels } from '../../../markdown-renderer/markdown-it-plugins/alert-container' import { BasicMarkdownItConfigurator } from '../../../markdown-renderer/markdown-it-configurator/BasicMarkdownItConfigurator'
import { alertContainer } from '../../../markdown-renderer/markdown-it-plugins/alert-container'
import { HighlightedCode } from '../../../markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code' import { HighlightedCode } from '../../../markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code'
import './cheatsheet.scss' import './cheatsheet.scss'
@ -31,10 +30,10 @@ export const Cheatsheet: React.FC = () => {
`:::info\n${t('editor.help.cheatsheet.exampleAlert')}\n:::` `:::info\n${t('editor.help.cheatsheet.exampleAlert')}\n:::`
] ]
const markdownItPlugins = useCallback((md: MarkdownIt) => { const markdownIt = useMemo(() => {
validAlertLevels.forEach(level => { return new BasicMarkdownItConfigurator()
md.use(markdownItContainer, level, { render: createRenderContainer(level) }) .pushConfig(alertContainer)
}) .buildConfiguredMarkdownIt()
}, []) }, [])
return ( return (
@ -53,7 +52,7 @@ export const Cheatsheet: React.FC = () => {
<BasicMarkdownRenderer <BasicMarkdownRenderer
content={code} content={code}
wide={false} wide={false}
onConfigureMarkdownIt={markdownItPlugins} markdownIt={markdownIt}
/> />
</td> </td>
<td className={'markdown-body'}> <td className={'markdown-body'}>

View file

@ -1,13 +1,4 @@
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import abbreviation from 'markdown-it-abbr'
import definitionList from 'markdown-it-deflist'
import emoji from 'markdown-it-emoji'
import footnote from 'markdown-it-footnote'
import imsize from 'markdown-it-imsize'
import inserted from 'markdown-it-ins'
import marked from 'markdown-it-mark'
import subscript from 'markdown-it-sub'
import superscript from 'markdown-it-sup'
import React, { ReactElement, RefObject, useMemo, useRef } from 'react' import React, { ReactElement, RefObject, useMemo, useRef } from 'react'
import { Alert } from 'react-bootstrap' import { Alert } from 'react-bootstrap'
import ReactHtmlParser from 'react-html-parser' import ReactHtmlParser from 'react-html-parser'
@ -15,9 +6,6 @@ import { Trans } from 'react-i18next'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux' import { ApplicationState } from '../../redux'
import { ShowIf } from '../common/show-if/show-if' import { ShowIf } from '../common/show-if/show-if'
import { combinedEmojiData } from './markdown-it-plugins/emoji/mapping'
import { linkifyExtra } from './markdown-it-plugins/linkify-extra'
import { MarkdownItParserDebugger } from './markdown-it-plugins/parser-debugger'
import './markdown-renderer.scss' import './markdown-renderer.scss'
import { ComponentReplacer } from './replace-components/ComponentReplacer' import { ComponentReplacer } from './replace-components/ComponentReplacer'
import { AdditionalMarkdownRendererProps, LineKeys } from './types' import { AdditionalMarkdownRendererProps, LineKeys } from './types'
@ -25,8 +13,8 @@ import { buildTransformer } from './utils/html-react-transformer'
import { calculateNewLineNumberMapping } from './utils/line-number-mapping' import { calculateNewLineNumberMapping } from './utils/line-number-mapping'
export interface BasicMarkdownRendererProps { export interface BasicMarkdownRendererProps {
componentReplacers?: ComponentReplacer[], componentReplacers?: () => ComponentReplacer[],
onConfigureMarkdownIt?: (md: MarkdownIt) => void, markdownIt: MarkdownIt,
documentReference?: RefObject<HTMLDivElement> documentReference?: RefObject<HTMLDivElement>
onBeforeRendering?: () => void onBeforeRendering?: () => void
} }
@ -36,44 +24,12 @@ export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & Additi
content, content,
wide, wide,
componentReplacers, componentReplacers,
onConfigureMarkdownIt, markdownIt,
documentReference, documentReference,
onBeforeRendering onBeforeRendering
}) => { }) => {
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength) const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
const markdownIt = useMemo(() => {
const md = new MarkdownIt('default', {
html: true,
breaks: true,
langPrefix: '',
typographer: true
})
md.use(emoji, {
defs: combinedEmojiData
})
md.use(abbreviation)
md.use(definitionList)
md.use(subscript)
md.use(superscript)
md.use(inserted)
md.use(marked)
md.use(footnote)
md.use(imsize)
if (onConfigureMarkdownIt) {
onConfigureMarkdownIt(md)
}
md.use(linkifyExtra)
if (process.env.NODE_ENV !== 'production') {
md.use(MarkdownItParserDebugger)
}
return md
}, [onConfigureMarkdownIt])
const oldMarkdownLineKeys = useRef<LineKeys[]>() const oldMarkdownLineKeys = useRef<LineKeys[]>()
const lastUsedLineId = useRef<number>(0) const lastUsedLineId = useRef<number>(0)
@ -87,7 +43,7 @@ export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & Additi
const { lines: newLines, lastUsedLineId: newLastUsedLineId } = calculateNewLineNumberMapping(contentLines, oldMarkdownLineKeys.current ?? [], lastUsedLineId.current) const { lines: newLines, lastUsedLineId: newLastUsedLineId } = calculateNewLineNumberMapping(contentLines, oldMarkdownLineKeys.current ?? [], lastUsedLineId.current)
oldMarkdownLineKeys.current = newLines oldMarkdownLineKeys.current = newLines
lastUsedLineId.current = newLastUsedLineId lastUsedLineId.current = newLastUsedLineId
const transformer = componentReplacers ? buildTransformer(newLines, componentReplacers) : undefined const transformer = componentReplacers ? buildTransformer(newLines, componentReplacers()) : undefined
return ReactHtmlParser(html, { transform: transformer }) return ReactHtmlParser(html, { transform: transformer })
}, [onBeforeRendering, content, maxLength, markdownIt, componentReplacers]) }, [onBeforeRendering, content, maxLength, markdownIt, componentReplacers])

View file

@ -1,65 +1,19 @@
import markdownItTaskLists from '@hedgedoc/markdown-it-task-lists' import React, { useCallback, useMemo, useRef, useState } from 'react'
import yaml from 'js-yaml'
import MarkdownIt from 'markdown-it'
import anchor from 'markdown-it-anchor'
import markdownItContainer from 'markdown-it-container'
import frontmatter from 'markdown-it-front-matter'
import mathJax from 'markdown-it-mathjax'
import plantuml from 'markdown-it-plantuml'
import markdownItRegex from 'markdown-it-regex'
import toc from 'markdown-it-toc-done-right'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Alert } from 'react-bootstrap' import { Alert } from 'react-bootstrap'
import { Trans } from 'react-i18next' import { Trans } from 'react-i18next'
import { useSelector } from 'react-redux'
import { TocAst } from '../../external-types/markdown-it-toc-done-right/interface' import { TocAst } from '../../external-types/markdown-it-toc-done-right/interface'
import { ApplicationState } from '../../redux'
import { InternalLink } from '../common/links/internal-link' import { InternalLink } from '../common/links/internal-link'
import { ShowIf } from '../common/show-if/show-if' import { ShowIf } from '../common/show-if/show-if'
import { slugify } from '../editor/table-of-contents/table-of-contents'
import { RawYAMLMetadata, YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata' import { RawYAMLMetadata, YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
import { BasicMarkdownRenderer } from './basic-markdown-renderer' import { BasicMarkdownRenderer } from './basic-markdown-renderer'
import { createRenderContainer, validAlertLevels } from './markdown-it-plugins/alert-container' import { FullMarkdownItConfigurator } from './markdown-it-configurator/FullMarkdownItConfigurator'
import { highlightedCode } from './markdown-it-plugins/highlighted-code' import { LineMarkers } from './replace-components/linemarker/line-number-marker'
import { LineMarkers, lineNumberMarker } from './markdown-it-plugins/line-number-marker'
import { plantumlError } from './markdown-it-plugins/plantuml-error'
import { replaceAsciinemaLink } from './regex-plugins/replace-asciinema-link'
import { replaceGistLink } from './regex-plugins/replace-gist-link'
import { replaceLegacyGistShortCode } from './regex-plugins/replace-legacy-gist-short-code'
import { replaceLegacySlideshareShortCode } from './regex-plugins/replace-legacy-slideshare-short-code'
import { replaceLegacySpeakerdeckShortCode } from './regex-plugins/replace-legacy-speakerdeck-short-code'
import { replaceLegacyVimeoShortCode } from './regex-plugins/replace-legacy-vimeo-short-code'
import { replaceLegacyYoutubeShortCode } from './regex-plugins/replace-legacy-youtube-short-code'
import { replacePdfShortCode } from './regex-plugins/replace-pdf-short-code'
import { replaceQuoteExtraAuthor } from './regex-plugins/replace-quote-extra-author'
import { replaceQuoteExtraColor } from './regex-plugins/replace-quote-extra-color'
import { replaceQuoteExtraTime } from './regex-plugins/replace-quote-extra-time'
import { replaceVimeoLink } from './regex-plugins/replace-vimeo-link'
import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
import { AbcReplacer } from './replace-components/abc/abc-replacer'
import { AsciinemaReplacer } from './replace-components/asciinema/asciinema-replacer'
import { CsvReplacer } from './replace-components/csv/csv-replacer'
import { FlowchartReplacer } from './replace-components/flow/flowchart-replacer'
import { GistReplacer } from './replace-components/gist/gist-replacer'
import { GraphvizReplacer } from './replace-components/graphviz/graphviz-replacer'
import { HighlightedCodeReplacer } from './replace-components/highlighted-fence/highlighted-fence-replacer'
import { ImageReplacer } from './replace-components/image/image-replacer'
import { KatexReplacer } from './replace-components/katex/katex-replacer'
import { LinemarkerReplacer } from './replace-components/linemarker/linemarker-replacer'
import { MarkmapReplacer } from './replace-components/markmap/markmap-replacer'
import { MermaidReplacer } from './replace-components/mermaid/mermaid-replacer'
import { PdfReplacer } from './replace-components/pdf/pdf-replacer'
import { PossibleWiderReplacer } from './replace-components/possible-wider/possible-wider-replacer'
import { QuoteOptionsReplacer } from './replace-components/quote-options/quote-options-replacer'
import { SequenceDiagramReplacer } from './replace-components/sequence-diagram/sequence-diagram-replacer'
import { TaskListReplacer } from './replace-components/task-list/task-list-replacer'
import { VegaReplacer } from './replace-components/vega-lite/vega-replacer'
import { VimeoReplacer } from './replace-components/vimeo/vimeo-replacer'
import { YoutubeReplacer } from './replace-components/youtube/youtube-replacer'
import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types' import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions' import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
import { usePostMetaDataOnChange } from './utils/use-post-meta-data-on-change' import { useReplacerInstanceListCreator } from './hooks/use-replacer-instance-list-creator'
import { usePostTocAstOnChange } from './utils/use-post-toc-ast-on-change' import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
import { usePostMetaDataOnChange } from './hooks/use-post-meta-data-on-change'
import { usePostTocAstOnChange } from './hooks/use-post-toc-ast-on-change'
export interface FullMarkdownRendererProps { export interface FullMarkdownRendererProps {
onFirstHeadingChange?: (firstHeading: string | undefined) => void onFirstHeadingChange?: (firstHeading: string | undefined) => void
@ -79,150 +33,36 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
className, className,
wide wide
}) => { }) => {
const allReplacers = useMemo(() => { const allReplacers = useReplacerInstanceListCreator(onTaskCheckedChange)
return [
new LinemarkerReplacer(),
new PossibleWiderReplacer(),
new GistReplacer(),
new YoutubeReplacer(),
new VimeoReplacer(),
new AsciinemaReplacer(),
new AbcReplacer(),
new PdfReplacer(),
new ImageReplacer(),
new SequenceDiagramReplacer(),
new CsvReplacer(),
new FlowchartReplacer(),
new MermaidReplacer(),
new GraphvizReplacer(),
new MarkmapReplacer(),
new VegaReplacer(),
new HighlightedCodeReplacer(),
new QuoteOptionsReplacer(),
new KatexReplacer(),
new TaskListReplacer(onTaskCheckedChange)
]
}, [onTaskCheckedChange])
const [yamlError, setYamlError] = useState(false) const [yamlError, setYamlError] = useState(false)
const plantumlServer = useSelector((state: ApplicationState) => state.config.plantumlServer)
const rawMetaRef = useRef<RawYAMLMetadata>() const rawMetaRef = useRef<RawYAMLMetadata>()
const firstHeadingRef = useRef<string>() const firstHeadingRef = useRef<string>()
const documentElement = useRef<HTMLDivElement>(null) const documentElement = useRef<HTMLDivElement>(null)
const currentLineMarkers = useRef<LineMarkers[]>() const currentLineMarkers = useRef<LineMarkers[]>()
usePostMetaDataOnChange(rawMetaRef.current, firstHeadingRef.current, onMetaDataChange, onFirstHeadingChange) usePostMetaDataOnChange(rawMetaRef.current, firstHeadingRef.current, onMetaDataChange, onFirstHeadingChange)
useCalculateLineMarkerPosition(documentElement, currentLineMarkers.current, onLineMarkerPositionChanged, documentElement.current?.offsetTop ?? 0) useCalculateLineMarkerPosition(documentElement, currentLineMarkers.current, onLineMarkerPositionChanged, documentElement.current?.offsetTop ?? 0)
useExtractFirstHeadline(documentElement, content, onFirstHeadingChange)
const tocAst = useRef<TocAst>() const tocAst = useRef<TocAst>()
usePostTocAstOnChange(tocAst, onTocChange) usePostTocAstOnChange(tocAst, onTocChange)
const extractInnerText = useCallback((node: ChildNode) => { const markdownIt = useMemo(() => {
let innerText = '' return (new FullMarkdownItConfigurator(
if (node.childNodes && node.childNodes.length > 0) { !!onMetaDataChange,
node.childNodes.forEach((child) => { innerText += extractInnerText(child) }) error => setYamlError(error),
} else if (node.nodeName === 'IMG') { rawMeta => {
innerText += (node as HTMLImageElement).getAttribute('alt') rawMetaRef.current = rawMeta
} else {
innerText += node.textContent
}
return innerText
}, [])
useEffect(() => {
if (onFirstHeadingChange && documentElement.current) {
const firstHeading = documentElement.current.getElementsByTagName('h1').item(0)
if (firstHeading) {
onFirstHeadingChange(extractInnerText(firstHeading))
}
}
}, [content, extractInnerText, onFirstHeadingChange])
const configureMarkdownIt = useCallback((md: MarkdownIt): void => {
if (onMetaDataChange) {
md.use(frontmatter, (rawMeta: string) => {
try {
const meta: RawYAMLMetadata = yaml.safeLoad(rawMeta) as RawYAMLMetadata
setYamlError(false)
rawMetaRef.current = meta
} catch (e) {
console.error(e)
setYamlError(true)
}
})
}
md.use(markdownItTaskLists, { lineNumber: true })
if (plantumlServer) {
md.use(plantuml, {
openMarker: '```plantuml',
closeMarker: '```',
server: plantumlServer
})
} else {
md.use(plantumlError)
}
if (onMetaDataChange) {
md.use(frontmatter, (rawMeta: string) => {
try {
const meta: RawYAMLMetadata = yaml.safeLoad(rawMeta) as RawYAMLMetadata
setYamlError(false)
rawMetaRef.current = meta
} catch (e) {
console.error(e)
setYamlError(true)
rawMetaRef.current = ({} as RawYAMLMetadata)
}
})
}
// noinspection CheckTagEmptyBody
md.use(anchor, {
permalink: true,
permalinkBefore: true,
permalinkClass: 'heading-anchor text-dark',
permalinkSymbol: '<i class="fa fa-link"></i>'
})
md.use(mathJax({
beforeMath: '<app-katex>',
afterMath: '</app-katex>',
beforeInlineMath: '<app-katex inline>',
afterInlineMath: '</app-katex>',
beforeDisplayMath: '<app-katex>',
afterDisplayMath: '</app-katex>'
}))
md.use(markdownItRegex, replaceLegacyYoutubeShortCode)
md.use(markdownItRegex, replaceLegacyVimeoShortCode)
md.use(markdownItRegex, replaceLegacyGistShortCode)
md.use(markdownItRegex, replaceLegacySlideshareShortCode)
md.use(markdownItRegex, replaceLegacySpeakerdeckShortCode)
md.use(markdownItRegex, replacePdfShortCode)
md.use(markdownItRegex, replaceAsciinemaLink)
md.use(markdownItRegex, replaceYouTubeLink)
md.use(markdownItRegex, replaceVimeoLink)
md.use(markdownItRegex, replaceGistLink)
md.use(highlightedCode)
md.use(markdownItRegex, replaceQuoteExtraAuthor)
md.use(markdownItRegex, replaceQuoteExtraColor)
md.use(markdownItRegex, replaceQuoteExtraTime)
md.use(toc, {
placeholder: '(\\[TOC\\]|\\[toc\\])',
listType: 'ul',
level: [1, 2, 3],
callback: (code: string, ast: TocAst): void => {
tocAst.current = ast
}, },
slugify: slugify toc => {
}) tocAst.current = toc
validAlertLevels.forEach(level => { },
md.use(markdownItContainer, level, { render: createRenderContainer(level) }) lineMarkers => {
})
md.use(lineNumberMarker(), {
postLineMarkers: (lineMarkers) => {
currentLineMarkers.current = lineMarkers currentLineMarkers.current = lineMarkers
} }
}) )).buildConfiguredMarkdownIt()
}, [onMetaDataChange, plantumlServer]) }, [onMetaDataChange])
const clearMetadata = useCallback(() => { const clearMetadata = useCallback(() => {
rawMetaRef.current = undefined rawMetaRef.current = undefined
@ -238,7 +78,7 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
</Alert> </Alert>
</ShowIf> </ShowIf>
<BasicMarkdownRenderer className={className} wide={wide} content={content} componentReplacers={allReplacers} <BasicMarkdownRenderer className={className} wide={wide} content={content} componentReplacers={allReplacers}
onConfigureMarkdownIt={configureMarkdownIt} documentReference={documentElement} markdownIt={markdownIt} documentReference={documentElement}
onBeforeRendering={clearMetadata}/> onBeforeRendering={clearMetadata}/>
</div> </div>
) )

View file

@ -0,0 +1,24 @@
import React, { useCallback, useEffect } from 'react'
export const useExtractFirstHeadline = (documentElement: React.RefObject<HTMLDivElement>, content: string, onFirstHeadingChange?: (firstHeading: string | undefined) => void): void => {
const extractInnerText = useCallback((node: ChildNode): string => {
let innerText = ''
if (node.childNodes && node.childNodes.length > 0) {
node.childNodes.forEach((child) => { innerText += extractInnerText(child) })
} else if (node.nodeName === 'IMG') {
innerText += (node as HTMLImageElement).getAttribute('alt')
} else {
innerText += node.textContent
}
return innerText
}, [])
useEffect(() => {
if (onFirstHeadingChange && documentElement.current) {
const firstHeading = documentElement.current.getElementsByTagName('h1').item(0)
if (firstHeading) {
onFirstHeadingChange(extractInnerText(firstHeading))
}
}
}, [documentElement, extractInnerText, onFirstHeadingChange, content])
}

View file

@ -0,0 +1,47 @@
import { useMemo } from 'react'
import { AbcReplacer } from '../replace-components/abc/abc-replacer'
import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer'
import { ComponentReplacer } from '../replace-components/ComponentReplacer'
import { CsvReplacer } from '../replace-components/csv/csv-replacer'
import { FlowchartReplacer } from '../replace-components/flow/flowchart-replacer'
import { GistReplacer } from '../replace-components/gist/gist-replacer'
import { GraphvizReplacer } from '../replace-components/graphviz/graphviz-replacer'
import { HighlightedCodeReplacer } from '../replace-components/highlighted-fence/highlighted-fence-replacer'
import { ImageReplacer } from '../replace-components/image/image-replacer'
import { KatexReplacer } from '../replace-components/katex/katex-replacer'
import { LinemarkerReplacer } from '../replace-components/linemarker/linemarker-replacer'
import { MarkmapReplacer } from '../replace-components/markmap/markmap-replacer'
import { MermaidReplacer } from '../replace-components/mermaid/mermaid-replacer'
import { PdfReplacer } from '../replace-components/pdf/pdf-replacer'
import { PossibleWiderReplacer } from '../replace-components/possible-wider/possible-wider-replacer'
import { QuoteOptionsReplacer } from '../replace-components/quote-options/quote-options-replacer'
import { SequenceDiagramReplacer } from '../replace-components/sequence-diagram/sequence-diagram-replacer'
import { TaskListReplacer } from '../replace-components/task-list/task-list-replacer'
import { VegaReplacer } from '../replace-components/vega-lite/vega-replacer'
import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer'
import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
export const useReplacerInstanceListCreator = (onTaskCheckedChange: (lineInMarkdown: number, checked: boolean) => void): ()=>ComponentReplacer[] => {
return useMemo(() => () => [
new LinemarkerReplacer(),
new PossibleWiderReplacer(),
new GistReplacer(),
new YoutubeReplacer(),
new VimeoReplacer(),
new AsciinemaReplacer(),
new AbcReplacer(),
new PdfReplacer(),
new ImageReplacer(),
new SequenceDiagramReplacer(),
new CsvReplacer(),
new FlowchartReplacer(),
new MermaidReplacer(),
new GraphvizReplacer(),
new MarkmapReplacer(),
new VegaReplacer(),
new HighlightedCodeReplacer(),
new QuoteOptionsReplacer(),
new KatexReplacer(),
new TaskListReplacer(onTaskCheckedChange)
], [onTaskCheckedChange])
}

View file

@ -0,0 +1,33 @@
import MarkdownIt from 'markdown-it'
import abbreviation from 'markdown-it-abbr'
import definitionList from 'markdown-it-deflist'
import footnote from 'markdown-it-footnote'
import imsize from 'markdown-it-imsize'
import inserted from 'markdown-it-ins'
import marked from 'markdown-it-mark'
import subscript from 'markdown-it-sub'
import superscript from 'markdown-it-sup'
import { linkifyExtra } from '../markdown-it-plugins/linkify-extra'
import { MarkdownItParserDebugger } from '../markdown-it-plugins/parser-debugger'
import { twitterEmojis } from '../markdown-it-plugins/twitter-emojis'
import { MarkdownItConfigurator } from './MarkdownItConfigurator'
export class BasicMarkdownItConfigurator extends MarkdownItConfigurator {
protected configure (markdownIt: MarkdownIt): void {
this.configurations.push(
twitterEmojis,
abbreviation,
definitionList,
subscript,
superscript,
inserted,
marked,
footnote,
imsize
)
this.postConfigurations.push(
linkifyExtra,
MarkdownItParserDebugger
)
}
}

View file

@ -0,0 +1,63 @@
import MarkdownIt from 'markdown-it'
import { TocAst } from '../../../external-types/markdown-it-toc-done-right/interface'
import { RawYAMLMetadata } from '../../editor/yaml-metadata/yaml-metadata'
import { alertContainer } from '../markdown-it-plugins/alert-container'
import { documentToc } from '../markdown-it-plugins/document-toc'
import { frontmatterExtract } from '../markdown-it-plugins/frontmatter'
import { headlineAnchors } from '../markdown-it-plugins/headline-anchors'
import { highlightedCode } from '../markdown-it-plugins/highlighted-code'
import { plantumlWithError } from '../markdown-it-plugins/plantuml'
import { quoteExtra } from '../markdown-it-plugins/quote-extra'
import { tasksLists } from '../markdown-it-plugins/tasks-lists'
import { legacySlideshareShortCode } from '../regex-plugins/replace-legacy-slideshare-short-code'
import { legacySpeakerdeckShortCode } from '../regex-plugins/replace-legacy-speakerdeck-short-code'
import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer'
import { GistReplacer } from '../replace-components/gist/gist-replacer'
import { KatexReplacer } from '../replace-components/katex/katex-replacer'
import { LineMarkers, lineNumberMarker } from '../replace-components/linemarker/line-number-marker'
import { PdfReplacer } from '../replace-components/pdf/pdf-replacer'
import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer'
import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
import { BasicMarkdownItConfigurator } from './BasicMarkdownItConfigurator'
export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
constructor (
private useFrontmatter: boolean,
private onYamlError: (error: boolean) => void,
private onRawMeta: (rawMeta: RawYAMLMetadata) => void,
private onToc: (toc: TocAst) => void,
private onLineMarkers: (lineMarkers: LineMarkers[]) => void
) {
super()
}
protected configure (markdownIt: MarkdownIt): void {
super.configure(markdownIt)
this.configurations.push(
plantumlWithError,
tasksLists,
(markdownIt) => {
frontmatterExtract(markdownIt,
!this.useFrontmatter ? undefined : {
onYamlError: (error: boolean) => this.onYamlError(error),
onRawMeta: (rawMeta: RawYAMLMetadata) => this.onRawMeta(rawMeta)
})
},
headlineAnchors,
KatexReplacer.markdownItPlugin,
YoutubeReplacer.markdownItPlugin,
VimeoReplacer.markdownItPlugin,
GistReplacer.markdownItPlugin,
legacySlideshareShortCode,
legacySpeakerdeckShortCode,
PdfReplacer.markdownItPlugin,
AsciinemaReplacer.markdownItPlugin,
highlightedCode,
quoteExtra,
(markdownIt) => documentToc(markdownIt, this.onToc),
alertContainer,
(markdownIt) => lineNumberMarker(markdownIt, (lineMarkers) => this.onLineMarkers(lineMarkers))
)
}
}

View file

@ -0,0 +1,26 @@
import MarkdownIt from 'markdown-it'
export abstract class MarkdownItConfigurator {
protected configurations:MarkdownIt.PluginSimple[] = [];
protected postConfigurations:MarkdownIt.PluginSimple[] = [];
protected abstract configure(markdownIt: MarkdownIt): void;
public pushConfig (plugin: MarkdownIt.PluginSimple): this {
this.configurations.push(plugin)
return this
}
public buildConfiguredMarkdownIt (): MarkdownIt {
const markdownIt = new MarkdownIt('default', {
html: true,
breaks: true,
langPrefix: '',
typographer: true
})
this.configure(markdownIt)
this.configurations.forEach((configuration) => markdownIt.use(configuration))
this.postConfigurations.forEach((postConfiguration) => markdownIt.use(postConfiguration))
return markdownIt
}
}

View file

@ -1,12 +1,14 @@
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import markdownItContainer from 'markdown-it-container'
import Renderer from 'markdown-it/lib/renderer' import Renderer from 'markdown-it/lib/renderer'
import Token from 'markdown-it/lib/token' import Token from 'markdown-it/lib/token'
import { MarkdownItPlugin } from '../replace-components/ComponentReplacer'
type RenderContainerReturn = (tokens: Token[], index: number, options: MarkdownIt.Options, env: unknown, self: Renderer) => void; type RenderContainerReturn = (tokens: Token[], index: number, options: MarkdownIt.Options, env: unknown, self: Renderer) => void;
type ValidAlertLevels = ('warning' | 'danger' | 'success' | 'info') type ValidAlertLevels = ('warning' | 'danger' | 'success' | 'info')
export const validAlertLevels: ValidAlertLevels[] = ['success', 'danger', 'info', 'warning'] export const validAlertLevels: ValidAlertLevels[] = ['success', 'danger', 'info', 'warning']
export const createRenderContainer = (level: ValidAlertLevels): RenderContainerReturn => { const createRenderContainer = (level: ValidAlertLevels): RenderContainerReturn => {
return (tokens: Token[], index: number, options: MarkdownIt.Options, env: unknown, self: Renderer) => { return (tokens: Token[], index: number, options: MarkdownIt.Options, env: unknown, self: Renderer) => {
tokens[index].attrJoin('role', 'alert') tokens[index].attrJoin('role', 'alert')
tokens[index].attrJoin('class', 'alert') tokens[index].attrJoin('class', 'alert')
@ -14,3 +16,9 @@ export const createRenderContainer = (level: ValidAlertLevels): RenderContainerR
return self.renderToken(tokens, index, options) return self.renderToken(tokens, index, options)
} }
} }
export const alertContainer: MarkdownItPlugin = (markdownIt: MarkdownIt) => {
validAlertLevels.forEach(level => {
markdownItContainer(markdownIt, level, { render: createRenderContainer(level) })
})
}

View file

@ -0,0 +1,21 @@
import MarkdownIt from 'markdown-it'
import toc from 'markdown-it-toc-done-right'
import { TocAst } from '../../../external-types/markdown-it-toc-done-right/interface'
import { slugify } from '../../editor/table-of-contents/table-of-contents'
export type DocumentTocPluginOptions = (ast: TocAst) => void
export const documentToc:MarkdownIt.PluginWithOptions<DocumentTocPluginOptions> = (markdownIt, onToc) => {
if (!onToc) {
return
}
toc(markdownIt, {
placeholder: '(\\[TOC\\]|\\[toc\\])',
listType: 'ul',
level: [1, 2, 3],
callback: (code: string, ast: TocAst): void => {
onToc(ast)
},
slugify: slugify
})
}

View file

@ -0,0 +1,26 @@
import yaml from 'js-yaml'
import MarkdownIt from 'markdown-it'
import frontmatter from 'markdown-it-front-matter'
import { RawYAMLMetadata } from '../../editor/yaml-metadata/yaml-metadata'
interface FrontmatterPluginOptions {
onYamlError: (error: boolean) => void,
onRawMeta: (rawMeta: RawYAMLMetadata) => void,
}
export const frontmatterExtract: MarkdownIt.PluginWithOptions<FrontmatterPluginOptions> = (markdownIt: MarkdownIt, options) => {
if (!options) {
return
}
frontmatter(markdownIt, (rawMeta: string) => {
try {
const meta: RawYAMLMetadata = yaml.safeLoad(rawMeta) as RawYAMLMetadata
options.onYamlError(false)
options.onRawMeta(meta)
} catch (e) {
console.error(e)
options.onYamlError(true)
options.onRawMeta({} as RawYAMLMetadata)
}
})
}

View file

@ -0,0 +1,12 @@
import MarkdownIt from 'markdown-it'
import anchor from 'markdown-it-anchor'
export const headlineAnchors: MarkdownIt.PluginSimple = (markdownIt) => {
// noinspection CheckTagEmptyBody
anchor(markdownIt, {
permalink: true,
permalinkBefore: true,
permalinkClass: 'heading-anchor text-dark',
permalinkSymbol: '<i class="fa fa-link"></i>'
})
}

View file

@ -1,76 +0,0 @@
import MarkdownIt from 'markdown-it/lib'
import Token from 'markdown-it/lib/token'
export interface LineMarkers {
startLine: number
endLine: number
}
export interface LineNumberMarkerOptions {
postLineMarkers: (lineMarkers: LineMarkers[]) => void
}
/**
* This plugin adds markers to the dom, that are used to map line numbers to dom elements.
* It also provides a list of line numbers for the top level dom elements.
*/
export const lineNumberMarker: () => MarkdownIt.PluginWithOptions<LineNumberMarkerOptions> = () => {
return (md: MarkdownIt, options) => {
// add app_linemarker token before each opening or self-closing level-0 tag
md.core.ruler.push('line_number_marker', (state) => {
const lineMarkers: LineMarkers[] = []
tagTokens(state.tokens, lineMarkers)
if (options?.postLineMarkers) {
options.postLineMarkers(lineMarkers)
}
return true
})
md.renderer.rules.app_linemarker = (tokens: Token[], index: number): string => {
const startLineNumber = tokens[index].attrGet('data-start-line')
const endLineNumber = tokens[index].attrGet('data-end-line')
if (!startLineNumber || !endLineNumber) {
// don't render broken linemarkers without a linenumber
return ''
}
// noinspection CheckTagEmptyBody
return `<app-linemarker data-start-line='${startLineNumber}' data-end-line='${endLineNumber}'></app-linemarker>`
}
const insertNewLineMarker = (startLineNumber: number, endLineNumber: number, tokenPosition: number, level: number, tokens: Token[]) => {
const startToken = new Token('app_linemarker', 'app-linemarker', 0)
startToken.level = level
startToken.attrPush(['data-start-line', `${startLineNumber}`])
startToken.attrPush(['data-end-line', `${endLineNumber}`])
tokens.splice(tokenPosition, 0, startToken)
}
const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[]) => {
for (let tokenPosition = 0; tokenPosition < tokens.length; tokenPosition++) {
const token = tokens[tokenPosition]
if (token.hidden) {
continue
}
if (!token.map) {
continue
}
const startLineNumber = token.map[0] + 1
const endLineNumber = token.map[1] + 1
if (token.level === 0) {
lineMarkers.push({ startLine: startLineNumber, endLine: endLineNumber })
}
insertNewLineMarker(startLineNumber, endLineNumber, tokenPosition, token.level, tokens)
tokenPosition += 1
if (token.children) {
tagTokens(token.children, lineMarkers)
}
}
}
}
}

View file

@ -1,8 +1,10 @@
import MarkdownIt from 'markdown-it/lib' import MarkdownIt from 'markdown-it/lib'
export const MarkdownItParserDebugger: MarkdownIt.PluginSimple = (md: MarkdownIt) => { export const MarkdownItParserDebugger: MarkdownIt.PluginSimple = (md: MarkdownIt) => {
md.core.ruler.push('test', (state) => { if (process.env.NODE_ENV !== 'production') {
console.log(state) md.core.ruler.push('test', (state) => {
return true console.log(state)
}) return true
})
}
} }

View file

@ -1,8 +1,24 @@
import plantuml from 'markdown-it-plantuml'
import MarkdownIt, { Options } from 'markdown-it/lib' import MarkdownIt, { Options } from 'markdown-it/lib'
import Renderer, { RenderRule } from 'markdown-it/lib/renderer' import Renderer, { RenderRule } from 'markdown-it/lib/renderer'
import Token from 'markdown-it/lib/token' import Token from 'markdown-it/lib/token'
import { store } from '../../../redux'
import { MarkdownItPlugin } from '../replace-components/ComponentReplacer'
export const plantumlError: MarkdownIt.PluginSimple = (md) => { export const plantumlWithError: MarkdownItPlugin = (markdownIt: MarkdownIt) => {
const plantumlServer = store.getState().config.plantumlServer
if (plantumlServer) {
plantuml(markdownIt, {
openMarker: '```plantuml',
closeMarker: '```',
server: plantumlServer
})
} else {
plantumlError(markdownIt)
}
}
const plantumlError: MarkdownIt.PluginSimple = (md) => {
const defaultRenderer: RenderRule = md.renderer.rules.fence || (() => '') const defaultRenderer: RenderRule = md.renderer.rules.fence || (() => '')
md.renderer.rules.fence = (tokens: Token[], idx: number, options: Options, env, slf: Renderer) => { md.renderer.rules.fence = (tokens: Token[], idx: number, options: Options, env, slf: Renderer) => {
const token = tokens[idx] const token = tokens[idx]

View file

@ -1,8 +1,26 @@
import MarkdownIt from 'markdown-it'
import markdownItRegex from 'markdown-it-regex'
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' import { RegexOptions } from '../../../external-types/markdown-it-regex/interface'
export const quoteExtra: MarkdownIt.PluginSimple = (markdownIt) => {
markdownItRegex(markdownIt, replaceQuoteExtraAuthor)
markdownItRegex(markdownIt, replaceQuoteExtraColor)
markdownItRegex(markdownIt, replaceQuoteExtraTime)
}
const replaceQuoteExtraTime: RegexOptions = {
name: 'quote-extra-time',
regex: /\[time=([^\]]+)]/,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<span class="quote-extra"><i class="fa fa-clock-o mx-1"></i> ${match}</span>`
}
}
const cssColorRegex = /\[color=(#(?:[0-9a-f]{2}){2,4}|(?:#[0-9a-f]{3})|black|silver|gray|whitesmoke|maroon|red|purple|fuchsia|green|lime|olivedrab|yellow|navy|blue|teal|aquamarine|orange|aliceblue|antiquewhite|aqua|azure|beige|bisque|blanchedalmond|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|currentcolor|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|gainsboro|ghostwhite|goldenrod|gold|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavenderblush|lavender|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|limegreen|linen|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|oldlace|olive|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|rebeccapurple|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|thistle|tomato|transparent|turquoise|violet|wheat|white|yellowgreen)]/i const cssColorRegex = /\[color=(#(?:[0-9a-f]{2}){2,4}|(?:#[0-9a-f]{3})|black|silver|gray|whitesmoke|maroon|red|purple|fuchsia|green|lime|olivedrab|yellow|navy|blue|teal|aquamarine|orange|aliceblue|antiquewhite|aqua|azure|beige|bisque|blanchedalmond|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|currentcolor|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|gainsboro|ghostwhite|goldenrod|gold|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavenderblush|lavender|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|limegreen|linen|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|oldlace|olive|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|rebeccapurple|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|thistle|tomato|transparent|turquoise|violet|wheat|white|yellowgreen)]/i
export const replaceQuoteExtraColor: RegexOptions = { const replaceQuoteExtraColor: RegexOptions = {
name: 'quote-extra-color', name: 'quote-extra-color',
regex: cssColorRegex, regex: cssColorRegex,
replace: (match) => { replace: (match) => {
@ -11,3 +29,13 @@ export const replaceQuoteExtraColor: RegexOptions = {
return `<span class="quote-extra" data-color='${match}' style='color: ${match}'><i class="fa fa-tag"></i></span>` return `<span class="quote-extra" data-color='${match}' style='color: ${match}'><i class="fa fa-tag"></i></span>`
} }
} }
const replaceQuoteExtraAuthor: RegexOptions = {
name: 'quote-extra-name',
regex: /\[name=([^\]]+)]/,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<span class="quote-extra"><i class="fa fa-user mx-1"></i> ${match}</span>`
}
}

View file

@ -0,0 +1,11 @@
import markdownItTaskLists from '@hedgedoc/markdown-it-task-lists'
import MarkdownIt from 'markdown-it'
export const tasksLists: MarkdownIt.PluginSimple = (markdownIt) => {
markdownItTaskLists(markdownIt, {
enabled: true,
label: true,
labelAfter: true,
lineNumber: true
})
}

View file

@ -0,0 +1,9 @@
import MarkdownIt from 'markdown-it'
import emoji from 'markdown-it-emoji'
import { combinedEmojiData } from './emoji/mapping'
export const twitterEmojis: MarkdownIt.PluginSimple = (markdownIt) => {
emoji(markdownIt, {
defs: combinedEmojiData
})
}

View file

@ -1,7 +1,13 @@
import markdownItRegex from 'markdown-it-regex'
import MarkdownIt from 'markdown-it/lib'
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' import { RegexOptions } from '../../../external-types/markdown-it-regex/interface'
const finalRegex = /^{%slideshare (\w+\/[\w-]+) ?%}$/ const finalRegex = /^{%slideshare (\w+\/[\w-]+) ?%}$/
export const legacySlideshareShortCode: MarkdownIt.PluginSimple = (markdownIt) => {
markdownItRegex(markdownIt, replaceLegacySlideshareShortCode)
}
export const replaceLegacySlideshareShortCode: RegexOptions = { export const replaceLegacySlideshareShortCode: RegexOptions = {
name: 'legacy-slideshare-short-code', name: 'legacy-slideshare-short-code',
regex: finalRegex, regex: finalRegex,

View file

@ -1,7 +1,13 @@
import markdownItRegex from 'markdown-it-regex'
import MarkdownIt from 'markdown-it/lib'
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' import { RegexOptions } from '../../../external-types/markdown-it-regex/interface'
const finalRegex = /^{%speakerdeck (\w+\/[\w-]+) ?%}$/ const finalRegex = /^{%speakerdeck (\w+\/[\w-]+) ?%}$/
export const legacySpeakerdeckShortCode: MarkdownIt.PluginSimple = (markdownIt) => {
markdownItRegex(markdownIt, replaceLegacySpeakerdeckShortCode)
}
export const replaceLegacySpeakerdeckShortCode: RegexOptions = { export const replaceLegacySpeakerdeckShortCode: RegexOptions = {
name: 'legacy-speakerdeck-short-code', name: 'legacy-speakerdeck-short-code',
regex: finalRegex, regex: finalRegex,

View file

@ -1,11 +0,0 @@
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface'
export const replaceQuoteExtraAuthor: RegexOptions = {
name: 'quote-extra-name',
regex: /\[name=([^\]]+)]/,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<span class="quote-extra"><i class="fa fa-user mx-1"></i> ${match}</span>`
}
}

View file

@ -1,11 +0,0 @@
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface'
export const replaceQuoteExtraTime: RegexOptions = {
name: 'quote-extra-time',
regex: /\[time=([^\]]+)]/,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<span class="quote-extra"><i class="fa fa-clock-o mx-1"></i> ${match}</span>`
}
}

View file

@ -1,10 +1,13 @@
import { DomElement } from 'domhandler' import { DomElement } from 'domhandler'
import MarkdownIt from 'markdown-it'
import { ReactElement } from 'react' import { ReactElement } from 'react'
export type SubNodeTransform = (node: DomElement, subIndex: number) => ReactElement | void | null export type SubNodeTransform = (node: DomElement, subIndex: number) => ReactElement | void | null
export type NativeRenderer = (node: DomElement, key: number) => ReactElement export type NativeRenderer = (node: DomElement, key: number) => ReactElement
export type MarkdownItPlugin = MarkdownIt.PluginSimple | MarkdownIt.PluginWithOptions | MarkdownIt.PluginWithParams
export abstract class ComponentReplacer { export abstract class ComponentReplacer {
public abstract getReplacement(node: DomElement, subNodeTransform: SubNodeTransform): (ReactElement | null | undefined); public abstract getReplacement (node: DomElement, subNodeTransform: SubNodeTransform): (ReactElement | null | undefined);
} }

View file

@ -1,8 +1,11 @@
import { DomElement } from 'domhandler' import { DomElement } from 'domhandler'
import MarkdownIt from 'markdown-it'
import markdownItRegex from 'markdown-it-regex'
import React from 'react' import React from 'react'
import { getAttributesFromHedgeDocTag } from '../utils'
import { ComponentReplacer } from '../ComponentReplacer' import { ComponentReplacer } from '../ComponentReplacer'
import { getAttributesFromHedgeDocTag } from '../utils'
import { AsciinemaFrame } from './asciinema-frame' import { AsciinemaFrame } from './asciinema-frame'
import { replaceAsciinemaLink } from './replace-asciinema-link'
export class AsciinemaReplacer extends ComponentReplacer { export class AsciinemaReplacer extends ComponentReplacer {
private counterMap: Map<string, number> = new Map<string, number>() private counterMap: Map<string, number> = new Map<string, number>()
@ -18,4 +21,8 @@ export class AsciinemaReplacer extends ComponentReplacer {
) )
} }
} }
public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => {
markdownItRegex(markdownIt, replaceAsciinemaLink)
}
} }

View file

@ -1,4 +1,4 @@
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
const protocolRegex = /(?:http(?:s)?:\/\/)?/ const protocolRegex = /(?:http(?:s)?:\/\/)?/
const domainRegex = /(?:asciinema\.org\/a\/)/ const domainRegex = /(?:asciinema\.org\/a\/)/

View file

@ -1,5 +1,9 @@
import { DomElement } from 'domhandler' import { DomElement } from 'domhandler'
import MarkdownIt from 'markdown-it'
import markdownItRegex from 'markdown-it-regex'
import React from 'react' import React from 'react'
import { replaceGistLink } from './replace-gist-link'
import { replaceLegacyGistShortCode } from './replace-legacy-gist-short-code'
import { getAttributesFromHedgeDocTag } from '../utils' import { getAttributesFromHedgeDocTag } from '../utils'
import { ComponentReplacer } from '../ComponentReplacer' import { ComponentReplacer } from '../ComponentReplacer'
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding' import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
@ -22,4 +26,9 @@ export class GistReplacer extends ComponentReplacer {
) )
} }
} }
public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => {
markdownItRegex(markdownIt, replaceGistLink)
markdownItRegex(markdownIt, replaceLegacyGistShortCode)
}
} }

View file

@ -1,4 +1,4 @@
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
const protocolRegex = /(?:http(?:s)?:\/\/)?/ const protocolRegex = /(?:http(?:s)?:\/\/)?/
const domainRegex = /(?:gist\.github\.com\/)/ const domainRegex = /(?:gist\.github\.com\/)/

View file

@ -1,4 +1,4 @@
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
const finalRegex = /^{%gist (\w+\/\w+) ?%}$/ const finalRegex = /^{%gist (\w+\/\w+) ?%}$/

View file

@ -1,9 +1,11 @@
import { DomElement } from 'domhandler' import { DomElement } from 'domhandler'
import MarkdownIt from 'markdown-it'
import mathJax from 'markdown-it-mathjax'
import React from 'react' import React from 'react'
import { ComponentReplacer } from '../ComponentReplacer' import { ComponentReplacer } from '../ComponentReplacer'
import './katex.scss' import './katex.scss'
const getNodeIfKatexBlock = (node: DomElement): (DomElement|undefined) => { const getNodeIfKatexBlock = (node: DomElement): (DomElement | undefined) => {
if (node.name !== 'p' || !node.children || node.children.length === 0) { if (node.name !== 'p' || !node.children || node.children.length === 0) {
return return
} }
@ -12,7 +14,7 @@ const getNodeIfKatexBlock = (node: DomElement): (DomElement|undefined) => {
}) })
} }
const getNodeIfInlineKatex = (node: DomElement): (DomElement|undefined) => { const getNodeIfInlineKatex = (node: DomElement): (DomElement | undefined) => {
return (node.name === 'app-katex' && node.attribs?.inline !== undefined) ? node : undefined return (node.name === 'app-katex' && node.attribs?.inline !== undefined) ? node : undefined
} }
@ -27,4 +29,13 @@ export class KatexReplacer extends ComponentReplacer {
return <KaTeX block={!isInline} math={mathJaxContent} errorColor={'#cc0000'}/> return <KaTeX block={!isInline} math={mathJaxContent} errorColor={'#cc0000'}/>
} }
} }
public static readonly markdownItPlugin: MarkdownIt.PluginSimple = mathJax({
beforeMath: '<app-katex>',
afterMath: '</app-katex>',
beforeInlineMath: '<app-katex inline>',
afterInlineMath: '</app-katex>',
beforeDisplayMath: '<app-katex>',
afterDisplayMath: '</app-katex>'
})
} }

View file

@ -0,0 +1,72 @@
import MarkdownIt from 'markdown-it/lib'
import Token from 'markdown-it/lib/token'
export interface LineMarkers {
startLine: number
endLine: number
}
export type LineNumberMarkerOptions = (lineMarkers: LineMarkers[]) => void;
/**
* This plugin adds markers to the dom, that are used to map line numbers to dom elements.
* It also provides a list of line numbers for the top level dom elements.
*/
export const lineNumberMarker: MarkdownIt.PluginWithOptions<LineNumberMarkerOptions> = (md: MarkdownIt, options) => {
// add app_linemarker token before each opening or self-closing level-0 tag
md.core.ruler.push('line_number_marker', (state) => {
const lineMarkers: LineMarkers[] = []
tagTokens(state.tokens, lineMarkers)
if (options) {
options(lineMarkers)
}
return true
})
md.renderer.rules.app_linemarker = (tokens: Token[], index: number): string => {
const startLineNumber = tokens[index].attrGet('data-start-line')
const endLineNumber = tokens[index].attrGet('data-end-line')
if (!startLineNumber || !endLineNumber) {
// don't render broken linemarkers without a linenumber
return ''
}
// noinspection CheckTagEmptyBody
return `<app-linemarker data-start-line='${startLineNumber}' data-end-line='${endLineNumber}'></app-linemarker>`
}
const insertNewLineMarker = (startLineNumber: number, endLineNumber: number, tokenPosition: number, level: number, tokens: Token[]) => {
const startToken = new Token('app_linemarker', 'app-linemarker', 0)
startToken.level = level
startToken.attrPush(['data-start-line', `${startLineNumber}`])
startToken.attrPush(['data-end-line', `${endLineNumber}`])
tokens.splice(tokenPosition, 0, startToken)
}
const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[]) => {
for (let tokenPosition = 0; tokenPosition < tokens.length; tokenPosition++) {
const token = tokens[tokenPosition]
if (token.hidden) {
continue
}
if (!token.map) {
continue
}
const startLineNumber = token.map[0] + 1
const endLineNumber = token.map[1] + 1
if (token.level === 0) {
lineMarkers.push({ startLine: startLineNumber, endLine: endLineNumber })
}
insertNewLineMarker(startLineNumber, endLineNumber, tokenPosition, token.level, tokens)
tokenPosition += 1
if (token.children) {
tagTokens(token.children, lineMarkers)
}
}
}
}

View file

@ -1,5 +1,8 @@
import { DomElement } from 'domhandler' import { DomElement } from 'domhandler'
import MarkdownIt from 'markdown-it'
import markdownItRegex from 'markdown-it-regex'
import React from 'react' import React from 'react'
import { replacePdfShortCode } from './replace-pdf-short-code'
import { getAttributesFromHedgeDocTag } from '../utils' import { getAttributesFromHedgeDocTag } from '../utils'
import { ComponentReplacer } from '../ComponentReplacer' import { ComponentReplacer } from '../ComponentReplacer'
import { PdfFrame } from './pdf-frame' import { PdfFrame } from './pdf-frame'
@ -16,4 +19,8 @@ export class PdfReplacer extends ComponentReplacer {
return <PdfFrame url={pdfUrl}/> return <PdfFrame url={pdfUrl}/>
} }
} }
public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => {
markdownItRegex(markdownIt, replacePdfShortCode)
}
} }

View file

@ -1,4 +1,4 @@
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
export const replacePdfShortCode: RegexOptions = { export const replacePdfShortCode: RegexOptions = {
name: 'pdf-short-code', name: 'pdf-short-code',

View file

@ -1,4 +1,4 @@
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
export const replaceLegacyVimeoShortCode: RegexOptions = { export const replaceLegacyVimeoShortCode: RegexOptions = {
name: 'legacy-vimeo-short-code', name: 'legacy-vimeo-short-code',

View file

@ -1,4 +1,4 @@
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
const protocolRegex = /(?:http(?:s)?:\/\/)?/ const protocolRegex = /(?:http(?:s)?:\/\/)?/
const domainRegex = /(?:player\.)?(?:vimeo\.com\/)(?:(?:channels|album|ondemand|groups)\/\w+\/)?(?:video\/)?/ const domainRegex = /(?:player\.)?(?:vimeo\.com\/)(?:(?:channels|album|ondemand|groups)\/\w+\/)?(?:video\/)?/

View file

@ -1,7 +1,11 @@
import { DomElement } from 'domhandler' import { DomElement } from 'domhandler'
import MarkdownIt from 'markdown-it'
import markdownItRegex from 'markdown-it-regex'
import React from 'react' import React from 'react'
import { getAttributesFromHedgeDocTag } from '../utils'
import { ComponentReplacer } from '../ComponentReplacer' 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' import { VimeoFrame } from './vimeo-frame'
export class VimeoReplacer extends ComponentReplacer { export class VimeoReplacer extends ComponentReplacer {
@ -16,4 +20,9 @@ export class VimeoReplacer extends ComponentReplacer {
return <VimeoFrame id={videoId}/> return <VimeoFrame id={videoId}/>
} }
} }
public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => {
markdownItRegex(markdownIt, replaceVimeoLink)
markdownItRegex(markdownIt, replaceLegacyVimeoShortCode)
}
} }

View file

@ -1,4 +1,4 @@
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
export const replaceLegacyYoutubeShortCode: RegexOptions = { export const replaceLegacyYoutubeShortCode: RegexOptions = {
name: 'legacy-youtube-short-code', name: 'legacy-youtube-short-code',

View file

@ -1,4 +1,4 @@
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface' import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
const protocolRegex = /(?:http(?:s)?:\/\/)?/ const protocolRegex = /(?:http(?:s)?:\/\/)?/
const subdomainRegex = /(?:www.)?/ const subdomainRegex = /(?:www.)?/

View file

@ -1,7 +1,11 @@
import { DomElement } from 'domhandler' import { DomElement } from 'domhandler'
import MarkdownIt from 'markdown-it'
import markdownItRegex from 'markdown-it-regex'
import React from 'react' import React from 'react'
import { getAttributesFromHedgeDocTag } from '../utils'
import { ComponentReplacer } from '../ComponentReplacer' 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' import { YouTubeFrame } from './youtube-frame'
export class YoutubeReplacer extends ComponentReplacer { export class YoutubeReplacer extends ComponentReplacer {
@ -16,4 +20,9 @@ export class YoutubeReplacer extends ComponentReplacer {
return <YouTubeFrame id={videoId}/> return <YouTubeFrame id={videoId}/>
} }
} }
public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => {
markdownItRegex(markdownIt, replaceYouTubeLink)
markdownItRegex(markdownIt, replaceLegacyYoutubeShortCode)
}
} }

View file

@ -2,7 +2,7 @@ import equal from 'fast-deep-equal'
import { RefObject, useCallback, useEffect, useRef } from 'react' import { RefObject, useCallback, useEffect, useRef } from 'react'
import useResizeObserver from 'use-resize-observer' import useResizeObserver from 'use-resize-observer'
import { LineMarkerPosition } from '../types' import { LineMarkerPosition } from '../types'
import { LineMarkers } from '../markdown-it-plugins/line-number-marker' import { LineMarkers } from '../replace-components/linemarker/line-number-marker'
export const calculateLineMarkerPositions = (documentElement: HTMLDivElement, currentLineMarkers: LineMarkers[], offset?: number): LineMarkerPosition[] => { export const calculateLineMarkerPositions = (documentElement: HTMLDivElement, currentLineMarkers: LineMarkers[], offset?: number): LineMarkerPosition[] => {
const lineMarkers = currentLineMarkers const lineMarkers = currentLineMarkers

View file

@ -1,5 +1,7 @@
declare module 'markdown-it-front-matter' { declare module 'markdown-it-front-matter' {
import MarkdownIt from 'markdown-it/lib' import MarkdownIt from 'markdown-it/lib'
const markdownItFrontMatter: MarkdownIt.PluginSimple export type FrontMatterPluginOptions = (rawMeta: string) => void
const markdownItFrontMatter: MarkdownIt.PluginWithOptions<FrontMatterPluginOptions>
export = markdownItFrontMatter export = markdownItFrontMatter
} }