mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-17 00:24:43 -04:00
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:
parent
e03da3bd76
commit
9e6edb0aeb
8 changed files with 103 additions and 1 deletions
|
@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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>`
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>`
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>`
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 }
|
Loading…
Add table
Add a link
Reference in a new issue