https://github.com/bigbag/md2mrkdwn
Optimized Python library to convert Markdown to Slack's mrkdwn format. Handles tables, nested lists, and edge cases.
https://github.com/bigbag/md2mrkdwn
formatting markdown mrkdwn slack
Last synced: 14 days ago
JSON representation
Optimized Python library to convert Markdown to Slack's mrkdwn format. Handles tables, nested lists, and edge cases.
- Host: GitHub
- URL: https://github.com/bigbag/md2mrkdwn
- Owner: bigbag
- License: mit
- Created: 2026-01-06T16:39:33.000Z (6 months ago)
- Default Branch: main
- Last Pushed: 2026-01-23T15:18:08.000Z (5 months ago)
- Last Synced: 2026-05-10T17:10:10.418Z (about 2 months ago)
- Topics: formatting, markdown, mrkdwn, slack
- Language: Python
- Homepage:
- Size: 184 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# md2mrkdwn
[](https://github.com/bigbag/md2mrkdwn/actions?query=workflow%3ACI)
[](https://pypi.python.org/pypi/md2mrkdwn)
[](https://pypistats.org/packages/md2mrkdwn)
[](https://github.com/bigbag/md2mrkdwn)
[](https://github.com/bigbag/md2mrkdwn/blob/master/LICENSE)
Pure Python library for converting Markdown to Slack's mrkdwn format. Zero dependencies, comprehensive formatting support, and proper handling of edge cases.
## Features
- **Zero dependencies** - Pure Python implementation with no external packages required
- **Comprehensive formatting** - Supports bold, italic, strikethrough, links, images, lists, and more
- **Configurable** - Customize symbols, formats, and enable/disable specific conversions
- **Code block handling** - Preserves content inside code blocks without conversion
- **Table support** - Wraps markdown tables in code blocks for Slack display
- **Task lists** - Converts checkbox syntax to Unicode symbols (☐/☑)
- **Edge case handling** - Properly handles nested formatting and special characters
## Quick Start
```python
from md2mrkdwn import convert
markdown = "**Hello** *World*! Check out [Slack](https://slack.com)"
mrkdwn = convert(markdown)
print(mrkdwn)
# Output: *Hello* _World_! Check out
```
## Installation
```bash
# Install with pip
pip install md2mrkdwn
# Or install with uv
uv add md2mrkdwn
# Or install with pipx (for CLI tools that use this library)
pipx install md2mrkdwn
```
## Usage
### Simple Function
The `convert()` function provides a simple interface for one-off conversions:
```python
from md2mrkdwn import convert
markdown = """
# Hello World
This is **bold** and *italic* text.
- Item 1
- Item 2
Check out [this link](https://example.com)!
"""
mrkdwn = convert(markdown)
print(mrkdwn)
```
Output:
```
*Hello World*
This is *bold* and _italic_ text.
• Item 1
• Item 2
Check out !
```
### Class-based Usage
For multiple conversions, use the `MrkdwnConverter` class:
```python
from md2mrkdwn import MrkdwnConverter
converter = MrkdwnConverter()
# Convert multiple texts
text1 = converter.convert("**bold** and *italic*")
text2 = converter.convert("# Header\n\n- List item")
print(text1) # *bold* and _italic_
print(text2) # *Header*\n\n• List item
```
### Custom Configuration
Use `MrkdwnConfig` to customize conversion behavior:
```python
from md2mrkdwn import convert, MrkdwnConfig, MrkdwnConverter
# Custom bullet character
config = MrkdwnConfig(bullet_char="-")
print(convert("- Item 1\n- Item 2", config=config))
# Output: - Item 1
# - Item 2
# Custom checkbox symbols
config = MrkdwnConfig(checkbox_checked="✓", checkbox_unchecked="○")
print(convert("- [x] Done\n- [ ] Todo", config=config))
# Output: • ✓ Done
# • ○ Todo
# Plain headers (no bold)
config = MrkdwnConfig(header_style="plain")
print(convert("# Title", config=config))
# Output: Title
# URL-only links (no link text)
config = MrkdwnConfig(link_format="url_only")
print(convert("[Click here](https://example.com)", config=config))
# Output:
# Disable specific conversions
config = MrkdwnConfig(convert_bold=False, convert_italic=False)
print(convert("**bold** and *italic*", config=config))
# Output: **bold** and *italic*
# Reusable converter with config
converter = MrkdwnConverter(MrkdwnConfig(
bullet_char="→",
horizontal_rule_char="=",
horizontal_rule_length=20
))
print(converter.convert("- Item\n\n---"))
# Output: → Item
#
# ====================
```
### Configuration Options
| Option | Type | Default | Description |
|----------------------------|-------------|------------------------|-------------------------------------------|
| `bullet_char` | str | `•` | Character for unordered list items |
| `checkbox_checked` | str | `☑` | Symbol for checked task items |
| `checkbox_unchecked` | str | `☐` | Symbol for unchecked task items |
| `horizontal_rule_char` | str | `─` | Character for horizontal rules |
| `horizontal_rule_length` | int | `10` | Length of horizontal rules |
| `header_style` | HeaderStyle | `HeaderStyle.BOLD` | `BOLD`, `PLAIN`, or `PREFIX` |
| `link_format` | LinkFormat | `LinkFormat.SLACK` | `SLACK`, `URL_ONLY`, or `TEXT_ONLY` |
| `table_mode` | TableMode | `TableMode.CODE_BLOCK` | `CODE_BLOCK` or `PRESERVE` |
| `table_link_format` | LinkFormat | `LinkFormat.URL_ONLY` | Link format inside tables |
| `strip_table_emoji` | bool | `True` | Strip emoji shortcodes from tables |
| `convert_table_links` | bool | `True` | Enable/disable link conversion in tables |
| `convert_bold` | bool | `True` | Enable/disable bold conversion |
| `convert_italic` | bool | `True` | Enable/disable italic conversion |
| `convert_strikethrough` | bool | `True` | Enable/disable strikethrough conversion |
| `convert_links` | bool | `True` | Enable/disable link conversion |
| `convert_images` | bool | `True` | Enable/disable image conversion |
| `convert_lists` | bool | `True` | Enable/disable list conversion |
| `convert_task_lists` | bool | `True` | Enable/disable task list conversion |
| `convert_headers` | bool | `True` | Enable/disable header conversion |
| `convert_horizontal_rules` | bool | `True` | Enable/disable horizontal rule conversion |
| `convert_tables` | bool | `True` | Enable/disable table wrapping |
### Handling Tables
Markdown tables are automatically wrapped in code blocks since Slack doesn't support native table rendering:
```python
from md2mrkdwn import convert
markdown = """
| Name | Age |
|------|-----|
| Alice | 30 |
| Bob | 25 |
"""
print(convert(markdown))
```
Output:
```
```
| Name | Age |
|------|-----|
| Alice | 30 |
| Bob | 25 |
```
```
#### Links in Tables
Links inside tables are converted to URL-only format by default (different from the global `link_format` setting):
```python
from md2mrkdwn import convert, MrkdwnConfig, LinkFormat
markdown = """
| App | Link |
|-----|------|
| Example | [Visit](https://example.com) |
"""
# Default: URL only
print(convert(markdown))
# | App | Link |
# | Example | https://example.com |
# Slack format
config = MrkdwnConfig(table_link_format=LinkFormat.SLACK)
print(convert(markdown, config))
# | Example | |
# Text only (link text, no URL)
config = MrkdwnConfig(table_link_format=LinkFormat.TEXT_ONLY)
print(convert(markdown, config))
# | Example | Visit |
```
## Conversion Reference
| Markdown | mrkdwn | Notes |
|----------------------------|--------------------|------------------------------|
| `**bold**` or `__bold__` | `*bold*` | Slack uses single asterisk |
| `*italic*` or `_italic_` | `_italic_` | Slack uses underscores |
| `***bold+italic***` | `*_text_*` | Combined formatting |
| `~~strikethrough~~` | `~text~` | Single tilde |
| `[text](url)` | `` | Slack link format |
| `` | `` | Images become plain URLs |
| `# Header` (all levels) | `*Header*` | Bold (Slack has no headers) |
| `- item` / `* item` | `• item` | Bullet character (U+2022) |
| `1. item` | `1. item` | Preserved as-is |
| `- [ ] task` | `• ☐ task` | Unchecked checkbox (U+2610) |
| `- [x] task` | `• ☑ task` | Checked checkbox (U+2611) |
| `> quote` | `> quote` | Same syntax |
| `` `code` `` | `` `code` `` | Same syntax |
| ``` code block ``` | ``` code block ``` | Same syntax |
| `---` / `***` | `──────────` | Horizontal rule (U+2500) |
| Tables | Wrapped in ``` | Slack has no native tables |
## How It Works
### Conversion Pipeline
md2mrkdwn processes text through a multi-stage pipeline:
1. **Table extraction** - Tables are detected, validated, and replaced with placeholders
2. **Code block tracking** - Lines inside code blocks are skipped during conversion
3. **Pattern application** - Regex patterns convert formatting using placeholder protection
4. **Placeholder restoration** - Tables and temporary markers are replaced with final output
### Pattern Interference Prevention
A key challenge in markdown conversion is preventing patterns from interfering with each other. For example, converting `**bold**` to `*bold*` could then be matched by the italic pattern.
md2mrkdwn solves this using placeholder substitution:
1. Bold text is temporarily marked with null-byte placeholders
2. Italic patterns run without matching the placeholders
3. Placeholders are replaced with final mrkdwn characters
### Table Handling
Tables are detected using these criteria:
- Lines matching `|...|` pattern
- Second row contains separator cells (dashes with optional alignment colons)
- Header and separator have matching column counts
Valid tables are wrapped in triple-backtick code blocks for monospace display in Slack.
Column alignment accounts for Unicode character display width. Emoji like ⭐ render as 2 columns wide in monospace fonts but have a character length of 1. The converter pads columns based on display width to maintain proper alignment.
### Code Block Protection
Content inside code blocks (both fenced and inline) is protected from conversion:
- Fenced blocks: State machine tracks opening/closing ``` markers
- Inline code: Segments are extracted before conversion and restored after
## Development
### Setup
```bash
git clone https://github.com/bigbag/md2mrkdwn.git
cd md2mrkdwn
make install
```
### Commands
```bash
make install # Install all dependencies
make test # Run tests with coverage
make lint # Run linters (ruff + mypy)
make format # Format code with ruff
make clean # Clean cache and build files
```
### Running Tests
```bash
# Run all tests with coverage
uv run pytest --cov=md2mrkdwn --cov-report=term-missing
# Run specific test class
uv run pytest tests/test_converter.py::TestBasicFormatting -v
# Run with verbose output
uv run pytest -v
```
### Project Structure
```
md2mrkdwn/
├── src/
│ └── md2mrkdwn/
│ ├── __init__.py # Package exports
│ └── converter.py # MrkdwnConverter, MrkdwnConfig classes
├── tests/
│ ├── conftest.py # Pytest fixtures
│ ├── test_converter.py # Converter tests
│ └── test_config.py # Configuration tests
├── pyproject.toml # Project configuration
├── Makefile # Development commands
└── README.md
```
## API Reference
### `convert(markdown: str, config: MrkdwnConfig | None = None) -> str`
Convert Markdown text to Slack mrkdwn format.
**Parameters:**
- `markdown` - Input text in Markdown format
- `config` - Optional configuration (uses defaults if not provided)
**Returns:**
- Text converted to Slack mrkdwn format
### `MrkdwnConverter`
Class for converting Markdown to mrkdwn.
**Constructor:**
- `MrkdwnConverter(config: MrkdwnConfig | None = None)`
**Methods:**
- `convert(markdown: str) -> str` - Convert Markdown text to mrkdwn
**Example:**
```python
converter = MrkdwnConverter()
result = converter.convert("**Hello** *World*")
# With custom config
config = MrkdwnConfig(bullet_char="-")
converter = MrkdwnConverter(config)
result = converter.convert("- Item")
```
### `MrkdwnConfig`
Immutable configuration dataclass for customizing conversion behavior.
**Example:**
```python
from md2mrkdwn import MrkdwnConfig, DEFAULT_CONFIG
# Create custom config
config = MrkdwnConfig(
bullet_char="→",
header_style="plain",
convert_bold=False
)
# Use the default config singleton
print(DEFAULT_CONFIG.bullet_char) # •
```
## See Also
- [Slack mrkdwn specification](https://api.slack.com/reference/surfaces/formatting) - Official Slack formatting documentation
- [markdown_to_mrkdwn](https://github.com/fla9ua/markdown_to_mrkdwn) - Related project for markdown to mrkdwn conversion
## License
MIT License - see [LICENSE](LICENSE) file.