An open API service indexing awesome lists of open source software.

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.

Awesome Lists containing this project

README

          

# md2mrkdwn

[![CI](https://github.com/bigbag/md2mrkdwn/workflows/CI/badge.svg)](https://github.com/bigbag/md2mrkdwn/actions?query=workflow%3ACI)
[![pypi](https://img.shields.io/pypi/v/md2mrkdwn.svg)](https://pypi.python.org/pypi/md2mrkdwn)
[![downloads](https://img.shields.io/pypi/dm/md2mrkdwn.svg)](https://pypistats.org/packages/md2mrkdwn)
[![versions](https://img.shields.io/pypi/pyversions/md2mrkdwn.svg)](https://github.com/bigbag/md2mrkdwn)
[![license](https://img.shields.io/github/license/bigbag/md2mrkdwn.svg)](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 |
| `![alt](url)` | `` | 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.