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')})' 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')})'\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')]), '')\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"
]
},
{