# PD4ML v42 — HTML5 Element Support Matrix

Each row describes the v42 engine's behavior: its CSS cascade, its built-in user-agent stylesheet, the presentational hints of legacy attributes, and its layout and painting. Where an attribute or CSS property is parsed but produces no visible effect, it's marked accordingly.

> Canonical tag/attribute/CSS-property lists per the WHATWG HTML Living Standard and the CSS specifications. Comparisons against the older v40 engine are stated only where the v42 source itself carries an explicit comment documenting the change — nothing below is an assumption about v40 behavior.

## Key cross-cutting findings

The dozen or so facts most worth knowing before relying on this engine for a given document — pulled out of the full matrix below because they cut across many tags or are easy to miss inside a single row.

* **Genuinely implemented** — `<form>` family — real interactive AcroForm generation is wired for v42 (`PdfTargetRenderer42#collectFormFields`/`emitFormFields`, gated by `PdfRenderContext#generateForms()` off `PD4ML.generateForms(...)`), mirroring v40's own per-renderer scope call-for-call: `<textarea>` and `<select>` always get a real widget; `<input>` gets one for text/password/hidden/checkbox/radio (and any unrecognized type, per HTML5's "defaults to text" rule) but deliberately NOT for `submit`/`reset`/`button`/`image`/`file`/`color`/`range`; `<button>` (bare/submit or `reset`) gets a real SubmitForm/ResetForm push-button action that reads its nearest `<form>` ancestor's `action`/`method`. `<label for>` is purely cosmetic — no click-association code exists anywhere in this path. Every widget carries its own appearance stream (value, password bullets, wrapped textarea, selected option, highlighted list rows), laid out as Acrobat lays out a field it is editing; the AcroForm sets no `NeedAppearances`, and the fields' DA font is a non-embedded WinAnsi TrueType font.
* **Genuinely implemented** — `<meta charset>` / `http-equiv` Content-Type decode the source when the calling code gives no encoding (`readHTML(InputStream)`, `readHTML(InputStream, URL)`, `readHTML(URL)`); a byte order mark comes first, an encoding passed explicitly by the caller wins, as a transport (HTTP) charset does.
* **Real gap** — `<plaintext>` doesn't switch the tokenizer into a permanent raw-text state — markup after it is still parsed normally, unlike every real browser's (admittedly bizarre) spec-mandated behavior.
* **Real gap** — `<dialog open>` is a complete no-op — content renders identically with or without the attribute, unlike the browser default of `dialog:not([open]){display:none}`.
* **Real gap** — `<xmp>`, `<listing>` and `<plaintext>` get the monospace/`white-space:pre` rule but no `display:block` — they lay out as inline elements (side by side, no vertical margins), where browsers make all three blocks like `<pre>`.
* **Placeholder only** — `<iframe>`, `<embed>`, `<object>`, `<canvas>`, `<video>`, `<audio>` all fall through the same generic branch: a sized light-gray box with a thin border and a centered label (the `title`, else the tag name), no fetch/decode/render of the referenced content at all. The one exception: an `<object>` with no `data` but with fallback content shows that fallback content instead.
* **Known limitation** — `<ol reversed>` counts down from the number of `<li>` children only — it ignores both the list's `start` and any `<li value=N>`, unlike the forward-counting path.
* **Known limitation** — The HTML5 "strip one leading newline after the opening tag" rule is implemented for `<textarea>` only — `<pre>` and `<listing>` both show a spurious leading blank line real browsers suppress.
* **Known limitation** — `<wbr>` offers no line-break opportunity: a long word with a `<wbr>` in it overflows its box instead of breaking there (nor does a U+200B zero-width space; a soft hyphen `&shy;` does break).
* **Genuinely implemented** — Flexbox and CSS Grid are real, working layout modes (dedicated `FlexLayout`/`GridLayout`), not just parsed-and-ignored. `<svg>` renders true vector output and `<math>` renders real MathML via JEuclid — neither is a placeholder.
* **Genuinely implemented** — `<details>`/`<summary>` disclosure markers, `<fieldset>`/`<legend>` notched borders, `<picture>`/`<source>` media-query selection, table colspan/rowspan with per-segment collapsed borders, and `<select>` dropdown/listbox modes are all real, specifically-built implementations, not incidental side effects.
* **Bold nesting** — `<strong>`/`<b>` use `font-weight: bolder` in the UA stylesheet, not a literal `bold` — this resolves relative to whatever weight is already inherited, so nesting under an already-bold/heavy ancestor compounds correctly (matching real browsers) instead of every level clamping to the same fixed weight.
* **PDF/UA structure tagging** — headings get real distinct `/H1`–`/H6` types (not a generic Paragraph tag), `<section>`/`<nav>`/`<main>`/`<article>` get distinct `/Sect`/`/Sect`/`/Part`/`/Art` types (not one shared generic tag), `<blockquote>` gets `/BlockQuote`, `<figure>` gets `/Figure` and `<figcaption>`/`<caption>` get `/Caption`, `<a>` gets `/Link`, a real `<img>` and an inline `<svg>` get `/Figure` (the image's alternate text from `alt`, else `title`; the SVG's from `aria-label`, `title` or its own `<title>`), `<math>` gets `/Formula` with alternate text (its `alttext`, else the formula's text), `<ul>`/`<ol>` get `/L` with each `<li>` split into `/Lbl` (the marker) and `/LBody`, `<dl>` gets `/L` with `<dt>` as `/LI`+`/Lbl` and `<dd>` as `/LI`+`/LBody`, and `<th>` carries its `/Scope` (from `scope`, else from its position). The page and canvas fills are marked as artifacts. `<aside>`/`<header>`/`<footer>`/`<address>` get no structure element of their own — their text is tagged as plain paragraphs.

## Document metadata, sectioning & grouping content

The document skeleton, landmarks, headings, and block-level grouping elements.

| Tag | Status | Attributes | CSS properties | Comments |
| --- | --- | --- | --- | --- |
| `<html>` | Full | `lang` (read generically for bidi/hyphenation elsewhere); `dir="rtl"\|"ltr"` (case-insensitive) | UA default: block, serif 16px — matches Chrome's actual default font, not a generic sans guess | Auto-inserted by the tree builder if omitted. |
| `<head>` | Full | none read | `display:none` | Auto-inserted if missing; base/basefont/bgsound/link/meta/title/style/script/template met before any body content go into it. Met after body content has started, they stay where they are in the body (a `<style>` there still applies), as in browsers. |
| `<title>` | Full | none | `display:none` | Content genuinely consumed — `Document.getTitle()` returns the trimmed, entity-decoded text (RCDATA parsing, tags inside not recognized) of the first `<title>` in `<head>`. Used for the PDF document title and e.g. for CLI output filenames. A stray second `<title>`, or one inside `<body>`, is ignored. |
| `<base>` | Full | `href` (real relative-URL resolution); ~~target~~ (no navigation concept in PDF output) | `display:none` | Only the first `<base href>` in the document is honored, matching the HTML5 rule. |
| `<link>` | Partial | `rel="stylesheet"` (real external CSS loading, gated by `config.loadExternalResources`); `href`, `media` both honored; `rel="alternate stylesheet"` correctly excluded; other `rel` values (icon, preload, manifest…) ~~inert~~ | `display:none` | The one `rel` value that matters for a print/PDF target is fully wired. |
| `<meta>` | Partial | `charset` and `http-equiv="Content-Type"` (its `charset=` part); `name="author"`/`"description"`/`"subject"`/`"keywords"` (DOCX/RTF document properties); ~~other http-equiv values~~ (refresh etc.), ~~name="viewport"~~ not read | `display:none` | The encoding declaration is found by an HTML5-style prescan of the first bytes and decodes the source when the calling code gives no encoding: a byte order mark comes first, an encoding passed explicitly by the caller wins, as a transport (HTTP) charset does. `viewport` having no effect is correct/expected for a paged renderer. |
| `<style>` | Full | `type` (must be empty/text/css or the block is skipped), `media` | `display:none`; content parsed as an author-origin stylesheet, incl. `@import` | Cleanly wired into the cascade. |
| `<body>` | Full | `text`→color, `bgcolor`→background-color, `background`→background-image url(), `link`/`vlink`/`alink`→ the colors of the UA's `a:any-link`/`a:visited`/`a:any-link:active` rules; `marginheight`/`marginwidth` (and the older `topmargin`/`bottommargin`/`leftmargin`/`rightmargin`)→margins | UA default `display:block`, no default margin — the page margins already inset the content | Legacy presentational attributes fully covered. `vlink`/`alink` are read, but nothing is ever visited or active in a static conversion, so only `link` shows. |
| `<article>` `<section>` `<nav>` `<aside>` `<header>` `<footer>` `<address>` `<main>` | Full | none tag-specific | block (UA); `address` also gets `font-style:italic` | Flex/grid containers work identically here (layout mode is display-driven, not tag-specific). PDF/UA tagging: `section`→`/Sect`, `nav`→`/Sect`, `main`→`/Part`, `article`→`/Art` (each distinct, not one shared generic tag); `aside`/`header`/`footer`/`address` get no structure element of their own (their text is tagged as plain paragraphs). Accessibility-tagging nuance only, not a visual gap. |
| `<h1>`–`<h6>` | Full | none | Per-level font-size/weight/margin matching Chrome's own defaults exactly (h1 2em…h6 0.67em) | Tagged as distinct PDF/UA `/H1`–`/H6` structure types. |
| `<hgroup>` | Full | none | `display:block` only | Plain block container — no special heading/subtitle grouping or outline behavior, which is an acceptable simplification for a static renderer. |
| `<p>` | Full | none | `display:block; margin:1em 0` | Implicit-close set (on encountering pre/table/headings/lists/hr/etc.) closely matches the HTML5 spec's own optional-tag rules. PDF/UA-tagged as a generic `/Div`, same as `<div>` — not its own `/P` structure type. |
| `<hr>` | Partial | `size`→height (does not flatten into a solid bar, matching Chrome); `noshade`→`border-style:solid`; `width`/`height` legacy attrs; `align` (left/right/center position the rule box via its margins); ~~color~~ (not read at all — a legacy `color="red"` is a no-op) | UA default `border:1px inset #808080; height:0; margin:0.5em auto` | size/noshade/width/align genuinely wired; color is the one legacy attribute left unimplemented. |
| `<pre>` | Partial | none tag-specific | `display:block; font-family:monospace; white-space:pre; margin:1em 0`; tabs advance to tab stops every `tab-size` spaces (8 by default) or every `tab-size` length, as in browsers | No leading-newline-after-opening-tag stripping (unlike `<textarea>`, which does this correctly) — a real spec deviation, produces a spurious leading blank line. |
| `<blockquote>` | Full | `cite` (parsed, never read — correct, real browsers also give it no visual effect) | `margin:1em 40px` | PDF/UA-tagged `/BlockQuote`. |
| `<ol>` | Partial | `start` (parsed int, default 1); `reversed` (counts down from total `<li>` count); `type="1"\|"a"\|"A"\|"i"\|"I"`→list-style-type | `list-style-type:decimal; padding-left:40px` | Known bug: `reversed` always counts down to 1 from the number of items — it ignores the list's `start` and any `<li value=N>` override, unlike the forward-counting path. Marker glyph support covers decimal/alpha/roman/greek/disc/circle/square/disclosure — other CSS keywords (armenian, georgian, hebrew, cjk-decimal…) silently fall back to plain decimal even if they parse. PDF/UA-tagged `/L`. |
| `<ul>` | Full | `type="disc"\|"circle"\|"square"\|"none"` | `list-style-type:disc`; nested `ul ul`→circle, 3-deep→square | Same marker-glyph coverage caveat as `<ol>` above. PDF/UA-tagged `/L`. |
| `<menu>` | Full | grouped identically to `<ul>` | same as `<ul>` | Matches the modern spec's treatment of `<menu>` as a `<ul>` synonym. |
| `<li>` | Full | `value` (own numbering + bumps subsequent forward-counted siblings; ignored inside `<ol reversed>`); `type` | `display:list-item; text-align:match-parent` | An orphan `<li>` with no list ancestor and no room for an outside marker falls back to inside-style positioning. PDF/UA-tagged as `/LI` with its marker as `/Lbl` and its content as `/LBody`. |
| `<dl>` | Full | none | `margin:1em 0` | PDF/UA-tagged as a list (`/L`, list numbering None). |
| `<dt>` | Full | none | `font-weight:normal` | PDF/UA-tagged as `/LI` with a `/Lbl`. |
| `<dd>` | Full | none | `margin-left:40px` | PDF/UA-tagged as `/LI` with a `/LBody`. |
| `<figure>` | Full | none | `margin:1em 40px` | PDF/UA-tagged `/Figure`. |
| `<figcaption>` | Full | none | `display:block`, no special margin | An ordinary block child — correctly NOT split into a separate out-of-flow box the way `<table>``<caption>` is (that's a CSS2.1-specific requirement figcaption doesn't share). PDF/UA-tagged `/Caption`. |
| `<div>` | Full | none | `display:block`; works identically as a flex/grid container | Generic container — full CSS property registry applies. |
| `<search>` | Full | none | `display:block` | Newer HTML5 landmark — confirmed present in both the UA stylesheet and the parser's known-element/implicit-close sets, not falling back to inline. |

## Text-level semantics & embedded/media content

Inline formatting, edits, and everything that embeds external or generated content — images, frames, media, vector/math graphics.

| Tag | Status | Attributes | CSS properties | Comments |
| --- | --- | --- | --- | --- |
| `<a>` | Full | `href` (real PDF hyperlink annotation); `title` (PDF text annotation/tooltip); ~~target~~, ~~download~~ (no-op — no window/download concept in PDF) | `a:any-link{color:#0000ee;text-decoration:underline}`, `a:visited{color:#551a8b}` — a real `:any-link` match, so a bare `<a name>` with no `href` is deliberately NOT link-styled | `:any-link` also matches `area[href]`/`link[href]`. PDF/UA-tagged `/Link`. |
| `<em>` `<cite>` `<dfn>` `<var>` `<i>` | Full | none | `font-style:italic` | Inline text formatting only — all five share one UA rule (`i, cite, em, var, dfn { font-style: italic; }`). |
| `<strong>` | Full | none | `font-weight:bolder` | Deliberately `bolder`, not an absolute `bold` — resolves relative to whatever weight is already inherited, so it compounds correctly under an already-bold/heavy ancestor (e.g. a `font-weight:200` context) instead of clamping to one fixed weight, matching real browsers. |
| `<small>` | Full | none | `font-size:smaller` | — |
| `<s>` | Full | none | `text-decoration:line-through` | Shares its UA rule with obsolete `<strike>`. |
| `<q>` | Full | none special | `display:inline`; `q::before { content: open-quote }`, `q::after { content: close-quote }` | Automatic quotation marks as in browsers: nested quotes alternate (“ ” outside, ‘ ’ inside), and the `quotes` property sets other pairs. |
| `<abbr>` | Full | `title` | `[title]{text-decoration:dotted underline}` — only fires when `title` is present | — |
| `<ruby>` | Partial | none | `display:inline` | Plain container — no real ruby-column layout algorithm, relies entirely on `<rt>`'s superscript trick. |
| `<rt>` | Partial | none | `display:inline; font-size:0.6em; vertical-align:super` | Approximated via superscript, not a distinct above-base-text line. |
| `<rp>` | Full | none | `display:none` | Correct fallback-parenthesis suppression. |
| `<data>` | Full | ~~value~~ (inert, as in real browsers) | none tag-specific | Plain inline element, no special-casing found. |
| `<time>` | Full | ~~datetime~~ (inert) | none tag-specific | Same treatment as `<data>`. |
| `<code>` `<samp>` `<kbd>` | Full | none | `font-family:monospace` | Inline text formatting only. |
| `<sub>` `<sup>` | Full | none | vertical-align + reduced font-size per UA rule | Inline text formatting only. |
| `<b>` | Full | none | `font-weight:bolder` | Shares its UA rule and the same ancestor-compounding reasoning with `<strong>` above — not paired with `<i>`'s italic rule; the two are separate, non-overlapping declarations. |
| `<u>` `<ins>` | Full | none | `text-decoration:underline` | — |
| `<del>` | Full | none | `text-decoration:line-through` | — |
| `<mark>` | Full | none | `background-color:#ffff00; color:#000` | — |
| `<bdi>` | Full | `dir` | `unicode-bidi:isolate` | Isolates its content, and without a `dir` takes its direction from its first strong character (FSI), as `dir="auto"` does on any element. |
| `<bdo>` | Full | `dir` | `unicode-bidi:bidi-override` | Real forced-direction override, feeding the same bidi machinery. |
| `<span>` | Full | none | none tag-specific | Plain generic inline container. |
| `<br>` | Full | `clear` (legacy, maps to CSS `clear`) | `display:inline` | Gets a real `forcedBreak=true` flag — a genuine hard line break. |
| `<wbr>` | Not supported | none | `display:inline` | Parsed as a void element but gives no line-break opportunity: a long word with a `<wbr>` in it stays on one line and overflows its box, where Chrome breaks it there. A U+200B zero-width space doesn't break either; a soft hyphen (`&shy;`) does. |
| `<picture>` | Partial | none tag-specific | `display:inline-block` | Real source selection: walks `<source>` children in order, evaluates each `media` query, picks the first match's `srcset`/`src`. `type` (MIME) matching on `<source>` is NOT checked — a `type="image/webp"` source is accepted regardless of whether the format can actually be decoded. |
| `<source>` | Partial | `srcset`, `media`, `src` — read only inside a `<picture>` parent | n/a | No dedicated handling inside `<video>`/`<audio>` (which never load any media at all — see below), so a `<source>` there is inert. |
| `<img>` | Full | `src`, `srcset` (real 1x/density and `w`-descriptor selection), `alt` (the PDF/UA `/Figure` alternate text, else `title`; never painted), `width`/`height`, `hspace`/`vspace`/`border`/`align` (legacy, mapped), `usemap` (see `<area>`) | `object-fit`/`object-position-x/-y` genuinely implemented and used at paint time | Real decode via `javax.imageio.ImageIO` (JPEG/PNG/GIF/BMP per JVM plugins — no WebP, no animated-GIF frames beyond ImageIO's single-frame default). JPEG EXIF orientation is read and baked in at load time. `<img src="*.svg">` is rasterized through the same SVG parser as inline `<svg>` (`ImageStore#svgRaster`) but comes back opaque with a white background rather than transparent (the SVG module fills white before painting in this mode) — a real difference from a transparent source SVG. A `<text>`-bearing SVG renders its text, and a non-square viewBox-less SVG keeps its proportions (its height is probed independently of its width). A broken image deliberately shows no placeholder icon or alt text (a document meant for print shouldn't visibly show a broken link) — only a thin light-gray frame where it still reserves its declared `width`/`height`. A real `<img>` (not CSS backgrounds/generated content) is PDF/UA-tagged `/Figure`. |
| `<iframe>` | Placeholder only | ~~src~~, ~~srcdoc~~ (not read); `title` shown as the placeholder's centered label; `width`/`height` (legacy, mapped) | `display:inline-block; border:2px inset` default, 300×150 | Confirmed inert: no nested-document fetch or render of any kind — a light-gray fill with a thin border, the tag's `title` (or bare tag name) centered as text. |
| `<embed>` | Placeholder only | ~~src~~ (not read); `title` shown as placeholder label; `width`/`height` mapped | `display:inline-block`, default 300×150 | Same generic gray-box placeholder as `<iframe>`/`<object>`/`<canvas>`/`<video>` — no fetch/render of the embedded resource. |
| `<object>` | Placeholder only | `data` (only checked for presence); `title` shown as placeholder label; `width`/`height`/`hspace`/`vspace`/`border`/`align` legacy attrs mapped | `display:inline-block`, default 300×150 | Same placeholder-box treatment — no PDF/image embedding of `data` is attempted. An `<object>` with no `data` but with fallback content (anything besides `<param>`s) shows that fallback content instead, laid out as an ordinary element, as browsers do. |
| `<param>` | N/A | ~~name~~, ~~value~~ (not read) | n/a | Entirely inert: `<object>` never renders real content, and a `<param>` doesn't count as fallback content. |
| `<video>` | Placeholder only | `width`/`height` (legacy, mapped); ~~src~~, ~~poster~~, ~~controls~~ (not read) | `display:inline-block`, default 300×150 box | No frame decode, no poster extraction, no playback — a gray box labeled "video" only. |
| `<audio>` | Placeholder only | `width`/`height` (size the box, though not mapped to CSS); ~~controls~~ (not read) | `display:inline-block` | Own dedicated default sizing (300×54, matching Chrome's native control-bar height) but still no audio decode/playback — a gray box labeled "audio", drawn whether or not `controls` is present (browsers show nothing for an `<audio>` without `controls`). |
| `<track>` | N/A | none read | n/a | Recognized only as a void element; no caption/subtitle text ever surfaced — consistent with video/audio never actually playing. |
| `<map>` | Partial | `name` (or `id`), matched by an `<img usemap="#…">` | none tag-specific (inline) | Not drawn itself; it is looked up as the image map of an `<img usemap>` — see `<area>`. |
| `<area>` | Partial | `href`, `shape` (rect, circle, poly, default), `coords`; the image's `usemap` | n/a | Image maps, in PDF output: each area with an `href` is a PDF link over its region of the image (a circle or polygon by its bounding rectangle; coordinates in CSS pixels from the image's top left corner, not scaled with it). `ismap` (server-side maps) does nothing, and DOCX/RTF output has no image maps. |
| `<math>` | Full | rendered via JEuclid, sized with the surrounding element's resolved font-size; `alttext` | `display:inline-block` | Genuine MathML rendering, not a placeholder — silently skipped (0×0) if disabled, JEuclid isn't on the classpath, or the formula fails to parse. PDF/UA-tagged `/Formula`, its alternate text from `alttext`, else the formula's own text. |
| `<svg>` | Full | `width`/`height` (legacy + CSS), `viewbox` | `display:inline-block`; with a `viewBox` but no width/height, only the aspect ratio is derived (default 300 width) — deliberately does not auto-size to content the way a referenced `<img src=*.svg>` does | Real vector rendering, painted directly onto the live graphics context (true vector PDF output, not rasterized) — needs no opaque backing fill, unlike a rasterized `<img src=*.svg>` (see above). Malformed/unsupported SVG is silently skipped, not fatal. Known caveat: narrow adjacent table cells with SVG can lose content. PDF/UA-tagged as `/Figure`, its alternate text from `aria-label`, `title` or the SVG's own `<title>`. |
| `<canvas>` | Placeholder only | `width`/`height` (legacy + CSS, real box sizing) | `display:inline-block`, default 300×150 | Confirmed no real 2D drawing context of any kind — same generic gray-box-with-label fallback as iframe/embed/object/video. Architecturally expected: there is no JS engine to run canvas-drawing script against. |

## Tables

Table structure, spanning, captions, and the collapsed/separated border models.

| Tag | Status | Attributes | CSS properties | Comments |
| --- | --- | --- | --- | --- |
| `<table>` | Full | `border=N` (1px outset gray border unless "0"; `frame="void"` clears border-style); `cellspacing`→border-spacing; `width`/`height`; `align` (left/right→float, center/middle→margin:auto) | `display:table; border-collapse; border-spacing-x/-y; table-layout; box-sizing:border-box` | `align="center"` centers the table box itself via margin:auto — a genuinely different code path from a replaced element's align (which uses vertical-align). |
| `<caption>` | Full | none special; ~~width~~/~~height~~ legacy attrs excluded | `caption-side:top\|bottom` — genuinely implemented | CSS2.1 box-model split honored: caption is painted in its own band OUTSIDE the table's own border, not merged into it. PDF/UA-tagged `/Caption` inside the `/Table`. |
| `<colgroup>` | Full | `width` (legacy px) | `display:table-column-group` | — |
| `<col>` | Full | `width` | `display:table-column` | Contributes a genuine per-column width hint to the column-width distribution algorithm. |
| `<thead>` | Full | ~~width~~/~~height~~ excluded | `display:table-header-group; vertical-align:middle; border-color:inherit` | Tagged as PDF/UA `THead` structure. |
| `<tbody>` | Full | same exclusion as thead | `display:table-row-group` | Tagged `TBody`. |
| `<tfoot>` | Full | same exclusion | `display:table-footer-group` | Tagged `TFoot`. |
| `<tr>` | Full | `width`/`height` work | `display:table-row` | — |
| `<td>` | Full | `colspan`/`rowspan` (incl. `rowspan="0"` = span to end of row-group, clamped to [1,1000]); inherited `cellpadding`/`border` from ancestor `<table>`; `nowrap`→white-space:nowrap; `width`/`height` | per-segment collapsed-border resolution for rowspan/colspan cells | A colspan-split synthetic column is weighted differently from a genuine one so it doesn't over-shrink. |
| `<th>` | Full | same as `<td>` plus `scope` ("col"/"row"/"colgroup"/"rowgroup") | `font-weight:bold; text-align:center` | PDF/UA `/TH` with a `/Scope` attribute: from `scope`, else Column for a header in a header row and Row for a header starting a body row. |

## Forms

Form controls, their rendering as static graphics, and the interactive PDF form fields generated when forms mode is on.

| Tag | Status | Attributes | CSS properties | Comments |
| --- | --- | --- | --- | --- |
| `<form>` | Partial | `action`/`method` — read to populate a submit `<button>`'s own PDF SubmitForm action (from its nearest `<form>` ancestor) | none special | Real AcroForm generation is wired for v42 (`PdfTargetRenderer42#collectFormFields`/`emitFormFields`), gated behind the `PD4ML.generateForms(...)` flag reaching `PdfRenderContext#generateForms()`. With that flag off, a plain block container painting static graphics only. |
| `<label>` | Partial | ~~for~~ (no association code, in the forms implementation or anywhere else) | `cursor:default` (inert in static PDF) | Purely a visual container; click-association is moot without real interactivity. |
| `<input>` | Full | `type`: checkbox/radio (native 13×13 box, checkmark/dot only when `checked`), range (dedicated slider paint), color (swatch), image (real replaced element), submit/reset/button (grey chrome), file (dedicated control), hidden (display:none), password (masked); email/number/date/search/tel/url/week/month/time ~~have no dedicated branch~~ — fall back to plain type=text rendering, same as real browsers do for unsupported types; `checked`, `disabled`, `value`, `placeholder` (greyed when empty) all read; `readonly`/`required` only as CSS pseudo-classes for rendering; `name` and `readonly` are read for the AcroForm field when forms are on; ~~maxlength~~ not read | checkbox/radio/range/color/image intrinsic sizing overrides | With `PD4ML.generateForms(...)` on, `text`/`password`/`hidden`/`checkbox`/`radio` (and any unrecognized type, since HTML5 defaults an unknown type to text) each get a real interactive AcroForm widget, and the widget's own appearance stream takes over from this engine's flat paint; `submit`/`reset`/`button`/`image`/`file`/`color`/`range` deliberately get NO widget (static graphics only), matching v40's own long-standing scope exactly. Radio buttons sharing a `name` are grouped into one real AcroForm radio group. |
| `<button>` | Full | `type=submit\|button\|reset` — with forms mode on, `reset` gets a real PDF ResetForm action and a bare/`submit` button gets a real SubmitForm action (reading the nearest `<form>` ancestor's `action`/`method`); with forms mode off, no functional difference (static graphics only) | grey button chrome, shared with input[type=submit/button/reset/file] | Unlike `<input type=submit>` (excluded from widget generation), the distinct `<button>` element itself DOES get a real push-button widget when forms mode is on. |
| `<select>` | Full | `multiple` and `size` (genuinely switch dropdown vs. listbox rendering, default 4 rows if multiple); HTML5 "last `<option selected>` wins" enforced for non-multiple selects | gray (not blue) row highlight in listbox mode — deliberate, matches unfocused-control rendering | With forms mode on, always gets a real interactive PDF choice widget (a dropdown, `size<=1`, sets the Combo flag; a multi-row listbox, `size>1`, does not — backwards from what the underlying type constant names suggest, but that's v40's own real, traced behavior, not a v42 choice). |
| `<datalist>` | N/A | ~~`<input list>` association~~ — no code found | `display:none` | Correctly invisible; inert as expected (no interactivity in static output), matching real browsers when JS-driven autocomplete isn't relevant. |
| `<optgroup>` | Full | `label` | `display:block; font-weight:bold` | Rendered as a bold row header inside the listbox/dropdown. |
| `<option>` | Partial | `selected` (last-wins rule); `value` (not used for visual rendering, only text content shown — with forms mode on, it is the choice's export value); ~~disabled~~ (no per-option greyed rendering) | `display:block` | — |
| `<textarea>` | Full | `rows`/`cols` (both directly size the box); `placeholder`; HTML5 leading-LF-after-opening-tag correctly stripped | `white-space:pre-wrap; font-family:monospace; padding:2px` | Word-wraps content at box width; wrap granularity approximate vs. Chrome's char-count algorithm but functionally correct. With forms mode on, always gets a real interactive multiline AcroForm text widget (its own appearance stream carries the value; this engine's flat paint is suppressed). |
| `<output>` | Full | none special | none special | Renders as an ordinary inline element showing its text content — correct, real browsers give `<output>` no special rendering either. |
| `<progress>` | Full | `value`/`max` (genuinely drive fill-bar ratio); no value → fixed indeterminate segment | `display:inline-block; height:8px`, 160px width | Fill color sampled directly from Chrome per code comment. |
| `<meter>` | Full | `value`/`min`/`max` (fill ratio); `low`/`high`/`optimum` (the value's region) | same box model as progress, 80px width | The fill is green in the optimum region, amber in the suboptimal one and red in the even less good one, as in Chrome. |
| `<fieldset>` | Full | none special | `margin:0 2px; padding:.35em .75em .625em; border:2px groove #c0c0c0` | Legend-centered-on-border + border-notch built from scratch, for every border style (not just solid). Known limitation noted in source: a float inside the legend, or a child laid out before the legend, isn't reserved for. |
| `<legend>` | Full | none special | `display:block; padding:0 2px` | Sized to its own text width, straddles the border midline — the "engraved" look. |
| `<keygen>` | N/A | obsolete, no crash | none special | Removed from the HTML5 spec in real browsers too — correctly a no-op void element. |

## Interactive & scripting elements

Disclosure widgets, dialogs, and everything that depends on script execution or Shadow DOM — neither of which exists in this engine.

| Tag | Status | Attributes | CSS properties | Comments |
| --- | --- | --- | --- | --- |
| `<details>` | Full | `open` (boolean, genuinely functional) | `display:block` | Closed state is real element-specific logic (not a CSS class): every child but the first `<summary>` (bare text included) is skipped outright during box construction when `open` is absent. A `<details>` without a `<summary>` gets a default one reading "Details", as in Chrome. Static conversion only — no interactive toggle in PDF output, fixed at render time. |
| `<summary>` | Full | none tag-specific | `display:list-item` + `list-style-type:disclosure-open\|disclosure-closed` + `list-style-position:inside`, applied only to the first `<summary>` child | The disclosure triangle (pointing down when open, right when closed) is drawn as a filled shape about half an em tall rather than taken from a font, so it doesn't depend on the font set having the glyph. A block-level first child (e.g. `<summary><p>`) reserves a line height for the marker instead of the triangle overlapping the child's first character. |
| `<dialog>` | Partial | `open` — ~~complete no-op~~ | `display:block`, no special rule | Real divergence from spec: unlike real browsers' `dialog:not([open]){display:none}`, content renders identically with or without `open`. No modal top-layer/backdrop/auto-centering either — correctly and deliberately absent since that has no equivalent in static PDF output. |
| `<script>` | N/A by design | ~~src~~, ~~type~~, ~~async~~, ~~defer~~ — all inert | `display:none` | No scripting engine exists anywhere in the codebase — content is tokenized as HTML5 raw text (so embedded JS syntax can't confuse the parser) then hidden, never executed. Any content a client-side script would inject is invisible to PD4ML. |
| `<noscript>` | Full | none | `display:inline` | The spec-correct "scripting disabled" branch — since PD4ML never executes scripts, this is the only behavior that makes sense, and it's what real browsers with JS off do too. |
| `<template>` | Partial | none tag-specific | `display:none` | No dedicated inert-DocumentFragment representation — falls through the ordinary display:none child-skip branch. Functionally equivalent to "not part of the rendered tree" for a non-interactive renderer, just implemented generically rather than as a special fragment type. |
| `<slot>` | N/A | ~~name~~ — inert | none (generic default) | No Shadow DOM support of any kind exists — treated exactly like any unknown tag, light-DOM children just render in place with no slot-projection semantics. |

## Obsolete & deprecated elements

Legacy HTML tags no longer part of the living standard — kept here because real-world documents (especially generated by old tools or Word) still contain plenty of them.

| Tag | Status | Attributes | CSS properties | Comments |
| --- | --- | --- | --- | --- |
| `<center>` | Full | none | `text-align:-webkit-center` — a proprietary keyword implemented specifically so `<center>` also centers a normal-flow block child with its own definite width, not just inline content | Matches real Chrome/Safari behavior, including the non-obvious block-centering quirk, rather than the CSS-spec-only `text-align:center` which would leave a block child flush left. |
| `<dir>` | Partial | ~~type~~ (not mapped to list-style-type the way it is for ol/ul/li — PresentationalHints only checks tag ol/ul/li) | grouped with ul/menu: `list-style-type:disc; padding-left:40px` | Rendered as a `<ul>` synonym for layout purposes, matching the spec's own "treat as ul" guidance — only its own rarely-used `type` attribute is left untranslated. |
| `<font>` | Full | `color`, `face`, `size` (1–7 legacy scale → 10/13/16/18/24/32/48px, base=3, `+N`/`-N` relative forms supported) | maps to color/font-family/font-size | Fully implemented legacy-attribute-to-CSS translation. |
| `<basefont>` | Partial | `color`; ~~size~~, ~~face~~ not read for basefont specifically | `display:none` — so the color mapping has no visible effect anyway | Real legacy semantics (setting a default for following sibling content) not replicated, but moot since the element is hidden either way. |
| `<big>` | Full | none | `font-size:larger` | Paired UA rule with `<small>`. |
| `<tt>` | Full | none | `font-family:monospace` | — |
| `<strike>` | Full | none | `text-decoration:line-through` | Shares its rule with `<s>`/`<del>`. |
| `<acronym>` | Full | `title` | `[title]{text-decoration:dotted underline}` | Treated identically to `<abbr>` — correct parity with its historical merge into abbr's role. |
| `<nobr>` | Full | none | `white-space:nowrap` | Also gets real HTML5 "active formatting element" parsing semantics (auto-close-on-reopen), grouped with b/big/code/em/font/i/s/small/strike/strong/tt/u. |
| `<xmp>` | Partial | none | `font-family:monospace; white-space:pre; margin:1em 0` — but no `display:block`, so it lays out inline | Correctly given HTML5's RAWTEXT tokenizer treatment — nested tags inside are literal text, not parsed as markup, matching the real spec/legacy-browser behavior. Unlike browsers, which make it a block like `<pre>`, it stays an inline element: two `<xmp>`s in a row sit side by side, with no vertical margins. |
| `<listing>` | Partial | none | shares the pre/xmp/plaintext monospace/pre rule — and, like them, no `display:block`, so it lays out inline | Deliberately NOT given raw-text treatment — and that's spec-correct: per the real WHATWG tokenizer, listing is not a RAWTEXT-state element (only script/style/xmp/iframe/noembed/noframes are). Easy to assume it behaves like xmp; it doesn't, in real browsers either. It is inline rather than a block (same gap as `<xmp>`), and the "strip one leading newline" rule isn't implemented for it (same gap as `<pre>`). |
| `<plaintext>` | Real gap | none | shares the same monospace/pre rule — no `display:block`, so inline | Genuine spec-compliance gap: HTML5 requires `<plaintext>` to permanently switch the tokenizer into a raw state for the rest of the document (no more tags recognized at all, ever, until EOF). This engine has no such state — content after `<plaintext>` continues to be parsed as ordinary markup, a real behavioral difference from every browser's (admittedly bizarre) handling of this tag. It also lays out inline, not as the block browsers make it. |
| `<applet>` | N/A | ~~code~~, ~~archive~~, ~~width~~, ~~height~~ — all inert | no UA rule at all — plain inline, not even boxed as a replaced element | No Java-applet execution of any kind, confirmed absent (PD4ML being JVM-based does not imply an embedded applet runtime). Fallback content inside renders as plain inline text. |
| `<bgsound>` | N/A | ~~src~~, ~~loop~~ — inert | `display:none` | Correctly hidden; no audio playback attempted (impossible in PDF output anyway). |
| `<blink>` | N/A | none | no UA rule; unknown-element generic inline default | Text content still shows, just no blink effect — expected/acceptable for a static PDF target, and already dropped from all real browsers. |
| `<marquee>` | N/A by design | ~~behavior~~, ~~direction~~, ~~scrollamount~~ — all inert | `display:block` | No scrolling/animation logic of any kind — renders as ordinary static block content, correct and deliberate for static PDF output, not a bug. |
| `<frame>` | N/A | ~~src~~, ~~name~~, ~~noresize~~ — all inert | `display:block` | No cross-document/navigation handling — renders as an empty block, no content loaded from `src`. Frame navigation has no single-document PDF equivalent, correctly not attempted. |
| `<frameset>` | N/A | ~~rows~~, ~~cols~~ — inert | `display:block` | No special composition logic — just stacks its (empty) `<frame>` children as ordinary blocks. |
| `<noframes>` | Full | none | `display:none` | Correctly hidden, mirroring a frame-capable browser's treatment. |
| `<noembed>` | Partial | none | no UA rule — ~~missing from the display:none list~~ that covers its rawtext siblings (noframes, basefont, bgsound, etc.) | Minor inconsistency: given true raw-text tokenizer treatment (like script/style/xmp), but since nothing hides it, its literal text content renders visibly by default. |
| `<menuitem>` | N/A | inert | no UA rule; unknown-element default | Not referenced anywhere — consistent with real modern browsers, which also dropped this abandoned proposal. |
| `<content>` | N/A | ~~select~~ — inert | none (unknown element default) | Shadow DOM v0 tag; no shadow DOM support exists at all (see `<slot>`) — falls through as a plain unknown element, matching modern browsers. |
| `<shadow>` | N/A | none | none (unknown element default) | Shadow DOM v0's deprecated insertion-point tag, removed even earlier than `<slot>`/`<content>` — correctly falls through as unknown, matching every real modern browser. |
| `<rb>` `<rtc>` | N/A | none | none (unknown element default) | Not in the parser's known-element list — falls through as plain unknown inline elements, matching real browsers since both were formally dropped from the Ruby markup spec in 2021. |

## Methodology

Audited against the PD4ML v42 engine source (com.pd4ml.html.engine, its css and layout subpackages) — the built-in user-agent stylesheet, the legacy presentational-hints mapper, the CSS property registry, and the relevant layout/paint classes. No v40 comparison is asserted anywhere above unless the v42 source itself carries an explicit comment documenting the change. Where reading the source was not conclusive — the gaps listed above (`<wbr>`, `<dialog>`, `<plaintext>`, `<ol reversed>`, the leading newline in `<pre>`/`<listing>`, inline `<xmp>`/`<listing>`/`<plaintext>`), `<meta charset>` decoding, image maps and the PDF/UA structure types — the behavior was confirmed by converting small test documents with the v42 engine and inspecting the resulting layout and PDF.

Reconciled against the master source on 2026-11-06.
