Bug 1646296 - part 8: Make stack only classes to group start/end boundary information of `WSRunScanner` r=m_kato
authorMasayuki Nakano <masayuki@d-toybox.com>
Mon, 22 Jun 2020 14:48:15 +0000
changeset 536699 135eb1ba22b62890eda413f6092c9ec60e8c4520
parent 536698 cc56a4cc04156b0c58944eb32ddf4f9592d2c0cd
child 536700 be987c4c9dd724285e22eb1e6b6521ad156d8e1e
push id37532
push userabutkovits@mozilla.com
push dateTue, 23 Jun 2020 16:15:06 +0000
treeherdermozilla-central@3add3a174755 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersm_kato
bugs1646296
milestone79.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1646296 - part 8: Make stack only classes to group start/end boundary information of `WSRunScanner` r=m_kato Differential Revision: https://phabricator.services.mozilla.com/D79973
editor/libeditor/WSRunObject.cpp
editor/libeditor/WSRunObject.h
--- a/editor/libeditor/WSRunObject.cpp
+++ b/editor/libeditor/WSRunObject.cpp
@@ -69,25 +69,19 @@ template WSScanResult WSRunScanner::Scan
 template <typename PT, typename CT>
 WSRunScanner::WSRunScanner(const HTMLEditor* aHTMLEditor,
                            const EditorDOMPointBase<PT, CT>& aScanStartPoint,
                            const EditorDOMPointBase<PT, CT>& aScanEndPoint)
     : mScanStartPoint(aScanStartPoint),
       mScanEndPoint(aScanEndPoint),
       mEditingHost(aHTMLEditor->GetActiveEditingHost()),
       mPRE(false),
-      mStartOffset(0),
-      mEndOffset(0),
-      mFirstNBSPOffset(0),
-      mLastNBSPOffset(0),
       mStartRun(nullptr),
       mEndRun(nullptr),
-      mHTMLEditor(aHTMLEditor),
-      mStartReason(WSType::NotInitialized),
-      mEndReason(WSType::NotInitialized) {
+      mHTMLEditor(aHTMLEditor) {
   MOZ_ASSERT(
       *nsContentUtils::ComparePoints(aScanStartPoint.ToRawRangeBoundary(),
                                      aScanEndPoint.ToRawRangeBoundary()) <= 0);
   DebugOnly<nsresult> rvIgnored = GetWSNodes();
   NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
                        "WSRunScanner::GetWSNodes() failed, but ignored");
   GetRuns();
 }
@@ -419,18 +413,18 @@ nsresult WSRunObject::InsertText(Documen
         if (atNextChar.IsSet() && !atNextChar.IsEndOfContainer() &&
             atNextChar.IsCharASCIISpace()) {
           theString.SetCharAt(kNBSP, lastCharIndex);
         }
       }
     } else if (afterRunObject.EndsByBlockBoundary()) {
       // When afterRun is null, it means that mScanEndPoint is last point in
       // editing host or editing block.
-      // If this text insertion replaces composition, this.mEndReason is
-      // start position of compositon. So we have to use afterRunObject's
+      // If this text insertion replaces composition, this.mEnd.mReason is
+      // start position of composition. So we have to use afterRunObject's
       // reason instead.
       theString.SetCharAt(kNBSP, lastCharIndex);
     }
   }
 
   // Next, scan string for adjacent ws and convert to nbsp/space combos
   // MOOSE: don't need to convert tabs here since that is done by
   // WillInsertText() before we are called.  Eventually, all that logic will be
@@ -630,22 +624,21 @@ WSScanResult WSRunScanner::ScanPreviousV
             atPreviousChar.IsCharASCIISpace() || atPreviousChar.IsCharNBSP()
                 ? WSType::NormalWhiteSpaces
                 : WSType::NormalText);
       }
       // If no text node, keep looking.  We should eventually fall out of loop
     }
   }
 
-  if (mStartReasonContent != mStartNode) {
-    // In this case, mStartOffset is not meaningful.
-    return WSScanResult(mStartReasonContent, mStartReason);
+  if (mStart.GetReasonContent() != mStart.PointRef().GetContainer()) {
+    // In this case, mStart.PointRef().Offset() is not meaningful.
+    return WSScanResult(mStart.GetReasonContent(), mStart.RawReason());
   }
-  return WSScanResult(EditorDOMPoint(mStartReasonContent, mStartOffset),
-                      mStartReason);
+  return WSScanResult(mStart.PointRef(), mStart.RawReason());
 }
 
 template <typename PT, typename CT>
 WSScanResult WSRunScanner::ScanNextVisibleNodeOrBlockBoundaryFrom(
     const EditorDOMPointBase<PT, CT>& aPoint) const {
   // Find first visible thing after the point.  Position
   // outVisNode/outVisOffset just _before_ that thing.  If we don't find
   // anything return end of ws.
@@ -666,29 +659,28 @@ WSScanResult WSRunScanner::ScanNextVisib
                     (atNextChar.IsCharASCIISpace() || atNextChar.IsCharNBSP())
                 ? WSType::NormalWhiteSpaces
                 : WSType::NormalText);
       }
       // If no text node, keep looking.  We should eventually fall out of loop
     }
   }
 
