Skip to main content
xriptxr

Markdown

Info:Content
35

Renders markdown as themed HTML, as a document or as an inline label, with an optional source view.

md gfm rich text prose render commonmark editor

Live demo

live · @xtyle/astro

Markdown

A document

Headings ride the type scale, rules and quotes ride the border ramp, and fences borrow the same --code-* family the Code component owns.

Derivation

An algorithm maps a few anchors and knobs into a full, internally consistent token set. The algorithm is the durable asset; a theme is a materialized invocation of one. See the derivation model for the long version.

What it covers

  1. the anchors you pin
  2. the knobs you turn
  3. everything else, derived

A theme is a print. The algorithm is the press.

Inline --accent reads the same as a fence:

const theme = derive({ bg: "#0f1115", accent: "#5b8cff" });

Rendered with no sanitizer at all.

Tables and task lists

GFM is on by default. A wide table scrolls inside its own box rather than pushing the document sideways.

component state notes
tooltip fixed tracks its trigger through scroll
swatch fixed same promotion, same defects
markdown new this one
  • escape raw HTML to text
  • allowlist link and image URLs
  • name the version

Autolinks too: https://xtyle.dev

An inline label

inline renders emphasis with no blocks and no paragraph wrapper, so it inherits whatever type surrounds it. Note the last one: a leading # stays literal text instead of erupting a heading into the row.

Fix tooltip placement Refactor AnchorTracker in xtyle keep the swatch fix Review PR #12 # Heading in a label

The same label inside running text: a task titled Fix tooltip in AnchorTracker inherits this sentence's size, weight, and color rather than imposing its own.

Untrusted input

Success:escaped by default

Raw HTML is escaped to text and refused URLs lose their href — so nothing here is live. The markup below is inert text, and the first link is a real <a> with no destination.

This came from an LLM and is rendered as-is:

<script>alert("this never runs")</script>

<img src=x onerror=alert("nor this")>

a link that looks fine and one that is fine.

Markdown you wrote yourself

Warning:allow-html

allow-html renders the source's markup instead of escaping it, for a document whose origin the app controls. The fragment format still has the last word: scripts, event handlers, and elements xtyle never declared are refused either way. The body carries data-allow-html — inspect it.

This one is the app's own copy, so its markup renders:

Ctrl+K opens the palette, and a highlight survives as a highlight.

Even a disclosure

…with markdown inside it.

Components inside a document

vocabulary

What survives allow-html is standard HTML and xtyle's own components, because that is what the fragment format declares. Release notes and changelogs can carry a real badge or alert without leaving markdown.

And a document can carry components, themed like every other one:

Shipped v0.10.0 with 3 fixes riding along

Rendered from markdown, not from Astro.

What xtyle never declared is still refused — a <script>, an onclick=, or someone else's <my-widget> does not survive the trip.

An editable source view

editable adds a toggle between the render and its markdown. The switch is a real node in the fragment fill, so a mod can reword it, make it an icon, or move it.

Edit me

Switch to source with the toggle, change something, and switch back.

  • the textarea is the source of truth while editing
  • input fires with the markdown as you type
Edit

Markdown renders GitHub-Flavored Markdown into HTML that themes entirely from the token register: headings ride the type scale, rules and quotes ride the border and surface ramps, and fenced code borrows the same --code-* family the Code component owns, so a fence inside a document and an <xtyle-code> beside it agree in any theme. GFM is on — tables, task lists, strikethrough, and autolinks all render.

inline switches to a label render: emphasis, code, links and strikethrough, but no blocks and no paragraph wrapper, so it drops into a tab title or a chip and inherits its type — a generated label that opens with # stays text instead of erupting a heading into a tab strip. editable adds a source view the reader can switch to, emitting input as it is typed. It ships no sanitizer, by design. Raw HTML in the source is escaped to text rather than rendered, and link and image URLs are written from a scheme allowlist, so everything that reaches the DOM is markup the renderer generated itself from a closed token set — the arbitrary-HTML problem never arises rather than being solved. allowHtml lifts the escaping for a source the app controls, and it is not the hole it sounds like: the body reaches the DOM through the component's fragment, so the format declared in component-host.json still has the last word and refuses <script>, event handlers, and elements outside xtyle's vocabulary no matter what the renderer emits. What survives is standard HTML and xtyle's own components, so a document can carry an <xtyle-badge> or an <xtyle-alert> inline. URL schemes are separate and app-wide rather than per-element: allowUriSchemes() from @xtyle/core/elements widens the renderer and the fragment format together, which is the only way to widen either — and xtyle adds no scheme on an app's behalf, because a hole nobody asked for is the worst kind.

When to use

How this component composes with the rest of the set.

Reach for inline whenever the markdown is a label rather than a document — a tab title, a chip, a table cell, a menu item. It inherits the type around it instead of imposing its own.
Untrusted markdown needs no extra handling: raw HTML is escaped and URLs are allowlisted, so an LLM-authored or user-authored string is safe to pass straight in. That is the default, and it is the right one for anything you did not write.
An image or link that renders blank is usually a scheme rather than a bug: xtyle allows http, https, mailto, tel and data, and nothing else until an app says so. A host protocol — asset: or tauri: in a desktop shell — needs allowUriSchemes("asset", "tauri") from @xtyle/core/elements, called once at startup before anything paints. It is app-wide rather than per-element on purpose: the renderer's allowlist and the fragment format's have to move together, and one call moves both.
allowHtml is what lets a document carry components. Markdown with <xtyle-badge tone="success">shipped</xtyle-badge> in it renders a real badge, themed like every other one — useful for release notes, changelogs, and anything generated where the markup is yours. Elements xtyle doesn't declare are still dropped, so it composes with your own components only if they're in the vocabulary.
Pair editable with the input event to keep your own state in sync; the event's detail.source carries the markdown as it's typed.
A fenced block inside a document borrows the same --code-* tokens as <xtyle-code>, so the two agree without any configuration.

Props

6 props, straight from the manifest.

PropTypeDefaultBindingsDescription
source string
html svelte astro
The markdown to render. Optional for html/svelte, where the element's text content is used instead.
inline boolean false
html svelte astro
Render as a label rather than a document: emphasis, code, links and strikethrough, with no blocks and no paragraph wrapper. It flows with the surrounding text and inherits its type, so it fits a tab title, a chip, or a table cell. Block syntax stays literal instead of erupting.
editable boolean false
html svelte astro
Offer a source view the reader can switch to, via a toggle the fragment fill draws.
editing boolean false
html svelte astro
Whether the source view is showing. Only meaningful alongside `editable`; setting it alone would strand the reader in a box with no way out, so it is ignored.
allowHtml boolean false
html svelte astro
Render the source's HTML instead of escaping it to text, for markdown whose origin the app controls — bundled release notes, a document your own app wrote, a file the user opened. It is floored rather than open: the body is painted through the component's fragment, so the format declared in `component-host.json` refuses `<script>`, `<iframe>`, event handlers, and any element outside xtyle's vocabulary regardless of what the renderer emits. What it admits is standard HTML **and xtyle's own components**, so `<xtyle-badge tone="danger">` inside a paragraph renders as a real badge. The rendered body carries `data-allow-html`, so the choice is visible where the markup landed.
processBbcode boolean | string false
html svelte astro
Also process BBCode, so one document can carry both languages. `true` reaches the whole BBCode registry; a string names a vocabulary, which is how one surface accepts a tag another refuses. The two renderers never read each other's output: BBCode's balanced constructs are lifted out before markdown parses and dropped back in afterward, so markdown never sees a `[quote]` and a tag inside a code fence stays literal. Prose *inside* a construct still gets markdown's inline render, so `[quote]some **bold** text[/quote]` gets the quote from one language and the emphasis from the other. Unlike `allowHtml` this widens what renders without widening what is reachable — BBCode has no raw-HTML passthrough, and every tag it emits came from a closed registry.

Appearance

States

editing

.xtyle-markdown__toggle[aria-pressed='true']

The source view is showing: the body is swapped for the textarea and the toggle reads as pressed.

inert-link

.xtyle-markdown__body a:not([href])

A link whose URL the renderer refused — a javascript: href, say. It keeps its text but loses its href, and reads dimmed and unclickable rather than lying about being a link.

Anatomy

The named parts that make up the component, with their selectors.

body

.xtyle-markdown__body

Where the rendered markdown lands. Every rule is scoped inside it and styles element types rather than named parts, since the content's structure is the author's, not ours — so nothing here reaches a consumer's own headings. Carries data-allow-html while allowHtml is set, so a body holding the author's own markup is recognizable in a DevTools inspection or a grep of the rendered page.

--fg-0 --font-sans --text-body --leading-normal --space-4

heading

.xtyle-markdown__body h1, .xtyle-markdown__body h2

Document headings, on the type scale; h1 and h2 carry a rule beneath them.

--text-2xl --text-xl --weight-semibold --leading-tight --field-border

inline-html

.xtyle-markdown__body :is(mark, kbd, abbr, details)

The HTML elements allowHtml makes reachable — mark, kbd, abbr, sub/sup, details/summary. Themed from the register rather than left to the browser, which would otherwise paint a mark in fixed yellow with no idea what theme it is in, and style a kbd unlike the Kbd component sitting next to it.

--warn-bg --warn-text --bg-2 --line-2 --border-thick --radius-sm

code

.xtyle-markdown__body code, .xtyle-markdown__body pre

Inline code and fenced blocks, borrowing the Code component's --code-fg / --code-bg rather than deriving a second opinion about what code looks like.

--code-fg --code-bg --font-mono --radius-md --field-border

table

.xtyle-markdown__body table

A GFM table: striped rows, a filled header, and its own horizontal scroll so a wide table never pushes the document sideways.

--bg-1 --bg-2 --field-border --text-sm --weight-semibold

quote

.xtyle-markdown__body blockquote

A blockquote, marked by a leading edge and dimmed a step off the body text.

--fg-1 --field-border --space-4

editor

.xtyle-markdown__editor

The source textarea shown while editing, in the mono face on the field surface.

toggle

.xtyle-markdown__toggle

The edit/view switch. Chrome the component invents, so it is a real node in the fragment fill: a mod can reword it, make it an icon, or move it, and the element keeps working.

Tokens & coverage

What the component consumes, checked live against what the algorithm produces.

Success:fully covered 35/35 consumed tokens produced default register: 310 tokens

Live coverage check against the xtyle-default register (derive(xtyleDefault, { anchors })coverComponent(manifest, register)). Every token this component consumes must be a key the algorithm produces.

--accent --accent-text --bg-1 --bg-2 --border-thick --border-thin --code-bg --code-fg --fg-0 --fg-1 --fg-2 --field-border --font-mono --font-sans --leading-normal --leading-tight --line-2 --radius-md --radius-sm --space-1 --space-2 --space-3 --space-4 --space-5 --space-6 --text-2xl --text-body --text-lg --text-sm --text-xl --text-xs --warn-bg --warn-text --weight-bold --weight-semibold

Slots

default
html svelte

The markdown as text content (html/svelte). For Astro, pass it via the source prop instead.

Accessibility

Renders semantic document structure — real <h1><h6>, <ul>/<ol>, <table> with <th>, <blockquote> — so assistive tech announces the content's shape rather than a wall of text.
inline emits no headings or landmarks, which is what makes it safe to drop into a tab title or a chip: a label never injects document structure into a strip of them.
The edit/view toggle is a real <button> carrying aria-pressed, so its state is announced; the source textarea has an accessible name.
A link whose URL was refused loses its href, so it is skipped by link navigation rather than being announced as a link that does nothing.
A wide table scrolls inside its own box rather than the page, so the document never scrolls horizontally (WCAG 1.4.10).

Code

A document

The block render: headings, emphasis, links and lists, themed from the register.

<xtyle-markdown source="## Release notes

The **tooltip** now tracks its trigger. See [the changelog](/changelog).

- fixed placement under scroll
- `Escape` dismisses a hover-raised hint"></xtyle-markdown>

An inline label

inline renders emphasis without blocks and without a paragraph wrapper, so a generated tab title or chip inherits its surroundings. Block syntax stays literal — a leading # is text, not a heading.

<!-- a label, not a document: emphasis renders, blocks stay literal -->
<xtyle-markdown inline source="Fix **tooltip** in `AnchorTracker`"></xtyle-markdown>

Tables and task lists

GFM is on: tables, task lists, strikethrough and autolinks all render. A wide table scrolls inside its own box.

<xtyle-markdown source="| component | state |
| --- | --- |
| tooltip | fixed |
| swatch | fixed |

- [x] track the anchor
- [ ] name the version"></xtyle-markdown>

Markdown you wrote yourself

allow-html renders the source's markup, so a document can carry an <xtyle-badge> and have it theme like any other. The fragment format still refuses scripts, handlers, and undeclared elements. A host protocol like asset: is a separate, app-wide decision: allowUriSchemes at startup, which widens the renderer and the fragment format together.

<!-- markdown the app wrote, so its markup renders — components included -->
<xtyle-markdown allow-html source="## Shipped

<kbd>Ctrl</kbd>+<kbd>K</kbd> opens the palette. <xtyle-badge tone=&quot;success&quot;>new</xtyle-badge>"></xtyle-markdown>

<!-- a host protocol is app-wide, declared once at startup rather than per element -->
<script type="module">
	import { allowUriSchemes } from "@xtyle/core/elements";

	allowUriSchemes("asset", "tauri");
</script>

An editable source view

editable adds a toggle between the render and its markdown source, emitting input with the source as it's typed.

<xtyle-markdown editable source="# Draft

Switch to the source and edit it."></xtyle-markdown>