Skip to content

fix(paragraph): guard listRendering destructure against null#2896

Open
kendaller wants to merge 2 commits intosuperdoc-dev:mainfrom
kendaller:fix/paragraph-null-list-rendering
Open

fix(paragraph): guard listRendering destructure against null#2896
kendaller wants to merge 2 commits intosuperdoc-dev:mainfrom
kendaller:fix/paragraph-null-list-rendering

Conversation

@kendaller
Copy link
Copy Markdown

Summary

Fixes a TypeError: Cannot destructure property 'suffix' of 'this.node.attrs.listRendering' as it is null thrown from ParagraphNodeView.

Reproduction

When a paragraph node carries listRendering: null (as an explicit null, not undefined) and the post-transaction list-styles re-pass runs, two call sites crash:

  1. #updateListStyles (line 235) destructures { suffix, justification } directly.
  2. #initList (lines 286-287) accesses listRendering.markerText and .suffix.

In my case this surfaced when dispatching a ProseMirror transaction that combined tr.setDocAttribute('bodySectPr', …) with a paragraph delete — the re-pass walks paragraphs and blows up on the first one whose listRendering is null.

Fix

One-line guards at each call site — ?? {} on the destructure, optional chaining on the property access. Existing defaults (suffix ?? 'tab' and the suffix == null branch in #createSeparator) absorb the null case cleanly.

Test plan

  • Added a regression test (does not throw when listRendering is null) that calls .update() with a listRendering: null node and asserts no throw.
  • All 14 ParagraphNodeView tests pass: pnpm vitest run src/editors/v1/extensions/paragraph/ParagraphNodeView.test.js.
  • pnpm run lint — 0 errors (warnings are pre-existing any usage elsewhere).
  • pnpm run format:check — clean.

ParagraphNodeView destructured `{ suffix, justification }` from
`this.node.attrs.listRendering` without a null-check in #updateListStyles,
and accessed `listRendering.markerText`/`.suffix` directly in #initList.

When a paragraph node carried `listRendering: null` — which can happen
after certain editor mutations (e.g. dispatching a transaction that
combines `setDocAttribute('bodySectPr', …)` with a paragraph delete) —
the post-transaction list-styles re-pass threw:

    TypeError: Cannot destructure property 'suffix' of
    'this.node.attrs.listRendering' as it is null

Use `?? {}` and optional chaining so a null value falls back through
the existing defaults (`suffix ?? 'tab'` and the `suffix == null` branch
in #createSeparator).

Adds a regression test.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 655c60a846

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 235 to 236
let { suffix, justification } = this.node.attrs.listRendering ?? {};
suffix = suffix ?? 'tab';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle null listRendering without forcing tab suffix

When listRendering is null, this fallback forces suffix to 'tab', but update() can still execute a queued RAF callback that first calls #initList(node.attrs.listRendering) with stale data from the previous node. If that stale list had suffix: 'space' or 'nothing', #initList creates a text-node separator, and then #updateListStyles takes the forced tab path and eventually writes this.separator.style.cssText, which throws because text nodes have no style. This means the null-guarded path can still crash in mixed-suffix updates instead of safely no-oping.

Useful? React with 👍 / 👎.

Previously the null-guarded path fell back to `suffix = 'tab'` and still
invoked `#calculateMarkerStyle`/`#calculateTabSeparatorStyle`. Reviewer
(codex-connector) flagged that in mixed-suffix updates — where a queued
RAF callback runs after a node transitions from `suffix: 'space'` to
`listRendering: null` — the separator may still be a Text node. Writing
`this.separator.style.cssText` on a Text node throws.

Change #updateListStyles and #initList to return early when
`listRendering` is null, leaving the existing marker/separator untouched.
Future updates (when the node gets a real listRendering or isList()
returns false) will clean up as before.

Adds a regression test covering the space→null transition.
@kendaller
Copy link
Copy Markdown
Author

Good catch — addressed in 0307343. Changed the null-guarded path from "force suffix='tab'" to "early return, leave the existing marker/separator untouched" in both #updateListStyles and #initList. Future updates (when listRendering gets a real value or isList() flips to false) will do the cleanup.

Added a second regression test (does not try to style a text-node separator when switching to null listRendering) that asserts the space→null transition doesn't replace or style the pre-existing Text node separator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant