/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef WSRunScanner_h
#define WSRunScanner_h
#include "EditorBase.h"
#include "EditorForwards.h"
#include "EditorDOMPoint.h" // for EditorDOMPoint
#include "EditorLineBreak.h" // for EditorLineBreakBase
#include "HTMLEditor.h"
#include "HTMLEditUtils.h"
#include "mozilla/Assertions.h"
#include "mozilla/Maybe.h"
#include "mozilla/Result.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/HTMLBRElement.h"
#include "mozilla/dom/Text.h"
#include "nsCOMPtr.h"
#include "nsIContent.h"
namespace mozilla {
/**
* WSScanResult is result of ScanNextVisibleNodeOrBlockBoundaryFrom(),
* ScanPreviousVisibleNodeOrBlockBoundaryFrom(), and their static wrapper
* methods. This will have information of found visible content (and its
* position) or reached block element or topmost editable content at the
* start of scanner.
*/
class MOZ_STACK_CLASS WSScanResult final {
private:
using Element = dom::Element;
using HTMLBRElement = dom::HTMLBRElement;
using Text = dom::Text;
enum class WSType : uint8_t {
NotInitialized,
// Could be the DOM tree is broken as like crash tests.
UnexpectedError,
// The scanner cannot work in uncomposed tree, but tried to scan in it.
InUncomposedDoc,
// The run is maybe collapsible white-spaces at start of a hard line.
LeadingWhiteSpaces,
// The run is maybe collapsible white-spaces at end of a hard line.
TrailingWhiteSpaces,
// Collapsible, but visible white-spaces.
CollapsibleWhiteSpaces,
// Visible characters except collapsible white-spaces.
NonCollapsibleCharacters,
// Empty inline container elemnet such as ``. Note that it may
// be visible if its border/padding is not 0 for example.
// NOTE: This won't be used if it's the inline editing host at the scan
// start point.
EmptyInlineContainerElement,
// Special content such as ``, etc.
SpecialContent,
// element.
BRElement,
// A linefeed which is preformatted.
PreformattedLineBreak,
// Other block's boundary (child block of current block, maybe).
OtherBlockBoundary,
// Current block's boundary.
CurrentBlockBoundary,
// Inline editing host boundary.
InlineEditingHostBoundary,
};
friend std::ostream& operator<<(std::ostream& aStream, const WSType& aType) {
switch (aType) {
case WSType::NotInitialized:
return aStream << "WSType::NotInitialized";
case WSType::UnexpectedError:
return aStream << "WSType::UnexpectedError";
case WSType::InUncomposedDoc:
return aStream << "WSType::InUncomposedDoc";
case WSType::LeadingWhiteSpaces:
return aStream << "WSType::LeadingWhiteSpaces";
case WSType::TrailingWhiteSpaces:
return aStream << "WSType::TrailingWhiteSpaces";
case WSType::CollapsibleWhiteSpaces:
return aStream << "WSType::CollapsibleWhiteSpaces";
case WSType::NonCollapsibleCharacters:
return aStream << "WSType::NonCollapsibleCharacters";
case WSType::EmptyInlineContainerElement:
return aStream << "WSType::EmptyInlineContainerElement";
case WSType::SpecialContent:
return aStream << "WSType::SpecialContent";
case WSType::BRElement:
return aStream << "WSType::BRElement";
case WSType::PreformattedLineBreak:
return aStream << "WSType::PreformattedLineBreak";
case WSType::OtherBlockBoundary:
return aStream << "WSType::OtherBlockBoundary";
case WSType::CurrentBlockBoundary:
return aStream << "WSType::CurrentBlockBoundary";
case WSType::InlineEditingHostBoundary:
return aStream << "WSType::InlineEditingHostBoundary";
}
return aStream << "";
}
friend class WSRunScanner; // Because of WSType.
explicit WSScanResult(WSType aReason) : mReason(aReason) {
MOZ_ASSERT(mReason == WSType::UnexpectedError ||
mReason == WSType::NotInitialized);
}
public:
WSScanResult() = delete;
enum class ScanDirection : bool { Backward, Forward };
WSScanResult(const WSRunScanner& aScanner, ScanDirection aScanDirection,
nsIContent& aContent, WSType aReason)
: mContent(&aContent), mReason(aReason), mDirection(aScanDirection) {
MOZ_ASSERT(aReason != WSType::CollapsibleWhiteSpaces &&
aReason != WSType::NonCollapsibleCharacters &&
aReason != WSType::PreformattedLineBreak);
AssertIfInvalidData(aScanner);
MaybeSetEditingHost(aScanner);
}
WSScanResult(const WSRunScanner& aScanner, ScanDirection aScanDirection,
const EditorDOMPoint& aPoint, WSType aReason)
: mContent(aPoint.GetContainerAs()),
mOffset(Some(aPoint.Offset())),
mReason(aReason),
mDirection(aScanDirection) {
AssertIfInvalidData(aScanner);
MaybeSetEditingHost(aScanner);
}
WSScanResult(WSScanResult&& aResult, EditorLineBreak&& aIgnoredLineBreak,
const Element& aEditingHost)
: WSScanResult(std::forward(aResult)) {
MOZ_ASSERT(ReachedBlockBoundary());
mIgnoredLineBreak.emplace(std::forward(aIgnoredLineBreak));
// If an editing host is specified and the reached content is outside the
// editing host, we should store the editing host to make the method
// returning the reached point to return a point in the editing host.
Element* const contentEditingHost = mContent->GetEditingHost();
if (contentEditingHost && contentEditingHost != &aEditingHost) {
mEditingHost = const_cast(&aEditingHost);
}
}
static WSScanResult Error() { return WSScanResult(WSType::UnexpectedError); }
void AssertIfInvalidData(const WSRunScanner& aScanner) const;
bool Failed() const {
return mReason == WSType::NotInitialized ||
mReason == WSType::UnexpectedError;
}
/**
* GetContent() returns found visible and editable content/element.
* See MOZ_ASSERT_IF()s in AssertIfInvalidData() for the detail.
*/
nsIContent* GetContent() const { return mContent; }
[[nodiscard]] bool ContentIsElement() const {
return mContent && mContent->IsElement();
}
[[nodiscard]] bool ContentIsText() const {
return mContent && mContent->IsText();
}
/**
* The following accessors makes it easier to understand each callers.
*/
MOZ_NEVER_INLINE_DEBUG Element* ElementPtr() const {
MOZ_DIAGNOSTIC_ASSERT(mContent->IsElement());
return mContent->AsElement();
}
MOZ_NEVER_INLINE_DEBUG HTMLBRElement* BRElementPtr() const {
MOZ_DIAGNOSTIC_ASSERT(mContent->IsHTMLElement(nsGkAtoms::br));
return static_cast(mContent.get());
}
MOZ_NEVER_INLINE_DEBUG Text* TextPtr() const {
MOZ_DIAGNOSTIC_ASSERT(mContent->IsText());
return mContent->AsText();
}
template
MOZ_NEVER_INLINE_DEBUG EditorLineBreakType CreateEditorLineBreak() const {
if (ReachedBRElement()) {
return EditorLineBreakType(*BRElementPtr());
}
if (ReachedPreformattedLineBreak()) {
MOZ_ASSERT_IF(mDirection == ScanDirection::Backward, *mOffset > 0);
return EditorLineBreakType(*TextPtr(),
mDirection == ScanDirection::Forward
? mOffset.valueOr(0)
: std::max(mOffset.valueOr(1), 1u) - 1);
}
MOZ_CRASH("Didn't reach a line break");
return EditorLineBreakType(*BRElementPtr());
}
/**
* Return true if found or reached content is editable.
*/
[[nodiscard]] bool ContentIsEditable() const {
return mContent && HTMLEditUtils::IsSimplyEditableNode(*mContent);
}
/**
* Return true if found or reached content is removable node.
*/
[[nodiscard]] bool ContentIsRemovable() const {
return mContent && HTMLEditUtils::IsRemovableNode(*mContent);
}
[[nodiscard]] bool ContentIsEditableRoot() const {
return ContentIsElement() &&
HTMLEditUtils::ElementIsEditableRoot(*ElementPtr());
}
[[nodiscard]] bool ReachedOutsideEditingHost() const {
return !!mEditingHost;
}
/**
* Offset_Deprecated() returns meaningful value only when
* InVisibleOrCollapsibleCharacters() returns true or the scanner reached to
* start or end of its scanning range and that is same as start or end
* container which are specified when the scanner is initialized. If it's
* result of scanning backward, this offset means the point of the found
* point. Otherwise, i.e., scanning forward, this offset means next point
* of the found point. E.g., if it reaches a collapsible white-space, this
* offset is at the first non-collapsible character after it.
*/
MOZ_NEVER_INLINE_DEBUG uint32_t Offset_Deprecated() const {
NS_ASSERTION(mOffset.isSome(), "Retrieved non-meaningful offset");
return mOffset.valueOr(0);
}
/**
* Point_Deprecated() returns the position in found visible node or reached
* block boundary. So, this returns meaningful point only when
* Offset_Deprecated() returns meaningful value.
*/
template
EditorDOMPointType Point_Deprecated() const {
NS_ASSERTION(mOffset.isSome(), "Retrieved non-meaningful point");
return EditorDOMPointType(mContent, mOffset.valueOr(0));
}
/**
* PointAtReachedContent() returns the position of found visible content or
* reached block element. Note that the returned point may be outside the
* editing host if WSRunScanner::Option::OnlyEditableNodes is not specified
* and the ancestor limiter is not specified or specified outside the editing
* host.
*/
template
EditorDOMPointType PointAtReachedContent() const {
MOZ_ASSERT(mContent);
switch (mReason) {
case WSType::CollapsibleWhiteSpaces:
case WSType::NonCollapsibleCharacters:
case WSType::PreformattedLineBreak:
MOZ_DIAGNOSTIC_ASSERT(mOffset.isSome());
return mDirection == ScanDirection::Forward
? EditorDOMPointType(mContent, mOffset.valueOr(0))
: EditorDOMPointType(mContent,
std::max(mOffset.valueOr(1), 1u) - 1);
default:
MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedCurrentBlockBoundary());
MOZ_ASSERT_IF(mContent == mEditingHost,
!ReachedInlineEditingHostBoundary());
return EditorDOMPointType(mContent);
}
}
/**
* Similar to PointAtReachedContent(), but return a boundary point of the
* editing host if the reached content is outside of the editing host.
*/
template
EditorDOMPointType PointAtReachedContentOrEditingHostBoundary() const {
if (mEditingHost) {
MOZ_ASSERT(mContent);
MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedCurrentBlockBoundary());
MOZ_ASSERT_IF(mContent == mEditingHost,
!ReachedInlineEditingHostBoundary());
return mDirection == ScanDirection::Forward
? EditorDOMPointType::AtEndOf(*mEditingHost)
: EditorDOMPointType(mEditingHost, 0u);
}
return PointAtReachedContent();
}
/**
* PointAfterReachedContent() returns the next position of found visible
* content or reached block element. Note that the returned point may be
* outside the editing host if WSRunScanner::Option::OnlyEditableNodes is not
* specified and the ancestor limiter is not specified or specified outside
* the editing host.
*/
template
EditorDOMPointType PointAfterReachedContent() const {
MOZ_ASSERT(mContent);
MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedCurrentBlockBoundary());
MOZ_ASSERT_IF(mContent == mEditingHost,
!ReachedInlineEditingHostBoundary());
return PointAtReachedContent()
.template NextPointOrAfterContainer();
}
/**
* Similar to PointAfterReachedContent(), but return a boundary point of the
* editing host if the reached content is outside of the editing host.
*/
template
EditorDOMPointType PointAfterReachedContentOrEditingHostBoundary() const {
if (mEditingHost) {
MOZ_ASSERT(mContent);
MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedCurrentBlockBoundary());
MOZ_ASSERT_IF(mContent == mEditingHost,
!ReachedInlineEditingHostBoundary());
return mDirection == ScanDirection::Forward
? EditorDOMPointType::AtEndOf(*mEditingHost)
: EditorDOMPointType(mEditingHost, 0u);
}
return PointAfterReachedContent();
}
/**
* Return the next position of found visible content node. So, this should
* not be used if it reached a visible character middle of a `Text`. Note that
* the returned point may be outside the editing host if
* WSRunScanner::Option::OnlyEditableNodes is not specified and the ancestor
* limiter is not specified or specified outside the editing host.
*/
template
EditorDOMPointType PointAfterReachedContentNode() const {
MOZ_ASSERT(mContent);
MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedCurrentBlockBoundary());
MOZ_ASSERT_IF(mContent == mEditingHost,
!ReachedInlineEditingHostBoundary());
return EditorDOMPointType::After(*mContent);
}
/**
* Similar to PointAfterReachedContentNode(), but return a boundary point of
* the editing host if the reached content is outside of the editing host.
*/
template
EditorDOMPointType PointAfterReachedContentNodeOrEditingHostBoundary() const {
MOZ_ASSERT(mContent);
if (mEditingHost) {
MOZ_ASSERT_IF(mContent == mEditingHost, !ReachedCurrentBlockBoundary());
MOZ_ASSERT_IF(mContent == mEditingHost,
!ReachedInlineEditingHostBoundary());
return mDirection == ScanDirection::Forward
? EditorDOMPointType::AtEndOf(*mEditingHost)
: EditorDOMPointType(mEditingHost, 0u);
}
return PointAfterReachedContentNode();
}
/**
* Return the point of the reached block boundary. If reached current block
* boundary, return its inner boundary. Otherwise, its outer boundary.
*/
template
EditorDOMPointType PointAtReachedBlockBoundary() const {
MOZ_ASSERT(ReachedBlockBoundary());
if (mDirection == ScanDirection::Forward) {
return ReachedCurrentBlockBoundary()
? EditorDOMPointType::AtEndOf(*mContent)
: EditorDOMPointType(mContent);
}
return ReachedCurrentBlockBoundary() ? EditorDOMPointType(mContent, 0u)
: EditorDOMPointType::After(*mContent);
}
/**
* Similar to PointAtReachedBlockBoundary() but the block is outside the
* editing host, return the point at the editing host boundary.
*/
template
EditorDOMPointType PointAtReachedBlockBoundaryOrEditingHostBoundary() const {
MOZ_ASSERT(ReachedBlockBoundary());
if (mEditingHost) {
return mDirection == ScanDirection::Forward
? EditorDOMPointType::AtEndOf(*mEditingHost)
: EditorDOMPointType(mEditingHost, 0u);
}
return PointAtReachedBlockBoundary();
}
/**
* Return the point of the reached line boundary. I.e., start or end of the
* line content, i.e., if scanned forward and reached a line break, return the
* point of the line break, not after the line break.
*/
template
EditorDOMPointType PointAtReachedLineBoundary() const {
MOZ_ASSERT(ReachedLineBoundary());
if (ReachedBlockBoundary()) {
return PointAtReachedBlockBoundary();
}
if (mDirection == ScanDirection::Forward) {
return PointAtReachedContent();
}
return PointAfterReachedContent();
}
/**
* Similar to PointAtReachedLineBoundary() but if the line boundary is outside
* the editing host, return the editing host boundary.
*/
template
EditorDOMPointType PointAtReachedLineBoundaryOrEditingHostBoundary() const {
MOZ_ASSERT(ReachedLineBoundary());
if (ReachedBlockBoundary()) {
return PointAtReachedBlockBoundaryOrEditingHostBoundary<
EditorDOMPointType>();
}
if (mDirection == ScanDirection::Forward) {
return PointAtReachedContentOrEditingHostBoundary();
}
return PointAfterReachedContentOrEditingHostBoundary();
}
/**
* The scanner reached an empty inline container element such as
* . Note that the element may be visible, e.g., may have
* non-zero border/padding.
*/
[[nodiscard]] constexpr bool ReachedEmptyInlineContainerElement() const {
return mReason == WSType::EmptyInlineContainerElement;
}
/**
* The scanner reached an editable empty inline container element such as
* . Note that the element may be visible, e.g., may have
* non-zero border/padding.
*/
[[nodiscard]] bool ReachedEditableEmptyInlineContainerElement() const {
return ReachedEmptyInlineContainerElement() && ContentIsEditable();
}
/**
* The scanner reached a removable empty inline container element such as
* . Note that the element may be visible, e.g., may have
* non-zero border/padding.
*/
[[nodiscard]] bool ReachedRemovableEmptyInlineContainerElement() const {
return ReachedEmptyInlineContainerElement() && ContentIsRemovable();
}
/**
* The scanner reached an empty inline container element which is visible.
*/
[[nodiscard]] bool ReachedVisibleEmptyInlineContainerElement(
const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const {
return ReachedEmptyInlineContainerElement() &&
HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*ElementPtr()) &&
!HTMLEditUtils::IsInclusiveAncestorCSSDisplayNone(
*ElementPtr(), aAncestorLimiterToCheckDisplayNone);
}
/**
* The scanner reached an editable empty inline container element which is
* visible.
*/
[[nodiscard]] bool ReachedEditableEmptyInlineContainerElement(
const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const {
return ReachedVisibleEmptyInlineContainerElement(
aAncestorLimiterToCheckDisplayNone) &&
ContentIsEditable();
}
/**
* The scanner reached a removable empty inline container element which is
* visible.
*/
[[nodiscard]] bool ReachedRemovableEmptyInlineContainerElement(
const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const {
return ReachedVisibleEmptyInlineContainerElement(
aAncestorLimiterToCheckDisplayNone) &&
ContentIsRemovable();
}
/**
* The scanner reached an empty inline container element which is invisible.
*/
[[nodiscard]] bool ReachedInvisibleEmptyInlineContainerElement(
const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const {
return ReachedEmptyInlineContainerElement() &&
(!HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*ElementPtr()) ||
HTMLEditUtils::IsInclusiveAncestorCSSDisplayNone(
*ElementPtr(), aAncestorLimiterToCheckDisplayNone));
}
/**
* The scanner reached an editable empty inline container element which is
* invisible.
*/
[[nodiscard]] bool ReachedEditableInvisibleEmptyInlineContainerElement(
const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const {
return ReachedInvisibleEmptyInlineContainerElement(
aAncestorLimiterToCheckDisplayNone) &&
ContentIsEditable();
}
/**
* The scanner reached a removable empty inline container element which is
* invisible.
*/
[[nodiscard]] bool ReachedRemovableInvisibleEmptyInlineContainerElement(
const nsIContent* aAncestorLimiterToCheckDisplayNone = nullptr) const {
return ReachedEditableInvisibleEmptyInlineContainerElement(
aAncestorLimiterToCheckDisplayNone) &&
ContentIsRemovable();
}
/**
* The scanner reached or something which is inline and is not a
* container.
*/
[[nodiscard]] constexpr bool ReachedSpecialContent() const {
return mReason == WSType::SpecialContent;
}
/**
* The point is in visible characters or collapsible white-spaces.
*/
bool InVisibleOrCollapsibleCharacters() const {
return mReason == WSType::CollapsibleWhiteSpaces ||
mReason == WSType::NonCollapsibleCharacters;
}
/**
* The point is in collapsible white-spaces.
*/
bool InCollapsibleWhiteSpaces() const {
return mReason == WSType::CollapsibleWhiteSpaces;
}
/**
* The point is in visible non-collapsible characters.
*/
bool InNonCollapsibleCharacters() const {
return mReason == WSType::NonCollapsibleCharacters;
}
/**
* The scanner reached a element.
*/
bool ReachedBRElement() const { return mReason == WSType::BRElement; }
bool ReachedBRElementNotFollowedByBlockBoundary() const {
return ReachedBRElement() &&
!HTMLEditUtils::IsBRElementFollowedByBlockBoundary(*BRElementPtr());
}
bool ReachedBRElementFollowedByBlockBoundary() const {
return ReachedBRElement() &&
HTMLEditUtils::IsBRElementFollowedByBlockBoundary(*BRElementPtr());
}
bool ReachedPreformattedLineBreak() const {
return mReason == WSType::PreformattedLineBreak;
}
/**
* Return true if reached a element or a preformatted line break.
* Return false when reached a block boundary. Use ReachedLineBoundary() if
* you want it to return true in the case too.
*/
[[nodiscard]] bool ReachedLineBreak() const {
return ReachedBRElement() || ReachedPreformattedLineBreak();
}
/**
* The scanner reached a element.
*/
bool ReachedHRElement() const {
return mContent && mContent->IsHTMLElement(nsGkAtoms::hr);
}
/**
* The scanner reached current block boundary or other block element.
*/
bool ReachedBlockBoundary() const {
return mReason == WSType::CurrentBlockBoundary ||
mReason == WSType::OtherBlockBoundary;
}
/**
* The scanner reached current block element boundary.
*/
bool ReachedCurrentBlockBoundary() const {
return mReason == WSType::CurrentBlockBoundary;
}
/**
* The scanner reached other block element.
*/
bool ReachedOtherBlockElement() const {
return mReason == WSType::OtherBlockBoundary;
}
/**
* The scanner reached other block element that isn't editable
*/
bool ReachedNonEditableOtherBlockElement() const {
return ReachedOtherBlockElement() && !GetContent()->IsEditable();
}
/**
* The scanner reached inline editing host boundary.
*/
[[nodiscard]] bool ReachedInlineEditingHostBoundary() const {
return mReason == WSType::InlineEditingHostBoundary;
}
/**
* The scanner reached something non-text node.
*/
bool ReachedSomethingNonTextContent() const {
return !InVisibleOrCollapsibleCharacters();
}
[[nodiscard]] bool ReachedLineBoundary() const {
switch (mReason) {
case WSType::CurrentBlockBoundary:
case WSType::OtherBlockBoundary:
case WSType::BRElement:
case WSType::PreformattedLineBreak:
return true;
default:
return ReachedHRElement();
}
}
/**
* Return a reference to editor line break which was ignored at scanning.
* If there is, it means that the line break is unnecessary. Note that the
* invisible line break may be outside the editing host.
*/
[[nodiscard]] const Maybe& MaybeIgnoredLineBreak() const {
return mIgnoredLineBreak;
}
friend std::ostream& operator<<(std::ostream& aStream,
const ScanDirection& aDirection) {
return aStream << (aDirection == ScanDirection::Backward
? "ScanDirection::Backward"
: "ScanDirection::Forward");
}
friend std::ostream& operator<<(std::ostream& aStream,
const WSScanResult& aResult) {
aStream << "{ mReason: " << aResult.mReason;
if (aResult.mReason == WSType::NotInitialized ||
aResult.mReason == WSType::InUncomposedDoc) {
return aStream << " }";
}
return aStream << ", mContent: " << aResult.mContent
<< ", mEditingHost: " << aResult.mEditingHost
<< "< mIgnoredLineBreak: " << aResult.mIgnoredLineBreak
<< ", mOffset: " << aResult.mOffset
<< ", mDirection: " << aResult.mDirection << " }";
}
private:
void MaybeSetEditingHost(const WSRunScanner& aScanner);
nsCOMPtr mContent;
RefPtr mEditingHost;
Maybe mIgnoredLineBreak;
Maybe mOffset;
WSType mReason = WSType::NotInitialized;
ScanDirection mDirection = ScanDirection::Backward;
};
class MOZ_STACK_CLASS WSRunScanner final {
private:
using Element = dom::Element;
using HTMLBRElement = dom::HTMLBRElement;
using Text = dom::Text;
public:
using WSType = WSScanResult::WSType;
enum class IgnoreNonEditableNodes : bool { No, Yes };
enum class StopAtNonEditableNode : bool { No, Yes };
enum class ReferHTMLDefaultStyle : bool { No, Yes };
enum class Option {
// If set, return only editable content or return non-editable content as a
// special content in the closest editing host if the scan start point is
// editable.
OnlyEditableNodes,
// If set, use the HTML default style to consider whether the found one is a
// block or an inline.
ReferHTMLDefaultStyle,
// If set, stop scanning the DOM when it reaches a `Comment` node.
StopAtComment,
// If set, stop at any empty inline containers, even when it's visible.
StopAtAnyEmptyInlineContainers,
// If set, stop ignoring visible empty inline containers such as
// or
// .
// XXX Currently, this does not work well if the inline container has only
// `::before` and/or `::after` content and the frame is dirty.
StopAtVisibleEmptyInlineContainers,
};
using Options = EnumSet