Module records and settings
A module record is the static description of an HTML module: what it imports, what it exports, and its
settings. It is plain JSON, and it is produced the same way whether the source was parsed by a DOM
(readHTMLModule, used by the browser loader) or scanned as text (scanHTMLModule, used by the compiler, which
needs no DOM). The loader links a record into a namespace; the compiler turns a record into JavaScript. Every
validation rule of the HTML syntax is applied while building the record, so both paths reject the
same modules with the same messages.
- The module record
readHTMLModule(doc, url),scanHTMLModule(source, url),recordFromRaw(raw, url),moduleImportOptions(record, importRecord)- Settings:
IMPORT_DEFAULTS,EXPORT_DEFAULTS,readImportSettings,readModuleSettings,readImportOptions,resolveImportOptions
The module record
Section titled “The module record”type ModuleRecord = { url: string, // as passed ("" when none) imports: ImportRecord[], // document order exports: ExportRecord[], // document order importSettings?: { // present when the module has an <html-import-settings> delimiter?: string, base?: string, conflict?: 'error' | 'reuse', load?: 'eager' | 'lazy', errors?: 'event' | 'throw', registry?: 'global' | 'scoped', // modules only }, moduleSettings?: { shadow?: 'open' | 'closed', delegatesFocus?: boolean }, // from <html-module-settings>};
type ImportRecord = { src: string, // as written as?: string, delimiter?: string, type?: string, conflict?: 'error' | 'reuse', load?: 'eager' | 'lazy', errors?: 'event' | 'throw', registry?: 'global' | 'scoped', bindings: BindingRecord[], // always present, possibly []};
type BindingRecord = { export: string, element?: string, adopt?: true };
type ExportRecord = | { kind: 'component', name: string | null, default?: true, template: string, shadow: 'open' | 'closed', delegatesFocus: boolean, styles: string[], props?: Array<{ name: string, type: 'string' | 'number' | 'boolean' }, formAssociated?: true, formControl?: string, formRole?: 'submit' | 'reset' } | { kind: 'stylesheet', name: string | null, default?: true, css: string } | { kind: 'data', name: string | null, default?: true, value: unknown } | { kind: 'reexport', src: string, type?: string, integrity?: string, name?: string | null, default?: true, import?: string };- Optional keys are present only when written in the source. An import’s
delimiter,conflict,loadanderrorsstay unset when absent (the module’simportSettings, then the built-in defaults, apply; seemoduleImportOptions). A page’s settings and the instance options never reach a module. name: nullwithdefault: trueis a default-only export; a stringnamewithdefault: trueis a named export that is also the default. At most one export hasdefault: true, and it may be a re-export.- A re-export without a
namekey is a star re-export (export * from);import: "*"is a namespace re-export (export * as <name> from); otherwiseimport(when present) is the source export’s name. Anames="…"list is expanded into one record per entry, so records never carrynames. propsis present only when the export wroteprops="…", in the order written, each with itstype(default"string"). It is the binding metadata of a record: the{{attribute}}sites themselves are found in the parsed template when the component is registered (see Data binding), identically for runtime-loaded and compiled modules.formAssociated: trueis present only when the export wroteform-associated(notform-associated="false"),formControlonly when it also wroteform-control="…", andformRoleonly when it also wroteform-role="submit"or"reset". See Form-associated components.shadowanddelegatesFocusare always present on components: the export’s attribute, else the module’smoduleSettings, else the built-in default (delegatesFocusistruefor a component with aform-control,falseotherwise).moduleSettingsis also kept on the record for reference.templateis the<template>’s content HTML (verbatim from the scanner; serialized by the DOM reader, so whitespace and attribute quoting may differ, never the structure).stylesare the<style>texts; a stylesheet’scssis its<style>texts joined with\n.
A full example: this module
<html-import-settings base="./vendor/" conflict="reuse"></html-import-settings><html-module-settings delegates-focus></html-module-settings><html-import src="./icons.html" as="icon" delimiter="-"></html-import><html-import src="./themes.html"><html-binding export="gold" adopt></html-binding></html-import><html-export name="price-tag" default> <style>:host { display: inline-block }</style> <template><icon-star></icon-star> <slot></slot></template></html-export><html-export name="sale" shadow="closed" delegates-focus="false"><template><b>Sale</b></template></html-export><html-export name="dark"><style>:host { color: white }</style></html-export><html-export name="config"><script type="application/json">{ "currency": "EUR" }</script></html-export><html-export src="./buttons.html"></html-export><html-export src="./forms.html" name="field" import="text-field"></html-export>reads (with url: 'shop.html') into:
{ "url": "shop.html", "imports": [ { "src": "./icons.html", "as": "icon", "delimiter": "-", "bindings": [] }, { "src": "./themes.html", "bindings": [{ "export": "gold", "adopt": true }] } ], "exports": [ { "kind": "component", "name": "price-tag", "default": true, "template": "<icon-star></icon-star> <slot></slot>", "shadow": "open", "delegatesFocus": true, "styles": [":host { display: inline-block }"] }, { "kind": "component", "name": "sale", "template": "<b>Sale</b>", "shadow": "closed", "delegatesFocus": false, "styles": [] }, { "kind": "stylesheet", "name": "dark", "css": ":host { color: white }" }, { "kind": "data", "name": "config", "value": { "currency": "EUR" } }, { "kind": "reexport", "src": "./buttons.html" }, { "kind": "reexport", "src": "./forms.html", "name": "field", "import": "text-field" }, { "kind": "reexport", "src": "./icons.html", "name": "icon", "import": "*" } ], "importSettings": { "base": "./vendor/", "conflict": "reuse" }, "moduleSettings": { "delegatesFocus": true }}The same module’s compiled output is shown on the Compiler page.
readHTMLModule(doc, url)
Section titled “readHTMLModule(doc, url)”readHTMLModule(doc: Document, url?: string /* = "" */): ModuleRecordRead a parsed HTML module document (from DOMParser, or any DOM) into a record. Collects every <html-import>,
<html-export>, <html-import-settings> and <html-module-settings> in document order (querySelectorAll, which
does not enter <template> content), then validates and builds the record with
recordFromRaw. Throws SyntaxError for any invalid module; messages end with in <url>
when a URL is given.
scanHTMLModule(source, url)
Section titled “scanHTMLModule(source, url)”scanHTMLModule(source: string, url?: string /* = "" */): ModuleRecordRead HTML source text into the same record, with a small dependency-free scanner (this is what the compiler uses). It follows the HTML tokenizer where it matters here:
- The input is preprocessed as a browser does: CRLF and lone CR become LF, so a Windows-edited file records the same
CSS and template text as it does at runtime; NUL becomes U+FFFD in
<style>/<script>text and attribute values. - Comments end at
-->or--!>, and<!-->and<!--->are complete (empty) comments.<!…>,<?…>and<![CDATA[…>are bogus comments that end at the first>;</followed by anything but a letter is too (</>vanishes, and</at the very end is text). - Raw-text elements (
script,style,textarea,title,xmp,iframe,noembed,noframes) end only at their own end tag, which may carry attributes (a>inside a quoted value does not end it).<script>follows the script-data escape states (<!--…<script>…</script>…-->), and<plaintext>never ends. - A tag the source ends inside (an unterminated
<html-export name="a"or an unclosed quote) is dropped, and a<template>that is never closed runs to the end of the source, as in a browser. - Attribute names are ASCII-lower-cased; values may be double-quoted, single-quoted or unquoted; the first of duplicate attributes wins; character references in values are decoded (see Character references).
- Void elements have no content; self-closing syntax is ignored on normal elements;
<template>content is kept verbatim (nested templates included) and is not part of the document. - Enough tree construction to know which elements are direct children of an
<html-export>:<p>closes an open<p>, an end tag for a non-“special” element does not close past a special one (an unclosed<div>keeps</html-export>from closing the export, as in a browser),<html>/<head>/<body>tags are ignored, and a<frameset>that replaces the body (nothing but neutral content before it) leaves the module empty.
The test suite checks that the scanner and a spec parser (parse5, reading through the same
readHTMLModule) produce the same record for every example module, for a table of tokenizer
edge cases, and for 4,000 seeded random tag-soup documents (test/scan-spec.test.js). Not modelled, so the two can
disagree: foreign content (<svg> / <math>, whose <style> and <script> are not raw text there and whose
self-closing tags are honoured), tables and other foster parenting, formatting elements and the adoption agency
(<a>, <b>, …), and <li>/<dd>/<option>-style implied end tags. Put an export’s children directly inside it, as
every example does, and none of this matters. The same goes for <select>: a current browser parses an
<html-export> inside a <select> (customizable select), as the scanner does; older parsers drop it. Both readers
record a nested <html-export> / <html-import> with its nearest enclosing export or import (nestedIn), and
recordFromRaw rejects it (see Placement and nesting rules).
Character references
Section titled “Character references”Attribute values are decoded exactly as the tokenizer does it, with the full WHATWG named table (2,125 names,
vendored as src/entities.js, generated by scripts/generate-entities.js; it is loaded by the scanner and the
compiler, never by the browser runtime). Numeric references follow the spec’s replacements: �, anything above
U+10FFFF and the surrogates become U+FFFD (never a RangeError), and €–Ÿ become the Windows-1252
characters. Legacy names work without the ; (©, &), but in an attribute value a legacy reference that is
followed by = or a letter or digit is left as written, so ?a=1©=2 and ¬it; survive. A name the table does
not know stays as written. Template, <style> and <script> text is not decoded here: it is kept verbatim.
recordFromRaw(raw, url)
Section titled “recordFromRaw(raw, url)”recordFromRaw(raw: { imports: RawElement[], exports: RawElement[], importSettings?: RawElement[], moduleSettings?: RawElement[], bindings?: Array<{ tag: 'html-binding', order?: number, attrs, parent: { tag: string, attrs } | null }>,}, url?: string): ModuleRecord
type RawElement = { tag: string, order?: number, attrs: Record<string, string>, children: Array<{ tag: string, attrs: Record<string, string>, html?: string, text?: string }>,};The shared back end of both readers: validate “raw elements” and build the record. order is the element’s
position among all collected elements, used for the placement rules (a settings element must precede every import
or export). A child’s html is a <template>’s content; text is any other child’s text content. Useful for
writing another front end (an HTML parser of your choice, a build tool’s AST).
bindings lists every <html-binding> of the document (outside templates) with its nearest enclosing element, so one that
is not a direct child of an <html-import> (typically swallowed by a self-closed <html-binding />) is rejected. A
raw <html-import>’s children must hold only <html-binding>s. Both readers fill these in.
Checks, in order: nesting; misplaced bindings; settings count and placement; <html-import-settings> and <html-module-settings> attributes;
each import (src, namespace, per-import options, bindings); lazy imports that adopt; each export (name, default,
kind, attributes, JSON); duplicate names; more than one default.
moduleImportOptions(record, importRecord)
Section titled “moduleImportOptions(record, importRecord)”moduleImportOptions(record: ModuleRecord, i: ImportRecord): { delimiter?, conflict?, load?, errors? }The options one of a module’s own imports is bound with: its own attributes, then the module’s
<html-import-settings>. Only values that were written are returned; anything absent is the built-in default,
because a page’s options never reach a module. The loader and the compiler both use it (the compiler writes the
result into each $imports entry).
Settings vocabulary
Section titled “Settings vocabulary”IMPORT_DEFAULTS, EXPORT_DEFAULTS
Section titled “IMPORT_DEFAULTS, EXPORT_DEFAULTS”IMPORT_DEFAULTS // Object.freeze({ delimiter: '--', conflict: 'error', load: 'eager', errors: 'event' })EXPORT_DEFAULTS // Object.freeze({ shadow: 'open', delegatesFocus: false })The built-in defaults: the last layer of import-option precedence, and the defaults of component exports.
readImportSettings(attrs, where)
Section titled “readImportSettings(attrs, where)”readImportSettings(attrs: Record<string, string>, where?: string): { delimiter?, base?, conflict?, load?, errors? }Validate the attributes of an <html-import-settings> (as a plain object) and return the options it sets, only
those written. Allowed: delimiter, base, conflict, load, errors, plus id, class, data-*. Throws
SyntaxError: Unknown attribute "<x>" on <html-import-settings><where>: use "delimiter", "base", "conflict", "load", "errors", Invalid <option>="<value>" on <html-import-settings><where>: use "…" or "…", Invalid delimiter …, or
Invalid base … : use a URL, relative to the document, e.g. "./vendor/ui@2/" (for a blank base). where is
appended to messages (e.g. " in ui.html").
readModuleSettings(attrs, where)
Section titled “readModuleSettings(attrs, where)”readModuleSettings(attrs: Record<string, string>, where?: string): { shadow?, delegatesFocus? }Validate the attributes of an <html-module-settings> and return the export defaults it sets. Allowed: shadow,
delegates-focus, plus id, class, data-*. delegates-focus is a boolean attribute.
readImportOptions(attrs, where)
Section titled “readImportOptions(attrs, where)”readImportOptions(attrs: Record<string, string>, where?: string): { delimiter?, conflict?, load?, errors? }The per-import options written on one <html-import> (its attributes as a plain object), validated. Other
attributes are ignored, except base, which throws SyntaxError: "base" cannot be set on <html-import><where>: it is document-level only; use <html-import-settings base="…">, or write the full path in src.
resolveImportOptions(...layers)
Section titled “resolveImportOptions(...layers)”resolveImportOptions(...layers: Array<{ delimiter?, conflict?, load?, errors? } | null | undefined>): { delimiter: string, conflict: 'error' | 'reuse', load: 'eager' | 'lazy', errors: 'event' | 'throw' }Merge option layers, most specific first: for each option, the first layer with a defined value wins, else
IMPORT_DEFAULTS. Does not validate and does not handle base.
resolveImportOptions({ load: 'lazy' }, { delimiter: '-', load: 'eager' }, null);// → { delimiter: '-', conflict: 'error', load: 'lazy', errors: 'event' }