md2mrkdwn 0.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,77 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ .idea
6
+ media
7
+
8
+ # C extensions
9
+ *.so
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ env/
14
+ build/
15
+ develop-eggs/
16
+ dist/
17
+ downloads/
18
+ eggs/
19
+ .eggs/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+
27
+ configs/local_settings.py
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .coverage
43
+ .coverage.*
44
+ .cache
45
+ nosetests.xml
46
+ coverage.xml
47
+ *,cover
48
+ .hypothesis/
49
+
50
+ # Translations
51
+ *.pot
52
+
53
+ # Django stuff:
54
+ *.log
55
+
56
+ # Sphinx documentation
57
+ docs/_build/
58
+
59
+ # PyBuilder
60
+ target/
61
+
62
+ #Ipython Notebook
63
+ .ipynb_checkpoints
64
+ venv/*
65
+ env/*
66
+ .venv/*
67
+ .env/*
68
+
69
+ # Idea modules
70
+ *.iml
71
+
72
+ .ruff_cache
73
+ .pytest_cache
74
+ .mypy_cache
75
+
76
+ .claude/
77
+ CLAUDE.md
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dave Allie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,419 @@
1
+ Metadata-Version: 2.4
2
+ Name: md2mrkdwn
3
+ Version: 0.4.2
4
+ Summary: Convert Markdown to Slack mrkdwn format
5
+ Project-URL: Homepage, https://github.com/bigbag/md2mrkdwn
6
+ Project-URL: Repository, https://github.com/bigbag/md2mrkdwn
7
+ Project-URL: Issues, https://github.com/bigbag/md2mrkdwn/issues
8
+ Author-email: Pavel Liashkov <pavel.liashkov@protonamil.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: converter,formatting,markdown,mrkdwn,slack
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Communications :: Chat
21
+ Classifier: Topic :: Text Processing :: Markup
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+
25
+ # md2mrkdwn
26
+
27
+ [![CI](https://github.com/bigbag/md2mrkdwn/workflows/CI/badge.svg)](https://github.com/bigbag/md2mrkdwn/actions?query=workflow%3ACI)
28
+ [![pypi](https://img.shields.io/pypi/v/md2mrkdwn.svg)](https://pypi.python.org/pypi/md2mrkdwn)
29
+ [![downloads](https://img.shields.io/pypi/dm/md2mrkdwn.svg)](https://pypistats.org/packages/md2mrkdwn)
30
+ [![versions](https://img.shields.io/pypi/pyversions/md2mrkdwn.svg)](https://github.com/bigbag/md2mrkdwn)
31
+ [![license](https://img.shields.io/github/license/bigbag/md2mrkdwn.svg)](https://github.com/bigbag/md2mrkdwn/blob/master/LICENSE)
32
+
33
+ Pure Python library for converting Markdown to Slack's mrkdwn format. Zero dependencies, comprehensive formatting support, and proper handling of edge cases.
34
+
35
+ ## Features
36
+
37
+ - **Zero dependencies** - Pure Python implementation with no external packages required
38
+ - **Comprehensive formatting** - Supports bold, italic, strikethrough, links, images, lists, and more
39
+ - **Configurable** - Customize symbols, formats, and enable/disable specific conversions
40
+ - **Code block handling** - Preserves content inside code blocks without conversion
41
+ - **Table support** - Wraps markdown tables in code blocks for Slack display
42
+ - **Task lists** - Converts checkbox syntax to Unicode symbols (☐/☑)
43
+ - **Edge case handling** - Properly handles nested formatting and special characters
44
+
45
+ ## Quick Start
46
+
47
+ ```python
48
+ from md2mrkdwn import convert
49
+
50
+ markdown = "**Hello** *World*! Check out [Slack](https://slack.com)"
51
+ mrkdwn = convert(markdown)
52
+ print(mrkdwn)
53
+ # Output: *Hello* _World_! Check out <https://slack.com|Slack>
54
+ ```
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ # Install with pip
60
+ pip install md2mrkdwn
61
+
62
+ # Or install with uv
63
+ uv add md2mrkdwn
64
+
65
+ # Or install with pipx (for CLI tools that use this library)
66
+ pipx install md2mrkdwn
67
+ ```
68
+
69
+ ## Usage
70
+
71
+ ### Simple Function
72
+
73
+ The `convert()` function provides a simple interface for one-off conversions:
74
+
75
+ ```python
76
+ from md2mrkdwn import convert
77
+
78
+ markdown = """
79
+ # Hello World
80
+
81
+ This is **bold** and *italic* text.
82
+
83
+ - Item 1
84
+ - Item 2
85
+
86
+ Check out [this link](https://example.com)!
87
+ """
88
+
89
+ mrkdwn = convert(markdown)
90
+ print(mrkdwn)
91
+ ```
92
+
93
+ Output:
94
+ ```
95
+ *Hello World*
96
+
97
+ This is *bold* and _italic_ text.
98
+
99
+ • Item 1
100
+ • Item 2
101
+
102
+ Check out <https://example.com|this link>!
103
+ ```
104
+
105
+ ### Class-based Usage
106
+
107
+ For multiple conversions, use the `MrkdwnConverter` class:
108
+
109
+ ```python
110
+ from md2mrkdwn import MrkdwnConverter
111
+
112
+ converter = MrkdwnConverter()
113
+
114
+ # Convert multiple texts
115
+ text1 = converter.convert("**bold** and *italic*")
116
+ text2 = converter.convert("# Header\n\n- List item")
117
+
118
+ print(text1) # *bold* and _italic_
119
+ print(text2) # *Header*\n\n• List item
120
+ ```
121
+
122
+ ### Custom Configuration
123
+
124
+ Use `MrkdwnConfig` to customize conversion behavior:
125
+
126
+ ```python
127
+ from md2mrkdwn import convert, MrkdwnConfig, MrkdwnConverter
128
+
129
+ # Custom bullet character
130
+ config = MrkdwnConfig(bullet_char="-")
131
+ print(convert("- Item 1\n- Item 2", config=config))
132
+ # Output: - Item 1
133
+ # - Item 2
134
+
135
+ # Custom checkbox symbols
136
+ config = MrkdwnConfig(checkbox_checked="✓", checkbox_unchecked="○")
137
+ print(convert("- [x] Done\n- [ ] Todo", config=config))
138
+ # Output: • ✓ Done
139
+ # • ○ Todo
140
+
141
+ # Plain headers (no bold)
142
+ config = MrkdwnConfig(header_style="plain")
143
+ print(convert("# Title", config=config))
144
+ # Output: Title
145
+
146
+ # URL-only links (no link text)
147
+ config = MrkdwnConfig(link_format="url_only")
148
+ print(convert("[Click here](https://example.com)", config=config))
149
+ # Output: <https://example.com>
150
+
151
+ # Disable specific conversions
152
+ config = MrkdwnConfig(convert_bold=False, convert_italic=False)
153
+ print(convert("**bold** and *italic*", config=config))
154
+ # Output: **bold** and *italic*
155
+
156
+ # Reusable converter with config
157
+ converter = MrkdwnConverter(MrkdwnConfig(
158
+ bullet_char="→",
159
+ horizontal_rule_char="=",
160
+ horizontal_rule_length=20
161
+ ))
162
+ print(converter.convert("- Item\n\n---"))
163
+ # Output: → Item
164
+ #
165
+ # ====================
166
+ ```
167
+
168
+ ### Configuration Options
169
+
170
+ | Option | Type | Default | Description |
171
+ |----------------------------|-------------|------------------------|-------------------------------------------|
172
+ | `bullet_char` | str | `•` | Character for unordered list items |
173
+ | `checkbox_checked` | str | `☑` | Symbol for checked task items |
174
+ | `checkbox_unchecked` | str | `☐` | Symbol for unchecked task items |
175
+ | `horizontal_rule_char` | str | `─` | Character for horizontal rules |
176
+ | `horizontal_rule_length` | int | `10` | Length of horizontal rules |
177
+ | `header_style` | HeaderStyle | `HeaderStyle.BOLD` | `BOLD`, `PLAIN`, or `PREFIX` |
178
+ | `link_format` | LinkFormat | `LinkFormat.SLACK` | `SLACK`, `URL_ONLY`, or `TEXT_ONLY` |
179
+ | `table_mode` | TableMode | `TableMode.CODE_BLOCK` | `CODE_BLOCK` or `PRESERVE` |
180
+ | `table_link_format` | LinkFormat | `LinkFormat.URL_ONLY` | Link format inside tables |
181
+ | `strip_table_emoji` | bool | `True` | Strip emoji shortcodes from tables |
182
+ | `convert_table_links` | bool | `True` | Enable/disable link conversion in tables |
183
+ | `convert_bold` | bool | `True` | Enable/disable bold conversion |
184
+ | `convert_italic` | bool | `True` | Enable/disable italic conversion |
185
+ | `convert_strikethrough` | bool | `True` | Enable/disable strikethrough conversion |
186
+ | `convert_links` | bool | `True` | Enable/disable link conversion |
187
+ | `convert_images` | bool | `True` | Enable/disable image conversion |
188
+ | `convert_lists` | bool | `True` | Enable/disable list conversion |
189
+ | `convert_task_lists` | bool | `True` | Enable/disable task list conversion |
190
+ | `convert_headers` | bool | `True` | Enable/disable header conversion |
191
+ | `convert_horizontal_rules` | bool | `True` | Enable/disable horizontal rule conversion |
192
+ | `convert_tables` | bool | `True` | Enable/disable table wrapping |
193
+
194
+ ### Handling Tables
195
+
196
+ Markdown tables are automatically wrapped in code blocks since Slack doesn't support native table rendering:
197
+
198
+ ```python
199
+ from md2mrkdwn import convert
200
+
201
+ markdown = """
202
+ | Name | Age |
203
+ |------|-----|
204
+ | Alice | 30 |
205
+ | Bob | 25 |
206
+ """
207
+
208
+ print(convert(markdown))
209
+ ```
210
+
211
+ Output:
212
+ ```
213
+ ```
214
+ | Name | Age |
215
+ |------|-----|
216
+ | Alice | 30 |
217
+ | Bob | 25 |
218
+ ```
219
+ ```
220
+
221
+ #### Links in Tables
222
+
223
+ Links inside tables are converted to URL-only format by default (different from the global `link_format` setting):
224
+
225
+ ```python
226
+ from md2mrkdwn import convert, MrkdwnConfig, LinkFormat
227
+
228
+ markdown = """
229
+ | App | Link |
230
+ |-----|------|
231
+ | Example | [Visit](https://example.com) |
232
+ """
233
+
234
+ # Default: URL only
235
+ print(convert(markdown))
236
+ # | App | Link |
237
+ # | Example | https://example.com |
238
+
239
+ # Slack format
240
+ config = MrkdwnConfig(table_link_format=LinkFormat.SLACK)
241
+ print(convert(markdown, config))
242
+ # | Example | <https://example.com|Visit> |
243
+
244
+ # Text only (link text, no URL)
245
+ config = MrkdwnConfig(table_link_format=LinkFormat.TEXT_ONLY)
246
+ print(convert(markdown, config))
247
+ # | Example | Visit |
248
+ ```
249
+
250
+ ## Conversion Reference
251
+
252
+ | Markdown | mrkdwn | Notes |
253
+ |----------------------------|--------------------|------------------------------|
254
+ | `**bold**` or `__bold__` | `*bold*` | Slack uses single asterisk |
255
+ | `*italic*` or `_italic_` | `_italic_` | Slack uses underscores |
256
+ | `***bold+italic***` | `*_text_*` | Combined formatting |
257
+ | `~~strikethrough~~` | `~text~` | Single tilde |
258
+ | `[text](url)` | `<url\|text>` | Slack link format |
259
+ | `![alt](url)` | `<url>` | Images become plain URLs |
260
+ | `# Header` (all levels) | `*Header*` | Bold (Slack has no headers) |
261
+ | `- item` / `* item` | `• item` | Bullet character (U+2022) |
262
+ | `1. item` | `1. item` | Preserved as-is |
263
+ | `- [ ] task` | `• ☐ task` | Unchecked checkbox (U+2610) |
264
+ | `- [x] task` | `• ☑ task` | Checked checkbox (U+2611) |
265
+ | `> quote` | `> quote` | Same syntax |
266
+ | `` `code` `` | `` `code` `` | Same syntax |
267
+ | ``` code block ``` | ``` code block ``` | Same syntax |
268
+ | `---` / `***` | `──────────` | Horizontal rule (U+2500) |
269
+ | Tables | Wrapped in ``` | Slack has no native tables |
270
+
271
+ ## How It Works
272
+
273
+ ### Conversion Pipeline
274
+
275
+ md2mrkdwn processes text through a multi-stage pipeline:
276
+
277
+ 1. **Table extraction** - Tables are detected, validated, and replaced with placeholders
278
+ 2. **Code block tracking** - Lines inside code blocks are skipped during conversion
279
+ 3. **Pattern application** - Regex patterns convert formatting using placeholder protection
280
+ 4. **Placeholder restoration** - Tables and temporary markers are replaced with final output
281
+
282
+ ### Pattern Interference Prevention
283
+
284
+ 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.
285
+
286
+ md2mrkdwn solves this using placeholder substitution:
287
+ 1. Bold text is temporarily marked with null-byte placeholders
288
+ 2. Italic patterns run without matching the placeholders
289
+ 3. Placeholders are replaced with final mrkdwn characters
290
+
291
+ ### Table Handling
292
+
293
+ Tables are detected using these criteria:
294
+ - Lines matching `|...|` pattern
295
+ - Second row contains separator cells (dashes with optional alignment colons)
296
+ - Header and separator have matching column counts
297
+
298
+ Valid tables are wrapped in triple-backtick code blocks for monospace display in Slack.
299
+
300
+ 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.
301
+
302
+ ### Code Block Protection
303
+
304
+ Content inside code blocks (both fenced and inline) is protected from conversion:
305
+ - Fenced blocks: State machine tracks opening/closing ``` markers
306
+ - Inline code: Segments are extracted before conversion and restored after
307
+
308
+ ## Development
309
+
310
+ ### Setup
311
+
312
+ ```bash
313
+ git clone https://github.com/bigbag/md2mrkdwn.git
314
+ cd md2mrkdwn
315
+ make install
316
+ ```
317
+
318
+ ### Commands
319
+
320
+ ```bash
321
+ make install # Install all dependencies
322
+ make test # Run tests with coverage
323
+ make lint # Run linters (ruff + mypy)
324
+ make format # Format code with ruff
325
+ make clean # Clean cache and build files
326
+ ```
327
+
328
+ ### Running Tests
329
+
330
+ ```bash
331
+ # Run all tests with coverage
332
+ uv run pytest --cov=md2mrkdwn --cov-report=term-missing
333
+
334
+ # Run specific test class
335
+ uv run pytest tests/test_converter.py::TestBasicFormatting -v
336
+
337
+ # Run with verbose output
338
+ uv run pytest -v
339
+ ```
340
+
341
+ ### Project Structure
342
+
343
+ ```
344
+ md2mrkdwn/
345
+ ├── src/
346
+ │ └── md2mrkdwn/
347
+ │ ├── __init__.py # Package exports
348
+ │ └── converter.py # MrkdwnConverter, MrkdwnConfig classes
349
+ ├── tests/
350
+ │ ├── conftest.py # Pytest fixtures
351
+ │ ├── test_converter.py # Converter tests
352
+ │ └── test_config.py # Configuration tests
353
+ ├── pyproject.toml # Project configuration
354
+ ├── Makefile # Development commands
355
+ └── README.md
356
+ ```
357
+
358
+ ## API Reference
359
+
360
+ ### `convert(markdown: str, config: MrkdwnConfig | None = None) -> str`
361
+
362
+ Convert Markdown text to Slack mrkdwn format.
363
+
364
+ **Parameters:**
365
+ - `markdown` - Input text in Markdown format
366
+ - `config` - Optional configuration (uses defaults if not provided)
367
+
368
+ **Returns:**
369
+ - Text converted to Slack mrkdwn format
370
+
371
+ ### `MrkdwnConverter`
372
+
373
+ Class for converting Markdown to mrkdwn.
374
+
375
+ **Constructor:**
376
+ - `MrkdwnConverter(config: MrkdwnConfig | None = None)`
377
+
378
+ **Methods:**
379
+ - `convert(markdown: str) -> str` - Convert Markdown text to mrkdwn
380
+
381
+ **Example:**
382
+ ```python
383
+ converter = MrkdwnConverter()
384
+ result = converter.convert("**Hello** *World*")
385
+
386
+ # With custom config
387
+ config = MrkdwnConfig(bullet_char="-")
388
+ converter = MrkdwnConverter(config)
389
+ result = converter.convert("- Item")
390
+ ```
391
+
392
+ ### `MrkdwnConfig`
393
+
394
+ Immutable configuration dataclass for customizing conversion behavior.
395
+
396
+ **Example:**
397
+ ```python
398
+ from md2mrkdwn import MrkdwnConfig, DEFAULT_CONFIG
399
+
400
+ # Create custom config
401
+ config = MrkdwnConfig(
402
+ bullet_char="→",
403
+ header_style="plain",
404
+ convert_bold=False
405
+ )
406
+
407
+ # Use the default config singleton
408
+ print(DEFAULT_CONFIG.bullet_char) # •
409
+ ```
410
+
411
+
412
+ ## See Also
413
+
414
+ - [Slack mrkdwn specification](https://api.slack.com/reference/surfaces/formatting) - Official Slack formatting documentation
415
+ - [markdown_to_mrkdwn](https://github.com/fla9ua/markdown_to_mrkdwn) - Related project for markdown to mrkdwn conversion
416
+
417
+ ## License
418
+
419
+ MIT License - see [LICENSE](LICENSE) file.