Skip to content

feat(html,core,cli): HTML reader — M5 5.1 - #36

Merged
aenawi merged 1 commit into
mainfrom
html-reader
Sep 5, 2026
Merged

aenawi merged 1 commit into
mainfrom
html-reader

Conversation

@aenawi

@aenawi aenawi commented Sep 5, 2026

Copy link
Copy Markdown
Owner

The first item of M5, and the third format. PLAN §5.1.

mirsam audit page.html now works. HTML is the first adapter that is not an OOXML package — which is the point of doing it before XLSX, whose adapter would have reused everything mirsam-ooxml already has and proved nothing about the abstraction.

The direction is usually not in the document

dir="rtl" is the version an attribute linter sees. body { direction: rtl } and .rtl { direction: rtl; text-align: right } are where a great many Arabic sites actually state it, and an adapter that read attributes alone would report direction-unset on every one of those pages. That is a finding on formatting the author chose — invariant 2, reached through the adapter rather than through a rule, and the failure mode this project treats as worse than a miss.

So css.rs runs a real, if small, cascade: type / * / #id / .class / [attr] / [attr=value] / :root, descendant and child combinators, specificity, !important, and the properties CSS says are inherited. It reads <style> elements, the style attribute, and stylesheets linked by a relative path on this machine.

At-rules are skipped whole. A declaration inside @media applies under a viewport this tool does not have, and applying one would be claiming to know the reader's screen.

Every omission can only cost a finding; none of them can manufacture one. That is the test each shortcut was held to. A selector this crate cannot match is dropped — that selector, never the whole rule — and a declaration nobody applied leaves the property where it already was.

dir is a hint the stylesheet outranks

In a browser dir="rtl" is a rule in the user agent's stylesheet, so an author's direction: ltr beats it. Reading dir as the last word would report the wrong direction for a page whose CSS disagrees with its markup, and then propose a repair that changes nothing a reader sees. CascadeOrigin models the three origins and the adapter reports what the reader gets.

dir="auto" is Unset, and it stops inheriting

auto does not state a direction. It asks the browser to take the first strong character, which is precisely the sentence Resolved::Unset already carries — nothing anywhere; the renderer picks. So Arabic under it is direction-unset: a warning, the fragile tier.

That is the honest reading rather than a harsh one. First-strong gets مرحبا 2026 right and 2026 مرحبا wrong — the second begins with a number, resolves left to right, and puts the year on the wrong side. A paragraph is correct under auto when today's text happens to agree, which is not a decision about the element.

It also replaces the inherited direction rather than deferring to it. Arabic marked auto under an ltr body renders right to left, and reporting it as contradicted-by-the-chain would be a finding on text the browser already gets right.

One font stack, so complex-font-missing cannot fire here

OOXML gives a run a Latin slot and a complex-script slot, and that rule reports the document that filled the first and left the second empty. CSS's font-family answers for every script on the element, so the stack lowers into both slots and the rule's precondition is unreachable. A defect a web author cannot write, exactly as a hard left edge is one a Word author cannot write — recorded as a refusal in the conformance suite, never a skip.

The mirror of it: CSS's left and right are physical, so alignment-incoherent is live on HTML in a way it is not on Word.

The tree is html5ever's, the DOM is ours

<p>a<p>b is two siblings; a <div> inside a <p> closes it; text between <table> and its first <tr> is foster-parented out of the table. Each of those moves a node's ancestors, which is exactly what dir is inherited along — a hand-rolled nesting stack would resolve direction from a chain no browser has.

markup5ever_rcdom is not taken: it describes itself as unsupported, unfuzzed and not for production. dom.rs is the ~200 lines that avoid depending on that claim.

The one core change — ADR 0009

A package holds everything that decides its own text. A page can link a stylesheet on a server this tool will never call, because mirsam performs no network I/O — and the rules in it are rules nobody applied, so a direction-unset finding on such a page may be answered by CSS the tool never saw.

Standing rule 4 says that has to be sayable. DocumentReader gains unread_sources(), defaulting to nothing, and every report carries:

"sources": { "unread": ["https://cdn.example.test/site.css"] }

beside fonts: {checked: ...}. It reads {"unread": []} on every .pptx and .docx, which for a package is the whole truth. It returns opaque strings the engine never parses — the core did not learn what a stylesheet is.

The alternative was for the CLI to downcast to HtmlDocument, which would put a format's name in the driving adapter and leave a library caller unable to ask.

The conformance suite moved

