marktripy 1.0.3__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.
- marktripy-1.0.3/.gitignore +90 -0
- marktripy-1.0.3/PKG-INFO +320 -0
- marktripy-1.0.3/README.md +296 -0
- marktripy-1.0.3/pyproject.toml +200 -0
- marktripy-1.0.3/src/marktripy/__init__.py +10 -0
- marktripy-1.0.3/src/marktripy/_version.py +21 -0
- marktripy-1.0.3/src/marktripy/core/__init__.py +7 -0
- marktripy-1.0.3/src/marktripy/core/ast.py +274 -0
- marktripy-1.0.3/src/marktripy/core/parser.py +199 -0
- marktripy-1.0.3/src/marktripy/core/validator.py +266 -0
- marktripy-1.0.3/src/marktripy/extensions/__init__.py +2 -0
- marktripy-1.0.3/src/marktripy/extensions/base.py +247 -0
- marktripy-1.0.3/src/marktripy/extensions/gfm.py +106 -0
- marktripy-1.0.3/src/marktripy/extensions/kbd.py +153 -0
- marktripy-1.0.3/src/marktripy/extensions/strikethrough.py +124 -0
- marktripy-1.0.3/src/marktripy/extensions/tasklist.py +138 -0
- marktripy-1.0.3/src/marktripy/parsers/__init__.py +2 -0
- marktripy-1.0.3/src/marktripy/parsers/markdown_it.py +352 -0
- marktripy-1.0.3/src/marktripy/parsers/mistletoe_parser.py +349 -0
- marktripy-1.0.3/src/marktripy/renderers/__init__.py +2 -0
- marktripy-1.0.3/src/marktripy/renderers/base.py +214 -0
- marktripy-1.0.3/src/marktripy/renderers/html.py +366 -0
- marktripy-1.0.3/src/marktripy/renderers/markdown.py +446 -0
- marktripy-1.0.3/src/marktripy/transformers/__init__.py +2 -0
- marktripy-1.0.3/src/marktripy/transformers/base.py +226 -0
- marktripy-1.0.3/src/marktripy/transformers/heading.py +217 -0
- marktripy-1.0.3/src/marktripy/transformers/id_generator.py +217 -0
- marktripy-1.0.3/src/marktripy/transformers/link_reference.py +258 -0
- marktripy-1.0.3/src/marktripy/transformers/toc.py +299 -0
- marktripy-1.0.3/src/marktripy/utils/__init__.py +2 -0
- marktripy-1.0.3/src/marktripy/utils/slugify.py +177 -0
- marktripy-1.0.3/tests/__init__.py +2 -0
- marktripy-1.0.3/tests/test_basic.py +254 -0
- marktripy-1.0.3/tests/test_benchmark.py +254 -0
- marktripy-1.0.3/tests/test_gfm_extensions.py +221 -0
- marktripy-1.0.3/tests/test_hypothesis.py +317 -0
- marktripy-1.0.3/tests/test_kbd_extension.py +206 -0
- marktripy-1.0.3/tests/test_roundtrip.py +405 -0
- marktripy-1.0.3/tests/test_transformers.py +424 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
__pypackages__/
|
|
3
|
+
.cache
|
|
4
|
+
.coverage
|
|
5
|
+
.coverage.*
|
|
6
|
+
.dmypy.json
|
|
7
|
+
.DS_Store
|
|
8
|
+
.eggs/
|
|
9
|
+
.env
|
|
10
|
+
.hypothesis/
|
|
11
|
+
.idea/
|
|
12
|
+
.installed.cfg
|
|
13
|
+
.ipynb_checkpoints
|
|
14
|
+
.mypy_cache/
|
|
15
|
+
.nox/
|
|
16
|
+
.pdm-python
|
|
17
|
+
.pdm.toml
|
|
18
|
+
.pybuilder/
|
|
19
|
+
.pyre/
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.Python
|
|
22
|
+
.python-version
|
|
23
|
+
.pytype/
|
|
24
|
+
.ropeproject
|
|
25
|
+
.scrapy
|
|
26
|
+
.spyderproject
|
|
27
|
+
.spyproject
|
|
28
|
+
.tox/
|
|
29
|
+
.venv
|
|
30
|
+
.vscode/
|
|
31
|
+
.webassets-cache
|
|
32
|
+
*.cover
|
|
33
|
+
*.egg
|
|
34
|
+
*.egg-info/
|
|
35
|
+
*.log
|
|
36
|
+
*.manifest
|
|
37
|
+
*.mo
|
|
38
|
+
*.pot
|
|
39
|
+
*.py,cover
|
|
40
|
+
*.py[cod]
|
|
41
|
+
*.sage.py
|
|
42
|
+
*.so
|
|
43
|
+
*.spec
|
|
44
|
+
*$py.class
|
|
45
|
+
/site
|
|
46
|
+
build/
|
|
47
|
+
celerybeat-schedule
|
|
48
|
+
celerybeat.pid
|
|
49
|
+
cover/
|
|
50
|
+
coverage.xml
|
|
51
|
+
cython_debug/
|
|
52
|
+
db.sqlite3
|
|
53
|
+
db.sqlite3-journal
|
|
54
|
+
debug_tokens.py
|
|
55
|
+
develop-eggs/
|
|
56
|
+
dist/
|
|
57
|
+
dmypy.json
|
|
58
|
+
docs/_build/
|
|
59
|
+
downloads/
|
|
60
|
+
eggs/
|
|
61
|
+
env.bak/
|
|
62
|
+
env/
|
|
63
|
+
ENV/
|
|
64
|
+
htmlcov/
|
|
65
|
+
instance/
|
|
66
|
+
ipython_config.py
|
|
67
|
+
lib/
|
|
68
|
+
lib64/
|
|
69
|
+
llms.txt
|
|
70
|
+
local_settings.py
|
|
71
|
+
MANIFEST
|
|
72
|
+
nosetests.xml
|
|
73
|
+
parts/
|
|
74
|
+
pdm.lock
|
|
75
|
+
pip-delete-this-directory.txt
|
|
76
|
+
pip-log.txt
|
|
77
|
+
Pipfile.lock
|
|
78
|
+
poetry.lock
|
|
79
|
+
profile_default/
|
|
80
|
+
sdist/
|
|
81
|
+
share/python-wheels/
|
|
82
|
+
target/
|
|
83
|
+
uv.lock
|
|
84
|
+
var/
|
|
85
|
+
venv.bak/
|
|
86
|
+
venv/
|
|
87
|
+
wheels/
|
|
88
|
+
|
|
89
|
+
# Generated version file
|
|
90
|
+
src/marktripy/_version.py
|
marktripy-1.0.3/PKG-INFO
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: marktripy
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: A Python package for converting Markdown to AST and back to Markdown
|
|
5
|
+
Project-URL: Homepage, https://github.com/twardoch/marktripy
|
|
6
|
+
Project-URL: Repository, https://github.com/twardoch/marktripy
|
|
7
|
+
Project-URL: Issues, https://github.com/twardoch/marktripy/issues
|
|
8
|
+
Author-email: Adam <adam@example.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: ast,commonmark,converter,markdown,parser
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Requires-Dist: fire>=0.7.0
|
|
19
|
+
Requires-Dist: loguru>=0.7.3
|
|
20
|
+
Requires-Dist: markdown-it-py[linkify,plugins]>=3.0.0
|
|
21
|
+
Requires-Dist: mistletoe>=1.4.0
|
|
22
|
+
Requires-Dist: rich>=14.1.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# marktripy
|
|
26
|
+
|
|
27
|
+
**TL;DR**: A Python package for parsing Markdown to AST, manipulating the tree structure, and serializing back to Markdown while preserving formatting. Built on `markdown-it-py` and `mistletoe` for maximum flexibility.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from marktripy import parse_markdown, render_markdown
|
|
31
|
+
|
|
32
|
+
# Parse Markdown to AST
|
|
33
|
+
ast = parse_markdown("# Hello\n\nThis is **bold** text.")
|
|
34
|
+
|
|
35
|
+
# Manipulate AST (e.g., downgrade headings)
|
|
36
|
+
for node in ast.walk():
|
|
37
|
+
if node.type == "heading":
|
|
38
|
+
node.level += 1
|
|
39
|
+
|
|
40
|
+
# Render back to Markdown
|
|
41
|
+
markdown = render_markdown(ast)
|
|
42
|
+
# Output: "## Hello\n\nThis is **bold** text."
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Using pip
|
|
49
|
+
pip install marktripy
|
|
50
|
+
|
|
51
|
+
# Using uv (recommended)
|
|
52
|
+
uv add marktripy
|
|
53
|
+
|
|
54
|
+
# Development installation
|
|
55
|
+
git clone https://github.com/yourusername/marktripy
|
|
56
|
+
cd marktripy
|
|
57
|
+
uv sync --dev
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quick Usage
|
|
61
|
+
|
|
62
|
+
### Basic Markdown to HTML
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from marktripy import markdown_to_html
|
|
66
|
+
|
|
67
|
+
html = markdown_to_html("# Hello World\n\nThis is **bold** and *italic*.")
|
|
68
|
+
# <h1>Hello World</h1><p>This is <strong>bold</strong> and <em>italic</em>.</p>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### AST Manipulation
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from marktripy import parse_markdown, render_markdown
|
|
75
|
+
|
|
76
|
+
# Parse Markdown to AST
|
|
77
|
+
ast = parse_markdown("""
|
|
78
|
+
# Main Title
|
|
79
|
+
## Section 1
|
|
80
|
+
Some content here.
|
|
81
|
+
## Section 2
|
|
82
|
+
More content.
|
|
83
|
+
""")
|
|
84
|
+
|
|
85
|
+
# Add IDs to all headings
|
|
86
|
+
for node in ast.walk():
|
|
87
|
+
if node.type == "heading":
|
|
88
|
+
# Generate ID from heading text
|
|
89
|
+
text = node.get_text().lower().replace(" ", "-")
|
|
90
|
+
node.attrs["id"] = text
|
|
91
|
+
|
|
92
|
+
# Downgrade all headings by one level
|
|
93
|
+
for node in ast.walk():
|
|
94
|
+
if node.type == "heading" and node.level < 6:
|
|
95
|
+
node.level += 1
|
|
96
|
+
|
|
97
|
+
# Render back to Markdown
|
|
98
|
+
result = render_markdown(ast)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Custom Syntax Extensions
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from marktripy import create_extension, Parser
|
|
105
|
+
|
|
106
|
+
# Create a custom extension for ++text++ → <kbd>text</kbd>
|
|
107
|
+
kbd_extension = create_extension(
|
|
108
|
+
pattern=r'\+\+([^+]+)\+\+',
|
|
109
|
+
node_type='kbd',
|
|
110
|
+
html_tag='kbd'
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Use parser with extension
|
|
114
|
+
parser = Parser(extensions=[kbd_extension])
|
|
115
|
+
ast = parser.parse("Press ++Ctrl+C++ to copy")
|
|
116
|
+
html = parser.render_html(ast)
|
|
117
|
+
# Output: Press <kbd>Ctrl+C</kbd> to copy
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### CLI Usage
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Convert Markdown to HTML
|
|
124
|
+
marktripy convert input.md -o output.html
|
|
125
|
+
|
|
126
|
+
# Parse and manipulate Markdown
|
|
127
|
+
marktripy transform input.md --downgrade-headings --add-ids -o output.md
|
|
128
|
+
|
|
129
|
+
# Validate Markdown structure
|
|
130
|
+
marktripy validate document.md --check-links --check-headings
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## The Backstory
|
|
134
|
+
|
|
135
|
+
### Why Another Markdown Parser?
|
|
136
|
+
|
|
137
|
+
The Python ecosystem has numerous Markdown parsers, each with different strengths:
|
|
138
|
+
|
|
139
|
+
- **`markdown`**: The original, extensible but with a complex API
|
|
140
|
+
- **`markdown2`**: Faster alternative but less extensible
|
|
141
|
+
- **`mistune`**: Fast and supports AST, but limited round-trip capability
|
|
142
|
+
- **`marko`**: Good AST support but newer with less ecosystem
|
|
143
|
+
- **`markdown-it-py`**: Port of markdown-it with excellent plugin system
|
|
144
|
+
|
|
145
|
+
After extensive research (see `/ref` directory), I found that no single library perfectly addressed the need for:
|
|
146
|
+
|
|
147
|
+
1. **Clean AST manipulation** - Easy traversal and modification of document structure
|
|
148
|
+
2. **Round-trip conversion** - Parse Markdown → AST → Markdown without losing formatting
|
|
149
|
+
3. **Extensibility** - Simple API for adding custom syntax
|
|
150
|
+
4. **Performance** - Fast enough for real-world documents
|
|
151
|
+
5. **Standards compliance** - CommonMark compliant with GFM extensions
|
|
152
|
+
|
|
153
|
+
### The Research Journey
|
|
154
|
+
|
|
155
|
+
The `/ref` directory contains comprehensive research comparing 8+ Python Markdown libraries across multiple dimensions:
|
|
156
|
+
|
|
157
|
+
- **ref1.md**: Practical guide to advanced Markdown processing in Python
|
|
158
|
+
- **ref2.md**: Detailed comparison of parser architectures and extension mechanisms
|
|
159
|
+
- **ref3.md**: Performance benchmarks and feature matrix
|
|
160
|
+
|
|
161
|
+
Key findings:
|
|
162
|
+
|
|
163
|
+
- `markdown-it-py` offers the best plugin architecture
|
|
164
|
+
- `mistletoe` has the cleanest AST representation
|
|
165
|
+
- `marko` provides good round-trip capabilities
|
|
166
|
+
- Performance varies by 10-100x between libraries
|
|
167
|
+
|
|
168
|
+
### Design Philosophy
|
|
169
|
+
|
|
170
|
+
`marktripy` combines the best ideas from existing libraries:
|
|
171
|
+
|
|
172
|
+
1. **Dual-parser architecture**: Use `markdown-it-py` for extensibility and `mistletoe` for AST manipulation
|
|
173
|
+
2. **Unified AST format**: Convert between parser representations transparently
|
|
174
|
+
3. **Preserving formatting**: Track source positions and whitespace for faithful round-trips
|
|
175
|
+
4. **Plugin-first design**: Everything beyond core CommonMark is a plugin
|
|
176
|
+
5. **Type safety**: Full type hints with `mypy --strict` compatibility
|
|
177
|
+
|
|
178
|
+
## Technical Architecture
|
|
179
|
+
|
|
180
|
+
### Core Components
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
marktripy/
|
|
184
|
+
├── ast.py # Unified AST node definitions
|
|
185
|
+
├── parser.py # Parser abstraction layer
|
|
186
|
+
├── renderer.py # Markdown/HTML renderers
|
|
187
|
+
├── extensions/ # Built-in extensions
|
|
188
|
+
│ ├── gfm.py # GitHub Flavored Markdown
|
|
189
|
+
│ ├── toc.py # Table of contents generator
|
|
190
|
+
│ └── ...
|
|
191
|
+
├── transformers/ # AST transformation utilities
|
|
192
|
+
│ ├── headings.py # Heading manipulation
|
|
193
|
+
│ ├── links.py # Link processing
|
|
194
|
+
│ └── ...
|
|
195
|
+
└── cli.py # Command-line interface
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### AST Structure
|
|
199
|
+
|
|
200
|
+
The AST uses a unified node structure compatible with both parsers:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
class ASTNode:
|
|
204
|
+
type: str # Node type (heading, paragraph, etc.)
|
|
205
|
+
children: List[ASTNode]
|
|
206
|
+
attrs: Dict[str, Any] # Attributes (id, class, etc.)
|
|
207
|
+
content: str # Text content for leaf nodes
|
|
208
|
+
meta: Dict[str, Any] # Source mapping, parser-specific data
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Parser Architecture
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
# Abstraction layer over multiple parsers
|
|
215
|
+
class Parser:
|
|
216
|
+
def __init__(self, parser_backend="markdown-it-py", extensions=None):
|
|
217
|
+
self.backend = self._create_backend(parser_backend)
|
|
218
|
+
self.extensions = extensions or []
|
|
219
|
+
|
|
220
|
+
def parse(self, markdown: str) -> ASTNode:
|
|
221
|
+
# Parse with backend
|
|
222
|
+
backend_ast = self.backend.parse(markdown)
|
|
223
|
+
# Convert to unified AST
|
|
224
|
+
return self._normalize_ast(backend_ast)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Extension System
|
|
228
|
+
|
|
229
|
+
Extensions can hook into multiple stages:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
class Extension:
|
|
233
|
+
def extend_parser(self, parser): ... # Modify parser rules
|
|
234
|
+
def transform_ast(self, ast): ... # Post-process AST
|
|
235
|
+
def extend_renderer(self, renderer): ... # Custom rendering
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Rendering Pipeline
|
|
239
|
+
|
|
240
|
+
1. **AST → Markdown**: Preserves formatting, handles custom nodes
|
|
241
|
+
2. **AST → HTML**: Configurable sanitization, custom handlers
|
|
242
|
+
3. **AST → JSON**: Serialization for processing pipelines
|
|
243
|
+
|
|
244
|
+
### Performance Optimizations
|
|
245
|
+
|
|
246
|
+
- Lazy parsing for large documents
|
|
247
|
+
- Streaming renderers for memory efficiency
|
|
248
|
+
- Optional C extensions via `umarkdown` backend
|
|
249
|
+
- Caching for repeated transformations
|
|
250
|
+
|
|
251
|
+
## Advanced Usage
|
|
252
|
+
|
|
253
|
+
### Custom Transformers
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from marktripy import Transformer
|
|
257
|
+
|
|
258
|
+
class HeaderAnchorTransformer(Transformer):
|
|
259
|
+
"""Add GitHub-style anchor links to headers"""
|
|
260
|
+
|
|
261
|
+
def transform(self, ast):
|
|
262
|
+
for node in ast.walk():
|
|
263
|
+
if node.type == "heading":
|
|
264
|
+
anchor = self.create_anchor(node)
|
|
265
|
+
node.children.insert(0, anchor)
|
|
266
|
+
return ast
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Parser Backends
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
# Use different backends for different needs
|
|
273
|
+
from marktripy import Parser
|
|
274
|
+
|
|
275
|
+
# Maximum compatibility
|
|
276
|
+
parser = Parser(backend="markdown")
|
|
277
|
+
|
|
278
|
+
# Best performance
|
|
279
|
+
parser = Parser(backend="mistletoe")
|
|
280
|
+
|
|
281
|
+
# Most extensions
|
|
282
|
+
parser = Parser(backend="markdown-it-py")
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Integration Examples
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
# Pelican static site generator
|
|
289
|
+
from marktripy import PelicanReader
|
|
290
|
+
|
|
291
|
+
# MkDocs documentation
|
|
292
|
+
from marktripy import MkDocsPlugin
|
|
293
|
+
|
|
294
|
+
# Jupyter notebook processing
|
|
295
|
+
from marktripy import MarkdownCell
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Contributing
|
|
299
|
+
|
|
300
|
+
We welcome contributions! Key areas:
|
|
301
|
+
|
|
302
|
+
- Additional extensions (math, diagrams, etc.)
|
|
303
|
+
- Performance improvements
|
|
304
|
+
- Better round-trip fidelity
|
|
305
|
+
- More transformer utilities
|
|
306
|
+
|
|
307
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
308
|
+
|
|
309
|
+
## License
|
|
310
|
+
|
|
311
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
312
|
+
|
|
313
|
+
## Acknowledgments
|
|
314
|
+
|
|
315
|
+
Built on the shoulders of giants:
|
|
316
|
+
|
|
317
|
+
- `markdown-it-py` developers for the excellent plugin system
|
|
318
|
+
- `mistletoe` for the clean AST design
|
|
319
|
+
- The CommonMark specification authors
|
|
320
|
+
- All researchers of the Python Markdown ecosystem
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# marktripy
|
|
2
|
+
|
|
3
|
+
**TL;DR**: A Python package for parsing Markdown to AST, manipulating the tree structure, and serializing back to Markdown while preserving formatting. Built on `markdown-it-py` and `mistletoe` for maximum flexibility.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from marktripy import parse_markdown, render_markdown
|
|
7
|
+
|
|
8
|
+
# Parse Markdown to AST
|
|
9
|
+
ast = parse_markdown("# Hello\n\nThis is **bold** text.")
|
|
10
|
+
|
|
11
|
+
# Manipulate AST (e.g., downgrade headings)
|
|
12
|
+
for node in ast.walk():
|
|
13
|
+
if node.type == "heading":
|
|
14
|
+
node.level += 1
|
|
15
|
+
|
|
16
|
+
# Render back to Markdown
|
|
17
|
+
markdown = render_markdown(ast)
|
|
18
|
+
# Output: "## Hello\n\nThis is **bold** text."
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Using pip
|
|
25
|
+
pip install marktripy
|
|
26
|
+
|
|
27
|
+
# Using uv (recommended)
|
|
28
|
+
uv add marktripy
|
|
29
|
+
|
|
30
|
+
# Development installation
|
|
31
|
+
git clone https://github.com/yourusername/marktripy
|
|
32
|
+
cd marktripy
|
|
33
|
+
uv sync --dev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Usage
|
|
37
|
+
|
|
38
|
+
### Basic Markdown to HTML
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from marktripy import markdown_to_html
|
|
42
|
+
|
|
43
|
+
html = markdown_to_html("# Hello World\n\nThis is **bold** and *italic*.")
|
|
44
|
+
# <h1>Hello World</h1><p>This is <strong>bold</strong> and <em>italic</em>.</p>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### AST Manipulation
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from marktripy import parse_markdown, render_markdown
|
|
51
|
+
|
|
52
|
+
# Parse Markdown to AST
|
|
53
|
+
ast = parse_markdown("""
|
|
54
|
+
# Main Title
|
|
55
|
+
## Section 1
|
|
56
|
+
Some content here.
|
|
57
|
+
## Section 2
|
|
58
|
+
More content.
|
|
59
|
+
""")
|
|
60
|
+
|
|
61
|
+
# Add IDs to all headings
|
|
62
|
+
for node in ast.walk():
|
|
63
|
+
if node.type == "heading":
|
|
64
|
+
# Generate ID from heading text
|
|
65
|
+
text = node.get_text().lower().replace(" ", "-")
|
|
66
|
+
node.attrs["id"] = text
|
|
67
|
+
|
|
68
|
+
# Downgrade all headings by one level
|
|
69
|
+
for node in ast.walk():
|
|
70
|
+
if node.type == "heading" and node.level < 6:
|
|
71
|
+
node.level += 1
|
|
72
|
+
|
|
73
|
+
# Render back to Markdown
|
|
74
|
+
result = render_markdown(ast)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Custom Syntax Extensions
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from marktripy import create_extension, Parser
|
|
81
|
+
|
|
82
|
+
# Create a custom extension for ++text++ → <kbd>text</kbd>
|
|
83
|
+
kbd_extension = create_extension(
|
|
84
|
+
pattern=r'\+\+([^+]+)\+\+',
|
|
85
|
+
node_type='kbd',
|
|
86
|
+
html_tag='kbd'
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Use parser with extension
|
|
90
|
+
parser = Parser(extensions=[kbd_extension])
|
|
91
|
+
ast = parser.parse("Press ++Ctrl+C++ to copy")
|
|
92
|
+
html = parser.render_html(ast)
|
|
93
|
+
# Output: Press <kbd>Ctrl+C</kbd> to copy
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### CLI Usage
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Convert Markdown to HTML
|
|
100
|
+
marktripy convert input.md -o output.html
|
|
101
|
+
|
|
102
|
+
# Parse and manipulate Markdown
|
|
103
|
+
marktripy transform input.md --downgrade-headings --add-ids -o output.md
|
|
104
|
+
|
|
105
|
+
# Validate Markdown structure
|
|
106
|
+
marktripy validate document.md --check-links --check-headings
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## The Backstory
|
|
110
|
+
|
|
111
|
+
### Why Another Markdown Parser?
|
|
112
|
+
|
|
113
|
+
The Python ecosystem has numerous Markdown parsers, each with different strengths:
|
|
114
|
+
|
|
115
|
+
- **`markdown`**: The original, extensible but with a complex API
|
|
116
|
+
- **`markdown2`**: Faster alternative but less extensible
|
|
117
|
+
- **`mistune`**: Fast and supports AST, but limited round-trip capability
|
|
118
|
+
- **`marko`**: Good AST support but newer with less ecosystem
|
|
119
|
+
- **`markdown-it-py`**: Port of markdown-it with excellent plugin system
|
|
120
|
+
|
|
121
|
+
After extensive research (see `/ref` directory), I found that no single library perfectly addressed the need for:
|
|
122
|
+
|
|
123
|
+
1. **Clean AST manipulation** - Easy traversal and modification of document structure
|
|
124
|
+
2. **Round-trip conversion** - Parse Markdown → AST → Markdown without losing formatting
|
|
125
|
+
3. **Extensibility** - Simple API for adding custom syntax
|
|
126
|
+
4. **Performance** - Fast enough for real-world documents
|
|
127
|
+
5. **Standards compliance** - CommonMark compliant with GFM extensions
|
|
128
|
+
|
|
129
|
+
### The Research Journey
|
|
130
|
+
|
|
131
|
+
The `/ref` directory contains comprehensive research comparing 8+ Python Markdown libraries across multiple dimensions:
|
|
132
|
+
|
|
133
|
+
- **ref1.md**: Practical guide to advanced Markdown processing in Python
|
|
134
|
+
- **ref2.md**: Detailed comparison of parser architectures and extension mechanisms
|
|
135
|
+
- **ref3.md**: Performance benchmarks and feature matrix
|
|
136
|
+
|
|
137
|
+
Key findings:
|
|
138
|
+
|
|
139
|
+
- `markdown-it-py` offers the best plugin architecture
|
|
140
|
+
- `mistletoe` has the cleanest AST representation
|
|
141
|
+
- `marko` provides good round-trip capabilities
|
|
142
|
+
- Performance varies by 10-100x between libraries
|
|
143
|
+
|
|
144
|
+
### Design Philosophy
|
|
145
|
+
|
|
146
|
+
`marktripy` combines the best ideas from existing libraries:
|
|
147
|
+
|
|
148
|
+
1. **Dual-parser architecture**: Use `markdown-it-py` for extensibility and `mistletoe` for AST manipulation
|
|
149
|
+
2. **Unified AST format**: Convert between parser representations transparently
|
|
150
|
+
3. **Preserving formatting**: Track source positions and whitespace for faithful round-trips
|
|
151
|
+
4. **Plugin-first design**: Everything beyond core CommonMark is a plugin
|
|
152
|
+
5. **Type safety**: Full type hints with `mypy --strict` compatibility
|
|
153
|
+
|
|
154
|
+
## Technical Architecture
|
|
155
|
+
|
|
156
|
+
### Core Components
|
|
157
|
+
|
|
158
|
+
```text
|
|
159
|
+
marktripy/
|
|
160
|
+
├── ast.py # Unified AST node definitions
|
|
161
|
+
├── parser.py # Parser abstraction layer
|
|
162
|
+
├── renderer.py # Markdown/HTML renderers
|
|
163
|
+
├── extensions/ # Built-in extensions
|
|
164
|
+
│ ├── gfm.py # GitHub Flavored Markdown
|
|
165
|
+
│ ├── toc.py # Table of contents generator
|
|
166
|
+
│ └── ...
|
|
167
|
+
├── transformers/ # AST transformation utilities
|
|
168
|
+
│ ├── headings.py # Heading manipulation
|
|
169
|
+
│ ├── links.py # Link processing
|
|
170
|
+
│ └── ...
|
|
171
|
+
└── cli.py # Command-line interface
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### AST Structure
|
|
175
|
+
|
|
176
|
+
The AST uses a unified node structure compatible with both parsers:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
class ASTNode:
|
|
180
|
+
type: str # Node type (heading, paragraph, etc.)
|
|
181
|
+
children: List[ASTNode]
|
|
182
|
+
attrs: Dict[str, Any] # Attributes (id, class, etc.)
|
|
183
|
+
content: str # Text content for leaf nodes
|
|
184
|
+
meta: Dict[str, Any] # Source mapping, parser-specific data
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Parser Architecture
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
# Abstraction layer over multiple parsers
|
|
191
|
+
class Parser:
|
|
192
|
+
def __init__(self, parser_backend="markdown-it-py", extensions=None):
|
|
193
|
+
self.backend = self._create_backend(parser_backend)
|
|
194
|
+
self.extensions = extensions or []
|
|
195
|
+
|
|
196
|
+
def parse(self, markdown: str) -> ASTNode:
|
|
197
|
+
# Parse with backend
|
|
198
|
+
backend_ast = self.backend.parse(markdown)
|
|
199
|
+
# Convert to unified AST
|
|
200
|
+
return self._normalize_ast(backend_ast)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Extension System
|
|
204
|
+
|
|
205
|
+
Extensions can hook into multiple stages:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
class Extension:
|
|
209
|
+
def extend_parser(self, parser): ... # Modify parser rules
|
|
210
|
+
def transform_ast(self, ast): ... # Post-process AST
|
|
211
|
+
def extend_renderer(self, renderer): ... # Custom rendering
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Rendering Pipeline
|
|
215
|
+
|
|
216
|
+
1. **AST → Markdown**: Preserves formatting, handles custom nodes
|
|
217
|
+
2. **AST → HTML**: Configurable sanitization, custom handlers
|
|
218
|
+
3. **AST → JSON**: Serialization for processing pipelines
|
|
219
|
+
|
|
220
|
+
### Performance Optimizations
|
|
221
|
+
|
|
222
|
+
- Lazy parsing for large documents
|
|
223
|
+
- Streaming renderers for memory efficiency
|
|
224
|
+
- Optional C extensions via `umarkdown` backend
|
|
225
|
+
- Caching for repeated transformations
|
|
226
|
+
|
|
227
|
+
## Advanced Usage
|
|
228
|
+
|
|
229
|
+
### Custom Transformers
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from marktripy import Transformer
|
|
233
|
+
|
|
234
|
+
class HeaderAnchorTransformer(Transformer):
|
|
235
|
+
"""Add GitHub-style anchor links to headers"""
|
|
236
|
+
|
|
237
|
+
def transform(self, ast):
|
|
238
|
+
for node in ast.walk():
|
|
239
|
+
if node.type == "heading":
|
|
240
|
+
anchor = self.create_anchor(node)
|
|
241
|
+
node.children.insert(0, anchor)
|
|
242
|
+
return ast
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Parser Backends
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
# Use different backends for different needs
|
|
249
|
+
from marktripy import Parser
|
|
250
|
+
|
|
251
|
+
# Maximum compatibility
|
|
252
|
+
parser = Parser(backend="markdown")
|
|
253
|
+
|
|
254
|
+
# Best performance
|
|
255
|
+
parser = Parser(backend="mistletoe")
|
|
256
|
+
|
|
257
|
+
# Most extensions
|
|
258
|
+
parser = Parser(backend="markdown-it-py")
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Integration Examples
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
# Pelican static site generator
|
|
265
|
+
from marktripy import PelicanReader
|
|
266
|
+
|
|
267
|
+
# MkDocs documentation
|
|
268
|
+
from marktripy import MkDocsPlugin
|
|
269
|
+
|
|
270
|
+
# Jupyter notebook processing
|
|
271
|
+
from marktripy import MarkdownCell
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Contributing
|
|
275
|
+
|
|
276
|
+
We welcome contributions! Key areas:
|
|
277
|
+
|
|
278
|
+
- Additional extensions (math, diagrams, etc.)
|
|
279
|
+
- Performance improvements
|
|
280
|
+
- Better round-trip fidelity
|
|
281
|
+
- More transformer utilities
|
|
282
|
+
|
|
283
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
288
|
+
|
|
289
|
+
## Acknowledgments
|
|
290
|
+
|
|
291
|
+
Built on the shoulders of giants:
|
|
292
|
+
|
|
293
|
+
- `markdown-it-py` developers for the excellent plugin system
|
|
294
|
+
- `mistletoe` for the clean AST design
|
|
295
|
+
- The CommonMark specification authors
|
|
296
|
+
- All researchers of the Python Markdown ecosystem
|