HTML validation
apps/frontend/src/Utils/HTML/htmlValidation.ts validates a JHTML tree. Pure functions, no DOM access; tests in Utils/HTML/__tests__/htmlValidation.test.ts.
Types
type HtmlIssueLevel = "error" | "warning" | "info";
type HtmlIssue = {
level: HtmlIssueLevel;
EID: string; // "" for page-level issues
rule: string; // stable id, see tables below
message: string; // user-facing
};
type PageContext = {
index: Map<string, JHTMLIndexEntry>; // from indexJHTML
idCounts: Map<string, number>;
labelFor: Set<string>; // ids referenced by <label for>
mapNames: Set<string>; // <map name> values
headings: { EID: string; level: number }[]; // document order
};Functions
| Function | Returns |
|---|---|
buildPageContext(tree) | PageContext. Tolerates tagless nodes (the editor’s {} placeholder before load). |
validateElement(eid, ctx) | Issues for one element; [] for unknown EIDs, tagless nodes, comments, doctype and document. |
validatePage(tree, ctx?) | Page-level issues plus every element’s issues, sorted error → warning → info. |
describeElement(node) | <tag>#id or <tag>.firstClass. |
@/Utils/JHTML.ts helpers
| Function | Returns |
|---|---|
indexJHTML(tree) | Map<EID, { node, parent, order }> in document order. |
textContentOf(node) | Concatenated text, comments excluded. |
Element rules
rule | Level | Condition |
|---|---|---|
duplicate-id | error | idCounts.get(id) > 1 |
id-whitespace | error | id contains whitespace |
required-attr | error | REQUIRED_ATTRS (img: src, track: src, map: name, optgroup: label, object: data, embed: src, area: alt); input[type=image] without alt; source without src and srcset |
parent | error | Tag in ALLOWED_PARENTS and parent tag not in its list |
p-block | error | p has a direct child in BLOCK_TAGS |
nested-interactive | error | a/button has an interactive descendant (isInteractive) |
nested-form | error | form with a form ancestor |
img-alt | warning | img without alt attribute (alt="" passes) |
iframe-title | warning | iframe without non-blank title |
empty-interactive | warning | button or a[href] fails hasAccessibleName (text, aria-label, aria-labelledby, title, or descendant img[alt] / [aria-label]) |
form-label | warning | Labelable control (non-excluded input, select, textarea) without aria-label, aria-labelledby, title, a <label for> pointing at it, or a label ancestor |
idref | warning | Any of IDREF_ATTRS names an id not in idCounts; usemap names a map not in mapNames |
heading-order | warning / info | warning: level > previous + 1; info: first heading isn’t h1 |
button-type | info | button without type inside a form |
target-blank | info | a[target=_blank] without noopener/noreferrer in rel |
IDREF_ATTRS: for, list, form, headers, popovertarget, aria-labelledby, aria-describedby, aria-controls, aria-owns, aria-activedescendant, aria-details, aria-errormessage.
Page rules
EID is "".
rule | Level | Condition |
|---|---|---|
page-lang | warning | <html> without non-blank lang |
page-title | warning | <head> has no <title> with text |
page-viewport | info | No meta[name=viewport] in <head> |
page-description | info | No meta[name=description] in <head> |
multiple-h1 | info | More than one h1 |
Head checks only run when the tree has a <head>.
Consumers
| Where | Call |
|---|---|
| Props tab → Element checks | validateElement for each selected node; badge counts non-info issues. |
| Props tab → Page settings → Accessibility & validation | validatePage(ctx.tree, ctx.page); badge counts non-info issues. |
IdClassTool | Uses page.idCounts directly for the duplicate-id hint. |
Related
- How-to: Add an HTML check
- Reference: Props tools framework