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 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.
@@ -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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Lightweight Markdown demo runner."""
2
+
3
+ __version__ = "0.1.0"
@@ -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)