Skip to content

Commit

Permalink
Merge pull request #370 from gadenbuie/feat/sidebar-options
Browse files Browse the repository at this point in the history
feat: Allow addings additional options to `sidebar`
  • Loading branch information
machow authored Nov 4, 2024
2 parents 8d7ade2 + 07d4a0f commit 334bdc1
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 11 deletions.
30 changes: 26 additions & 4 deletions docs/get-started/sidebar.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,36 @@ quartodoc:
```

Note that running `python -m quartodoc build` will now produce a file called `_sidebar.yml`,
with a [quarto sidebar configuration](https://quarto.org/docs/websites/website-navigation.html#side-navigation).
The quarto [`metadata-files` option](https://quarto.org/docs/projects/quarto-projects.html#metadata-includes) ensures
with a [Quarto sidebar configuration](https://quarto.org/docs/websites/website-navigation.html#side-navigation).
The Quarto [`metadata-files` option](https://quarto.org/docs/projects/quarto-projects.html#metadata-includes) ensures
it's included with the configuration in `_quarto.yml`.

Here is what the sidebar for the [quartodoc reference page](/api) looks like:

<div class="sourceCode">
<pre class="sourceCode yaml"><code class="sourceCode yaml">
{{{< include /api/_sidebar.yml >}}}
<pre class="sourceCode yaml"><code class="sourceCode yaml">{{< include /api/_sidebar.yml >}}
</code></pre>
</div>

## Customizing the sidebar

`sidebar` can also accept additional [sidebar options from the choices available in Quarto](https://quarto.org/docs/websites/website-navigation.html#side-navigation). These options are passed directly to Quarto, and can be used to customize the sidebar's appearance and behavior, or include additional content.

When using a dictionary for `sidebar`, use `file` to specify the sidebar file (defaults to `_quartodoc-sidebar.yml` if not provided). You can also provide additional content in the sidebar. Tell quartodoc where to include your package documentation in the sidebar with the `"{{ contents }}"` placeholder.

```yaml
quartodoc:
sidebar:
file: "_sidebar.yml"
style: docked
search: true
collapse-level: 2
contents:
- text: "Introduction"
href: introduction.qmd
- section: "Reference"
contents:
- "{{ contents }}"
- text: "Basics"
href: basics-summary.qmd
```
77 changes: 70 additions & 7 deletions quartodoc/autosummary.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,38 @@ def _is_valueless(obj: dc.Object):
return False


def _insert_contents(
x: dict | list,
contents: list,
sentinel: str = "{{ contents }}",
):
"""Splice `contents` into a list.
Splices `contents` into the first element in `x` that exactly matches
`sentinel`. This functions recurses into dictionaries, but because
`contents` is a list, we only look within lists in `x` for the sentinel
value where `contents` should be inserted.
Returns
-------
:
Returns `True` if `contents` was inserted into `x`, otherwise returns
`False`. Note that `x` is modified in place.
"""
if isinstance(x, dict):
for value in x.values():
if _insert_contents(value, contents):
return True
elif isinstance(x, list):
for i, item in enumerate(x):
if item == sentinel:
x[i : i + 1] = contents # noqa: E203
return True
elif _insert_contents(item, contents):
return True
return False


# pkgdown =====================================================================


Expand Down Expand Up @@ -426,7 +458,11 @@ class Builder:
out_index:
The output path of the index file, used to list all API functions.
sidebar:
The output path for a sidebar yaml config (by default no config generated).
The output path for a sidebar yaml config (by default no config
generated). Alternatively, can be a dictionary of [Quarto sidebar
options](https://quarto.org/docs/websites/website-navigation.html#side-navigation)
with an additional `file` key containing the output path for the sidebar
YAML config file (by default `_quartodoc-sidebar.yml` if not specified).
css:
The output path for the default css styles.
rewrite_all_pages:
Expand Down Expand Up @@ -487,7 +523,7 @@ def __init__(
title: str = "Function reference",
renderer: "dict | Renderer | str" = "markdown",
out_index: str = None,
sidebar: "str | None" = None,
sidebar: "str | dict[str, Any] | None" = None,
css: "str | None" = None,
rewrite_all_pages=False,
source_dir: "str | None" = None,
Expand All @@ -504,7 +540,13 @@ def __init__(
self.version = None
self.dir = dir
self.title = title
self.sidebar = sidebar

if isinstance(sidebar, str):
sidebar = {"file": sidebar}
elif isinstance(sidebar, dict) and "file" not in sidebar:
sidebar["file"] = "_quartodoc-sidebar.yml"
self.sidebar: "dict[str, Any] | None" = sidebar

self.css = css
self.parser = parser

Expand Down Expand Up @@ -588,7 +630,7 @@ def build(self, filter: str = "*"):
# sidebar ----

if self.sidebar:
_log.info(f"Writing sidebar yaml to {self.sidebar}")
_log.info(f"Writing sidebar yaml to {self.sidebar['file']}")
self.write_sidebar(blueprint)

# css ----
Expand Down Expand Up @@ -659,7 +701,9 @@ def create_inventory(self, items):

# sidebar ----

def _generate_sidebar(self, blueprint: layout.Layout):
def _generate_sidebar(
self, blueprint: layout.Layout, options: "dict | None" = None
):
contents = [f"{self.dir}/index{self.out_page_suffix}"]
in_subsection = False
crnt_entry = {}
Expand All @@ -686,14 +730,33 @@ def _generate_sidebar(self, blueprint: layout.Layout):
if crnt_entry:
contents.append(crnt_entry)

entries = [{"id": self.dir, "contents": contents}, {"id": "dummy-sidebar"}]
# Create sidebar with user options, ensuring we control `id` and `contents`
if self.sidebar is None:
sidebar = {}
else:
sidebar = {k: v for k, v in self.sidebar.items() if k != "file"}

if "id" not in sidebar:
sidebar["id"] = self.dir

if "contents" not in sidebar:
sidebar["contents"] = contents
else:
if not isinstance(sidebar["contents"], list):
raise TypeError("`sidebar.contents` must be a list")

if not _insert_contents(sidebar["contents"], contents):
# otherwise append contents to existing list
sidebar["contents"].extend(contents)

entries = [sidebar, {"id": "dummy-sidebar"}]
return {"website": {"sidebar": entries}}

def write_sidebar(self, blueprint: layout.Layout):
"""Write a yaml config file for API sidebar."""

d_sidebar = self._generate_sidebar(blueprint)
yaml.dump(d_sidebar, open(self.sidebar, "w"))
yaml.dump(d_sidebar, open(self.sidebar["file"], "w"))

def write_css(self):
"""Write default css styles to a file."""
Expand Down
27 changes: 27 additions & 0 deletions quartodoc/tests/__snapshots__/test_builder.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,30 @@

'''
# ---
# name: test_builder_generate_sidebar_options
'''
website:
sidebar:
- contents:
- href: introduction.qmd
text: Introduction
- contents:
- reference/index.qmd
- contents:
- reference/a_func.qmd
section: first section
- contents:
- contents:
- reference/a_attr.qmd
section: a subsection
section: second section
section: Reference
- href: basics-summary.qmd
text: Basics
id: reference
search: true
style: docked
- id: dummy-sidebar

'''
# ---
46 changes: 46 additions & 0 deletions quartodoc/tests/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,49 @@ def test_builder_generate_sidebar(tmp_path, snapshot):
d_sidebar = builder._generate_sidebar(bp)

assert yaml.dump(d_sidebar) == snapshot


def test_builder_generate_sidebar_options(tmp_path, snapshot):
cfg = yaml.safe_load(
"""
quartodoc:
package: quartodoc.tests.example
sidebar:
style: docked
search: true
contents:
- text: "Introduction"
href: introduction.qmd
- section: "Reference"
contents:
- "{{ contents }}"
- text: "Basics"
href: basics-summary.qmd
sections:
- title: first section
desc: some description
contents: [a_func]
- title: second section
desc: title description
- subtitle: a subsection
desc: subtitle description
contents:
- a_attr
"""
)

builder = Builder.from_quarto_config(cfg)
assert builder.sidebar["file"] == "_quartodoc-sidebar.yml" # default value

bp = blueprint(builder.layout)

d_sidebar = builder._generate_sidebar(bp)
assert "website" in d_sidebar
assert "sidebar" in d_sidebar["website"]

qd_sidebar = d_sidebar["website"]["sidebar"][0]
assert "file" not in qd_sidebar
assert qd_sidebar["style"] == "docked"
assert qd_sidebar["search"]

assert yaml.dump(d_sidebar) == snapshot

0 comments on commit 334bdc1

Please sign in to comment.