Documentation
¶
Overview ¶
Command inventory lists the custom elements a page uses and says which of them nothing defines.
$ inventory page.html 6 custom elements used, 2 defined, 4 undefined element used defined my-badge 1 page.html my-card 3 page.html my-div (is=) 1 - site-header 1 - ui-button 4 - x-tooltip 1 - 2 names that look like components and can never be one my_card 1 an underscore is not a hyphen mycard 1 spelled <myCard>, and HTML lower-cases a tag name 2 hyphenated names that are not custom elements annotation-xml 1 reserved by the specification font-face 1 reserved by the specification
It exits non-zero when anything is undefined, since that is a component the page asks for and nothing provides.
A definition comes from `customElements.define("name"` in a <script> in the document, or from -defined for the ones a bundle registers. Everything else it finds is reported as undefined, which is the point: an undefined custom element renders as an unstyled inline box and nothing in the console says which one was missing.
What counts as a custom element ¶
The specification's rule is narrower than "has a hyphen", and an inventory that uses the wide version is wrong in both directions. A valid name starts with an ASCII lower-case letter, contains a hyphen, and is not one of eight names the specification reserves for SVG and MathML - annotation-xml, color-profile, font-face, font-face-src, font-face-uri, font-face-format, font-face-name and missing-glyph. Those eight have hyphens and are not custom elements.
The other direction is the one that costs time. HTML lower-cases a tag name, so a component written the way a framework spells it does not survive the parse:
source TagName a custom element? <my-card> my-card yes <MY-CARD> my-card yes, the same one <myCard> mycard no - no hyphen once lower-cased <my_card> my_card no - an underscore is not a hyphen
A name with no hyphen and no capitals - <fancybox> - is not reported at all, because it is indistinguishable from a typo of a built-in and reporting every unknown element would bury the ones that matter. The two rows above are reported, because a spelling with capitals or an underscore is evidence that someone meant a component.
Measured with lolhtml.Element.TagName, which reports what the tokenizer produced. lolhtml.Element.TagNamePreserveCase reports the source spelling, and the inventory keeps both: the lower-case name is what a definition has to match, and the spelling is what tells the reader why their component never upgraded.
One rewriter per document, not one per fragment ¶
The package documentation promises that chunk boundaries never affect handler behaviour, and they do not - for one rewriter. Rewriting two fragments with two rewriters and joining the outputs is a different thing, and it is not safe, because a fragment that ends inside a tag is invisible to every handler and is emitted verbatim.
Measured on `<p>a</p><script>alert(1)</script><p>b</p>`, removing every script, over all forty places the document can be cut in two:
one rewriter, whole document saw p script p <p>a</p><p>b</p> one rewriter, two writes, any cut saw p script p <p>a</p><p>b</p> two rewriters, cuts 9 to 15 saw p and p <p>a</p><script>alert(1)</script><p>b</p> two rewriters, cut 16 saw p script, p <p>a</p>alert(1)</script><p>b</p> two rewriters, every other cut the script is removed
Seven cuts land strictly inside the eight bytes of `<script>`, and each of them puts a live script element back into the output that neither pass ever saw: the first fragment ends mid-tag, so no element handler runs for it and the bytes pass through, and the second begins mid-name, so its remainder is text and passes through too. The join reassembles them.
Cut 16 is the boundary immediately after the complete start tag, and it fails differently: the first pass does see the script and removes it, but the content was in the other fragment, so the payload survives as text next to a stray end tag. Not executable, and not the document either.
Every one of those is silent - no error from either pass. So an inventory of a page assembled from separately-rewritten fragments undercounts, and a sanitiser built the same way has a hole. Feed the fragments to one rewriter as successive writes instead, which the measurement above shows is correct at every boundary. This program reads its input with io.Copy into a single writer for that reason.