fix: nunjuclk rendering

This commit is contained in:
2026-03-11 00:05:32 +01:00
parent 6014f34f76
commit 08d66b59c2

View File

@@ -480,12 +480,14 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
return return
} }
setResolvedContent(afterNunjucks) // Collapse runs of newlines so {% for %} / {{ }} on their own lines don't leave blank lines
const resolved = afterNunjucks.replace(/(\r?\n)(\s*\r?\n)+/g, '$1').trim()
setResolvedContent(resolved)
const typeRenderer = getConfigType(configTypeId) const typeRenderer = getConfigType(configTypeId)
try { try {
const renderOptions = configTypeId === 'wireframe' ? { width: viewportWidth, height: viewportHeight } : undefined const renderOptions = configTypeId === 'wireframe' ? { width: viewportWidth, height: viewportHeight } : undefined
const htmlOrSvg = await typeRenderer.render(afterNunjucks, renderOptions) const htmlOrSvg = await typeRenderer.render(resolved, renderOptions)
if (cancelled || thisRunId !== runIdRef.current) return if (cancelled || thisRunId !== runIdRef.current) return
setRenderedContent(htmlOrSvg) setRenderedContent(htmlOrSvg)
setError(null) setError(null)
@@ -539,6 +541,18 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
// Kroki and other SVG sources may prepend <?xml ... ?> so we detect by presence of <svg> tag // Kroki and other SVG sources may prepend <?xml ... ?> so we detect by presence of <svg> tag
const isSvgOutput = Boolean(renderedContent?.trim() && /<svg[\s>]/i.test(renderedContent.trim())) const isSvgOutput = Boolean(renderedContent?.trim() && /<svg[\s>]/i.test(renderedContent.trim()))
/** Strip remaining Nunjucks tags from resolved content for display in raw view (so tags don't show as literal lines). */
const rawDisplayContent = useMemo(() => {
if (resolvedContent == null) return ''
return resolvedContent
.replace(/\{%[\s\S]*?%\}/g, '')
.replace(/\{\{[\s\S]*?\}\}/g, '')
.replace(/\{#[\s\S]*?#\}/g, '')
.replace(/(\r?\n)\s*(\r?\n)/g, '$1$2')
.replace(/^\s*\n|\n\s*$/g, (m) => (m === '\n' ? '\n' : ''))
.trim()
}, [resolvedContent])
/** Process SVG HTML so it keeps aspect ratio and fills the viewport (used only for display, not download). */ /** Process SVG HTML so it keeps aspect ratio and fills the viewport (used only for display, not download). */
const displayContent = useMemo(() => { const displayContent = useMemo(() => {
if (!renderedContent || !isSvgOutput) return renderedContent if (!renderedContent || !isSvgOutput) return renderedContent
@@ -650,16 +664,34 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
<ToggleGroup <ToggleGroup
type="single" type="single"
value={viewMode} value={viewMode}
onValueChange={(v) => v && (v === 'preview' || v === 'raw') && setViewMode(v)} onValueChange={(v) => {
if (v === 'preview' || v === 'raw') setViewMode(v)
}}
aria-label="View mode" aria-label="View mode"
variant="outline" variant="outline"
size="sm" size="sm"
className="gap-0 rounded-md p-0.5 [&>button]:rounded-none [&>button:first-child]:rounded-l-md [&>button:last-child]:rounded-r-md [&>button:not(:first-child)]:border-l-0" className="gap-0 rounded-md p-0.5 [&>button]:rounded-none [&>button:first-child]:rounded-l-md [&>button:last-child]:rounded-r-md [&>button:not(:first-child)]:border-l-0"
> >
<ToggleGroupItem value="preview" aria-label="Preview" className="gap-1.5 px-2.5 h-7"> <ToggleGroupItem
value="preview"
aria-label="Preview"
className="gap-1.5 px-2.5 h-7"
onClick={(e) => {
e.stopPropagation()
setViewMode('preview')
}}
>
Preview Preview
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem value="raw" aria-label="Raw config" className="gap-1.5 px-2.5 h-7"> <ToggleGroupItem
value="raw"
aria-label="Raw config"
className="gap-1.5 px-2.5 h-7"
onClick={(e) => {
e.stopPropagation()
setViewMode('raw')
}}
>
Raw Raw
</ToggleGroupItem> </ToggleGroupItem>
</ToggleGroup> </ToggleGroup>
@@ -800,18 +832,18 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
size="icon" size="icon"
className="absolute top-2 right-2 z-10 h-7 w-7 shrink-0 rounded-md shadow-sm" className="absolute top-2 right-2 z-10 h-7 w-7 shrink-0 rounded-md shadow-sm"
onClick={() => { onClick={() => {
const text = resolvedContent ?? '' const text = rawDisplayContent
if (text) { if (text) {
navigator.clipboard?.writeText(text).then(() => toast.success('Copied to clipboard')).catch(() => {}) navigator.clipboard?.writeText(text).then(() => toast.success('Copied to clipboard')).catch(() => {})
} }
}} }}
disabled={!resolvedContent} disabled={!rawDisplayContent}
title="Copy raw output" title="Copy raw output"
> >
<Copy className="size-3.5" /> <Copy className="size-3.5" />
</Button> </Button>
<CodeMirror <CodeMirror
value={resolvedContent ?? ''} value={rawDisplayContent}
height={`${rawEditorHeight}px`} height={`${rawEditorHeight}px`}
theme={theme} theme={theme}
extensions={rawExtensions} extensions={rawExtensions}
@@ -917,7 +949,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
<BaseNodeFooter> <BaseNodeFooter>
<NodeFooterEdgeIndicators nodeId={id} nodeType="render"> <NodeFooterEdgeIndicators nodeId={id} nodeType="render">
{viewMode === 'raw' {viewMode === 'raw'
? (resolvedContent != null ? `Raw · ${resolvedContent.length} chars` : '—') ? (rawDisplayContent ? `Raw · ${rawDisplayContent.length} chars` : '—')
: renderedContent : renderedContent
? `${configType.label} · ${renderedContent.length} chars` ? `${configType.label} · ${renderedContent.length} chars`
: error : error