Skip to the content

Capsid menu

render-url.mjs

55 lines and 4 functions in DrDustinEdwards/prelum, at commit e389900 on 2026-10-24. This file on GitHub.

Functions

The functions of render-url.mjs
FunctionLinesComplexityCoveredCRAPMutation
normalizeForProtocolTest14 to 246100%WatchCRAP 6No data: mutation testing has not run on this repo yet
protocolOf30 to 332100%OkCRAP 2No data: mutation testing has not run on this repo yet
decodeOrSelf39 to 45271%OkCRAP 2.1No data: mutation testing has not run on this repo yet
isAllowedUrl54 to 604100%OkCRAP 4No data: mutation testing has not run on this repo yet

Imports

It imports no other module of this repo. Imported by render-nomath.mjs, render-safe.mjs.

Source

// The URL allowlist lives at the render layer so one guard binds every writer: a site's build, its save path and Carrel's preview all
// call render. Pure predicates: rehypeUrlProtocols in render-nomath.mjs applies them to the rendered tree, and assertSafe applies them to the
// output.

const ALLOWED_PROTOCOLS = new Set(["https:", "http:", "mailto:"]);

/**
 * Browsers drop tab and newline anywhere in a URL, so java<TAB>script: is live. Every C0 control and DEL goes, stricter than the
 * spec, which can only block more: a NUL left in makes the scheme look relative.
 *
 * @param {string} raw
 * @returns {string}
 */
function normalizeForProtocolTest(raw) {
  let out = "";
  for (const character of raw) {
    const code = character.codePointAt(0) ?? 0;
    if (code <= 0x20 || code === 0x7f) continue;
    // U+FFFD is what markdown makes of a NUL in a link destination; removing it exposes a hidden scheme.
    if (code === 0xfffd) continue;
    out += character;
  }
  return out.toLowerCase();
}

/**
 * @param {string} value
 * @returns {string | null}
 */
function protocolOf(value) {
  const match = /^([a-z][a-z0-9+.-]*):/.exec(normalizeForProtocolTest(value));
  return match ? `${match[1]}:` : null;
}

/**
 * @param {string} value
 * @returns {string}
 */
function decodeOrSelf(value) {
  try {
    return decodeURIComponent(value);
  } catch {
    return value;
  }
}

/**
 * Schemeless means relative and is allowed: footnotes and heading autolinks are fragments. The decoded form must pass too, so a
 * percent-encoded scheme cannot be smuggled past.
 *
 * @param {string} value
 * @returns {boolean}
 */
export function isAllowedUrl(value) {
  for (const form of [value, decodeOrSelf(value)]) {
    const protocol = protocolOf(form);
    if (protocol !== null && !ALLOWED_PROTOCOLS.has(protocol)) return false;
  }
  return true;
}