# Markdown — rendering markdown text

> `Markdown(text)` renders a markdown string as formatted content — headings, lists, tables, code, links. It is a read-only renderer, not an editor: use it for a description field, a help panel, release notes, a chat message, or any stored text an author wrote in markdown. Markup inside the document is shown as text, never treated as markup, so a document is always content and never a way into the page.

<!-- id: ui-markdown · area: ui · stability: stable · html: https://osysharp.com/reference/ui/markdown/ -->

## Summary        {#summary}
Markdown is how people write text that has shape — a heading, a list, a link, a table. `Markdown(text)` takes such a
string and renders it:

```osy syntax
Markdown(article.Body)
```

It is a **renderer, not an editor**. It has no toolbar, no cursor and no storage: you give it a string and it draws
the document that string describes. That makes it the right thing for the many places that want formatted text and
nothing else — a product description, a help panel, a policy page, release notes, an agent's reply.

The rendered document inherits your app's [theme tokens](https://osysharp.com/reference/ui/theming/) tokens, so its headings use your heading face, its links
take your primary colour, and it follows your app through light and dark without being told to.

## Signature      {#signature}
```osy syntax
Markdown(<string expression>)                       // render a settled document

Markdown(text, streaming: <bool>)                   // the document is still arriving
Markdown(text, copyIcon: <icon>, copyTooltip: "…")  // give every code block a copy button
```

The first argument is an ordinary expression — a property, a member, a literal, the result of a call. There is
nothing to declare and nothing to register. The two options are described below and both default to off.

## Description    {#description}

### It renders whatever the string says   {#syntax}

Everything below is understood. Anything else is shown as the text the author typed.

| | |
|---|---|
| **Headings** | `# One` … `###### Six`, and the underlined form. A trailing `{#custom-id}` becomes the heading's id, so a `#link` can land on it |
| **Text** | `*emphasis*`, `**strong**`, `~~struck~~`, `` `code` ``, and a line ending in two spaces for a hard break |
| **Lists** | bullets and numbers, nested, tight or spaced; `- [ ]` / `- [x]` render a checkbox |
| **Blocks** | fenced code with its language, indented code, block quotes, horizontal rules |
| **Tables** | the pipe form, including per-column alignment |
| **Links** | `[text](url)`, bare `<https://…>`, `<name@example.com>`, and images `![alt](url)` |
| **Alerts** | `> [!NOTE]` and `TIP`, `IMPORTANT`, `WARNING`, `CAUTION` — the GitHub callout boxes |

#### Alerts — a callout box    {#alerts}

A block quote whose first line is a marker on its own becomes a coloured callout:

```md
> [!WARNING]
> This deletes the record and cannot be undone.
```

The marker is case-insensitive, and the title is supplied for you — the five kinds are the whole vocabulary, and
`NOTE`, `TIP`, `IMPORTANT`, `WARNING` and `CAUTION` are exactly the ones GitHub renders, so a document written for
one reads the same in the other.

**A marker that is not one of the five is not an alert.** `> [!WARNIGN]` stays an ordinary quote with its marker
visible, which is what every reader that has never heard of alerts already does. That is deliberate: a typo turning
into a confident box titled *"Warnign"* would look intentional, and a wrong-looking callout is worse than a plain
quote.

It renders as a box with its own role rather than a tinted quote, because a quote means *someone else said this* and
an alert means *the author is raising their voice* — and a screen reader announces the two differently. Each kind
takes its colour from one token (`Colors { Markdown { AlertWarning } }`), which sets the bar, the title and the
tint together.

⚠ The markdown **editor** does not yet style these — it round-trips them safely and shows the marker as text, so a
document is never damaged by being edited there; it just does not draw the box.

Three things are deliberately NOT rendered. **Raw HTML is shown as text** — see below, it is the security rule.
**Footnotes** (`[^1]`) are not yet formatted and appear as written; the markdown *editor* does handle them, so a
document may contain one. **Math** (`$x^2$`, `$$…$$`) is the same: the editor renders formulae, this renderer shows
their source. That difference is deliberate rather than pending — drawing maths needs a typesetting library, and this
renderer is part of the platform's client, which carries no third-party code at all. An editor is an app's own
component and can choose to ship one.

#### A rule under the title    {#heading-rule}

Long documents read better when the title area closes off. Set one token and the document's opening heading gets a
hairline underneath:

```osy title="one token puts a hairline under the document's title" syntax
theme Default {
  Colors { Markdown { HeadingRule = "#E5E3DC"; } }
  Space  { Markdown { HeadingRuleGap = "0.3em"; } }   // optional breathing room; omit for a flush line
}
```

