Skip to content

Commit 6b31596

Browse files
snomiaoclaude
andauthored
[feat] Support Markdown rendering for node descriptions in NodePreview (#4684)
Co-authored-by: Claude <[email protected]>
1 parent c6a9f43 commit 6b31596

File tree

2 files changed

+173
-7
lines changed

2 files changed

+173
-7
lines changed

src/components/node/NodePreview.spec.ts

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { mount } from '@vue/test-utils'
22
import { createPinia } from 'pinia'
33
import PrimeVue from 'primevue/config'
4-
import { beforeAll, describe, expect, it } from 'vitest'
4+
import { beforeAll, describe, expect, it, vi } from 'vitest'
55
import { createApp } from 'vue'
66
import { createI18n } from 'vue-i18n'
77

88
import type { ComfyNodeDef as ComfyNodeDefV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
9+
import * as markdownRendererUtil from '@/utils/markdownRendererUtil'
910

1011
import NodePreview from './NodePreview.vue'
1112

@@ -129,4 +130,164 @@ describe('NodePreview', () => {
129130

130131
expect(headdot.classes()).toContain('pr-3')
131132
})
133+
134+
describe('Description Rendering', () => {
135+
it('renders plain text description as HTML', () => {
136+
const plainTextNodeDef: ComfyNodeDefV2 = {
137+
...mockNodeDef,
138+
description: 'This is a plain text description'
139+
}
140+
141+
const wrapper = mountComponent(plainTextNodeDef)
142+
const description = wrapper.find('._sb_description')
143+
144+
expect(description.exists()).toBe(true)
145+
expect(description.html()).toContain('This is a plain text description')
146+
})
147+
148+
it('renders markdown description with formatting', () => {
149+
const markdownNodeDef: ComfyNodeDefV2 = {
150+
...mockNodeDef,
151+
description: '**Bold text** and *italic text* with `code`'
152+
}
153+
154+
const wrapper = mountComponent(markdownNodeDef)
155+
const description = wrapper.find('._sb_description')
156+
157+
expect(description.exists()).toBe(true)
158+
expect(description.html()).toContain('<strong>Bold text</strong>')
159+
expect(description.html()).toContain('<em>italic text</em>')
160+
expect(description.html()).toContain('<code>code</code>')
161+
})
162+
163+
it('does not render description element when description is empty', () => {
164+
const noDescriptionNodeDef: ComfyNodeDefV2 = {
165+
...mockNodeDef,
166+
description: ''
167+
}
168+
169+
const wrapper = mountComponent(noDescriptionNodeDef)
170+
const description = wrapper.find('._sb_description')
171+
172+
expect(description.exists()).toBe(false)
173+
})
174+
175+
it('does not render description element when description is undefined', () => {
176+
const { description, ...nodeDefWithoutDescription } = mockNodeDef
177+
const wrapper = mountComponent(
178+
nodeDefWithoutDescription as ComfyNodeDefV2
179+
)
180+
const descriptionElement = wrapper.find('._sb_description')
181+
182+
expect(descriptionElement.exists()).toBe(false)
183+
})
184+
185+
it('calls renderMarkdownToHtml utility function', () => {
186+
const spy = vi.spyOn(markdownRendererUtil, 'renderMarkdownToHtml')
187+
const testDescription = 'Test **markdown** description'
188+
189+
const nodeDefWithDescription: ComfyNodeDefV2 = {
190+
...mockNodeDef,
191+
description: testDescription
192+
}
193+
194+
mountComponent(nodeDefWithDescription)
195+
196+
expect(spy).toHaveBeenCalledWith(testDescription)
197+
spy.mockRestore()
198+
})
199+
200+
it('handles potentially unsafe markdown content safely', () => {
201+
const unsafeNodeDef: ComfyNodeDefV2 = {
202+
...mockNodeDef,
203+
description:
204+
'Safe **markdown** content <script>alert("xss")</script> with `code` blocks'
205+
}
206+
207+
const wrapper = mountComponent(unsafeNodeDef)
208+
const description = wrapper.find('._sb_description')
209+
210+
// The description should still exist because there's safe content
211+
if (description.exists()) {
212+
// Should not contain script tags (sanitized by DOMPurify)
213+
expect(description.html()).not.toContain('<script>')
214+
expect(description.html()).not.toContain('alert("xss")')
215+
// Should contain the safe markdown content rendered as HTML
216+
expect(description.html()).toContain('<strong>markdown</strong>')
217+
expect(description.html()).toContain('<code>code</code>')
218+
} else {
219+
// If DOMPurify removes everything, that's also acceptable for security
220+
expect(description.exists()).toBe(false)
221+
}
222+
})
223+
224+
it('handles markdown with line breaks', () => {
225+
const multilineNodeDef: ComfyNodeDefV2 = {
226+
...mockNodeDef,
227+
description: 'Line 1\n\nLine 3 after empty line'
228+
}
229+
230+
const wrapper = mountComponent(multilineNodeDef)
231+
const description = wrapper.find('._sb_description')
232+
233+
expect(description.exists()).toBe(true)
234+
// Should contain paragraph tags for proper line break handling
235+
expect(description.html()).toContain('<p>')
236+
})
237+
238+
it('handles markdown lists', () => {
239+
const listNodeDef: ComfyNodeDefV2 = {
240+
...mockNodeDef,
241+
description: '- Item 1\n- Item 2\n- Item 3'
242+
}
243+
244+
const wrapper = mountComponent(listNodeDef)
245+
const description = wrapper.find('._sb_description')
246+
247+
expect(description.exists()).toBe(true)
248+
expect(description.html()).toContain('<ul>')
249+
expect(description.html()).toContain('<li>')
250+
})
251+
252+
it('applies correct styling classes to description', () => {
253+
const wrapper = mountComponent()
254+
const description = wrapper.find('._sb_description')
255+
256+
expect(description.classes()).toContain('_sb_description')
257+
})
258+
259+
it('uses v-html directive for rendered content', () => {
260+
const htmlNodeDef: ComfyNodeDefV2 = {
261+
...mockNodeDef,
262+
description: 'Content with **bold** text'
263+
}
264+
265+
const wrapper = mountComponent(htmlNodeDef)
266+
const description = wrapper.find('._sb_description')
267+
268+
// The component should render the HTML, not escape it
269+
expect(description.html()).toContain('<strong>bold</strong>')
270+
expect(description.html()).not.toContain('&lt;strong&gt;')
271+
})
272+
273+
it('prevents XSS attacks by sanitizing dangerous HTML elements', () => {
274+
const maliciousNodeDef: ComfyNodeDefV2 = {
275+
...mockNodeDef,
276+
description:
277+
'Normal text <img src="x" onerror="alert(\'XSS\')" /> and **bold** text'
278+
}
279+
280+
const wrapper = mountComponent(maliciousNodeDef)
281+
const description = wrapper.find('._sb_description')
282+
283+
if (description.exists()) {
284+
// Should not contain dangerous event handlers
285+
expect(description.html()).not.toContain('onerror')
286+
expect(description.html()).not.toContain('alert(')
287+
// Should still contain safe markdown content
288+
expect(description.html()).toContain('<strong>bold</strong>')
289+
// May or may not contain img tag depending on DOMPurify config
290+
}
291+
})
292+
})
132293
})

