Skip to main content

VARS_JS

Constant VARS_JS 

Source
pub const VARS_JS: &str = "// The numbers an emitter hands the stylesheet, set where a policy allows it.\n//\n// A bar\'s value, a row\'s depth, a track\'s place and a split\'s share are custom\n// properties the rules read. A `style-src` without `\'unsafe-inline\'` refuses\n// every style attribute, so the emitters write the declarations into\n// `data-vars` instead, and this sets each one through the CSSOM, which the\n// policy does not police.\n//\n// A classic script in the head, not deferred, on purpose: the observer is\n// running before the parser reaches the body, so every element gets its\n// properties as it is inserted and the first paint is already right. A deferred\n// script would paint every chart flat and then grow it.\n//\n// Only custom properties. Applying any declaration found in markup would hand\n// back exactly what the policy refused, so a name that does not start with\n// `--` is skipped.\n//\n// It also keeps this device\'s density choice, for the same reason it is not\n// deferred. The choice lives in the browser rather than with the app, because\n// it is about what is pointing at this screen, and it is worn as an attribute\n// on the root that the geometry sheet keys a block on. A picker carrying\n// `data-density-choice` and no name is how a reader changes it: nothing is\n// submitted, the store is written, and the page redraws at the new density.\n(() => {\n    \"use strict\";\n\n    /** The attribute the emitters write: `makeover_webview::VARS_ATTR`. */\n    const ATTR = \"data-vars\";\n\n    /** `makeover_webview::DENSITY_STORE_KEY`. */\n    const DENSITY_KEY = \"makeover-density\";\n    /** `makeover_webview::DENSITY_CHOICE_ATTR`. */\n    const DENSITY_CHOICE = \"data-density-choice\";\n    /** `makeover_geometry::DENSITY_ATTRIBUTE`. */\n    const DENSITY = \"data-density\";\n    /** Every `makeover_geometry::Density::attribute_value`. */\n    const DENSITIES = [\"pointer\", \"touch\"];\n\n    /**\n     * The stored choice, or \"\" for none.\n     *\n     * Storage can throw (a private window, a blocked origin) and can hold\n     * anything, so both read as no choice: the page keeps what it detects.\n     */\n    const chosen = () => {\n        try {\n            const value = localStorage.getItem(DENSITY_KEY) ?? \"\";\n            return DENSITIES.includes(value) ? value : \"\";\n        } catch {\n            return \"\";\n        }\n    };\n\n    /** Wear a choice on the root, or hand the root back to detection. */\n    const wear = (value) => {\n        const root = document.documentElement;\n        if (value) root.setAttribute(DENSITY, value);\n        else root.removeAttribute(DENSITY);\n    };\n\n    /** Show this device\'s choice on a picker that has just arrived. */\n    const show = (picker) => {\n        picker.value = chosen();\n    };\n\n    wear(chosen());\n\n    document.addEventListener(\"change\", (event) => {\n        const picker = event.target;\n        if (!(picker instanceof Element) || !picker.hasAttribute(DENSITY_CHOICE)) return;\n        const value = DENSITIES.includes(picker.value) ? picker.value : \"\";\n        try {\n            if (value) localStorage.setItem(DENSITY_KEY, value);\n            else localStorage.removeItem(DENSITY_KEY);\n        } catch {\n            // Unstored, still worn: the choice holds for this page.\n        }\n        wear(value);\n    });\n\n    /** What this script set on each element, so an update can take it back. */\n    const set = new WeakMap();\n\n    /** `--a: 1; --b: 2` as pairs, keeping only custom properties. */\n    const pairs = (text) => {\n        const found = new Map();\n        for (const part of text.split(\";\")) {\n            const at = part.indexOf(\":\");\n            if (at < 0) continue;\n            const name = part.slice(0, at).trim();\n            if (!name.startsWith(\"--\")) continue;\n            found.set(name, part.slice(at + 1).trim());\n        }\n        return found;\n    };\n\n    /**\n     * Make the element\'s properties what its attribute says now.\n     *\n     * A morph that changes the attribute in place leaves the element, so a\n     * property the new text no longer names is removed rather than kept from\n     * the old one.\n     */\n    const apply = (el) => {\n        const next = pairs(el.getAttribute(ATTR) ?? \"\");\n        for (const name of set.get(el) ?? []) {\n            if (!next.has(name)) el.style.removeProperty(name);\n        }\n        for (const [name, value] of next) el.style.setProperty(name, value);\n        set.set(el, [...next.keys()]);\n    };\n\n    /** An inserted subtree: the node itself and everything under it. */\n    const within = (node) => {\n        if (node.nodeType !== Node.ELEMENT_NODE) return;\n        if (node.hasAttribute(ATTR)) apply(node);\n        for (const el of node.querySelectorAll(`[${ATTR}]`)) apply(el);\n        if (node.hasAttribute(DENSITY_CHOICE)) show(node);\n        for (const el of node.querySelectorAll(`[${DENSITY_CHOICE}]`)) show(el);\n    };\n\n    new MutationObserver((records) => {\n        for (const record of records) {\n            if (record.type === \"attributes\") apply(record.target);\n            else record.addedNodes.forEach(within);\n        }\n    }).observe(document.documentElement, {\n        childList: true,\n        subtree: true,\n        attributes: true,\n        attributeFilter: [ATTR],\n    });\n\n    // Whatever the parser had already written before this ran.\n    within(document.documentElement);\n})();\n";
Expand description

The script that applies VARS_ATTR, for a host to serve beside its other assets.

Without it every number the rules read from that attribute is missing: bars and charts flat, tree rows unindented, a split screen at even halves.

It also restores this device’s density choice onto the root element (DENSITY_STORE_KEY into DENSITY_ATTRIBUTE) and answers a DENSITY_CHOICE_ATTR picker, for the same reason it is not deferred: a choice applied after first paint is a page that jumps. Serve it as a classic script in the head, not deferred, so its observer is running before the parser reaches the body and the first paint already has every property.

std::fs::write("static/vars.js", makeover_webview::VARS_JS)?;