REGRESSION (r281306): Non-breaking space incorrectly collapsed when webkit-nbsp-mode is set to "space"
https://bugs.webkit.org/show_bug.cgi?id=235627
<rdar://88004831>

Reviewed by Antti Koivisto.

Source/WebCore:

"-webkit-nbsp-mode: space" has a peculiar collapsing behavior. It only collapses as leading content
preceded by forced line break.

<div style="-webkit-nbsp-mode: space; width: 0px;">
  first_line&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;second_line
</div>
The non-breaking space is collapsed. It does not show on the second line as leading content.

<div style="-webkit-nbsp-mode: space; width: 0px;">
  first_line<br>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;second_line
</div>
The non-breaking space is NOT collapsed. It shows up as leading content on the second line, indenting the "second_line" text content.

To mimic this legacy behavior, it's easier just to switch back to handling nbsp; as a non-whitespace inline item and
collapse it as leading content (depending on the breaking context).
(as opposed to customize regular whitespace content collapsing behavior just to support this quirk)

Test: fast/text/collapsible-non-breaking-space.html

* layout/formattingContexts/inline/InlineItemsBuilder.cpp:
(WebCore::Layout::moveToNextNonWhitespacePosition): remove special nbsp; handling
(WebCore::Layout::InlineItemsBuilder::handleTextContent): create an inline text item for the special mon-breaking space content.
* layout/formattingContexts/inline/InlineLine.cpp:
(WebCore::Layout::Line::initialize):
(WebCore::Layout::Line::appendTextContent):
* layout/formattingContexts/inline/InlineLine.h:
* layout/formattingContexts/inline/InlineLineBuilder.cpp:
(WebCore::Layout::LineBuilder::inlineItemWidth const):
(WebCore::Layout::LineBuilder::initialize):
(WebCore::Layout::LineBuilder::candidateContentForLine):
(WebCore::Layout::LineBuilder::handleInlineContent):
(WebCore::Layout::LineBuilder::rebuildLine):
* layout/formattingContexts/inline/InlineLineBuilder.h:
(WebCore::Layout::LineBuilder::isFirstLine const):
* layout/formattingContexts/inline/InlineTextItem.cpp:
(WebCore::Layout::InlineTextItem::isCollapsibleNonBreakingSpace const):
* layout/formattingContexts/inline/InlineTextItem.h:

LayoutTests:

* fast/text/collapsible-non-breaking-space-expected.html: Added.
* fast/text/collapsible-non-breaking-space.html: Added.
* platform/ios/fast/text/whitespace/nbsp-mode-and-linewraps-expected.txt: A slight behavior change with trailing content trimming, but
not generally noticeable and very specific to this non-standard property.
* platform/mac/fast/text/whitespace/nbsp-mode-and-linewraps-expected.txt:


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@288650 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 4dcb2c5..5e42ccb 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,17 @@
+2022-01-26  Alan Bujtas  <zalan@apple.com>
+
+        REGRESSION (r281306): Non-breaking space incorrectly collapsed when webkit-nbsp-mode is set to "space"
+        https://bugs.webkit.org/show_bug.cgi?id=235627
+        <rdar://88004831>
+
+        Reviewed by Antti Koivisto.
+
+        * fast/text/collapsible-non-breaking-space-expected.html: Added.
+        * fast/text/collapsible-non-breaking-space.html: Added.
+        * platform/ios/fast/text/whitespace/nbsp-mode-and-linewraps-expected.txt: A slight behavior change with trailing content trimming, but
+        not generally noticeable and very specific to this non-standard property.
+        * platform/mac/fast/text/whitespace/nbsp-mode-and-linewraps-expected.txt:
+
 2022-01-26  Alexey Shvayka  <ashvayka@apple.com>
 
         globalThis.queueMicrotask() should report thrown exceptions
diff --git a/LayoutTests/fast/text/collapsible-non-breaking-space-expected.html b/LayoutTests/fast/text/collapsible-non-breaking-space-expected.html
new file mode 100644
index 0000000..b6095d1
--- /dev/null
+++ b/LayoutTests/fast/text/collapsible-non-breaking-space-expected.html
@@ -0,0 +1,8 @@
+<style>
+div {
+  background-color: green;
+  width: 80px;
+  height: 220px;
+}
+</style>
+<div></div>
diff --git a/LayoutTests/fast/text/collapsible-non-breaking-space.html b/LayoutTests/fast/text/collapsible-non-breaking-space.html
new file mode 100644
index 0000000..a261a41
--- /dev/null
+++ b/LayoutTests/fast/text/collapsible-non-breaking-space.html
@@ -0,0 +1,22 @@
+<style>
+body {
+  font-size: 20px;
+  font-family: Ahem;
+  color: green;
+}
+div {
+  -webkit-nbsp-mode: space;
+  background-color: green;
+  float: left;
+  clear: both;
+}
+</style>
+<div>&nbsp;&nbsp;&nbsp;&nbsp;</div>
+<div>XX&nbsp;&nbsp;</div>
+<div>X&nbsp;&nbsp;X</div>
+<div style="width: 0px;">XXXX&nbsp;&nbsp;XXXX</div>
+<div style="white-space: pre">XX&nbsp;&nbsp;
+XXX</div>
+<div style="white-space: pre">XX&nbsp;&nbsp;
+&nbsp;&nbsp;XX</div>
+<div>XXX&nbsp;<br>&nbsp;XXX</div>
diff --git a/LayoutTests/platform/ios/fast/text/whitespace/nbsp-mode-and-linewraps-expected.txt b/LayoutTests/platform/ios/fast/text/whitespace/nbsp-mode-and-linewraps-expected.txt
index 7ab5f63..0f64007 100644
--- a/LayoutTests/platform/ios/fast/text/whitespace/nbsp-mode-and-linewraps-expected.txt
+++ b/LayoutTests/platform/ios/fast/text/whitespace/nbsp-mode-and-linewraps-expected.txt
@@ -5,10 +5,10 @@
     RenderBody {BODY} at (8,8) size 784x584
 layer at (100,8) size 700x208
   RenderBlock (positioned) {DIV} at (100,8) size 700x208 [border: (2px solid #FF0000)]
-    RenderText {#text} at (14,15) size 659x178
-      text run at (14,15) width 659: "This div is absolutely positioned to the left. All the spaces in this div"
-      text run at (14,45) width 638: "are replaced with non-breaking spaces, and the nbsp-mode for this"
-      text run at (14,75) width 610: "div is 'space', so when calculating line breaks, the non-breaking"
-      text run at (14,105) width 651: "spaces should be treated as normal spaces. If this isn't the case, then"
-      text run at (14,135) width 622: "all the text for this paragraph will be rendered in two lines, and a"
+    RenderText {#text} at (14,15) size 665x178
+      text run at (14,15) width 665: "This div is absolutely positioned to the left. All the spaces in this div "
+      text run at (14,45) width 644: "are replaced with non-breaking spaces, and the nbsp-mode for this "
+      text run at (14,75) width 616: "div is 'space', so when calculating line breaks, the non-breaking "
+      text run at (14,105) width 657: "spaces should be treated as normal spaces. If this isn't the case, then "
+      text run at (14,135) width 628: "all the text for this paragraph will be rendered in two lines, and a "
       text run at (14,165) width 482: "horizontal scroll bar will appear along the bottom."
diff --git a/LayoutTests/platform/mac/fast/text/whitespace/nbsp-mode-and-linewraps-expected.txt b/LayoutTests/platform/mac/fast/text/whitespace/nbsp-mode-and-linewraps-expected.txt
index c699d9e..8e2fcd0 100644
--- a/LayoutTests/platform/mac/fast/text/whitespace/nbsp-mode-and-linewraps-expected.txt
+++ b/LayoutTests/platform/mac/fast/text/whitespace/nbsp-mode-and-linewraps-expected.txt
@@ -5,10 +5,10 @@
     RenderBody {BODY} at (8,8) size 784x584
 layer at (100,8) size 700x196
   RenderBlock (positioned) {DIV} at (100,8) size 700x196 [border: (2px solid #FF0000)]
-    RenderText {#text} at (14,14) size 659x168
-      text run at (14,14) width 659: "This div is absolutely positioned to the left. All the spaces in this div"
-      text run at (14,42) width 638: "are replaced with non-breaking spaces, and the nbsp-mode for this"
-      text run at (14,70) width 610: "div is 'space', so when calculating line breaks, the non-breaking"
-      text run at (14,98) width 651: "spaces should be treated as normal spaces. If this isn't the case, then"
-      text run at (14,126) width 622: "all the text for this paragraph will be rendered in two lines, and a"
+    RenderText {#text} at (14,14) size 665x168
+      text run at (14,14) width 665: "This div is absolutely positioned to the left. All the spaces in this div "
+      text run at (14,42) width 644: "are replaced with non-breaking spaces, and the nbsp-mode for this "
+      text run at (14,70) width 616: "div is 'space', so when calculating line breaks, the non-breaking "
+      text run at (14,98) width 657: "spaces should be treated as normal spaces. If this isn't the case, then "
+      text run at (14,126) width 628: "all the text for this paragraph will be rendered in two lines, and a "
       text run at (14,154) width 482: "horizontal scroll bar will appear along the bottom."
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index ae32a71..b8e8bd4 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,50 @@
+2022-01-26  Alan Bujtas  <zalan@apple.com>
+
+        REGRESSION (r281306): Non-breaking space incorrectly collapsed when webkit-nbsp-mode is set to "space"
+        https://bugs.webkit.org/show_bug.cgi?id=235627
+        <rdar://88004831>
+
+        Reviewed by Antti Koivisto.
+
+        "-webkit-nbsp-mode: space" has a peculiar collapsing behavior. It only collapses as leading content
+        preceded by forced line break.
+
+        <div style="-webkit-nbsp-mode: space; width: 0px;">
+          first_line&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;second_line
+        </div>
+        The non-breaking space is collapsed. It does not show on the second line as leading content.
+
+        <div style="-webkit-nbsp-mode: space; width: 0px;">
+          first_line<br>
+          &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;second_line
+        </div>
+        The non-breaking space is NOT collapsed. It shows up as leading content on the second line, indenting the "second_line" text content.
+
+        To mimic this legacy behavior, it's easier just to switch back to handling nbsp; as a non-whitespace inline item and
+        collapse it as leading content (depending on the breaking context).
+        (as opposed to customize regular whitespace content collapsing behavior just to support this quirk)
+
+        Test: fast/text/collapsible-non-breaking-space.html
+
+        * layout/formattingContexts/inline/InlineItemsBuilder.cpp:
+        (WebCore::Layout::moveToNextNonWhitespacePosition): remove special nbsp; handling
+        (WebCore::Layout::InlineItemsBuilder::handleTextContent): create an inline text item for the special mon-breaking space content. 
+        * layout/formattingContexts/inline/InlineLine.cpp:
+        (WebCore::Layout::Line::initialize):
+        (WebCore::Layout::Line::appendTextContent):
+        * layout/formattingContexts/inline/InlineLine.h:
+        * layout/formattingContexts/inline/InlineLineBuilder.cpp:
+        (WebCore::Layout::LineBuilder::inlineItemWidth const):
+        (WebCore::Layout::LineBuilder::initialize):
+        (WebCore::Layout::LineBuilder::candidateContentForLine):
+        (WebCore::Layout::LineBuilder::handleInlineContent):
+        (WebCore::Layout::LineBuilder::rebuildLine):
+        * layout/formattingContexts/inline/InlineLineBuilder.h:
+        (WebCore::Layout::LineBuilder::isFirstLine const):
+        * layout/formattingContexts/inline/InlineTextItem.cpp:
+        (WebCore::Layout::InlineTextItem::isCollapsibleNonBreakingSpace const):
+        * layout/formattingContexts/inline/InlineTextItem.h:
+
 2022-01-26  Alexey Shvayka  <ashvayka@apple.com>
 
         JSEventListener::replaceJSFunctionForAttributeListener() should not replace m_wrapper unconditionally
diff --git a/Source/WebCore/layout/formattingContexts/inline/InlineItemsBuilder.cpp b/Source/WebCore/layout/formattingContexts/inline/InlineItemsBuilder.cpp
index 5307220..7d92785 100644
--- a/Source/WebCore/layout/formattingContexts/inline/InlineItemsBuilder.cpp
+++ b/Source/WebCore/layout/formattingContexts/inline/InlineItemsBuilder.cpp
@@ -42,13 +42,13 @@
     size_t length { 0 };
     bool isWordSeparator { true };
 };
-static std::optional<WhitespaceContent> moveToNextNonWhitespacePosition(StringView textContent, size_t startPosition, bool preserveNewline, bool preserveTab, bool treatNonBreakingSpaceAsRegularSpace, bool stopAtWordSeparatorBoundary)
+static std::optional<WhitespaceContent> moveToNextNonWhitespacePosition(StringView textContent, size_t startPosition, bool preserveNewline, bool preserveTab, bool stopAtWordSeparatorBoundary)
 {
     auto hasWordSeparatorCharacter = false;
     auto isWordSeparatorCharacter = false;
     auto isWhitespaceCharacter = [&](auto character) {
         // white space processing in CSS affects only the document white space characters: spaces (U+0020), tabs (U+0009), and segment breaks.
-        auto isTreatedAsSpaceCharacter = character == space || (character == newlineCharacter && !preserveNewline) || (character == tabCharacter && !preserveTab) || (character == noBreakSpace && treatNonBreakingSpaceAsRegularSpace);
+        auto isTreatedAsSpaceCharacter = character == space || (character == newlineCharacter && !preserveNewline) || (character == tabCharacter && !preserveTab);
         isWordSeparatorCharacter = isTreatedAsSpaceCharacter;
         hasWordSeparatorCharacter = hasWordSeparatorCharacter || isWordSeparatorCharacter;
         return isTreatedAsSpaceCharacter || character == tabCharacter;
@@ -490,7 +490,6 @@
     auto& style = inlineTextBox.style();
     auto shouldPreserveSpacesAndTabs = TextUtil::shouldPreserveSpacesAndTabs(inlineTextBox);
     auto shouldPreserveNewline = TextUtil::shouldPreserveNewline(inlineTextBox);
-    auto shouldTreatNonBreakingSpaceAsRegularSpace = style.nbspMode() == NBSPMode::Space;
     auto lineBreakIterator = LazyLineBreakIterator { text, style.computedLocale(), TextUtil::lineBreakIteratorMode(style.lineBreak()) };
     unsigned currentPosition = 0;
 
@@ -508,7 +507,7 @@
 
         auto handleWhitespace = [&] {
             auto stopAtWordSeparatorBoundary = shouldPreserveSpacesAndTabs && style.fontCascade().wordSpacing();
-            auto whitespaceContent = moveToNextNonWhitespacePosition(text, currentPosition, shouldPreserveNewline, shouldPreserveSpacesAndTabs, shouldTreatNonBreakingSpaceAsRegularSpace, stopAtWordSeparatorBoundary);
+            auto whitespaceContent = moveToNextNonWhitespacePosition(text, currentPosition, shouldPreserveNewline, shouldPreserveSpacesAndTabs, stopAtWordSeparatorBoundary);
             if (!whitespaceContent)
                 return false;
 
@@ -527,6 +526,26 @@
         if (handleWhitespace())
             continue;
 
+        auto handleNonBreakingSpace = [&] {
+            if (style.nbspMode() != NBSPMode::Space) {
+                // Let's just defer to regular non-whitespace inline items when non breaking space needs no special handling.
+                return false;
+            }
+            auto startPosition = currentPosition;
+            auto endPosition = startPosition;
+            for (; endPosition < contentLength; ++endPosition) {
+                if (text[endPosition] != noBreakSpace)
+                    break;
+            }
+            if (startPosition == endPosition)
+                return false;
+            inlineItems.append(InlineTextItem::createNonWhitespaceItem(inlineTextBox, startPosition, endPosition - startPosition, UBIDI_DEFAULT_LTR, { }, { }));
+            currentPosition = endPosition;
+            return true;
+        };
+        if (handleNonBreakingSpace())
+            continue;
+
         auto handleNonWhitespace = [&] {
             auto startPosition = currentPosition;
             auto endPosition = startPosition;
diff --git a/Source/WebCore/layout/formattingContexts/inline/InlineLine.cpp b/Source/WebCore/layout/formattingContexts/inline/InlineLine.cpp
index 8702c41..24a2b4a 100644
--- a/Source/WebCore/layout/formattingContexts/inline/InlineLine.cpp
+++ b/Source/WebCore/layout/formattingContexts/inline/InlineLine.cpp
@@ -49,8 +49,9 @@
 {
 }
 
-void Line::initialize(const Vector<InlineItem>& lineSpanningInlineBoxes)
+void Line::initialize(const Vector<InlineItem>& lineSpanningInlineBoxes, bool collapseLeadingNonBreakingSpace)
 {
+    m_collapseLeadingNonBreakingSpace = collapseLeadingNonBreakingSpace;
     m_inlineBoxListWithClonedDecorationEnd.clear();
     m_clonedEndDecorationWidthForInlineBoxRuns = { };
     m_nonSpanningInlineLevelBoxCount = 0;
@@ -261,8 +262,19 @@
 void Line::appendTextContent(const InlineTextItem& inlineTextItem, const RenderStyle& style, InlineLayoutUnit logicalWidth)
 {
     auto willCollapseCompletely = [&] {
-        if (!inlineTextItem.isWhitespace())
-            return false;
+        if (!inlineTextItem.isWhitespace()) {
+            auto isLeadingCollapsibleNonBreakingSpace = [&] {
+                // Let's check for leading non-breaking space collapsing to match legacy line layout quirk.
+                if (!inlineTextItem.isCollapsibleNonBreakingSpace() || !m_collapseLeadingNonBreakingSpace)
+                    return false;
+                for (auto& run : makeReversedRange(m_runs)) {
+                    if (run.isBox() || run.isText())
+                        return false;
+                }
+                return true;
+            };
+            return isLeadingCollapsibleNonBreakingSpace();
+        }
         if (InlineTextItem::shouldPreserveSpacesAndTabs(inlineTextItem))
             return false;
         // This content is collapsible. Let's check if the last item is collapsed.
diff --git a/Source/WebCore/layout/formattingContexts/inline/InlineLine.h b/Source/WebCore/layout/formattingContexts/inline/InlineLine.h
index a1f9d43..fa9dfbe 100644
--- a/Source/WebCore/layout/formattingContexts/inline/InlineLine.h
+++ b/Source/WebCore/layout/formattingContexts/inline/InlineLine.h
@@ -43,7 +43,7 @@
     Line(const InlineFormattingContext&);
     ~Line();
 
-    void initialize(const Vector<InlineItem>& lineSpanningInlineBoxes);
+    void initialize(const Vector<InlineItem>& lineSpanningInlineBoxes, bool collapseLeadingNonBreakingSpace);
 
     void append(const InlineItem&, const RenderStyle&, InlineLayoutUnit logicalWidth);
 
@@ -229,6 +229,8 @@
     InlineBoxListWithClonedDecorationEnd m_inlineBoxListWithClonedDecorationEnd;
     InlineLayoutUnit m_clonedEndDecorationWidthForInlineBoxRuns { 0 };
     bool m_hasNonDefaultBidiLevelRun { false };
+    // Note that this is only needed for the special (and ancient and not supported by other browsers) "-webkit-nbsp-mode: space".
+    bool m_collapseLeadingNonBreakingSpace { false };
 };
 
 
diff --git a/Source/WebCore/layout/formattingContexts/inline/InlineLineBuilder.cpp b/Source/WebCore/layout/formattingContexts/inline/InlineLineBuilder.cpp
index e677d0d..6556ef6 100644
--- a/Source/WebCore/layout/formattingContexts/inline/InlineLineBuilder.cpp
+++ b/Source/WebCore/layout/formattingContexts/inline/InlineLineBuilder.cpp
@@ -259,7 +259,7 @@
         auto& inlineTextItem = downcast<InlineTextItem>(inlineItem);
         if (auto contentWidth = inlineTextItem.width())
             return *contentWidth;
-        auto& fontCascade = m_isFirstLine ? inlineTextItem.firstLineStyle().fontCascade() : inlineTextItem.style().fontCascade();
+        auto& fontCascade = isFirstLine() ? inlineTextItem.firstLineStyle().fontCascade() : inlineTextItem.style().fontCascade();
         if (!inlineTextItem.isWhitespace() || InlineTextItem::shouldPreserveSpacesAndTabs(inlineTextItem))
             return TextUtil::width(inlineTextItem, fontCascade, contentLogicalLeft);
         return TextUtil::width(inlineTextItem, fontCascade, inlineTextItem.start(), inlineTextItem.start() + 1, contentLogicalLeft);
@@ -280,7 +280,7 @@
     if (inlineItem.isInlineBoxStart()) {
         auto logicalWidth = boxGeometry.marginStart() + boxGeometry.borderStart() + boxGeometry.paddingStart().value_or(0);
 #if ENABLE(CSS_BOX_DECORATION_BREAK)
-        auto& style = m_isFirstLine ? inlineItem.firstLineStyle() : inlineItem.style();
+        auto& style = isFirstLine() ? inlineItem.firstLineStyle() : inlineItem.style();
         if (style.boxDecorationBreak() == BoxDecorationBreak::Clone)
             logicalWidth += boxGeometry.borderEnd() + boxGeometry.paddingEnd().value_or(0_lu);
 #endif
@@ -401,7 +401,7 @@
 
 void LineBuilder::initialize(const UsedConstraints& lineConstraints, size_t leadingInlineItemIndex, const std::optional<PreviousLine>& previousLine)
 {
-    m_isFirstLine = !previousLine;
+    m_previousLine = previousLine;
     m_floats.clear();
     m_lineSpanningInlineBoxes.clear();
     m_wrapOpportunityList.clear();
@@ -439,7 +439,7 @@
             m_lineSpanningInlineBoxes.append({ *spanningInlineBox, InlineItem::Type::InlineBoxStart, bidiLevelForOpaqueInlineItem });
     };
     createLineSpanningInlineBoxes();
-    m_line.initialize(m_lineSpanningInlineBoxes);
+    m_line.initialize(m_lineSpanningInlineBoxes, m_previousLine && !m_previousLine->endsWithLineBreak);
 
     m_lineMarginStart = lineConstraints.marginStart;
     m_lineLogicalRect = lineConstraints.logicalRect;
@@ -686,7 +686,7 @@
     for (auto index = currentInlineItemIndex; index < softWrapOpportunityIndex; ++index) {
         auto& inlineItem = m_inlineItems[index];
         auto& style = [&] () -> const RenderStyle& {
-            return m_isFirstLine ? inlineItem.firstLineStyle() : inlineItem.style();
+            return isFirstLine() ? inlineItem.firstLineStyle() : inlineItem.style();
         }();
         if (inlineItem.isFloat()) {
             lineCandidate.floatItem = &inlineItem;
@@ -959,7 +959,7 @@
         if (inlineTextItem.isWhitespace())
             return { };
         auto& overflowingRun = candidateRuns.first();
-        if (m_isFirstLine) {
+        if (isFirstLine()) {
             auto& usedStyle = overflowingRun.style;
             auto& style = overflowingRun.inlineItem.style();
             if (&usedStyle != &style && usedStyle.fontCascade() != style.fontCascade()) {
@@ -1060,7 +1060,7 @@
     ASSERT(!m_wrapOpportunityList.isEmpty());
     // We might already have added floats. They shrink the available horizontal space for the line.
     // Let's just reuse what the line has at this point.
-    m_line.initialize(m_lineSpanningInlineBoxes);
+    m_line.initialize(m_lineSpanningInlineBoxes, m_previousLine && !m_previousLine->endsWithLineBreak);
     auto currentItemIndex = layoutRange.start;
     if (m_partialLeadingTextItem) {
         m_line.append(*m_partialLeadingTextItem, m_partialLeadingTextItem->style(), inlineItemWidth(*m_partialLeadingTextItem, { }));
@@ -1070,7 +1070,7 @@
     }
     for (; currentItemIndex < layoutRange.end; ++currentItemIndex) {
         auto& inlineItem = m_inlineItems[currentItemIndex];
-        auto& style = m_isFirstLine ? inlineItem.firstLineStyle() : inlineItem.style();
+        auto& style = isFirstLine() ? inlineItem.firstLineStyle() : inlineItem.style();
         m_line.append(inlineItem, style, inlineItemWidth(inlineItem, m_line.contentLogicalRight()));
         if (&inlineItem == &lastInlineItemToAdd)
             return currentItemIndex - layoutRange.start + 1;
diff --git a/Source/WebCore/layout/formattingContexts/inline/InlineLineBuilder.h b/Source/WebCore/layout/formattingContexts/inline/InlineLineBuilder.h
index 7646acd..99d940c 100644
--- a/Source/WebCore/layout/formattingContexts/inline/InlineLineBuilder.h
+++ b/Source/WebCore/layout/formattingContexts/inline/InlineLineBuilder.h
@@ -125,6 +125,8 @@
     std::optional<IntrinsicWidthMode> intrinsicWidthMode() const { return m_intrinsicWidthMode; }
     bool isInIntrinsicWidthMode() const { return !!intrinsicWidthMode(); }
 
+    bool isFirstLine() const { return !m_previousLine.has_value(); }
+
     const InlineFormattingContext& formattingContext() const { return m_inlineFormattingContext; }
     InlineFormattingState* formattingState() { return m_inlineFormattingState; }
     FloatingState* floatingState() { return m_floatingState; }
@@ -133,7 +135,7 @@
     const LayoutState& layoutState() const;
 
 private:
-    bool m_isFirstLine { false };
+    std::optional<PreviousLine> m_previousLine { };
     std::optional<IntrinsicWidthMode> m_intrinsicWidthMode;
     const InlineFormattingContext& m_inlineFormattingContext;
     InlineFormattingState* m_inlineFormattingState { nullptr };
diff --git a/Source/WebCore/layout/formattingContexts/inline/InlineTextItem.cpp b/Source/WebCore/layout/formattingContexts/inline/InlineTextItem.cpp
index de15dd0..858e8f7 100644
--- a/Source/WebCore/layout/formattingContexts/inline/InlineTextItem.cpp
+++ b/Source/WebCore/layout/formattingContexts/inline/InlineTextItem.cpp
@@ -88,6 +88,14 @@
     return !m_length || (m_length == 1 && inlineTextBox().content()[start()] == zeroWidthSpace); 
 }
 
+bool InlineTextItem::isCollapsibleNonBreakingSpace() const
+{
+    if (style().nbspMode() != NBSPMode::Space)
+        return false;
+    // Note that this text item may be longer than just one character.
+    return m_length && inlineTextBox().content()[start()] == noBreakSpace;
+}
+
 bool InlineTextItem::shouldPreserveSpacesAndTabs(const InlineTextItem& inlineTextItem)
 {
     ASSERT(inlineTextItem.isWhitespace());
diff --git a/Source/WebCore/layout/formattingContexts/inline/InlineTextItem.h b/Source/WebCore/layout/formattingContexts/inline/InlineTextItem.h
index 6918457..5c9382f 100644
--- a/Source/WebCore/layout/formattingContexts/inline/InlineTextItem.h
+++ b/Source/WebCore/layout/formattingContexts/inline/InlineTextItem.h
@@ -50,6 +50,7 @@
     bool isWhitespace() const { return m_textItemType == TextItemType::Whitespace; }
     bool isWordSeparator() const { return m_isWordSeparator; }
     bool isZeroWidthSpaceSeparator() const;
+    bool isCollapsibleNonBreakingSpace() const;
     bool hasTrailingSoftHyphen() const { return m_hasTrailingSoftHyphen; }
     std::optional<InlineLayoutUnit> width() const { return m_hasWidth ? std::make_optional(m_width) : std::optional<InlineLayoutUnit> { }; }