Add blockquote extras ([name=...], [time=...], [color=...]) (#249)

* Added regexes to replace name and time extras

* Added [color=#abc] replacements inside of blockquotes

This works with the following "algorithm":
1. Transform any [color=]-Tags with a valid css color into a codimd-quote-options element.
2. While transforming blockquotes, check if one of their paragraphs contains a codimd-quote-options element. If multiple are found, only the first one will be used.
3. Remove the codimd-quote-options element and set the border-left-color of the blockquote appropriately.

* Added correct CSS styling of blockquote extras

* Added tag icon when [color=...] is used outside a blockquote

In version 1.6 of CodiMD the [color=...] tag renders a tag-icon in the specified color when used outside of a blockquote paragraph.

* Added changelog entry

* Flip if-else in quote-options for better readability

* Flip another if-else in quote-options for better readability
This commit is contained in:
Erik Michelson 2020-06-22 23:37:20 +02:00 committed by GitHub
parent e03da3bd76
commit 9e6edb0aeb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 103 additions and 1 deletions

View file

@ -34,6 +34,7 @@
- The history shows both the entries saved in LocalStorage and the entries saved on the server together - The history shows both the entries saved in LocalStorage and the entries saved on the server together
- The gist and pdf embeddings now use a one-click aproach similar to vimeo and youtube - The gist and pdf embeddings now use a one-click aproach similar to vimeo and youtube
- Use [Twemoji](https://twemoji.twitter.com/) as icon font - Use [Twemoji](https://twemoji.twitter.com/) as icon font
- The `[name=...]`, `[time=...]` and `[color=...]` tags may now be used anywhere in the document and not just inside of blockquotes and lists.
--- ---

View file

@ -15,6 +15,11 @@ const Editor: React.FC = () => {
const [markdownContent, setMarkdownContent] = useState(`# Embedding demo const [markdownContent, setMarkdownContent] = useState(`# Embedding demo
[TOC] [TOC]
## Blockquote
> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
> [color=red] [name=John Doe] [time=2020-06-21 22:50]
## Slideshare ## Slideshare
{%slideshare mazlan1/internet-of-things-the-tip-of-an-iceberg %} {%slideshare mazlan1/internet-of-things-the-tip-of-an-iceberg %}

View file

@ -18,4 +18,15 @@
opacity: 1; opacity: 1;
} }
} }
blockquote .quote-extra {
font-size: 0.85em;
margin-inline-start: 0.5em;
&:first-of-type {
&::before {
content: '\2014 \00A0'
}
}
}
} }

View file