src/components/node/NodePreview.vue

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,14 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
7070
</div>
7171
</div>
7272
<div
73-
v-if="nodeDef.description"
73+
v-if="renderedDescription"
7474
class="_sb_description"
7575
:style="{
7676
color: litegraphColors.WIDGET_SECONDARY_TEXT_COLOR,
7777
backgroundColor: litegraphColors.WIDGET_BGCOLOR
7878
}"
79-
>
80-
{{ nodeDef.description }}
81-
</div>
79+
v-html="renderedDescription"
80+
/>
8281
</div>
8382
</template>
8483

@@ -89,8 +88,9 @@ import { computed } from 'vue'
8988
import type { ComfyNodeDef as ComfyNodeDefV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
9089
import { useWidgetStore } from '@/stores/widgetStore'
9190
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
91+
import { renderMarkdownToHtml } from '@/utils/markdownRendererUtil'
9292
93-
const props = defineProps<{
93+
const { nodeDef } = defineProps<{
9494
nodeDef: ComfyNodeDefV2
9595
}>()
9696
@@ -101,7 +101,12 @@ const litegraphColors = computed(
101101
102102
const widgetStore = useWidgetStore()
103103
104-
const nodeDef = props.nodeDef
104+
const { description } = nodeDef
105+
const renderedDescription = computed(() => {
106+
if (!description) return ''
107+
return renderMarkdownToHtml(description)
108+
})
109+
105110
const allInputDefs = Object.values(nodeDef.inputs)
106111
const allOutputDefs = nodeDef.outputs
107112
const slotInputDefs = allInputDefs.filter(

0 commit comments

Comments
 (0)