Created
June 9, 2026 09:09
-
-
Save ultrox/45cbd3f723c941225fa28b60e0804e21 to your computer and use it in GitHub Desktop.
temper monkey script for wisp docs
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 Wisp HexDocs — Enhanced Sidebar | |
| // @namespace https://wisp.hexdocs.pm/ | |
| // @version 2.2 | |
| // @description Groups the Wisp docs sidebar into logical categories, widens it slightly, indents items under their headers, and adds a filter box, collapsible categories with expand/collapse-all controls, and inline HTTP status numbers. | |
| // @author you | |
| // @match https://wisp.hexdocs.pm/wisp.html* | |
| // @grant none | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| const SIDEBAR_WIDTH = '280px'; | |
| const ITEM_INDENT = '14px'; // how far function names sit in from their category header | |
| const STATUS = { | |
| ok: 200, created: 201, accepted: 202, no_content: 204, | |
| redirect: 303, permanent_redirect: 308, | |
| bad_request: 400, not_found: 404, method_not_allowed: 405, | |
| content_too_large: 413, unsupported_media_type: 415, unprocessable_content: 422, | |
| internal_server_error: 500 | |
| }; | |
| const CATEGORIES = [ | |
| ["Success responses (2xx)", ["ok", "created", "accepted", "no_content"]], | |
| ["Redirect responses (3xx)", ["redirect", "permanent_redirect"]], | |
| ["Client error responses (4xx)", ["bad_request", "not_found", "method_not_allowed", "content_too_large", "unsupported_media_type", "unprocessable_content"]], | |
| ["Server error responses (5xx)", ["internal_server_error"]], | |
| ["Response builders", ["response", "set_body", "set_header", "string_body", "string_tree_body", "html_body", "html_response", "json_body", "json_response", "file_download", "file_download_from_memory"]], | |
| ["Request reading & parsing", ["read_body_bits", "require_string_body", "require_bit_array_body", "require_form", "require_json", "require_content_type", "require_method", "get_query", "path_segments", "parse_range_header", "method_override"]], | |
| ["Middleware", ["handle_head", "log_request", "rescue_crashes", "serve_static", "content_security_policy_protection", "csrf_known_header_protection"]], | |
| ["Cookies & signing", ["set_cookie", "get_cookie", "sign_message", "verify_signed_message", "get_secret_key_base", "set_secret_key_base"]], | |
| ["Request configuration", ["set_max_body_size", "get_max_body_size", "set_max_files_size", "get_max_files_size", "set_read_chunk_size", "get_read_chunk_size"]], | |
| ["Logging", ["configure_logger", "set_logger_level", "log_emergency", "log_alert", "log_critical", "log_error", "log_warning", "log_notice", "log_info", "log_debug"]], | |
| ["Files & utilities", ["new_temporary_file", "delete_temporary_files", "priv_directory", "escape_html", "random_string"]], | |
| ]; | |
| function injectStyles() { | |
| if (document.getElementById('wisp-enh-style')) return; | |
| const s = document.createElement('style'); | |
| s.id = 'wisp-enh-style'; | |
| s.textContent = ` | |
| :root { --sidebarWidth: ${SIDEBAR_WIDTH}; } | |
| nav.sidebar { width: ${SIDEBAR_WIDTH} !important; } | |
| .content { margin-left: ${SIDEBAR_WIDTH} !important; } | |
| .wisp-filter { box-sizing:border-box; width:calc(100% - 4px); margin:6px 2px 4px; padding:6px 8px; font-size:0.8rem; border:1px solid rgba(128,128,128,.4); border-radius:6px; background:rgba(128,128,128,.07); color:inherit; } | |
| .wisp-actions { display:flex; gap:6px; margin:0 2px 8px; } | |
| .wisp-actions button { flex:1; cursor:pointer; font-size:0.7rem; padding:4px 6px; border:1px solid rgba(128,128,128,.4); border-radius:6px; background:rgba(128,128,128,.07); color:inherit; } | |
| .wisp-actions button:hover { background:rgba(128,128,128,.18); } | |
| .wisp-cat { cursor:pointer; user-select:none; font-weight:700; font-size:0.72rem; text-transform:uppercase; letter-spacing:.04em; opacity:.65; margin:.9em 0 .2em; list-style:none; display:flex; align-items:center; gap:.4em; } | |
| .wisp-cat .wisp-arrow { transition:transform .15s; display:inline-block; font-size:.7em; } | |
| .wisp-cat.collapsed .wisp-arrow { transform:rotate(-90deg); } | |
| .wisp-cat .wisp-count { margin-left:auto; font-weight:600; opacity:.8; } | |
| .wisp-item { padding-left: ${ITEM_INDENT}; } | |
| .wisp-status { opacity:.55; font-size:.85em; margin-left:.35em; } | |
| `; | |
| document.head.appendChild(s); | |
| } | |
| function build() { | |
| const nav = document.querySelector('nav.sidebar'); | |
| if (!nav) return false; | |
| const h2 = Array.from(nav.querySelectorAll('h2')).find(h => h.textContent.trim() === 'Values'); | |
| if (!h2) return false; | |
| const ul = h2.nextElementSibling; | |
| if (!ul || ul.tagName !== 'UL' || ul.dataset.enh) return false; | |
| const map = {}; | |
| Array.from(ul.querySelectorAll(':scope > li')).forEach(li => { | |
| const a = li.querySelector('a'); | |
| if (a) map[a.textContent.trim()] = li; | |
| }); | |
| const seen = new Set(); | |
| CATEGORIES.forEach(([, n]) => n.forEach(x => seen.add(x))); | |
| const leftovers = Object.keys(map).filter(n => !seen.has(n)); | |
| const groups = leftovers.length ? CATEGORIES.concat([["Other", leftovers]]) : CATEGORIES; | |
| // Inline HTTP status numbers | |
| Object.keys(STATUS).forEach(n => { | |
| if (map[n]) { | |
| const a = map[n].querySelector('a'); | |
| if (a && !a.querySelector('.wisp-status')) { | |
| const sp = document.createElement('span'); | |
| sp.className = 'wisp-status'; | |
| sp.textContent = STATUS[n]; | |
| a.appendChild(sp); | |
| } | |
| } | |
| }); | |
| ul.innerHTML = ''; | |
| ul.dataset.enh = '1'; | |
| // Filter box | |
| const filter = document.createElement('input'); | |
| filter.type = 'search'; | |
| filter.placeholder = 'Filter functions…'; | |
| filter.className = 'wisp-filter'; | |
| h2.insertAdjacentElement('afterend', filter); | |
| // Expand all / Collapse all controls | |
| const actions = document.createElement('div'); | |
| actions.className = 'wisp-actions'; | |
| const expandBtn = document.createElement('button'); | |
| expandBtn.type = 'button'; | |
| expandBtn.textContent = 'Expand all'; | |
| const collapseBtn = document.createElement('button'); | |
| collapseBtn.type = 'button'; | |
| collapseBtn.textContent = 'Collapse all'; | |
| actions.append(expandBtn, collapseBtn); | |
| filter.insertAdjacentElement('afterend', actions); | |
| const sections = []; | |
| function applyVisibility(sec) { | |
| const collapsed = sec.head.classList.contains('collapsed'); | |
| sec.items.forEach(li => { | |
| li.style.display = (!collapsed && !li.dataset.filterHidden) ? '' : 'none'; | |
| }); | |
| } | |
| function setCollapsed(sec, state) { | |
| sec.head.classList.toggle('collapsed', state); | |
| applyVisibility(sec); | |
| } | |
| groups.forEach(([title, names]) => { | |
| const head = document.createElement('li'); | |
| head.className = 'wisp-cat'; | |
| const arrow = document.createElement('span'); arrow.className = 'wisp-arrow'; arrow.textContent = '▾'; | |
| const label = document.createElement('span'); label.textContent = title; | |
| const count = document.createElement('span'); count.className = 'wisp-count'; | |
| count.textContent = names.filter(n => map[n]).length; | |
| head.append(arrow, label, count); | |
| ul.appendChild(head); | |
| const items = []; | |
| names.forEach(n => { | |
| if (map[n]) { | |
| map[n].classList.add('wisp-item'); // indent function items | |
| ul.appendChild(map[n]); | |
| items.push(map[n]); | |
| } | |
| }); | |
| const sec = { head, items, count }; | |
| head.addEventListener('click', () => setCollapsed(sec, !head.classList.contains('collapsed'))); | |
| sections.push(sec); | |
| }); | |
| expandBtn.addEventListener('click', () => sections.forEach(s => setCollapsed(s, false))); | |
| collapseBtn.addEventListener('click', () => sections.forEach(s => setCollapsed(s, true))); | |
| filter.addEventListener('input', () => { | |
| const q = filter.value.trim().toLowerCase(); | |
| sections.forEach(sec => { | |
| let visible = 0; | |
| sec.items.forEach(li => { | |
| const name = li.querySelector('a').textContent.toLowerCase(); | |
| const match = !q || name.includes(q); | |
| li.dataset.filterHidden = match ? '' : '1'; | |
| if (match) visible++; | |
| }); | |
| sec.count.textContent = visible; | |
| applyVisibility(sec); | |
| sec.head.style.display = (q && visible === 0) ? 'none' : ''; | |
| }); | |
| }); | |
| return true; | |
| } | |
| function run() { injectStyles(); return build(); } | |
| if (!run()) { | |
| let tries = 0; | |
| const t = setInterval(() => { if (run() || ++tries > 20) clearInterval(t); }, 250); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment