diff --git a/src/ext/hx-browser-indicator.js b/src/ext/hx-browser-indicator.js index 01f3a6f39..62dd1df3d 100644 --- a/src/ext/hx-browser-indicator.js +++ b/src/ext/hx-browser-indicator.js @@ -19,7 +19,7 @@ navigation.addEventListener('navigate', (event) => { if (!event.canIntercept) return; - // save state before intercept — navigation.navigate() with {history:'replace'} wipes it + // save state before intercept (navigation.navigate() with {history:'replace'} wipes it) let savedState = history.state; let hideBrowserIndicator; @@ -40,7 +40,7 @@ cleanupNavigation = () => { hideBrowserIndicator(); - // restore after resolving — replaceState during a pending intercept aborts the signal early + // restore after resolving (replaceState during a pending intercept aborts the signal early) history.replaceState(savedState, ''); }; }, {once: true}); diff --git a/src/ext/hx-csp.js b/src/ext/hx-csp.js index 21d57da36..25cbaa091 100644 --- a/src/ext/hx-csp.js +++ b/src/ext/hx-csp.js @@ -5,7 +5,7 @@ // // Provides three layers of Content Security Policy integration: // -// 1. Nonce gating — gates htmx attribute processing behind CSP +// 1. Nonce gating: gates htmx attribute processing behind CSP // nonces to prevent HTML injection attacks. Every htmx element // must carry an hx-nonce attribute matching the page nonce or // its htmx attributes are stripped. Fail closed if no page @@ -13,12 +13,12 @@ // to also cover extension eval use like hx-live. // Nonce source: script[nonce].nonce property on page load. // -// 2. Trusted Types — creates an 'htmx' TT policy (passthrough — +// 2. Trusted Types: creates an 'htmx' TT policy (passthrough; // trust established by the nonce gate). Add trusted-types htmx // to your CSP to enforce that only htmx touches DOM sinks. // Fail closed if policy creation is blocked by CSP. // -// 3. Safe eval — set config.safeEval:true to replace htmx's +// 3. Safe eval: set config.safeEval:true to replace htmx's // Function/AsyncFunction with nonce-based script injection, // enabling hx-on:/hx-vals js:/hx-confirm js: without // unsafe-eval in your CSP. @@ -64,7 +64,7 @@ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } - // Rewrites responseNonce → replacement in raw HTML before DOM parsing, + // Rewrites responseNonce -> replacement in raw HTML before DOM parsing, // covering hx-nonce and script nonce attributes in one pass. // Pass replacement='' to strip nonce attributes entirely (stolen-nonce scrub). function rewriteNoncesInText(text, responseNonce, replacement = pageNonce) { @@ -88,7 +88,7 @@ let tag = elt.tagName?.toLowerCase(); let id = elt.id ? `#${elt.id}` : ''; let reason = eltNonce == null ? 'missing-nonce' : 'nonce-mismatch'; - console.error(`htmx: [hx-csp] blocked <${tag}${id}> — ${eltNonce == null ? 'no hx-nonce attribute' : 'nonce mismatch (possible injection)'}`, { elt, reason }); + console.error(`htmx: [hx-csp] blocked <${tag}${id}>: ${eltNonce == null ? 'no hx-nonce attribute' : 'nonce mismatch (possible injection)'}`, { elt, reason }); htmx.trigger(elt, 'htmx:security:strip', { reason, stripped }); return true; } @@ -103,18 +103,18 @@ pageNonce = document.querySelector('script[nonce]')?.nonce || null; if (!pageNonce) { - console.error('htmx: [hx-csp] no page nonce found — blocking all htmx. Add a nonce to your script tags.'); + console.error('htmx: [hx-csp] no page nonce found, blocking all htmx. Add a nonce to your script tags.'); return; } - // Passthrough TT policy — trust established by nonce gate. + // Passthrough TT policy (trust established by nonce gate). // Fail closed if 'htmx' is not in the trusted-types CSP whitelist. try { ttPolicy = typeof trustedTypes !== 'undefined' ? trustedTypes.createPolicy('htmx', { createHTML: s => s, createScript: s => s }) : { createHTML: s => s, createScript: s => s }; } catch (e) { - console.error("htmx: [hx-csp] TrustedTypes policy 'htmx' blocked — add 'htmx' to trusted-types CSP directive. Blocking all htmx."); + console.error("htmx: [hx-csp] TrustedTypes policy 'htmx' blocked, add 'htmx' to trusted-types CSP directive. Blocking all htmx."); pageNonce = null; return; } @@ -123,7 +123,7 @@ let NativeFunction = Function; let NativeAsyncFunction = Object.getPrototypeOf(async function(){}).constructor; // Cache compiled script-injected functions by "keys|body" so each unique - // expression is only injected once — critical for hx-live which re-evaluates + // expression is only injected once (critical for hx-live which re-evaluates // the same expressions on every DOM/input tick. let safeEvalCache = new Map(); @@ -135,7 +135,7 @@ if (getNonce(thisArg) !== pageNonce) { let tag = thisArg?.tagName?.toLowerCase(); let id = thisArg?.id ? `#${thisArg.id}` : ''; - console.error(`htmx: [hx-csp] blocked eval on <${tag}${id}> — nonce mismatch`, { elt: thisArg }); + console.error(`htmx: [hx-csp] blocked eval on <${tag}${id}>: nonce mismatch`, { elt: thisArg }); htmx.trigger(thisArg, 'htmx:security:violation', { reason: 'nonce-mismatch-at-eval' }); return; } @@ -175,13 +175,13 @@ if (!pageNonce) return false; let ctx = detail.ctx; - // Always scrub stolen pageNonce from any response — the server cannot know the + // Always scrub stolen pageNonce from any response. The server cannot know the // page nonce, so its presence indicates a stolen-nonce injection attempt. ctx.text = rewriteNoncesInText(ctx.text, pageNonce, ''); // Only promote response nonce for verified same-origin responses let responseURL = ctx?.response?.raw?.url; - if (!responseURL) return; // can't verify origin — scrub only, no promotion + if (!responseURL) return; // can't verify origin; scrub only, no promotion try { if (new URL(responseURL).origin !== location.origin) return; } catch (_) { return; } @@ -193,7 +193,7 @@ }, // Blocks boosted form submissions where an unnonced submitter overrides formaction. - // Also blocks js:/javascript: action URLs — entity encoding doesn't neutralise these + // Also blocks js:/javascript: action URLs (entity encoding doesn't neutralise these // so they may survive template rendering and execute unexpectedly. htmx_config_request: (elt, detail) => { if (!pageNonce) return false; @@ -208,7 +208,7 @@ if (!elt._htmx?.boosted || !submitter?.getAttribute('formaction')) return; if (getNonce(submitter) !== pageNonce) { let id = submitter?.id ? `#${submitter.id}` : ''; - console.error(`htmx: [hx-csp] blocked boosted form — unnonced submitter${id} overrode formaction`); + console.error(`htmx: [hx-csp] blocked boosted form: unnonced submitter${id} overrode formaction`); htmx.trigger(elt, 'htmx:security:violation', { reason: 'unnonced-submitter', submitter, ctx: detail.ctx }); detail.cancelled = true; return false; diff --git a/src/ext/hx-head.js b/src/ext/hx-head.js index 7238c9304..937939629 100644 --- a/src/ext/hx-head.js +++ b/src/ext/hx-head.js @@ -14,7 +14,7 @@ for (const attr of newNode.attributes) newElt.setAttribute(attr.name, attr.value) newElt.textContent = newNode.textContent - // stylesheet — await CSSOM or content will flash unstyled + // stylesheet: await CSSOM or content will flash unstyled if (newNode.tagName === "LINK" && newNode.rel === "stylesheet") { return new Promise(resolve => { newElt.onload = resolve @@ -23,7 +23,7 @@ }) } - // blocking external script (no async/defer) — must init before swap + // blocking external script (no async/defer): must init before swap if (newNode.tagName === "SCRIPT" && newNode.src && !newNode.async && !newNode.defer) { return new Promise((resolve, reject) => { newElt.onload = resolve @@ -32,7 +32,7 @@ }) } - // meta, title, base, preload/prefetch/icon links, async scripts — fire-and-forget + // meta, title, base, preload/prefetch/icon links, async scripts: fire-and-forget document.head.appendChild(newElt) if (newNode._preloadHint) newElt.addEventListener("load", () => newNode._preloadHint.remove(), {once: true}) return null @@ -111,7 +111,7 @@ // nodes to append to the head tag nodesToAppend.push(...srcToNewHeadNodes.values()) - // defer scripts need the swapped DOM to exist — split them out + // defer scripts need the swapped DOM to exist, so split them out for (const newNode of nodesToAppend) { if (newNode.tagName === "SCRIPT" && newNode.defer) { deferred.push(newNode) @@ -176,7 +176,7 @@ if (detail.head) { // mergeHead awaits stylesheets/blocking scripts, returns deferred scripts. // Set detail.ready so history-cache awaits before swapping body. - // Stash deferred scripts on detail — history-cache copies them onto the swap ctx + // Stash deferred scripts on detail so history-cache copies them onto the swap ctx // so htmx_after_swap picks them up. detail.ready = mergeHead(detail.head, 'merge').then(deferred => { detail._deferredHeadScripts = deferred; diff --git a/src/ext/hx-live.js b/src/ext/hx-live.js index d5cd101ff..fbbef26e1 100644 --- a/src/ext/hx-live.js +++ b/src/ext/hx-live.js @@ -323,11 +323,11 @@ * * @example * toggle('.active') // toggle class - * toggle('aria-expanded') // flip "true" ↔ "false" + * toggle('aria-expanded') // flip "true" <-> "false" * toggle('hidden') // toggle attribute presence * toggle('data-view', 'grid|list|table') // cycle attribute through values * toggle('.size', 'sm|md|lg') // cycle classes (one at a time) - * toggle('data-open', 'on|') // 'on' ↔ absent slot + * toggle('data-open', 'on|') // 'on' <-> absent slot */ function applyToggle(name, values, element) { let isClass = name.startsWith('.'); @@ -497,7 +497,7 @@ if (extra === undefined) { if (window.Alpine) { extra = ''; - console.warn('hx-live: Alpine.js detected — ":" short-form bindings disabled. Set htmx.config.live.bindPrefix to configure.'); + console.warn('hx-live: Alpine.js detected; ":" short-form bindings disabled. Set htmx.config.live.bindPrefix to configure.'); } else { extra = ':'; } diff --git a/src/ext/hx-multipart.js b/src/ext/hx-multipart.js index 0f40f80a3..dc0fc366e 100644 --- a/src/ext/hx-multipart.js +++ b/src/ext/hx-multipart.js @@ -553,7 +553,7 @@ class MultipartParser { onPull = null constructor(boundary) { - // RFC 2046 §5.1.1 limits the boundary to 1-70 ASCII characters from a + // RFC 2046 S5.1.1 limits the boundary to 1-70 ASCII characters from a // small subset. Real-world implementations stick to printable ASCII; we // enforce that broader range so non-ASCII boundaries fail loudly instead // of silently misaligning the parser's char-length arithmetic. @@ -587,7 +587,7 @@ class MultipartParser { * @param {((part: BodyPart) => void) | null} [onPart] */ write(chunk, onPart = null) { - // Discard epilogue bytes after the closing boundary (RFC 2046 §5.1.1). + // Discard epilogue bytes after the closing boundary (RFC 2046 S5.1.1). if (this.#state === State.DONE) return let index = 0 @@ -723,7 +723,7 @@ class MultipartParser { this.#buffer = chunk break } - // Discard preamble bytes before the opening boundary (RFC 2046 §5.1.1). + // Discard preamble bytes before the opening boundary (RFC 2046 S5.1.1). const openingIndex = this.#findOpeningBoundary(chunk) if (openingIndex === -1) { const tailStart = chunkLength - (this.#openingBoundaryLength - 1) diff --git a/src/ext/hx-sse.js b/src/ext/hx-sse.js index a1c8e148a..13716a770 100644 --- a/src/ext/hx-sse.js +++ b/src/ext/hx-sse.js @@ -170,7 +170,7 @@ try { while (element.isConnected) { - // Reconnection (not on first iteration — we already have the response) + // Reconnection (not on first iteration, we already have the response) if (connection.attempt > 0) { // Wait while paused (tab backgrounded with pauseOnBackground) if (paused) { @@ -268,7 +268,7 @@ delete detail.message.cancelled; api.triggerHtmxEvent(element, 'htmx:sse:after:message', detail); - // hx-sse:close="eventname" — close connection on matching event + // hx-sse:close="eventname": close connection on matching event let closeEvent = api.attributeValue(element, 'hx-sse:close'); if (closeEvent && detail.message.event === closeEvent) { cleanup(element, 'message'); @@ -370,7 +370,7 @@ let contentType = ctx.response.raw.headers.get('Content-Type'); if (!contentType?.includes('text/event-stream')) return; - // Take over — core will return without calling response.text() + // Take over; core will return without calling response.text() handleSSEResponse(ctx).catch(e => { api.triggerHtmxEvent(element, 'htmx:sse:error', {error: e, url: ctx.request.action}); cleanup(element); diff --git a/src/ext/hx-ws.js b/src/ext/hx-ws.js index 3138c6bad..a67cfdbd5 100644 --- a/src/ext/hx-ws.js +++ b/src/ext/hx-ws.js @@ -189,7 +189,7 @@ if (elt) { api.triggerHtmxEvent(elt, 'htmx:after:ws:connection', {connection}); } else { - // Element was removed while connecting — orphaned socket + // Element was removed while connecting (orphaned socket) cleanupOrphanedConnection(url, connection); return; } @@ -216,7 +216,7 @@ if (config.reconnect && findConnectedElement(url)) { scheduleReconnect(url, connection); } else { - // No element or reconnect disabled — full cleanup + // No element or reconnect disabled: full cleanup cleanupOrphanedConnection(url, connection); } }, opts); @@ -267,7 +267,7 @@ return; } } else { - // Element gone — no point scheduling reconnect + // Element gone, no point scheduling reconnect cleanupOrphanedConnection(url, connection); return; } @@ -433,7 +433,7 @@ } if (!connectionElement) { - // No element in DOM for this connection — orphan cleanup + // No element in DOM for this connection (orphan cleanup) cleanupOrphanedConnection(connection.url, connection); return; } diff --git a/src/htmx.js b/src/htmx.js index ce6f83bfa..a96f88182 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -655,14 +655,14 @@ var htmx = (() => { requestQueue.finish() if (requestQueue.more()) { - // intentionally not awaited — __issueRequest has its own try/catch + // intentionally not awaited; __issueRequest has its own try/catch this.__issueRequest(requestQueue.next()) } } } // Extract HX-* response headers into ctx.hx - // Maps: HX-Trigger → ctx.hx.trigger, HX-Push-Url → ctx.hx.pushurl, etc. + // Maps: HX-Trigger -> ctx.hx.trigger, HX-Push-Url -> ctx.hx.pushurl, etc. __extractHxHeaders(ctx) { ctx.hx = {} for (let [k, v] of ctx.response.raw.headers) { @@ -1353,7 +1353,7 @@ var htmx = (() => { } let swapStyle = swapSpec.style; if (swapStyle === 'none') return; - // full-page response: fragment has a wrapper — upgrade outerHTML to outerSync, strip for everything else + // full-page response: fragment has a wrapper, so upgrade outerHTML to outerSync, strip for everything else if (fragment.firstElementChild?.tagName === 'BODY') { if (swapStyle === 'outerHTML') swapStyle = 'outerSync'; else if (!swapStyle.startsWith('outer')) swapSpec.strip = true; @@ -1839,7 +1839,7 @@ var htmx = (() => { } else if (['INPUT', 'SELECT', 'TEXTAREA', 'FIELDSET'].includes(tag) || !isGet) { inputs = this.__queryEltAndDescendants(elt, '[name]:not(button)'); } - // GET on non-form-control containers (div, etc.) sends nothing — use hx-include for explicit inclusion + // GET on non-form-control containers (div, etc.) sends nothing; use hx-include for explicit inclusion for (let input of inputs) { let name = input.name || input.getAttribute?.('name'); @@ -2109,7 +2109,7 @@ var htmx = (() => { } __findBestMatch(ctx, node, startPoint, endPoint) { - // text nodes match positionally — patch in place via __morphNode, 3 = TEXT_NODE + // text nodes match positionally (patch in place via __morphNode), 3 = TEXT_NODE if (node.nodeType === 3) return startPoint?.nodeType === 3 ? startPoint : null; if (!(node instanceof Element)) return null; let softMatch = null, displaceMatchCount = 0, scanLimit = this.config.morphScanLimit; diff --git a/src/scripts/content/check_ascii.py b/src/scripts/content/check_ascii.py new file mode 100644 index 000000000..9993cc564 --- /dev/null +++ b/src/scripts/content/check_ascii.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +"""Check that source files contain only ASCII characters.""" + +from pathlib import Path + +SRC = Path(__file__).parents[2] +ERRORS = [] + +for f in list(SRC.glob("*.js")) + list(SRC.glob("ext/*.js")): + for i, line in enumerate(f.read_bytes().splitlines(), 1): + if any(b > 127 for b in line): + ERRORS.append(f"{f.name}:{i}: {line!r}") + +if ERRORS: + print("Non-ASCII characters found:") + for e in ERRORS: + print(f" {e}") + raise SystemExit(1) + +print("OK: All source files are ASCII-clean")