Repository: paw-lu/nbpreview (language: python)

Issue:
Debugger output
Check on how debugger output is rendered if pdb/ipbd is activated, used, and quit.



Discussion / hints:

Fix patch:
diff --git a/justfile b/justfile
index d875443c..74bdc49e 100644
--- a/justfile
+++ b/justfile
@@ -5,3 +5,7 @@ merge_dependabot:
     | jq 'select(.author.login == "dependabot")' \
     | jq '.number' \
     | parallel "gh pr comment --body='@dependabot merge'"
+
+# Run the test in the clipboad in debug mode
+debug_test:
+    poetry run pytest --pdb -k $(pbpaste)
diff --git a/poetry.lock b/poetry.lock
index 462ed9c0..107cc9f4 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -193,7 +193,7 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
 
 [[package]]
 name = "coverage"
-version = "6.0.1"
+version = "6.0.2"
 description = "Code coverage measurement for Python"
 category = "dev"
 optional = false
@@ -678,6 +678,25 @@ html5 = ["html5lib"]
 htmlsoup = ["beautifulsoup4"]
 source = ["Cython (>=0.29.7)"]
 
+[[package]]
+name = "markdown-it-py"
+version = "1.1.0"
+description = "Python port of markdown-it. Markdown parsing, done right!"
+category = "main"
+optional = false
+python-versions = "~=3.6"
+
+[package.dependencies]
+attrs = ">=19,<22"
+
+[package.extras]
+code_style = ["pre-commit (==2.6)"]
+compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.2.2,<3.3.0)", "mistletoe-ebp (>=0.10.0,<0.11.0)", "mistune (>=0.8.4,<0.9.0)", "panflute (>=1.12,<2.0)"]
+linkify = ["linkify-it-py (>=1.0,<2.0)"]
+plugins = ["mdit-py-plugins"]
+rtd = ["myst-nb (==0.13.0a1)", "pyyaml", "sphinx (>=2,<4)", "sphinx-copybutton", "sphinx-panels (>=0.4.0,<0.5.0)", "sphinx-book-theme"]
+testing = ["coverage", "psutil", "pytest (>=3.6,<4)", "pytest-benchmark (>=3.2,<4.0)", "pytest-cov", "pytest-regressions"]
+
 [[package]]
 name = "markupsafe"
 version = "2.0.1"
@@ -1674,7 +1693,7 @@ multidict = ">=4.0"
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.8.0"
-content-hash = "4e1fac710280658b1d97f8fdece1605669d06536411eb4051900c02289d2799a"
+content-hash = "9132b6d33906ac26ebdb3226f9b34fa4614f4666c562daab91c91dd62c721304"
 
 [metadata.files]
 alabaster = [
@@ -1789,39 +1808,39 @@ commonmark = [
     {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
 ]
 coverage = [
-    {file = "coverage-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:abe8207dfb8a61ded9cd830d26c1073c8218fc0ae17eb899cfe8ec0fafae6e22"},
-    {file = "coverage-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83faa3692e8306b20293889714fdf573d10ef5efc5843bd7c7aea6971487bd6a"},
-    {file = "coverage-6.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f82a17f2a77958f3eef40ad385fc82d4c6ba9a77a51a174efe03ce75daebbc16"},
-    {file = "coverage-6.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b06f4f1729e2963281d9cd6e65e6976bf27b44d4c07ac5b47223ce45f822cec"},
-    {file = "coverage-6.0.1-cp310-cp310-win32.whl", hash = "sha256:7600fac458f74c68b097379f76f3a6e3a630493fc7fc94b6508fedd9d498c194"},
-    {file = "coverage-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:2c5f39d1556e75fc3c4fb071f9e7cfa618895a999a0de763a541d730775d0d5f"},
-    {file = "coverage-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3edbb3ec580c73e5a264f5d04f30245bc98eff1a26765d46c5c65134f0a0e2f7"},
-    {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea452a2d83964d08232ade470091015e7ab9b8f53acbec10f2210fbab4ce7e43"},
-    {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1770d24f45f1f2daeae34cfa3b33fcb29702153544cd2ad40d58399dd4ff53b5"},
-    {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ad7182a82843f9f85487f44567c8c688f16c906bdb8d0e44ae462aed61cb8f1b"},
-    {file = "coverage-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:9d242a2434801ef5125330deddb4cddba8990c9a49b3dec99dca17dd7eefba5a"},
-    {file = "coverage-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e66c50f0ab445fec920a9f084914ea1776a809e3016c3738519048195f851bbb"},
-    {file = "coverage-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5b1ceacb86e0a9558061dcc6baae865ed25933ea57effea644f21657cdce19bc"},
-    {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e15ab5afbee34abf716fece80ea33ea09a82e7450512f022723b1a82ec9a4e"},
-    {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6873f3f954d3e3ab8b1881f4e5307cc19f70c9f931c41048d9f7e6fd946eabe7"},
-    {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0898d6948b31df13391cd40568de8f35fa5901bc922c5ae05cf070587cb9c666"},
-    {file = "coverage-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9c416ba03844608f45661a5b48dc59c6b5e89956efe388564dd138ca8caf540b"},
-    {file = "coverage-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:66fe33e9e0df58675e08e83fe257f89e7f625e7633ea93d0872154e09cce2724"},
-    {file = "coverage-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:353a50f123f0185cdb7a1e1e3e2cfb9d1fd7e293cfaf68eedaf5bd8e02e3ec32"},
-    {file = "coverage-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b81a4e667c45b13658b84f9b8f1d32ef86d5405fabcbd181b76b9e51d295f397"},
-    {file = "coverage-6.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:17426808e8e0824f864876312d41961223bf5e503bf8f1f846735279a60ea345"},
-    {file = "coverage-6.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e11cca9eb5c9b3eaad899728ee2ce916138399ee8cbbccaadc1871fecb750827"},
-    {file = "coverage-6.0.1-cp38-cp38-win32.whl", hash = "sha256:0a7e55cc9f7efa22d5cc9966276ec7a40a8803676f6ccbfdc06a486fba9aa9ee"},
-    {file = "coverage-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:4eb9cd910ca8e243f930243a9940ea1a522e32435d15668445753d087c30ee12"},
-    {file = "coverage-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:83682b73785d2e078e0b5f63410b8125b122e1a22422640c57edd4011c950f3e"},
-    {file = "coverage-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b45f89a8ef65c29195f8f28dbe215f44ccb29d934f3e862d2a5c12e38698a793"},
-    {file = "coverage-6.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:73880a80fad0597eca43e213e5e1711bf6c0fcdb7eb6b01b3b17841ebe5a7f8d"},
-    {file = "coverage-6.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f398d38e6ebc2637863db1d7be3d4f9c5174e7d24bb3b0716cdb1f204669cbcf"},
-    {file = "coverage-6.0.1-cp39-cp39-win32.whl", hash = "sha256:1864bdf9b2ccb43e724051bc23a1c558daf101ad4488ede1945f2a8be1facdad"},
-    {file = "coverage-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:c9c413c4397d4cdc7ca89286158d240ce524f9667b52c9a64dd7e13d16cf8815"},
-    {file = "coverage-6.0.1-pp36-none-any.whl", hash = "sha256:65da6e3e8325291f012921bbf71fea0a97824e1c573981871096aac6e2cf0ec5"},
-    {file = "coverage-6.0.1-pp37-none-any.whl", hash = "sha256:07efe1fbd72e67df026ad5109bcd216acbbd4a29d5208b3dab61779bae6b7b26"},
-    {file = "coverage-6.0.1.tar.gz", hash = "sha256:3490ff6dbf3f7accf0750136ed60ae1f487bccc1f097740e3b21262bc9c89854"},
+    {file = "coverage-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1549e1d08ce38259de2bc3e9a0d5f3642ff4a8f500ffc1b2df73fd621a6cdfc0"},
+    {file = "coverage-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcae10fccb27ca2a5f456bf64d84110a5a74144be3136a5e598f9d9fb48c0caa"},
+    {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:53a294dc53cfb39c74758edaa6305193fb4258a30b1f6af24b360a6c8bd0ffa7"},
+    {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8251b37be1f2cd9c0e5ccd9ae0380909c24d2a5ed2162a41fcdbafaf59a85ebd"},
+    {file = "coverage-6.0.2-cp310-cp310-win32.whl", hash = "sha256:db42baa892cba723326284490283a68d4de516bfb5aaba369b4e3b2787a778b7"},
+    {file = "coverage-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbffde2a68398682623d9dd8c0ca3f46fda074709b26fcf08ae7a4c431a6ab2d"},
+    {file = "coverage-6.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:60e51a3dd55540bec686d7fff61b05048ca31e804c1f32cbb44533e6372d9cc3"},
+    {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6a9409223a27d5ef3cca57dd7cd4dfcb64aadf2fad5c3b787830ac9223e01a"},
+    {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b34ae4f51bbfa5f96b758b55a163d502be3dcb24f505d0227858c2b3f94f5b9"},
+    {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbda1b550e70fa6ac40533d3f23acd4f4e9cb4e6e77251ce77fdf41b3309fb2"},
+    {file = "coverage-6.0.2-cp36-cp36m-win32.whl", hash = "sha256:4e28d2a195c533b58fc94a12826f4431726d8eb029ac21d874345f943530c122"},
+    {file = "coverage-6.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a82d79586a0a4f5fd1cf153e647464ced402938fbccb3ffc358c7babd4da1dd9"},
+    {file = "coverage-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3be1206dc09fb6298de3fce70593e27436862331a85daee36270b6d0e1c251c4"},
+    {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cd3828bbe1a40070c11fe16a51df733fd2f0cb0d745fb83b7b5c1f05967df7"},
+    {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d036dc1ed8e1388e995833c62325df3f996675779541f682677efc6af71e96cc"},
+    {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04560539c19ec26995ecfb3d9307ff154fbb9a172cb57e3b3cfc4ced673103d1"},
+    {file = "coverage-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:e4fb7ced4d9dec77d6cf533acfbf8e1415fe799430366affb18d69ee8a3c6330"},
+    {file = "coverage-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:77b1da5767ed2f44611bc9bc019bc93c03fa495728ec389759b6e9e5039ac6b1"},
+    {file = "coverage-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b598cbdbaae22d9e34e3f675997194342f866bb1d781da5d0be54783dce1ff"},
+    {file = "coverage-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e9040a43d2017f2787b28d365a4bb33fcd792c7ff46a047a04094dc0e2a30d"},
+    {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9f1627e162e3864a596486774876415a7410021f4b67fd2d9efdf93ade681afc"},
+    {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7a0b42db2a47ecb488cde14e0f6c7679a2c5a9f44814393b162ff6397fcdfbb"},
+    {file = "coverage-6.0.2-cp38-cp38-win32.whl", hash = "sha256:a1b73c7c4d2a42b9d37dd43199c5711d91424ff3c6c22681bc132db4a4afec6f"},
+    {file = "coverage-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1db67c497688fd4ba85b373b37cc52c50d437fd7267520ecd77bddbd89ea22c9"},
+    {file = "coverage-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f184bf38e74f152eed7f87e345b51f3ab0b703842f447c22efe35e59942c24"},
+    {file = "coverage-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1cf1deb3d5544bd942356364a2fdc8959bad2b6cf6eb17f47d301ea34ae822"},
+    {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad9b8c1206ae41d46ec7380b78ba735ebb77758a650643e841dd3894966c31d0"},
+    {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:381d773d896cc7f8ba4ff3b92dee4ed740fb88dfe33b6e42efc5e8ab6dfa1cfe"},
+    {file = "coverage-6.0.2-cp39-cp39-win32.whl", hash = "sha256:424c44f65e8be58b54e2b0bd1515e434b940679624b1b72726147cfc6a9fc7ce"},
+    {file = "coverage-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:abbff240f77347d17306d3201e14431519bf64495648ca5a49571f988f88dee9"},
+    {file = "coverage-6.0.2-pp36-none-any.whl", hash = "sha256:7092eab374346121805fb637572483270324407bf150c30a3b161fc0c4ca5164"},
+    {file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"},
+    {file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"},
 ]
 darglint = [
     {file = "darglint-1.8.0-py3-none-any.whl", hash = "sha256:ac6797bcc918cd8d8f14c168a4a364f54e1aeb4ced59db58e7e4c6dfec2fe15c"},
@@ -1989,8 +2008,6 @@ lxml = [
     {file = "lxml-4.6.3-cp27-cp27m-win_amd64.whl", hash = "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee"},
     {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f"},
     {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"},
-    {file = "lxml-4.6.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:64812391546a18896adaa86c77c59a4998f33c24788cadc35789e55b727a37f4"},
-    {file = "lxml-4.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c1a40c06fd5ba37ad39caa0b3144eb3772e813b5fb5b084198a985431c2f1e8d"},
     {file = "lxml-4.6.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51"},
     {file = "lxml-4.6.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586"},
     {file = "lxml-4.6.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354"},
@@ -2031,23 +2048,17 @@ lxml = [
     {file = "lxml-4.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83"},
     {file = "lxml-4.6.3.tar.gz", hash = "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468"},
 ]
+markdown-it-py = [
+    {file = "markdown-it-py-1.1.0.tar.gz", hash = "sha256:36be6bb3ad987bfdb839f5ba78ddf094552ca38ccbd784ae4f74a4e1419fc6e3"},
+    {file = "markdown_it_py-1.1.0-py3-none-any.whl", hash = "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389"},
+]
 markupsafe = [
-    {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"},
-    {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"},
-    {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"},
-    {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"},
-    {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"},
-    {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"},
-    {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"},
     {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
     {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
     {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
     {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
     {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
     {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
-    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"},
-    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"},
-    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"},
     {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
     {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
     {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
@@ -2056,21 +2067,14 @@ markupsafe = [
     {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
     {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
     {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
-    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"},
-    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"},
-    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"},
     {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
     {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
-    {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"},
     {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
     {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
     {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
     {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
     {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
     {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
-    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"},
-    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"},
-    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"},
     {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
     {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
     {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
@@ -2080,9 +2084,6 @@ markupsafe = [
     {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
     {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
     {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
-    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"},
-    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"},
-    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"},
     {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
     {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
     {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
@@ -2253,16 +2254,11 @@ pillow = [
     {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8"},
     {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b"},
     {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629"},
-    {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7"},
-    {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550"},
     {file = "Pillow-8.3.2-cp36-cp36m-win32.whl", hash = "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073"},
     {file = "Pillow-8.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196"},
     {file = "Pillow-8.3.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71"},
-    {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83"},
     {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba"},
     {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1"},
-    {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b"},
-    {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da"},
     {file = "Pillow-8.3.2-cp37-cp37m-win32.whl", hash = "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9"},
     {file = "Pillow-8.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3"},
     {file = "Pillow-8.3.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616"},
@@ -2270,8 +2266,6 @@ pillow = [
     {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979"},
     {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4"},
     {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30"},
-    {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6"},
-    {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b"},
     {file = "Pillow-8.3.2-cp38-cp38-win32.whl", hash = "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341"},
     {file = "Pillow-8.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb"},
     {file = "Pillow-8.3.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9"},
@@ -2279,18 +2273,12 @@ pillow = [
     {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056"},
     {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6"},
     {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d"},
-    {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b"},
-    {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441"},
     {file = "Pillow-8.3.2-cp39-cp39-win32.whl", hash = "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09"},
     {file = "Pillow-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19"},
     {file = "Pillow-8.3.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864"},
-    {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa"},
-    {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd"},
     {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624"},
     {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"},
     {file = "Pillow-8.3.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87"},
-    {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5"},
-    {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355"},
     {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6"},
     {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1"},
     {file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"},
diff --git a/pyproject.toml b/pyproject.toml
index 66c96dae..84e8db6f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -35,6 +35,7 @@ terminedia = "^0.4.1"
 picharsso = "^2.0.1"
 validators = "^0.18.2"
 yarl = "^1.6.3"
+markdown-it-py = "^1.1.0"
 
 [tool.poetry.dev-dependencies]
 pytest = "^6.2.5"
@@ -96,6 +97,10 @@ ignore_missing_imports = true
 module = "nbpreview"
 disallow_untyped_calls = false
 
+[[tool.mypy.overrides]]
+module = "markdown_it"
+no_implicit_reexport = false
+
 # Might change, see https://github.com/python/mypy/issues/10198
 [tool.mypy-jinja2]
 no_implicit_reexport = false
diff --git a/src/nbpreview/__main__.py b/src/nbpreview/__main__.py
index 0476fd3b..0bedb4fe 100644
--- a/src/nbpreview/__main__.py
+++ b/src/nbpreview/__main__.py
@@ -7,7 +7,7 @@
 import typer
 from rich import console, traceback
 
-from nbpreview import __version__, notebook
+from nbpreview import __version__, errors, notebook
 
 app = typer.Typer()
 traceback.install(theme="ansi_dark")
@@ -103,7 +103,7 @@ def main(
             plain=plain,
             unicode=unicode,
         )
-    except nbformat.reader.NotJSONError as exception:
+    except (nbformat.reader.NotJSONError, errors.InvalidNotebookError) as exception:
         stderr_console.print(f"{file} is not a valid Jupyter Notebook path.")
         raise typer.Exit(1) from exception
 
diff --git a/src/nbpreview/component/content/input.py b/src/nbpreview/component/content/input.py
index 82afbd78..584b530b 100644
--- a/src/nbpreview/component/content/input.py
+++ b/src/nbpreview/component/content/input.py
@@ -5,7 +5,6 @@
 from typing import Literal, Optional, Union
 
 import pygments
-from picharsso.draw import gradient
 from rich import padding, panel, syntax, text
 from rich.console import RenderableType
 from rich.padding import Padding, PaddingDimensions
@@ -57,7 +56,7 @@ def __init__(
         hyperlinks: bool,
         files: bool,
         hide_hyperlink_hints: bool,
-        characters: str = gradient.DEFAULT_CHARSET,
+        characters: Optional[str] = None,
     ) -> None:
         """Constructor."""
         super().__init__(source, plain=True)
@@ -79,14 +78,14 @@ def __rich__(self) -> Padding:
         rendered_markdown = padding.Padding(
             markdown.CustomMarkdown(
                 self.source,
-                inline_code_theme=self.theme,
+                theme=self.theme,
                 nerd_font=self.nerd_font,
                 unicode=self.unicode,
                 images=self.images,
                 image_drawing=self.image_drawing,
                 color=self.color,
                 negative_space=self.negative_space,
-                hyperlinks_=self.hyperlinks,
+                hyperlinks=self.hyperlinks,
                 files=self.files,
                 hide_hyperlink_hints=self.hide_hyperlink_hints,
                 characters=self.characters,
diff --git a/src/nbpreview/component/content/output/__init__.py b/src/nbpreview/component/content/output/__init__.py
index fc6262a8..8d8841d1 100644
--- a/src/nbpreview/component/content/output/__init__.py
+++ b/src/nbpreview/component/content/output/__init__.py
@@ -1,8 +1,1 @@
 """Jupyter notebook output data."""
-from typing import Union
-
-from nbpreview.component.content.output.error import Error
-from nbpreview.component.content.output.result import Result
-from nbpreview.component.content.output.stream import Stream
-
-Output = Union[Result, Error, Stream]
diff --git a/src/nbpreview/component/content/output/output.py b/src/nbpreview/component/content/output/output.py
new file mode 100644
index 00000000..323336d4
--- /dev/null
+++ b/src/nbpreview/component/content/output/output.py
@@ -0,0 +1,8 @@
+"""Jupyter notebook output data."""
+from typing import Union
+
+from nbpreview.component.content.output.error import Error
+from nbpreview.component.content.output.result.result import Result
+from nbpreview.component.content.output.stream import Stream
+
+Output = Union[Result, Error, Stream]
diff --git a/src/nbpreview/component/content/output/result/__init__.py b/src/nbpreview/component/content/output/result/__init__.py
index f4fca16c..90c76354 100644
--- a/src/nbpreview/component/content/output/result/__init__.py
+++ b/src/nbpreview/component/content/output/result/__init__.py
@@ -1,54 +1,1 @@
 """Execution results from Jupyter notebooks."""
-from typing import Dict, Iterator, Literal, Union
-
-from nbformat import NotebookNode
-
-from nbpreview.component.content.output.result import display_data, link
-from nbpreview.component.content.output.result.display_data import DisplayData
-from nbpreview.component.content.output.result.drawing import Drawing
-from nbpreview.component.content.output.result.execution_indicator import Execution
-from nbpreview.component.content.output.result.link import FileLink
-
-Result = Union[FileLink, DisplayData, Drawing]
-
-
-def render_result(
-    output: NotebookNode,
-    plain: bool,
-    unicode: bool,
-    execution: Union[Execution, None],
-    hyperlinks: bool,
-    nerd_font: bool,
-    files: bool,
-    hide_hyperlink_hints: bool,
-    theme: str,
-    images: bool,
-    image_drawing: Literal["block", "character", "braille"],
-    color: bool,
-    negative_space: bool,
-) -> Iterator[Result]:
-    """Render executed result outputs."""
-    data: Dict[str, Union[str, NotebookNode]] = output.get("data", {})
-    link_result = link.render_link(
-        data,
-        unicode=unicode,
-        hyperlinks=hyperlinks,
-        execution=execution,
-        nerd_font=nerd_font,
-        files=files,
-        hide_hyperlink_hints=hide_hyperlink_hints,
-    )
-    main_result = display_data.render_display_data(
-        data,
-        unicode=unicode,
-        plain=plain,
-        nerd_font=nerd_font,
-        theme=theme,
-        images=images,
-        image_drawing=image_drawing,
-        color=color,
-        negative_space=negative_space,
-    )
-    for result in (link_result, main_result):
-        if result is not None:
-            yield result
diff --git a/src/nbpreview/component/content/output/result/display_data.py b/src/nbpreview/component/content/output/result/display_data.py
index e83febce..fc810b7f 100644
--- a/src/nbpreview/component/content/output/result/display_data.py
+++ b/src/nbpreview/component/content/output/result/display_data.py
@@ -3,47 +3,99 @@
 
 import collections
 import dataclasses
+import enum
 import json
-from typing import ClassVar, Dict, List, Literal, Union
+from typing import ClassVar, Dict, Iterator, List, Literal, Optional, Union
 
 import html2text
 from lxml import html
 from lxml.html import HtmlElement
 from pylatexenc import latex2text
-from rich import box, markdown, style, syntax, table, text
-from rich.console import ConsoleRenderable
+from rich import measure, syntax, text
+from rich.console import Console, ConsoleOptions, ConsoleRenderable
 from rich.emoji import Emoji
-from rich.markdown import Markdown
-from rich.style import Style
+from rich.measure import Measurement
 from rich.syntax import Syntax
 from rich.table import Table
 from rich.text import Text
 
-from nbpreview.component.content.output.result import drawing, link
+from nbpreview.component import markdown
+from nbpreview.component.content.output.result import drawing, link, table
 from nbpreview.component.content.output.result.drawing import Drawing
+from nbpreview.component.markdown import CustomMarkdown
 from nbpreview.data import Data
 
 
 def _render_html(
-    data: Data, unicode: bool, theme: str
+    data: Data,
+    theme: str,
+    nerd_font: bool,
+    unicode: bool,
+    images: bool,
+    image_drawing: Literal["block", "character", "braille"],
+    color: bool,
+    negative_space: bool,
+    hyperlinks: bool,
+    files: bool,
+    hide_hyperlink_hints: bool,
+    characters: Optional[str] = None,
 ) -> Union[DataFrameDisplay, HTMLDisplay]:
     """Render HTML output."""
     display_data: Union[DataFrameDisplay, HTMLDisplay]
     html_data = data["text/html"]
-    if DataFrameDisplay.is_dataframe(html_data):
-        display_data = DataFrameDisplay.from_data(data, unicode=unicode)
+    dataframe_display_type = DataFrameDisplay.dataframe_display_type(html_data)
+    styled = dataframe_display_type == DataFrameDisplayType.STYLED
+    if dataframe_display_type is not None:
+        display_data = DataFrameDisplay.from_data(data, unicode=unicode, styled=styled)
     else:
-        display_data = HTMLDisplay.from_data(data, theme=theme)
+        display_data = HTMLDisplay.from_data(
+            data,
+            theme=theme,
+            nerd_font=nerd_font,
+            unicode=unicode,
+            images=images,
+            image_drawing=image_drawing,
+            color=color,
+            negative_space=negative_space,
+            hyperlinks=hyperlinks,
+            files=files,
+            hide_hyperlink_hints=hide_hyperlink_hints,
+            characters=characters,
+        )
     return display_data
 
 
 def _choose_basic_renderer(
-    data: Data, unicode: bool, nerd_font: bool, theme: str
+    data: Data,
+    unicode: bool,
+    nerd_font: bool,
+    theme: str,
+    images: bool,
+    image_drawing: Literal["block", "character", "braille"],
+    color: bool,
+    negative_space: bool,
+    hyperlinks: bool,
+    files: bool,
+    hide_hyperlink_hints: bool,
+    characters: Optional[str] = None,
 ) -> Union[MarkdownDisplay, LaTeXDisplay, JSONDisplay, PDFDisplay, PlainDisplay, None]:
     """Render straightforward text data."""
     display_data: DisplayData
     if "text/markdown" in data:
-        display_data = MarkdownDisplay.from_data(data, theme=theme)
+        display_data = MarkdownDisplay.from_data(
+            data,
+            theme=theme,
+            nerd_font=nerd_font,
+            unicode=unicode,
+            images=images,
+            image_drawing=image_drawing,
+            color=color,
+            negative_space=negative_space,
+            hyperlinks=hyperlinks,
+            files=files,
+            hide_hyperlink_hints=hide_hyperlink_hints,
+            characters=characters,
+        )
         return display_data
     elif unicode and "text/latex" in data:
         display_data = LaTeXDisplay.from_data(data)
@@ -71,6 +123,10 @@ def render_display_data(
     image_drawing: Literal["block", "character", "braille"],
     color: bool,
     negative_space: bool,
+    hyperlinks: bool,
+    files: bool,
+    hide_hyperlink_hints: bool,
+    characters: Optional[str] = None,
 ) -> Union[DisplayData, None, Drawing]:
     """Render the notebook display data."""
     display_data: Union[DisplayData, None, Drawing]
@@ -95,11 +151,35 @@ def render_display_data(
                     return display_data
 
     if not plain and "text/html" in data:
-        display_data = _render_html(data, unicode=unicode, theme=theme)
+        display_data = _render_html(
+            data,
+            unicode=unicode,
+            theme=theme,
+            nerd_font=nerd_font,
+            images=images,
+            image_drawing=image_drawing,
+            color=color,
+            negative_space=negative_space,
+            hyperlinks=hyperlinks,
+            files=files,
+            hide_hyperlink_hints=hide_hyperlink_hints,
+            characters=characters,
+        )
         return display_data
     else:
         display_data = _choose_basic_renderer(
-            data, unicode=unicode, nerd_font=nerd_font, theme=theme
+            data,
+            unicode=unicode,
+            nerd_font=nerd_font,
+            theme=theme,
+            images=images,
+            image_drawing=image_drawing,
+            color=color,
+            negative_space=negative_space,
+            hyperlinks=hyperlinks,
+            files=files,
+            hide_hyperlink_hints=hide_hyperlink_hints,
+            characters=characters,
         )
         return display_data
 
@@ -134,19 +214,66 @@ class HTMLDisplay(DisplayData):
     """Notebook HTML display data."""
 
     theme: str
+    nerd_font: bool
+    unicode: bool
+    images: bool
+    image_drawing: Literal["block", "character", "braille"]
+    color: bool
+    negative_space: bool
+    hyperlinks: bool
+    files: bool
+    hide_hyperlink_hints: bool
+    characters: Optional[str] = None
     data_type: ClassVar[str] = "text/html"
 
     @classmethod
-    def from_data(cls, data: Data, theme: str) -> HTMLDisplay:
+    def from_data(
+        cls,
+        data: Data,
+        theme: str,
+        nerd_font: bool,
+        unicode: bool,
+        images: bool,
+        image_drawing: Literal["block", "character", "braille"],
+        color: bool,
+        negative_space: bool,
+        hyperlinks: bool,
+        files: bool,
+        hide_hyperlink_hints: bool,
+        characters: Optional[str] = None,
+    ) -> HTMLDisplay:
         """Create an HTML display data from notebook data."""
         content = data[cls.data_type]
-        return cls(content, theme=theme)
+        return cls(
+            content,
+            theme=theme,
+            nerd_font=nerd_font,
+            unicode=unicode,
+            images=images,
+            image_drawing=image_drawing,
+            color=color,
+            negative_space=negative_space,
+            hyperlinks=hyperlinks,
+            files=files,
+            hide_hyperlink_hints=hide_hyperlink_hints,
+            characters=characters,
+        )
 
-    def __rich__(self) -> Markdown:
+    def __rich__(self) -> CustomMarkdown:
         """Render the HTML display data."""
         converted_markdown = html2text.html2text(self.content)
-        rendered_html = markdown.Markdown(
-            converted_markdown, inline_code_theme=self.theme
+        rendered_html = markdown.CustomMarkdown(
+            converted_markdown,
+            theme=self.theme,
+            unicode=self.unicode,
+            images=self.images,
+            image_drawing=self.image_drawing,
+            color=self.color,
+            negative_space=self.negative_space,
+            hyperlinks=self.hyperlinks,
+            files=self.files,
+            hide_hyperlink_hints=self.hide_hyperlink_hints,
+            characters=self.characters,
         )
         return rendered_html
 
@@ -163,84 +290,127 @@ def _render_table_element(column: HtmlElement, column_width: int) -> List[Text]:
     """
     attributes = column.attrib
     column_width = int(attributes.get("colspan", 1))
-    text_style: Union[str, Style] = style.Style(bold=True) if column.tag == "th" else ""
-    column_string = column.text if column.text is not None else ""
-    element_text = text.Text(column_string, style=text_style)
+    header = column.tag == "th"
+    column_string = column.text.strip() if column.text is not None else ""
+    element_text = table.create_table_element(column_string, header=header)
     table_element = (column_width - 1) * [text.Text("")] + [element_text]
     return table_element
 
 
-def _render_dataframe(table_html: List[HtmlElement], unicode: bool) -> Table:
+@dataclasses.dataclass
+class HTMLDataFrameRender:
+    """Rich counterpart of HTML table."""
+
+    unicode: bool
+
+    def __post_init__(self) -> None:
+        """Constructor."""
+        self.table = table.create_table(unicode=self.unicode)
+
+    def add_headers(self, column_rows: List[HtmlElement]) -> None:
+        """Add headers to table."""
+        n_column_rows = len(column_rows)
+        for i, column_row in enumerate(column_rows):
+
+            table_row = []
+            for column in column_row.xpath("th|td"):
+                attributes = column.attrib
+                column_width = int(attributes.get("colspan", 1))
+
+                if i == 0:
+                    for _ in range(column_width):
+                        self.table.add_column(justify="right")
+
+                table_element = _render_table_element(column, column_width=column_width)
+                table_row.extend(table_element)
+
+            end_section = i == n_column_rows - 1
+            self.table.add_row(*table_row, end_section=end_section)
+
+    def add_data(self, data_rows: List[HtmlElement]) -> None:
+        """Add data rows to table."""
+        previous_row_spans: Dict[int, int] = {}
+        for row in data_rows:
+
+            table_row = []
+            current_row_spans: Dict[int, int] = collections.defaultdict(int)
+            for i, column in enumerate(row.xpath("th|td")):
+                attributes = column.attrib
+                column_width = int(attributes.get("colspan", 1))
+                row_span = int(attributes.get("rowspan", 1))
+                table_element = _render_table_element(column, column_width=column_width)
+                table_row.extend(table_element)
+
+                if 1 < row_span:
+                    current_row_spans[i] += row_span
+
+            for column, row_span in previous_row_spans.copy().items():
+                table_row.insert(column, text.Text(""))
+                remaining_span = row_span - 1
+
+                if 1 < remaining_span:
+                    previous_row_spans[column] = remaining_span
+                else:
+                    previous_row_spans.pop(column, None)
+
+            previous_row_spans = {
+                column: previous_row_spans.get(column, 0)
+                + current_row_spans.get(column, 0)
+                for column in previous_row_spans.keys() | current_row_spans.keys()
+            }
+            self.table.add_row(*table_row)
+
+        if table.is_only_header(self.table):
+            # Divide won't show up unless there is content underneath
+            self.table.add_row("")
+
+    def __rich_console__(
+        self, console: Console, options: ConsoleOptions
+    ) -> Iterator[Table]:
+        """Render the DataFrame table."""
+        yield self.table
+
+    def __rich_measure__(
+        self, console: Console, options: ConsoleOptions
+    ) -> Measurement:
+        """Define the dimensions of the rendered DataFrame."""
+        measurement = measure.Measurement.get(
+            console=console, options=options, renderable=self.table
+        )
+        return measurement
+
+
+def _render_dataframe(
+    dataframe_html: HtmlElement, unicode: bool
+) -> HTMLDataFrameRender:
     """Render a DataFrame from its HTML.
 
     Args:
-        table_html (List[HtmlElement]): The DataFrame rendered as HTML.
+        dataframe_html (HtmlElement): The DataFrame rendered as HTML.
         unicode (bool): Whether to use unicode characters when rendering
             the table.
 
     Returns:
-        Table: The DataFrame rendered as a Rich Table.
+        HTMLDataFrameRender: The DataFrame rendered as a Rich Table.
     """
-    dataframe_html = table_html[0]
-    column_rows = dataframe_html.find("thead").findall("tr")
-
-    dataframe_table = table.Table(
-        show_edge=False,
-        show_header=False,
-        box=box.HORIZONTALS,
-        show_footer=False,
-        safe_box=not unicode,
-    )
+    thead_element = dataframe_html.find("thead")
+    column_rows = thead_element.findall("tr") if thead_element is not None else []
 
-    n_column_rows = len(column_rows)
-    for i, column_row in enumerate(column_rows):
+    rendered_html_dataframe = HTMLDataFrameRender(unicode=unicode)
+    rendered_html_dataframe.add_headers(column_rows)
 
-        table_row = []
-        for column in column_row.xpath("th|td"):
-            attributes = column.attrib
-            column_width = int(attributes.get("colspan", 1))
+    tbody_element = dataframe_html.find("tbody")
+    data_rows = tbody_element.findall("tr") if tbody_element is not None else []
+    rendered_html_dataframe.add_data(data_rows)
 
-            if i == 0:
-                for _ in range(column_width):
-                    dataframe_table.add_column(justify="right")
+    return rendered_html_dataframe
 
-            table_element = _render_table_element(column, column_width=column_width)
-            table_row.extend(table_element)
 
-        end_section = i == n_column_rows - 1
-        dataframe_table.add_row(*table_row, end_section=end_section)
+class DataFrameDisplayType(enum.Enum):
+    """The type of DataFrame HTML output."""
 
-    previous_row_spans: Dict[int, int] = {}
-    for row in dataframe_html.find("tbody").findall("tr"):
-
-        table_row = []
-        current_row_spans: Dict[int, int] = collections.defaultdict(int)
-        for i, column in enumerate(row.xpath("th|td")):
-            attributes = column.attrib
-            column_width = int(attributes.get("colspan", 1))
-            row_span = int(attributes.get("rowspan", 1))
-            table_element = _render_table_element(column, column_width=column_width)
-            table_row.extend(table_element)
-
-            if 1 < row_span:
-                current_row_spans[i] += row_span
-
-        for column, row_span in previous_row_spans.copy().items():
-            table_row.insert(column, text.Text(""))
-            remaining_span = row_span - 1
-
-            if 1 < remaining_span:
-                previous_row_spans[column] = remaining_span
-            else:
-                previous_row_spans.pop(column, None)
-
-        previous_row_spans = {
-            column: previous_row_spans.get(column, 0) + current_row_spans.get(column, 0)
-            for column in previous_row_spans.keys() | current_row_spans.keys()
-        }
-        dataframe_table.add_row(*table_row)
-
-    return dataframe_table
+    PLAIN = enum.auto()
+    STYLED = enum.auto()
 
 
 @dataclasses.dataclass
@@ -248,27 +418,49 @@ class DataFrameDisplay(DisplayData):
     """Notebook DataFrame display data."""
 
     unicode: bool
+    styled: bool = False
     data_type: ClassVar[str] = "text/html"
 
     @staticmethod
-    def is_dataframe(content: str) -> bool:
-        """Determine if DataFrame is contained with in HTML."""
-        table_html = html.fromstring(content).find_class("dataframe")
+    def dataframe_display_type(content: str) -> Union[DataFrameDisplayType, None]:
+        """Determine the type of DataFrame output."""
+        html_element = html.fromstring(content)
+
+        table_html = html_element.find_class("dataframe")
         if table_html and table_html[0].tag == "table":
-            return True
+            return DataFrameDisplayType.PLAIN
+
+        # Basic check for styled dataframe
+        try:
+            style_element, *non_style_elements = html_element.head.iterchildren()
+            *non_table_elements, table_element = html_element.body.iterchildren()
+        except (ValueError, IndexError):
+            pass
         else:
-            return False
+            if (
+                len(non_style_elements) == 0
+                and style_element.tag == "style"
+                and style_element.attrib == {"type": "text/css"}
+                and len(non_table_elements) <= 1
+                and table_element.tag == "table"
+            ):
+                return DataFrameDisplayType.STYLED
+
+        return None
 
     @classmethod
-    def from_data(cls, data: Data, unicode: bool) -> DataFrameDisplay:
+    def from_data(cls, data: Data, unicode: bool, styled: bool) -> DataFrameDisplay:
         """Create DataFrame display data from notebook data."""
         content = data[cls.data_type]
-        return cls(content, unicode=unicode)
+        return cls(content, unicode=unicode, styled=styled)
 
-    def __rich__(self) -> Table:
+    def __rich__(self) -> HTMLDataFrameRender:
         """Render the DataFrame display data."""
-        table_html = html.fromstring(self.content).find_class("dataframe")
-        rendered_dataframe = _render_dataframe(table_html, unicode=self.unicode)
+        if self.styled:
+            dataframe_html, *_ = html.fromstring(self.content).xpath("//body/table")
+        else:
+            dataframe_html, *_ = html.fromstring(self.content).find_class("dataframe")
+        rendered_dataframe = _render_dataframe(dataframe_html, unicode=self.unicode)
         return rendered_dataframe
 
 
@@ -277,18 +469,65 @@ class MarkdownDisplay(DisplayData):
     """Notebook Markdown display data."""
 
     theme: str
+    nerd_font: bool
+    unicode: bool
+    images: bool
+    image_drawing: Literal["block", "character", "braille"]
+    color: bool
+    negative_space: bool
+    hyperlinks: bool
+    files: bool
+    hide_hyperlink_hints: bool
+    characters: Optional[str] = None
     data_type: ClassVar[str] = "text/markdown"
 
     @classmethod
-    def from_data(cls, data: Data, theme: str) -> MarkdownDisplay:
+    def from_data(
+        cls,
+        data: Data,
+        theme: str,
+        nerd_font: bool,
+        unicode: bool,
+        images: bool,
+        image_drawing: Literal["block", "character", "braille"],
+        color: bool,
+        negative_space: bool,
+        hyperlinks: bool,
+        files: bool,
+        hide_hyperlink_hints: bool,
+        characters: Optional[str] = None,
+    ) -> MarkdownDisplay:
         """Create Markdown display data from notebook data."""
         content = data[cls.data_type]
-        return cls(content, theme=theme)
+        return cls(
+            content,
+            theme=theme,
+            nerd_font=nerd_font,
+            unicode=unicode,
+            images=images,
+            image_drawing=image_drawing,
+            color=color,
+            negative_space=negative_space,
+            hyperlinks=hyperlinks,
+            files=files,
+            hide_hyperlink_hints=hide_hyperlink_hints,
+            characters=characters,
+        )
 
-    def __rich__(self) -> Markdown:
+    def __rich__(self) -> CustomMarkdown:
         """Render the Markdown display data."""
-        rendered_markdown = markdown.Markdown(
-            self.content, inline_code_theme=self.theme
+        rendered_markdown = markdown.CustomMarkdown(
+            self.content,
+            theme=self.theme,
+            unicode=self.unicode,
+            images=self.images,
+            image_drawing=self.image_drawing,
+            color=self.color,
+            negative_space=self.negative_space,
+            hyperlinks=self.hyperlinks,
+            files=self.files,
+            hide_hyperlink_hints=self.hide_hyperlink_hints,
+            characters=self.characters,
         )
         return rendered_markdown
 
diff --git a/src/nbpreview/component/content/output/result/drawing.py b/src/nbpreview/component/content/output/result/drawing.py
index b219412f..17bbeb42 100644
--- a/src/nbpreview/component/content/output/result/drawing.py
+++ b/src/nbpreview/component/content/output/result/drawing.py
@@ -52,7 +52,7 @@ def choose_drawing(
     image_drawing: Literal["block", "character", "braille"],
     color: bool,
     negative_space: bool,
-    characters: str = gradient.DEFAULT_CHARSET,
+    characters: Optional[str] = None,
 ) -> Union[Drawing, None]:
     """Choose which drawing to render an image with."""
     rendered_image: Drawing
@@ -91,7 +91,7 @@ def render_drawing(
     image_type: str,
     color: bool,
     negative_space: bool,
-    characters: str = gradient.DEFAULT_CHARSET,
+    characters: Optional[str] = None,
 ) -> Union[Drawing, None]:
     """Render a drawing of an image."""
     image = _get_image(data, image_type=image_type)
@@ -108,7 +108,7 @@ def render_drawing(
     return rendered_drawing
 
 
-class Bottleneck(str, enum.Enum):
+class Bottleneck(enum.Enum):
     """The bottleneck when rendering a drawing."""
 
     WIDTH = enum.auto()
@@ -341,11 +341,12 @@ def _render_character_drawing(
     max_width: int,
     max_height: int,
     fallback_text: str,
-    characters: str = gradient.DEFAULT_CHARSET,
+    characters: Optional[str] = None,
     negative_space: bool = True,
 ) -> Tuple[Text, ...]:
     """Render a representation of an image with text characters."""
     rendered_character_drawing: Tuple[Text, ...]
+    characters = characters if characters is not None else gradient.DEFAULT_CHARSET
     try:
         pil_image = PIL.Image.open(io.BytesIO(image))
 
@@ -385,7 +386,7 @@ def __init__(
         fallback_text: str,
         color: bool,
         negative_space: bool,
-        characters: str = gradient.DEFAULT_CHARSET,
+        characters: Optional[str] = None,
     ) -> None:
         """Constructor."""
         super().__init__(image=image, fallback_text=fallback_text)
@@ -410,7 +411,7 @@ def from_data(
         image_type: str,
         color: bool,
         negative_space: bool,
-        characters: str = gradient.DEFAULT_CHARSET,
+        characters: Optional[str] = None,
     ) -> CharacterDrawing:
         """Create a drawing from notebook data."""
         encoded_image = data[image_type]
diff --git a/src/nbpreview/component/content/output/result/result.py b/src/nbpreview/component/content/output/result/result.py
new file mode 100644
index 00000000..24cf8eab
--- /dev/null
+++ b/src/nbpreview/component/content/output/result/result.py
@@ -0,0 +1,57 @@
+"""Render execution results from Jupyter Notebooks."""
+from typing import Dict, Iterator, Literal, Union
+
+from nbformat import NotebookNode
+
+from nbpreview.component.content.output.result import display_data, link
+from nbpreview.component.content.output.result.display_data import DisplayData
+from nbpreview.component.content.output.result.drawing import Drawing
+from nbpreview.component.content.output.result.execution_indicator import Execution
+from nbpreview.component.content.output.result.link import FileLink
+
+Result = Union[FileLink, DisplayData, Drawing]
+
+
+def render_result(
+    output: NotebookNode,
+    plain: bool,
+    unicode: bool,
+    execution: Union[Execution, None],
+    hyperlinks: bool,
+    nerd_font: bool,
+    files: bool,
+    hide_hyperlink_hints: bool,
+    theme: str,
+    images: bool,
+    image_drawing: Literal["block", "character", "braille"],
+    color: bool,
+    negative_space: bool,
+) -> Iterator[Result]:
+    """Render executed result outputs."""
+    data: Dict[str, Union[str, NotebookNode]] = output.get("data", {})
+    link_result = link.render_link(
+        data,
+        unicode=unicode,
+        hyperlinks=hyperlinks,
+        execution=execution,
+        nerd_font=nerd_font,
+        files=files,
+        hide_hyperlink_hints=hide_hyperlink_hints,
+    )
+    main_result = display_data.render_display_data(
+        data,
+        unicode=unicode,
+        plain=plain,
+        nerd_font=nerd_font,
+        theme=theme,
+        images=images,
+        image_drawing=image_drawing,
+        color=color,
+        negative_space=negative_space,
+        hyperlinks=hyperlinks,
+        files=files,
+        hide_hyperlink_hints=hide_hyperlink_hints,
+    )
+    for result in (link_result, main_result):
+        if result is not None:
+            yield result
diff --git a/src/nbpreview/component/content/output/result/table.py b/src/nbpreview/component/content/output/result/table.py
new file mode 100644
index 00000000..43ef1ba9
--- /dev/null
+++ b/src/nbpreview/component/content/output/result/table.py
@@ -0,0 +1,135 @@
+"""Render a table."""
+import dataclasses
+import itertools
+from collections import abc
+from typing import Iterator, Union
+
+from markdown_it import MarkdownIt
+from markdown_it.token import Token
+from rich import box, markdown, style, table, text
+from rich.markdown import Markdown
+from rich.style import Style
+from rich.table import Table
+from rich.text import Text
+
+from nbpreview import errors
+
+
+def create_table(unicode: bool) -> Table:
+    """Create a rich table."""
+    rich_table = table.Table(
+        show_edge=False,
+        show_header=False,
+        box=box.HORIZONTALS,
+        show_footer=False,
+        safe_box=not unicode,
+    )
+    return rich_table
+
+
+def create_table_element(element_data: str, header: bool) -> Text:
+    """Create a single table element."""
+    text_style: Union[str, Style] = style.Style(bold=True) if header else ""
+    rich_text = text.Text(element_data, style=text_style)
+    return rich_text
+
+
+def is_only_header(rich_table: Table) -> bool:
+    """Detect if table is only headers and no content."""
+    only_header = 1 <= rich_table.row_count and rich_table.rows[-1].end_section
+    return only_header
+
+
+@dataclasses.dataclass
+class TableSection:
+    """A section of rendered Markdown containing a table."""
+
+    table: Table
+    start_line: int
+    end_line: int
+
+
+class NotIteratorError(ValueError, errors.NBPreviewError):
+    """Error when not an iterator."""
+
+    def __init__(self, arg_name: str) -> None:
+        """Constructor."""
+        super().__init__(f"{arg_name} not an iterator")
+
+
+class NotUniqueError(ValueError, errors.NBPreviewError):
+    """Error when arguments are not unique."""
+
+    def __init__(self, *args: str) -> None:
+        """Constructor."""
+        argument_names = ", ".join(args)
+        super().__init__(f"{argument_names} must be unique")
+
+
+def _group_tokens(
+    iterator: Iterator[Token], open_tag: str, close_tag: str
+) -> Iterator[Iterator[Token]]:
+    """Group tokens bounded by tags into separate iterators."""
+    if not isinstance(iterator, abc.Iterator):
+        raise NotIteratorError("iterator")
+    if open_tag == close_tag:
+        raise NotUniqueError(open_tag, close_tag)
+    for token in iterator:
+        if token.type == open_tag:
+            yield itertools.takewhile(lambda token_: token_.type != close_tag, iterator)
+
+
+def _parse_table_element(element: str, header: bool) -> Markdown:
+    """Parse the text in a table element as Markdown."""
+    if header and element.strip():
+        element = f"**{element}**"
+    table_element = markdown.Markdown(element)
+    return table_element
+
+
+def parse_markdown_tables(markup: str, unicode: bool) -> Iterator[TableSection]:
+    """Return parsed tables from markdown."""
+    markdown_parser = MarkdownIt("zero").enable("table")
+    parsed_markup = markdown_parser.parse(markup)
+    is_header = {"th_open": True, "td_open": False}
+    iter_parsed_markup = iter(parsed_markup)
+    for parsed_table in _group_tokens(
+        iter_parsed_markup, open_tag="table_open", close_tag="table_close"
+    ):
+        start_line = None
+        rich_table = create_table(unicode)
+        for parsed_row in _group_tokens(
+            parsed_table, open_tag="tr_open", close_tag="tr_close"
+        ):
+            row_text = []
+            header = False
+            end_section = True
+            for token in parsed_row:
+                if token.type in is_header:
+                    header = is_header[token.type]
+                    if token.type == "td_open":
+                        end_section = False
+
+                elif token.type == "inline":
+                    row_text.append(
+                        _parse_table_element(
+                            token.children[0].content
+                            if token.children
+                            else token.content,
+                            header=header,
+                        )
+                    )
+                    current_line, *_ = token.map or (0, None)
+                    if start_line is None:
+                        start_line, *_ = token.map or (0, None)
+                        end_line = start_line + 1
+                    end_line = max(end_line, current_line)
+                else:
+                    header = False
+
+            rich_table.add_row(*row_text, end_section=end_section)
+
+        if is_only_header(rich_table):
+            rich_table.add_row("")
+
+        yield TableSection(rich_table, start_line=start_line or 0, end_line=end_line)
diff --git a/src/nbpreview/component/content/output/stream.py b/src/nbpreview/component/content/output/stream.py
index 81fec3c9..9e9bfad9 100644
--- a/src/nbpreview/component/content/output/stream.py
+++ b/src/nbpreview/component/content/output/stream.py
@@ -5,9 +5,9 @@
 from typing import ClassVar, Iterator, Union
 
 from nbformat import NotebookNode
-from rich import style, text
+from rich import padding, style, text
 from rich.console import ConsoleRenderable
-from rich.text import Text
+from rich.padding import Padding
 
 
 def render_stream(output: NotebookNode) -> Iterator[Stream]:
@@ -42,7 +42,8 @@ def __rich__(self) -> Union[ConsoleRenderable, str]:
     @classmethod
     def from_output(cls, output: NotebookNode) -> Stream:
         """Create stream from notebook output."""
-        text = output.get("text", "")
+        stream_text = output.get("text", "")
+        text = stream_text[:-1] if stream_text.endswith("\n") else stream_text
         return cls(text)
 
 
@@ -60,9 +61,10 @@ def from_output(cls, output: NotebookNode) -> StdErr:
         text = output.get("text", "")
         return cls(text)
 
-    def __rich__(self) -> Text:
+    def __rich__(self) -> Padding:
         """Render a stderr stream."""
-        rendered_stderr = text.Text(
-            self.content, style=style.Style(color="color(237)", bgcolor="color(174)")
+        stderr_text = text.Text(self.content, style=style.Style(color="color(237)"))
+        rendered_stderr = padding.Padding(
+            stderr_text, pad=(1, 1, 0, 1), style=style.Style(bgcolor="color(174)")
         )
         return rendered_stderr
diff --git a/src/nbpreview/component/markdown.py b/src/nbpreview/component/markdown.py
index e3be8d05..bf9c85b6 100644
--- a/src/nbpreview/component/markdown.py
+++ b/src/nbpreview/component/markdown.py
@@ -1,26 +1,30 @@
 """Override rich's markdown renderer with custom components."""
 from __future__ import annotations
 
+import dataclasses
 import io
 import os
 import pathlib
 import textwrap
 from io import BytesIO
 from pathlib import Path
-from typing import Literal, Optional, Union
+from typing import Iterable, Iterator, Literal, Optional, Union
 from urllib import parse
 
 import httpx
 import PIL
 import validators
 import yarl
-from picharsso.draw import gradient
 from PIL import Image
-from rich import _loop, markdown, rule, segment, style, syntax, text
+from rich import _loop, markdown, measure, rule, segment, style, syntax, text
 from rich.console import Console, ConsoleOptions, JustifyMethod, RenderResult
+from rich.measure import Measurement
 from rich.style import Style
+from rich.table import Table
+from rich.text import Text
 
-from nbpreview.component.content.output.result import drawing, link
+from nbpreview.component.content.output.result import drawing, link, table
+from nbpreview.component.content.output.result.table import TableSection
 
 
 class CustomCodeBlock(markdown.CodeBlock):
@@ -138,6 +142,20 @@ def render_number(
             yield new_line
 
 
+def _get_url_content(url: str) -> Union[BytesIO, None]:
+    """Return content from URL."""
+    try:
+        response = httpx.get(url)
+    except httpx.RequestError:
+        content = None
+    else:
+        try:
+            content = io.BytesIO(response.content)
+        except TypeError:
+            content = None
+    return content
+
+
 class CustomImageItem(markdown.ImageItem):
     """Renders a placeholder for an image."""
 
@@ -146,8 +164,7 @@ class CustomImageItem(markdown.ImageItem):
     images: bool = True
     image_drawing: Literal["block", "character", "braille"] = "block"
     color: bool = True
-    characters: str = gradient.DEFAULT_CHARSET
-    hyperlinks_: bool = True
+    characters: Optional[str] = None
     files: bool = True
     hide_hyperlink_hints: bool = False
     negative_space: bool = True
@@ -169,12 +186,7 @@ def __init__(self, destination: str, hyperlinks: bool) -> None:
         else:
             self.is_url = True
             self.path = pathlib.Path(yarl.URL(self.destination).path)
-            try:
-                response = httpx.get(self.destination)
-            except httpx.RequestError:
-                content = None
-            else:
-                content = io.BytesIO(response.content)
+            content = _get_url_content(self.destination)
         self.extension = self.path.suffix.lstrip(".")
         if content is not None:
             try:
@@ -208,7 +220,7 @@ def __rich_console__(
                 subject=title,
                 emoji_name="globe_with_meridians",
                 nerd_font_icon="爵",
-                hyperlinks=self.hyperlinks_,
+                hyperlinks=self.hyperlinks,
                 hide_hyperlink_hints=self.hide_hyperlink_hints,
             )
         else:
@@ -216,7 +228,7 @@ def __rich_console__(
                 content=self.image_data,
                 file_extension=self.extension,
                 unicode=self.unicode,
-                hyperlinks=self.hyperlinks_,
+                hyperlinks=self.hyperlinks,
                 nerd_font=self.nerd_font,
                 files=self.files,
                 hide_hyperlink_hints=self.hide_hyperlink_hints,
@@ -239,7 +251,7 @@ def __rich_console__(
             yield rendered_drawing
 
 
-class CustomMarkdown(markdown.Markdown):
+class MarkdownOverwrite(markdown.Markdown):
     """A custom markdown renderer."""
 
     def __init__(
@@ -257,11 +269,10 @@ def __init__(
         image_drawing: Literal["block", "character", "braille"] = "block",
         color: bool = True,
         negative_space: bool = True,
-        characters: str = gradient.DEFAULT_CHARSET,
-        hyperlinks_: bool = True,
+        characters: Optional[str] = None,
         files: bool = True,
         hide_hyperlink_hints: bool = False,
-    ):
+    ) -> None:
         """Constructor."""
         self.elements["code_block"] = CustomCodeBlock
         self.elements["heading"] = CustomHeading
@@ -277,7 +288,6 @@ def __init__(
         CustomImageItem.color = color
         CustomImageItem.negative_space = negative_space
         CustomImageItem.characters = characters
-        CustomImageItem.hyperlinks_ = hyperlinks_
         CustomImageItem.files = files
         CustomImageItem.hide_hyperlink_hints = hide_hyperlink_hints
         super().__init__(
@@ -289,3 +299,115 @@ def __init__(
             inline_code_lexer=inline_code_lexer,
             inline_code_theme=inline_code_theme,
         )
+
+
+@dataclasses.dataclass
+class CustomMarkdown:
+    """A custom markdown renderer with table support."""
+
+    source: str
+    theme: str
+    hyperlinks: bool = True
+    nerd_font: bool = False
+    unicode: bool = True
+    images: bool = True
+    image_drawing: Literal["block", "character", "braille"] = "block"
+    color: bool = True
+    negative_space: bool = True
+    characters: Optional[str] = None
+    files: bool = True
+    hide_hyperlink_hints: bool = False
+
+    def __post_init__(self) -> None:
+        """Constructor."""
+        table_sections = table.parse_markdown_tables(self.source, unicode=self.unicode)
+        self.renderables = [
+            renderable
+            for renderable in _splice_tables(
+                self.source,
+                table_sections=table_sections,
+                theme=self.theme,
+                hyperlinks=self.hyperlinks,
+                nerd_font=self.nerd_font,
+                unicode=self.unicode,
+                images=self.images,
+                image_drawing=self.image_drawing,
+                color=self.color,
+                negative_space=self.negative_space,
+                characters=self.characters,
+                files=self.files,
+                hide_hyperlink_hints=self.hide_hyperlink_hints,
+            )
+        ]
+
+    def __rich_console__(
+        self, console: Console, options: ConsoleOptions
+    ) -> RenderResult:
+        """Render the markdown."""
+        yield from self.renderables
+
+    def __rich_measure__(
+        self, console: Console, options: ConsoleOptions
+    ) -> Measurement:
+        """Define the dimensions of the rendered markdown."""
+        measurement = measure.measure_renderables(
+            console=console, options=options, renderables=self.renderables
+        )
+        return measurement
+
+
+def _splice_tables(
+    markup: str,
+    table_sections: Iterable[TableSection],
+    theme: str,
+    hyperlinks: bool,
+    nerd_font: bool,
+    unicode: bool,
+    images: bool,
+    image_drawing: Literal["block", "character", "braille"],
+    color: bool,
+    negative_space: bool,
+    files: bool,
+    hide_hyperlink_hints: bool,
+    characters: Optional[str] = None,
+) -> Iterator[Union[MarkdownOverwrite, Table, Text]]:
+    """Mix in tables with traditional markdown parser."""
+    markup_lines = markup.splitlines()
+    last_end_point = 0
+    for table_section in table_sections:
+        non_table_section = "\n".join(
+            markup_lines[last_end_point : table_section.start_line]
+        )
+        yield MarkdownOverwrite(
+            non_table_section,
+            code_theme=theme,
+            hyperlinks=hyperlinks,
+            nerd_font=nerd_font,
+            unicode=unicode,
+            images=images,
+            image_drawing=image_drawing,
+            color=color,
+            negative_space=negative_space,
+            characters=characters,
+            files=files,
+            hide_hyperlink_hints=hide_hyperlink_hints,
+        )
+        yield text.Text()
+        yield table_section.table
+        yield text.Text()
+        last_end_point = table_section.end_line + 1
+    end_section = "\n".join(markup_lines[last_end_point:])
+    yield MarkdownOverwrite(
+        end_section,
+        code_theme=theme,
+        hyperlinks=hyperlinks,
+        nerd_font=nerd_font,
+        unicode=unicode,
+        images=images,
+        image_drawing=image_drawing,
+        color=color,
+        negative_space=negative_space,
+        characters=characters,
+        files=files,
+        hide_hyperlink_hints=hide_hyperlink_hints,
+    )
diff --git a/src/nbpreview/component/row.py b/src/nbpreview/component/row.py
index c51bcfb4..15e7574c 100644
--- a/src/nbpreview/component/row.py
+++ b/src/nbpreview/component/row.py
@@ -5,14 +5,14 @@
 from typing import Iterator, List, Literal, Optional, Tuple, Union
 
 from nbformat import NotebookNode
-from picharsso.draw import gradient
 from rich import padding
 from rich.padding import Padding, PaddingDimensions
 
 from nbpreview.component.content import input
 from nbpreview.component.content.input import Cell
-from nbpreview.component.content.output import Output, error, result, stream
-from nbpreview.component.content.output.result import execution_indicator
+from nbpreview.component.content.output import error, stream
+from nbpreview.component.content.output.output import Output
+from nbpreview.component.content.output.result import execution_indicator, result
 from nbpreview.component.content.output.result.execution_indicator import Execution
 
 Content = Union[Cell, Padding]
@@ -73,9 +73,9 @@ def render_input_row(
     hyperlinks: bool,
     files: bool,
     hide_hyperlink_hints: bool,
-    characters: str = gradient.DEFAULT_CHARSET,
+    characters: Optional[str] = None,
     unicode_border: Optional[bool] = None,
-) -> Row:
+) -> Union[Row, None]:
     """Render a Jupyter Notebook cell.
 
     Args:
@@ -105,14 +105,15 @@ def render_input_row(
         hide_hyperlink_hints (bool):
             Whether to hide hyperlink hints.
         characters (str):
-            The characters to draw images with.
+            The characters to draw images with. If set to None will
+            default to ' :!?PG@'.
         unicode_border (Optional[bool]): Whether to render the cell
             borders using unicode characters. Will autodetect by
             default.
 
     Returns:
-        Row: The execution count indicator and cell
-            content.
+        Row: The execution count indicator and cell content if cell type
+            is known, else None.
     """
     cell_type = cell.get("cell_type")
     source = cell.source
@@ -148,10 +149,12 @@ def render_input_row(
             default_lexer_name=default_lexer_name,
         )
 
-    # Includes cell_type == "raw"
-    else:
+    elif cell_type == "raw":
         rendered_cell = Cell(source, plain=plain, safe_box=safe_box)
 
+    else:
+        return None
+
     cell_row = Row(rendered_cell, plain=plain, execution=execution)
     return cell_row
 
diff --git a/src/nbpreview/errors.py b/src/nbpreview/errors.py
new file mode 100644
index 00000000..344b28e9
--- /dev/null
+++ b/src/nbpreview/errors.py
@@ -0,0 +1,9 @@
+"""nbpreview errors."""
+
+
+class NBPreviewError(Exception):
+    """Base nbpreview error."""
+
+
+class InvalidNotebookError(NBPreviewError):
+    """Error when input notebook in invalid."""
diff --git a/src/nbpreview/notebook.py b/src/nbpreview/notebook.py
index 0e03eb38..c4052639 100644
--- a/src/nbpreview/notebook.py
+++ b/src/nbpreview/notebook.py
@@ -8,11 +8,11 @@
 
 import nbformat
 from nbformat.notebooknode import NotebookNode
-from picharsso.draw import gradient
 from rich import table
 from rich.console import Console, ConsoleOptions
 from rich.table import Table
 
+from nbpreview import errors
 from nbpreview.component import row
 
 # terminedia depends on fcntl, which is not present on Windows platforms
@@ -107,7 +107,7 @@ def _render_notebook(
     image_drawing: Literal["block", "character", "braille"],
     color: bool,
     negative_space: bool,
-    characters: str = gradient.DEFAULT_CHARSET,
+    characters: Optional[str] = None,
 ) -> Table:
     """Create a table representing a notebook."""
     grid = table.Table.grid(padding=(1, 1, 1, 0))
@@ -136,7 +136,8 @@ def _render_notebook(
             hide_hyperlink_hints=hide_hyperlink_hints,
             characters=characters,
         )
-        grid.add_row(*cell_row.to_table_row())
+        if cell_row is not None:
+            grid.add_row(*cell_row.to_table_row())
 
         outputs = cell.get("outputs")
         if not hide_output and outputs is not None:
@@ -230,7 +231,11 @@ def from_file(
         image_drawing: Literal["block", "character", "braille", None] = None,
     ) -> Notebook:
         """Create Notebook from notebook file."""
-        notebook_node = nbformat.read(file, as_version=4)
+        try:
+            notebook_node = nbformat.read(file, as_version=4)
+        except AttributeError as exception:
+            raise errors.InvalidNotebookError from exception
+
         return cls(
             notebook_node,
             theme=theme,


Test patch:
diff --git a/tests/component/content/output/result/test_drawing.py b/tests/component/content/output/result/test_drawing.py
index 4c6b60b7..235477d5 100644
--- a/tests/component/content/output/result/test_drawing.py
+++ b/tests/component/content/output/result/test_drawing.py
@@ -133,7 +133,7 @@ def test_character_drawing_repr(image: Image) -> None:
     output = repr(character_drawing)
     expected_output = (
         "CharacterDrawing(image=sefi, fallback_text=Hey,"
-        " color=True, negative_space=True, characters= :!?PG@)"
+        " color=True, negative_space=True, characters=None)"
     )
     assert output == expected_output
 
diff --git a/tests/component/content/output/result/test_table.py b/tests/component/content/output/result/test_table.py
new file mode 100644
index 00000000..f773cff9
--- /dev/null
+++ b/tests/component/content/output/result/test_table.py
@@ -0,0 +1,28 @@
+"""Test for nbpreview.component.content.output.result.table."""
+import pytest
+from markdown_it import token
+
+from nbpreview.component.content.output.result import table
+
+
+def test_group_tokens_equal_tags() -> None:
+    """It raises a NotUniqueError when tags are equal."""
+    with pytest.raises(table.NotUniqueError):
+        grouped_tokens = table._group_tokens(
+            (token.Token("", tag="", nesting=0) for _ in range(2)),
+            open_tag="hey",
+            close_tag="hey",
+        )
+        next(grouped_tokens)
+
+
+@pytest.mark.no_typeguard
+def test_group_tokens_not_iterator() -> None:
+    """It raises a NotIteratorError when not an iterator."""
+    with pytest.raises(table.NotIteratorError):
+        grouped_tokens = table._group_tokens(
+            [1, 2, 3],  # type: ignore[arg-type]
+            open_tag="open",
+            close_tag="close",
+        )
+        next(grouped_tokens)
diff --git a/tests/conftest.py b/tests/conftest.py
index b82692bc..39aca7e4 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -11,19 +11,11 @@
 
 
 @pytest.fixture
-def make_notebook() -> Callable[[Optional[Dict[str, Any]]], NotebookNode]:
-    """Fixture that returns a function that creates a base notebook."""
-
-    def _make_notebook(cell: Optional[Dict[str, Any]] = None) -> NotebookNode:
-        """Create a NotebookNode.
+def make_notebook_dict() -> Callable[[Optional[Dict[str, Any]]], Dict[str, Any]]:
+    """Fixture that returns function that constructs notebook dict."""
 
-        Args:
-            cell (Optional[Dict[str, Any]], optional): The cell for the
-                NotebookNode. Defaults to None.
-
-        Returns:
-            NotebookNode: The NotebookNode containing the inputted cell.
-        """
+    def _make_notebook_dict(cell: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
+        """Create valid notebook dictionary around single cell."""
         notebook = {
             "cells": [
                 {
@@ -56,8 +48,28 @@ def _make_notebook(cell: Optional[Dict[str, Any]] = None) -> NotebookNode:
         }
         if cell is not None:
             notebook["cells"] = [cell]
-        nbformat.validate(notebook, version=4)
+        return notebook
+
+    return _make_notebook_dict
+
 
+@pytest.fixture
+def make_notebook(
+    make_notebook_dict: Callable[[Optional[Dict[str, Any]]], Dict[str, Any]]
+) -> Callable[[Optional[Dict[str, Any]]], NotebookNode]:
+    """Fixture that returns a function that creates a base notebook."""
+
+    def _make_notebook(cell: Optional[Dict[str, Any]] = None) -> NotebookNode:
+        """Create a NotebookNode.
+
+        Args:
+            cell (Optional[Dict[str, Any]], optional): The cell for the
+                NotebookNode. Defaults to None.
+
+        Returns:
+            NotebookNode: The NotebookNode containing the inputted cell.
+        """
+        notebook = make_notebook_dict(cell)
         return nbformat.from_dict(notebook)
 
     return _make_notebook
diff --git a/tests/expected_outputs/test_render_debugger_output.txt b/tests/expected_outputs/test_render_debugger_output.txt
new file mode 100644
index 00000000..648b5bc6
--- /dev/null
+++ b/tests/expected_outputs/test_render_debugger_output.txt
@@ -0,0 +1,20 @@
+     ╭─────────────────────────────────────────────────────────────────────────╮
+[38;5;247m[4]:[0m │ [49m%[0m[94;49mdebug[0m                                                                  │
+     ╰─────────────────────────────────────────────────────────────────────────╯
+                                                                                
+      > [1;32m<ipython-input-4-a2d401806d89>[0m(1)[0;36m<module>[1;34m()[0m     
+      [1;32m----> 1                                                             
+      [1;33m[0m_jupyterlab_variableinspector_dict_list[0m[1;33m([0m[1;33m)[0m[1…
+      [0m                                                                       
+                                                                                
+      ipdb>  ll                                                                 
+                                                                                
+      [1;32m----> 1                                                             
+      [1;33m[0m_jupyterlab_variableinspector_dict_list[0m[1;33m([0m[1;33m)[0m[1…
+      [0m                                                                       
+                                                                                
+      ipdb>  sticky                                                             
+                                                                                
+      *** NameError: name 'sticky' is not defined                               
+                                                                                
+      ipdb>  q                                                                  
diff --git a/tests/expected_outputs/test_render_error_traceback_no_hang.txt b/tests/expected_outputs/test_render_error_traceback_no_hang.txt
index 83fb1f3b..7944d4d7 100644
--- a/tests/expected_outputs/test_render_error_traceback_no_hang.txt
+++ b/tests/expected_outputs/test_render_error_traceback_no_hang.txt
@@ -4,8 +4,9 @@
      │                                                                         │
      ╰─────────────────────────────────────────────────────────────────────────╯
                                                                                 
-      [38;5;237;48;5;174mbash: line 1: ech: command not found                                      [0m
-      [38;5;237;48;5;174m                                                                          [0m
+      [48;5;174m                                                                          [0m
+      [48;5;174m [0m[38;5;237;48;5;174mbash: line 1: ech: command not found                                    [0m[48;5;174m [0m
+      [48;5;174m [0m[38;5;237;48;5;174m                                                                        [0m[48;5;174m [0m
                                                                                 
       [1;31m-------------------------------------------------------------------------…[0m
       [1;31mCalledProcessError[0m                        Traceback (most recent call     
diff --git a/tests/test_main.py b/tests/test_main.py
index 69fb0b18..6d1e932d 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -1,4 +1,5 @@
 """Test cases for the __main__ module."""
+import json
 import pathlib
 import shlex
 import tempfile
@@ -254,11 +255,11 @@ def test_render_markdown(run_cli: RunCli) -> None:
 
 
 @pytest.mark.parametrize(
-    "args, env",
+    "arg, env",
     (("--plain", None), ("-p", None), (None, {"NBPREVIEW_PLAIN": "TRUE"})),
 )
 def test_force_plain(
-    args: Optional[str],
+    arg: Optional[str],
     env: Optional[Mapping[str, str]],
     runner: CliRunner,
     write_notebook: Callable[[Union[Dict[str, Any], None]], str],
@@ -273,12 +274,46 @@ def test_force_plain(
         "source": "def foo(x: float, y: float) -> float:\n    return x + y",
     }
     notebook_path = write_notebook(code_cell)
-    result = runner.invoke(
-        app, args=["--unicode", "--width=80", notebook_path], env=env
-    )
+    args = ["--unicode", "--width=80", notebook_path]
+    if arg is not None:
+        args = [arg] + args
+    result = runner.invoke(app, args=args, env=env)
     expected_output = (
         "def foo(x: float, y: float) -> float:                         "
         "                  \n    return x + y                          "
         "                                      \n"
     )
     assert result.output == expected_output
+
+
+def test_raise_no_source(
+    runner: CliRunner,
+    temp_file: Callable[[Optional[str]], str],
+    make_notebook_dict: Callable[[Optional[Dict[str, Any]]], Dict[str, Any]],
+) -> None:
+    """It returns an error message if there is no source."""
+    no_source_cell = {
+        "cell_type": "code",
+        "outputs": [],
+    }
+    notebook_dict = make_notebook_dict(no_source_cell)
+    notebook_path = temp_file(json.dumps(notebook_dict))
+    result = runner.invoke(app, args=[notebook_path])
+    output = result.output.replace("\n", "")
+    expected_output = f"{notebook_path} is not a valid Jupyter Notebook path."
+    assert output == expected_output
+
+
+def test_raise_no_output(
+    runner: CliRunner,
+    temp_file: Callable[[Optional[str]], str],
+    make_notebook_dict: Callable[[Optional[Dict[str, Any]]], Dict[str, Any]],
+) -> None:
+    """It returns an error message if no output in a code cell."""
+    no_source_cell = {"cell_type": "code", "source": ["x = 1\n"]}
+    notebook_dict = make_notebook_dict(no_source_cell)
+    notebook_path = temp_file(json.dumps(notebook_dict))
+    result = runner.invoke(app, args=[notebook_path])
+    output = result.output.replace("\n", "")
+    expected_output = f"{notebook_path} is not a valid Jupyter Notebook path."
+    assert output == expected_output
diff --git a/tests/test_notebook.py b/tests/test_notebook.py
index ac30367e..f27a4879 100644
--- a/tests/test_notebook.py
+++ b/tests/test_notebook.py
@@ -403,22 +403,120 @@ def test_code_markdown_cell(rich_notebook_output: RichOutput) -> None:
     }
     output = rich_notebook_output(markdown_cell)
     expected_output = (
-        "  \x1b[38;2;248;248;242;49m    \x1b[0m\x1b[38;2;1"
-        "02;217;239;49mfor\x1b[0m\x1b[38;2;248;248;242;"
-        "49m \x1b[0m\x1b[38;2;248;248;242;49mi\x1b[0m\x1b[38;"
-        "2;248;248;242;49m \x1b[0m\x1b[38;2;249;38;114;"
-        "49min\x1b[0m\x1b[38;2;248;248;242;49m \x1b[0m\x1b[38"
-        ";2;248;248;242;49mrange\x1b[0m\x1b[38;2;248;24"
-        "8;242;49m(\x1b[0m\x1b[38;2;174;129;255;49m20\x1b["
-        "0m\x1b[38;2;248;248;242;49m)\x1b[0m\x1b[38;2;248;"
-        "248;242;49m:\x1b[0m                        "
-        "                               \n  \x1b[38;2"
-        ";248;248;242;49m        \x1b[0m\x1b[38;2;248;2"
-        "48;242;49mprint\x1b[0m\x1b[38;2;248;248;242;49"
-        "m(\x1b[0m\x1b[38;2;248;248;242;49mi\x1b[0m\x1b[38;2;"
-        "248;248;242;49m)\x1b[0m                    "
+        "  \x1b[49m    \x1b[0m\x1b[94;49mfor\x1b[0m\x1b[49m \x1b[0m"
+        "\x1b[49mi\x1b[0m\x1b[49m \x1b[0m\x1b[95;49min\x1b[0m\x1b[49m "
+        "\x1b[0m\x1b[96;49mrange\x1b[0m\x1b[49m(\x1b[0m\x1b[94;49m2"
+        "0\x1b[0m\x1b[49m)\x1b[0m\x1b[49m:\x1b[0m               "
         "                                        "
-        "  \n"
+        "\n  \x1b[49m        \x1b[0m\x1b[96;49mprint\x1b[0m\x1b[4"
+        "9m(\x1b[0m\x1b[49mi\x1b[0m\x1b[49m)\x1b[0m             "
+        "                                        "
+        "         \n"
+    )
+    assert output == expected_output
+
+
+def test_table_markdown_cell(rich_notebook_output: RichOutput) -> None:
+    """It renders a markdown cell with tables."""
+    markdown_cell = {
+        "cell_type": "markdown",
+        "id": "academic-bride",
+        "metadata": {},
+        "source": """# Hey buddy
+
+*did you hear the news?*
+
+ ```python
+for i in range(20):
+    print(i)
+```
+
+| aaa | bbbb **ccc** |
+| --- | --- |
+| 111 **222** 333 | 222 |
+| susu | lulu|
+
+- so there you are
+- words
+
+| ddd | `eeee` fff |
+| --- | --- |
+
+| | |
+--- | ---
+sus | *spect*
+
+rak
+     """,
+    }
+    output = rich_notebook_output(markdown_cell)
+    expected_output = (
+        "  \x1b[1;38;2;255;255;255;48;2;96;2;238m \x1b["
+        "0m\x1b[1;38;2;255;255;255;48;2;96;2;238mHey"
+        " buddy\x1b[0m\x1b[1;38;2;255;255;255;48;2;96;2"
+        ";238m \x1b[0m\x1b[1;38;2;255;255;255;48;2;96;2"
+        ";238m                                   "
+        "                                \x1b[0m\n  \x1b"
+        "[2;38;2;96;2;238m───────────────────────"
+        "────────────────────────────────────────"
+        "───────────────\x1b[0m\n                    "
+        "                                        "
+        "                    \n  \x1b[3mdid you hear "
+        "the news?\x1b[0m                           "
+        "                             \n          "
+        "                                        "
+        "                              \n  \x1b[49m  "
+        "  \x1b[0m\x1b[94;49mfor\x1b[0m\x1b[49m \x1b[0m\x1b[49mi\x1b[0"
+        "m\x1b[49m \x1b[0m\x1b[95;49min\x1b[0m\x1b[49m \x1b[0m\x1b[96;"
+        "49mrange\x1b[0m\x1b[49m(\x1b[0m\x1b[94;49m20\x1b[0m\x1b[49"
+        "m)\x1b[0m\x1b[49m:\x1b[0m                        "
+        "                               \n  \x1b[49m "
+        "      \x1b[0m\x1b[96;49mprint\x1b[0m\x1b[49m(\x1b[0m\x1b[4"
+        "9mi\x1b[0m\x1b[49m)\x1b[0m                       "
+        "                                        "
+        "\n                                       "
+        "                                        "
+        " \n   \x1b[1maaa\x1b[0m                        "
+        "             \x1b[1mbbbb \x1b[0m\x1b[1mccc\x1b[0m   "
+        "                          \n  ───────────"
+        "────────────────────────────────────────"
+        "───────────────────────────\n   111 \x1b[1m2"
+        "22\x1b[0m 333                             2"
+        "22                                  \n   "
+        "susu                                    "
+        "lulu                                 \n  "
+        "                                        "
+        "                                      \n "
+        "                                        "
+        "                                       \n"
+        "   • so there you are                   "
+        "                                        "
+        "\n   • words                             "
+        "                                        "
+        " \n                                      "
+        "                                        "
+        "  \n   \x1b[1mddd\x1b[0m                       "
+        "              \x1b[1;97;40meeee\x1b[0m\x1b[1m fff"
+        "\x1b[0m                             \n  ────"
+        "────────────────────────────────────────"
+        "──────────────────────────────────\n     "
+        "                                        "
+        "                                   \n    "
+        "                                        "
+        "                                    \n   "
+        "                                        "
+        "                                     \n  "
+        "                                        "
+        "                                      \n "
+        " ───────────────────────────────────────"
+        "───────────────────────────────────────\n"
+        "   sus                                  "
+        "   \x1b[3mspect\x1b[0m                        "
+        "        \n                               "
+        "                                        "
+        "         \n  rak                         "
+        "                                        "
+        "          \n"
     )
     assert output == expected_output
 
@@ -878,13 +976,215 @@ def test_render_dataframe(
     assert remove_link_ids(output) == remove_link_ids(expected_output)
 
 
-def test_render_plain_dataframe(
+def test_only_header_dataframe(
     rich_notebook_output: RichOutput,
     mock_tempfile_file: Generator[Mock, None, None],
     remove_link_ids: Callable[[str], str],
     tempfile_path: Path,
 ) -> None:
-    """It renders a DataFrame in a plain style."""
+    """It renders a DataFrame with only headers."""
+    code_cell = {
+        "cell_type": "code",
+        "execution_count": 2,
+        "id": "mighty-oasis",
+        "metadata": {},
+        "outputs": [
+            {
+                "data": {
+                    "text/html": (
+                        "<div>\\n<style scoped>\\n    .dataframe tb"
+                        "ody tr th:only-of-type {\\n        vertic"
+                        "al-align: middle;\\n    }\\n\\n    .datafra"
+                        "me tbody tr th {\\n        vertical-align"
+                        ": top;\\n    }\\n\\n    .dataframe thead tr"
+                        " th {\\n        text-align: left;\\n    }\\"
+                        "n\\n    .dataframe thead tr:last-of-type "
+                        "th {\\n        text-align: right;\\n    }\\"
+                        'n</style>\\n<table border="1" class="data'
+                        'frame">\\n  <thead>\\n    <tr>\\n      <th>'
+                        'Model:</th>\\n      <th colspan="2" halig'
+                        'n="left">Decision Tree</th>\\n      <th c'
+                        'olspan="2" halign="left">Regression</th>'
+                        '\\n      <th colspan="2" halign="left">Ra'
+                        "ndom</th>\\n    </tr>\\n    <tr>\\n      <t"
+                        "h>Predicted:</th>\\n      <th>Tumour</th>"
+                        "\\n      <th>Non-Tumour</th>\\n      <th>T"
+                        "umour</th>\\n      <th>Non-Tumour</th>\\n "
+                        "     <th>Tumour</th>\\n      <th>Non-Tumo"
+                        "ur</th>\\n    </tr>\\n    <tr>\\n      <th>"
+                        "Actual Label:</th>\\n      <th></th>\\n   "
+                        "   <th></th>\\n      <th></th>\\n      <th"
+                        "></th>\\n      <th></th>\\n      <th></th>"
+                        "\\n    </tr>\\n  </thead>\\n  <tbody>\\n  </"
+                        "tbody>\\n</table>\\n</div>"
+                    ),
+                    "text/plain": (
+                        "lorep              hey                by"
+                        "e\nipsum               hi very_long_word "
+                        " hi\nfirst second third                  "
+                        "     \nbar   one    1       1            "
+                        "  2   4\n             10      3          "
+                        "    4  -1\n      three  3       3        "
+                        "      4  -1\nfoo   one    1       3      "
+                        "        4  -1"
+                    ),
+                },
+                "execution_count": 2,
+                "metadata": {},
+                "output_type": "execute_result",
+            }
+        ],
+        "source": "",
+    }
+    expected_output = (
+        "     ╭──────────────────────────────────"
+        "───────────────────────────────────────╮"
+        "\n\x1b[38;5;247m[2]:\x1b[0m │                  "
+        "                                        "
+        "               │\n     ╰─────────────────"
+        "────────────────────────────────────────"
+        "────────────────╯\n                      "
+        "                                        "
+        "                  \n\x1b[38;5;247m[2]:\x1b[0m  "
+        f"\x1b]8;id=360825;file://{tempfile_path}0.html\x1b\\\x1b"
+        "[94m🌐 Click to view"
+        " HTML\x1b[0m\x1b]8;;\x1b\\                        "
+        "                             \n          "
+        "                                        "
+        "                              \n\x1b[38;5;24"
+        "7m[2]:\x1b[0m   \x1b[1m   Model:\x1b[0m          "
+        "  \x1b[1m Decision\x1b[0m            \x1b[1mRegre"
+        "ssi…\x1b[0m            \x1b[1m   Random\x1b[0m \n "
+        "                           \x1b[1m     Tree"
+        "\x1b[0m                                    "
+        "       \n       \x1b[1mPredicte…\x1b[0m   \x1b[1mT"
+        "umour\x1b[0m   \x1b[1mNon-Tumo…\x1b[0m   \x1b[1mTumo"
+        "ur\x1b[0m   \x1b[1mNon-Tumo…\x1b[0m   \x1b[1mTumour\x1b"
+        "[0m   \x1b[1mNon-Tumo…\x1b[0m \n       \x1b[1m   A"
+        "ctual\x1b[0m   \x1b[1m      \x1b[0m   \x1b[1m       "
+        "  \x1b[0m   \x1b[1m      \x1b[0m   \x1b[1m         \x1b"
+        "[0m   \x1b[1m      \x1b[0m   \x1b[1m         \x1b[0m"
+        " \n       \x1b[1m   Label:\x1b[0m              "
+        "                                        "
+        "          \n      ───────────────────────"
+        "────────────────────────────────────────"
+        "───────────\n                            "
+        "                                        "
+        "            \n"
+    )
+    output = rich_notebook_output(code_cell)
+    assert remove_link_ids(output) == remove_link_ids(expected_output)
+
+
+def test_render_mistagged_dataframe(
+    rich_notebook_output: RichOutput,
+    mock_tempfile_file: Generator[Mock, None, None],
+    remove_link_ids: Callable[[str], str],
+    tempfile_path: Path,
+) -> None:
+    """It doesn't detect a DataFrame when it is not a table."""
+    code_cell = {
+        "cell_type": "code",
+        "execution_count": 2,
+        "id": "mighty-oasis",
+        "metadata": {},
+        "outputs": [
+            {
+                "data": {
+                    "text/html": (
+                        "<div>\n<style scoped>\n    .dataframe tbod"
+                        "y tr th:only-of-type {\n        vertical-"
+                        "align: middle;\n    }\n\n    .dataframe tbo"
+                        "dy tr th {\n        vertical-align: top;\n"
+                        "    }\n\n    .dataframe thead tr th {\n    "
+                        "    text-align: left;\n    }\n\n    .datafr"
+                        "ame thead tr:last-of-type th {\n        t"
+                        "ext-align: right;\n    }\n</style>\n<not-a-table "
+                        'border="1" class="dataframe">\n  <thead>\n'
+                        "    <tr>\n      <th>Model:</th>\n      <th"
+                        ' colspan="2" halign="left">Decision Tree'
+                        '</th>\n      <th colspan="2" halign="left'
+                        '">Regression</th>\n      <th colspan="2" '
+                        'halign="left">Random</th>\n    </tr>\n    '
+                        "<tr>\n      <th>Predicted:</th>\n      <th"
+                        ">Tumour</th>\n      <th>Non-Tumour</th>\n "
+                        "     <th>Tumour</th>\n      <th>Non-Tumou"
+                        "r</th>\n      <th>Tumour</th>\n      <th>N"
+                        "on-Tumour</th>\n    </tr>\n    <tr>\n      "
+                        "<th>Actual Label:</th>\n      <th></th>\n "
+                        "     <th></th>\n      <th></th>\n      <th"
+                        "></th>\n      <th></th>\n      <th></th>\n "
+                        "   </tr>\n  </thead>\n  <tbody>\n    <tr>\n "
+                        "     <th>Tumour (Positive)</th>\n      <t"
+                        "d>38.0</td>\n      <td>2.0</td>\n      <td"
+                        ">18.0</td>\n      <td>22.0</td>\n      <td"
+                        ">21</td>\n      <td>NaN</td>\n    </tr>\n  "
+                        "  <tr>\n      <th>Non-Tumour (Negative)</"
+                        "th>\n      <td>19.0</td>\n      <td>439.0<"
+                        "/td>\n      <td>6.0</td>\n      <td>452.0<"
+                        "/td>\n      <td>226</td>\n      <td>232.0<"
+                        "/td>\n    </tr>\n  </tbody>\n</not-a-table>\n</div"
+                        ">"
+                    ),
+                    "text/plain": (
+                        "lorep              hey                by"
+                        "e\nipsum               hi very_long_word "
+                        " hi\nfirst second third                  "
+                        "     \nbar   one    1       1            "
+                        "  2   4\n             10      3          "
+                        "    4  -1\n      three  3       3        "
+                        "      4  -1\nfoo   one    1       3      "
+                        "        4  -1"
+                    ),
+                },
+                "execution_count": 2,
+                "metadata": {},
+                "output_type": "execute_result",
+            }
+        ],
+        "source": "",
+    }
+    expected_output = (
+        "     ╭──────────────────────────────────"
+        "───────────────────────────────────────╮"
+        "\n\x1b[38;5;247m[2]:\x1b[0m │                  "
+        "                                        "
+        "               │\n     ╰─────────────────"
+        "────────────────────────────────────────"
+        "────────────────╯\n                      "
+        "                                        "
+        "                  \n\x1b[38;5;247m[2]:\x1b[0m  "
+        f"\x1b]8;id=968899;file://{tempfile_path}0.html\x1b\\\x1b"
+        "[94m🌐 Click to view"
+        " HTML\x1b[0m\x1b]8;;\x1b\\                        "
+        "                             \n          "
+        "                                        "
+        "                              \n\x1b[38;5;24"
+        "7m[2]:\x1b[0m  Model: | Decision Tree | Reg"
+        "ression | Random                        "
+        "      \n      Predicted: | Tumour | Non-T"
+        "umour | Tumour | Non-Tumour | Tumour |  "
+        "       \n      Non-Tumour                "
+        "                                        "
+        "        \n      Actual Label: |  |  |  | "
+        " |  |                                   "
+        "         \n      Tumour (Positive) | 38.0"
+        " | 2.0 | 18.0 | 22.0 | 21 | NaN         "
+        "          \n      Non-Tumour (Negative) |"
+        " 19.0 | 439.0 | 6.0 | 452.0 | 226 | 232."
+        "0          \n"
+    )
+    output = rich_notebook_output(code_cell)
+    assert remove_link_ids(output) == remove_link_ids(expected_output)
+
+
+def test_render_multiindex_dataframe(
+    rich_notebook_output: RichOutput,
+    mock_tempfile_file: Generator[Mock, None, None],
+    remove_link_ids: Callable[[str], str],
+    tempfile_path: Path,
+) -> None:
+    """It renders a multiindex DataFrame."""
     code_cell = {
         "cell_type": "code",
         "execution_count": 2,
@@ -903,30 +1203,162 @@ def test_render_plain_dataframe(
                         "ame thead tr:last-of-type th {\n        t"
                         "ext-align: right;\n    }\n</style>\n<table "
                         'border="1" class="dataframe">\n  <thead>\n'
-                        "    <tr>\n      <th></th>\n      <th></th>"
-                        "\n      <th>lorep</th>\n      <th colspan="
-                        '"2" halign="left">hey</th>\n      <th>bye'
-                        "</th>\n    </tr>\n    <tr>\n      <th></th>"
-                        "\n      <th></th>\n      <th>ipsum</th>\n  "
-                        "    <th>hi</th>\n      <th>very_long_word"
-                        "</th>\n      <th>hi</th>\n    </tr>\n    <t"
-                        "r>\n      <th>first</th>\n      <th>second"
-                        "</th>\n      <th>third</th>\n      <th></t"
-                        "h>\n      <th></th>\n      <th></th>\n    <"
-                        "/tr>\n  </thead>\n  <tbody>\n    <tr>\n     "
-                        ' <th rowspan="3" valign="top">bar</th>\n '
-                        '     <th rowspan="2" valign="top">one</t'
-                        "h>\n      <th>1</th>\n      <td>1</td>\n   "
-                        "   <td>2</td>\n      <td>4</td>\n    </tr>"
-                        "\n    <tr>\n      <th>10</th>\n      <td>3<"
-                        "/td>\n      <td>4</td>\n      <td>-1</td>\n"
-                        "    </tr>\n    <tr>\n      <th>three</th>\n"
-                        "      <th>3</th>\n      <td>3</td>\n      "
-                        "<td>4</td>\n      <td>-1</td>\n    </tr>\n "
-                        "   <tr>\n      <th>foo</th>\n      <th>one"
-                        "</th>\n      <th>1</th>\n      <td>3</td>\n"
-                        "      <td>4</td>\n      <td>-1</td>\n    <"
-                        "/tr>\n  </tbody>\n</table>\n</div>"
+                        "    <tr>\n      <th>Model:</th>\n      <th"
+                        ' colspan="2" halign="left">Decision Tree'
+                        '</th>\n      <th colspan="2" halign="left'
+                        '">Regression</th>\n      <th colspan="2" '
+                        'halign="left">Random</th>\n    </tr>\n    '
+                        "<tr>\n      <th>Predicted:</th>\n      <th"
+                        ">Tumour</th>\n      <th>Non-Tumour</th>\n "
+                        "     <th>Tumour</th>\n      <th>Non-Tumou"
+                        "r</th>\n      <th>Tumour</th>\n      <th>N"
+                        "on-Tumour</th>\n    </tr>\n    <tr>\n      "
+                        "<th>Actual Label:</th>\n      <th></th>\n "
+                        "     <th></th>\n      <th></th>\n      <th"
+                        "></th>\n      <th></th>\n      <th></th>\n "
+                        "   </tr>\n  </thead>\n  <tbody>\n    <tr>\n "
+                        "     <th>Tumour (Positive)</th>\n      <t"
+                        "d>38.0</td>\n      <td>2.0</td>\n      <td"
+                        ">18.0</td>\n      <td>22.0</td>\n      <td"
+                        ">21</td>\n      <td>NaN</td>\n    </tr>\n  "
+                        "  <tr>\n      <th>Non-Tumour (Negative)</"
+                        "th>\n      <td>19.0</td>\n      <td>439.0<"
+                        "/td>\n      <td>6.0</td>\n      <td>452.0<"
+                        "/td>\n      <td>226</td>\n      <td>232.0<"
+                        "/td>\n    </tr>\n  </tbody>\n</table>\n</div"
+                        ">"
+                    ),
+                    "text/plain": (
+                        "lorep              hey                by"
+                        "e\nipsum               hi very_long_word "
+                        " hi\nfirst second third                  "
+                        "     \nbar   one    1       1            "
+                        "  2   4\n             10      3          "
+                        "    4  -1\n      three  3       3        "
+                        "      4  -1\nfoo   one    1       3      "
+                        "        4  -1"
+                    ),
+                },
+                "execution_count": 2,
+                "metadata": {},
+                "output_type": "execute_result",
+            }
+        ],
+        "source": "",
+    }
+    expected_output = (
+        "     ╭──────────────────────────────────"
+        "───────────────────────────────────────╮"
+        "\n\x1b[38;5;247m[2]:\x1b[0m │                  "
+        "                                        "
+        "               │\n     ╰─────────────────"
+        "────────────────────────────────────────"
+        "────────────────╯\n                      "
+        "                                        "
+        "                  \n\x1b[38;5;247m[2]:\x1b[0m  "
+        f"\x1b]8;id=888128;file://{tempfile_path}0.html\x1b\\\x1b"
+        "[94m🌐 Click to view"
+        " HTML\x1b[0m\x1b]8;;\x1b\\                        "
+        "                             \n          "
+        "                                        "
+        "                              \n\x1b[38;5;24"
+        "7m[2]:\x1b[0m   \x1b[1m   Model:\x1b[0m          "
+        "  \x1b[1m Decision\x1b[0m            \x1b[1mRegre"
+        "ssi…\x1b[0m            \x1b[1m   Random\x1b[0m \n "
+        "                           \x1b[1m     Tree"
+        "\x1b[0m                                    "
+        "       \n       \x1b[1mPredicte…\x1b[0m   \x1b[1mT"
+        "umour\x1b[0m   \x1b[1mNon-Tumo…\x1b[0m   \x1b[1mTumo"
+        "ur\x1b[0m   \x1b[1mNon-Tumo…\x1b[0m   \x1b[1mTumour\x1b"
+        "[0m   \x1b[1mNon-Tumo…\x1b[0m \n       \x1b[1m   A"
+        "ctual\x1b[0m   \x1b[1m      \x1b[0m   \x1b[1m       "
+        "  \x1b[0m   \x1b[1m      \x1b[0m   \x1b[1m         \x1b"
+        "[0m   \x1b[1m      \x1b[0m   \x1b[1m         \x1b[0m"
+        " \n       \x1b[1m   Label:\x1b[0m              "
+        "                                        "
+        "          \n      ───────────────────────"
+        "────────────────────────────────────────"
+        "───────────\n       \x1b[1m   Tumour\x1b[0m    "
+        " 38.0         2.0     18.0        22.0  "
+        "     21         NaN \n       \x1b[1m(Positiv"
+        "…\x1b[0m                                   "
+        "                             \n       \x1b[1"
+        "mNon-Tumo…\x1b[0m     19.0       439.0     "
+        " 6.0       452.0      226       232.0 \n "
+        "      \x1b[1m(Negativ…\x1b[0m                 "
+        "                                        "
+        "       \n"
+    )
+    output = rich_notebook_output(code_cell)
+    assert remove_link_ids(output) == remove_link_ids(expected_output)
+
+
+def test_render_styled_dataframe(
+    rich_notebook_output: RichOutput,
+    mock_tempfile_file: Generator[Mock, None, None],
+    remove_link_ids: Callable[[str], str],
+    tempfile_path: Path,
+) -> None:
+    """It renders a styled DataFrame."""
+    code_cell = {
+        "cell_type": "code",
+        "execution_count": 2,
+        "id": "mighty-oasis",
+        "metadata": {},
+        "outputs": [
+            {
+                "data": {
+                    "text/html": (
+                        '<style type="text/css">\n#T_7cafb_ td:hov'
+                        "er {\n  background-color: #ffffb3;\n}\n#T_7"
+                        "cafb_ .index_name {\n  font-style: italic"
+                        ";\n  color: darkgrey;\n  font-weight: norm"
+                        "al;\n}\n#T_7cafb_ th:not(.index_name) {\n  "
+                        "background-color: #000066;\n  color: whit"
+                        "e;\n}\n#T_7cafb_ .true {\n  background-colo"
+                        "r: #e6ffe6;\n}\n#T_7cafb_ .false {\n  backg"
+                        "round-color: #ffe6e6;\n}\n</style>\n<table "
+                        'id="T_7cafb_">\n  <thead>\n    <tr>\n      '
+                        '<th class="index_name level0" >Model:</t'
+                        'h>\n      <th class="col_heading level0 c'
+                        'ol0" colspan="2">Decision Tree</th>\n    '
+                        '  <th class="col_heading level0 col2" co'
+                        'lspan="2">Regression</th>\n    </tr>\n    '
+                        '<tr>\n      <th class="index_name level1"'
+                        ' >Predicted:</th>\n      <th class="col_h'
+                        'eading level1 col0" >Tumour</th>\n      <'
+                        'th class="col_heading level1 col1" >Non-'
+                        'Tumour</th>\n      <th class="col_heading'
+                        ' level1 col2" >Tumour</th>\n      <th cla'
+                        'ss="col_heading level1 col3" >Non-Tumour'
+                        "</th>\n    </tr>\n    <tr>\n      <th class"
+                        '="index_name level0" >Actual Label:</th>'
+                        '\n      <th class="blank col0" >&nbsp;</t'
+                        'h>\n      <th class="blank col1" >&nbsp;<'
+                        '/th>\n      <th class="blank col2" >&nbsp'
+                        ';</th>\n      <th class="blank col3" >&nb'
+                        "sp;</th>\n    </tr>\n  </thead>\n  <tbody>\n"
+                        '    <tr>\n      <th id="T_7cafb_level0_ro'
+                        'w0" class="row_heading level0 row0" >Tum'
+                        'our (Positive)</th>\n      <td id="T_7caf'
+                        'b_row0_col0" class="data row0 col0 true '
+                        '" >38</td>\n      <td id="T_7cafb_row0_co'
+                        'l1" class="data row0 col1 false " >2</td'
+                        '>\n      <td id="T_7cafb_row0_col2" class'
+                        '="data row0 col2 true " >18</td>\n      <'
+                        'td id="T_7cafb_row0_col3" class="data ro'
+                        'w0 col3 false " >22</td>\n    </tr>\n    <'
+                        'tr>\n      <th id="T_7cafb_level0_row1" c'
+                        'lass="row_heading level0 row1" >Non-Tumo'
+                        'ur (Negative)</th>\n      <td id="T_7cafb'
+                        '_row1_col0" class="data row1 col0 false '
+                        '" >19</td>\n      <td id="T_7cafb_row1_co'
+                        'l1" class="data row1 col1 true " >439</t'
+                        'd>\n      <td id="T_7cafb_row1_col2" clas'
+                        's="data row1 col2 false " >6</td>\n      '
+                        '<td id="T_7cafb_row1_col3" class="data r'
+                        'ow1 col3 true " >452</td>\n    </tr>\n  </'
+                        "tbody>\n</table>\n"
                     ),
                     "text/plain": (
                         "lorep              hey                by"
@@ -947,33 +1379,963 @@ def test_render_plain_dataframe(
         "source": "",
     }
     expected_output = (
+        "     ╭──────────────────────────────────"
+        "───────────────────────────────────────╮"
+        "\n\x1b[38;5;247m[2]:\x1b[0m │                  "
         "                                        "
+        "               │\n     ╰─────────────────"
+        "────────────────────────────────────────"
+        "────────────────╯\n                      "
         "                                        "
-        "\n                                       "
+        "                  \n\x1b[38;5;247m[2]:\x1b[0m  "
+        f"\x1b]8;id=698065;file://{tempfile_path}0.html\x1b\\\x1b"
+        "[94m🌐 Click to view"
+        " HTML\x1b[0m\x1b]8;;\x1b\\                        "
+        "                             \n          "
         "                                        "
-        " \n\x1b]8;id=1627258290.675266-113809;file:/"
-        f"/{tempfile_path}0.html\x1b\\"
-        "\x1b[94m🌐 Click to view HTML\x1b[0m\x1b]8;;\x1b\\    "
+        "                              \n\x1b[38;5;24"
+        "7m[2]:\x1b[0m   \x1b[1m               Model:\x1b["
+        "0m            \x1b[1mDecision Tree\x1b[0m     "
+        "       \x1b[1mRegression\x1b[0m     \n       \x1b["
+        "1m           Predicted:\x1b[0m   \x1b[1mTumour"
+        "\x1b[0m   \x1b[1m   Non-Tumour\x1b[0m   \x1b[1mTumou"
+        "r\x1b[0m   \x1b[1mNon-Tumour\x1b[0m     \n       \x1b"
+        "[1m        Actual Label:\x1b[0m   \x1b[1m     "
+        " \x1b[0m   \x1b[1m             \x1b[0m   \x1b[1m    "
+        "  \x1b[0m   \x1b[1m          \x1b[0m     \n      ─"
+        "────────────────────────────────────────"
+        "─────────────────────────────    \n      "
+        " \x1b[1m    Tumour (Positive)\x1b[0m       38 "
+        "              2       18           22   "
+        "  \n       \x1b[1mNon-Tumour (Negative)\x1b[0m "
+        "      19             439        6       "
+        "   452     \n"
+    )
+    output = rich_notebook_output(code_cell)
+    assert remove_link_ids(output) == remove_link_ids(expected_output)
+
+
+def test_render_missing_column_name_dataframe(
+    rich_notebook_output: RichOutput,
+    mock_tempfile_file: Generator[Mock, None, None],
+    remove_link_ids: Callable[[str], str],
+    tempfile_path: Path,
+) -> None:
+    """It renders a DataFrame with a missing column index name."""
+    code_cell = {
+        "cell_type": "code",
+        "execution_count": 2,
+        "id": "mighty-oasis",
+        "metadata": {},
+        "outputs": [
+            {
+                "data": {
+                    "text/html": (
+                        "<div>\n<style scoped>\n    .dataframe tbod"
+                        "y tr th:only-of-type {\n        vertical-"
+                        "align: middle;\n    }\n\n    .dataframe tbo"
+                        "dy tr th {\n        vertical-align: top;\n"
+                        "    }\n\n    .dataframe thead tr th {\n    "
+                        "    text-align: left;\n    }\n\n    .datafr"
+                        "ame thead tr:last-of-type th {\n        t"
+                        "ext-align: right;\n    }\n</style>\n<table "
+                        'border="1" class="dataframe">\n  <thead>\n'
+                        "    <tr>\n      <th></th>\n      <th>lorep"
+                        "</th>\n      <th>hey</th>\n      <th>sup</"
+                        "th>\n      <th>bye</th>\n    </tr>\n    <tr"
+                        ">\n      <th>hey</th>\n      <th></th>\n   "
+                        "   <th></th>\n      <th></th>\n      <th><"
+                        "/th>\n    </tr>\n  </thead>\n  <tbody>\n    "
+                        "<tr>\n      <th>3</th>\n      <th>1</th>\n "
+                        "     <td>1</td>\n      <td>4</td>\n      <"
+                        "td>6</td>\n    </tr>\n    <tr>\n      <th>4"
+                        "</th>\n      <th>1</th>\n      <td>2</td>\n"
+                        "      <td>5</td>\n      <td>7</td>\n    </"
+                        "tr>\n  </tbody>\n</table>\n</div>"
+                    ),
+                    "text/plain": (
+                        "lorep              hey                by"
+                        "e\nipsum               hi very_long_word "
+                        " hi\nfirst second third                  "
+                        "     \nbar   one    1       1            "
+                        "  2   4\n             10      3          "
+                        "    4  -1\n      three  3       3        "
+                        "      4  -1\nfoo   one    1       3      "
+                        "        4  -1"
+                    ),
+                },
+                "execution_count": 2,
+                "metadata": {},
+                "output_type": "execute_result",
+            }
+        ],
+        "source": "",
+    }
+    expected_output = (
+        "     ╭──────────────────────────────────"
+        "───────────────────────────────────────╮"
+        "\n\x1b[38;5;247m[2]:\x1b[0m │                  "
+        "                                        "
+        "               │\n     ╰─────────────────"
+        "────────────────────────────────────────"
+        "────────────────╯\n                      "
+        "                                        "
+        "                  \n\x1b[38;5;247m[2]:\x1b[0m  "
+        f"\x1b]8;id=337911;file://{tempfile_path}0.html\x1b\\\x1b[94m🌐 "
+        "Click to view"
+        " HTML\x1b[0m\x1b]8;;\x1b\\                        "
+        "                             \n          "
+        "                                        "
+        "                              \n\x1b[38;5;24"
+        "7m[2]:\x1b[0m   \x1b[1m   \x1b[0m   \x1b[1mlorep\x1b[0m"
+        "   \x1b[1mhey\x1b[0m   \x1b[1msup\x1b[0m   \x1b[1mbye\x1b["
+        "0m                                      "
+        "      \n       \x1b[1mhey\x1b[0m   \x1b[1m     \x1b[0"
+        "m   \x1b[1m   \x1b[0m   \x1b[1m   \x1b[0m   \x1b[1m   \x1b"
+        "[0m                                     "
+        "       \n      ──────────────────────────"
+        "─────                                   "
+        "        \n       \x1b[1m  3\x1b[0m   \x1b[1m    1\x1b"
+        "[0m     1     4     6                   "
+        "                         \n       \x1b[1m  4"
+        "\x1b[0m   \x1b[1m    1\x1b[0m     2     5     7  "
+        "                                        "
+        "  \n"
+    )
+    output = rich_notebook_output(code_cell)
+    assert remove_link_ids(output) == remove_link_ids(expected_output)
+
+
+def test_render_missing_index_name_dataframe(
+    rich_notebook_output: RichOutput,
+    mock_tempfile_file: Generator[Mock, None, None],
+    remove_link_ids: Callable[[str], str],
+    tempfile_path: Path,
+) -> None:
+    """It renders a DataFrame with a missing index index name."""
+    code_cell = {
+        "cell_type": "code",
+        "execution_count": 2,
+        "id": "mighty-oasis",
+        "metadata": {},
+        "outputs": [
+            {
+                "data": {
+                    "text/html": (
+                        "<div>\n<style scoped>\n    .dataframe tbod"
+                        "y tr th:only-of-type {\n        vertical-"
+                        "align: middle;\n    }\n\n    .dataframe tbo"
+                        "dy tr th {\n        vertical-align: top;\n"
+                        "    }\n\n    .dataframe thead th {\n       "
+                        " text-align: right;\n    }\n</style>\n<tabl"
+                        'e border="1" class="dataframe">\n  <thead'
+                        '>\n    <tr style="text-align: right;">\n  '
+                        "    <th></th>\n      <th></th>\n      <th>"
+                        "a</th>\n      <th>b</th>\n      <th>c</th>"
+                        "\n    </tr>\n    <tr>\n      <th></th>\n    "
+                        "  <th>hey</th>\n      <th></th>\n      <th"
+                        "></th>\n      <th></th>\n    </tr>\n  </the"
+                        "ad>\n  <tbody>\n    <tr>\n      <th>3</th>\n"
+                        "      <th>1</th>\n      <td>1</td>\n      "
+                        "<td>4</td>\n      <td>6</td>\n    </tr>\n  "
+                        "  <tr>\n      <th>4</th>\n      <th>1</th>"
+                        "\n      <td>2</td>\n      <td>5</td>\n     "
+                        " <td>7</td>\n    </tr>\n  </tbody>\n</table"
+                        ">\n</div>"
+                    ),
+                    "text/plain": (
+                        "lorep              hey                by"
+                        "e\nipsum               hi very_long_word "
+                        " hi\nfirst second third                  "
+                        "     \nbar   one    1       1            "
+                        "  2   4\n             10      3          "
+                        "    4  -1\n      three  3       3        "
+                        "      4  -1\nfoo   one    1       3      "
+                        "        4  -1"
+                    ),
+                },
+                "execution_count": 2,
+                "metadata": {},
+                "output_type": "execute_result",
+            }
+        ],
+        "source": "",
+    }
+    expected_output = (
+        "     ╭──────────────────────────────────"
+        "───────────────────────────────────────╮"
+        "\n\x1b[38;5;247m[2]:\x1b[0m │                  "
+        "                                        "
+        "               │\n     ╰─────────────────"
+        "────────────────────────────────────────"
+        "────────────────╯\n                      "
+        "                                        "
+        "                  \n\x1b[38;5;247m[2]:\x1b[0m  "
+        f"\x1b]8;id=308498;file://{tempfile_path}0.html\x1b\\\x1b"
+        "[94m🌐 Click to view"
+        " HTML\x1b[0m\x1b]8;;\x1b\\                        "
+        "                             \n          "
+        "                                        "
+        "                              \n\x1b[38;5;24"
+        "7m[2]:\x1b[0m   \x1b[1m \x1b[0m   \x1b[1m   \x1b[0m   \x1b"
+        "[1ma\x1b[0m   \x1b[1mb\x1b[0m   \x1b[1mc\x1b[0m        "
+        "                                        "
+        "      \n       \x1b[1m \x1b[0m   \x1b[1mhey\x1b[0m   "
+        "\x1b[1m \x1b[0m   \x1b[1m \x1b[0m   \x1b[1m \x1b[0m       "
+        "                                        "
+        "       \n      ─────────────────────     "
+        "                                        "
+        "        \n       \x1b[1m3\x1b[0m   \x1b[1m  1\x1b[0m "
+        "  1   4   6                             "
+        "                         \n       \x1b[1m4\x1b["
+        "0m   \x1b[1m  1\x1b[0m   2   5   7            "
+        "                                        "
+        "  \n"
+    )
+    output = rich_notebook_output(code_cell)
+    assert remove_link_ids(output) == remove_link_ids(expected_output)
+
+
+def test_render_missing_last_index_name_dataframe(
+    rich_notebook_output: RichOutput,
+    mock_tempfile_file: Generator[Mock, None, None],
+    remove_link_ids: Callable[[str], str],
+    tempfile_path: Path,
+) -> None:
+    """It renders a DataFrame with missing lasst index index name."""
+    code_cell = {
+        "cell_type": "code",
+        "execution_count": 2,
+        "id": "mighty-oasis",
+        "metadata": {},
+        "outputs": [
+            {
+                "data": {
+                    "text/html": (
+                        "<div>\n<style scoped>\n    .dataframe tbod"
+                        "y tr th:only-of-type {\n        vertical-"
+                        "align: middle;\n    }\n\n    .dataframe tbo"
+                        "dy tr th {\n        vertical-align: top;\n"
+                        "    }\n\n    .dataframe thead th {\n       "
+                        " text-align: right;\n    }\n</style>\n<tabl"
+                        'e border="1" class="dataframe">\n  <thead'
+                        '>\n    <tr style="text-align: right;">\n  '
+                        "    <th></th>\n      <th></th>\n      <th>"
+                        "a</th>\n      <th>b</th>\n      <th>c</th>"
+                        "\n    </tr>\n    <tr>\n      <th>hey</th>\n "
+                        "     <th></th>\n      <th></th>\n      <th"
+                        "></th>\n      <th></th>\n    </tr>\n  </the"
+                        "ad>\n  <tbody>\n    <tr>\n      <th>3</th>\n"
+                        "      <th>1</th>\n      <td>1</td>\n      "
+                        "<td>4</td>\n      <td>6</td>\n    </tr>\n  "
+                        "  <tr>\n      <th>4</th>\n      <th>1</th>"
+                        "\n      <td>2</td>\n      <td>5</td>\n     "
+                        " <td>7</td>\n    </tr>\n  </tbody>\n</table"
+                        ">\n</div>"
+                    ),
+                    "text/plain": (
+                        "lorep              hey                by"
+                        "e\nipsum               hi very_long_word "
+                        " hi\nfirst second third                  "
+                        "     \nbar   one    1       1            "
+                        "  2   4\n             10      3          "
+                        "    4  -1\n      three  3       3        "
+                        "      4  -1\nfoo   one    1       3      "
+                        "        4  -1"
+                    ),
+                },
+                "execution_count": 2,
+                "metadata": {},
+                "output_type": "execute_result",
+            }
+        ],
+        "source": "",
+    }
+    expected_output = (
+        "     ╭──────────────────────────────────"
+        "───────────────────────────────────────╮"
+        "\n\x1b[38;5;247m[2]:\x1b[0m │                  "
+        "                                        "
+        "               │\n     ╰─────────────────"
+        "────────────────────────────────────────"
+        "────────────────╯\n                      "
+        "                                        "
+        "                  \n\x1b[38;5;247m[2]:\x1b[0m  "
+        f"\x1b]8;id=59302;file://{tempfile_path}0.html\x1b\\\x1b"
+        "[94m🌐 Click to view "
+        "HTML\x1b[0m\x1b]8;;\x1b\\                         "
+        "                            \n           "
+        "                                        "
+        "                             \n\x1b[38;5;247"
+        "m[2]:\x1b[0m   \x1b[1m   \x1b[0m   \x1b[1m \x1b[0m   \x1b["
+        "1ma\x1b[0m   \x1b[1mb\x1b[0m   \x1b[1mc\x1b[0m         "
+        "                                        "
+        "     \n       \x1b[1mhey\x1b[0m   \x1b[1m \x1b[0m   \x1b"
+        "[1m \x1b[0m   \x1b[1m \x1b[0m   \x1b[1m \x1b[0m        "
+        "                                        "
+        "      \n      ─────────────────────      "
+        "                                        "
+        "       \n       \x1b[1m  3\x1b[0m   \x1b[1m1\x1b[0m  "
+        " 1   4   6                              "
+        "                        \n       \x1b[1m  4\x1b"
+        "[0m   \x1b[1m1\x1b[0m   2   5   7             "
+        "                                        "
+        " \n"
+    )
+    output = rich_notebook_output(code_cell)
+    assert remove_link_ids(output) == remove_link_ids(expected_output)
+
+
+def test_render_plain_dataframe(
+    rich_notebook_output: RichOutput,
+    mock_tempfile_file: Generator[Mock, None, None],
+    remove_link_ids: Callable[[str], str],
+    tempfile_path: Path,
+) -> None:
+    """It renders a DataFrame in a plain style."""
+    code_cell = {
+        "cell_type": "code",
+        "execution_count": 2,
+        "id": "mighty-oasis",
+        "metadata": {},
+        "outputs": [
+            {
+                "data": {
+                    "text/html": (
+                        "<div>\n<style scoped>\n    .dataframe tbod"
+                        "y tr th:only-of-type {\n        vertical-"
+                        "align: middle;\n    }\n\n    .dataframe tbo"
+                        "dy tr th {\n        vertical-align: top;\n"
+                        "    }\n\n    .dataframe thead tr th {\n    "
+                        "    text-align: left;\n    }\n\n    .datafr"
+                        "ame thead tr:last-of-type th {\n        t"
+                        "ext-align: right;\n    }\n</style>\n<table "
+                        'border="1" class="dataframe">\n  <thead>\n'
+                        "    <tr>\n      <th></th>\n      <th></th>"
+                        "\n      <th>lorep</th>\n      <th colspan="
+                        '"2" halign="left">hey</th>\n      <th>bye'
+                        "</th>\n    </tr>\n    <tr>\n      <th></th>"
+                        "\n      <th></th>\n      <th>ipsum</th>\n  "
+                        "    <th>hi</th>\n      <th>very_long_word"
+                        "</th>\n      <th>hi</th>\n    </tr>\n    <t"
+                        "r>\n      <th>first</th>\n      <th>second"
+                        "</th>\n      <th>third</th>\n      <th></t"
+                        "h>\n      <th></th>\n      <th></th>\n    <"
+                        "/tr>\n  </thead>\n  <tbody>\n    <tr>\n     "
+                        ' <th rowspan="3" valign="top">bar</th>\n '
+                        '     <th rowspan="2" valign="top">one</t'
+                        "h>\n      <th>1</th>\n      <td>1</td>\n   "
+                        "   <td>2</td>\n      <td>4</td>\n    </tr>"
+                        "\n    <tr>\n      <th>10</th>\n      <td>3<"
+                        "/td>\n      <td>4</td>\n      <td>-1</td>\n"
+                        "    </tr>\n    <tr>\n      <th>three</th>\n"
+                        "      <th>3</th>\n      <td>3</td>\n      "
+                        "<td>4</td>\n      <td>-1</td>\n    </tr>\n "
+                        "   <tr>\n      <th>foo</th>\n      <th>one"
+                        "</th>\n      <th>1</th>\n      <td>3</td>\n"
+                        "      <td>4</td>\n      <td>-1</td>\n    <"
+                        "/tr>\n  </tbody>\n</table>\n</div>"
+                    ),
+                    "text/plain": (
+                        "lorep              hey                by"
+                        "e\nipsum               hi very_long_word "
+                        " hi\nfirst second third                  "
+                        "     \nbar   one    1       1            "
+                        "  2   4\n             10      3          "
+                        "    4  -1\n      three  3       3        "
+                        "      4  -1\nfoo   one    1       3      "
+                        "        4  -1"
+                    ),
+                },
+                "execution_count": 2,
+                "metadata": {},
+                "output_type": "execute_result",
+            }
+        ],
+        "source": "",
+    }
+    expected_output = (
+        "                                        "
+        "                                        "
+        "\n                                       "
+        "                                        "
+        " \n\x1b]8;id=1627258290.675266-113809;file:/"
+        f"/{tempfile_path}0.html\x1b\\"
+        "\x1b[94m🌐 Click to view HTML\x1b[0m\x1b]8;;\x1b\\    "
+        "                                        "
+        "               \n                        "
+        "                                        "
+        "                \nlorep              hey "
+        "               bye                      "
+        "                 \nipsum               hi"
+        " very_long_word  hi                     "
+        "                  \nfirst second third   "
+        "                                        "
+        "                   \nbar   one    1      "
+        " 1              2   4                   "
+        "                    \n             10    "
+        "  3              4  -1                  "
+        "                     \n      three  3    "
+        "   3              4  -1                 "
+        "                      \nfoo   one    1   "
+        "    3              4  -1                "
+        "                       \n"
+    )
+    output = rich_notebook_output(code_cell, plain=True)
+    assert remove_link_ids(output) == remove_link_ids(expected_output)
+
+
+def test_render_uneven_columns_dataframe(
+    rich_notebook_output: RichOutput,
+    mock_tempfile_file: Generator[Mock, None, None],
+    remove_link_ids: Callable[[str], str],
+    tempfile_path: Path,
+) -> None:
+    """It renders a DataFrame with missing columns."""
+    code_cell = {
+        "cell_type": "code",
+        "execution_count": 2,
+        "id": "mighty-oasis",
+        "metadata": {},
+        "outputs": [
+            {
+                "data": {
+                    "text/html": (
+                        """
+                        <style type="text/css">
+  \n</style
+>\n
+<table id="T_aba0a_">
+  \n
+  <thead>
+    \n
+    <tr>
+      \n
+      <th class="index_name level0">Model:</th>
+      \n
+      <th class="col_heading level0 col0" colspan="2">Decision Tree</th>
+      \n
+      <th class="col_heading level0 col2" colspan="2">Regression</th>
+      \n
+    </tr>
+    \n
+    <tr>
+      \n
+      <th class="col_heading level1 col0">Tumour</th>
+      \n
+      <th class="col_heading level1 col1">Non-Tumour</th>
+      \n
+      <th class="col_heading level1 col2">Tumour</th>
+      \n
+      <th class="col_heading level1 col3">Non-Tumour</th>
+      \n
+    </tr>
+    \n
+    <tr>
+      \n
+      <th class="index_name level0">Actual Label:</th>
+      \n
+      <th class="blank col0">&nbsp;</th>
+      \n
+      <th class="blank col1">&nbsp;</th>
+      \n
+      <th class="blank col2">&nbsp;</th>
+      \n
+      <th class="blank col3">&nbsp;</th>
+      \n
+    </tr>
+    \n
+  </thead>
+  \n
+  <tbody>
+    \n
+    <tr>
+      \n
+      <th id="T_aba0a_level0_row0" class="row_heading level0 row0">
+        Tumour (Positive)
+      </th>
+      \n
+      <td id="T_aba0a_row0_col0" class="data row0 col0">38</td>
+      \n
+      <td id="T_aba0a_row0_col1" class="data row0 col1">2</td>
+      \n
+      <td id="T_aba0a_row0_col2" class="data row0 col2">18</td>
+      \n
+      <td id="T_aba0a_row0_col3" class="data row0 col3">22</td>
+      \n
+    </tr>
+    \n
+    <tr>
+      \n
+      <th id="T_aba0a_level0_row1" class="row_heading level0 row1">
+        Non-Tumour (Negative)
+      </th>
+      \n
+      <td id="T_aba0a_row1_col0" class="data row1 col0">19</td>
+      \n
+      <td id="T_aba0a_row1_col1" class="data row1 col1">439</td>
+      \n
+      <td id="T_aba0a_row1_col2" class="data row1 col2">6</td>
+      \n
+      <td id="T_aba0a_row1_col3" class="data row1 col3">452</td>
+      \n
+    </tr>
+    \n
+  </tbody>
+  \n
+</table>
+\n
+
+                        """
+                    ),
+                    "text/plain": (
+                        "lorep              hey                by"
+                        "e\nipsum               hi very_long_word "
+                        " hi\nfirst second third                  "
+                        "     \nbar   one    1       1            "
+                        "  2   4\n             10      3          "
+                        "    4  -1\n      three  3       3        "
+                        "      4  -1\nfoo   one    1       3      "
+                        "        4  -1"
+                    ),
+                },
+                "execution_count": 2,
+                "metadata": {},
+                "output_type": "execute_result",
+            }
+        ],
+        "source": "",
+    }
+    expected_output = (
+        "     ╭──────────────────────────────────"
+        "───────────────────────────────────────╮"
+        "\n\x1b[38;5;247m[2]:\x1b[0m │                  "
+        "                                        "
+        "               │\n     ╰─────────────────"
+        "────────────────────────────────────────"
+        "────────────────╯\n                      "
+        "                                        "
+        "                  \n\x1b[38;5;247m[2]:\x1b[0m  "
+        f"\x1b]8;id=635975;file://{tempfile_path}0.html\x1b\\\x1b"
+        "[94m🌐 Click to view"
+        " HTML\x1b[0m\x1b]8;;\x1b\\                        "
+        "                             \n          "
+        "                                        "
+        "                              \n\x1b[38;5;24"
+        "7m[2]:\x1b[0m   \x1b[1m           Model:\x1b[0m  "
+        "              \x1b[1mDecision Tree\x1b[0m     "
+        "           \x1b[1mRegression\x1b[0m \n       \x1b["
+        "1m           Tumour\x1b[0m   \x1b[1mNon-Tumour"
+        "\x1b[0m   \x1b[1m       Tumour\x1b[0m   \x1b[1mNon-T"
+        "umour\x1b[0m              \n       \x1b[1m    A"
+        "ctual Label:\x1b[0m   \x1b[1m          \x1b[0m   "
+        "\x1b[1m             \x1b[0m   \x1b[1m          \x1b["
+        "0m   \x1b[1m          \x1b[0m \n      ─────────"
+        "────────────────────────────────────────"
+        "─────────────────────────\n       \x1b[1mTum"
+        "our (Positive)\x1b[0m           38         "
+        "      2           18           22 \n     "
+        "  \x1b[1m       Non-Tumour\x1b[0m           19"
+        "             439            6          4"
+        "52 \n       \x1b[1m       (Negative)\x1b[0m    "
+        "                                        "
+        "            \n"
+    )
+    output = rich_notebook_output(code_cell)
+    assert remove_link_ids(output) == remove_link_ids(expected_output)
+
+
+def test_render_no_columns_dataframe(
+    rich_notebook_output: RichOutput,
+    mock_tempfile_file: Generator[Mock, None, None],
+    remove_link_ids: Callable[[str], str],
+    tempfile_path: Path,
+) -> None:
+    """It renders a DataFrame with missing columns."""
+    code_cell = {
+        "cell_type": "code",
+        "execution_count": 2,
+        "id": "mighty-oasis",
+        "metadata": {},
+        "outputs": [
+            {
+                "data": {
+                    "text/html": (
+                        """
+<style type="text/css">
+  \n</style
+>\n
+<table id="T_aba0a_">
+  \n
+  <thead>
+  </thead>
+  \n
+  <tbody>
+    \n
+    <tr>
+      \n
+      <th id="T_aba0a_level0_row0" class="row_heading level0 row0">
+        Tumour (Positive)
+      </th>
+      \n
+      <td id="T_aba0a_row0_col0" class="data row0 col0">38</td>
+      \n
+      <td id="T_aba0a_row0_col1" class="data row0 col1">2</td>
+      \n
+      <td id="T_aba0a_row0_col2" class="data row0 col2">18</td>
+      \n
+      <td id="T_aba0a_row0_col3" class="data row0 col3">22</td>
+      \n
+    </tr>
+    \n
+    <tr>
+      \n
+      <th id="T_aba0a_level0_row1" class="row_heading level0 row1">
+        Non-Tumour (Negative)
+      </th>
+      \n
+      <td id="T_aba0a_row1_col0" class="data row1 col0">19</td>
+      \n
+      <td id="T_aba0a_row1_col1" class="data row1 col1">439</td>
+      \n
+      <td id="T_aba0a_row1_col2" class="data row1 col2">6</td>
+      \n
+      <td id="T_aba0a_row1_col3" class="data row1 col3">452</td>
+      \n
+    </tr>
+    \n
+  </tbody>
+  \n
+</table>
+\n
+                        """
+                    ),
+                    "text/plain": (
+                        "lorep              hey                by"
+                        "e\nipsum               hi very_long_word "
+                        " hi\nfirst second third                  "
+                        "     \nbar   one    1       1            "
+                        "  2   4\n             10      3          "
+                        "    4  -1\n      three  3       3        "
+                        "      4  -1\nfoo   one    1       3      "
+                        "        4  -1"
+                    ),
+                },
+                "execution_count": 2,
+                "metadata": {},
+                "output_type": "execute_result",
+            }
+        ],
+        "source": "",
+    }
+    expected_output = (
+        "     ╭──────────────────────────────────"
+        "───────────────────────────────────────╮"
+        "\n\x1b[38;5;247m[2]:\x1b[0m │                  "
+        "                                        "
+        "               │\n     ╰─────────────────"
+        "────────────────────────────────────────"
+        "────────────────╯\n                      "
+        "                                        "
+        "                  \n\x1b[38;5;247m[2]:\x1b[0m  "
+        f"\x1b]8;id=380451;file://{tempfile_path}0.html\x1b\\\x1b"
+        "[94m🌐 Click to view"
+        " HTML\x1b[0m\x1b]8;;\x1b\\                        "
+        "                             \n          "
+        "                                        "
+        "                              \n\x1b[38;5;24"
+        "7m[2]:\x1b[0m   \x1b[1mTumour (Positive)    \x1b["
+        "0m   38   2     18   22                 "
+        "              \n       \x1b[1mNon-Tumour (Ne"
+        "gative)\x1b[0m   19   439   6    452       "
+        "                       \n"
+    )
+    output = rich_notebook_output(code_cell)
+    assert remove_link_ids(output) == remove_link_ids(expected_output)
+
+
+def test_render_uneven_data_dataframe(
+    rich_notebook_output: RichOutput,
+    mock_tempfile_file: Generator[Mock, None, None],
+    remove_link_ids: Callable[[str], str],
+    tempfile_path: Path,
+) -> None:
+    """It renders a DataFrame with non square data."""
+    code_cell = {
+        "cell_type": "code",
+        "execution_count": 2,
+        "id": "mighty-oasis",
+        "metadata": {},
+        "outputs": [
+            {
+                "data": {
+                    "text/html": (
+                        """
+<style type="text/css">
+  \n</style
+>\n
+<table id="T_aba0a_">
+  \n
+  <thead>
+  </thead>
+  \n
+  <tbody>
+    \n
+    <tr>
+      \n
+      <th id="T_aba0a_level0_row0" class="row_heading level0 row0">
+        Tumour (Positive)
+      </th>
+      \n
+      <td id="T_aba0a_row0_col1" class="data row0 col1">2</td>
+      \n
+      <td id="T_aba0a_row0_col2" class="data row0 col2">18</td>
+      \n
+      <td id="T_aba0a_row0_col3" class="data row0 col3">22</td>
+      \n
+    </tr>
+    \n
+    <tr>
+      \n
+      <th id="T_aba0a_level0_row1" class="row_heading level0 row1">
+        Non-Tumour (Negative)
+      </th>
+      \n
+      <td id="T_aba0a_row1_col0" class="data row1 col0">19</td>
+      \n
+      <td id="T_aba0a_row1_col1" class="data row1 col1">439</td>
+      \n
+      <td id="T_aba0a_row1_col2" class="data row1 col2">6</td>
+      \n
+      <td id="T_aba0a_row1_col3" class="data row1 col3">452</td>
+      \n
+    </tr>
+    \n
+  </tbody>
+  \n
+</table>
+\n
+                        """
+                    ),
+                    "text/plain": (
+                        "lorep              hey                by"
+                        "e\nipsum               hi very_long_word "
+                        " hi\nfirst second third                  "
+                        "     \nbar   one    1       1            "
+                        "  2   4\n             10      3          "
+                        "    4  -1\n      three  3       3        "
+                        "      4  -1\nfoo   one    1       3      "
+                        "        4  -1"
+                    ),
+                },
+                "execution_count": 2,
+                "metadata": {},
+                "output_type": "execute_result",
+            }
+        ],
+        "source": "",
+    }
+    expected_output = (
+        "     ╭──────────────────────────────────"
+        "───────────────────────────────────────╮"
+        "\n\x1b[38;5;247m[2]:\x1b[0m │                  "
+        "                                        "
+        "               │\n     ╰─────────────────"
+        "────────────────────────────────────────"
+        "────────────────╯\n                      "
+        "                                        "
+        "                  \n\x1b[38;5;247m[2]:\x1b[0m  "
+        f"\x1b]8;id=330589;file://{tempfile_path}0.html\x1b\\\x1b"
+        "[94m🌐 Click to view"
+        " HTML\x1b[0m\x1b]8;;\x1b\\                        "
+        "                             \n          "
+        "                                        "
+        "                              \n\x1b[38;5;24"
+        "7m[2]:\x1b[0m   \x1b[1mTumour (Positive)    \x1b["
+        "0m   2    18    22                      "
+        "              \n       \x1b[1mNon-Tumour (Ne"
+        "gative)\x1b[0m   19   439   6    452       "
+        "                       \n"
+    )
+    output = rich_notebook_output(code_cell)
+    assert remove_link_ids(output) == remove_link_ids(expected_output)
+
+
+def test_render_uneven_index_dataframe(
+    rich_notebook_output: RichOutput,
+    mock_tempfile_file: Generator[Mock, None, None],
+    remove_link_ids: Callable[[str], str],
+    tempfile_path: Path,
+) -> None:
+    """It renders a DataFrame with uneven index names."""
+    code_cell = {
+        "cell_type": "code",
+        "execution_count": 2,
+        "id": "mighty-oasis",
+        "metadata": {},
+        "outputs": [
+            {
+                "data": {
+                    "text/html": (
+                        """
+<style type="text/css">
+  \n</style
+>\n
+<table id="T_aba0a_">
+  \n
+  <thead>
+  </thead>
+  \n
+  <tbody>
+    \n
+    <tr>
+      \n
+      <td id="T_aba0a_row0_col1" class="data row0 col1">2</td>
+      \n
+      <td id="T_aba0a_row0_col2" class="data row0 col2">18</td>
+      \n
+      <td id="T_aba0a_row0_col3" class="data row0 col3">22</td>
+      \n
+    </tr>
+    \n
+    <tr>
+      \n
+      <th id="T_aba0a_level0_row1" class="row_heading level0 row1">
+        Non-Tumour (Negative)
+      </th>
+      \n
+      <td id="T_aba0a_row1_col0" class="data row1 col0">19</td>
+      \n
+      <td id="T_aba0a_row1_col1" class="data row1 col1">439</td>
+      \n
+      <td id="T_aba0a_row1_col2" class="data row1 col2">6</td>
+      \n
+      <td id="T_aba0a_row1_col3" class="data row1 col3">452</td>
+      \n
+    </tr>
+    \n
+  </tbody>
+  \n
+</table>
+\n
+
+                        """
+                    ),
+                    "text/plain": (
+                        "lorep              hey                by"
+                        "e\nipsum               hi very_long_word "
+                        " hi\nfirst second third                  "
+                        "     \nbar   one    1       1            "
+                        "  2   4\n             10      3          "
+                        "    4  -1\n      three  3       3        "
+                        "      4  -1\nfoo   one    1       3      "
+                        "        4  -1"
+                    ),
+                },
+                "execution_count": 2,
+                "metadata": {},
+                "output_type": "execute_result",
+            }
+        ],
+        "source": "",
+    }
+    expected_output = (
+        "     ╭──────────────────────────────────"
+        "───────────────────────────────────────╮"
+        "\n\x1b[38;5;247m[2]:\x1b[0m │                  "
+        "                                        "
+        "               │\n     ╰─────────────────"
+        "────────────────────────────────────────"
+        "────────────────╯\n                      "
+        "                                        "
+        "                  \n\x1b[38;5;247m[2]:\x1b[0m  "
+        f"\x1b]8;id=487619;file://{tempfile_path}0.html\x1b\\\x1b"
+        "[94m🌐 Click to view"
+        " HTML\x1b[0m\x1b]8;;\x1b\\                        "
+        "                             \n          "
+        "                                        "
+        "                              \n\x1b[38;5;24"
+        "7m[2]:\x1b[0m   2                       18 "
+        "  22                                    "
+        "      \n       \x1b[1mNon-Tumour (Negative)\x1b"
+        "[0m   19   439   6   452                "
+        "               \n"
+    )
+    output = rich_notebook_output(code_cell)
+    assert remove_link_ids(output) == remove_link_ids(expected_output)
+
+
+def test_render_empty_html_dataframe(
+    rich_notebook_output: RichOutput,
+    mock_tempfile_file: Generator[Mock, None, None],
+    remove_link_ids: Callable[[str], str],
+    tempfile_path: Path,
+) -> None:
+    """It renders a blank output when given an empty table."""
+    code_cell = {
+        "cell_type": "code",
+        "execution_count": 2,
+        "id": "mighty-oasis",
+        "metadata": {},
+        "outputs": [
+            {
+                "data": {
+                    "text/html": (
+                        """
+<style type="text/css">
+  \n</style
+>\n
+<table id="T_aba0a_">
+  \n
+  <thead>
+  </thead>
+  \n
+  <tbody>
+  </tbody>
+  \n
+</table>
+\n
+                        """
+                    ),
+                    "text/plain": (
+                        "lorep              hey                by"
+                        "e\nipsum               hi very_long_word "
+                        " hi\nfirst second third                  "
+                        "     \nbar   one    1       1            "
+                        "  2   4\n             10      3          "
+                        "    4  -1\n      three  3       3        "
+                        "      4  -1\nfoo   one    1       3      "
+                        "        4  -1"
+                    ),
+                },
+                "execution_count": 2,
+                "metadata": {},
+                "output_type": "execute_result",
+            }
+        ],
+        "source": "",
+    }
+    expected_output = (
+        "     ╭──────────────────────────────────"
+        "───────────────────────────────────────╮"
+        "\n\x1b[38;5;247m[2]:\x1b[0m │                  "
+        "                                        "
+        "               │\n     ╰─────────────────"
+        "────────────────────────────────────────"
+        "────────────────╯\n                      "
         "                                        "
-        "               \n                        "
+        "                  \n\x1b[38;5;247m[2]:\x1b[0m  "
+        f"\x1b]8;id=316923;file://{tempfile_path}0.html"
+        "\x1b\\\x1b[94m🌐 Click to view"
+        " HTML\x1b[0m\x1b]8;;\x1b\\                        "
+        "                             \n          "
         "                                        "
-        "                \nlorep              hey "
-        "               bye                      "
-        "                 \nipsum               hi"
-        " very_long_word  hi                     "
-        "                  \nfirst second third   "
+        "                              \n\x1b[38;5;24"
+        "7m[2]:\x1b[0m                              "
         "                                        "
-        "                   \nbar   one    1      "
-        " 1              2   4                   "
-        "                    \n             10    "
-        "  3              4  -1                  "
-        "                     \n      three  3    "
-        "   3              4  -1                 "
-        "                      \nfoo   one    1   "
-        "    3              4  -1                "
-        "                       \n"
+        "      \n"
     )
-    output = rich_notebook_output(code_cell, plain=True)
+    output = rich_notebook_output(code_cell)
     assert remove_link_ids(output) == remove_link_ids(expected_output)
 
 
@@ -1003,15 +2365,19 @@ def test_render_stderr_stream(rich_notebook_output: RichOutput) -> None:
         "────────────────────────────────────────"
         "────────────────╯\n                      "
         "                                        "
-        "                  \n      \x1b[38;5;237;48;5"
-        ";174m<ipython-input-5-bc08279b5148>:2: U"
-        "serWarning: Lorep                      \x1b"
-        "[0m\n      \x1b[38;5;237;48;5;174m warnings."
-        'warn("Lorep")                           '
-        "                        \x1b[0m\n      \x1b[38;"
-        "5;237;48;5;174m                         "
+        "                  \n      \x1b[48;5;174m    "
+        "                                        "
+        "                              \x1b[0m\n     "
+        " \x1b[48;5;174m \x1b[0m\x1b[38;5;237;48;5;174m<ip"
+        "ython-input-5-bc08279b5148>:2: UserWarni"
+        "ng: Lorep                    \x1b[0m\x1b[48;5;"
+        "174m \x1b[0m\n      \x1b[48;5;174m \x1b[0m\x1b[38;5;2"
+        '37;48;5;174m warnings.warn("Lorep")     '
         "                                        "
-        "         \x1b[0m\n"
+        "    \x1b[0m\x1b[48;5;174m \x1b[0m\n      \x1b[48;5;17"
+        "4m \x1b[0m\x1b[38;5;237;48;5;174m             "
+        "                                        "
+        "                   \x1b[0m\x1b[48;5;174m \x1b[0m\n"
     )
     output = rich_notebook_output(stderr_cell)
     assert output == expected_output
@@ -1038,9 +2404,7 @@ def test_render_stream_stdout(rich_notebook_output: RichOutput) -> None:
         "                                        "
         "                  \n      Lorep          "
         "                                        "
-        "                   \n                    "
-        "                                        "
-        "                    \n"
+        "                   \n"
     )
     output = rich_notebook_output(stdout_cell)
     assert output == expected_output
@@ -1247,6 +2611,49 @@ def test_render_error_traceback_no_hang(
     assert output == expected_output
 
 
+def test_render_debugger_output(
+    rich_notebook_output: RichOutput, expected_output: str
+) -> None:
+    """It renders the output from the debugger."""
+    debugger_output_cell = {
+        "cell_type": "code",
+        "execution_count": 4,
+        "id": "fa534da6-88ac-43bc-b00f-cc68ace69fb7",
+        "metadata": {},
+        "outputs": [
+            {
+                "name": "stdout",
+                "output_type": "stream",
+                "text": "> \x1b[1;32m<ipython-input-4-a2d401806d89>\x1b"
+                "[0m(1)\x1b[0;36m<module>\x1b[1;34m()\x1b[0m\n\x1b"
+                "[1;32m----> 1 \x1b[1;33m\x1b[0m_jupyterlab_variable"
+                "inspector_dict_list\x1b[0m\x1b[1;33m(\x1b[0m\x1b"
+                "[1;33m)\x1b[0m\x1b[1;33m\x1b[0m\x1b[1;33m\x1b[0m\x1b"
+                "[0m\n\x1b[0m\n",
+            },
+            {"name": "stdin", "output_type": "stream", "text": "ipdb>  ll\n"},
+            {
+                "name": "stdout",
+                "output_type": "stream",
+                "text": "\x1b[1;32m----> 1 \x1b[1;33m\x1b[0m_"
+                "jupyterlab_variableinspector_dict_list\x1b[0m\x1b"
+                "[1;33m(\x1b[0m\x1b[1;33m)\x1b[0m\x1b[1;33m\x1b[0m\x1b"
+                "[1;33m\x1b[0m\x1b[0m\n\x1b[0m\n",
+            },
+            {"name": "stdin", "output_type": "stream", "text": "ipdb>  sticky\n"},
+            {
+                "name": "stdout",
+                "output_type": "stream",
+                "text": "*** NameError: name 'sticky' is not defined\n",
+            },
+            {"name": "stdin", "output_type": "stream", "text": "ipdb>  q\n"},
+        ],
+        "source": "%debug",
+    }
+    output = rich_notebook_output(debugger_output_cell)
+    assert output == expected_output
+
+
 def test_render_result(rich_notebook_output: RichOutput) -> None:
     """It renders a result."""
     output_cell = {
@@ -1516,6 +2923,41 @@ def test_render_latex_output(rich_notebook_output: RichOutput) -> None:
     assert expected_output == output
 
 
+def test_render_invalid_latex_output(rich_notebook_output: RichOutput) -> None:
+    """It renders invalid LaTeX output."""
+    latex_output_cell = {
+        "cell_type": "code",
+        "execution_count": 15,
+        "id": "sapphire-harmony",
+        "metadata": {},
+        "outputs": [
+            {
+                "data": {
+                    "text/latex": r"garbledmess \sef{}",
+                },
+                "metadata": {},
+                "output_type": "display_data",
+            }
+        ],
+        "source": "",
+    }
+    expected_output = (
+        "      ╭─────────────────────────────────"
+        "───────────────────────────────────────╮"
+        "\n\x1b[38;5;247m[15]:\x1b[0m │                 "
+        "                                        "
+        "               │\n      ╰────────────────"
+        "────────────────────────────────────────"
+        "────────────────╯\n                      "
+        "                                        "
+        "                  \n       garbledmess   "
+        "                                        "
+        "                   \n"
+    )
+    output = rich_notebook_output(latex_output_cell)
+    assert expected_output == output
+
+
 def test_render_latex_output_no_unicode(rich_notebook_output: RichOutput) -> None:
     """It does not render LaTeX output if unicode is False."""
     latex_output_cell = {
@@ -1831,6 +3273,55 @@ def test_vega_output(
     assert remove_link_ids(output) == remove_link_ids(expected_output)
 
 
+def test_invalid_vega_output(
+    rich_notebook_output: RichOutput,
+    mock_tempfile_file: Generator[Mock, None, None],
+    remove_link_ids: Callable[[str], str],
+    tempfile_path: Path,
+) -> None:
+    """It renders a hyperlink to an invalid Vega plot."""
+    vega_output_cell = {
+        "cell_type": "code",
+        "execution_count": 3,
+        "metadata": {"tags": []},
+        "outputs": [
+            {
+                "data": {
+                    "application/vnd.vega.v5+json": {
+                        "invalid": "no",
+                    },
+                },
+                "metadata": {},
+                "output_type": "display_data",
+            }
+        ],
+        "source": "",
+    }
+    expected_output = (
+        "     ╭──────────────────────────────────"
+        "───────────────────────────────────────╮"
+        "\n\x1b[38;5;247m[3]:\x1b[0m │                  "
+        "                                        "
+        "               │\n     ╰─────────────────"
+        "────────────────────────────────────────"
+        "────────────────╯\n                      "
+        "                                        "
+        "                  \n      \x1b]8;id=16281369"
+        f"58.012196-350876;file://{tempfile_path}0.html\x1b\\\x1b[94m\uf080"
+        " Click to v"
+        "iew Vega chart\x1b[0m\x1b]8;;\x1b\\               "
+        "                                 \n"
+    )
+    output = rich_notebook_output(
+        vega_output_cell,
+        nerd_font=True,
+        files=True,
+        hyperlinks=True,
+        hide_hyperlink_hints=False,
+    )
+    assert remove_link_ids(output) == remove_link_ids(expected_output)
+
+
 def test_vegalite_output(
     rich_notebook_output: RichOutput,
     mock_tempfile_file: Generator[Mock, None, None],
@@ -2606,6 +4097,85 @@ def test_render_html(
     assert remove_link_ids(output) == remove_link_ids(expected_output)
 
 
+def test_render_html_table(
+    rich_notebook_output: RichOutput,
+    mock_tempfile_file: Generator[Mock, None, None],
+    remove_link_ids: Callable[[str], str],
+    tempfile_path: Path,
+) -> None:
+    """It renders an HTML table."""
+    html_cell = {
+        "cell_type": "code",
+        "execution_count": 7,
+        "id": "43e39858-6416-4dc8-9d7e-7905127e7452",
+        "metadata": {},
+        "outputs": [
+            {
+                "data": {
+                    "text/html": """\
+<table>
+  <tr>
+    <th>Company</th>
+    <th>Contact</th>
+    <th>Country</th>
+  </tr>
+  <tr>
+    <td>Alfreds Futterkiste</td>
+    <td>Maria Anders</td>
+    <td>Germany</td>
+  </tr>
+  <tr>
+    <td>Centro comercial Moctezuma</td>
+    <td>Francisco Chang</td>
+    <td>Mexico</td>
+  </tr>
+</table>
+                    """,
+                    "text/plain": "<IPython.core.display.HTML object>",
+                },
+                "metadata": {},
+                "output_type": "display_data",
+            }
+        ],
+        "source": "",
+    }
+
+    expected_output = (
+        "     ╭──────────────────────────────────"
+        "───────────────────────────────────────╮"
+        "\n\x1b[38;5;247m[7]:\x1b[0m │                  "
+        "                                        "
+        "               │\n     ╰─────────────────"
+        "────────────────────────────────────────"
+        "────────────────╯\n                      "
+        "                                        "
+        "                  \n      \x1b]8;id=58222;fi"
+        f"le://{tempfile_path}0.ht"
+        "ml\x1b\\\x1b[94m🌐 Click to view HTML\x1b[0m\x1b]8;;\x1b\\"
+        "                                        "
+        "             \n                          "
+        "                                        "
+        "              \n                         "
+        "                                        "
+        "               \n       \x1b[1mCompany\x1b[0m  "
+        "                \x1b[1mContact\x1b[0m         "
+        "         \x1b[1mCountry\x1b[0m                "
+        "\n      ─────────────────────────────────"
+        "────────────────────────────────────────"
+        "─\n       Alfreds Futterkiste      Maria "
+        "Anders             Germany              "
+        "  \n       Centro comercial         Franc"
+        "isco Chang          Mexico              "
+        "   \n       Moctezuma                    "
+        "                                        "
+        "    \n                                   "
+        "                                        "
+        "     \n"
+    )
+    output = rich_notebook_output(html_cell)
+    assert remove_link_ids(output) == remove_link_ids(expected_output)
+
+
 def test_render_unknown_data_type(rich_notebook_output: RichOutput) -> None:
     """It skips rendering an unknown output type."""
     unknown_data_type = {
@@ -2971,7 +4541,7 @@ def test_charater_drawing(
     remove_link_ids: Callable[[str], str],
     expected_output: str,
 ) -> None:
-    """It renders a block drawing of an image."""
+    """It renders a character drawing of an image."""
     image_cell = {
         "cell_type": "code",
         "execution_count": 1,
@@ -3263,6 +4833,63 @@ def test_braille_drawing(
     assert output == expected_output
 
 
+def test_invalid_image_drawing(
+    rich_notebook_output: RichOutput,
+    mock_tempfile_file: Generator[Mock, None, None],
+    remove_link_ids: Callable[[str], str],
+) -> None:
+    """It fallsback to text when failing to draw image."""
+    image_cell = {
+        "cell_type": "code",
+        "execution_count": 1,
+        "id": "43e39858-6416-4dc8-9d7e-7905127e7452",
+        "metadata": {},
+        "outputs": [
+            {
+                "data": {"text/plain": "<AxesSubplot:>"},
+                "execution_count": 1,
+                "metadata": {},
+                "output_type": "execute_result",
+            },
+            {
+                "data": {
+                    "image/png": "ib45",
+                    "text/plain": "<Figure size 432x288 with 1 Axes>",
+                },
+                "metadata": {"needs_background": "light"},
+                "output_type": "display_data",
+            },
+        ],
+        "source": "",
+    }
+    output = rich_notebook_output(
+        image_cell, images=True, image_drawing="character", files=False
+    )
+    expected_output = (
+        "     ╭──────────────────────────────────"
+        "───────────────────────────────────────╮"
+        "\n\x1b[38;5;247m[1]:\x1b[0m │                  "
+        "                                        "
+        "               │\n     ╰─────────────────"
+        "────────────────────────────────────────"
+        "────────────────╯\n                      "
+        "                                        "
+        "                  \n\x1b[38;5;247m[1]:\x1b[0m  "
+        "<AxesSubplot:>                          "
+        "                                  \n     "
+        "                                        "
+        "                                   \n    "
+        "  🖼 Image                               "
+        "                                    \n   "
+        "                                        "
+        "                                     \n  "
+        "    \x1b[38;2;187;134;252m<Figure size 432x"
+        "288 with 1 Axes>                        "
+        "                 \x1b[0m\n"
+    )
+    assert output == expected_output
+
+
 def test_render_image_link_no_image(
     rich_notebook_output: RichOutput,
     mock_tempfile_file: Generator[Mock, None, None],
@@ -3535,3 +5162,55 @@ def test_unknown_language() -> None:
     expected_output = "python"
     acutal_output = rendered_notebook.language
     assert acutal_output == expected_output
+
+
+def test_skip_unknown_cell_type(rich_notebook_output: RichOutput) -> None:
+    """It skips rendering a cell if the type is not known."""
+    markdown_cell = {
+        "cell_type": "unknown",
+        "id": "academic-bride",
+        "metadata": {},
+        "source": "### Lorep ipsum\n\n**dolor** _sit_ `amet`",
+    }
+    output = rich_notebook_output(markdown_cell)
+    expected_output = ""
+    assert output == expected_output
+
+
+def test_skip_no_cell_type(rich_notebook_output: RichOutput) -> None:
+    """It skips rendering a cell if there is not cell type."""
+    markdown_cell = {
+        "metadata": {"no"},
+        "source": "### Lorep ipsum\n\n**dolor** _sit_ `amet`",
+    }
+    output = rich_notebook_output(markdown_cell)
+    expected_output = ""
+    assert output == expected_output
+
+
+def test_image_link_not_image(
+    rich_notebook_output: RichOutput,
+    mocker: MockerFixture,
+    remove_link_ids: Callable[[str], str],
+) -> None:
+    """It falls back to skipping drawing if content is not an image."""
+    mock = mocker.patch("httpx.get")
+    mock.return_value.content = "Bad image"
+    markdown_cell = {
+        "cell_type": "markdown",
+        "id": "academic-bride",
+        "metadata": {},
+        "source": "![Azores](https://github.com/paw-lu/nbpreview/tests"
+        "/assets/outline_article_white_48dp.png)",
+    }
+    output = rich_notebook_output(markdown_cell, image_drawing="character")
+    expected_output = (
+        "  \x1b]8;id=246597;https://github.com/paw-l"
+        "u/nbpreview/tests/assets/outline_article"
+        "_white_48dp.png\x1b\\\x1b[94m🌐 Click to view Az"
+        "ores\x1b[0m\x1b]8;;\x1b\\                         "
+        "                              \n         "
+        "                                        "
+        "                               \n"
+    )
+    assert remove_link_ids(output) == remove_link_ids(expected_output)