HTML¶
One HTML file is one screen. ps2ui-layout parses it, applies the CSS file beside it, and emits the IR.
What it is¶
The parser reads the well-formed subset of HTML and refuses everything else with a line number. It is written for markup you author, not markup you scrape. There is no error recovery: a mismatched close tag stops the compile rather than producing a tree nobody wrote.
Structure comes from the element tree. Every visual decision comes from the stylesheet, which the compiler reads from the second positional argument. The document supplies no styling of its own, so a style attribute and an inline <style> block both go unread.
Thirteen attributes reach the compiler. They are listed under Reference table. Every other attribute is parsed, kept on the element, and never looked at again.
Minimal example¶
This is the screen from the tutorial. data-repeat stamps out six rows. Each row holds two slots and one focus node.
<screen name="library">
<div class="page">
<div class="header">
<span class="title">Game Library</span>
<span class="count" data-slot="count" data-slot-capacity="16">0 titles</span>
</div>
<div class="row" data-repeat="6" id="row-{i}" focusable>
<span class="name" data-slot="name-{i}" data-slot-capacity="40">--</span>
<span class="size" data-slot="size-{i}" data-slot-capacity="12">--</span>
</div>
</div>
</screen>
<screen> is not a ps2ui tag. It is an ordinary element, and the name on it does nothing, because the root is not focusable. The IR that run wrote carries 13 slots and 6 focus nodes named row-0 to row-5.
Reference table¶
| attribute | element | effect | page |
|---|---|---|---|
id |
any | Matches #id in the stylesheet. Names the focus node when the element is focusable. |
CSS |
class |
any | Matches .class in the stylesheet. Whitespace-separated. |
CSS |
name |
any focusable element | Names the focus node when the element carries no id. |
Focus and navigation |
focusable |
any | Adds a focus node and opens a focus scope. Presence only. | Focus and navigation |
autofocus |
any focusable element | Makes that node the initial focus. Presence only. | Focus and navigation |
src |
<img> |
PNG path, resolved against the document's directory. | Images |
palettize |
<img> |
Quantizes the image to 8-bit indexed plus CLUT at bake time. Presence only. | Images |
data-tex-slot |
<img> |
Reserves a streamed texture slot under this name instead of baking a file. | Images |
data-slot |
any | Marks the element's text as runtime-replaceable under this name. | Dynamic text |
data-slot-capacity |
a data-slot element |
Bytes reserved for the runtime string. Defaults to 63. | Dynamic text |
data-repeat |
any element below the root | Stamps out that many copies of the element at compile time. | Lists |
data-keep |
any | Exempts the element's own geometry from the baker's dead-geometry trim. Presence only. | ps2ui-bake |
data-nocontrast |
any | Exempts the element's text from the contrast lint. Presence only. | CRT linter |
focusable, autofocus, palettize, data-keep and data-nocontrast test for presence. The value is never read, so focusable="false" is focusable.
Behaviour¶
Tags¶
There is no tag whitelist. Any name matching [a-zA-Z0-9-]+ becomes an element and lays out under whatever CSS selects it. Tag names and attribute names fold to lower case. Attribute values do not.
| tags | treatment |
|---|---|
br, hr, img, input, meta, link |
Void. They take no closing tag and hold no children. |
html, body |
Transparent. Both tags are dropped and the children attach to the enclosing element. |
head, style, script, title |
Skipped whole, from the open tag to the matching close tag. |
<!doctype ...>, <!-- ... --> |
Discarded. A comment does not split the text around it. |
| everything else | An ordinary element. |
A document with one top-level element compiles to that element. A document with several gets a synthetic root.
<!doctype html>
<html>
<head><title>skipped</title><style>.page { background: red }</style></head>
<body>
<div class="page">
<section><my-thing>Any tag lays out</my-thing></section>
<hr>
<span>After the rule</span>
</div>
<script>ignored()</script>
</body>
</html>
The four commands are the page rect, the my-thing text, the hr rect and the span text. The inline sheet's red never reaches the page rect, which keeps the external sheet's #10141f.
Text and whitespace¶
Whitespace collapses once, at parse time. Each text run is entity-decoded, then every whitespace run becomes one space. A run that trims to nothing emits no text node. The stylesheet cannot undo this, and there is no white-space: pre.
decodes to U+00A0 and then collapses with everything else, so it reaches the IR as an ordinary space. Author a no-break space and it becomes breakable.
Entities¶
| entity | character |
|---|---|
& |
& |
< |
< |
> |
> |
" |
" |
' |
' |
|
U+00A0, then collapsed to a space |
· |
· |
… |
… |
&#N; |
code point N, decimal |
&#xN; |
code point N, hexadecimal |
Any other name is left in the text exactly as written. Attribute values decode the same way.
<div class="page">
<span class="label">a b</span>
<span class="label">one two
three</span>
<span class="label">joined<!-- comment -->up</span>
<span class="label">& < > " ' · … × △ ©</span>
</div>
That block is the text field of each text command in build/text.json, and the stylesheet behind it sets white-space: pre.
Repeat substitution¶
Inside a data-repeat subtree, {i} becomes the 0-based index and {n} the 1-based one. Substitution runs over every attribute value and every text node in the subtree. data-repeat itself is consumed and never reaches the cascade. Outside a repeat, the braces stay in the text. Counts and nesting rules live on Lists.
Naming focus nodes¶
A focus node takes its name from the element id. Failing that it takes the name attribute. Failing both it is called box followed by the box id, which changes whenever the document changes. Name every focusable element the runtime has to address.
Limits and errors¶
Unknown data- attributes warn and compile. Unknown attributes without that prefix are silent, so a misspelt focusabel produces a screen with no navigation and no message.
warning: unknown attribute: <span> line 2: data-capacity is not read by anything — did you mean data-slot-capacity?
warning: unknown attribute: <span> line 3: data-wobble is not read by anything — known: data-keep, data-nocontrast, data-repeat, data-slot, data-slot-capacity, data-tex-slot
ps2ui-layout: 2 paint commands, 0 focusables -> build/typo.json
Compiling the same document with and without style, width, height and alt gives two byte-identical IR files and no warning:
ps2ui-layout plain.html inert.css --fonts fonts/fonts.json -o build/plain.json
ps2ui-layout inert.html inert.css --fonts fonts/fonts.json -o build/inert.json
diff build/plain.json build/inert.json
ps2ui-layout: 3 paint commands, 0 focusables -> build/plain.json
ps2ui-layout: 3 paint commands, 0 focusables -> build/inert.json
Every row below stops the compile. ps2ui-layout prints the message on stderr with an error: prefix and exits 1. Full catalogue on Diagnostics.
| message | cause |
|---|---|
html: line N: unexpected end of input in tag |
Input ends between the tag name and >. |
html: line N: malformed attribute |
An attribute name came out empty, from a leading = or a stray / inside the tag. |
html: line N: attribute A must be quoted |
= is followed by something other than " or '. |
html: line N: unterminated attribute A |
Input ends before the closing quote. |
html: line N: unterminated comment |
<!-- has no -->. |
html: line N: closing </T> with no open element |
A close tag appears with nothing open. |
html: line N: closing </T> but <U> (line M) is open |
The close tag does not match the innermost open element. |
html: line N: bare "<" in content; write < |
< is followed by something that cannot start a tag name. |
html: line N: unterminated <T> |
A head, style, script or title element has no close tag. |
html: <T> opened on line N is never closed |
The stack is not empty at end of input. This one carries no line N: prefix. |
layout: <T> line N: data-slot needs a name |
data-slot is present with an empty value. |
layout: <T> line N: a data-slot element must contain exactly one text node (the placeholder), no child elements |
A data-slot element is empty or holds a child element. |
The last two come from the box builder, so they carry the layout: prefix rather than html:.
error: layout: <span> line 2: a data-slot element must contain exactly one text node (the placeholder), no child elements
packages/layout/src/html.js carries an eleventh throw, malformed tag <T>, which no input reaches. Its attribute reader returns only when the cursor sits on > or />, and the caller handles both cases above the throw.
Related pages¶
- CSS for selectors, properties and the two hard rules.
- Dynamic text for what a slot does at runtime.
- Lists for
data-repeatcounts and the runtime list window. - Images for
src,palettizeand streamed slots. - Focus and navigation for the solver and reachability.
- CRT linter for the contrast rule
data-nocontrastopts out of. - ps2ui-layout for the flags that compile a document.
- ui.json for the IR every example above printed.