Skip to content

Render markdown in Vue chat TextContent#730

Open
shogun444 wants to merge 3 commits into
thesysdev:mainfrom
shogun444:fix/vue-chat-textcontent-markdown-728
Open

Render markdown in Vue chat TextContent#730
shogun444 wants to merge 3 commits into
thesysdev:mainfrom
shogun444:fix/vue-chat-textcontent-markdown-728

Conversation

@shogun444

@shogun444 shogun444 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #728

What

The Vue chat example claims that TextContent supports markdown, but the component rendered text using plain Vue interpolation, causing markdown syntax (e.g. **NVDA**) to appear literally instead of being formatted.

This PR updates TextContent to render markdown as the prompt already describes.

Changes

  • Parse TextContent using marked
  • Sanitize the generated HTML with DOMPurify
  • Render the sanitized HTML using v-html
  • Add marked and dompurify as direct dependencies for the Vue chat example

Why

The issue proposed two possible fixes:

  • Render markdown correctly
  • Remove markdown support from the prompt

This PR chooses the first approach so the implementation matches the documented behavior instead of reducing functionality.

The change is intentionally scoped to examples/vue-chat only. No changes were made to @openuidev/vue-lang since the runtime is functioning correctly; the issue was limited to the example's rendering implementation.

Test Plan

  • Plain text rendering is unchanged
  • HTML is sanitized before rendering (DOMPurify)

Checklist

  • I linked a related issue
  • I considered backwards compatibility
  • No documentation changes required

shogun444 added 2 commits July 7, 2026 16:50
TextContent's prompt/description claims markdown support, but the
component rendered text with plain interpolation, showing literal
markdown syntax (e.g. **NVDA**) instead of formatted output. Parse
text with marked and sanitize with DOMPurify before binding via
v-html.

Scoped to examples/vue-chat only; @openuidev/vue-lang runtime is
unchanged.

Fixes thesysdev#728
Adds the lockfile entries for the marked/dompurify dependencies
added to examples/vue-chat/package.json in the previous commit, so
pnpm install --frozen-lockfile (CI) matches the package.json specs.
Both packages were already fully resolved elsewhere in the lockfile
as transitive deps, so this only adds the importer specifiers for
examples/vue-chat.
@AlexQuidditch

Copy link
Copy Markdown

I would be happy to see this feature merged asap 🙏

@vishxrad vishxrad left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing #728. I am requesting changes for two issues before merge: model-produced text should stay within a Markdown-only rendering surface, and the component should consistently support either inline Markdown or correctly structured and styled block Markdown. The dependency and lockfile changes look good, and both CI jobs pass.


const html = computed(() => {
const raw = marked.parse(props.text ?? "", { async: false });
return DOMPurify.sanitize(raw);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Please disable or escape raw HTML before sanitizing. marked.parse() preserves embedded HTML, while the default DOMPurify policy still permits forms, inputs, external form actions and images, and fixed-position inline styles; a model response can therefore render UI outside the component library controls. HTML-looking plain text such as <script> also loses content instead of being displayed. Prefer treating Marked HTML tokens as text, then keep DOMPurify with a Markdown-specific tag and attribute allowlist.


<template>
<p class="text-sm leading-relaxed text-zinc-700 dark:text-zinc-300">{{ props.text ?? "" }}</p>
<p class="text-sm leading-relaxed text-zinc-700 dark:text-zinc-300" v-html="html"></p>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] marked.parse() emits block elements; even plain text becomes

...

, so injecting the result into an outer

creates invalid nested or block markup. Tailwind preflight also removes list markers, heading and link styling, and default spacing, leaving most block Markdown visually unformatted. If this component only needs inline formatting, use marked.parseInline(); if it should support full Markdown, use a

host and add descendant Markdown styles.

@AlexQuidditch

Copy link
Copy Markdown

@shogun444 maybe https://github.com/Simon-He95/markstream-vue would be more suitable for this case

- Escape raw HTML tokens in marked renderer (html override) to prevent
  model-produced UI outside component controls
- Add strict DOMPurify allowlist as defense-in-depth
- Change host from <p> to <div> to fix invalid nested block markup
- Add scoped CSS for markdown descendants (lists, headings, code blocks,
  blockquotes, tables, links) to counter Tailwind preflight resets
@shogun444

shogun444 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@AlexQuidditch Thanks for the suggestion! markstream-vue looks great for streaming use cases. For now I've addressed the @vishxrad feedback with a minimal fix (HTML escaping, proper block structure, scoped markdown styles). If there's interest in a fuller streaming markdown solution for the Vue chat example, that could be a follow-up.

Sorry for the late PR.

@AlexQuidditch

Copy link
Copy Markdown

@shogun444

I've implemented markstream-vue into my fork with vue-headless package. It's working great.

Screenshot 2026-07-15 at 13 12 46

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vue chat TextContent claims markdown support but renders markdown literally

3 participants