From 08d66b59c2079e75c3e105597505fafe08dc6c2a Mon Sep 17 00:00:00 2001 From: dtoro Date: Wed, 11 Mar 2026 00:05:32 +0100 Subject: [PATCH] fix: nunjuclk rendering --- .../src/components/nodes/RenderingNode.tsx | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/nodes/RenderingNode.tsx b/frontend/src/components/nodes/RenderingNode.tsx index a701d94..287dec4 100644 --- a/frontend/src/components/nodes/RenderingNode.tsx +++ b/frontend/src/components/nodes/RenderingNode.tsx @@ -480,12 +480,14 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) { 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) try { 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 setRenderedContent(htmlOrSvg) setError(null) @@ -539,6 +541,18 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) { // Kroki and other SVG sources may prepend so we detect by presence of tag const isSvgOutput = Boolean(renderedContent?.trim() && /]/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). */ const displayContent = useMemo(() => { if (!renderedContent || !isSvgOutput) return renderedContent @@ -650,16 +664,34 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) { v && (v === 'preview' || v === 'raw') && setViewMode(v)} + onValueChange={(v) => { + if (v === 'preview' || v === 'raw') setViewMode(v) + }} aria-label="View mode" variant="outline" 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" > - + { + e.stopPropagation() + setViewMode('preview') + }} + > Preview - + { + e.stopPropagation() + setViewMode('raw') + }} + > Raw @@ -800,18 +832,18 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) { size="icon" className="absolute top-2 right-2 z-10 h-7 w-7 shrink-0 rounded-md shadow-sm" onClick={() => { - const text = resolvedContent ?? '' + const text = rawDisplayContent if (text) { navigator.clipboard?.writeText(text).then(() => toast.success('Copied to clipboard')).catch(() => {}) } }} - disabled={!resolvedContent} + disabled={!rawDisplayContent} title="Copy raw output" > {viewMode === 'raw' - ? (resolvedContent != null ? `Raw · ${resolvedContent.length} chars` : '—') + ? (rawDisplayContent ? `Raw · ${rawDisplayContent.length} chars` : '—') : renderedContent ? `${configType.label} · ${renderedContent.length} chars` : error