markwright 0.1.0__tar.gz → 0.2.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.
- {markwright-0.1.0 → markwright-0.2.0}/PKG-INFO +9 -5
- {markwright-0.1.0 → markwright-0.2.0}/README.md +8 -4
- {markwright-0.1.0 → markwright-0.2.0}/pyproject.toml +1 -1
- {markwright-0.1.0 → markwright-0.2.0}/pyproject.toml.orig +1 -1
- markwright-0.2.0/src/markwright/cli.py +245 -0
- {markwright-0.1.0 → markwright-0.2.0}/src/markwright/codepen.py +3 -1
- markwright-0.2.0/src/markwright/config.py +220 -0
- {markwright-0.1.0 → markwright-0.2.0}/src/markwright/fence.py +21 -4
- {markwright-0.1.0 → markwright-0.2.0}/src/markwright/highlight.py +3 -1
- {markwright-0.1.0 → markwright-0.2.0}/src/markwright/image_compare.py +3 -1
- {markwright-0.1.0 → markwright-0.2.0}/src/markwright/instagram.py +3 -1
- {markwright-0.1.0 → markwright-0.2.0}/src/markwright/registry.py +6 -4
- {markwright-0.1.0 → markwright-0.2.0}/src/markwright/slideshow.py +3 -1
- {markwright-0.1.0 → markwright-0.2.0}/src/markwright/twitter.py +3 -1
- {markwright-0.1.0 → markwright-0.2.0}/src/markwright/youtube.py +3 -1
- markwright-0.1.0/src/markwright/cli.py +0 -147
- {markwright-0.1.0 → markwright-0.2.0}/LICENSE +0 -0
- {markwright-0.1.0 → markwright-0.2.0}/LICENSE-Apache-2.0 +0 -0
- {markwright-0.1.0 → markwright-0.2.0}/NOTICE +0 -0
- {markwright-0.1.0 → markwright-0.2.0}/src/markwright/__init__.py +0 -0
- {markwright-0.1.0 → markwright-0.2.0}/src/markwright/_util.py +0 -0
- {markwright-0.1.0 → markwright-0.2.0}/src/markwright/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markwright
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python-Markdown extensions ported from DigitalOcean's do-markdownit
|
|
5
5
|
Keywords: markdown,python-markdown,do-markdownit,mkdocs,hugo
|
|
6
6
|
Author: Mason Egger
|
|
@@ -26,11 +26,11 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
|
|
27
27
|
# markwright
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
markwright is a set of [Python-Markdown](https://python-markdown.github.io/) extensions plus a companion command-line tool, `mw`, for author-friendly markdown: highlighted text, labeled code fences, and a set of media embeds.
|
|
30
|
+
Use the extensions in any tool built on Python-Markdown (including [MkDocs](https://www.mkdocs.org/), Flask, or a plain script), or the `mw` pipeline in toolchains that are not, like Hugo.
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
A bundled command-line tool, `mw`, runs the same extensions as pre and post filters around any renderer, so the syntax also works in toolchains that are not built on Python-Markdown, like Hugo.
|
|
32
|
+
The author-facing syntax and HTML output are derived from DigitalOcean's [`do-markdownit`](https://github.com/digitalocean/do-markdownit), so rendered pages match what DigitalOcean produces.
|
|
33
|
+
markwright ports those extensions to Python-Markdown and adds its own pieces on top, most notably the `mw` CLI, which runs the same extensions as pre and post filters around any renderer.
|
|
34
34
|
|
|
35
35
|
Full documentation and a live demo: https://masonegger.github.io/markwright/
|
|
36
36
|
|
|
@@ -139,8 +139,12 @@ mw pre < in.md | your-renderer | mw post > out.html
|
|
|
139
139
|
- `mw pre` expands the embeds and extracts fence directives into an `mw-fence` comment.
|
|
140
140
|
- `mw post` applies the fence styling, resolves the highlights, and injects each embed script once.
|
|
141
141
|
- `mw render` runs the full pipeline in one shot for callers without their own renderer.
|
|
142
|
+
- `mw config` prints the resolved configuration and the source of each value.
|
|
142
143
|
- `mw list` prints every extension and the stages it provides.
|
|
143
144
|
|
|
145
|
+
Which extensions run, and how each is tuned, come from a `markwright.toml` (or a `[tool.markwright]` table in `pyproject.toml`) that the CLI discovers by walking up from the current directory.
|
|
146
|
+
See the [configuration docs](https://masonegger.github.io/markwright/config/).
|
|
147
|
+
|
|
144
148
|
Your renderer must pass raw HTML and HTML comments through.
|
|
145
149
|
Hugo is a worked, tested example: see the [Hugo guide](https://masonegger.github.io/markwright/integrations/hugo/).
|
|
146
150
|
The full command and flag reference is in the [CLI docs](https://masonegger.github.io/markwright/cli/).
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# markwright
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
markwright is a set of [Python-Markdown](https://python-markdown.github.io/) extensions plus a companion command-line tool, `mw`, for author-friendly markdown: highlighted text, labeled code fences, and a set of media embeds.
|
|
4
|
+
Use the extensions in any tool built on Python-Markdown (including [MkDocs](https://www.mkdocs.org/), Flask, or a plain script), or the `mw` pipeline in toolchains that are not, like Hugo.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
A bundled command-line tool, `mw`, runs the same extensions as pre and post filters around any renderer, so the syntax also works in toolchains that are not built on Python-Markdown, like Hugo.
|
|
6
|
+
The author-facing syntax and HTML output are derived from DigitalOcean's [`do-markdownit`](https://github.com/digitalocean/do-markdownit), so rendered pages match what DigitalOcean produces.
|
|
7
|
+
markwright ports those extensions to Python-Markdown and adds its own pieces on top, most notably the `mw` CLI, which runs the same extensions as pre and post filters around any renderer.
|
|
8
8
|
|
|
9
9
|
Full documentation and a live demo: https://masonegger.github.io/markwright/
|
|
10
10
|
|
|
@@ -113,8 +113,12 @@ mw pre < in.md | your-renderer | mw post > out.html
|
|
|
113
113
|
- `mw pre` expands the embeds and extracts fence directives into an `mw-fence` comment.
|
|
114
114
|
- `mw post` applies the fence styling, resolves the highlights, and injects each embed script once.
|
|
115
115
|
- `mw render` runs the full pipeline in one shot for callers without their own renderer.
|
|
116
|
+
- `mw config` prints the resolved configuration and the source of each value.
|
|
116
117
|
- `mw list` prints every extension and the stages it provides.
|
|
117
118
|
|
|
119
|
+
Which extensions run, and how each is tuned, come from a `markwright.toml` (or a `[tool.markwright]` table in `pyproject.toml`) that the CLI discovers by walking up from the current directory.
|
|
120
|
+
See the [configuration docs](https://masonegger.github.io/markwright/config/).
|
|
121
|
+
|
|
118
122
|
Your renderer must pass raw HTML and HTML comments through.
|
|
119
123
|
Hugo is a worked, tested example: see the [Hugo guide](https://masonegger.github.io/markwright/integrations/hugo/).
|
|
120
124
|
The full command and flag reference is in the [CLI docs](https://masonegger.github.io/markwright/cli/).
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# ABOUTME: Command-line entry point for the mw markwright pipeline tool.
|
|
2
|
+
# Builds the argparse parser and dispatches list/pre/post/render; --version reports the package version.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
from importlib.metadata import version
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import markdown
|
|
12
|
+
|
|
13
|
+
from markwright import config, registry
|
|
14
|
+
from markwright.config import Config, ConfigError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _package_version() -> str:
|
|
18
|
+
"""Return the installed markwright distribution version.
|
|
19
|
+
|
|
20
|
+
:returns: The version string for the ``markwright`` distribution.
|
|
21
|
+
"""
|
|
22
|
+
return version("markwright")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
26
|
+
"""Construct the ``mw`` argument parser with its subcommands.
|
|
27
|
+
|
|
28
|
+
:returns: A parser exposing ``--version`` and the ``list`` subcommand.
|
|
29
|
+
"""
|
|
30
|
+
parser = argparse.ArgumentParser(prog="mw", description="markwright Markdown pipeline CLI.")
|
|
31
|
+
parser.add_argument("--version", action="version", version=f"mw {_package_version()}")
|
|
32
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
33
|
+
subparsers.add_parser("list", help="List registered extensions and the stages each provides.")
|
|
34
|
+
pre_parser = subparsers.add_parser("pre", help="Expand markwright source directives read from stdin.")
|
|
35
|
+
_add_selection_flags(pre_parser)
|
|
36
|
+
post_parser = subparsers.add_parser("post", help="Post-process rendered HTML read from stdin.")
|
|
37
|
+
_add_selection_flags(post_parser)
|
|
38
|
+
post_parser.add_argument("--warn", action="store_true", help="Report skipped markers to stderr.")
|
|
39
|
+
render_parser = subparsers.add_parser("render", help="Render Markdown from stdin to final HTML.")
|
|
40
|
+
_add_selection_flags(render_parser)
|
|
41
|
+
config_parser = subparsers.add_parser("config", help="Print the resolved configuration and each value's source.")
|
|
42
|
+
_add_selection_flags(config_parser)
|
|
43
|
+
return parser
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _add_selection_flags(subparser: argparse.ArgumentParser) -> None:
|
|
47
|
+
"""Add the shared ``--config`` and ``--exclude`` selection flags to a subparser.
|
|
48
|
+
|
|
49
|
+
:param subparser: The subcommand parser to extend.
|
|
50
|
+
"""
|
|
51
|
+
subparser.add_argument("--config", default=None, help="Load configuration from PATH, bypassing discovery.")
|
|
52
|
+
subparser.add_argument("--exclude", action="append", default=[], help="Drop the named extension (repeatable).")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _run_list() -> int:
|
|
56
|
+
"""Print each registered extension and its available stages.
|
|
57
|
+
|
|
58
|
+
:returns: Always ``0``.
|
|
59
|
+
"""
|
|
60
|
+
for name, stages in registry.describe():
|
|
61
|
+
print(f"{name}: {', '.join(stages)}")
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _resolve(args: argparse.Namespace) -> tuple[Config, list[str]] | None:
|
|
66
|
+
"""Load config and resolve the active extension names, reporting failures to stderr.
|
|
67
|
+
|
|
68
|
+
Selection resolves from config (``enable`` allowlist or ``disable`` denylist) with the
|
|
69
|
+
repeatable CLI ``--exclude`` applied on top. A config error or an unknown extension name
|
|
70
|
+
is reported to stderr and yields ``None`` so the caller returns exit code ``2``.
|
|
71
|
+
|
|
72
|
+
:param args: Parsed arguments carrying ``config`` and ``exclude``.
|
|
73
|
+
:returns: The resolved config and selected extension names, or ``None`` on failure.
|
|
74
|
+
"""
|
|
75
|
+
explicit_path = Path(args.config) if args.config else None
|
|
76
|
+
try:
|
|
77
|
+
resolved_config = config.load_config(explicit_path, Path.cwd())
|
|
78
|
+
except ConfigError as config_error:
|
|
79
|
+
print(config_error, file=sys.stderr)
|
|
80
|
+
return None
|
|
81
|
+
use = list(resolved_config.enable) if resolved_config.enable is not None else []
|
|
82
|
+
exclude = [*resolved_config.disable, *args.exclude]
|
|
83
|
+
try:
|
|
84
|
+
names = registry.select_extensions(use, exclude)
|
|
85
|
+
except ValueError as selection_error:
|
|
86
|
+
print(selection_error, file=sys.stderr)
|
|
87
|
+
return None
|
|
88
|
+
return resolved_config, names
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _extension_configs(resolved_config: Config, names: list[str]) -> dict[str, dict[str, object]]:
|
|
92
|
+
"""Assemble ``markdown.Markdown`` extension configs from the resolved config.
|
|
93
|
+
|
|
94
|
+
Each selected ``markwright.*`` extension with a per-extension option table contributes
|
|
95
|
+
it, alongside the fixed ``pymdownx.highlight`` entry that keeps language classes on.
|
|
96
|
+
|
|
97
|
+
:param resolved_config: The resolved config carrying per-extension option dicts.
|
|
98
|
+
:param names: The selected extension names.
|
|
99
|
+
:returns: A mapping of extension name to its option dict.
|
|
100
|
+
"""
|
|
101
|
+
configs: dict[str, dict[str, object]] = {"pymdownx.highlight": {"pygments_lang_class": True}}
|
|
102
|
+
for name in names:
|
|
103
|
+
options = resolved_config.options.get(name)
|
|
104
|
+
if options:
|
|
105
|
+
configs[f"markwright.{name}"] = options
|
|
106
|
+
return configs
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _run_pre(args: argparse.Namespace) -> int:
|
|
110
|
+
"""Expand source directives from stdin and write the result to stdout.
|
|
111
|
+
|
|
112
|
+
:param args: Parsed arguments carrying ``config`` and ``exclude``.
|
|
113
|
+
:returns: ``0`` on success, ``2`` if config fails to load or a name is unknown.
|
|
114
|
+
"""
|
|
115
|
+
resolved = _resolve(args)
|
|
116
|
+
if resolved is None:
|
|
117
|
+
return 2
|
|
118
|
+
resolved_config, names = resolved
|
|
119
|
+
sys.stdout.write(registry.run_pre(sys.stdin.read(), names, resolved_config.options))
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _run_post(args: argparse.Namespace) -> int:
|
|
124
|
+
"""Post-process HTML from stdin and write the result to stdout.
|
|
125
|
+
|
|
126
|
+
:param args: Parsed arguments carrying ``config``, ``exclude``, and ``warn``.
|
|
127
|
+
:returns: ``0`` on success, ``2`` if config fails to load or a name is unknown.
|
|
128
|
+
"""
|
|
129
|
+
resolved = _resolve(args)
|
|
130
|
+
if resolved is None:
|
|
131
|
+
return 2
|
|
132
|
+
_, names = resolved
|
|
133
|
+
warnings: list[str] | None = [] if args.warn else None
|
|
134
|
+
rendered_html = registry.run_post(sys.stdin.read(), names, warnings)
|
|
135
|
+
sys.stdout.write(rendered_html)
|
|
136
|
+
if warnings is not None:
|
|
137
|
+
for warning in warnings:
|
|
138
|
+
print(warning, file=sys.stderr)
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _run_render(args: argparse.Namespace) -> int:
|
|
143
|
+
"""Render Markdown from stdin to final HTML using the in-process stack.
|
|
144
|
+
|
|
145
|
+
Builds a ``markdown.Markdown`` instance configured with ``pymdownx.superfences``
|
|
146
|
+
and ``pymdownx.highlight`` plus the selected ``markwright.*`` extensions, mirroring
|
|
147
|
+
the site stack so fence and highlight render correctly.
|
|
148
|
+
|
|
149
|
+
:param args: Parsed arguments carrying ``config`` and ``exclude``.
|
|
150
|
+
:returns: ``0`` on success, ``2`` if config fails to load or a name is unknown.
|
|
151
|
+
"""
|
|
152
|
+
resolved = _resolve(args)
|
|
153
|
+
if resolved is None:
|
|
154
|
+
return 2
|
|
155
|
+
resolved_config, names = resolved
|
|
156
|
+
instance = markdown.Markdown(
|
|
157
|
+
extensions=["pymdownx.superfences", "pymdownx.highlight", *(f"markwright.{name}" for name in names)],
|
|
158
|
+
extension_configs=_extension_configs(resolved_config, names),
|
|
159
|
+
)
|
|
160
|
+
sys.stdout.write(instance.convert(sys.stdin.read()))
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _extension_status(name: str, resolved_config: Config, excluded: list[str]) -> tuple[str, str]:
|
|
165
|
+
"""Report whether an extension is on or off and where that decision came from.
|
|
166
|
+
|
|
167
|
+
Precedence matches selection: a CLI ``--exclude`` wins, then a config ``enable`` allowlist
|
|
168
|
+
or ``disable`` denylist, then the all-on default.
|
|
169
|
+
|
|
170
|
+
:param name: The extension name.
|
|
171
|
+
:param resolved_config: The resolved config.
|
|
172
|
+
:param excluded: Names passed to ``--exclude``.
|
|
173
|
+
:returns: An ``(status, source)`` pair, ``status`` being ``"on"`` or ``"off"``.
|
|
174
|
+
"""
|
|
175
|
+
config_source = str(resolved_config.source_path) if resolved_config.source_path is not None else "default"
|
|
176
|
+
if name in excluded:
|
|
177
|
+
return "off", "--exclude"
|
|
178
|
+
if resolved_config.enable is not None:
|
|
179
|
+
return ("on" if name in resolved_config.enable else "off"), config_source
|
|
180
|
+
if name in resolved_config.disable:
|
|
181
|
+
return "off", config_source
|
|
182
|
+
return "on", "default"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _format_config(resolved_config: Config, excluded: list[str]) -> list[str]:
|
|
186
|
+
"""Format the resolved configuration and value sources as printable lines.
|
|
187
|
+
|
|
188
|
+
:param resolved_config: The resolved config.
|
|
189
|
+
:param excluded: Names passed to ``--exclude``.
|
|
190
|
+
:returns: Output lines for ``mw config``, one per rendered value.
|
|
191
|
+
"""
|
|
192
|
+
lines = ["extensions:"]
|
|
193
|
+
for name in registry.EXTENSION_NAMES:
|
|
194
|
+
status, source = _extension_status(name, resolved_config, excluded)
|
|
195
|
+
lines.append(f" {name}: {status} ({source})")
|
|
196
|
+
warn_source = resolved_config.sources.get("warn", "default")
|
|
197
|
+
lines.append(f"warn: {str(resolved_config.warn).lower()} ({warn_source})")
|
|
198
|
+
if resolved_config.options:
|
|
199
|
+
lines.append("options:")
|
|
200
|
+
for extension_name, option_table in resolved_config.options.items():
|
|
201
|
+
source = resolved_config.sources.get(extension_name, "default")
|
|
202
|
+
lines.append(f" {extension_name}: {option_table} ({source})")
|
|
203
|
+
else:
|
|
204
|
+
lines.append("options: none")
|
|
205
|
+
return lines
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _run_config(args: argparse.Namespace) -> int:
|
|
209
|
+
"""Print the resolved configuration and the source of each value.
|
|
210
|
+
|
|
211
|
+
:param args: Parsed arguments carrying ``config`` and ``exclude``.
|
|
212
|
+
:returns: ``0`` on success, ``2`` if config fails to load or a name is unknown.
|
|
213
|
+
"""
|
|
214
|
+
resolved = _resolve(args)
|
|
215
|
+
if resolved is None:
|
|
216
|
+
return 2
|
|
217
|
+
resolved_config, _ = resolved
|
|
218
|
+
for line in _format_config(resolved_config, args.exclude):
|
|
219
|
+
print(line)
|
|
220
|
+
return 0
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def main(argv: list[str] | None = None) -> int:
|
|
224
|
+
"""Parse ``argv`` and dispatch to the selected subcommand.
|
|
225
|
+
|
|
226
|
+
:param argv: Argument vector, or ``None`` to read from ``sys.argv``.
|
|
227
|
+
:returns: Process exit code (``0`` success, ``2`` usage error).
|
|
228
|
+
"""
|
|
229
|
+
parser = build_parser()
|
|
230
|
+
try:
|
|
231
|
+
args = parser.parse_args(argv)
|
|
232
|
+
except SystemExit as exit_error:
|
|
233
|
+
return exit_error.code if isinstance(exit_error.code, int) else 2
|
|
234
|
+
if args.command == "list":
|
|
235
|
+
return _run_list()
|
|
236
|
+
if args.command == "pre":
|
|
237
|
+
return _run_pre(args)
|
|
238
|
+
if args.command == "post":
|
|
239
|
+
return _run_post(args)
|
|
240
|
+
if args.command == "render":
|
|
241
|
+
return _run_render(args)
|
|
242
|
+
if args.command == "config":
|
|
243
|
+
return _run_config(args)
|
|
244
|
+
parser.print_usage()
|
|
245
|
+
return 2
|
|
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
import html
|
|
7
7
|
import re
|
|
8
8
|
import urllib.parse
|
|
9
|
+
from collections.abc import Mapping
|
|
9
10
|
|
|
10
11
|
from markdown import Markdown
|
|
11
12
|
from markdown.extensions import Extension
|
|
@@ -115,13 +116,14 @@ def _render_match(line: str) -> str | None:
|
|
|
115
116
|
)
|
|
116
117
|
|
|
117
118
|
|
|
118
|
-
def expand_source(text: str) -> str:
|
|
119
|
+
def expand_source(text: str, options: Mapping[str, object] | None = None) -> str:
|
|
119
120
|
"""Expand standalone CodePen embed lines to embed HTML in raw source.
|
|
120
121
|
|
|
121
122
|
Used by the ``mw pre`` CLI stage. Unlike the preprocessor, this emits the
|
|
122
123
|
HTML inline without any Python-Markdown stash placeholder.
|
|
123
124
|
|
|
124
125
|
:param text: The source text.
|
|
126
|
+
:param options: Ignored; accepted so the registry calls every pre stage with one signature.
|
|
125
127
|
:returns: The text with standalone CodePen embeds replaced by HTML.
|
|
126
128
|
"""
|
|
127
129
|
return "\n".join(_render_match(line) or line for line in text.split("\n"))
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# ABOUTME: Config discovery, TOML parsing, validation, and the resolved Config object.
|
|
2
|
+
# Owns the markwright.toml / [tool.markwright] tuning surface; no dependency on cli.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import tomllib
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from markwright.registry import EXTENSION_NAMES
|
|
12
|
+
|
|
13
|
+
_TOP_LEVEL_KEYS: frozenset[str] = frozenset({"enable", "disable", "warn"})
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConfigError(Exception):
|
|
17
|
+
"""Raised for any config discovery, parse, or validation failure.
|
|
18
|
+
|
|
19
|
+
Carries a message meant for the user at the CLI boundary, never a traceback.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class Config:
|
|
25
|
+
"""Resolved markwright configuration.
|
|
26
|
+
|
|
27
|
+
:ivar enable: Allowlist of extension names, or ``None`` when unset.
|
|
28
|
+
:ivar disable: Denylist of extension names, empty when unset.
|
|
29
|
+
:ivar options: Per-extension option tables keyed by extension name.
|
|
30
|
+
:ivar warn: Default for the post stage's marker warnings.
|
|
31
|
+
:ivar source_path: The file this config was loaded from, or ``None`` for built-in defaults.
|
|
32
|
+
:ivar sources: Maps each key set in the file (``enable``, ``disable``, ``warn``, or an
|
|
33
|
+
extension name) to the file path it came from; keys not present use defaults.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
enable: tuple[str, ...] | None = None
|
|
37
|
+
disable: tuple[str, ...] = ()
|
|
38
|
+
options: dict[str, dict[str, object]] = field(default_factory=dict)
|
|
39
|
+
warn: bool = False
|
|
40
|
+
source_path: Path | None = None
|
|
41
|
+
sources: dict[str, str] = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_config(explicit_path: Path | None, start_dir: Path) -> Config:
|
|
45
|
+
"""Resolve the configuration from an explicit path or by walk-up discovery.
|
|
46
|
+
|
|
47
|
+
:param explicit_path: A specific config file to load, bypassing discovery, or ``None``.
|
|
48
|
+
:param start_dir: The directory to begin walk-up discovery from when no explicit path is given.
|
|
49
|
+
:returns: The resolved :class:`Config`; built-in defaults when nothing is found.
|
|
50
|
+
:raises ConfigError: If a file is missing, unparseable, or fails schema validation.
|
|
51
|
+
"""
|
|
52
|
+
if explicit_path is not None:
|
|
53
|
+
path, table = _load_explicit(explicit_path)
|
|
54
|
+
else:
|
|
55
|
+
discovered = _discover(start_dir)
|
|
56
|
+
if discovered is None:
|
|
57
|
+
return Config()
|
|
58
|
+
path, table = discovered
|
|
59
|
+
return _build_config(table, path)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _load_explicit(path: Path) -> tuple[Path, dict[str, object]]:
|
|
63
|
+
"""Load a config table from an explicitly named file.
|
|
64
|
+
|
|
65
|
+
:param path: The file named by ``--config``.
|
|
66
|
+
:returns: The path and its config table.
|
|
67
|
+
:raises ConfigError: If the file is missing, or a ``pyproject.toml`` lacks a
|
|
68
|
+
``[tool.markwright]`` table.
|
|
69
|
+
"""
|
|
70
|
+
if not path.is_file():
|
|
71
|
+
raise ConfigError(f"config file not found: {path}")
|
|
72
|
+
parsed = _read_toml(path)
|
|
73
|
+
if path.name == "pyproject.toml":
|
|
74
|
+
table = _pyproject_markwright_table(parsed)
|
|
75
|
+
if table is None:
|
|
76
|
+
raise ConfigError(f"no [tool.markwright] table in {path}")
|
|
77
|
+
return path, table
|
|
78
|
+
return path, parsed
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _discover(start_dir: Path) -> tuple[Path, dict[str, object]] | None:
|
|
82
|
+
"""Walk up from ``start_dir`` to the filesystem root looking for a config file.
|
|
83
|
+
|
|
84
|
+
In each directory a ``markwright.toml`` wins over a ``pyproject.toml`` that carries a
|
|
85
|
+
``[tool.markwright]`` table. The first directory with either stops the search.
|
|
86
|
+
|
|
87
|
+
:param start_dir: The directory to begin the walk from.
|
|
88
|
+
:returns: The winning path and its config table, or ``None`` if none is found.
|
|
89
|
+
:raises ConfigError: If a candidate file is present but unparseable.
|
|
90
|
+
"""
|
|
91
|
+
for directory in [start_dir, *start_dir.parents]:
|
|
92
|
+
standalone = directory / "markwright.toml"
|
|
93
|
+
if standalone.is_file():
|
|
94
|
+
return standalone, _read_toml(standalone)
|
|
95
|
+
pyproject = directory / "pyproject.toml"
|
|
96
|
+
if pyproject.is_file():
|
|
97
|
+
table = _pyproject_markwright_table(_read_toml(pyproject))
|
|
98
|
+
if table is not None:
|
|
99
|
+
return pyproject, table
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _read_toml(path: Path) -> dict[str, object]:
|
|
104
|
+
"""Parse a TOML file, converting parse failures into a clear boundary error.
|
|
105
|
+
|
|
106
|
+
:param path: The TOML file to read.
|
|
107
|
+
:returns: The parsed top-level table.
|
|
108
|
+
:raises ConfigError: If the file is not valid TOML.
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
with path.open("rb") as handle:
|
|
112
|
+
parsed: dict[str, object] = tomllib.load(handle)
|
|
113
|
+
except tomllib.TOMLDecodeError as decode_error:
|
|
114
|
+
raise ConfigError(f"could not parse {path}: {decode_error}") from decode_error
|
|
115
|
+
return parsed
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _pyproject_markwright_table(parsed: Mapping[str, object]) -> dict[str, object] | None:
|
|
119
|
+
"""Extract the ``[tool.markwright]`` table from a parsed ``pyproject.toml``.
|
|
120
|
+
|
|
121
|
+
:param parsed: The parsed ``pyproject.toml`` top-level table.
|
|
122
|
+
:returns: The markwright config table, or ``None`` if the ``[tool.markwright]`` path is absent.
|
|
123
|
+
"""
|
|
124
|
+
tool_section = parsed.get("tool")
|
|
125
|
+
if not isinstance(tool_section, dict):
|
|
126
|
+
return None
|
|
127
|
+
markwright_section = tool_section.get("markwright")
|
|
128
|
+
if not isinstance(markwright_section, dict):
|
|
129
|
+
return None
|
|
130
|
+
return markwright_section
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _build_config(table: Mapping[str, object], path: Path) -> Config:
|
|
134
|
+
"""Validate a raw config table and resolve it into a :class:`Config`.
|
|
135
|
+
|
|
136
|
+
:param table: The parsed config table (from a standalone file or a ``[tool.markwright]`` table).
|
|
137
|
+
:param path: The source path, recorded for the source map.
|
|
138
|
+
:returns: The resolved config.
|
|
139
|
+
:raises ConfigError: If any key, type, or extension name is invalid.
|
|
140
|
+
"""
|
|
141
|
+
_reject_unknown_keys(table, path)
|
|
142
|
+
enable = _selection_list(table, "enable", path)
|
|
143
|
+
disable = _selection_list(table, "disable", path)
|
|
144
|
+
if enable is not None and disable is not None:
|
|
145
|
+
raise ConfigError(f"{path}: 'enable' and 'disable' are mutually exclusive")
|
|
146
|
+
return Config(
|
|
147
|
+
enable=enable,
|
|
148
|
+
disable=disable if disable is not None else (),
|
|
149
|
+
options=_extension_options(table, path),
|
|
150
|
+
warn=_warn_value(table, path),
|
|
151
|
+
source_path=path,
|
|
152
|
+
sources={key: str(path) for key in table},
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _reject_unknown_keys(table: Mapping[str, object], path: Path) -> None:
|
|
157
|
+
"""Reject any top-level key that is neither a recognized setting nor an extension name.
|
|
158
|
+
|
|
159
|
+
:param table: The config table.
|
|
160
|
+
:param path: The source path, for the error message.
|
|
161
|
+
:raises ConfigError: On the first unrecognized key.
|
|
162
|
+
"""
|
|
163
|
+
for key in table:
|
|
164
|
+
if key not in _TOP_LEVEL_KEYS and key not in EXTENSION_NAMES:
|
|
165
|
+
raise ConfigError(f"{path}: unknown configuration key: {key!r}")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _selection_list(table: Mapping[str, object], key: str, path: Path) -> tuple[str, ...] | None:
|
|
169
|
+
"""Read and validate an ``enable`` or ``disable`` list of extension names.
|
|
170
|
+
|
|
171
|
+
:param table: The config table.
|
|
172
|
+
:param key: Either ``"enable"`` or ``"disable"``.
|
|
173
|
+
:param path: The source path, for error messages.
|
|
174
|
+
:returns: The names as a tuple, or ``None`` if the key is absent.
|
|
175
|
+
:raises ConfigError: If the value is not a list of known extension names.
|
|
176
|
+
"""
|
|
177
|
+
if key not in table:
|
|
178
|
+
return None
|
|
179
|
+
value = table[key]
|
|
180
|
+
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
|
181
|
+
raise ConfigError(f"{path}: '{key}' must be a list of extension names")
|
|
182
|
+
for name in value:
|
|
183
|
+
if name not in EXTENSION_NAMES:
|
|
184
|
+
raise ConfigError(f"{path}: unknown extension in '{key}': {name!r}")
|
|
185
|
+
return tuple(value)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _warn_value(table: Mapping[str, object], path: Path) -> bool:
|
|
189
|
+
"""Read and validate the boolean ``warn`` default.
|
|
190
|
+
|
|
191
|
+
:param table: The config table.
|
|
192
|
+
:param path: The source path, for the error message.
|
|
193
|
+
:returns: The ``warn`` value, defaulting to ``False`` when absent.
|
|
194
|
+
:raises ConfigError: If ``warn`` is present but not a boolean.
|
|
195
|
+
"""
|
|
196
|
+
if "warn" not in table:
|
|
197
|
+
return False
|
|
198
|
+
value = table["warn"]
|
|
199
|
+
if not isinstance(value, bool):
|
|
200
|
+
raise ConfigError(f"{path}: 'warn' must be a boolean")
|
|
201
|
+
return value
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _extension_options(table: Mapping[str, object], path: Path) -> dict[str, dict[str, object]]:
|
|
205
|
+
"""Collect the per-extension option tables from the config.
|
|
206
|
+
|
|
207
|
+
:param table: The config table.
|
|
208
|
+
:param path: The source path, for error messages.
|
|
209
|
+
:returns: Option dicts keyed by extension name, in registry order.
|
|
210
|
+
:raises ConfigError: If an extension key is present but not a table.
|
|
211
|
+
"""
|
|
212
|
+
options: dict[str, dict[str, object]] = {}
|
|
213
|
+
for name in EXTENSION_NAMES:
|
|
214
|
+
if name not in table:
|
|
215
|
+
continue
|
|
216
|
+
extension_table = table[name]
|
|
217
|
+
if not isinstance(extension_table, dict):
|
|
218
|
+
raise ConfigError(f"{path}: '{name}' must be a table of options")
|
|
219
|
+
options[name] = extension_table
|
|
220
|
+
return options
|
|
@@ -30,6 +30,7 @@ from __future__ import annotations
|
|
|
30
30
|
import html
|
|
31
31
|
import json
|
|
32
32
|
import re
|
|
33
|
+
from collections.abc import Mapping
|
|
33
34
|
|
|
34
35
|
from markdown import Markdown
|
|
35
36
|
from markdown.extensions import Extension
|
|
@@ -267,16 +268,32 @@ def _expand_lines(lines: list[str], allowed_environments: list[str] | None) -> l
|
|
|
267
268
|
return output
|
|
268
269
|
|
|
269
270
|
|
|
270
|
-
def expand_source(text: str) -> str:
|
|
271
|
+
def expand_source(text: str, options: Mapping[str, object] | None = None) -> str:
|
|
271
272
|
"""Extract fence directives and emit mw-fence marker comments in raw source.
|
|
272
273
|
|
|
273
|
-
Used by the ``mw pre`` CLI stage.
|
|
274
|
-
|
|
274
|
+
Used by the ``mw pre`` CLI stage. When ``options`` carries an
|
|
275
|
+
``allowed_environments`` list, ``[environment ...]`` directives outside it are left
|
|
276
|
+
unextracted, matching the in-process ``allowed_environments`` config option.
|
|
275
277
|
|
|
276
278
|
:param text: The source text.
|
|
279
|
+
:param options: The fence option table from config, or ``None`` for no restriction.
|
|
277
280
|
:returns: The text with directives replaced by mw-fence marker comments.
|
|
278
281
|
"""
|
|
279
|
-
return "\n".join(_expand_lines(text.split("\n"),
|
|
282
|
+
return "\n".join(_expand_lines(text.split("\n"), _allowed_environments(options)))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _allowed_environments(options: Mapping[str, object] | None) -> list[str] | None:
|
|
286
|
+
"""Read the ``allowed_environments`` list from a fence option table.
|
|
287
|
+
|
|
288
|
+
:param options: The fence option table, or ``None``.
|
|
289
|
+
:returns: The allowed environment names, or ``None`` to allow every environment.
|
|
290
|
+
"""
|
|
291
|
+
if options is None:
|
|
292
|
+
return None
|
|
293
|
+
value = options.get("allowed_environments")
|
|
294
|
+
if isinstance(value, list):
|
|
295
|
+
return [str(item) for item in value]
|
|
296
|
+
return None
|
|
280
297
|
|
|
281
298
|
|
|
282
299
|
class FencePreprocessor(Preprocessor):
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import re
|
|
7
7
|
import xml.etree.ElementTree as etree
|
|
8
|
+
from collections.abc import Mapping
|
|
8
9
|
|
|
9
10
|
from markdown import Markdown
|
|
10
11
|
from markdown.extensions import Extension
|
|
@@ -98,7 +99,7 @@ def _highlight_prose(segment: str) -> str:
|
|
|
98
99
|
return _PROSE_HIGHLIGHT_RE.sub(r"<mark>\1</mark>", segment)
|
|
99
100
|
|
|
100
101
|
|
|
101
|
-
def expand_source(text: str) -> str:
|
|
102
|
+
def expand_source(text: str, options: Mapping[str, object] | None = None) -> str:
|
|
102
103
|
"""Wrap prose highlight markers in ``<mark>`` outside code regions.
|
|
103
104
|
|
|
104
105
|
The source-stage transform for ``mw pre``. Fenced code blocks, inline code
|
|
@@ -107,6 +108,7 @@ def expand_source(text: str) -> str:
|
|
|
107
108
|
post stage.
|
|
108
109
|
|
|
109
110
|
:param text: Raw Markdown source.
|
|
111
|
+
:param options: Ignored; accepted so the registry calls every pre stage with one signature.
|
|
110
112
|
:returns: Source with unescaped prose ``<^>...<^>`` markers wrapped in
|
|
111
113
|
``<mark>``.
|
|
112
114
|
"""
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import html
|
|
7
7
|
import re
|
|
8
|
+
from collections.abc import Mapping
|
|
8
9
|
|
|
9
10
|
from markdown import Markdown
|
|
10
11
|
from markdown.extensions import Extension
|
|
@@ -43,13 +44,14 @@ def _render_match(line: str) -> str | None:
|
|
|
43
44
|
return _build_compare_html(left_url, right_url, height, width)
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
def expand_source(text: str) -> str:
|
|
47
|
+
def expand_source(text: str, options: Mapping[str, object] | None = None) -> str:
|
|
47
48
|
"""Expand standalone compare embed lines to HTML in raw source.
|
|
48
49
|
|
|
49
50
|
Used by the ``mw pre`` CLI stage. Emits the HTML inline without any
|
|
50
51
|
Python-Markdown stash placeholder.
|
|
51
52
|
|
|
52
53
|
:param text: The source text.
|
|
54
|
+
:param options: Ignored; accepted so the registry calls every pre stage with one signature.
|
|
53
55
|
:returns: The text with standalone compare embeds replaced by HTML.
|
|
54
56
|
"""
|
|
55
57
|
return "\n".join(_render_match(line) or line for line in text.split("\n"))
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import html
|
|
7
7
|
import re
|
|
8
|
+
from collections.abc import Mapping
|
|
8
9
|
|
|
9
10
|
from markdown import Markdown
|
|
10
11
|
from markdown.extensions import Extension
|
|
@@ -102,13 +103,14 @@ def _render_match(line: str) -> str | None:
|
|
|
102
103
|
)
|
|
103
104
|
|
|
104
105
|
|
|
105
|
-
def expand_source(text: str) -> str:
|
|
106
|
+
def expand_source(text: str, options: Mapping[str, object] | None = None) -> str:
|
|
106
107
|
"""Expand standalone Instagram embed lines to embed HTML in raw source.
|
|
107
108
|
|
|
108
109
|
Used by the ``mw pre`` CLI stage. Unlike the preprocessor, this emits the
|
|
109
110
|
HTML inline without any Python-Markdown stash placeholder.
|
|
110
111
|
|
|
111
112
|
:param text: The source text.
|
|
113
|
+
:param options: Ignored; accepted so the registry calls every pre stage with one signature.
|
|
112
114
|
:returns: The text with standalone Instagram embeds replaced by HTML.
|
|
113
115
|
"""
|
|
114
116
|
return "\n".join(_render_match(line) or line for line in text.split("\n"))
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
-
from collections.abc import Callable
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
7
|
from typing import TypedDict
|
|
8
8
|
|
|
9
9
|
from markwright.codepen import apply_html as codepen_post
|
|
@@ -20,7 +20,7 @@ from markwright.twitter import apply_html as twitter_post
|
|
|
20
20
|
from markwright.twitter import expand_source as twitter_pre
|
|
21
21
|
from markwright.youtube import expand_source as youtube_pre
|
|
22
22
|
|
|
23
|
-
PreFn = Callable[[str], str]
|
|
23
|
+
PreFn = Callable[[str, Mapping[str, object] | None], str]
|
|
24
24
|
PostFn = Callable[[str, list[str] | None], str]
|
|
25
25
|
|
|
26
26
|
|
|
@@ -87,17 +87,19 @@ def _ordered(
|
|
|
87
87
|
return sorted(selected, key=lambda name: get_priority(REGISTRY[name]), reverse=True)
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
def run_pre(text: str, names: list[str]) -> str:
|
|
90
|
+
def run_pre(text: str, names: list[str], options: Mapping[str, Mapping[str, object]] | None = None) -> str:
|
|
91
91
|
"""Apply each selected pre-stage transform to ``text`` in descending priority order.
|
|
92
92
|
|
|
93
93
|
:param text: Markdown source text.
|
|
94
94
|
:param names: Selected extension names.
|
|
95
|
+
:param options: Per-extension option tables keyed by extension name; each stage receives
|
|
96
|
+
its own table, or ``None`` when it has none.
|
|
95
97
|
:returns: Source text after every selected pre stage has run.
|
|
96
98
|
"""
|
|
97
99
|
for name in _ordered(names, lambda spec: spec["pre"], lambda spec: spec["pre_priority"]):
|
|
98
100
|
pre_fn = REGISTRY[name]["pre"]
|
|
99
101
|
assert pre_fn is not None
|
|
100
|
-
text = pre_fn(text)
|
|
102
|
+
text = pre_fn(text, options.get(name) if options is not None else None)
|
|
101
103
|
return text
|
|
102
104
|
|
|
103
105
|
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import html
|
|
7
7
|
import re
|
|
8
|
+
from collections.abc import Mapping
|
|
8
9
|
|
|
9
10
|
from markdown import Markdown
|
|
10
11
|
from markdown.extensions import Extension
|
|
@@ -64,13 +65,14 @@ def _render_match(line: str) -> str | None:
|
|
|
64
65
|
return _build_slideshow_html(urls, height, width)
|
|
65
66
|
|
|
66
67
|
|
|
67
|
-
def expand_source(text: str) -> str:
|
|
68
|
+
def expand_source(text: str, options: Mapping[str, object] | None = None) -> str:
|
|
68
69
|
"""Expand standalone slideshow embed lines to HTML in raw source.
|
|
69
70
|
|
|
70
71
|
Used by the ``mw pre`` CLI stage. Emits the HTML inline without any
|
|
71
72
|
Python-Markdown stash placeholder.
|
|
72
73
|
|
|
73
74
|
:param text: The source text.
|
|
75
|
+
:param options: Ignored; accepted so the registry calls every pre stage with one signature.
|
|
74
76
|
:returns: The text with standalone slideshow embeds replaced by HTML.
|
|
75
77
|
"""
|
|
76
78
|
return "\n".join(_render_match(line) or line for line in text.split("\n"))
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import html
|
|
7
7
|
import re
|
|
8
|
+
from collections.abc import Mapping
|
|
8
9
|
|
|
9
10
|
from markdown import Markdown
|
|
10
11
|
from markdown.extensions import Extension
|
|
@@ -96,13 +97,14 @@ def _render_match(line: str) -> str | None:
|
|
|
96
97
|
)
|
|
97
98
|
|
|
98
99
|
|
|
99
|
-
def expand_source(text: str) -> str:
|
|
100
|
+
def expand_source(text: str, options: Mapping[str, object] | None = None) -> str:
|
|
100
101
|
"""Expand standalone Twitter embed lines to embed HTML in raw source.
|
|
101
102
|
|
|
102
103
|
Used by the ``mw pre`` CLI stage. Unlike the preprocessor, this emits the
|
|
103
104
|
HTML inline without any Python-Markdown stash placeholder.
|
|
104
105
|
|
|
105
106
|
:param text: The source text.
|
|
107
|
+
:param options: Ignored; accepted so the registry calls every pre stage with one signature.
|
|
106
108
|
:returns: The text with standalone Twitter embeds replaced by HTML.
|
|
107
109
|
"""
|
|
108
110
|
return "\n".join(_render_match(line) or line for line in text.split("\n"))
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import re
|
|
7
7
|
import urllib.parse
|
|
8
|
+
from collections.abc import Mapping
|
|
8
9
|
|
|
9
10
|
from markdown import Markdown
|
|
10
11
|
from markdown.extensions import Extension
|
|
@@ -48,13 +49,14 @@ def _render_match(line: str) -> str | None:
|
|
|
48
49
|
)
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
def expand_source(text: str) -> str:
|
|
52
|
+
def expand_source(text: str, options: Mapping[str, object] | None = None) -> str:
|
|
52
53
|
"""Expand standalone YouTube embed lines to iframe HTML in raw source.
|
|
53
54
|
|
|
54
55
|
Used by the ``mw pre`` CLI stage. Unlike the preprocessor, this emits the
|
|
55
56
|
HTML inline without any Python-Markdown stash placeholder.
|
|
56
57
|
|
|
57
58
|
:param text: The source text.
|
|
59
|
+
:param options: Ignored; accepted so the registry calls every pre stage with one signature.
|
|
58
60
|
:returns: The text with standalone YouTube embeds replaced by iframe HTML.
|
|
59
61
|
"""
|
|
60
62
|
return "\n".join(_render_match(line) or line for line in text.split("\n"))
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
# ABOUTME: Command-line entry point for the mw markwright pipeline tool.
|
|
2
|
-
# Builds the argparse parser and dispatches list/pre/post/render; --version reports the package version.
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import argparse
|
|
7
|
-
import sys
|
|
8
|
-
from importlib.metadata import version
|
|
9
|
-
|
|
10
|
-
import markdown
|
|
11
|
-
|
|
12
|
-
from markwright import registry
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _package_version() -> str:
|
|
16
|
-
"""Return the installed markwright distribution version.
|
|
17
|
-
|
|
18
|
-
:returns: The version string for the ``markwright`` distribution.
|
|
19
|
-
"""
|
|
20
|
-
return version("markwright")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def build_parser() -> argparse.ArgumentParser:
|
|
24
|
-
"""Construct the ``mw`` argument parser with its subcommands.
|
|
25
|
-
|
|
26
|
-
:returns: A parser exposing ``--version`` and the ``list`` subcommand.
|
|
27
|
-
"""
|
|
28
|
-
parser = argparse.ArgumentParser(prog="mw", description="markwright Markdown pipeline CLI.")
|
|
29
|
-
parser.add_argument("--version", action="version", version=f"mw {_package_version()}")
|
|
30
|
-
subparsers = parser.add_subparsers(dest="command")
|
|
31
|
-
subparsers.add_parser("list", help="List registered extensions and the stages each provides.")
|
|
32
|
-
pre_parser = subparsers.add_parser("pre", help="Expand markwright source directives read from stdin.")
|
|
33
|
-
_add_selection_flags(pre_parser)
|
|
34
|
-
post_parser = subparsers.add_parser("post", help="Post-process rendered HTML read from stdin.")
|
|
35
|
-
_add_selection_flags(post_parser)
|
|
36
|
-
post_parser.add_argument("--warn", action="store_true", help="Report skipped markers to stderr.")
|
|
37
|
-
render_parser = subparsers.add_parser("render", help="Render Markdown from stdin to final HTML.")
|
|
38
|
-
_add_selection_flags(render_parser)
|
|
39
|
-
return parser
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _add_selection_flags(subparser: argparse.ArgumentParser) -> None:
|
|
43
|
-
"""Add the shared ``--use`` and ``--exclude`` selection flags to a subparser.
|
|
44
|
-
|
|
45
|
-
:param subparser: The subcommand parser to extend.
|
|
46
|
-
"""
|
|
47
|
-
subparser.add_argument("--use", action="append", default=[], help="Restrict to the named extension (repeatable).")
|
|
48
|
-
subparser.add_argument("--exclude", action="append", default=[], help="Drop the named extension (repeatable).")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _run_list() -> int:
|
|
52
|
-
"""Print each registered extension and its available stages.
|
|
53
|
-
|
|
54
|
-
:returns: Always ``0``.
|
|
55
|
-
"""
|
|
56
|
-
for name, stages in registry.describe():
|
|
57
|
-
print(f"{name}: {', '.join(stages)}")
|
|
58
|
-
return 0
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _resolve_selection(args: argparse.Namespace) -> list[str] | None:
|
|
62
|
-
"""Resolve the active extension names, reporting unknown names to stderr.
|
|
63
|
-
|
|
64
|
-
:param args: Parsed arguments carrying ``use`` and ``exclude`` lists.
|
|
65
|
-
:returns: Selected extension names, or ``None`` if a name is unknown (the
|
|
66
|
-
caller should then return exit code ``2``).
|
|
67
|
-
"""
|
|
68
|
-
try:
|
|
69
|
-
return registry.select_extensions(args.use, args.exclude)
|
|
70
|
-
except ValueError as selection_error:
|
|
71
|
-
print(selection_error, file=sys.stderr)
|
|
72
|
-
return None
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _run_pre(args: argparse.Namespace) -> int:
|
|
76
|
-
"""Expand source directives from stdin and write the result to stdout.
|
|
77
|
-
|
|
78
|
-
:param args: Parsed arguments carrying ``use`` and ``exclude``.
|
|
79
|
-
:returns: ``0`` on success, ``2`` if a selected extension name is unknown.
|
|
80
|
-
"""
|
|
81
|
-
names = _resolve_selection(args)
|
|
82
|
-
if names is None:
|
|
83
|
-
return 2
|
|
84
|
-
sys.stdout.write(registry.run_pre(sys.stdin.read(), names))
|
|
85
|
-
return 0
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def _run_post(args: argparse.Namespace) -> int:
|
|
89
|
-
"""Post-process HTML from stdin and write the result to stdout.
|
|
90
|
-
|
|
91
|
-
:param args: Parsed arguments carrying ``use``, ``exclude``, and ``warn``.
|
|
92
|
-
:returns: ``0`` on success, ``2`` if a selected extension name is unknown.
|
|
93
|
-
"""
|
|
94
|
-
names = _resolve_selection(args)
|
|
95
|
-
if names is None:
|
|
96
|
-
return 2
|
|
97
|
-
warnings: list[str] | None = [] if args.warn else None
|
|
98
|
-
rendered_html = registry.run_post(sys.stdin.read(), names, warnings)
|
|
99
|
-
sys.stdout.write(rendered_html)
|
|
100
|
-
if warnings is not None:
|
|
101
|
-
for warning in warnings:
|
|
102
|
-
print(warning, file=sys.stderr)
|
|
103
|
-
return 0
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def _run_render(args: argparse.Namespace) -> int:
|
|
107
|
-
"""Render Markdown from stdin to final HTML using the in-process stack.
|
|
108
|
-
|
|
109
|
-
Builds a ``markdown.Markdown`` instance configured with ``pymdownx.superfences``
|
|
110
|
-
and ``pymdownx.highlight`` plus the selected ``markwright.*`` extensions, mirroring
|
|
111
|
-
the site stack so fence and highlight render correctly.
|
|
112
|
-
|
|
113
|
-
:param args: Parsed arguments carrying ``use`` and ``exclude``.
|
|
114
|
-
:returns: ``0`` on success, ``2`` if a selected extension name is unknown.
|
|
115
|
-
"""
|
|
116
|
-
names = _resolve_selection(args)
|
|
117
|
-
if names is None:
|
|
118
|
-
return 2
|
|
119
|
-
instance = markdown.Markdown(
|
|
120
|
-
extensions=["pymdownx.superfences", "pymdownx.highlight", *(f"markwright.{name}" for name in names)],
|
|
121
|
-
extension_configs={"pymdownx.highlight": {"pygments_lang_class": True}},
|
|
122
|
-
)
|
|
123
|
-
sys.stdout.write(instance.convert(sys.stdin.read()))
|
|
124
|
-
return 0
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def main(argv: list[str] | None = None) -> int:
|
|
128
|
-
"""Parse ``argv`` and dispatch to the selected subcommand.
|
|
129
|
-
|
|
130
|
-
:param argv: Argument vector, or ``None`` to read from ``sys.argv``.
|
|
131
|
-
:returns: Process exit code (``0`` success, ``2`` usage error).
|
|
132
|
-
"""
|
|
133
|
-
parser = build_parser()
|
|
134
|
-
try:
|
|
135
|
-
args = parser.parse_args(argv)
|
|
136
|
-
except SystemExit as exit_error:
|
|
137
|
-
return exit_error.code if isinstance(exit_error.code, int) else 2
|
|
138
|
-
if args.command == "list":
|
|
139
|
-
return _run_list()
|
|
140
|
-
if args.command == "pre":
|
|
141
|
-
return _run_pre(args)
|
|
142
|
-
if args.command == "post":
|
|
143
|
-
return _run_post(args)
|
|
144
|
-
if args.command == "render":
|
|
145
|
-
return _run_render(args)
|
|
146
|
-
parser.print_usage()
|
|
147
|
-
return 2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|