utility.js
Path: js/utility.js | Language: JavaScript | Lines: ~1,679
General-purpose utility functions used throughout the gwern.net frontend
Overview
This file provides the foundational utility layer for gwern.net's frontend JavaScript. It contains no gwern-specific business logic—just general-purpose helpers that every other module depends on.
The utilities fall into several categories: prototype extensions to built-in objects (Array, String, URL, Element, Node), DOM manipulation helpers (creating, wrapping, unwrapping elements), geometry/visibility functions for intersection testing, and AJAX primitives. The design philosophy favors extending native prototypes over utility namespaces, so you'll see array.first and "my-string".kebabCaseToCamelCase() rather than Utils.first(array).
Most functions are pure or have minimal side effects. The file loads early and has no dependencies on other gwern modules.
Public API
DOM Element Creation
newElement(tagName, attributes, properties) → HTMLElement
Creates and returns a new DOM element. This is the workhorse for element creation throughout the codebase.
let button = newElement("BUTTON", { class: "action-btn", "data-id": "123" }, { disabled: true });
Called by: Nearly everything—popups.js, sidenotes.js, extracts.js, rewrite.js, transclude.js
Calls: document.createElement
newDocument(content) → DocumentFragment
Creates a DocumentFragment from various input types: null (empty), string (parsed HTML), Node, NodeList, or another DocumentFragment.
let frag = newDocument("<p>Hello</p><p>World</p>");
Called by: transclude.js, extracts.js, rewrite.js, content.js
Calls: newElement
elementFromHTML(html) → HTMLElement | null
Parses an HTML string and returns the single root element, or null if the HTML doesn't define exactly one element.
Called by: Various modules needing to parse HTML snippets
Calls: newDocument
DOM Wrapping/Unwrapping
wrapElement(element, wrapperSpec, options) → HTMLElement
Wraps an element in a new container. The wrapperSpec format is "tagName.class1.class2" (tag defaults to "div").
wrapElement(img, "figure.image-container", { moveClasses: true });
Options:
useExistingWrapper: Reuse parent if it matchesmoveClasses: Transfer classes from element to wrapper (boolean or array)
Called by: rewrite.js, sidenotes.js, transclude.js
Calls: newElement, isOnlyChild, transferClasses
unwrap(wrapper, options) → Node[]
Removes a wrapper element, leaving its children in place. Returns array of unwrapped nodes.
Options:
moveID: Transfer wrapper's ID to single childmoveClasses: Copy classes to children (boolean or array)moveAttributes: Copy specified attributes to childrenpreserveBlockSpacing: Preserve--bsmCSS property
Called by: rewrite.js, transclude.js
Calls: copyClasses, copyAttributes
wrapAll(selector, wrapperSpec, options)
Wraps all elements matching a selector. The wrapperSpec can be a string or a function.
Options:
root: Element to search within (default: document)
unwrapAll(selector, options)
Unwraps all elements matching a selector.
URL Utilities
URLFromString(urlString, baseURL?) → URL
Creates a URL object from strings that new URL() can't handle: hash-only (#foo), absolute paths (/page), or relative paths (../other).
URLFromString("#section"); // → full URL with hash
URLFromString("/about"); // → https://gwern.net/about
URLFromString("sibling.html"); // → relative to current page
Called by: Nearly all modules dealing with links
Calls: new URL()
modifiedURL(url, mods) → URL
Returns a new URL with specified properties changed.
modifiedURL(link.href, { hash: "", search: "" });
URL.prototype.getQueryVariable(key) → string | null
URL.prototype.setQueryVariable(key, value)
URL.prototype.deleteQueryVariable(key)
URL.prototype.pathSegments (getter)
URL prototype extensions for query parameter manipulation and path parsing.
getQueryVariable(variable) → string | null
Gets a URL query parameter from window.location.
selectorFromHash(hash) → string | null
Converts a URL hash to a CSS selector, with proper escaping. Returns null for empty hashes or Chrome's text fragment syntax (#:~:).
selectorFromHash("#fn:1"); // → "#fn\\:1"
AJAX
doAjax(options)
XMLHttpRequest wrapper with sensible defaults and callback-based API.
doAjax({
location: "/api/data",
method: "POST",
params: { id: 123 },
serialization: "JSON",
onSuccess: (event) => { /* handle response */ },
onFailure: (event) => { /* handle error */ }
});
Options:
location: URL (default: document.location)method: "GET" or "POST"params: Object of parametersserialization: "URL" or "JSON" (for POST)responseType: XHR response typeheaders: Custom headers objectonLoadStart,onProgress,onSuccess,onFailure: CallbackscheckFor404Redirect: Detect soft 404s (default: true)checkFor404RedirectURL: URL that indicates a 404
Called by: content.js (for fetching pages/annotations)
Calls: XMLHttpRequest, urlEncodeQuery, URLFromString
urlEncodeQuery(params) → string
Encodes an object as URL query string.
Geometry & Visibility
isOnScreen(element, margin?) → boolean
Returns true if the element intersects the viewport.
isWithinRect(element, rect, margin?) → boolean
Returns true if the element intersects the given DOMRect.
isWithinRectOf(firstElement, secondElement, margin?) → boolean
Returns true if firstElement intersects secondElement's bounding rect (or viewport if secondElement is null).
doRectsIntersect(rectA, rectB, margin?) → boolean
Tests whether two DOMRects intersect, with optional margin for "close enough" detection.
pointWithinRect(point, rect) → boolean
Tests whether a point {x, y} falls within a DOMRect.
rectUnion(rect, ...rects) → DOMRect
Returns a DOMRect that is the union of all provided rects.
Observers
lazyLoadObserver(fn, target, options) → IntersectionObserver
Sets up an IntersectionObserver that calls fn when target enters view, then disconnects. Includes short-circuit optimization for already-visible elements.
Options: Standard IntersectionObserver options plus checkPositionImmediately
Called by: Lazy-loading throughout the codebase
resizeObserver(fn, target)
Sets up a ResizeObserver. If fn returns false, the observer disconnects.
Class & Style Manipulation
copyClasses(source, target, classes?)
removeClasses(element, classes?)
transferClasses(source, target, classes?)
Copy, remove, or transfer CSS classes between elements. If classes array is omitted, operates on all classes.
saveStyles(element, options)
restoreStyles(element)
stripStyles(element, options)
Save inline styles to a .savedStyles property, restore them later, or strip styles (optionally preserving some).
Node/Element Inspection
isOnlyChild(node) → boolean | undefined
Returns true if node is the only non-empty child of its parent.
isNodeEmpty(node, options) → boolean | undefined
Returns true if node contains only whitespace. Many options for excluding certain elements (identified elements, specific selectors, media elements).
Text & Whitespace
paragraphizeTextNodesOfElement(element, options)
Wraps loose text nodes and inline elements in <p> tags. Used for normalizing content structure.
Element.prototype.trimWhitespace(options)
Element.prototype.trimWhitespaceFromStart(options)
Element.prototype.trimWhitespaceFromEnd(options)
Remove empty nodes from element boundaries. Options control recursion and within-node trimming.
Miscellaneous
doIfAllowed(fn, passHolder, passName, options)
Simple mutex: only runs fn if passHolder[passName] is true, then sets it false until next frame (or immediately if releaseImmediately: true).
onEventAfterDelayDo(target, event, delay, fn, options) → Function
Sets up delayed event handling with optional cancellation events. Returns a cleanup function.
relocate(s)
Updates browser URL via history.replaceState without navigation.
getHashTargetedElement() → Element | null
Returns the element targeted by the current URL hash.
selectElementContents(element)
Programmatically selects an element's contents.
getSelectionAsDocument(doc?) → DocumentFragment
Returns current selection as a DocumentFragment.
Prototype Extensions
The file extends several built-in prototypes:
Array
.first/.last(getters): First/last element or null.remove(item): Remove first occurrence.removeIf(test): Remove first item passing test.insertBefore(item, test): Insert before first item passing test.findLastIndex(test): Polyfill for older browsers.unique(): Return array with duplicates removed
String
.capitalizeWords(): Capitalize first letter of each word.trimQuotes(): Remove surrounding quotes.startsWithAnyOf(prefixes)/.endsWithAnyOf(suffixes)/.includesAnyOf(substrings).hashCode(): Numeric hash (for caching/deduplication).kebabCaseToCamelCase()/.camelCaseToKebabCase()
URL
.getQueryVariable(key)/.setQueryVariable(key, value)/.deleteQueryVariable(key).pathSegments(getter): Array of path segments
HTMLAnchorElement
- Same query variable methods as URL
Element
.addActivateEvent(fn, options)/.removeActivateEvent(): Handle click + keyboard activation.swapClasses(classes, whichToAdd): Toggle between two classes.trimQuotes(): Remove quote characters from element text
Node
.textNodes(getter): All text nodes within.firstTextNode/.lastTextNode(getters)
DOMTokenList
.containsAnyOf(tokens)/.containsAllOf(tokens)
Document / DocumentFragment
.innerHTML(getter): Serialize contents to HTML string
Selection
.selectNode(node): Select a specific node
Set
.intersection(other)/.union(other): Set operations (polyfills)
Polyfills
The file includes polyfills for:
Array.prototype.findLastIndex(Firefox ≤103, Chrome ≤96)crypto.randomUUID(Safari <15.4)window.requestIdleCallback(Safari)Set.prototype.intersection/Set.prototype.union
Key Patterns
Consistent Options Pattern
Most functions accept an options object with Object.assign for defaults:
function wrapElement(element, wrapperSpec = "", options) {
options = Object.assign({
useExistingWrapper: false,
moveClasses: false
}, options);
// ...
}
This allows callers to pass only the options they care about.
Prototype Extension Style
Rather than GW.utils.first(arr), the codebase uses arr.first. This reads more naturally but means this file must load before anything else. The pattern is consistent throughout.
DocumentFragment as Interchange Format
newDocument() is the standard way to create DOM content from strings, nodes, or other fragments. Many functions return DocumentFragments rather than elements, allowing flexible composition.
Configuration
No configuration—these are pure utilities.
Integration Points
No Events
This module doesn't fire or listen to events. It's a pure utility layer.
No Shared State
Functions are stateless (except for .savedStyles property set by saveStyles()).
Load Order
Must load before all other gwern JS modules. It's typically the first or second script loaded (after possibly initial.js).
See Also
- initial.js - GW namespace and notification center; loads alongside utility.js
- rewrite.js - Heavy user of DOM manipulation utilities (wrapElement, unwrap, newElement)
- transclude.js - Uses newDocument, wrap/unwrap for content embedding
- content.js - Uses doAjax for fetching content from server
- popups.js - Uses geometry functions and element creation utilities
- extracts.js - Uses URL utilities and DOM manipulation helpers