Skip to main content

Markdown formatting

crocbot formats outbound Markdown by converting it into a shared intermediate representation (IR) before rendering channel-specific output. The IR keeps the source text intact while carrying 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 Telegram HTML without re-parsing Markdown.

Pipeline

  1. Parse Markdown -> IR
    • IR is plain text plus style spans (bold/italic/strike/code/spoiler) and link spans.
    • Offsets are UTF-16 code units.
    • Tables are parsed only when a channel opts into table conversion.
  2. Chunk IR (format-first)
    • Chunking happens on the IR text before rendering.
    • Inline formatting does not split across chunks; spans are sliced per chunk.
  3. Render per channel
    • Telegram: HTML tags (<b>, <i>, <s>, <code>, <pre><code>, <a href>).

IR example

Input Markdown:
Hello **world** — see [docs](https://aiwithapex.mintlify.app).
IR (schematic):
{
  "text": "Hello world — see docs.",
  "styles": [
    { "start": 6, "end": 11, "style": "bold" }
  ],
  "links": [
    { "start": 19, "end": 23, "href": "https://aiwithapex.mintlify.app" }
  ]
}

Where it is used

  • Telegram outbound adapters render from the IR.

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.
  • off: disable table parsing and conversion; raw table text passes through.
Config keys:
channels:
  telegram:
    markdown:
      tables: code
    accounts:
      work:
        markdown:
          tables: off

Chunking rules

  • Chunk limits come from channel adapters/config and are applied to the IR text.
  • Code fences are preserved as a single block with a trailing newline so channels render them correctly.
  • List prefixes and blockquote prefixes are part of the IR text, so chunking does not split mid-prefix.
  • Inline styles (bold/italic/strike/inline-code/spoiler) are never split across chunks; the renderer reopens styles inside each chunk.
If you need more on chunking behavior across channels, see Streaming + chunking.
  • Telegram: [label](url) -> <a href="url">label</a> (HTML parse mode).

How to add or update a channel formatter

  1. Parse once: use the shared markdownToIR(...) helper with channel-appropriate options (autolink, heading style, blockquote prefix).
  2. Render: implement a renderer with renderMarkdownWithMarkers(...) and a style marker map.
  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

  • Telegram HTML requires escaping text outside tags to avoid broken markup.
  • Preserve trailing newlines for fenced code blocks so closing markers land on their own line.