diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cb618584..b838e368a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - 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 - 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. --- diff --git a/src/components/editor/editor.tsx b/src/components/editor/editor.tsx index 6050341d3..5047197ad 100644 --- a/src/components/editor/editor.tsx +++ b/src/components/editor/editor.tsx @@ -15,6 +15,11 @@ const Editor: React.FC = () => { const [markdownContent, setMarkdownContent] = useState(`# Embedding demo [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 mazlan1/internet-of-things-the-tip-of-an-iceberg %} diff --git a/src/components/editor/markdown-renderer/markdown-renderer.scss b/src/components/editor/markdown-renderer/markdown-renderer.scss index a4e286a4e..0979151fc 100644 --- a/src/components/editor/markdown-renderer/markdown-renderer.scss +++ b/src/components/editor/markdown-renderer/markdown-renderer.scss @@ -18,4 +18,15 @@ opacity: 1; } } + + blockquote .quote-extra { + font-size: 0.85em; + margin-inline-start: 0.5em; + + &:first-of-type { + &::before { + content: '\2014 \00A0' + } + } + } } diff --git a/src/components/editor/markdown-renderer/markdown-renderer.tsx b/src/components/editor/markdown-renderer/markdown-renderer.tsx index 7b53ca150..9bc21fd1a 100644 --- a/src/components/editor/markdown-renderer/markdown-renderer.tsx +++ b/src/components/editor/markdown-renderer/markdown-renderer.tsx @@ -26,6 +26,9 @@ import { replaceLegacySpeakerdeckShortCode } from './regex-plugins/replace-legac import { replaceLegacyVimeoShortCode } from './regex-plugins/replace-legacy-vimeo-short-code' import { replaceLegacyYoutubeShortCode } from './regex-plugins/replace-legacy-youtube-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 { replaceYouTubeLink } from './regex-plugins/replace-youtube-link' 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 { getTOCReplacement } from './replace-components/toc/toc-replacer' 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' export interface MarkdownPreviewProps { @@ -43,7 +47,7 @@ export type SubNodeConverter = (node: DomElement, index: number) => ReactElement export type ComponentReplacer = (node: DomElement, index: number, counterMap: Map, nodeConverter: SubNodeConverter) => (ReactElement | undefined); type ComponentReplacer2Identifier2CounterMap = Map> -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) => { return allComponentReplacers @@ -90,6 +94,9 @@ const MarkdownRenderer: React.FC = ({ content }) => { md.use(markdownItRegex, replaceVimeoLink) md.use(markdownItRegex, replaceGistLink) md.use(highlightedCode) + md.use(markdownItRegex, replaceQuoteExtraAuthor) + md.use(markdownItRegex, replaceQuoteExtraColor) + md.use(markdownItRegex, replaceQuoteExtraTime) md.use(MarkdownItParserDebugger) validAlertLevels.forEach(level => { diff --git a/src/components/editor/markdown-renderer/regex-plugins/replace-quote-extra-author.ts b/src/components/editor/markdown-renderer/regex-plugins/replace-quote-extra-author.ts new file mode 100644 index 000000000..785cb6e88 --- /dev/null +++ b/src/components/editor/markdown-renderer/regex-plugins/replace-quote-extra-author.ts @@ -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 ` ${match}` + } +} diff --git a/src/components/editor/markdown-renderer/regex-plugins/replace-quote-extra-color.ts b/src/components/editor/markdown-renderer/regex-plugins/replace-quote-extra-color.ts new file mode 100644 index 000000000..d8d806088 --- /dev/null +++ b/src/components/editor/markdown-renderer/regex-plugins/replace-quote-extra-color.ts @@ -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 `` + } +} diff --git a/src/components/editor/markdown-renderer/regex-plugins/replace-quote-extra-time.ts b/src/components/editor/markdown-renderer/regex-plugins/replace-quote-extra-time.ts new file mode 100644 index 000000000..7556a3880 --- /dev/null +++ b/src/components/editor/markdown-renderer/regex-plugins/replace-quote-extra-time.ts @@ -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 ` ${match}` + } +} diff --git a/src/components/editor/markdown-renderer/replace-components/quote-options/quote-options.tsx b/src/components/editor/markdown-renderer/replace-components/quote-options/quote-options.tsx new file mode 100644 index 000000000..08db5436c --- /dev/null +++ b/src/components/editor/markdown-renderer/replace-components/quote-options/quote-options.tsx @@ -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, 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 }