/* -*- 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 "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>& aArrayOfNodes) const; template void DOMIterator::AppendAllNodesToArray( nsTArray>& aArrayOfNodes) const; template void DOMIterator::AppendNodesToArray( BoolFunctor aFunctor, nsTArray>& aArrayOfNodes, void* aClosure) const; template void DOMIterator::AppendNodesToArray( BoolFunctor aFunctor, nsTArray>& aArrayOfNodes, void* aClosure) const; template void DOMIterator::AppendNodesToArray( BoolFunctor aFunctor, nsTArray>& 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 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 = 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 frameSelection = selection->GetFrameSelection(); if (NS_WARN_IF(!frameSelection)) { return Err(NS_ERROR_NOT_INITIALIZED); } Result, nsresult> result(NS_ERROR_UNEXPECTED); nsIEditor::EDirection directionAndAmountResult = aDirectionAndAmount; switch (aDirectionAndAmount) { case nsIEditor::eNextWord: result = frameSelection->CreateRangeExtendedToNextWordBoundary(); 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(); 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(); 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(); 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(); 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(); 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 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 found = false; for (OwningNonNull& range : mRanges) { if (range == mAnchorFocusRange) { range = *extendedRange; found = true; break; } } MOZ_ASSERT(found); mAnchorFocusRange.swap(extendedRange); return directionAndAmountResult; } Result 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 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 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 void DOMIterator::AppendAllNodesToArray( nsTArray>& aArrayOfNodes) const { for (; !mIter->IsDone(); mIter->Next()) { if (NodeClass* node = NodeClass::FromNode(mIter->GetCurrentNode())) { aArrayOfNodes.AppendElement(*node); } } } template void DOMIterator::AppendNodesToArray( BoolFunctor aFunctor, nsTArray>& 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 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 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