It asks whether the adapters agree, so it has to see all of them. Inside mirsam-ooxml it would have made that crate depend on its peers — the hexagon leaking through the test tree. It now lives in mirsam-conformance, a crate with no library code in it, and its vocabulary trait writes a document rather than a package, so a single-file format needs no exception to join.

Acceptance

All 29 conformance cases pass with HTML among the formats, 24 of them without a line changing — the same situation comes back as the same finding whichever of the three applications wrote the file.

The five that moved:

  • two new refusals, both properties of CSS rather than of the adapter — text-align has no distributed value, and one font-family cannot fill a Latin slot and leave the complex-script one empty;
  • a_latin_font_with_an_empty_arabic_slot_… now asserts the two formats with two slots report it and the one with a single stack cannot state it;
  • a_hard_left_edge_… now asserts both formats that can state it report it identically, where it used to assert only one format could;
  • the committed refusal list grew by those two entries.

Corpus: quarterly-page.html carries one instance of each defect the adapter can see — including the unread stylesheet and the exit code repair gives a readable format with no writer — and quarterly-page-correct.html is the page the tool must leave completely alone. Unlike the decks beside them they are not generated: a page is its own source.

make verify is green.

The web is the third format, and the first that is not an OOXML package.
It is also the first where the direction is usually not in the document.

`dir="rtl"` is the version an attribute linter sees. `body { direction:
rtl }` in a stylesheet is where a great many Arabic sites actually state
it, and an adapter reading attributes alone would report
`direction-unset` on every one of them — a finding on formatting the
author chose, which is invariant 2 reached through the adapter rather
than through a rule. So `css.rs` runs a real, if small, cascade:
selectors, specificity, `!important`, the inherited properties, over
`<style>`, the `style` attribute and stylesheets linked by a relative
path on this machine. At-rules are skipped whole, because a declaration
inside `@media` applies under a viewport this tool does not have.

Every omission can only cost a finding; none can manufacture one. That
is the test each shortcut was held to.

`dir` enters the cascade at the origin a browser gives it — the user
agent's — so a page whose CSS says `direction: ltr` under a `dir="rtl"`
is reported as left to right. What the reader sees is what the tool
reports.

`dir="auto"` is `Unset`: it asks the browser to guess from the first
strong character, which is what that state already says. It gets
`مرحبا 2026` right and `2026 مرحبا` wrong, so a paragraph correct under
it is correct by luck of today's text. A warning, the fragile tier. It
also stops inheriting, because a browser stops consulting the ancestors
too.

CSS gives an element one `font-family` for every script, not OOXML's
Latin and complex-script pair, so the stack lowers into both slots and
`complex-font-missing` is structurally silent on HTML — a defect a web
author cannot write, as a hard left edge is one a Word author cannot
write. The mirror holds: CSS's `left` is a physical edge, so
`alignment-incoherent` is live here in a way it is not in Word.

The tree is html5ever's, through a sink this crate owns.
`markup5ever_rcdom` describes itself as unsupported and not for
production. Tree construction matters because `dir` is inherited along
ancestors and HTML moves them: `<p>a<p>b` is two siblings, and text
between `<table>` and its first `<tr>` is foster-parented out of the
table.

One core change, and ADR 0009 records why the port is where it belongs.
A package holds everything that decides its own text; a page can link a
stylesheet on a server this tool will never call, because mirsam
performs no network I/O. The rules in it are rules nobody applied, so a
`direction-unset` finding on such a page may be answered by CSS the tool
never saw. `DocumentReader::unread_sources` defaults to nothing, and
every report now carries `sources: {unread: [...]}` beside `fonts:
{checked: ...}`. It returns opaque strings the engine never parses.

The conformance suite moved to `mirsam-conformance`, a crate with no
library code: it asks whether the adapters agree, so it has to see all
of them, and inside `mirsam-ooxml` it would have made that crate depend
on its peers. Its vocabulary trait now writes a document rather than a
package, so a single-file format needs no exception.

Twenty-four of twenty-nine cases pass with HTML among the formats
without a line changing. The five that moved record the two refusals CSS
adds and make the two assertions those refusals touch sharper.

`quarterly-page.html` carries one instance of each defect the adapter
can see, `quarterly-page-correct.html` is the page the tool must leave
completely alone. Unlike the decks beside them they are not generated: a
page is its own source.
@aenawi
aenawi merged commit f87eaaf into main Sep 5, 2026
7 checks passed
@aenawi
aenawi deleted the html-reader branch September 5, 2026 08:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant