From 5f45c5f18d9e0d64e0db6d69db0e104c9bc0c0e9 Mon Sep 17 00:00:00 2001 From: Jonathan Kew Date: Mon, 22 Jan 2024 12:57:54 +0000 Subject: [PATCH] Bug 1852478 - Convert CSS `white-space` into a shorthand that expands to `white-space-collapse` and `text-wrap-mode` longhands. r=firefox-style-system-reviewers,emilio Note that although this builds, it would (by itself) result in some test breakage; this is resolved in the following patches that build on this. Differential Revision: https://phabricator.services.mozilla.com/D198790 --- dom/base/use_counter_metrics.yaml | 82 ++++++++++++-- dom/html/HTMLPreElement.cpp | 7 +- dom/html/HTMLTableCellElement.cpp | 6 +- dom/html/HTMLTextAreaElement.cpp | 14 ++- editor/libeditor/EditorUtils.cpp | 11 +- editor/libeditor/EditorUtils.h | 8 +- editor/libeditor/HTMLEditorDeleteHandler.cpp | 91 ++++++++------- editor/libeditor/WSRunObject.cpp | 4 +- layout/generic/nsIFrame.cpp | 4 +- layout/generic/nsTextFrame.cpp | 34 +++--- layout/style/ServoBindings.toml | 3 +- layout/style/nsStyleConsts.h | 19 ++-- layout/style/nsStyleStruct.cpp | 7 +- layout/style/nsStyleStruct.h | 37 +++---- servo/components/style/properties/data.py | 11 +- .../longhands/inherited_text.mako.rs | 64 ++++------- .../shorthands/inherited_text.mako.rs | 104 ++++++++++++++++++ servo/ports/geckolib/glue.rs | 3 +- 18 files changed, 339 insertions(+), 170 deletions(-) diff --git a/dom/base/use_counter_metrics.yaml b/dom/base/use_counter_metrics.yaml index a09896938605..dd8a726ca869 100644 --- a/dom/base/use_counter_metrics.yaml +++ b/dom/base/use_counter_metrics.yaml @@ -131,7 +131,7 @@ use.counter.error: send_in_pings: - use-counters -# Total of 2299 use counter metrics (excludes denominators). +# Total of 2303 use counter metrics (excludes denominators). # Total of 356 'page' use counters. use.counter.page: svgsvgelement_getelementbyid: @@ -15667,7 +15667,7 @@ use.counter.deprecated_ops.doc: send_in_pings: - use-counters -# Total of 693 'CSS (page)' use counters. +# Total of 695 'CSS (page)' use counters. use.counter.css.page: css_align_content: type: counter @@ -17641,6 +17641,23 @@ use.counter.css.page: send_in_pings: - use-counters + css_text_wrap_mode: + type: counter + description: > + Whether a page used the CSS property text-wrap-mode. + Compare against `use.counter.top_level_content_documents_destroyed` + to calculate the rate. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + notification_emails: + - dom-core@mozilla.com + - emilio@mozilla.com + expires: never + send_in_pings: + - use-counters + css_touch_action: type: counter description: > @@ -17794,10 +17811,10 @@ use.counter.css.page: send_in_pings: - use-counters - css_white_space: + css_white_space_collapse: type: counter description: > - Whether a page used the CSS property white-space. + Whether a page used the CSS property white-space-collapse. Compare against `use.counter.top_level_content_documents_destroyed` to calculate the rate. bugs: @@ -23183,6 +23200,23 @@ use.counter.css.page: send_in_pings: - use-counters + css_white_space: + type: counter + description: > + Whether a page used the CSS property white-space. + Compare against `use.counter.top_level_content_documents_destroyed` + to calculate the rate. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + notification_emails: + - dom-core@mozilla.com + - emilio@mozilla.com + expires: never + send_in_pings: + - use-counters + css_webkit_text_stroke: type: counter description: > @@ -27450,7 +27484,7 @@ use.counter.css.page: send_in_pings: - use-counters -# Total of 693 'CSS (document)' use counters. +# Total of 695 'CSS (document)' use counters. use.counter.css.doc: css_align_content: type: counter @@ -29424,6 +29458,23 @@ use.counter.css.doc: send_in_pings: - use-counters + css_text_wrap_mode: + type: counter + description: > + Whether a document used the CSS property text-wrap-mode. + Compare against `use.counter.content_documents_destroyed` + to calculate the rate. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + notification_emails: + - dom-core@mozilla.com + - emilio@mozilla.com + expires: never + send_in_pings: + - use-counters + css_touch_action: type: counter description: > @@ -29577,10 +29628,10 @@ use.counter.css.doc: send_in_pings: - use-counters - css_white_space: + css_white_space_collapse: type: counter description: > - Whether a document used the CSS property white-space. + Whether a document used the CSS property white-space-collapse. Compare against `use.counter.content_documents_destroyed` to calculate the rate. bugs: @@ -34966,6 +35017,23 @@ use.counter.css.doc: send_in_pings: - use-counters + css_white_space: + type: counter + description: > + Whether a document used the CSS property white-space. + Compare against `use.counter.content_documents_destroyed` + to calculate the rate. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + notification_emails: + - dom-core@mozilla.com + - emilio@mozilla.com + expires: never + send_in_pings: + - use-counters + css_webkit_text_stroke: type: counter description: > diff --git a/dom/html/HTMLPreElement.cpp b/dom/html/HTMLPreElement.cpp index feaa764bd594..13628400d041 100644 --- a/dom/html/HTMLPreElement.cpp +++ b/dom/html/HTMLPreElement.cpp @@ -38,8 +38,11 @@ void HTMLPreElement::MapAttributesIntoRule( MappedDeclarationsBuilder& aBuilder) { // wrap: empty if (aBuilder.GetAttr(nsGkAtoms::wrap)) { - aBuilder.SetKeywordValue(eCSSProperty_white_space, - StyleWhiteSpace::PreWrap); + // Equivalent to expanding `white-space: pre-wrap` + aBuilder.SetKeywordValue(eCSSProperty_white_space_collapse, + StyleWhiteSpaceCollapse::Preserve); + aBuilder.SetKeywordValue(eCSSProperty_text_wrap_mode, + StyleTextWrapMode::Wrap); } nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); diff --git a/dom/html/HTMLTableCellElement.cpp b/dom/html/HTMLTableCellElement.cpp index 4c2e33cb3ac3..c260323a8bd8 100644 --- a/dom/html/HTMLTableCellElement.cpp +++ b/dom/html/HTMLTableCellElement.cpp @@ -163,7 +163,7 @@ void HTMLTableCellElement::MapAttributesIntoRule( MappedDeclarationsBuilder& aBuilder) { MapImageSizeAttributesInto(aBuilder); - if (!aBuilder.PropertyIsSet(eCSSProperty_white_space)) { + if (!aBuilder.PropertyIsSet(eCSSProperty_text_wrap_mode)) { // nowrap: enum if (aBuilder.GetAttr(nsGkAtoms::nowrap)) { // See if our width is not a nonzero integer width. @@ -171,8 +171,8 @@ void HTMLTableCellElement::MapAttributesIntoRule( nsCompatibility mode = aBuilder.Document().GetCompatibilityMode(); if (!value || value->Type() != nsAttrValue::eInteger || value->GetIntegerValue() == 0 || eCompatibility_NavQuirks != mode) { - aBuilder.SetKeywordValue(eCSSProperty_white_space, - StyleWhiteSpace::Nowrap); + aBuilder.SetKeywordValue(eCSSProperty_text_wrap_mode, + StyleTextWrapMode::Nowrap); } } } diff --git a/dom/html/HTMLTextAreaElement.cpp b/dom/html/HTMLTextAreaElement.cpp index 046655453859..afcb1f75d08b 100644 --- a/dom/html/HTMLTextAreaElement.cpp +++ b/dom/html/HTMLTextAreaElement.cpp @@ -365,12 +365,14 @@ bool HTMLTextAreaElement::ParseAttribute(int32_t aNamespaceID, void HTMLTextAreaElement::MapAttributesIntoRule( MappedDeclarationsBuilder& aBuilder) { // wrap=off - if (!aBuilder.PropertyIsSet(eCSSProperty_white_space)) { - const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::wrap); - if (value && value->Type() == nsAttrValue::eString && - value->Equals(nsGkAtoms::OFF, eIgnoreCase)) { - aBuilder.SetKeywordValue(eCSSProperty_white_space, StyleWhiteSpace::Pre); - } + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::wrap); + if (value && value->Type() == nsAttrValue::eString && + value->Equals(nsGkAtoms::OFF, eIgnoreCase)) { + // Equivalent to expanding `white-space; pre` + aBuilder.SetKeywordValue(eCSSProperty_white_space_collapse, + StyleWhiteSpaceCollapse::Preserve); + aBuilder.SetKeywordValue(eCSSProperty_text_wrap_mode, + StyleTextWrapMode::Nowrap); } nsGenericHTMLFormControlElementWithState::MapDivAlignAttributeInto(aBuilder); diff --git a/editor/libeditor/EditorUtils.cpp b/editor/libeditor/EditorUtils.cpp index c37f75047085..d9df9338e075 100644 --- a/editor/libeditor/EditorUtils.cpp +++ b/editor/libeditor/EditorUtils.cpp @@ -89,8 +89,8 @@ bool EditorUtils::IsDescendantOf(const nsINode& aNode, const nsINode& aParent, } // static -Maybe EditorUtils::GetComputedWhiteSpaceStyle( - const nsIContent& aContent) { +Maybe> +EditorUtils::GetComputedWhiteSpaceStyles(const nsIContent& aContent) { if (MOZ_UNLIKELY(!aContent.IsElement() && !aContent.GetParentElement())) { return Nothing(); } @@ -101,7 +101,9 @@ Maybe EditorUtils::GetComputedWhiteSpaceStyle( if (NS_WARN_IF(!elementStyle)) { return Nothing(); } - return Some(elementStyle->StyleText()->mWhiteSpace); + const auto* styleText = elementStyle->StyleText(); + return Some( + std::pair(styleText->mWhiteSpaceCollapse, styleText->mTextWrapMode)); } // static @@ -164,7 +166,8 @@ bool EditorUtils::IsOnlyNewLinePreformatted(const nsIContent& aContent) { return false; } - return elementStyle->StyleText()->mWhiteSpace == StyleWhiteSpace::PreLine; + return elementStyle->StyleText()->mWhiteSpaceCollapse == + StyleWhiteSpaceCollapse::PreserveBreaks; } // static diff --git a/editor/libeditor/EditorUtils.h b/editor/libeditor/EditorUtils.h index 80de25b53274..c6b08952dd4f 100644 --- a/editor/libeditor/EditorUtils.h +++ b/editor/libeditor/EditorUtils.h @@ -401,10 +401,10 @@ class EditorUtils final { } /** - * Get computed white-space style of aContent. + * Get the two longhands that make up computed white-space style of aContent. */ - static Maybe GetComputedWhiteSpaceStyle( - const nsIContent& aContent); + static Maybe> + GetComputedWhiteSpaceStyles(const nsIContent& aContent); /** * IsWhiteSpacePreformatted() checks the style info for the node for the @@ -421,7 +421,7 @@ class EditorUtils final { /** * IsOnlyNewLinePreformatted() checks whether the linefeed characters are * preformated but white-spaces are collapsed, or otherwise. I.e., this - * returns true only when `white-space:pre-line`. + * returns true only when `white-space-collapse:pre-line`. */ static bool IsOnlyNewLinePreformatted(const nsIContent& aContent); diff --git a/editor/libeditor/HTMLEditorDeleteHandler.cpp b/editor/libeditor/HTMLEditorDeleteHandler.cpp index 07738de6ee12..4bf3e4851374 100644 --- a/editor/libeditor/HTMLEditorDeleteHandler.cpp +++ b/editor/libeditor/HTMLEditorDeleteHandler.cpp @@ -5353,8 +5353,10 @@ HTMLEditor::AutoMoveOneLineHandler::ConsiderWhetherPreserveWhiteSpaceStyle( // If the content has different `white-space` style from
, we
     // shouldn't treat it as a descendant of 
 because web apps or
     // the user intent to treat the white-spaces in aContent not as `pre`.
-    if (EditorUtils::GetComputedWhiteSpaceStyle(aContent).valueOr(
-            StyleWhiteSpace::Normal) != StyleWhiteSpace::Pre) {
+    if (EditorUtils::GetComputedWhiteSpaceStyles(aContent).valueOr(std::pair(
+            StyleWhiteSpaceCollapse::Collapse, StyleTextWrapMode::Wrap)) !=
+        std::pair(StyleWhiteSpaceCollapse::Preserve,
+                  StyleTextWrapMode::Nowrap)) {
       return false;
     }
     for (const Element* element :
@@ -5801,52 +5803,65 @@ Result HTMLEditor::MoveNodeOrChildrenWithTransaction(
   MOZ_ASSERT(IsEditActionDataAvailable());
   MOZ_ASSERT(aPointToInsert.IsInContentNode());
 
-  const auto destWhiteSpaceStyle = [&]() -> Maybe {
+  const auto destWhiteSpaceStyles =
+      [&]() -> Maybe> {
     if (aPreserveWhiteSpaceStyle == PreserveWhiteSpaceStyle::No ||
         !aPointToInsert.IsInContentNode()) {
       return Nothing();
     }
-    auto style = EditorUtils::GetComputedWhiteSpaceStyle(
+    auto styles = EditorUtils::GetComputedWhiteSpaceStyles(
         *aPointToInsert.ContainerAs());
-    if (NS_WARN_IF(style.isSome() &&
-                   style.value() == StyleWhiteSpace::PreSpace)) {
+    if (NS_WARN_IF(styles.isSome() &&
+                   styles.value().first ==
+                       StyleWhiteSpaceCollapse::PreserveSpaces)) {
       return Nothing();
     }
-    return style;
+    return styles;
   }();
-  const auto srcWhiteSpaceStyle = [&]() -> Maybe {
+  const auto srcWhiteSpaceStyles =
+      [&]() -> Maybe> {
     if (aPreserveWhiteSpaceStyle == PreserveWhiteSpaceStyle::No) {
       return Nothing();
     }
-    auto style = EditorUtils::GetComputedWhiteSpaceStyle(aContentToMove);
-    if (NS_WARN_IF(style.isSome() &&
-                   style.value() == StyleWhiteSpace::PreSpace)) {
+    auto styles = EditorUtils::GetComputedWhiteSpaceStyles(aContentToMove);
+    if (NS_WARN_IF(styles.isSome() &&
+                   styles.value().first ==
+                       StyleWhiteSpaceCollapse::PreserveSpaces)) {
       return Nothing();
     }
-    return style;
+    return styles;
   }();
-  const auto GetWhiteSpaceStyleValue = [](StyleWhiteSpace aStyleWhiteSpace) {
-    switch (aStyleWhiteSpace) {
-      case StyleWhiteSpace::Normal:
-        return u"normal"_ns;
-      case StyleWhiteSpace::Pre:
-        return u"pre"_ns;
-      case StyleWhiteSpace::Nowrap:
-        return u"nowrap"_ns;
-      case StyleWhiteSpace::PreWrap:
-        return u"pre-wrap"_ns;
-      case StyleWhiteSpace::PreLine:
-        return u"pre-line"_ns;
-      case StyleWhiteSpace::BreakSpaces:
-        return u"break-spaces"_ns;
-      case StyleWhiteSpace::PreSpace:
-        MOZ_ASSERT_UNREACHABLE("Don't handle -moz-pre-space");
-        return u""_ns;
-      default:
-        MOZ_ASSERT_UNREACHABLE("Handle the new white-space value");
-        return u""_ns;
-    }
-  };
+  // Get the `white-space` shorthand form for the given collapse + mode pair.
+  const auto GetWhiteSpaceStyleValue =
+      [](std::pair aStyles) {
+        if (aStyles.second == StyleTextWrapMode::Wrap) {
+          switch (aStyles.first) {
+            case StyleWhiteSpaceCollapse::Collapse:
+              return u"normal"_ns;
+            case StyleWhiteSpaceCollapse::Preserve:
+              return u"pre-wrap"_ns;
+            case StyleWhiteSpaceCollapse::PreserveBreaks:
+              return u"pre-line"_ns;
+            case StyleWhiteSpaceCollapse::PreserveSpaces:
+              return u"preserve-spaces"_ns;
+            case StyleWhiteSpaceCollapse::BreakSpaces:
+              return u"break-spaces"_ns;
+          }
+        } else {
+          switch (aStyles.first) {
+            case StyleWhiteSpaceCollapse::Collapse:
+              return u"nowrap"_ns;
+            case StyleWhiteSpaceCollapse::Preserve:
+              return u"pre"_ns;
+            case StyleWhiteSpaceCollapse::PreserveBreaks:
+              return u"nowrap preserve-breaks"_ns;
+            case StyleWhiteSpaceCollapse::PreserveSpaces:
+              return u"nowrap preserve-spaces"_ns;
+            case StyleWhiteSpaceCollapse::BreakSpaces:
+              return u"nowrap break-spaces"_ns;
+          }
+        }
+      };
 
   if (aRemoveIfCommentNode == RemoveIfCommentNode::Yes &&
       aContentToMove.IsComment()) {
@@ -5872,15 +5887,15 @@ Result HTMLEditor::MoveNodeOrChildrenWithTransaction(
     // Preserve white-space in the new position with using `style` attribute.
     // This is additional path from point of view of our traditional behavior.
     // Therefore, ignore errors especially if we got unexpected DOM tree.
-    if (destWhiteSpaceStyle.isSome() && srcWhiteSpaceStyle.isSome() &&
-        destWhiteSpaceStyle.value() != srcWhiteSpaceStyle.value()) {
+    if (destWhiteSpaceStyles.isSome() && srcWhiteSpaceStyles.isSome() &&
+        destWhiteSpaceStyles.value() != srcWhiteSpaceStyles.value()) {
       // Set `white-space` with `style` attribute if it's nsStyledElement.
       if (nsStyledElement* styledElement =
               nsStyledElement::FromNode(&aContentToMove)) {
         DebugOnly rvIgnored =
             CSSEditUtils::SetCSSPropertyWithTransaction(
                 *this, MOZ_KnownLive(*styledElement), *nsGkAtoms::white_space,
-                GetWhiteSpaceStyleValue(srcWhiteSpaceStyle.value()));
+                GetWhiteSpaceStyleValue(srcWhiteSpaceStyles.value()));
         if (NS_WARN_IF(Destroyed())) {
           return Err(NS_ERROR_EDITOR_DESTROYED);
         }
@@ -5900,7 +5915,7 @@ Result HTMLEditor::MoveNodeOrChildrenWithTransaction(
         }
         nsAutoString styleAttrValue(u"white-space: "_ns);
         styleAttrValue.Append(
-            GetWhiteSpaceStyleValue(srcWhiteSpaceStyle.value()));
+            GetWhiteSpaceStyleValue(srcWhiteSpaceStyles.value()));
         IgnoredErrorResult error;
         newSpanElement->SetAttr(nsGkAtoms::style, styleAttrValue, error);
         NS_WARNING_ASSERTION(!error.Failed(),
diff --git a/editor/libeditor/WSRunObject.cpp b/editor/libeditor/WSRunObject.cpp
index 3136c468dec2..7149578be113 100644
--- a/editor/libeditor/WSRunObject.cpp
+++ b/editor/libeditor/WSRunObject.cpp
@@ -724,8 +724,8 @@ Result WhiteSpaceVisibilityKeeper::
       // a bug to manage only the change.
       (aLeftBlockElement.NodeInfo()->NameAtom() ==
            aRightBlockElement.NodeInfo()->NameAtom() &&
-       EditorUtils::GetComputedWhiteSpaceStyle(aLeftBlockElement) ==
-           EditorUtils::GetComputedWhiteSpaceStyle(aRightBlockElement))) {
+       EditorUtils::GetComputedWhiteSpaceStyles(aLeftBlockElement) ==
+           EditorUtils::GetComputedWhiteSpaceStyles(aRightBlockElement))) {
     // Nodes are same type.  merge them.
     EditorDOMPoint atFirstChildOfRightNode;
     nsresult rv = aHTMLEditor.JoinNearestEditableNodesWithTransaction(
diff --git a/layout/generic/nsIFrame.cpp b/layout/generic/nsIFrame.cpp
index a65f6921fb69..e3356d1be2fd 100644
--- a/layout/generic/nsIFrame.cpp
+++ b/layout/generic/nsIFrame.cpp
@@ -9109,8 +9109,8 @@ nsresult nsIFrame::PeekOffsetForWord(PeekOffsetStruct* aPos, int32_t aOffset) {
       // significant.
       if (next.mJumpedLine && wordSelectEatSpace &&
           current.mFrame->HasSignificantTerminalNewline() &&
-          current.mFrame->StyleText()->mWhiteSpace !=
-              StyleWhiteSpace::PreLine) {
+          current.mFrame->StyleText()->mWhiteSpaceCollapse !=
+              StyleWhiteSpaceCollapse::PreserveBreaks) {
         current.mOffset -= 1;
       }
       break;
diff --git a/layout/generic/nsTextFrame.cpp b/layout/generic/nsTextFrame.cpp
index 6ec63b8d2684..27ce8801a22f 100644
--- a/layout/generic/nsTextFrame.cpp
+++ b/layout/generic/nsTextFrame.cpp
@@ -781,7 +781,8 @@ static bool IsTrimmableSpace(const nsTextFragment* aFrag, uint32_t aPos,
              !IsSpaceCombiningSequenceTail(aFrag, aPos + 1);
     case '\n':
       return !aStyleText->NewlineIsSignificantStyle() &&
-             aStyleText->mWhiteSpace != mozilla::StyleWhiteSpace::PreSpace;
+             aStyleText->mWhiteSpaceCollapse !=
+                 StyleWhiteSpaceCollapse::PreserveSpaces;
     case '\t':
     case '\r':
     case '\f':
@@ -1171,27 +1172,23 @@ static bool TextContainsLineBreakerWhiteSpace(const void* aText,
 
 static nsTextFrameUtils::CompressionMode GetCSSWhitespaceToCompressionMode(
     nsTextFrame* aFrame, const nsStyleText* aStyleText) {
-  switch (aStyleText->mWhiteSpace) {
-    case StyleWhiteSpace::Normal:
-    case StyleWhiteSpace::Nowrap:
+  switch (aStyleText->mWhiteSpaceCollapse) {
+    case StyleWhiteSpaceCollapse::Collapse:
       return nsTextFrameUtils::COMPRESS_WHITESPACE_NEWLINE;
-    case StyleWhiteSpace::Pre:
-    case StyleWhiteSpace::PreWrap:
-    case StyleWhiteSpace::BreakSpaces:
+    case StyleWhiteSpaceCollapse::PreserveBreaks:
+      return nsTextFrameUtils::COMPRESS_WHITESPACE;
+    case StyleWhiteSpaceCollapse::Preserve:
+    case StyleWhiteSpaceCollapse::PreserveSpaces:
+    case StyleWhiteSpaceCollapse::BreakSpaces:
       if (!aStyleText->NewlineIsSignificant(aFrame)) {
         // If newline is set to be preserved, but then suppressed,
         // transform newline to space.
         return nsTextFrameUtils::COMPRESS_NONE_TRANSFORM_TO_SPACE;
       }
       return nsTextFrameUtils::COMPRESS_NONE;
-    case StyleWhiteSpace::PreSpace:
-      return nsTextFrameUtils::COMPRESS_NONE_TRANSFORM_TO_SPACE;
-    case StyleWhiteSpace::PreLine:
-      return nsTextFrameUtils::COMPRESS_WHITESPACE;
-    default:
-      MOZ_ASSERT_UNREACHABLE("Unknown white-space value");
-      return nsTextFrameUtils::COMPRESS_WHITESPACE_NEWLINE;
   }
+  MOZ_ASSERT_UNREACHABLE("Unknown white-space-collapse value");
+  return nsTextFrameUtils::COMPRESS_WHITESPACE_NEWLINE;
 }
 
 struct FrameTextTraversal {
@@ -9539,7 +9536,8 @@ void nsTextFrame::ReflowText(nsLineLayout& aLineLayout, nscoord aAvailableWidth,
   }
   bool canTrimTrailingWhitespace = !textStyle->WhiteSpaceIsSignificant() ||
                                    HasAnyStateBits(TEXT_IS_IN_TOKEN_MATHML);
-  bool isBreakSpaces = textStyle->mWhiteSpace == StyleWhiteSpace::BreakSpaces;
+  bool isBreakSpaces =
+      textStyle->mWhiteSpaceCollapse == StyleWhiteSpaceCollapse::BreakSpaces;
   // allow whitespace to overflow the container
   bool whitespaceCanHang = textStyle->WhiteSpaceCanHangOrVisuallyCollapse();
   gfxBreakPriority breakPriority = aLineLayout.LastOptionalBreakPriority();
@@ -10307,9 +10305,9 @@ bool nsTextFrame::IsEmpty() {
     return true;
   }
 
-  bool isEmpty =
-      IsAllWhitespace(TextFragment(), textStyle->mWhiteSpace !=
-                                          mozilla::StyleWhiteSpace::PreLine);
+  bool isEmpty = IsAllWhitespace(TextFragment(),
+                                 textStyle->mWhiteSpaceCollapse !=
+                                     StyleWhiteSpaceCollapse::PreserveBreaks);
   AddStateBits(isEmpty ? TEXT_IS_ONLY_WHITESPACE : TEXT_ISNOT_ONLY_WHITESPACE);
   return isEmpty;
 }
diff --git a/layout/style/ServoBindings.toml b/layout/style/ServoBindings.toml
index 2deafbfb14b6..23eabb98d8e3 100644
--- a/layout/style/ServoBindings.toml
+++ b/layout/style/ServoBindings.toml
@@ -137,7 +137,8 @@ rusty-enums = [
     "mozilla::StyleListStylePosition",
     "mozilla::StylePointerEvents",
     "mozilla::StyleScrollbarWidth",
-    "mozilla::StyleWhiteSpace",
+    "mozilla::StyleWhiteSpaceCollapse",
+    "mozilla::StyleTextWrapMode",
     "mozilla::StyleTextRendering",
     "mozilla::StyleFlexDirection",
     "mozilla::StyleStrokeLinecap",
diff --git a/layout/style/nsStyleConsts.h b/layout/style/nsStyleConsts.h
index 8152dfc9dea8..9888d6e73596 100644
--- a/layout/style/nsStyleConsts.h
+++ b/layout/style/nsStyleConsts.h
@@ -389,16 +389,21 @@ enum class StyleVisibility : uint8_t {
 };
 
 // See nsStyleText
-enum class StyleWhiteSpace : uint8_t {
-  Normal = 0,
-  Pre,
-  Nowrap,
-  PreWrap,
-  PreLine,
-  PreSpace,
+enum class StyleWhiteSpaceCollapse : uint8_t {
+  Collapse = 0,
+  // TODO: Discard not yet supported
+  Preserve,
+  PreserveBreaks,
+  PreserveSpaces,
   BreakSpaces,
 };
 
+// See nsStyleText
+enum class StyleTextWrapMode : uint8_t {
+  Wrap = 0,
+  Nowrap,
+};
+
 // See nsStyleText
 // TODO: this will become StyleTextWrapStyle when we turn text-wrap
 // (see https://bugzilla.mozilla.org/show_bug.cgi?id=1758391) and
diff --git a/layout/style/nsStyleStruct.cpp b/layout/style/nsStyleStruct.cpp
index d18c7e14c013..07798f55d86c 100644
--- a/layout/style/nsStyleStruct.cpp
+++ b/layout/style/nsStyleStruct.cpp
@@ -2791,7 +2791,6 @@ nsStyleText::nsStyleText(const Document& aDocument)
       mTextAlign(StyleTextAlign::Start),
       mTextAlignLast(StyleTextAlignLast::Auto),
       mTextJustify(StyleTextJustify::Auto),
-      mWhiteSpace(StyleWhiteSpace::Normal),
       mHyphens(StyleHyphens::Manual),
       mRubyAlign(StyleRubyAlign::SpaceAround),
       mRubyPosition(StyleRubyPosition::AlternateOver),
@@ -2828,7 +2827,8 @@ nsStyleText::nsStyleText(const nsStyleText& aSource)
       mTextAlign(aSource.mTextAlign),
       mTextAlignLast(aSource.mTextAlignLast),
       mTextJustify(aSource.mTextJustify),
-      mWhiteSpace(aSource.mWhiteSpace),
+      mWhiteSpaceCollapse(aSource.mWhiteSpaceCollapse),
+      mTextWrapMode(aSource.mTextWrapMode),
       mLineBreak(aSource.mLineBreak),
       mWordBreak(aSource.mWordBreak),
       mOverflowWrap(aSource.mOverflowWrap),
@@ -2875,7 +2875,8 @@ nsChangeHint nsStyleText::CalcDifference(const nsStyleText& aNewData) const {
   if ((mTextAlign != aNewData.mTextAlign) ||
       (mTextAlignLast != aNewData.mTextAlignLast) ||
       (mTextTransform != aNewData.mTextTransform) ||
-      (mWhiteSpace != aNewData.mWhiteSpace) ||
+      (mWhiteSpaceCollapse != aNewData.mWhiteSpaceCollapse) ||
+      (mTextWrapMode != aNewData.mTextWrapMode) ||
       (mLineBreak != aNewData.mLineBreak) ||
       (mWordBreak != aNewData.mWordBreak) ||
       (mOverflowWrap != aNewData.mOverflowWrap) ||
diff --git a/layout/style/nsStyleStruct.h b/layout/style/nsStyleStruct.h
index 6bd14e505c9a..8c1895a3dbd4 100644
--- a/layout/style/nsStyleStruct.h
+++ b/layout/style/nsStyleStruct.h
@@ -845,7 +845,9 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleText {
   mozilla::StyleTextAlign mTextAlign;
   mozilla::StyleTextAlignLast mTextAlignLast;
   mozilla::StyleTextJustify mTextJustify;
-  mozilla::StyleWhiteSpace mWhiteSpace;
+  mozilla::StyleWhiteSpaceCollapse mWhiteSpaceCollapse =
+      mozilla::StyleWhiteSpaceCollapse::Collapse;
+  mozilla::StyleTextWrapMode mTextWrapMode = mozilla::StyleTextWrapMode::Wrap;
   mozilla::StyleLineBreak mLineBreak = mozilla::StyleLineBreak::Auto;
 
  private:
@@ -918,10 +920,9 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleText {
   }
 
   bool WhiteSpaceIsSignificant() const {
-    return mWhiteSpace == mozilla::StyleWhiteSpace::Pre ||
-           mWhiteSpace == mozilla::StyleWhiteSpace::PreWrap ||
-           mWhiteSpace == mozilla::StyleWhiteSpace::BreakSpaces ||
-           mWhiteSpace == mozilla::StyleWhiteSpace::PreSpace;
+    return mWhiteSpaceCollapse != mozilla::StyleWhiteSpaceCollapse::Collapse &&
+           mWhiteSpaceCollapse !=
+               mozilla::StyleWhiteSpaceCollapse::PreserveBreaks;
   }
 
   bool WhiteSpaceCanHangOrVisuallyCollapse() const {
@@ -930,35 +931,27 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleText {
     //       WhiteSpaceCanWrapStyle() &&
     //       WhiteSpaceIsSignificant()
     // which simplifies to:
-    return mWhiteSpace == mozilla::StyleWhiteSpace::PreWrap;
+    return mTextWrapMode == mozilla::StyleTextWrapMode::Wrap &&
+           mWhiteSpaceCollapse != mozilla::StyleWhiteSpaceCollapse::BreakSpaces;
   }
 
   bool NewlineIsSignificantStyle() const {
-    return mWhiteSpace == mozilla::StyleWhiteSpace::Pre ||
-           mWhiteSpace == mozilla::StyleWhiteSpace::PreWrap ||
-           mWhiteSpace == mozilla::StyleWhiteSpace::BreakSpaces ||
-           mWhiteSpace == mozilla::StyleWhiteSpace::PreLine;
+    return mWhiteSpaceCollapse == mozilla::StyleWhiteSpaceCollapse::Preserve ||
+           mWhiteSpaceCollapse ==
+               mozilla::StyleWhiteSpaceCollapse::PreserveBreaks ||
+           mWhiteSpaceCollapse == mozilla::StyleWhiteSpaceCollapse::BreakSpaces;
   }
 
   bool WhiteSpaceOrNewlineIsSignificant() const {
-    return mWhiteSpace == mozilla::StyleWhiteSpace::Pre ||
-           mWhiteSpace == mozilla::StyleWhiteSpace::PreWrap ||
-           mWhiteSpace == mozilla::StyleWhiteSpace::BreakSpaces ||
-           mWhiteSpace == mozilla::StyleWhiteSpace::PreLine ||
-           mWhiteSpace == mozilla::StyleWhiteSpace::PreSpace;
+    return NewlineIsSignificantStyle() || WhiteSpaceIsSignificant();
   }
 
   bool TabIsSignificant() const {
-    return mWhiteSpace == mozilla::StyleWhiteSpace::Pre ||
-           mWhiteSpace == mozilla::StyleWhiteSpace::PreWrap ||
-           mWhiteSpace == mozilla::StyleWhiteSpace::BreakSpaces;
+    return !WhiteSpaceCanWrapStyle() && WhiteSpaceIsSignificant();
   }
 
   bool WhiteSpaceCanWrapStyle() const {
-    return mWhiteSpace == mozilla::StyleWhiteSpace::Normal ||
-           mWhiteSpace == mozilla::StyleWhiteSpace::PreWrap ||
-           mWhiteSpace == mozilla::StyleWhiteSpace::BreakSpaces ||
-           mWhiteSpace == mozilla::StyleWhiteSpace::PreLine;
+    return mTextWrapMode == mozilla::StyleTextWrapMode::Wrap;
   }
 
   bool WordCanWrapStyle() const {
diff --git a/servo/components/style/properties/data.py b/servo/components/style/properties/data.py
index e4c0a79ab60b..098518d9fabb 100644
--- a/servo/components/style/properties/data.py
+++ b/servo/components/style/properties/data.py
@@ -893,8 +893,8 @@ def _remove_common_first_line_and_first_letter_properties(props, engine):
     props.remove("overflow-wrap")
     props.remove("text-align")
     props.remove("text-justify")
-    props.remove("white-space")
-    props.remove("text-wrap")
+    props.remove("white-space-collapse")
+    props.remove("text-wrap-mode")
     props.remove("word-break")
     props.remove("text-indent")
 
@@ -996,11 +996,12 @@ class PropertyRestrictions:
     def placeholder(data):
         props = PropertyRestrictions.first_line(data)
         props.add("opacity")
-        props.add("white-space")
         props.add("text-wrap")
         props.add("text-overflow")
         props.add("text-align")
         props.add("text-justify")
+        for p in PropertyRestrictions.shorthand(data, "white-space"):
+            props.add(p)
         return props
 
     # https://drafts.csswg.org/css-pseudo/#marker-pseudo
@@ -1008,7 +1009,6 @@ class PropertyRestrictions:
     def marker(data):
         return set(
             [
-                "white-space",
                 "text-wrap",
                 "color",
                 "text-combine-upright",
@@ -1019,6 +1019,7 @@ class PropertyRestrictions:
                 "line-height",
                 "-moz-osx-font-smoothing",
             ]
+            + PropertyRestrictions.shorthand(data, "white-space")
             + PropertyRestrictions.spec(data, "css-fonts")
             + PropertyRestrictions.spec(data, "css-animations")
             + PropertyRestrictions.spec(data, "css-transitions")
@@ -1033,7 +1034,6 @@ class PropertyRestrictions:
                 "opacity",
                 "visibility",
                 "text-shadow",
-                "white-space",
                 "text-wrap",
                 "text-combine-upright",
                 "ruby-position",
@@ -1045,6 +1045,7 @@ class PropertyRestrictions:
                 "background-blend-mode",
             ]
             + PropertyRestrictions.shorthand(data, "text-decoration")
+            + PropertyRestrictions.shorthand(data, "white-space")
             + PropertyRestrictions.shorthand(data, "background")
             + PropertyRestrictions.shorthand(data, "outline")
             + PropertyRestrictions.shorthand(data, "font")
diff --git a/servo/components/style/properties/longhands/inherited_text.mako.rs b/servo/components/style/properties/longhands/inherited_text.mako.rs
index 00543e0c849e..284183f98056 100644
--- a/servo/components/style/properties/longhands/inherited_text.mako.rs
+++ b/servo/components/style/properties/longhands/inherited_text.mako.rs
@@ -149,52 +149,26 @@ ${helpers.predefined_type(
     affects="layout",
 )}
 
-<%helpers:single_keyword
-    name="white-space"
-    values="normal pre nowrap pre-wrap pre-line"
-    engines="gecko servo-2013 servo-2020",
-    extra_gecko_values="break-spaces -moz-pre-space"
-    gecko_enum_prefix="StyleWhiteSpace"
-    needs_conversion="True"
-    animation_value_type="discrete"
-    spec="https://drafts.csswg.org/css-text/#propdef-white-space"
-    servo_restyle_damage="rebuild_and_reflow"
-    affects="layout"
->
-    % if engine in ["servo-2013", "servo-2020"]:
-    impl SpecifiedValue {
-        pub fn allow_wrap(&self) -> bool {
-            match *self {
-                SpecifiedValue::Nowrap |
-                SpecifiedValue::Pre => false,
-                SpecifiedValue::Normal |
-                SpecifiedValue::PreWrap |
-                SpecifiedValue::PreLine => true,
-            }
-        }
+// TODO: `white-space-collapse: discard` not yet supported
+${helpers.single_keyword(
+    name="white-space-collapse",
+    values="collapse preserve preserve-breaks preserve-spaces break-spaces",
+    engines="gecko",
+    gecko_enum_prefix="StyleWhiteSpaceCollapse",
+    animation_value_type="discrete",
+    spec="https://drafts.csswg.org/css-text-4/#propdef-white-space-collapse",
+    affects="layout",
+)}
 
-        pub fn preserve_newlines(&self) -> bool {
-            match *self {
-                SpecifiedValue::Normal |
-                SpecifiedValue::Nowrap => false,
-                SpecifiedValue::Pre |
-                SpecifiedValue::PreWrap |
-                SpecifiedValue::PreLine => true,
-            }
-        }
-
-        pub fn preserve_spaces(&self) -> bool {
-            match *self {
-                SpecifiedValue::Normal |
-                SpecifiedValue::Nowrap |
-                SpecifiedValue::PreLine => false,
-                SpecifiedValue::Pre |
-                SpecifiedValue::PreWrap => true,
-            }
-        }
-    }
-    % endif
-
+${helpers.single_keyword(
+    name="text-wrap-mode",
+    values="wrap nowrap",
+    engines="gecko",
+    gecko_enum_prefix="StyleTextWrapMode",
+    animation_value_type="discrete",
+    spec="https://drafts.csswg.org/css-text-4/#propdef-text-wrap-mode",
+    affects="layout",
+)}
 
 ${helpers.predefined_type(
     "text-shadow",
diff --git a/servo/components/style/properties/shorthands/inherited_text.mako.rs b/servo/components/style/properties/shorthands/inherited_text.mako.rs
index 9eb278da05ca..5711e9b09e58 100644
--- a/servo/components/style/properties/shorthands/inherited_text.mako.rs
+++ b/servo/components/style/properties/shorthands/inherited_text.mako.rs
@@ -46,6 +46,110 @@
     }
 
 
+<%helpers:shorthand
+    name="white-space"
+    engines="gecko"
+    sub_properties="text-wrap-mode white-space-collapse"
+    spec="https://www.w3.org/TR/css-text-4/#white-space-property"
+>
+    use crate::properties::longhands::{text_wrap_mode, white_space_collapse};
+
+    pub fn parse_value<'i, 't>(
+        context: &ParserContext,
+        input: &mut Parser<'i, 't>,
+    ) -> Result> {
+        use white_space_collapse::computed_value::T as Collapse;
+        use text_wrap_mode::computed_value::T as Wrap;
+
+        fn parse_special_shorthands<'i, 't>(input: &mut Parser<'i, 't>) -> Result> {
+            let (mode, collapse) = try_match_ident_ignore_ascii_case! { input,
+                "normal" => (Wrap::Wrap, Collapse::Collapse),
+                "pre" => (Wrap::Nowrap, Collapse::Preserve),
+                "pre-wrap" => (Wrap::Wrap, Collapse::Preserve),
+                "pre-line" => (Wrap::Wrap, Collapse::PreserveBreaks),
+                // TODO: deprecate/remove -moz-pre-space; the white-space-collapse: preserve-spaces value
+                // should serve this purpose?
+                "-moz-pre-space" => (Wrap::Wrap, Collapse::PreserveSpaces),
+            };
+            Ok(expanded! {
+                text_wrap_mode: mode,
+                white_space_collapse: collapse,
+            })
+        }
+
+        if let Ok(result) = input.try_parse(parse_special_shorthands) {
+            return Ok(result);
+        }
+
+        let mut wrap = None;
+        let mut collapse = None;
+
+        loop {
+            if wrap.is_none() {
+                if let Ok(value) = input.try_parse(|input| text_wrap_mode::parse(context, input)) {
+                    wrap = Some(value);
+                    continue
+                }
+            }
+            if collapse.is_none() {
+                if let Ok(value) = input.try_parse(|input| white_space_collapse::parse(context, input)) {
+                    collapse = Some(value);
+                    continue
+                }
+            }
+            break
+        }
+
+        if wrap.is_some() || collapse.is_some() {
+            Ok(expanded! {
+                text_wrap_mode: unwrap_or_initial!(text_wrap_mode, wrap),
+                white_space_collapse: unwrap_or_initial!(white_space_collapse, collapse),
+            })
+        } else {
+            Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+        }
+    }
+
+    impl<'a> ToCss for LonghandsToSerialize<'a>  {
+        fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: fmt::Write {
+            use white_space_collapse::computed_value::T as Collapse;
+            use text_wrap_mode::computed_value::T as Wrap;
+
+            match *self.text_wrap_mode {
+                Wrap::Wrap => {
+                    match *self.white_space_collapse {
+                        Collapse::Collapse => return dest.write_str("normal"),
+                        Collapse::Preserve => return dest.write_str("pre-wrap"),
+                        Collapse::PreserveBreaks => return dest.write_str("pre-line"),
+                        Collapse::PreserveSpaces => return dest.write_str("-moz-pre-space"),
+                        _ => (),
+                    }
+                },
+                Wrap::Nowrap => {
+                    if let Collapse::Preserve = *self.white_space_collapse {
+                        return dest.write_str("pre");
+                    }
+                },
+            }
+
+            let mut has_value = false;
+            if *self.white_space_collapse != Collapse::Collapse {
+                self.white_space_collapse.to_css(dest)?;
+                has_value = true;
+            }
+
+            if *self.text_wrap_mode != Wrap::Wrap {
+                if has_value {
+                    dest.write_char(' ')?;
+                }
+                self.text_wrap_mode.to_css(dest)?;
+            }
+
+            Ok(())
+        }
+    }
+
+
 // CSS Compatibility
 // https://compat.spec.whatwg.org/
 <%helpers:shorthand name="-webkit-text-stroke"
diff --git a/servo/ports/geckolib/glue.rs b/servo/ports/geckolib/glue.rs
index 1eab502a8fea..d36e1057810d 100644
--- a/servo/ports/geckolib/glue.rs
+++ b/servo/ports/geckolib/glue.rs
@@ -5315,7 +5315,8 @@ pub extern "C" fn Servo_DeclarationBlock_SetKeywordValue(
         ListStyleType => Box::new(longhands::list_style_type::SpecifiedValue::from_gecko_keyword(value)),
         MathStyle => longhands::math_style::SpecifiedValue::from_gecko_keyword(value),
         MozMathVariant => longhands::_moz_math_variant::SpecifiedValue::from_gecko_keyword(value),
-        WhiteSpace => longhands::white_space::SpecifiedValue::from_gecko_keyword(value),
+        WhiteSpaceCollapse => get_from_computed::(value),
+        TextWrapMode => get_from_computed::(value),
         CaptionSide => get_from_computed::(value),
         BorderTopStyle => get_from_computed::(value),
         BorderRightStyle => get_from_computed::(value),