TL;DR
I like the aesthetics of this idea, but Iâm not sure that we can / want to do this in a completely kernel agnostic manner.
Context
This topic is one that Iâm deeply interested in. I remember discussing the concept of ârich markdownâ (concerning widgets) with @jasongrout a while back. We both (IIRC) expressed an interested in the ability to have a WYSIWYG kind of editor for widgets-in-text. Other products like Observable have this feature, which gives something to experiment with. Fundamentally, in-line widgets are a similar problem to in-line variables â rendering kernel variables in-line in markup.
I think others have raised the question of
What is a notebook for?
and this captures my philosophical concerns for this idea. Iâm not going to state what I think a notebook is, because I think I need to give that more thought myself.
Additionally, there are already other conversations happening at the moment that ask:
How can we make notebooks more reproducible w.r.t embedded widgets / interactive outputs?
and I think this thread is related.
Using JupyterLab-Markup
Before I go any further, we could bolt on an interactive Markdown renderer on-top of the existing Jupyter notebook, and it would work. A brief discussion of this follows. However, I think now is a good time to discuss wider changes that make the Notebook more forward-looking, and this is discussed in the next section.
As @bollwyvl mentioned, we could already make a good start on this with jupyterlab-markup
. It wouldnât (famous last words) be that hard to write a plugin that embeds variables in markup (using the DAP, I think), and for non-reactive notebooks this would work reasonably well. It would also support widgets (I believe), which would be interesting. As Nick states, the bigger Jupyter-scale problem is that if we start investing heavily into the jupyterlab-markup
route (for this plugin or others) without some way to demand that these extensions (be-it in JupyterLab, or in any frontend) be supported, we start to move towards the old-style Notebook free-for-all. More-over, we lose the robustness of Markdown rendering as a universal feature.
Add Interactive Markdown Cells?
To my mind, this should not be something that is performed by the frontend, at least not directly. Requiring a kernel to render Markdown would preclude running such a notebook in a non-kernel backed viewer (something that several commenters have pointed to).
What we could do is establish a special MIME-bundle that represents a Markdown object that containers rich display objects, i.e.
{
content: "Live long and {{ what }}",
data: {
"what": {
"text/plain": "prosper"
}
}
}
Here, the kernel is responsible for sending over these mimebundles, but it doesnât need to know how they are assembled, and is no longer required after the cell has been executed (i.e. the notebook can be viewed in nbviewer).
Hereâs a mockup based upon IPythonâs Markdown
object:
from IPython.display import IMarkdown
what = "prosper"
IMarkdown(
"Live long and {{ what }}",
data={
"what": what
}
)
or
%%imarkdown what=what
Live long and {{ what }}
We could parse the Markdown in the kernel (e.g. via magics) to look for variable definitions, but as soon as we start needing to infer the variables in the kernel, we require the kernel-library to maintain awareness over the syntax, which (as discussed) can vary between plugins.
For posterity, we can do a hack job by just using f-strings:
import ast
from IPython.core.magic import register_cell_magic
@register_cell_magic
def imarkdown(line, cell):
shell = get_ipython()
expr = ast.Constant(cell)
formatter = ast.Call(
func=ast.Attribute(value=expr, attr="format", ctx=ast.Load()),
args=[],
keywords=[
ast.keyword(
value=ast.Call(func=ast.Name(id="globals"), args=[], keywords=[])
)
],
)
node = ast.Call(
func=ast.Expr(
value=ast.Attribute(
value=ast.Call(
func=ast.Name(id="__import__", ctx=ast.Load()),
args=[ast.Constant(value="IPython.display")],
keywords=[
ast.keyword(arg="fromlist", value=ast.Constant(value="display"))
],
),
attr="Markdown",
ctx=ast.Load(),
)
),
args=[formatter],
keywords=[],
)
shell.run_cell(ast.unparse(node))
%%imarkdown
Live long and { what }
But this is just to make a point.