Branch: refs/heads/main
  Home:   https://github.com/WebKit/WebKit
  Commit: 1f01e406b70ecb01f1e605b490badbd9c9b3f9ac
      
https://github.com/WebKit/WebKit/commit/1f01e406b70ecb01f1e605b490badbd9c9b3f9ac
  Author: Alan Baradlay <[email protected]>
  Date:   2026-04-15 (Wed, 15 Apr 2026)

  Changed paths:
    A LayoutTests/editing/selection/first-letter-caret-single-char-expected.txt
    A LayoutTests/editing/selection/first-letter-caret-single-char.html
    A 
LayoutTests/editing/selection/first-letter-selection-cross-element-expected.txt
    A LayoutTests/editing/selection/first-letter-selection-cross-element.html
    A LayoutTests/editing/selection/first-letter-selection-expected.txt
    A 
LayoutTests/editing/selection/first-letter-selection-painting-expected.html
    A LayoutTests/editing/selection/first-letter-selection-painting.html
    A LayoutTests/editing/selection/first-letter-selection.html
    M LayoutTests/editing/text-iterator/first-letter-word-boundary-expected.txt
    A LayoutTests/fast/css/first-letter-pre-text-iterator-expected.txt
    A LayoutTests/fast/css/first-letter-pre-text-iterator.html
    M LayoutTests/fast/text/first-letter-partial-invalidation-crash-expected.txt
    M 
LayoutTests/platform/glib/editing/selection/extend-by-word-002-expected.txt
    M LayoutTests/platform/ios/editing/selection/extend-by-word-002-expected.txt
    M LayoutTests/platform/mac-wk2/TestExpectations
    M LayoutTests/platform/mac/editing/selection/extend-by-word-002-expected.txt
    M Source/WebCore/dom/Position.cpp
    M Source/WebCore/dom/Position.h
    M Source/WebCore/dom/PositionIterator.cpp
    M Source/WebCore/editing/Editing.cpp
    M Source/WebCore/editing/Editing.h
    M Source/WebCore/editing/FrameSelection.cpp
    M Source/WebCore/editing/RenderedPosition.cpp
    M Source/WebCore/editing/RenderedPosition.h
    M Source/WebCore/editing/TextIterator.cpp
    M Source/WebCore/editing/VisiblePosition.cpp
    M Source/WebCore/editing/VisibleUnits.cpp
    M Source/WebCore/rendering/RenderObject.cpp
    M Source/WebCore/rendering/RenderTextFragment.cpp

  Log Message:
  -----------
  CSS1: character styled with :first-letter is not selectable
https://bugs.webkit.org/show_bug.cgi?id=6185
<rdar://problem/5688237>

Reviewed by Ryosuke Niwa.

Given

  <style>
        div::first-letter { color: red; }
  </style>
  <div>Hello</div>

the DOM (inside the block container) has a single text node "Hello"
but WebKit splits this into two renderers to be able to style the first letter.

  RenderBlock (div)
        RenderInline (::first-letter, anonymous)
          RenderText "H"            <- offsets 0-1 (no DOM node)
        RenderTextFragment "ello"   <- offsets 0-4 (owns the DOM text node)

The existing code assumes each DOM node maps to exactly one renderer.
Every call to node->renderer() or Position::renderer() returns the
remaining fragment - the first-letter renderer is never consulted.

This means DOM offset 0 ("H") gets checked against the remaining
fragment's text boxes, which start at "e". Selection, caret positioning,
and offset validation all fail for the first-letter range because the
wrong renderer is asked about offsets it doesn't own.

The fix is to never consult node->renderer() directly for text offset
operations on first-letter content. Instead, all call sites go through
Position::rendererAndOffset() which resolves the 1-DOM-node-to-2-renderers
split by picking the correct renderer and converting to its local offset.
This is the central invariant of the fix - every place that previously
called node->renderer() and used a DOM offset against it now uses this
helper (or one of its wrappers) to get the right renderer + offset pair.

Key helpers introduced:
- Position::rendererAndOffset(): the single source of truth for resolving
  a DOM offset to the correct renderer + fragment-local offset. Routes to
  the first-letter renderer for offsets below fragmentStart, otherwise
  returns the remaining fragment with adjusted offset.
- Position::resolvedTextRendererAndOffset(): convenience wrapper that also
  casts to RenderText.
- convertOffsetInTextFragmentToNodeOffset(): converts fragment-local offset 
back to
  DOM offset. Used when exiting renderer space (caretMaxOffset,
  createPositionWithAffinity, endPositionForLine, positionAtRightBoundary-
  OfBiDiRun, TextIterator::emitText).
- convertNodeOffsetToOffsetInTextFragment(): converts DOM offset to
  fragment-local. Used in Position::upstream/downstream where there is no
  Position object to call rendererAndOffset() on.
- crossesFirstLetterBoundary(): prevents upstream/downstream from
  canonicalizing positions across the first-letter split. Without this,
  VisiblePosition(offset 0) and VisiblePosition(offset 1) would collapse
  to the same canonical position, making the first letter unselectable.
  Must run before the isStreamer update to see the pre-update lastVisible.

Position::isCandidate, isRenderedCharacter, rendersInDifferentPosition:
  These check text offset validity against renderer text boxes. They now
  use resolvedTextRendererAndOffset() to ask the correct renderer
  (first-letter or remaining fragment) instead of always asking the
  remaining fragment with a raw DOM offset.

Position::inlineBoxAndOffset: resolves to the correct inline box for
  caret/selection positioning. Now delegates to rendererAndOffset()
  instead of duplicating the first-letter routing logic.

