> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fased.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Markdown Formatting

# Markdown formatting

Fased formats outbound Markdown by converting it into a shared intermediate
representation (IR) before rendering channel-specific output. The IR stores the
rendered plain text plus style/link spans, so chunking and rendering can stay
consistent across channels.

## Goals

* **Consistency:** one parse step, multiple renderers.
* **Safe chunking:** split text before rendering so inline formatting never
  breaks across chunks.
* **Channel fit:** map the same IR to Slack mrkdwn, Telegram HTML, and Signal
  style ranges without re-parsing Markdown.

## Pipeline

1. **Parse Markdown to IR**
   * IR is plain text plus style spans (bold/italic/strike/code/spoiler) and link spans.
   * Offsets are UTF-16 code units so Signal style ranges align with its API.
   * Tables are parsed only when `tableMode` is enabled for that channel/account path.
2. **Chunk IR (format-first)**
   * Chunking happens on the IR text before rendering.
   * Style and link spans are sliced per chunk so rendered markers/ranges remain valid.
3. **Render per channel**
   * **Slack:** mrkdwn tokens (bold/italic/strike/code), links as `<url|label>`.
   * **Telegram:** HTML tags (`<b>`, `<i>`, `<s>`, `<code>`, `<pre><code>`, `<a href>`, `<tg-spoiler>`).
   * **Signal:** plain text + `text-style` ranges; links become `label (url)` when label differs.

## IR example

Input Markdown:

```markdown theme={"theme":{"light":"min-light","dark":"min-dark"}}
Hello **world** - see [docs](https://docs.fased.ai).
```

IR (schematic):

```json theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  "text": "Hello world - see docs.",
  "styles": [{ "start": 6, "end": 11, "style": "bold" }],
  "links": [{ "start": 19, "end": 23, "href": "https://docs.fased.ai" }]
}
```

## Where it is used

* Slack, Telegram, and Signal render full outbound Markdown from the IR.
* Other channels use plain text or their own formatting rules, with Markdown
  table conversion applied before chunking when enabled.
* LINE has its own Markdown-to-Line renderer for table/card conversion.

## Table handling

Markdown tables are not consistently supported across chat clients. Use
`markdown.tables` to control conversion per channel (and per account).

* `code`: render tables as code blocks (default for most channels).
* `bullets`: convert each row into bullet points (default for Signal + WhatsApp).
* `off`: disable table parsing and conversion; raw table text passes through.

Config keys:

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  channels: {
    discord: {
      markdown: {
        tables: "code",
      },
      accounts: {
        work: {
          markdown: {
            tables: "off",
          },
        },
      },
    },
  },
}
```

## Chunking rules

* Chunk limits come from channel adapters/config and are applied to the IR text.
* Fenced code keeps a `code_block` style. If a long code block must be split,
  each chunk receives the relevant slice of the code style.
* List prefixes and blockquote prefixes are part of the IR text, so chunking
  does not split mid-prefix.
* Inline styles and links are sliced across chunks; renderers reopen markers or
  emit style ranges for the chunk-local span.

If you need more on chunking behavior across channels, see
[Streaming + chunking](/concepts/streaming).

## Link policy

* **Slack:** `[label](url)` -> `<url|label>`; bare URLs remain bare. Autolink
  is disabled during parse to avoid double-linking.
* **Telegram:** `[label](url)` -> `<a href="url">label</a>` (HTML parse mode).
* **Signal:** `[label](url)` -> `label (url)` unless label matches the URL.

## Spoilers

Spoiler markers (`||spoiler||`) are parsed for Signal and Telegram:

* **Signal:** `SPOILER` style ranges.
* **Telegram:** `<tg-spoiler>` HTML tags.
* Other IR renderers treat spoiler spans as plain text unless they define a
  spoiler marker.

## How to add or update a channel formatter

1. **Parse once:** use the shared `markdownToIR(...)` helper with channel-appropriate
   options (`linkify`, `autolink`, heading style, blockquote prefix, spoilers,
   and table mode).
2. **Render:** implement a renderer with `renderMarkdownWithMarkers(...)` and a
   style marker map (or Signal style ranges).
3. **Chunk:** call `chunkMarkdownIR(...)` before rendering; render each chunk.
4. **Wire adapter:** update the channel outbound adapter to use the new chunker
   and renderer.
5. **Test:** add or update format tests and an outbound delivery test if the
   channel uses chunking.

## Common gotchas

* Slack angle-bracket tokens (`<@U123>`, `<#C123>`, `<https://...>`) must be
  preserved; escape raw HTML safely.
* Telegram HTML requires escaping text outside tags to avoid broken markup.
* Signal style ranges depend on UTF-16 offsets; do not use code point offsets.
* Preserve trailing newlines for fenced code blocks so closing markers land on
  their own line.
