markdown-exec 1.10.1__py3-none-any.whl → 1.10.2__py3-none-any.whl
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.
- markdown_exec/__init__.py +50 -138
- markdown_exec/_internal/__init__.py +0 -0
- markdown_exec/{debug.py → _internal/debug.py} +13 -15
- markdown_exec/_internal/formatters/__init__.py +1 -0
- markdown_exec/{formatters → _internal/formatters}/_exec_python.py +1 -1
- markdown_exec/_internal/formatters/base.py +191 -0
- markdown_exec/_internal/formatters/bash.py +32 -0
- markdown_exec/_internal/formatters/console.py +29 -0
- markdown_exec/_internal/formatters/markdown.py +11 -0
- markdown_exec/_internal/formatters/pycon.py +26 -0
- markdown_exec/_internal/formatters/pyodide.py +73 -0
- markdown_exec/_internal/formatters/python.py +85 -0
- markdown_exec/_internal/formatters/sh.py +32 -0
- markdown_exec/_internal/formatters/tree.py +60 -0
- markdown_exec/_internal/logger.py +89 -0
- markdown_exec/_internal/main.py +123 -0
- markdown_exec/_internal/mkdocs_plugin.py +143 -0
- markdown_exec/_internal/processors.py +136 -0
- markdown_exec/_internal/rendering.py +280 -0
- markdown_exec/{pyodide.js → assets/pyodide.js} +12 -6
- markdown_exec/formatters/__init__.py +17 -1
- markdown_exec/formatters/base.py +12 -183
- markdown_exec/formatters/bash.py +10 -25
- markdown_exec/formatters/console.py +12 -24
- markdown_exec/formatters/markdown.py +11 -5
- markdown_exec/formatters/pycon.py +12 -24
- markdown_exec/formatters/pyodide.py +12 -65
- markdown_exec/formatters/python.py +11 -79
- markdown_exec/formatters/sh.py +10 -25
- markdown_exec/formatters/tree.py +12 -55
- markdown_exec/logger.py +12 -87
- markdown_exec/mkdocs_plugin.py +11 -135
- markdown_exec/processors.py +12 -118
- markdown_exec/rendering.py +11 -270
- {markdown_exec-1.10.1.dist-info → markdown_exec-1.10.2.dist-info}/METADATA +4 -3
- markdown_exec-1.10.2.dist-info/RECORD +42 -0
- markdown_exec-1.10.2.dist-info/entry_points.txt +7 -0
- markdown_exec-1.10.1.dist-info/RECORD +0 -26
- markdown_exec-1.10.1.dist-info/entry_points.txt +0 -7
- /markdown_exec/{ansi.css → assets/ansi.css} +0 -0
- /markdown_exec/{pyodide.css → assets/pyodide.css} +0 -0
- {markdown_exec-1.10.1.dist-info → markdown_exec-1.10.2.dist-info}/WHEEL +0 -0
- {markdown_exec-1.10.1.dist-info → markdown_exec-1.10.2.dist-info}/licenses/LICENSE +0 -0
markdown_exec/__init__.py
CHANGED
@@ -3,141 +3,53 @@
|
|
3
3
|
Utilities to execute code blocks in Markdown files.
|
4
4
|
"""
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
from
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
from markdown_exec.
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
"
|
34
|
-
"
|
35
|
-
"
|
36
|
-
"
|
37
|
-
"
|
38
|
-
"
|
39
|
-
"
|
40
|
-
"
|
41
|
-
"
|
42
|
-
"
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
"""Validate code blocks inputs.
|
57
|
-
|
58
|
-
Parameters:
|
59
|
-
language: The code language, like python or bash.
|
60
|
-
inputs: The code block inputs, to be sorted into options and attrs.
|
61
|
-
options: The container for options.
|
62
|
-
attrs: The container for attrs:
|
63
|
-
md: The Markdown instance.
|
64
|
-
|
65
|
-
Returns:
|
66
|
-
Success or not.
|
67
|
-
"""
|
68
|
-
exec_value = language in MARKDOWN_EXEC_AUTO or _to_bool(inputs.pop("exec", "no"))
|
69
|
-
if language not in {"tree", "pyodide"} and not exec_value:
|
70
|
-
return False
|
71
|
-
id_value = inputs.pop("id", "")
|
72
|
-
id_prefix_value = inputs.pop("idprefix", None)
|
73
|
-
html_value = _to_bool(inputs.pop("html", "no"))
|
74
|
-
source_value = inputs.pop("source", "")
|
75
|
-
result_value = inputs.pop("result", "")
|
76
|
-
returncode_value = int(inputs.pop("returncode", "0"))
|
77
|
-
session_value = inputs.pop("session", "")
|
78
|
-
update_toc_value = _to_bool(inputs.pop("updatetoc", "yes"))
|
79
|
-
tabs_value = inputs.pop("tabs", "|".join(default_tabs))
|
80
|
-
tabs = tuple(_tabs_re.split(tabs_value, maxsplit=1))
|
81
|
-
workdir_value = inputs.pop("workdir", None)
|
82
|
-
width_value = int(inputs.pop("width", "0"))
|
83
|
-
options["id"] = id_value
|
84
|
-
options["id_prefix"] = id_prefix_value
|
85
|
-
options["html"] = html_value
|
86
|
-
options["source"] = source_value
|
87
|
-
options["result"] = result_value
|
88
|
-
options["returncode"] = returncode_value
|
89
|
-
options["session"] = session_value
|
90
|
-
options["update_toc"] = update_toc_value
|
91
|
-
options["tabs"] = tabs
|
92
|
-
options["workdir"] = workdir_value
|
93
|
-
options["width"] = width_value
|
94
|
-
options["extra"] = inputs
|
95
|
-
return True
|
96
|
-
|
97
|
-
|
98
|
-
def formatter(
|
99
|
-
source: str,
|
100
|
-
language: str,
|
101
|
-
css_class: str, # noqa: ARG001
|
102
|
-
options: dict[str, Any],
|
103
|
-
md: Markdown,
|
104
|
-
classes: list[str] | None = None, # noqa: ARG001
|
105
|
-
id_value: str = "", # noqa: ARG001
|
106
|
-
attrs: dict[str, Any] | None = None, # noqa: ARG001
|
107
|
-
**kwargs: Any, # noqa: ARG001
|
108
|
-
) -> str:
|
109
|
-
"""Execute code and return HTML.
|
110
|
-
|
111
|
-
Parameters:
|
112
|
-
source: The code to execute.
|
113
|
-
language: The code language, like python or bash.
|
114
|
-
css_class: The CSS class to add to the HTML element.
|
115
|
-
options: The container for options.
|
116
|
-
attrs: The container for attrs:
|
117
|
-
md: The Markdown instance.
|
118
|
-
classes: Additional CSS classes.
|
119
|
-
id_value: An optional HTML id.
|
120
|
-
attrs: Additional attributes
|
121
|
-
**kwargs: Additional arguments passed to SuperFences default formatters.
|
122
|
-
|
123
|
-
Returns:
|
124
|
-
HTML contents.
|
125
|
-
"""
|
126
|
-
fmt = formatters.get(language, lambda source, **kwargs: source)
|
127
|
-
return fmt(code=source, md=md, **options) # type: ignore[operator]
|
128
|
-
|
129
|
-
|
130
|
-
falsy_values = {"", "no", "off", "false", "0"}
|
131
|
-
truthy_values = {"yes", "on", "true", "1"}
|
132
|
-
|
133
|
-
|
134
|
-
def _to_bool(value: str) -> bool:
|
135
|
-
return value.lower() not in falsy_values
|
136
|
-
|
137
|
-
|
138
|
-
def _to_bool_or_value(value: str) -> bool | str:
|
139
|
-
if value.lower() in falsy_values:
|
140
|
-
return False
|
141
|
-
if value.lower() in truthy_values:
|
142
|
-
return True
|
143
|
-
return value
|
6
|
+
from markdown_exec._internal.formatters.base import (
|
7
|
+
ExecutionError,
|
8
|
+
base_format,
|
9
|
+
console_width,
|
10
|
+
default_tabs,
|
11
|
+
working_directory,
|
12
|
+
)
|
13
|
+
from markdown_exec._internal.logger import get_logger, patch_loggers
|
14
|
+
from markdown_exec._internal.main import MARKDOWN_EXEC_AUTO, formatter, formatters, validator
|
15
|
+
from markdown_exec._internal.mkdocs_plugin import MarkdownExecPlugin, MarkdownExecPluginConfig
|
16
|
+
from markdown_exec._internal.processors import (
|
17
|
+
HeadingReportingTreeprocessor,
|
18
|
+
IdPrependingTreeprocessor,
|
19
|
+
InsertHeadings,
|
20
|
+
RemoveHeadings,
|
21
|
+
)
|
22
|
+
from markdown_exec._internal.rendering import (
|
23
|
+
MarkdownConfig,
|
24
|
+
MarkdownConverter,
|
25
|
+
add_source,
|
26
|
+
code_block,
|
27
|
+
markdown_config,
|
28
|
+
tabbed,
|
29
|
+
)
|
30
|
+
|
31
|
+
__all__ = [
|
32
|
+
"MARKDOWN_EXEC_AUTO",
|
33
|
+
"ExecutionError",
|
34
|
+
"HeadingReportingTreeprocessor",
|
35
|
+
"IdPrependingTreeprocessor",
|
36
|
+
"InsertHeadings",
|
37
|
+
"MarkdownConfig",
|
38
|
+
"MarkdownConverter",
|
39
|
+
"MarkdownExecPlugin",
|
40
|
+
"MarkdownExecPluginConfig",
|
41
|
+
"RemoveHeadings",
|
42
|
+
"add_source",
|
43
|
+
"base_format",
|
44
|
+
"code_block",
|
45
|
+
"console_width",
|
46
|
+
"default_tabs",
|
47
|
+
"formatter",
|
48
|
+
"formatters",
|
49
|
+
"get_logger",
|
50
|
+
"markdown_config",
|
51
|
+
"patch_loggers",
|
52
|
+
"tabbed",
|
53
|
+
"validator",
|
54
|
+
"working_directory",
|
55
|
+
]
|
File without changes
|
@@ -1,5 +1,3 @@
|
|
1
|
-
"""Debugging utilities."""
|
2
|
-
|
3
1
|
from __future__ import annotations
|
4
2
|
|
5
3
|
import os
|
@@ -10,7 +8,7 @@ from importlib import metadata
|
|
10
8
|
|
11
9
|
|
12
10
|
@dataclass
|
13
|
-
class
|
11
|
+
class _Variable:
|
14
12
|
"""Dataclass describing an environment variable."""
|
15
13
|
|
16
14
|
name: str
|
@@ -20,7 +18,7 @@ class Variable:
|
|
20
18
|
|
21
19
|
|
22
20
|
@dataclass
|
23
|
-
class
|
21
|
+
class _Package:
|
24
22
|
"""Dataclass describing a Python package."""
|
25
23
|
|
26
24
|
name: str
|
@@ -30,7 +28,7 @@ class Package:
|
|
30
28
|
|
31
29
|
|
32
30
|
@dataclass
|
33
|
-
class
|
31
|
+
class _Environment:
|
34
32
|
"""Dataclass to store environment information."""
|
35
33
|
|
36
34
|
interpreter_name: str
|
@@ -41,9 +39,9 @@ class Environment:
|
|
41
39
|
"""Path to Python executable."""
|
42
40
|
platform: str
|
43
41
|
"""Operating System."""
|
44
|
-
packages: list[
|
42
|
+
packages: list[_Package]
|
45
43
|
"""Installed packages."""
|
46
|
-
variables: list[
|
44
|
+
variables: list[_Variable]
|
47
45
|
"""Environment variables."""
|
48
46
|
|
49
47
|
|
@@ -58,7 +56,7 @@ def _interpreter_name_version() -> tuple[str, str]:
|
|
58
56
|
return "", "0.0.0"
|
59
57
|
|
60
58
|
|
61
|
-
def
|
59
|
+
def _get_version(dist: str = "markdown-exec") -> str:
|
62
60
|
"""Get version of the given distribution.
|
63
61
|
|
64
62
|
Parameters:
|
@@ -73,7 +71,7 @@ def get_version(dist: str = "markdown-exec") -> str:
|
|
73
71
|
return "0.0.0"
|
74
72
|
|
75
73
|
|
76
|
-
def
|
74
|
+
def _get_debug_info() -> _Environment:
|
77
75
|
"""Get debug/environment information.
|
78
76
|
|
79
77
|
Returns:
|
@@ -82,19 +80,19 @@ def get_debug_info() -> Environment:
|
|
82
80
|
py_name, py_version = _interpreter_name_version()
|
83
81
|
packages = ["markdown-exec"]
|
84
82
|
variables = ["PYTHONPATH", *[var for var in os.environ if var.startswith("MARKDOWN_EXEC")]]
|
85
|
-
return
|
83
|
+
return _Environment(
|
86
84
|
interpreter_name=py_name,
|
87
85
|
interpreter_version=py_version,
|
88
86
|
interpreter_path=sys.executable,
|
89
87
|
platform=platform.platform(),
|
90
|
-
variables=[
|
91
|
-
packages=[
|
88
|
+
variables=[_Variable(var, val) for var in variables if (val := os.getenv(var))],
|
89
|
+
packages=[_Package(pkg, _get_version(pkg)) for pkg in packages],
|
92
90
|
)
|
93
91
|
|
94
92
|
|
95
|
-
def
|
93
|
+
def _print_debug_info() -> None:
|
96
94
|
"""Print debug/environment information."""
|
97
|
-
info =
|
95
|
+
info = _get_debug_info()
|
98
96
|
print(f"- __System__: {info.platform}")
|
99
97
|
print(f"- __Python__: {info.interpreter_name} {info.interpreter_version} ({info.interpreter_path})")
|
100
98
|
print("- __Environment variables__:")
|
@@ -106,4 +104,4 @@ def print_debug_info() -> None:
|
|
106
104
|
|
107
105
|
|
108
106
|
if __name__ == "__main__":
|
109
|
-
|
107
|
+
_print_debug_info()
|
@@ -0,0 +1 @@
|
|
1
|
+
# This subpackage contains all the formatters.
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# Generic formatter for executing code.
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import os
|
6
|
+
from contextlib import contextmanager
|
7
|
+
from textwrap import indent
|
8
|
+
from typing import TYPE_CHECKING, Any, Callable
|
9
|
+
from uuid import uuid4
|
10
|
+
|
11
|
+
from markupsafe import Markup
|
12
|
+
|
13
|
+
from markdown_exec._internal.logger import get_logger
|
14
|
+
from markdown_exec._internal.rendering import MarkdownConverter, add_source, code_block
|
15
|
+
|
16
|
+
if TYPE_CHECKING:
|
17
|
+
from collections.abc import Iterator
|
18
|
+
|
19
|
+
from markdown.core import Markdown
|
20
|
+
|
21
|
+
_logger = get_logger(__name__)
|
22
|
+
|
23
|
+
default_tabs = ("Source", "Result")
|
24
|
+
"""Default tab titles."""
|
25
|
+
|
26
|
+
|
27
|
+
@contextmanager
|
28
|
+
def working_directory(path: str | None = None) -> Iterator[None]:
|
29
|
+
"""Change the working directory for the duration of the context.
|
30
|
+
|
31
|
+
Parameters:
|
32
|
+
path: The path to change the working directory to.
|
33
|
+
"""
|
34
|
+
if path:
|
35
|
+
old_cwd = os.getcwd()
|
36
|
+
os.chdir(path)
|
37
|
+
try:
|
38
|
+
yield
|
39
|
+
finally:
|
40
|
+
os.chdir(old_cwd)
|
41
|
+
else:
|
42
|
+
yield
|
43
|
+
|
44
|
+
|
45
|
+
@contextmanager
|
46
|
+
def console_width(width: int | None = None) -> Iterator[None]:
|
47
|
+
"""Set the console width for the duration of the context.
|
48
|
+
|
49
|
+
The console width is set using the `COLUMNS` environment variable.
|
50
|
+
|
51
|
+
Parameters:
|
52
|
+
width: The width to set the console to.
|
53
|
+
"""
|
54
|
+
if width:
|
55
|
+
old_width = os.environ.get("COLUMNS", None)
|
56
|
+
os.environ["COLUMNS"] = str(width)
|
57
|
+
try:
|
58
|
+
yield
|
59
|
+
finally:
|
60
|
+
if old_width is None:
|
61
|
+
del os.environ["COLUMNS"]
|
62
|
+
else:
|
63
|
+
os.environ["COLUMNS"] = old_width
|
64
|
+
else:
|
65
|
+
yield
|
66
|
+
|
67
|
+
|
68
|
+
class ExecutionError(Exception):
|
69
|
+
"""Exception raised for errors during execution of a code block.
|
70
|
+
|
71
|
+
Attributes:
|
72
|
+
message: The exception message.
|
73
|
+
returncode: The code returned by the execution of the code block.
|
74
|
+
"""
|
75
|
+
|
76
|
+
def __init__(self, message: str, returncode: int | None = None) -> None:
|
77
|
+
super().__init__(message)
|
78
|
+
self.returncode = returncode
|
79
|
+
"""The code returned by the execution of the code block."""
|
80
|
+
|
81
|
+
|
82
|
+
def _format_log_details(details: str, *, strip_fences: bool = False) -> str:
|
83
|
+
if strip_fences:
|
84
|
+
lines = details.split("\n")
|
85
|
+
if lines[0].startswith("```") and lines[-1].startswith("```"):
|
86
|
+
details = "\n".join(lines[1:-1])
|
87
|
+
return indent(details, " " * 2)
|
88
|
+
|
89
|
+
|
90
|
+
def base_format(
|
91
|
+
*,
|
92
|
+
language: str,
|
93
|
+
run: Callable,
|
94
|
+
code: str,
|
95
|
+
md: Markdown,
|
96
|
+
html: bool = False,
|
97
|
+
source: str = "",
|
98
|
+
result: str = "",
|
99
|
+
tabs: tuple[str, str] = default_tabs,
|
100
|
+
id: str = "", # noqa: A002
|
101
|
+
id_prefix: str | None = None,
|
102
|
+
returncode: int = 0,
|
103
|
+
transform_source: Callable[[str], tuple[str, str]] | None = None,
|
104
|
+
session: str | None = None,
|
105
|
+
update_toc: bool = True,
|
106
|
+
workdir: str | None = None,
|
107
|
+
width: int | None = None,
|
108
|
+
**options: Any,
|
109
|
+
) -> Markup:
|
110
|
+
"""Execute code and return HTML.
|
111
|
+
|
112
|
+
Parameters:
|
113
|
+
language: The code language.
|
114
|
+
run: Function that runs code and returns output.
|
115
|
+
code: The code to execute.
|
116
|
+
md: The Markdown instance.
|
117
|
+
html: Whether to inject output as HTML directly, without rendering.
|
118
|
+
source: Whether to show source as well, and where.
|
119
|
+
result: If provided, use as language to format result in a code block.
|
120
|
+
tabs: Titles of tabs (if used).
|
121
|
+
id: An optional ID for the code block (useful when warning about errors).
|
122
|
+
id_prefix: A string used to prefix HTML ids in the generated HTML.
|
123
|
+
returncode: The expected exit code.
|
124
|
+
transform_source: An optional callable that returns transformed versions of the source.
|
125
|
+
The input source is the one that is ran, the output source is the one that is
|
126
|
+
rendered (when the source option is enabled).
|
127
|
+
session: A session name, to persist state between executed code blocks.
|
128
|
+
update_toc: Whether to include generated headings
|
129
|
+
into the Markdown table of contents (toc extension).
|
130
|
+
workdir: The working directory to use for the execution.
|
131
|
+
**options: Additional options passed from the formatter.
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
HTML contents.
|
135
|
+
"""
|
136
|
+
markdown = MarkdownConverter(md, update_toc=update_toc)
|
137
|
+
extra = options.get("extra", {})
|
138
|
+
|
139
|
+
if transform_source:
|
140
|
+
source_input, source_output = transform_source(code)
|
141
|
+
else:
|
142
|
+
source_input = code
|
143
|
+
source_output = code
|
144
|
+
|
145
|
+
try:
|
146
|
+
with working_directory(workdir), console_width(width):
|
147
|
+
output = run(source_input, returncode=returncode, session=session, id=id, **extra)
|
148
|
+
except ExecutionError as error:
|
149
|
+
identifier = id or extra.get("title", "")
|
150
|
+
identifier = identifier and f"'{identifier}' "
|
151
|
+
exit_message = "errors" if error.returncode is None else f"unexpected code {error.returncode}"
|
152
|
+
log_message = (
|
153
|
+
f"Execution of {language} code block {identifier}exited with {exit_message}\n\n"
|
154
|
+
f"Code block is:\n\n{_format_log_details(source_input)}\n\n"
|
155
|
+
f"Output is:\n\n{_format_log_details(str(error), strip_fences=True)}\n"
|
156
|
+
)
|
157
|
+
_logger.warning(log_message)
|
158
|
+
return markdown.convert(str(error))
|
159
|
+
|
160
|
+
if not output and not source:
|
161
|
+
return Markup()
|
162
|
+
|
163
|
+
if html:
|
164
|
+
if source:
|
165
|
+
placeholder = f'<div class="{uuid4()}"></div>'
|
166
|
+
wrapped_output = add_source(
|
167
|
+
source=source_output,
|
168
|
+
location=source,
|
169
|
+
output=placeholder,
|
170
|
+
language=language,
|
171
|
+
tabs=tabs,
|
172
|
+
**extra,
|
173
|
+
)
|
174
|
+
return markdown.convert(wrapped_output, stash={placeholder: output})
|
175
|
+
return Markup(output) # noqa: S704
|
176
|
+
|
177
|
+
wrapped_output = output
|
178
|
+
if result and source != "console":
|
179
|
+
wrapped_output = code_block(result, output)
|
180
|
+
if source:
|
181
|
+
wrapped_output = add_source(
|
182
|
+
source=source_output,
|
183
|
+
location=source,
|
184
|
+
output=wrapped_output,
|
185
|
+
language=language,
|
186
|
+
tabs=tabs,
|
187
|
+
result=result,
|
188
|
+
**extra,
|
189
|
+
)
|
190
|
+
prefix = id_prefix if id_prefix is not None else (f"{id}-" if id else None)
|
191
|
+
return markdown.convert(wrapped_output, id_prefix=prefix)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Formatter for executing shell code.
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import subprocess
|
6
|
+
from typing import Any
|
7
|
+
|
8
|
+
from markdown_exec._internal.formatters.base import ExecutionError, base_format
|
9
|
+
from markdown_exec._internal.rendering import code_block
|
10
|
+
|
11
|
+
|
12
|
+
def _run_bash(
|
13
|
+
code: str,
|
14
|
+
returncode: int | None = None,
|
15
|
+
session: str | None = None, # noqa: ARG001
|
16
|
+
id: str | None = None, # noqa: A002,ARG001
|
17
|
+
**extra: str,
|
18
|
+
) -> str:
|
19
|
+
process = subprocess.run( # noqa: S603
|
20
|
+
["bash", "-c", code], # noqa: S607
|
21
|
+
stdout=subprocess.PIPE,
|
22
|
+
stderr=subprocess.STDOUT,
|
23
|
+
text=True,
|
24
|
+
check=False,
|
25
|
+
)
|
26
|
+
if process.returncode != returncode:
|
27
|
+
raise ExecutionError(code_block("sh", process.stdout, **extra), process.returncode)
|
28
|
+
return process.stdout
|
29
|
+
|
30
|
+
|
31
|
+
def _format_bash(**kwargs: Any) -> str:
|
32
|
+
return base_format(language="bash", run=_run_bash, **kwargs)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Formatter for executing shell console code.
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import textwrap
|
6
|
+
from typing import TYPE_CHECKING, Any
|
7
|
+
|
8
|
+
from markdown_exec._internal.formatters.base import base_format
|
9
|
+
from markdown_exec._internal.formatters.sh import _run_sh
|
10
|
+
from markdown_exec._internal.logger import get_logger
|
11
|
+
|
12
|
+
if TYPE_CHECKING:
|
13
|
+
from markupsafe import Markup
|
14
|
+
|
15
|
+
_logger = get_logger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
def _transform_source(code: str) -> tuple[str, str]:
|
19
|
+
sh_lines = []
|
20
|
+
for line in code.split("\n"):
|
21
|
+
prompt = line[:2]
|
22
|
+
if prompt in {"$ ", "% "}:
|
23
|
+
sh_lines.append(line[2:])
|
24
|
+
sh_code = "\n".join(sh_lines)
|
25
|
+
return sh_code, textwrap.indent(sh_code, prompt)
|
26
|
+
|
27
|
+
|
28
|
+
def _format_console(**kwargs: Any) -> Markup:
|
29
|
+
return base_format(language="console", run=_run_sh, transform_source=_transform_source, **kwargs)
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# Formatter for literate Markdown.
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from markdown_exec._internal.formatters.base import base_format
|
8
|
+
|
9
|
+
|
10
|
+
def _format_markdown(**kwargs: Any) -> str:
|
11
|
+
return base_format(language="md", run=lambda code, **_: code, **kwargs)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Formatter for executing `pycon` code.
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import TYPE_CHECKING, Any
|
6
|
+
|
7
|
+
from markdown_exec._internal.formatters.base import base_format
|
8
|
+
from markdown_exec._internal.formatters.python import _run_python
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from markupsafe import Markup
|
12
|
+
|
13
|
+
|
14
|
+
def _transform_source(code: str) -> tuple[str, str]:
|
15
|
+
python_lines = []
|
16
|
+
pycon_lines = []
|
17
|
+
for line in code.split("\n"):
|
18
|
+
if line.startswith((">>> ", "... ")):
|
19
|
+
pycon_lines.append(line)
|
20
|
+
python_lines.append(line[4:])
|
21
|
+
python_code = "\n".join(python_lines)
|
22
|
+
return python_code, "\n".join(pycon_lines)
|
23
|
+
|
24
|
+
|
25
|
+
def _format_pycon(**kwargs: Any) -> Markup:
|
26
|
+
return base_format(language="pycon", run=_run_python, transform_source=_transform_source, **kwargs)
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# Formatter for creating a Pyodide interactive editor.
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import TYPE_CHECKING, Any
|
6
|
+
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from markdown import Markdown
|
9
|
+
|
10
|
+
# All Ace.js themes listed here:
|
11
|
+
# https://github.com/ajaxorg/ace/tree/master/src/theme
|
12
|
+
|
13
|
+
_play_emoji = (
|
14
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 5.14v14l11-7-11-7Z"></path></svg>'
|
15
|
+
)
|
16
|
+
_clear_emoji = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.14 3c-.51 0-1.02.2-1.41.59L2.59 14.73c-.78.77-.78 2.04 0 2.83L5.03 20h7.66l8.72-8.73c.79-.77.79-2.04 0-2.83l-4.85-4.85c-.39-.39-.91-.59-1.42-.59M17 18l-2 2h7v-2"></path></svg>'
|
17
|
+
|
18
|
+
_assets = """
|
19
|
+
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.16.0/ace.js"></script>
|
20
|
+
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
21
|
+
<script type="text/javascript" src="https://cdn.jsdelivr.net/pyodide/v{version}/full/pyodide.js"></script>
|
22
|
+
<link title="light" rel="alternate stylesheet" href="https://cdn.jsdelivr.net/npm/highlightjs-themes@1.0.0/tomorrow.min.css" disabled="disabled">
|
23
|
+
<link title="dark" rel="alternate stylesheet" href="https://cdn.jsdelivr.net/npm/highlightjs-themes@1.0.0/tomorrow-night-blue.min.css" disabled="disabled">
|
24
|
+
"""
|
25
|
+
|
26
|
+
_template = """
|
27
|
+
<div class="pyodide">
|
28
|
+
<div class="pyodide-editor-bar">
|
29
|
+
<span class="pyodide-bar-item">Editor (session: %(session)s)</span><span id="%(id_prefix)srun" title="Run: press Ctrl-Enter" class="pyodide-bar-item pyodide-clickable"><span class="twemoji">%(play_emoji)s</span> Run</span>
|
30
|
+
</div>
|
31
|
+
<div><pre id="%(id_prefix)seditor" class="pyodide-editor">%(initial_code)s</pre></div>
|
32
|
+
<div class="pyodide-editor-bar">
|
33
|
+
<span class="pyodide-bar-item">Output</span><span id="%(id_prefix)sclear" class="pyodide-bar-item pyodide-clickable"><span class="twemoji">%(clear_emoji)s</span> Clear</span>
|
34
|
+
</div>
|
35
|
+
<pre><code id="%(id_prefix)soutput" class="pyodide-output"></code></pre>
|
36
|
+
</div>
|
37
|
+
|
38
|
+
<script>
|
39
|
+
document.addEventListener('DOMContentLoaded', (event) => {
|
40
|
+
setupPyodide('%(id_prefix)s', install=%(install)s, themeLight='%(theme_light)s', themeDark='%(theme_dark)s', session='%(session)s');
|
41
|
+
});
|
42
|
+
</script>
|
43
|
+
"""
|
44
|
+
|
45
|
+
_counter = 0
|
46
|
+
|
47
|
+
|
48
|
+
def _format_pyodide(code: str, md: Markdown, session: str, extra: dict, **options: Any) -> str: # noqa: ARG001
|
49
|
+
global _counter # noqa: PLW0603
|
50
|
+
_counter += 1
|
51
|
+
version = extra.pop("version", "0.26.4").lstrip("v")
|
52
|
+
install = extra.pop("install", "")
|
53
|
+
install = install.split(",") if install else []
|
54
|
+
exclude_assets = extra.pop("assets", "1").lower() in {"0", "false", "no", "off"}
|
55
|
+
theme = extra.pop("theme", "tomorrow,tomorrow_night")
|
56
|
+
if "," not in theme:
|
57
|
+
theme = f"{theme},{theme}"
|
58
|
+
theme_light, theme_dark = theme.split(",")
|
59
|
+
|
60
|
+
data = {
|
61
|
+
"id_prefix": f"exec-{_counter}--",
|
62
|
+
"initial_code": code,
|
63
|
+
"install": install,
|
64
|
+
"theme_light": theme_light.strip(),
|
65
|
+
"theme_dark": theme_dark.strip(),
|
66
|
+
"session": session or "default",
|
67
|
+
"play_emoji": _play_emoji,
|
68
|
+
"clear_emoji": _clear_emoji,
|
69
|
+
}
|
70
|
+
rendered = _template % data
|
71
|
+
if exclude_assets:
|
72
|
+
return rendered
|
73
|
+
return _assets.format(version=version) + rendered
|