Metadata-Version: 2.4
Name: markdown-to-slack-blocks
Version: 1.0.0
Summary: Convert Markdown (including GFM tables, mentions, and LLM output) into Slack Block Kit JSON, and back.
Author: Nikita Nefedov
License-Expression: MIT
Project-URL: Homepage, https://github.com/nikita2206/markdown-to-slack-blocks
Project-URL: Source, https://github.com/nikita2206/markdown-to-slack-blocks
Keywords: slack,markdown,block-kit,slack-blocks,gfm
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: markdown-it-py>=3.0.0
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Dynamic: license-file

# markdown-to-slack-blocks

Convert Markdown into Slack [Block Kit](https://api.slack.com/block-kit) JSON, and render blocks back to Markdown or plain text.

This is a Python port of [udivankin/markdown-to-slack-blocks](https://github.com/udivankin/markdown-to-slack-blocks) v1.6.1 (MIT), released here as 1.0.0. It is aimed at the same job: take Markdown from people or from an LLM and post it to Slack without losing headings, lists, code, tables, or mentions.

```bash
pip install markdown-to-slack-blocks
```

```python
from markdown_to_slack_blocks import markdown_to_blocks

blocks = markdown_to_blocks("""
# Hello World
This is a **bold** statement.
""")
```

`markdown_to_blocks` is also available as `markdownToBlocks` if you are moving a call site over from the JavaScript package. The same aliases exist for `splitBlocks`, `splitBlocksWithText`, `blocksToMarkdown`, and `blocksToPlainText`.

## What it emits

| Markdown | Block |
| --- | --- |
| Paragraphs | `section` (`mrkdwn`) by default, or `rich_text` |
| `#` / `##` | `header` |
| `###` and below | bold `section`, or a bold `rich_text` section |
| Lists, quotes, fenced code | `rich_text` (`rich_text_list`, `rich_text_quote`, `rich_text_preformatted`) |
| `---` | `divider` |
| A paragraph that is only an image | `image` |
| GFM tables | `data_table` (or legacy `table`) |

Inline styles become Slack mrkdwn (`*bold*`, `_italic_`, `~strike~`, `` `code` ``) inside sections, and `rich_text` style objects otherwise. Links become `<url|label>`.

Slack-specific tokens are recognized in the text:

- `<@U…>`, `<#C…>`, `<!subteam^S…>`, `<!subteam^T…>`
- `<!here>`, `<!channel>`, `<!everyone>`
- `<!date^timestamp^format|fallback>`
- `:emoji:` shortcodes
- `#rrggbb` color swatches when color detection is on

## Options

```python
blocks = markdown_to_blocks(markdown, {
    "mentions": {
        "users": {"username": "U123456"},
        "channels": {"general": "C123456"},
        "user_groups": {"engineers": "S123456"},  # or "userGroups"
        "teams": {"myteam": "T123456"},
    },
    "detect_colors": True,            # detectColors
    "prefer_section_blocks": True,    # preferSectionBlocks, default True
    "table_block_type": "data_table", # "table" for the legacy block
    "table_caption": "Data table",    # "" omits the caption
})
```

Mention IDs are checked before conversion:

- users start with `U` or `W`
- channels start with `C`
- user groups start with `S`
- teams start with `T`

and the rest of the ID is uppercase alphanumeric.

### XML tag handlers

Tags the library does not know, such as `<sources>` or `<detailed>`, are not Slack blocks. Pass `xml_tag_handlers` (`xmlTagHandlers`) to turn specific elements into whatever blocks you want. The handler is called with an `XmlTagContext`: the element name, its attributes, and the inner Markdown. `convert` parses that inner Markdown with the same options, so nested elements work too.

The tags are parsed with Python's [expat](https://docs.python.org/3/library/pyexpat.html) XML parser, not a regular expression. Names are case-sensitive. Attributes follow XML rules: values are quoted, and entities such as `&amp;` are decoded. The text inside the element is Markdown, so it is not parsed as XML. `a < b` and a raw `&` in the body are kept as written. A start tag that never closes, and a close tag that was never opened, stay as Markdown and do not swallow a later well-formed element. Tags inside fenced code are left alone too.

Slack's [`container`](https://docs.slack.dev/reference/block-kit/blocks/container-block/) block is the usual wrapper. `container_block` builds one. `child_blocks` holds at most 10 blocks, and the plain-text title is at most 150 characters.

```python
from markdown_to_slack_blocks import container_block, markdown_to_blocks

def sources(tag):
    children = tag.convert(tag.body)
    if not children:
        return []
    return container_block(tag.attrs.get("title") or "Sources", children, collapsible=True)

def detailed(tag):
    return container_block(
        "Details",
        tag.convert(tag.body),
        collapsible=True,
        default_collapsed=True,
    )

blocks = markdown_to_blocks(agent_markdown, {
    "xml_tag_handlers": {"sources": sources, "detailed": detailed},
})
```

```xml
Answer text.

<sources title="References">
- [Runbook](https://example.com/runbook)
</sources>

<detailed>
## Investigation
The check failed because **disk** was full.
</detailed>
```

Return one block, a list of blocks, or an empty list to drop the element. Return `None` to leave that occurrence as normal Markdown.

`register_xml_tag_handler("sources", sources)` installs a process-wide default. An `xml_tag_handlers` entry overrides it, and setting the name to `None` there turns the global handler off for that call. `clear_xml_tag_handlers()` removes the defaults.

### Tables

Cells are typed from their content:

| Cell | Slack cell |
| --- | --- |
| Plain text | `raw_text` |
| A plain number (`10`, `-3.5`) | `raw_number` |
| Styles, links, mentions, emoji | `rich_text` |

### Large messages

Slack rejects messages that are too big. `split_blocks` cuts on block boundaries, then inside `rich_text`, then by line inside code blocks. Section and header text is chunked at 3,000 characters first.

```python
from markdown_to_slack_blocks import markdown_to_blocks, split_blocks_with_text

for batch in split_blocks_with_text(markdown_to_blocks(very_long_markdown)):
    client.chat_postMessage(channel=channel, text=batch["text"], blocks=batch["blocks"])
```

Limits default to 40 blocks and 12,000 JSON characters (`max_blocks` / `maxBlocks`, `max_characters` / `maxCharacters`).

### Back to Markdown or plain text

```python
from markdown_to_slack_blocks import blocks_to_markdown, blocks_to_plain_text

text = blocks_to_plain_text(blocks)  # chat.postMessage fallback
markdown = blocks_to_markdown(blocks, {
    "mentions": {
        "users": {"U123456": "username"},
        "channels": {"C123456": "general"},
        "user_groups": {"S123456": "engineers"},
        "teams": {"T123456": "myteam"},
    }
})
```

The Markdown is canonical rather than byte-for-byte identical to the source. Blocks this library produced round-trip cleanly.

## Development

```bash
pip install -e ".[dev]"
pytest
```

The tests include the upstream fixture corpus (`tests/fixtures`) and check both directions against it.

## License

MIT. The original library is copyright https://github.com/udivankin. This Python port is copyright Nikita Nefedov. See [LICENSE](LICENSE).
