Skip to content

fix: lowercase HTML attribute names like browsers do - #364

Open
privatenumber wants to merge 1 commit into
WebReflection:mainfrom
privatenumber:fix/html-attribute-case
Open

privatenumber wants to merge 1 commit into
WebReflection:mainfrom
privatenumber:fix/html-attribute-case

Conversation

@privatenumber

@privatenumber privatenumber commented Nov 1, 2026 •

Copy link
Copy Markdown

Problem

Browsers treat HTML attribute names as case-insensitive, but linkedom keeps them as written. Code that works in a browser silently finds nothing here:

import { parseHTML } from 'linkedom'

const { document } = parseHTML('<div data-itemId="a"></div>')

document.querySelector('[data-itemid]') // null (browser: the <div>)
document.querySelector('div').getAttribute('data-itemid') // null (browser: "a")
document.querySelector('div').outerHTML // <div data-itemId="a"> (browser: data-itemid)

In browsers, the HTML tokenizer lowercases attribute names, and DOM methods such as getAttribute and createAttribute lowercase their argument for HTML elements in HTML documents. Even [data-itemId] fails in linkedom today, because css-select lowercases the selector's name while the stored name keeps its capital letter. This came up in #235.

Changes

Attribute names on HTML elements in HTML documents are now stored in lowercase, so lookups, selectors, and serialization match browsers.

  • Normalized once, matched exactly. Parsing, setAttribute, and createAttribute store lowercase names. Lookups (getAttribute, hasAttribute, removeAttribute, toggleAttribute, getAttributeNode) try the name as given first and lowercase it only after a miss, so no comparison is case-insensitive. Lookups that find the attribute do no extra work; a miss scans the name once for capital letters.
  • SVG and XML are unchanged. htmlparser2's lowerCaseAttributeNames stays off. SVG elements, including svg.innerHTML, keep viewBox and textLength, and text/xml / image/svg+xml documents keep all casing.
  • Known difference from the spec: an Attr attached from another document with setAttributeNode keeps its mixed-case name and is still found by that exact name. MathML attributes are lowercased too, because linkedom creates MathML elements as HTML elements.

Measured with interleaved CPU-time A/B runs against main (2M calls per round, median of 9 rounds):

Operation main This PR
getAttribute, attribute found 3.5 ns 3.9 ns
getAttribute, attribute missing 6.6 ns 14.0 ns
getAttribute('class') 59.9 ns 64.5 ns
hasAttribute, attribute found 6.8 ns 7.6 ns

Code that depends on preserved casing changes: setAttribute('someProp', v) now stores someprop, and getAttributeNames() returns lowercase names for HTML elements. A minor release seems appropriate. uhtml-ssr lowercases its own .prop and ?bool bindings and doesn't depend on linkedom, so it isn't affected.

@privatenumber
privatenumber marked this pull request as ready for review November 4, 2026 16:05
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