Skip to content

Instantly share code, notes, and snippets.

@ultrox
Created June 9, 2026 09:09
Show Gist options
  • Select an option

  • Save ultrox/45cbd3f723c941225fa28b60e0804e21 to your computer and use it in GitHub Desktop.

Select an option

Save ultrox/45cbd3f723c941225fa28b60e0804e21 to your computer and use it in GitHub Desktop.
temper monkey script for wisp docs
// ==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