From 4022975e4d81c3fa27d39189a41cf093fc78099d Mon Sep 17 00:00:00 2001 From: dtoro Date: Sun, 8 Mar 2026 21:56:08 +0100 Subject: [PATCH] add markdown support --- package-lock.json | 91 +++++++++++++++ package.json | 2 + src/components/graph/BaseNode.tsx | 6 +- src/components/graph/ConfigNode.tsx | 138 +++++++++++++++------- src/components/graph/NodeMenubar.tsx | 8 +- src/components/graph/RenderingNode.tsx | 103 ++++++++--------- src/lib/configTypes.ts | 152 +++++++++++++++++++++++++ src/lib/flowUtils.ts | 4 +- 8 files changed, 403 insertions(+), 101 deletions(-) create mode 100644 src/lib/configTypes.ts diff --git a/package-lock.json b/package-lock.json index eca1d98..d5a241f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@codemirror/lang-javascript": "^6.2.2", + "@codemirror/lang-markdown": "^6.5.0", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-menubar": "^1.1.16", "@radix-ui/react-select": "^2.2.6", @@ -20,6 +21,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.577.0", + "marked": "^17.0.4", "nunjucks": "^3.2.4", "react": "18.2.0", "react-dom": "18.2.0", @@ -572,6 +574,36 @@ "@lezer/common": "^1.1.0" } }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, "node_modules/@codemirror/lang-javascript": { "version": "6.2.5", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", @@ -587,6 +619,21 @@ "@lezer/javascript": "^1.0.0" } }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz", + "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, "node_modules/@codemirror/language": { "version": "6.12.2", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz", @@ -1444,6 +1491,17 @@ "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", "license": "MIT" }, + "node_modules/@lezer/css": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.1.tgz", + "integrity": "sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, "node_modules/@lezer/highlight": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", @@ -1453,6 +1511,17 @@ "@lezer/common": "^1.3.0" } }, + "node_modules/@lezer/html": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/javascript": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", @@ -1473,6 +1542,16 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@lezer/markdown": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz", + "integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0" + } + }, "node_modules/@marijn/find-cluster-break": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", @@ -5764,6 +5843,18 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/marked": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz", + "integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index 7a4f1af..d7078b3 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@codemirror/lang-javascript": "^6.2.2", + "@codemirror/lang-markdown": "^6.5.0", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-menubar": "^1.1.16", "@radix-ui/react-select": "^2.2.6", @@ -20,6 +21,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.577.0", + "marked": "^17.0.4", "nunjucks": "^3.2.4", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/src/components/graph/BaseNode.tsx b/src/components/graph/BaseNode.tsx index 80ef236..9829ba1 100644 --- a/src/components/graph/BaseNode.tsx +++ b/src/components/graph/BaseNode.tsx @@ -115,10 +115,13 @@ export function BaseNodeHeader({ export function BaseNodeHeaderRow({ icon, title, + right, className, }: { icon: ReactNode; title: ReactNode; + /** Optional right-side content (e.g. type selector). */ + right?: ReactNode; className?: string; }) { return ( @@ -131,10 +134,11 @@ export function BaseNodeHeaderRow({ {icon}

{title}

