md-demo 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.
- md_demo-0.1.0/PKG-INFO +238 -0
- md_demo-0.1.0/README.md +222 -0
- md_demo-0.1.0/pyproject.toml +45 -0
- md_demo-0.1.0/setup.cfg +4 -0
- md_demo-0.1.0/src/md_demo/__init__.py +3 -0
- md_demo-0.1.0/src/md_demo/cli.py +73 -0
- md_demo-0.1.0/src/md_demo/config.py +107 -0
- md_demo-0.1.0/src/md_demo/document.py +343 -0
- md_demo-0.1.0/src/md_demo/errors.py +10 -0
- md_demo-0.1.0/src/md_demo/manual.py +195 -0
- md_demo-0.1.0/src/md_demo/runners.py +123 -0
- md_demo-0.1.0/src/md_demo.egg-info/PKG-INFO +238 -0
- md_demo-0.1.0/src/md_demo.egg-info/SOURCES.txt +17 -0
- md_demo-0.1.0/src/md_demo.egg-info/dependency_links.txt +1 -0
- md_demo-0.1.0/src/md_demo.egg-info/entry_points.txt +2 -0
- md_demo-0.1.0/src/md_demo.egg-info/requires.txt +4 -0
- md_demo-0.1.0/src/md_demo.egg-info/top_level.txt +1 -0
- md_demo-0.1.0/tests/test_cli.py +79 -0
- md_demo-0.1.0/tests/test_document.py +452 -0
md_demo-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: md-demo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight Markdown demo runner.
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: PyYAML>=6
|
|
14
|
+
Provides-Extra: test
|
|
15
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
16
|
+
|
|
17
|
+
# md-demo
|
|
18
|
+
|
|
19
|
+
`md-demo` is a lightweight Markdown demo runner. It executes explicitly marked code blocks, captures stdout and stderr, and writes generated output back into the Markdown file.
|
|
20
|
+
|
|
21
|
+
It is meant for readable demo documents that stay useful as plain Markdown. It is not a notebook system, a sandbox, or a runner for untrusted code.
|
|
22
|
+
|
|
23
|
+
Warning: `md-demo` executes code from the document. Run only trusted files.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
From a source checkout:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python -m pip install -e ".[test]"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Verify the checkout:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python -m compileall -q src
|
|
37
|
+
pytest
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
Create a Markdown file with one runtime and one executable block.
|
|
43
|
+
|
|
44
|
+
````markdown
|
|
45
|
+
---
|
|
46
|
+
md-demo:
|
|
47
|
+
runtime: python
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
```python exe
|
|
51
|
+
print("hello")
|
|
52
|
+
```
|
|
53
|
+
````
|
|
54
|
+
|
|
55
|
+
Run:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
md-demo demo.md
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`md-demo` updates the file in place by default and inserts a generated result block:
|
|
62
|
+
|
|
63
|
+
````markdown
|
|
64
|
+
```python exe
|
|
65
|
+
print("hello")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
<!-- md-demo: result start. Do not edit; this block is overwritten. -->
|
|
69
|
+
```text
|
|
70
|
+
hello
|
|
71
|
+
```
|
|
72
|
+
<!-- md-demo: result end -->
|
|
73
|
+
````
|
|
74
|
+
|
|
75
|
+
Do not edit generated result blocks. They are cleared and recreated on normal runs.
|
|
76
|
+
|
|
77
|
+
## Document config
|
|
78
|
+
|
|
79
|
+
Every runnable document needs config with one runtime. There are two supported forms.
|
|
80
|
+
|
|
81
|
+
Use YAML front matter by default:
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
---
|
|
85
|
+
md-demo:
|
|
86
|
+
runtime: python
|
|
87
|
+
---
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
If your Markdown renderer shows front matter as visible page content, use hidden HTML comment config instead:
|
|
91
|
+
|
|
92
|
+
````markdown
|
|
93
|
+
<!-- md-demo
|
|
94
|
+
runtime: python
|
|
95
|
+
-->
|
|
96
|
+
````
|
|
97
|
+
|
|
98
|
+
Both forms are parsed only at the top of the document. `md-demo` preserves whichever form the document already uses by default.
|
|
99
|
+
|
|
100
|
+
To convert config style while running or clearing a document, use `--config-style`:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
md-demo demo.md --config-style preserve
|
|
104
|
+
md-demo demo.md --config-style front-matter
|
|
105
|
+
md-demo demo.md --config-style hidden
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`preserve` is the default and does not rewrite the config style. `front-matter` rewrites the document's `md-demo` config as YAML front matter. `hidden` rewrites the document's `md-demo` config as an HTML comment. Only the `md-demo` config is converted; unrelated front matter is preserved when practical.
|
|
109
|
+
|
|
110
|
+
Supported runtime values:
|
|
111
|
+
|
|
112
|
+
- `python`
|
|
113
|
+
- `python3`
|
|
114
|
+
- `bash`
|
|
115
|
+
- `shell`
|
|
116
|
+
|
|
117
|
+
`python3` is an alias for the Python runner. `shell` is an alias for the bash runner, not `/bin/sh`.
|
|
118
|
+
|
|
119
|
+
## Output labels
|
|
120
|
+
|
|
121
|
+
You can optionally add visible text before every generated output block with `preface-text`.
|
|
122
|
+
|
|
123
|
+
YAML front matter:
|
|
124
|
+
|
|
125
|
+
```yaml
|
|
126
|
+
---
|
|
127
|
+
md-demo:
|
|
128
|
+
runtime: python
|
|
129
|
+
preface-text: "Output:"
|
|
130
|
+
---
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Hidden HTML comment config:
|
|
134
|
+
|
|
135
|
+
````markdown
|
|
136
|
+
<!-- md-demo
|
|
137
|
+
runtime: python
|
|
138
|
+
preface-text: "Output:"
|
|
139
|
+
-->
|
|
140
|
+
````
|
|
141
|
+
|
|
142
|
+
If `preface-text` is missing, empty, or `null`, no label is inserted. The label is generated inside the result region, so changing `preface-text` updates existing results the next time `md-demo` runs.
|
|
143
|
+
|
|
144
|
+
## Executable blocks
|
|
145
|
+
|
|
146
|
+
Only matching-language fenced code blocks marked with `exe` run.
|
|
147
|
+
|
|
148
|
+
````markdown
|
|
149
|
+
```python exe
|
|
150
|
+
print("runs")
|
|
151
|
+
```
|
|
152
|
+
````
|
|
153
|
+
|
|
154
|
+
Ordinary code blocks are examples only:
|
|
155
|
+
|
|
156
|
+
````markdown
|
|
157
|
+
```python
|
|
158
|
+
print("shown, not run")
|
|
159
|
+
```
|
|
160
|
+
````
|
|
161
|
+
|
|
162
|
+
Executable blocks run top-to-bottom in one persistent runtime. Python variables, imports, functions, shell variables, and shell directory changes can carry forward to later executable blocks.
|
|
163
|
+
|
|
164
|
+
`md-demo` captures stdout and stderr. Python blocks should use `print` for values that should appear in the document. Python last-expression display is not part of v1.
|
|
165
|
+
|
|
166
|
+
## CLI
|
|
167
|
+
|
|
168
|
+
Update a document in place:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
md-demo demo.md
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Write the updated Markdown elsewhere:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
md-demo demo.md --output rendered.md
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Write the updated Markdown to stdout:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
md-demo demo.md --output -
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Clear generated result blocks without executing code:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
md-demo demo.md --clear
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Rewrite config style without executing code:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
md-demo demo.md --clear --config-style hidden
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Print concise help:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
md-demo --help
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Print the detailed manual:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
md-demo --manual
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Failure behavior
|
|
211
|
+
|
|
212
|
+
A normal run behaves like clear and execute:
|
|
213
|
+
|
|
214
|
+
1. Old generated results are cleared.
|
|
215
|
+
2. Executable blocks run top-to-bottom.
|
|
216
|
+
3. Fresh result blocks are inserted for blocks that actually ran.
|
|
217
|
+
|
|
218
|
+
If a block fails, `md-demo` writes output through the failed block, stops before later executable blocks, and exits nonzero. Later executable blocks are left without result blocks because they did not run.
|
|
219
|
+
|
|
220
|
+
Intentional failures should be handled inside the demo code:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
try:
|
|
224
|
+
validate("")
|
|
225
|
+
except ValueError as exc:
|
|
226
|
+
print(type(exc).__name__, exc)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Converting existing documents
|
|
230
|
+
|
|
231
|
+
- See [docs/markdown-conversion.md](docs/markdown-conversion.md) for converting ordinary Markdown documents.
|
|
232
|
+
- See [docs/jupyter-conversion.md](docs/jupyter-conversion.md) for converting Jupyter notebooks, usually by exporting to Markdown first.
|
|
233
|
+
- See [docs/design.md](docs/design.md) for the design.
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
## AI Disclosure
|
|
237
|
+
|
|
238
|
+
This tool was primarily generated with assistance from ChatGPT Codex, guided and directed by a human developer. Human involvement included requirements definition, some implementation direction, and cursory code review. The code has not undergone a comprehensive human audit or formal security review.
|
md_demo-0.1.0/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# md-demo
|
|
2
|
+
|
|
3
|
+
`md-demo` is a lightweight Markdown demo runner. It executes explicitly marked code blocks, captures stdout and stderr, and writes generated output back into the Markdown file.
|
|
4
|
+
|
|
5
|
+
It is meant for readable demo documents that stay useful as plain Markdown. It is not a notebook system, a sandbox, or a runner for untrusted code.
|
|
6
|
+
|
|
7
|
+
Warning: `md-demo` executes code from the document. Run only trusted files.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
From a source checkout:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python -m pip install -e ".[test]"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Verify the checkout:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python -m compileall -q src
|
|
21
|
+
pytest
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
Create a Markdown file with one runtime and one executable block.
|
|
27
|
+
|
|
28
|
+
````markdown
|
|
29
|
+
---
|
|
30
|
+
md-demo:
|
|
31
|
+
runtime: python
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
```python exe
|
|
35
|
+
print("hello")
|
|
36
|
+
```
|
|
37
|
+
````
|
|
38
|
+
|
|
39
|
+
Run:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
md-demo demo.md
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`md-demo` updates the file in place by default and inserts a generated result block:
|
|
46
|
+
|
|
47
|
+
````markdown
|
|
48
|
+
```python exe
|
|
49
|
+
print("hello")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
<!-- md-demo: result start. Do not edit; this block is overwritten. -->
|
|
53
|
+
```text
|
|
54
|
+
hello
|
|
55
|
+
```
|
|
56
|
+
<!-- md-demo: result end -->
|
|
57
|
+
````
|
|
58
|
+
|
|
59
|
+
Do not edit generated result blocks. They are cleared and recreated on normal runs.
|
|
60
|
+
|
|
61
|
+
## Document config
|
|
62
|
+
|
|
63
|
+
Every runnable document needs config with one runtime. There are two supported forms.
|
|
64
|
+
|
|
65
|
+
Use YAML front matter by default:
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
---
|
|
69
|
+
md-demo:
|
|
70
|
+
runtime: python
|
|
71
|
+
---
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
If your Markdown renderer shows front matter as visible page content, use hidden HTML comment config instead:
|
|
75
|
+
|
|
76
|
+
````markdown
|
|
77
|
+
<!-- md-demo
|
|
78
|
+
runtime: python
|
|
79
|
+
-->
|
|
80
|
+
````
|
|
81
|
+
|
|
82
|
+
Both forms are parsed only at the top of the document. `md-demo` preserves whichever form the document already uses by default.
|
|
83
|
+
|
|
84
|
+
To convert config style while running or clearing a document, use `--config-style`:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
md-demo demo.md --config-style preserve
|
|
88
|
+
md-demo demo.md --config-style front-matter
|
|
89
|
+
md-demo demo.md --config-style hidden
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`preserve` is the default and does not rewrite the config style. `front-matter` rewrites the document's `md-demo` config as YAML front matter. `hidden` rewrites the document's `md-demo` config as an HTML comment. Only the `md-demo` config is converted; unrelated front matter is preserved when practical.
|
|
93
|
+
|
|
94
|
+
Supported runtime values:
|
|
95
|
+
|
|
96
|
+
- `python`
|
|
97
|
+
- `python3`
|
|
98
|
+
- `bash`
|
|
99
|
+
- `shell`
|
|
100
|
+
|
|
101
|
+
`python3` is an alias for the Python runner. `shell` is an alias for the bash runner, not `/bin/sh`.
|
|
102
|
+
|
|
103
|
+
## Output labels
|
|
104
|
+
|
|
105
|
+
You can optionally add visible text before every generated output block with `preface-text`.
|
|
106
|
+
|
|
107
|
+
YAML front matter:
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
---
|
|
111
|
+
md-demo:
|
|
112
|
+
runtime: python
|
|
113
|
+
preface-text: "Output:"
|
|
114
|
+
---
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Hidden HTML comment config:
|
|
118
|
+
|
|
119
|
+
````markdown
|
|
120
|
+
<!-- md-demo
|
|
121
|
+
runtime: python
|
|
122
|
+
preface-text: "Output:"
|
|
123
|
+
-->
|
|
124
|
+
````
|
|
125
|
+
|
|
126
|
+
If `preface-text` is missing, empty, or `null`, no label is inserted. The label is generated inside the result region, so changing `preface-text` updates existing results the next time `md-demo` runs.
|
|
127
|
+
|
|
128
|
+
## Executable blocks
|
|
129
|
+
|
|
130
|
+
Only matching-language fenced code blocks marked with `exe` run.
|
|
131
|
+
|
|
132
|
+
````markdown
|
|
133
|
+
```python exe
|
|
134
|
+
print("runs")
|
|
135
|
+
```
|
|
136
|
+
````
|
|
137
|
+
|
|
138
|
+
Ordinary code blocks are examples only:
|
|
139
|
+
|
|
140
|
+
````markdown
|
|
141
|
+
```python
|
|
142
|
+
print("shown, not run")
|
|
143
|
+
```
|
|
144
|
+
````
|
|
145
|
+
|
|
146
|
+
Executable blocks run top-to-bottom in one persistent runtime. Python variables, imports, functions, shell variables, and shell directory changes can carry forward to later executable blocks.
|
|
147
|
+
|
|
148
|
+
`md-demo` captures stdout and stderr. Python blocks should use `print` for values that should appear in the document. Python last-expression display is not part of v1.
|
|
149
|
+
|
|
150
|
+
## CLI
|
|
151
|
+
|
|
152
|
+
Update a document in place:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
md-demo demo.md
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Write the updated Markdown elsewhere:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
md-demo demo.md --output rendered.md
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Write the updated Markdown to stdout:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
md-demo demo.md --output -
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Clear generated result blocks without executing code:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
md-demo demo.md --clear
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Rewrite config style without executing code:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
md-demo demo.md --clear --config-style hidden
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Print concise help:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
md-demo --help
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Print the detailed manual:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
md-demo --manual
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Failure behavior
|
|
195
|
+
|
|
196
|
+
A normal run behaves like clear and execute:
|
|
197
|
+
|
|
198
|
+
1. Old generated results are cleared.
|
|
199
|
+
2. Executable blocks run top-to-bottom.
|
|
200
|
+
3. Fresh result blocks are inserted for blocks that actually ran.
|
|
201
|
+
|
|
202
|
+
If a block fails, `md-demo` writes output through the failed block, stops before later executable blocks, and exits nonzero. Later executable blocks are left without result blocks because they did not run.
|
|
203
|
+
|
|
204
|
+
Intentional failures should be handled inside the demo code:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
try:
|
|
208
|
+
validate("")
|
|
209
|
+
except ValueError as exc:
|
|
210
|
+
print(type(exc).__name__, exc)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Converting existing documents
|
|
214
|
+
|
|
215
|
+
- See [docs/markdown-conversion.md](docs/markdown-conversion.md) for converting ordinary Markdown documents.
|
|
216
|
+
- See [docs/jupyter-conversion.md](docs/jupyter-conversion.md) for converting Jupyter notebooks, usually by exporting to Markdown first.
|
|
217
|
+
- See [docs/design.md](docs/design.md) for the design.
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
## AI Disclosure
|
|
221
|
+
|
|
222
|
+
This tool was primarily generated with assistance from ChatGPT Codex, guided and directed by a human developer. Human involvement included requirements definition, some implementation direction, and cursory code review. The code has not undergone a comprehensive human audit or formal security review.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68,<77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "md-demo"
|
|
7
|
+
description = "A lightweight Markdown demo runner."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
dependencies = ["PyYAML>=6"]
|
|
12
|
+
dynamic = ["version"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
test = ["pytest>=8"]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
md-demo = "md_demo.cli:main"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
license-files = []
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.dynamic]
|
|
34
|
+
version = {attr = "md_demo.__version__"}
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
pythonpath = ["src"]
|
|
38
|
+
|
|
39
|
+
[tool.black]
|
|
40
|
+
line-length = 100
|
|
41
|
+
target-version = ["py310"]
|
|
42
|
+
|
|
43
|
+
[tool.isort]
|
|
44
|
+
profile = "black"
|
|
45
|
+
line_length = 100
|
md_demo-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .document import process_file, write_output
|
|
8
|
+
from .errors import ExecutionFailed, MdDemoError
|
|
9
|
+
from .manual import MANUAL
|
|
10
|
+
|
|
11
|
+
HELP_DESCRIPTION = """A lightweight Markdown demo runner.
|
|
12
|
+
|
|
13
|
+
By default, md-demo updates FILE in place.
|
|
14
|
+
Use --output PATH to write elsewhere, or --output - to write to stdout.
|
|
15
|
+
|
|
16
|
+
Warning: md-demo executes code from the document. Run only trusted files.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
prog="md-demo",
|
|
23
|
+
description=HELP_DESCRIPTION,
|
|
24
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument("file", nargs="?", help="Markdown file to process")
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--clear", action="store_true", help="remove generated result blocks without executing code"
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--config-style",
|
|
32
|
+
choices=["preserve", "front-matter", "hidden"],
|
|
33
|
+
default="preserve",
|
|
34
|
+
help="config rewrite style; default preserve keeps the existing style",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument("--output", help="write updated Markdown to PATH; use - for stdout")
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--manual", action="store_true", help="print the detailed authoring and usage guide"
|
|
39
|
+
)
|
|
40
|
+
return parser
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main(argv: list[str] | None = None) -> int:
|
|
44
|
+
parser = build_parser()
|
|
45
|
+
args = parser.parse_args(argv)
|
|
46
|
+
if args.manual:
|
|
47
|
+
print(MANUAL, end="")
|
|
48
|
+
return 0
|
|
49
|
+
if not args.file:
|
|
50
|
+
parser.error("FILE is required unless --manual is used")
|
|
51
|
+
path = Path(args.file)
|
|
52
|
+
try:
|
|
53
|
+
result = process_file(path, clear=args.clear, config_style=args.config_style)
|
|
54
|
+
write_output(path, result.text, args.output)
|
|
55
|
+
for warning in result.warnings:
|
|
56
|
+
print(warning, file=sys.stderr)
|
|
57
|
+
if args.output is None:
|
|
58
|
+
print(f"updated {path}", file=sys.stderr)
|
|
59
|
+
return 0
|
|
60
|
+
except ExecutionFailed as exc:
|
|
61
|
+
write_output(path, exc.document, args.output)
|
|
62
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
63
|
+
return 1
|
|
64
|
+
except MdDemoError as exc:
|
|
65
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
66
|
+
return 1
|
|
67
|
+
except OSError as exc:
|
|
68
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
69
|
+
return 1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from .errors import MdDemoError
|
|
9
|
+
|
|
10
|
+
ConfigStyle = Literal["preserve", "front-matter", "hidden"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclasses.dataclass(frozen=True)
|
|
14
|
+
class ConfigBlock:
|
|
15
|
+
config: dict
|
|
16
|
+
body_start: int
|
|
17
|
+
style: str
|
|
18
|
+
front_matter: dict
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_config(lines: list[str]) -> ConfigBlock:
|
|
22
|
+
if not lines:
|
|
23
|
+
raise MdDemoError("missing md-demo.runtime")
|
|
24
|
+
first = lines[0].strip()
|
|
25
|
+
if first == "---":
|
|
26
|
+
end_index = _find_end(lines, start=1, marker="---", error="unterminated YAML front matter")
|
|
27
|
+
data = load_yaml_config("".join(lines[1:end_index]), "front matter")
|
|
28
|
+
config = data.get("md-demo")
|
|
29
|
+
if not isinstance(config, dict):
|
|
30
|
+
raise MdDemoError("missing md-demo.runtime in front matter")
|
|
31
|
+
return ConfigBlock(
|
|
32
|
+
config=config, body_start=end_index + 1, style="front-matter", front_matter=data
|
|
33
|
+
)
|
|
34
|
+
if first.startswith("<!-- md-demo") and first[len("<!-- md-demo") :].strip() in {"", "-->"}:
|
|
35
|
+
if first.endswith("-->"):
|
|
36
|
+
raise MdDemoError("missing md-demo.runtime")
|
|
37
|
+
end_index = _find_end(
|
|
38
|
+
lines,
|
|
39
|
+
start=1,
|
|
40
|
+
marker="-->",
|
|
41
|
+
error="unterminated md-demo HTML comment config",
|
|
42
|
+
)
|
|
43
|
+
data = load_yaml_config("".join(lines[1:end_index]), "md-demo HTML comment config")
|
|
44
|
+
config = data.get("md-demo") if isinstance(data.get("md-demo"), dict) else data
|
|
45
|
+
if not isinstance(config, dict):
|
|
46
|
+
raise MdDemoError("md-demo HTML comment config must be a mapping")
|
|
47
|
+
body_start = end_index + 1
|
|
48
|
+
front_matter: dict = {}
|
|
49
|
+
front_matter_start = body_start
|
|
50
|
+
while front_matter_start < len(lines) and lines[front_matter_start].strip() == "":
|
|
51
|
+
front_matter_start += 1
|
|
52
|
+
if front_matter_start < len(lines) and lines[front_matter_start].strip() == "---":
|
|
53
|
+
fm_end = _find_end(
|
|
54
|
+
lines,
|
|
55
|
+
start=front_matter_start + 1,
|
|
56
|
+
marker="---",
|
|
57
|
+
error="unterminated YAML front matter",
|
|
58
|
+
)
|
|
59
|
+
front_matter = load_yaml_config(
|
|
60
|
+
"".join(lines[front_matter_start + 1 : fm_end]), "front matter"
|
|
61
|
+
)
|
|
62
|
+
body_start = fm_end + 1
|
|
63
|
+
return ConfigBlock(
|
|
64
|
+
config=config, body_start=body_start, style="hidden", front_matter=front_matter
|
|
65
|
+
)
|
|
66
|
+
raise MdDemoError("missing md-demo.runtime")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def render_config(block: ConfigBlock, newline: str, style: ConfigStyle) -> list[str]:
|
|
70
|
+
if style == "preserve":
|
|
71
|
+
return []
|
|
72
|
+
if style == "front-matter":
|
|
73
|
+
data = dict(block.front_matter)
|
|
74
|
+
data["md-demo"] = dict(block.config)
|
|
75
|
+
return ["---" + newline, dump_yaml(data, newline), "---" + newline]
|
|
76
|
+
if style == "hidden":
|
|
77
|
+
front_matter = dict(block.front_matter)
|
|
78
|
+
front_matter.pop("md-demo", None)
|
|
79
|
+
lines = ["<!-- md-demo" + newline, dump_yaml(block.config, newline), "-->" + newline]
|
|
80
|
+
if front_matter:
|
|
81
|
+
lines.extend(
|
|
82
|
+
[newline, "---" + newline, dump_yaml(front_matter, newline), "---" + newline]
|
|
83
|
+
)
|
|
84
|
+
return lines
|
|
85
|
+
raise MdDemoError(f"unsupported config style: {style}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def load_yaml_config(text: str, label: str) -> dict:
|
|
89
|
+
try:
|
|
90
|
+
data = yaml.safe_load(text) or {}
|
|
91
|
+
except yaml.YAMLError as exc:
|
|
92
|
+
raise MdDemoError(f"invalid YAML {label}: {exc}") from exc
|
|
93
|
+
if not isinstance(data, dict):
|
|
94
|
+
raise MdDemoError(f"YAML {label} must be a mapping")
|
|
95
|
+
return data
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def dump_yaml(data: dict, newline: str) -> str:
|
|
99
|
+
text = yaml.safe_dump(data, sort_keys=False, default_flow_style=False)
|
|
100
|
+
return text.replace("\n", newline)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _find_end(lines: list[str], *, start: int, marker: str, error: str) -> int:
|
|
104
|
+
for index in range(start, len(lines)):
|
|
105
|
+
if lines[index].strip() == marker:
|
|
106
|
+
return index
|
|
107
|
+
raise MdDemoError(error)
|