From 6e1d08cdb191c93bac735b70b6925d92ead1309a Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 9 Oct 2024 09:26:40 -0400 Subject: [PATCH 1/6] feat: Add `sidebar_options` to control quarto sidebar options --- quartodoc/autosummary.py | 16 ++++++++--- .../tests/__snapshots__/test_builder.ambr | 21 +++++++++++++++ quartodoc/tests/test_builder.py | 27 +++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/quartodoc/autosummary.py b/quartodoc/autosummary.py index c6bea4e3..5fe921ca 100644 --- a/quartodoc/autosummary.py +++ b/quartodoc/autosummary.py @@ -427,6 +427,10 @@ class Builder: 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). + sidebar_options: + Additional options to be included in the sidebar definition. See + [Side navigation](https://quarto.org/docs/websites/website-navigation.html#side-navigation) + in Quarto's documentation for more information. css: The output path for the default css styles. rewrite_all_pages: @@ -488,6 +492,7 @@ def __init__( renderer: "dict | Renderer | str" = "markdown", out_index: str = None, sidebar: "str | None" = None, + sidebar_options: "dict | None" = None, css: "str | None" = None, rewrite_all_pages=False, source_dir: "str | None" = None, @@ -505,6 +510,7 @@ def __init__( self.dir = dir self.title = title self.sidebar = sidebar + self.sidebar_options = sidebar_options self.css = css self.parser = parser @@ -589,7 +595,7 @@ def build(self, filter: str = "*"): if self.sidebar: _log.info(f"Writing sidebar yaml to {self.sidebar}") - self.write_sidebar(blueprint) + self.write_sidebar(blueprint, self.sidebar_options) # css ---- @@ -659,7 +665,7 @@ 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 = {} @@ -686,7 +692,11 @@ 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` + sidebar = {**(self.sidebar_options or {})} + sidebar.update({"id": self.dir, "contents": contents}) + + entries = [sidebar, {"id": "dummy-sidebar"}] return {"website": {"sidebar": entries}} def write_sidebar(self, blueprint: layout.Layout): diff --git a/quartodoc/tests/__snapshots__/test_builder.ambr b/quartodoc/tests/__snapshots__/test_builder.ambr index 928ed84d..cf3882b6 100644 --- a/quartodoc/tests/__snapshots__/test_builder.ambr +++ b/quartodoc/tests/__snapshots__/test_builder.ambr @@ -18,3 +18,24 @@ ''' # --- +# name: test_builder_generate_sidebar_options + ''' + website: + sidebar: + - contents: + - reference/index.qmd + - contents: + - reference/a_func.qmd + section: first section + - contents: + - contents: + - reference/a_attr.qmd + section: a subsection + section: second section + id: reference + search: true + style: docked + - id: dummy-sidebar + + ''' +# --- diff --git a/quartodoc/tests/test_builder.py b/quartodoc/tests/test_builder.py index 932bc975..ea1fedd1 100644 --- a/quartodoc/tests/test_builder.py +++ b/quartodoc/tests/test_builder.py @@ -82,3 +82,30 @@ 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_options: + style: docked + search: true + 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) + bp = blueprint(builder.layout) + d_sidebar = builder._generate_sidebar(bp) + + assert yaml.dump(d_sidebar) == snapshot From 9e677a2eb0892dfb637f6a2fe66d5368423276f1 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 9 Oct 2024 09:36:44 -0400 Subject: [PATCH 2/6] docs(sidebar): Fix example sidebar yaml --- docs/get-started/sidebar.qmd | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/get-started/sidebar.qmd b/docs/get-started/sidebar.qmd index 62da0b24..f64a4a85 100644 --- a/docs/get-started/sidebar.qmd +++ b/docs/get-started/sidebar.qmd @@ -26,7 +26,6 @@ it's included with the configuration in `_quarto.yml`. Here is what the sidebar for the [quartodoc reference page](/api) looks like:
-

-{{{< include /api/_sidebar.yml >}}}
+
{{< include /api/_sidebar.yml >}}
 
From d5a611ead308b9dd3ab0f61139ed7e2b5db382f0 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 9 Oct 2024 09:40:52 -0400 Subject: [PATCH 3/6] docs: Document `sidebar_options` --- docs/get-started/sidebar.qmd | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/get-started/sidebar.qmd b/docs/get-started/sidebar.qmd index f64a4a85..8376cf71 100644 --- a/docs/get-started/sidebar.qmd +++ b/docs/get-started/sidebar.qmd @@ -19,10 +19,21 @@ 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`. +Additional [sidebar options are available in Quarto](https://quarto.org/docs/websites/website-navigation.html#side-navigation) and can be passed from quartodoc to Quarto via `sidebar_options`: + +```yaml +quartodoc: + sidebar: "_sidebar.yml" + sidebar_options: + style: docked + search: true + collapse-level: 2 +``` + Here is what the sidebar for the [quartodoc reference page](/api) looks like:
From e8a624f15fd02811a3ec08661f5e0cf8c58599cb Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 16 Oct 2024 08:34:27 -0400 Subject: [PATCH 4/6] feat(sidebar): Allow dictionary of options, passed directly to quarto * Use `file` to specify the sidebar file * Use "{{ contents }}" as a placeholder for quartodoc's contents anywhere in `quartodoc.sidebar.contents`. --- docs/get-started/sidebar.qmd | 16 ++++- quartodoc/autosummary.py | 68 ++++++++++++++----- .../tests/__snapshots__/test_builder.ambr | 20 ++++-- quartodoc/tests/test_builder.py | 21 +++++- 4 files changed, 98 insertions(+), 27 deletions(-) diff --git a/docs/get-started/sidebar.qmd b/docs/get-started/sidebar.qmd index 8376cf71..1adfe6e7 100644 --- a/docs/get-started/sidebar.qmd +++ b/docs/get-started/sidebar.qmd @@ -23,15 +23,25 @@ with a [Quarto sidebar configuration](https://quarto.org/docs/websites/website-n 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`. -Additional [sidebar options are available in Quarto](https://quarto.org/docs/websites/website-navigation.html#side-navigation) and can be passed from quartodoc to Quarto via `sidebar_options`: +`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: "_sidebar.yml" - sidebar_options: + 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 ``` Here is what the sidebar for the [quartodoc reference page](/api) looks like: diff --git a/quartodoc/autosummary.py b/quartodoc/autosummary.py index 5fe921ca..101d0150 100644 --- a/quartodoc/autosummary.py +++ b/quartodoc/autosummary.py @@ -426,11 +426,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). - sidebar_options: - Additional options to be included in the sidebar definition. See - [Side navigation](https://quarto.org/docs/websites/website-navigation.html#side-navigation) - in Quarto's documentation for more information. + 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: @@ -491,8 +491,7 @@ def __init__( title: str = "Function reference", renderer: "dict | Renderer | str" = "markdown", out_index: str = None, - sidebar: "str | None" = None, - sidebar_options: "dict | None" = None, + sidebar: "str | dict[str, Any] | None" = None, css: "str | None" = None, rewrite_all_pages=False, source_dir: "str | None" = None, @@ -509,8 +508,13 @@ def __init__( self.version = None self.dir = dir self.title = title - self.sidebar = sidebar - self.sidebar_options = sidebar_options + + 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 @@ -594,8 +598,8 @@ def build(self, filter: str = "*"): # sidebar ---- if self.sidebar: - _log.info(f"Writing sidebar yaml to {self.sidebar}") - self.write_sidebar(blueprint, self.sidebar_options) + _log.info(f"Writing sidebar yaml to {self.sidebar['file']}") + self.write_sidebar(blueprint) # css ---- @@ -665,7 +669,9 @@ def create_inventory(self, items): # sidebar ---- - def _generate_sidebar(self, blueprint: layout.Layout, options: "dict | None" = None): + 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 = {} @@ -693,17 +699,47 @@ def _generate_sidebar(self, blueprint: layout.Layout, options: "dict | None" = N contents.append(crnt_entry) # Create sidebar with user options, ensuring we control `id` and `contents` - sidebar = {**(self.sidebar_options or {})} - sidebar.update({"id": self.dir, "contents": 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 - entries = [sidebar, {"id": "dummy-sidebar"}] + if "contents" not in sidebar: + sidebar["contents"] = contents + else: + if not isinstance(sidebar["contents"], list): + raise TypeError("`sidebar.contents` must be a list") + + def splice_contents_recursive(sidebar, contents): + """Splice quartodoc contents into first element exactly '{{ contents }}'""" + if isinstance(sidebar, dict): + for value in sidebar.values(): + if splice_contents_recursive(value, contents): + return True + elif isinstance(sidebar, list): + for i, item in enumerate(sidebar): + if item == "{{ contents }}": + sidebar[i : i + 1] = contents # noqa: E203 + return True + elif splice_contents_recursive(item, contents): + return True + return False + + if not splice_contents_recursive(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.""" diff --git a/quartodoc/tests/__snapshots__/test_builder.ambr b/quartodoc/tests/__snapshots__/test_builder.ambr index cf3882b6..0925ad91 100644 --- a/quartodoc/tests/__snapshots__/test_builder.ambr +++ b/quartodoc/tests/__snapshots__/test_builder.ambr @@ -23,15 +23,21 @@ website: sidebar: - contents: - - reference/index.qmd - - contents: - - reference/a_func.qmd - section: first section + - href: introduction.qmd + text: Introduction - contents: + - reference/index.qmd - contents: - - reference/a_attr.qmd - section: a subsection - section: second section + - 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 diff --git a/quartodoc/tests/test_builder.py b/quartodoc/tests/test_builder.py index ea1fedd1..78f92ee1 100644 --- a/quartodoc/tests/test_builder.py +++ b/quartodoc/tests/test_builder.py @@ -83,14 +83,23 @@ def test_builder_generate_sidebar(tmp_path, snapshot): 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_options: + 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 @@ -105,7 +114,17 @@ def test_builder_generate_sidebar_options(tmp_path, snapshot): ) 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 From b5d3c6987244a0e0241e50e83bc940f05f1afc44 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 18 Oct 2024 17:14:24 -0400 Subject: [PATCH 5/6] refactor: Pull `_insert_contents()` into top-level function --- quartodoc/autosummary.py | 49 +++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/quartodoc/autosummary.py b/quartodoc/autosummary.py index 101d0150..b3169bdb 100644 --- a/quartodoc/autosummary.py +++ b/quartodoc/autosummary.py @@ -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 ===================================================================== @@ -713,22 +745,7 @@ def _generate_sidebar( if not isinstance(sidebar["contents"], list): raise TypeError("`sidebar.contents` must be a list") - def splice_contents_recursive(sidebar, contents): - """Splice quartodoc contents into first element exactly '{{ contents }}'""" - if isinstance(sidebar, dict): - for value in sidebar.values(): - if splice_contents_recursive(value, contents): - return True - elif isinstance(sidebar, list): - for i, item in enumerate(sidebar): - if item == "{{ contents }}": - sidebar[i : i + 1] = contents # noqa: E203 - return True - elif splice_contents_recursive(item, contents): - return True - return False - - if not splice_contents_recursive(sidebar["contents"], contents): + if not _insert_contents(sidebar["contents"], contents): # otherwise append contents to existing list sidebar["contents"].extend(contents) From 07d4a0f8b5511891e90ce6048beb1dba6de121d2 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Mon, 4 Nov 2024 11:35:11 -0500 Subject: [PATCH 6/6] docs: create section for sidebar config --- docs/get-started/sidebar.qmd | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/get-started/sidebar.qmd b/docs/get-started/sidebar.qmd index 1adfe6e7..0e1af47f 100644 --- a/docs/get-started/sidebar.qmd +++ b/docs/get-started/sidebar.qmd @@ -23,6 +23,15 @@ with a [Quarto sidebar configuration](https://quarto.org/docs/websites/website-n 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: + +
+
{{< include /api/_sidebar.yml >}}
+
+
+ +## 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. @@ -43,10 +52,3 @@ quartodoc: - text: "Basics" href: basics-summary.qmd ``` - -Here is what the sidebar for the [quartodoc reference page](/api) looks like: - -
-
{{< include /api/_sidebar.yml >}}
-
-