Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions aidialog/_modidx.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
'aidialog.dialog.Message.add_output': ('dialog.html#message.add_output', 'aidialog/dialog.py'),
'aidialog.dialog.Message.ai_output': ('dialog.html#message.ai_output', 'aidialog/dialog.py'),
'aidialog.dialog.Message.ai_res': ('dialog.html#message.ai_res', 'aidialog/dialog.py'),
'aidialog.dialog.Message.body': ('dialog.html#message.body', 'aidialog/dialog.py'),
'aidialog.dialog.Message.cell_type': ('dialog.html#message.cell_type', 'aidialog/dialog.py'),
'aidialog.dialog.Message.clear_inp_cache': ('dialog.html#message.clear_inp_cache', 'aidialog/dialog.py'),
'aidialog.dialog.Message.clear_out_cache': ('dialog.html#message.clear_out_cache', 'aidialog/dialog.py'),
Expand Down Expand Up @@ -80,6 +81,7 @@
'aidialog.dialog.RunResult.show': ('dialog.html#runresult.show', 'aidialog/dialog.py'),
'aidialog.dialog._code_span': ('dialog.html#_code_span', 'aidialog/dialog.py'),
'aidialog.dialog._fmt_param': ('dialog.html#_fmt_param', 'aidialog/dialog.py'),
'aidialog.dialog._fold': ('dialog.html#_fold', 'aidialog/dialog.py'),
'aidialog.dialog._get_exported': ('dialog.html#_get_exported', 'aidialog/dialog.py'),
'aidialog.dialog._get_mexp': ('dialog.html#_get_mexp', 'aidialog/dialog.py'),
'aidialog.dialog._inline_atts': ('dialog.html#_inline_atts', 'aidialog/dialog.py'),
Expand Down
32 changes: 26 additions & 6 deletions aidialog/dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,13 @@ def has_directive(self:Message, name):
"Does this message carry directive `name`, in content or meta?"
return name in self.directives

@patch(as_prop=True)
def body(self:Message):
"Content with leading directive comments removed; cell magics kept"
c = mk_cell(self.content, metadata=self.meta)
c.remove_directives()
return c.source

