/* -*- 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 "EditorDOMPoint.h" // for EditorDOMPoint, EditorDOMRange, etc #include "HTMLEditHelpers.h" // for MoveNodeResult #include "HTMLEditUtils.h" // for HTMLEditUtils #include "TextEditor.h" // for TextEditor #include "mozilla/ComputedStyle.h" // for ComputedStyle #include "mozilla/IntegerRange.h" // for IntegerRange #include "mozilla/dom/Document.h" // for dom::Document #include "mozilla/dom/Selection.h" // for dom::Selection #include "mozilla/dom/Text.h" // for dom::Text #include "nsComponentManagerUtils.h" // for do_CreateInstance #include "nsContentUtils.h" // for nsContentUtils #include "nsComputedDOMStyle.h" // for nsComputedDOMStyle #include "nsError.h" // for NS_SUCCESS_* and NS_ERROR_* #include "nsFrameSelection.h" // for nsFrameSelection #include "nsIContent.h" // for nsIContent #include "nsINode.h" // for nsINode #include "nsITransferable.h" // for nsITransferable #include "nsRange.h" // for nsRange #include "nsStyleConsts.h" // for StyleWhiteSpace #include "nsStyleStruct.h" // for nsStyleText, etc namespace mozilla { using namespace dom; /****************************************************************************** * mozilla::EditActionResult *****************************************************************************/ EditActionResult& EditActionResult::operator|=( const MoveNodeResult& aMoveNodeResult) { mHandled |= aMoveNodeResult.Handled(); return *this; } /****************************************************************************** * 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 Maybe EditorUtils::GetComputedWhiteSpaceStyle( const nsIContent& aContent) { if (MOZ_UNLIKELY(!aContent.IsElement() && !aContent.GetParentElement())) { return Nothing(); } RefPtr elementStyle = nsComputedDOMStyle::GetComputedStyleNoFlush( aContent.IsElement() ? aContent.AsElement() : aContent.GetParentElement()); if (NS_WARN_IF(!elementStyle)) { return Nothing(); } return Some(elementStyle->StyleText()->mWhiteSpace); } // static bool EditorUtils::IsWhiteSpacePreformatted(const 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); 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(); } // static bool EditorUtils::IsNewLinePreformatted(const 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); 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()->NewlineIsSignificantStyle(); } // static bool EditorUtils::IsOnlyNewLinePreformatted(const 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); 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()->mWhiteSpace == StyleWhiteSpace::PreLine; } bool EditorUtils::IsPointInSelection(const Selection& aSelection, const nsINode& aParentNode, uint32_t aOffset) { if (aSelection.IsCollapsed()) { return false; } const uint32_t rangeCount = aSelection.RangeCount(); for (const uint32_t i : IntegerRange(rangeCount)) { MOZ_ASSERT(aSelection.RangeCount() == rangeCount); RefPtr range = aSelection.GetRangeAt(i); if (MOZ_UNLIKELY(NS_WARN_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; } // static Result, nsresult> EditorUtils::CreateTransferableForPlainText(const Document& aDocument) { // Create generic Transferable for getting the data nsresult rv; nsCOMPtr transferable = do_CreateInstance("@mozilla.org/widget/transferable;1", &rv); if (NS_FAILED(rv)) { NS_WARNING("do_CreateInstance() failed to create nsITransferable instance"); return Err(rv); } if (!transferable) { NS_WARNING("do_CreateInstance() returned nullptr, but ignored"); return nsCOMPtr(); } DebugOnly rvIgnored = transferable->Init(aDocument.GetLoadContext()); NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "nsITransferable::Init() failed, but ignored"); rvIgnored = transferable->AddDataFlavor(kUnicodeMime); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "nsITransferable::AddDataFlavor(kUnicodeMime) failed, but ignored"); rvIgnored = transferable->AddDataFlavor(kMozTextInternal); NS_WARNING_ASSERTION( NS_SUCCEEDED(rvIgnored), "nsITransferable::AddDataFlavor(kMozTextInternal) failed, but ignored"); return transferable; } /****************************************************************************** * mozilla::EditorDOMPointBase *****************************************************************************/ NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool, IsCharCollapsibleASCIISpace); template bool EditorDOMPointBase::IsCharCollapsibleASCIISpace() const { if (IsCharNewLine()) { return !EditorUtils::IsNewLinePreformatted(*ContainerAs()); } return IsCharASCIISpace() && !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs()); } NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool, IsCharCollapsibleNBSP); template bool EditorDOMPointBase::IsCharCollapsibleNBSP() const { // TODO: Perhaps, we should return false if neither previous char nor // next char is collapsible white-space or NBSP. return IsCharNBSP() && !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs()); } NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool, IsCharCollapsibleASCIISpaceOrNBSP); template bool EditorDOMPointBase::IsCharCollapsibleASCIISpaceOrNBSP() const { if (IsCharNewLine()) { return !EditorUtils::IsNewLinePreformatted(*ContainerAs()); } return IsCharASCIISpaceOrNBSP() && !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs()); } NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD( bool, IsPreviousCharCollapsibleASCIISpace); template bool EditorDOMPointBase::IsPreviousCharCollapsibleASCIISpace() const { if (IsPreviousCharNewLine()) { return !EditorUtils::IsNewLinePreformatted(*ContainerAs()); } return IsPreviousCharASCIISpace() && !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs()); } NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool, IsPreviousCharCollapsibleNBSP); template bool EditorDOMPointBase::IsPreviousCharCollapsibleNBSP() const { return IsPreviousCharNBSP() && !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs()); } NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD( bool, IsPreviousCharCollapsibleASCIISpaceOrNBSP); template bool EditorDOMPointBase::IsPreviousCharCollapsibleASCIISpaceOrNBSP() const { if (IsPreviousCharNewLine()) { return !EditorUtils::IsNewLinePreformatted(*ContainerAs()); } return IsPreviousCharASCIISpaceOrNBSP() && !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs()); } NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool, IsNextCharCollapsibleASCIISpace); template bool EditorDOMPointBase::IsNextCharCollapsibleASCIISpace() const { if (IsNextCharNewLine()) { return !EditorUtils::IsNewLinePreformatted(*ContainerAs()); } return IsNextCharASCIISpace() && !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs()); } NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool, IsNextCharCollapsibleNBSP); template bool EditorDOMPointBase::IsNextCharCollapsibleNBSP() const { return IsNextCharNBSP() && !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs()); } NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD( bool, IsNextCharCollapsibleASCIISpaceOrNBSP); template bool EditorDOMPointBase::IsNextCharCollapsibleASCIISpaceOrNBSP() const { if (IsNextCharNewLine()) { return !EditorUtils::IsNewLinePreformatted(*ContainerAs()); } return IsNextCharASCIISpaceOrNBSP() && !EditorUtils::IsWhiteSpacePreformatted(*ContainerAs()); } NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool, IsCharPreformattedNewLine); template bool EditorDOMPointBase::IsCharPreformattedNewLine() const { return IsCharNewLine() && EditorUtils::IsNewLinePreformatted(*ContainerAs()); } NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD( bool, IsCharPreformattedNewLineCollapsedWithWhiteSpaces); template bool EditorDOMPointBase< PT, CT>::IsCharPreformattedNewLineCollapsedWithWhiteSpaces() const { return IsCharNewLine() && EditorUtils::IsOnlyNewLinePreformatted(*ContainerAs()); } NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool, IsPreviousCharPreformattedNewLine); template bool EditorDOMPointBase::IsPreviousCharPreformattedNewLine() const { return IsPreviousCharNewLine() && EditorUtils::IsNewLinePreformatted(*ContainerAs()); } NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD( bool, IsPreviousCharPreformattedNewLineCollapsedWithWhiteSpaces); template bool EditorDOMPointBase< PT, CT>::IsPreviousCharPreformattedNewLineCollapsedWithWhiteSpaces() const { return IsPreviousCharNewLine() && EditorUtils::IsOnlyNewLinePreformatted(*ContainerAs()); } NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD(bool, IsNextCharPreformattedNewLine); template bool EditorDOMPointBase::IsNextCharPreformattedNewLine() const { return IsNextCharNewLine() && EditorUtils::IsNewLinePreformatted(*ContainerAs()); } NS_INSTANTIATE_EDITOR_DOM_POINT_CONST_METHOD( bool, IsNextCharPreformattedNewLineCollapsedWithWhiteSpaces); template bool EditorDOMPointBase< PT, CT>::IsNextCharPreformattedNewLineCollapsedWithWhiteSpaces() const { return IsNextCharNewLine() && EditorUtils::IsOnlyNewLinePreformatted(*ContainerAs()); } /****************************************************************************** * mozilla::EditorDOMRangeBase *****************************************************************************/ NS_INSTANTIATE_EDITOR_DOM_RANGE_CONST_METHOD(nsINode*, GetClosestCommonInclusiveAncestor); template nsINode* EditorDOMRangeBase< EditorDOMPointType>::GetClosestCommonInclusiveAncestor() const { if (NS_WARN_IF(!IsPositioned())) { return nullptr; } return nsContentUtils::GetClosestCommonInclusiveAncestor( mStart.GetContainer(), mEnd.GetContainer()); } /****************************************************************************** * mozilla::CaretPoint *****************************************************************************/ nsresult CaretPoint::SuggestCaretPointTo( const EditorBase& aEditorBase, const SuggestCaretOptions& aOptions) const { mHandledCaretPoint = true; if (!mCaretPoint.IsSet()) { if (aOptions.contains(SuggestCaret::OnlyIfHasSuggestion)) { return NS_OK; } NS_WARNING("There was no suggestion to put caret"); return NS_ERROR_FAILURE; } if (aOptions.contains(SuggestCaret::OnlyIfTransactionsAllowedToDoIt) && !aEditorBase.AllowsTransactionsToChangeSelection()) { return NS_OK; } nsresult rv = aEditorBase.CollapseSelectionTo(mCaretPoint); if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { NS_WARNING( "EditorBase::CollapseSelectionTo() caused destroying the editor"); return NS_ERROR_EDITOR_DESTROYED; } return aOptions.contains(SuggestCaret::AndIgnoreTrivialError) && NS_FAILED(rv) ? NS_SUCCESS_EDITOR_BUT_IGNORED_TRIVIAL_ERROR : rv; } bool CaretPoint::CopyCaretPointTo(EditorDOMPoint& aPointToPutCaret, const EditorBase& aEditorBase, const SuggestCaretOptions& aOptions) const { MOZ_ASSERT(!aOptions.contains(SuggestCaret::AndIgnoreTrivialError)); mHandledCaretPoint = true; if (aOptions.contains(SuggestCaret::OnlyIfHasSuggestion) && !mCaretPoint.IsSet()) { return false; } if (aOptions.contains(SuggestCaret::OnlyIfTransactionsAllowedToDoIt) && !aEditorBase.AllowsTransactionsToChangeSelection()) { return false; } aPointToPutCaret = mCaretPoint; return true; } bool CaretPoint::MoveCaretPointTo(EditorDOMPoint& aPointToPutCaret, const EditorBase& aEditorBase, const SuggestCaretOptions& aOptions) { MOZ_ASSERT(!aOptions.contains(SuggestCaret::AndIgnoreTrivialError)); mHandledCaretPoint = true; if (aOptions.contains(SuggestCaret::OnlyIfHasSuggestion) && !mCaretPoint.IsSet()) { return false; } if (aOptions.contains(SuggestCaret::OnlyIfTransactionsAllowedToDoIt) && !aEditorBase.AllowsTransactionsToChangeSelection()) { return false; } aPointToPutCaret = UnwrapCaretPoint(); return true; } } // namespace mozilla