---
description: Convert notebook content and Markdown to HTML and GitHub-flavored Markdown
output-file: core.html
title: core

---



<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

Use `octavo.core` to turn a document’s Markdown into article HTML or GitHub-flavored Markdown. For notebook content, first convert the cells and their saved outputs to Markdown. This conversion does not run the notebook.

Parse the Markdown once, then render either format:

```python
doc = md2doc("# Installation\n\nRun `pip install -e .`.")
html = doc.to_html()
markdown = doc.to_gfm()
```

`html.html` contains the article HTML. `html.toc` contains a separate table of contents that a page layout can place beside the article. `markdown` contains the GitHub-flavored Markdown text.

YAML frontmatter supplies page metadata such as `title` and `description`. HTML rendering adds these above the article. Markdown export retains the frontmatter.

This module converts document content. Reading source directories, building navigation, and writing the website belong to `octavo.pages` and `octavo.site`.

## Notebook to md

---

### msg2md

```python
def msg2md(
    m, # The message to render
    lang:str='python', # Language for the code fence
):
```

*Render a message as document Markdown, applying publishing directives*

[`msg2md`](core.md#msg2md) applies code-cell publishing directives without executing code. `include: false` omits the cell. `echo: false` hides its source, and `output: false` hides its saved outputs. Notes, raw messages, and prompt/reply pairs retain aidialog's Markdown rendering.

Code-cell publishing supports these directives:

| Directive | Effect |
|---|---|
| `include: false` | Omit the entire cell. |
| `echo: false` | Omit the source. |
| `output: false` | Omit saved outputs. |
| `output: asis` | Insert saved output as text without output fences. |
| `code-*` | Add `data-code-*` attributes to the source fence. |

Directives can appear in `#|` lines or in `nbdev` cell metadata. [`msg2md`](core.md#msg2md) removes directive lines from the published source. It never executes code. `eval: false` therefore does not hide an existing saved output.

By default, the Markdown contains both the source and its saved output. These examples print the generated Markdown rather than render it.

```python
shown = msg2md(Message('8*8', output:=code_output('64')))
print(shown)
```

````output
```python
8*8
```

::: output
```output
64
```
:::
````

Use `echo: false` to publish the saved output without its source.

```python
hidden_code = msg2md(Message('#| echo: false\n8*8', output))
print(hidden_code)
```

````output
::: output
```output
64
```
:::
````

Use `output: false` to publish the source without its saved output. This example sets the directive through cell metadata.

```python
source_only = msg2md(Message('8*8', output, meta={'nbdev': {'output': 'false'}}))
print(source_only)
```

````output
```python
8*8
```
````

Use `include: false` to omit both source and output. The converter returns `None` for an omitted cell.

```python
omitted = msg2md(Message('#| include: false\n8*8', output))
test_eq(omitted, None)
```

Use `output: asis` when saved text contains Markdown to include in the document. Combine it with `echo: false` to show only that Markdown.

```python
asis = msg2md(Message('#| echo: false\n#| output: asis\nprint("**Result:** 64")', code_output('**Result:** 64')))
print(asis)
```

```output
**Result:** 64
```

Use `code-fold` to put source code in a collapsible panel and `code-summary` to name the panel. [`msg2md`](core.md#msg2md) carries these settings as fence attributes for the document renderer. It removes directive comments from the displayed code and escapes attribute values.

```python
code_md = msg2md(Message('#| code-fold: show\n#| code-summary: Setup\nx=1'))
print(code_md)
```

````output
```python {data-code-fold="show" data-code-summary="Setup"}
x=1
```
````

---

### dlg2md

```python
def dlg2md(
    d
):
```

*Render a dialog as document Markdown, fencing code in the notebook's language*

[`dlg2md`](core.md#dlg2md) joins the rendered messages with blank lines and ends the document with a newline. It includes every message unless a code cell's publishing directives exclude it.

Code fences use the notebook's kernel language, from `kernelspec.language` or else `language_info.name`, and fall back to `python`. Quarto does the same, so a notebook running a non-Python kernel keeps its language class in HTML, such as `language-bpl`, and its fence label in Markdown. Here the dialog's notebook metadata names the BPL kernel:

````python
bpl_md = dlg2md(Dialog([Message('1 2 3+4', output=code_output('5 6 7'))], meta={'kernelspec': {'name': 'bpl', 'language': 'bpl'}}))
assert bpl_md.startswith('```bpl\n1 2 3+4\n```')
print(bpl_md)
````

````output
```bpl
1 2 3+4
```

::: output
```output
5 6 7
```
:::
````

## Md to Doc

[`md2doc`](core.md#md2doc) separates YAML frontmatter from the Markdown body. A [`Doc`](core.md#doc) stores the parsed settings in `meta` and the original frontmatter text in `fm`. Keeping the original text preserves its formatting when exporting Markdown.

`body` retains the source Markdown. `frag` contains the parsed MDHTML used for HTML export. Code folds are prepared separately for each output without modifying `body`.

---

### Doc

```python
def Doc(
    body:str, # The original Markdown body, without frontmatter
    frag:str, # The document as mdhtml
    meta:dict, # Parsed YAML frontmatter
    fm:str, # The frontmatter block as the author wrote it; empty where there was none
    warnings:list, # Whatever mdhtml reported while parsing
):
```

*A document’s source Markdown, parsed mdhtml, metadata, and warnings*

Settings are written with hyphens, as in Quarto: `toc-depth`, `open-graph`, `image-alt`. Python names can't contain a hyphen, so [`snake_keys`](core.md#snake_keys) replaces each `-` in a key with `_`, at every level, before Octavo reads the settings. Code then reads `meta['toc_depth']`, or `o.toc_depth`, and takes `toc_depth=2` as a keyword argument. Values don't change.

---

### snake_keys

```python
def snake_keys(
    o
):
```

*`o` with each `-` in the keys of its dicts replaced by `_`, at every level*

```python
settings = snake_keys({'toc-depth': 2, 'open-graph': {'image-alt': 'A chart'}, 'nav-links': [{'text': 'Docs', 'href': 'docs.html'}]})
test_eq(settings, {'toc_depth': 2, 'open_graph': {'image_alt': 'A chart'}, 'nav_links': [{'text': 'Docs', 'href': 'docs.html'}]})
settings
```

```output
{'toc_depth': 2,
 'open_graph': {'image_alt': 'A chart'},
 'nav_links': [{'text': 'Docs', 'href': 'docs.html'}]}
```

A key spelt both ways results in the last one winning:

```python
test_eq(snake_keys({'toc-depth': 2, 'toc_depth': 3}),{'toc_depth':3})
```

---

### md2doc

```python
def md2doc(
    md:str, # Markdown, with optional YAML frontmatter
    math:str='brackets', # Inline maths syntax: `brackets` for `\(…\)`, or `dollars` to also read `$…$`
):
```

*Parse Markdown `md` to a [`Doc`](core.md#doc), retaining its source and preparing code folds for HTML*

Put a page’s `title` and `description` in YAML frontmatter when they need to appear in navigation or listings. Octavo renders them above the article. Start the body with prose and use headings for its sections.

For nbdev notebooks, nbdev converts the opening `# title` and `> description` into frontmatter before Octavo reads the document.

```python
sample = """---
title: config
description: Configuring nbdev and bootstrapping notebook export
order: 3
---

Configuration lives in `pyproject.toml`, and [export](04_export.md) reads it.

## Reading config

`get_config` walks up from the current directory until it finds one.

### Search order

Each directory from the current one to the root, taking the first hit.

## Writing config

`nbdev-create-config` writes a starting one.
"""
res = md2doc(sample)
res
```

```output
Doc(config: 405 chars, 0 warnings)
```

Parsed YAML preserves value types. In this example, `order` is the integer `3`.

```python
test_eq(res.meta['title'], 'config')
test_eq(res.meta['order'], 3)
res.meta
```

```output
{'title': 'config',
 'description': 'Configuring nbdev and bootstrapping notebook export',
 'order': 3}
```

Inline maths uses `\(…\)` by default, and display maths `$$…$$`. Pass `math='dollars'` to also read `$…$` as inline maths, as Pandoc and Quarto do. The default leaves a single `$` in prose, such as a price, as text.

With `dollars`, a price and maths in the same paragraph can merge into one maths span, because the price's `$` opens it. Write `\$` for a literal dollar sign there.

```python
area = 'The area is $r^2$.'
assert 'class="math inline"' not in md2doc(area).frag
test_eq(md2doc(area, math='dollars').frag.count('class="math inline"'), 1)
test_eq(md2doc(r'It costs \$5, and the area is $r^2$.', math='dollars').frag.count('class="math inline"'), 1)
```

`fm` retains the original frontmatter block, including its line breaks and spelling.

```python
assert sample.startswith(res.fm)
print(res.fm)
```

```output
---
title: config
description: Configuring nbdev and bootstrapping notebook export
order: 3
---
```

## Page title

[`Doc.header`](core.md#doc.header) renders a frontmatter title as an `<h1>` with an optional description. Without a frontmatter title, it returns an empty string. This lets a Markdown file supply its own heading instead.

---

### Doc.header

```python
def header():
```

*Return an HTML title block, or an empty string without a title*

```python
res.header
```

```output
'<header class="title-block"><h1>config</h1><p class="description">Configuring nbdev and bootstrapping notebook export</p></header>'
```

This document supplies `# Hand written` in its body. Octavo adds no separate title block.

```python
plain = md2doc("# Hand written\n\nNo frontmatter here.\n")
test_eq(plain.header, '')
test_eq(plain.fm, '')
plain.frag
```

```output
'<h1>Hand written</h1>\n<p>No frontmatter here.</p>\n'
```

## Rewriting links

Source links need to point to published files. For example, a link to `guide.ipynb` should point to `guide.html` in the website.

[`relink`](core.md#relink) applies a callable to each link and image address. Return a replacement string to change an address, or `None` to keep it. `octavo.site` supplies a callable that looks up page destinations.

---

### relink

```python
def relink(
    frag:str, # A document as MDHTML
    link, # Address -> replacement, or None to keep it
):
```

*Rewrite every link and image address in `frag` through `link`*

A dictionary’s `get` method works as the callable. Listed addresses are replaced; unlisted ones, such as the external URL below, remain unchanged.

```python
where = {'a.md':'a.html', 'i.png':'assets/i.png'}
relink('<p><a href="a.md">a</a> <a href="https://x.com">x</a> <img src="i.png"></p>', where.get)
```

```output
'<p><a href="a.html">a</a> <a href="https://x.com">x</a> <img src="assets/i.png"></p>'
```

## Rendering HTML

[`Doc.to_html`](core.md#doc.to_html) renders the article and its table of contents. It returns a [`Rendered`](core.md#rendered) object with separate `html`, `toc`, and `warnings` attributes. Separating the article from the table of contents lets the page layout place them in different columns.

Use `str(result)` or an f-string to insert the article HTML into a page. Use the named attributes to access the table of contents and warnings.

---

### Rendered

```python
def Rendered(
    html:str, # The document as HTML, title block included, contents removed
    toc:str, # Table of contents as a `nav` element; empty where the document has no headings
    warnings:list, # Whatever mdhtml reported while parsing and lowering
):
```

*Rendered article HTML, table of contents, and warnings*

---

### Doc.to_html

```python
def to_html(
    link:NoneType=None, header:bool=True, **kwargs
):
```

*Render HTML with an optional link-rewriting callable; pass `kwargs` to `mdhtml2html`*

HTML includes the frontmatter title block by default. Pass `header=False` when the caller supplies its own title or layout.

```python
rend = res.to_html()
assert rend.html.startswith('<header class="title-block">')
assert 'title: config' not in rend.html
assert not rend.html.startswith('<nav')
rend
bare = res.to_html(header=False)
assert not bare.html.startswith('<header class="title-block">')
bare
```

```output
Rendered(463 chars, 0 warnings)
```

[`Rendered`](core.md#rendered) supports string conversion, but not string slicing. Access `.html` to work with the article as a string, or `.toc` for its table of contents.

```python
test_eq(f'<main>{rend}</main>', f'<main>{rend.html}</main>')
with expect_fail(TypeError): rend[:10]
rend.toc
```

```output
'<nav class="toc"><ol><li><a href="#reading-config">Reading config</a><ol><li><a href="#search-order">Search order</a></li></ol></li><li><a href="#writing-config">Writing config</a></li></ol></nav>'
```

Link replacements apply to one export. A subsequent export without the callable still uses the original address.

```python
linked = res.to_html(link={'04_export.md':'export.html'}.get)
assert 'href="export.html"' in linked.html
assert 'href="04_export.md"' in res.to_html().html
linked
```

```output
Rendered(592 chars, 0 warnings)
```

## Trimming the table of contents

Limit the table of contents when a page has more subsection detail than readers need in the sidebar. [`trim_toc`](core.md#trim_toc) keeps the requested number of nested list levels. A depth of `1` shows top-level entries; `None` leaves the table unchanged.

---

### trim_toc

```python
def trim_toc(
    toc:str, depth:int=None
):
```

*Limit a table of contents to `depth` nested levels, or retain all when None*

Depth `1` retains “Reading config” but removes its “Search order” subsection. A depth larger than the existing nesting leaves all entries visible.

```python
flat = trim_toc(rend.toc, 1)
assert 'Reading config' in flat and 'Search order' not in flat
test_eq(trim_toc(rend.toc, 9), rend.toc)
test_eq(trim_toc(rend.toc), rend.toc)
test_eq(trim_toc('', 1), '')
flat
```

```output
'<nav class="toc"><ol><li><a href="#reading-config">Reading config</a></li><li><a href="#writing-config">Writing config</a></li></ol></nav>'
```

## Exporting GFM

[`Doc.to_gfm`](core.md#doc.to_gfm) converts Markdown extensions into GitHub-flavored Markdown and restores the original frontmatter. It prepares code folds from `body` on each export.

Ordinary Markdown retains its source spelling. Constructs requiring conversion are rewritten by `md2gfm`. An optional `link` callable replaces inline link and image destinations without rewriting the surrounding text.

---

### Doc.to_gfm

```python
def to_gfm(
    link:NoneType=None
):
```

*Export GFM with original frontmatter and optional link rewriting*

Here, the notebook link changes to `guide.md#part`. The frontmatter, code fence, math delimiters, and other text remain unchanged.

````python
source_md = r'''---
title: Source spelling
---

A_name links to [the guide](guide.ipynb#part) and [the web](https://example.com).

```python
items = ["a_b"]
```

\[x_1 + y_2\]
'''
link = {'guide.ipynb#part':'guide.md#part'}.get
expected_gfm = source_md.replace('guide.ipynb#part', 'guide.md#part')
actual_gfm = md2doc(source_md).to_gfm(link=link)
test_eq(actual_gfm, expected_gfm)
print(actual_gfm)
````

````output
---
title: Source spelling
---

A_name links to [the guide](guide.md#part) and [the web](https://example.com).

```python
items = ["a_b"]
```

\[x_1 + y_2\]
````

## Code folding

Quarto has a `code-fold` directive which octavo supports. `code-fold` puts the source in a disclosure panel, while saved output stays outside it. `code-summary` supplies the panel label, defaulting to “Code”.

Use `code-fold: show` to start expanded. Here is a “Setup” panel with its output below:

```python
source = Message('#| code-fold: show\n#| code-summary: Setup\n1 + 1', output=code_output('2'))
folded_doc = md2doc(dlg2md(Dialog(messages=[source])))
display(HTML(folded_doc.to_html().html))
```


```python
print(folded_doc.to_gfm())
```

````output
<details open>
<summary>Setup</summary>


```python
1 + 1
```

</details>

```output
2
```
````

Here's another example where the input is folded and the panel title falls back to the default:

````python
source = Message('#| code-fold\nx = "```"', output=code_output('visible result'))
folded_doc = md2doc(dlg2md(Dialog(messages=[source])))
display(HTML(folded_doc.to_html().html))
````


```python
print(folded_doc.to_gfm())
```

`````output
<details>
<summary>Code</summary>


````python
x = "```"
````

</details>

```output
visible result
```
`````
