mdast-util-from-slack-blocks
v0.1.0
Published
Convert Slack Block Kit rich_text blocks into mdast
Maintainers
Readme
mdast-util-from-slack-blocks
Convert Slack Block Kit rich_text blocks into an mdast Root.
Slack messages can contain structured rich text, while most markdown tooling expects mdast. This package bridges that gap so you can keep using the unified ecosystem.
Install
pnpm add mdast-util-from-slack-blocksIf you want Markdown output, add a serializer too:
pnpm add mdast-util-to-markdown mdast-util-gfmUse
Minimal Slack blocks to Markdown example:
import { fromSlackBlocks, type SlackBlock } from 'mdast-util-from-slack-blocks'
import { gfmToMarkdown } from 'mdast-util-gfm'
import { toMarkdown } from 'mdast-util-to-markdown'
const blocks: SlackBlock[] = [
{
type: 'rich_text',
elements: [
{
type: 'rich_text_section',
elements: [
{ type: 'text', text: 'Ship ' },
{ type: 'text', text: 'today', style: { strike: true } }
]
}
]
}
]
const root = fromSlackBlocks(blocks)
const markdown = toMarkdown(root, { extensions: [gfmToMarkdown()] })
console.log(markdown.trim())
//=> Ship ~~today~~Use resolvers and deterministic date options when you want stable output:
import { fromSlackBlocks, type SlackBlock } from 'mdast-util-from-slack-blocks'
import { gfmToMarkdown } from 'mdast-util-gfm'
import { toMarkdown } from 'mdast-util-to-markdown'
const blocks: SlackBlock[] = [
{
type: 'rich_text',
elements: [
{
type: 'rich_text_section',
elements: [
{ type: 'text', text: 'Hello ' },
{ type: 'user', user_id: 'U123' },
{ type: 'text', text: ', please review ' },
{ type: 'channel', channel_id: 'C456' },
{ type: 'text', text: ' by ' },
{ type: 'date', timestamp: 1_720_710_212, format: '{date_num} ({ago})' },
{ type: 'text', text: '. ' },
{ type: 'text', text: 'Old deadline', style: { strike: true } }
]
}
]
}
]
const root = fromSlackBlocks(blocks, {
resolveUser: (userId) => (userId === 'U123' ? 'alice' : userId),
resolveChannel: (channelId) => (channelId === 'C456' ? 'release-notes' : channelId),
dateLocale: 'en-US',
dateTimeZone: 'UTC',
now: new Date('2024-07-11T15:03:32.000Z')
})
const markdown = toMarkdown(root, { extensions: [gfmToMarkdown()] })
console.log(markdown.trim())
//=> Hello @alice, please review #release-notes by 2024-07-11 (now). ~~Old deadline~~mdast-util-gfm is only needed when you serialize mdast to Markdown and want GFM features such as strikethrough.
API
fromSlackBlocks(blocks, options?)
- Input:
SlackBlock[] - Output: mdast
Root
All resolver and formatter options are synchronous.
| Option | What it affects | Default / fallback |
| ------------------------------- | ----------------------------------------------------------- | ---------------------------------- |
| resolveUser(userId) | Replaces @U123-style user mentions | Uses the raw Slack user ID |
| resolveChannel(channelId) | Replaces #C123-style channel mentions | Uses the raw Slack channel ID |
| resolveUsergroup(usergroupId) | Replaces @S123-style user group mentions | Uses the raw Slack user group ID |
| resolveTeam(teamId) | Replaces @T123-style team mentions | Uses the raw Slack team ID |
| renderDate(element, context) | Overrides built-in Slack date rendering | Uses the built-in renderer |
| dateLocale | Locale for built-in date token rendering | Uses the runtime default locale |
| dateTimeZone | Time zone for built-in date token rendering | Uses the runtime default time zone |
| now | Reference time for built-in relative tokens such as {ago} | Uses new Date() |
| unknownTypeHandling | Behavior for unknown Slack block/element types | 'ignore' |
Built-in date rendering uses format first, then template. If a template is unsupported, it falls back to fallback when present, otherwise to Date#toISOString().
Supported Slack Rich Text
Block-level mapping
| Slack type | mdast output | Notes |
| ------------------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| rich_text_section | One or more paragraph nodes | Double newlines split paragraphs; single newlines become hard breaks |
| rich_text_list | list | style: 'ordered' sets ordered: true; ordered start comes from offset + 1, with offset taking precedence over legacy start |
| rich_text_quote | blockquote | Section-only quotes become paragraph children inside the blockquote |
| rich_text_preformatted | code | Content is flattened to plain text; language maps to code.lang |
Inline element support
Supported inline element types:
textlinkuserchannelusergroupteamemojidatebroadcastcolor
Style and field handling
bold,italic, andstrikemap to mdaststrong,emphasis, anddelete.codemaps to mdastinlineCodeand is exclusive over the other text styles.underline,highlight,client_highlight, andunlinkare preserved innode.data.slack.stylebecause mdast has no native equivalent.date.urlwraps the rendered date text in an mdastlink.coloris preserved as plain text such as#36c5f0.rich_text_list.border,rich_text_preformatted.border,rich_text_quote.border,link.unsafe,emoji.unicode, andemoji.urlare accepted input fields but do not change mdast output.
Important Behavior
- Only Slack blocks with
type: 'rich_text'are processed. - Non-
rich_textblocks are skipped intentionally. - Empty rich text sections and whitespace-only paragraphs are omitted from the mdast tree.
- Preformatted content ignores inline styling and serializes as plain code block text.
- Unknown block or inline types respect
unknownTypeHandling:'ignore','warn', or'throw'.
Compatibility
- Node.js
>=20 - ESM-only package
Development
Tooling uses pnpm.
pnpm run check:all
pnpm run test:all
pnpm run ci:checkRun pnpm run build before publishing and before snapshot render scripts when source mapping logic, exports, or package output changes.
Snapshot Testing
Markdown snapshots live under test/snapshots/<case>/:
input.json: Slack message payload with a top-levelblocksarrayexpected.md: committed, human-reviewed Markdown outputmeta.json: source provenance and upstream example metadata
Useful commands:
pnpm run test:snapshots
pnpm run snapshot:render test/snapshots/<case>/input.json
pnpm run snapshot:render:all
pnpm run snapshot:import:slack-rich-text
pnpm run snapshot:refresh:slack-rich-textDo not use Vitest snapshot auto-update for these fixtures. Update snapshots through the render scripts and review the generated Markdown diffs.
Imported snapshot fixtures currently come from slack-samples/bolt-js-examples, path block-kit/src/blocks/rich_text.js, commit 68028560836a97e90a16859f2d0017ec9d2f0eec.
Related
mdast-util-to-markdownmdast-util-gfmmdast-util-to-hastmdast-util-from-adf
