ReferenceHTML validation

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

FunctionReturns
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

FunctionReturns
indexJHTML(tree)Map<EID, { node, parent, order }> in document order.
textContentOf(node)Concatenated text, comments excluded.

Element rules

ruleLevelCondition
duplicate-iderroridCounts.get(id) > 1
id-whitespaceerrorid contains whitespace
required-attrerrorREQUIRED_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
parenterrorTag in ALLOWED_PARENTS and parent tag not in its list
p-blockerrorp has a direct child in BLOCK_TAGS
nested-interactiveerrora/button has an interactive descendant (isInteractive)
nested-formerrorform with a form ancestor
img-altwarningimg without alt attribute (alt="" passes)
iframe-titlewarningiframe without non-blank title
empty-interactivewarningbutton or a[href] fails hasAccessibleName (text, aria-label, aria-labelledby, title, or descendant img[alt] / [aria-label])
form-labelwarningLabelable control (non-excluded input, select, textarea) without aria-label, aria-labelledby, title, a <label for> pointing at it, or a label ancestor
idrefwarningAny of IDREF_ATTRS names an id not in idCounts; usemap names a map not in mapNames
heading-orderwarning / infowarning: level > previous + 1; info: first heading isn’t h1
button-typeinfobutton without type inside a form
target-blankinfoa[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 "".

ruleLevelCondition
page-langwarning<html> without non-blank lang
page-titlewarning<head> has no <title> with text
page-viewportinfoNo meta[name=viewport] in <head>
page-descriptioninfoNo meta[name=description] in <head>
multiple-h1infoMore than one h1

Head checks only run when the tree has a <head>.

Consumers

WhereCall
Props tab → Element checksvalidateElement for each selected node; badge counts non-info issues.
Props tab → Page settings → Accessibility & validationvalidatePage(ctx.tree, ctx.page); badge counts non-info issues.
IdClassToolUses page.idCounts directly for the duplicate-id hint.