md2typst 0.1.0__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,215 @@
1
+ Metadata-Version: 2.3
2
+ Name: md2typst
3
+ Version: 0.1.0
4
+ Summary: Markdown to Typst converter with multiple parser support
5
+ Author: Stefane Fermigier
6
+ Author-email: Stefane Fermigier <sf@abilian.com>
7
+ Requires-Dist: markdown-it-py>=3.0.0
8
+ Requires-Dist: click>=8.0.0
9
+ Requires-Dist: mistune>=3.0.0
10
+ Requires-Dist: marko>=2.0.0
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+
14
+ # md2typst
15
+
16
+ A robust Markdown to [Typst](https://typst.app/) converter in Python with support for multiple Markdown parsers.
17
+
18
+ ## Features
19
+
20
+ - **Multiple parser backends**: Choose from markdown-it-py, mistune, or marko at runtime
21
+ - **GFM support**: Tables, strikethrough, and other GitHub Flavored Markdown extensions
22
+ - **Configurable**: TOML configuration files and CLI options
23
+ - **Extensible**: Plugin support for parser-specific extensions
24
+ - **Well-tested**: Comprehensive test suite with TCK validation against CommonMark
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ # Using pip
30
+ pip install md2typst
31
+
32
+ # Using uv
33
+ uv add md2typst
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ### Command Line
39
+
40
+ ```bash
41
+ # Convert a file
42
+ md2typst input.md -o output.typ
43
+
44
+ # Convert from stdin
45
+ echo "# Hello **World**" | md2typst
46
+
47
+ # Use a specific parser
48
+ md2typst --parser mistune input.md
49
+
50
+ # List available parsers
51
+ md2typst --list-parsers
52
+ ```
53
+
54
+ ### Python API
55
+
56
+ ```python
57
+ from md2typst import convert
58
+
59
+ # Simple conversion
60
+ typst = convert("# Hello **World**")
61
+ print(typst)
62
+ # Output: = Hello *World*
63
+
64
+ # With specific parser
65
+ typst = convert("~~deleted~~", parser="mistune")
66
+ print(typst)
67
+ # Output: #strike[deleted]
68
+
69
+ # With configuration
70
+ from md2typst import convert_with_config
71
+ from md2typst.config import Config
72
+
73
+ config = Config(parser="marko", plugins=["gfm"])
74
+ typst = convert_with_config("| A | B |\n|---|---|\n| 1 | 2 |", config)
75
+ ```
76
+
77
+ ## Supported Parsers
78
+
79
+ | Parser | CLI Name | Description |
80
+ |--------|----------|-------------|
81
+ | [markdown-it-py](https://github.com/executablebooks/markdown-it-py) | `markdown-it` | Default. CommonMark compliant, extensible |
82
+ | [mistune](https://github.com/lepture/mistune) | `mistune` | Fast, pure Python |
83
+ | [marko](https://github.com/frostming/marko) | `marko` | CommonMark compliant, extensible |
84
+
85
+ All parsers have GFM extensions (tables, strikethrough) enabled by default.
86
+
87
+ ## Markdown to Typst Mapping
88
+
89
+ | Markdown | Typst |
90
+ |----------|-------|
91
+ | `# Heading` | `= Heading` |
92
+ | `## Heading 2` | `== Heading 2` |
93
+ | `*italic*` | `_italic_` |
94
+ | `**bold**` | `*bold*` |
95
+ | `~~strike~~` | `#strike[strike]` |
96
+ | `` `code` `` | `` `code` `` |
97
+ | `[text](url)` | `#link("url")[text]` |
98
+ | `![alt](url)` | `#image("url", alt: "alt")` |
99
+ | `> quote` | `#block(...)[quote]` |
100
+ | `---` | `#line(length: 100%)` |
101
+ | GFM tables | `#table(...)` |
102
+
103
+ ## Configuration
104
+
105
+ Configuration is loaded from multiple sources (highest priority first):
106
+
107
+ 1. CLI arguments (`--parser`, `--plugin`)
108
+ 2. Explicit config file (`--config path/to/config.toml`)
109
+ 3. `.md2typst.toml` in the current or parent directories
110
+ 4. `[tool.md2typst]` section in `pyproject.toml`
111
+
112
+ ### Example Configuration
113
+
114
+ **.md2typst.toml**:
115
+ ```toml
116
+ parser = "mistune"
117
+ plugins = ["strikethrough", "table"]
118
+
119
+ [parser_options]
120
+ html = true
121
+ ```
122
+
123
+ **pyproject.toml**:
124
+ ```toml
125
+ [tool.md2typst]
126
+ parser = "markdown-it"
127
+ plugins = ["gfm"]
128
+ ```
129
+
130
+ ### CLI Options
131
+
132
+ ```bash
133
+ md2typst --help
134
+
135
+ Options:
136
+ -o, --output FILE Output file (default: stdout)
137
+ -p, --parser NAME Parser to use (markdown-it, mistune, marko)
138
+ --plugin NAME Load parser plugin (can be repeated)
139
+ --config FILE Path to configuration file
140
+ --list-parsers List available parsers
141
+ --show-config Show effective configuration
142
+ ```
143
+
144
+ ## Development
145
+
146
+ ### Setup
147
+
148
+ ```bash
149
+ git clone https://github.com/user/md2typst.git
150
+ cd md2typst
151
+ uv sync
152
+ ```
153
+
154
+ ### Running Tests
155
+
156
+ ```bash
157
+ # Run all tests (benchmarks skipped by default)
158
+ uv run pytest
159
+
160
+ # Run by category
161
+ uv run pytest -m unit # Unit tests (fast)
162
+ uv run pytest -m integration # Integration tests
163
+ uv run pytest -m e2e # End-to-end tests
164
+ uv run pytest -m benchmark # Benchmark tests
165
+ ```
166
+
167
+ ### Test Structure
168
+
169
+ ```
170
+ tests/
171
+ ├── a_unit/ # Unit tests (AST, generator)
172
+ ├── b_integration/ # Integration tests (parsers, config, TCK)
173
+ ├── c_e2e/ # End-to-end tests
174
+ ├── d_benchmark/ # Performance benchmarks
175
+ └── fixtures/ # Test fixtures (CommonMark, GFM)
176
+ ```
177
+
178
+ ### Code Quality
179
+
180
+ ```bash
181
+ # Type checking
182
+ uv run mypy src/
183
+
184
+ # Linting
185
+ uv run ruff check src/
186
+
187
+ # Formatting
188
+ uv run ruff format src/
189
+ ```
190
+
191
+ ## Architecture
192
+
193
+ ```
194
+ Markdown Input → Parser → AST → Generator → Typst Output
195
+ ```
196
+
197
+ The converter uses a parser-agnostic AST (Abstract Syntax Tree) that decouples parsing from code generation. This allows:
198
+
199
+ - Swapping parsers without changing the generator
200
+ - Consistent output regardless of parser choice
201
+ - Easy extension with new parsers
202
+
203
+ ## License
204
+
205
+ MIT
206
+
207
+ ## Contributing
208
+
209
+ Contributions are welcome! Please:
210
+
211
+ 1. Fork the repository
212
+ 2. Create a feature branch
213
+ 3. Add tests for new functionality
214
+ 4. Ensure all tests pass
215
+ 5. Submit a pull request
@@ -0,0 +1,202 @@
1
+ # md2typst
2
+
3
+ A robust Markdown to [Typst](https://typst.app/) converter in Python with support for multiple Markdown parsers.
4
+
5
+ ## Features
6
+
7
+ - **Multiple parser backends**: Choose from markdown-it-py, mistune, or marko at runtime
8
+ - **GFM support**: Tables, strikethrough, and other GitHub Flavored Markdown extensions
9
+ - **Configurable**: TOML configuration files and CLI options
10
+ - **Extensible**: Plugin support for parser-specific extensions
11
+ - **Well-tested**: Comprehensive test suite with TCK validation against CommonMark
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ # Using pip
17
+ pip install md2typst
18
+
19
+ # Using uv
20
+ uv add md2typst
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ### Command Line
26
+
27
+ ```bash
28
+ # Convert a file
29
+ md2typst input.md -o output.typ
30
+
31
+ # Convert from stdin
32
+ echo "# Hello **World**" | md2typst
33
+
34
+ # Use a specific parser
35
+ md2typst --parser mistune input.md
36
+
37
+ # List available parsers
38
+ md2typst --list-parsers
39
+ ```
40
+
41
+ ### Python API
42
+
43
+ ```python
44
+ from md2typst import convert
45
+
46
+ # Simple conversion
47
+ typst = convert("# Hello **World**")
48
+ print(typst)
49
+ # Output: = Hello *World*
50
+
51
+ # With specific parser
52
+ typst = convert("~~deleted~~", parser="mistune")
53
+ print(typst)
54
+ # Output: #strike[deleted]
55
+
56
+ # With configuration
57
+ from md2typst import convert_with_config
58
+ from md2typst.config import Config
59
+
60
+ config = Config(parser="marko", plugins=["gfm"])
61
+ typst = convert_with_config("| A | B |\n|---|---|\n| 1 | 2 |", config)
62
+ ```
63
+
64
+ ## Supported Parsers
65
+
66
+ | Parser | CLI Name | Description |
67
+ |--------|----------|-------------|
68
+ | [markdown-it-py](https://github.com/executablebooks/markdown-it-py) | `markdown-it` | Default. CommonMark compliant, extensible |
69
+ | [mistune](https://github.com/lepture/mistune) | `mistune` | Fast, pure Python |
70
+ | [marko](https://github.com/frostming/marko) | `marko` | CommonMark compliant, extensible |
71
+
72
+ All parsers have GFM extensions (tables, strikethrough) enabled by default.
73
+
74
+ ## Markdown to Typst Mapping
75
+
76
+ | Markdown | Typst |
77
+ |----------|-------|
78
+ | `# Heading` | `= Heading` |
79
+ | `## Heading 2` | `== Heading 2` |
80
+ | `*italic*` | `_italic_` |
81
+ | `**bold**` | `*bold*` |
82
+ | `~~strike~~` | `#strike[strike]` |
83
+ | `` `code` `` | `` `code` `` |
84
+ | `[text](url)` | `#link("url")[text]` |
85
+ | `![alt](url)` | `#image("url", alt: "alt")` |
86
+ | `> quote` | `#block(...)[quote]` |
87
+ | `---` | `#line(length: 100%)` |
88
+ | GFM tables | `#table(...)` |
89
+
90
+ ## Configuration
91
+
92
+ Configuration is loaded from multiple sources (highest priority first):
93
+
94
+ 1. CLI arguments (`--parser`, `--plugin`)
95
+ 2. Explicit config file (`--config path/to/config.toml`)
96
+ 3. `.md2typst.toml` in the current or parent directories
97
+ 4. `[tool.md2typst]` section in `pyproject.toml`
98
+
99
+ ### Example Configuration
100
+
101
+ **.md2typst.toml**:
102
+ ```toml
103
+ parser = "mistune"
104
+ plugins = ["strikethrough", "table"]
105
+
106
+ [parser_options]
107
+ html = true
108
+ ```
109
+
110
+ **pyproject.toml**:
111
+ ```toml
112
+ [tool.md2typst]
113
+ parser = "markdown-it"
114
+ plugins = ["gfm"]
115
+ ```
116
+
117
+ ### CLI Options
118
+
119
+ ```bash
120
+ md2typst --help
121
+
122
+ Options:
123
+ -o, --output FILE Output file (default: stdout)
124
+ -p, --parser NAME Parser to use (markdown-it, mistune, marko)
125
+ --plugin NAME Load parser plugin (can be repeated)
126
+ --config FILE Path to configuration file
127
+ --list-parsers List available parsers
128
+ --show-config Show effective configuration
129
+ ```
130
+
131
+ ## Development
132
+
133
+ ### Setup
134
+
135
+ ```bash
136
+ git clone https://github.com/user/md2typst.git
137
+ cd md2typst
138
+ uv sync
139
+ ```
140
+
141
+ ### Running Tests
142
+
143
+ ```bash
144
+ # Run all tests (benchmarks skipped by default)
145
+ uv run pytest
146
+
147
+ # Run by category
148
+ uv run pytest -m unit # Unit tests (fast)
149
+ uv run pytest -m integration # Integration tests
150
+ uv run pytest -m e2e # End-to-end tests
151
+ uv run pytest -m benchmark # Benchmark tests
152
+ ```
153
+
154
+ ### Test Structure
155
+
156
+ ```
157
+ tests/
158
+ ├── a_unit/ # Unit tests (AST, generator)
159
+ ├── b_integration/ # Integration tests (parsers, config, TCK)
160
+ ├── c_e2e/ # End-to-end tests
161
+ ├── d_benchmark/ # Performance benchmarks
162
+ └── fixtures/ # Test fixtures (CommonMark, GFM)
163
+ ```
164
+
165
+ ### Code Quality
166
+
167
+ ```bash
168
+ # Type checking
169
+ uv run mypy src/
170
+
171
+ # Linting
172
+ uv run ruff check src/
173
+
174
+ # Formatting
175
+ uv run ruff format src/
176
+ ```
177
+
178
+ ## Architecture
179
+
180
+ ```
181
+ Markdown Input → Parser → AST → Generator → Typst Output
182
+ ```
183
+
184
+ The converter uses a parser-agnostic AST (Abstract Syntax Tree) that decouples parsing from code generation. This allows:
185
+
186
+ - Swapping parsers without changing the generator
187
+ - Consistent output regardless of parser choice
188
+ - Easy extension with new parsers
189
+
190
+ ## License
191
+
192
+ MIT
193
+
194
+ ## Contributing
195
+
196
+ Contributions are welcome! Please:
197
+
198
+ 1. Fork the repository
199
+ 2. Create a feature branch
200
+ 3. Add tests for new functionality
201
+ 4. Ensure all tests pass
202
+ 5. Submit a pull request
@@ -0,0 +1,46 @@
1
+ [project]
2
+ name = "md2typst"
3
+ version = "0.1.0"
4
+ description = "Markdown to Typst converter with multiple parser support"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Stefane Fermigier", email = "sf@abilian.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "markdown-it-py>=3.0.0",
12
+ "click>=8.0.0",
13
+ "mistune>=3.0.0",
14
+ "marko>=2.0.0",
15
+ ]
16
+
17
+ [project.scripts]
18
+ md2typst = "md2typst:main"
19
+
20
+ [dependency-groups]
21
+ dev = [
22
+ "pytest>=8.0.0",
23
+ "pytest-cov>=4.0.0",
24
+ "pytest-benchmark>=4.0.0",
25
+ "hypothesis>=6.0.0",
26
+ "pyyaml>=6.0.0",
27
+ "ty>=0.0.8",
28
+ "pyrefly>=0.46.3",
29
+ "mypy>=1.19.1",
30
+ ]
31
+
32
+ [build-system]
33
+ requires = ["uv_build>=0.9.13,<0.10.0"]
34
+ build-backend = "uv_build"
35
+
36
+ [tool.pytest.ini_options]
37
+ testpaths = ["tests"]
38
+ pythonpath = ["src"]
39
+ addopts = "-m 'not benchmark'"
40
+ markers = [
41
+ "unit: Unit tests (fast, isolated)",
42
+ "integration: Integration tests (multiple components)",
43
+ "e2e: End-to-end tests (full workflow)",
44
+ "benchmark: Benchmark tests (slow, run with -m benchmark)",
45
+ "slow: Slow tests",
46
+ ]
@@ -0,0 +1,169 @@
1
+ """md2typst - Markdown to Typst converter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from md2typst.ast import Document
10
+ from md2typst.config import Config, load_config
11
+ from md2typst.generator import TypstGenerator, generate_typst
12
+ from md2typst.parsers import get_parser, list_parsers
13
+
14
+ __version__ = "0.1.0"
15
+
16
+ __all__ = [
17
+ "Config",
18
+ "Document",
19
+ "TypstGenerator",
20
+ "convert",
21
+ "convert_with_config",
22
+ "generate_typst",
23
+ "get_parser",
24
+ "list_parsers",
25
+ "main",
26
+ ]
27
+
28
+
29
+ def convert(
30
+ markdown: str,
31
+ parser: str | None = None,
32
+ parser_options: dict[str, Any] | None = None,
33
+ plugins: list[str] | None = None,
34
+ ) -> str:
35
+ """Convert Markdown text to Typst.
36
+
37
+ Args:
38
+ markdown: The Markdown source text.
39
+ parser: Optional parser name. Uses default if not specified.
40
+ parser_options: Optional parser-specific options.
41
+ plugins: Optional list of parser plugins to load.
42
+
43
+ Returns:
44
+ The generated Typst source code.
45
+ """
46
+ p = get_parser(parser)
47
+
48
+ if parser_options:
49
+ p.configure(parser_options)
50
+
51
+ if plugins:
52
+ for plugin in plugins:
53
+ with contextlib.suppress(NotImplementedError):
54
+ p.load_plugin(plugin)
55
+
56
+ doc = p.parse(markdown)
57
+ return generate_typst(doc)
58
+
59
+
60
+ def convert_with_config(markdown: str, config: Config) -> str:
61
+ """Convert Markdown text to Typst using a Config object.
62
+
63
+ Args:
64
+ markdown: The Markdown source text.
65
+ config: Configuration object.
66
+
67
+ Returns:
68
+ The generated Typst source code.
69
+ """
70
+ return convert(
71
+ markdown,
72
+ parser=config.parser,
73
+ parser_options=config.parser_options,
74
+ plugins=config.plugins,
75
+ )
76
+
77
+
78
+ def main() -> None:
79
+ """CLI entry point."""
80
+ import sys
81
+
82
+ import click
83
+
84
+ @click.command()
85
+ @click.argument("input", type=click.Path(exists=True), required=False)
86
+ @click.option(
87
+ "-o", "--output", type=click.Path(), help="Output file (default: stdout)"
88
+ )
89
+ @click.option("-p", "--parser", default=None, help="Parser to use")
90
+ @click.option(
91
+ "-c",
92
+ "--config",
93
+ "config_file",
94
+ type=click.Path(exists=True),
95
+ help="Config file path",
96
+ )
97
+ @click.option(
98
+ "--plugin", multiple=True, help="Load parser plugin (can be repeated)"
99
+ )
100
+ @click.option("--list-parsers", is_flag=True, help="List available parsers")
101
+ @click.option("--show-config", is_flag=True, help="Show effective configuration")
102
+ @click.version_option(__version__)
103
+ def cli(
104
+ input: str | None,
105
+ output: str | None,
106
+ parser: str | None,
107
+ config_file: str | None,
108
+ plugin: tuple[str, ...],
109
+ list_parsers: bool,
110
+ show_config: bool,
111
+ ) -> None:
112
+ """Convert Markdown to Typst.
113
+
114
+ INPUT is the Markdown file to convert. Use - for stdin.
115
+ """
116
+ if list_parsers:
117
+ from md2typst.parsers import list_parsers as lp
118
+
119
+ click.echo("Available parsers:")
120
+ for name in lp():
121
+ click.echo(f" - {name}")
122
+ return
123
+
124
+ # Determine start directory for config search
125
+ if input and input != "-":
126
+ start_dir = Path(input).parent
127
+ else:
128
+ start_dir = Path.cwd()
129
+
130
+ # Build CLI overrides
131
+ cli_overrides: dict[str, Any] = {}
132
+ if parser:
133
+ cli_overrides["parser"] = parser
134
+ if plugin:
135
+ cli_overrides["plugins"] = list(plugin)
136
+
137
+ # Load configuration
138
+ config = load_config(
139
+ config_file=Path(config_file) if config_file else None,
140
+ start_dir=start_dir,
141
+ cli_overrides=cli_overrides,
142
+ )
143
+
144
+ if show_config:
145
+ click.echo("Effective configuration:")
146
+ click.echo(f" parser: {config.parser}")
147
+ click.echo(f" plugins: {config.plugins}")
148
+ click.echo(f" parser_options: {config.parser_options}")
149
+ click.echo(f" output_options: {config.output_options}")
150
+ return
151
+
152
+ if input is None:
153
+ # Read from stdin
154
+ text = sys.stdin.read()
155
+ elif input == "-":
156
+ text = sys.stdin.read()
157
+ else:
158
+ with Path(input).open() as f:
159
+ text = f.read()
160
+
161
+ result = convert_with_config(text, config)
162
+
163
+ if output:
164
+ with Path(output).open("w") as f:
165
+ f.write(result)
166
+ else:
167
+ click.echo(result)
168
+
169
+ cli()