Compare commits

...

2 Commits

Author SHA1 Message Date
08d66b59c2 fix: nunjuclk rendering 2026-03-11 00:05:32 +01:00
6014f34f76 fix: aspec ratio of rendering 2026-03-10 23:59:04 +01:00

View File

@@ -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,35 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
// 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()))
/** 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
let html = renderedContent
// Force preserve aspect ratio so the diagram is not stretched (Kroki often returns preserveAspectRatio="none")
html = html.replace(/\bpreserveAspectRatio\s*=\s*["']none["']/gi, 'preserveAspectRatio="xMidYMid meet"')
// Make root SVG fill container so it scales uniformly with meet
html = html.replace(/\bwidth\s*=\s*["'][^"']*["']/i, 'width="100%"')
html = html.replace(/\bheight\s*=\s*["'][^"']*["']/i, 'height="100%"')
// Override inline style width/height so they don't override the attributes
html = html.replace(/\bstyle\s*=\s*["']([^"']*)["']/i, (_, style) => {
const overridden = style.replace(/\b(width|height):[^;]+/gi, '$1:100%')
return `style="${overridden}"`
})
return html
}, [renderedContent, isSvgOutput])
const downloadSvg = useCallback(() => {
if (!renderedContent || !isSvgOutput) return
const blob = new Blob([renderedContent], { type: 'image/svg+xml' })
@@ -633,16 +664,34 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
<ToggleGroup
type="single"
value={viewMode}
onValueChange={(v) => 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"
>
<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
</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
</ToggleGroupItem>
</ToggleGroup>
@@ -783,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"
>
<Copy className="size-3.5" />
</Button>
<CodeMirror
value={resolvedContent ?? ''}
value={rawDisplayContent}
height={`${rawEditorHeight}px`}
theme={theme}
extensions={rawExtensions}
@@ -872,14 +921,14 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
<RotateCcw className="size-3 max-w-[12px] max-h-[12px]" />
</button>
</div>
<div className="absolute inset-0 nodrag nopan overflow-hidden">
<div className="absolute inset-0 nodrag nopan overflow-hidden flex flex-col">
<TransformComponent
wrapperClass="!w-full !h-full"
contentClass="inline-flex nodrag nopan"
contentClass="inline-flex flex-col nodrag nopan !w-full !min-h-0 !flex-1"
>
<div
className="rendering-diagram inline-flex p-4 nodrag nopan"
dangerouslySetInnerHTML={{ __html: renderedContent }}
className="rendering-diagram inline-flex w-full min-h-0 flex-1 p-4 nodrag nopan"
dangerouslySetInnerHTML={{ __html: displayContent ?? '' }}
/>
</TransformComponent>
</div>
@@ -900,7 +949,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
<BaseNodeFooter>
<NodeFooterEdgeIndicators nodeId={id} nodeType="render">
{viewMode === 'raw'
? (resolvedContent != null ? `Raw · ${resolvedContent.length} chars` : '—')
? (rawDisplayContent ? `Raw · ${rawDisplayContent.length} chars` : '—')
: renderedContent
? `${configType.label} · ${renderedContent.length} chars`
: error