Skip to main content

ELEMENTS_JS

Constant ELEMENTS_JS 

Source
pub const ELEMENTS_JS: &str = "// The built-in view elements.\n//\n// These are the only elements a program can use until user-defined\n// components land (spec \u{a7}14D). Each is a thin mapping onto DOM structure\n// plus a small amount of default styling, so that a program with no style\n// declarations still renders as something a person would recognise.\n//\n// Input elements bind two-way, and only to `client`-placed signals \u{2014} a\n// keystroke must not silently become a network write (spec \u{a7}14B.5). The\n// compiler enforces the placement rule; the runtime just wires the event.\n//\n// THE DIRECTORY OF THE VOCABULARY IS THE EXPORT LIST, and there is no\n// object holding one property per element. There was: `BUILTINS`, which\n// nothing in the runtime or the compiler read, whose one consumer was a\n// test asserting it existed. It was removed rather than kept, because it\n// had a measured cost and no benefit. `boa`, the engine both parity\n// suites run this file in, aborts the *process* with a Rust-level\n// `BorrowMutError` inside its own `Set` builtin once a context crosses an\n// allocation threshold \u{2014} the defect BENCHMARKS.md records as making\n// signal fan-out unmeasurable here \u{2014} and this file sat on that threshold.\n// Building the object on demand instead of at load bought about a dozen\n// elements and then stopped working too, because the function itself is\n// an object holding a reference per element.\n//\n// Nothing is lost. `element_parity.rs` calls each name in this file\n// directly, once per built-in, so an element the compiler knows and this\n// file does not export fails there with the name in the message.\n\nimport { el, safeUrl, text, variant } from \'./dom.js\';\nimport { effect } from \'./signal.js\';\nimport { markup } from \'./markup.js\';\n\n// Base styling is a CLASS NAME, not an inline style object (spec \u{a7}16.2 R6).\n// \u{a7}6 already specifies that styles compile to static CSS with generated\n// scoped class names and zero runtime cost; an inline style object costs one\n// effect and one `setProperty` per declaration, which is seven of each for a\n// `Column` and a `Row` that can never change. The declarations themselves\n// live in `base.css`, which `zdc build` copies into `styles.css`.\nconst BASE = {\n  column: \'zd-col\',\n  row: \'zd-row\',\n  error: \'zd-err\',\n  prose: \'zd-prose\',\n  preformatted: \'zd-pre\',\n};\n\n/**\n * Put a base class in front of whatever class the program asked for.\n *\n * The program\'s `class` may be a getter, so the join has to stay reactive\n * rather than stringifying a function into the attribute.\n */\nfunction withBase(p, base) {\n  const given = p.class;\n  if (given === undefined) {\n    p.class = base;\n  } else if (typeof given === \'function\') {\n    p.class = () => `${base} ${given()}`;\n  } else {\n    p.class = `${base} ${given}`;\n  }\n  return p;\n}\n\n/**\n * A `Truth` as the word an ARIA state attribute is spelled with.\n *\n * Reactive when it has to be: the program\'s value may be a getter, and\n * stringifying the function itself would write `aria-selected=\"() => \u{2026}\"`.\n */\nfunction word(value) {\n  return typeof value === \'function\' ? () => String(value()) : String(value);\n}\n\n/** Split ZDeceptron element arguments into DOM props. */\nfunction props(args = {}) {\n  const out = {};\n  const style = {};\n  for (const [name, value] of Object.entries(args)) {\n    switch (name) {\n      case \'padding\':\n        style.padding = typeof value === \'function\' ? () => `${value()}px` : `${value}px`;\n        break;\n      case \'weight\':\n        style[\'font-weight\'] = value;\n        break;\n      case \'hint\':\n        out.placeholder = value;\n        break;\n      // The ZDeceptron spelling of `src`. Filtered, not merely renamed:\n      // an image source is a request the browser issues to whatever host\n      // the value names (spec \u{a7}16.3.5, corrected).\n      case \'source\':\n        out.src = typeof value === \'function\' ? () => safeUrl(value()) : safeUrl(value);\n        break;\n      // The still a video shows before it plays: a request the browser\n      // issues at once, so it is filtered exactly as `source` is.\n      case \'poster\':\n        out.poster = typeof value === \'function\' ? () => safeUrl(value()) : safeUrl(value);\n        break;\n      case \'src\':\n      case \'href\':\n        out[name] = typeof value === \'function\' ? () => safeUrl(value()) : safeUrl(value);\n        break;\n      case \'exact\':\n        out.datetime = value;\n        break;\n      // What the letters stand for. It is `title` in the DOM, and the\n      // compiler requires it, because an `abbr` with no expansion is an\n      // acronym with nothing behind it.\n      case \'expansion\':\n        out.title = value;\n        break;\n      // What this element operates, by `id`. `Label` renames it to `for`\n      // itself, below, because that is the only element that has one.\n      case \'controls\':\n        out[\'aria-controls\'] = value;\n        break;\n      // The element that explains this one, and the element that names\n      // it. Both are an `id` and neither translates.\n      case \'describedBy\':\n        out[\'aria-describedby\'] = value;\n        break;\n      case \'labelledBy\':\n        out[\'aria-labelledby\'] = value;\n        break;\n      // Which of a set you are looking at, and how urgently a region\n      // interrupts. Both are one word from a closed set the compiler\n      // checks; nothing is translated, so both pass straight through.\n      case \'current\':\n        out[\'aria-current\'] = value;\n        break;\n      case \'live\':\n        out[\'aria-live\'] = value;\n        break;\n      // The ARIA states, and the one thing about them that is not a\n      // rename. `setAttribute` in `dom.js` implements HTML\'s boolean\n      // attributes \u{2014} `false` removes, `true` sets the empty string \u{2014} and\n      // an `aria-*` state is not one of those. Its value is the *word*\n      // `true` or the word `false`, and an unselected tab carrying no\n      // `aria-selected` announces a tablist with nothing chosen. So each\n      // of these is stringified here, reactively when it is a getter.\n      case \'selected\':\n      case \'expanded\':\n      case \'pressed\':\n      case \'checked\':\n        out[`aria-${name}`] = word(value);\n        break;\n      case \'disabled\':\n        out[\'aria-disabled\'] = word(value);\n        break;\n      // `aria-hidden`, named for the only thing it is ever right for.\n      // It hides nothing: the element stays where it was and a screen\n      // reader stops reading it.\n      case \'decorative\':\n        out[\'aria-hidden\'] = word(value);\n        break;\n      // What this control is called when no text beside it says so.\n      // `Checkbox` and `Radio` never reach here \u{2014} they read `label`\n      // themselves and wrap their box in a `<label>` holding it.\n      case \'label\':\n        out[\'aria-label\'] = value;\n        break;\n      // The ends and the landmarks of a measured range, in English.\n      case \'least\':\n        out.min = value;\n        break;\n      case \'most\':\n        out.max = value;\n        break;\n      case \'best\':\n        out.optimum = value;\n        break;\n      case \'message\':\n        break; // consumed by the element itself, never an attribute\n      case \'class\':\n        out.class = value;\n        break;\n      default:\n        out[name] = value;\n    }\n  }\n  if (Object.keys(style).length > 0) out.style = style;\n  return out;\n}\n\n/**\n * The two layout containers.\n *\n * Both take an optional leading text slot, ratified in \u{a7}4.4: `Row item.name`\n * is one text node followed by the row\'s children, exactly as `Button`\n * already is. A row with nothing to say of its own passes `undefined`,\n * which is what a source program with no leading argument compiles to.\n */\nexport function Column(value, args = {}, children = []) {\n  return el(\n    \'div\',\n    withBase(props(args), BASE.column),\n    value === undefined ? children : [text(value), ...children],\n  );\n}\n\nexport function Row(value, args = {}, children = []) {\n  return el(\n    \'div\',\n    withBase(props(args), BASE.row),\n    value === undefined ? children : [text(value), ...children],\n  );\n}\n\nexport function Text(value, args = {}) {\n  return el(\'span\', props(args), [text(value)]);\n}\n\n/**\n * A heading, at the level its nesting says.\n *\n * The compiler chooses the tag from how many sectioning elements enclose\n * the heading, so `h1` is what a heading at the top of a document is. This\n * reference implementation has no enclosing context to consult, so it\n * renders the top level, which is the case the parity test compares.\n */\nexport function Heading(value, args = {}) {\n  return el(\'h1\', props(args), [text(value)]);\n}\n\nexport function Button(label, args = {}, children = []) {\n  return el(\'button\', { type: \'button\', ...props(args) }, [text(label), ...children]);\n}\n\n/**\n * A text input bound two-way to a client signal.\n *\n * `binding` is the [read, write] pair the compiler emits for a `client`\n * signal. Passing a server or durable signal here is a compile error\n * (\u{a7}14B.5), so the runtime can assume the write is local and synchronous.\n */\nexport function Input(binding, args = {}) {\n  const [get, set] = binding;\n  return el(\'input\', {\n    type: \'text\',\n    value: get,\n    onInput: (e) => set(e.target.value),\n    ...props(args),\n  });\n}\n\n/**\n * A multi-line field, bound the way `Input` is.\n *\n * A `textarea` holds its value as a property rather than as an attribute,\n * which `setAttribute` in `dom.js` already knows; nothing here is special\n * about the binding except the tag.\n */\nexport function TextArea(binding, args = {}) {\n  const [get, set] = binding;\n  return el(\'textarea\', {\n    value: get,\n    onInput: (e) => set(e.target.value),\n    ...props(args),\n  });\n}\n\n/**\n * A masked field.\n *\n * The three baked attributes are the whole of what the browser gives a\n * password field and nothing else does. What the *compiler* adds is a rule\n * about where the bound signal may appear, which has no counterpart here\n * because this file builds nodes and does not read programs; `elements.rs`\n * states the decision and `view.rs` enforces it.\n */\nexport function PasswordInput(binding, args = {}) {\n  const [get, set] = binding;\n  return el(\'input\', {\n    type: \'password\',\n    autocomplete: \'current-password\',\n    spellcheck: \'false\',\n    value: get,\n    onInput: (e) => set(e.target.value),\n    ...props(args),\n  });\n}\n\n/**\n * A number, typed. And a date, picked, which is the same control with a\n * different `type` and a different reading of the same number.\n *\n * # Both bind an `Option`, and both bind through `valueAsNumber`\n *\n * A `Slider` always has a number, because a track always has a thumb on\n * it. A box a person types in does not: empty, a lone `-` and a\n * half-written `1e` all report `valueAsNumber` `NaN`, which is not a\n * value ZDeceptron has. So the read is `None` or `Some n`.\n *\n * The write is `valueAsNumber` and not `value`. A number field runs\n * HTML\'s value sanitisation, so `value` is the empty string while a\n * reader is part way through `1.`; comparing text would rewrite the box\n * on every keystroke and a decimal point could never be typed at all.\n *\n * A date field\'s `valueAsNumber` is defined by HTML as the moment at\n * midnight UTC on the chosen day, which is what `prelude/time.zd` means\n * by a moment. So the browser renders `YYYY-MM-DD` from the number and\n * reads the number back, and no calendar is written here.\n *\n * # Why the two rules are spelled here rather than imported\n *\n * The compiler emits them into a program\'s own preamble\n * (`intrinsics.rs`\'s `$optionalNumber` and `$numberField`) rather than\n * exporting them from `dom.js`, because the shipped runtime is against\n * the size gate `zdc-bench` holds it to. This file is a reference\n * implementation and is never shipped, so it says the same two things in\n * its own words \u{2014} which is what this file is *for*: `element_parity.rs`\n * compares the node against the compiler\'s, and `vocabulary.rs` drives\n * the behaviour.\n */\nexport function NumberInput(binding, args = {}) {\n  return numericField(\'number\', binding, args);\n}\n\nexport function DateInput(binding, args = {}) {\n  return numericField(\'date\', binding, args);\n}\n\nfunction numericField(type, [get, set], args) {\n  const node = el(\'input\', {\n    type,\n    onInput: (e) => {\n      const read = e.target.valueAsNumber;\n      // `Number.isNaN`, not the coercing global: `isNaN(\'\')` is `false`.\n      set(Number.isNaN(read) ? variant(\'None\') : variant(\'Some\', read));\n    },\n    ...props(args),\n  });\n  effect(() => {\n    const held = get();\n    // `NaN` empties the box, which is what `None` looks like and where a\n    // non-finite number has to go too: the setter throws on an infinity.\n    const shown =\n      held.tag === \'Some\' && Number.isFinite(held.fields[0]) ? held.fields[0] : NaN;\n    if (!Object.is(node.valueAsNumber, shown)) node.valueAsNumber = shown;\n  });\n  return node;\n}\n\n/**\n * A bounded number, dragged.\n *\n * The listener reads `valueAsNumber` and not `value`: the signal holds a\n * number, and `value` is the text of one, so a `Whole` given `\'55\'` would\n * render `551` the moment anything added to it.\n */\nexport function Slider(binding, args = {}) {\n  const [get, set] = binding;\n  return el(\'input\', {\n    type: \'range\',\n    value: get,\n    onInput: (e) => set(e.target.valueAsNumber),\n    ...props(args),\n  });\n}\n\n/**\n * One variant of a `choice`, picked from a list.\n *\n * `variants` is the choice\'s own arms, in declaration order, which the\n * compiler writes from the declaration. The value on the wire is the\n * variant\'s tag, because an option\'s value is one string.\n */\nexport function Select(binding, variants = [], args = {}) {\n  const [get, set] = binding;\n  return el(\n    \'select\',\n    {\n      value: () => get().tag,\n      onChange: (e) => set(variant(e.target.value)),\n      ...props(args),\n    },\n    variants.map((name) => el(\'option\', { value: name }, [name])),\n  );\n}\n\n/**\n * One radio of a group.\n *\n * `option` is the variant\'s tag, which the compiler writes down: it is\n * this button\'s value in the markup and the tag the binding compares\n * against. The group is the signal, named by `group`, so the browser\n * clears the others when one is picked.\n */\nexport function Radio(binding, group, option, args = {}) {\n  const [get, set] = binding;\n  const button = el(\'input\', {\n    type: \'radio\',\n    name: group,\n    checked: () => get().tag === option,\n    onChange: () => set(variant(option)),\n  });\n  // The attribute, not the property. A radio\'s value never changes, so it\n  // is markup on both sides: the compiler bakes it into the template, and\n  // routing it through `el` here would set the property instead and the\n  // two trees would differ by exactly that.\n  button.setAttribute(\'value\', option);\n  if (args.label === undefined) return button;\n  return el(\'label\', { class: BASE.row }, [button, text(args.label)]);\n}\n\nexport function Checkbox(binding, args = {}) {\n  const [get, set] = binding;\n  const box = el(\'input\', {\n    type: \'checkbox\',\n    checked: get,\n    onChange: (e) => set(e.target.checked),\n  });\n  if (args.label === undefined) return box;\n  return el(\'label\', { class: BASE.row }, [box, text(args.label)]);\n}\n\n/**\n * Completion toward a goal, bound one way.\n *\n * The leading argument is the value, and nothing writes back: this is a\n * report rather than a control, so there is no listener.\n */\nexport function Progress(value, args = {}) {\n  return el(\'progress\', { value, ...props(args) });\n}\n\n/** A value inside a range, with the landmarks a browser colours it by. */\nexport function Meter(value, args = {}) {\n  return el(\'meter\', { value, ...props(args) });\n}\n\nexport function Spinner(args = {}) {\n  return el(\'span\', { \'aria-busy\': \'true\', ...props(args) }, [\'\u{2026}\']);\n}\n\nexport function ErrorBar(args = {}) {\n  return el(\'div\', withBase({ role: \'alert\', ...props(args) }, BASE.error), [\n    text(args.message ?? \'\'),\n  ]);\n}\n\n// --- structure, text, lists and media --------------------------------------\n//\n// These carry no base class and no baked-in attribute: they are the\n// language\'s semantic vocabulary, and what they mean is the tag itself.\n// Each is written out rather than generated from a table, because the whole\n// value of this file is being an *independent* statement of the DOM shape\n// that `element_parity.rs` checks the compiler\'s table against. A table\n// here would be the compiler\'s table again, in JavaScript.\n\n/** A container: everything it shows is nested inside it. */\nfunction group(tag) {\n  return (args = {}, children = []) => el(tag, props(args), children);\n}\n\n/** An element whose leading argument is one text node, before children. */\nfunction shown(tag) {\n  return (value, args = {}, children = []) =>\n    el(tag, props(args), value === undefined ? children : [text(value), ...children]);\n}\n\n/** An element with no children at all. */\nfunction empty(tag) {\n  return (args = {}) => el(tag, props(args));\n}\n\nexport const Main = group(\'main\');\nexport const Section = group(\'section\');\nexport const Article = group(\'article\');\nexport const Aside = group(\'aside\');\nexport const Navigation = group(\'nav\');\nexport const Header = group(\'header\');\nexport const Footer = group(\'footer\');\nexport const Address = group(\'address\');\nexport const Quote = group(\'blockquote\');\nexport const List = group(\'ul\');\nexport const NumberedList = group(\'ol\');\nexport const Terms = group(\'dl\');\nexport const HeaderRow = group(\'tr\');\nexport const TableRow = group(\'tr\');\nexport const Figure = group(\'figure\');\nexport const Form = group(\'form\');\nexport const Fieldset = group(\'fieldset\');\nexport const Details = group(\'details\');\n\nexport const Paragraph = shown(\'p\');\nexport const Emphasis = shown(\'em\');\nexport const Strong = shown(\'strong\');\nexport const Code = shown(\'code\');\nexport const CodeBlock = shown(\'pre\');\nexport const Key = shown(\'kbd\');\nexport const Time = shown(\'time\');\nexport const Small = shown(\'small\');\nexport const Mark = shown(\'mark\');\nexport const Abbreviation = shown(\'abbr\');\n/**\n * The name of a control, tied to it by `id`.\n *\n * The one element where `controls` is not `aria-controls`. A `label` has\n * HTML\'s own `for`, which is what clicking the label acts on and what the\n * accessible-name computation reads there, so the same word reaches the\n * browser by the route that works \u{2014} exactly as `label` itself does, going\n * to `aria-label` here and into a wrapping `<label>` on a `Checkbox`. The\n * compiler\'s table makes the same split, keyed on the element.\n */\nexport function Label(value, args = {}, children = []) {\n  const p = props(args);\n  if (\'aria-controls\' in p) {\n    p.for = p[\'aria-controls\'];\n    delete p[\'aria-controls\'];\n  }\n  return el(\'label\', p, value === undefined ? children : [text(value), ...children]);\n}\nexport const Legend = shown(\'legend\');\nexport const Summary = shown(\'summary\');\nexport const Superscript = shown(\'sup\');\nexport const Subscript = shown(\'sub\');\nexport const Item = shown(\'li\');\nexport const Term = shown(\'dt\');\nexport const Description = shown(\'dd\');\nexport const Caption = shown(\'figcaption\');\nexport const Cell = shown(\'td\');\n\n/**\n * A table, whose rows sit in a row group this function writes.\n *\n * The parser inserts a `tbody` of its own around any `tr` found directly\n * inside a `table`, so a table built without one here and cloned from a\n * template there would be two different trees.\n */\nexport function Table(args = {}, children = []) {\n  return el(\'table\', props(args), [el(\'tbody\', {}, children)]);\n}\n\n/** A column heading, which says so: a `th` that heads its column. */\nexport function HeaderCell(value, args = {}, children = []) {\n  return el(\n    \'th\',\n    { scope: \'col\', ...props(args) },\n    value === undefined ? children : [text(value), ...children],\n  );\n}\n\n/**\n * A rendered document: markup, parsed as markup.\n *\n * The one built-in whose content is parsed rather than assigned as a text\n * node. It is safe for the reason `dom.js`\'s `markup` is safe and for no\n * other: its argument\'s type is `Markup`, the compiler admits nothing else\n * there, and the only producer of a `Markup` is `build markdown`, which\n * escapes raw HTML and rewrites script-bearing URLs before it returns.\n */\nexport function Prose(value, args = {}) {\n  const p = props(args);\n  withBase(p, BASE.prose);\n  const node = el(\'div\', p);\n  markup(node, typeof value === \'function\' ? value() : value);\n  return node;\n}\n\nexport const Divider = empty(\'hr\');\nexport const Break = empty(\'br\');\nexport const Canvas = empty(\'canvas\');\n\n/**\n * Preserved whitespace that is not code.\n *\n * A `pre`, as `CodeBlock` is, and told apart by its class: `zd-pre` takes\n * the document\'s own typeface and lets long lines wrap, which is what a\n * poem or an address block wants and what a listing must not have.\n */\nexport function Preformatted(value, args = {}, children = []) {\n  return el(\n    \'pre\',\n    withBase(props(args), BASE.preformatted),\n    value === undefined ? children : [text(value), ...children],\n  );\n}\n\n/** An image. `source` and `alt` are required by the compiler, not here. */\nexport function Image(args = {}) {\n  return el(\'img\', props(args));\n}\n\n/**\n * A video. `controls` is baked rather than offered: a media element with\n * no controls can be operated by a pointer and by nothing else.\n */\nexport function Video(args = {}) {\n  return el(\'video\', { controls: \'\', ...props(args) });\n}\n\n/** Audio, on the same terms as `Video`. */\nexport function Audio(args = {}) {\n  return el(\'audio\', { controls: \'\', ...props(args) });\n}\n\n/**\n * An embedded document, sandboxed to nothing.\n *\n * The empty `sandbox` grants no capability at all: no script, no form, no\n * top-level navigation, no popup, and an opaque origin, so the framed\n * document can read nothing of the page that embedded it. There is no\n * argument that widens it; `elements.rs` states why.\n */\nexport function Frame(args = {}) {\n  return el(\'iframe\', {\n    sandbox: \'\',\n    referrerpolicy: \'no-referrer\',\n    loading: \'lazy\',\n    ...props(args),\n  });\n}\n\n/**\n * A hyperlink, and routing\'s one element (spec \u{a7}14G.2 revision 1).\n *\n * The leading argument is where it goes \u{2014} \u{a7}14G.2 writes `Link Home` with\n * the destination first and the content nested under it \u{2014} and it is\n * filtered, because `setAttribute(\'href\', \'javascript:\u{2026}\')` is script\n * execution that no amount of HTML escaping would have caught.\n *\n * A real anchor with a real `href`, because that is the whole argument:\n * clicking one is a browser navigation, so every navigation is crawlable,\n * works with a middle click, and needs no runtime at all. When the\n * destination is one of the program\'s routes the compiler has already\n * rendered the URL; nothing here parses a path or matches a pattern.\n */\nexport function Link(destination, args = {}, children = []) {\n  const href =\n    typeof destination === \'function\' ? () => safeUrl(destination()) : safeUrl(destination);\n  return el(\'a\', { href, ...props(args) }, children);\n}\n";
Expand description

The built-in view elements.