Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Allow unlimited multi-level navigation #1431

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c45f622
Allow unlimited multi-level navigation
pdmosses Feb 24, 2024
4919cb1
Update Gemfile.lock
pdmosses Feb 24, 2024
f9db7fd
Update Gemfile
pdmosses Feb 24, 2024
7b99d02
Add `jekyll-default-layout` in gemspec
pdmosses Feb 24, 2024
1a02812
Correct link filename case
pdmosses Feb 24, 2024
9e1fce3
Add the `jekyll-default-layout` gem in fixtures
pdmosses Feb 25, 2024
5c7d2d1
Remove `has_children` test
pdmosses Feb 25, 2024
8dc6153
Add to comment
pdmosses Feb 25, 2024
47a5c79
Replace condition by size test
pdmosses Feb 25, 2024
655d11a
Detect cyclic parenthood
pdmosses Feb 25, 2024
25e412e
Filter child pages for ancestors
pdmosses Feb 26, 2024
6cdc654
Avoid Jekyll 4 syntax
pdmosses Feb 26, 2024
5da5c87
Adjust generated line breaks
pdmosses Feb 27, 2024
c4f762a
Filter children
pdmosses Feb 27, 2024
eecba73
Filter children
pdmosses Feb 27, 2024
f50c207
Fix `child_nav_order`
pdmosses Feb 27, 2024
2ee3af5
Add quick check for no children
pdmosses Feb 28, 2024
a649a47
Support `has_children: false`
pdmosses Feb 28, 2024
c31d6e1
Fix breadcrumbs for excluded pages
pdmosses Mar 4, 2024
c832248
Add nav_error_report warning in main navigation
pdmosses Mar 9, 2024
9be113b
Fix breadcrumbs for excluded pages
pdmosses Mar 9, 2024
c825c8b
Merge remote-tracking branch 'upstream/main' into multi-level
pdmosses Mar 9, 2024
2b6d919
Add nav_error_report setting
pdmosses Mar 9, 2024
6061496
Control flow simplified
pdmosses Mar 11, 2024
ce52b12
Update to multi-level
pdmosses Mar 12, 2024
53b47c3
Optimise breadcrumbs for excluded pages
pdmosses Mar 13, 2024
d3e5290
Cache site-nav with links to all pages
pdmosses Mar 15, 2024
7deaf18
Remove redundant code
pdmosses Mar 15, 2024
f454aee
Avoid code duplication
pdmosses Mar 15, 2024
00b182d
Clarify comments
pdmosses Mar 17, 2024
bf2230c
Remove docs changes
pdmosses Mar 17, 2024
c744937
Update Gemfile.lock
pdmosses Mar 17, 2024
61a97d0
Merge branch 'main' into multi-level
mattxwang Apr 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions _config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
168 changes: 60 additions & 108 deletions _includes/components/breadcrumbs.html
Original file line number Diff line number Diff line change
@@ -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_link_class %} class="nav-list-link">
{%- 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>" -%}

{%- 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 -%}
{%- capture nav_list_simple -%}
<ul class="nav-list">
{%- endcapture -%}

{%- assign nav_parent_link = nav_breadcrumbs[-1] -%}
{%- assign nav_grandparent_link = nav_breadcrumbs[-2] -%}
{%- capture nav_list_link_class %} class="nav-list-link">
{%- endcapture -%}

{%- else -%}
{%- capture nav_category -%}
<div class="nav-category">
{%- endcapture -%}

{%- 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 nav_anchor_splits =
site_nav | split: nav_list_link |
first | split: nav_category |
last | split: "</a>" -%}

{%- 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.

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.

{%- if node.has_children and page.grand_parent -%}
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 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 -%}
{%- assign nav_breadcrumbs = "" | split: "" -%}

{%- elsif node.has_children and node.title == page.parent and node.parent == nil -%}
{%- for nav_split in nav_anchor_splits -%}
{%- unless forloop.last -%}

{%- assign parent_page = node -%}
{%- break -%}
{%- assign nav_split_next = nav_anchor_splits[forloop.index] | strip -%}

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

{%- capture nav_parent_link -%}
<a href="{{ parent_page.url | relative_url }}">{{ page.parent }}</a>
{%- endcapture -%}

{%- if page.grand_parent %}
{%- capture nav_grandparent_link -%}
<a href="{{ grandparent_page.url | relative_url }}">{{ page.grand_parent }}</a>
{%- endcapture -%}
{%- endif -%}

{%- 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 -%}
88 changes: 72 additions & 16 deletions _includes/components/children_nav.html
Original file line number Diff line number Diff line change
@@ -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 -%}

{%- if page.child_nav_order == 'desc' or page.child_nav_order == 'reversed' -%}
{%- assign sorted_pages = sorted_pages | reverse -%}
{%- 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 nav_children = nav_children | reverse -%}
{%- 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 -%}
75 changes: 0 additions & 75 deletions _includes/components/nav.html

This file was deleted.