makeover-webview 0.98.6

The webview renderer for makeover-layout. Emits CSS, and is the one renderer that needs no palette: var() is the late binding, so resolution stays with the browser. With the build feature, writes a consumer's generated frontend files and checks the hand-written ones against them.
Documentation
// The numbers an emitter hands the stylesheet, set where a policy allows it.
//
// A bar's value, a row's depth, a track's place and a split's share are custom
// properties the rules read. A `style-src` without `'unsafe-inline'` refuses
// every style attribute, so the emitters write the declarations into
// `data-vars` instead, and this sets each one through the CSSOM, which the
// policy does not police.
//
// A classic script in the head, not deferred, on purpose: the observer is
// running before the parser reaches the body, so every element gets its
// properties as it is inserted and the first paint is already right. A deferred
// script would paint every chart flat and then grow it.
//
// Only custom properties. Applying any declaration found in markup would hand
// back exactly what the policy refused, so a name that does not start with
// `--` is skipped.
(() => {
    "use strict";

    /** The attribute the emitters write: `makeover_webview::VARS_ATTR`. */
    const ATTR = "data-vars";

    /** What this script set on each element, so an update can take it back. */
    const set = new WeakMap();

    /** `--a: 1; --b: 2` as pairs, keeping only custom properties. */
    const pairs = (text) => {
        const found = new Map();
        for (const part of text.split(";")) {
            const at = part.indexOf(":");
            if (at < 0) continue;
            const name = part.slice(0, at).trim();
            if (!name.startsWith("--")) continue;
            found.set(name, part.slice(at + 1).trim());
        }
        return found;
    };

    /**
     * Make the element's properties what its attribute says now.
     *
     * A morph that changes the attribute in place leaves the element, so a
     * property the new text no longer names is removed rather than kept from
     * the old one.
     */
    const apply = (el) => {
        const next = pairs(el.getAttribute(ATTR) ?? "");
        for (const name of set.get(el) ?? []) {
            if (!next.has(name)) el.style.removeProperty(name);
        }
        for (const [name, value] of next) el.style.setProperty(name, value);
        set.set(el, [...next.keys()]);
    };

    /** An inserted subtree: the node itself and everything under it. */
    const within = (node) => {
        if (node.nodeType !== Node.ELEMENT_NODE) return;
        if (node.hasAttribute(ATTR)) apply(node);
        for (const el of node.querySelectorAll(`[${ATTR}]`)) apply(el);
    };

    new MutationObserver((records) => {
        for (const record of records) {
            if (record.type === "attributes") apply(record.target);
            else record.addedNodes.forEach(within);
        }
    }).observe(document.documentElement, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: [ATTR],
    });

    // Whatever the parser had already written before this ran.
    within(document.documentElement);
})();