{"id":50753565,"url":"https://github.com/bigbag/md2mrkdwn","last_synced_at":"2026-06-11T03:03:33.183Z","repository":{"id":356950278,"uuid":"1129129114","full_name":"bigbag/md2mrkdwn","owner":"bigbag","description":"Optimized Python library to convert Markdown to Slack's mrkdwn format. Handles tables, nested lists, and edge cases.","archived":false,"fork":false,"pushed_at":"2026-01-23T15:18:08.000Z","size":188,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-10T17:10:10.418Z","etag":null,"topics":["formatting","markdown","mrkdwn","slack"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bigbag.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-06T16:39:33.000Z","updated_at":"2026-04-26T20:19:42.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bigbag/md2mrkdwn","commit_stats":null,"previous_names":["bigbag/md2mrkdwn"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/bigbag/md2mrkdwn","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigbag%2Fmd2mrkdwn","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigbag%2Fmd2mrkdwn/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigbag%2Fmd2mrkdwn/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigbag%2Fmd2mrkdwn/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bigbag","download_url":"https://codeload.github.com/bigbag/md2mrkdwn/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigbag%2Fmd2mrkdwn/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34180147,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-11T02:00:06.485Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["formatting","markdown","mrkdwn","slack"],"created_at":"2026-06-11T03:03:32.459Z","updated_at":"2026-06-11T03:03:33.175Z","avatar_url":"https://github.com/bigbag.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# md2mrkdwn\n\n[![CI](https://github.com/bigbag/md2mrkdwn/workflows/CI/badge.svg)](https://github.com/bigbag/md2mrkdwn/actions?query=workflow%3ACI)\n[![pypi](https://img.shields.io/pypi/v/md2mrkdwn.svg)](https://pypi.python.org/pypi/md2mrkdwn)\n[![downloads](https://img.shields.io/pypi/dm/md2mrkdwn.svg)](https://pypistats.org/packages/md2mrkdwn)\n[![versions](https://img.shields.io/pypi/pyversions/md2mrkdwn.svg)](https://github.com/bigbag/md2mrkdwn)\n[![license](https://img.shields.io/github/license/bigbag/md2mrkdwn.svg)](https://github.com/bigbag/md2mrkdwn/blob/master/LICENSE)\n\nPure Python library for converting Markdown to Slack's mrkdwn format. Zero dependencies, comprehensive formatting support, and proper handling of edge cases.\n\n## Features\n\n- **Zero dependencies** - Pure Python implementation with no external packages required\n- **Comprehensive formatting** - Supports bold, italic, strikethrough, links, images, lists, and more\n- **Configurable** - Customize symbols, formats, and enable/disable specific conversions\n- **Code block handling** - Preserves content inside code blocks without conversion\n- **Table support** - Wraps markdown tables in code blocks for Slack display\n- **Task lists** - Converts checkbox syntax to Unicode symbols (☐/☑)\n- **Edge case handling** - Properly handles nested formatting and special characters\n\n## Quick Start\n\n```python\nfrom md2mrkdwn import convert\n\nmarkdown = \"**Hello** *World*! Check out [Slack](https://slack.com)\"\nmrkdwn = convert(markdown)\nprint(mrkdwn)\n# Output: *Hello* _World_! Check out \u003chttps://slack.com|Slack\u003e\n```\n\n## Installation\n\n```bash\n# Install with pip\npip install md2mrkdwn\n\n# Or install with uv\nuv add md2mrkdwn\n\n# Or install with pipx (for CLI tools that use this library)\npipx install md2mrkdwn\n```\n\n## Usage\n\n### Simple Function\n\nThe `convert()` function provides a simple interface for one-off conversions:\n\n```python\nfrom md2mrkdwn import convert\n\nmarkdown = \"\"\"\n# Hello World\n\nThis is **bold** and *italic* text.\n\n- Item 1\n- Item 2\n\nCheck out [this link](https://example.com)!\n\"\"\"\n\nmrkdwn = convert(markdown)\nprint(mrkdwn)\n```\n\nOutput:\n```\n*Hello World*\n\nThis is *bold* and _italic_ text.\n\n• Item 1\n• Item 2\n\nCheck out \u003chttps://example.com|this link\u003e!\n```\n\n### Class-based Usage\n\nFor multiple conversions, use the `MrkdwnConverter` class:\n\n```python\nfrom md2mrkdwn import MrkdwnConverter\n\nconverter = MrkdwnConverter()\n\n# Convert multiple texts\ntext1 = converter.convert(\"**bold** and *italic*\")\ntext2 = converter.convert(\"# Header\\n\\n- List item\")\n\nprint(text1)  # *bold* and _italic_\nprint(text2)  # *Header*\\n\\n• List item\n```\n\n### Custom Configuration\n\nUse `MrkdwnConfig` to customize conversion behavior:\n\n```python\nfrom md2mrkdwn import convert, MrkdwnConfig, MrkdwnConverter\n\n# Custom bullet character\nconfig = MrkdwnConfig(bullet_char=\"-\")\nprint(convert(\"- Item 1\\n- Item 2\", config=config))\n# Output: - Item 1\n#         - Item 2\n\n# Custom checkbox symbols\nconfig = MrkdwnConfig(checkbox_checked=\"✓\", checkbox_unchecked=\"○\")\nprint(convert(\"- [x] Done\\n- [ ] Todo\", config=config))\n# Output: • ✓ Done\n#         • ○ Todo\n\n# Plain headers (no bold)\nconfig = MrkdwnConfig(header_style=\"plain\")\nprint(convert(\"# Title\", config=config))\n# Output: Title\n\n# URL-only links (no link text)\nconfig = MrkdwnConfig(link_format=\"url_only\")\nprint(convert(\"[Click here](https://example.com)\", config=config))\n# Output: \u003chttps://example.com\u003e\n\n# Disable specific conversions\nconfig = MrkdwnConfig(convert_bold=False, convert_italic=False)\nprint(convert(\"**bold** and *italic*\", config=config))\n# Output: **bold** and *italic*\n\n# Reusable converter with config\nconverter = MrkdwnConverter(MrkdwnConfig(\n    bullet_char=\"→\",\n    horizontal_rule_char=\"=\",\n    horizontal_rule_length=20\n))\nprint(converter.convert(\"- Item\\n\\n---\"))\n# Output: → Item\n#\n#         ====================\n```\n\n### Configuration Options\n\n| Option                     | Type        | Default                | Description                               |\n|----------------------------|-------------|------------------------|-------------------------------------------|\n| `bullet_char`              | str         | `•`                    | Character for unordered list items        |\n| `checkbox_checked`         | str         | `☑`                    | Symbol for checked task items             |\n| `checkbox_unchecked`       | str         | `☐`                    | Symbol for unchecked task items           |\n| `horizontal_rule_char`     | str         | `─`                    | Character for horizontal rules            |\n| `horizontal_rule_length`   | int         | `10`                   | Length of horizontal rules                |\n| `header_style`             | HeaderStyle | `HeaderStyle.BOLD`     | `BOLD`, `PLAIN`, or `PREFIX`              |\n| `link_format`              | LinkFormat  | `LinkFormat.SLACK`     | `SLACK`, `URL_ONLY`, or `TEXT_ONLY`       |\n| `table_mode`               | TableMode   | `TableMode.CODE_BLOCK` | `CODE_BLOCK` or `PRESERVE`              |\n| `table_link_format`        | LinkFormat  | `LinkFormat.URL_ONLY`  | Link format inside tables                 |\n| `strip_table_emoji`        | bool        | `True`                 | Strip emoji shortcodes from tables        |\n| `convert_table_links`      | bool        | `True`                 | Enable/disable link conversion in tables  |\n| `convert_bold`             | bool        | `True`                 | Enable/disable bold conversion            |\n| `convert_italic`           | bool        | `True`                 | Enable/disable italic conversion          |\n| `convert_strikethrough`    | bool        | `True`                 | Enable/disable strikethrough conversion   |\n| `convert_links`            | bool        | `True`                 | Enable/disable link conversion            |\n| `convert_images`           | bool        | `True`                 | Enable/disable image conversion           |\n| `convert_lists`            | bool        | `True`                 | Enable/disable list conversion            |\n| `convert_task_lists`       | bool        | `True`                 | Enable/disable task list conversion       |\n| `convert_headers`          | bool        | `True`                 | Enable/disable header conversion          |\n| `convert_horizontal_rules` | bool        | `True`                 | Enable/disable horizontal rule conversion |\n| `convert_tables`           | bool        | `True`                 | Enable/disable table wrapping             |\n\n### Handling Tables\n\nMarkdown tables are automatically wrapped in code blocks since Slack doesn't support native table rendering:\n\n```python\nfrom md2mrkdwn import convert\n\nmarkdown = \"\"\"\n| Name | Age |\n|------|-----|\n| Alice | 30 |\n| Bob | 25 |\n\"\"\"\n\nprint(convert(markdown))\n```\n\nOutput:\n```\n```\n| Name | Age |\n|------|-----|\n| Alice | 30 |\n| Bob | 25 |\n```\n```\n\n#### Links in Tables\n\nLinks inside tables are converted to URL-only format by default (different from the global `link_format` setting):\n\n```python\nfrom md2mrkdwn import convert, MrkdwnConfig, LinkFormat\n\nmarkdown = \"\"\"\n| App | Link |\n|-----|------|\n| Example | [Visit](https://example.com) |\n\"\"\"\n\n# Default: URL only\nprint(convert(markdown))\n# | App | Link |\n# | Example | https://example.com |\n\n# Slack format\nconfig = MrkdwnConfig(table_link_format=LinkFormat.SLACK)\nprint(convert(markdown, config))\n# | Example | \u003chttps://example.com|Visit\u003e |\n\n# Text only (link text, no URL)\nconfig = MrkdwnConfig(table_link_format=LinkFormat.TEXT_ONLY)\nprint(convert(markdown, config))\n# | Example | Visit |\n```\n\n## Conversion Reference\n\n| Markdown                   | mrkdwn             | Notes                        |\n|----------------------------|--------------------|------------------------------|\n| `**bold**` or `__bold__`   | `*bold*`           | Slack uses single asterisk   |\n| `*italic*` or `_italic_`   | `_italic_`         | Slack uses underscores       |\n| `***bold+italic***`        | `*_text_*`         | Combined formatting          |\n| `~~strikethrough~~`        | `~text~`           | Single tilde                 |\n| `[text](url)`              | `\u003curl\\|text\u003e`      | Slack link format            |\n| `![alt](url)`              | `\u003curl\u003e`            | Images become plain URLs     |\n| `# Header` (all levels)    | `*Header*`         | Bold (Slack has no headers)  |\n| `- item` / `* item`        | `• item`           | Bullet character (U+2022)    |\n| `1. item`                  | `1. item`          | Preserved as-is              |\n| `- [ ] task`               | `• ☐ task`         | Unchecked checkbox (U+2610)  |\n| `- [x] task`               | `• ☑ task`         | Checked checkbox (U+2611)    |\n| `\u003e quote`                  | `\u003e quote`          | Same syntax                  |\n| `` `code` ``               | `` `code` ``       | Same syntax                  |\n| ``` code block ```         | ``` code block ``` | Same syntax                  |\n| `---` / `***`              | `──────────`       | Horizontal rule (U+2500)     |\n| Tables                     | Wrapped in ```     | Slack has no native tables   |\n\n## How It Works\n\n### Conversion Pipeline\n\nmd2mrkdwn processes text through a multi-stage pipeline:\n\n1. **Table extraction** - Tables are detected, validated, and replaced with placeholders\n2. **Code block tracking** - Lines inside code blocks are skipped during conversion\n3. **Pattern application** - Regex patterns convert formatting using placeholder protection\n4. **Placeholder restoration** - Tables and temporary markers are replaced with final output\n\n### Pattern Interference Prevention\n\nA 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.\n\nmd2mrkdwn solves this using placeholder substitution:\n1. Bold text is temporarily marked with null-byte placeholders\n2. Italic patterns run without matching the placeholders\n3. Placeholders are replaced with final mrkdwn characters\n\n### Table Handling\n\nTables are detected using these criteria:\n- Lines matching `|...|` pattern\n- Second row contains separator cells (dashes with optional alignment colons)\n- Header and separator have matching column counts\n\nValid tables are wrapped in triple-backtick code blocks for monospace display in Slack.\n\nColumn 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.\n\n### Code Block Protection\n\nContent inside code blocks (both fenced and inline) is protected from conversion:\n- Fenced blocks: State machine tracks opening/closing ``` markers\n- Inline code: Segments are extracted before conversion and restored after\n\n## Development\n\n### Setup\n\n```bash\ngit clone https://github.com/bigbag/md2mrkdwn.git\ncd md2mrkdwn\nmake install\n```\n\n### Commands\n\n```bash\nmake install  # Install all dependencies\nmake test     # Run tests with coverage\nmake lint     # Run linters (ruff + mypy)\nmake format   # Format code with ruff\nmake clean    # Clean cache and build files\n```\n\n### Running Tests\n\n```bash\n# Run all tests with coverage\nuv run pytest --cov=md2mrkdwn --cov-report=term-missing\n\n# Run specific test class\nuv run pytest tests/test_converter.py::TestBasicFormatting -v\n\n# Run with verbose output\nuv run pytest -v\n```\n\n### Project Structure\n\n```\nmd2mrkdwn/\n├── src/\n│   └── md2mrkdwn/\n│       ├── __init__.py      # Package exports\n│       └── converter.py     # MrkdwnConverter, MrkdwnConfig classes\n├── tests/\n│   ├── conftest.py          # Pytest fixtures\n│   ├── test_converter.py    # Converter tests\n│   └── test_config.py       # Configuration tests\n├── pyproject.toml           # Project configuration\n├── Makefile                 # Development commands\n└── README.md\n```\n\n## API Reference\n\n### `convert(markdown: str, config: MrkdwnConfig | None = None) -\u003e str`\n\nConvert Markdown text to Slack mrkdwn format.\n\n**Parameters:**\n- `markdown` - Input text in Markdown format\n- `config` - Optional configuration (uses defaults if not provided)\n\n**Returns:**\n- Text converted to Slack mrkdwn format\n\n### `MrkdwnConverter`\n\nClass for converting Markdown to mrkdwn.\n\n**Constructor:**\n- `MrkdwnConverter(config: MrkdwnConfig | None = None)`\n\n**Methods:**\n- `convert(markdown: str) -\u003e str` - Convert Markdown text to mrkdwn\n\n**Example:**\n```python\nconverter = MrkdwnConverter()\nresult = converter.convert(\"**Hello** *World*\")\n\n# With custom config\nconfig = MrkdwnConfig(bullet_char=\"-\")\nconverter = MrkdwnConverter(config)\nresult = converter.convert(\"- Item\")\n```\n\n### `MrkdwnConfig`\n\nImmutable configuration dataclass for customizing conversion behavior.\n\n**Example:**\n```python\nfrom md2mrkdwn import MrkdwnConfig, DEFAULT_CONFIG\n\n# Create custom config\nconfig = MrkdwnConfig(\n    bullet_char=\"→\",\n    header_style=\"plain\",\n    convert_bold=False\n)\n\n# Use the default config singleton\nprint(DEFAULT_CONFIG.bullet_char)  # •\n```\n\n\n## See Also\n\n- [Slack mrkdwn specification](https://api.slack.com/reference/surfaces/formatting) - Official Slack formatting documentation\n- [markdown_to_mrkdwn](https://github.com/fla9ua/markdown_to_mrkdwn) - Related project for markdown to mrkdwn conversion\n\n## License\n\nMIT License - see [LICENSE](LICENSE) file.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbigbag%2Fmd2mrkdwn","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbigbag%2Fmd2mrkdwn","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbigbag%2Fmd2mrkdwn/lists"}