diff --git a/package.json b/package.json index e2e3e77d1..ab135b7b4 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "eslint-plugin-node": "11.1.0", "eslint-plugin-promise": "4.2.1", "eslint-plugin-standard": "4.0.1", + "flowchart.js": "1.14.0", "fork-awesome": "1.1.7", "github-markdown-css": "4.0.0", "highlight.js": "10.1.2", diff --git a/public/locales/en.json b/public/locales/en.json index 8fdf0ff1a..f1d42db00 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -3,6 +3,11 @@ "slogan": "The best platform to write and share markdown.", "title": "Collaborative markdown notes" }, + "renderer": { + "flowchart": { + "invalidSyntax": "Invalid flowchart.js syntax!" + } + }, "landing": { "intro": { "exploreFeatures": "Explore all features", diff --git a/src/components/editor/editorTestContent.ts b/src/components/editor/editorTestContent.ts index 15ec8c0b5..d06776585 100644 --- a/src/components/editor/editorTestContent.ts +++ b/src/components/editor/editorTestContent.ts @@ -9,6 +9,20 @@ opengraph: # Embedding demo [TOC] +## Flowchart + +\`\`\`flow +st=>start: Start +e=>end: End +op=>operation: My Operation +op2=>operation: lalala +cond=>condition: Yes or No? + +st->op->op2->cond +cond(yes)->e +cond(no)->op2 +\`\`\` + ## CSV \`\`\`csv delimiter=; header diff --git a/src/components/markdown-renderer/markdown-renderer.tsx b/src/components/markdown-renderer/markdown-renderer.tsx index fbc79b9a0..8c3c2dece 100644 --- a/src/components/markdown-renderer/markdown-renderer.tsx +++ b/src/components/markdown-renderer/markdown-renderer.tsx @@ -57,6 +57,7 @@ import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link' import { AsciinemaReplacer } from './replace-components/asciinema/asciinema-replacer' import { ComponentReplacer, SubNodeConverter } from './replace-components/ComponentReplacer' import { CsvReplacer } from './replace-components/csv/csv-replacer' +import { FlowchartReplacer } from './replace-components/flow/flowchart-replacer' import { GistReplacer } from './replace-components/gist/gist-replacer' import { HighlightedCodeReplacer } from './replace-components/highlighted-fence/highlighted-fence-replacer' import { ImageReplacer } from './replace-components/image/image-replacer' @@ -311,6 +312,7 @@ export const MarkdownRenderer: React.FC = ({ content, onM new ImageReplacer(), new TocReplacer(), new CsvReplacer(), + new FlowchartReplacer(), new HighlightedCodeReplacer(), new QuoteOptionsReplacer(), new KatexReplacer() diff --git a/src/components/markdown-renderer/replace-components/flow/flowchart-replacer.tsx b/src/components/markdown-renderer/replace-components/flow/flowchart-replacer.tsx new file mode 100644 index 000000000..f7c537e34 --- /dev/null +++ b/src/components/markdown-renderer/replace-components/flow/flowchart-replacer.tsx @@ -0,0 +1,16 @@ +import { DomElement } from 'domhandler' +import React from 'react' +import { ComponentReplacer } from '../ComponentReplacer' +import { FlowChart } from './flowchart/flowchart' + +export class FlowchartReplacer implements ComponentReplacer { + getReplacement (codeNode: DomElement, index: number): React.ReactElement | undefined { + if (codeNode.name !== 'code' || !codeNode.attribs || !codeNode.attribs['data-highlight-language'] || codeNode.attribs['data-highlight-language'] !== 'flow' || !codeNode.children || !codeNode.children[0]) { + return + } + + const code = codeNode.children[0].data as string + + return + } +} diff --git a/src/components/markdown-renderer/replace-components/flow/flowchart/flowchart.tsx b/src/components/markdown-renderer/replace-components/flow/flowchart/flowchart.tsx new file mode 100644 index 000000000..f3b0b2e5c --- /dev/null +++ b/src/components/markdown-renderer/replace-components/flow/flowchart/flowchart.tsx @@ -0,0 +1,48 @@ +import { parse } from 'flowchart.js' +import React, { useEffect, useRef, useState } from 'react' +import { Alert } from 'react-bootstrap' +import { Trans, useTranslation } from 'react-i18next' + +export interface FlowChartProps { + code: string +} + +export const FlowChart: React.FC = ({ code }) => { + const diagramRef = useRef(null) + const [error, setError] = useState(false) + + useTranslation() + + useEffect(() => { + if (diagramRef.current === null) { + return + } + const parserOutput = parse(code) + try { + parserOutput.drawSVG(diagramRef.current, { + 'line-width': 2, + fill: 'none', + 'font-size': '16px', + 'font-family': 'Source Code Pro, twemoji, monospace' + }) + setError(false) + } catch (error) { + setError(true) + } + + const currentDiagramRef = diagramRef.current + + return () => { + Array.from(currentDiagramRef.children).forEach(value => value.remove()) + } + }, [code]) + + if (error) { + return ( + + + + ) + } + return
+} diff --git a/src/external-types/flowchart.js/index.d.ts b/src/external-types/flowchart.js/index.d.ts new file mode 100644 index 000000000..b6891a7a1 --- /dev/null +++ b/src/external-types/flowchart.js/index.d.ts @@ -0,0 +1,13 @@ +declare module 'flowchart.js' { + type Options = { + 'line-width': number, + 'fill': string, + 'font-size': string, + 'font-family': string + } + type ParseOutput = { + clean: () => void, + drawSVG: (container: HTMLElement, options: Options) => void, + } + export const parse: (code: string) => ParseOutput +} diff --git a/yarn.lock b/yarn.lock index 429e2884e..f489995f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5225,6 +5225,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +eve-raphael@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/eve-raphael/-/eve-raphael-0.5.0.tgz#17c754b792beef3fa6684d79cf5a47c63c4cda30" + integrity sha1-F8dUt5K+7z+maE15z1pHxjxM2jA= + eventemitter2@^6.4.2: version "6.4.3" resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.3.tgz#35c563619b13f3681e7eb05cbdaf50f56ba58820" @@ -5659,6 +5664,13 @@ flatten@^1.0.2: resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg== +flowchart.js@1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/flowchart.js/-/flowchart.js-1.14.0.tgz#b468501be54aa279bada8ec5da222e9eec542492" + integrity sha512-XpXh6v2EUWJ1FyHdwFX1aZKLRpZXZmkMNcqVAV3dbIAmWGW++Oop31SpZRA/cJ8yaFjM16GZl/tb768fG8mBmg== + dependencies: + raphael "2.3.0" + flush-write-stream@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" @@ -10367,6 +10379,13 @@ range-parser@^1.2.1, range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== +raphael@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/raphael/-/raphael-2.3.0.tgz#eabeb09dba861a1d4cee077eaafb8c53f3131f89" + integrity sha512-w2yIenZAQnp257XUWGni4bLMVxpUpcIl7qgxEgDIXtmSypYtlNxfXWpOBxs7LBTps5sDwhRnrToJrMUrivqNTQ== + dependencies: + eve-raphael "0.5.0" + raw-body@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"