Skip to content

Commit 91a95ae

Browse files
committed
feat: Add ability to render using tabs
1 parent 99c5975 commit 91a95ae

4 files changed

Lines changed: 85 additions & 14 deletions

File tree

src/markdown_exec/__init__.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ def validator(
4242
if not exec_value:
4343
return False
4444
isolate_value = _to_bool(inputs.pop("isolate", "no"))
45+
show_source_value = inputs.pop("show_source", "")
4546
options["exec"] = exec_value
4647
options["isolate"] = isolate_value
48+
options["show_source"] = show_source_value
4749
return True
4850

4951

@@ -56,7 +58,7 @@ def formatter(
5658
classes: list[str] | None = None,
5759
id_value: str = "",
5860
attrs: dict[str, Any] | None = None,
59-
**kwargs,
61+
**kwargs: Any,
6062
) -> str:
6163
"""Execute code and return HTML.
6264
@@ -76,8 +78,20 @@ def formatter(
7678
HTML contents.
7779
"""
7880
fmt = _formatters.get(language, lambda source, *args, **kwargs: source)
79-
return fmt(source, md)
81+
return fmt(source, md, **options)
8082

8183

82-
def _to_bool(value):
83-
return value.lower() not in {"", "no", "off", "false", "0"}
84+
falsy_values = {"", "no", "off", "false", "0"}
85+
truthy_values = {"yes", "on", "true", "1"}
86+
87+
88+
def _to_bool(value: str) -> bool:
89+
return value.lower() not in falsy_values
90+
91+
92+
def _to_bool_or_value(value: str) -> bool | str:
93+
if value.lower() in falsy_values:
94+
return False
95+
if value.lower() in truthy_values:
96+
return True
97+
return value

src/markdown_exec/python.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
"""Formatter and utils for executing Python code."""
22

33
import traceback
4-
from copy import deepcopy
4+
from textwrap import indent
55

66
from markdown.core import Markdown
77
from markupsafe import Markup
88

9+
from markdown_exec.utils import code_block, tabbed
10+
11+
md_copy = None
12+
913

1014
class MarkdownOutput(Exception): # noqa: N818
1115
"""Exception to return Markdown."""
@@ -39,22 +43,42 @@ def output_html(text: str) -> None:
3943
raise HTMLOutput(text)
4044

4145

42-
def exec_python(source: str, md: Markdown) -> str:
46+
def exec_python(source: str, md: Markdown, isolate: bool = False, show_source: str = "", **options) -> str:
4347
"""Execute code and return HTML.
4448
4549
Parameters:
4650
source: The code to execute.
4751
md: The Markdown instance.
52+
isolate: Whether to run the code in isolation.
53+
show_source: Whether to show source as well, and where.
4854
4955
Returns:
5056
HTML contents.
5157
"""
58+
global md_copy
59+
if md_copy is None:
60+
md_copy = Markdown()
61+
md_copy.registerExtensions(md.registeredExtensions, {})
62+
if isolate:
63+
exec_source = f"def _function():\n{indent(source, prefix=' ' * 4)}\n_function()\n"
64+
else:
65+
exec_source = source
5266
try:
53-
exec(source) # noqa: S102
54-
except MarkdownOutput as output:
55-
return Markup(deepcopy(md).convert(str(output)))
56-
except HTMLOutput as output:
57-
return str(output)
67+
exec(exec_source) # noqa: S102
68+
except MarkdownOutput as raised_output:
69+
output = str(raised_output)
70+
except HTMLOutput as raised_output:
71+
output = f'<div markdown="0">{str(raised_output)}</div>'
5872
except Exception:
59-
return Markup(deepcopy(md).convert(f"```python\n{traceback.format_exc()}\n```"))
60-
return ""
73+
output = code_block("python", traceback.format_exc())
74+
if show_source:
75+
source_block = code_block("python", source)
76+
if show_source == "above":
77+
output = source_block + "\n\n" + output
78+
elif show_source == "below":
79+
output = output + "\n\n" + source_block
80+
elif show_source == "tabbed-left":
81+
output = tabbed(("Source", source_block), ("Result", output))
82+
elif show_source == "tabbed-right":
83+
output = tabbed(("Result", output), ("Source", source_block))
84+
return Markup(md_copy.convert(output))

src/markdown_exec/utils.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from __future__ import annotations
2+
3+
from textwrap import indent
4+
5+
6+
def code_block(language: str, source: str) -> str:
7+
"""Format source as a code block.
8+
9+
Parameters:
10+
language: The code block language.
11+
source: The source code to format.
12+
13+
Returns:
14+
The formatted code block.
15+
"""
16+
return f"```{language}\n{source}\n```"
17+
18+
19+
def tabbed(*tabs: tuple[str, str]) -> str:
20+
"""Format tabs using `pymdownx.tabbed` extension.
21+
22+
Parameters:
23+
*tabs: Tuples of strings: title and text.
24+
25+
Returns:
26+
The formatted tabs.
27+
"""
28+
parts = []
29+
for title, text in tabs:
30+
parts.append(f'=== "{title}"')
31+
parts.append(indent(text, prefix=" " * 4))
32+
parts.append("")
33+
return "\n".join(parts)

tests/test_python.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,4 @@ def test_output_html(md: Markdown) -> None:
3838
"""
3939
)
4040
)
41-
assert html == "<p>**Bold!**</p>"
41+
assert html == '<div markdown="0">**Bold!**</div>'

0 commit comments

Comments
 (0)