RenderedPosition: stores the original DOM node (m_node) from the Position
  during construction. Without this, positionAtLeftBoundaryOfBiDiRun()
  produces a null-node Position when the inline box belongs to the
  anonymous first-letter text, causing adjustEndpointsAtBidiBoundary()
  to collapse the selection when dragging left past the first letter.

VisiblePosition::rightVisuallyDistinctCandidate: when bidi traversal
  lands on the anonymous first-letter text box, resolves the DOM node
  via remainingTextFragmentForFirstLetter() before creating the Position.

RenderTextFragment::canBeSelectionLeaf: returns true for remaining
  fragments with first-letter, matching base RenderText behavior so
  cross-element selections (Cmd+A) include the remaining text.

* LayoutTests/editing/selection/first-letter-caret-single-char-expected.txt: 
Added.
* LayoutTests/editing/selection/first-letter-caret-single-char.html: Added.
* 
LayoutTests/editing/selection/first-letter-selection-cross-element-expected.txt:
 Added.
* LayoutTests/editing/selection/first-letter-selection-cross-element.html: 
Added.
* LayoutTests/editing/selection/first-letter-selection-expected.txt: Added.
* LayoutTests/editing/selection/first-letter-selection-painting-expected.html: 
Added.
* LayoutTests/editing/selection/first-letter-selection-painting.html: Added.
* LayoutTests/editing/selection/first-letter-selection.html: Added.
* LayoutTests/editing/text-iterator/first-letter-word-boundary-expected.txt:
  Progression. The backwards word boundary with white-space:pre now
  correctly finds offset 1 instead of 0.

* LayoutTests/fast/css/first-letter-pre-text-iterator-expected.txt: Added.
* LayoutTests/fast/css/first-letter-pre-text-iterator.html: Added.
* LayoutTests/fast/text/first-letter-partial-invalidation-crash-expected.txt:
  Progression. We match other engines now.

* LayoutTests/platform/glib/editing/selection/extend-by-word-002-expected.txt:
* LayoutTests/platform/ios/editing/selection/extend-by-word-002-expected.txt:
* LayoutTests/platform/mac-wk2/TestExpectations:
  Remove [ Crash ] for editing/text-iterator/backwards-text-iterator-basic.html.
  Progression — the null-check in handleFirstLetter prevents the crash.

* LayoutTests/platform/mac/editing/selection/extend-by-word-002-expected.txt:
  Progression. Content is selected now across <li>s with first-letter.

* Source/WebCore/dom/Position.cpp:
(WebCore::crossesFirstLetterBoundary):
(WebCore::Position::upstream const):
(WebCore::Position::downstream const):
(WebCore::Position::hasRenderedNonAnonymousDescendantsWithHeight):
  Was skipping ::first-letter pseudo renderers because they have no
  associated DOM node. This made positions after first-letter content
  fail isCandidate() -- the function thought the element had no visible
  content. The fix lets first-letter renderers count as rendered
  descendants with height.
(WebCore::Position::isCandidate const):
(WebCore::Position::isRenderedCharacter const):
(WebCore::Position::rendersInDifferentPosition const):
(WebCore::Position::rendererAndOffset const):
(WebCore::Position::resolvedTextRendererAndOffset const):
(WebCore::Position::inlineBoxAndOffset const):
* Source/WebCore/dom/Position.h:
* Source/WebCore/dom/PositionIterator.cpp:
(WebCore::PositionIterator::isCandidate const):
* Source/WebCore/editing/Editing.cpp:
(WebCore::caretMaxOffset):
(WebCore::convertOffsetInTextFragmentToNodeOffset):
(WebCore::convertNodeOffsetToOffsetInTextFragment):
* Source/WebCore/editing/Editing.h:
* Source/WebCore/editing/FrameSelection.cpp:
(WebCore::FrameSelection::updateAppearance):
* Source/WebCore/editing/RenderedPosition.cpp:
(WebCore::nodeFromPosition):
(WebCore::rendererFromPosition):
(WebCore::RenderedPosition::RenderedPosition):
(WebCore::RenderedPosition::positionAtLeftBoundaryOfBiDiRun const):
(WebCore::RenderedPosition::positionAtRightBoundaryOfBiDiRun const):
* Source/WebCore/editing/RenderedPosition.h:
* Source/WebCore/editing/TextIterator.cpp:
(WebCore::TextIterator::handleTextNode):
(WebCore::TextIterator::handleTextRun):
(WebCore::TextIterator::emitText):
(WebCore::SimplifiedBackwardsTextIterator::handleFirstLetter):
  Null-check firstLetterRenderer before dereferencing. The first-letter
  renderer can be destroyed by DOM mutations (e.g. deleteFromDocument)
  while the backwards iterator is still referencing the text node.
* Source/WebCore/editing/VisiblePosition.cpp:
(WebCore::remainingTextFragmentForFirstLetter):
(WebCore::VisiblePosition::leftVisuallyDistinctCandidate const):
(WebCore::VisiblePosition::rightVisuallyDistinctCandidate const):
* Source/WebCore/editing/VisibleUnits.cpp:
(WebCore::endPositionForLine):
(WebCore::findEndOfParagraph):
* Source/WebCore/rendering/RenderObject.cpp:
(WebCore::RenderObject::createPositionWithAffinity const):
* Source/WebCore/rendering/RenderTextFragment.cpp:
(WebCore::RenderTextFragment::canBeSelectionLeaf const):

Canonical link: https://commits.webkit.org/311287@main



To unsubscribe from these emails, change your notification settings at 
https://github.com/WebKit/WebKit/settings/notifications

Reply via email to