mystquarto 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.
- mystquarto-0.1.0/.gitignore +7 -0
- mystquarto-0.1.0/LICENSE +21 -0
- mystquarto-0.1.0/PKG-INFO +52 -0
- mystquarto-0.1.0/README.md +32 -0
- mystquarto-0.1.0/pyproject.toml +39 -0
- mystquarto-0.1.0/src/mystquarto/__init__.py +3 -0
- mystquarto-0.1.0/src/mystquarto/cli.py +175 -0
- mystquarto-0.1.0/src/mystquarto/config.py +271 -0
- mystquarto-0.1.0/src/mystquarto/convert.py +399 -0
- mystquarto-0.1.0/src/mystquarto/frontmatter.py +195 -0
- mystquarto-0.1.0/src/mystquarto/scanner.py +258 -0
- mystquarto-0.1.0/src/mystquarto/transforms/__init__.py +1 -0
- mystquarto-0.1.0/src/mystquarto/transforms/myst_to_quarto.py +431 -0
- mystquarto-0.1.0/src/mystquarto/transforms/quarto_to_myst.py +634 -0
- mystquarto-0.1.0/src/mystquarto/warnings.py +77 -0
- mystquarto-0.1.0/tests/conftest.py +101 -0
- mystquarto-0.1.0/tests/fixtures/simple_myst.md +44 -0
- mystquarto-0.1.0/tests/fixtures/simple_quarto.md +32 -0
- mystquarto-0.1.0/tests/fixtures/simple_quarto_expected_myst.md +37 -0
- mystquarto-0.1.0/tests/fixtures/simple_quarto_input.md +30 -0
- mystquarto-0.1.0/tests/test_cli.py +638 -0
- mystquarto-0.1.0/tests/test_config.py +766 -0
- mystquarto-0.1.0/tests/test_directives.py +504 -0
- mystquarto-0.1.0/tests/test_inline.py +249 -0
- mystquarto-0.1.0/tests/test_roundtrip.py +538 -0
mystquarto-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Max Ghenis
|
|
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,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mystquarto
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bidirectional MyST ↔ Quarto converter
|
|
5
|
+
Project-URL: Homepage, https://github.com/MaxGhenis/mystquarto
|
|
6
|
+
Project-URL: Repository, https://github.com/MaxGhenis/mystquarto
|
|
7
|
+
Author: Max Ghenis
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: converter,markdown,myst,quarto
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: click>=8.0
|
|
18
|
+
Requires-Dist: pyyaml>=6.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# mystquarto
|
|
22
|
+
|
|
23
|
+
Bidirectional MyST ↔ Quarto converter.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install mystquarto
|
|
29
|
+
# or
|
|
30
|
+
uvx mystquarto
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Convert MyST → Quarto
|
|
37
|
+
myst2quarto docs/
|
|
38
|
+
mystquarto to-quarto docs/
|
|
39
|
+
|
|
40
|
+
# Convert Quarto → MyST
|
|
41
|
+
quarto2myst docs/
|
|
42
|
+
mystquarto to-myst docs/
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Options
|
|
46
|
+
|
|
47
|
+
- `--output DIR` / `-o DIR`: Output directory (default: converts in-place)
|
|
48
|
+
- `--in-place`: Modify files in-place
|
|
49
|
+
- `--config-only`: Only convert config files (myst.yml ↔ _quarto.yml)
|
|
50
|
+
- `--no-config`: Skip config file conversion
|
|
51
|
+
- `--dry-run`: Show what would be changed without writing
|
|
52
|
+
- `--strict`: Treat warnings as errors
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# mystquarto
|
|
2
|
+
|
|
3
|
+
Bidirectional MyST ↔ Quarto converter.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install mystquarto
|
|
9
|
+
# or
|
|
10
|
+
uvx mystquarto
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Convert MyST → Quarto
|
|
17
|
+
myst2quarto docs/
|
|
18
|
+
mystquarto to-quarto docs/
|
|
19
|
+
|
|
20
|
+
# Convert Quarto → MyST
|
|
21
|
+
quarto2myst docs/
|
|
22
|
+
mystquarto to-myst docs/
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Options
|
|
26
|
+
|
|
27
|
+
- `--output DIR` / `-o DIR`: Output directory (default: converts in-place)
|
|
28
|
+
- `--in-place`: Modify files in-place
|
|
29
|
+
- `--config-only`: Only convert config files (myst.yml ↔ _quarto.yml)
|
|
30
|
+
- `--no-config`: Skip config file conversion
|
|
31
|
+
- `--dry-run`: Show what would be changed without writing
|
|
32
|
+
- `--strict`: Treat warnings as errors
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mystquarto"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Bidirectional MyST ↔ Quarto converter"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Max Ghenis" }]
|
|
13
|
+
keywords = ["myst", "quarto", "markdown", "converter"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Text Processing :: Markup",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"click>=8.0",
|
|
23
|
+
"pyyaml>=6.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
myst2quarto = "mystquarto.cli:myst2quarto"
|
|
28
|
+
quarto2myst = "mystquarto.cli:quarto2myst"
|
|
29
|
+
mystquarto = "mystquarto.cli:main"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/MaxGhenis/mystquarto"
|
|
33
|
+
Repository = "https://github.com/MaxGhenis/mystquarto"
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
line-length = 88
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Click CLI for mystquarto: bidirectional MyST <-> Quarto converter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from mystquarto.convert import Direction, convert_directory
|
|
10
|
+
from mystquarto.warnings import WarningCollector
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _run_conversion(
|
|
14
|
+
path: str,
|
|
15
|
+
output: str | None,
|
|
16
|
+
in_place: bool,
|
|
17
|
+
config_only: bool,
|
|
18
|
+
no_config: bool,
|
|
19
|
+
dry_run: bool,
|
|
20
|
+
strict: bool,
|
|
21
|
+
direction: Direction,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Shared implementation for both conversion directions.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
path: Input file or directory path.
|
|
27
|
+
output: Output directory path (or None).
|
|
28
|
+
in_place: Whether to modify files in-place.
|
|
29
|
+
config_only: Only convert config files.
|
|
30
|
+
no_config: Skip config file conversion.
|
|
31
|
+
dry_run: Show what would change without writing.
|
|
32
|
+
strict: Treat warnings as errors.
|
|
33
|
+
direction: Conversion direction.
|
|
34
|
+
"""
|
|
35
|
+
collector = WarningCollector(strict=strict)
|
|
36
|
+
|
|
37
|
+
results = convert_directory(
|
|
38
|
+
input_dir=path,
|
|
39
|
+
output_dir=output,
|
|
40
|
+
direction=direction,
|
|
41
|
+
in_place=in_place,
|
|
42
|
+
config_only=config_only,
|
|
43
|
+
no_config=no_config,
|
|
44
|
+
dry_run=dry_run,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Collect warnings and errors from results
|
|
48
|
+
for result in results:
|
|
49
|
+
for warning in result.warnings:
|
|
50
|
+
collector.warn(warning)
|
|
51
|
+
for error in result.errors:
|
|
52
|
+
collector.error(error)
|
|
53
|
+
|
|
54
|
+
# Report
|
|
55
|
+
converted_count = sum(
|
|
56
|
+
1 for r in results if not r.skipped and not r.errors
|
|
57
|
+
)
|
|
58
|
+
label = "Would convert" if dry_run else "Converted"
|
|
59
|
+
|
|
60
|
+
if dry_run:
|
|
61
|
+
for result in results:
|
|
62
|
+
if not result.skipped and result.output_path:
|
|
63
|
+
click.echo(f" {result.input_path} -> {result.output_path}")
|
|
64
|
+
|
|
65
|
+
click.echo(f"{label} {converted_count} file(s).")
|
|
66
|
+
|
|
67
|
+
if collector.warnings or collector.errors:
|
|
68
|
+
click.echo(collector.report())
|
|
69
|
+
|
|
70
|
+
if collector.has_errors():
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@click.command()
|
|
75
|
+
@click.argument("path", type=click.Path(exists=True))
|
|
76
|
+
@click.option("--output", "-o", type=click.Path(), help="Output directory")
|
|
77
|
+
@click.option("--in-place", is_flag=True, help="Modify files in-place")
|
|
78
|
+
@click.option("--config-only", is_flag=True, help="Only convert config files")
|
|
79
|
+
@click.option("--no-config", is_flag=True, help="Skip config file conversion")
|
|
80
|
+
@click.option(
|
|
81
|
+
"--dry-run", is_flag=True, help="Show what would change without writing"
|
|
82
|
+
)
|
|
83
|
+
@click.option("--strict", is_flag=True, help="Treat warnings as errors")
|
|
84
|
+
def myst2quarto(path, output, in_place, config_only, no_config, dry_run, strict):
|
|
85
|
+
"""Convert MyST markdown files to Quarto format."""
|
|
86
|
+
_run_conversion(
|
|
87
|
+
path=path,
|
|
88
|
+
output=output,
|
|
89
|
+
in_place=in_place,
|
|
90
|
+
config_only=config_only,
|
|
91
|
+
no_config=no_config,
|
|
92
|
+
dry_run=dry_run,
|
|
93
|
+
strict=strict,
|
|
94
|
+
direction=Direction.MYST_TO_QUARTO,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@click.command()
|
|
99
|
+
@click.argument("path", type=click.Path(exists=True))
|
|
100
|
+
@click.option("--output", "-o", type=click.Path(), help="Output directory")
|
|
101
|
+
@click.option("--in-place", is_flag=True, help="Modify files in-place")
|
|
102
|
+
@click.option("--config-only", is_flag=True, help="Only convert config files")
|
|
103
|
+
@click.option("--no-config", is_flag=True, help="Skip config file conversion")
|
|
104
|
+
@click.option(
|
|
105
|
+
"--dry-run", is_flag=True, help="Show what would change without writing"
|
|
106
|
+
)
|
|
107
|
+
@click.option("--strict", is_flag=True, help="Treat warnings as errors")
|
|
108
|
+
def quarto2myst(path, output, in_place, config_only, no_config, dry_run, strict):
|
|
109
|
+
"""Convert Quarto markdown files to MyST format."""
|
|
110
|
+
_run_conversion(
|
|
111
|
+
path=path,
|
|
112
|
+
output=output,
|
|
113
|
+
in_place=in_place,
|
|
114
|
+
config_only=config_only,
|
|
115
|
+
no_config=no_config,
|
|
116
|
+
dry_run=dry_run,
|
|
117
|
+
strict=strict,
|
|
118
|
+
direction=Direction.QUARTO_TO_MYST,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@click.group(invoke_without_command=True)
|
|
123
|
+
@click.pass_context
|
|
124
|
+
def main(ctx):
|
|
125
|
+
"""Bidirectional MyST <-> Quarto converter."""
|
|
126
|
+
if ctx.invoked_subcommand is None:
|
|
127
|
+
click.echo(ctx.get_help())
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@main.command("to-quarto")
|
|
131
|
+
@click.argument("path", type=click.Path(exists=True))
|
|
132
|
+
@click.option("--output", "-o", type=click.Path(), help="Output directory")
|
|
133
|
+
@click.option("--in-place", is_flag=True, help="Modify files in-place")
|
|
134
|
+
@click.option("--config-only", is_flag=True, help="Only convert config files")
|
|
135
|
+
@click.option("--no-config", is_flag=True, help="Skip config file conversion")
|
|
136
|
+
@click.option(
|
|
137
|
+
"--dry-run", is_flag=True, help="Show what would change without writing"
|
|
138
|
+
)
|
|
139
|
+
@click.option("--strict", is_flag=True, help="Treat warnings as errors")
|
|
140
|
+
def to_quarto(path, output, in_place, config_only, no_config, dry_run, strict):
|
|
141
|
+
"""Convert MyST markdown files to Quarto format."""
|
|
142
|
+
_run_conversion(
|
|
143
|
+
path=path,
|
|
144
|
+
output=output,
|
|
145
|
+
in_place=in_place,
|
|
146
|
+
config_only=config_only,
|
|
147
|
+
no_config=no_config,
|
|
148
|
+
dry_run=dry_run,
|
|
149
|
+
strict=strict,
|
|
150
|
+
direction=Direction.MYST_TO_QUARTO,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@main.command("to-myst")
|
|
155
|
+
@click.argument("path", type=click.Path(exists=True))
|
|
156
|
+
@click.option("--output", "-o", type=click.Path(), help="Output directory")
|
|
157
|
+
@click.option("--in-place", is_flag=True, help="Modify files in-place")
|
|
158
|
+
@click.option("--config-only", is_flag=True, help="Only convert config files")
|
|
159
|
+
@click.option("--no-config", is_flag=True, help="Skip config file conversion")
|
|
160
|
+
@click.option(
|
|
161
|
+
"--dry-run", is_flag=True, help="Show what would change without writing"
|
|
162
|
+
)
|
|
163
|
+
@click.option("--strict", is_flag=True, help="Treat warnings as errors")
|
|
164
|
+
def to_myst(path, output, in_place, config_only, no_config, dry_run, strict):
|
|
165
|
+
"""Convert Quarto markdown files to MyST format."""
|
|
166
|
+
_run_conversion(
|
|
167
|
+
path=path,
|
|
168
|
+
output=output,
|
|
169
|
+
in_place=in_place,
|
|
170
|
+
config_only=config_only,
|
|
171
|
+
no_config=no_config,
|
|
172
|
+
dry_run=dry_run,
|
|
173
|
+
strict=strict,
|
|
174
|
+
direction=Direction.QUARTO_TO_MYST,
|
|
175
|
+
)
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Config conversion: myst.yml <-> _quarto.yml."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _is_book_project(myst_config: dict) -> bool:
|
|
11
|
+
"""Detect if a MyST config represents a book-type project.
|
|
12
|
+
|
|
13
|
+
Book type is indicated by either:
|
|
14
|
+
- site.template == "book-theme"
|
|
15
|
+
- presence of project.toc
|
|
16
|
+
"""
|
|
17
|
+
site = myst_config.get("site", {})
|
|
18
|
+
if site.get("template") == "book-theme":
|
|
19
|
+
return True
|
|
20
|
+
project = myst_config.get("project", {})
|
|
21
|
+
if "toc" in project:
|
|
22
|
+
return True
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _toc_to_chapters(toc: list[dict]) -> list[str]:
|
|
27
|
+
"""Convert MyST toc entries to Quarto chapter file list.
|
|
28
|
+
|
|
29
|
+
Each entry is a dict with a 'file' key (with or without extension).
|
|
30
|
+
Returns list of filenames with .qmd extension.
|
|
31
|
+
"""
|
|
32
|
+
chapters = []
|
|
33
|
+
for entry in toc:
|
|
34
|
+
if isinstance(entry, dict) and "file" in entry:
|
|
35
|
+
name = entry["file"]
|
|
36
|
+
elif isinstance(entry, str):
|
|
37
|
+
name = entry
|
|
38
|
+
else:
|
|
39
|
+
continue
|
|
40
|
+
# Strip existing .md extension before adding .qmd
|
|
41
|
+
if name.endswith(".md"):
|
|
42
|
+
name = name[:-3]
|
|
43
|
+
chapters.append(f"{name}.qmd")
|
|
44
|
+
return chapters
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _chapters_to_toc(chapters: list[str]) -> list[dict]:
|
|
48
|
+
"""Convert Quarto chapter file list to MyST toc entries.
|
|
49
|
+
|
|
50
|
+
Each chapter is a filename (possibly with .qmd extension).
|
|
51
|
+
Returns list of dicts with 'file' key (no extension).
|
|
52
|
+
"""
|
|
53
|
+
toc = []
|
|
54
|
+
for chapter in chapters:
|
|
55
|
+
filename = chapter
|
|
56
|
+
# Strip .qmd or .md extension
|
|
57
|
+
for ext in (".qmd", ".md"):
|
|
58
|
+
if filename.endswith(ext):
|
|
59
|
+
filename = filename[: -len(ext)]
|
|
60
|
+
break
|
|
61
|
+
toc.append({"file": filename})
|
|
62
|
+
return toc
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _convert_authors_myst_to_quarto(authors: list[dict]) -> list[dict]:
|
|
66
|
+
"""Convert MyST author entries to Quarto format.
|
|
67
|
+
|
|
68
|
+
Both use similar structures with name and affiliations.
|
|
69
|
+
"""
|
|
70
|
+
result = []
|
|
71
|
+
for author in authors:
|
|
72
|
+
entry = {}
|
|
73
|
+
if "name" in author:
|
|
74
|
+
entry["name"] = author["name"]
|
|
75
|
+
if "affiliations" in author:
|
|
76
|
+
entry["affiliations"] = author["affiliations"]
|
|
77
|
+
# Pass through any other fields
|
|
78
|
+
for key, value in author.items():
|
|
79
|
+
if key not in ("name", "affiliations"):
|
|
80
|
+
entry[key] = value
|
|
81
|
+
result.append(entry)
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _convert_exports_to_format(exports: list[dict]) -> dict:
|
|
86
|
+
"""Convert MyST exports list to Quarto format block.
|
|
87
|
+
|
|
88
|
+
Each export has a 'format' key (pdf, docx, etc.) and other options.
|
|
89
|
+
"""
|
|
90
|
+
format_block = {}
|
|
91
|
+
for export in exports:
|
|
92
|
+
fmt = export.get("format")
|
|
93
|
+
if not fmt:
|
|
94
|
+
continue
|
|
95
|
+
# Copy all options except 'format' itself
|
|
96
|
+
options = {k: v for k, v in export.items() if k != "format"}
|
|
97
|
+
format_block[fmt] = options if options else {}
|
|
98
|
+
return format_block
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _convert_format_to_exports(format_block: dict) -> list[dict]:
|
|
102
|
+
"""Convert Quarto format block to MyST exports list."""
|
|
103
|
+
exports = []
|
|
104
|
+
for fmt, options in format_block.items():
|
|
105
|
+
export = {"format": fmt}
|
|
106
|
+
if isinstance(options, dict):
|
|
107
|
+
export.update(options)
|
|
108
|
+
exports.append(export)
|
|
109
|
+
return exports
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def myst_to_quarto_config(myst_config: dict) -> dict:
|
|
113
|
+
"""Convert a parsed myst.yml dict to a _quarto.yml dict.
|
|
114
|
+
|
|
115
|
+
Handles both book-type and article/manuscript projects.
|
|
116
|
+
"""
|
|
117
|
+
if not myst_config:
|
|
118
|
+
return {}
|
|
119
|
+
|
|
120
|
+
project = myst_config.get("project", {})
|
|
121
|
+
if not project:
|
|
122
|
+
return {}
|
|
123
|
+
|
|
124
|
+
result = {}
|
|
125
|
+
is_book = _is_book_project(myst_config)
|
|
126
|
+
|
|
127
|
+
if is_book:
|
|
128
|
+
# Book-type project
|
|
129
|
+
result["project"] = {"type": "book"}
|
|
130
|
+
book = {}
|
|
131
|
+
|
|
132
|
+
if "title" in project:
|
|
133
|
+
book["title"] = project["title"]
|
|
134
|
+
if "authors" in project:
|
|
135
|
+
book["author"] = _convert_authors_myst_to_quarto(project["authors"])
|
|
136
|
+
if "toc" in project:
|
|
137
|
+
book["chapters"] = _toc_to_chapters(project["toc"])
|
|
138
|
+
|
|
139
|
+
result["book"] = book
|
|
140
|
+
else:
|
|
141
|
+
# Article/manuscript project
|
|
142
|
+
if "title" in project:
|
|
143
|
+
result["title"] = project["title"]
|
|
144
|
+
if "authors" in project:
|
|
145
|
+
result["author"] = _convert_authors_myst_to_quarto(project["authors"])
|
|
146
|
+
|
|
147
|
+
# Fields that apply to both types
|
|
148
|
+
if "bibliography" in project:
|
|
149
|
+
result["bibliography"] = project["bibliography"]
|
|
150
|
+
|
|
151
|
+
if "exports" in project:
|
|
152
|
+
result["format"] = _convert_exports_to_format(project["exports"])
|
|
153
|
+
|
|
154
|
+
if "github" in project:
|
|
155
|
+
result["repo-url"] = project["github"]
|
|
156
|
+
|
|
157
|
+
if "license" in project:
|
|
158
|
+
result["license"] = project["license"]
|
|
159
|
+
|
|
160
|
+
if "keywords" in project:
|
|
161
|
+
result["keywords"] = project["keywords"]
|
|
162
|
+
|
|
163
|
+
if "date" in project:
|
|
164
|
+
result["date"] = project["date"]
|
|
165
|
+
|
|
166
|
+
if "subject" in project:
|
|
167
|
+
result["description"] = project["subject"]
|
|
168
|
+
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def quarto_to_myst_config(quarto_config: dict) -> dict:
|
|
173
|
+
"""Convert a parsed _quarto.yml dict to a myst.yml dict.
|
|
174
|
+
|
|
175
|
+
Handles both book-type and article/manuscript projects.
|
|
176
|
+
"""
|
|
177
|
+
if not quarto_config:
|
|
178
|
+
return {}
|
|
179
|
+
|
|
180
|
+
result = {}
|
|
181
|
+
project = {}
|
|
182
|
+
is_book = (
|
|
183
|
+
quarto_config.get("project", {}).get("type") == "book"
|
|
184
|
+
or "book" in quarto_config
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if is_book:
|
|
188
|
+
book = quarto_config.get("book", {})
|
|
189
|
+
if "title" in book:
|
|
190
|
+
project["title"] = book["title"]
|
|
191
|
+
if "author" in book:
|
|
192
|
+
project["authors"] = book["author"]
|
|
193
|
+
if "chapters" in book:
|
|
194
|
+
project["toc"] = _chapters_to_toc(book["chapters"])
|
|
195
|
+
result["site"] = {"template": "book-theme"}
|
|
196
|
+
else:
|
|
197
|
+
if "title" in quarto_config:
|
|
198
|
+
project["title"] = quarto_config["title"]
|
|
199
|
+
if "author" in quarto_config:
|
|
200
|
+
project["authors"] = quarto_config["author"]
|
|
201
|
+
|
|
202
|
+
# Fields that apply to both types
|
|
203
|
+
if "bibliography" in quarto_config:
|
|
204
|
+
project["bibliography"] = quarto_config["bibliography"]
|
|
205
|
+
|
|
206
|
+
if "format" in quarto_config:
|
|
207
|
+
project["exports"] = _convert_format_to_exports(quarto_config["format"])
|
|
208
|
+
|
|
209
|
+
if "repo-url" in quarto_config:
|
|
210
|
+
project["github"] = quarto_config["repo-url"]
|
|
211
|
+
|
|
212
|
+
if "license" in quarto_config:
|
|
213
|
+
project["license"] = quarto_config["license"]
|
|
214
|
+
|
|
215
|
+
if "keywords" in quarto_config:
|
|
216
|
+
project["keywords"] = quarto_config["keywords"]
|
|
217
|
+
|
|
218
|
+
if "date" in quarto_config:
|
|
219
|
+
project["date"] = quarto_config["date"]
|
|
220
|
+
|
|
221
|
+
if "description" in quarto_config:
|
|
222
|
+
project["subject"] = quarto_config["description"]
|
|
223
|
+
|
|
224
|
+
if project:
|
|
225
|
+
result["project"] = project
|
|
226
|
+
|
|
227
|
+
return result
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def convert_myst_config(myst_yml_path: str, output_dir: str) -> str:
|
|
231
|
+
"""Read myst.yml, convert to _quarto.yml, write to output_dir.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
myst_yml_path: Path to the input myst.yml file.
|
|
235
|
+
output_dir: Directory to write _quarto.yml to.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Path to the output _quarto.yml file.
|
|
239
|
+
"""
|
|
240
|
+
with open(myst_yml_path) as f:
|
|
241
|
+
myst_config = yaml.safe_load(f) or {}
|
|
242
|
+
|
|
243
|
+
quarto_config = myst_to_quarto_config(myst_config)
|
|
244
|
+
|
|
245
|
+
output_path = os.path.join(output_dir, "_quarto.yml")
|
|
246
|
+
with open(output_path, "w") as f:
|
|
247
|
+
yaml.dump(quarto_config, f, default_flow_style=False, sort_keys=False)
|
|
248
|
+
|
|
249
|
+
return output_path
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def convert_quarto_config(quarto_yml_path: str, output_dir: str) -> str:
|
|
253
|
+
"""Read _quarto.yml, convert to myst.yml, write to output_dir.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
quarto_yml_path: Path to the input _quarto.yml file.
|
|
257
|
+
output_dir: Directory to write myst.yml to.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Path to the output myst.yml file.
|
|
261
|
+
"""
|
|
262
|
+
with open(quarto_yml_path) as f:
|
|
263
|
+
quarto_config = yaml.safe_load(f) or {}
|
|
264
|
+
|
|
265
|
+
myst_config = quarto_to_myst_config(quarto_config)
|
|
266
|
+
|
|
267
|
+
output_path = os.path.join(output_dir, "myst.yml")
|
|
268
|
+
with open(output_path, "w") as f:
|
|
269
|
+
yaml.dump(myst_config, f, default_flow_style=False, sort_keys=False)
|
|
270
|
+
|
|
271
|
+
return output_path
|