+ {right != null ?
{right}
: null} ); } diff --git a/src/components/graph/ConfigNode.tsx b/src/components/graph/ConfigNode.tsx index 5691305..cf0c9eb 100644 --- a/src/components/graph/ConfigNode.tsx +++ b/src/components/graph/ConfigNode.tsx @@ -1,12 +1,22 @@ import React, { memo, useCallback, useContext, useMemo, useRef } from 'react' import { autocompletion } from '@codemirror/autocomplete' import CodeMirror from '@uiw/react-codemirror' +import { markdown } from '@codemirror/lang-markdown' import FlowContext from '../../lib/flowContext' import { nodePropsAreEqual } from '../../lib/flowUtils' import { useResizeHeight } from '../../hooks/useResizeHeight' import { nunjucksCompletionSource } from '../../lib/nunjucksAutocomplete' import { plantumlLanguage } from '../../lib/plantumlLanguage' import { useTheme } from '../../lib/themeContext' +import { + CONFIG_TYPES, + getConfigContent, + getConfigType, + getConfigTypeId, + getDefaultContentForConfigType, + isGroup, + type ConfigTypeId, +} from '../../lib/configTypes' import { BaseNode, BaseNodeContent, @@ -20,6 +30,7 @@ import { MenubarSubContent, MenubarSubTrigger, } from '../ui/menubar' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' import { InputHandle, OutputHandle } from './NodeHandles' import { NodeFooterEdgeIndicators } from './NodeFooterEdgeIndicators' import { NodeHeaderTitle } from './NodeHeaderTitle' @@ -32,10 +43,10 @@ type Props = { height?: number } -const DEFAULT_PLANTUML = '@startuml\n\n@enduml\n' - export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: Props) { - const plantumlValue = data?.plantuml ?? DEFAULT_PLANTUML + const configTypeId = getConfigTypeId(data) + const configType = getConfigType(configTypeId) + const content = getConfigContent(data) || getDefaultContentForConfigType(configTypeId) const { theme } = useTheme() const ctx = useContext(FlowContext) const setNodes = ctx?.setNodes @@ -63,10 +74,37 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: const onChange = useCallback( (val: string) => { if (setNodes) { - setNodes((nds: any[]) => nds.map((n) => (n.id === id ? { ...n, data: { ...n.data, plantuml: val } } : n))) + setNodes((nds: any[]) => + nds.map((n) => + n.id === id ? { ...n, data: { ...n.data, content: val, configType: configTypeId } } : n + ) + ) } }, - [id, setNodes] + [id, setNodes, configTypeId] + ) + + const setConfigType = useCallback( + (newTypeId: ConfigTypeId) => { + if (!setNodes || newTypeId === configTypeId) return + setNodes((nds: any[]) => + nds.map((n) => + n.id === id + ? { + ...n, + data: { + ...n.data, + configType: newTypeId, + content: + getConfigContent(n.data) || + getDefaultContentForConfigType(newTypeId), + }, + } + : n + ) + ) + }, + [id, setNodes, configTypeId] ) const insertAt = useCallback( @@ -91,12 +129,12 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: return } if (mode === 'prepend') { - onChange(insertText + plantumlValue) + onChange(insertText + content) } else { - onChange(plantumlValue + insertText) + onChange(content + insertText) } }, - [onChange, plantumlValue] + [onChange, content] ) const insertExtendsFromNode = useCallback( @@ -142,30 +180,44 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: ) const extensions = useMemo( () => [ - plantumlLanguage.extension, + configType.language === 'plantuml' ? plantumlLanguage.extension : markdown(), autocompletion({ override: [nunjucksCompletionSource(variableIds, configTitles, functionIds)], activateOnTyping: true, }), ], - [variableIds, functionIds, configTitles], + [configType.language, variableIds, functionIds, configTitles], ) const [editorHeight, editorContainerRef] = useResizeHeight(180) - const nunjucksTagSnippets = useMemo( - () => [ - { label: 'Variable {{ }}', snippet: '{{ }}' }, - { label: 'if / endif', snippet: '{% if %}\n \n{% endif %}' }, - { label: 'for / endfor', snippet: '{% for in %}\n \n{% endfor %}' }, - { label: 'set', snippet: '{% set = %}' }, - { label: 'block / endblock', snippet: '{% block %}\n \n{% endblock %}' }, - { label: 'extends', snippet: '{% extends "" %}' }, - { label: 'include', snippet: '{% include "" %}' }, - { label: 'import', snippet: '{% import "" as %}' }, - { label: 'raw / endraw', snippet: '{% raw %}\n \n{% endraw %}' }, - ], - [] - ) + const insertBlocksContent = useMemo(() => { + return configType.insertBlocks.map((block, idx) => + isGroup(block) ? ( + + {block.label} + + {block.items.map(({ label, snippet }) => ( + insertAt(snippet, 'cursor')} + > + {label} + + ))} + + + ) : ( + insertAt(block.snippet, 'cursor')} + > + {block.label} + + ) + ) + }, [configType.insertBlocks, insertAt]) const dimensions = width != null && height != null && width > 0 && height > 0 @@ -174,7 +226,24 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: return ( }> - } title={} /> + } + title={} + right={ + + } + />
@@ -247,19 +316,8 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: ) : undefined } - insertTagsContent={ - <> - {nunjucksTagSnippets.map(({ label, snippet }) => ( - insertAt(snippet, 'cursor')} - > - {label} - - ))} - - } + insertTagsContent={insertBlocksContent} + insertTagsLabel={configType.label} />
@@ -267,7 +325,7 @@ export const ConfigNode = memo(function ConfigNode({ id, data, width, height }: - {plantumlValue ? `${plantumlValue.length} chars` : 'none'} + {content ? `${content.length} chars` : 'none'}
diff --git a/src/components/graph/NodeMenubar.tsx b/src/components/graph/NodeMenubar.tsx index 64b9c24..f26b2d5 100644 --- a/src/components/graph/NodeMenubar.tsx +++ b/src/components/graph/NodeMenubar.tsx @@ -22,13 +22,15 @@ type Props = { nodeType: NodeType /** Content for Insert → Inputs (config and function nodes only) */ editInputsContent?: React.ReactNode - /** Content for Insert → Tags (e.g. Nunjucks tag snippets, config nodes) */ + /** Content for Insert → [Blocks/Tags] (e.g. type-specific snippets, config nodes) */ insertTagsContent?: React.ReactNode + /** Label for Insert submenu that shows insertTagsContent (default "Tags") */ + insertTagsLabel?: string /** Extra content in Node menu (e.g. Export submenu for render nodes), before the separator */ nodeMenuExtraContent?: React.ReactNode } -export function NodeMenubar({ nodeId, nodeType, editInputsContent, insertTagsContent, nodeMenuExtraContent }: Props) { +export function NodeMenubar({ nodeId, nodeType, editInputsContent, insertTagsContent, insertTagsLabel = 'Tags', nodeMenuExtraContent }: Props) { const ctx = useContext(FlowContext) const nodes = ctx?.nodes ?? [] const setNodes = ctx?.setNodes @@ -123,7 +125,7 @@ export function NodeMenubar({ nodeId, nodeType, editInputsContent, insertTagsCon {insertTagsContent != null && ( - Tags + {insertTagsLabel} {insertTagsContent} diff --git a/src/components/graph/RenderingNode.tsx b/src/components/graph/RenderingNode.tsx index 8502901..a62588d 100644 --- a/src/components/graph/RenderingNode.tsx +++ b/src/components/graph/RenderingNode.tsx @@ -1,6 +1,7 @@ import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import nunjucks from 'nunjucks' import FlowContext from '../../lib/flowContext' +import { getConfigContent, getConfigType, getConfigTypeId } from '../../lib/configTypes' import { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia } from '../ui/empty' import { BaseNode, @@ -17,9 +18,6 @@ import { MenubarItem, MenubarSeparator, MenubarSub, MenubarSubContent, MenubarSu import { Sparkles } from 'lucide-react' import { InputHandle } from './NodeHandles' -// Use relative URL so Vite dev proxy (and optional prod proxy) avoids CORS -const KROKI_PLANTUML_SVG = '/api/kroki/plantuml/svg' - type Props = { id: string data?: any @@ -27,7 +25,7 @@ type Props = { } export const RenderingNode = memo(function RenderingNode({ id, width, height }: Props) { - const [svgContent, setSvgContent] = useState(null) + const [renderedContent, setRenderedContent] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const runIdRef = useRef(0) @@ -44,7 +42,8 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: const incomingIds = useMemo(() => incomingEdges.map((e: any) => e.source).sort(), [incomingEdges]) const srcId = incomingIds.length > 0 ? incomingIds[0] : null const srcNode = nodes.find((n: any) => n.id === srcId) - const plantumlText = srcNode && srcNode.type === 'config' ? srcNode?.data?.plantuml ?? '' : '' + const configTypeId = srcNode?.type === 'config' ? getConfigTypeId(srcNode.data) : 'plantuml' + const sourceContent = srcNode?.type === 'config' ? getConfigContent(srcNode.data) : '' const srcData = srcNode?.data ?? {} /** Set of node IDs that can affect this render node (configs in the chain + variables/functions feeding them) */ @@ -86,7 +85,7 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: if (!node) return visited.add(nodeId) out.add(nodeId) - const content = String(node.data?.plantuml ?? '') + const content = getConfigContent(node.data) for (const ref of getTemplateRefs(content)) { const refId = resolveRef(ref) if (refId && nodes.some((n: any) => n.id === refId && n.type === 'config') && isReachable(refId, id)) @@ -109,7 +108,7 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: () => nodes .filter((n: any) => n.type === 'config' && connectedNodeIds.has(n.id)) - .map((n: any) => `${n.id}:${n.data?.title ?? ''}:${n.data?.plantuml ?? ''}`) + .map((n: any) => `${n.id}:${n.data?.title ?? ''}:${getConfigContent(n.data)}`) .sort() .join('|'), [nodes, connectedNodeIds] @@ -146,15 +145,15 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: ) useEffect(() => { - if (!plantumlText) { - if (incomingIds.length === 0) { - setSvgContent(null) - setError(null) - setLoading(false) - return - } - setSvgContent(null) - setError({ kind: 'no-plantuml', message: 'No PlantUML found on connected configuration node' }) + if (!sourceContent && incomingIds.length > 0) { + setRenderedContent(null) + setError({ kind: 'no-content', message: 'No content on connected configuration node' }) + setLoading(false) + return + } + if (incomingIds.length === 0) { + setRenderedContent(null) + setError(null) setLoading(false) return } @@ -213,7 +212,7 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: throw new Error(`Referenced config not connected to renderer: ${templateName}`) visited.add(refId) configIdsUsed.add(refId) - const content = String(node.data?.plantuml ?? '') + const content = getConfigContent(node.data) for (const ref of getTemplateRefs(content)) addConfigAndRefs(ref, visited) } @@ -228,7 +227,7 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: if (refId !== srcId && !isReachable(refId, id)) throw new Error(`Referenced config not connected to renderer: ${name}`) return { - src: String(node.data?.plantuml ?? ''), + src: getConfigContent(node.data), path: name, } }, @@ -428,30 +427,18 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: return } - const resolvedPlantuml = afterNunjucks + const resolvedContent = afterNunjucks + const typeRenderer = getConfigType(configTypeId) try { - const res = await fetch(KROKI_PLANTUML_SVG, { - method: 'POST', - headers: { 'Content-Type': 'text/plain' }, - body: resolvedPlantuml, - }) - + const htmlOrSvg = await typeRenderer.render(resolvedContent) if (cancelled || thisRunId !== runIdRef.current) return - - if (!res.ok) { - const errText = await res.text() - throw new Error(res.status === 400 ? errText || 'Invalid PlantUML' : `Kroki error: ${res.status}`) - } - - const svg = await res.text() - if (thisRunId !== runIdRef.current) return - setSvgContent(svg) + setRenderedContent(htmlOrSvg) setError(null) } catch (err: any) { if (cancelled || thisRunId !== runIdRef.current) return - const msg = err?.message ?? 'PlantUML render error' - setSvgContent(null) + const msg = err?.message ?? 'Render error' + setRenderedContent(null) setError({ kind: 'render', message: msg }) } finally { if (!cancelled && thisRunId === runIdRef.current) { @@ -471,8 +458,8 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: }) } catch (err: any) { if (!cancelled && thisRunId === runIdRef.current) { - setSvgContent(null) - setError({ kind: 'render', message: err?.message ?? 'PlantUML render error' }) + setRenderedContent(null) + setError({ kind: 'render', message: err?.message ?? 'Render error' }) setLoading(false) } } @@ -486,29 +473,31 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: minLoadingTimeoutRef.current = null } } - // Only re-run when inputs that affect the resolved diagram change (signatures + source). + // Only re-run when inputs that affect the resolved output change (signatures + source). // Do not depend on nodes/edges refs to avoid flicker from unnecessary re-renders. - }, [id, plantumlText, configSignature, edgesSignature, variablesSignature, functionsSignature]) + }, [id, sourceContent, configTypeId, configSignature, edgesSignature, variablesSignature, functionsSignature]) const dimensions = width != null && height != null && width > 0 && height > 0 ? { width, height } : undefined + const isSvgOutput = Boolean(renderedContent?.trim().startsWith(' { - if (!svgContent) return - const blob = new Blob([svgContent], { type: 'image/svg+xml' }) + if (!renderedContent || !isSvgOutput) return + const blob = new Blob([renderedContent], { type: 'image/svg+xml' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `${id}.svg` a.click() URL.revokeObjectURL(url) - }, [id, svgContent]) + }, [id, renderedContent, isSvgOutput]) const downloadPng = useCallback(() => { - if (!svgContent) return - const dataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgContent) + if (!renderedContent || !isSvgOutput) return + const dataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(renderedContent) const img = new Image() img.onload = () => { const canvas = document.createElement('canvas') @@ -525,9 +514,9 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: } img.onerror = () => {} img.src = dataUrl - }, [id, svgContent]) + }, [id, renderedContent, isSvgOutput]) - const status = loading ? 'loading' : error ? 'error' : svgContent ? 'success' : 'initial' + const status = loading ? 'loading' : error ? 'error' : renderedContent ? 'success' : 'initial' return ( @@ -542,14 +531,14 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: nodeMenuExtraContent={ - + Export - + PNG - + SVG @@ -565,7 +554,7 @@ export const RenderingNode = memo(function RenderingNode({ id, width, height }: No configuration connected - Connect a Configuration node or create one. The renderer will display the PlantUML diagram. + Connect a Configuration node or create one. The renderer will display the diagram or document.