Refuse markup that does not parse, and four things a browser hides [#32] - #148
Merged
Conversation
A browser recovers from broken markup, so a template can produce an unclosed element for months while every page still renders. Nothing in a review, a build or a reader's visit says a word about it, and the first thing that notices is a screen reader announcing a heading that swallowed the rest of the document. Every produced page is now read strictly: every element closed, in the order it was opened, or the page is refused. That is stricter than HTML, which lets a paragraph end where the next one begins, and it is deliberate. This repository writes every byte of its own markup out of templates, so an end tag left out is a choice somebody made once, and a rule that has to model a browser's recovery is a rule nobody can read. Four properties are read off the same walk, because the walk is where they are cheap and because each is a page that renders perfectly. A duplicate identifier means a reference reaches whichever element the reader's software picked. A heading level that skips leaves somebody moving by heading unable to tell what is under what. An image with no alternative text is a hole in the page for anybody who cannot see it. A control with nothing naming it asks for a field to be filled in without saying what goes in it. They are five rows and not one. The repairs are different: a page that does not parse is a template somebody has to fix before anything on it can be judged, and a heading that skips is a page that parses. A page that does not parse is reported once rather than four times. Everything after a broken tag is a guess about what the author meant, and a page buried under guesses is one nobody reads the first line of. No browser and no network. The page is read as bytes, so this stays inside a suite that runs on a machine with no display, and what only a render can decide is left to the run that has one. Signed-off-by: Nils Lehnen <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
Nothing read the markup this repository produces. The gate decided what a grep
can decide about a page, which is whether a particular string is there, and a
string being there says nothing about whether the document holds together:
Run 2026-08-12 on
195020a. A browser recovers from broken markup, so atemplate can produce an unclosed element for months while every page renders and
nothing says a word about it.
What this does
Reads every produced page strictly. Every element is closed, in the order it was
opened, or the page is refused.
That is stricter than HTML, which lets a paragraph or a list item end where the
next one begins, and it is deliberate. This repository writes every byte of its
own markup out of templates, so an optional end tag left out is a choice
somebody made once, and a rule that has to model a browser's recovery is a rule
nobody can read.
Four more properties come off the same walk, because the walk is where they are
cheap and because each one is a page that renders perfectly:
Five rows and not one, because the repairs differ. A page that does not parse is
a template somebody has to fix before anything on it can be judged; a heading
that skips is a page that parses.
A page that does not parse is reported once rather than five times. Everything
after a broken tag is a guess about what the author meant, and a page buried
under guesses is one nobody reads the first line of.
The population is what the build produces, which today is one page. The rows are
landed while that is true because retrofitting a strict read across seventeen
generated pages later is the expensive version of this, and because the plugin
pages this issue names as its dependency are what they are for.
Closes
Closes #32.
Its done-when in order: every produced page parses, which the run above says; a
deliberately unclosed element reds the check, a duplicate id reds it, and an
image without alt reds it, all three below; and the whole leg runs with no
browser installed, which is what the package is written to make true.
The two properties named in the body beside those three, a heading level that
skips and a control with no label, are rows as well.
What was run
Twenty-two decided against seventeen. The one still owed is
image-dimensions-match-the-file, waiting on the first image the build writes,which is #69.
Both run 2026-08-12.
The guards, deleted and watched go red
One at a time, with the rest of the tree left alone. Each shows the package's
own case and the row that reads it:
The near miss worth the effort here is the other direction. A strict reader that
refuses a well-formed page is worse than no reader, because the repair for it is
somebody loosening the rule, and the shapes a template writes every day are
exactly the ones a hand-written reader gets wrong. So the suite carries a page
that breaks nothing and seven shapes that must be left alone: a void element
written with a slash and without one, an attribute with no value, values in
double quotes, single quotes and none, a comment carrying a tag inside it, a
stylesheet carrying a
<inside it, and a heading falling back to a levelalready used.
One of those found a defect in this change before it landed. Raw-text elements
were stepped over without being put on the stack, so
</title>was read as anend tag closing
<head>, and the first run over the real page refused it:The repair is that a raw-text element goes on the stack like any other and the
walk steps over what is inside it to its end tag.
No test was skipped. Nothing here needs a display, a browser, elevation or the
network, which is the property this issue asks for in its last clause.
The means
Go, in a package of its own, with no dependency added. The alternative worth
naming is a real HTML parser from outside the toolchain, and it is the wrong
tool twice over: a conforming parser implements the recovery this rule exists to
refuse, so it would accept the unclosed element and report a tree it repaired,
and it would be the first dependency in a repository whose bill of materials
says it has none.
The rows live in the invariant table, decided by the same verb as every other
row and reported under the same check name, so a contributor meets them by
running one verb rather than by remembering a second one.
Who read it
Nobody else has read this. The ruleset requires no approving review, so this is
merged by whoever opened it, and the evidence above stands in place of a second
reader.