# Concepts

> Channels, options, the repair report and the streaming contract — shared by every mdwire API.

HTML: https://minjun.kim/mdwire/docs/

Every front end — the Rust core, the npm package, the Go implementation and the CLI — takes the same
inputs and produces the same output; they are held to the same
[test cases](https://github.com/minjun0219/mdwire/tree/main/corpus/cases). This page covers what they
share. The per-language pages list the exact names.

These docs track the `main` branch. Anything marked (next release) is not in the current release on npm, crates.io
and Go yet; it ships with the next one.

## Channels

A channel is where the output goes. Its name is the same string everywhere.

| Name | Goes to | Limit |
|---|---|---|
| `telegram-html` | Telegram `parse_mode: "HTML"`. Nine tags (`b i u s code pre a blockquote tg-spoiler`), no headings or tables — a table goes out as a monospace block. | 4,096 |
| `slack-markdown` | Slack `markdown_text`. Standard Markdown, which Slack converts itself; mdwire repairs, escapes a `*` meant as a character (`\*`) and splits. | 12,000 |
| `github-markdown` | GitHub comments and PR bodies (GFM). A `~` or `<` meant as a character is escaped, and bold that GFM would not close (before a Korean particle) is written as `<strong>`. | 65,536 |
| `notion-markdown` | Notion page body (Notion-flavored Markdown). Four heading levels; inline HTML Notion cannot draw is stripped, and an autolink becomes `[url](url)`. | 65,536 (not measured) |
| `plain` | Plain text — every marker removed. The fallback. | 12,000 |
| `html` | An HTML fragment for the browser, safe to set as `innerHTML`: text is escaped, inline tags from the source keep no attributes, and only allowed schemes become links. | none |

The limit is a character count on the rendered output. The sender can set its own (see
[options](#options)). For example, `plain` sent to Telegram has to fit 4,096.

## Options

| Option | Default | Meaning |
|---|---|---|
| limit | the channel's limit | Characters per part. Values below 256 are raised to 256 — every part needs room to close and reopen markup. Streaming never splits, and neither does `html`. |
| html · line breaks | `br` | A line break inside a block: `<br>`, or `space` to let the browser fold it (for prose wrapped at 80 columns). |
| html · images | `link` | `![alt](url)` as a link that loads nothing until clicked, or `load` for `<img>` (allowed schemes only). |
| html · schemes | `http`, `https`, `mailto` | Schemes allowed in links and image URLs. Giving a list replaces the default; it does not add to it. |

The three `html` options apply only to the `html` channel.

## The repair report

Rendering also counts what it had to do. The first five are **repairs** — how often the model broke
its own formatting:

| Field | Counts |
|---|---|
| closed emphasis | Emphasis still open at the end of a block, closed (`**impact scope`). |
| closed fence | A code fence still open at the end of the document, closed. |
| reverted code span | A backtick run with no partner, kept as text. |
| dropped marker | A stray `**` dropped (`tail**`, where text comes before it). |
| guessed pair | Emphasis paired by a guess rather than by the rules — an opener CommonMark would not open (`값**(합계)**를`) matched with a mirror-shaped closer on the same line. Math with ASCII on both ends (`x**(y)**z`) is left out. A non-zero count marks an answer worth a look. (next release) |

The other six are **channel rewrites** — what the channel needed changed before posting:

| Field | Counts |
|---|---|
| escaped char | Characters the channel would read as syntax, escaped (GitHub's `\~` `\<` `\*`; Slack's and Notion's `\*`). |
| tag emphasis | Emphasis the channel cannot read as markers, written another way — GitHub's `<strong>`, or an invisible U+2060 inside Slack's markers. |
| stripped HTML | Source HTML removed: tags the channel cannot draw, comments, `<br>` turned into a line break. |
| rewritten bullet | List markers rewritten (`* ` `• ` → `- `, Telegram's `- ` → `• `, `1)` → `1.`). |
| rewritten table | Tables rewritten (separator and cell spacing, or turned into a monospace block). |
| converted marker | Emphasis and links respelled — `_italic_` → `*italic*`, `__bold__` → `**bold**`, `<url\|text>` → `[text](url)`. Markdown channels only. |

## The streaming contract

A streamer takes the output token by token and returns what is safe to send now.

- **What `push` returns is final.** A later chunk never rewrites it, and `finish` only appends the
  tail. So the pieces concatenated equal a one-shot render, whatever the chunk size — unless the
  document is long enough to be split into parts.
- **It holds back only what it must:** a prefix it cannot classify yet, a marker run at the end of
  a chunk, and the inside of emphasis that has not closed. A whole paragraph is never held.
- **Streaming does not split by the limit.** One exception: a Notion table over the limit
  goes out as several tables with the header repeated, and streaming does the same.

Two ways to send while streaming, depending on the channel:

| The channel… | Send | Example |
|---|---|---|
| rewrites the whole message | accumulated output **plus `preview()`** — what is still held, drawn as if the input ended here. After `finish`, skip the last edit when `revised()` is false. | Telegram `editMessageText`, Slack `chat.update`, React |
| only appends | each `push` piece as is. Never `preview()`. | Slack `appendStream` |

`closeOpen()` is the stricter sibling of `preview()`: it only closes block markup that is already out
(`<blockquote>`, `<pre>`), without drawing what is held. Neither changes the streamer's state.
