Merge pull request #2822 from ehuss/dynamic-toc
Add sidebar heading navigation
This commit is contained in:
commit
06af133838
24 changed files with 732 additions and 6 deletions
|
|
@ -485,6 +485,9 @@ pub struct HtmlConfig {
|
||||||
///
|
///
|
||||||
/// The default is `true`.
|
/// The default is `true`.
|
||||||
pub hash_files: bool,
|
pub hash_files: bool,
|
||||||
|
/// If enabled, the sidebar includes navigation for headers on the current
|
||||||
|
/// page. Default is `true`.
|
||||||
|
pub sidebar_header_nav: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HtmlConfig {
|
impl Default for HtmlConfig {
|
||||||
|
|
@ -512,6 +515,7 @@ impl Default for HtmlConfig {
|
||||||
live_reload_endpoint: None,
|
live_reload_endpoint: None,
|
||||||
redirect: HashMap::new(),
|
redirect: HashMap::new(),
|
||||||
hash_files: true,
|
hash_files: true,
|
||||||
|
sidebar_header_nav: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -730,3 +730,21 @@ html:not(.sidebar-resizing) .sidebar {
|
||||||
/* mdbook's margin for h2 is way too large. */
|
/* mdbook's margin for h2 is way too large. */
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.current-header {
|
||||||
|
/* Allows the circle positioning. */
|
||||||
|
position: relative
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Places a circle just before the current header in the sidebar. */
|
||||||
|
.current-header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -16px;
|
||||||
|
top: 0;
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--sidebar-active);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,3 +72,312 @@ class MDBookSidebarScrollbox extends HTMLElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.customElements.define('mdbook-sidebar-scrollbox', MDBookSidebarScrollbox);
|
window.customElements.define('mdbook-sidebar-scrollbox', MDBookSidebarScrollbox);
|
||||||
|
|
||||||
|
{{#if sidebar_header_nav}}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Support for dynamically adding headers to the sidebar.
|
||||||
|
|
||||||
|
// This is a debugging tool for the threshold which you can enable in the console.
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
let mdbookThresholdDebug = false;
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
// This is used to detect which direction the page has scrolled since the
|
||||||
|
// last scroll event.
|
||||||
|
let lastKnownScrollPosition = 0;
|
||||||
|
// This is the threshold in px from the top of the screen where it will
|
||||||
|
// consider a header the "current" header when scrolling down.
|
||||||
|
const defaultDownThreshold = 150;
|
||||||
|
// Same as defaultDownThreshold, except when scrolling up.
|
||||||
|
const defaultUpThreshold = 300;
|
||||||
|
// The threshold is a virtual horizontal line on the screen where it
|
||||||
|
// considers the "current" header to be above the line. The threshold is
|
||||||
|
// modified dynamically to handle headers that are near the bottom of the
|
||||||
|
// screen, and to slightly offset the behavior when scrolling up vs down.
|
||||||
|
let threshold = defaultDownThreshold;
|
||||||
|
// This is used to disable updates while scrolling. This is needed when
|
||||||
|
// clicking the header in the sidebar, which triggers a scroll event. It
|
||||||
|
// is somewhat finicky to detect when the scroll has finished, so this
|
||||||
|
// uses a relatively dumb system of disabling scroll updates for a short
|
||||||
|
// time after the click.
|
||||||
|
let disableScroll = false;
|
||||||
|
// Array of header elements on the page.
|
||||||
|
let headers;
|
||||||
|
// Array of li elements that are initially collapsed headers in the sidebar.
|
||||||
|
// I'm not sure why eslint seems to have a false positive here.
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
let headerToggles = [];
|
||||||
|
|
||||||
|
function drawDebugLine() {
|
||||||
|
if (!document.body) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = 'mdbook-threshold-debug-line';
|
||||||
|
const existingLine = document.getElementById(id);
|
||||||
|
if (existingLine) {
|
||||||
|
existingLine.remove();
|
||||||
|
}
|
||||||
|
const line = document.createElement('div');
|
||||||
|
line.id = id;
|
||||||
|
line.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: ${threshold}px;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 2px;
|
||||||
|
background-color: red;
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: none;
|
||||||
|
`;
|
||||||
|
document.body.appendChild(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the threshold based on the scroll position.
|
||||||
|
function updateThreshold() {
|
||||||
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
const documentHeight = document.documentElement.scrollHeight;
|
||||||
|
// The number of pixels below the viewport, at most documentHeight.
|
||||||
|
// This is used to push the threshold down to the bottom of the page
|
||||||
|
// as the user scrolls towards the bottom.
|
||||||
|
const pixelsBelow = Math.max(0, documentHeight - (scrollTop + windowHeight));
|
||||||
|
// The number of pixels above the viewport, at most defaultDownThreshold.
|
||||||
|
// Similar to pixelsBelow, this is used to push the threshold back towards
|
||||||
|
// the top when reaching the top of the page.
|
||||||
|
const pixelsAbove = Math.max(0, defaultDownThreshold - scrollTop);
|
||||||
|
// How much the threshold should be offset once it gets close to the
|
||||||
|
// bottom of the page.
|
||||||
|
let bottomAdd = Math.max(0, windowHeight - pixelsBelow - defaultDownThreshold);
|
||||||
|
|
||||||
|
// Adjusts bottomAdd for a small document. The calculation above
|
||||||
|
// assumes the document is at least twice the windowheight in size. If
|
||||||
|
// it is less than that, then bottomAdd needs to be shrunk
|
||||||
|
// proportional to the difference in size.
|
||||||
|
if (documentHeight < windowHeight * 2) {
|
||||||
|
const maxPixelsBelow = documentHeight - windowHeight;
|
||||||
|
const t = 1 - pixelsBelow / maxPixelsBelow;
|
||||||
|
const clamp = Math.max(0, Math.min(1, t));
|
||||||
|
bottomAdd *= clamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scrollingDown = true;
|
||||||
|
if (scrollTop < lastKnownScrollPosition) {
|
||||||
|
scrollingDown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollingDown) {
|
||||||
|
// When scrolling down, move the threshold up towards the default
|
||||||
|
// downwards threshold position. If near the bottom of the page,
|
||||||
|
// bottomAdd will offset the threshold towards the bottom of the
|
||||||
|
// page.
|
||||||
|
const amountScrolledDown = scrollTop - lastKnownScrollPosition;
|
||||||
|
const adjustedDefault = defaultDownThreshold + bottomAdd;
|
||||||
|
threshold = Math.max(adjustedDefault, threshold - amountScrolledDown);
|
||||||
|
} else {
|
||||||
|
// When scrolling up, move the threshold down towards the default
|
||||||
|
// upwards threshold position. If near the bottom of the page,
|
||||||
|
// quickly transition the threshold back up where it normally
|
||||||
|
// belongs.
|
||||||
|
const amountScrolledUp = lastKnownScrollPosition - scrollTop;
|
||||||
|
const adjustedDefault = defaultUpThreshold - pixelsAbove
|
||||||
|
+ Math.max(0, bottomAdd - defaultDownThreshold);
|
||||||
|
threshold = Math.min(adjustedDefault, threshold + amountScrolledUp);
|
||||||
|
}
|
||||||
|
lastKnownScrollPosition = scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates which headers in the sidebar should be expanded. If the current
|
||||||
|
// header is inside a collapsed group, then it, and all its parents should
|
||||||
|
// be expanded.
|
||||||
|
function updateHeaderExpanded(currentA) {
|
||||||
|
// Add expanded to all header-item li ancestors.
|
||||||
|
let current = currentA.parentElement.parentElement.parentElement;
|
||||||
|
while (current.tagName === 'LI') {
|
||||||
|
const prevSibling = current.previousElementSibling;
|
||||||
|
if (prevSibling !== null
|
||||||
|
&& prevSibling.tagName === 'LI'
|
||||||
|
&& prevSibling.classList.contains('header-item')) {
|
||||||
|
prevSibling.classList.add('expanded');
|
||||||
|
current = prevSibling.parentElement.parentElement;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates which header is marked as the "current" header in the sidebar.
|
||||||
|
// This is done with a virtual Y threshold, where headers at or below
|
||||||
|
// that line will be considered the current one.
|
||||||
|
function updateCurrentHeader() {
|
||||||
|
if (mdbookThresholdDebug) {
|
||||||
|
drawDebugLine();
|
||||||
|
}
|
||||||
|
if (!headers || !headers.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the classes, which will be rebuilt below.
|
||||||
|
const els = document.getElementsByClassName('current-header');
|
||||||
|
for (const el of els) {
|
||||||
|
el.classList.remove('current-header');
|
||||||
|
}
|
||||||
|
for (const toggle of headerToggles) {
|
||||||
|
toggle.classList.remove('expanded');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the last header that is above the threshold.
|
||||||
|
let lastHeader = null;
|
||||||
|
for (const header of headers) {
|
||||||
|
const rect = header.getBoundingClientRect();
|
||||||
|
if (rect.top <= threshold) {
|
||||||
|
lastHeader = header;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastHeader === null) {
|
||||||
|
lastHeader = headers[0];
|
||||||
|
const rect = lastHeader.getBoundingClientRect();
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
if (rect.top >= windowHeight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the anchor in the summary.
|
||||||
|
const href = '#' + lastHeader.id;
|
||||||
|
const a = [...document.querySelectorAll('.header-in-summary')]
|
||||||
|
.find(element => element.getAttribute('href') === href);
|
||||||
|
if (!a) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.classList.add('current-header');
|
||||||
|
|
||||||
|
updateHeaderExpanded(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates which header is "current" based on the threshold line.
|
||||||
|
function reloadCurrentHeader() {
|
||||||
|
if (disableScroll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateThreshold();
|
||||||
|
updateCurrentHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// When clicking on a header in the sidebar, this adjusts the threshold so
|
||||||
|
// that it is located next to the header. This is so that header becomes
|
||||||
|
// "current".
|
||||||
|
function headerThresholdClick(event) {
|
||||||
|
// See disableScroll description why this is done.
|
||||||
|
disableScroll = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
disableScroll = false;
|
||||||
|
}, 100);
|
||||||
|
// requestAnimationFrame is used to delay the update of the "current"
|
||||||
|
// header until after the scroll is done, and the header is in the new
|
||||||
|
// position.
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
// Closest is needed because if it has child elements like <code>.
|
||||||
|
const a = event.target.closest('a');
|
||||||
|
const href = a.getAttribute('href');
|
||||||
|
const targetId = href.substring(1);
|
||||||
|
const targetElement = document.getElementById(targetId);
|
||||||
|
if (targetElement) {
|
||||||
|
threshold = targetElement.getBoundingClientRect().bottom;
|
||||||
|
updateCurrentHeader();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scans page for headers and adds them to the sidebar.
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const activeSection = document.querySelector('#mdbook-sidebar .active');
|
||||||
|
if (activeSection === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const activeItem = activeSection.parentElement;
|
||||||
|
const activeList = activeItem.parentElement;
|
||||||
|
|
||||||
|
// Build a tree of headers in the sidebar.
|
||||||
|
const rootLi = document.createElement('li');
|
||||||
|
rootLi.classList.add('header-item');
|
||||||
|
rootLi.classList.add('expanded');
|
||||||
|
const rootOl = document.createElement('ol');
|
||||||
|
rootOl.classList.add('section');
|
||||||
|
rootLi.appendChild(rootOl);
|
||||||
|
const stack = [{ level: 0, ol: rootOl }];
|
||||||
|
// The level where it will start folding deeply nested headers.
|
||||||
|
const foldLevel = 3;
|
||||||
|
|
||||||
|
const main = document.getElementsByTagName('main')[0];
|
||||||
|
headers = Array.from(main.querySelectorAll('h2, h3, h4, h5, h6'))
|
||||||
|
.filter(h => h.id !== '' && h.children.length && h.children[0].tagName === 'A');
|
||||||
|
|
||||||
|
if (headers.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < headers.length; i++) {
|
||||||
|
const header = headers[i];
|
||||||
|
const level = parseInt(header.tagName.charAt(1));
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.classList.add('header-item');
|
||||||
|
li.classList.add('expanded');
|
||||||
|
if (level < foldLevel) {
|
||||||
|
li.classList.add('expanded');
|
||||||
|
}
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = '#' + header.id;
|
||||||
|
a.classList.add('header-in-summary');
|
||||||
|
a.innerHTML = header.children[0].innerHTML;
|
||||||
|
a.addEventListener('click', headerThresholdClick);
|
||||||
|
li.appendChild(a);
|
||||||
|
const nextHeader = headers[i + 1];
|
||||||
|
if (nextHeader !== undefined) {
|
||||||
|
const nextLevel = parseInt(nextHeader.tagName.charAt(1));
|
||||||
|
if (nextLevel > level && level >= foldLevel) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = '❱';
|
||||||
|
const toggle = document.createElement('a');
|
||||||
|
toggle.classList.add('toggle');
|
||||||
|
toggle.classList.add('header-toggle');
|
||||||
|
toggle.appendChild(div);
|
||||||
|
toggle.addEventListener('click', () => {
|
||||||
|
li.classList.toggle('expanded');
|
||||||
|
});
|
||||||
|
li.appendChild(toggle);
|
||||||
|
headerToggles.push(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the appropriate parent level.
|
||||||
|
while (stack.length > 1 && stack[stack.length - 1].level >= level) {
|
||||||
|
stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentParent = stack[stack.length - 1];
|
||||||
|
currentParent.ol.appendChild(li);
|
||||||
|
|
||||||
|
// Create new nested ol for potential children.
|
||||||
|
const nestedOl = document.createElement('ol');
|
||||||
|
nestedOl.classList.add('section');
|
||||||
|
const nestedLi = document.createElement('li');
|
||||||
|
nestedLi.appendChild(nestedOl);
|
||||||
|
currentParent.ol.appendChild(nestedLi);
|
||||||
|
stack.push({ level: level, ol: nestedOl });
|
||||||
|
}
|
||||||
|
|
||||||
|
activeList.insertBefore(rootLi, activeItem.nextSibling);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', reloadCurrentHeader);
|
||||||
|
document.addEventListener('scroll', reloadCurrentHeader, { passive: true });
|
||||||
|
})();
|
||||||
|
|
||||||
|
{{/if}}
|
||||||
|
|
|
||||||
|
|
@ -615,6 +615,10 @@ fn make_data(
|
||||||
data.insert("print_enable".to_owned(), json!(html_config.print.enable));
|
data.insert("print_enable".to_owned(), json!(html_config.print.enable));
|
||||||
data.insert("fold_enable".to_owned(), json!(html_config.fold.enable));
|
data.insert("fold_enable".to_owned(), json!(html_config.fold.enable));
|
||||||
data.insert("fold_level".to_owned(), json!(html_config.fold.level));
|
data.insert("fold_level".to_owned(), json!(html_config.fold.level));
|
||||||
|
data.insert(
|
||||||
|
"sidebar_header_nav".to_owned(),
|
||||||
|
json!(html_config.sidebar_header_nav),
|
||||||
|
);
|
||||||
|
|
||||||
let search = html_config.search.clone();
|
let search = html_config.search.clone();
|
||||||
if cfg!(feature = "search") {
|
if cfg!(feature = "search") {
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@ edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path
|
||||||
site-url = "/example-book/"
|
site-url = "/example-book/"
|
||||||
cname = "myproject.rs"
|
cname = "myproject.rs"
|
||||||
input-404 = "not-found.md"
|
input-404 = "not-found.md"
|
||||||
|
sidebar-header-nav = true
|
||||||
```
|
```
|
||||||
|
|
||||||
The following configuration options are available:
|
The following configuration options are available:
|
||||||
|
|
@ -166,6 +167,7 @@ The following configuration options are available:
|
||||||
Chapter HTML files are not renamed.
|
Chapter HTML files are not renamed.
|
||||||
Static CSS and JS files can reference each other using `{{ resource "filename" }}` directives.
|
Static CSS and JS files can reference each other using `{{ resource "filename" }}` directives.
|
||||||
Defaults to `true`.
|
Defaults to `true`.
|
||||||
|
- **sidebar-header-nav:** If `true`, the sidebar will contain navigation for headers on the current page. Default is `true`.
|
||||||
|
|
||||||
[custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site
|
[custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browser-ui-test": "0.21.1",
|
"browser-ui-test": "0.21.2",
|
||||||
"eslint": "^9.34.0"
|
"eslint": "^9.34.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,14 @@
|
||||||
- [Syntax Highlight](languages/highlight.md)
|
- [Syntax Highlight](languages/highlight.md)
|
||||||
- [Rust Specific](rust/README.md)
|
- [Rust Specific](rust/README.md)
|
||||||
- [Rust Codeblocks](rust/rust_codeblock.md)
|
- [Rust Codeblocks](rust/rust_codeblock.md)
|
||||||
|
- [Heading Navigation](headings/README.md)
|
||||||
|
- [Empty page](headings/empty.md)
|
||||||
|
- [Large text before first heading](headings/large-intro.md)
|
||||||
|
- [Normal text before first heading](headings/normal-intro.md)
|
||||||
|
- [Collapsed headings](headings/collapsed.md)
|
||||||
|
- [Headings with markup](headings/markup.md)
|
||||||
|
- [Current scrolls to bottom](headings/current-to-bottom.md)
|
||||||
|
- [Last numbered chapter](last.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
1
test_book/src/headings/README.md
Normal file
1
test_book/src/headings/README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Heading Navigation
|
||||||
91
test_book/src/headings/collapsed.md
Normal file
91
test_book/src/headings/collapsed.md
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
# Collapsed headings
|
||||||
|
|
||||||
|
Tests collapsed headings.
|
||||||
|
|
||||||
|
## Heading 1
|
||||||
|
|
||||||
|
1\
|
||||||
|
2\
|
||||||
|
3\
|
||||||
|
4\
|
||||||
|
5
|
||||||
|
|
||||||
|
### Heading 1.1
|
||||||
|
|
||||||
|
1\
|
||||||
|
2\
|
||||||
|
3\
|
||||||
|
4\
|
||||||
|
5
|
||||||
|
|
||||||
|
### Heading 1.2
|
||||||
|
|
||||||
|
1\
|
||||||
|
2\
|
||||||
|
3\
|
||||||
|
4\
|
||||||
|
5
|
||||||
|
|
||||||
|
#### Heading 1.2.1
|
||||||
|
|
||||||
|
1\
|
||||||
|
2\
|
||||||
|
3\
|
||||||
|
4\
|
||||||
|
5
|
||||||
|
|
||||||
|
#### Heading 1.2.2
|
||||||
|
|
||||||
|
1\
|
||||||
|
2\
|
||||||
|
3\
|
||||||
|
4\
|
||||||
|
5
|
||||||
|
|
||||||
|
### Heading 1.3
|
||||||
|
|
||||||
|
1\
|
||||||
|
2\
|
||||||
|
3\
|
||||||
|
4\
|
||||||
|
5
|
||||||
|
|
||||||
|
## Heading 2
|
||||||
|
|
||||||
|
1\
|
||||||
|
2\
|
||||||
|
3\
|
||||||
|
4\
|
||||||
|
5
|
||||||
|
|
||||||
|
### Heading 2.1
|
||||||
|
|
||||||
|
1\
|
||||||
|
2\
|
||||||
|
3\
|
||||||
|
4\
|
||||||
|
5
|
||||||
|
|
||||||
|
#### Heading 2.1.1
|
||||||
|
|
||||||
|
1\
|
||||||
|
2\
|
||||||
|
3\
|
||||||
|
4\
|
||||||
|
5
|
||||||
|
|
||||||
|
##### Heading 2.1.1.1
|
||||||
|
|
||||||
|
1\
|
||||||
|
2\
|
||||||
|
3\
|
||||||
|
4\
|
||||||
|
5
|
||||||
|
|
||||||
|
###### Heading 2.1.1.1.1
|
||||||
|
|
||||||
|
1\
|
||||||
|
2\
|
||||||
|
3\
|
||||||
|
4\
|
||||||
|
5
|
||||||
46
test_book/src/headings/current-to-bottom.md
Normal file
46
test_book/src/headings/current-to-bottom.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Current scrolls to bottom
|
||||||
|
|
||||||
|
Checks that the "current" header works even when there are headers near the bottom.
|
||||||
|
|
||||||
|
## First header
|
||||||
|
|
||||||
|
<span id="scroll-to-1">1</span>\
|
||||||
|
<span id="scroll-to-2">2</span>\
|
||||||
|
<span id="scroll-to-3">3</span>\
|
||||||
|
<span id="scroll-to-4">4</span>\
|
||||||
|
<span id="scroll-to-5">5</span>\
|
||||||
|
<span id="scroll-to-6">6</span>\
|
||||||
|
<span id="scroll-to-7">7</span>\
|
||||||
|
<span id="scroll-to-8">8</span>\
|
||||||
|
<span id="scroll-to-9">9</span>\
|
||||||
|
<span id="scroll-to-10">10</span>\
|
||||||
|
<span id="scroll-to-11">11</span>\
|
||||||
|
<span id="scroll-to-12">12</span>\
|
||||||
|
<span id="scroll-to-13">13</span>\
|
||||||
|
<span id="scroll-to-14">14</span>\
|
||||||
|
<span id="scroll-to-15">15</span>\
|
||||||
|
<span id="scroll-to-16">16</span>\
|
||||||
|
<span id="scroll-to-17">17</span>\
|
||||||
|
<span id="scroll-to-18">18</span>\
|
||||||
|
<span id="scroll-to-19">19</span>\
|
||||||
|
<span id="scroll-to-20">20</span>
|
||||||
|
|
||||||
|
## Second header
|
||||||
|
|
||||||
|
<span id="scroll-to-21">21</span>
|
||||||
|
|
||||||
|
### Second sub-header
|
||||||
|
|
||||||
|
<span id="scroll-to-22">22</span>
|
||||||
|
|
||||||
|
## Third header
|
||||||
|
|
||||||
|
<span id="scroll-to-23">23</span>
|
||||||
|
|
||||||
|
## Fourth header
|
||||||
|
|
||||||
|
<span id="scroll-to-24">24</span>
|
||||||
|
|
||||||
|
## Fifth header
|
||||||
|
|
||||||
|
<span id="scroll-to-25">25</span>
|
||||||
1
test_book/src/headings/empty.md
Normal file
1
test_book/src/headings/empty.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Empty page
|
||||||
47
test_book/src/headings/large-intro.md
Normal file
47
test_book/src/headings/large-intro.md
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Large text before first heading
|
||||||
|
|
||||||
|
This tests what happens if there is a lot of text before the first header, which is off the bottom of the screen.
|
||||||
|
|
||||||
|
1
|
||||||
|
|
||||||
|
2
|
||||||
|
|
||||||
|
3
|
||||||
|
|
||||||
|
4
|
||||||
|
|
||||||
|
5
|
||||||
|
|
||||||
|
6
|
||||||
|
|
||||||
|
7
|
||||||
|
|
||||||
|
8
|
||||||
|
|
||||||
|
9
|
||||||
|
|
||||||
|
10
|
||||||
|
|
||||||
|
11
|
||||||
|
|
||||||
|
12
|
||||||
|
|
||||||
|
13
|
||||||
|
|
||||||
|
14
|
||||||
|
|
||||||
|
15
|
||||||
|
|
||||||
|
16
|
||||||
|
|
||||||
|
17
|
||||||
|
|
||||||
|
18
|
||||||
|
|
||||||
|
19
|
||||||
|
|
||||||
|
20
|
||||||
|
|
||||||
|
## First header
|
||||||
|
|
||||||
|
Text for first header.
|
||||||
19
test_book/src/headings/markup.md
Normal file
19
test_book/src/headings/markup.md
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Headings with markup
|
||||||
|
|
||||||
|
Tests that heading markup gets copied to the sidebar.
|
||||||
|
|
||||||
|
## Heading with `code` or *italic* or **bold** or ~~strike~~
|
||||||
|
|
||||||
|
Basic markup should be copied.
|
||||||
|
|
||||||
|
## Heading with a [link](../index.html)
|
||||||
|
|
||||||
|
Probably not super-wise to have headings with links, but at least they shouldn't explode.
|
||||||
|
|
||||||
|
## Heading with a custom id { #custom-id .custom-class }
|
||||||
|
|
||||||
|
Make sure navigation works on a custom id.
|
||||||
|
|
||||||
|
## Heading with <span>html</span>
|
||||||
|
|
||||||
|
What happens if there is inline HTML?
|
||||||
15
test_book/src/headings/normal-intro.md
Normal file
15
test_book/src/headings/normal-intro.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Normal text before first heading
|
||||||
|
|
||||||
|
This test is to ensure the first heading shows up as "current" on page load.
|
||||||
|
|
||||||
|
## The first heading
|
||||||
|
|
||||||
|
1
|
||||||
|
|
||||||
|
2
|
||||||
|
|
||||||
|
3
|
||||||
|
|
||||||
|
## The second heading
|
||||||
|
|
||||||
|
### And a sub heading
|
||||||
1
test_book/src/last.md
Normal file
1
test_book/src/last.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Last numbered chapter
|
||||||
51
tests/gui/heading-nav-collapsed.goml
Normal file
51
tests/gui/heading-nav-collapsed.goml
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
// Tests for collapsed heading sidebar navigation.
|
||||||
|
|
||||||
|
set-window-size: (1400, 800)
|
||||||
|
go-to: |DOC_PATH| + "headings/collapsed.html"
|
||||||
|
|
||||||
|
assert-count: (".header-item", 12)
|
||||||
|
assert-count: (".current-header", 1)
|
||||||
|
assert-text: (".current-header", "Heading 1")
|
||||||
|
// Collapsed elements do not have "expanded" class.
|
||||||
|
assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item"})
|
||||||
|
assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item"})
|
||||||
|
|
||||||
|
// Click 1.2, doesn't change expanded.
|
||||||
|
click: "a.header-in-summary[href='#heading-12']"
|
||||||
|
assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item"})
|
||||||
|
assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item"})
|
||||||
|
assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-12']]]/ol", {"display": "none"})
|
||||||
|
// Click expand chevron.
|
||||||
|
// 1.2.1 and 1.2.2 should be visible
|
||||||
|
click: "a.header-in-summary[href='#heading-12'] ~ a.header-toggle"
|
||||||
|
assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item expanded"})
|
||||||
|
assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item"})
|
||||||
|
assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-12']]]/ol", {"display": "block"})
|
||||||
|
|
||||||
|
// Click 1.3
|
||||||
|
click: "a.header-in-summary[href='#heading-13']"
|
||||||
|
// Everything should be collapsed
|
||||||
|
assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item"})
|
||||||
|
assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item"})
|
||||||
|
assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-12']]]/ol", {"display": "none"})
|
||||||
|
assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-21']]]/ol", {"display": "none"})
|
||||||
|
|
||||||
|
|
||||||
|
assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item"})
|
||||||
|
assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item"})
|
||||||
|
assert-attribute: ("li:has(> a[href='#heading-211'])", {"class": "header-item"})
|
||||||
|
assert-attribute: ("li:has(> a[href='#heading-2111'])", {"class": "header-item"})
|
||||||
|
// Scroll to bottom of page
|
||||||
|
press-key: 'PageDown'
|
||||||
|
press-key: 'PageDown'
|
||||||
|
press-key: 'PageDown'
|
||||||
|
press-key: 'PageDown'
|
||||||
|
// 2.1.1.1.1 should be visible, and all the chevrons should be open, and expanded should be on each one
|
||||||
|
assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item"})
|
||||||
|
assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item expanded"})
|
||||||
|
assert-attribute: ("li:has(> a[href='#heading-211'])", {"class": "header-item expanded"})
|
||||||
|
assert-attribute: ("li:has(> a[href='#heading-2111'])", {"class": "header-item expanded"})
|
||||||
|
assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-12']]]/ol", {"display": "none"})
|
||||||
|
assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-21']]]/ol", {"display": "block"})
|
||||||
|
assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-211']]]/ol", {"display": "block"})
|
||||||
|
assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-2111']]]/ol", {"display": "block"})
|
||||||
58
tests/gui/heading-nav-current-to-bottom.goml
Normal file
58
tests/gui/heading-nav-current-to-bottom.goml
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
// Checks that the "current" header works even when there are headers near the
|
||||||
|
// bottom.
|
||||||
|
|
||||||
|
set-window-size: (1400, 800)
|
||||||
|
go-to: |DOC_PATH| + "headings/current-to-bottom.html"
|
||||||
|
assert-count: (".current-header", 1)
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
|
||||||
|
scroll-to: "#scroll-to-1"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-2"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-3"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-4"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-5"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-6"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-7"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-8"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-9"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-10"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-11"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-12"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-13"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-14"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-15"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-16"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-17"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-18"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-19"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-20"
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
scroll-to: "#scroll-to-21"
|
||||||
|
wait-for-text: (".current-header", "Second sub-header")
|
||||||
|
scroll-to: "#scroll-to-22"
|
||||||
|
assert-text: (".current-header", "Second sub-header")
|
||||||
|
scroll-to: "#scroll-to-23"
|
||||||
|
assert-text: (".current-header", "Second sub-header")
|
||||||
|
scroll-to: "#scroll-to-24"
|
||||||
|
assert-text: (".current-header", "Second sub-header")
|
||||||
|
scroll-to: "#scroll-to-25"
|
||||||
|
wait-for-text: (".current-header", "Fifth header")
|
||||||
6
tests/gui/heading-nav-empty.goml
Normal file
6
tests/gui/heading-nav-empty.goml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// When there aren't any headings, there shouldn't be any header items in the sidebar.
|
||||||
|
|
||||||
|
set-window-size: (1400, 800)
|
||||||
|
go-to: |DOC_PATH| + "headings/empty.html"
|
||||||
|
assert-count: (".header-item", 0)
|
||||||
|
assert-count: (".current-header", 0)
|
||||||
15
tests/gui/heading-nav-large-intro.goml
Normal file
15
tests/gui/heading-nav-large-intro.goml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
// When there is a large intro, there shouldn't be any "current" headers until
|
||||||
|
// you scroll down and make it visible on screen.
|
||||||
|
|
||||||
|
set-window-size: (1400, 800)
|
||||||
|
go-to: |DOC_PATH| + "headings/large-intro.html"
|
||||||
|
assert-count: (".header-item", 2)
|
||||||
|
assert-count: (".current-header", 0)
|
||||||
|
|
||||||
|
scroll-to: "#first-header"
|
||||||
|
wait-for-count: (".current-header", 1)
|
||||||
|
assert-text: (".current-header", "First header")
|
||||||
|
|
||||||
|
// Scrolling back to the top should set it to 0.
|
||||||
|
scroll-to: (0, 0)
|
||||||
|
wait-for-count: (".current-header", 0)
|
||||||
19
tests/gui/heading-nav-markup.goml
Normal file
19
tests/gui/heading-nav-markup.goml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
// When a header has various markup, the sidebar should replicate it.
|
||||||
|
|
||||||
|
set-window-size: (1400, 800)
|
||||||
|
go-to: |DOC_PATH| + "headings/markup.html"
|
||||||
|
|
||||||
|
assert-count: (".header-item", 5)
|
||||||
|
assert-count: (".current-header", 1)
|
||||||
|
assert-text: (".current-header", "Heading with code or italic or bold or strike")
|
||||||
|
assert-property: (".current-header", {"innerHTML": "Heading with <code>code</code> or <em>italic</em> or <strong>bold</strong> or <del>strike</del>"})
|
||||||
|
|
||||||
|
// Clicking the custom one should work and should make it current.
|
||||||
|
click: "a.header-in-summary[href='#custom-id']"
|
||||||
|
assert-count: (".current-header", 1)
|
||||||
|
assert-text: (".current-header", "Heading with a custom id")
|
||||||
|
|
||||||
|
// Click the one with HTML, and check it.
|
||||||
|
click: "a.header-in-summary[href='#heading-with-html']"
|
||||||
|
assert-count: (".current-header", 1)
|
||||||
|
assert-text: (".current-header", "Heading with html")
|
||||||
11
tests/gui/heading-nav-normal-intro.goml
Normal file
11
tests/gui/heading-nav-normal-intro.goml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
// When there is a normal-sized intro, when the page loads the first heading
|
||||||
|
// should be "current".
|
||||||
|
|
||||||
|
set-window-size: (1400, 800)
|
||||||
|
go-to: |DOC_PATH| + "headings/normal-intro.html"
|
||||||
|
assert-count: (".header-item", 4)
|
||||||
|
assert-count: (".current-header", 1)
|
||||||
|
assert-text: (".current-header", "The first heading")
|
||||||
|
|
||||||
|
click: "a[href='#and-a-sub-heading']"
|
||||||
|
wait-for-text: (".current-header", "And a sub heading")
|
||||||
|
|
@ -24,8 +24,8 @@ press-key: 'ArrowRight'
|
||||||
assert-text: ("title", "Heading - mdBook test book")
|
assert-text: ("title", "Heading - mdBook test book")
|
||||||
|
|
||||||
// Last numbered page
|
// Last numbered page
|
||||||
go-to: "../rust/rust_codeblock.html"
|
go-to: "../last.html"
|
||||||
assert-text: ("title", "Rust Codeblocks - mdBook test book")
|
assert-text: ("title", "Last numbered chapter - mdBook test book")
|
||||||
|
|
||||||
// Go to the suffix chapter
|
// Go to the suffix chapter
|
||||||
press-key: 'ArrowRight'
|
press-key: 'ArrowRight'
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ assert-css: ("#mdbook-search-wrapper", {"display": "none"})
|
||||||
press-key: 's'
|
press-key: 's'
|
||||||
wait-for-css-false: ("#mdbook-search-wrapper", {"display": "none"})
|
wait-for-css-false: ("#mdbook-search-wrapper", {"display": "none"})
|
||||||
// We ensure the search bar has the focus.
|
// We ensure the search bar has the focus.
|
||||||
assert: "#mdbook-searchbar:focus"
|
wait-for: "#mdbook-searchbar:focus"
|
||||||
// Pressing a key will therefore update the search input.
|
// Pressing a key will therefore update the search input.
|
||||||
press-key: 't'
|
press-key: 't'
|
||||||
assert-text: ("#mdbook-searchbar", "t")
|
assert-text: ("#mdbook-searchbar", "t")
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
// We disable javascript
|
// We disable javascript
|
||||||
javascript: false
|
javascript: false
|
||||||
go-to: |DOC_PATH| + "index.html"
|
go-to: |DOC_PATH| + "index.html"
|
||||||
store-value: (height, 1000)
|
store-value: (height, 1028)
|
||||||
set-window-size: (1000, |height|)
|
set-window-size: (1028, |height|)
|
||||||
|
|
||||||
within-iframe: (".sidebar-iframe-outer", block {
|
within-iframe: (".sidebar-iframe-outer", block {
|
||||||
assert-size: (" body", {"height": |height|})
|
assert-size: (" body", {"height": |height|})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue