Skip to the code
cf.

Next node in document order

MediumDOM

Write nextNode(node, root). It returns the node that comes after node in document order, without leaving root, or null when node is the last node inside root.

The node after node is its first child if it has one; otherwise its next sibling; otherwise the next sibling of its nearest ancestor that has one. Never climb past root: nothing outside it counts.

Every kind of node counts: elements, text, and comments. This is what TreeWalker.nextNode() does; write it without a TreeWalker or NodeIterator.

// <div id="root"><h1>Hi</h1><p>Bye</p></div>
nextNode(root, root);          // <h1>
nextNode(h1, root);            // the text "Hi"
nextNode(h1.firstChild, root); // <p>
nextNode(p.firstChild, root);  // null: "Bye" is the last node inside root

What the tests check

  1. goes to the first child
    const root = document.getElementById("root");
    expect(nextNode(root, root)).toBe(document.getElementById("title"));
    
  2. includes text nodes
    const root = document.getElementById("root");
    const title = document.getElementById("title");
    expect(nextNode(title, root)).toBe(title.firstChild);
    
  3. goes to the next sibling when there's no child
    const root = document.getElementById("root");
    expect(nextNode(document.getElementById("rule"), root)).toBe(document.getElementById("list"));
    
  4. climbs to the next sibling of an ancestor
    const root = document.getElementById("root");
    const b = document.getElementById("b");
    const comment = b.lastChild;
    expect(nextNode(document.getElementById("em").firstChild, root)).toBe(comment);
    expect(nextNode(comment, root)).toBe(document.getElementById("end"));
    
  5. returns null after the last node inside root
    const root = document.getElementById("root");
    expect(nextNode(document.getElementById("end").firstChild, root)).toBeNull();
    
  6. never climbs past root
    const list = document.getElementById("list");
    const b = document.getElementById("b");
    const rule = document.getElementById("rule");
    expect(nextNode(document.getElementById("a").firstChild, list)).toBe(b);
    expect(nextNode(b.lastChild, b)).toBeNull();
    expect(nextNode(rule, rule)).toBeNull();
    
  7. visits every node in document order, without a TreeWalker
    const banned = ["createTreeWalker", "createNodeIterator"].map((name) => ({ name, original: Document.prototype[name] }));
    for (const { name } of banned) {
      Document.prototype[name] = () => {
        throw new Error("Don't call " + name + ": write the walk yourself with firstChild, nextSibling, and parentNode");
      };
    }
    const label = (node) => (node.nodeType === Node.ELEMENT_NODE ? "#" + node.id : node.nodeType === Node.TEXT_NODE ? node.data : "<!--" + node.data + "-->");
    const root = document.getElementById("root");
    const seen = [];
    try {
      for (let node = root; node; node = nextNode(node, root)) {
        seen.push(label(node));
        if (seen.length > 50) break;
      }
    } finally {
      for (const { name, original } of banned) Document.prototype[name] = original;
    }
    expect(seen).toEqual(["#root", "#title", "Title", "#rule", "#list", "#a", "A", "#b", "#em", "B", "<!--note-->", "#end", "End"]);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).