inventory

command
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 28, 2026 License: BSD-3-Clause Imports: 8 Imported by: 0

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL