Execution — HTML Parser: Foster Parenting

The first lab of the track. No Chromium build required.

Track §6 (HTML Parser Internals), §7 (mini parser), §44 (Reading Ladder), §46 (complexity notebook). Phase 0. No Chromium build required — this lab runs entirely on Code Search, a spec, and any Chrome.

Time budget: ~3 hours. If step 4 exceeds 60 minutes, stop and ask — the navigation technique is the deliverable, not the answer.


Why this lab is first

It is the smallest task in the entire track that exercises the complete loop — predict, verify, spec, source, test, break, explain — and it needs zero setup. It also lands on the single best example in the web platform of "production complexity caused by a constraint you would never have guessed," which is the §46 skill the whole track is built around.


Rules

  1. Write every prediction down before running anything. Predictions made after observation are worthless. Put them in docs/observation.md before you open a browser.
  2. Do not use innerHTML to verify. It invokes the fragment parsing algorithm with a different context element and can produce a different tree. Load real documents.
  3. No source paths will be given to you. Record the search that worked, not the path.
  4. Do not read §9 (Answer key) until §8 is written.

Step 1 — Predict

For each case, write out the resulting DOM tree of document.body — every element, every text node, with exact text contents. Also answer, per case: what is table.parentNode? What is table.childNodes.length?

Case A — canonical

<!DOCTYPE html><html><body><table>hello<div>world</div></table></body></html>

Case B — the special case

<!DOCTYPE html><html><body><table><input type="hidden" name="a"><input type="text" name="b"></table></body></html>

Case C — the track's example, verbatim (whitespace matters)

<html>
  <body>
    <table>
      hello
      <div>world</div>
    </table>
  </body>
</html>

Additional predictions for Case C:

  • How many text nodes exist in total, and what are their exact contents?
  • Does <table> end up with any children at all?

Case D — stretch

<!DOCTYPE html><html><body><table><tr><td>a</td></tr></table></body></html>

Which element in the result was never written in the source, and which spec rule creates it?


Step 2 — Verify

Write the three files and open them from file:// (not via innerHTML, not via DOMParser — a real navigation).

# from labs/
mkdir -p lab-01 && cd lab-01
printf '%s' '<!DOCTYPE html><html><body><table>hello<div>world</div></table></body></html>' > a.html
printf '%s' '<!DOCTYPE html><html><body><table><input type="hidden" name="a"><input type="text" name="b"></table></body></html>' > b.html
printf '<html>\n  <body>\n    <table>\n      hello\n      <div>world</div>\n    </table>\n  </body>\n</html>\n' > c.html
printf '%s' '<!DOCTYPE html><html><body><table><tr><td>a</td></tr></table></body></html>' > d.html

In the console, dump the tree with whitespace made visible:

const show = (n, d = 0) => {
  const pad = '  '.repeat(d);
  if (n.nodeType === 3) return pad + '#text ' + JSON.stringify(n.data) + '\n';
  if (n.nodeType === 8) return pad + '#comment ' + JSON.stringify(n.data) + '\n';
  const attrs = n.attributes?.length
    ? ' [' + [...n.attributes].map(a => `${a.name}=${JSON.stringify(a.value)}`).join(' ') + ']'
    : '';
  let s = pad + n.nodeName.toLowerCase() + attrs + '\n';
  for (const c of n.childNodes) s += show(c, d + 1);
  return s;
};
console.log(show(document.body));

Score each prediction: exactly right / structurally right, details wrong / wrong. Record the score honestly in PROGRESS.md §7.


Step 3 — Spec

Find, in the HTML Standard (the parsing section, "tree construction"), the rules that produce what you observed. You are looking for:

  • the insertion mode that is active when the parser is inside <table>
  • the sub-mode that handles character tokens there, and why one exists separately
  • the algorithm that decides where a node actually gets inserted, which is not simply "into the current node"
  • the specific exception in Case B

Deliverable: for each case, quote the spec sentence that causes the observed behavior, and name the algorithm that relocates the node.

Do not skip this. The whole point of §23/WPT later is that spec text is the shared contract between browsers; Blink comments quote it, which is what makes step 4 tractable.


Tool: Chromium Code Search — https://source.chromium.org/chromium/chromium/src

Strategies, in the order to try them:

  1. Spec-phrase search. Blink's parser mirrors the spec closely and quotes step text in comments. Take the most distinctive phrase from the algorithm you found in step 3 — a two-or-three-word term of art, not a common word — and search it in quotes. This is the highest-yield technique in the entire track for spec-defined subsystems.
  2. Symbol search. Once you have a plausible name, symbol:Name finds the declaration rather than every mention.
  3. Cross-references. Click the symbol → the references panel gives you callers and callees. This answers gate questions 3 and 4 mechanically; do not guess them.
  4. Scoping. file:third_party/blink/renderer/core/html/parser/ narrows noise. case:yes matters for C++ identifiers.
  5. History. git log-equivalent blame in Code Search: find the CL that introduced the Case B exception. Its commit message is the answer to "why does this exist."

Deliverables:

  • the function that performs the relocation, and the class that owns it
  • the call site(s) that decide relocation is needed
  • the exact predicate that implements the Case B exception
  • the search query that found each one, written down

Step 5 — Find the tests

Locate at least one of each:

  • a C++ unit test in the parser directory (file:_test.cc)
  • a Blink web test exercising this behavior (search under third_party/blink/web_tests/)
  • a Web Platform Test (third_party/blink/web_tests/external/wpt/) — note that WPT is the cross-browser contract, and finding it here is your §23 warm-up

Deliverable: file paths, plus one sentence on what each test would catch that the others would not.


Step 6 — Build a small version

Write foster.js — under ~80 lines, no dependencies. It does not need to be a real tokenizer. Take a pre-tokenized array of tokens as input:

const tokens = [
  { type: 'startTag', name: 'table' },
  { type: 'character', data: 'hello' },
  { type: 'startTag', name: 'div' },
  { type: 'character', data: 'world' },
  { type: 'endTag', name: 'div' },
  { type: 'endTag', name: 'table' },
];

Implement only:

  • a stack of open elements
  • two insertion modes: inBody, inTable
  • an appropriateInsertionPlace(fosterParenting) function
  • the Case B exception

It must produce the correct tree for cases A, B and D. Case C (whitespace batching) is the stretch goal, and if you attempt it you will discover by yourself why the spec needs a separate character sub-mode — that discovery is the real prize here.

This file is milestone M2 of mini-browser/. Keep it.


Step 7 — Break it

Analytically (no local build yet — you will do this for real in Phase 6):

  1. Delete the relocation step: every node goes into the current node. Which of your four cases changes? What class of real-world page breaks?
  2. Delete the Case B exception. What breaks, and for whom?
  3. Delete the character sub-mode and insert characters immediately. What changes about Case C, and why is the batching load-bearing rather than an optimization?
  4. For each: name the specific test from step 5 that would fail first.

Also break your own foster.js the same three ways and observe the diffs. Your toy is the control group.


Step 8 — The gate (§44)

Write answers for the relocation function. All eight, in prose, in docs/observation.md:

  1. Why does this code exist?
  2. What invariant does it maintain?
  3. Who calls it?
  4. What does it call?
  5. Which process and thread executes it? (Be precise — and check whether the answer is always the same one. The parser directory contains a hint that it may not be.)
  6. What happens if it is removed?
  7. How is it tested?
  8. What simpler design would fail, and why?

Then open complexity-notebook entry #1 (HTML parser insertion modes) in PROGRESS.md §5:

Observed complexity:
My simpler design:
What requirement breaks my design:
Production constraint:
Resulting architecture:
Essential architecture, or accreted complexity? (defend it)

Case B is the sharpest possible input to that entry. Ask specifically: is this exception principled, or is it a fossil? Defend the answer with evidence from the CL history.


Step 9 — Answer key

The key is on disk at answer-key.md, behind a stop banner. It is not locked. Nobody is checking. Open it after §8 and not before — the value of this lab is entirely in the gap between your prediction and reality.


Done when

  • Predictions written before observation, and scored
  • Spec sentences quoted per case
  • Implementation located, with the working search queries recorded
  • Three kinds of test located
  • foster.js passes cases A, B, D
  • Three analytical breakages reasoned through, with the first-failing test named
  • Eight gate questions answered
  • Complexity notebook entry #1 written
  • PROGRESS.md lab log updated