**The LEADING `h1` only** — the document's title. A second `h1` further down takes no line, and neither does an
`h2`: this closes the title area, it is not heading decoration. A document that does not open with an `h1` gets no
rule at all, which is correct — it has no title area. Use `HeadingRuleGap` to make the line *close* the title area
rather than underline the words.

**It is off unless you ask for it, and off costs nothing.** With no `HeadingRule` declared the headings are exactly
what they were: no line, and — the part that matters — *no space reserved for one*. A document you are already
rendering does not move by a pixel because this feature exists.

⚠ Do not confuse this with a `---` in the text. That is a **thematic break**, part of the document, and it renders
as a full-width rule wherever the author put one (`Colors { Markdown { Rule } }`). The heading rule is your theme's
opinion about headings; the thematic break is the author's content. A tool like Craft draws both, which is why they
can look like the same feature.

### A document is content, never markup   {#no-html}

This is the rule worth knowing, because it is what makes the atom safe to point at data:

> Anything in the document that looks like HTML reaches the page as **text**. `<b>` renders as the four characters
> `<b>`, not as bold. A `<script>` is four-and-a-bit characters of visible text, not a script.

Text in a database is written by people, and on this platform sometimes by agents. If a stored description could
smuggle a `<script>` into the page that shows it, then every page that shows a description would be a hazard. So it
cannot: there is no setting that turns raw HTML on.

It also happens to be what authors mean. A sentence containing `List<string>` renders as a sentence containing
`List<string>`, rather than losing the word to a tag nobody wrote.

Links are held to the same rule: a URL that is not an ordinary web link is rendered inert rather than followed, so
`[click](javascript:…)` shows the word "click" and goes nowhere.

### Styling it   {#styling}

**Placing it.** The atom takes the same [style props](https://osysharp.com/reference/ui/styling/) props as anything else — most often a reading measure:

```osy title="placing the atom — usually just a reading measure" syntax
Markdown(article.Body, maxW: 672, mx: "auto")
```

**Its look, in three tiers.** You will usually only need the first.

1. **Do nothing.** The document reads in your app's voice already: it takes `Fonts { Heading }` for its headings,
   `Colors { Primary }` for its links, `Colors { Muted }` behind code, `Colors { Border }` for rules and table
   borders, and your radii.
2. **Move a semantic token** and the document moves with everything else — change `Colors { Primary }` and its links
   follow, because that is where they came from.
3. **Give the document its own value** by nesting a `Markdown` group inside the token's category. A nested group
   namespaces *within* that category, so it never disturbs the token it overrides for everyone else:

```osy title="giving a DOCUMENT its own tokens without disturbing the app's" syntax
theme Docs {
  Colors {
    Primary = "#0F766E";                  // the app's links, buttons, focus rings
    Markdown { Link = "#B45309"; }        // …but a DOCUMENT's links, only
  }
  FontSize   { Markdown { H1 = "2.4rem"; H2 = "1.7rem"; H3 = "1.3rem"; } }
  FontWeight { Markdown { HeadingWeight = 700; } }
  Space      { Markdown { Block = "1.25em"; HeadingTop = "2em"; ListIndent = "1.8em"; } }
  Length     { Markdown { LineHeight = "1.75"; } }
}
```

The full set, each falling back to the app token in brackets and then to a built-in default:

| Category | Keys |
|---|---|
| `Colors { Markdown { … } }` | `Link` *(Primary)* · `CodeBg` *(Muted)* · `TableHeadBg` *(Muted)* · `TableBorder` *(Border)* · `Rule` *(Border)* · `QuoteBar` *(Border)* · `QuoteText` *(TextMuted)* · `AlertNote` · `AlertTip` · `AlertImportant` · `AlertWarning` · `AlertCaution` · `HeadingRule` |
| `FontSize { Markdown { … } }` | `H1` · `H2` · `H3` · `H4` · `CodeSize` |
| `FontWeight { Markdown { … } }` | `HeadingWeight` · `TableHeadWeight` · `AlertTitle` |
| `Fonts { Markdown { … } }` | `HeadingFace` *(Fonts.Heading)* · `CodeFace` *(Fonts.Mono)* |
| `Space { Markdown { … } }` | `Block` · `HeadingTop` · `HeadingBottom` · `ListIndent` · `ItemGap` · `CodePad` · `CellPad` · `QuotePad` · `RuleGap` · `AlertPad` · `AlertTitleGap` · `HeadingRuleGap` |
| `Length { Markdown { … } }` | `LineHeight` · `HeadingLineHeight` · `QuoteBarW` · `AlertBarW` · `HeadingRuleW` |
| `Radius { Markdown { … } }` | `CodeRadius` *(Radius.Sm)* · `BlockRadius` *(Radius.Md)* |

> **Token names are global.** A theme's leaf names must each denote one value — a bare `Heading` in a variant has to
> mean something definite — so a nested group namespaces the *variable*, not the *name*. That is why these read
> `HeadingFace` and `HeadingWeight` rather than `Heading` twice: an app almost certainly already has
> `Fonts { Heading }`, and a collision is a compile error naming both groups.

### Rendering a reply while it is still streaming in   {#streaming}
An agent's reply arrives a few characters at a time, and a document being typed is briefly not valid markdown: the
`**` of a bold run has no closing pair yet, a `|` is not yet a table. Rendering that literally shows the reader the
raw syntax for a frame or two, which looks like a glitch.

Say the text is still arriving and the renderer holds the unfinished tail — an in-progress construct is drawn as
though it were already closed, and a caret marks the end:

```osy title="a reply as it arrives" test app=ui-markdown-streaming
component Reply(string Body, bool Done) {
  render {
    Markdown(Body, streaming: !Done);
  }
}
```

Two things are worth knowing:

- **`streaming: false` is the half that matters.** The renderer can tell that text was appended, so it can stabilise
  a half-typed construct by itself. What it cannot tell is that the stream has *ended* — so turning the flag off is
  what settles the document, re-rendering the tail as ordinary markdown. A document left permanently `streaming: true`
  keeps its caret forever.
- It does not make rendering faster. Rendering is already incremental: editing one paragraph of a long document
  repaints that paragraph, whether or not anything is streaming.

### A copy button on every code block   {#code-actions}
A document full of commands or snippets wants a copy button on each one. Your app cannot reach inside the rendered
document to add one, so ask for it here — and supply the icon and the words yourself, because they belong to your
design and your language:

```osy title="copyable code blocks" test app=markdown-demo
component Guide(string Body) {
  render {
    Markdown(Body, copyIcon: copy, copyTooltip: "Copy code");
  }
}
```

`copyIcon` is one of your app's own [icons](https://osysharp.com/reference/ui/icons/) — a bare name, checked at compile time like any other icon, so a glyph you have not declared is a compile error rather than an empty square on the page. The
tooltip is also the button's accessible name, which is why the two are required **together**: an icon with no
tooltip is a button nobody using a screen reader can identify, and a tooltip with no icon describes a control that
never appears.

It is **opt-in on purpose**. A markdown atom is otherwise inert output — no buttons, no state — and that is a large
part of why it is safe to point at text an agent or a customer wrote. A help panel should not sprout controls
because it happened to contain a fenced block.

The button appears on hover, and on keyboard focus so it is reachable without a mouse. On touch, where there is no
hover, it is always visible. To copy text that is **not** inside a rendered document, use [Clipboard](https://osysharp.com/reference/ui/clipboard/).

### Where the text comes from   {#source}

Anywhere. A `Markdown` property on an entity is the common case — a document-backed member whose text is stored as
sections — but a plain `string` works exactly as well, and so does a value your code just computed.

## Examples       {#examples}

A product description rendered under its name:

```osy title="a description field" test app=ui-markdown
entity Article {
  [Required] [MaxLength(200)] string Title;
  string Body;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

[Page("/article/{id}")]
[Render(SSR)]
component ArticlePage(Guid id) {
  var article = Article.Where(a => a.Id == id).FirstOrDefault();

  render {
    Stack(gap: 3, maxW: 720, mx: "auto") {
      Text(article.Title, fontSize: 32);
      Markdown(article.Body);
    }
  }
}
```

Text the page itself holds — a help panel that opens and closes, with no storage behind it at all:

```osy title="a help panel" test app=ui-markdown-help
[Page("/help")]
[Render(CSR)]
component HelpPage() {
  bool open = false;
  string help = "## Getting started\n\n1. Create a project\n2. Add a page\n3. Compile\n\nSee the **guide** for more.";

  action Toggle() { open = !open; }

  render {
    Stack(gap: 2) {
      Button(open ? "Hide help" : "Show help", onPress: Toggle);
      if (open) { Markdown(help, maxW: 640); }
    }
  }
}
```

## See also       {#see-also}
- [Clipboard](https://osysharp.com/reference/ui/clipboard/) — `Clipboard.Copy(text)`, for copying anything outside a rendered document
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the tokens a rendered document reads for its typefaces and colours.
- [style props](https://osysharp.com/reference/ui/styling/) — the style props the atom takes, like any other element.
- [entity members](https://osysharp.com/reference/entity/properties/) — declaring the property the text lives in.
- [component](https://osysharp.com/reference/ui/component/) — the component the atom is written inside.
