-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Fix incorrect surround behaviour next to <br> tag #4518
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
b6c74f6 to
5cdbc09
Compare
|
I'm worried about regressions here. Would be nice to fix the root cause, but that might require a deep understanding of the surrounding logic. |
|
I let GPT run for an hour or two and put together this fix of the deeper issue. I looked it over and it seems mostly reasonable, but it could definitely use a thorough review from someone who knows the codebase. It works for the cases I've tested and I haven't found any regressions, though more testing is warranted. Here is its report: PR: Node-anchored FlatRange for Surround (remove index shifting; add regression tests)SummaryThis change replaces the index-based FlatRange used by the surround formatting pipeline with a node-anchored representation and removes apply-time index shifting. Instead of persisting Why this change was madeThe surround pipeline performs DOM mutations during:
When FlatRange is index-based, any of those mutations can invalidate stored sibling indices, leading to subtle mis-selection or By anchoring FlatRange to actual nodes rather than indices, we avoid having to “repair” indices as the DOM changes and can remove the complex apply-time shifting logic entirely. Implementation details1) FlatRange is now node-anchoredFile: ts/lib/domlib/surround/flat-range.ts
Derived boundary behavior:
This keeps the external FlatRange API largely intact (consumers can still call toDOMRange(), access 2) Remove apply-time shiftingFile: ts/lib/domlib/surround/apply/index.ts
Because FlatRange indices are now derived, there is no longer any valid “shift” operation to apply. 3) Adjust unwrap rebasing hook usageFile: ts/lib/domlib/surround/build/build-tree.ts
How it works end-to-end
Potential concerns / risks
|
0833afa to
7946bb6
Compare
|
Will keep this until someone has the chance to dive into the surround logic. |
When applying bold to text that sits immediately after an existing
<b>…</b>which contains a<br>, the formatting merge logic could incorrectly move the<br>out of the bold element.Example minimal reproduction steps
Input HTML:
Action:
Actual output:
Expected output:
High-level explanation
The surround algorithm builds a formatting tree, may unwrap/remove matching elements (like existing
<b>), and then re-applies formatting using FlatRange sibling indices.Previously, when no matching ancestor was found, we sometimes started the build/apply process from the selection’s text node (range.commonAncestorContainer). During tree building, removing/unwrapping adjacent formatting elements can change sibling structure, making those precomputed sibling indices effectively stale and causing the final wrap to exclude the
<br>portion.Change: When the computed “correct node” is a text/comment node, we now build/apply starting from its parent node instead. This keeps the build/apply operations anchored to a stable container and preserves
<br>inside the merged bold.The root cause of this issue appears to be that the surround algorithm can reference stale sibling indices caused
build-tree's structural splices (replaceWith(...children)). The apply step only compensates for the splice effect ofsurroundContents, not for the earlier splices. The fix authored here works by anchoring the operation to a stable container by starting from the parent element when the chosen build node is a text/comment node.Note: This fix was discovered and authored primarily by GPT-5.2, but initiated, reviewed, and tested by me. When asked about the deeper issue it provided some ideas for solutions, which I'm happy to get it to generate if there is interest. The ideas are:
build.announceElementRemoval(element)beforeelement.replaceWith(...)and extendingSplitRangeand/orFlatRangeshifting to track removals that happen before apply.FlatRange(parent,index,index)and toward something anchored to actual nodes: i.e. store boundary nodes(startNode, endNode)and re-derive indices late, or storeRangeboundary points more directly (but you’d need to keep it “surroundContents-safe”).