feat: refactor rendering I

This commit is contained in:
2026-03-12 16:24:24 +01:00
parent 2a0507ca02
commit 79e4586ed1
12 changed files with 1211 additions and 769 deletions

View File

@@ -24,6 +24,20 @@ export type RenderOptions = { width?: number; height?: number }
/** How the renderer should display this type: HTML in a div, or image (SVG/PNG) in a viewport. */
export type ConfigOutputType = 'html' | 'image'
/** Data-only descriptor for output menu items on the Rendering node (e.g. Export SVG/PNG). Provided by config/source types. */
export type OutputMenuAction = 'downloadSvg' | 'downloadPng' | 'copySvg' | 'copyPng'
export type OutputMenuItemDescriptor = { id: string; label: string; action: OutputMenuAction }
export type OutputMenuDescriptor = {
/** Submenu label (e.g. "Export"). Omit for inline items. */
submenuLabel?: string
items: OutputMenuItemDescriptor[]
}
/** Whether this output menu action requires SVG content (disabled when no SVG). */
export function outputMenuActionRequiresSvg(action: OutputMenuAction): boolean {
return action === 'downloadSvg' || action === 'downloadPng' || action === 'copySvg' || action === 'copyPng'
}
export type ConfigType = {
id: ConfigTypeId
label: string
@@ -35,6 +49,8 @@ export type ConfigType = {
insertBlocks: InsertBlockOrGroup[]
/** Render resolved content (after Nunjucks) to HTML/SVG string for the renderer. */
render: (content: string, options?: RenderOptions) => Promise<string>
/** Optional. Output menu items for the Rendering node when this type is shown (e.g. Export submenu for image types). */
outputMenuDescriptor?: OutputMenuDescriptor
}
const KROKI_PLANTUML_SVG = '/api/kroki/plantuml/svg'
@@ -169,6 +185,15 @@ export const CONFIG_TYPES: ConfigType[] = [
throw err
}
},
outputMenuDescriptor: {
submenuLabel: 'Export',
items: [
{ id: 'downloadSvg', label: 'Download SVG', action: 'downloadSvg' },
{ id: 'downloadPng', label: 'Download PNG', action: 'downloadPng' },
{ id: 'copySvg', label: 'Copy SVG', action: 'copySvg' },
{ id: 'copyPng', label: 'Copy image', action: 'copyPng' },
],
},
},
{
id: 'markdown',
@@ -185,6 +210,15 @@ export const CONFIG_TYPES: ConfigType[] = [
language: 'markdown',
insertBlocks: WIREFRAME_INSERT_BLOCKS,
render: renderWireframe,
outputMenuDescriptor: {
submenuLabel: 'Export',
items: [
{ id: 'downloadSvg', label: 'Download SVG', action: 'downloadSvg' },
{ id: 'downloadPng', label: 'Download PNG', action: 'downloadPng' },
{ id: 'copySvg', label: 'Copy SVG', action: 'copySvg' },
{ id: 'copyPng', label: 'Copy image', action: 'copyPng' },
],
},
},
]

View File

@@ -0,0 +1,39 @@
/**
* Shared rendering utilities used by the Rendering node and output views.
* Pure functions only; no React or node-specific logic.
*/
/** Extract <think>...</think> blocks from HTML/markdown; return main (with blocks removed) and think for collapsible. */
export function parseThinkSections(html: string): { main: string; think: string } {
const thinkRegex = /<think>([\s\S]*?)<\/think>/gi
const thinkParts: string[] = []
let match
while ((match = thinkRegex.exec(html)) !== null) thinkParts.push(match[1].trim())
const think = thinkParts.join('\n\n')
const main = html.replace(thinkRegex, '').replace(/\n{3,}/g, '\n\n').trim()
return { main, think }
}
/** Process SVG HTML for viewport display (aspect ratio, fill container). */
export function processSvgDisplay(html: string): string {
let out = html
out = out.replace(/\bpreserveAspectRatio\s*=\s*["']none["']/gi, 'preserveAspectRatio="xMidYMid meet"')
out = out.replace(/\bwidth\s*=\s*["'][^"']*["']/i, 'width="100%"')
out = out.replace(/\bheight\s*=\s*["'][^"']*["']/i, 'height="100%"')
out = out.replace(/\bstyle\s*=\s*["']([^"']*)["']/i, (_, style) => {
const overridden = style.replace(/\b(width|height):[^;]+/gi, '$1:100%')
return `style="${overridden}"`
})
return out
}
/** Strip Nunjucks/template syntax for raw view. */
export function stripTemplateSyntax(text: string): string {
return text
.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()
}

View File

@@ -7,7 +7,9 @@
* Node types that are allowed sources for the Renderer should register here (see nodeRegistry NodeTypeDescriptor).
*/
import type { ConfigTypeId } from './configTypes'
import type { ConfigTypeId, OutputMenuDescriptor } from './configTypes'
export type { OutputMenuDescriptor, OutputMenuItemDescriptor, OutputMenuAction } from './configTypes'
export type SourceRenderingLogicContext = {
nodes: { id: string; type?: string; data?: unknown }[]
@@ -41,10 +43,13 @@ export type ResolvedContentResult = {
* Rendering logic provided by a source node type (e.g. config, agent).
* - defaultUpdateMode: 'auto' = re-render on upstream changes; 'manual' = only on Run
* - getResolvedContent: async resolve step; returns resolved string and which renderer (ConfigTypeId) to use
* - getOutputMenuDescriptor: optional; returns output menu items for the Rendering node (e.g. Export submenu for image types)
*/
export type SourceRenderingLogic = {
defaultUpdateMode: 'auto' | 'manual'
getResolvedContent: (context: SourceRenderingLogicContext) => Promise<ResolvedContentResult>
/** Optional. When the renderer displays this source's output, which extra output menu items to show (e.g. Export SVG/PNG). */
getOutputMenuDescriptor?: (outputTypeId: ConfigTypeId) => OutputMenuDescriptor | null
}
const registry = new Map<string, SourceRenderingLogic>()