Skip to content

feat: add literalTagContent prop for plain-text custom tag children#423

Open
sleitor wants to merge 2 commits intovercel:mainfrom
sleitor:feat/literal-tag-content
Open

feat: add literalTagContent prop for plain-text custom tag children#423
sleitor wants to merge 2 commits intovercel:mainfrom
sleitor:feat/literal-tag-content

Conversation

@sleitor
Copy link
Contributor

@sleitor sleitor commented Feb 25, 2026

Summary

Adds an opt-in literalTagContent prop that renders children of specified custom tags as plain text (no markdown parsing). This is useful for mention/entity tags in AI UIs where child content is a data label (e.g. a username or handle) rather than prose.

Motivation

When using allowedTags with custom components, markdown inside tag children is still parsed. For example:

<mention user_id="123">@_some_username_</mention>

renders with emphasis on _some_username_, which is unintended for entity tags.

Requiring model-side escaping (\_) everywhere is fragile and adds prompt/runtime complexity.

Solution

A new literalTagContent?: string[] prop. For tags in that list, markdown metacharacters inside the tag content are escaped before the markdown parser runs, so the parser treats the children as plain text.

<Streamdown
  allowedTags={{ mention: ['user_id'] }}
  literalTagContent={['mention']}
>
  {`<mention user_id="123">@_some_username_</mention>`}
</Streamdown>

Output: renders @_some_username_ as literal text — no emphasis.

Why preprocessing, not a rehype plugin

The earlier approach (a rehype plugin that flattens children to text post-parse) loses the original characters — by the time rehype runs, _some_username_ has already been parsed into <em>some_username</em> so the underscores are gone. Escaping before parsing preserves the original content.

Changes

  • New packages/streamdown/lib/preprocess-literal-tag-content.ts — escapes markdown metacharacters inside matching tag content before parsing
  • Modified packages/streamdown/index.tsx — adds literalTagContent?: string[] to StreamdownProps and wires the preprocessor into processedChildren
  • Modified packages/streamdown/__tests__/allowed-tags.test.tsx — 5 new tests for the literalTagContent prop
  • New packages/streamdown/__tests__/preprocess-literal-tag-content.test.ts — 10 unit tests for the preprocessor

All 807 existing tests continue to pass.

Closes #419

@vercel
Copy link
Contributor

vercel bot commented Feb 25, 2026

Someone is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

@sleitor sleitor force-pushed the feat/literal-tag-content branch from 7ea3dab to ad9dcaf Compare February 25, 2026 22:21
Copy link
Contributor

@vercel vercel bot left a comment

Choose a reason for hiding this comment

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

Additional Suggestion:

When a tag appears in both allowedTags and literalTagContent and its content contains blank lines, the <!----> HTML comment inserted by preprocessCustomTags gets corrupted to <\!\-\-\-\-> by the subsequent preprocessLiteralTagContent call.

Fix on Vercel

…revent HTML comment corruption

When a tag appears in both allowedTags and literalTagContent and its
content contains blank lines, the '<!---->' HTML comment inserted by
preprocessCustomTags was subsequently corrupted to '<\!\-\-\-\->' by
preprocessLiteralTagContent (which escapes '!' and '-' as markdown
metacharacters).

Fix: swap the execution order so preprocessLiteralTagContent runs first,
then preprocessCustomTags inserts its markers. The HTML comments are
never seen by the markdown escaper.
@sleitor
Copy link
Contributor Author

sleitor commented Feb 26, 2026

Thanks for the automated review! The HTML comment corruption issue is fixed in the latest commit (465de11).

Root cause: preprocessCustomTags ran before preprocessLiteralTagContent. When a tag appeared in both lists and its content contained blank lines, the <!----> markers inserted by preprocessCustomTags were subsequently escaped to <\!\-\-\-\-> by preprocessLiteralTagContent (which treats ! and - as markdown metacharacters).

Fix: Swapped the execution order — preprocessLiteralTagContent now runs first, so the markdown escaping happens before the blank-line markers are inserted. The <!----> comments are never seen by the escaper.

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.

Feature request: opt-in literal text mode for custom tag children (allowedTags)

1 participant