1"""
2HTML5 authoring-conformance checking with a severity model.
3
4:func:`check` runs the WHATWG document-conformance requirements the parser does not raise
5as a :class:`~turbohtml.ParseError` -- a missing ``img`` alt, obsolete presentational
6markup, a duplicate ``id``, an invalid or redundant ARIA role, an empty heading, a
7``section`` without a heading, a document with no title or language -- over a tree parsed
8with :func:`turbohtml.parse`. It returns a :class:`ConformanceReport`: a ``valid`` verdict
9(the document is valid exactly when nothing is an error) and every :class:`ConformanceMessage`
10found. Each message carries a stable ``code``, a ``severity`` (``"error"``, ``"warning"``, or
11``"info"``), a human-readable ``message``, and the source ``line``/``column``, the way the Nu
12Html Checker (validator.nu) classifies its findings by type and subType.
13
14The walk, the verdict and the severity views run in the C core; the Python layer only
15wraps the C findings into the report records. :func:`check_html` is the shorthand that parses a markup
16string first.
17"""
18
19from __future__ import annotations
20
21from itertools import starmap
22from typing import TYPE_CHECKING, Literal, NamedTuple
23
24from ._html import _conformance_check, _conformance_filter, parse
25
26if TYPE_CHECKING:
27 from ._html import Document, Node
28
29__all__ = [
30 "ConformanceMessage",
31 "ConformanceReport",
32 "Severity",
33 "check",
34 "check_html",
35]
36
37Severity = Literal["error", "warning", "info"]
38"""The three severities a :class:`ConformanceMessage` carries, ordered most to least serious."""
39
40
41class ConformanceMessage(NamedTuple):
42 """
43 One conformance finding.
44
45 :param code: a stable machine-readable identifier for the rule, such as ``"img-missing-alt"``.
46 :param severity: ``"error"`` for a violated requirement, ``"warning"`` for an authoring
47 recommendation, ``"info"`` for an advisory note.
48 :param message: a human-readable description of what was found.
49 :param line: the 1-based source line of the offending node, or ``0`` when unknown.
50 :param column: the 0-based source column of the offending node, or ``0`` when unknown.
51 """
52
53 code: str
54 severity: Severity
55 message: str
56 line: int
57 column: int
58
59
60class ConformanceReport(NamedTuple):
61 """
62 The outcome of a conformance check: a ``valid`` verdict and every message found.
63
64 The report is truthy exactly when the document is valid (no message is an error), so
65 ``if check(doc):`` reads naturally. Warnings and info messages never affect the verdict.
66 """
67
68 valid: bool
69 messages: tuple[ConformanceMessage, ...]
70
71 def __bool__(self) -> bool:
72 """Whether the document is valid, so the report reads as a truth value."""
73 return self.valid
74
75 @property
76 def errors(self) -> tuple[ConformanceMessage, ...]:
77 """Only the ``"error"`` messages -- the findings that make the document invalid."""
78 return _conformance_filter(self.messages, "error")
79
80 @property
81 def warnings(self) -> tuple[ConformanceMessage, ...]:
82 """Only the ``"warning"`` messages -- the authoring recommendations."""
83 return _conformance_filter(self.messages, "warning")
84
85 @property
86 def infos(self) -> tuple[ConformanceMessage, ...]:
87 """Only the ``"info"`` messages -- the advisory notes."""
88 return _conformance_filter(self.messages, "info")
89
90
91def check(document: Document | Node) -> ConformanceReport:
92 """
93 Check a parsed document (or any subtree) against the HTML5 authoring rules.
94
95 The document-level rules (a missing title or ``lang``) apply only when ``document`` is a
96 whole :class:`~turbohtml.Document`; on a fragment or a subtree only the per-element rules run.
97
98 :param document: a tree parsed with :func:`turbohtml.parse` (or an element within one).
99 :returns: the :class:`ConformanceReport` with the verdict and every message.
100 """
101 valid, findings = _conformance_check(document)
102 return ConformanceReport(valid, tuple(starmap(ConformanceMessage, findings)))
103
104
105def check_html(markup: str) -> ConformanceReport:
106 """
107 Parse a markup string with :func:`turbohtml.parse` and check the resulting document.
108
109 :param markup: the HTML to parse and check.
110 :returns: the :class:`ConformanceReport` for the parsed document.
111 """
112 return check(parse(markup))