Add quote extra markdown it plugin (#1020)

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
Tilman Vatteroth 2021-02-08 18:29:02 +01:00 committed by GitHub
parent 7f6e0e53a7
commit 5b1940f0ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 309 additions and 160 deletions

View file

@ -19,7 +19,7 @@ import { LinemarkerReplacer } from '../replace-components/linemarker/linemarker-
import { LinkReplacer } from '../replace-components/link-replacer/link-replacer'
import { MarkmapReplacer } from '../replace-components/markmap/markmap-replacer'
import { MermaidReplacer } from '../replace-components/mermaid/mermaid-replacer'
import { QuoteOptionsReplacer } from '../replace-components/quote-options/quote-options-replacer'
import { ColoredBlockquoteReplacer } from '../replace-components/colored-blockquote/colored-blockquote-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'
@ -45,7 +45,7 @@ export const useReplacerInstanceListCreator = (onTaskCheckedChange?: (lineInMark
new MarkmapReplacer(),
new VegaReplacer(),
new HighlightedCodeReplacer(),
new QuoteOptionsReplacer(),
new ColoredBlockquoteReplacer(),
new KatexReplacer(),
new TaskListReplacer(onTaskCheckedChange)
], [onImageClick, onTaskCheckedChange, baseUrl])

View file

@ -22,6 +22,7 @@ import { LineMarkers, lineNumberMarker } from '../replace-components/linemarker/
import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer'
import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
import { BasicMarkdownItConfigurator } from './BasicMarkdownItConfigurator'
import { quoteExtraColor } from '../markdown-it-plugins/quote-extra-color'
export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
constructor(
@ -57,7 +58,15 @@ export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
legacySpeakerdeckShortCode,
AsciinemaReplacer.markdownItPlugin,
highlightedCode,
quoteExtra,
quoteExtraColor,
quoteExtra({
quoteLabel: 'name',
icon: 'user'
}),
quoteExtra({
quoteLabel: 'time',
icon: 'clock-o'
}),
(markdownIt) => documentToc(markdownIt, this.onToc))
if (this.onLineMarkers) {
const callback = this.onLineMarkers

View file

@ -10,7 +10,7 @@ export const MarkdownItParserDebugger: MarkdownIt.PluginSimple = (md: MarkdownIt
if (process.env.NODE_ENV !== 'production') {
md.core.ruler.push('test', (state) => {
console.log(state)
return true
return false
})
}
}

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import MarkdownIt from 'markdown-it/lib'
import { parseQuoteExtraTag } from './quote-extra'
const cssColorRegex = /(#(?:[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 quoteExtraColor: MarkdownIt.PluginSimple = (md) => {
md.inline.ruler.push(`extraQuote_color`, (state) => {
const quoteExtraTagValues = parseQuoteExtraTag(state.src, state.pos, state.posMax)
if (!quoteExtraTagValues || quoteExtraTagValues.label !== 'color') {
return false
}
state.pos = quoteExtraTagValues.valueEndIndex + 1
if (!cssColorRegex.exec(quoteExtraTagValues.value)) {
return false
}
state.pos = quoteExtraTagValues.valueEndIndex + 1
const token = state.push('quote-extra-color', '', 0)
token.attrSet('color', quoteExtraTagValues.value)
return true
})
md.renderer.rules['quote-extra-color'] = (tokens, idx) => {
const token = tokens[idx]
const color = token.attrGet('color') ?? ''
return `<span class="quote-extra" data-color='${ color }' style='color: ${ color }'><i class="fa fa-tag"></i></span>`
}
}

View file

@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { parseQuoteExtraTag, QuoteExtraTagValues } from './quote-extra'
describe('Quote extra syntax parser', () => {
it('should parse a valid tag', () => {
const expected: QuoteExtraTagValues = {
labelStartIndex: 1,
labelEndIndex: 4,
valueStartIndex: 5,
valueEndIndex: 8,
label: 'abc',
value: 'def'
}
expect(parseQuoteExtraTag('[abc=def]', 0, 1000))
.toEqual(expected)
})
it('shouldn\'t parse a tag with no opener bracket', () => {
expect(parseQuoteExtraTag('abc=def]', 0, 1000))
.toEqual(undefined)
})
it('shouldn\'t parse a tag with no closing bracket', () => {
expect(parseQuoteExtraTag('[abc=def', 0, 1000))
.toEqual(undefined)
})
it('shouldn\'t parse a tag with no separation character', () => {
expect(parseQuoteExtraTag('[abcdef]', 0, 1000))
.toEqual(undefined)
})
it('shouldn\'t parse a tag with an empty label', () => {
expect(parseQuoteExtraTag('[=def]', 0, 1000))
.toEqual(undefined)
})
it('shouldn\'t parse a tag with an empty value', () => {
expect(parseQuoteExtraTag('[abc=]', 0, 1000))
.toEqual(undefined)
})
it('shouldn\'t parse a tag with an empty body', () => {
expect(parseQuoteExtraTag('[]', 0, 1000))
.toEqual(undefined)
})
it('shouldn\'t parse a tag with an empty body', () => {
expect(parseQuoteExtraTag('[]', 0, 1000))
.toEqual(undefined)
})
it('shouldn\'t parse a correct tag if start index isn\'t at the opening bracket', () => {
expect(parseQuoteExtraTag('[abc=def]', 1, 1000))
.toEqual(undefined)
})
it('shouldn\'t parse a correct tag if maxPos ends before tag end', () => {
expect(parseQuoteExtraTag('[abc=def]', 0, 1))
.toEqual(undefined)
})
it('shouldn\'t parse a correct tag if start index is after maxPos', () => {
expect(parseQuoteExtraTag(' [abc=def]', 3, 2))
.toEqual(undefined)
})
})

View file

@ -4,44 +4,112 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import MarkdownIt from 'markdown-it'
import markdownItRegex from 'markdown-it-regex'
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface'
import MarkdownIt from 'markdown-it/lib'
import Token from 'markdown-it/lib/token'
import { IconName } from '../../common/fork-awesome/types'
export const quoteExtra: MarkdownIt.PluginSimple = (markdownIt) => {
markdownItRegex(markdownIt, replaceQuoteExtraAuthor)
markdownItRegex(markdownIt, replaceQuoteExtraColor)
markdownItRegex(markdownIt, replaceQuoteExtraTime)
export interface QuoteExtraOptions {
quoteLabel: string
icon: IconName
}
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>`
export const quoteExtra: (pluginOptions: QuoteExtraOptions) => MarkdownIt.PluginSimple =
(pluginOptions) => (md) => {
md.inline.ruler.push(`extraQuote_${ pluginOptions.quoteLabel }`, (state) => {
const quoteExtraTagValues = parseQuoteExtraTag(state.src, state.pos, state.posMax)
if (!quoteExtraTagValues || quoteExtraTagValues.label !== pluginOptions.quoteLabel) {
return false
}
state.pos = quoteExtraTagValues.valueEndIndex + 1
const tokens: Token[] = []
state.md.inline.parse(
quoteExtraTagValues.value,
state.md,
state.env,
tokens
)
const token = state.push('quote-extra', '', 0)
token.attrSet('icon', pluginOptions.icon)
token.children = tokens
return true
})
if (md.renderer.rules['quote-extra']) {
return
}
md.renderer.rules['quote-extra'] = (tokens, idx, options: MarkdownIt.Options, env: unknown) => {
const token = tokens[idx]
const innerTokens = token.children
if (!innerTokens) {
return ''
}
const innerHtml = md.renderer.renderInline(innerTokens, options, env)
return `<span class="quote-extra"><i class="fa fa-${ token.attrGet('icon') ?? '' } mx-1"></i>${ innerHtml }</span>`
}
}
export interface QuoteExtraTagValues {
labelStartIndex: number,
labelEndIndex: number,
valueStartIndex: number,
valueEndIndex: number,
label: string,
value: string
}
export const parseQuoteExtraTag = (line: string, start: number, maxPos: number): QuoteExtraTagValues | undefined => {
if (line[start] !== '[') {
return
}
const labelStartIndex = start + 1
const labelEndIndex = parseLabel(line, labelStartIndex, maxPos)
if (!labelEndIndex || labelStartIndex === labelEndIndex) {
return
}
const valueStartIndex = labelEndIndex + 1
const valueEndIndex = parseValue(line, valueStartIndex, maxPos)
if (!valueEndIndex || valueStartIndex === valueEndIndex) {
return
}
return {
labelStartIndex,
labelEndIndex,
valueStartIndex,
valueEndIndex,
label: line.substr(labelStartIndex, labelEndIndex - labelStartIndex),
value: line.substr(valueStartIndex, valueEndIndex - valueStartIndex)
}
}
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 replaceQuoteExtraColor: RegexOptions = {
name: 'quote-extra-color',
regex: cssColorRegex,
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" data-color='${ match }' style='color: ${ match }'><i class="fa fa-tag"></i></span>`
const parseValue = (line: string, start: number, maxPos: number): number | undefined => {
let level = 1
for (let pos = start; pos <= maxPos; pos += 1) {
const currentCharacter = line[pos]
if (currentCharacter === ']') {
level -= 1
if (level === 0) {
return pos
}
} else if (currentCharacter === '[') {
level += 1
}
}
}
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>`
const parseLabel = (line: string, start: number, maxPos: number): number | undefined => {
for (let pos = start; pos <= maxPos; pos += 1) {
if (line[pos] === '=') {
return pos
}
}
}

View file

@ -1,12 +1,12 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { DomElement } from 'domhandler'
import { ReactElement } from 'react'
import { ComponentReplacer } from '../ComponentReplacer'
import { ComponentReplacer, NativeRenderer, SubNodeTransform } from '../ComponentReplacer'
const isColorExtraElement = (node: DomElement | undefined): boolean => {
if (!node || !node.attribs || !node.attribs.class || !node.attribs['data-color']) {
@ -24,8 +24,8 @@ const findQuoteOptionsParent = (nodes: DomElement[]): DomElement | undefined =>
})
}
export class QuoteOptionsReplacer extends ComponentReplacer {
public getReplacement(node: DomElement): ReactElement | undefined {
export class ColoredBlockquoteReplacer extends ComponentReplacer {
public getReplacement(node: DomElement, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): ReactElement | undefined {
if (node.name !== 'blockquote' || !node.children || node.children.length < 1) {
return
}
@ -44,5 +44,6 @@ export class QuoteOptionsReplacer extends ComponentReplacer {
return
}
node.attribs = Object.assign(node.attribs || {}, { style: `border-left-color: ${ attributes['data-color'] };` })
return nativeRenderer()
}
}