mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-20 10:15:17 -04:00
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:
parent
89968387c2
commit
0670cddb0b
42 changed files with 524 additions and 360 deletions
|
@ -1,10 +1,13 @@
|
|||
import { DomElement } from 'domhandler'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { ReactElement } from 'react'
|
||||
|
||||
export type SubNodeTransform = (node: DomElement, subIndex: number) => ReactElement | void | null
|
||||
|
||||
export type NativeRenderer = (node: DomElement, key: number) => ReactElement
|
||||
|
||||
export type MarkdownItPlugin = MarkdownIt.PluginSimple | MarkdownIt.PluginWithOptions | MarkdownIt.PluginWithParams
|
||||
|
||||
export abstract class ComponentReplacer {
|
||||
public abstract getReplacement(node: DomElement, subNodeTransform: SubNodeTransform): (ReactElement | null | undefined);
|
||||
public abstract getReplacement (node: DomElement, subNodeTransform: SubNodeTransform): (ReactElement | null | undefined);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { DomElement } from 'domhandler'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import React from 'react'
|
||||
import { getAttributesFromHedgeDocTag } from '../utils'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { getAttributesFromHedgeDocTag } from '../utils'
|
||||
import { AsciinemaFrame } from './asciinema-frame'
|
||||
import { replaceAsciinemaLink } from './replace-asciinema-link'
|
||||
|
||||
export class AsciinemaReplacer extends ComponentReplacer {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
||||
|
||||
const protocolRegex = /(?:http(?:s)?:\/\/)?/
|
||||
const domainRegex = /(?:asciinema\.org\/a\/)/
|
||||
const idRegex = /(\d+)/
|
||||
const tailRegex = /(?:[./?#].*)?/
|
||||
const gistUrlRegex = new RegExp(`(?:${protocolRegex.source}${domainRegex.source}${idRegex.source}${tailRegex.source})`)
|
||||
const linkRegex = new RegExp(`^${gistUrlRegex.source}$`, 'i')
|
||||
|
||||
export const replaceAsciinemaLink: RegexOptions = {
|
||||
name: 'asciinema-link',
|
||||
regex: linkRegex,
|
||||
replace: (match) => {
|
||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-asciinema id="${match}"></app-asciinema>`
|
||||
}
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
import { DomElement } from 'domhandler'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import React from 'react'
|
||||
import { replaceGistLink } from './replace-gist-link'
|
||||
import { replaceLegacyGistShortCode } from './replace-legacy-gist-short-code'
|
||||
import { getAttributesFromHedgeDocTag } from '../utils'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
||||
|
||||
const protocolRegex = /(?:http(?:s)?:\/\/)?/
|
||||
const domainRegex = /(?:gist\.github\.com\/)/
|
||||
const idRegex = /(\w+\/\w+)/
|
||||
const tailRegex = /(?:[./?#].*)?/
|
||||
const gistUrlRegex = new RegExp(`(?:${protocolRegex.source}${domainRegex.source}${idRegex.source}${tailRegex.source})`)
|
||||
const linkRegex = new RegExp(`^${gistUrlRegex.source}$`, 'i')
|
||||
|
||||
export const replaceGistLink: RegexOptions = {
|
||||
name: 'gist-link',
|
||||
regex: linkRegex,
|
||||
replace: (match) => {
|
||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-gist id="${match}"></app-gist>`
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
||||
|
||||
const finalRegex = /^{%gist (\w+\/\w+) ?%}$/
|
||||
|
||||
export const replaceLegacyGistShortCode: RegexOptions = {
|
||||
name: 'legacy-gist-short-code',
|
||||
regex: finalRegex,
|
||||
replace: (match) => {
|
||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-gist id="${match}"></app-gist>`
|
||||
}
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
import { DomElement } from 'domhandler'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import mathJax from 'markdown-it-mathjax'
|
||||
import React from 'react'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -27,4 +29,13 @@ export class KatexReplacer extends ComponentReplacer {
|
|||
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>'
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
import { DomElement } from 'domhandler'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import React from 'react'
|
||||
import { replacePdfShortCode } from './replace-pdf-short-code'
|
||||
import { getAttributesFromHedgeDocTag } from '../utils'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { PdfFrame } from './pdf-frame'
|
||||
|
@ -16,4 +19,8 @@ export class PdfReplacer extends ComponentReplacer {
|
|||
return <PdfFrame url={pdfUrl}/>
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => {
|
||||
markdownItRegex(markdownIt, replacePdfShortCode)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
||||
|
||||
export const replacePdfShortCode: RegexOptions = {
|
||||
name: 'pdf-short-code',
|
||||
regex: /^{%pdf (.*) ?%}$/,
|
||||
replace: (match) => {
|
||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-pdf url="${match}"></app-pdf>`
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
||||
|
||||
export const replaceLegacyVimeoShortCode: RegexOptions = {
|
||||
name: 'legacy-vimeo-short-code',
|
||||
regex: /^{%vimeo ([\d]{6,11}) ?%}$/,
|
||||
replace: (match) => {
|
||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-vimeo id="${match}"></app-vimeo>`
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
||||
|
||||
const protocolRegex = /(?:http(?:s)?:\/\/)?/
|
||||
const domainRegex = /(?:player\.)?(?:vimeo\.com\/)(?:(?:channels|album|ondemand|groups)\/\w+\/)?(?:video\/)?/
|
||||
const idRegex = /([\d]{6,11})/
|
||||
const tailRegex = /(?:[?#].*)?/
|
||||
const vimeoVideoUrlRegex = new RegExp(`(?:${protocolRegex.source}${domainRegex.source}${idRegex.source}${tailRegex.source})`)
|
||||
const linkRegex = new RegExp(`^${vimeoVideoUrlRegex.source}$`, 'i')
|
||||
|
||||
export const replaceVimeoLink: RegexOptions = {
|
||||
name: 'vimeo-link',
|
||||
regex: linkRegex,
|
||||
replace: (match) => {
|
||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-vimeo id="${match}"></app-vimeo>`
|
||||
}
|
||||
}
|
|
@ -1,7 +1,11 @@
|
|||
import { DomElement } from 'domhandler'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import React from 'react'
|
||||
import { getAttributesFromHedgeDocTag } from '../utils'
|
||||
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'
|
||||
|
||||
export class VimeoReplacer extends ComponentReplacer {
|
||||
|
@ -16,4 +20,9 @@ export class VimeoReplacer extends ComponentReplacer {
|
|||
return <VimeoFrame id={videoId}/>
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => {
|
||||
markdownItRegex(markdownIt, replaceVimeoLink)
|
||||
markdownItRegex(markdownIt, replaceLegacyVimeoShortCode)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
||||
|
||||
export const replaceLegacyYoutubeShortCode: RegexOptions = {
|
||||
name: 'legacy-youtube-short-code',
|
||||
regex: /^{%youtube ([^"&?\\/\s]{11}) ?%}$/,
|
||||
replace: (match) => {
|
||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-youtube id="${match}"></app-youtube>`
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
||||
|
||||
const protocolRegex = /(?:http(?:s)?:\/\/)?/
|
||||
const subdomainRegex = /(?:www.)?/
|
||||
const pathRegex = /(?:youtube(?:-nocookie)?\.com\/(?:[^\\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)/
|
||||
const idRegex = /([^"&?\\/\s]{11})/
|
||||
const tailRegex = /(?:[?&#].*)?/
|
||||
const youtubeVideoUrlRegex = new RegExp(`(?:${protocolRegex.source}${subdomainRegex.source}${pathRegex.source}${idRegex.source}${tailRegex.source})`)
|
||||
const linkRegex = new RegExp(`^${youtubeVideoUrlRegex.source}$`, 'i')
|
||||
|
||||
export const replaceYouTubeLink: RegexOptions = {
|
||||
name: 'youtube-link',
|
||||
regex: linkRegex,
|
||||
replace: (match) => {
|
||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-youtube id="${match}"></app-youtube>`
|
||||
}
|
||||
}
|
|
@ -1,7 +1,11 @@
|
|||
import { DomElement } from 'domhandler'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import React from 'react'
|
||||
import { getAttributesFromHedgeDocTag } from '../utils'
|
||||
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'
|
||||
|
||||
export class YoutubeReplacer extends ComponentReplacer {
|
||||
|
@ -16,4 +20,9 @@ export class YoutubeReplacer extends ComponentReplacer {
|
|||
return <YouTubeFrame id={videoId}/>
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly markdownItPlugin: MarkdownIt.PluginSimple = (markdownIt) => {
|
||||
markdownItRegex(markdownIt, replaceYouTubeLink)
|
||||
markdownItRegex(markdownIt, replaceLegacyYoutubeShortCode)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue