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
12 changed files with 526 additions and 464 deletions

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 -%}