# %% ../nbs/01_dialog.ipynb #e4fb159d
@patch
def update(self:Message,
Expand Down Expand Up @@ -862,21 +869,34 @@ def _inline_atts(m, text):
for a in m.attachments: text = text.replace(f'attachment:{a.id}', f'data:{a.content_type};base64,{base64.b64encode(a.data).decode()}')
return text

def _fold(m, code):
"Wrap `code` in a `::: {.details .code-fold}` div when `m` has `code-fold: true|show`, labelled by `code-summary`"
fold = m.directive('code-fold')
if fold not in ('', 'true', 'show'): return code
attrs = ' {.details .code-fold' + (' open=""' if fold=='show' else '') + '}'
summary = m.directive('code-summary') or 'Code'
if len(summary)>1 and summary[0] in "\"'" and summary[-1]==summary[0]: summary = summary[1:-1]
return fenced(f"## {summary}\n\n{code}", attrs, ch=':')

def msg2md(m,
weave:bool=False, # Code messages contribute only `render_text` over their outputs, as document prose?
):
"One message as Markdown: notes verbatim with attachments inlined as data URIs, code in a ```python fence with outputs in a `::: output` div, prompts and replies in `::: prompt`/`::: reply` divs; None for empty code"
"Render one message as Markdown; a code message applies its `include`, `echo`, `output`, and `code-fold` directives and hides the directive lines"
if m.msg_type == snote: return _inline_atts(m, m.content)
if m.msg_type == sraw: return fenced(m.content)
if m.msg_type == sprompt:
parts = [fenced(_inline_atts(m, m.content), ' prompt', ch=':')]
if (ai := (m.ai_res or '')).strip(): parts.append(fenced(fmt_tools(ai), ' reply', ch=':'))
return '\n\n'.join(parts)
if weave: return render_text(m.output or []) or None
if not m.content.strip(): return None
parts = [fenced(m.content, 'python')]
if (outs := render_md(m.output or [])): parts.append(fenced(outs, ' output', ch=':'))
return '\n\n'.join(parts)
if m.directive('include')=='false': return None
outdir = m.directive('output')
if weave: return None if outdir=='false' else render_text(m.output or []) or None
src = m.body
parts = [] if m.directive('echo')=='false' or not src.strip() else [_fold(m, fenced(src, 'python'))]
if outdir!='false':
rf, wrap = (render_text, noop) if outdir=='asis' else (render_md, partial(fenced, info=' output', ch=':'))
if outs := rf(m.output or []): parts.append(wrap(outs))
return '\n\n'.join(parts) or None

def dlg2md(d,
exportfilter:bool=False, # Keep only the messages `export_filter` selects?
Expand Down
177 changes: 151 additions & 26 deletions nbs/01_dialog.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -559,10 +559,10 @@
{
"data": {
"text/markdown": [
"64ace59d:n:in ⇒ out(3)"
"1f90ddc0:n:in ⇒ out(3)"
],
"text/plain": [
"Message(id='64ace59d', content='in', output='out', msg_type='note')"
"Message(id='1f90ddc0', content='in', output='out', msg_type='note')"
]
},
"execution_count": null,
Expand All @@ -584,9 +584,9 @@
{
"data": {
"text/plain": [
"4fdd721b:p:q\n",
"d3127c9b:p:q\n",
"> Edited reply\n",
"95575dfa:c:x=1 ⇒ out(601)"
"fd009f7e:c:x=1 ⇒ out(601)"
]
},
"execution_count": null,
Expand Down Expand Up @@ -771,10 +771,10 @@
{
"data": {
"text/markdown": [
"d6b8bf64:c:8*8 ⇒ out(103)"
"cff8b95e:c:8*8 ⇒ out(103)"
],
"text/plain": [
"Message(id='d6b8bf64', content='8*8', output=[{'output_type': 'execute_result', 'metadata': {}, 'data': {'text/plain': '64'}, 'execution_count': 1}], msg_type='code')"
"Message(id='cff8b95e', content='8*8', output=[{'output_type': 'execute_result', 'metadata': {}, 'data': {'text/plain': '64'}, 'execution_count': 1}], msg_type='code')"
]
},
"execution_count": null,
Expand Down Expand Up @@ -1763,7 +1763,7 @@
"id": "ac7d65ba",
"metadata": {},
"source": [
"`directive` and `has_directive` are the same queries `NbCell` answers, with the same names and meanings, so code written against cells (such as `fastcore.nbio.select_cells` and its filters) works on messages unchanged:"
"`directive` and `has_directive` are the same queries `NbCell` answers, with the same names and meanings, so code written against cells (such as `fastcore.nbio.select_cells` and its filters) works on messages unchanged. `body` is the content without its directive lines, for renderers that act on directives rather than display them:"
]
},
{
Expand All @@ -1782,7 +1782,14 @@
"@patch\n",
"def has_directive(self:Message, name):\n",
" \"Does this message carry directive `name`, in content or meta?\"\n",
" return name in self.directives"
" return name in self.directives\n",
"\n",
"@patch(as_prop=True)\n",
"def body(self:Message):\n",
" \"Content with leading directive comments removed; cell magics kept\"\n",
" c = mk_cell(self.content, metadata=self.meta)\n",
" c.remove_directives()\n",
" return c.source"
]
},
{
Expand All @@ -1806,6 +1813,8 @@
"em = Message('#| eval: false\\n1+1')\n",
"test_eq(em.directive('eval'), 'false')\n",
"assert em.has_directive('eval') and not em.has_directive('export')\n",
"test_eq(em.body, '1+1')\n",
"test_eq(Message('%%time\\n#| eval: false\\nx').body, '%%time\\nx')\n",
"Message('#| export\\nx=1').directive('export')"
]
},
Expand Down Expand Up @@ -2720,8 +2729,8 @@
{
"data": {
"text/plain": [
"a9a03f99: ok\n",
"9ed1a7ce: ok\n",
"c2599acf: ok\n",
"009b926b: ok\n",
"run: 2 msgs ok"
]
},
Expand Down Expand Up @@ -2804,7 +2813,7 @@
{
"data": {
"text/plain": [
"d9705aed: ok\n",
"dae451f3: ok\n",
"run: 1 msg ok"
]
},
Expand Down Expand Up @@ -2839,7 +2848,7 @@
{
"data": {
"text/plain": [
"d9705aed: NameError: name 'y' is not defined\n",
"dae451f3: NameError: name 'y' is not defined\n",
"run: 0 msgs ok, 1 failed"
]
},
Expand Down Expand Up @@ -2871,8 +2880,8 @@
{
"data": {
"text/plain": [
"a9a03f99: ok\n",
"197f3ee9: ZeroDivisionError: division by zero\n",
"c2599acf: ok\n",
"e6847e8d: ZeroDivisionError: division by zero\n",
"run: 1 msg ok, 1 failed"
]
},
Expand Down Expand Up @@ -2906,10 +2915,10 @@
{
"data": {
"text/plain": [
"a9a03f99: ok\n",
"197f3ee9: ZeroDivisionError: division by zero\n",
"9ed1a7ce: ok\n",
"d9705aed: ok\n",
"c2599acf: ok\n",
"e6847e8d: ZeroDivisionError: division by zero\n",
"009b926b: ok\n",
"dae451f3: ok\n",
"run: 3 msgs ok, 1 failed"
]
},
Expand Down Expand Up @@ -2960,7 +2969,9 @@
"id": "a4ecf11a",
"metadata": {},
"source": [
"A dialog projects to a plain Markdown document: notes appear verbatim, code messages become ```python fences with their outputs rendered by fastcore's `render_md` in a `::: output` div, and prompts and replies appear in `::: prompt`/`::: reply` divs (with tool wire blocks displayed via `fmt_tools`). The `:::` class words and the `python` fence language are the format, not options: viewers key their styling off these exact spellings. Two flags adapt the projection for document filling, shown below: `weave` replaces each code message with its rendered outputs, and `exportfilter` keeps only the participating messages."
"A dialog projects to a plain Markdown document: notes appear verbatim, code messages become ```python fences with their outputs rendered by fastcore's `render_md` in a `::: output` div, and prompts and replies appear in `::: prompt`/`::: reply` divs (with tool wire blocks displayed via `fmt_tools`). The `:::` class words and the `python` fence language are the format, not options: viewers key their styling off these exact spellings.\n",
"\n",
"A code message's nbdev directives adjust that projection, and the directive lines themselves are not shown. `include: false` drops the whole message, `echo: false` drops only its source, `output: false` drops only its output, and `output: asis` inserts textual output as document Markdown. `code-fold: true` or `show` wraps the source in a `::: {.details .code-fold}` div (open for `show`), with `code-summary` as its label. Full HTML remains explicit by printing a `{=html}` raw fence. Two flags adapt the projection further: `weave` replaces each code message with its textual output, and `exportfilter` keeps only the participating messages."
]
},
{
Expand All @@ -2976,21 +2987,34 @@
" for a in m.attachments: text = text.replace(f'attachment:{a.id}', f'data:{a.content_type};base64,{base64.b64encode(a.data).decode()}')\n",
" return text\n",
"\n",
"def _fold(m, code):\n",
" \"Wrap `code` in a `::: {.details .code-fold}` div when `m` has `code-fold: true|show`, labelled by `code-summary`\"\n",
" fold = m.directive('code-fold')\n",
" if fold not in ('', 'true', 'show'): return code\n",
" attrs = ' {.details .code-fold' + (' open=\"\"' if fold=='show' else '') + '}'\n",
" summary = m.directive('code-summary') or 'Code'\n",
" if len(summary)>1 and summary[0] in \"\\\"'\" and summary[-1]==summary[0]: summary = summary[1:-1]\n",
" return fenced(f\"## {summary}\\n\\n{code}\", attrs, ch=':')\n",
"\n",
"def msg2md(m,\n",
" weave:bool=False, # Code messages contribute only `render_text` over their outputs, as document prose?\n",
"):\n",
" \"One message as Markdown: notes verbatim with attachments inlined as data URIs, code in a ```python fence with outputs in a `::: output` div, prompts and replies in `::: prompt`/`::: reply` divs; None for empty code\"\n",
" \"Render one message as Markdown; a code message applies its `include`, `echo`, `output`, and `code-fold` directives and hides the directive lines\"\n",
" if m.msg_type == snote: return _inline_atts(m, m.content)\n",
" if m.msg_type == sraw: return fenced(m.content)\n",
" if m.msg_type == sprompt:\n",
" parts = [fenced(_inline_atts(m, m.content), ' prompt', ch=':')]\n",
" if (ai := (m.ai_res or '')).strip(): parts.append(fenced(fmt_tools(ai), ' reply', ch=':'))\n",
" return '\\n\\n'.join(parts)\n",
" if weave: return render_text(m.output or []) or None\n",
" if not m.content.strip(): return None\n",
" parts = [fenced(m.content, 'python')]\n",
" if (outs := render_md(m.output or [])): parts.append(fenced(outs, ' output', ch=':'))\n",
" return '\\n\\n'.join(parts)\n",
" if m.directive('include')=='false': return None\n",
" outdir = m.directive('output')\n",
" if weave: return None if outdir=='false' else render_text(m.output or []) or None\n",
" src = m.body\n",
" parts = [] if m.directive('echo')=='false' or not src.strip() else [_fold(m, fenced(src, 'python'))]\n",
" if outdir!='false':\n",
" rf, wrap = (render_text, noop) if outdir=='asis' else (render_md, partial(fenced, info=' output', ch=':'))\n",
" if outs := rf(m.output or []): parts.append(wrap(outs))\n",
" return '\\n\\n'.join(parts) or None\n",
"\n",
"def dlg2md(d,\n",
" exportfilter:bool=False, # Keep only the messages `export_filter` selects?\n",
Expand Down Expand Up @@ -3147,7 +3171,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "9af22810",
"id": "1a9c09d5",
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -3158,6 +3182,107 @@
"assert '**bold**' in msg2md(mdm) # markdown outputs inline via render_md"
]
},
{
"cell_type": "markdown",
"id": "58cb5952",
"metadata": {},
"source": [
"echo hides source, not the output"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e1160e8c",
"metadata": {},
"outputs": [],
"source": [
"hidden = Message('secret()', output=code_output('64'), meta={'nbdev': {'echo':'false'}})\n",
"hidden_md = msg2md(hidden)\n",
"assert 'secret()' not in hidden_md and '64' in hidden_md # "
]
},
{
"cell_type": "markdown",
"id": "2aa9a2d4",
"metadata": {},
"source": [
"textual output becomes document Markdown with `'output':'asis`"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "61d7bf1d",
"metadata": {},
"outputs": [],
"source": [
"asis = Message('make_md()', output=[dict(output_type='stream', name='stdout', text='## Install\\n')],\n",
" meta={'nbdev': {'echo':'false', 'output':'asis'}})\n",
"test_eq(msg2md(asis), '## Install\\n')\n",
"html_asis = Message('make_html()', output=[dict(output_type='stream', name='stdout', text='```{=html}\\n<b>Hi</b>\\n```\\n')],\n",
" meta={'nbdev': {'echo':'false', 'output':'asis'}})\n",
"assert '```{=html}' in msg2md(html_asis)"
]
},
{
"cell_type": "markdown",
"id": "ff2ce730",
"metadata": {},
"source": [
"both input and output can be excluded independently"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "de1a3a49",
"metadata": {},
"outputs": [],
"source": [
"test_eq(msg2md(Message('x=1', output=code_output('1'), meta={'nbdev': {'output':'false'}})), fenced('x=1', 'python'))\n",
"test_eq(msg2md(Message('x=1', output=code_output('1'), meta={'nbdev': {'include':'false'}})), None)"
]
},
{
"cell_type": "markdown",
"id": "0ab146f9",
"metadata": {},
"source": [
"Directive lines never reach the page"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6d2a9f90",
"metadata": {},
"outputs": [],
"source": [
"test_eq(msg2md(Message('#| eval: false\\nx=1')), fenced('x=1', 'python'))"
]
},
{
"cell_type": "markdown",
"id": "a757614e",
"metadata": {},
"source": [
"`code-fold` turns the code msg in an mdhtml flavored md styled `details` block, and `code-summary` becomes a header which mdhtml knows to turn into a summary tag."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a98fa805",
"metadata": {},
"outputs": [],
"source": [
"for label in ('Setup', '\"Setup\"', \"'Setup'\"):\n",
" folded = msg2md(Message(f'#| code-fold: show\\n#| code-summary: {label}\\nx=1'))\n",
" assert folded.startswith('::: {.details .code-fold open=\"\"}') and '## Setup\\n' in folded and '#|' not in folded\n",
"assert msg2md(Message('#| code-fold: true\\nx=1')).startswith('::: {.details .code-fold}\\n## Code') # `true` starts collapsed"
]
},
{
"cell_type": "code",
"execution_count": null,
Expand Down