Skip to content

Sanitise speaker extraction: mid-text label in a paragraph's last word span dereferences a null nextElementSibling #418

Description

@maboa

Summary

In the sanitise pass's speaker break-out (js/editor-core.js), the non-startsWithSpeaker branch assumes the span containing the bracketed label always has a following word span:

// js/editor-core.js:628 (as of 2fdb333)
let nextStart = walker.currentNode.parentNode.nextElementSibling.getAttribute("data-m");

If the label was typed after the last word of a paragraph — i.e. the bracket text sits at the end of the text of the paragraph's final word span — nextElementSibling is null and this line throws a TypeError, aborting the rest of the sanitise pass for that keystroke.

The caret-restore a few lines below has the same shape of assumption:

// js/editor-core.js:640
range.setStartBefore(newSpan.nextElementSibling);

newSpan is inserted with span.after(newSpan), so when the label is at the end of the paragraph, newSpan.nextElementSibling is also null and setStartBefore(null) throws even if line 628 is survived.

Preconditions for reaching the branch

  1. Text lands inside an existing word span (typed mid-span, not as its own span) — the Adding a speaker name where none exists no longer gets labelled as a speaker #416 isSpeakerText() guard now keeps such text unsplit until sanitise sees it, which makes this branch more reachable than before, not less.
  2. The span's text does not start with [ (otherwise the startsWithSpeaker branch runs, which uses the parent's own data-m and is safe).
  3. The text contains a complete [...] pair.

So the candidate repro is typing something like word [Alice] at the end of a paragraph's last word.

How to investigate

  1. Load any transcript in the editor (the default demo transcript is fine).
  2. Click at the end of the last word of a paragraph (immediately after its trailing character, inside the same span).
  3. Type [Alice] and stop typing so the debounced sanitise pass runs.
  4. Watch the console for TypeError: Cannot read properties of null (reading 'getAttribute') at editor-core.js:628 (or the setStartBefore failure at :640).
  5. If step 3–4 doesn't reproduce, check where the browser actually puts the typed text (inspect the DOM around the caret): contenteditable may create a bare text node or a new span rather than appending to the word span's text — in that case document the actual node shape here and re-test with a paste (Cmd+V) of word [Alice] replacing the last word, which is likelier to land inside the span.
  6. Also worth one test in each of Chrome/Safari/Firefox — contenteditable text-node placement differs by engine, and this branch's reachability depends on it.

If it reproduces — suggested fix shape

When there is no next sibling, fall back for nextStart to the parent span's own end (data-m + data-d) or, if the next paragraph's first span exists, its data-m; and guard the caret restore (if (newSpan.nextElementSibling) … else place the range after newSpan). A regression fixture (paragraph-final multi-word label) would fit the planned #58 Playwright suite.

If it does not reproduce

Document the node shapes observed per browser in a comment here and close — but consider still adding the two null guards, since the branch is one contenteditable behaviour change away from throwing.

Related: #413 (multi-word label extraction on the same code path, fixed in 2fdb333), #416 (guard that routes bracketed text to this branch), #394 (normalization that made sanitise run more often).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions