Skip to content

Latest commit

 

History

History
284 lines (214 loc) · 12.3 KB

File metadata and controls

284 lines (214 loc) · 12.3 KB

Markdown to Slack Blocks

npm version npm downloads CI TypeScript Minzipped size Node.js License: MIT

A powerful library to convert Markdown text into Slack's Block Kit JSON format (and back).

Turn Markdown — including content generated by LLMs — into ready-to-post Slack Block Kit messages, with full support for rich text, code blocks, tables, mentions, and more. The conversion is bidirectional: render Slack blocks back to Markdown or plain text whenever you need a fallback.

Motivation

While Slack does offer native markdown support in blocks, there are significant limitations. The following markdown features are not supported by Slack's native markdown:

  • Code blocks with syntax highlighting — Slack renders code blocks but ignores language hints
  • Horizontal rules — No native support for --- or *** dividers
  • Tables — Markdown tables are not rendered
  • Task lists — Checkbox-style lists (- [ ], - [x]) are not recognized

This library is particularly useful for apps that leverage platform AI features where you expect a markdown response from an LLM. Instead of sending raw markdown that Slack can't fully render, this library converts it to proper Block Kit JSON that displays correctly.

Additional Features

Beyond basic markdown conversion, this library provides:

  • Mention Support: User, channel, user group, and team mentions (@username, #channel) are automatically detected and converted to Slack's native mention format when ID mappings are provided. Without mappings, mentions are rendered as plain text in some cases.
  • Native Slack Dates: Support for Slack's date formatting syntax, allowing dynamic date rendering that respects user timezones.
  • Color Detection: Optional color detection that converts color values (hex, rgb, named colors) into Slack's native color elements for rich visual formatting.

How It Works

This library uses a two-step conversion process:

  1. Markdown → AST: The markdown string is parsed into an Abstract Syntax Tree (AST) using mdast-util-from-markdown, with GitHub Flavored Markdown (GFM) support via mdast-util-gfm.

  2. AST → Slack Blocks: The AST is traversed and converted into Slack's Block Kit JSON format, mapping markdown elements to their corresponding Slack block types and rich text elements.

Key Libraries

Supported Output

Blocks

Block Type Description
rich_text Primary block type for formatted text content
header H1 headings rendered as header blocks
divider Horizontal rules converted to divider blocks
image Standalone images
section Text sections with optional accessories
context Smaller context text and images
data_table Slack's native data table (default for Markdown tables); cell types are chosen automatically per cell
table Legacy table block (opt-in via tableBlockType: "table"); every cell is a rich_text block

Rich Text Elements

Element Type Description
rich_text_section Container for inline text elements
rich_text_list Ordered and unordered lists (supports nesting)
rich_text_preformatted Code blocks
rich_text_quote Blockquotes

Rich Text Section Elements

Element Type Description
text Plain text with optional styling (bold, italic, strike, code)
link Hyperlinks
emoji Emoji shortcodes (:emoji_name:)
user User mentions (@username)
channel Channel mentions (#channel)
usergroup User group mentions
team Team mentions
broadcast Broadcast mentions (@here, @channel, @everyone)
date Formatted date objects
color Color values (when detectColors is enabled)

Text Styles

Style Markdown Syntax
Bold **text** or __text__
Italic *text* or _text_
Strikethrough ~~text~~
Code `text`

Features

  • Standard Markdown Support: Converts headings, lists, bold, italic, code blocks, blockquotes, and links.
  • Tables: GFM tables become Slack's native data_table block, with each cell typed dynamically (raw_text, raw_number, or rich_text).
  • Bidirectional: Convert Markdown → Block Kit and Block Kit → Markdown/plain text for fallbacks.
  • Slack-Specific Extensions: Support for user mentions, channel mentions, user groups, and team mentions.
  • LLM-Ready: Designed to clean up Markdown returned by LLMs into valid Block Kit JSON.
  • Configurable: Options to customize behavior, such as color detection and table block type.
  • Type-Safe: Written in TypeScript with full type definitions.

Installation

npm install markdown-to-slack-blocks

Usage

import { markdownToBlocks } from 'markdown-to-slack-blocks';

const markdown = `
# Hello World
This is a **bold** statement.
`;

const blocks = markdownToBlocks(markdown);
console.log(JSON.stringify(blocks, null, 2));

Options

You can pass an options object to markdownToBlocks, otherwise the mentions will be rendered as text in some blocks (e.g. tables):

const options = {
    mentions: {
        users: { 'username': 'U123456' },
        channels: { 'general': 'C123456' },
        userGroups: { 'engineers': 'S123456' },
        teams: { 'myteam': 'T123456' }
    },
    detectColors: true,
    preferSectionBlocks: true // default: true
};

const blocks = markdownToBlocks(markdown, options);

Validation

The library validates that the IDs provided in the mentions option adhere to Slack's ID format:

  • User IDs: Must start with U or W.
  • Channel IDs: Must start with C.
  • User Group IDs: Must start with S.
  • Team IDs: Must start with T.

All IDs must be alphanumeric.

Tables

Markdown (GFM) tables are converted to Slack's native data_table block by default. The cell type for every cell is chosen dynamically, based on its content, so the output is as faithful and lightweight as possible:

Cell content Emitted cell type
Plain, unstyled text raw_text
A pure number (e.g. 10, -3.5) raw_number (keeps value for sorting/formatting)
Styled text, links, mentions, emoji, … rich_text
const markdown = `
| Name  | Amount | Stage         |
| ----- | ------ | ------------- |
| Alpha | 10     | **Negotiation** |
| Bravo | 2.5    | Waiting on *review* |
`;

const blocks = markdownToBlocks(markdown);
{
  "type": "data_table",
  "caption": "Data table",
  "rows": [
    [
      { "type": "raw_text", "text": "Name" },
      { "type": "raw_text", "text": "Amount" },
      { "type": "raw_text", "text": "Stage" }
    ],
    [
      { "type": "raw_text", "text": "Alpha" },
      { "type": "raw_number", "value": 10, "text": "10" },
      {
        "type": "rich_text",
        "elements": [
          {
            "type": "rich_text_section",
            "elements": [{ "type": "text", "text": "Negotiation", "style": { "bold": true } }]
          }
        ]
      }
    ]
  ]
}

Options:

markdownToBlocks(markdown, {
  tableBlockType: "data_table", // default; use "table" for the legacy rich_text-cell block
  tableCaption: "Data table",   // default caption; pass "" to omit it
});

blocksToMarkdown and blocksToPlainText render both data_table and legacy table blocks back to Markdown tables, so tables round-trip cleanly.

Handling Large Messages

Slack limits messages to ~45 blocks and ~12KB of JSON. Use splitBlocks to split large outputs into several messages, or splitBlocksWithText if you also need a plain-text fallback for postMessage:

import { markdownToBlocks, splitBlocks, splitBlocksWithText } from 'markdown-to-slack-blocks';

const blocks = markdownToBlocks(veryLongMarkdown);

// Blocks-only batches
const batches = splitBlocks(blocks);
for (const batch of batches) {
    await slack.postMessage({ channel, blocks: batch });
}

// Batches with text fallback
const batchesWithText = splitBlocksWithText(blocks);
for (const batch of batchesWithText) {
    await slack.postMessage({ channel, text: batch.text, blocks: batch.blocks });
}

Options:

splitBlocks(blocks, { maxBlocks: 40, maxCharacters: 12000 });
splitBlocksWithText(blocks, { maxBlocks: 40, maxCharacters: 12000 });

Splitting happens at natural boundaries: between blocks first, then within rich_text elements, and finally within large code blocks by line. splitBlocksWithText additionally generates a concise plaintext summary per batch (headers, sections, rich text, tables, etc.) suitable for Slack's text field.

Plain text rendering

If you already have a block array and need a lightweight plaintext fallback (for example to populate the text field in chat.postMessage), use blocksToPlainText:

import { blocksToPlainText } from 'markdown-to-slack-blocks';

const text = blocksToPlainText(blocks);
// -> "Hello world" or similar, depending on your blocks

The function walks the rendered blocks and returns a joined string that keeps list markers, quotes, tables (as cell | cell rows), mentions, dates (using the provided fallback or ISO string), and basic formatting markers where possible. The output is best-effort and is intended for concise fallbacks rather than full fidelity rendering.

Markdown rendering

If you need a best-effort Markdown representation of an existing block array, use blocksToMarkdown:

import { blocksToMarkdown } from 'markdown-to-slack-blocks';

const markdown = blocksToMarkdown(blocks);
// -> "# Title\n\nParagraph with **bold** text" or similar

If you want Slack IDs rendered back to visible mention names (which might be useful for LLMs), pass the inverse mention map:

const markdown = blocksToMarkdown(blocks, {
    mentions: {
        users: { 'U123456': 'username' },
        channels: { 'C123456': 'general' },
        userGroups: { 'S123456': 'engineers' },
        teams: { 'T123456': 'myteam' }
    }
});

With these options, mentions like <@U123456> or <#C123456> are rendered as @username and #general where possible. The same ID format validation applies to these reverse mention maps.

The renderer converts rich_text, section mrkdwn, code blocks, quotes, lists, tables, images, and Slack entities back into normalized Markdown. Because Slack blocks do not preserve every detail of the original source, the output is canonicalized rather than byte-for-byte identical to the input, but it round-trips cleanly for this library's generated blocks.