---
description: Convert Quarto sources and settings for Octavo
output-file: migrate.html
title: migrate

---



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

[`migrate_quarto`](migrate.md#migrate_quarto) converts Quarto sources in place. Commit or back up the source tree before calling it. It renames QMD documents, updates links and metadata, and reports unsupported settings. It preserves an existing `octavo.yml` and leaves the original Quarto configuration for review.

The conversion does not port themes, CSS, layouts, shortcodes, or execution settings. Review the reported differences and compare the rendered site with a saved Quarto build before publishing.

The caller supplies a configuration callback and can pass extra accepted page keys. Octavo does not read nbdev project configuration.

## Source conversion

Renaming a document also requires updating references to it. A page beside `guide.qmd` refers to it differently from a page in a subdirectory: `guide.qmd` versus `../guide.qmd`. Migration calculates replacement paths from the documentation root and from each source file’s directory.

The current implementation performs text replacement rather than parsing links. Matching text outside link destinations can also change.

The Markdown copy of each page also moves. Quarto published `page.html.md` beside `page.html`; Octavo publishes `page.md`. Hand-written indexes such as `llms.txt` often link these copies, so migration rewrites `.html.md` targets to `.md` in Markdown sources and `.txt` files.

---

### repl_html_md_links

```python
def repl_html_md_links(
    txt
):
```

*Rewrite Quarto `.html.md` Markdown-copy targets to Octavo's `.md`*

---

### repl_qmd_links

```python
def repl_qmd_links(
    txt, moves, src, root
):
```

*Rewrite relative and root-relative `.qmd` link targets to `.md`*

Quarto lets authors mark a block of content for a particular output format—for example, HTML for the website or Markdown for a README. Migration replaces Quarto’s block markers with mdhtml's raw-format fences.

For example, this Quarto block:

````markdown
::: {.content-visible when-format="html"}
<p>Only include this in HTML output.</p>
:::
````

becomes:

````markdown
```{=html}
<p>Only include this in HTML output.</p>
```
````

The function changes the surrounding markers, not the content inside them. Quarto’s `markdown`, `commonmark`, and `md` format names all become `md`. If an HTML block has a `style` attribute, the function preserves it by wrapping the content in a `<div>` with that style.

This conversion changes block markers, not the language of the body. Markdown inside an HTML-targeted block is not converted to HTML. The current helper also does not preserve `unless-format` conditions. Review these cases before relying on the converted output.

---

### repl_content_visible

```python
def repl_content_visible(
    s
):
```

*Convert Quarto `::: {.content-visible when-format=...}` to ````{=md}```` / ````{=html}````*

Octavo reads Quarto's frontmatter keys as they are, such as `toc-depth`, `page-layout` and `categories`, so migration leaves them alone. It changes one thing: Octavo shows a page's `description` under its title, where Quarto shows `subtitle`, so a `subtitle`, or a legacy `summary`, becomes the description. A page that already has a description keeps it, and its `subtitle` is reported as unsupported.

---

### repl_frontmatter_keys

```python
def repl_frontmatter_keys(
    s
):
```

*Move Quarto's `subtitle`, or a legacy `summary`, to the `description` Octavo shows, unless the page has one*

[`report_page_keys`](migrate.md#report_page_keys) reports frontmatter settings outside Octavo's `PAGE_KEYS`. Integrations pass `extra_keys` for settings they consume themselves. [`source_frontmatter`](migrate.md#source_frontmatter) reads the metadata from a document or notebook without executing its code.

Callers with another source format can pass parsed `metadata` to [`report_page_keys`](migrate.md#report_page_keys); Octavo then uses the path only to label the diagnostic.

---

### report_page_keys

```python
def report_page_keys(
    p, # Source path used in diagnostics
    extra_keys:tuple=(), # Metadata keys consumed by the calling integration
    metadata:NoneType=None, # Parsed metadata; otherwise read from the document
):
```

*Print frontmatter keys outside Octavo's settings and `extra_keys`*

---

### source_frontmatter

```python
def source_frontmatter(
    p
):
```

*Frontmatter of a document or notebook*

Documentation conversion must not rewrite executable notebook code or discard saved results. [`migrate_nb_cells`](migrate.md#migrate_nb_cells) applies the text conversions only to Markdown cells. Code cells and their saved outputs are retained.

[`migrate_nb_cells`](migrate.md#migrate_nb_cells) and [`migrate_md_file`](migrate.md#migrate_md_file) write a file only when a conversion changes its content.

---

### migrate_md_file

```python
def migrate_md_file(
    path, procs
):
```

*Apply `procs` to a Markdown, QMD or text file in-place*

---

### migrate_nb_cells

```python
def migrate_nb_cells(
    path, procs
):
```

*Apply `procs` to all markdown cells in a notebook in-place*

## Navigation and configuration

Dropdown entries keep their labels and point to the rendered HTML pages.

```python
test_eq(_nav_item(dict(text='Help', menu=[dict(text='Guide', href='guide.qmd')])),
    dict(text='Help', children=[dict(text='Guide', href='guide.html')]))
```

Icon names belong to the renderer. Unsupported icons are reported rather than emitted as broken tool links.

Quarto and Octavo use different settings for navigation bars. Migration converts text links and dropdowns into Octavo navigation links, and icon links into tools. Links to documentation sources are changed to HTML destinations so visitors reach rendered pages. Explicit accessibility labels are preserved. Icons not bundled with Octavo are reported and omitted for manual replacement.

[`quarto_sidebar`](migrate.md#quarto_sidebar) converts parsed Quarto sidebar settings into `sidebar-links`. Pages become links and sections become groups. Entries without text take the page's title at build time. The caller chooses which configuration file supplies the sidebar and passes its path for diagnostic messages. Quarto's `auto` entries and glob patterns are reported and omitted.

---

### quarto_sidebar

```python
def quarto_sidebar(
    sb, path
):
```

*Convert Quarto sidebar settings; use `path` to identify reported omissions*

[`read_quarto_config`](migrate.md#read_quarto_config) reads a YAML mapping and rejects scalars and lists. Missing or empty files return empty settings.

[`quarto_config`](migrate.md#quarto_config) copies supported website and HTML settings into Octavo's flat configuration and converts the navbar. It accepts an optional settings overlay, such as nbdev's generated configuration. The caller handles output paths and adds any result from [`quarto_sidebar`](migrate.md#quarto_sidebar).

This is not a complete translation of Quarto configuration. Unsupported settings are reported for manual review. In particular, the conversion leaves themes and site CSS to the caller.

---

### quarto_config

```python
def quarto_config(
    qy, root, overrides:NoneType=None
):
```

*Convert parsed Quarto settings and optional overlay, reporting omissions under `root`*

---

### read_quarto_config

```python
def read_quarto_config(
    path
):
```

*Read a Quarto YAML mapping; missing or empty files give empty settings*

## Applying the migration

Switching builders requires source changes as well as new configuration: renamed documents need updated links, and Quarto-specific markup needs conversion. Migration checks rename conflicts and configuration before editing, preserves an existing `octavo.yml`, and retains `_quarto.yml` for comparison. Changes are made in place without automatic rollback, so commit or back up the project first.

---

### migrate_quarto

```python
def migrate_quarto(
    path, # Source directory
    config, # Callable taking the source directory and returning Octavo settings
    extra_keys:tuple=(), # Page metadata keys consumed by the calling integration
    extra_moves:NoneType=None, # Additional source-to-destination Path pairs; supplied relative paths include `path`
):
```

*Convert sources in place, create missing configuration, and return the rename mapping*

The caller supplies a configuration function. It runs after rename-conflict checks and before source changes, and only when `octavo.yml` is absent. This example uses a source-root Quarto project. nbdev supplies its own function to combine `nbdev.yml`, a custom sidebar, and project-relative output paths.

`extra_moves` maps source Paths to destination Paths, using filesystem paths just like the returned rename map. These pairs join document renames for conflict checks, link updates, and renaming. Extra files outside the document formats are not read or converted.

```python
def example_config(root):
    qy = read_quarto_config(root/'_quarto.yml')
    cfg = quarto_config(qy, root)
    if out := nested_idx(qy, 'project', 'output-dir'): cfg['output'] = out
    if sb := nested_idx(qy, 'website', 'sidebar'): cfg |= quarto_sidebar(sb, root/'_quarto.yml')
    return cfg
```

The notebook below contains a saved result and code that must not execute during migration.

```python
migration_tmp = tempfile.TemporaryDirectory()
root = Path(migration_tmp.name)
(root/'_quarto.yml').write_text('website:\n  title: Demo\n')
(root/'guide.qmd').write_text('---\nsubtitle: A guide\n---\n\n[Notebook](index.ipynb)')
code = mk_cell('raise RuntimeError("do not run")')
code.outputs = [dict(output_type='stream', name='stdout', text='Saved result\n')]
book = new_nb([mk_cell('[Guide](guide.qmd)', 'markdown'), code])
write_nb(book, root/'index.ipynb')
```

Migration renames the guide and moves its subtitle into the description.

```python
moves = migrate_quarto(root, example_config)
test_eq(moves, {root/'guide.qmd': root/'guide.md'})
test_eq(source_frontmatter(root/'guide.md')['description'], 'A guide')
```

Notebook Markdown links change, while executable code and saved outputs remain intact.

```python
migrated = read_nb(root/'index.ipynb')
test_eq(migrated.cells[0].source, '[Guide](guide.md)')
test_eq(migrated.cells[1].source, code.source)
test_eq(migrated.cells[1].outputs, code.outputs)
```

A second migration has no files to rename and preserves the existing Octavo configuration.

```python
before = (root/'octavo.yml').read_text()
test_eq(migrate_quarto(root, example_config), {})
test_eq((root/'octavo.yml').read_text(), before)
test_eq(yaml.safe_load(before)['title'], 'Demo')
```

```python
migration_tmp.cleanup()
```

Callers can supply additional renames. Their contents are not converted, but document links follow the new filenames. Existing destinations are rejected before source changes.

```python
with tempfile.TemporaryDirectory() as d:
    root = Path(d)
    (root/'octavo.yml').write_text('{}')
    old,new = root/'old.bin',root/'new.bin'
    old.write_bytes(b'opaque payload')
    new.touch()
    (root/'guide.qmd').write_text('[Asset](old.bin)')
    with expect_fail(FileExistsError): migrate_quarto(root, example_config, extra_moves={old:new})
    test_eq((root/'guide.qmd').read_text(), '[Asset](old.bin)')
    new.unlink()
    moves = migrate_quarto(root, example_config, extra_moves={old:new})
    test_eq(moves[old], new)
    test_eq(new.read_bytes(), b'opaque payload')
    test_eq((root/'guide.md').read_text(), '[Asset](new.bin)')
```

A destination collision must stop migration before an existing file can be overwritten.

```python
with tempfile.TemporaryDirectory() as d:
    p = Path(d)
    (p/'page.qmd').touch()
    (p/'page.md').touch()
    with expect_fail(FileExistsError, contains='page.md'): migrate_quarto(p, example_config)
```

Malformed YAML must fail before any source rename. This differs from a valid YAML value of the wrong type.

```python
with tempfile.TemporaryDirectory() as d:
    p = Path(d)
    (p/'guide.qmd').write_text('# Guide')
    (p/'_quarto.yml').write_text('website: [invalid')
    with expect_fail(yaml.YAMLError): migrate_quarto(p, example_config)
    assert (p/'guide.qmd').exists() and not (p/'guide.md').exists()
```

Valid YAML is not always valid configuration. Lists, booleans, and numbers must fail before source changes, whether they came from Quarto or an existing Octavo configuration.

```python
with tempfile.TemporaryDirectory() as d:
    nbs = Path(d)
    (nbs/'guide.qmd').write_text('# Guide')
    (nbs/'_quarto.yml').write_text('website:\n  title: Demo\n')
    for invalid in ('[]', 'false', '0'):
        (nbs/'octavo.yml').write_text(invalid)
        with expect_fail(ValueError, contains='mapping'): migrate_quarto(nbs, example_config)
        assert (nbs/'guide.qmd').exists() and (nbs/'_quarto.yml').exists()
    (nbs/'octavo.yml').unlink()
    for invalid in ('[]', 'false', '0'):
        (nbs/'_quarto.yml').write_text(invalid)
        with expect_fail(ValueError, contains='mapping'): migrate_quarto(nbs, example_config)
        assert (nbs/'guide.qmd').exists() and not (nbs/'guide.md').exists()
```
