Feature: Allow unlimited multi-level navigation (#1431)

* Allow unlimited multi-level navigation

This PR supersedes #462.

The only user-level difference from #462 is that disambiguation of parent pages has to use either `grand_parent` or `ancestor` titles: the somewhat unnatural `section_id` and `in_section` fields are not supported.

The implementation has been significantly simplified by the changes introduced in v0.7.0 of the theme.

* Detect cyclic parenthood

A page should not have a parent or ancestor with the same title. If it does, the location of the repeated link is marked by ∞, to facilitate debugging the navigation (and an unbounded loop leading to a build exception is avoided).

* Add nav_error_report warning in main navigation

When activated by `nav_error_report: true` in `_config.yml`, displays warnings about pages with the same title as their parent page or an ancestral page.

* Cache site-nav with links to all pages

The extra cached site-nav is used for determining breadcrumbs and children navigation, which may involve pages that are excluded from the main navigation.

* Replace code for determining children by inclusion of components/nav/children.html

* Update CHANGELOG.md

---------

Co-authored-by: Matt Wang <matt@matthewwang.me>
This commit is contained in:
Peter Mosses 2024-08-20 22:34:11 +02:00 committed by GitHub
parent 0fc476871c
commit a4e4e312aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 526 additions and 464 deletions

View File

@ -17,12 +17,16 @@ This website is built from the `HEAD` of the `main` branch of the theme reposito
Code changes to `main` that are *not* in the latest release:
- N/A
### New Features
- Added: Allow unlimited multi-level navigation by [@pdmosses] in [#1431]
Docs changes made since the latest release:
- N/A
[#1431]: https://github.com/just-the-docs/just-the-docs/pull/1431
## Release v0.9.0
Hi folks! This minor release adds a `nav_enabled` set of variables to enable/disable the navigation at a site, layout, and page level --- and uses that to add search and auxilary links to the `minimal` layout. In addition, it fixes `search-data.json` corruption with default layouts and some minor CSS/SCSS issues.

View File

@ -122,6 +122,9 @@ nav_external_links:
- title: Just the Docs on GitHub
url: https://github.com/just-the-docs/just-the-docs
# Show navigation error report
nav_error_report: true # default is false/nil.
liquid:
error_mode: strict
strict_filters: true

View File

@ -1,144 +1,96 @@
{%- comment -%}
Include as: {%- include components/breadcrumbs.html -%}
Depends on: page, site.
Includes: components/site_nav.html.
Results in: HTML for the breadcrumbs component.
Overwrites:
node, pages_list, parent_page, grandparent_page.
nav_list_link, site_nav, nav_list_simple, nav_list_link_class, nav_category,
nav_anchor_splits, nav_breadcrumbs, nav_split, nav_split_next, nav_split_test,
nav_breadcrumb_link, nav_list_end_less, nav_list_end_count, nav_end_index, nav_breadcrumb.
{%- endcomment -%}
{%- if page.url != "/" and page.parent -%}
{%- if page.url != "/" and page.parent and page.title -%}
{%- capture nav_list_link -%}
<a href="{{ page.url | relative_url }}" class="nav-list-link">
{%- endcapture -%}
{%- capture site_nav -%}
{%- include_cached components/site_nav.html -%}
{%- include_cached components/site_nav.html all=true -%}
{%- endcapture -%}
{%- if site_nav contains nav_list_link -%}
{%- capture nav_list_simple -%}
<ul class="nav-list">
{%- endcapture -%}
{%- capture nav_list_simple -%}
<ul class="nav-list">
{%- endcapture -%}
{%- capture nav_list_link_class %} class="nav-list-link">
{%- endcapture -%}
{%- capture nav_list_link_class %} class="nav-list-link">
{%- endcapture -%}
{%- capture nav_category -%}
<div class="nav-category">
{%- endcapture -%}
{%- capture nav_category -%}
<div class="nav-category">
{%- endcapture -%}
{%- assign nav_anchor_splits =
site_nav | split: nav_list_link |
first | split: nav_category |
last | split: "</a>" -%}
{%- assign nav_anchor_splits =
site_nav | split: nav_list_link |
first | split: nav_category |
last | split: "</a>" -%}
{%- comment -%}
The ordinary pages (if any) and the collections pages (if any) are separated by
occurrences of nav_category.
Any ancestor nav-links of the page are contained in the last group of pages,
immediately preceding nav-lists. After splitting at "</a>", the anchor that
was split is a potential ancestor link when the following split starts with
a nav-list.
The array nav_breadcrumbs is the stack of current potential ancestors of the
current page. A split that contains one or more "</ul>"s requires that number
of potential ancestors to be popped from the stack.
The number of occurrences of a string in nav_split_next is computed by removing
them all, then dividing the resulting size difference by the length of the string.
{%- endcomment %}
{%- assign nav_breadcrumbs = "" | split: "" -%}
{%- for nav_split in nav_anchor_splits -%}
{%- unless forloop.last -%}
{%- assign nav_split_next = nav_anchor_splits[forloop.index] | strip -%}
{%- assign nav_split_test =
nav_split_next | remove_first: nav_list_simple | prepend: nav_list_simple -%}
{%- if nav_split_test == nav_split_next -%}
{%- assign nav_breadcrumb_link =
nav_split | split: "<a " | last | prepend: "<a " |
replace: nav_list_link_class, ">" | append: "</a>" -%}
{%- assign nav_breadcrumbs = nav_breadcrumbs | push: nav_breadcrumb_link -%}
{%- endif -%}
{%- if nav_split_next contains "</ul>" -%}
{%- assign nav_list_end_less = nav_split_next | remove: "</ul>" -%}
{%- assign nav_list_end_count =
nav_split_next.size | minus: nav_list_end_less.size | divided_by: 5 -%}
{% for nav_end_index in (1..nav_list_end_count) %}
{%- assign nav_breadcrumbs = nav_breadcrumbs | pop -%}
{%- endfor -%}
{%- endif -%}
{%- endunless -%}
{%- endfor -%}
{%- assign nav_parent_link = nav_breadcrumbs[-1] -%}
{%- assign nav_grandparent_link = nav_breadcrumbs[-2] -%}
{%- else -%}
{%- comment -%}
Pages whose links are excluded from the main navigation may still have
breadcrumbs. Determining them appears to require inspecting the front matter
of all the pages in the same group. For sites with 100s of pages, this is too
inefficient in Jekyll 3 (also when the for-loop is replaced by where-filters).
{%- endcomment -%}
{%- assign pages_list = site[page.collection] | default: site.html_pages -%}
{%- comment -%}
The ordinary pages (if any) and the collections pages (if any) are separated by
occurrences of nav_category.
{%- assign parent_page = nil -%}
{%- assign grandparent_page = nil -%}
{%- for node in pages_list -%}
Any ancestor nav-links of the page are contained in the last group of pages,
immediately preceding nav-lists. After splitting at "</a>", the anchor that
was split is a potential ancestor link when the following split starts with
a nav-list.
{%- if node.has_children and page.grand_parent -%}
{%- if node.title == page.parent and node.parent == page.grand_parent -%}
{%- assign parent_page = node -%}
{%- endif -%}
{%- if node.title == page.grand_parent -%}
{%- assign grandparent_page = node -%}
{%- endif -%}
{%- if parent_page and grandparent_page -%}
{%- break -%}
{%- endif -%}
{%- elsif node.has_children and node.title == page.parent and node.parent == nil -%}
{%- assign parent_page = node -%}
{%- break -%}
{%- endif -%}
{%- endfor -%}
The array nav_breadcrumbs is the stack of current potential ancestors of the
current page. A split that contains one or more "</ul>"s requires that number
of potential ancestors to be popped from the stack.
{%- capture nav_parent_link -%}
<a href="{{ parent_page.url | relative_url }}">{{ page.parent }}</a>
{%- endcapture -%}
The number of occurrences of a string in nav_split_next is computed by removing
them all, then dividing the resulting size difference by the length of the string.
{%- endcomment %}
{%- if page.grand_parent %}
{%- capture nav_grandparent_link -%}
<a href="{{ grandparent_page.url | relative_url }}">{{ page.grand_parent }}</a>
{%- endcapture -%}
{%- endif -%}
{%- assign nav_breadcrumbs = "" | split: "" -%}
{%- for nav_split in nav_anchor_splits -%}
{%- unless forloop.last -%}
{%- assign nav_split_next = nav_anchor_splits[forloop.index] | strip -%}
{%- assign nav_split_test =
nav_split_next | remove_first: nav_list_simple | prepend: nav_list_simple -%}
{%- if nav_split_test == nav_split_next -%}
{%- assign nav_breadcrumb_link =
nav_split | split: "<a " | last | prepend: "<a " |
replace: nav_list_link_class, ">" | append: "</a>" -%}
{%- assign nav_breadcrumbs = nav_breadcrumbs | push: nav_breadcrumb_link -%}
{%- endif -%}
{%- if nav_split_next contains "</ul>" -%}
{%- assign nav_list_end_less = nav_split_next | remove: "</ul>" -%}
{%- assign nav_list_end_count =
nav_split_next.size | minus: nav_list_end_less.size | divided_by: 5 -%}
{% for nav_end_index in (1..nav_list_end_count) %}
{%- assign nav_breadcrumbs = nav_breadcrumbs | pop -%}
{%- endfor -%}
{%- endif -%}
{%- endunless -%}
{%- endfor -%}
<nav aria-label="Breadcrumb" class="breadcrumb-nav">
<ol class="breadcrumb-nav-list">
{%- if nav_grandparent_link %}
<li class="breadcrumb-nav-list-item">{{ nav_grandparent_link }}</li>
{%- endif %}
<li class="breadcrumb-nav-list-item">{{ nav_parent_link }}</li>
{%- for nav_breadcrumb in nav_breadcrumbs %}
<li class="breadcrumb-nav-list-item">{{ nav_breadcrumb }}</li>
{%- endfor %}
<li class="breadcrumb-nav-list-item"><span>{{ page.title }}</span></li>
</ol>
</nav>
{% if site.nav_error_report %}
{{ nav_error_report }}
{% endif %}
{%- endif -%}

View File

@ -1,33 +1,89 @@
{%- comment -%}
Include as: {%- include components/children_nav.html -%}
Depends on: page, site.
Depends on: page, site, nav_breadcrumbs.
Results in: HTML for the children-navigation component.
Includes:
sorted_pages.html
toc_heading_custom.html
Includes: components/nav/sorted.html, toc_heading_custom.html.
Overwrites:
child_pages.
nav_ancestor_links, nav_top_node_titles, nav_child_candidates, nav_children,
nav_child, nav_child_ok, nav_child_ancestor, nav_sorted.
{%- endcomment -%}
{%- if page.has_children == true and page.has_toc != false -%}
{%- assign child_pages = site[page.collection]
| default: site.html_pages
| where: "parent", page.title
| where: "grand_parent", page.parent -%}
{%- comment -%}
Whether a page has any children is checked efficiently by inspecting the cached
site_nav. If the page has no children, nav_children is set to an empty array;
otherwise nav_children is left unset.
{%- endcomment -%}
{%- include sorted_pages.html pages = child_pages -%}
{%- if page.has_children == false -%}
{%- assign nav_children = "" | split: "" -%}
{%- else -%}
{%- assign nav_children = nil -%}
{%- capture nav_list_link -%}
<a href="{{ page.url | relative_url }}" class="nav-list-link">
{%- endcapture -%}
{%- capture site_nav -%}
{%- include_cached components/site_nav.html all=true -%}
{%- endcapture -%}
{%- capture nav_list_simple -%}
<ul class="nav-list">
{%- endcapture -%}
{%- assign nav_child_start = site_nav
| split: nav_list_link | last
| split: "</a>" | slice: 1 | first -%}
{%- assign nav_child_test = nav_child_start
| remove_first: nav_list_simple | prepend: nav_list_simple -%}
{%- if nav_child_start != nav_child_test -%}
{%- assign nav_children = "" | split: "" -%}
{%- endif -%}
{%- endif -%}
{%- unless nav_children -%}
{%- comment -%}
The layout is assumed to include components/breadcrumbs.html before this file,
otherwise it needs to be included here.
{%- endcomment -%}
{%- assign nav_ancestors = "" | split: "" -%}
{%- for nav_link in nav_breadcrumbs -%}
{%- assign nav_title = nav_link | split: ">" | slice: 1 | first | append: ">" | remove: "</a>" -%}
{%- assign nav_ancestors = nav_ancestors | push: nav_title -%}
{%- endfor -%}
{%- assign nav_parenthood = site[page.collection] | default: site.html_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_top_node_titles = nav_top_nodes | map: "title" -%}
{%- include components/nav/children.html node=page ancestors=nav_ancestors all=true -%}
{%- endunless -%}
{%- if nav_children.size >= 1 -%}
{%- if page.child_nav_order == 'desc' or page.child_nav_order == 'reversed' -%}
{%- assign sorted_pages = sorted_pages | reverse -%}
{%- assign nav_children = nav_children | reverse -%}
{%- endif -%}
{%- endif -%}
<hr>
{% include toc_heading_custom.html %}
<ul>
{% for child in sorted_pages %}
{% for nav_child in nav_children %}
<li>
<a href="{{ child.url | relative_url }}">{{ child.title }}</a>{% if child.summary %} - {{ child.summary }}{% endif %}
<a href="{{ nav_child.url | relative_url }}">{{ nav_child.title }}</a>{% if nav_child.summary %} - {{ nav_child.summary }}{% endif %}
</li>
{% endfor %}
{% endfor %}
</ul>
{%- endif -%}

View File

@ -1,75 +0,0 @@
{%- comment -%}
Include as: {%- include components/nav.html pages=pages -%}
Depends on: include.pages.
Results in: HTML for the navigation panel.
Includes:
sorted_pages.html
Overwrites:
nav_pages, first_level_pages, second_level_pages, third_level_pages,
node, children_list, child, grand_children_list, grand_child.
{%- endcomment -%}
{%- assign nav_pages = include.pages
| where_exp: "item", "item.title != nil"
| where_exp: "item", "item.nav_exclude != true" -%}
{%- include sorted_pages.html pages = nav_pages -%}
{%- comment -%}
It might be more efficient to sort the pages at each level separately.
{%- endcomment -%}
{%- assign first_level_pages = sorted_pages
| where_exp: "item", "item.parent == nil" -%}
{%- assign second_level_pages = sorted_pages
| where_exp: "item", "item.parent != nil"
| where_exp: "item", "item.grand_parent == nil" -%}
{%- assign third_level_pages = sorted_pages
| where_exp: "item", "item.grand_parent != nil" -%}
<ul class="nav-list">
{%- for node in first_level_pages -%}
<li class="nav-list-item">
{%- if node.has_children -%}
<button class="nav-list-expander btn-reset" aria-label="toggle items in {{ node.title }} category" aria-pressed="false">
<svg viewBox="0 0 24 24" aria-hidden="true"><use xlink:href="#svg-arrow-right"></use></svg>
</button>
{%- endif -%}
<a href="{{ node.url | relative_url }}" class="nav-list-link">{{ node.title }}</a>
{%- if node.has_children -%}
{%- assign children_list = second_level_pages
| where: "parent", node.title -%}
{%- if node.child_nav_order == 'desc' or node.child_nav_order == 'reversed' -%}
{%- assign children_list = children_list | reverse -%}
{%- endif -%}
<ul class="nav-list">
{%- for child in children_list -%}
<li class="nav-list-item">
{%- if child.has_children -%}
<button class="nav-list-expander btn-reset" aria-label="toggle items in {{ child.title }} category" aria-pressed="false">
<svg viewBox="0 0 24 24" aria-hidden="true"><use xlink:href="#svg-arrow-right"></use></svg>
</button>
{%- endif -%}
<a href="{{ child.url | relative_url }}" class="nav-list-link">{{ child.title }}</a>
{%- if child.has_children -%}
{%- assign grand_children_list = third_level_pages
| where: "parent", child.title
| where: "grand_parent", node.title -%}
{%- if child.child_nav_order == 'desc' or child.child_nav_order == 'reversed' -%}
{%- assign grand_children_list = grand_children_list | reverse -%}
{%- endif -%}
<ul class="nav-list">
{%- for grand_child in grand_children_list -%}
<li class="nav-list-item">
<a href="{{ grand_child.url | relative_url }}" class="nav-list-link">{{ grand_child.title }}</a>
</li>
{%- endfor -%}
</ul>
{%- endif -%}
</li>
{%- endfor -%}
</ul>
{%- endif -%}
</li>
{%- endfor -%}
</ul>

View File

@ -0,0 +1,48 @@
{%- comment -%}
Include as: {%- include components/nav/children.html node=node ancestors=title_array all=bool -%}
Depends on: include.node, include.ancestors, include.all, nav_parenthood, nav_top_node_titles.
Includes: components/nav/sorted.html.
Assigns to: nav_children.
Overwrites:
nav_candidates, nav_child, nav_child_ok.
{%- endcomment -%}
{%- assign nav_children = "" | split: "" -%}
{%- if include.all == true or include.node.has_children != false -%}
{%- assign nav_candidates = nav_parenthood
| where: "name", include.node.title | map: "items" | first -%}
{%- for nav_child in nav_candidates -%}
{%- assign nav_child_ok = true -%}
{%- if nav_child.grand_parent and nav_child.grand_parent != include.node.parent -%}
{%- assign nav_child_ok = false -%}
{%- endif -%}
{%- if nav_child.ancestor and nav_child.ancestor != include.node.title -%}
{%- unless include.ancestors contains nav_child.ancestor -%}
{%- assign nav_child_ok = false -%}
{%- endunless -%}
{%- endif -%}
{%- comment -%}
The following check rejects nav_child as 3rd-level when include.node is 2nd-level
and nav_child can also be 2nd-level. This is for backwards compatibility with
existing 3-level sites.
{%- endcomment -%}
{%- if nav_child.grand_parent == nil and nav_child.ancestor == nil and
nav_top_node_titles contains nav_child.parent and include.ancestors.size >= 1 -%}
{%- assign nav_child_ok = false -%}
{%- endif -%}
{%- if nav_child_ok -%}
{%- assign nav_children = nav_children | push: nav_child -%}
{%- endif -%}
{%- endfor -%}
{%- endif -%}
{%- include components/nav/sorted.html pages=nav_children -%}
{%- assign nav_children = nav_sorted -%}

View File

@ -0,0 +1,53 @@
{%- comment -%}
Include as: {%- include components/nav/links.html pages=page_array ancestors=title_array all=bool -%}
Depends on: include.pages, include.ancestors, include.all.
Results in: HTML for the main navigation when all is nil or false;
includes links to pages excluded from the main navigation when all is true.
Includes: components/nav/sorted.html, components/nav/children.html, components/nav/links.html.
Overwrites:
node, nav_children, nav_ancestors.
{%- endcomment -%}
<ul class="nav-list">
{%- for node in include.pages -%}
{%- if include.all == true or node.nav_exclude != true -%}
{%- if include.ancestors contains node.title -%}
<li class="nav-list-item">
<a href="{{ node.url | relative_url }}" class="nav-list-link"></a>
</li>
{%- capture nav_error_report -%}
<blockquote class="warning">
A page has the same title as its parent page or one of its ancestral pages!<br>
This causes an incorrect link in the main navigation panel.<br>
Page title: <code>{{ node.title }}</code>, location: <code>{{ node.path }}</code>.
</blockquote>
{%- endcapture -%}
{%- else -%}
{%- include components/nav/children.html node=node ancestors=include.ancestors all=include.all -%}
<li class="nav-list-item">
{%- if nav_children.size >= 1 -%}
<button class="nav-list-expander btn-reset" aria-label="toggle items in {{ node.title }} category" aria-pressed="false">
<svg viewBox="0 0 24 24" aria-hidden="true"><use xlink:href="#svg-arrow-right"></use></svg>
</button>
{%- endif -%}
<a href="{{ node.url | relative_url }}" class="nav-list-link">{{ node.title }}</a>
{%- if nav_children.size >= 1 -%}
{%- if node.child_nav_order == 'desc' or node.child_nav_order == 'reversed' -%}
{%- assign nav_children = nav_children | reverse -%}
{%- endif -%}
{%- assign nav_ancestors = include.ancestors | push: node.title -%}
{%- include components/nav/links.html pages=nav_children ancestors=nav_ancestors all=include.all -%}
{%- endif -%}
</li>
{%- endif -%}
{%- endif -%}
{%- endfor -%}
</ul>
{%- comment -%}{%- endcomment -%}

View File

@ -0,0 +1,23 @@
{%- comment -%}
Include as: {%- include components/nav/pages.html pages=page_array all=bool -%}
Depends on: include.pages.
Results in: HTML for the main navigation when all is nil or false;
adds links to pages excluded from the main navigation when all is true.
Includes: components/nav/links.html
Assigns to:
nav_parenthood, nav_top_nodes, nav_top_node_titles, nav_ancestors.
{%- 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 -%}
{%- include components/nav/sorted.html pages=nav_top_nodes -%}
{% assign nav_top_node_titles = nav_top_nodes | map: "title" -%}
{%- assign nav_ancestors = "" | split: "" -%}
{%- include components/nav/links.html pages=nav_sorted ancestors=nav_ancestors all=include.all -%}

View File

@ -1,7 +1,7 @@
{%- comment -%}
Include as: {%- include sorted_pages.html pages=array_of_pages -%}
Include as: {%- include components/nav/sorted.html pages=page_array -%}
Depends on: include.pages.
Assigns to: sorted_pages.
Assigns to: nav_sorted.
Overwrites:
nav_order_pages, title_order_pages, double_quote, empty_array,
nav_number_pages, nav_string_pages, nav_order_groups, group,
@ -103,7 +103,7 @@
{%- endif -%}
{%- endunless -%}
{%- assign sorted_pages = nav_number_pages
{%- assign nav_sorted = nav_number_pages
| concat: nav_string_pages
| concat: title_number_pages
| concat: title_string_pages -%}

View File

@ -1,9 +1,10 @@
{%- comment -%}
Include as: {%- include_cached components/site_nav.html -%}
Include as: {%- include_cached components/site_nav.html all=bool -%}
Depends on: site.
Results in: HTML for the site-nav.
Results in: cached HTML for the main navigation when `all` is nil or false;
includes links to pages excluded from the main navigation when `all` is true.
Includes:
components/nav.html
components/nav/pages.html
Overwrites:
pages_top_size, collections_size, collection_entry,
collection_key, collection_value, collection.
@ -16,7 +17,7 @@
| where_exp:"item", "item.nav_exclude != true"
| size %}
{% if pages_top_size > 0 %}
{% include components/nav.html pages=site.html_pages %}
{% include components/nav/pages.html pages=site.html_pages all=include.all %}
{% endif %}
{%- if site.nav_external_links -%}
<ul class="nav-list">
@ -51,17 +52,21 @@
</button>
{%- endif -%}
<div class="nav-category">{{ collection_value.name }}</div>
{% include components/nav.html pages=collection %}
{% include components/nav/pages.html pages=collection all=include.all %}
</li>
</ul>
{% else %}
<div class="nav-category">{{ collection_value.name }}</div>
{% include components/nav.html pages=collection %}
{% include components/nav/pages.html pages=collection all=include.all %}
{% endif %}
{% else %}
{% include components/nav.html pages=collection %}
{% include components/nav/pages.html pages=collection all=include.all %}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
</nav>
{% if site.nav_error_report %}
{{ nav_error_report }}
{%- endif %}

View File

@ -21,18 +21,18 @@
{%- endcomment -%}
{%- capture activation_no_nav_link %}
.site-nav ul li a {
background-image: none;
}
.site-nav ul li a {
background-image: none;
}
{%- if site.just_the_docs.collections %}
.site-nav > ul.nav-category-list > li > button svg {
transform: rotate(-90deg);
}
.site-nav > ul.nav-category-list > li.nav-list-item > ul.nav-list {
display: block;
}
{%- endif %}
{%- if site.just_the_docs.collections %}
.site-nav > ul.nav-category-list > li > button svg {
transform: rotate(-90deg);
}
.site-nav > ul.nav-category-list > li.nav-list-item > ul.nav-list {
display: block;
}
{%- endif %}
{% endcapture -%}
{%- capture nav_list_link -%}
@ -45,288 +45,281 @@
{%- if site_nav contains nav_list_link -%}
{%- capture nav_list -%}
<ul class="nav-list
{%- endcapture -%}
{%- capture nav_list -%}
<ul class="nav-list
{%- endcapture -%}
{%- assign nav_list_end = "</ul>" -%}
{%- assign nav_list_end = "</ul>" -%}
{%- capture nav_list_item -%}
<li class="nav-list-item
{%- endcapture -%}
{%- capture nav_list_item -%}
<li class="nav-list-item
{%- endcapture -%}
{%- capture nav_category_list -%}
<ul class="nav-list nav-category-list">
{%- endcapture -%}
{%- capture nav_category_list -%}
<ul class="nav-list nav-category-list">
{%- endcapture -%}
{%- assign nav_list_link_prefix =
site_nav | split: nav_list_link | first | append: nav_list_link -%}
{%- assign nav_list_link_prefix =
site_nav | split: nav_list_link | first | append: nav_list_link -%}
{%- assign nav_splits =
nav_list_link_prefix | split: nav_list_item | pop -%}
{%- assign nav_splits =
nav_list_link_prefix | split: nav_list_item | pop -%}
{%- assign nav_levels = "" | split: "" | push: 1 -%}
{%- assign nav_levels = "" | split: "" | push: 1 -%}
{%- comment -%}
The pattern of occurrences of list and list-item tags in the site_nav string
is included in the language defined by the following context-free grammar:
site_nav = PAGES? EXTERNALS? COLLECTION*
PAGES = nav_list (nav_list_item PAGES?)+ nav_list_end
EXTERNALS = nav_list nav_list_item+ nav_list_end
COLLECTION = nav_list (nav_list_item PAGES?)* nav_list_end
| nav_category_list
nav_list_item nav_list (nav_list_item PAGES?)* nav_list_end
nav_list_end
To determine the path in the site_nav to the nav_list_link for the current page,
the prefix of nav_list_link in site_nav is split at each nav_list_item to give
an array of nav_splits.
In site_nav, occurrences of nav_list must be separated by at least one
nav_list_item or nav_list_end. Moreover, when a nav_list_end is followed by a
nav_list without an intervening nav_list_item, that nav_list must start either
a list of external links or a collection. And when a nav_list is followed by a
nav_list_end without an intervening nav_list_item, they form an empty collection.
nav_levels is the path determined by the previously inspected nav_splits.
How many times nav_list and nav_list_end occur in the current nav_split
determines how to update the path.
A nav_split can contain any number of empty non-foldable collections.
The path element produced by the outer nav_list of a foldable collection is
irrelevant; it is set to zero and subsequently removed.
{%- comment -%}
The pattern of occurrences of list and list-item tags in the site_nav string
is included in the language defined by the following context-free grammar:
The number of occurrences of a string in a nav_split is computed by removing
them all, then dividing the resulting size difference by the length of the string.
Case analysis on (nav_list_count, nav_list_end_count):
- when (0, 0), the nav_split was between two nav_list_items at the same level;
- when (0, N+1), all the nav_list_ends in the nav_split are in PAGES or in one
COLLECTION;
site_nav = PAGES? EXTERNALS? COLLECTION*
PAGES = nav_list (nav_list_item PAGES?)+ nav_list_end
EXTERNALS = nav_list nav_list_item+ nav_list_end
COLLECTION = nav_list (nav_list_item PAGES?)* nav_list_end
| nav_category_list
nav_list_item nav_list (nav_list_item PAGES?)* nav_list_end
nav_list_end
- when (M+1, 0), M must be 0 (because two nav_lists in the same nav_split must
be separated by at least one nav_list_end);
To determine the path in the site_nav to the nav_list_link for the current page,
the prefix of nav_list_link in site_nav is split at each nav_list_item to give
an array of nav_splits.
- when (M+1, N+1), the nav_split must contain either:
- the end of PAGES followed by the start of EXTERNALS, or
- the end of PAGES followed by the start of a COLLECTION, or
- the end of EXTERNALS followed by the start of a COLLECTION, or
- the end of one COLLECTION followed by the start of another, or
- (in the first nav_split) one or more empty non-foldable COLLECTIONS
followed by the start of a COLLECTION.
In general, nav_levels[0] here needs to be incremented by nav_list_count.
However, a nav_split that contains the end of an empty foldable collection
followed by the just the start of a collection has two occurrences of nav_list,
and the increment needs to be one less; similarly when the first nav_split
starts with an empty non-foldable collection.
{%- endcomment %}
In site_nav, occurrences of nav_list must be separated by at least one
nav_list_item or nav_list_end. Moreover, when a nav_list_end is followed by a
nav_list without an intervening nav_list_item, that nav_list must start either
a list of external links or a collection. And when a nav_list is followed by a
nav_list_end without an intervening nav_list_item, they form an empty collection.
nav_levels is the path determined by the previously inspected nav_splits.
How many times nav_list and nav_list_end occur in the current nav_split
determines how to update the path.
A nav_split can contain any number of empty non-foldable collections.
The path element produced by the outer nav_list of a foldable collection is
irrelevant; it is set to zero and subsequently removed.
The number of occurrences of a string in a nav_split is computed by removing
them all, then dividing the resulting size difference by the length of the string.
Case analysis on (nav_list_count, nav_list_end_count):
- when (0, 0), the nav_split was between two nav_list_items at the same level;
- when (0, N+1), all the nav_list_ends in the nav_split are in PAGES or in one
COLLECTION;
- when (M+1, 0), M must be 0 (because two nav_lists in the same nav_split must
be separated by at least one nav_list_end);
- when (M+1, N+1), the nav_split must contain either:
- the end of PAGES followed by the start of EXTERNALS, or
- the end of PAGES followed by the start of a COLLECTION, or
- the end of EXTERNALS followed by the start of a COLLECTION, or
- the end of one COLLECTION followed by the start of another, or
- (in the first nav_split) one or more empty non-foldable COLLECTIONS
followed by the start of a COLLECTION.
In general, nav_levels[0] here needs to be incremented by nav_list_count.
However, a nav_split that contains the end of an empty foldable collection
followed by the just the start of a collection has two occurrences of nav_list,
and the increment needs to be one less; similarly when the first nav_split
starts with an empty non-foldable collection.
{%- endcomment %}
{%- for nav_split in nav_splits -%}
{%- for nav_split in nav_splits -%}
{%- assign nav_list_less = nav_split | remove: nav_list -%}
{%- assign nav_list_count =
nav_split.size | minus: nav_list_less.size |
divided_by: nav_list.size -%}
{%- assign nav_list_less = nav_split | remove: nav_list -%}
{%- assign nav_list_count =
nav_split.size | minus: nav_list_less.size |
divided_by: nav_list.size -%}
{%- assign nav_list_end_less = nav_split | remove: nav_list_end -%}
{%- assign nav_list_end_count =
nav_split.size | minus: nav_list_end_less.size |
divided_by: nav_list_end.size -%}
{%- assign nav_list_end_less = nav_split | remove: nav_list_end -%}
{%- assign nav_list_end_count =
nav_split.size | minus: nav_list_end_less.size |
divided_by: nav_list_end.size -%}
{%- if nav_list_count == 0 and nav_list_end_count == 0 -%}
{%- if nav_list_count == 0 and nav_list_end_count == 0 -%}
{%- assign nav_index = nav_levels[-1] | plus: 1 -%}
{%- assign nav_levels = nav_levels | pop | push: nav_index -%}
{%- assign nav_index = nav_levels[-1] | plus: 1 -%}
{%- assign nav_levels = nav_levels | pop | push: nav_index -%}
{%- elsif nav_list_count == 0 and nav_list_end_count >= 1 -%}
{%- elsif nav_list_count == 0 and nav_list_end_count >= 1 -%}
{%- assign nav_slice_size = nav_levels.size | minus: nav_list_end_count -%}
{%- assign nav_levels = nav_levels | slice: 0, nav_slice_size -%}
{%- assign nav_index = nav_levels[-1] | plus: 1 -%}
{%- assign nav_levels = nav_levels | pop | push: nav_index -%}
{%- assign nav_slice_size = nav_levels.size | minus: nav_list_end_count -%}
{%- assign nav_levels = nav_levels | slice: 0, nav_slice_size -%}
{%- assign nav_index = nav_levels[-1] | plus: 1 -%}
{%- assign nav_levels = nav_levels | pop | push: nav_index -%}
{%- elsif nav_list_count == 1 and nav_list_end_count == 0 -%}
{%- elsif nav_list_count == 1 and nav_list_end_count == 0 -%}
{%- if nav_split contains nav_category_list -%}
{%- assign nav_levels = nav_levels | push: 0 -%}
{%- else -%}
{%- assign nav_levels = nav_levels | push: 1 -%}
{%- endif -%}
{%- elsif nav_list_count >= 1 and nav_list_end_count >= 1 -%}
{%- assign nav_index = nav_levels[0] | plus: nav_list_count -%}
{%- if nav_levels[-1] == 0 or forloop.first -%}
{%- assign nav_index = nav_index | minus: 1 -%}
{%- endif -%}
{%- assign nav_levels = nav_levels | slice: 0, 0 | push: nav_index -%}
{%- if nav_split contains nav_category_list -%}
{%- assign nav_levels = nav_levels | push: 0 -%}
{%- else -%}
{%- assign nav_levels = nav_levels | push: 1 -%}
{%- endif -%}
{%- if nav_split contains nav_category_list -%}
{%- assign nav_levels = nav_levels | push: 0 -%}
{%- else -%}
{%- assign nav_levels = nav_levels | push: 1 -%}
{%- endif -%}
{%- elsif nav_list_count >= 1 and nav_list_end_count >= 1 -%}
{%- endfor -%}
{%- assign nav_index = nav_levels[0] | plus: nav_list_count -%}
{%- if nav_levels[-1] == 0 or forloop.first -%}
{%- assign nav_index = nav_index | minus: 1 -%}
{%- endif -%}
{%- assign nav_levels = nav_levels | slice: 0, 0 | push: nav_index -%}
{%- if nav_split contains nav_category_list -%}
{%- assign nav_levels = nav_levels | push: 0 -%}
{%- else -%}
{%- assign nav_levels = nav_levels | push: 1 -%}
{%- endif -%}
{%- assign nav_levels = nav_levels | where_exp: "item", "item >= 1" -%}
{%- endif -%}
{%- comment -%}
The generated CSS depends on the position of the current page in each level in
the navigation.
{%- endfor -%}
nav_page_level is the depth of the navigation hierarchy (ignoring collections).
In existing 3-level sites, nav_page_level <= 3.
In multi-level sites, nav_page_level is unbounded.
{%- endcomment -%}
{%- assign nav_levels = nav_levels | where_exp: "item", "item >= 1" -%}
{%- assign nav_page_level = nav_levels.size | minus: 1 -%}
{%- assign nav_page_parent_level = nav_page_level | minus: 1 -%}
{%- assign nav_page_grandparent_level = nav_page_level | minus: 2 -%}
{%- endif -%}
{%- comment -%}
The generated CSS depends on the position of the current page in each level in
the navigation.
{%- endcomment -%}
{%- if nav_levels[1] == nil -%}
{{ activation_no_nav_link }}
{%- else -%}
{%- if nav_levels[2] == nil and nav_levels[3] -%}
{{ activation_no_nav_link }}
{%- else -%}
{%- comment -%}
The site-nav is:
- an optional ul.nav-list with li.nav-list-items for non-collection top-level pages
- an optional ul.nav-list with li.nav-list-item.externals
- any number of just-the-docs.collections
A non-foldable collection is:
- a div.nav-category with the collection name, followed by:
- a ul.nav-list with li.nav-list-items for its top-level pages
A foldable collection is:
- a ul.nav-list.nav-category-list with a single li.nav-list-item containing:
- an optional button with the expander svg
- a div.nav-category with the collection name
{%- comment -%}
The site-nav is:
- an optional ul.nav-list with li.nav-list-items for non-collection top-level pages
- an optional ul.nav-list with li.nav-list-item.externals
- any number of just-the-docs.collections
A non-foldable collection is:
- a div.nav-category with the collection name, followed by:
- a ul.nav-list with li.nav-list-items for its top-level pages
The generated CSS uses:
- activation_collection_prefix, to select the site-nav > ul.nav-list for the page
- activation_other_collection_prefix, to select all the other site-nav > ul.nav-lists
{%- endcomment -%}
A foldable collection is:
- a ul.nav-list.nav-category-list with a single li.nav-list-item containing:
- an optional button with the expander svg
- a div.nav-category with the collection name
- a ul.nav-list with li.nav-list-items for its top-level pages
The generated CSS uses:
- activation_collection_prefix, to select the site-nav > ul.nav-list for the page
- activation_other_collection_prefix, to select all the other site-nav > ul.nav-lists
{%- endcomment -%}
{%- if page.collection == nil -%}
{%- if page.collection == nil -%}
{%- capture activation_collection_prefix -%}
.site-nav > ul.nav-list:first-child
{%- endcapture -%}
{%- capture activation_collection_prefix -%}
.site-nav > ul.nav-list:first-child
{%- endcapture -%}
{%- capture activation_other_collection_prefix -%}
.site-nav > ul.nav-list:not(:first-child)
{%- endcapture -%}
{%- capture activation_other_collection_prefix -%}
.site-nav > ul.nav-list:not(:first-child)
{%- endcapture -%}
{%- else -%}
{%- else -%}
{%- capture activation_collection_prefix -%}
.site-nav > ul:nth-of-type({{ nav_levels[0] }})
{%- if site.just_the_docs.collections[page.collection].nav_fold %} > li > ul
{%- endif -%}
{%- endcapture -%}
{%- capture activation_other_collection_prefix -%}
.site-nav > ul:not(:nth-of-type({{ nav_levels[0] }}))
{%- endcapture -%}
{%- capture activation_collection_prefix -%}
.site-nav > ul:nth-of-type({{ nav_levels[0] }})
{%- if site.just_the_docs.collections[page.collection].nav_fold %} > li > ul
{%- endif -%}
{%- endcapture -%}
{%- capture activation_other_collection_prefix -%}
.site-nav > ul:not(:nth-of-type({{ nav_levels[0] }}))
{%- endcapture -%}
{%- comment -%}
The required background image of the link to the current page may involve SCSS.
To avoid page-dependent SCSS, all nav links initially have that background image.
The following rule removes the image from the links to all parents, siblings,
and children of the current page.
{%- endcomment %}
{%- endif -%}
{% if nav_page_level >= 2 -%}
{%- for i in (1..nav_page_parent_level) -%}
{{ activation_collection_prefix }} >
{%- for j in (2..i) %} li > ul >
{%- endfor %} li > a,
{% endfor -%}
{%- endif %}
{{ activation_collection_prefix }} >
{%- for i in (1..nav_page_parent_level) %} li > ul >
{%- endfor %} li:not(:nth-child({{ nav_levels[nav_page_level] }})) > a,
{{ activation_collection_prefix }} >
{%- for i in (1..nav_page_level) %} li > ul >
{%- endfor %} li a {
background-image: none;
}
{%- comment -%}
The required background image of the link to the current page may involve SCSS.
To avoid page-dependent SCSS, all nav links initially have that background image.
The following rule removes the image from the links to all parents, siblings,
and children of the current page.
{%- endcomment %}
{%- comment -%}
The following rule removes the image from the links to pages in other collections.
{%- endcomment %}
{% if nav_levels[3] -%}
{{ activation_other_collection_prefix }} a,
.site-nav li.external a {
background-image: none;
}
{{ activation_collection_prefix }} > li > a,
{{ activation_collection_prefix }} > li > ul > li > a,
{{ activation_collection_prefix }} > li > ul > li > ul > li:not(:nth-child({{ nav_levels[3] }})) > a {
background-image: none;
}
{%- comment -%}
The following rule styles the link to the current page.
{%- endcomment %}
{%- elsif nav_levels[2] -%}
{{ activation_collection_prefix }} > li:nth-child({{ nav_levels[1] }})
{%- for i in (2..nav_page_level) %} > ul > li:nth-child({{ nav_levels[i] }})
{%- endfor %} > a {
font-weight: 600;
text-decoration: none;
}
{{ activation_collection_prefix }} > li > a,
{{ activation_collection_prefix }} > li > ul > li:not(:nth-child({{ nav_levels[2] }})) > a,
{{ activation_collection_prefix }} > li > ul > li > ul > li > a {
background-image: none;
}
{%- comment -%}
The following rules unfold all collections, and display the links to any children
of the current page.
To avoid dependence on the SCSS variable nav-list-expander-right, the direction
of the rotation of the expander icon is fixed, and corresponds to the appearance
when nav-list-expander-right is true. This results in a minor visual difference
between the appearance of active expander icons when JS is enabled/disabled and
nav-list-expander-right is false, which seems unavoidable.
{%- endcomment %}
{%- if site.just_the_docs.collections %}
.site-nav > ul.nav-category-list > li > button svg,
{% endif -%}
{{ activation_collection_prefix }} > li:nth-child({{ nav_levels[1] }}) > button svg
{%- for i in (2..nav_page_level) %},
{{ activation_collection_prefix }} > li:nth-child({{ nav_levels[1] }}) >
{%- for j in (2..i) %} ul > li:nth-child({{ nav_levels[j] }}) >
{%- endfor %} button svg
{%- endfor %} {
transform: rotate(-90deg);
}
{%- if site.just_the_docs.collections %}
.site-nav > ul.nav-category-list > li.nav-list-item > ul.nav-list,
{% endif -%}
{{ activation_collection_prefix }} > li.nav-list-item:nth-child({{ nav_levels[1] }}) > ul.nav-list
{%- for i in (2..nav_page_level) %},
{{ activation_collection_prefix }} > li.nav-list-item:nth-child({{ nav_levels[1] }}) >
{%- for j in (2..i) %} ul.nav-list > li.nav-list-item:nth-child({{ nav_levels[j] }}) >
{%- endfor %} ul.nav-list
{%- endfor %} {
display: block;
}
{%- else -%}
{{ activation_collection_prefix }} > li:not(:nth-child({{ nav_levels[1] }})) > a,
{{ activation_collection_prefix }} > li > ul > li > a,
{{ activation_collection_prefix }} > li > ul > li > ul > li > a {
background-image: none;
}
{%- comment -%}
not site_nav contains nav_list_link
{%- endcomment -%}
{%- endif %}
{%- comment -%}
The following rule removes the image from the links to pages in other collections.
{%- endcomment %}
{{ activation_other_collection_prefix }} a,
.site-nav li.external a {
background-image: none;
}
{%- comment -%}
The following rule styles the link to the current page.
{%- endcomment %}
{{ activation_collection_prefix }} > li:nth-child({{ nav_levels[1] }})
{%- if nav_levels[2] %} > ul > li:nth-child({{ nav_levels[2] }})
{%- if nav_levels[3] %} > ul > li:nth-child({{ nav_levels[3] }})
{%- endif -%}
{%- endif %} > a {
font-weight: 600;
text-decoration: none;
}
{%- comment -%}
The following rules unfold all collections, and display the links to any children
of the current page.
To avoid dependence on the SCSS variable nav-list-expander-right, the direction
of the rotation of the expander icon is fixed, and corresponds to the appearance
when nav-list-expander-right is true. This results in a minor visual difference
between the appearance of active expander icons when JS is enabled/disabled and
nav-list-expander-right is false, which seems unavoidable.
{%- endcomment %}
{%- if site.just_the_docs.collections %}
.site-nav > ul.nav-category-list > li > button svg,
{% endif -%}
{{ activation_collection_prefix }} > li:nth-child({{ nav_levels[1] }}) > button svg
{%- if nav_levels[2] -%},
{{ activation_collection_prefix }} > li:nth-child({{ nav_levels[1] }}) > ul > li:nth-child({{ nav_levels[2] }}) > button svg
{%- endif %} {
transform: rotate(-90deg);
}
{%- if site.just_the_docs.collections %}
.site-nav > ul.nav-category-list > li.nav-list-item > ul.nav-list,
{% endif -%}
{{ activation_collection_prefix }} > li.nav-list-item:nth-child({{ nav_levels[1] }}) > ul.nav-list
{%- if nav_levels[2] %},
{{ activation_collection_prefix }} > li.nav-list-item:nth-child({{ nav_levels[1] }}) > ul.nav-list > li.nav-list-item:nth-child({{ nav_levels[2] }}) > ul.nav-list
{%- endif %} {
display: block;
}
{{ activation_no_nav_link }}
{%- endif -%}
{%- endif -%}

View File

@ -28,7 +28,7 @@ layout: table_wrappers
{{ content }}
{% endif %}
{% if page.has_children == true and page.has_toc != false %}
{% if page.has_toc != false %}
{% include components/children_nav.html %}
{% endif %}
</main>