gecko-dev/editor/libeditor/HTMLEditHelpers.h

1119 строки
43 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 HTMLEditHelpers_h
#define HTMLEditHelpers_h
/**
* This header declares/defines trivial helper classes which are used by
* HTMLEditor. If you want to create or look for static utility methods,
* see HTMLEditUtils.h.
*/
#include "EditorDOMPoint.h"
#include "EditorForwards.h"
#include "EditorUtils.h" // for CaretPoint
#include "JoinSplitNodeDirection.h"
#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/Attributes.h"
#include "mozilla/ContentIterator.h"
#include "mozilla/IntegerRange.h"
#include "mozilla/Maybe.h"
#include "mozilla/RangeBoundary.h"
#include "mozilla/Result.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/StaticRange.h"
#include "nsCOMPtr.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsGkAtoms.h"
#include "nsIContent.h"
#include "nsRange.h"
#include "nsString.h"
class nsISimpleEnumerator;
namespace mozilla {
enum class WithTransaction { No, Yes };
inline std::ostream& operator<<(std::ostream& aStream,
WithTransaction aWithTransaction) {
aStream << "WithTransaction::"
<< (aWithTransaction == WithTransaction::Yes ? "Yes" : "No");
return aStream;
}
/*****************************************************************************
* MoveNodeResult is a simple class for MoveSomething() methods.
* This stores whether it's handled or not, and next insertion point and a
* suggestion for new caret position.
*****************************************************************************/
class MOZ_STACK_CLASS MoveNodeResult final : public CaretPoint {
public:
constexpr bool Handled() const { return mHandled; }
constexpr bool Ignored() const { return !Handled(); }
constexpr const EditorDOMPoint& NextInsertionPointRef() const {
return mNextInsertionPoint;
}
constexpr EditorDOMPoint&& UnwrapNextInsertionPoint() {
return std::move(mNextInsertionPoint);
}
template <typename EditorDOMPointType>
EditorDOMPointType NextInsertionPoint() const {
return mNextInsertionPoint.To<EditorDOMPointType>();
}
/**
* Override the result as "handled" forcibly.
*/
void MarkAsHandled() { mHandled = true; }
MoveNodeResult(const MoveNodeResult& aOther) = delete;
MoveNodeResult& operator=(const MoveNodeResult& aOther) = delete;
MoveNodeResult(MoveNodeResult&& aOther) = default;
MoveNodeResult& operator=(MoveNodeResult&& aOther) = default;
MoveNodeResult& operator|=(const MoveNodeResult& aOther) {
MOZ_ASSERT(this != &aOther);
// aOther is merged with this instance so that its caret suggestion
// shouldn't be handled anymore.
aOther.IgnoreCaretPointSuggestion();
// Should be handled again even if it's already handled
UnmarkAsHandledCaretPoint();
mHandled |= aOther.mHandled;
// Take the new one for the next insertion point.
mNextInsertionPoint = aOther.mNextInsertionPoint;
// Take the new caret point if and only if it's suggested.
if (aOther.HasCaretPointSuggestion()) {
SetCaretPoint(aOther.CaretPointRef());
}
return *this;
}
#ifdef DEBUG
~MoveNodeResult() {
MOZ_ASSERT_IF(Handled(), !HasCaretPointSuggestion() || CaretPointHandled());
}
#endif
/*****************************************************************************
* When a move node handler (or its helper) does nothing,
* the result of these factory methods should be returned.
* aNextInsertionPoint Must be set and valid.
*****************************************************************************/
static MoveNodeResult IgnoredResult(
const EditorDOMPoint& aNextInsertionPoint) {
return MoveNodeResult(aNextInsertionPoint, false);
}
static MoveNodeResult IgnoredResult(EditorDOMPoint&& aNextInsertionPoint) {
return MoveNodeResult(std::move(aNextInsertionPoint), false);
}
/*****************************************************************************
* When a move node handler (or its helper) handled and not canceled,
* the result of these factory methods should be returned.
* aNextInsertionPoint Must be set and valid.
*****************************************************************************/
static MoveNodeResult HandledResult(
const EditorDOMPoint& aNextInsertionPoint) {
return MoveNodeResult(aNextInsertionPoint, true);
}
static MoveNodeResult HandledResult(EditorDOMPoint&& aNextInsertionPoint) {
return MoveNodeResult(std::move(aNextInsertionPoint), true);
}
static MoveNodeResult HandledResult(const EditorDOMPoint& aNextInsertionPoint,
const EditorDOMPoint& aPointToPutCaret) {
return MoveNodeResult(aNextInsertionPoint, aPointToPutCaret);
}
static MoveNodeResult HandledResult(EditorDOMPoint&& aNextInsertionPoint,
const EditorDOMPoint& aPointToPutCaret) {
return MoveNodeResult(std::move(aNextInsertionPoint), aPointToPutCaret);
}
static MoveNodeResult HandledResult(const EditorDOMPoint& aNextInsertionPoint,
EditorDOMPoint&& aPointToPutCaret) {
return MoveNodeResult(aNextInsertionPoint, std::move(aPointToPutCaret));
}
static MoveNodeResult HandledResult(EditorDOMPoint&& aNextInsertionPoint,
EditorDOMPoint&& aPointToPutCaret) {
return MoveNodeResult(std::move(aNextInsertionPoint),
std::move(aPointToPutCaret));
}
private:
explicit MoveNodeResult(const EditorDOMPoint& aNextInsertionPoint,
bool aHandled)
: CaretPoint(),
mNextInsertionPoint(aNextInsertionPoint),
mHandled(aHandled && aNextInsertionPoint.IsSet()) {
AutoEditorDOMPointChildInvalidator computeOffsetAndForgetChild(
mNextInsertionPoint);
}
explicit MoveNodeResult(EditorDOMPoint&& aNextInsertionPoint, bool aHandled)
: CaretPoint(),
mNextInsertionPoint(std::move(aNextInsertionPoint)),
mHandled(aHandled && mNextInsertionPoint.IsSet()) {
AutoEditorDOMPointChildInvalidator computeOffsetAndForgetChild(
mNextInsertionPoint);
}
explicit MoveNodeResult(const EditorDOMPoint& aNextInsertionPoint,
const EditorDOMPoint& aPointToPutCaret)
: CaretPoint(aPointToPutCaret),
mNextInsertionPoint(aNextInsertionPoint),
mHandled(mNextInsertionPoint.IsSet()) {
AutoEditorDOMPointChildInvalidator computeOffsetAndForgetChild(
mNextInsertionPoint);
}
explicit MoveNodeResult(EditorDOMPoint&& aNextInsertionPoint,
const EditorDOMPoint& aPointToPutCaret)
: CaretPoint(aPointToPutCaret),
mNextInsertionPoint(std::move(aNextInsertionPoint)),
mHandled(mNextInsertionPoint.IsSet()) {
AutoEditorDOMPointChildInvalidator computeOffsetAndForgetChild(
mNextInsertionPoint);
}
explicit MoveNodeResult(const EditorDOMPoint& aNextInsertionPoint,
EditorDOMPoint&& aPointToPutCaret)
: CaretPoint(std::move(aPointToPutCaret)),
mNextInsertionPoint(aNextInsertionPoint),
mHandled(mNextInsertionPoint.IsSet()) {
AutoEditorDOMPointChildInvalidator computeOffsetAndForgetChild(
mNextInsertionPoint);
}
explicit MoveNodeResult(EditorDOMPoint&& aNextInsertionPoint,
EditorDOMPoint&& aPointToPutCaret)
: CaretPoint(std::move(aPointToPutCaret)),
mNextInsertionPoint(std::move(aNextInsertionPoint)),
mHandled(mNextInsertionPoint.IsSet()) {
AutoEditorDOMPointChildInvalidator computeOffsetAndForgetChild(
mNextInsertionPoint);
}
EditorDOMPoint mNextInsertionPoint;
bool mHandled;
};
/*****************************************************************************
* SplitNodeResult is a simple class for
* HTMLEditor::SplitNodeDeepWithTransaction().
* This makes the callers' code easier to read.
*****************************************************************************/
class MOZ_STACK_CLASS SplitNodeResult final : public CaretPoint {
public:
bool Handled() const { return mPreviousNode || mNextNode; }
/**
* DidSplit() returns true if a node was actually split.
*/
bool DidSplit() const { return mPreviousNode && mNextNode; }
/**
* GetPreviousContent() returns previous content node at the split point.
*/
MOZ_KNOWN_LIVE nsIContent* GetPreviousContent() const {
if (mGivenSplitPoint.IsSet()) {
return mGivenSplitPoint.IsEndOfContainer() ? mGivenSplitPoint.GetChild()
: nullptr;
}
return mPreviousNode;
}
template <typename NodeType>
MOZ_KNOWN_LIVE NodeType* GetPreviousContentAs() const {
return NodeType::FromNodeOrNull(GetPreviousContent());
}
template <typename EditorDOMPointType>
EditorDOMPointType AtPreviousContent() const {
if (nsIContent* previousContent = GetPreviousContent()) {
return EditorDOMPointType(previousContent);
}
return EditorDOMPointType();
}
/**
* GetNextContent() returns next content node at the split point.
*/
MOZ_KNOWN_LIVE nsIContent* GetNextContent() const {
if (mGivenSplitPoint.IsSet()) {
return !mGivenSplitPoint.IsEndOfContainer() ? mGivenSplitPoint.GetChild()
: nullptr;
}
return mNextNode;
}
template <typename NodeType>
MOZ_KNOWN_LIVE NodeType* GetNextContentAs() const {
return NodeType::FromNodeOrNull(GetNextContent());
}
template <typename EditorDOMPointType>
EditorDOMPointType AtNextContent() const {
if (nsIContent* nextContent = GetNextContent()) {
return EditorDOMPointType(nextContent);
}
return EditorDOMPointType();
}
/**
* Returns new content node which is created at splitting a node. I.e., this
* returns nullptr if no node was split.
*/
MOZ_KNOWN_LIVE nsIContent* GetNewContent() const {
if (!DidSplit()) {
return nullptr;
}
return mDirection == SplitNodeDirection::LeftNodeIsNewOne ? mPreviousNode
: mNextNode;
}
template <typename NodeType>
MOZ_KNOWN_LIVE NodeType* GetNewContentAs() const {
return NodeType::FromNodeOrNull(GetNewContent());
}
template <typename EditorDOMPointType>
EditorDOMPointType AtNewContent() const {
if (nsIContent* newContent = GetNewContent()) {
return EditorDOMPointType(newContent);
}
return EditorDOMPointType();
}
/**
* Returns original content node which is (or is just tried to be) split.
*/
MOZ_KNOWN_LIVE nsIContent* GetOriginalContent() const {
if (mGivenSplitPoint.IsSet()) {
// Different from previous/next content, if the creator didn't split a
// node, the container of the split point is the original node.
return mGivenSplitPoint.GetContainerAs<nsIContent>();
}
if (mDirection == SplitNodeDirection::LeftNodeIsNewOne) {
return mNextNode ? mNextNode : mPreviousNode;
}
return mPreviousNode ? mPreviousNode : mNextNode;
}
template <typename NodeType>
MOZ_KNOWN_LIVE NodeType* GetOriginalContentAs() const {
return NodeType::FromNodeOrNull(GetOriginalContent());
}
template <typename EditorDOMPointType>
EditorDOMPointType AtOriginalContent() const {
if (nsIContent* originalContent = GetOriginalContent()) {
return EditorDOMPointType(originalContent);
}
return EditorDOMPointType();
}
/**
* AtSplitPoint() returns the split point in the container.
* HTMLEditor::CreateAndInsertElement() or something similar methods.
*/
template <typename EditorDOMPointType>
EditorDOMPointType AtSplitPoint() const {
if (mGivenSplitPoint.IsSet()) {
return mGivenSplitPoint.To<EditorDOMPointType>();
}
if (!mPreviousNode) {
return EditorDOMPointType(mNextNode);
}
return EditorDOMPointType::After(mPreviousNode);
}
SplitNodeResult() = delete;
SplitNodeResult(const SplitNodeResult&) = delete;
SplitNodeResult& operator=(const SplitNodeResult&) = delete;
SplitNodeResult(SplitNodeResult&&) = default;
SplitNodeResult& operator=(SplitNodeResult&&) = default;
/**
* This constructor should be used for setting specific caret point instead of
* aSplitResult's one.
*/
SplitNodeResult(SplitNodeResult&& aSplitResult,
const EditorDOMPoint& aNewCaretPoint)
: SplitNodeResult(std::move(aSplitResult)) {
SetCaretPoint(aNewCaretPoint);
}
SplitNodeResult(SplitNodeResult&& aSplitResult,
EditorDOMPoint&& aNewCaretPoint)
: SplitNodeResult(std::move(aSplitResult)) {
SetCaretPoint(std::move(aNewCaretPoint));
}
/**
* This constructor shouldn't be used by anybody except methods which
* use this as result when it succeeds.
*
* @param aNewNode The node which is newly created.
* @param aSplitNode The node which was split.
* @param aDirection The split direction which the HTML editor tried to split
* a node with.
* @param aNewCaretPoint
* An optional new caret position. If this is omitted,
* the point between new node and split node will be
* suggested.
*/
SplitNodeResult(nsIContent& aNewNode, nsIContent& aSplitNode,
SplitNodeDirection aDirection,
const Maybe<EditorDOMPoint>& aNewCaretPoint = Nothing())
: CaretPoint(aNewCaretPoint.isSome()
? aNewCaretPoint.ref()
: EditorDOMPoint::AtEndOf(*PreviousNode(
aDirection, &aNewNode, &aSplitNode))),
mPreviousNode(PreviousNode(aDirection, &aNewNode, &aSplitNode)),
mNextNode(NextNode(aDirection, &aNewNode, &aSplitNode)),
mDirection(aDirection) {}
SplitNodeResult ToHandledResult() const {
mHandledCaretPoint = true;
SplitNodeResult result(mDirection);
result.mPreviousNode = GetPreviousContent();
result.mNextNode = GetNextContent();
MOZ_DIAGNOSTIC_ASSERT(result.Handled());
// Don't recompute the caret position because in this case, split has not
// occurred yet. In the case, the caller shouldn't need to update
// selection.
result.mCaretPoint = mCaretPoint;
return result;
}
/**
* The following factory methods creates a SplitNodeResult instance for the
* special cases.
*
* @param aDeeperSplitNodeResult
* If the splitter has already split a child or a
* descendant of the latest split node, the split node
* result should be specified.
*/
static inline SplitNodeResult HandledButDidNotSplitDueToEndOfContainer(
nsIContent& aNotSplitNode, SplitNodeDirection aDirection,
const SplitNodeResult* aDeeperSplitNodeResult = nullptr) {
SplitNodeResult result(aDirection);
result.mPreviousNode = &aNotSplitNode;
// Caret should be put at the last split point instead of current node.
if (aDeeperSplitNodeResult) {
result.SetCaretPoint(aDeeperSplitNodeResult->CaretPointRef());
aDeeperSplitNodeResult->IgnoreCaretPointSuggestion();
}
return result;
}
static inline SplitNodeResult HandledButDidNotSplitDueToStartOfContainer(
nsIContent& aNotSplitNode, SplitNodeDirection aDirection,
const SplitNodeResult* aDeeperSplitNodeResult = nullptr) {
SplitNodeResult result(aDirection);
result.mNextNode = &aNotSplitNode;
// Caret should be put at the last split point instead of current node.
if (aDeeperSplitNodeResult) {
result.SetCaretPoint(aDeeperSplitNodeResult->CaretPointRef());
aDeeperSplitNodeResult->IgnoreCaretPointSuggestion();
}
return result;
}
template <typename PT, typename CT>
static inline SplitNodeResult NotHandled(
const EditorDOMPointBase<PT, CT>& aGivenSplitPoint,
SplitNodeDirection aDirection,
const SplitNodeResult* aDeeperSplitNodeResult = nullptr) {
SplitNodeResult result(aDirection);
result.mGivenSplitPoint = aGivenSplitPoint;
// Caret should be put at the last split point instead of current node.
if (aDeeperSplitNodeResult) {
result.SetCaretPoint(aDeeperSplitNodeResult->CaretPointRef());
aDeeperSplitNodeResult->IgnoreCaretPointSuggestion();
}
return result;
}
/**
* Returns aSplitNodeResult as-is unless it didn't split a node but
* aDeeperSplitNodeResult has already split a child or a descendant and has a
* valid point to put caret around there. In the case, this return
* aSplitNodeResult which suggests a caret position around the last split
* point.
*/
static inline SplitNodeResult MergeWithDeeperSplitNodeResult(
SplitNodeResult&& aSplitNodeResult,
const SplitNodeResult& aDeeperSplitNodeResult) {
aSplitNodeResult.UnmarkAsHandledCaretPoint();
aDeeperSplitNodeResult.IgnoreCaretPointSuggestion();
if (aSplitNodeResult.DidSplit() ||
!aDeeperSplitNodeResult.mCaretPoint.IsSet()) {
return std::move(aSplitNodeResult);
}
SplitNodeResult result(std::move(aSplitNodeResult));
result.SetCaretPoint(aDeeperSplitNodeResult.CaretPointRef());
return result;
}
#ifdef DEBUG
~SplitNodeResult() {
MOZ_ASSERT(!HasCaretPointSuggestion() || CaretPointHandled());
}
#endif
private:
explicit SplitNodeResult(SplitNodeDirection aDirection)
: mDirection(aDirection) {}
// Helper methods to consider previous/next node from new/old node and split
// direction.
static nsIContent* PreviousNode(SplitNodeDirection aDirection,
nsIContent* aNewOne, nsIContent* aOldOne) {
return aDirection == SplitNodeDirection::LeftNodeIsNewOne ? aNewOne
: aOldOne;
}
static nsIContent* NextNode(SplitNodeDirection aDirection,
nsIContent* aNewOne, nsIContent* aOldOne) {
return aDirection == SplitNodeDirection::LeftNodeIsNewOne ? aOldOne
: aNewOne;
}
// When methods which return this class split some nodes actually, they
// need to set a set of left node and right node to this class. However,
// one or both of them may be moved or removed by mutation observer.
// In such case, we cannot represent the point with EditorDOMPoint since
// it requires current container node. Therefore, we need to use
// nsCOMPtr<nsIContent> here instead.
nsCOMPtr<nsIContent> mPreviousNode;
nsCOMPtr<nsIContent> mNextNode;
// Methods which return this class may not split any nodes actually. Then,
// they may want to return given split point as is since such behavior makes
// their callers simpler. In this case, the point may be in a text node
// which cannot be represented as a node. Therefore, we need EditorDOMPoint
// for representing the point.
EditorDOMPoint mGivenSplitPoint;
// The point which is a good point to put caret from point of view the
// splitter.
EditorDOMPoint mCaretPoint;
SplitNodeDirection mDirection;
bool mutable mHandledCaretPoint = false;
};
/*****************************************************************************
* JoinNodesResult is a simple class for HTMLEditor::JoinNodesWithTransaction().
* This makes the callers' code easier to read.
*****************************************************************************/
class MOZ_STACK_CLASS JoinNodesResult final {
public:
MOZ_KNOWN_LIVE nsIContent* ExistingContent() const {
return mJoinedPoint.ContainerAs<nsIContent>();
}
template <typename EditorDOMPointType>
EditorDOMPointType AtExistingContent() const {
return EditorDOMPointType(mJoinedPoint.ContainerAs<nsIContent>());
}
MOZ_KNOWN_LIVE nsIContent* RemovedContent() const { return mRemovedContent; }
template <typename EditorDOMPointType>
EditorDOMPointType AtRemovedContent() const {
if (mRemovedContent) {
return EditorDOMPointType(mRemovedContent);
}
return EditorDOMPointType();
}
template <typename EditorDOMPointType>
EditorDOMPointType AtJoinedPoint() const {
return mJoinedPoint.To<EditorDOMPointType>();
}
JoinNodesResult() = delete;
/**
* This constructor shouldn't be used by anybody except methods which
* use this as result when it succeeds.
*
* @param aJoinedPoint First child of right node or first character.
* @param aRemovedContent The node which was removed from the parent.
* @param aDirection The join direction which the HTML editor tried
* to join the nodes with.
*/
JoinNodesResult(const EditorDOMPoint& aJoinedPoint,
nsIContent& aRemovedContent, JoinNodesDirection aDirection)
: mJoinedPoint(aJoinedPoint), mRemovedContent(&aRemovedContent) {
MOZ_DIAGNOSTIC_ASSERT(aJoinedPoint.IsInContentNode());
}
JoinNodesResult(const JoinNodesResult& aOther) = delete;
JoinNodesResult& operator=(const JoinNodesResult& aOther) = delete;
JoinNodesResult(JoinNodesResult&& aOther) = default;
JoinNodesResult& operator=(JoinNodesResult&& aOther) = default;
private:
EditorDOMPoint mJoinedPoint;
MOZ_KNOWN_LIVE nsCOMPtr<nsIContent> mRemovedContent;
};
/*****************************************************************************
* SplitRangeOffFromNodeResult class is a simple class for methods which split a
* node at 2 points for making part of the node split off from the node.
*****************************************************************************/
class MOZ_STACK_CLASS SplitRangeOffFromNodeResult final : public CaretPoint {
public:
/**
* GetLeftContent() returns new created node before the part of quarried out.
* This may return nullptr if the method didn't split at start edge of
* the node.
*/
MOZ_KNOWN_LIVE nsIContent* GetLeftContent() const { return mLeftContent; }
template <typename ContentNodeType>
MOZ_KNOWN_LIVE ContentNodeType* GetLeftContentAs() const {
return ContentNodeType::FromNodeOrNull(GetLeftContent());
}
constexpr nsCOMPtr<nsIContent>&& UnwrapLeftContent() {
mMovedContent = true;
return std::move(mLeftContent);
}
/**
* GetMiddleContent() returns new created node between left node and right
* node. I.e., this is quarried out from the node. This may return nullptr
* if the method unwrapped the middle node.
*/
MOZ_KNOWN_LIVE nsIContent* GetMiddleContent() const { return mMiddleContent; }
template <typename ContentNodeType>
MOZ_KNOWN_LIVE ContentNodeType* GetMiddleContentAs() const {
return ContentNodeType::FromNodeOrNull(GetMiddleContent());
}
constexpr nsCOMPtr<nsIContent>&& UnwrapMiddleContent() {
mMovedContent = true;
return std::move(mMiddleContent);
}
/**
* GetRightContent() returns the right node after the part of quarried out.
* This may return nullptr it the method didn't split at end edge of the
* node.
*/
MOZ_KNOWN_LIVE nsIContent* GetRightContent() const { return mRightContent; }
template <typename ContentNodeType>
MOZ_KNOWN_LIVE ContentNodeType* GetRightContentAs() const {
return ContentNodeType::FromNodeOrNull(GetRightContent());
}
constexpr nsCOMPtr<nsIContent>&& UnwrapRightContent() {
mMovedContent = true;
return std::move(mRightContent);
}
/**
* GetLeftmostContent() returns the leftmost content after trying to
* split twice. If the node was not split, this returns the original node.
*/
MOZ_KNOWN_LIVE nsIContent* GetLeftmostContent() const {
MOZ_ASSERT(!mMovedContent);
return mLeftContent ? mLeftContent
: (mMiddleContent ? mMiddleContent : mRightContent);
}
template <typename ContentNodeType>
MOZ_KNOWN_LIVE ContentNodeType* GetLeftmostContentAs() const {
return ContentNodeType::FromNodeOrNull(GetLeftmostContent());
}
/**
* GetRightmostContent() returns the rightmost content after trying to
* split twice. If the node was not split, this returns the original node.
*/
MOZ_KNOWN_LIVE nsIContent* GetRightmostContent() const {
MOZ_ASSERT(!mMovedContent);
return mRightContent ? mRightContent
: (mMiddleContent ? mMiddleContent : mLeftContent);
}
template <typename ContentNodeType>
MOZ_KNOWN_LIVE ContentNodeType* GetRightmostContentAs() const {
return ContentNodeType::FromNodeOrNull(GetRightmostContent());
}
SplitRangeOffFromNodeResult() = delete;
SplitRangeOffFromNodeResult(nsIContent* aLeftContent,
nsIContent* aMiddleContent,
nsIContent* aRightContent)
: CaretPoint(),
mLeftContent(aLeftContent),
mMiddleContent(aMiddleContent),
mRightContent(aRightContent) {}
SplitRangeOffFromNodeResult(nsIContent* aLeftContent,
nsIContent* aMiddleContent,
nsIContent* aRightContent,
EditorDOMPoint&& aPointToPutCaret)
: CaretPoint(std::move(aPointToPutCaret)),
mLeftContent(aLeftContent),
mMiddleContent(aMiddleContent),
mRightContent(aRightContent) {}
SplitRangeOffFromNodeResult(const SplitRangeOffFromNodeResult& aOther) =
delete;
SplitRangeOffFromNodeResult& operator=(
const SplitRangeOffFromNodeResult& aOther) = delete;
SplitRangeOffFromNodeResult(SplitRangeOffFromNodeResult&& aOther) noexcept
: CaretPoint(aOther.UnwrapCaretPoint()),
mLeftContent(std::move(aOther.mLeftContent)),
mMiddleContent(std::move(aOther.mMiddleContent)),
mRightContent(std::move(aOther.mRightContent)) {
MOZ_ASSERT(!aOther.mMovedContent);
}
SplitRangeOffFromNodeResult& operator=(SplitRangeOffFromNodeResult&& aOther) =
delete; // due to bug 1792638
#ifdef DEBUG
~SplitRangeOffFromNodeResult() {
MOZ_ASSERT(!HasCaretPointSuggestion() || CaretPointHandled());
}
#endif
private:
MOZ_KNOWN_LIVE nsCOMPtr<nsIContent> mLeftContent;
MOZ_KNOWN_LIVE nsCOMPtr<nsIContent> mMiddleContent;
MOZ_KNOWN_LIVE nsCOMPtr<nsIContent> mRightContent;
bool mutable mMovedContent = false;
};
/*****************************************************************************
* SplitRangeOffResult class is a simple class for methods which splits
* specific ancestor elements at 2 DOM points.
*****************************************************************************/
class MOZ_STACK_CLASS SplitRangeOffResult final : public CaretPoint {
public:
constexpr bool Handled() const { return mHandled; }
/**
* The start boundary is at the right of split at split point. The end
* boundary is at right node of split at end point, i.e., the end boundary
* points out of the range to have been split off.
*/
constexpr const EditorDOMRange& RangeRef() const { return mRange; }
SplitRangeOffResult() = delete;
/**
* Constructor for success case.
*
* @param aTrackedRangeStart The range whose start is at topmost
* right node child at start point if
* actually split there, or at the point
* to be tried to split, and whose end is
* at topmost right node child at end point
* if actually split there, or at the point
* to be tried to split. Note that if the
* method allows to run script after
* splitting the range boundaries, they
* should be tracked with
* AutoTrackDOMRange.
* @param aSplitNodeResultAtStart Raw split node result at start point.
* @param aSplitNodeResultAtEnd Raw split node result at start point.
*/
SplitRangeOffResult(EditorDOMRange&& aTrackedRange,
SplitNodeResult&& aSplitNodeResultAtStart,
SplitNodeResult&& aSplitNodeResultAtEnd)
: CaretPoint(),
mRange(std::move(aTrackedRange)),
mHandled(aSplitNodeResultAtStart.Handled() ||
aSplitNodeResultAtEnd.Handled()) {
MOZ_ASSERT(mRange.StartRef().IsSet());
MOZ_ASSERT(mRange.EndRef().IsSet());
// The given results are created for creating this instance so that the
// caller may not need to handle with them. For making who taking the
// responsible clearer, we should move them into this constructor.
EditorDOMPoint pointToPutCaret;
SplitNodeResult splitNodeResultAtStart(std::move(aSplitNodeResultAtStart));
SplitNodeResult splitNodeResultAtEnd(std::move(aSplitNodeResultAtEnd));
splitNodeResultAtStart.MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
splitNodeResultAtEnd.MoveCaretPointTo(pointToPutCaret,
{SuggestCaret::OnlyIfHasSuggestion});
SetCaretPoint(std::move(pointToPutCaret));
}
SplitRangeOffResult(const SplitRangeOffResult& aOther) = delete;
SplitRangeOffResult& operator=(const SplitRangeOffResult& aOther) = delete;
SplitRangeOffResult(SplitRangeOffResult&& aOther) = default;
SplitRangeOffResult& operator=(SplitRangeOffResult&& aOther) = default;
private:
EditorDOMRange mRange;
// If you need to store previous and/or next node at start/end point,
// you might be able to use `SplitNodeResult::GetPreviousNode()` etc in the
// constructor only when `SplitNodeResult::Handled()` returns true. But
// the node might have gone with another DOM tree mutation. So, be careful
// if you do it.
bool mHandled;
};
/******************************************************************************
* DOM tree iterators
*****************************************************************************/
class MOZ_RAII DOMIterator {
public:
explicit DOMIterator();
explicit DOMIterator(nsINode& aNode);
virtual ~DOMIterator() = default;
nsresult Init(nsRange& aRange);
nsresult Init(const RawRangeBoundary& aStartRef,
const RawRangeBoundary& aEndRef);
template <class NodeClass>
void AppendAllNodesToArray(
nsTArray<OwningNonNull<NodeClass>>& aArrayOfNodes) const;
/**
* AppendNodesToArray() calls aFunctor before appending found node to
* aArrayOfNodes. If aFunctor returns false, the node will be ignored.
* You can use aClosure instead of capturing something with lambda.
* Note that aNode is guaranteed that it's an instance of NodeClass
* or its sub-class.
* XXX If we can make type of aNode templated without std::function,
* it'd be better, though.
*/
typedef bool (*BoolFunctor)(nsINode& aNode, void* aClosure);
template <class NodeClass>
void AppendNodesToArray(BoolFunctor aFunctor,
nsTArray<OwningNonNull<NodeClass>>& aArrayOfNodes,
void* aClosure = nullptr) const;
protected:
ContentIteratorBase* mIter;
PostContentIterator mPostOrderIter;
};
class MOZ_RAII DOMSubtreeIterator final : public DOMIterator {
public:
explicit DOMSubtreeIterator();
virtual ~DOMSubtreeIterator() = default;
nsresult Init(nsRange& aRange);
private:
ContentSubtreeIterator mSubtreeIter;
explicit DOMSubtreeIterator(nsINode& aNode) = delete;
};
/******************************************************************************
* ReplaceRangeData
*
* This represents range to be replaced and replacing string.
*****************************************************************************/
template <typename EditorDOMPointType>
class MOZ_STACK_CLASS ReplaceRangeDataBase final {
public:
ReplaceRangeDataBase() = default;
template <typename OtherEditorDOMRangeType>
ReplaceRangeDataBase(const OtherEditorDOMRangeType& aRange,
const nsAString& aReplaceString)
: mRange(aRange), mReplaceString(aReplaceString) {}
template <typename StartPointType, typename EndPointType>
ReplaceRangeDataBase(const StartPointType& aStart, const EndPointType& aEnd,
const nsAString& aReplaceString)
: mRange(aStart, aEnd), mReplaceString(aReplaceString) {}
bool IsSet() const { return mRange.IsPositioned(); }
bool IsSetAndValid() const { return mRange.IsPositionedAndValid(); }
bool Collapsed() const { return mRange.Collapsed(); }
bool HasReplaceString() const { return !mReplaceString.IsEmpty(); }
const EditorDOMPointType& StartRef() const { return mRange.StartRef(); }
const EditorDOMPointType& EndRef() const { return mRange.EndRef(); }
const EditorDOMRangeBase<EditorDOMPointType>& RangeRef() const {
return mRange;
}
const nsString& ReplaceStringRef() const { return mReplaceString; }
template <typename PointType>
MOZ_NEVER_INLINE_DEBUG void SetStart(const PointType& aStart) {
mRange.SetStart(aStart);
}
template <typename PointType>
MOZ_NEVER_INLINE_DEBUG void SetEnd(const PointType& aEnd) {
mRange.SetEnd(aEnd);
}
template <typename StartPointType, typename EndPointType>
MOZ_NEVER_INLINE_DEBUG void SetStartAndEnd(const StartPointType& aStart,
const EndPointType& aEnd) {
mRange.SetRange(aStart, aEnd);
}
template <typename OtherEditorDOMRangeType>
MOZ_NEVER_INLINE_DEBUG void SetRange(const OtherEditorDOMRangeType& aRange) {
mRange = aRange;
}
void SetReplaceString(const nsAString& aReplaceString) {
mReplaceString = aReplaceString;
}
template <typename StartPointType, typename EndPointType>
MOZ_NEVER_INLINE_DEBUG void SetStartAndEnd(const StartPointType& aStart,
const EndPointType& aEnd,
const nsAString& aReplaceString) {
SetStartAndEnd(aStart, aEnd);
SetReplaceString(aReplaceString);
}
template <typename OtherEditorDOMRangeType>
MOZ_NEVER_INLINE_DEBUG void Set(const OtherEditorDOMRangeType& aRange,
const nsAString& aReplaceString) {
SetRange(aRange);
SetReplaceString(aReplaceString);
}
private:
EditorDOMRangeBase<EditorDOMPointType> mRange;
// This string may be used with ReplaceTextTransaction. Therefore, for
// avoiding memory copy, we should store it with nsString rather than
// nsAutoString.
nsString mReplaceString;
};
/******************************************************************************
* EditorElementStyle represents a generic style of element
******************************************************************************/
class MOZ_STACK_CLASS EditorElementStyle {
public:
#define DEFINE_FACTORY(aName, aAttr) \
constexpr static EditorElementStyle aName() { \
return EditorElementStyle(*(aAttr)); \
}
// text-align, caption-side, a pair of margin-left and margin-right
DEFINE_FACTORY(Align, nsGkAtoms::align)
// background-color
DEFINE_FACTORY(BGColor, nsGkAtoms::bgcolor)
// background-image
DEFINE_FACTORY(Background, nsGkAtoms::background)
// border
DEFINE_FACTORY(Border, nsGkAtoms::border)
// height
DEFINE_FACTORY(Height, nsGkAtoms::height)
// color
DEFINE_FACTORY(Text, nsGkAtoms::text)
// list-style-type
DEFINE_FACTORY(Type, nsGkAtoms::type)
// vertical-align
DEFINE_FACTORY(VAlign, nsGkAtoms::valign)
// width
DEFINE_FACTORY(Width, nsGkAtoms::width)
static EditorElementStyle Create(const nsAtom& aAttribute) {
MOZ_DIAGNOSTIC_ASSERT(IsHTMLStyle(&aAttribute));
return EditorElementStyle(*aAttribute.AsStatic());
}
[[nodiscard]] static bool IsHTMLStyle(const nsAtom* aAttribute) {
return aAttribute == nsGkAtoms::align || aAttribute == nsGkAtoms::bgcolor ||
aAttribute == nsGkAtoms::background ||
aAttribute == nsGkAtoms::border || aAttribute == nsGkAtoms::height ||
aAttribute == nsGkAtoms::text || aAttribute == nsGkAtoms::type ||
aAttribute == nsGkAtoms::valign || aAttribute == nsGkAtoms::width;
}
[[nodiscard]] bool IsCSSEditable(const dom::Element& aElement) const;
nsStaticAtom* Style() const { return mStyle; }
[[nodiscard]] bool IsInlineStyle() const { return !mStyle; }
inline EditorInlineStyle& AsInlineStyle();
inline const EditorInlineStyle& AsInlineStyle() const;
protected:
MOZ_KNOWN_LIVE nsStaticAtom* mStyle = nullptr;
EditorElementStyle() = default;
private:
constexpr explicit EditorElementStyle(const nsStaticAtom& aStyle)
// Needs const_cast hack here because the this class users may want
// non-const nsStaticAtom pointer due to bug 1794954
: mStyle(const_cast<nsStaticAtom*>(&aStyle)) {}
};
/******************************************************************************
* EditorInlineStyle represents an inline style.
******************************************************************************/
struct MOZ_STACK_CLASS EditorInlineStyle : public EditorElementStyle {
// nullptr if you want to remove all inline styles.
// Otherwise, one of the presentation tag names which we support in style
// editor, and there special cases: nsGkAtoms::href means <a href="...">,
// and nsGkAtoms::name means <a name="...">.
MOZ_KNOWN_LIVE nsStaticAtom* const mHTMLProperty = nullptr;
// For some mHTMLProperty values, need to be set to its attribute name.
// E.g., nsGkAtoms::size and nsGkAtoms::face for nsGkAtoms::font.
// Otherwise, nullptr.
// TODO: Once we stop using these structure to wrap selected content nodes
// with <a href> elements, we can make this nsStaticAtom*.
MOZ_KNOWN_LIVE const RefPtr<nsAtom> mAttribute;
/**
* Returns true if the style means that all inline styles should be removed.
*/
[[nodiscard]] bool IsStyleToClearAllInlineStyles() const {
return !mHTMLProperty;
}
/**
* Returns true if the style is about <a>.
*/
[[nodiscard]] bool IsStyleOfAnchorElement() const {
return mHTMLProperty == nsGkAtoms::a || mHTMLProperty == nsGkAtoms::href ||
mHTMLProperty == nsGkAtoms::name;
}
/**
* Returns true if the style is invertible with CSS.
*/
[[nodiscard]] bool IsInvertibleWithCSS() const {
return mHTMLProperty == nsGkAtoms::b;
}
/**
* Returns true if the style can be specified with text-decoration.
*/
enum class IgnoreSElement { No, Yes };
[[nodiscard]] bool IsStyleOfTextDecoration(
IgnoreSElement aIgnoreSElement) const {
return mHTMLProperty == nsGkAtoms::u ||
mHTMLProperty == nsGkAtoms::strike ||
(aIgnoreSElement == IgnoreSElement::No &&
mHTMLProperty == nsGkAtoms::s);
}
/**
* Returns true if the style is conflict with vertical-align even though
* they are not mapped to vertical-align in the CSS mode.
*/
[[nodiscard]] bool IsStyleConflictingWithVerticalAlign() const {
return mHTMLProperty == nsGkAtoms::sup || mHTMLProperty == nsGkAtoms::sub;
}
/**
* If the style has a similar element which should be removed when applying
* the style, this retuns an element name. Otherwise, returns nullptr.
*/
[[nodiscard]] nsStaticAtom* GetSimilarElementNameAtom() const {
if (mHTMLProperty == nsGkAtoms::b) {
return nsGkAtoms::strong;
}
if (mHTMLProperty == nsGkAtoms::i) {
return nsGkAtoms::em;
}
if (mHTMLProperty == nsGkAtoms::strike) {
return nsGkAtoms::s;
}
return nullptr;
}
/**
* Returns true if aContent is an HTML element and represents the style.
*/
[[nodiscard]] bool IsRepresentedBy(const nsIContent& aContent) const;
explicit EditorInlineStyle(nsStaticAtom& aHTMLProperty,
nsAtom* aAttribute = nullptr)
: mHTMLProperty(&aHTMLProperty), mAttribute(aAttribute) {}
EditorInlineStyle(nsStaticAtom& aHTMLProperty, RefPtr<nsAtom>&& aAttribute)
: mHTMLProperty(&aHTMLProperty), mAttribute(std::move(aAttribute)) {}
/**
* Returns the instance which means remove all inline styles.
*/
static EditorInlineStyle RemoveAllStyles() { return EditorInlineStyle(); }
PendingStyleCache ToPendingStyleCache(nsAString&& aValue) const;
bool operator==(const EditorInlineStyle& aOther) const {
return mHTMLProperty == aOther.mHTMLProperty &&
mAttribute == aOther.mAttribute;
}
private:
EditorInlineStyle() = default;
using EditorElementStyle::AsInlineStyle;
using EditorElementStyle::IsInlineStyle;
using EditorElementStyle::Style;
};
inline EditorInlineStyle& EditorElementStyle::AsInlineStyle() {
return reinterpret_cast<EditorInlineStyle&>(*this);
}
inline const EditorInlineStyle& EditorElementStyle::AsInlineStyle() const {
return reinterpret_cast<const EditorInlineStyle&>(*this);
}
/******************************************************************************
* EditorInlineStyleAndValue represents an inline style and stores its value.
******************************************************************************/
struct MOZ_STACK_CLASS EditorInlineStyleAndValue : public EditorInlineStyle {
// Stores the value of mAttribute.
nsString const mAttributeValue;
bool IsStyleToClearAllInlineStyles() const = delete;
EditorInlineStyleAndValue() = delete;
explicit EditorInlineStyleAndValue(nsStaticAtom& aHTMLProperty)
: EditorInlineStyle(aHTMLProperty) {}
EditorInlineStyleAndValue(nsStaticAtom& aHTMLProperty, nsAtom& aAttribute,
const nsAString& aValue)
: EditorInlineStyle(aHTMLProperty, &aAttribute),
mAttributeValue(aValue) {}
EditorInlineStyleAndValue(nsStaticAtom& aHTMLProperty,
RefPtr<nsAtom>&& aAttribute,
const nsAString& aValue)
: EditorInlineStyle(aHTMLProperty, std::move(aAttribute)),
mAttributeValue(aValue) {
MOZ_ASSERT(mAttribute);
}
EditorInlineStyleAndValue(nsStaticAtom& aHTMLProperty, nsAtom& aAttribute,
nsString&& aValue)
: EditorInlineStyle(aHTMLProperty, &aAttribute),
mAttributeValue(std::move(aValue)) {}
EditorInlineStyleAndValue(nsStaticAtom& aHTMLProperty,
RefPtr<nsAtom>&& aAttribute, nsString&& aValue)
: EditorInlineStyle(aHTMLProperty, std::move(aAttribute)),
mAttributeValue(aValue) {}
[[nodiscard]] static EditorInlineStyleAndValue ToInvert(
const EditorInlineStyle& aStyle) {
MOZ_ASSERT(aStyle.IsInvertibleWithCSS());
return EditorInlineStyleAndValue(aStyle, u"-moz-editor-invert-value"_ns);
}
// mHTMLProperty is never nullptr since all constructors guarantee it.
// Therefore, hide it and expose its reference instead.
MOZ_KNOWN_LIVE nsStaticAtom& HTMLPropertyRef() const {
MOZ_DIAGNOSTIC_ASSERT(mHTMLProperty);
return *mHTMLProperty;
}
[[nodiscard]] bool IsStyleToInvert() const {
return mAttributeValue.EqualsLiteral(u"-moz-editor-invert-value");
}
private:
using EditorInlineStyle::mHTMLProperty;
EditorInlineStyleAndValue(const EditorInlineStyle& aStyle,
const nsAString& aValue)
: EditorInlineStyle(aStyle), mAttributeValue(aValue) {}
};
} // namespace mozilla
#endif // #ifndef HTMLEditHelpers_h