Skip to content

getAttribute("class") trims the value and returns "" when absent #342

Open

Description

@robertmorelli

issue:
getAttribute("class") should return the attribute exactly as written. linkedom strips the whitespace (" ab" → "ab") and returns "" instead of null when there is no class. Selectors see the stripped value too, so [class^=" a"] misses.

repro:

import { parseHTML } from "linkedom";
const { document } = parseHTML('<html><body><i class=" ab"></i><b></b></body></html>');
const [i, b] = document.body.children;
console.log(JSON.stringify(i.getAttribute("class")));           // "ab"
console.log(JSON.stringify(b.getAttribute("class")));           // ""
console.log(document.querySelectorAll('[class^=" a"]').length); // 0

expected:
" ab", null, 1 (as Chrome, Firefox, Safari)

system:
linkedom 0.18.13, Node 25.8.1
macOS 27.0, arm64

Activity

  1. osazemeu commented on Oct 26, 2026

    @osazemeu

    I can reproduce both halves of this on 0.18.14 and on main at e2ee208, and I can open a PR. Which fix would you take?

    1. Only the null half. getAttribute('class') returns null when the element has no class attribute. This is one line in Element#getAttribute. We ship it as a local patch today, because a parser that falls back to a default only on null (TipTap's Link mark, for one) reads '' as "class set to empty".
    2. Both halves. The class attribute keeps the value as written, and the token list derives from it. Today the parser (parse-from-string.js), Element#setAttribute and the setAttribute helper in shared/attributes.js all write through className, which splits the value and joins it again. So the stored Attr value is already normalized, and the selector path, which since perf: read class attribute directly in selector adapter #339 reads getAttributeNode('class')?.value ?? null, also sees "ab". A fix would store the raw value and rewrite it only when classList itself is mutated. That overlaps the parse path perf: skip the token list while parsing class attributes #336 changes, so I would rebase on it, or wait for it, whichever you prefer.

    Repro on 0.18.14 (unpatched) and on main at e2ee208 (Node 24.14.1), extended with an explicit empty class:

    import { parseHTML } from "linkedom";
    const { document } = parseHTML('<html><body><i class=" ab"></i><b></b><u class=""></u></body></html>');
    const [i, b, u] = document.body.children;
    i.getAttribute("class");                        // "ab"   (expected " ab")
    b.getAttribute("class");                        // ""     (expected null)
    u.getAttribute("class");                        // ""     (correct)
    b.hasAttribute("class");                        // false  (correct)
    document.querySelectorAll('[class^=" a"]').length; // 0   (expected 1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions