Skip to content

feat(markdown): shared Markdown renderer - #2664

Open
camielvs wants to merge 2 commits into
masterfrom
banners-01-markdown
Open

feat(markdown): shared Markdown renderer#2664
camielvs wants to merge 2 commits into
masterfrom
banners-01-markdown

Conversation

@camielvs

@camielvs camielvs commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Why

AiChat had built a full markdown renderer for itself. The notices feature later in this stack needs to render markdown too — and from a source outside the app, so it needs stricter handling. Rather than keep a second copy, this lifts AiChat's renderer into a shared component.

What you get

Two entry points:

  • Markdown — for text the app authored. The same styling AiChat had, now in one place.
  • UntrustedMarkdown — for text the app did not author. Raw HTML never renders, links are narrowed to absolute http(s) URLs (so nothing can point into our own routes or run javascript:), and images degrade to their alt text instead of fetching a remote file.

AiChat keeps only its own overrides: entity and component chips, and syntax-highlighted fenced code blocks.

Links render through the Link primitive rather than a raw anchor, so an internal href stays in the same tab and only absolute http(s) gets target="_blank" and the external icon. That needed one fix in the primitive: it wrapped its children in a div, which is invalid inside a <p> and which React flagged on every render — AiChat already renders Link inside markdown today. A span fixes it with no visual change.

Reviewer notes

This is the one PR in the stack that changes something you can already see. AiChat's markdown picks up the shared styling, which differs slightly from what it had:

element before after
h1 medium large, slightly tighter top margin
h2 small bold medium semibold
h3 small semibold marginally more space below
h4 small semibold small semibold, dimmed
h5 h6 unstyled small, dimmed
bullet / number markers default colour dimmed
task lists (- [ ]) rendered with a bullet bullet removed, checkbox spaced
table cells vertically centred top-aligned
images unstyled rounded, capped at container width

These are the shared component's choices; worth a glance to confirm they're an improvement and not a surprise. An earlier revision of this PR also dropped last:border-b-0 from table rows — that was an accident in the extraction and is restored.

Tests: both entry points, with the untrusted path's escapes (script tags, javascript: links, relative URLs, remote images) asserted directly, plus unit tests for the URL guard.

Where this sits

PR
1 #2664 shared Markdown renderer (you are here)
2 #2666 validated host contract
3 #2667 notice banners on the dashboard home — announcement parity
4 #2668 notices button in the header
5 #2681 hide a banner without retiring it

This one stands alone: it is useful as a de-duplication whether or not anything above it lands.

camielvs commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown

🎩 Preview

A preview build has been created at: banners-01-markdown/adef9bb

Comment thread src/components/shared/Markdown/Markdown.tsx
Comment thread src/components/shared/Markdown/Markdown.tsx Outdated
Comment thread src/components/shared/Markdown/Markdown.tsx Outdated
Comment thread src/components/shared/Markdown/Markdown.tsx Outdated
Comment thread src/utils/URL.ts
Comment thread src/routes/v2/shared/components/AiChat/components/renderMarkdown.tsx Outdated
@camielvs
camielvs force-pushed the banners-01-markdown branch from 47a18de to b525bf9 Compare August 27, 2026 01:18
@camielvs
camielvs marked this pull request as ready for review August 28, 2026 00:02
@camielvs
camielvs requested a review from a team as a code owner August 28, 2026 00:02
camielvs and others added 2 commits September 3, 2026 11:48
Extracts the markdown rendering AiChat had built for itself into a shared
`Markdown` component, and adds an `UntrustedMarkdown` variant for text the app
did not author. AiChat now renders through the shared base, keeping only its
own overrides (entity/component chips, fenced code blocks).
- Fenced blocks no longer inherit the inline-code pill; `pre` owns the
  block surround for labelled and unlabelled fences alike.
- Links render through `Link` rather than a raw anchor. That needed one
  fix in the primitive: it wrapped its children in a `div`, which is not
  allowed inside a `<p>` and which React flagged on every render — AiChat
  already renders `Link` inside markdown today. A `span` fixes it with no
  visual change, since the layout comes from the flex classes.
- Internal links stay in the same tab and gain no external affordance;
  only absolute http(s) hrefs get `target="_blank"` and the icon.
- Restore `last:border-b-0` on table rows.
- `UntrustedMarkdown` locks `img` after the caller spread so the guard
  cannot be overridden.
- `chatComponents` regains its `satisfies Components` check.
- Add `toAbsoluteHttpUrl` unit tests covering the protocol filtering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@camielvs
camielvs force-pushed the banners-01-markdown branch from b525bf9 to adef9bb Compare September 3, 2026 18:50

@morgan-wowk morgan-wowk left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Automated review

Approving. This is an exemplary trust boundary. Verified against the code and tests:

  • Neither entry point loads rehype-raw, so raw HTML never renders — the "does not render raw HTML" test confirms a <script> body doesn't execute and <b> doesn't render.
  • UntrustedMarkdown locks urlTransform to toAbsoluteHttpUrl, so javascript:/data:/relative hrefs collapse to "" and render as plain text (not a dead anchor); img is forced to alt-text-only and can't be re-enabled by a caller (tested).
  • URL.ts's toAbsoluteHttpUrl rejects non-string host input, non-http(s) protocols, and protocol-relative/relative/anchor URLs — all directly unit-tested.
  • The link.tsx divspan fix is correct (a div is invalid inside <p>), and the new dir is added to the react-compiler allowlist alongside its peers.

Clean.

@Mbeaulne Mbeaulne left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the shared renderer and its trusted/untrusted URL handling. No additional findings.

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.

3 participants