@ -26,6 +26,9 @@ import { replaceLegacySpeakerdeckShortCode } from './regex-plugins/replace-legac
import { replaceLegacyVimeoShortCode } from './regex-plugins/replace-legacy-vimeo-short-code' import { replaceLegacyVimeoShortCode } from './regex-plugins/replace-legacy-vimeo-short-code'
import { replaceLegacyYoutubeShortCode } from './regex-plugins/replace-legacy-youtube-short-code' import { replaceLegacyYoutubeShortCode } from './regex-plugins/replace-legacy-youtube-short-code'
import { replacePdfShortCode } from './regex-plugins/replace-pdf-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 { replaceVimeoLink } from './regex-plugins/replace-vimeo-link'
import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link' import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
import { getGistReplacement } from './replace-components/gist/gist-frame' import { getGistReplacement } from './replace-components/gist/gist-frame'
@ -33,6 +36,7 @@ import { getHighlightedCodeBlock } from './replace-components/highlighted-code/h
import { getPDFReplacement } from './replace-components/pdf/pdf-frame' import { getPDFReplacement } from './replace-components/pdf/pdf-frame'
import { getTOCReplacement } from './replace-components/toc/toc-replacer' import { getTOCReplacement } from './replace-components/toc/toc-replacer'
import { getVimeoReplacement } from './replace-components/vimeo/vimeo-frame' import { getVimeoReplacement } from './replace-components/vimeo/vimeo-frame'
import { getQuoteOptionsReplacement } from './replace-components/quote-options/quote-options'
import { getYouTubeReplacement } from './replace-components/youtube/youtube-frame' import { getYouTubeReplacement } from './replace-components/youtube/youtube-frame'
export interface MarkdownPreviewProps { export interface MarkdownPreviewProps {
@ -43,7 +47,7 @@ export type SubNodeConverter = (node: DomElement, index: number) => ReactElement
export type ComponentReplacer = (node: DomElement, index: number, counterMap: Map<string, number>, nodeConverter: SubNodeConverter) => (ReactElement | undefined); export type ComponentReplacer = (node: DomElement, index: number, counterMap: Map<string, number>, nodeConverter: SubNodeConverter) => (ReactElement | undefined);
type ComponentReplacer2Identifier2CounterMap = Map<ComponentReplacer, Map<string, number>> type ComponentReplacer2Identifier2CounterMap = Map<ComponentReplacer, Map<string, number>>
const allComponentReplacers: ComponentReplacer[] = [getYouTubeReplacement, getVimeoReplacement, getGistReplacement, getPDFReplacement, getTOCReplacement, getHighlightedCodeBlock] const allComponentReplacers: ComponentReplacer[] = [getYouTubeReplacement, getVimeoReplacement, getGistReplacement, getPDFReplacement, getTOCReplacement, getHighlightedCodeBlock, getQuoteOptionsReplacement]
const tryToReplaceNode = (node: DomElement, index:number, componentReplacer2Identifier2CounterMap: ComponentReplacer2Identifier2CounterMap, nodeConverter: SubNodeConverter) => { const tryToReplaceNode = (node: DomElement, index:number, componentReplacer2Identifier2CounterMap: ComponentReplacer2Identifier2CounterMap, nodeConverter: SubNodeConverter) => {
return allComponentReplacers return allComponentReplacers
@ -90,6 +94,9 @@ const MarkdownRenderer: React.FC<MarkdownPreviewProps> = ({ content }) => {
md.use(markdownItRegex, replaceVimeoLink) md.use(markdownItRegex, replaceVimeoLink)
md.use(markdownItRegex, replaceGistLink) md.use(markdownItRegex, replaceGistLink)
md.use(highlightedCode) md.use(highlightedCode)
md.use(markdownItRegex, replaceQuoteExtraAuthor)
md.use(markdownItRegex, replaceQuoteExtraColor)
md.use(markdownItRegex, replaceQuoteExtraTime)
md.use(MarkdownItParserDebugger) md.use(MarkdownItParserDebugger)
validAlertLevels.forEach(level => { validAlertLevels.forEach(level => {

View file

@ -0,0 +1,11 @@
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

@ -0,0 +1,13 @@
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
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 = {
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>`
}
}

View file

@ -0,0 +1,11 @@
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

@ -0,0 +1,43 @@
import { DomElement } from 'domhandler'
import { ReactElement } from 'react'
import { SubNodeConverter } from '../../markdown-renderer'
const isColorExtraElement = (node: DomElement | undefined): boolean => {
if (!node || !node.attribs || !node.attribs.class || !node.attribs['data-color']) {
return false
}
return (node.name === 'span' && node.attribs.class === 'quote-extra')
}
const findQuoteOptionsParent = (nodes: DomElement[]): DomElement | undefined => {
return nodes.find((child) => {
if (child.name !== 'p' || !child.children || child.children.length < 1) {
return false
}
return child.children.find(isColorExtraElement) !== undefined
})
}
const getElementReplacement = (node: DomElement, index: number, counterMap: Map<string, number>, nodeConverter: SubNodeConverter): (ReactElement | undefined) => {
if (node.name !== 'blockquote' || !node.children || node.children.length < 1) {
return
}
const paragraph = findQuoteOptionsParent(node.children)
if (!paragraph) {
return
}
const childElements = paragraph.children || []
const optionsTag = childElements.find(isColorExtraElement)
if (!optionsTag) {
return
}
paragraph.children = childElements.filter(elem => !isColorExtraElement(elem))
const attributes = optionsTag.attribs
if (!attributes || !attributes['data-color']) {
return
}
node.attribs = Object.assign(node.attribs || {}, { style: `border-left-color: ${attributes['data-color']};` })
return nodeConverter(node, index)
}
export { getElementReplacement as getQuoteOptionsReplacement }