Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
67 lines
2.3 KiB
JavaScript
67 lines
2.3 KiB
JavaScript
// The only document.createElement in the app.
|
|
//
|
|
// Everything the kit renders comes through here, which is what makes the
|
|
// no-styling rule checkable: a module has no route to a raw element, and the
|
|
// scanner only has one construction site to watch.
|
|
|
|
import { asText } from "./text.js";
|
|
|
|
const UNSAFE_ATTRS = new Set(["style", "class", "srcdoc", "onload", "onerror"]);
|
|
|
|
/**
|
|
* el("div", { cls, text, attrs, on, children })
|
|
*
|
|
* `cls` takes kit class names only -- callers inside the kit. `text` goes
|
|
* through asText. `attrs` refuses style/class and any inline handler, so no
|
|
* amount of prop-forwarding can turn data into presentation or script.
|
|
*/
|
|
export function el(tag, opts = {}) {
|
|
const node = document.createElement(tag);
|
|
const { cls, text, attrs, on, children } = opts;
|
|
|
|
if (cls) node.className = Array.isArray(cls) ? cls.filter(Boolean).join(" ") : cls;
|
|
if (text !== undefined) node.appendChild(asText(text));
|
|
|
|
if (attrs) {
|
|
for (const [name, value] of Object.entries(attrs)) {
|
|
if (value == null || value === false) continue;
|
|
const lower = name.toLowerCase();
|
|
if (UNSAFE_ATTRS.has(lower) || lower.startsWith("on")) {
|
|
throw new TypeError(`Refusing to set "${name}": presentation and handlers are the kit's, not a caller's.`);
|
|
}
|
|
node.setAttribute(name, value === true ? "" : String(value));
|
|
}
|
|
}
|
|
|
|
if (on) for (const [event, handler] of Object.entries(on)) node.addEventListener(event, handler);
|
|
if (children) for (const child of [].concat(children)) if (child) node.appendChild(child);
|
|
|
|
return node;
|
|
}
|
|
|
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
|
|
/**
|
|
* An SVG element. Separate because SVG needs createElementNS -- createElement
|
|
* would produce an HTMLUnknownElement that renders as nothing at all.
|
|
* Attributes only: SVG carries no module content, so there is no text path here.
|
|
*/
|
|
export function svg(tag, attrs = {}) {
|
|
const node = document.createElementNS(SVG_NS, tag);
|
|
for (const [name, value] of Object.entries(attrs)) {
|
|
if (value == null || value === false) continue;
|
|
node.setAttribute(name, String(value));
|
|
}
|
|
return node;
|
|
}
|
|
|
|
/** A detached fragment. Panels build into one so a half-built tree never lands. */
|
|
export function frag() {
|
|
return document.createDocumentFragment();
|
|
}
|
|
|
|
export function clear(node) {
|
|
while (node.firstChild) node.removeChild(node.firstChild);
|
|
return node;
|
|
}
|