'
sponsors.forEach(function (sponsor) {
html += `
`
});
html += '
{code}
"))
markdown-exec-1.8.0/docs/snippets/gallery/runpy.py 0000664 0000000 0000000 00000000676 14546046442 0022271 0 ustar 00root root 0000000 0000000 import sys
import warnings
from contextlib import suppress
from io import StringIO
from runpy import run_module
old_argv = list(sys.argv)
sys.argv = ["mkdocs"]
old_stdout = sys.stdout
sys.stdout = StringIO()
warnings.filterwarnings("ignore", category=RuntimeWarning)
with suppress(SystemExit):
run_module("mkdocs", run_name="__main__")
output = sys.stdout.getvalue()
sys.stdout = old_stdout
sys.argv = old_argv
print(f"```\n{output}\n```")
markdown-exec-1.8.0/docs/snippets/gallery/textual.py 0000664 0000000 0000000 00000001132 14546046442 0022566 0 ustar 00root root 0000000 0000000 from textual.app import App, ComposeResult
from textual.widgets import Static
from textual._doc import take_svg_screenshot
class TextApp(App):
CSS = """
Screen {
background: darkblue;
color: white;
layout: vertical;
}
Static {
height: auto;
padding: 2;
border: heavy white;
background: #ffffff 30%;
content-align: center middle;
}
"""
def compose(self) -> ComposeResult:
yield Static("Hello")
yield Static("[b]World![/b]")
print(take_svg_screenshot(app=TextApp(), terminal_size=(80, 24)))
markdown-exec-1.8.0/docs/snippets/usage/ 0000775 0000000 0000000 00000000000 14546046442 0020176 5 ustar 00root root 0000000 0000000 markdown-exec-1.8.0/docs/snippets/usage/boolean_matrix.py 0000664 0000000 0000000 00000000244 14546046442 0023553 0 ustar 00root root 0000000 0000000 print()
print("a | b | a \\|\\| b")
print("--- | --- | ---")
for a in (True, False):
for b in (True, False):
print(f"{a} | {b} | {a or b}")
print()
markdown-exec-1.8.0/docs/snippets/usage/hide.py 0000664 0000000 0000000 00000000073 14546046442 0021461 0 ustar 00root root 0000000 0000000 print("Hello World!")
print("{platform.machine()}
{platform.version()}
{platform.platform()}
{platform.system()}
'
src = Path(__file__).parent.parent / "src"
for path in sorted(src.rglob("*.py")):
module_path = path.relative_to(src).with_suffix("")
doc_path = path.relative_to(src).with_suffix(".md")
full_doc_path = Path("reference", doc_path)
parts = tuple(module_path.parts)
if parts[-1] == "__init__":
parts = parts[:-1]
doc_path = doc_path.with_name("index.md")
full_doc_path = full_doc_path.with_name("index.md")
elif parts[-1].startswith("_"):
continue
nav_parts = [f"{mod_symbol} {part}" for part in parts]
nav[tuple(nav_parts)] = doc_path.as_posix()
with mkdocs_gen_files.open(full_doc_path, "w") as fd:
ident = ".".join(parts)
fd.write(f"::: {ident}")
mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path)
with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file:
nav_file.writelines(nav.build_literate_nav())
markdown-exec-1.8.0/scripts/insiders.py 0000664 0000000 0000000 00000014573 14546046442 0020170 0 ustar 00root root 0000000 0000000 """Functions related to Insiders funding goals."""
from __future__ import annotations
import json
import logging
import os
import posixpath
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from itertools import chain
from pathlib import Path
from typing import Iterable, cast
from urllib.error import HTTPError
from urllib.parse import urljoin
from urllib.request import urlopen
import yaml
logger = logging.getLogger(f"mkdocs.logs.{__name__}")
def human_readable_amount(amount: int) -> str: # noqa: D103
str_amount = str(amount)
if len(str_amount) >= 4: # noqa: PLR2004
return f"{str_amount[:len(str_amount)-3]},{str_amount[-3:]}"
return str_amount
@dataclass
class Project:
"""Class representing an Insiders project."""
name: str
url: str
@dataclass
class Feature:
"""Class representing an Insiders feature."""
name: str
ref: str | None
since: date | None
project: Project | None
def url(self, rel_base: str = "..") -> str | None: # noqa: D102
if not self.ref:
return None
if self.project:
rel_base = self.project.url
return posixpath.join(rel_base, self.ref.lstrip("/"))
def render(self, rel_base: str = "..", *, badge: bool = False) -> None: # noqa: D102
new = ""
if badge:
recent = self.since and date.today() - self.since <= timedelta(days=60) # noqa: DTZ011
if recent:
ft_date = self.since.strftime("%B %d, %Y") # type: ignore[union-attr]
new = f' :material-alert-decagram:{{ .new-feature .vibrate title="Added on {ft_date}" }}'
project = f"[{self.project.name}]({self.project.url}) — " if self.project else ""
feature = f"[{self.name}]({self.url(rel_base)})" if self.ref else self.name
print(f"- [{'x' if self.since else ' '}] {project}{feature}{new}")
@dataclass
class Goal:
"""Class representing an Insiders goal."""
name: str
amount: int
features: list[Feature]
complete: bool = False
@property
def human_readable_amount(self) -> str: # noqa: D102
return human_readable_amount(self.amount)
def render(self, rel_base: str = "..") -> None: # noqa: D102
print(f"#### $ {self.human_readable_amount} — {self.name}\n")
for feature in self.features:
feature.render(rel_base)
print("")
def load_goals(data: str, funding: int = 0, project: Project | None = None) -> dict[int, Goal]:
"""Load goals from JSON data.
Parameters:
data: The JSON data.
funding: The current total funding, per month.
origin: The origin of the data (URL).
Returns:
A dictionaries of goals, keys being their target monthly amount.
"""
goals_data = yaml.safe_load(data)["goals"]
return {
amount: Goal(
name=goal_data["name"],
amount=amount,
complete=funding >= amount,
features=[
Feature(
name=feature_data["name"],
ref=feature_data.get("ref"),
since=feature_data.get("since")
and datetime.strptime(feature_data["since"], "%Y/%m/%d").date(), # noqa: DTZ007
project=project,
)
for feature_data in goal_data["features"]
],
)
for amount, goal_data in goals_data.items()
}
def _load_goals_from_disk(path: str, funding: int = 0) -> dict[int, Goal]:
project_dir = os.getenv("MKDOCS_CONFIG_DIR", ".")
try:
data = Path(project_dir, path).read_text()
except OSError as error:
raise RuntimeError(f"Could not load data from disk: {path}") from error
return load_goals(data, funding)
def _load_goals_from_url(source_data: tuple[str, str, str], funding: int = 0) -> dict[int, Goal]:
project_name, project_url, data_fragment = source_data
data_url = urljoin(project_url, data_fragment)
try:
with urlopen(data_url) as response: # noqa: S310
data = response.read()
except HTTPError as error:
raise RuntimeError(f"Could not load data from network: {data_url}") from error
return load_goals(data, funding, project=Project(name=project_name, url=project_url))
def _load_goals(source: str | tuple[str, str, str], funding: int = 0) -> dict[int, Goal]:
if isinstance(source, str):
return _load_goals_from_disk(source, funding)
return _load_goals_from_url(source, funding)
def funding_goals(source: str | list[str | tuple[str, str, str]], funding: int = 0) -> dict[int, Goal]:
"""Load funding goals from a given data source.
Parameters:
source: The data source (local file path or URL).
funding: The current total funding, per month.
Returns:
A dictionaries of goals, keys being their target monthly amount.
"""
if isinstance(source, str):
return _load_goals_from_disk(source, funding)
goals = {}
for src in source:
source_goals = _load_goals(src)
for amount, goal in source_goals.items():
if amount not in goals:
goals[amount] = goal
else:
goals[amount].features.extend(goal.features)
return {amount: goals[amount] for amount in sorted(goals)}
def feature_list(goals: Iterable[Goal]) -> list[Feature]:
"""Extract feature list from funding goals.
Parameters:
goals: A list of funding goals.
Returns:
A list of features.
"""
return list(chain.from_iterable(goal.features for goal in goals))
def load_json(url: str) -> str | list | dict: # noqa: D103
with urlopen(url) as response: # noqa: S310
return json.loads(response.read().decode())
data_source = globals()["data_source"]
sponsor_url = "https://github.com/sponsors/pawamoy"
data_url = "https://raw.githubusercontent.com/pawamoy/sponsors/main"
numbers: dict[str, int] = load_json(f"{data_url}/numbers.json") # type: ignore[assignment]
sponsors: list[dict] = load_json(f"{data_url}/sponsors.json") # type: ignore[assignment]
current_funding = numbers["total"]
sponsors_count = numbers["count"]
goals = funding_goals(data_source, funding=current_funding)
ongoing_goals = [goal for goal in goals.values() if not goal.complete]
unreleased_features = sorted(
(ft for ft in feature_list(ongoing_goals) if ft.since),
key=lambda ft: cast(date, ft.since),
reverse=True,
)
markdown-exec-1.8.0/scripts/setup.sh 0000775 0000000 0000000 00000000602 14546046442 0017461 0 ustar 00root root 0000000 0000000 #!/usr/bin/env bash
set -e
if ! command -v pdm &>/dev/null; then
if ! command -v pipx &>/dev/null; then
python3 -m pip install --user pipx
fi
pipx install pdm
fi
if ! pdm self list 2>/dev/null | grep -q pdm-multirun; then
pdm install --plugins
fi
if [ -n "${PDM_MULTIRUN_VERSIONS}" ]; then
pdm multirun -v pdm install -G:all
else
pdm install -G:all
fi
markdown-exec-1.8.0/src/ 0000775 0000000 0000000 00000000000 14546046442 0015064 5 ustar 00root root 0000000 0000000 markdown-exec-1.8.0/src/markdown_exec/ 0000775 0000000 0000000 00000000000 14546046442 0017712 5 ustar 00root root 0000000 0000000 markdown-exec-1.8.0/src/markdown_exec/__init__.py 0000664 0000000 0000000 00000010416 14546046442 0022025 0 ustar 00root root 0000000 0000000 """Markdown Exec package.
Utilities to execute code blocks in Markdown files.
"""
# https://facelessuser.github.io/pymdown-extensions/extensions/superfences/#custom-fences
# https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/#snippets
from __future__ import annotations
import re
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from markdown import Markdown
from markdown_exec.formatters.base import default_tabs
from markdown_exec.formatters.bash import _format_bash
from markdown_exec.formatters.console import _format_console
from markdown_exec.formatters.markdown import _format_markdown
from markdown_exec.formatters.pycon import _format_pycon
from markdown_exec.formatters.pyodide import _format_pyodide
from markdown_exec.formatters.python import _format_python
from markdown_exec.formatters.sh import _format_sh
from markdown_exec.formatters.tree import _format_tree
__all__: list[str] = ["formatter", "validator"]
formatters = {
"bash": _format_bash,
"console": _format_console,
"md": _format_markdown,
"markdown": _format_markdown,
"py": _format_python,
"python": _format_python,
"pycon": _format_pycon,
"pyodide": _format_pyodide,
"sh": _format_sh,
"tree": _format_tree,
}
# negative look behind: matches only if | (pipe) if not preceded by \ (backslash)
_tabs_re = re.compile(r"(? bool:
"""Validate code blocks inputs.
Parameters:
language: The code language, like python or bash.
inputs: The code block inputs, to be sorted into options and attrs.
options: The container for options.
attrs: The container for attrs:
md: The Markdown instance.
Returns:
Success or not.
"""
exec_value = _to_bool(inputs.pop("exec", "no"))
if language not in {"tree", "pyodide"} and not exec_value:
return False
id_value = inputs.pop("id", "")
id_prefix_value = inputs.pop("idprefix", None)
html_value = _to_bool(inputs.pop("html", "no"))
source_value = inputs.pop("source", "")
result_value = inputs.pop("result", "")
returncode_value = int(inputs.pop("returncode", "0"))
session_value = inputs.pop("session", "")
update_toc_value = _to_bool(inputs.pop("updatetoc", "yes"))
tabs_value = inputs.pop("tabs", "|".join(default_tabs))
tabs = tuple(_tabs_re.split(tabs_value, maxsplit=1))
options["id"] = id_value
options["id_prefix"] = id_prefix_value
options["html"] = html_value
options["source"] = source_value
options["result"] = result_value
options["returncode"] = returncode_value
options["session"] = session_value
options["update_toc"] = update_toc_value
options["tabs"] = tabs
options["extra"] = inputs
return True
def formatter(
source: str,
language: str,
css_class: str, # noqa: ARG001
options: dict[str, Any],
md: Markdown,
classes: list[str] | None = None, # noqa: ARG001
id_value: str = "", # noqa: ARG001
attrs: dict[str, Any] | None = None, # noqa: ARG001
**kwargs: Any, # noqa: ARG001
) -> str:
"""Execute code and return HTML.
Parameters:
source: The code to execute.
language: The code language, like python or bash.
css_class: The CSS class to add to the HTML element.
options: The container for options.
attrs: The container for attrs:
md: The Markdown instance.
classes: Additional CSS classes.
id_value: An optional HTML id.
attrs: Additional attributes
**kwargs: Additional arguments passed to SuperFences default formatters.
Returns:
HTML contents.
"""
fmt = formatters.get(language, lambda source, **kwargs: source)
return fmt(code=source, md=md, **options) # type: ignore[operator]
falsy_values = {"", "no", "off", "false", "0"}
truthy_values = {"yes", "on", "true", "1"}
def _to_bool(value: str) -> bool:
return value.lower() not in falsy_values
def _to_bool_or_value(value: str) -> bool | str:
if value.lower() in falsy_values:
return False
if value.lower() in truthy_values:
return True
return value
markdown-exec-1.8.0/src/markdown_exec/ansi.css 0000664 0000000 0000000 00000013031 14546046442 0021354 0 ustar 00root root 0000000 0000000 /*
Inspired by https://spec.draculatheme.com/ specification, they should work
decently with both dark and light themes.
*/
:root {
--ansi-red: #ff5555;
--ansi-green: #50fa7b;
--ansi-blue: #265285;
--ansi-yellow: #ffb86c;
--ansi-magenta: #bd93f9;
--ansi-cyan: #8be9fd;
--ansi-black: #282a36;
--ansi-white: #f8f8f2;
}
.-Color-Green,
.-Color-Faint-Green,
.-Color-Bold-Green {
color: var(--ansi-green);
}
.-Color-Red,
.-Color-Faint-Red,
.-Color-Bold-Red {
color: var(--ansi-red);
}
.-Color-Yellow,
.-Color-Faint-Yellow,
.-Color-Bold-Yellow {
color: var(--ansi-yellow);
}
.-Color-Blue,
.-Color-Faint-Blue,
.-Color-Bold-Blue {
color: var(--ansi-blue);
}
.-Color-Magenta,
.-Color-Faint-Magenta,
.-Color-Bold-Magenta {
color: var(--ansi-magenta);
}
.-Color-Cyan,
.-Color-Faint-Cyan,
.-Color-Bold-Cyan {
color: var(--ansi-cyan);
}
.-Color-White,
.-Color-Faint-White,
.-Color-Bold-White {
color: var(--ansi-white);
}
.-Color-Black,
.-Color-Faint-Black,
.-Color-Bold-Black {
color: var(--ansi-black);
}
.-Color-Faint {
opacity: 0.5;
}
.-Color-Bold {
font-weight: bold;
}
.-Color-BGBlack,
.-Color-Black-BGBlack,
.-Color-Blue-BGBlack,
.-Color-Bold-BGBlack,
.-Color-Bold-Black-BGBlack,
.-Color-Bold-Green-BGBlack,
.-Color-Bold-Cyan-BGBlack,
.-Color-Bold-Blue-BGBlack,
.-Color-Bold-Magenta-BGBlack,
.-Color-Bold-Red-BGBlack,
.-Color-Bold-White-BGBlack,
.-Color-Bold-Yellow-BGBlack,
.-Color-Cyan-BGBlack,
.-Color-Green-BGBlack,
.-Color-Magenta-BGBlack,
.-Color-Red-BGBlack,
.-Color-White-BGBlack,
.-Color-Yellow-BGBlack {
background-color: var(--ansi-black);
}
.-Color-BGRed,
.-Color-Black-BGRed,
.-Color-Blue-BGRed,
.-Color-Bold-BGRed,
.-Color-Bold-Black-BGRed,
.-Color-Bold-Green-BGRed,
.-Color-Bold-Cyan-BGRed,
.-Color-Bold-Blue-BGRed,
.-Color-Bold-Magenta-BGRed,
.-Color-Bold-Red-BGRed,
.-Color-Bold-White-BGRed,
.-Color-Bold-Yellow-BGRed,
.-Color-Cyan-BGRed,
.-Color-Green-BGRed,
.-Color-Magenta-BGRed,
.-Color-Red-BGRed,
.-Color-White-BGRed,
.-Color-Yellow-BGRed {
background-color: var(--ansi-red);
}
.-Color-BGGreen,
.-Color-Black-BGGreen,
.-Color-Blue-BGGreen,
.-Color-Bold-BGGreen,
.-Color-Bold-Black-BGGreen,
.-Color-Bold-Green-BGGreen,
.-Color-Bold-Cyan-BGGreen,
.-Color-Bold-Blue-BGGreen,
.-Color-Bold-Magenta-BGGreen,
.-Color-Bold-Red-BGGreen,
.-Color-Bold-White-BGGreen,
.-Color-Bold-Yellow-BGGreen,
.-Color-Cyan-BGGreen,
.-Color-Green-BGGreen,
.-Color-Magenta-BGGreen,
.-Color-Red-BGGreen,
.-Color-White-BGGreen,
.-Color-Yellow-BGGreen {
background-color: var(--ansi-green);
}
.-Color-BGYellow,
.-Color-Black-BGYellow,
.-Color-Blue-BGYellow,
.-Color-Bold-BGYellow,
.-Color-Bold-Black-BGYellow,
.-Color-Bold-Green-BGYellow,
.-Color-Bold-Cyan-BGYellow,
.-Color-Bold-Blue-BGYellow,
.-Color-Bold-Magenta-BGYellow,
.-Color-Bold-Red-BGYellow,
.-Color-Bold-White-BGYellow,
.-Color-Bold-Yellow-BGYellow,
.-Color-Cyan-BGYellow,
.-Color-Green-BGYellow,
.-Color-Magenta-BGYellow,
.-Color-Red-BGYellow,
.-Color-White-BGYellow,
.-Color-Yellow-BGYellow {
background-color: var(--ansi-yellow);
}
.-Color-BGBlue,
.-Color-Black-BGBlue,
.-Color-Blue-BGBlue,
.-Color-Bold-BGBlue,
.-Color-Bold-Black-BGBlue,
.-Color-Bold-Green-BGBlue,
.-Color-Bold-Cyan-BGBlue,
.-Color-Bold-Blue-BGBlue,
.-Color-Bold-Magenta-BGBlue,
.-Color-Bold-Red-BGBlue,
.-Color-Bold-White-BGBlue,
.-Color-Bold-Yellow-BGBlue,
.-Color-Cyan-BGBlue,
.-Color-Green-BGBlue,
.-Color-Magenta-BGBlue,
.-Color-Red-BGBlue,
.-Color-White-BGBlue,
.-Color-Yellow-BGBlue {
background-color: var(--ansi-blue);
}
.-Color-BGMagenta,
.-Color-Black-BGMagenta,
.-Color-Blue-BGMagenta,
.-Color-Bold-BGMagenta,
.-Color-Bold-Black-BGMagenta,
.-Color-Bold-Green-BGMagenta,
.-Color-Bold-Cyan-BGMagenta,
.-Color-Bold-Blue-BGMagenta,
.-Color-Bold-Magenta-BGMagenta,
.-Color-Bold-Red-BGMagenta,
.-Color-Bold-White-BGMagenta,
.-Color-Bold-Yellow-BGMagenta,
.-Color-Cyan-BGMagenta,
.-Color-Green-BGMagenta,
.-Color-Magenta-BGMagenta,
.-Color-Red-BGMagenta,
.-Color-White-BGMagenta,
.-Color-Yellow-BGMagenta {
background-color: var(--ansi-magenta);
}
.-Color-BGCyan,
.-Color-Black-BGCyan,
.-Color-Blue-BGCyan,
.-Color-Bold-BGCyan,
.-Color-Bold-Black-BGCyan,
.-Color-Bold-Green-BGCyan,
.-Color-Bold-Cyan-BGCyan,
.-Color-Bold-Blue-BGCyan,
.-Color-Bold-Magenta-BGCyan,
.-Color-Bold-Red-BGCyan,
.-Color-Bold-White-BGCyan,
.-Color-Bold-Yellow-BGCyan,
.-Color-Cyan-BGCyan,
.-Color-Green-BGCyan,
.-Color-Magenta-BGCyan,
.-Color-Red-BGCyan,
.-Color-White-BGCyan,
.-Color-Yellow-BGCyan {
background-color: var(--ansi-cyan);
}
.-Color-BGWhite,
.-Color-Black-BGWhite,
.-Color-Blue-BGWhite,
.-Color-Bold-BGWhite,
.-Color-Bold-Black-BGWhite,
.-Color-Bold-Green-BGWhite,
.-Color-Bold-Cyan-BGWhite,
.-Color-Bold-Blue-BGWhite,
.-Color-Bold-Magenta-BGWhite,
.-Color-Bold-Red-BGWhite,
.-Color-Bold-White-BGWhite,
.-Color-Bold-Yellow-BGWhite,
.-Color-Cyan-BGWhite,
.-Color-Green-BGWhite,
.-Color-Magenta-BGWhite,
.-Color-Red-BGWhite,
.-Color-White-BGWhite,
.-Color-Yellow-BGWhite {
background-color: var(--ansi-white);
}
.-Color-Black,
.-Color-Bold-Black,
.-Color-Black-BGBlack,
.-Color-Bold-Black-BGBlack,
.-Color-Black-BGGreen,
.-Color-Red-BGRed,
.-Color-Bold-Red-BGRed,
.-Color-Bold-Blue-BGBlue,
.-Color-Blue-BGBlue {
text-shadow: 0 0 1px var(--ansi-white);
}
.-Color-Bold-Cyan-BGCyan,
.-Color-Bold-Magenta-BGMagenta,
.-Color-Bold-White,
.-Color-Bold-Yellow-BGYellow,
.-Color-Bold-Green-BGGreen,
.-Color-Cyan-BGCyan,
.-Color-Cyan-BGGreen,
.-Color-Green-BGCyan,
.-Color-Green-BGGreen,
.-Color-Magenta-BGMagenta,
.-Color-White,
.-Color-White-BGWhite,
.-Color-Yellow-BGYellow {
text-shadow: 0 0 1px var(--ansi-black);
} markdown-exec-1.8.0/src/markdown_exec/debug.py 0000664 0000000 0000000 00000005226 14546046442 0021357 0 ustar 00root root 0000000 0000000 """Debugging utilities."""
from __future__ import annotations
import os
import platform
import sys
from dataclasses import dataclass
from importlib import metadata
@dataclass
class Variable:
"""Dataclass describing an environment variable."""
name: str
"""Variable name."""
value: str
"""Variable value."""
@dataclass
class Package:
"""Dataclass describing a Python package."""
name: str
"""Package name."""
version: str
"""Package version."""
@dataclass
class Environment:
"""Dataclass to store environment information."""
interpreter_name: str
"""Python interpreter name."""
interpreter_version: str
"""Python interpreter version."""
platform: str
"""Operating System."""
packages: list[Package]
"""Installed packages."""
variables: list[Variable]
"""Environment variables."""
def _interpreter_name_version() -> tuple[str, str]:
if hasattr(sys, "implementation"):
impl = sys.implementation.version
version = f"{impl.major}.{impl.minor}.{impl.micro}"
kind = impl.releaselevel
if kind != "final":
version += kind[0] + str(impl.serial)
return sys.implementation.name, version
return "", "0.0.0"
def get_version(dist: str = "markdown-exec") -> str:
"""Get version of the given distribution.
Parameters:
dist: A distribution name.
Returns:
A version number.
"""
try:
return metadata.version(dist)
except metadata.PackageNotFoundError:
return "0.0.0"
def get_debug_info() -> Environment:
"""Get debug/environment information.
Returns:
Environment information.
"""
py_name, py_version = _interpreter_name_version()
packages = ["markdown-exec"]
variables = ["PYTHONPATH", *[var for var in os.environ if var.startswith("MARKDOWN_EXEC")]]
return Environment(
interpreter_name=py_name,
interpreter_version=py_version,
platform=platform.platform(),
variables=[Variable(var, val) for var in variables if (val := os.getenv(var))],
packages=[Package(pkg, get_version(pkg)) for pkg in packages],
)
def print_debug_info() -> None:
"""Print debug/environment information."""
info = get_debug_info()
print(f"- __System__: {info.platform}")
print(f"- __Python__: {info.interpreter_name} {info.interpreter_version}")
print("- __Environment variables__:")
for var in info.variables:
print(f" - `{var.name}`: `{var.value}`")
print("- __Installed packages__:")
for pkg in info.packages:
print(f" - `{pkg.name}` v{pkg.version}")
if __name__ == "__main__":
print_debug_info()
markdown-exec-1.8.0/src/markdown_exec/formatters/ 0000775 0000000 0000000 00000000000 14546046442 0022100 5 ustar 00root root 0000000 0000000 markdown-exec-1.8.0/src/markdown_exec/formatters/__init__.py 0000664 0000000 0000000 00000000063 14546046442 0024210 0 ustar 00root root 0000000 0000000 """This subpackage contains all the formatters."""
markdown-exec-1.8.0/src/markdown_exec/formatters/base.py 0000664 0000000 0000000 00000011231 14546046442 0023362 0 ustar 00root root 0000000 0000000 """Generic formatter for executing code."""
from __future__ import annotations
from textwrap import indent
from typing import TYPE_CHECKING, Any, Callable
from uuid import uuid4
from markupsafe import Markup
from markdown_exec.logger import get_logger
from markdown_exec.rendering import MarkdownConverter, add_source, code_block
if TYPE_CHECKING:
from markdown.core import Markdown
logger = get_logger(__name__)
default_tabs = ("Source", "Result")
class ExecutionError(Exception):
"""Exception raised for errors during execution of a code block.
Attributes:
message: The exception message.
returncode: The code returned by the execution of the code block.
"""
def __init__(self, message: str, returncode: int | None = None) -> None: # noqa: D107
super().__init__(message)
self.returncode = returncode
def _format_log_details(details: str, *, strip_fences: bool = False) -> str:
if strip_fences:
lines = details.split("\n")
if lines[0].startswith("```") and lines[-1].startswith("```"):
details = "\n".join(lines[1:-1])
return indent(details, " " * 2)
def base_format(
*,
language: str,
run: Callable,
code: str,
md: Markdown,
html: bool = False,
source: str = "",
result: str = "",
tabs: tuple[str, str] = default_tabs,
id: str = "", # noqa: A002
id_prefix: str | None = None,
returncode: int = 0,
transform_source: Callable[[str], tuple[str, str]] | None = None,
session: str | None = None,
update_toc: bool = True,
**options: Any,
) -> Markup:
"""Execute code and return HTML.
Parameters:
language: The code language.
run: Function that runs code and returns output.
code: The code to execute.
md: The Markdown instance.
html: Whether to inject output as HTML directly, without rendering.
source: Whether to show source as well, and where.
result: If provided, use as language to format result in a code block.
tabs: Titles of tabs (if used).
id: An optional ID for the code block (useful when warning about errors).
id_prefix: A string used to prefix HTML ids in the generated HTML.
returncode: The expected exit code.
transform_source: An optional callable that returns transformed versions of the source.
The input source is the one that is ran, the output source is the one that is
rendered (when the source option is enabled).
session: A session name, to persist state between executed code blocks.
update_toc: Whether to include generated headings
into the Markdown table of contents (toc extension).
**options: Additional options passed from the formatter.
Returns:
HTML contents.
"""
markdown = MarkdownConverter(md, update_toc=update_toc)
extra = options.get("extra", {})
if transform_source:
source_input, source_output = transform_source(code)
else:
source_input = code
source_output = code
try:
output = run(source_input, returncode=returncode, session=session, id=id, **extra)
except ExecutionError as error:
identifier = id or extra.get("title", "")
identifier = identifier and f"'{identifier}' "
exit_message = "errors" if error.returncode is None else f"unexpected code {error.returncode}"
log_message = (
f"Execution of {language} code block {identifier}exited with {exit_message}\n\n"
f"Code block is:\n\n{_format_log_details(source_input)}\n\n"
f"Output is:\n\n{_format_log_details(str(error), strip_fences=True)}\n"
)
logger.warning(log_message)
return markdown.convert(str(error))
if html:
if source:
placeholder = str(uuid4())
wrapped_output = add_source(
source=source_output,
location=source,
output=placeholder,
language=language,
tabs=tabs,
**extra,
)
return markdown.convert(wrapped_output, stash={placeholder: output})
return Markup(output)
wrapped_output = output
if result and source != "console":
wrapped_output = code_block(result, output)
if source:
wrapped_output = add_source(
source=source_output,
location=source,
output=wrapped_output,
language=language,
tabs=tabs,
result=result,
**extra,
)
prefix = id_prefix if id_prefix is not None else (f"{id}-" if id else None)
return markdown.convert(wrapped_output, id_prefix=prefix)
markdown-exec-1.8.0/src/markdown_exec/formatters/bash.py 0000664 0000000 0000000 00000001566 14546046442 0023377 0 ustar 00root root 0000000 0000000 """Formatter for executing shell code."""
from __future__ import annotations
import subprocess
from typing import Any
from markdown_exec.formatters.base import ExecutionError, base_format
from markdown_exec.rendering import code_block
def _run_bash(
code: str,
returncode: int | None = None,
session: str | None = None, # noqa: ARG001
id: str | None = None, # noqa: A002,ARG001
**extra: str,
) -> str:
process = subprocess.run(
["bash", "-c", code], # noqa: S603,S607
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
check=False,
)
if process.returncode != returncode:
raise ExecutionError(code_block("sh", process.stdout, **extra), process.returncode)
return process.stdout
def _format_bash(**kwargs: Any) -> str:
return base_format(language="bash", run=_run_bash, **kwargs)
markdown-exec-1.8.0/src/markdown_exec/formatters/console.py 0000664 0000000 0000000 00000001457 14546046442 0024123 0 ustar 00root root 0000000 0000000 """Formatter for executing shell console code."""
from __future__ import annotations
import textwrap
from typing import TYPE_CHECKING, Any
from markdown_exec.formatters.base import base_format
from markdown_exec.formatters.sh import _run_sh
from markdown_exec.logger import get_logger
if TYPE_CHECKING:
from markupsafe import Markup
logger = get_logger(__name__)
def _transform_source(code: str) -> tuple[str, str]:
sh_lines = []
for line in code.split("\n"):
prompt = line[:2]
if prompt in {"$ ", "% "}:
sh_lines.append(line[2:])
sh_code = "\n".join(sh_lines)
return sh_code, textwrap.indent(sh_code, prompt)
def _format_console(**kwargs: Any) -> Markup:
return base_format(language="console", run=_run_sh, transform_source=_transform_source, **kwargs)
markdown-exec-1.8.0/src/markdown_exec/formatters/markdown.py 0000664 0000000 0000000 00000000424 14546046442 0024274 0 ustar 00root root 0000000 0000000 """Formatter for literate Markdown."""
from __future__ import annotations
from typing import Any
from markdown_exec.formatters.base import base_format
def _format_markdown(**kwargs: Any) -> str:
return base_format(language="md", run=lambda code, **_: code, **kwargs)
markdown-exec-1.8.0/src/markdown_exec/formatters/pycon.py 0000664 0000000 0000000 00000001526 14546046442 0023606 0 ustar 00root root 0000000 0000000 """Formatter for executing `pycon` code."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from markdown_exec.formatters.base import base_format
from markdown_exec.formatters.python import _run_python
from markdown_exec.logger import get_logger
if TYPE_CHECKING:
from markupsafe import Markup
logger = get_logger(__name__)
def _transform_source(code: str) -> tuple[str, str]:
python_lines = []
pycon_lines = []
for line in code.split("\n"):
if line.startswith((">>> ", "... ")):
pycon_lines.append(line)
python_lines.append(line[4:])
python_code = "\n".join(python_lines)
return python_code, "\n".join(pycon_lines)
def _format_pycon(**kwargs: Any) -> Markup:
return base_format(language="pycon", run=_run_python, transform_source=_transform_source, **kwargs)
markdown-exec-1.8.0/src/markdown_exec/formatters/pyodide.py 0000664 0000000 0000000 00000005522 14546046442 0024113 0 ustar 00root root 0000000 0000000 """Formatter for creating a Pyodide interactive editor."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from markdown import Markdown
play_emoji = ''
clear_emoji = ''
template = """
%(initial_code)s
"
def _run_python(
code: str,
returncode: int | None = None, # noqa: ARG001
session: str | None = None,
id: str | None = None, # noqa: A002
**extra: str,
) -> str:
title = extra.get("title", None)
code_block_id = _code_block_id(id, session, title)
_code_blocks[code_block_id] = code.split("\n")
exec_globals = _sessions_globals[session] if session else {}
buffer = StringIO()
exec_globals["print"] = partial(_buffer_print, buffer)
try:
compiled = compile(code, filename=code_block_id, mode="exec")
exec(compiled, exec_globals) # noqa: S102
except Exception as error: # noqa: BLE001
trace = traceback.TracebackException.from_exception(error)
for frame in trace.stack:
if frame.filename.startswith(" str:
return base_format(language="python", run=_run_python, **kwargs)
markdown-exec-1.8.0/src/markdown_exec/formatters/sh.py 0000664 0000000 0000000 00000001554 14546046442 0023071 0 ustar 00root root 0000000 0000000 """Formatter for executing shell code."""
from __future__ import annotations
import subprocess
from typing import Any
from markdown_exec.formatters.base import ExecutionError, base_format
from markdown_exec.rendering import code_block
def _run_sh(
code: str,
returncode: int | None = None,
session: str | None = None, # noqa: ARG001
id: str | None = None, # noqa: A002,ARG001
**extra: str,
) -> str:
process = subprocess.run(
["sh", "-c", code], # noqa: S603,S607
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
check=False,
)
if process.returncode != returncode:
raise ExecutionError(code_block("sh", process.stdout, **extra), process.returncode)
return process.stdout
def _format_sh(**kwargs: Any) -> str:
return base_format(language="sh", run=_run_sh, **kwargs)
markdown-exec-1.8.0/src/markdown_exec/formatters/tree.py 0000664 0000000 0000000 00000003776 14546046442 0023426 0 ustar 00root root 0000000 0000000 """Formatter for file-system trees."""
from __future__ import annotations
from textwrap import dedent
from typing import TYPE_CHECKING, Any
from markdown_exec.rendering import MarkdownConverter, code_block
if TYPE_CHECKING:
from markdown import Markdown
def _rec_build_tree(lines: list[str], parent: list, offset: int, base_indent: int) -> int:
while offset < len(lines):
line = lines[offset]
lstripped = line.lstrip()
indent = len(line) - len(lstripped)
if indent == base_indent:
parent.append((lstripped, []))
offset += 1
elif indent > base_indent:
offset = _rec_build_tree(lines, parent[-1][1], offset, indent)
else:
return offset
return offset
def _build_tree(code: str) -> list[tuple[str, list]]:
lines = dedent(code.strip()).split("\n")
root_layer: list[tuple[str, list]] = []
_rec_build_tree(lines, root_layer, 0, 0)
return root_layer
def _rec_format_tree(tree: list[tuple[str, list]], *, root: bool = True) -> list[str]:
lines = []
n_items = len(tree)
for index, node in enumerate(tree):
last = index == n_items - 1
prefix = "" if root else f"{'└' if last else '├'}── "
if node[1]:
lines.append(f"{prefix}📁 {node[0]}")
sublines = _rec_format_tree(node[1], root=False)
if root:
lines.extend(sublines)
else:
indent_char = " " if last else "│"
lines.extend([f"{indent_char} {line}" for line in sublines])
else:
name = node[0].split()[0]
icon = "📁" if name.endswith("/") else "📄"
lines.append(f"{prefix}{icon} {node[0]}")
return lines
def _format_tree(code: str, md: Markdown, result: str, **options: Any) -> str:
markdown = MarkdownConverter(md)
output = "\n".join(_rec_format_tree(_build_tree(code)))
return markdown.convert(code_block(result or "bash", output, **options.get("extra", {})))
markdown-exec-1.8.0/src/markdown_exec/logger.py 0000664 0000000 0000000 00000004075 14546046442 0021551 0 ustar 00root root 0000000 0000000 """This module contains logging utilities.
We provide the [`patch_loggers`][markdown_exec.logger.patch_loggers]
function so dependant libraries can patch loggers as they see fit.
For example, to fit in the MkDocs logging configuration
and prefix each log message with the module name:
```python
import logging
from markdown_exec.logger import patch_loggers
class LoggerAdapter(logging.LoggerAdapter):
def __init__(self, prefix, logger):
super().__init__(logger, {})
self.prefix = prefix
def process(self, msg, kwargs):
return f"{self.prefix}: {msg}", kwargs
def get_logger(name):
logger = logging.getLogger(f"mkdocs.plugins.{name}")
return LoggerAdapter(name.split(".", 1)[0], logger)
patch_loggers(get_logger)
```
"""
from __future__ import annotations
import logging
from typing import Any, Callable, ClassVar
class _Logger:
_default_logger: Any = logging.getLogger
_instances: ClassVar[dict[str, _Logger]] = {}
def __init__(self, name: str) -> None:
# default logger that can be patched by third-party
self._logger = self.__class__._default_logger(name)
# register instance
self._instances[name] = self
def __getattr__(self, name: str) -> Any:
# forward everything to the logger
return getattr(self._logger, name)
@classmethod
def _patch_loggers(cls, get_logger_func: Callable) -> None:
# patch current instances
for name, instance in cls._instances.items():
instance._logger = get_logger_func(name)
# future instances will be patched as well
cls._default_logger = get_logger_func
def get_logger(name: str) -> _Logger:
"""Create and return a new logger instance.
Parameters:
name: The logger name.
Returns:
The logger.
"""
return _Logger(name)
def patch_loggers(get_logger_func: Callable[[str], Any]) -> None:
"""Patch loggers.
Parameters:
get_logger_func: A function accepting a name as parameter and returning a logger.
"""
_Logger._patch_loggers(get_logger_func)
markdown-exec-1.8.0/src/markdown_exec/mkdocs_plugin.py 0000664 0000000 0000000 00000011135 14546046442 0023123 0 ustar 00root root 0000000 0000000 """This module contains an optional plugin for MkDocs."""
from __future__ import annotations
import logging
import os
from pathlib import Path
from typing import TYPE_CHECKING, Any, MutableMapping
from mkdocs.config import config_options
from mkdocs.config.base import Config
from mkdocs.plugins import BasePlugin
from mkdocs.utils import write_file
from markdown_exec import formatter, formatters, validator
from markdown_exec.logger import patch_loggers
from markdown_exec.rendering import MarkdownConverter, markdown_config
if TYPE_CHECKING:
from jinja2 import Environment
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.structure.files import Files
try:
__import__("pygments_ansi_color")
except ImportError:
ansi_ok = False
else:
ansi_ok = True
class _LoggerAdapter(logging.LoggerAdapter):
def __init__(self, prefix: str, logger: logging.Logger) -> None:
super().__init__(logger, {})
self.prefix = prefix
def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, MutableMapping[str, Any]]:
return f"{self.prefix}: {msg}", kwargs
def _get_logger(name: str) -> _LoggerAdapter:
logger = logging.getLogger(f"mkdocs.plugins.{name}")
return _LoggerAdapter(name.split(".", 1)[0], logger)
patch_loggers(_get_logger)
class MarkdownExecPluginConfig(Config):
"""Configuration of the plugin (for `mkdocs.yml`)."""
ansi = config_options.Choice(("auto", "off", "required", True, False), default="auto")
"""Whether the `ansi` extra is required when installing the package."""
languages = config_options.ListOfItems(
config_options.Choice(formatters.keys()),
default=list(formatters.keys()),
)
"""Which languages to enabled the extension for."""
class MarkdownExecPlugin(BasePlugin[MarkdownExecPluginConfig]):
"""MkDocs plugin to easily enable custom fences for code blocks execution."""
def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None:
"""Configure the plugin.
Hook for the [`on_config` event](https://www.mkdocs.org/user-guide/plugins/#on_config).
In this hook, we add custom fences for all the supported languages.
We also save the Markdown extensions configuration
into [`markdown_config`][markdown_exec.rendering.markdown_config].
Arguments:
config: The MkDocs config object.
Returns:
The modified config.
"""
self.mkdocs_config_dir = os.getenv("MKDOCS_CONFIG_DIR")
os.environ["MKDOCS_CONFIG_DIR"] = os.path.dirname(config["config_file_path"])
self.languages = self.config.languages
mdx_configs = config.setdefault("mdx_configs", {})
superfences = mdx_configs.setdefault("pymdownx.superfences", {})
custom_fences = superfences.setdefault("custom_fences", [])
for language in self.languages:
custom_fences.append(
{
"name": language,
"class": language,
"validator": validator,
"format": formatter,
},
)
markdown_config.save(config.markdown_extensions, config.mdx_configs)
return config
def on_env( # noqa: D102
self,
env: Environment,
*,
config: MkDocsConfig,
files: Files, # noqa: ARG002
) -> Environment | None:
if self.config.ansi in ("required", True) or (self.config.ansi == "auto" and ansi_ok):
self._add_css(config, "ansi.css")
if "pyodide" in self.languages:
self._add_css(config, "pyodide.css")
self._add_js(config, "pyodide.js")
return env
def on_post_build(self, *, config: MkDocsConfig) -> None: # noqa: ARG002,D102
MarkdownConverter.counter = 0
markdown_config.reset()
if self.mkdocs_config_dir is None:
os.environ.pop("MKDOCS_CONFIG_DIR", None)
else:
os.environ["MKDOCS_CONFIG_DIR"] = self.mkdocs_config_dir
def _add_asset(self, config: MkDocsConfig, asset_file: str, asset_type: str) -> None:
asset_filename = f"assets/_markdown_exec_{asset_file}"
asset_content = Path(__file__).parent.joinpath(asset_file).read_text()
write_file(asset_content.encode("utf-8"), os.path.join(config.site_dir, asset_filename))
config[f"extra_{asset_type}"].insert(0, asset_filename)
def _add_css(self, config: MkDocsConfig, css_file: str) -> None:
self._add_asset(config, css_file, "css")
def _add_js(self, config: MkDocsConfig, js_file: str) -> None:
self._add_asset(config, js_file, "javascript")
markdown-exec-1.8.0/src/markdown_exec/processors.py 0000664 0000000 0000000 00000010303 14546046442 0022463 0 ustar 00root root 0000000 0000000 """This module contains a Markdown extension allowing to integrate generated headings into the ToC."""
from __future__ import annotations
import copy
import re
from typing import TYPE_CHECKING
from xml.etree.ElementTree import Element
from markdown.treeprocessors import Treeprocessor
from markdown.util import HTML_PLACEHOLDER_RE
if TYPE_CHECKING:
from markdown import Markdown
from markupsafe import Markup
# code taken from mkdocstrings, credits to @oprypin
class IdPrependingTreeprocessor(Treeprocessor):
"""Prepend the configured prefix to IDs of all HTML elements."""
name = "markdown_exec_ids"
def __init__(self, md: Markdown, id_prefix: str) -> None: # noqa: D107
super().__init__(md)
self.id_prefix = id_prefix
def run(self, root: Element) -> None: # noqa: D102
if not self.id_prefix:
return
for el in root.iter():
id_attr = el.get("id")
if id_attr:
el.set("id", self.id_prefix + id_attr)
href_attr = el.get("href")
if href_attr and href_attr.startswith("#"):
el.set("href", "#" + self.id_prefix + href_attr[1:])
name_attr = el.get("name")
if name_attr:
el.set("name", self.id_prefix + name_attr)
if el.tag == "label":
for_attr = el.get("for")
if for_attr:
el.set("for", self.id_prefix + for_attr)
# code taken from mkdocstrings, credits to @oprypin
class HeadingReportingTreeprocessor(Treeprocessor):
"""Records the heading elements encountered in the document."""
name = "markdown_exec_record_headings"
regex = re.compile("[Hh][1-6]")
def __init__(self, md: Markdown, headings: list[Element]): # noqa: D107
super().__init__(md)
self.headings = headings
def run(self, root: Element) -> None: # noqa: D102
for el in root.iter():
if self.regex.fullmatch(el.tag):
el = copy.copy(el) # noqa: PLW2901
# 'toc' extension's first pass (which we require to build heading stubs/ids) also edits the HTML.
# Undo the permalink edit so we can pass this heading to the outer pass of the 'toc' extension.
if len(el) > 0 and el[-1].get("class") == self.md.treeprocessors["toc"].permalink_class: # type: ignore[attr-defined]
del el[-1]
self.headings.append(el)
class InsertHeadings(Treeprocessor):
"""Our headings insertor."""
name = "markdown_exec_insert_headings"
def __init__(self, md: Markdown):
"""Initialize the object.
Arguments:
md: A `markdown.Markdown` instance.
"""
super().__init__(md)
self.headings: dict[Markup, list[Element]] = {}
def run(self, root: Element) -> None: # noqa: D102 (ignore missing docstring)
if not self.headings:
return
for el in root.iter():
match = HTML_PLACEHOLDER_RE.match(el.text or "")
if match:
counter = int(match.group(1))
markup: Markup = self.md.htmlStash.rawHtmlBlocks[counter] # type: ignore[assignment]
if markup in self.headings:
div = Element("div", {"class": "markdown-exec"})
div.extend(self.headings[markup])
el.append(div)
class RemoveHeadings(Treeprocessor):
"""Our headings remover."""
name = "markdown_exec_remove_headings"
def run(self, root: Element) -> None: # noqa: D102
carry_text = ""
for el in reversed(root): # Reversed mainly for the ability to mutate during iteration.
for subel in reversed(el):
if subel.tag == "div" and subel.get("class") == "markdown-exec":
# Delete the duplicated headings along with their container, but keep the text (i.e. the actual HTML).
carry_text = (subel.text or "") + carry_text
el.remove(subel)
elif carry_text:
subel.tail = (subel.tail or "") + carry_text
carry_text = ""
if carry_text:
el.text = (el.text or "") + carry_text
markdown-exec-1.8.0/src/markdown_exec/py.typed 0000664 0000000 0000000 00000000000 14546046442 0021377 0 ustar 00root root 0000000 0000000 markdown-exec-1.8.0/src/markdown_exec/pyodide.css 0000664 0000000 0000000 00000001521 14546046442 0022060 0 ustar 00root root 0000000 0000000 html[data-theme="light"] {
@import "https://cdn.jsdelivr.net/npm/highlightjs-themes@1.0.0/tomorrow.css"
}
html[data-theme="dark"] {
@import "https://cdn.jsdelivr.net/npm/highlightjs-themes@1.0.0/tomorrow-night-blue.min.css"
}
.ace_gutter {
z-index: 1;
}
.pyodide-editor {
width: 100%;
min-height: 200px;
max-height: 400px;
font-size: .85em;
}
.pyodide-editor-bar {
color: var(--md-primary-bg-color);
background-color: var(--md-primary-fg-color);
width: 100%;
font: monospace;
font-size: 0.75em;
padding: 2px 0 2px;
}
.pyodide-bar-item {
padding: 0 18px 0;
display: inline-block;
width: 50%;
}
.pyodide pre {
margin: 0;
}
.pyodide-output {
width: 100%;
margin-bottom: -15px;
max-height: 400px
}
.pyodide-clickable {
cursor: pointer;
text-align: right;
} markdown-exec-1.8.0/src/markdown_exec/pyodide.js 0000664 0000000 0000000 00000007405 14546046442 0021713 0 ustar 00root root 0000000 0000000 var _sessions = {};
function getSession(name, pyodide) {
if (!(name in _sessions)) {
_sessions[name] = pyodide.globals.get("dict")();
}
return _sessions[name];
}
function writeOutput(element, string) {
element.innerHTML += string + '\n';
}
function clearOutput(element) {
element.innerHTML = '';
}
async function evaluatePython(pyodide, editor, output, session) {
pyodide.setStdout({ batched: (string) => { writeOutput(output, string); } });
let result, code = editor.getValue();
clearOutput(output);
try {
result = await pyodide.runPythonAsync(code, { globals: getSession(session, pyodide) });
} catch (error) {
writeOutput(output, error);
}
if (result) writeOutput(output, result);
hljs.highlightElement(output);
}
async function initPyodide() {
try {
let pyodide = await loadPyodide();
await pyodide.loadPackage("micropip");
return pyodide;
} catch(error) {
return null;
}
}
function getTheme() {
return document.body.getAttribute('data-md-color-scheme');
}
function setTheme(editor, currentTheme, light, dark) {
// https://gist.github.com/RyanNutt/cb8d60997d97905f0b2aea6c3b5c8ee0
if (currentTheme === "default") {
editor.setTheme("ace/theme/" + light);
document.querySelector(`link[title="light"]`).removeAttribute("disabled");
document.querySelector(`link[title="dark"]`).setAttribute("disabled", "disabled");
} else if (currentTheme === "slate") {
editor.setTheme("ace/theme/" + dark);
document.querySelector(`link[title="dark"]`).removeAttribute("disabled");
document.querySelector(`link[title="light"]`).setAttribute("disabled", "disabled");
}
}
function updateTheme(editor, light, dark) {
// Create a new MutationObserver instance
const observer = new MutationObserver((mutations) => {
// Loop through the mutations that occurred
mutations.forEach((mutation) => {
// Check if the mutation was a change to the data-md-color-scheme attribute
if (mutation.attributeName === 'data-md-color-scheme') {
// Get the new value of the attribute
const newColorScheme = mutation.target.getAttribute('data-md-color-scheme');
// Update the editor theme
setTheme(editor, newColorScheme, light, dark);
}
});
});
// Configure the observer to watch for changes to the data-md-color-scheme attribute
observer.observe(document.body, {
attributes: true,
attributeFilter: ['data-md-color-scheme'],
});
}
async function setupPyodide(idPrefix, install = null, themeLight = 'tomorrow', themeDark = 'tomorrow_night', session = null) {
const editor = ace.edit(idPrefix + "editor");
const run = document.getElementById(idPrefix + "run");
const clear = document.getElementById(idPrefix + "clear");
const output = document.getElementById(idPrefix + "output");
updateTheme(editor, themeLight, themeDark);
editor.session.setMode("ace/mode/python");
setTheme(editor, getTheme(), themeLight, themeDark);
writeOutput(output, "Initializing...");
let pyodide = await pyodidePromise;
if (install && install.length) {
micropip = pyodide.pyimport("micropip");
for (const package of install)
await micropip.install(package);
}
clearOutput(output);
run.onclick = () => evaluatePython(pyodide, editor, output, session);
clear.onclick = () => clearOutput(output);
output.parentElement.parentElement.addEventListener("keydown", (event) => {
if (event.ctrlKey && event.key.toLowerCase() === 'enter') {
event.preventDefault();
run.click();
}
});
}
var pyodidePromise = initPyodide();
markdown-exec-1.8.0/src/markdown_exec/rendering.py 0000664 0000000 0000000 00000022363 14546046442 0022247 0 ustar 00root root 0000000 0000000 """Markdown extensions and helpers."""
from __future__ import annotations
from contextlib import contextmanager
from functools import lru_cache
from textwrap import indent
from typing import TYPE_CHECKING, Any, Iterator
from markdown import Markdown
from markupsafe import Markup
from markdown_exec.processors import (
HeadingReportingTreeprocessor,
IdPrependingTreeprocessor,
InsertHeadings,
RemoveHeadings,
)
if TYPE_CHECKING:
from xml.etree.ElementTree import Element
from markdown import Extension
def code_block(language: str, code: str, **options: str) -> str:
"""Format code as a code block.
Parameters:
language: The code block language.
code: The source code to format.
**options: Additional options passed from the source, to add back to the generated code block.
Returns:
The formatted code block.
"""
opts = " ".join(f'{opt_name}="{opt_value}"' for opt_name, opt_value in options.items())
return f"````````{language} {opts}\n{code}\n````````"
def tabbed(*tabs: tuple[str, str]) -> str:
"""Format tabs using `pymdownx.tabbed` extension.
Parameters:
*tabs: Tuples of strings: title and text.
Returns:
The formatted tabs.
"""
parts = []
for title, text in tabs:
title = title.replace(r"\|", "|").strip() # noqa: PLW2901
parts.append(f'=== "{title}"')
parts.append(indent(text, prefix=" " * 4))
parts.append("")
return "\n".join(parts)
def _hide_lines(source: str) -> str:
return "\n".join(line for line in source.split("\n") if "markdown-exec: hide" not in line).strip()
def add_source(
*,
source: str,
location: str,
output: str,
language: str,
tabs: tuple[str, str],
result: str = "",
**extra: str,
) -> str:
"""Add source code block to the output.
Parameters:
source: The source code block.
location: Where to add the source (above, below, tabbed-left, tabbed-right, console).
output: The current output.
language: The code language.
tabs: Tabs titles (if used).
result: Syntax to use when concatenating source and result with "console" location.
**extra: Extra options added back to source code block.
Raises:
ValueError: When the given location is not supported.
Returns:
The updated output.
"""
source = _hide_lines(source)
if location == "console":
return code_block(result or language, source + "\n" + output, **extra)
source_block = code_block(language, source, **extra)
if location == "above":
return source_block + "\n\n" + output
if location == "below":
return output + "\n\n" + source_block
if location == "material-block":
return source_block + f'\n\n\n\n{output}\n\n'
source_tab_title, result_tab_title = tabs
if location == "tabbed-left":
return tabbed((source_tab_title, source_block), (result_tab_title, output))
if location == "tabbed-right":
return tabbed((result_tab_title, output), (source_tab_title, source_block))
raise ValueError(f"unsupported location for sources: {location}")
class MarkdownConfig:
"""This class returns a singleton used to store Markdown extensions configuration.
You don't have to instantiate the singleton yourself:
we provide it as [`markdown_config`][markdown_exec.rendering.markdown_config].
"""
_singleton: MarkdownConfig | None = None
def __new__(cls) -> MarkdownConfig: # noqa: D102,PYI034
if cls._singleton is None:
cls._singleton = super().__new__(cls)
return cls._singleton
def __init__(self) -> None: # noqa: D107
self.exts: list[str] | None = None
self.exts_config: dict[str, dict[str, Any]] | None = None
def save(self, exts: list[str], exts_config: dict[str, dict[str, Any]]) -> None:
"""Save Markdown extensions and their configuration.
Parameters:
exts: The Markdown extensions.
exts_config: The extensions configuration.
"""
self.exts = exts
self.exts_config = exts_config
def reset(self) -> None:
"""Reset Markdown extensions and their configuration."""
self.exts = None
self.exts_config = None
markdown_config = MarkdownConfig()
"""This object can be used to save the configuration of your Markdown extensions.
For example, since we provide a MkDocs plugin, we use it to store the configuration
that was read from `mkdocs.yml`:
```python
from markdown_exec.rendering import markdown_config
# ...in relevant events/hooks, access and modify extensions and their configs, then:
markdown_config.save(extensions, extensions_config)
```
See the actual event hook: [`on_config`][markdown_exec.mkdocs_plugin.MarkdownExecPlugin.on_config].
See the [`save`][markdown_exec.rendering.MarkdownConfig.save]
and [`reset`][markdown_exec.rendering.MarkdownConfig.reset] methods.
Without it, Markdown Exec will rely on the `registeredExtensions` attribute
of the original Markdown instance, which does not forward everything
that was configured, notably extensions like `tables`. Other extensions
such as `attr_list` are visible, but fail to register properly when
reusing their instances. It means that the rendered HTML might differ
from what you expect (tables not rendered, attribute lists not injected,
emojis not working, etc.).
"""
# FIXME: When a heading contains an XML entity such as —,
# the entity is stashed and replaced with a placeholder.
# The heading therefore contains this placeholder.
# When reporting the heading to the upper conversion layer (for the ToC),
# the placeholder gets unstashed using the upper Markdown instance
# instead of the neste one. If the upper instance doesn't know the placeholder,
# nothing happens. But if it knows it, we then get a heading with garbabe/previous
# contents within it, messing up the ToC.
# We should fix this somehow. In the meantime, the workaround is to avoid
# XML entities that get stashed in headings.
@lru_cache(maxsize=None)
def _register_headings_processors(md: Markdown) -> None:
md.treeprocessors.register(
InsertHeadings(md),
InsertHeadings.name,
priority=75, # right before markdown.blockprocessors.HashHeaderProcessor
)
md.treeprocessors.register(
RemoveHeadings(md),
RemoveHeadings.name,
priority=4, # right after toc
)
def _mimic(md: Markdown, headings: list[Element], *, update_toc: bool = True) -> Markdown:
new_md = Markdown()
extensions: list[Extension | str] = markdown_config.exts or md.registeredExtensions # type: ignore[assignment]
extensions_config: dict[str, dict[str, Any]] = markdown_config.exts_config or {}
new_md.registerExtensions(extensions, extensions_config)
new_md.treeprocessors.register(
IdPrependingTreeprocessor(md, ""),
IdPrependingTreeprocessor.name,
priority=4, # right after 'toc' (needed because that extension adds ids to headings)
)
new_md._original_md = md # type: ignore[attr-defined]
if update_toc:
_register_headings_processors(md)
new_md.treeprocessors.register(
HeadingReportingTreeprocessor(new_md, headings),
HeadingReportingTreeprocessor.name,
priority=1, # Close to the end.
)
return new_md
@contextmanager
def _id_prefix(md: Markdown, prefix: str | None) -> Iterator[None]:
MarkdownConverter.counter += 1
id_prepending_processor = md.treeprocessors[IdPrependingTreeprocessor.name]
id_prepending_processor.id_prefix = prefix if prefix is not None else f"exec-{MarkdownConverter.counter}--" # type: ignore[attr-defined]
try:
yield
finally:
id_prepending_processor.id_prefix = "" # type: ignore[attr-defined]
class MarkdownConverter:
"""Helper class to avoid breaking the original Markdown instance state."""
counter: int = 0
def __init__(self, md: Markdown, *, update_toc: bool = True) -> None: # noqa: D107
self._md_ref: Markdown = md
self._headings: list[Element] = []
self._update_toc = update_toc
@property
def _original_md(self) -> Markdown:
return getattr(self._md_ref, "_original_md", self._md_ref)
def _report_headings(self, markup: Markup) -> None:
self._original_md.treeprocessors[InsertHeadings.name].headings[markup] = self._headings # type: ignore[attr-defined]
self._headings = []
def convert(self, text: str, stash: dict[str, str] | None = None, id_prefix: str | None = None) -> Markup:
"""Convert Markdown text to safe HTML.
Parameters:
text: Markdown text.
stash: An HTML stash.
Returns:
Safe HTML.
"""
md = _mimic(self._original_md, self._headings, update_toc=self._update_toc)
# convert markdown to html
with _id_prefix(md, id_prefix):
converted = md.convert(text)
# restore html from stash
for placeholder, stashed in (stash or {}).items():
converted = converted.replace(placeholder, stashed)
markup = Markup(converted)
# pass headings to upstream conversion layer
if self._update_toc:
self._report_headings(markup)
return markup
markdown-exec-1.8.0/tests/ 0000775 0000000 0000000 00000000000 14546046442 0015437 5 ustar 00root root 0000000 0000000 markdown-exec-1.8.0/tests/__init__.py 0000664 0000000 0000000 00000000246 14546046442 0017552 0 ustar 00root root 0000000 0000000 """Tests suite for `markdown_exec`."""
from pathlib import Path
TESTS_DIR = Path(__file__).parent
TMP_DIR = TESTS_DIR / "tmp"
FIXTURES_DIR = TESTS_DIR / "fixtures"
markdown-exec-1.8.0/tests/conftest.py 0000664 0000000 0000000 00000001173 14546046442 0017640 0 ustar 00root root 0000000 0000000 """Configuration for the pytest test suite."""
import pytest
from markdown import Markdown
from markdown_exec import formatter, formatters, validator
@pytest.fixture()
def md() -> Markdown:
"""Return a Markdown instance.
Returns:
Markdown instance.
"""
fences = [
{
"name": language,
"class": language,
"validator": validator,
"format": formatter,
}
for language in formatters
]
return Markdown(
extensions=["pymdownx.superfences"],
extension_configs={"pymdownx.superfences": {"custom_fences": fences}},
)
markdown-exec-1.8.0/tests/test_base_formatter.py 0000664 0000000 0000000 00000002643 14546046442 0022052 0 ustar 00root root 0000000 0000000 """Tests for the base formatter."""
import pytest
from markdown import Markdown
from markdown_exec.formatters.base import base_format
def test_no_p_around_html(md: Markdown) -> None:
"""Assert HTML isn't wrapped in a `p` tag.
Parameters:
md: A Markdown instance (fixture).
"""
code = "hello
"
html = base_format(
language="whatever",
run=lambda code, **_: code,
code=code,
md=md,
html=True,
)
assert html == code
@pytest.mark.parametrize("html", [True, False])
def test_render_source(md: Markdown, html: bool) -> None:
"""Assert source is rendered.
Parameters:
md: A Markdown instance (fixture).
html: Whether output is HTML or not.
"""
markup = base_format(
language="python",
run=lambda code, **_: code,
code="hello",
md=md,
html=html,
source="tabbed-left",
)
assert "Source" in markup
def test_render_console_plus_ansi_result(md: Markdown) -> None:
"""Assert we can render source as console style with `ansi` highlight.
Parameters:
md: A Markdown instance (fixture).
"""
markup = base_format(
language="bash",
run=lambda code, **_: code,
code="echo -e '\033[31mhello'",
md=md,
html=False,
source="console",
result="ansi",
)
assert "ansi" in markup
markdown-exec-1.8.0/tests/test_converter.py 0000664 0000000 0000000 00000004040 14546046442 0021055 0 ustar 00root root 0000000 0000000 """Tests for the Markdown converter."""
from __future__ import annotations
import re
from textwrap import dedent
from typing import TYPE_CHECKING
import pytest
from markdown.extensions.toc import TocExtension
from markdown_exec.rendering import MarkdownConfig, markdown_config
if TYPE_CHECKING:
from markdown import Markdown
def test_rendering_nested_blocks(md: Markdown) -> None:
"""Assert nested blocks are properly handled.
Parameters:
md: A Markdown instance (fixture).
"""
html = md.convert(
dedent(
"""
````md exec="1"
```python exec="1"
print("**Bold!**")
```
````
""",
),
)
assert html == "Bold!
"
def test_instantiating_config_singleton() -> None:
"""Assert that the Markdown config instances act as a singleton."""
assert MarkdownConfig() is markdown_config
markdown_config.save([], {})
markdown_config.reset()
@pytest.mark.parametrize(
("id", "id_prefix", "expected"),
[
("", None, 'id="exec-\\d+--heading"'),
("", "", 'id="heading"'),
("", "some-prefix-", 'id="some-prefix-heading"'),
("some-id", None, 'id="some-id-heading"'),
("some-id", "", 'id="heading"'),
("some-id", "some-prefix-", 'id="some-prefix-heading"'),
],
)
def test_prefixing_headings(md: Markdown, id: str, id_prefix: str | None, expected: str) -> None: # noqa: A002
"""Assert that we prefix headings as specified.
Parameters:
md: A Markdown instance (fixture).
id: The code block id.
id_prefix: The code block id prefix.
expected: The id we expect to find in the HTML.
"""
TocExtension().extendMarkdown(md)
prefix = f'idprefix="{id_prefix}"' if id_prefix is not None else ""
html = md.convert(
dedent(
f"""
```python exec="1" id="{id}" {prefix}
print("# HEADING")
```
""",
),
)
assert re.search(expected, html)
markdown-exec-1.8.0/tests/test_python.py 0000664 0000000 0000000 00000010361 14546046442 0020372 0 ustar 00root root 0000000 0000000 """Tests for the Python formatters."""
from __future__ import annotations
from textwrap import dedent
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import pytest
from markdown import Markdown
def test_output_markdown(md: Markdown) -> None:
"""Assert Markdown is converted to HTML.
Parameters:
md: A Markdown instance (fixture).
"""
html = md.convert(
dedent(
"""
```python exec="yes"
print("**Bold!**")
```
""",
),
)
assert html == "Bold!
"
def test_output_html(md: Markdown) -> None:
"""Assert HTML is injected as is.
Parameters:
md: A Markdown instance (fixture).
"""
html = md.convert(
dedent(
"""
```python exec="yes" html="yes"
print("**Bold!**")
```
""",
),
)
assert html == "**Bold!**\n
"
def test_error_raised(md: Markdown, caplog: pytest.LogCaptureFixture) -> None:
"""Assert errors properly log a warning and return a formatted traceback.
Parameters:
md: A Markdown instance (fixture).
caplog: Pytest fixture to capture logs.
"""
html = md.convert(
dedent(
"""
```python exec="yes"
raise ValueError("oh no!")
```
""",
),
)
assert "Traceback" in html
assert "ValueError" in html
assert "oh no!" in html
assert "Execution of python code block exited with errors" in caplog.text
def test_can_print_non_string_objects(md: Markdown) -> None:
"""Assert we can print non-string objects.
Parameters:
md: A Markdown instance (fixture).
"""
html = md.convert(
dedent(
"""
```python exec="yes"
class NonString:
def __str__(self):
return "string"
nonstring = NonString()
print(nonstring, nonstring)
```
""",
),
)
assert "Traceback" not in html
def test_sessions(md: Markdown) -> None:
"""Assert sessions can be reused.
Parameters:
md: A Markdown instance (fixture).
"""
html = md.convert(
dedent(
"""
```python exec="1" session="a"
a = 1
```
```pycon exec="1" session="b"
>>> b = 2
```
```pycon exec="1" session="a"
>>> print(f"a = {a}")
>>> try:
... print(b)
... except NameError:
... print("ok")
... else:
... print("ko")
```
```python exec="1" session="b"
print(f"b = {b}")
try:
print(a)
except NameError:
print("ok")
else:
print("ko")
```
""",
),
)
assert "a = 1" in html
assert "b = 2" in html
assert "ok" in html
assert "ko" not in html
def test_reporting_errors_in_sessions(md: Markdown, caplog: pytest.LogCaptureFixture) -> None:
"""Assert errors and source lines are correctly reported across sessions.
Parameters:
md: A Markdown instance (fixture).
caplog: Pytest fixture to capture logs.
"""
html = md.convert(
dedent(
"""
```python exec="1" session="a"
def fraise():
raise RuntimeError("strawberry")
```
```python exec="1" session="a"
print("hello")
fraise()
```
""",
),
)
assert "Traceback" in html
assert "strawberry" in html
assert "fraise()" in caplog.text
assert 'raise RuntimeError("strawberry")' in caplog.text
def test_removing_output_from_pycon_code(md: Markdown) -> None:
"""Assert output lines are removed from pycon snippets.
Parameters:
md: A Markdown instance (fixture).
"""
html = md.convert(
dedent(
"""
```pycon exec="1" source="console"
>>> print("ok")
ko
```
""",
),
)
assert "ok" in html
assert "ko" not in html
markdown-exec-1.8.0/tests/test_shell.py 0000664 0000000 0000000 00000003753 14546046442 0020167 0 ustar 00root root 0000000 0000000 """Tests for the shell formatters."""
from __future__ import annotations
from textwrap import dedent
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import pytest
from markdown import Markdown
def test_output_markdown(md: Markdown) -> None:
"""Assert Markdown is converted to HTML.
Parameters:
md: A Markdown instance (fixture).
"""
html = md.convert(
dedent(
"""
```sh exec="yes"
echo "**Bold!**"
```
""",
),
)
assert html == "Bold!
"
def test_output_html(md: Markdown) -> None:
"""Assert HTML is injected as is.
Parameters:
md: A Markdown instance (fixture).
"""
html = md.convert(
dedent(
"""
```sh exec="yes" html="yes"
echo "**Bold!**"
```
""",
),
)
assert html == "**Bold!**\n
"
def test_error_raised(md: Markdown, caplog: pytest.LogCaptureFixture) -> None:
"""Assert errors properly log a warning and return a formatted traceback.
Parameters:
md: A Markdown instance (fixture).
caplog: Pytest fixture to capture logs.
"""
html = md.convert(
dedent(
"""
```sh exec="yes"
echo("wrong syntax")
```
""",
),
)
assert "error" in html
assert "Execution of sh code block exited with unexpected code 2" in caplog.text
def test_return_code(md: Markdown, caplog: pytest.LogCaptureFixture) -> None:
"""Assert return code is used correctly.
Parameters:
md: A Markdown instance (fixture).
caplog: Pytest fixture to capture logs.
"""
html = md.convert(
dedent(
"""
```sh exec="yes" returncode="1"
echo Not in the mood
exit 1
```
""",
),
)
assert "Not in the mood" in html
assert "exited with" not in caplog.text
markdown-exec-1.8.0/tests/test_toc.py 0000664 0000000 0000000 00000004437 14546046442 0017645 0 ustar 00root root 0000000 0000000 """Tests for the logic updating the table of contents."""
from __future__ import annotations
from textwrap import dedent
from typing import TYPE_CHECKING
from markdown.extensions.toc import TocExtension
if TYPE_CHECKING:
from markdown import Markdown
def test_updating_toc(md: Markdown) -> None:
"""Assert ToC is updated with generated headings.
Parameters:
md: A Markdown instance (fixture).
"""
TocExtension().extendMarkdown(md)
html = md.convert(
dedent(
"""
```python exec="yes"
print("# big heading")
```
""",
),
)
assert " None:
"""Assert ToC is not updated with generated headings.
Parameters:
md: A Markdown instance (fixture).
"""
TocExtension().extendMarkdown(md)
html = md.convert(
dedent(
"""
```python exec="yes" updatetoc="no"
print("# big heading")
```
""",
),
)
assert " None:
"""Assert ToC is not updated with generated headings.
Parameters:
md: A Markdown instance (fixture).
"""
TocExtension().extendMarkdown(md)
html = md.convert(
dedent(
"""
```python exec="yes" updatetoc="no"
print("# big heading")
```
```python exec="yes" updatetoc="yes"
print("## medium heading")
```
```python exec="yes" updatetoc="no"
print("### small heading")
```
```python exec="yes" updatetoc="yes"
print("#### tiny heading")
```
""",
),
)
assert "