Passer au contenu principal

MarkdownRenderer

Implemented

XSS-safe Markdown-subset → HTML renderer (h1-h3, bold/italic, inline+block code, ul/ol, blockquote, safe links; no images/tables). Raw HTML inside content is escaped FIRST. Allowlist for links: http(s), mailto, /, #, ./, ../ (javascript: stripped). html mode renders pre-sanitized HTML verbatim.

Categorymolecules
Sourcepatterns/molecules/markdownrenderer.html
AI HintRender stored markdown (descriptions, terms) safely. For WYSIWYG output use the html mode with sanitize-html at the trust boundary (RichTextEditor produces raw HTML). Do not claim MDX support — it is a markdown subset only.

XSS-safe Markdown → HTML renderer with an explicit trusted-HTML mode. Raw HTML inside markdown is escaped FIRST (allowlist by construction: only tags the generator emits can exist in the output); link protocols are allowlisted (javascript: degrades to plain text). The html prop renders caller-sanitized HTML verbatim.

MarkdownRenderer

molecules

Markdown mode (default). The container carries the scoped .ep-mdr-content token styles — headings/lists/blockquote/code follow the DS type scale in both light and dark themes (the old `prose prose-sm` classes were ghosts). The empty state renders role="status" aria-live="polite".

Markdown mode (escaped)

Headline act

Paragraph with bold, italic and inline code.

  • Doors at 8:00 PM
  • All ages welcome
  1. First support act
  2. Late-night set
Doors open ninety minutes before showtime.

const ticket = await checkout(cartId);

Safe link and a raw <script>alert(1)</script> that renders as inert text.

Trusted-HTML mode (caller-sanitized)

Persisted rich blob

Sanitized upstream with DOMPurify and rendered verbatim.

Empty state (custom label, FR locale available)

No description available for this event yet.

This component supports light and dark mode.

Usage

content is required. Markdown mode escapes raw HTML before parsing — user-supplied markdown can never inject tags. For persisted rich HTML (e.g. RichTextEditor output), sanitize first and pass html: the sanitized blob renders verbatim and wins over content.

import { MarkdownRenderer } from '@epasslive/design-system'

// Markdown mode — raw HTML in user content is escaped (XSS-safe)
<MarkdownRenderer content={"# Headline act\n\n- Doors at 8:00 PM"} />

// Trusted-HTML mode — caller-sanitized blob rendered verbatim
// (sanitize first, e.g. storefront/src/lib/sanitize-html.ts)
<MarkdownRenderer content="" html={sanitizeHtml(richBlob)} />

// Empty state — label defaults to the i18n catalog, override or set locale
<MarkdownRenderer content="" locale="fr" />
<MarkdownRenderer content="" emptyLabel="No description available." />

Security

  • Escape-first: every inline fragment is HTML-escaped before any transform runs, so the output can only contain tags the generator itself emits (h1–h3, p, ul/ol/li, blockquote, pre/code, strong, em, a).
  • Link allowlist: only http(s):, mailto:, /, #, ./, ../ become anchors; anything else (e.g. javascript:) degrades to plain text. Every generated link carries rel="noopener noreferrer".
  • html prop trust boundary: the value is rendered verbatim via dangerouslySetInnerHTML — the caller MUST sanitize. The canonical sanitizer is storefront/src/lib/sanitize-html.ts (DOMPurify allowlist). RichTextEditor is the input side of this contract; MarkdownRenderer is the read side.

Props

Prop
Type
Default
Description
content*
string
—

Markdown source. Raw HTML inside is escaped first (XSS-safe). Subset: h1–h3, bold, italic, inline + fenced code, ul/ol, blockquote, safe links — images and tables are NOT supported (literal passthrough).

html
string
—

Pre-sanitized HTML rendered verbatim via dangerouslySetInnerHTML. TRUST BOUNDARY: the caller MUST sanitize (e.g. DOMPurify — see storefront/src/lib/sanitize-html.ts). html wins over content when both are non-empty.

emptyLabel
string
i18n catalog: markdownRenderer.empty

Shown when content is empty. Defaults to the DS i18n catalog value for the active locale ("Nothing to show yet" / "Rien à afficher pour le moment"); an explicit value always wins.

locale
"en" | "fr"
"en"

Locale used to resolve the built-in empty-state label from i18n/{en,fr}.json (markdownRenderer.empty).

className
string
—

Additional CSS classes on the container div (both states).

id
string
—

Stable id for the container (defaults to an auto-generated markdown-* id from React.useId).

Props

Preview

MarkdownRenderer

Prop
Type
Required
Default
Description
content
string
Required
—

Markdown source — raw HTML inside is escaped (XSS-safe).

emptyLabel
string
Optional
i18n catalog (`markdownRenderer.empty`)

Shown when content is empty — defaults to the DS i18n catalog value for the active locale ('Nothing to show yet' / 'Rien à afficher pour le moment').

html
string
Optional
—

Pre-sanitized HTML rendered verbatim via dangerouslySetInnerHTML, bypassing markdown parsing (§126 Wave 3 #4). TRUST BOUNDARY: caller must sanitize; html wins over content when both non-empty.

locale
'en' | 'fr'
Optional
'en'

Resolves the built-in empty-state label from the DS i18n catalog; an explicit emptyLabel always wins.

className
string
Optional
—

Additional CSS classes on the container.

id
string
Optional
—

Stable container id.

Quick use:<MarkdownRenderer />

Accessibility

ARIA

1 rule

WCAG 4.1.3

empty state renders role='status' + aria-live so SR users hear 'nothing to show'; generated links force rel='noopener noreferrer'

Type

1 rule

NASA Rule 5

SAFE_HREF allowlist — only http(s), mailto, /, #, ./ and ../ hrefs survive; javascript: URLs degrade to plain text

Usage

Import

tsxSnippet
import { MarkdownRenderer } from "@epasslive/design-system"

Example 1

tsxSnippet
<MarkdownRenderer content={"## Doors open\n- 19:00 doors\n- 20:00 show"} />

Example 2

tsxSnippet
<MarkdownRenderer content="" html={sanitizeHtml(description)} emptyLabel="Nothing to show yet." />

Anti-patterns

— Do not pass unsanitized HTML into html — sanitize at the trust boundary first

— Do not wrap it in hand-rolled prose utilities — scoped token styling is built in

Design System v4.1.0 | ePassLive