Last active
May 18, 2026 19:12
-
-
Save milo-minderbinder/2f8f084e3a6ab3adfbf91f0d5b79b2c4 to your computer and use it in GitHub Desktop.
Tampermonkey user script that adds collapsable links for each heading, subheading, and tag (e.g. API methods, subtopics, etc.) in the right-hand navigation section.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name neovim-docs-helper | |
| // @namespace https://gist.github.com/milo-minderbinder/ | |
| // @version 0.3 | |
| // @description adds collapsable links for each heading, subheading, and tag (e.g. API methods, subtopics, etc.) in neovim user doc pages. | |
| // @author Chris Passarello | |
| // @match https://neovim.io/doc/user/* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=neovim.io | |
| // @downloadURL https://gist.github.com/milo-minderbinder/2f8f084e3a6ab3adfbf91f0d5b79b2c4/raw/neovim-docs-helper.user.js | |
| // @updateURL https://gist.github.com/milo-minderbinder/2f8f084e3a6ab3adfbf91f0d5b79b2c4/raw/neovim-docs-helper.user.js | |
| // @run-at document-idle | |
| // @grant none | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| console.log('neovim-docs-helper'); | |
| function waitForElement(selector, f, delay = 500) { | |
| console.log(`waiting ${delay}ms for element with selector: ${selector}`); | |
| setTimeout(() => { | |
| const e = document.querySelector(selector); | |
| if (e != null) { | |
| console.log(`got element with selector "${selector}": ${e}`); | |
| return f(e); | |
| } | |
| waitForElement(selector, f, delay); | |
| }, delay); | |
| } | |
| function showHide(show, querySelectorRoot, selectors) { | |
| querySelectorRoot.querySelectorAll(selectors).forEach((e) => { | |
| if (show === true) { | |
| e.removeAttribute('hidden'); | |
| } else { | |
| e.setAttribute('hidden', true); | |
| } | |
| }); | |
| } | |
| /** | |
| * Create & append buttons to container that will toggle the 'hidden' attribute for matching Nodes. | |
| * | |
| * @param container {Element} to append the buttons to | |
| * @param querySelectorRoot {Element|Document} to match the selectors in | |
| * @param selectors {string} the CSS selectors to match the nodes which will be toggled by the buttons | |
| * @returns {Element} | |
| */ | |
| function createShowHideButtons( | |
| container, | |
| querySelectorRoot = document, | |
| selectors = 'div.help-toc-h2:has(>a)', | |
| ) { | |
| if (container == null) { | |
| container = document.createElement('div'); | |
| } | |
| for (const v of ['+', '-']) { | |
| const btn = document.createElement('a'); | |
| btn.classList.add('small'); | |
| btn.setAttribute('name', v === '+' ? 'expandAll' : 'collapseAll'); | |
| btn.setAttribute('href', document.location.href); | |
| btn.text = v; | |
| btn.addEventListener('click', (event) => { | |
| btn.setAttribute('href', document.location.href); | |
| showHide(v === '+', querySelectorRoot, selectors); | |
| }); | |
| container.appendChild(btn); | |
| } | |
| return container; | |
| } | |
| class HelpToc { | |
| e; | |
| constructor(e) { | |
| this.e = e; | |
| } | |
| get parent() { | |
| if (this.e.classList.contains('help-toc-h2')) { | |
| return new HelpToc(this.e.parentElement); | |
| } | |
| return null; | |
| } | |
| get linkedFragment() { | |
| return this.e.querySelector(':scope>a').hash.slice(1); | |
| } | |
| static create(section) { | |
| const div = document.createElement('div'); | |
| div.classList.add('help-toc-h2'); | |
| const a = document.createElement('a'); | |
| const sectionId = section.e.id; | |
| a.setAttribute('href', `#${sectionId}`); | |
| a.text = decodeURIComponent(sectionId); | |
| div.appendChild(a); | |
| } | |
| /** | |
| * Get the HelpToc that links to the fragment with the given element ID. | |
| * | |
| * @param elementId {string} the element ID to get the HelpToc for | |
| * @returns {HelpToc|null} the HelpToc which links to the fragment for the | |
| * element ID if one exists; otherwise returns null | |
| */ | |
| static withTarget(elementId) { | |
| const link = `#${elementId}`; | |
| let helpTocElement = document.querySelector(`div:is(.help-toc-h1,.help-toc-h2):has(>a[href="#${link}"])`); | |
| if (helpTocElement == null) { | |
| helpTocElement = Array.from( | |
| document.querySelectorAll('div:is(.help-toc-h1,.help-toc-h2):has(>a[href])'), | |
| ).find((e) => { | |
| return decodeURIComponent(e.querySelector('a').hash) === link; | |
| }); | |
| if (helpTocElement == null) { | |
| return null; | |
| } | |
| } | |
| return new HelpToc(helpTocElement); | |
| } | |
| * children() { | |
| for (const c of this.e.querySelectorAll(':scope>div.help-toc-h2:has(>a[href])')) { | |
| yield new HelpToc(c); | |
| } | |
| } | |
| /** | |
| * Get the child HelpToc which links to the fragment with the given element ID | |
| * | |
| * @param elementId {string} the element ID for the linked fragment to get | |
| * the HelpToc for | |
| */ | |
| getChildWithTarget(elementId) { | |
| const link = `#${elementId}`; | |
| let helpTocElement = this.e.querySelector(`:scope>div.help-toc-h2:has(>a[href="${link}"])`); | |
| if (helpTocElement == null) { | |
| helpTocElement = Array.from( | |
| this.e.querySelectorAll(':scope>div.help-toc-h2:has(>a[href])'), | |
| ).find((e) => { | |
| return decodeURIComponent(e.querySelector('a').hash) === link; | |
| }); | |
| if (helpTocElement == null) { | |
| return null; | |
| } | |
| } | |
| return new HelpToc(helpTocElement); | |
| } | |
| } | |
| class HelpSection { | |
| static selector = 'div:is(.old-help-para,.help-para)>:is(:is(h2,h3).help-heading, .help-tag-right)'; | |
| #children = []; | |
| e; | |
| parent; | |
| helpToc; | |
| constructor(e, parent) { | |
| this.e = e; | |
| this.parent = parent; | |
| if (this.parent != null) { | |
| this.parent.#children.push(this); | |
| } | |
| this.helpToc = this.#getOrCreateHelpToc(); | |
| } | |
| get children() { | |
| return this.#children; | |
| } | |
| #getOrCreateHelpToc() { | |
| let helpToc; | |
| if (this.parent == null) { | |
| helpToc = HelpToc.withTarget(this.e.id); | |
| if (helpToc == null) { | |
| throw new Error(`unable to find top-level HelpToc linking to fragment: ${this.e}`); | |
| } | |
| return helpToc; | |
| } | |
| helpToc = this.parent.helpToc.getChildWithTarget(this.e.id); | |
| if (helpToc != null) { | |
| return helpToc; | |
| } | |
| const div = document.createElement('div'); | |
| div.classList.add('help-toc-h2'); | |
| const a = document.createElement('a'); | |
| a.setAttribute('href', `#${this.e.id}`); | |
| a.text = decodeURIComponent(this.e.id); | |
| div.appendChild(a); | |
| if (this.parent.children.length === 0) { | |
| throw new Error(`invalid state - this.parent.children should include at least this: ${this}`); | |
| } else if (this.parent.children.length === 1) { | |
| this.parent.helpToc.e.querySelector(':scope>a[href]').after(div); | |
| } else { | |
| this.parent.children.at(-2).helpToc.e.after(div); | |
| } | |
| helpToc = this.parent.helpToc.getChildWithTarget(this.e.id); | |
| if (helpToc != null) { | |
| return helpToc; | |
| } | |
| throw new Error(`error - created but could not find HelpToc linking to fragment: ${this.e}`); | |
| } | |
| addChild(c) { | |
| return new HelpSection(c, this); | |
| } | |
| } | |
| function run() { | |
| const runSelector = 'div.col-narrow.toc'; | |
| waitForElement(runSelector, (_) => { | |
| let section; | |
| document.querySelectorAll( | |
| HelpSection.selector, | |
| ).forEach((e) => { | |
| if (e.className === 'help-heading' && e.tagName === 'H2') { | |
| section = new HelpSection(e); | |
| } else if (e.className === 'help-heading' && e.tagName === 'H3') { | |
| if (section.e.tagName === 'H2') { | |
| // if e is an H3 tag section and follows an H2 tag section, then e | |
| // is a child section of the previous section | |
| section = section.addChild(e); | |
| } else if (section.e.tagName === 'H3') { | |
| // else if e is an H3 tag section and follows another H3 tag | |
| // section, then this is a sibling of the previous section, so | |
| // append to its parent | |
| section = section.parent.addChild(e); | |
| } else { | |
| console.log(`error: ${e}`); | |
| } | |
| } else { | |
| // e is a help tag within the section | |
| section.addChild(e); | |
| } | |
| }); | |
| const navDiv = document.querySelector(runSelector); | |
| const expandCollapseDiv = createShowHideButtons( | |
| null, | |
| navDiv, | |
| 'div:is(.help-toc-h1,.help-toc-h2):has(>a)>div.help-toc-h2:has(>a)', | |
| ); | |
| navDiv.firstElementChild.before(expandCollapseDiv); | |
| document.querySelectorAll( | |
| 'div:is(.help-toc-h1,.help-toc-h2):has(>a):has(>div.help-toc-h2>a)', | |
| ).forEach((e) => { | |
| const listToggles = createShowHideButtons( | |
| document.createElement('span'), | |
| e, | |
| ':scope>div.help-toc-h2:has(>a)' | |
| ); | |
| e.querySelector('a').after(listToggles); | |
| listToggles.before(' '); | |
| }); | |
| }); | |
| } | |
| run(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment