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,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);
}

View file

@ -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)
}
}

View file

@ -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>`
}
}

View file

@ -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)
}
}

View file

@ -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>`
}
}

View file

@ -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>`
}
}

View file

@ -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>'
})
}

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 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)
}
}

View file

@ -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>`
}
}

View file

@ -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>`
}
}

View file

@ -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>`
}
}

View file

@ -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)
}
}

View file

@ -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>`
}
}

View file

@ -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>`
}
}

View file

@ -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)
}
}