diff options
Diffstat (limited to 'editor/libeditor/EditorUtils.cpp')
-rw-r--r-- | editor/libeditor/EditorUtils.cpp | 522 |
1 files changed, 522 insertions, 0 deletions
diff --git a/editor/libeditor/EditorUtils.cpp b/editor/libeditor/EditorUtils.cpp new file mode 100644 index 0000000000..42e3f36d7b --- /dev/null +++ b/editor/libeditor/EditorUtils.cpp @@ -0,0 +1,522 @@ +/* -*- 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/. */ + +#include "EditorUtils.h" + +#include "gfxFontUtils.h" +#include "WSRunObject.h" +#include "mozilla/ComputedStyle.h" +#include "mozilla/ContentIterator.h" +#include "mozilla/EditorDOMPoint.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/TextEditor.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/HTMLBRElement.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/Text.h" +#include "nsContentUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsComputedDOMStyle.h" +#include "nsError.h" +#include "nsFrameSelection.h" +#include "nsIContent.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsINode.h" +#include "nsRange.h" +#include "nsStyleStruct.h" +#include "nsTextFragment.h" + +class nsISupports; + +namespace mozilla { + +using namespace dom; + +template void DOMIterator::AppendAllNodesToArray( + nsTArray<OwningNonNull<nsIContent>>& aArrayOfNodes) const; +template void DOMIterator::AppendAllNodesToArray( + nsTArray<OwningNonNull<HTMLBRElement>>& aArrayOfNodes) const; +template void DOMIterator::AppendNodesToArray( + BoolFunctor aFunctor, nsTArray<OwningNonNull<nsIContent>>& aArrayOfNodes, + void* aClosure) const; +template void DOMIterator::AppendNodesToArray( + BoolFunctor aFunctor, nsTArray<OwningNonNull<Element>>& aArrayOfNodes, + void* aClosure) const; +template void DOMIterator::AppendNodesToArray( + BoolFunctor aFunctor, nsTArray<OwningNonNull<Text>>& aArrayOfNodes, + void* aClosure) const; + +/****************************************************************************** + * mozilla::EditActionResult + *****************************************************************************/ + +EditActionResult& EditActionResult::operator|=( + const MoveNodeResult& aMoveNodeResult) { + mHandled |= aMoveNodeResult.Handled(); + // When both result are same, keep the result. + if (mRv == aMoveNodeResult.Rv()) { + return *this; + } + // If one of the result is NS_ERROR_EDITOR_DESTROYED, use it since it's + // the most important error code for editor. + if (EditorDestroyed() || aMoveNodeResult.EditorDestroyed()) { + mRv = NS_ERROR_EDITOR_DESTROYED; + return *this; + } + // If aMoveNodeResult hasn't been set explicit nsresult value, keep current + // result. + if (aMoveNodeResult.Rv() == NS_ERROR_NOT_INITIALIZED) { + return *this; + } + // If one of the results is error, use NS_ERROR_FAILURE. + if (Failed() || aMoveNodeResult.Failed()) { + mRv = NS_ERROR_FAILURE; + return *this; + } + // Otherwise, use generic success code, NS_OK. + mRv = NS_OK; + return *this; +} + +/****************************************************************************** + * mozilla::AutoRangeArray + *****************************************************************************/ + +Result<nsIEditor::EDirection, nsresult> +AutoRangeArray::ExtendAnchorFocusRangeFor( + const EditorBase& aEditorBase, nsIEditor::EDirection aDirectionAndAmount) { + MOZ_ASSERT(aEditorBase.IsEditActionDataAvailable()); + MOZ_ASSERT(mAnchorFocusRange); + MOZ_ASSERT(mAnchorFocusRange->IsPositioned()); + MOZ_ASSERT(mAnchorFocusRange->StartRef().IsSet()); + MOZ_ASSERT(mAnchorFocusRange->EndRef().IsSet()); + + if (!EditorUtils::IsFrameSelectionRequiredToExtendSelection( + aDirectionAndAmount, *this)) { + return aDirectionAndAmount; + } + + const RefPtr<Selection>& selection = aEditorBase.SelectionRefPtr(); + if (NS_WARN_IF(!selection->RangeCount())) { + return Err(NS_ERROR_FAILURE); + } + + // At this point, the anchor-focus ranges must match for bidi information. + // See `EditorBase::AutoCaretBidiLevelManager`. + MOZ_ASSERT(selection->GetAnchorFocusRange()->StartRef() == + mAnchorFocusRange->StartRef()); + MOZ_ASSERT(selection->GetAnchorFocusRange()->EndRef() == + mAnchorFocusRange->EndRef()); + + RefPtr<nsFrameSelection> frameSelection = selection->GetFrameSelection(); + if (NS_WARN_IF(!frameSelection)) { + return Err(NS_ERROR_NOT_INITIALIZED); + } + + Result<RefPtr<nsRange>, nsresult> result(NS_ERROR_UNEXPECTED); + nsIEditor::EDirection directionAndAmountResult = aDirectionAndAmount; + switch (aDirectionAndAmount) { + case nsIEditor::eNextWord: + result = frameSelection->CreateRangeExtendedToNextWordBoundary<nsRange>(); + if (NS_WARN_IF(aEditorBase.Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION( + result.isOk(), + "nsFrameSelection::CreateRangeExtendedToNextWordBoundary() failed"); + // DeleteSelectionWithTransaction() doesn't handle these actions + // because it's inside batching, so don't confuse it: + directionAndAmountResult = nsIEditor::eNone; + break; + case nsIEditor::ePreviousWord: + result = + frameSelection->CreateRangeExtendedToPreviousWordBoundary<nsRange>(); + if (NS_WARN_IF(aEditorBase.Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION( + result.isOk(), + "nsFrameSelection::CreateRangeExtendedToPreviousWordBoundary() " + "failed"); + // DeleteSelectionWithTransaction() doesn't handle these actions + // because it's inside batching, so don't confuse it: + directionAndAmountResult = nsIEditor::eNone; + break; + case nsIEditor::eNext: + result = + frameSelection + ->CreateRangeExtendedToNextGraphemeClusterBoundary<nsRange>(); + if (NS_WARN_IF(aEditorBase.Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION(result.isOk(), + "nsFrameSelection::" + "CreateRangeExtendedToNextGraphemeClusterBoundary() " + "failed"); + // Don't set directionAndAmount to eNone (see Bug 502259) + break; + case nsIEditor::ePrevious: { + // Only extend the selection where the selection is after a UTF-16 + // surrogate pair or a variation selector. + // For other cases we don't want to do that, in order + // to make sure that pressing backspace will only delete the last + // typed character. + // XXX This is odd if the previous one is a sequence for a grapheme + // cluster. + EditorDOMPoint atStartOfSelection(GetStartPointOfFirstRange()); + if (NS_WARN_IF(!atStartOfSelection.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + + // node might be anonymous DIV, so we find better text node + EditorRawDOMPoint insertionPoint = + aEditorBase.FindBetterInsertionPoint(atStartOfSelection); + if (!insertionPoint.IsSet()) { + NS_WARNING( + "EditorBase::FindBetterInsertionPoint() failed, but ignored"); + return aDirectionAndAmount; + } + + if (!insertionPoint.IsInTextNode()) { + return aDirectionAndAmount; + } + + const nsTextFragment* data = + &insertionPoint.GetContainerAsText()->TextFragment(); + uint32_t offset = insertionPoint.Offset(); + if (!(offset > 1 && + data->IsLowSurrogateFollowingHighSurrogateAt(offset - 1)) && + !(offset > 0 && + gfxFontUtils::IsVarSelector(data->CharAt(offset - 1)))) { + return aDirectionAndAmount; + } + // Different from the `eNext` case, we look for character boundary. + // I'm not sure whether this inconsistency between "Delete" and + // "Backspace" is intentional or not. + result = frameSelection + ->CreateRangeExtendedToPreviousCharacterBoundary<nsRange>(); + if (NS_WARN_IF(aEditorBase.Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION( + result.isOk(), + "nsFrameSelection::" + "CreateRangeExtendedToPreviousGraphemeClusterBoundary() failed"); + break; + } + case nsIEditor::eToBeginningOfLine: + result = + frameSelection->CreateRangeExtendedToPreviousHardLineBreak<nsRange>(); + if (NS_WARN_IF(aEditorBase.Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION( + result.isOk(), + "nsFrameSelection::CreateRangeExtendedToPreviousHardLineBreak() " + "failed"); + directionAndAmountResult = nsIEditor::eNone; + break; + case nsIEditor::eToEndOfLine: + result = + frameSelection->CreateRangeExtendedToNextHardLineBreak<nsRange>(); + if (NS_WARN_IF(aEditorBase.Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION( + result.isOk(), + "nsFrameSelection::CreateRangeExtendedToNextHardLineBreak() failed"); + directionAndAmountResult = nsIEditor::eNext; + break; + default: + return aDirectionAndAmount; + } + + if (result.isErr()) { + return Err(result.inspectErr()); + } + RefPtr<nsRange> extendedRange(result.unwrap().forget()); + if (!extendedRange || NS_WARN_IF(!extendedRange->IsPositioned())) { + NS_WARNING("Failed to extend the range, but ignored"); + return directionAndAmountResult; + } + + if (NS_WARN_IF(!frameSelection->IsValidSelectionPoint( + extendedRange->GetStartContainer())) || + NS_WARN_IF(!frameSelection->IsValidSelectionPoint( + extendedRange->GetEndContainer()))) { + NS_WARNING("A range was extended to outer of selection limiter"); + return Err(NS_ERROR_FAILURE); + } + + // Swap focus/anchor range with the extended range. + DebugOnly<bool> found = false; + for (OwningNonNull<nsRange>& range : mRanges) { + if (range == mAnchorFocusRange) { + range = *extendedRange; + found = true; + break; + } + } + MOZ_ASSERT(found); + mAnchorFocusRange.swap(extendedRange); + return directionAndAmountResult; +} + +Result<bool, nsresult> +AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent( + const HTMLEditor& aHTMLEditor, nsIEditor::EDirection aDirectionAndAmount, + IfSelectingOnlyOneAtomicContent aIfSelectingOnlyOneAtomicContent, + const Element* aEditingHost) { + if (IsCollapsed()) { + return false; + } + + switch (aDirectionAndAmount) { + case nsIEditor::eNext: + case nsIEditor::eNextWord: + case nsIEditor::ePrevious: + case nsIEditor::ePreviousWord: + break; + default: + return false; + } + + bool changed = false; + for (auto& range : mRanges) { + MOZ_ASSERT(!range->IsInSelection(), + "Changing range in selection may cause running script"); + Result<bool, nsresult> result = + WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent( + aHTMLEditor, range, aEditingHost); + if (result.isErr()) { + NS_WARNING( + "WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent() " + "failed"); + return Err(result.inspectErr()); + } + changed |= result.inspect(); + } + + if (mRanges.Length() == 1 && aIfSelectingOnlyOneAtomicContent == + IfSelectingOnlyOneAtomicContent::Collapse) { + MOZ_ASSERT(mRanges[0].get() == mAnchorFocusRange.get()); + if (mAnchorFocusRange->GetStartContainer() == + mAnchorFocusRange->GetEndContainer() && + mAnchorFocusRange->GetChildAtStartOffset() && + mAnchorFocusRange->StartRef().GetNextSiblingOfChildAtOffset() == + mAnchorFocusRange->GetChildAtEndOffset()) { + mAnchorFocusRange->Collapse(aDirectionAndAmount == nsIEditor::eNext || + aDirectionAndAmount == nsIEditor::eNextWord); + changed = true; + } + } + + return changed; +} + +/****************************************************************************** + * some helper classes for iterating the dom tree + *****************************************************************************/ + +DOMIterator::DOMIterator(nsINode& aNode) : mIter(&mPostOrderIter) { + DebugOnly<nsresult> rv = mIter->Init(&aNode); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +nsresult DOMIterator::Init(nsRange& aRange) { return mIter->Init(&aRange); } + +nsresult DOMIterator::Init(const RawRangeBoundary& aStartRef, + const RawRangeBoundary& aEndRef) { + return mIter->Init(aStartRef, aEndRef); +} + +DOMIterator::DOMIterator() : mIter(&mPostOrderIter) {} + +template <class NodeClass> +void DOMIterator::AppendAllNodesToArray( + nsTArray<OwningNonNull<NodeClass>>& aArrayOfNodes) const { + for (; !mIter->IsDone(); mIter->Next()) { + if (NodeClass* node = NodeClass::FromNode(mIter->GetCurrentNode())) { + aArrayOfNodes.AppendElement(*node); + } + } +} + +template <class NodeClass> +void DOMIterator::AppendNodesToArray( + BoolFunctor aFunctor, nsTArray<OwningNonNull<NodeClass>>& aArrayOfNodes, + void* aClosure /* = nullptr */) const { + for (; !mIter->IsDone(); mIter->Next()) { + NodeClass* node = NodeClass::FromNode(mIter->GetCurrentNode()); + if (node && aFunctor(*node, aClosure)) { + aArrayOfNodes.AppendElement(*node); + } + } +} + +DOMSubtreeIterator::DOMSubtreeIterator() : DOMIterator() { + mIter = &mSubtreeIter; +} + +nsresult DOMSubtreeIterator::Init(nsRange& aRange) { + return mIter->Init(&aRange); +} + +/****************************************************************************** + * some general purpose editor utils + *****************************************************************************/ + +bool EditorUtils::IsDescendantOf(const nsINode& aNode, const nsINode& aParent, + EditorRawDOMPoint* aOutPoint /* = nullptr */) { + if (aOutPoint) { + aOutPoint->Clear(); + } + + if (&aNode == &aParent) { + return false; + } + + for (const nsINode* node = &aNode; node; node = node->GetParentNode()) { + if (node->GetParentNode() == &aParent) { + if (aOutPoint) { + MOZ_ASSERT(node->IsContent()); + aOutPoint->Set(node->AsContent()); + } + return true; + } + } + + return false; +} + +bool EditorUtils::IsDescendantOf(const nsINode& aNode, const nsINode& aParent, + EditorDOMPoint* aOutPoint) { + MOZ_ASSERT(aOutPoint); + aOutPoint->Clear(); + if (&aNode == &aParent) { + return false; + } + + for (const nsINode* node = &aNode; node; node = node->GetParentNode()) { + if (node->GetParentNode() == &aParent) { + MOZ_ASSERT(node->IsContent()); + aOutPoint->Set(node->AsContent()); + return true; + } + } + + return false; +} + +// static +void EditorUtils::MaskString(nsString& aString, Text* aText, + uint32_t aStartOffsetInString, + uint32_t aStartOffsetInText) { + MOZ_ASSERT(aText->HasFlag(NS_MAYBE_MASKED)); + MOZ_ASSERT(aStartOffsetInString == 0 || aStartOffsetInText == 0); + + uint32_t unmaskStart = UINT32_MAX, unmaskLength = 0; + TextEditor* textEditor = + nsContentUtils::GetTextEditorFromAnonymousNodeWithoutCreation(aText); + if (textEditor && textEditor->UnmaskedLength() > 0) { + unmaskStart = textEditor->UnmaskedStart(); + unmaskLength = textEditor->UnmaskedLength(); + // If text is copied from after unmasked range, we can treat this case + // as mask all. + if (aStartOffsetInText >= unmaskStart + unmaskLength) { + unmaskLength = 0; + unmaskStart = UINT32_MAX; + } else { + // If text is copied from middle of unmasked range, reduce the length + // and adjust start offset. + if (aStartOffsetInText > unmaskStart) { + unmaskLength = unmaskStart + unmaskLength - aStartOffsetInText; + unmaskStart = 0; + } + // If text is copied from before start of unmasked range, just adjust + // the start offset. + else { + unmaskStart -= aStartOffsetInText; + } + // Make the range is in the string. + unmaskStart += aStartOffsetInString; + } + } + + const char16_t kPasswordMask = TextEditor::PasswordMask(); + for (uint32_t i = aStartOffsetInString; i < aString.Length(); ++i) { + bool isSurrogatePair = NS_IS_HIGH_SURROGATE(aString.CharAt(i)) && + i < aString.Length() - 1 && + NS_IS_LOW_SURROGATE(aString.CharAt(i + 1)); + if (i < unmaskStart || i >= unmaskStart + unmaskLength) { + if (isSurrogatePair) { + aString.SetCharAt(kPasswordMask, i); + aString.SetCharAt(kPasswordMask, i + 1); + } else { + aString.SetCharAt(kPasswordMask, i); + } + } + + // Skip the following low surrogate. + if (isSurrogatePair) { + ++i; + } + } +} + +// static +bool EditorUtils::IsContentPreformatted(nsIContent& aContent) { + // Look at the node (and its parent if it's not an element), and grab its + // ComputedStyle. + Element* element = aContent.GetAsElementOrParentElement(); + if (!element) { + return false; + } + + RefPtr<ComputedStyle> elementStyle = + nsComputedDOMStyle::GetComputedStyleNoFlush(element, nullptr); + if (!elementStyle) { + // Consider nodes without a ComputedStyle to be NOT preformatted: + // For instance, this is true of JS tags inside the body (which show + // up as #text nodes but have no ComputedStyle). + return false; + } + + return elementStyle->StyleText()->WhiteSpaceIsSignificant(); +} + +bool EditorUtils::IsPointInSelection(const Selection& aSelection, + const nsINode& aParentNode, + int32_t aOffset) { + if (aSelection.IsCollapsed()) { + return false; + } + + uint32_t rangeCount = aSelection.RangeCount(); + for (uint32_t i = 0; i < rangeCount; i++) { + RefPtr<const nsRange> range = aSelection.GetRangeAt(i); + if (!range) { + // Don't bail yet, iterate through them all + continue; + } + + IgnoredErrorResult ignoredError; + bool nodeIsInSelection = + range->IsPointInRange(aParentNode, aOffset, ignoredError) && + !ignoredError.Failed(); + NS_WARNING_ASSERTION(!ignoredError.Failed(), + "nsRange::IsPointInRange() failed"); + + // Done when we find a range that we are in + if (nodeIsInSelection) { + return true; + } + } + + return false; +} + +} // namespace mozilla |