-  if (mEndReasonContent != mEndNode) {
-    // In this case, mEndOffset is not meaningful.
-    return WSScanResult(mEndReasonContent, mEndReason);
+  if (mEnd.GetReasonContent() != mEnd.PointRef().GetContainer()) {
+    // In this case, mEnd.PointRef().Offset() is not meaningful.
+    return WSScanResult(mEnd.GetReasonContent(), mEnd.RawReason());
   }
-  return WSScanResult(EditorDOMPoint(mEndReasonContent, mEndOffset),
-                      mEndReason);
+  return WSScanResult(mEnd.PointRef(), mEnd.RawReason());
 }
 
 nsresult WSRunObject::AdjustWhiteSpace() {
   // this routine examines a run of ws and tries to get rid of some unneeded
-  // nbsp's, replacing them with regualr ascii space if possible.  Keeping
+  // nbsp's, replacing them with regular ascii space if possible.  Keeping
   // things simple for now and just trying to fix up the trailing ws in the run.
-  if (!mLastNBSPNode) {
+  if (!mNBSPData.FoundNBSP()) {
     // nothing to do!
     return NS_OK;
   }
   for (WSFragment* run = mStartRun; run; run = run->mRight) {
     if (!run->IsVisibleAndMiddleOfHardLine()) {
       continue;
     }
     nsresult rv = NormalizeWhiteSpacesAtEndOf(*run);
@@ -765,29 +757,24 @@ bool WSRunScanner::InitializeRangeStartW
   for (uint32_t i = std::min(aPoint.Offset(), textFragment.GetLength()); i;
        i--) {
     char16_t ch = textFragment.CharAt(i - 1);
     if (nsCRT::IsAsciiSpace(ch)) {
       continue;
     }
 
     if (ch == HTMLEditUtils::kNBSP) {
-      mFirstNBSPNode = aPoint.ContainerAsText();
-      mFirstNBSPOffset = i - 1;
-      if (!mLastNBSPNode) {
-        mLastNBSPNode = aPoint.ContainerAsText();
-        mLastNBSPOffset = i - 1;
-      }
+      mNBSPData.NotifyNBSP(
+          EditorDOMPointInText(aPoint.ContainerAsText(), i - 1),
+          NoBreakingSpaceData::Scanning::Backward);
       continue;
     }
 
-    mStartNode = aPoint.ContainerAsText();
-    mStartOffset = i;
-    mStartReason = WSType::NormalText;
-    mStartReasonContent = aPoint.ContainerAsText();
+    mStart = BoundaryData(EditorDOMPoint(aPoint.ContainerAsText(), i),
+                          *aPoint.ContainerAsText(), WSType::NormalText);
     return true;
   }
 
   return false;
 }
 
 template <typename EditorDOMPointType>
 void WSRunScanner::InitializeRangeStart(
@@ -810,44 +797,41 @@ void WSRunScanner::InitializeRangeStart(
   // we haven't found the start of ws yet.  Keep looking
   nsIContent* previousLeafContentOrBlock =
       HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
           aPoint, aEditableBlockParentOrTopmostEditableInlineContent,
           mEditingHost);
   if (!previousLeafContentOrBlock) {
     // no prior node means we exhausted
     // aEditableBlockParentOrTopmostEditableInlineContent
-    mStartNode = aPoint.GetContainer();
-    mStartOffset = aPoint.Offset();
-    mStartReason = WSType::CurrentBlockBoundary;
-    // mStartReasonContent can be either a block element or any non-editable
+    // mStart.mReasonContent can be either a block element or any non-editable
     // content in this case.
-    mStartReasonContent = const_cast<nsIContent*>(
-        &aEditableBlockParentOrTopmostEditableInlineContent);
+    mStart =
+        BoundaryData(aPoint,
+                     const_cast<nsIContent&>(
+                         aEditableBlockParentOrTopmostEditableInlineContent),
+                     WSType::CurrentBlockBoundary);
     return;
   }
 
   if (HTMLEditUtils::IsBlockElement(*previousLeafContentOrBlock)) {
-    mStartNode = aPoint.GetContainer();
-    mStartOffset = aPoint.Offset();
-    mStartReason = WSType::OtherBlockBoundary;
-    mStartReasonContent = previousLeafContentOrBlock;
+    mStart = BoundaryData(aPoint, *previousLeafContentOrBlock,
+                          WSType::OtherBlockBoundary);
     return;
   }
 
   if (!previousLeafContentOrBlock->IsText() ||
       !previousLeafContentOrBlock->IsEditable()) {
     // it's a break or a special node, like <img>, that is not a block and
     // not a break but still serves as a terminator to ws runs.
-    mStartNode = aPoint.GetContainer();
-    mStartOffset = aPoint.Offset();
-    mStartReason = previousLeafContentOrBlock->IsHTMLElement(nsGkAtoms::br)
-                       ? WSType::BRElement
-                       : WSType::SpecialContent;
-    mStartReasonContent = previousLeafContentOrBlock;
+    mStart =
+        BoundaryData(aPoint, *previousLeafContentOrBlock,
+                     previousLeafContentOrBlock->IsHTMLElement(nsGkAtoms::br)
+                         ? WSType::BRElement
+                         : WSType::SpecialContent);
     return;
   }
 
   if (!previousLeafContentOrBlock->AsText()->TextFragment().GetLength()) {
     // Zero length text node. Set start point to it
     // so we can get past it!
     InitializeRangeStart(
         EditorDOMPointInText(previousLeafContentOrBlock->AsText(), 0),
@@ -876,29 +860,23 @@ bool WSRunScanner::InitializeRangeEndWit
   const nsTextFragment& textFragment = aPoint.ContainerAsText()->TextFragment();
   for (uint32_t i = aPoint.Offset(); i < textFragment.GetLength(); i++) {
     char16_t ch = textFragment.CharAt(i);
     if (nsCRT::IsAsciiSpace(ch)) {
       continue;
     }
 
     if (ch == HTMLEditUtils::kNBSP) {
-      mLastNBSPNode = aPoint.ContainerAsText();
-      mLastNBSPOffset = i;
-      if (!mFirstNBSPNode) {
-        mFirstNBSPNode = aPoint.ContainerAsText();
-        mFirstNBSPOffset = i;
-      }
+      mNBSPData.NotifyNBSP(EditorDOMPointInText(aPoint.ContainerAsText(), i),
+                           NoBreakingSpaceData::Scanning::Forward);
       continue;
     }
 
-    mEndNode = aPoint.ContainerAsText();
-    mEndOffset = i;
-    mEndReason = WSType::NormalText;
-    mEndReasonContent = aPoint.ContainerAsText();
+    mEnd = BoundaryData(EditorDOMPoint(aPoint.ContainerAsText(), i),
+                        *aPoint.ContainerAsText(), WSType::NormalText);
     return true;
   }
 
   return false;
 }
 
 template <typename EditorDOMPointType>
 void WSRunScanner::InitializeRangeEnd(
@@ -920,46 +898,41 @@ void WSRunScanner::InitializeRangeEnd(
   // we haven't found the end of ws yet.  Keep looking
   nsIContent* nextLeafContentOrBlock =
       HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
           aPoint, aEditableBlockParentOrTopmostEditableInlineContent,
           mEditingHost);
   if (!nextLeafContentOrBlock) {
     // no next node means we exhausted
     // aEditableBlockParentOrTopmostEditableInlineContent
-    mEndNode = aPoint.GetContainer();
-    mEndOffset = aPoint.Offset();
-    mEndReason = WSType::CurrentBlockBoundary;
-    // mEndReasonContent can be either a block element or any non-editable
+    // mEnd.mReasonContent can be either a block element or any non-editable
     // content in this case.
-    mEndReasonContent = const_cast<nsIContent*>(
-        &aEditableBlockParentOrTopmostEditableInlineContent);
+    mEnd = BoundaryData(aPoint,
+                        const_cast<nsIContent&>(
+                            aEditableBlockParentOrTopmostEditableInlineContent),
+                        WSType::CurrentBlockBoundary);
     return;
   }
 
   if (HTMLEditUtils::IsBlockElement(*nextLeafContentOrBlock)) {
     // we encountered a new block.  therefore no more ws.
-    mEndNode = aPoint.GetContainer();
-    mEndOffset = aPoint.Offset();
-    mEndReason = WSType::OtherBlockBoundary;
-    mEndReasonContent = nextLeafContentOrBlock;
+    mEnd = BoundaryData(aPoint, *nextLeafContentOrBlock,
+                        WSType::OtherBlockBoundary);
     return;
   }
 
   if (!nextLeafContentOrBlock->IsText() ||
       !nextLeafContentOrBlock->IsEditable()) {
     // we encountered a break or a special node, like <img>,
     // that is not a block and not a break but still
     // serves as a terminator to ws runs.
-    mEndNode = aPoint.GetContainer();
-    mEndOffset = aPoint.Offset();
-    mEndReason = nextLeafContentOrBlock->IsHTMLElement(nsGkAtoms::br)
-                     ? WSType::BRElement
-                     : WSType::SpecialContent;
-    mEndReasonContent = nextLeafContentOrBlock;
+    mEnd = BoundaryData(aPoint, *nextLeafContentOrBlock,
+                        nextLeafContentOrBlock->IsHTMLElement(nsGkAtoms::br)
+                            ? WSType::BRElement
+                            : WSType::SpecialContent);
     return;
   }
 
   if (!nextLeafContentOrBlock->AsText()->TextFragment().GetLength()) {
     // Zero length text node. Set end point to it
     // so we can get past it!
     InitializeRangeEnd(
         EditorDOMPointInText(nextLeafContentOrBlock->AsText(), 0),
@@ -997,109 +970,133 @@ void WSRunScanner::GetRuns() {
     InitializeWithSingleFragment(WSFragment::Visible::Yes,
                                  WSFragment::StartOfHardLine::No,
                                  WSFragment::EndOfHardLine::No);
     return;
   }
 
   // if we are before or after a block (or after a break), and there are no
   // nbsp's, then it's all non-rendering ws.
-  if (!mFirstNBSPNode && !mLastNBSPNode &&
+  if (!mNBSPData.FoundNBSP() &&
       (StartsFromHardLineBreak() || EndsByBlockBoundary())) {
     InitializeWithSingleFragment(
         WSFragment::Visible::No,
         StartsFromHardLineBreak() ? WSFragment::StartOfHardLine::Yes
                                   : WSFragment::StartOfHardLine::No,
         EndsByBlockBoundary() ? WSFragment::EndOfHardLine::Yes
                               : WSFragment::EndOfHardLine::No);
     return;
   }
 
   // otherwise a little trickier.  shucks.
   mStartRun = new WSFragment();
-  mStartRun->mStartNode = mStartNode;
-  mStartRun->mStartOffset = mStartOffset;
+  if (mStart.PointRef().IsSet()) {
+    mStartRun->mStartNode = mStart.PointRef().GetContainer();
+    mStartRun->mStartOffset = mStart.PointRef().Offset();
+  }
 
   if (StartsFromHardLineBreak()) {
     // set up mStartRun
     mStartRun->MarkAsStartOfHardLine();
-    mStartRun->mEndNode = mFirstNBSPNode;
-    mStartRun->mEndOffset = mFirstNBSPOffset;
-    mStartRun->SetStartFrom(mStartReason);
+    if (mNBSPData.FirstPointRef().IsSet()) {
+      mStartRun->mEndNode = mNBSPData.FirstPointRef().GetContainer();
+      mStartRun->mEndOffset = mNBSPData.FirstPointRef().Offset();
+    }
+    mStartRun->SetStartFrom(mStart.RawReason());
     mStartRun->SetEndByNormalWiteSpaces();
 
     // set up next run
     WSFragment* normalRun = new WSFragment();
     mStartRun->mRight = normalRun;
     normalRun->MarkAsVisible();
-    normalRun->mStartNode = mFirstNBSPNode;
-    normalRun->mStartOffset = mFirstNBSPOffset;
+    if (mNBSPData.FirstPointRef().IsSet()) {
+      normalRun->mStartNode = mNBSPData.FirstPointRef().GetContainer();
+      normalRun->mStartOffset = mNBSPData.FirstPointRef().Offset();
+    }
     normalRun->SetStartFromLeadingWhiteSpaces();
     normalRun->mLeft = mStartRun;
     if (!EndsByBlockBoundary()) {
       // then no trailing ws.  this normal run ends the overall ws run.
-      normalRun->SetEndBy(mEndReason);
-      normalRun->mEndNode = mEndNode;
-      normalRun->mEndOffset = mEndOffset;
+      normalRun->SetEndBy(mEnd.RawReason());
+      if (mEnd.PointRef().IsSet()) {
+        normalRun->mEndNode = mEnd.PointRef().GetContainer();
+        normalRun->mEndOffset = mEnd.PointRef().Offset();
+      }
       mEndRun = normalRun;
     } else {
       // we might have trailing ws.
       // it so happens that *if* there is an nbsp at end,
       // {mEndNode,mEndOffset-1} will point to it, even though in general
       // start/end points not guaranteed to be in text nodes.
-      if (mLastNBSPNode == mEndNode && mLastNBSPOffset == mEndOffset - 1) {
+      if (mNBSPData.LastPointRef().IsSet() && mEnd.PointRef().IsSet() &&
+          mNBSPData.LastPointRef().GetContainer() ==
+              mEnd.PointRef().GetContainer() &&
+          mNBSPData.LastPointRef().Offset() == mEnd.PointRef().Offset() - 1) {
         // normal ws runs right up to adjacent block (nbsp next to block)
-        normalRun->SetEndBy(mEndReason);
-        normalRun->mEndNode = mEndNode;
-        normalRun->mEndOffset = mEndOffset;
+        normalRun->SetEndBy(mEnd.RawReason());
+        normalRun->mEndNode = mEnd.PointRef().GetContainer();
+        normalRun->mEndOffset = mEnd.PointRef().Offset();
         mEndRun = normalRun;
       } else {
-        normalRun->mEndNode = mLastNBSPNode;
-        normalRun->mEndOffset = mLastNBSPOffset + 1;
+        if (mNBSPData.LastPointRef().IsSet()) {
+          normalRun->mEndNode = mNBSPData.LastPointRef().GetContainer();
+          normalRun->mEndOffset = mNBSPData.LastPointRef().Offset() + 1;
+        }
         normalRun->SetEndByTrailingWhiteSpaces();
 
         // set up next run
         WSFragment* lastRun = new WSFragment();
         lastRun->MarkAsEndOfHardLine();
-        lastRun->mStartNode = mLastNBSPNode;
-        lastRun->mStartOffset = mLastNBSPOffset + 1;
-        lastRun->mEndNode = mEndNode;
-        lastRun->mEndOffset = mEndOffset;
+        if (mNBSPData.LastPointRef().IsSet()) {
+          lastRun->mStartNode = mNBSPData.LastPointRef().GetContainer();
+          lastRun->mStartOffset = mNBSPData.LastPointRef().Offset() + 1;
+        }
+        if (mEnd.PointRef().IsSet()) {
+          lastRun->mEndNode = mEnd.PointRef().GetContainer();
+          lastRun->mEndOffset = mEnd.PointRef().Offset();
+        }
         lastRun->SetStartFromNormalWhiteSpaces();
         lastRun->mLeft = normalRun;
-        lastRun->SetEndBy(mEndReason);
+        lastRun->SetEndBy(mEnd.RawReason());
         mEndRun = lastRun;
         normalRun->mRight = lastRun;
       }
     }
   } else {
     MOZ_ASSERT(!StartsFromHardLineBreak());
     mStartRun->MarkAsVisible();
-    mStartRun->mEndNode = mLastNBSPNode;
-    mStartRun->mEndOffset = mLastNBSPOffset + 1;
-    mStartRun->SetStartFrom(mStartReason);
+    if (mNBSPData.LastPointRef().IsSet()) {
+      mStartRun->mEndNode = mNBSPData.LastPointRef().GetContainer();
+      mStartRun->mEndOffset = mNBSPData.LastPointRef().Offset() + 1;
+    }
+    mStartRun->SetStartFrom(mStart.RawReason());
 
     // we might have trailing ws.
     // it so happens that *if* there is an nbsp at end, {mEndNode,mEndOffset-1}
     // will point to it, even though in general start/end points not
     // guaranteed to be in text nodes.
-    if (mLastNBSPNode == mEndNode && mLastNBSPOffset == (mEndOffset - 1)) {
-      mStartRun->SetEndBy(mEndReason);
-      mStartRun->mEndNode = mEndNode;
-      mStartRun->mEndOffset = mEndOffset;
+    if (mNBSPData.LastPointRef().IsSet() && mEnd.PointRef().IsSet() &&
+        mNBSPData.LastPointRef().GetContainer() ==
+            mEnd.PointRef().GetContainer() &&
+        mNBSPData.LastPointRef().Offset() == mEnd.PointRef().Offset() - 1) {
+      mStartRun->SetEndBy(mEnd.RawReason());
+      mStartRun->mEndNode = mEnd.PointRef().GetContainer();
+      mStartRun->mEndOffset = mEnd.PointRef().Offset();
       mEndRun = mStartRun;
     } else {
       // set up next run
       WSFragment* lastRun = new WSFragment();
       lastRun->MarkAsEndOfHardLine();
-      lastRun->mStartNode = mLastNBSPNode;
-      lastRun->mStartOffset = mLastNBSPOffset + 1;
+      if (mNBSPData.LastPointRef().IsSet()) {
+        lastRun->mStartNode = mNBSPData.LastPointRef().GetContainer();
+        lastRun->mStartOffset = mNBSPData.LastPointRef().Offset() + 1;
+      }
       lastRun->SetStartFromNormalWhiteSpaces();
       lastRun->mLeft = mStartRun;
-      lastRun->SetEndBy(mEndReason);
+      lastRun->SetEndBy(mEnd.RawReason());
       mEndRun = lastRun;
       mStartRun->mRight = lastRun;
       mStartRun->SetEndByTrailingWhiteSpaces();
     }
   }
 }
 
 void WSRunScanner::ClearRuns() {
@@ -1118,31 +1115,35 @@ void WSRunScanner::InitializeWithSingleF
     WSFragment::Visible aIsVisible,
     WSFragment::StartOfHardLine aIsStartOfHardLine,
     WSFragment::EndOfHardLine aIsEndOfHardLine) {
   MOZ_ASSERT(!mStartRun);
   MOZ_ASSERT(!mEndRun);
 
   mStartRun = new WSFragment();
 
-  mStartRun->mStartNode = mStartNode;
-  mStartRun->mStartOffset = mStartOffset;
+  if (mStart.PointRef().IsSet()) {
+    mStartRun->mStartNode = mStart.PointRef().GetContainer();
+    mStartRun->mStartOffset = mStart.PointRef().Offset();
+  }
   if (aIsVisible == WSFragment::Visible::Yes) {
     mStartRun->MarkAsVisible();
   }
   if (aIsStartOfHardLine == WSFragment::StartOfHardLine::Yes) {
     mStartRun->MarkAsStartOfHardLine();
   }
   if (aIsEndOfHardLine == WSFragment::EndOfHardLine::Yes) {
     mStartRun->MarkAsEndOfHardLine();
   }
-  mStartRun->mEndNode = mEndNode;
-  mStartRun->mEndOffset = mEndOffset;
-  mStartRun->SetStartFrom(mStartReason);
-  mStartRun->SetEndBy(mEndReason);
+  if (mEnd.PointRef().IsSet()) {
+    mStartRun->mEndNode = mEnd.PointRef().GetContainer();
+    mStartRun->mEndOffset = mEnd.PointRef().Offset();
+  }
+  mStartRun->SetStartFrom(mStart.RawReason());
+  mStartRun->SetEndBy(mEnd.RawReason());
 
   mEndRun = mStartRun;
 }
 
 nsresult WSRunObject::PrepareToDeleteRangePriv(WSRunObject* aEndObject) {
   // this routine adjust white-space before *this* and after aEndObject
   // in preperation for the two areas to become adjacent after the
   // intervening content is deleted.  It's overly agressive right
@@ -1348,17 +1349,17 @@ EditorDOMPointInText WSRunScanner::GetIn
   // If it points a character in a text node, return it.
   // XXX For the performance, this does not check whether the container
   //     is outside of our range.
   if (point.IsInTextNode() && point.GetContainer()->IsEditable() &&
       !point.IsEndOfContainer()) {
     return EditorDOMPointInText(point.ContainerAsText(), point.Offset());
   }
 
-  if (point.GetContainer() == mEndReasonContent) {
+  if (point.GetContainer() == mEnd.GetReasonContent()) {
     return EditorDOMPointInText();
   }
 
   nsIContent* editableBlockParentOrTopmotEditableInlineContent =
       GetEditableBlockParentOrTopmotEditableInlineContent(
           mScanStartPoint.ContainerAsContent());
   if (NS_WARN_IF(!editableBlockParentOrTopmotEditableInlineContent)) {
     // Meaning that the container of `mScanStartPoint` is not editable.
@@ -1370,17 +1371,17 @@ EditorDOMPointInText WSRunScanner::GetIn
            HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
                *point.ContainerAsContent(),
                *editableBlockParentOrTopmotEditableInlineContent, mEditingHost);
        nextContent;
        nextContent = HTMLEditUtils::GetNextLeafContentOrNextBlockElement(
            *nextContent, *editableBlockParentOrTopmotEditableInlineContent,
            mEditingHost)) {
     if (!nextContent->IsText() || !nextContent->IsEditable()) {
-      if (nextContent == mEndReasonContent) {
+      if (nextContent == mEnd.GetReasonContent()) {
         break;  // Reached end of current runs.
       }
       continue;
     }
     return EditorDOMPointInText(nextContent->AsText(), 0);
   }
   return EditorDOMPointInText();
 }
@@ -1416,17 +1417,17 @@ EditorDOMPointInText WSRunScanner::GetPr
   // in it, return its previous point.
   // XXX For the performance, this does not check whether the container
   //     is outside of our range.
   if (point.IsInTextNode() && point.GetContainer()->IsEditable() &&
       !point.IsStartOfContainer()) {
     return EditorDOMPointInText(point.ContainerAsText(), point.Offset() - 1);
   }
 
-  if (point.GetContainer() == mStartReasonContent) {
+  if (point.GetContainer() == mStart.GetReasonContent()) {
     return EditorDOMPointInText();
   }
 
   nsIContent* editableBlockParentOrTopmotEditableInlineContent =
       GetEditableBlockParentOrTopmotEditableInlineContent(
           mScanStartPoint.ContainerAsContent());
   if (NS_WARN_IF(!editableBlockParentOrTopmotEditableInlineContent)) {
     // Meaning that the container of `mScanStartPoint` is not editable.
@@ -1440,17 +1441,17 @@ EditorDOMPointInText WSRunScanner::GetPr
                *editableBlockParentOrTopmotEditableInlineContent, mEditingHost);
        previousContent;
        previousContent =
            HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
                *previousContent,
                *editableBlockParentOrTopmotEditableInlineContent,
                mEditingHost)) {
     if (!previousContent->IsText() || !previousContent->IsEditable()) {
-      if (previousContent == mStartReasonContent) {
+      if (previousContent == mStart.GetReasonContent()) {
         break;  // Reached start of current runs.
       }
       continue;
     }
     return EditorDOMPointInText(
         previousContent->AsText(),
         previousContent->AsText()->TextLength()
             ? previousContent->AsText()->TextLength() - 1
--- a/editor/libeditor/WSRunObject.h
+++ b/editor/libeditor/WSRunObject.h
@@ -374,72 +374,52 @@ class MOZ_STACK_CLASS WSRunScanner {
    * was found by scanning from mScanStartPoint backward or mScanEndPoint
    * forward.  If there was white-spaces or text from the point, returns the
    * text node.  Otherwise, returns an element which is explained by the
    * following methods.  Note that when the reason is
    * WSType::CurrentBlockBoundary, In most cases, it's current block element
    * which is editable, but also may be non-element and/or non-editable.  See
    * MOZ_ASSERT_IF()s in WSScanResult::AssertIfInvalidData() for the detail.
    */
-  nsIContent* GetStartReasonContent() const { return mStartReasonContent; }
-  nsIContent* GetEndReasonContent() const { return mEndReasonContent; }
-
-  bool StartsFromNormalText() const {
-    return mStartReason == WSType::NormalText;
+  nsIContent* GetStartReasonContent() const {
+    return mStart.GetReasonContent();
   }
-  bool StartsFromSpecialContent() const {
-    return mStartReason == WSType::SpecialContent;
-  }
-  bool StartsFromBRElement() const { return mStartReason == WSType::BRElement; }
+  nsIContent* GetEndReasonContent() const { return mEnd.GetReasonContent(); }
+
+  bool StartsFromNormalText() const { return mStart.IsNormalText(); }
+  bool StartsFromSpecialContent() const { return mStart.IsSpecialContent(); }
+  bool StartsFromBRElement() const { return mStart.IsBRElement(); }
   bool StartsFromCurrentBlockBoundary() const {
-    return mStartReason == WSType::CurrentBlockBoundary;
+    return mStart.IsCurrentBlockBoundary();
   }
   bool StartsFromOtherBlockElement() const {
-    return mStartReason == WSType::OtherBlockBoundary;
-  }
-  bool StartsFromBlockBoundary() const {
-    return mStartReason == WSType::CurrentBlockBoundary ||
-           mStartReason == WSType::OtherBlockBoundary;
-  }
-  bool StartsFromHardLineBreak() const {
-    return mStartReason == WSType::CurrentBlockBoundary ||
-           mStartReason == WSType::OtherBlockBoundary ||
-           mStartReason == WSType::BRElement;
+    return mStart.IsOtherBlockBoundary();
   }
-  bool EndsByNormalText() const { return mEndReason == WSType::NormalText; }
-  bool EndsBySpecialContent() const {
-    return mEndReason == WSType::SpecialContent;
-  }
-  bool EndsByBRElement() const { return mEndReason == WSType::BRElement; }
+  bool StartsFromBlockBoundary() const { return mStart.IsBlockBoundary(); }
+  bool StartsFromHardLineBreak() const { return mStart.IsHardLineBreak(); }
+  bool EndsByNormalText() const { return mEnd.IsNormalText(); }
+  bool EndsBySpecialContent() const { return mEnd.IsSpecialContent(); }
+  bool EndsByBRElement() const { return mEnd.IsBRElement(); }
   bool EndsByCurrentBlockBoundary() const {
-    return mEndReason == WSType::CurrentBlockBoundary;
+    return mEnd.IsCurrentBlockBoundary();
   }
-  bool EndsByOtherBlockElement() const {
-    return mEndReason == WSType::OtherBlockBoundary;
-  }
-  bool EndsByBlockBoundary() const {
-    return mEndReason == WSType::CurrentBlockBoundary ||
-           mEndReason == WSType::OtherBlockBoundary;
-  }
+  bool EndsByOtherBlockElement() const { return mEnd.IsOtherBlockBoundary(); }
+  bool EndsByBlockBoundary() const { return mEnd.IsBlockBoundary(); }
 
   MOZ_NEVER_INLINE_DEBUG dom::Element* StartReasonOtherBlockElementPtr() const {
-    MOZ_DIAGNOSTIC_ASSERT(mStartReasonContent->IsElement());
-    return mStartReasonContent->AsElement();
+    return mStart.OtherBlockElementPtr();
   }
   MOZ_NEVER_INLINE_DEBUG dom::HTMLBRElement* StartReasonBRElementPtr() const {
-    MOZ_DIAGNOSTIC_ASSERT(mStartReasonContent->IsHTMLElement(nsGkAtoms::br));
-    return static_cast<dom::HTMLBRElement*>(mStartReasonContent.get());
+    return mStart.BRElementPtr();
   }
   MOZ_NEVER_INLINE_DEBUG dom::Element* EndReasonOtherBlockElementPtr() const {
-    MOZ_DIAGNOSTIC_ASSERT(mEndReasonContent->IsElement());
-    return mEndReasonContent->AsElement();
+    return mEnd.OtherBlockElementPtr();
   }
   MOZ_NEVER_INLINE_DEBUG dom::HTMLBRElement* EndReasonBRElementPtr() const {
-    MOZ_DIAGNOSTIC_ASSERT(mEndReasonContent->IsHTMLElement(nsGkAtoms::br));
-    return static_cast<dom::HTMLBRElement*>(mEndReasonContent.get());
+    return mEnd.BRElementPtr();
   }
 
   /**
    * Active editing host when this instance is created.
    */
   dom::Element* GetEditingHost() const { return mEditingHost; }
 
  protected:
@@ -575,28 +555,28 @@ class MOZ_STACK_CLASS WSRunScanner {
   template <typename PT, typename CT>
   const WSFragment* FindNearestFragment(
       const EditorDOMPointBase<PT, CT>& aPoint, bool aForward) const;
 
   /**
    * GetInclusiveNextEditableCharPoint() returns aPoint if it points a character
    * in an editable text node, or start of next editable text node otherwise.
    * FYI: For the performance, this does not check whether given container
-   *      is not after mStartReasonContent or not.
+   *      is not after mStart.mReasonContent or not.
    */
   template <typename PT, typename CT>
   EditorDOMPointInText GetInclusiveNextEditableCharPoint(
       const EditorDOMPointBase<PT, CT>& aPoint) const;
 
   /**
    * GetPreviousEditableCharPoint() returns previous editable point in a
    * text node.  Note that this returns last character point when it meets
    * non-empty text node, otherwise, returns a point in an empty text node.
    * FYI: For the performance, this does not check whether given container
-   *      is not before mEndReasonContent or not.
+   *      is not before mEnd.mReasonContent or not.
    */
   template <typename PT, typename CT>
   EditorDOMPointInText GetPreviousEditableCharPoint(
       const EditorDOMPointBase<PT, CT>& aPoint) const;
 
   /**
    * GetEndOfCollapsibleASCIIWhiteSpaces() returns the next visible char
    * (meaning a character except ASCII white-spaces) point or end of last text
@@ -627,16 +607,93 @@ class MOZ_STACK_CLASS WSRunScanner {
   nsIContent* GetEditableBlockParentOrTopmotEditableInlineContent(
       nsIContent* aContent) const;
 
   EditorDOMPointInText GetPreviousCharPointFromPointInText(
       const EditorDOMPointInText& aPoint) const;
 
   char16_t GetCharAt(dom::Text* aTextNode, int32_t aOffset) const;
 
+  class MOZ_STACK_CLASS BoundaryData final {
+   public:
+    BoundaryData() : mReason(WSType::NotInitialized) {}
+    template <typename EditorDOMPointType>
+    BoundaryData(const EditorDOMPointType& aPoint, nsIContent& aReasonContent,
+                 WSType aReason)
+        : mReasonContent(&aReasonContent), mPoint(aPoint), mReason(aReason) {}
+    bool Initialized() const { return mReasonContent && mPoint.IsSet(); }
+
+    nsIContent* GetReasonContent() const { return mReasonContent; }
+    const EditorDOMPoint& PointRef() const { return mPoint; }
+    WSType RawReason() const { return mReason; }
+
+    bool IsNormalText() const { return mReason == WSType::NormalText; }
+    bool IsSpecialContent() const { return mReason == WSType::SpecialContent; }
+    bool IsBRElement() const { return mReason == WSType::BRElement; }
+    bool IsCurrentBlockBoundary() const {
+      return mReason == WSType::CurrentBlockBoundary;
+    }
+    bool IsOtherBlockBoundary() const {
+      return mReason == WSType::OtherBlockBoundary;
+    }
+    bool IsBlockBoundary() const {
+      return mReason == WSType::CurrentBlockBoundary ||
+             mReason == WSType::OtherBlockBoundary;
+    }
+    bool IsHardLineBreak() const {
+      return mReason == WSType::CurrentBlockBoundary ||
+             mReason == WSType::OtherBlockBoundary ||
+             mReason == WSType::BRElement;
+    }
+    MOZ_NEVER_INLINE_DEBUG dom::Element* OtherBlockElementPtr() const {
+      MOZ_DIAGNOSTIC_ASSERT(mReasonContent->IsElement());
+      return mReasonContent->AsElement();
+    }
+    MOZ_NEVER_INLINE_DEBUG dom::HTMLBRElement* BRElementPtr() const {
+      MOZ_DIAGNOSTIC_ASSERT(mReasonContent->IsHTMLElement(nsGkAtoms::br));
+      return static_cast<dom::HTMLBRElement*>(mReasonContent.get());
+    }
+
+   private:
+    nsCOMPtr<nsIContent> mReasonContent;
+    EditorDOMPoint mPoint;
+    // Must be one of WSType::NotInitialized, WSType::NormalText,
+    // WSType::SpecialContent, WSType::BRElement, WSType::CurrentBlockBoundary
+    // or WSType::OtherBlockBoundary.
+    WSType mReason;
+  };
+
+  class MOZ_STACK_CLASS NoBreakingSpaceData final {
+   public:
+    enum class Scanning { Forward, Backward };
+    void NotifyNBSP(const EditorDOMPointInText& aPoint,
+                    Scanning aScanningDirection) {
+      MOZ_ASSERT(aPoint.IsSetAndValid());
+      MOZ_ASSERT(aPoint.IsCharNBSP());
+      if (!mFirst.IsSet() || aScanningDirection == Scanning::Backward) {
+        mFirst = aPoint;
+      }
+      if (!mLast.IsSet() || aScanningDirection == Scanning::Forward) {
+        mLast = aPoint;
+      }
+    }
+
+    const EditorDOMPointInText& FirstPointRef() const { return mFirst; }
+    const EditorDOMPointInText& LastPointRef() const { return mLast; }
+
+    bool FoundNBSP() const {
+      MOZ_ASSERT(mFirst.IsSet() == mLast.IsSet());
+      return mFirst.IsSet();
+    }
+
+   private:
+    EditorDOMPointInText mFirst;
+    EditorDOMPointInText mLast;
+  };
+
   void GetRuns();
   void ClearRuns();
   void InitializeWithSingleFragment(
       WSFragment::Visible aIsVisible,
       WSFragment::StartOfHardLine aIsStartOfHardLine,
       WSFragment::EndOfHardLine aIsEndOfHardLine);
   template <typename EditorDOMPointType>
   void InitializeRangeStart(
@@ -658,52 +715,29 @@ class MOZ_STACK_CLASS WSRunScanner {
   // info.
 
   // The editing host when the instance is created.
   RefPtr<dom::Element> mEditingHost;
 
   // true if we are in preformatted white-space context.
   bool mPRE;
 
-  // Node/offset where ws starts and ends.
-  nsCOMPtr<nsINode> mStartNode;
-  int32_t mStartOffset;
-
-  // Node/offset where ws ends.
-  nsCOMPtr<nsINode> mEndNode;
-  int32_t mEndOffset;
-
-  // Location of first nbsp in ws run, if any.
-  RefPtr<dom::Text> mFirstNBSPNode;
-  int32_t mFirstNBSPOffset;
-
-  // Location of last nbsp in ws run, if any.
-  RefPtr<dom::Text> mLastNBSPNode;
-  int32_t mLastNBSPOffset;
-
   // The first WSFragment in the run.
   WSFragment* mStartRun;
 
   // The last WSFragment in the run, may be same as first.
   WSFragment* mEndRun;
 
-  // See above comment for GetStartReasonContent() and GetEndReasonContent().
-  nsCOMPtr<nsIContent> mStartReasonContent;
-  nsCOMPtr<nsIContent> mEndReasonContent;
-
   // Non-owning.
   const HTMLEditor* mHTMLEditor;
 
- private:
-  // Must be one of WSType::NotInitialized, WSType::NormalText,
-  // WSType::SpecialContent, WSType::BRElement, WSType::CurrentBlockBoundary or
-  // WSType::OtherBlockBoundary.  Access these values with StartsFrom*() and
-  // EndsBy*() accessors.
-  WSType mStartReason;
-  WSType mEndReason;
+ protected:
+  BoundaryData mStart;
+  BoundaryData mEnd;
+  NoBreakingSpaceData mNBSPData;
 };
 
 class MOZ_STACK_CLASS WSRunObject final : public WSRunScanner {
  protected:
   typedef EditorBase::AutoTransactionsConserveSelection
       AutoTransactionsConserveSelection;
 
  public:
@@ -891,23 +925,16 @@ class MOZ_STACK_CLASS WSRunObject final 
    *                    character of next editable text node is checked.
    */
   [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
   MaybeReplaceInclusiveNextNBSPWithASCIIWhiteSpace(
       const WSFragment& aRun, const EditorDOMPoint& aPoint);
 
   MOZ_CAN_RUN_SCRIPT nsresult Scrub();
 
-  EditorDOMPoint StartPoint() const {
-    return EditorDOMPoint(mStartNode, mStartOffset);
-  }
-  EditorDOMPoint EndPoint() const {
-    return EditorDOMPoint(mEndNode, mEndOffset);
-  }
-
   // Because of MOZ_CAN_RUN_SCRIPT constructors, each instanciater of this class
   // guarantees the lifetime of the HTMLEditor.
   HTMLEditor& mHTMLEditor;
 };
 
 }  // namespace mozilla
 
 #endif  // #ifndef WSRunObject_h