From f8c0850aa18f5c3f29d3301f95e3573c544118f6 Mon Sep 17 00:00:00 2001 From: Rens Date: Wed, 29 Jul 2026 15:22:44 +0200 Subject: [PATCH] Add ansi= mode to render_md: 'html' renders ANSI colors in pooled text as an {=html} block --- fastcore/nbio.py | 10 ++++++---- nbs/13_nbio.ipynb | 16 +++++++++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/fastcore/nbio.py b/fastcore/nbio.py index 8bb7a393..440b7ded 100644 --- a/fastcore/nbio.py +++ b/fastcore/nbio.py @@ -542,7 +542,7 @@ def _render_md(out, html1st=True): mime,d = preferred_msg_out(out, html1st=html1st, include_imgs=True) d = _join(d) if not d: return None - if mime=='text/plain': return 'txt', strip_ansi(d) + if mime=='text/plain': return 'txt', d elif mime=='text/html': return 'md', fenced(d.strip(), '{=html}') elif mime=='application/javascript': return 'md', fenced(f'', '{=html}') elif mime=='image/svg+xml': return 'md', fenced(d.strip(), '{=html}') @@ -550,12 +550,14 @@ def _render_md(out, html1st=True): elif mime in ('image/jpeg','image/png'): return 'md', f'![](data:{mime};base64,{"".join(d.split())})' return None -def render_md(outputs, html1st=True): - "Render notebook outputs as Markdown: text pooled into ```output fences, HTML in `{=html}` fences, markdown inlined, images as data URIs" +def render_md(outputs, html1st=True, ansi='strip'): + "Render notebook outputs as Markdown: text pooled into ```output fences, HTML in `{=html}` fences, markdown inlined, images as data URIs; `ansi='html'` renders ANSI colors in pooled text as an `{=html}` block instead of stripping them" if (not isinstance(outputs, (list,tuple))) or (outputs and not isinstance(outputs[0],dict)): return '' parts,buf = [],[] def _flush(): - if (txt := ''.join(buf).rstrip()): parts.append(fenced(txt, 'output')) + if (txt := ''.join(buf).rstrip()): + if ansi=='html' and '\x1b' in txt: parts.append(fenced(f'
{ansi2html(txt)}
', '{=html}')) + else: parts.append(fenced(strip_ansi(txt), 'output')) buf.clear() for out in concat_streams(outputs): if not (r := _render_md(out, html1st=html1st)): continue diff --git a/nbs/13_nbio.ipynb b/nbs/13_nbio.ipynb index c3a0c409..fd1b906e 100644 --- a/nbs/13_nbio.ipynb +++ b/nbs/13_nbio.ipynb @@ -2028,7 +2028,7 @@ " mime,d = preferred_msg_out(out, html1st=html1st, include_imgs=True)\n", " d = _join(d)\n", " if not d: return None\n", - " if mime=='text/plain': return 'txt', strip_ansi(d)\n", + " if mime=='text/plain': return 'txt', d\n", " elif mime=='text/html': return 'md', fenced(d.strip(), '{=html}')\n", " elif mime=='application/javascript': return 'md', fenced(f'', '{=html}')\n", " elif mime=='image/svg+xml': return 'md', fenced(d.strip(), '{=html}')\n", @@ -2036,12 +2036,14 @@ " elif mime in ('image/jpeg','image/png'): return 'md', f'![](data:{mime};base64,{\"\".join(d.split())})'\n", " return None\n", "\n", - "def render_md(outputs, html1st=True):\n", - " \"Render notebook outputs as Markdown: text pooled into ```output fences, HTML in `{=html}` fences, markdown inlined, images as data URIs\"\n", + "def render_md(outputs, html1st=True, ansi='strip'):\n", + " \"Render notebook outputs as Markdown: text pooled into ```output fences, HTML in `{=html}` fences, markdown inlined, images as data URIs; `ansi='html'` renders ANSI colors in pooled text as an `{=html}` block instead of stripping them\"\n", " if (not isinstance(outputs, (list,tuple))) or (outputs and not isinstance(outputs[0],dict)): return ''\n", " parts,buf = [],[]\n", " def _flush():\n", - " if (txt := ''.join(buf).rstrip()): parts.append(fenced(txt, 'output'))\n", + " if (txt := ''.join(buf).rstrip()):\n", + " if ansi=='html' and '\\x1b' in txt: parts.append(fenced(f'
{ansi2html(txt)}
', '{=html}'))\n", + " else: parts.append(fenced(strip_ansi(txt), 'output'))\n", " buf.clear()\n", " for out in concat_streams(outputs):\n", " if not (r := _render_md(out, html1st=html1st)): continue\n", @@ -2088,7 +2090,11 @@ "\n", "test_eq(render_md([mk_display(image_png='abc')]), '![](data:image/png;base64,abc)')\n", "test_eq(render_md([mk_error('oops')]), fenced('oops', 'output'))\n", - "test_eq(render_md([mk_stream('stdout', 'a `` b\\n')]), '```output\\na `` b\\n```')" + "test_eq(render_md([mk_stream('stdout', 'a `` b\\n')]), '```output\\na `` b\\n```')\n", + "s = '\\x1b[1;31mred\\x1b[0m \\n'\n", + "test_eq(render_md([mk_stream('stdout', s)]), fenced('red ', 'output'))\n", + "h = render_md([mk_stream('stdout', s)], ansi='html')\n", + "assert h.startswith('```{=html}') and 'ansi-red-intense-fg' in h and '<b>' in h" ] }, {