Skip to content

Commit

Permalink
Add Multi-level navigation (just-the-docs#462)
Browse files Browse the repository at this point in the history
  • Loading branch information
simonebortolin committed Dec 24, 2022
1 parent b6d725d commit 4b8d5df
Show file tree
Hide file tree
Showing 19 changed files with 569 additions and 54 deletions.
5 changes: 5 additions & 0 deletions _config_dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# For local development and testing, use the option
# --config _config.yml,_config_dev.yml
# See https://stackoverflow.com/a/27400343

url: http://localhost:4000/
22 changes: 22 additions & 0 deletions _include_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Configuration addition to include regression tests
# Use jekyll option --config _config.yml,_include_tests.yml

collections:
tests:
permalink: "/:collection/:path:output_ext"
output: true

just_the_docs:
collections:
tests:
name: Tests
nav_exclude: false
search_exclude: true

defaults:
-
scope:
path: "" # an empty string here means all files in the project
type: "tests"
values:
layout: "default"
41 changes: 41 additions & 0 deletions _includes/nav/children.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{%- comment -%}
{%- include nav/children.html
node = node
path = string
in_section = value_or_nil -%}
assigns to `nav_children` an unsorted array containing children of `node`,
including `child` when all its specified disambiguation fields match:
- `child.grand_parent == node.parent`,
- `child.in_section == in_section`, and
- `path contains child.ancestor`.
{%- endcomment -%}

{%- assign nav_candidates = nav_parenthood
| where: "name", include.node.title | map: "items" | first -%}
{%- assign nav_children = "" | split: "X" -%}

{%- for child in nav_candidates -%}
{%- assign nav_child_ok = true -%}

{%- if child.grand_parent and child.grand_parent != include.node.parent -%}
{%- assign nav_child_ok = false -%}
{%- endif -%}

{%- if child.in_section and child.in_section != include.in_section -%}
{%- assign nav_child_ok = false -%}
{%- endif -%}

{%- capture nav_newline %}
{% endcapture -%}

{%- if child.ancestor -%}
{%- assign nav_child_ancestor_title = nav_newline | append: child.ancestor | append: nav_newline -%}
{%- unless include.path contains nav_child_ancestor_title -%}
{%- assign nav_child_ok = false -%}
{%- endunless -%}
{%- endif -%}

{%- if nav_child_ok -%}
{%- assign nav_children = nav_children | push: child -%}
{%- endif -%}
{%- endfor -%}
77 changes: 77 additions & 0 deletions _includes/nav/collection.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{%- comment -%}
{%- include nav/collection.html
pages = array
key = name_or_nil -%}
outputs the main navigation links for `pages`. It also assigns arrays of pages
to variables inspected by `nav/crumbs` and `nav/toc`.

When `key` is `nil`, `pages` is `site.html_pages`; otherwise `key` is the name
of a collection, and `pages` are the pages of that collection. (Pages without
a `title` are automatically excluded in the former case, but Jekyll provides
default titles for pages in collections, so their untitled pages are not
automatically excluded.)

`collection` first groups `pages` by their `parent` fields, creating an
unsorted array of hashes, assigned to `nav_parenthood`.

When `pages` contains the current `page`, `collection` calls `nav/page` to
search for the navigation path to `page`, then:
- assigns to `nav_page_path` the string of page titles leading to `page`,
- assigns to `nav_page_ancestors` the array of ancestors of `page`, and
- assigns to `nav_page_children` the array of children of `page`.

When pages are stored in folders whose nesting corresponds to the parent
relation, only nodes whose folder path is a prefix of `page.path` need to be
searched. For larger sites, using folder paths to direct searching for `page`
can exponentially reduce the build time. To avoid dependence on assumptions
about the folders used to store pages, however, exhaustive search is needed
as a fall-back when directed search fails.

Finally, `collection` outputs the main navigation links for `pages`.

The presence of navigation expanders requires the navigation links for the
entire site to be output on every page. To reduce build time, the navigation
links for inactive top-level nodes are cached. (It would be possible to cache
also the navigation links for inactive lower-level nodes, but that does not
reduce the build time signficantly.)
{%- endcomment -%}

{%- assign nav_parenthood = include.pages
| where_exp: "item", "item.title != nil" | group_by: "parent" -%}

{%- assign nav_top_nodes = nav_parenthood
| where_exp: "item", "item.name == ''" | map: "items" | first -%}

{%- assign nav_page_path = nil -%}

{%- if include.key == nil or include.key == page.collection -%}

{%- assign nav_page_dir = page.path | split: "/" | pop | join: "/" -%}
{%- assign nav_ancestors = "" | split: "X" -%}

{%- capture nav_newline %}
{% endcapture -%}

{%- include nav/page.html
direct = true
nodes = nav_top_nodes
ancestors = nav_ancestors
path = nav_newline
in_section = nil -%}

{%- unless nav_page_path -%}
{%- include nav/page.html
direct = false
nodes = nav_top_nodes
ancestors = nav_ancestors
path = nav_newline
in_section = nil -%}
{%- endunless -%}

{%- endif -%}

{%- include nav/links.html
nodes = nav_top_nodes
path = nav_newline
in_section = nil
pages = include.pages -%}
19 changes: 19 additions & 0 deletions _includes/nav/crumbs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{%- comment -%}
{%- include nav/crumbs.html
nodes = array -%}
outputs breadcrumb navigation links for the nodes, and the title of the
current page.
{%- endcomment -%}

<nav aria-label="Breadcrumb" class="breadcrumb-nav">
<ol class="breadcrumb-nav-list">
{%- for node in include.nodes -%}
<li class="breadcrumb-nav-list-item">
<a href="{{ node.url | absolute_url }}">{{ node.title }}</a>
</li>
{%- endfor -%}
<li class="breadcrumb-nav-list-item">
<span>{{ page.title }}</span>
</li>
</ol>
</nav>
38 changes: 38 additions & 0 deletions _includes/nav/links.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{%- comment -%}
{%- include nav/links.html
nodes = array
path = string
in_section = value_or_nil
pages = array -%}
outputs links for non-excluded nodes and their children. The links for an
inactive node are independent of the path to the current page, but only those
for top-level nodes are cached.
{%- endcomment -%}

{%- include nav/sorted.html
nodes = include.nodes -%}

<ul class="nav-list">
{%- for node in nav_sorted -%}
{%- unless node.nav_exclude == true -%}

{%- capture nav_newline %}
{% endcapture -%}

{%- assign nav_path = include.path | append: node.title | append: nav_newline -%}
{%- if nav_page_path contains nav_path -%}
{%- assign nav_active = true -%}
{%- else -%}
{%- assign nav_active = false -%}
{%- endif -%}

{%- include nav/node.html
node = node
path = nav_path
in_section = include.in_section
pages = include.pages
active = nav_active -%}

{%- endunless -%}
{%- endfor -%}
</ul>
26 changes: 26 additions & 0 deletions _includes/nav/links_inactive.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{%- comment -%}
{%- include nav/links_inactive.html
nodes = array
path = string
in_section = value_or_nil
pages = array -%}
outputs links for inactive nodes and their children.
{%- endcomment -%}

{%- include nav/sorted.html
nodes = include.nodes -%}

<ul class="nav-list">
{%- for node in nav_sorted -%}
{%- unless node.nav_exclude == true -%}

{%- include nav/node_inactive.html
node = node
path = include.path
in_section = include.in_section
pages = include.pages
cached = false -%}

{%- endunless -%}
{%- endfor -%}
</ul>
40 changes: 40 additions & 0 deletions _includes/nav/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{%- comment -%}
{%- include nav/main.html -%}
outputs links for the main navigation. It also locates the current page in the
navigation hierarchy, and assigns to the variables `nav_page_ancestors`,
`nav_page_path`, and `nav_page_children`.

`nav/main` first outputs the main navigation for pages not in any collection.
It then outputs the main navigation for the pages in each collection (preceded
by the name of the collection, unless all pages are in a single collection.)
{%- endcomment -%}

{%- assign pages_top_size = site.html_pages
| where_exp:"item", "item.title != nil"
| where_exp:"item", "item.parent == nil"
| where_exp:"item", "item.nav_exclude != true"
| size -%}
{%- if pages_top_size > 0 -%}
{%- include nav/collection.html
pages = site.html_pages
key = nil -%}
{%- endif -%}

{%- if site.just_the_docs.collections -%}
{%- assign collections_size = site.just_the_docs.collections | size -%}
{%- for collection_entry in site.just_the_docs.collections -%}

{%- assign collection_key = collection_entry[0] -%}
{%- assign collection_value = collection_entry[1] -%}
{%- assign collection = site[collection_key] -%}
{%- if collection_value.nav_exclude != true -%}
{%- if collections_size > 1 or pages_top_size > 0 -%}
<div class="nav-category">{{ collection_value.name }}</div>
{%- endif -%}
{%- include nav/collection.html
pages = collection
key = collection_key -%}
{%- endif -%}

{%- endfor -%}
{%- endif -%}
45 changes: 45 additions & 0 deletions _includes/nav/node.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{%- comment -%}
{%- include nav/node.html
node = node
path = string
in_section = value_or_nil
pages = array
active = bool -%}
outputs links for the node and its children.
{%- endcomment -%}

{%- if node.section_id -%}
{%- assign nav_section = node.section_id -%}
{%- else -%}
{%- assign nav_section = include.in_section -%}
{%- endif -%}

{%- include nav/children.html
node = include.node
path = include.path
in_section = nav_section -%}

<li class="nav-list-item{% if include.active %} active{% endif %}">

{%- if nav_children.size >= 1 -%}
<a href="#" class="nav-list-expander"><svg viewBox="0 0 24 24"><use xlink:href="#svg-arrow-right"></use></svg></a>
{%- endif -%}

<a href="{{ node.url | absolute_url }}" class="nav-list-link{% if include.active %} active{% endif %}">{{ node.title }}</a>

{%- if nav_children.size >= 1 -%}
{%- if nav_page_path == include.path -%}
{%- include nav/links_inactive.html
nodes = nav_children
path = include.path
in_section = nav_section
pages = include.pages -%}
{%- else -%}
{%- include nav/links.html
nodes = nav_children
path = include.path
in_section = nav_section
pages = include.pages -%}
{%- endif -%}
{%- endif -%}
</li>
47 changes: 47 additions & 0 deletions _includes/nav/node_inactive.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{%- comment -%}
{%- include nav/node_inactive.html
node = node
path = string
in_section = value_or_nil
pages = array
cached = bool -%}
outputs links for the inactive node and its children. The
{%- endcomment -%}

{%- if include.cached -%}
{%- assign nav_parenthood = include.pages
| where_exp: "item", "item.title != nil"
| group_by: "parent" -%}
{%- endif -%}

{%- if node.section_id -%}
{%- assign nav_section = node.section_id -%}
{%- else -%}
{%- assign nav_section = include.in_section -%}
{%- endif -%}

{%- include nav/children.html
node = node
path = include.path
in_section = nav_section -%}

<li class="nav-list-item">

{%- if nav_children.size >= 1 -%}
<a href="#" class="nav-list-expander"><svg viewBox="0 0 24 24"><use xlink:href="#svg-arrow-right"></use></svg></a>
{%- endif -%}

<a href="{{ node.url | absolute_url }}" class="nav-list-link">{{ node.title }}</a>

{%- capture nav_newline %}
{% endcapture -%}

{%- if nav_children.size >= 1 -%}
{%- assign nav_path = include.path | append: node.title | append: nav_newline -%}
{%- include nav/links_inactive.html
nodes = nav_children
path = nav_path
in_section = nav_section
pages = include.pages -%}
{%- endif -%}
</li>
Loading

0 comments on commit 4b8d5df

Please sign in to comment.