import { escapeHtml, safeUrl } from "../inline/render.js"; /** * Attribute rendering for handlers that emit tags with caller-supplied * attributes. * * Escaping the *value* is not sufficient: an attribute **name** is written * outside quotes, so a name like `a" onload="alert(1)` breaks out of the tag * regardless of how its value is escaped. Names are therefore validated * against a strict pattern rather than escaped, and event handlers and * URL-bearing attributes are filtered on top of that. */ /** HTML attribute names we are willing to emit: letters, digits, `-`, `_`, `:`. */ const VALID_ATTR_NAME = /^[a-zA-Z][a-zA-Z0-9\-_:]*$/; /** Attributes whose value is a URL and therefore needs scheme filtering. */ const URL_ATTRS = new Set([ "href", "src", "action", "formaction", "poster", "cite", "data", "background", "xlink:href", ]); /** * Custom element / tag names we are willing to emit. Mirrors the HTML custom * element grammar closely enough to make tag-name breakout impossible. */ const VALID_TAG_NAME = /^[a-zA-Z][a-zA-Z0-9]*(?:-[a-zA-Z0-9]+)*$/; const FALSY_ATTR_VALUES = new Set(["", "false", "null", "undefined", "0"]); export function isFalsyAttrValue(value: string | undefined): boolean { return value === undefined || FALSY_ATTR_VALUES.has(value.trim().toLowerCase()); } /** True when `name` is safe to emit as an attribute name. */ export function isSafeAttrName(name: string): boolean { if (!VALID_ATTR_NAME.test(name)) return false; // `onclick`, `onfocus`, … execute script; never emit them from markdown. if (/^on/i.test(name)) return false; return true; } /** True when `name` is safe to emit as a tag name. */ export function isSafeTagName(name: string): boolean { return VALID_TAG_NAME.test(name); } /** * Render an attribute record to a ` k="v"` string, dropping unsafe names and * neutralising script-bearing URLs. Returns "" when nothing survives. */ export function renderAttrs( attrs?: Record, booleans?: ReadonlySet, ): string { const parts: string[] = []; for (const [k, v] of Object.entries(attrs ?? {})) { if (!isSafeAttrName(k)) continue; if (booleans?.has(k.toLowerCase())) { if (!isFalsyAttrValue(v)) parts.push(k); continue; } const value = URL_ATTRS.has(k.toLowerCase()) ? safeUrl(v) : v; parts.push(`${k}="${escapeHtml(value)}"`); } return parts.length > 0 ? ` ${parts.join(" ")}` : ""; } /** Error box matching the style other handlers use for malformed input. */ export function renderBlockError(message: string): string { return `
${escapeHtml(message)}
`; }