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.
Files changed (43) hide show
  1. markdown_exec/__init__.py +50 -138
  2. markdown_exec/_internal/__init__.py +0 -0
  3. markdown_exec/{debug.py → _internal/debug.py} +13 -15
  4. markdown_exec/_internal/formatters/__init__.py +1 -0
  5. markdown_exec/{formatters → _internal/formatters}/_exec_python.py +1 -1
  6. markdown_exec/_internal/formatters/base.py +191 -0
  7. markdown_exec/_internal/formatters/bash.py +32 -0
  8. markdown_exec/_internal/formatters/console.py +29 -0
  9. markdown_exec/_internal/formatters/markdown.py +11 -0
  10. markdown_exec/_internal/formatters/pycon.py +26 -0
  11. markdown_exec/_internal/formatters/pyodide.py +73 -0
  12. markdown_exec/_internal/formatters/python.py +85 -0
  13. markdown_exec/_internal/formatters/sh.py +32 -0
  14. markdown_exec/_internal/formatters/tree.py +60 -0
  15. markdown_exec/_internal/logger.py +89 -0
  16. markdown_exec/_internal/main.py +123 -0
  17. markdown_exec/_internal/mkdocs_plugin.py +143 -0
  18. markdown_exec/_internal/processors.py +136 -0
  19. markdown_exec/_internal/rendering.py +280 -0
  20. markdown_exec/{pyodide.js → assets/pyodide.js} +12 -6
  21. markdown_exec/formatters/__init__.py +17 -1
  22. markdown_exec/formatters/base.py +12 -183
  23. markdown_exec/formatters/bash.py +10 -25
  24. markdown_exec/formatters/console.py +12 -24
  25. markdown_exec/formatters/markdown.py +11 -5
  26. markdown_exec/formatters/pycon.py +12 -24
  27. markdown_exec/formatters/pyodide.py +12 -65
  28. markdown_exec/formatters/python.py +11 -79
  29. markdown_exec/formatters/sh.py +10 -25
  30. markdown_exec/formatters/tree.py +12 -55
  31. markdown_exec/logger.py +12 -87
  32. markdown_exec/mkdocs_plugin.py +11 -135
  33. markdown_exec/processors.py +12 -118
  34. markdown_exec/rendering.py +11 -270
  35. {markdown_exec-1.10.1.dist-info → markdown_exec-1.10.2.dist-info}/METADATA +4 -3
  36. markdown_exec-1.10.2.dist-info/RECORD +42 -0
  37. markdown_exec-1.10.2.dist-info/entry_points.txt +7 -0
  38. markdown_exec-1.10.1.dist-info/RECORD +0 -26
  39. markdown_exec-1.10.1.dist-info/entry_points.txt +0 -7
  40. /markdown_exec/{ansi.css → assets/ansi.css} +0 -0
  41. /markdown_exec/{pyodide.css → assets/pyodide.css} +0 -0
  42. {markdown_exec-1.10.1.dist-info → markdown_exec-1.10.2.dist-info}/WHEEL +0 -0
  43. {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
- # https://facelessuser.github.io/pymdown-extensions/extensions/superfences/#custom-fences
7
- # https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/#snippets
8
-
9
- from __future__ import annotations
10
-
11
- import os
12
- import re
13
- from typing import TYPE_CHECKING, Any
14
-
15
- if TYPE_CHECKING:
16
- from markdown import Markdown
17
-
18
- from markdown_exec.formatters.base import default_tabs
19
- from markdown_exec.formatters.bash import _format_bash
20
- from markdown_exec.formatters.console import _format_console
21
- from markdown_exec.formatters.markdown import _format_markdown
22
- from markdown_exec.formatters.pycon import _format_pycon
23
- from markdown_exec.formatters.pyodide import _format_pyodide
24
- from markdown_exec.formatters.python import _format_python
25
- from markdown_exec.formatters.sh import _format_sh
26
- from markdown_exec.formatters.tree import _format_tree
27
-
28
- __all__: list[str] = ["formatter", "validator"]
29
-
30
- MARKDOWN_EXEC_AUTO = [lang.strip() for lang in os.getenv("MARKDOWN_EXEC_AUTO", "").split(",")]
31
-
32
- formatters = {
33
- "bash": _format_bash,
34
- "console": _format_console,
35
- "md": _format_markdown,
36
- "markdown": _format_markdown,
37
- "py": _format_python,
38
- "python": _format_python,
39
- "pycon": _format_pycon,
40
- "pyodide": _format_pyodide,
41
- "sh": _format_sh,
42
- "tree": _format_tree,
43
- }
44
-
45
- # negative look behind: matches only if | (pipe) if not preceded by \ (backslash)
46
- _tabs_re = re.compile(r"(?<!\\)\|")
47
-
48
-
49
- def validator(
50
- language: str,
51
- inputs: dict[str, str],
52
- options: dict[str, Any],
53
- attrs: dict[str, Any], # noqa: ARG001
54
- md: Markdown, # noqa: ARG001
55
- ) -> bool:
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 Variable:
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 Package:
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 Environment:
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[Package]
42
+ packages: list[_Package]
45
43
  """Installed packages."""
46
- variables: list[Variable]
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 get_version(dist: str = "markdown-exec") -> str:
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 get_debug_info() -> Environment:
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 Environment(
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=[Variable(var, val) for var in variables if (val := os.getenv(var))],
91
- packages=[Package(pkg, get_version(pkg)) for pkg in 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 print_debug_info() -> None:
93
+ def _print_debug_info() -> None:
96
94
  """Print debug/environment information."""
97
- info = get_debug_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
- print_debug_info()
107
+ _print_debug_info()
@@ -0,0 +1 @@
1
+ # This subpackage contains all the formatters.
@@ -1,4 +1,4 @@
1
- """Special module without future annotations for executing Python code."""
1
+ # Special module without future annotations for executing Python code.
2
2
 
3
3
  from typing import Any, Optional
4
4
 
@@ -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