mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-13 22:54:42 -04:00
Add quote extra markdown it plugin (#1020)
Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
7f6e0e53a7
commit
5b1940f0ba
10 changed files with 309 additions and 160 deletions
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>`
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue