/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 sw=2 et tw=79: */ /* 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 "HTMLEditRules.h" #include <stdlib.h> #include "HTMLEditUtils.h" #include "TextEditUtils.h" #include "WSRunObject.h" #include "mozilla/Assertions.h" #include "mozilla/CSSEditUtils.h" #include "mozilla/EditorUtils.h" #include "mozilla/HTMLEditor.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/Preferences.h" #include "mozilla/dom/Selection.h" #include "mozilla/dom/Element.h" #include "mozilla/OwningNonNull.h" #include "mozilla/mozalloc.h" #include "nsAutoPtr.h" #include "nsAString.h" #include "nsAlgorithm.h" #include "nsCRT.h" #include "nsCRTGlue.h" #include "nsComponentManagerUtils.h" #include "nsContentUtils.h" #include "nsDebug.h" #include "nsError.h" #include "nsGkAtoms.h" #include "nsIAtom.h" #include "nsIContent.h" #include "nsIContentIterator.h" #include "nsID.h" #include "nsIDOMCharacterData.h" #include "nsIDOMDocument.h" #include "nsIDOMElement.h" #include "nsIDOMNode.h" #include "nsIDOMText.h" #include "nsIFrame.h" #include "nsIHTMLAbsPosEditor.h" #include "nsIHTMLDocument.h" #include "nsINode.h" #include "nsLiteralString.h" #include "nsRange.h" #include "nsReadableUtils.h" #include "nsString.h" #include "nsStringFwd.h" #include "nsTArray.h" #include "nsTextNode.h" #include "nsThreadUtils.h" #include "nsUnicharUtils.h" #include <algorithm> // Workaround for windows headers #ifdef SetProp #undef SetProp #endif class nsISupports; namespace mozilla { class RulesInfo; using namespace dom; //const static char* kMOZEditorBogusNodeAttr="MOZ_EDITOR_BOGUS_NODE"; //const static char* kMOZEditorBogusNodeValue="TRUE"; enum { kLonely = 0, kPrevSib = 1, kNextSib = 2, kBothSibs = 3 }; /******************************************************** * first some helpful functors we will use ********************************************************/ static bool IsBlockNode(const nsINode& node) { return HTMLEditor::NodeIsBlockStatic(&node); } static bool IsInlineNode(const nsINode& node) { return !IsBlockNode(node); } static bool IsStyleCachePreservingAction(EditAction action) { return action == EditAction::deleteSelection || action == EditAction::insertBreak || action == EditAction::makeList || action == EditAction::indent || action == EditAction::outdent || action == EditAction::align || action == EditAction::makeBasicBlock || action == EditAction::removeList || action == EditAction::makeDefListItem || action == EditAction::insertElement || action == EditAction::insertQuotation; } class TableCellAndListItemFunctor final : public BoolDomIterFunctor { public: // Used to build list of all li's, td's & th's iterator covers virtual bool operator()(nsINode* aNode) const { return HTMLEditUtils::IsTableCell(aNode) || HTMLEditUtils::IsListItem(aNode); } }; class BRNodeFunctor final : public BoolDomIterFunctor { public: virtual bool operator()(nsINode* aNode) const { return aNode->IsHTMLElement(nsGkAtoms::br); } }; class EmptyEditableFunctor final : public BoolDomIterFunctor { public: explicit EmptyEditableFunctor(HTMLEditor* aHTMLEditor) : mHTMLEditor(aHTMLEditor) {} virtual bool operator()(nsINode* aNode) const { if (mHTMLEditor->IsEditable(aNode) && (HTMLEditUtils::IsListItem(aNode) || HTMLEditUtils::IsTableCellOrCaption(*aNode))) { bool bIsEmptyNode; nsresult rv = mHTMLEditor->IsEmptyNode(aNode, &bIsEmptyNode, false, false); NS_ENSURE_SUCCESS(rv, false); if (bIsEmptyNode) { return true; } } return false; } protected: HTMLEditor* mHTMLEditor; }; /******************************************************** * mozilla::HTMLEditRules ********************************************************/ HTMLEditRules::HTMLEditRules() : mHTMLEditor(nullptr) , mListenerEnabled(false) , mReturnInEmptyLIKillsList(false) , mDidDeleteSelection(false) , mDidRangedDelete(false) , mRestoreContentEditableCount(false) , mJoinOffset(0) { InitFields(); } void HTMLEditRules::InitFields() { mHTMLEditor = nullptr; mDocChangeRange = nullptr; mListenerEnabled = true; mReturnInEmptyLIKillsList = true; mDidDeleteSelection = false; mDidRangedDelete = false; mRestoreContentEditableCount = false; mUtilRange = nullptr; mJoinOffset = 0; mNewBlock = nullptr; mRangeItem = new RangeItem(); // populate mCachedStyles mCachedStyles[0] = StyleCache(nsGkAtoms::b, EmptyString(), EmptyString()); mCachedStyles[1] = StyleCache(nsGkAtoms::i, EmptyString(), EmptyString()); mCachedStyles[2] = StyleCache(nsGkAtoms::u, EmptyString(), EmptyString()); mCachedStyles[3] = StyleCache(nsGkAtoms::font, NS_LITERAL_STRING("face"), EmptyString()); mCachedStyles[4] = StyleCache(nsGkAtoms::font, NS_LITERAL_STRING("size"), EmptyString()); mCachedStyles[5] = StyleCache(nsGkAtoms::font, NS_LITERAL_STRING("color"), EmptyString()); mCachedStyles[6] = StyleCache(nsGkAtoms::tt, EmptyString(), EmptyString()); mCachedStyles[7] = StyleCache(nsGkAtoms::em, EmptyString(), EmptyString()); mCachedStyles[8] = StyleCache(nsGkAtoms::strong, EmptyString(), EmptyString()); mCachedStyles[9] = StyleCache(nsGkAtoms::dfn, EmptyString(), EmptyString()); mCachedStyles[10] = StyleCache(nsGkAtoms::code, EmptyString(), EmptyString()); mCachedStyles[11] = StyleCache(nsGkAtoms::samp, EmptyString(), EmptyString()); mCachedStyles[12] = StyleCache(nsGkAtoms::var, EmptyString(), EmptyString()); mCachedStyles[13] = StyleCache(nsGkAtoms::cite, EmptyString(), EmptyString()); mCachedStyles[14] = StyleCache(nsGkAtoms::abbr, EmptyString(), EmptyString()); mCachedStyles[15] = StyleCache(nsGkAtoms::acronym, EmptyString(), EmptyString()); mCachedStyles[16] = StyleCache(nsGkAtoms::backgroundColor, EmptyString(), EmptyString()); mCachedStyles[17] = StyleCache(nsGkAtoms::sub, EmptyString(), EmptyString()); mCachedStyles[18] = StyleCache(nsGkAtoms::sup, EmptyString(), EmptyString()); } HTMLEditRules::~HTMLEditRules() { // remove ourselves as a listener to edit actions // In some cases, we have already been removed by // ~HTMLEditor, in which case we will get a null pointer here // which we ignore. But this allows us to add the ability to // switch rule sets on the fly if we want. if (mHTMLEditor) { mHTMLEditor->RemoveEditActionListener(this); } } NS_IMPL_ADDREF_INHERITED(HTMLEditRules, TextEditRules) NS_IMPL_RELEASE_INHERITED(HTMLEditRules, TextEditRules) NS_INTERFACE_TABLE_HEAD_CYCLE_COLLECTION_INHERITED(HTMLEditRules) NS_INTERFACE_TABLE_INHERITED(HTMLEditRules, nsIEditActionListener) NS_INTERFACE_TABLE_TAIL_INHERITING(TextEditRules) NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLEditRules, TextEditRules, mDocChangeRange, mUtilRange, mNewBlock, mRangeItem) NS_IMETHODIMP HTMLEditRules::Init(TextEditor* aTextEditor) { InitFields(); mHTMLEditor = static_cast<HTMLEditor*>(aTextEditor); // call through to base class Init nsresult rv = TextEditRules::Init(aTextEditor); NS_ENSURE_SUCCESS(rv, rv); // cache any prefs we care about static const char kPrefName[] = "editor.html.typing.returnInEmptyListItemClosesList"; nsAdoptingCString returnInEmptyLIKillsList = Preferences::GetCString(kPrefName); // only when "false", becomes FALSE. Otherwise (including empty), TRUE. // XXX Why was this pref designed as a string and not bool? mReturnInEmptyLIKillsList = !returnInEmptyLIKillsList.EqualsLiteral("false"); // make a utility range for use by the listenter nsCOMPtr<nsINode> node = mHTMLEditor->GetRoot(); if (!node) { node = mHTMLEditor->GetDocument(); } NS_ENSURE_STATE(node); mUtilRange = new nsRange(node); // set up mDocChangeRange to be whole doc // temporarily turn off rules sniffing AutoLockRulesSniffing lockIt(this); if (!mDocChangeRange) { mDocChangeRange = new nsRange(node); } if (node->IsElement()) { ErrorResult rv; mDocChangeRange->SelectNode(*node, rv); NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult()); AdjustSpecialBreaks(); } // add ourselves as a listener to edit actions return mHTMLEditor->AddEditActionListener(this); } NS_IMETHODIMP HTMLEditRules::DetachEditor() { if (mHTMLEditor) { mHTMLEditor->RemoveEditActionListener(this); } mHTMLEditor = nullptr; return TextEditRules::DetachEditor(); } NS_IMETHODIMP HTMLEditRules::BeforeEdit(EditAction action, nsIEditor::EDirection aDirection) { if (mLockRulesSniffing) { return NS_OK; } NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); AutoLockRulesSniffing lockIt(this); mDidExplicitlySetInterline = false; if (!mActionNesting) { mActionNesting++; // Clear our flag about if just deleted a range mDidRangedDelete = false; // Remember where our selection was before edit action took place: // Get selection RefPtr<Selection> selection = htmlEditor->GetSelection(); // Get the selection location if (NS_WARN_IF(!selection) || !selection->RangeCount()) { return NS_ERROR_UNEXPECTED; } mRangeItem->startNode = selection->GetRangeAt(0)->GetStartParent(); mRangeItem->startOffset = selection->GetRangeAt(0)->StartOffset(); mRangeItem->endNode = selection->GetRangeAt(0)->GetEndParent(); mRangeItem->endOffset = selection->GetRangeAt(0)->EndOffset(); nsCOMPtr<nsINode> selStartNode = mRangeItem->startNode; nsCOMPtr<nsINode> selEndNode = mRangeItem->endNode; // Register with range updater to track this as we perturb the doc htmlEditor->mRangeUpdater.RegisterRangeItem(mRangeItem); // Clear deletion state bool mDidDeleteSelection = false; // Clear out mDocChangeRange and mUtilRange if (mDocChangeRange) { // Clear out our accounting of what changed mDocChangeRange->Reset(); } if (mUtilRange) { // Ditto for mUtilRange. mUtilRange->Reset(); } // Remember current inline styles for deletion and normal insertion ops if (action == EditAction::insertText || action == EditAction::insertIMEText || action == EditAction::deleteSelection || IsStyleCachePreservingAction(action)) { nsCOMPtr<nsINode> selNode = aDirection == nsIEditor::eNext ? selEndNode : selStartNode; nsresult rv = CacheInlineStyles(GetAsDOMNode(selNode)); NS_ENSURE_SUCCESS(rv, rv); } // Stabilize the document against contenteditable count changes nsCOMPtr<nsIDOMDocument> doc = htmlEditor->GetDOMDocument(); NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED); nsCOMPtr<nsIHTMLDocument> htmlDoc = do_QueryInterface(doc); NS_ENSURE_TRUE(htmlDoc, NS_ERROR_FAILURE); if (htmlDoc->GetEditingState() == nsIHTMLDocument::eContentEditable) { htmlDoc->ChangeContentEditableCount(nullptr, +1); mRestoreContentEditableCount = true; } // Check that selection is in subtree defined by body node ConfirmSelectionInBody(); // Let rules remember the top level action mTheAction = action; } return NS_OK; } NS_IMETHODIMP HTMLEditRules::AfterEdit(EditAction action, nsIEditor::EDirection aDirection) { if (mLockRulesSniffing) { return NS_OK; } NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); AutoLockRulesSniffing lockIt(this); MOZ_ASSERT(mActionNesting > 0); nsresult rv = NS_OK; mActionNesting--; if (!mActionNesting) { // Do all the tricky stuff rv = AfterEditInner(action, aDirection); // Free up selectionState range item htmlEditor->mRangeUpdater.DropRangeItem(mRangeItem); // Reset the contenteditable count to its previous value if (mRestoreContentEditableCount) { nsCOMPtr<nsIDOMDocument> doc = htmlEditor->GetDOMDocument(); NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED); nsCOMPtr<nsIHTMLDocument> htmlDoc = do_QueryInterface(doc); NS_ENSURE_TRUE(htmlDoc, NS_ERROR_FAILURE); if (htmlDoc->GetEditingState() == nsIHTMLDocument::eContentEditable) { htmlDoc->ChangeContentEditableCount(nullptr, -1); } mRestoreContentEditableCount = false; } } NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult HTMLEditRules::AfterEditInner(EditAction action, nsIEditor::EDirection aDirection) { ConfirmSelectionInBody(); if (action == EditAction::ignore) { return NS_OK; } NS_ENSURE_STATE(mHTMLEditor); RefPtr<Selection> selection = mHTMLEditor->GetSelection(); NS_ENSURE_STATE(selection); nsCOMPtr<nsIDOMNode> rangeStartParent, rangeEndParent; int32_t rangeStartOffset = 0, rangeEndOffset = 0; // do we have a real range to act on? bool bDamagedRange = false; if (mDocChangeRange) { mDocChangeRange->GetStartContainer(getter_AddRefs(rangeStartParent)); mDocChangeRange->GetEndContainer(getter_AddRefs(rangeEndParent)); mDocChangeRange->GetStartOffset(&rangeStartOffset); mDocChangeRange->GetEndOffset(&rangeEndOffset); if (rangeStartParent && rangeEndParent) bDamagedRange = true; } if (bDamagedRange && !((action == EditAction::undo) || (action == EditAction::redo))) { // don't let any txns in here move the selection around behind our back. // Note that this won't prevent explicit selection setting from working. NS_ENSURE_STATE(mHTMLEditor); AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor); // expand the "changed doc range" as needed PromoteRange(*mDocChangeRange, action); // if we did a ranged deletion or handling backspace key, make sure we have // a place to put caret. // Note we only want to do this if the overall operation was deletion, // not if deletion was done along the way for EditAction::loadHTML, EditAction::insertText, etc. // That's why this is here rather than DidDeleteSelection(). if (action == EditAction::deleteSelection && mDidRangedDelete) { nsresult rv = InsertBRIfNeeded(selection); NS_ENSURE_SUCCESS(rv, rv); } // add in any needed <br>s, and remove any unneeded ones. AdjustSpecialBreaks(); // merge any adjacent text nodes if (action != EditAction::insertText && action != EditAction::insertIMEText) { NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->CollapseAdjacentTextNodes(mDocChangeRange); NS_ENSURE_SUCCESS(rv, rv); } // clean up any empty nodes in the selection nsresult rv = RemoveEmptyNodes(); NS_ENSURE_SUCCESS(rv, rv); // attempt to transform any unneeded nbsp's into spaces after doing various operations if (action == EditAction::insertText || action == EditAction::insertIMEText || action == EditAction::deleteSelection || action == EditAction::insertBreak || action == EditAction::htmlPaste || action == EditAction::loadHTML) { rv = AdjustWhitespace(selection); NS_ENSURE_SUCCESS(rv, rv); // also do this for original selection endpoints. NS_ENSURE_STATE(mHTMLEditor); NS_ENSURE_STATE(mRangeItem->startNode); NS_ENSURE_STATE(mRangeItem->endNode); WSRunObject(mHTMLEditor, mRangeItem->startNode, mRangeItem->startOffset).AdjustWhitespace(); // we only need to handle old selection endpoint if it was different from start if (mRangeItem->startNode != mRangeItem->endNode || mRangeItem->startOffset != mRangeItem->endOffset) { NS_ENSURE_STATE(mHTMLEditor); WSRunObject(mHTMLEditor, mRangeItem->endNode, mRangeItem->endOffset).AdjustWhitespace(); } } // if we created a new block, make sure selection lands in it if (mNewBlock) { rv = PinSelectionToNewBlock(selection); mNewBlock = nullptr; } // adjust selection for insert text, html paste, and delete actions if (action == EditAction::insertText || action == EditAction::insertIMEText || action == EditAction::deleteSelection || action == EditAction::insertBreak || action == EditAction::htmlPaste || action == EditAction::loadHTML) { rv = AdjustSelection(selection, aDirection); NS_ENSURE_SUCCESS(rv, rv); } // check for any styles which were removed inappropriately if (action == EditAction::insertText || action == EditAction::insertIMEText || action == EditAction::deleteSelection || IsStyleCachePreservingAction(action)) { NS_ENSURE_STATE(mHTMLEditor); mHTMLEditor->mTypeInState->UpdateSelState(selection); rv = ReapplyCachedStyles(); NS_ENSURE_SUCCESS(rv, rv); ClearCachedStyles(); } } NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->HandleInlineSpellCheck(action, selection, GetAsDOMNode(mRangeItem->startNode), mRangeItem->startOffset, rangeStartParent, rangeStartOffset, rangeEndParent, rangeEndOffset); NS_ENSURE_SUCCESS(rv, rv); // detect empty doc rv = CreateBogusNodeIfNeeded(selection); NS_ENSURE_SUCCESS(rv, rv); // adjust selection HINT if needed if (!mDidExplicitlySetInterline) { CheckInterlinePosition(*selection); } return NS_OK; } NS_IMETHODIMP HTMLEditRules::WillDoAction(Selection* aSelection, RulesInfo* aInfo, bool* aCancel, bool* aHandled) { MOZ_ASSERT(aInfo && aCancel && aHandled); *aCancel = false; *aHandled = false; // my kingdom for dynamic cast TextRulesInfo* info = static_cast<TextRulesInfo*>(aInfo); // Deal with actions for which we don't need to check whether the selection is // editable. if (info->action == EditAction::outputText || info->action == EditAction::undo || info->action == EditAction::redo) { return TextEditRules::WillDoAction(aSelection, aInfo, aCancel, aHandled); } // Nothing to do if there's no selection to act on if (!aSelection) { return NS_OK; } NS_ENSURE_TRUE(aSelection->RangeCount(), NS_OK); RefPtr<nsRange> range = aSelection->GetRangeAt(0); nsCOMPtr<nsINode> selStartNode = range->GetStartParent(); NS_ENSURE_STATE(mHTMLEditor); if (!mHTMLEditor->IsModifiableNode(selStartNode)) { *aCancel = true; return NS_OK; } nsCOMPtr<nsINode> selEndNode = range->GetEndParent(); if (selStartNode != selEndNode) { NS_ENSURE_STATE(mHTMLEditor); if (!mHTMLEditor->IsModifiableNode(selEndNode)) { *aCancel = true; return NS_OK; } NS_ENSURE_STATE(mHTMLEditor); if (!mHTMLEditor->IsModifiableNode(range->GetCommonAncestor())) { *aCancel = true; return NS_OK; } } switch (info->action) { case EditAction::insertText: case EditAction::insertIMEText: UndefineCaretBidiLevel(aSelection); return WillInsertText(info->action, aSelection, aCancel, aHandled, info->inString, info->outString, info->maxLength); case EditAction::loadHTML: return WillLoadHTML(aSelection, aCancel); case EditAction::insertBreak: UndefineCaretBidiLevel(aSelection); return WillInsertBreak(*aSelection, aCancel, aHandled); case EditAction::deleteSelection: return WillDeleteSelection(aSelection, info->collapsedAction, info->stripWrappers, aCancel, aHandled); case EditAction::makeList: return WillMakeList(aSelection, info->blockType, info->entireList, info->bulletType, aCancel, aHandled); case EditAction::indent: return WillIndent(aSelection, aCancel, aHandled); case EditAction::outdent: return WillOutdent(*aSelection, aCancel, aHandled); case EditAction::setAbsolutePosition: return WillAbsolutePosition(*aSelection, aCancel, aHandled); case EditAction::removeAbsolutePosition: return WillRemoveAbsolutePosition(aSelection, aCancel, aHandled); case EditAction::align: return WillAlign(*aSelection, *info->alignType, aCancel, aHandled); case EditAction::makeBasicBlock: return WillMakeBasicBlock(*aSelection, *info->blockType, aCancel, aHandled); case EditAction::removeList: return WillRemoveList(aSelection, info->bOrdered, aCancel, aHandled); case EditAction::makeDefListItem: return WillMakeDefListItem(aSelection, info->blockType, info->entireList, aCancel, aHandled); case EditAction::insertElement: WillInsert(*aSelection, aCancel); return NS_OK; case EditAction::decreaseZIndex: return WillRelativeChangeZIndex(aSelection, -1, aCancel, aHandled); case EditAction::increaseZIndex: return WillRelativeChangeZIndex(aSelection, 1, aCancel, aHandled); default: return TextEditRules::WillDoAction(aSelection, aInfo, aCancel, aHandled); } } NS_IMETHODIMP HTMLEditRules::DidDoAction(Selection* aSelection, RulesInfo* aInfo, nsresult aResult) { TextRulesInfo* info = static_cast<TextRulesInfo*>(aInfo); switch (info->action) { case EditAction::insertBreak: return DidInsertBreak(aSelection, aResult); case EditAction::deleteSelection: return DidDeleteSelection(aSelection, info->collapsedAction, aResult); case EditAction::makeBasicBlock: case EditAction::indent: case EditAction::outdent: case EditAction::align: return DidMakeBasicBlock(aSelection, aInfo, aResult); case EditAction::setAbsolutePosition: { nsresult rv = DidMakeBasicBlock(aSelection, aInfo, aResult); NS_ENSURE_SUCCESS(rv, rv); return DidAbsolutePosition(); } default: // pass through to TextEditRules return TextEditRules::DidDoAction(aSelection, aInfo, aResult); } } nsresult HTMLEditRules::GetListState(bool* aMixed, bool* aOL, bool* aUL, bool* aDL) { NS_ENSURE_TRUE(aMixed && aOL && aUL && aDL, NS_ERROR_NULL_POINTER); *aMixed = false; *aOL = false; *aUL = false; *aDL = false; bool bNonList = false; nsTArray<OwningNonNull<nsINode>> arrayOfNodes; nsresult rv = GetListActionNodes(arrayOfNodes, EntireList::no, TouchContent::no); NS_ENSURE_SUCCESS(rv, rv); // Examine list type for nodes in selection. for (const auto& curNode : arrayOfNodes) { if (!curNode->IsElement()) { bNonList = true; } else if (curNode->IsHTMLElement(nsGkAtoms::ul)) { *aUL = true; } else if (curNode->IsHTMLElement(nsGkAtoms::ol)) { *aOL = true; } else if (curNode->IsHTMLElement(nsGkAtoms::li)) { if (dom::Element* parent = curNode->GetParentElement()) { if (parent->IsHTMLElement(nsGkAtoms::ul)) { *aUL = true; } else if (parent->IsHTMLElement(nsGkAtoms::ol)) { *aOL = true; } } } else if (curNode->IsAnyOfHTMLElements(nsGkAtoms::dl, nsGkAtoms::dt, nsGkAtoms::dd)) { *aDL = true; } else { bNonList = true; } } // hokey arithmetic with booleans if ((*aUL + *aOL + *aDL + bNonList) > 1) { *aMixed = true; } return NS_OK; } nsresult HTMLEditRules::GetListItemState(bool* aMixed, bool* aLI, bool* aDT, bool* aDD) { NS_ENSURE_TRUE(aMixed && aLI && aDT && aDD, NS_ERROR_NULL_POINTER); *aMixed = false; *aLI = false; *aDT = false; *aDD = false; bool bNonList = false; nsTArray<OwningNonNull<nsINode>> arrayOfNodes; nsresult rv = GetListActionNodes(arrayOfNodes, EntireList::no, TouchContent::no); NS_ENSURE_SUCCESS(rv, rv); // examine list type for nodes in selection for (const auto& node : arrayOfNodes) { if (!node->IsElement()) { bNonList = true; } else if (node->IsAnyOfHTMLElements(nsGkAtoms::ul, nsGkAtoms::ol, nsGkAtoms::li)) { *aLI = true; } else if (node->IsHTMLElement(nsGkAtoms::dt)) { *aDT = true; } else if (node->IsHTMLElement(nsGkAtoms::dd)) { *aDD = true; } else if (node->IsHTMLElement(nsGkAtoms::dl)) { // need to look inside dl and see which types of items it has bool bDT, bDD; GetDefinitionListItemTypes(node->AsElement(), &bDT, &bDD); *aDT |= bDT; *aDD |= bDD; } else { bNonList = true; } } // hokey arithmetic with booleans if (*aDT + *aDD + bNonList > 1) { *aMixed = true; } return NS_OK; } nsresult HTMLEditRules::GetAlignment(bool* aMixed, nsIHTMLEditor::EAlignment* aAlign) { MOZ_ASSERT(aMixed && aAlign); NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); // For now, just return first alignment. We'll lie about if it's mixed. // This is for efficiency given that our current ui doesn't care if it's // mixed. // cmanske: NOT TRUE! We would like to pay attention to mixed state in Format // | Align submenu! // This routine assumes that alignment is done ONLY via divs // Default alignment is left *aMixed = false; *aAlign = nsIHTMLEditor::eLeft; // Get selection NS_ENSURE_STATE(htmlEditor->GetSelection()); OwningNonNull<Selection> selection = *htmlEditor->GetSelection(); // Get selection location NS_ENSURE_TRUE(htmlEditor->GetRoot(), NS_ERROR_FAILURE); OwningNonNull<Element> root = *htmlEditor->GetRoot(); int32_t rootOffset = root->GetParentNode() ? root->GetParentNode()->IndexOf(root) : -1; NS_ENSURE_STATE(selection->GetRangeAt(0) && selection->GetRangeAt(0)->GetStartParent()); OwningNonNull<nsINode> parent = *selection->GetRangeAt(0)->GetStartParent(); int32_t offset = selection->GetRangeAt(0)->StartOffset(); // Is the selection collapsed? nsCOMPtr<nsINode> nodeToExamine; if (selection->Collapsed() || parent->GetAsText()) { // If selection is collapsed, we want to look at 'parent' and its ancestors // for divs with alignment on them. If we are in a text node, then that is // the node of interest. nodeToExamine = parent; } else if (parent->IsHTMLElement(nsGkAtoms::html) && offset == rootOffset) { // If we have selected the body, let's look at the first editable node nodeToExamine = htmlEditor->GetNextNode(parent, offset, true); } else { nsTArray<RefPtr<nsRange>> arrayOfRanges; GetPromotedRanges(selection, arrayOfRanges, EditAction::align); // Use these ranges to construct a list of nodes to act on. nsTArray<OwningNonNull<nsINode>> arrayOfNodes; nsresult rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes, EditAction::align, TouchContent::no); NS_ENSURE_SUCCESS(rv, rv); nodeToExamine = arrayOfNodes.SafeElementAt(0); } NS_ENSURE_TRUE(nodeToExamine, NS_ERROR_NULL_POINTER); NS_NAMED_LITERAL_STRING(typeAttrName, "align"); nsCOMPtr<Element> blockParent = htmlEditor->GetBlock(*nodeToExamine); NS_ENSURE_TRUE(blockParent, NS_ERROR_FAILURE); if (htmlEditor->IsCSSEnabled() && htmlEditor->mCSSEditUtils->IsCSSEditableProperty(blockParent, nullptr, &typeAttrName)) { // We are in CSS mode and we know how to align this element with CSS nsAutoString value; // Let's get the value(s) of text-align or margin-left/margin-right htmlEditor->mCSSEditUtils->GetCSSEquivalentToHTMLInlineStyleSet( blockParent, nullptr, &typeAttrName, value, CSSEditUtils::eComputed); if (value.EqualsLiteral("center") || value.EqualsLiteral("-moz-center") || value.EqualsLiteral("auto auto")) { *aAlign = nsIHTMLEditor::eCenter; return NS_OK; } if (value.EqualsLiteral("right") || value.EqualsLiteral("-moz-right") || value.EqualsLiteral("auto 0px")) { *aAlign = nsIHTMLEditor::eRight; return NS_OK; } if (value.EqualsLiteral("justify")) { *aAlign = nsIHTMLEditor::eJustify; return NS_OK; } *aAlign = nsIHTMLEditor::eLeft; return NS_OK; } // Check up the ladder for divs with alignment bool isFirstNodeToExamine = true; for (; nodeToExamine; nodeToExamine = nodeToExamine->GetParentNode()) { if (!isFirstNodeToExamine && nodeToExamine->IsHTMLElement(nsGkAtoms::table)) { // The node to examine is a table and this is not the first node we // examine; let's break here to materialize the 'inline-block' behaviour // of html tables regarding to text alignment return NS_OK; } if (HTMLEditUtils::SupportsAlignAttr(GetAsDOMNode(nodeToExamine))) { // Check for alignment nsAutoString typeAttrVal; nodeToExamine->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::align, typeAttrVal); ToLowerCase(typeAttrVal); if (!typeAttrVal.IsEmpty()) { if (typeAttrVal.EqualsLiteral("center")) { *aAlign = nsIHTMLEditor::eCenter; } else if (typeAttrVal.EqualsLiteral("right")) { *aAlign = nsIHTMLEditor::eRight; } else if (typeAttrVal.EqualsLiteral("justify")) { *aAlign = nsIHTMLEditor::eJustify; } else { *aAlign = nsIHTMLEditor::eLeft; } return NS_OK; } } isFirstNodeToExamine = false; } return NS_OK; } static nsIAtom& MarginPropertyAtomForIndent(CSSEditUtils& aHTMLCSSUtils, nsINode& aNode) { nsAutoString direction; aHTMLCSSUtils.GetComputedProperty(aNode, *nsGkAtoms::direction, direction); return direction.EqualsLiteral("rtl") ? *nsGkAtoms::marginRight : *nsGkAtoms::marginLeft; } nsresult HTMLEditRules::GetIndentState(bool* aCanIndent, bool* aCanOutdent) { // XXX Looks like that this is implementation of // nsIHTMLEditor::getIndentState() however nobody calls this method // even with the interface method. NS_ENSURE_TRUE(aCanIndent && aCanOutdent, NS_ERROR_FAILURE); *aCanIndent = true; *aCanOutdent = false; // get selection NS_ENSURE_STATE(mHTMLEditor && mHTMLEditor->GetSelection()); OwningNonNull<Selection> selection = *mHTMLEditor->GetSelection(); // contruct a list of nodes to act on. nsTArray<OwningNonNull<nsINode>> arrayOfNodes; nsresult rv = GetNodesFromSelection(*selection, EditAction::indent, arrayOfNodes, TouchContent::no); NS_ENSURE_SUCCESS(rv, rv); // examine nodes in selection for blockquotes or list elements; // these we can outdent. Note that we return true for canOutdent // if *any* of the selection is outdentable, rather than all of it. NS_ENSURE_STATE(mHTMLEditor); bool useCSS = mHTMLEditor->IsCSSEnabled(); for (auto& curNode : Reversed(arrayOfNodes)) { if (HTMLEditUtils::IsNodeThatCanOutdent(GetAsDOMNode(curNode))) { *aCanOutdent = true; break; } else if (useCSS) { // we are in CSS mode, indentation is done using the margin-left (or margin-right) property NS_ENSURE_STATE(mHTMLEditor); nsIAtom& marginProperty = MarginPropertyAtomForIndent(*mHTMLEditor->mCSSEditUtils, curNode); nsAutoString value; // retrieve its specified value NS_ENSURE_STATE(mHTMLEditor); mHTMLEditor->mCSSEditUtils->GetSpecifiedProperty(*curNode, marginProperty, value); float f; nsCOMPtr<nsIAtom> unit; // get its number part and its unit NS_ENSURE_STATE(mHTMLEditor); mHTMLEditor->mCSSEditUtils->ParseLength(value, &f, getter_AddRefs(unit)); // if the number part is strictly positive, outdent is possible if (0 < f) { *aCanOutdent = true; break; } } } if (!*aCanOutdent) { // if we haven't found something to outdent yet, also check the parents // of selection endpoints. We might have a blockquote or list item // in the parent hierarchy. // gather up info we need for test NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<nsIDOMNode> parent, tmp, root = do_QueryInterface(mHTMLEditor->GetRoot()); NS_ENSURE_TRUE(root, NS_ERROR_NULL_POINTER); int32_t selOffset; NS_ENSURE_STATE(mHTMLEditor); RefPtr<Selection> selection = mHTMLEditor->GetSelection(); NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); // test start parent hierarchy NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->GetStartNodeAndOffset(selection, getter_AddRefs(parent), &selOffset); NS_ENSURE_SUCCESS(rv, rv); while (parent && parent != root) { if (HTMLEditUtils::IsNodeThatCanOutdent(parent)) { *aCanOutdent = true; break; } tmp = parent; tmp->GetParentNode(getter_AddRefs(parent)); } // test end parent hierarchy NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->GetEndNodeAndOffset(selection, getter_AddRefs(parent), &selOffset); NS_ENSURE_SUCCESS(rv, rv); while (parent && parent != root) { if (HTMLEditUtils::IsNodeThatCanOutdent(parent)) { *aCanOutdent = true; break; } tmp = parent; tmp->GetParentNode(getter_AddRefs(parent)); } } return NS_OK; } nsresult HTMLEditRules::GetParagraphState(bool* aMixed, nsAString& outFormat) { // This routine is *heavily* tied to our ui choices in the paragraph // style popup. I can't see a way around that. NS_ENSURE_TRUE(aMixed, NS_ERROR_NULL_POINTER); *aMixed = true; outFormat.Truncate(0); bool bMixed = false; // using "x" as an uninitialized value, since "" is meaningful nsAutoString formatStr(NS_LITERAL_STRING("x")); nsTArray<OwningNonNull<nsINode>> arrayOfNodes; nsresult rv = GetParagraphFormatNodes(arrayOfNodes, TouchContent::no); NS_ENSURE_SUCCESS(rv, rv); // post process list. We need to replace any block nodes that are not format // nodes with their content. This is so we only have to look "up" the hierarchy // to find format nodes, instead of both up and down. for (int32_t i = arrayOfNodes.Length() - 1; i >= 0; i--) { auto& curNode = arrayOfNodes[i]; nsAutoString format; // if it is a known format node we have it easy if (IsBlockNode(curNode) && !HTMLEditUtils::IsFormatNode(curNode)) { // arrayOfNodes.RemoveObject(curNode); rv = AppendInnerFormatNodes(arrayOfNodes, curNode); NS_ENSURE_SUCCESS(rv, rv); } } // we might have an empty node list. if so, find selection parent // and put that on the list if (arrayOfNodes.IsEmpty()) { nsCOMPtr<nsINode> selNode; int32_t selOffset; NS_ENSURE_STATE(mHTMLEditor); RefPtr<Selection> selection = mHTMLEditor->GetSelection(); NS_ENSURE_STATE(selection); NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->GetStartNodeAndOffset(selection, getter_AddRefs(selNode), &selOffset); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_TRUE(selNode, NS_ERROR_NULL_POINTER); arrayOfNodes.AppendElement(*selNode); } // remember root node NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<nsIDOMElement> rootElem = do_QueryInterface(mHTMLEditor->GetRoot()); NS_ENSURE_TRUE(rootElem, NS_ERROR_NULL_POINTER); // loop through the nodes in selection and examine their paragraph format for (auto& curNode : Reversed(arrayOfNodes)) { nsAutoString format; // if it is a known format node we have it easy if (HTMLEditUtils::IsFormatNode(curNode)) { GetFormatString(GetAsDOMNode(curNode), format); } else if (IsBlockNode(curNode)) { // this is a div or some other non-format block. // we should ignore it. Its children were appended to this list // by AppendInnerFormatNodes() call above. We will get needed // info when we examine them instead. continue; } else { nsCOMPtr<nsIDOMNode> node, tmp = GetAsDOMNode(curNode); tmp->GetParentNode(getter_AddRefs(node)); while (node) { if (node == rootElem) { format.Truncate(0); break; } else if (HTMLEditUtils::IsFormatNode(node)) { GetFormatString(node, format); break; } // else keep looking up tmp = node; tmp->GetParentNode(getter_AddRefs(node)); } } // if this is the first node, we've found, remember it as the format if (formatStr.EqualsLiteral("x")) { formatStr = format; } // else make sure it matches previously found format else if (format != formatStr) { bMixed = true; break; } } *aMixed = bMixed; outFormat = formatStr; return NS_OK; } nsresult HTMLEditRules::AppendInnerFormatNodes(nsTArray<OwningNonNull<nsINode>>& aArray, nsINode* aNode) { MOZ_ASSERT(aNode); // we only need to place any one inline inside this node onto // the list. They are all the same for purposes of determining // paragraph style. We use foundInline to track this as we are // going through the children in the loop below. bool foundInline = false; for (nsIContent* child = aNode->GetFirstChild(); child; child = child->GetNextSibling()) { bool isBlock = IsBlockNode(*child); bool isFormat = HTMLEditUtils::IsFormatNode(child); if (isBlock && !isFormat) { // if it's a div, etc., recurse AppendInnerFormatNodes(aArray, child); } else if (isFormat) { aArray.AppendElement(*child); } else if (!foundInline) { // if this is the first inline we've found, use it foundInline = true; aArray.AppendElement(*child); } } return NS_OK; } nsresult HTMLEditRules::GetFormatString(nsIDOMNode* aNode, nsAString& outFormat) { NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER); if (HTMLEditUtils::IsFormatNode(aNode)) { nsCOMPtr<nsIAtom> atom = EditorBase::GetTag(aNode); atom->ToString(outFormat); } else { outFormat.Truncate(); } return NS_OK; } void HTMLEditRules::WillInsert(Selection& aSelection, bool* aCancel) { MOZ_ASSERT(aCancel); TextEditRules::WillInsert(aSelection, aCancel); NS_ENSURE_TRUE_VOID(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); // Adjust selection to prevent insertion after a moz-BR. This next only // works for collapsed selections right now, because selection is a pain to // work with when not collapsed. (no good way to extend start or end of // selection), so we ignore those types of selections. if (!aSelection.Collapsed()) { return; } // If we are after a mozBR in the same block, then move selection to be // before it NS_ENSURE_TRUE_VOID(aSelection.GetRangeAt(0) && aSelection.GetRangeAt(0)->GetStartParent()); OwningNonNull<nsINode> selNode = *aSelection.GetRangeAt(0)->GetStartParent(); int32_t selOffset = aSelection.GetRangeAt(0)->StartOffset(); // Get prior node nsCOMPtr<nsIContent> priorNode = htmlEditor->GetPriorHTMLNode(selNode, selOffset); if (priorNode && TextEditUtils::IsMozBR(priorNode)) { nsCOMPtr<Element> block1 = htmlEditor->GetBlock(selNode); nsCOMPtr<Element> block2 = htmlEditor->GetBlockNodeParent(priorNode); if (block1 && block1 == block2) { // If we are here then the selection is right after a mozBR that is in // the same block as the selection. We need to move the selection start // to be before the mozBR. selNode = priorNode->GetParentNode(); selOffset = selNode->IndexOf(priorNode); nsresult rv = aSelection.Collapse(selNode, selOffset); NS_ENSURE_SUCCESS_VOID(rv); } } if (mDidDeleteSelection && (mTheAction == EditAction::insertText || mTheAction == EditAction::insertIMEText || mTheAction == EditAction::deleteSelection)) { nsresult rv = ReapplyCachedStyles(); NS_ENSURE_SUCCESS_VOID(rv); } // For most actions we want to clear the cached styles, but there are // exceptions if (!IsStyleCachePreservingAction(mTheAction)) { ClearCachedStyles(); } } nsresult HTMLEditRules::WillInsertText(EditAction aAction, Selection* aSelection, bool* aCancel, bool* aHandled, const nsAString* inString, nsAString* outString, int32_t aMaxLength) { if (!aSelection || !aCancel || !aHandled) { return NS_ERROR_NULL_POINTER; } if (inString->IsEmpty() && aAction != EditAction::insertIMEText) { // HACK: this is a fix for bug 19395 // I can't outlaw all empty insertions // because IME transaction depend on them // There is more work to do to make the // world safe for IME. *aCancel = true; *aHandled = false; return NS_OK; } // initialize out param *aCancel = false; *aHandled = true; // If the selection isn't collapsed, delete it. Don't delete existing inline // tags, because we're hopefully going to insert text (bug 787432). if (!aSelection->Collapsed()) { NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->DeleteSelection(nsIEditor::eNone, nsIEditor::eNoStrip); NS_ENSURE_SUCCESS(rv, rv); } WillInsert(*aSelection, aCancel); // initialize out param // we want to ignore result of WillInsert() *aCancel = false; // we need to get the doc NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<nsIDocument> doc = mHTMLEditor->GetDocument(); NS_ENSURE_STATE(doc); // for every property that is set, insert a new inline style node nsresult rv = CreateStyleForInsertText(*aSelection, *doc); NS_ENSURE_SUCCESS(rv, rv); // get the (collapsed) selection location NS_ENSURE_STATE(mHTMLEditor); NS_ENSURE_STATE(aSelection->GetRangeAt(0)); nsCOMPtr<nsINode> selNode = aSelection->GetRangeAt(0)->GetStartParent(); int32_t selOffset = aSelection->GetRangeAt(0)->StartOffset(); NS_ENSURE_STATE(selNode); // dont put text in places that can't have it NS_ENSURE_STATE(mHTMLEditor); if (!mHTMLEditor->IsTextNode(selNode) && (!mHTMLEditor || !mHTMLEditor->CanContainTag(*selNode, *nsGkAtoms::textTagName))) { return NS_ERROR_FAILURE; } if (aAction == EditAction::insertIMEText) { // Right now the WSRunObject code bails on empty strings, but IME needs // the InsertTextImpl() call to still happen since empty strings are meaningful there. NS_ENSURE_STATE(mHTMLEditor); // If there is one or more IME selections, its minimum offset should be // the insertion point. int32_t IMESelectionOffset = mHTMLEditor->GetIMESelectionStartOffsetIn(selNode); if (IMESelectionOffset >= 0) { selOffset = IMESelectionOffset; } if (inString->IsEmpty()) { rv = mHTMLEditor->InsertTextImpl(*inString, address_of(selNode), &selOffset, doc); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { WSRunObject wsObj(mHTMLEditor, selNode, selOffset); rv = wsObj.InsertText(*inString, address_of(selNode), &selOffset, doc); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } // aAction == kInsertText else { // find where we are nsCOMPtr<nsINode> curNode = selNode; int32_t curOffset = selOffset; // is our text going to be PREformatted? // We remember this so that we know how to handle tabs. bool isPRE; NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->IsPreformatted(GetAsDOMNode(selNode), &isPRE); NS_ENSURE_SUCCESS(rv, rv); // turn off the edit listener: we know how to // build the "doc changed range" ourselves, and it's // must faster to do it once here than to track all // the changes one at a time. AutoLockListener lockit(&mListenerEnabled); // don't spaz my selection in subtransactions NS_ENSURE_STATE(mHTMLEditor); AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor); nsAutoString tString(*inString); const char16_t *unicodeBuf = tString.get(); int32_t pos = 0; NS_NAMED_LITERAL_STRING(newlineStr, LFSTR); // for efficiency, break out the pre case separately. This is because // its a lot cheaper to search the input string for only newlines than // it is to search for both tabs and newlines. if (isPRE || IsPlaintextEditor()) { while (unicodeBuf && pos != -1 && pos < static_cast<int32_t>(inString->Length())) { int32_t oldPos = pos; int32_t subStrLen; pos = tString.FindChar(nsCRT::LF, oldPos); if (pos != -1) { subStrLen = pos - oldPos; // if first char is newline, then use just it if (!subStrLen) { subStrLen = 1; } } else { subStrLen = tString.Length() - oldPos; pos = tString.Length(); } nsDependentSubstring subStr(tString, oldPos, subStrLen); // is it a return? if (subStr.Equals(newlineStr)) { NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<Element> br = mHTMLEditor->CreateBRImpl(address_of(curNode), &curOffset, nsIEditor::eNone); NS_ENSURE_STATE(br); pos++; } else { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->InsertTextImpl(subStr, address_of(curNode), &curOffset, doc); NS_ENSURE_SUCCESS(rv, rv); } } } else { NS_NAMED_LITERAL_STRING(tabStr, "\t"); NS_NAMED_LITERAL_STRING(spacesStr, " "); char specialChars[] = {TAB, nsCRT::LF, 0}; while (unicodeBuf && pos != -1 && pos < static_cast<int32_t>(inString->Length())) { int32_t oldPos = pos; int32_t subStrLen; pos = tString.FindCharInSet(specialChars, oldPos); if (pos != -1) { subStrLen = pos - oldPos; // if first char is newline, then use just it if (!subStrLen) { subStrLen = 1; } } else { subStrLen = tString.Length() - oldPos; pos = tString.Length(); } nsDependentSubstring subStr(tString, oldPos, subStrLen); NS_ENSURE_STATE(mHTMLEditor); WSRunObject wsObj(mHTMLEditor, curNode, curOffset); // is it a tab? if (subStr.Equals(tabStr)) { rv = wsObj.InsertText(spacesStr, address_of(curNode), &curOffset, doc); NS_ENSURE_SUCCESS(rv, rv); pos++; } // is it a return? else if (subStr.Equals(newlineStr)) { nsCOMPtr<Element> br = wsObj.InsertBreak(address_of(curNode), &curOffset, nsIEditor::eNone); NS_ENSURE_TRUE(br, NS_ERROR_FAILURE); pos++; } else { rv = wsObj.InsertText(subStr, address_of(curNode), &curOffset, doc); NS_ENSURE_SUCCESS(rv, rv); } } } aSelection->SetInterlinePosition(false); if (curNode) aSelection->Collapse(curNode, curOffset); // manually update the doc changed range so that AfterEdit will clean up // the correct portion of the document. if (!mDocChangeRange) { mDocChangeRange = new nsRange(selNode); } rv = mDocChangeRange->SetStart(selNode, selOffset); NS_ENSURE_SUCCESS(rv, rv); if (curNode) { rv = mDocChangeRange->SetEnd(curNode, curOffset); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { rv = mDocChangeRange->SetEnd(selNode, selOffset); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } return NS_OK; } nsresult HTMLEditRules::WillLoadHTML(Selection* aSelection, bool* aCancel) { NS_ENSURE_TRUE(aSelection && aCancel, NS_ERROR_NULL_POINTER); *aCancel = false; // Delete mBogusNode if it exists. If we really need one, // it will be added during post-processing in AfterEditInner(). if (mBogusNode) { mTextEditor->DeleteNode(mBogusNode); mBogusNode = nullptr; } return NS_OK; } nsresult HTMLEditRules::WillInsertBreak(Selection& aSelection, bool* aCancel, bool* aHandled) { MOZ_ASSERT(aCancel && aHandled); *aCancel = false; *aHandled = false; NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); // If the selection isn't collapsed, delete it. if (!aSelection.Collapsed()) { nsresult rv = htmlEditor->DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip); NS_ENSURE_SUCCESS(rv, rv); } WillInsert(aSelection, aCancel); // Initialize out param. We want to ignore result of WillInsert(). *aCancel = false; // Split any mailcites in the way. Should we abort this if we encounter // table cell boundaries? if (IsMailEditor()) { nsresult rv = SplitMailCites(&aSelection, aHandled); NS_ENSURE_SUCCESS(rv, rv); if (*aHandled) { return NS_OK; } } // Smart splitting rules NS_ENSURE_TRUE(aSelection.GetRangeAt(0) && aSelection.GetRangeAt(0)->GetStartParent(), NS_ERROR_FAILURE); OwningNonNull<nsINode> node = *aSelection.GetRangeAt(0)->GetStartParent(); int32_t offset = aSelection.GetRangeAt(0)->StartOffset(); // Do nothing if the node is read-only if (!htmlEditor->IsModifiableNode(node)) { *aCancel = true; return NS_OK; } // Identify the block nsCOMPtr<Element> blockParent = htmlEditor->GetBlock(node); NS_ENSURE_TRUE(blockParent, NS_ERROR_FAILURE); // If the active editing host is an inline element, or if the active editing // host is the block parent itself, just append a br. nsCOMPtr<Element> host = htmlEditor->GetActiveEditingHost(); if (!EditorUtils::IsDescendantOf(blockParent, host)) { nsresult rv = StandardBreakImpl(node, offset, aSelection); NS_ENSURE_SUCCESS(rv, rv); *aHandled = true; return NS_OK; } // If block is empty, populate with br. (For example, imagine a div that // contains the word "text". The user selects "text" and types return. // "Text" is deleted leaving an empty block. We want to put in one br to // make block have a line. Then code further below will put in a second br.) bool isEmpty; IsEmptyBlock(*blockParent, &isEmpty); if (isEmpty) { nsCOMPtr<Element> br = htmlEditor->CreateBR(blockParent, blockParent->Length()); NS_ENSURE_STATE(br); } nsCOMPtr<Element> listItem = IsInListItem(blockParent); if (listItem && listItem != host) { ReturnInListItem(aSelection, *listItem, node, offset); *aHandled = true; return NS_OK; } else if (HTMLEditUtils::IsHeader(*blockParent)) { // Headers: close (or split) header ReturnInHeader(aSelection, *blockParent, node, offset); *aHandled = true; return NS_OK; } else if (blockParent->IsHTMLElement(nsGkAtoms::p)) { // Paragraphs: special rules to look for <br>s nsresult rv = ReturnInParagraph(&aSelection, GetAsDOMNode(blockParent), GetAsDOMNode(node), offset, aCancel, aHandled); NS_ENSURE_SUCCESS(rv, rv); // Fall through, we may not have handled it in ReturnInParagraph() } // If not already handled then do the standard thing if (!(*aHandled)) { *aHandled = true; return StandardBreakImpl(node, offset, aSelection); } return NS_OK; } nsresult HTMLEditRules::StandardBreakImpl(nsINode& aNode, int32_t aOffset, Selection& aSelection) { NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); nsCOMPtr<Element> brNode; bool bAfterBlock = false; bool bBeforeBlock = false; nsCOMPtr<nsINode> node = &aNode; if (IsPlaintextEditor()) { brNode = htmlEditor->CreateBR(node, aOffset); NS_ENSURE_STATE(brNode); } else { WSRunObject wsObj(htmlEditor, node, aOffset); int32_t visOffset = 0; WSType wsType; nsCOMPtr<nsINode> visNode; wsObj.PriorVisibleNode(node, aOffset, address_of(visNode), &visOffset, &wsType); if (wsType & WSType::block) { bAfterBlock = true; } wsObj.NextVisibleNode(node, aOffset, address_of(visNode), &visOffset, &wsType); if (wsType & WSType::block) { bBeforeBlock = true; } nsCOMPtr<nsIDOMNode> linkDOMNode; if (htmlEditor->IsInLink(GetAsDOMNode(node), address_of(linkDOMNode))) { // Split the link nsCOMPtr<Element> linkNode = do_QueryInterface(linkDOMNode); NS_ENSURE_STATE(linkNode || !linkDOMNode); nsCOMPtr<nsINode> linkParent = linkNode->GetParentNode(); aOffset = htmlEditor->SplitNodeDeep(*linkNode, *node->AsContent(), aOffset, HTMLEditor::EmptyContainers::no); NS_ENSURE_STATE(aOffset != -1); node = linkParent; } brNode = wsObj.InsertBreak(address_of(node), &aOffset, nsIEditor::eNone); NS_ENSURE_TRUE(brNode, NS_ERROR_FAILURE); } node = brNode->GetParentNode(); NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER); int32_t offset = node->IndexOf(brNode); if (bAfterBlock && bBeforeBlock) { // We just placed a br between block boundaries. This is the one case // where we want the selection to be before the br we just placed, as the // br will be on a new line, rather than at end of prior line. aSelection.SetInterlinePosition(true); nsresult rv = aSelection.Collapse(node, offset); NS_ENSURE_SUCCESS(rv, rv); } else { WSRunObject wsObj(htmlEditor, node, offset + 1); nsCOMPtr<nsINode> secondBR; int32_t visOffset = 0; WSType wsType; wsObj.NextVisibleNode(node, offset + 1, address_of(secondBR), &visOffset, &wsType); if (wsType == WSType::br) { // The next thing after the break we inserted is another break. Move the // second break to be the first break's sibling. This will prevent them // from being in different inline nodes, which would break // SetInterlinePosition(). It will also assure that if the user clicks // away and then clicks back on their new blank line, they will still get // the style from the line above. nsCOMPtr<nsINode> brParent = secondBR->GetParentNode(); int32_t brOffset = brParent ? brParent->IndexOf(secondBR) : -1; if (brParent != node || brOffset != offset + 1) { nsresult rv = htmlEditor->MoveNode(secondBR->AsContent(), node, offset + 1); NS_ENSURE_SUCCESS(rv, rv); } } // SetInterlinePosition(true) means we want the caret to stick to the // content on the "right". We want the caret to stick to whatever is past // the break. This is because the break is on the same line we were on, // but the next content will be on the following line. // An exception to this is if the break has a next sibling that is a block // node. Then we stick to the left to avoid an uber caret. nsCOMPtr<nsIContent> siblingNode = brNode->GetNextSibling(); if (siblingNode && IsBlockNode(*siblingNode)) { aSelection.SetInterlinePosition(false); } else { aSelection.SetInterlinePosition(true); } nsresult rv = aSelection.Collapse(node, offset + 1); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } nsresult HTMLEditRules::DidInsertBreak(Selection* aSelection, nsresult aResult) { return NS_OK; } nsresult HTMLEditRules::SplitMailCites(Selection* aSelection, bool* aHandled) { NS_ENSURE_TRUE(aSelection && aHandled, NS_ERROR_NULL_POINTER); nsCOMPtr<nsIContent> leftCite, rightCite; nsCOMPtr<nsINode> selNode; nsCOMPtr<Element> citeNode; int32_t selOffset; NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetStartNodeAndOffset(aSelection, getter_AddRefs(selNode), &selOffset); NS_ENSURE_SUCCESS(rv, rv); citeNode = GetTopEnclosingMailCite(*selNode); if (citeNode) { // If our selection is just before a break, nudge it to be // just after it. This does two things for us. It saves us the trouble of having to add // a break here ourselves to preserve the "blockness" of the inline span mailquote // (in the inline case), and : // it means the break won't end up making an empty line that happens to be inside a // mailquote (in either inline or block case). // The latter can confuse a user if they click there and start typing, // because being in the mailquote may affect wrapping behavior, or font color, etc. NS_ENSURE_STATE(mHTMLEditor); WSRunObject wsObj(mHTMLEditor, selNode, selOffset); nsCOMPtr<nsINode> visNode; int32_t visOffset=0; WSType wsType; wsObj.NextVisibleNode(selNode, selOffset, address_of(visNode), &visOffset, &wsType); if (wsType == WSType::br) { // ok, we are just before a break. is it inside the mailquote? if (visNode != citeNode && citeNode->Contains(visNode)) { // it is. so lets reset our selection to be just after it. NS_ENSURE_STATE(mHTMLEditor); selNode = mHTMLEditor->GetNodeLocation(visNode, &selOffset); ++selOffset; } } NS_ENSURE_STATE(mHTMLEditor); NS_ENSURE_STATE(selNode->IsContent()); int32_t newOffset = mHTMLEditor->SplitNodeDeep(*citeNode, *selNode->AsContent(), selOffset, HTMLEditor::EmptyContainers::no, getter_AddRefs(leftCite), getter_AddRefs(rightCite)); NS_ENSURE_STATE(newOffset != -1); // Add an invisible <br> to the end of the left part if it was a <span> of // style="display: block". This is important, since when serialising the // cite to plain text, the span which caused the visual break is discarded. // So the added <br> will guarantee that the serialiser will insert a // break where the user saw one. if (leftCite && leftCite->IsHTMLElement(nsGkAtoms::span) && leftCite->GetPrimaryFrame()->IsFrameOfType(nsIFrame::eBlockFrame)) { nsCOMPtr<nsINode> lastChild = leftCite->GetLastChild(); if (lastChild && !lastChild->IsHTMLElement(nsGkAtoms::br)) { // We ignore the result here. nsCOMPtr<Element> invisBR = mHTMLEditor->CreateBR(leftCite, leftCite->Length()); } } selNode = citeNode->GetParentNode(); NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<Element> brNode = mHTMLEditor->CreateBR(selNode, newOffset); NS_ENSURE_STATE(brNode); // want selection before the break, and on same line aSelection->SetInterlinePosition(true); rv = aSelection->Collapse(selNode, newOffset); NS_ENSURE_SUCCESS(rv, rv); // if citeNode wasn't a block, we might also want another break before it. // We need to examine the content both before the br we just added and also // just after it. If we don't have another br or block boundary adjacent, // then we will need a 2nd br added to achieve blank line that user expects. if (IsInlineNode(*citeNode)) { NS_ENSURE_STATE(mHTMLEditor); WSRunObject wsObj(mHTMLEditor, selNode, newOffset); nsCOMPtr<nsINode> visNode; int32_t visOffset=0; WSType wsType; wsObj.PriorVisibleNode(selNode, newOffset, address_of(visNode), &visOffset, &wsType); if (wsType == WSType::normalWS || wsType == WSType::text || wsType == WSType::special) { NS_ENSURE_STATE(mHTMLEditor); WSRunObject wsObjAfterBR(mHTMLEditor, selNode, newOffset+1); wsObjAfterBR.NextVisibleNode(selNode, newOffset + 1, address_of(visNode), &visOffset, &wsType); if (wsType == WSType::normalWS || wsType == WSType::text || wsType == WSType::special || // In case we're at the very end. wsType == WSType::thisBlock) { NS_ENSURE_STATE(mHTMLEditor); brNode = mHTMLEditor->CreateBR(selNode, newOffset); NS_ENSURE_STATE(brNode); } } } // delete any empty cites bool bEmptyCite = false; if (leftCite) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->IsEmptyNode(leftCite, &bEmptyCite, true, false); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (bEmptyCite) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteNode(leftCite); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } if (rightCite) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->IsEmptyNode(rightCite, &bEmptyCite, true, false); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (bEmptyCite) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteNode(rightCite); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } *aHandled = true; } return NS_OK; } nsresult HTMLEditRules::WillDeleteSelection(Selection* aSelection, nsIEditor::EDirection aAction, nsIEditor::EStripWrappers aStripWrappers, bool* aCancel, bool* aHandled) { MOZ_ASSERT(aStripWrappers == nsIEditor::eStrip || aStripWrappers == nsIEditor::eNoStrip); if (!aSelection || !aCancel || !aHandled) { return NS_ERROR_NULL_POINTER; } // Initialize out params *aCancel = false; *aHandled = false; // Remember that we did a selection deletion. Used by CreateStyleForInsertText() mDidDeleteSelection = true; // If there is only bogus content, cancel the operation if (mBogusNode) { *aCancel = true; return NS_OK; } // First check for table selection mode. If so, hand off to table editor. nsCOMPtr<nsIDOMElement> cell; NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetFirstSelectedCell(nullptr, getter_AddRefs(cell)); if (NS_SUCCEEDED(rv) && cell) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteTableCellContents(); *aHandled = true; return rv; } cell = nullptr; // origCollapsed is used later to determine whether we should join blocks. We // don't really care about bCollapsed because it will be modified by // ExtendSelectionForDelete later. JoinBlocks should happen if the original // selection is collapsed and the cursor is at the end of a block element, in // which case ExtendSelectionForDelete would always make the selection not // collapsed. bool bCollapsed = aSelection->Collapsed(); bool join = false; bool origCollapsed = bCollapsed; nsCOMPtr<nsINode> selNode; int32_t selOffset; NS_ENSURE_STATE(aSelection->GetRangeAt(0)); nsCOMPtr<nsINode> startNode = aSelection->GetRangeAt(0)->GetStartParent(); int32_t startOffset = aSelection->GetRangeAt(0)->StartOffset(); NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE); if (bCollapsed) { // If we are inside an empty block, delete it. NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<Element> host = mHTMLEditor->GetActiveEditingHost(); NS_ENSURE_TRUE(host, NS_ERROR_FAILURE); rv = CheckForEmptyBlock(startNode, host, aSelection, aAction, aHandled); NS_ENSURE_SUCCESS(rv, rv); if (*aHandled) { return NS_OK; } // Test for distance between caret and text that will be deleted rv = CheckBidiLevelForDeletion(aSelection, GetAsDOMNode(startNode), startOffset, aAction, aCancel); NS_ENSURE_SUCCESS(rv, rv); if (*aCancel) { return NS_OK; } NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->ExtendSelectionForDelete(aSelection, &aAction); NS_ENSURE_SUCCESS(rv, rv); // We should delete nothing. if (aAction == nsIEditor::eNone) { return NS_OK; } // ExtendSelectionForDelete() may have changed the selection, update it NS_ENSURE_STATE(aSelection->GetRangeAt(0)); startNode = aSelection->GetRangeAt(0)->GetStartParent(); startOffset = aSelection->GetRangeAt(0)->StartOffset(); NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE); bCollapsed = aSelection->Collapsed(); } if (bCollapsed) { // What's in the direction we are deleting? NS_ENSURE_STATE(mHTMLEditor); WSRunObject wsObj(mHTMLEditor, startNode, startOffset); nsCOMPtr<nsINode> visNode; int32_t visOffset; WSType wsType; // Find next visible node if (aAction == nsIEditor::eNext) { wsObj.NextVisibleNode(startNode, startOffset, address_of(visNode), &visOffset, &wsType); } else { wsObj.PriorVisibleNode(startNode, startOffset, address_of(visNode), &visOffset, &wsType); } if (!visNode) { // Can't find anything to delete! *aCancel = true; // XXX This is the result of mHTMLEditor->GetFirstSelectedCell(). // The value could be both an error and NS_OK. return rv; } if (wsType == WSType::normalWS) { // We found some visible ws to delete. Let ws code handle it. *aHandled = true; if (aAction == nsIEditor::eNext) { rv = wsObj.DeleteWSForward(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { rv = wsObj.DeleteWSBackward(); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } return InsertBRIfNeeded(aSelection); } if (wsType == WSType::text) { // Found normal text to delete. OwningNonNull<Text> nodeAsText = *visNode->GetAsText(); int32_t so = visOffset; int32_t eo = visOffset + 1; if (aAction == nsIEditor::ePrevious) { if (!so) { return NS_ERROR_UNEXPECTED; } so--; eo--; // Bug 1068979: delete both codepoints if surrogate pair if (so > 0) { const nsTextFragment *text = nodeAsText->GetText(); if (NS_IS_LOW_SURROGATE(text->CharAt(so)) && NS_IS_HIGH_SURROGATE(text->CharAt(so - 1))) { so--; } } } else { RefPtr<nsRange> range = aSelection->GetRangeAt(0); NS_ENSURE_STATE(range); NS_ASSERTION(range->GetStartParent() == visNode, "selection start not in visNode"); NS_ASSERTION(range->GetEndParent() == visNode, "selection end not in visNode"); so = range->StartOffset(); eo = range->EndOffset(); } NS_ENSURE_STATE(mHTMLEditor); rv = WSRunObject::PrepareToDeleteRange(mHTMLEditor, address_of(visNode), &so, address_of(visNode), &eo); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); *aHandled = true; rv = mHTMLEditor->DeleteText(nodeAsText, std::min(so, eo), DeprecatedAbs(eo - so)); NS_ENSURE_SUCCESS(rv, rv); // XXX When Backspace key is pressed, Chromium removes following empty // text nodes when removing the last character of the non-empty text // node. However, Edge never removes empty text nodes even if // selection is in the following empty text node(s). For now, we // should keep our traditional behavior same as Edge for backward // compatibility. // XXX When Delete key is pressed, Edge removes all preceding empty // text nodes when removing the first character of the non-empty // text node. Chromium removes only selected empty text node and // following empty text nodes and the first character of the // non-empty text node. For now, we should keep our traditional // behavior same as Chromium for backward compatibility. DeleteNodeIfCollapsedText(nodeAsText); rv = InsertBRIfNeeded(aSelection); NS_ENSURE_SUCCESS(rv, rv); // Remember that we did a ranged delete for the benefit of // AfterEditInner(). mDidRangedDelete = true; return NS_OK; } if (wsType == WSType::special || wsType == WSType::br || visNode->IsHTMLElement(nsGkAtoms::hr)) { // Short circuit for invisible breaks. delete them and recurse. if (visNode->IsHTMLElement(nsGkAtoms::br) && (!mHTMLEditor || !mHTMLEditor->IsVisBreak(visNode))) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteNode(visNode); NS_ENSURE_SUCCESS(rv, rv); return WillDeleteSelection(aSelection, aAction, aStripWrappers, aCancel, aHandled); } // Special handling for backspace when positioned after <hr> if (aAction == nsIEditor::ePrevious && visNode->IsHTMLElement(nsGkAtoms::hr)) { // Only if the caret is positioned at the end-of-hr-line position, we // want to delete the <hr>. // // In other words, we only want to delete, if our selection position // (indicated by startNode and startOffset) is the position directly // after the <hr>, on the same line as the <hr>. // // To detect this case we check: // startNode == parentOfVisNode // and // startOffset -1 == visNodeOffsetToVisNodeParent // and // interline position is false (left) // // In any other case we set the position to startnode -1 and // interlineposition to false, only moving the caret to the // end-of-hr-line position. bool moveOnly = true; selNode = visNode->GetParentNode(); selOffset = selNode ? selNode->IndexOf(visNode) : -1; bool interLineIsRight; rv = aSelection->GetInterlinePosition(&interLineIsRight); NS_ENSURE_SUCCESS(rv, rv); if (startNode == selNode && startOffset - 1 == selOffset && !interLineIsRight) { moveOnly = false; } if (moveOnly) { // Go to the position after the <hr>, but to the end of the <hr> line // by setting the interline position to left. ++selOffset; aSelection->Collapse(selNode, selOffset); aSelection->SetInterlinePosition(false); mDidExplicitlySetInterline = true; *aHandled = true; // There is one exception to the move only case. If the <hr> is // followed by a <br> we want to delete the <br>. WSType otherWSType; nsCOMPtr<nsINode> otherNode; int32_t otherOffset; wsObj.NextVisibleNode(startNode, startOffset, address_of(otherNode), &otherOffset, &otherWSType); if (otherWSType == WSType::br) { // Delete the <br> NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<nsIContent> otherContent(do_QueryInterface(otherNode)); rv = WSRunObject::PrepareToDeleteNode(mHTMLEditor, otherContent); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteNode(otherNode); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } // Else continue with normal delete code } // Found break or image, or hr. NS_ENSURE_STATE(mHTMLEditor); NS_ENSURE_STATE(visNode->IsContent()); rv = WSRunObject::PrepareToDeleteNode(mHTMLEditor, visNode->AsContent()); NS_ENSURE_SUCCESS(rv, rv); // Remember sibling to visnode, if any NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<nsIContent> sibling = mHTMLEditor->GetPriorHTMLSibling(visNode); // Delete the node, and join like nodes if appropriate NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteNode(visNode); NS_ENSURE_SUCCESS(rv, rv); // We did something, so let's say so. *aHandled = true; // Is there a prior node and are they siblings? nsCOMPtr<nsINode> stepbrother; if (sibling) { NS_ENSURE_STATE(mHTMLEditor); stepbrother = mHTMLEditor->GetNextHTMLSibling(sibling); } // Are they both text nodes? If so, join them! if (startNode == stepbrother && startNode->GetAsText() && sibling->GetAsText()) { EditorDOMPoint pt = JoinNodesSmart(*sibling, *startNode->AsContent()); NS_ENSURE_STATE(pt.node); // Fix up selection rv = aSelection->Collapse(pt.node, pt.offset); NS_ENSURE_SUCCESS(rv, rv); } rv = InsertBRIfNeeded(aSelection); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } if (wsType == WSType::otherBlock) { // Make sure it's not a table element. If so, cancel the operation // (translation: users cannot backspace or delete across table cells) if (HTMLEditUtils::IsTableElement(visNode)) { *aCancel = true; return NS_OK; } // Next to a block. See if we are between a block and a br. If so, we // really want to delete the br. Else join content at selection to the // block. bool bDeletedBR = false; WSType otherWSType; nsCOMPtr<nsINode> otherNode; int32_t otherOffset; // Find node in other direction if (aAction == nsIEditor::eNext) { wsObj.PriorVisibleNode(startNode, startOffset, address_of(otherNode), &otherOffset, &otherWSType); } else { wsObj.NextVisibleNode(startNode, startOffset, address_of(otherNode), &otherOffset, &otherWSType); } // First find the adjacent node in the block nsCOMPtr<nsIContent> leafNode; nsCOMPtr<nsINode> leftNode, rightNode; if (aAction == nsIEditor::ePrevious) { NS_ENSURE_STATE(mHTMLEditor); leafNode = mHTMLEditor->GetLastEditableLeaf(*visNode); leftNode = leafNode; rightNode = startNode; } else { NS_ENSURE_STATE(mHTMLEditor); leafNode = mHTMLEditor->GetFirstEditableLeaf(*visNode); leftNode = startNode; rightNode = leafNode; } if (otherNode->IsHTMLElement(nsGkAtoms::br)) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteNode(otherNode); NS_ENSURE_SUCCESS(rv, rv); // XXX Only in this case, setting "handled" to true only when it // succeeds? *aHandled = true; bDeletedBR = true; } // Don't cross table boundaries if (leftNode && rightNode && InDifferentTableElements(leftNode, rightNode)) { return NS_OK; } if (bDeletedBR) { // Put selection at edge of block and we are done. NS_ENSURE_STATE(leafNode); EditorDOMPoint newSel = GetGoodSelPointForNode(*leafNode, aAction); NS_ENSURE_STATE(newSel.node); aSelection->Collapse(newSel.node, newSel.offset); return NS_OK; } // Else we are joining content to block nsCOMPtr<nsINode> selPointNode = startNode; int32_t selPointOffset = startOffset; { NS_ENSURE_STATE(mHTMLEditor); AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater, address_of(selPointNode), &selPointOffset); NS_ENSURE_STATE(leftNode && leftNode->IsContent() && rightNode && rightNode->IsContent()); *aHandled = true; rv = JoinBlocks(*leftNode->AsContent(), *rightNode->AsContent(), aCancel); NS_ENSURE_SUCCESS(rv, rv); } aSelection->Collapse(selPointNode, selPointOffset); return NS_OK; } if (wsType == WSType::thisBlock) { // At edge of our block. Look beside it and see if we can join to an // adjacent block // Make sure it's not a table element. If so, cancel the operation // (translation: users cannot backspace or delete across table cells) if (HTMLEditUtils::IsTableElement(visNode)) { *aCancel = true; return NS_OK; } // First find the relevant nodes nsCOMPtr<nsINode> leftNode, rightNode; if (aAction == nsIEditor::ePrevious) { NS_ENSURE_STATE(mHTMLEditor); leftNode = mHTMLEditor->GetPriorHTMLNode(visNode); rightNode = startNode; } else { NS_ENSURE_STATE(mHTMLEditor); rightNode = mHTMLEditor->GetNextHTMLNode(visNode); leftNode = startNode; } // Nothing to join if (!leftNode || !rightNode) { *aCancel = true; return NS_OK; } // Don't cross table boundaries -- cancel it if (InDifferentTableElements(leftNode, rightNode)) { *aCancel = true; return NS_OK; } nsCOMPtr<nsINode> selPointNode = startNode; int32_t selPointOffset = startOffset; { NS_ENSURE_STATE(mHTMLEditor); AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater, address_of(selPointNode), &selPointOffset); NS_ENSURE_STATE(leftNode->IsContent() && rightNode->IsContent()); *aHandled = true; rv = JoinBlocks(*leftNode->AsContent(), *rightNode->AsContent(), aCancel); NS_ENSURE_SUCCESS(rv, rv); } aSelection->Collapse(selPointNode, selPointOffset); return NS_OK; } } // Else we have a non-collapsed selection. First adjust the selection. rv = ExpandSelectionForDeletion(*aSelection); NS_ENSURE_SUCCESS(rv, rv); // Remember that we did a ranged delete for the benefit of AfterEditInner(). mDidRangedDelete = true; // Refresh start and end points NS_ENSURE_STATE(aSelection->GetRangeAt(0)); startNode = aSelection->GetRangeAt(0)->GetStartParent(); startOffset = aSelection->GetRangeAt(0)->StartOffset(); NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE); nsCOMPtr<nsINode> endNode = aSelection->GetRangeAt(0)->GetEndParent(); int32_t endOffset = aSelection->GetRangeAt(0)->EndOffset(); NS_ENSURE_TRUE(endNode, NS_ERROR_FAILURE); // Figure out if the endpoints are in nodes that can be merged. Adjust // surrounding whitespace in preparation to delete selection. if (!IsPlaintextEditor()) { NS_ENSURE_STATE(mHTMLEditor); AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor); rv = WSRunObject::PrepareToDeleteRange(mHTMLEditor, address_of(startNode), &startOffset, address_of(endNode), &endOffset); NS_ENSURE_SUCCESS(rv, rv); } { // Track location of where we are deleting NS_ENSURE_STATE(mHTMLEditor); AutoTrackDOMPoint startTracker(mHTMLEditor->mRangeUpdater, address_of(startNode), &startOffset); AutoTrackDOMPoint endTracker(mHTMLEditor->mRangeUpdater, address_of(endNode), &endOffset); // We are handling all ranged deletions directly now. *aHandled = true; if (endNode == startNode) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteSelectionImpl(aAction, aStripWrappers); NS_ENSURE_SUCCESS(rv, rv); } else { // Figure out mailcite ancestors nsCOMPtr<Element> startCiteNode = GetTopEnclosingMailCite(*startNode); nsCOMPtr<Element> endCiteNode = GetTopEnclosingMailCite(*endNode); // If we only have a mailcite at one of the two endpoints, set the // directionality of the deletion so that the selection will end up // outside the mailcite. if (startCiteNode && !endCiteNode) { aAction = nsIEditor::eNext; } else if (!startCiteNode && endCiteNode) { aAction = nsIEditor::ePrevious; } // Figure out block parents NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<Element> leftParent = mHTMLEditor->GetBlock(*startNode); nsCOMPtr<Element> rightParent = mHTMLEditor->GetBlock(*endNode); // Are endpoint block parents the same? Use default deletion if (leftParent && leftParent == rightParent) { NS_ENSURE_STATE(mHTMLEditor); mHTMLEditor->DeleteSelectionImpl(aAction, aStripWrappers); } else { // Deleting across blocks. Are the blocks of same type? NS_ENSURE_STATE(leftParent && rightParent); // Are the blocks siblings? nsCOMPtr<nsINode> leftBlockParent = leftParent->GetParentNode(); nsCOMPtr<nsINode> rightBlockParent = rightParent->GetParentNode(); // MOOSE: this could conceivably screw up a table.. fix me. NS_ENSURE_STATE(mHTMLEditor); if (leftBlockParent == rightBlockParent && mHTMLEditor->NodesSameType(GetAsDOMNode(leftParent), GetAsDOMNode(rightParent)) && // XXX What's special about these three types of block? (leftParent->IsHTMLElement(nsGkAtoms::p) || HTMLEditUtils::IsListItem(leftParent) || HTMLEditUtils::IsHeader(*leftParent))) { // First delete the selection NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteSelectionImpl(aAction, aStripWrappers); NS_ENSURE_SUCCESS(rv, rv); // Join blocks NS_ENSURE_STATE(mHTMLEditor); EditorDOMPoint pt = mHTMLEditor->JoinNodeDeep(*leftParent, *rightParent); NS_ENSURE_STATE(pt.node); // Fix up selection rv = aSelection->Collapse(pt.node, pt.offset); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } // Else blocks not same type, or not siblings. Delete everything // except table elements. join = true; uint32_t rangeCount = aSelection->RangeCount(); for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) { OwningNonNull<nsRange> range = *aSelection->GetRangeAt(rangeIdx); // Build a list of nodes in the range nsTArray<OwningNonNull<nsINode>> arrayOfNodes; TrivialFunctor functor; DOMSubtreeIterator iter; nsresult rv = iter.Init(*range); NS_ENSURE_SUCCESS(rv, rv); iter.AppendList(functor, arrayOfNodes); // Now that we have the list, delete non-table elements int32_t listCount = arrayOfNodes.Length(); for (int32_t j = 0; j < listCount; j++) { nsCOMPtr<nsINode> somenode = do_QueryInterface(arrayOfNodes[0]); NS_ENSURE_STATE(somenode); DeleteNonTableElements(somenode); arrayOfNodes.RemoveElementAt(0); // If something visible is deleted, no need to join. Visible means // all nodes except non-visible textnodes and breaks. if (join && origCollapsed) { if (!somenode->IsContent()) { join = false; continue; } nsCOMPtr<nsIContent> content = somenode->AsContent(); if (content->NodeType() == nsIDOMNode::TEXT_NODE) { NS_ENSURE_STATE(mHTMLEditor); mHTMLEditor->IsVisTextNode(content, &join, true); } else { NS_ENSURE_STATE(mHTMLEditor); join = content->IsHTMLElement(nsGkAtoms::br) && !mHTMLEditor->IsVisBreak(somenode); } } } } // Check endpoints for possible text deletion. We can assume that if // text node is found, we can delete to end or to begining as // appropriate, since the case where both sel endpoints in same text // node was already handled (we wouldn't be here) if (startNode->GetAsText() && startNode->Length() > static_cast<uint32_t>(startOffset)) { // Delete to last character OwningNonNull<nsGenericDOMDataNode> dataNode = *static_cast<nsGenericDOMDataNode*>(startNode.get()); NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteText(dataNode, startOffset, startNode->Length() - startOffset); NS_ENSURE_SUCCESS(rv, rv); } if (endNode->GetAsText() && endOffset) { // Delete to first character NS_ENSURE_STATE(mHTMLEditor); OwningNonNull<nsGenericDOMDataNode> dataNode = *static_cast<nsGenericDOMDataNode*>(endNode.get()); rv = mHTMLEditor->DeleteText(dataNode, 0, endOffset); NS_ENSURE_SUCCESS(rv, rv); } if (join) { rv = JoinBlocks(*leftParent, *rightParent, aCancel); NS_ENSURE_SUCCESS(rv, rv); } } } } // We might have left only collapsed whitespace in the start/end nodes { AutoTrackDOMPoint startTracker(mHTMLEditor->mRangeUpdater, address_of(startNode), &startOffset); AutoTrackDOMPoint endTracker(mHTMLEditor->mRangeUpdater, address_of(endNode), &endOffset); DeleteNodeIfCollapsedText(*startNode); DeleteNodeIfCollapsedText(*endNode); } // If we're joining blocks: if deleting forward the selection should be // collapsed to the end of the selection, if deleting backward the selection // should be collapsed to the beginning of the selection. But if we're not // joining then the selection should collapse to the beginning of the // selection if we'redeleting forward, because the end of the selection will // still be in the next block. And same thing for deleting backwards // (selection should collapse to the end, because the beginning will still be // in the first block). See Bug 507936 if (aAction == (join ? nsIEditor::eNext : nsIEditor::ePrevious)) { rv = aSelection->Collapse(endNode, endOffset); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { rv = aSelection->Collapse(startNode, startOffset); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } return NS_OK; } /** * If aNode is a text node that contains only collapsed whitespace, delete it. * It doesn't serve any useful purpose, and we don't want it to confuse code * that doesn't correctly skip over it. * * If deleting the node fails (like if it's not editable), the caller should * proceed as usual, so don't return any errors. */ void HTMLEditRules::DeleteNodeIfCollapsedText(nsINode& aNode) { if (!aNode.GetAsText()) { return; } bool empty; nsresult rv = mHTMLEditor->IsVisTextNode(aNode.AsContent(), &empty, false); NS_ENSURE_SUCCESS_VOID(rv); if (empty) { mHTMLEditor->DeleteNode(&aNode); } } /** * InsertBRIfNeeded() determines if a br is needed for current selection to not * be spastic. If so, it inserts one. Callers responsibility to only call * with collapsed selection. * * @param aSelection The collapsed selection. */ nsresult HTMLEditRules::InsertBRIfNeeded(Selection* aSelection) { NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER); // get selection nsCOMPtr<nsINode> node; int32_t offset; nsresult rv = mTextEditor->GetStartNodeAndOffset(aSelection, getter_AddRefs(node), &offset); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_TRUE(node, NS_ERROR_FAILURE); // inline elements don't need any br if (!IsBlockNode(*node)) { return NS_OK; } // examine selection NS_ENSURE_STATE(mHTMLEditor); WSRunObject wsObj(mHTMLEditor, node, offset); if (((wsObj.mStartReason & WSType::block) || (wsObj.mStartReason & WSType::br)) && (wsObj.mEndReason & WSType::block)) { // if we are tucked between block boundaries then insert a br // first check that we are allowed to NS_ENSURE_STATE(mHTMLEditor); if (mHTMLEditor->CanContainTag(*node, *nsGkAtoms::br)) { NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<Element> br = mHTMLEditor->CreateBR(node, offset, nsIEditor::ePrevious); return br ? NS_OK : NS_ERROR_FAILURE; } } return NS_OK; } /** * GetGoodSelPointForNode() finds where at a node you would want to set the * selection if you were trying to have a caret next to it. Always returns a * valid value (unless mHTMLEditor has gone away). * * @param aNode The node * @param aAction Which edge to find: * eNext/eNextWord/eToEndOfLine indicates beginning, * ePrevious/PreviousWord/eToBeginningOfLine ending. */ EditorDOMPoint HTMLEditRules::GetGoodSelPointForNode(nsINode& aNode, nsIEditor::EDirection aAction) { MOZ_ASSERT(aAction == nsIEditor::eNext || aAction == nsIEditor::eNextWord || aAction == nsIEditor::ePrevious || aAction == nsIEditor::ePreviousWord || aAction == nsIEditor::eToBeginningOfLine || aAction == nsIEditor::eToEndOfLine); bool isPreviousAction = (aAction == nsIEditor::ePrevious || aAction == nsIEditor::ePreviousWord || aAction == nsIEditor::eToBeginningOfLine); NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint()); if (aNode.GetAsText() || mHTMLEditor->IsContainer(&aNode) || NS_WARN_IF(!aNode.GetParentNode())) { return EditorDOMPoint(&aNode, isPreviousAction ? aNode.Length() : 0); } EditorDOMPoint ret; ret.node = aNode.GetParentNode(); ret.offset = ret.node ? ret.node->IndexOf(&aNode) : -1; NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint()); if ((!aNode.IsHTMLElement(nsGkAtoms::br) || mHTMLEditor->IsVisBreak(&aNode)) && isPreviousAction) { ret.offset++; } return ret; } /** * This method is used to join two block elements. The right element is always * joined to the left element. If the elements are the same type and not * nested within each other, JoinNodesSmart is called (example, joining two * list items together into one). If the elements are not the same type, or * one is a descendant of the other, we instead destroy the right block placing * its children into leftblock. DTD containment rules are followed throughout. */ nsresult HTMLEditRules::JoinBlocks(nsIContent& aLeftNode, nsIContent& aRightNode, bool* aCanceled) { MOZ_ASSERT(aCanceled); NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); nsCOMPtr<Element> leftBlock = htmlEditor->GetBlock(aLeftNode); nsCOMPtr<Element> rightBlock = htmlEditor->GetBlock(aRightNode); // Sanity checks NS_ENSURE_TRUE(leftBlock && rightBlock, NS_ERROR_NULL_POINTER); NS_ENSURE_STATE(leftBlock != rightBlock); if (HTMLEditUtils::IsTableElement(leftBlock) || HTMLEditUtils::IsTableElement(rightBlock)) { // Do not try to merge table elements *aCanceled = true; return NS_OK; } // Make sure we don't try to move things into HR's, which look like blocks // but aren't containers if (leftBlock->IsHTMLElement(nsGkAtoms::hr)) { leftBlock = htmlEditor->GetBlockNodeParent(leftBlock); } if (rightBlock->IsHTMLElement(nsGkAtoms::hr)) { rightBlock = htmlEditor->GetBlockNodeParent(rightBlock); } NS_ENSURE_STATE(leftBlock && rightBlock); // Bail if both blocks the same if (leftBlock == rightBlock) { *aCanceled = true; return NS_OK; } // Joining a list item to its parent is a NOP. if (HTMLEditUtils::IsList(leftBlock) && HTMLEditUtils::IsListItem(rightBlock) && rightBlock->GetParentNode() == leftBlock) { return NS_OK; } // Special rule here: if we are trying to join list items, and they are in // different lists, join the lists instead. bool mergeLists = false; nsIAtom* existingList = nsGkAtoms::_empty; int32_t offset; nsCOMPtr<Element> leftList, rightList; if (HTMLEditUtils::IsListItem(leftBlock) && HTMLEditUtils::IsListItem(rightBlock)) { leftList = leftBlock->GetParentElement(); rightList = rightBlock->GetParentElement(); if (leftList && rightList && leftList != rightList && !EditorUtils::IsDescendantOf(leftList, rightBlock, &offset) && !EditorUtils::IsDescendantOf(rightList, leftBlock, &offset)) { // There are some special complications if the lists are descendants of // the other lists' items. Note that it is okay for them to be // descendants of the other lists themselves, which is the usual case for // sublists in our implementation. leftBlock = leftList; rightBlock = rightList; mergeLists = true; existingList = leftList->NodeInfo()->NameAtom(); } } AutoTransactionsConserveSelection dontSpazMySelection(htmlEditor); int32_t rightOffset = 0; int32_t leftOffset = -1; // offset below is where you find yourself in rightBlock when you traverse // upwards from leftBlock if (EditorUtils::IsDescendantOf(leftBlock, rightBlock, &rightOffset)) { // Tricky case. Left block is inside right block. Do ws adjustment. This // just destroys non-visible ws at boundaries we will be joining. rightOffset++; nsresult rv = WSRunObject::ScrubBlockBoundary(htmlEditor, WSRunObject::kBlockEnd, leftBlock); NS_ENSURE_SUCCESS(rv, rv); { // We can't just track rightBlock because it's an Element. nsCOMPtr<nsINode> trackingRightBlock(rightBlock); AutoTrackDOMPoint tracker(htmlEditor->mRangeUpdater, address_of(trackingRightBlock), &rightOffset); rv = WSRunObject::ScrubBlockBoundary(htmlEditor, WSRunObject::kAfterBlock, rightBlock, rightOffset); NS_ENSURE_SUCCESS(rv, rv); if (trackingRightBlock->IsElement()) { rightBlock = trackingRightBlock->AsElement(); } else { NS_ENSURE_STATE(trackingRightBlock->GetParentElement()); rightBlock = trackingRightBlock->GetParentElement(); } } // Do br adjustment. nsCOMPtr<Element> brNode = CheckForInvisibleBR(*leftBlock, BRLocation::blockEnd); if (mergeLists) { // The idea here is to take all children in rightList that are past // offset, and pull them into leftlist. for (nsCOMPtr<nsIContent> child = rightList->GetChildAt(offset); child; child = rightList->GetChildAt(rightOffset)) { rv = htmlEditor->MoveNode(child, leftList, -1); NS_ENSURE_SUCCESS(rv, rv); } } else { MoveBlock(*leftBlock, *rightBlock, leftOffset, rightOffset); } if (brNode) { htmlEditor->DeleteNode(brNode); } // Offset below is where you find yourself in leftBlock when you traverse // upwards from rightBlock } else if (EditorUtils::IsDescendantOf(rightBlock, leftBlock, &leftOffset)) { // Tricky case. Right block is inside left block. Do ws adjustment. This // just destroys non-visible ws at boundaries we will be joining. nsresult rv = WSRunObject::ScrubBlockBoundary(htmlEditor, WSRunObject::kBlockStart, rightBlock); NS_ENSURE_SUCCESS(rv, rv); { // We can't just track leftBlock because it's an Element, so track // something else. nsCOMPtr<nsINode> trackingLeftBlock(leftBlock); AutoTrackDOMPoint tracker(htmlEditor->mRangeUpdater, address_of(trackingLeftBlock), &leftOffset); rv = WSRunObject::ScrubBlockBoundary(htmlEditor, WSRunObject::kBeforeBlock, leftBlock, leftOffset); NS_ENSURE_SUCCESS(rv, rv); if (trackingLeftBlock->IsElement()) { leftBlock = trackingLeftBlock->AsElement(); } else { NS_ENSURE_STATE(trackingLeftBlock->GetParentElement()); leftBlock = trackingLeftBlock->GetParentElement(); } } // Do br adjustment. nsCOMPtr<Element> brNode = CheckForInvisibleBR(*leftBlock, BRLocation::beforeBlock, leftOffset); if (mergeLists) { MoveContents(*rightList, *leftList, &leftOffset); } else { // Left block is a parent of right block, and the parent of the previous // visible content. Right block is a child and contains the contents we // want to move. int32_t previousContentOffset; nsCOMPtr<nsINode> previousContentParent; if (&aLeftNode == leftBlock) { // We are working with valid HTML, aLeftNode is a block node, and is // therefore allowed to contain rightBlock. This is the simple case, // we will simply move the content in rightBlock out of its block. previousContentParent = leftBlock; previousContentOffset = leftOffset; } else { // We try to work as well as possible with HTML that's already invalid. // Although "right block" is a block, and a block must not be contained // in inline elements, reality is that broken documents do exist. The // DIRECT parent of "left NODE" might be an inline element. Previous // versions of this code skipped inline parents until the first block // parent was found (and used "left block" as the destination). // However, in some situations this strategy moves the content to an // unexpected position. (see bug 200416) The new idea is to make the // moving content a sibling, next to the previous visible content. previousContentParent = aLeftNode.GetParentNode(); previousContentOffset = previousContentParent ? previousContentParent->IndexOf(&aLeftNode) : -1; // We want to move our content just after the previous visible node. previousContentOffset++; } // Because we don't want the moving content to receive the style of the // previous content, we split the previous content's style. nsCOMPtr<Element> editorRoot = htmlEditor->GetEditorRoot(); if (!editorRoot || &aLeftNode != editorRoot) { nsCOMPtr<nsIContent> splittedPreviousContent; rv = htmlEditor->SplitStyleAbovePoint( address_of(previousContentParent), &previousContentOffset, nullptr, nullptr, nullptr, getter_AddRefs(splittedPreviousContent)); NS_ENSURE_SUCCESS(rv, rv); if (splittedPreviousContent) { previousContentParent = splittedPreviousContent->GetParentNode(); previousContentOffset = previousContentParent ? previousContentParent->IndexOf(splittedPreviousContent) : -1; } } NS_ENSURE_TRUE(previousContentParent, NS_ERROR_NULL_POINTER); rv = MoveBlock(*previousContentParent->AsElement(), *rightBlock, previousContentOffset, rightOffset); NS_ENSURE_SUCCESS(rv, rv); } if (brNode) { htmlEditor->DeleteNode(brNode); } } else { // Normal case. Blocks are siblings, or at least close enough. An example // of the latter is <p>paragraph</p><ul><li>one<li>two<li>three</ul>. The // first li and the p are not true siblings, but we still want to join them // if you backspace from li into p. // Adjust whitespace at block boundaries nsresult rv = WSRunObject::PrepareToJoinBlocks(htmlEditor, leftBlock, rightBlock); NS_ENSURE_SUCCESS(rv, rv); // Do br adjustment. nsCOMPtr<Element> brNode = CheckForInvisibleBR(*leftBlock, BRLocation::blockEnd); if (mergeLists || leftBlock->NodeInfo()->NameAtom() == rightBlock->NodeInfo()->NameAtom()) { // Nodes are same type. merge them. EditorDOMPoint pt = JoinNodesSmart(*leftBlock, *rightBlock); if (pt.node && mergeLists) { nsCOMPtr<Element> newBlock; ConvertListType(rightBlock, getter_AddRefs(newBlock), existingList, nsGkAtoms::li); } } else { // Nodes are dissimilar types. rv = MoveBlock(*leftBlock, *rightBlock, leftOffset, rightOffset); NS_ENSURE_SUCCESS(rv, rv); } if (brNode) { rv = htmlEditor->DeleteNode(brNode); NS_ENSURE_SUCCESS(rv, rv); } } return NS_OK; } /** * Moves the content from aRightBlock starting from aRightOffset into * aLeftBlock at aLeftOffset. Note that the "block" might merely be inline * nodes between <br>s, or between blocks, etc. DTD containment rules are * followed throughout. */ nsresult HTMLEditRules::MoveBlock(Element& aLeftBlock, Element& aRightBlock, int32_t aLeftOffset, int32_t aRightOffset) { nsTArray<OwningNonNull<nsINode>> arrayOfNodes; // GetNodesFromPoint is the workhorse that figures out what we wnat to move. nsresult rv = GetNodesFromPoint(EditorDOMPoint(&aRightBlock, aRightOffset), EditAction::makeList, arrayOfNodes, TouchContent::yes); NS_ENSURE_SUCCESS(rv, rv); for (uint32_t i = 0; i < arrayOfNodes.Length(); i++) { // get the node to act on if (IsBlockNode(arrayOfNodes[i])) { // For block nodes, move their contents only, then delete block. rv = MoveContents(*arrayOfNodes[i]->AsElement(), aLeftBlock, &aLeftOffset); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteNode(arrayOfNodes[i]); } else { // Otherwise move the content as is, checking against the DTD. rv = MoveNodeSmart(*arrayOfNodes[i]->AsContent(), aLeftBlock, &aLeftOffset); } } // XXX We're only checking return value of the last iteration NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } /** * This method is used to move node aNode to (aDestElement, aInOutDestOffset). * DTD containment rules are followed throughout. aInOutDestOffset is updated * to point _after_ inserted content. */ nsresult HTMLEditRules::MoveNodeSmart(nsIContent& aNode, Element& aDestElement, int32_t* aInOutDestOffset) { MOZ_ASSERT(aInOutDestOffset); NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); // Check if this node can go into the destination node if (htmlEditor->CanContain(aDestElement, aNode)) { // If it can, move it there nsresult rv = htmlEditor->MoveNode(&aNode, &aDestElement, *aInOutDestOffset); NS_ENSURE_SUCCESS(rv, rv); if (*aInOutDestOffset != -1) { (*aInOutDestOffset)++; } } else { // If it can't, move its children (if any), and then delete it. if (aNode.IsElement()) { nsresult rv = MoveContents(*aNode.AsElement(), aDestElement, aInOutDestOffset); NS_ENSURE_SUCCESS(rv, rv); } nsresult rv = htmlEditor->DeleteNode(&aNode); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } /** * Moves the _contents_ of aElement to (aDestElement, aInOutDestOffset). DTD * containment rules are followed throughout. aInOutDestOffset is updated to * point _after_ inserted content. */ nsresult HTMLEditRules::MoveContents(Element& aElement, Element& aDestElement, int32_t* aInOutDestOffset) { MOZ_ASSERT(aInOutDestOffset); NS_ENSURE_TRUE(&aElement != &aDestElement, NS_ERROR_ILLEGAL_VALUE); while (aElement.GetFirstChild()) { nsresult rv = MoveNodeSmart(*aElement.GetFirstChild(), aDestElement, aInOutDestOffset); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } nsresult HTMLEditRules::DeleteNonTableElements(nsINode* aNode) { MOZ_ASSERT(aNode); if (!HTMLEditUtils::IsTableElementButNotTable(aNode)) { NS_ENSURE_STATE(mHTMLEditor); return mHTMLEditor->DeleteNode(aNode->AsDOMNode()); } for (int32_t i = aNode->GetChildCount() - 1; i >= 0; --i) { nsresult rv = DeleteNonTableElements(aNode->GetChildAt(i)); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } nsresult HTMLEditRules::DidDeleteSelection(Selection* aSelection, nsIEditor::EDirection aDir, nsresult aResult) { if (!aSelection) { return NS_ERROR_NULL_POINTER; } // find where we are nsCOMPtr<nsINode> startNode; int32_t startOffset; nsresult rv = mTextEditor->GetStartNodeAndOffset(aSelection, getter_AddRefs(startNode), &startOffset); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE); // find any enclosing mailcite nsCOMPtr<Element> citeNode = GetTopEnclosingMailCite(*startNode); if (citeNode) { bool isEmpty = true, seenBR = false; NS_ENSURE_STATE(mHTMLEditor); mHTMLEditor->IsEmptyNodeImpl(citeNode, &isEmpty, true, true, false, &seenBR); if (isEmpty) { int32_t offset; nsCOMPtr<nsINode> parent = EditorBase::GetNodeLocation(citeNode, &offset); NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteNode(citeNode); NS_ENSURE_SUCCESS(rv, rv); if (parent && seenBR) { NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<Element> brNode = mHTMLEditor->CreateBR(parent, offset); NS_ENSURE_STATE(brNode); aSelection->Collapse(parent, offset); } } } // call through to base class return TextEditRules::DidDeleteSelection(aSelection, aDir, aResult); } nsresult HTMLEditRules::WillMakeList(Selection* aSelection, const nsAString* aListType, bool aEntireList, const nsAString* aBulletType, bool* aCancel, bool* aHandled, const nsAString* aItemType) { if (!aSelection || !aListType || !aCancel || !aHandled) { return NS_ERROR_NULL_POINTER; } OwningNonNull<nsIAtom> listType = NS_Atomize(*aListType); WillInsert(*aSelection, aCancel); // initialize out param // we want to ignore result of WillInsert() *aCancel = false; *aHandled = false; // deduce what tag to use for list items nsCOMPtr<nsIAtom> itemType; if (aItemType) { itemType = NS_Atomize(*aItemType); NS_ENSURE_TRUE(itemType, NS_ERROR_OUT_OF_MEMORY); } else if (listType == nsGkAtoms::dl) { itemType = nsGkAtoms::dd; } else { itemType = nsGkAtoms::li; } // convert the selection ranges into "promoted" selection ranges: // this basically just expands the range to include the immediate // block parent, and then further expands to include any ancestors // whose children are all in the range *aHandled = true; nsresult rv = NormalizeSelection(aSelection); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor); nsTArray<OwningNonNull<nsINode>> arrayOfNodes; rv = GetListActionNodes(arrayOfNodes, aEntireList ? EntireList::yes : EntireList::no); NS_ENSURE_SUCCESS(rv, rv); // check if all our nodes are <br>s, or empty inlines bool bOnlyBreaks = true; for (auto& curNode : arrayOfNodes) { // if curNode is not a Break or empty inline, we're done if (!TextEditUtils::IsBreak(curNode) && !IsEmptyInline(curNode)) { bOnlyBreaks = false; break; } } // if no nodes, we make empty list. Ditto if the user tried to make a list // of some # of breaks. if (arrayOfNodes.IsEmpty() || bOnlyBreaks) { // if only breaks, delete them if (bOnlyBreaks) { for (auto& node : arrayOfNodes) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteNode(node); NS_ENSURE_SUCCESS(rv, rv); } } // get selection location NS_ENSURE_STATE(aSelection->RangeCount()); nsCOMPtr<nsINode> parent = aSelection->GetRangeAt(0)->GetStartParent(); int32_t offset = aSelection->GetRangeAt(0)->StartOffset(); NS_ENSURE_STATE(parent); // make sure we can put a list here NS_ENSURE_STATE(mHTMLEditor); if (!mHTMLEditor->CanContainTag(*parent, listType)) { *aCancel = true; return NS_OK; } rv = SplitAsNeeded(listType, parent, offset); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<Element> theList = mHTMLEditor->CreateNode(listType, parent, offset); NS_ENSURE_STATE(theList); NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<Element> theListItem = mHTMLEditor->CreateNode(itemType, theList, 0); NS_ENSURE_STATE(theListItem); // remember our new block for postprocessing mNewBlock = theListItem; // put selection in new list item *aHandled = true; rv = aSelection->Collapse(theListItem, 0); // Don't restore the selection selectionRestorer.Abort(); return rv; } // if there is only one node in the array, and it is a list, div, or // blockquote, then look inside of it until we find inner list or content. LookInsideDivBQandList(arrayOfNodes); // Ok, now go through all the nodes and put then in the list, // or whatever is approriate. Wohoo! uint32_t listCount = arrayOfNodes.Length(); nsCOMPtr<nsINode> curParent; nsCOMPtr<Element> curList, prevListItem; for (uint32_t i = 0; i < listCount; i++) { // here's where we actually figure out what to do nsCOMPtr<Element> newBlock; NS_ENSURE_STATE(arrayOfNodes[i]->IsContent()); OwningNonNull<nsIContent> curNode = *arrayOfNodes[i]->AsContent(); int32_t offset; curParent = EditorBase::GetNodeLocation(curNode, &offset); // make sure we don't assemble content that is in different table cells // into the same list. respect table cell boundaries when listifying. if (curList && InDifferentTableElements(curList, curNode)) { curList = nullptr; } // if curNode is a Break, delete it, and quit remembering prev list item if (TextEditUtils::IsBreak(curNode)) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteNode(curNode); NS_ENSURE_SUCCESS(rv, rv); prevListItem = nullptr; continue; } else if (IsEmptyInline(curNode)) { // if curNode is an empty inline container, delete it NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteNode(curNode); NS_ENSURE_SUCCESS(rv, rv); continue; } if (HTMLEditUtils::IsList(curNode)) { // do we have a curList already? if (curList && !EditorUtils::IsDescendantOf(curNode, curList)) { // move all of our children into curList. cheezy way to do it: move // whole list and then RemoveContainer() on the list. ConvertListType // first: that routine handles converting the list item types, if // needed NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->MoveNode(curNode, curList, -1); NS_ENSURE_SUCCESS(rv, rv); rv = ConvertListType(curNode->AsElement(), getter_AddRefs(newBlock), listType, itemType); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->RemoveBlockContainer(*newBlock); NS_ENSURE_SUCCESS(rv, rv); } else { // replace list with new list type rv = ConvertListType(curNode->AsElement(), getter_AddRefs(newBlock), listType, itemType); NS_ENSURE_SUCCESS(rv, rv); curList = newBlock; } prevListItem = nullptr; continue; } if (HTMLEditUtils::IsListItem(curNode)) { NS_ENSURE_STATE(mHTMLEditor); if (!curParent->IsHTMLElement(listType)) { // list item is in wrong type of list. if we don't have a curList, // split the old list and make a new list of correct type. if (!curList || EditorUtils::IsDescendantOf(curNode, curList)) { NS_ENSURE_STATE(mHTMLEditor); NS_ENSURE_STATE(curParent->IsContent()); ErrorResult rv; nsCOMPtr<nsIContent> splitNode = mHTMLEditor->SplitNode(*curParent->AsContent(), offset, rv); NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult()); newBlock = splitNode ? splitNode->AsElement() : nullptr; int32_t offset; nsCOMPtr<nsINode> parent = EditorBase::GetNodeLocation(curParent, &offset); NS_ENSURE_STATE(mHTMLEditor); curList = mHTMLEditor->CreateNode(listType, parent, offset); NS_ENSURE_STATE(curList); } // move list item to new list NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->MoveNode(curNode, curList, -1); NS_ENSURE_SUCCESS(rv, rv); // convert list item type if needed NS_ENSURE_STATE(mHTMLEditor); if (!curNode->IsHTMLElement(itemType)) { NS_ENSURE_STATE(mHTMLEditor); newBlock = mHTMLEditor->ReplaceContainer(curNode->AsElement(), itemType); NS_ENSURE_STATE(newBlock); } } else { // item is in right type of list. But we might still have to move it. // and we might need to convert list item types. if (!curList) { curList = curParent->AsElement(); } else if (curParent != curList) { // move list item to new list NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->MoveNode(curNode, curList, -1); NS_ENSURE_SUCCESS(rv, rv); } NS_ENSURE_STATE(mHTMLEditor); if (!curNode->IsHTMLElement(itemType)) { NS_ENSURE_STATE(mHTMLEditor); newBlock = mHTMLEditor->ReplaceContainer(curNode->AsElement(), itemType); NS_ENSURE_STATE(newBlock); } } NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<nsIDOMElement> curElement = do_QueryInterface(curNode); NS_NAMED_LITERAL_STRING(typestr, "type"); if (aBulletType && !aBulletType->IsEmpty()) { rv = mHTMLEditor->SetAttribute(curElement, typestr, *aBulletType); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { rv = mHTMLEditor->RemoveAttribute(curElement, typestr); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } continue; } // if we hit a div clear our prevListItem, insert divs contents // into our node array, and remove the div if (curNode->IsHTMLElement(nsGkAtoms::div)) { prevListItem = nullptr; int32_t j = i + 1; GetInnerContent(*curNode, arrayOfNodes, &j); NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->RemoveContainer(curNode); NS_ENSURE_SUCCESS(rv, rv); listCount = arrayOfNodes.Length(); continue; } // need to make a list to put things in if we haven't already, if (!curList) { rv = SplitAsNeeded(listType, curParent, offset); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); curList = mHTMLEditor->CreateNode(listType, curParent, offset); // remember our new block for postprocessing mNewBlock = curList; // curList is now the correct thing to put curNode in prevListItem = nullptr; } // if curNode isn't a list item, we must wrap it in one nsCOMPtr<Element> listItem; if (!HTMLEditUtils::IsListItem(curNode)) { if (IsInlineNode(curNode) && prevListItem) { // this is a continuation of some inline nodes that belong together in // the same list item. use prevListItem NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->MoveNode(curNode, prevListItem, -1); NS_ENSURE_SUCCESS(rv, rv); } else { // don't wrap li around a paragraph. instead replace paragraph with li if (curNode->IsHTMLElement(nsGkAtoms::p)) { NS_ENSURE_STATE(mHTMLEditor); listItem = mHTMLEditor->ReplaceContainer(curNode->AsElement(), itemType); NS_ENSURE_STATE(listItem); } else { NS_ENSURE_STATE(mHTMLEditor); listItem = mHTMLEditor->InsertContainerAbove(curNode, itemType); NS_ENSURE_STATE(listItem); } if (IsInlineNode(curNode)) { prevListItem = listItem; } else { prevListItem = nullptr; } } } else { listItem = curNode->AsElement(); } if (listItem) { // if we made a new list item, deal with it: tuck the listItem into the // end of the active list NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->MoveNode(listItem, curList, -1); NS_ENSURE_SUCCESS(rv, rv); } } return NS_OK; } nsresult HTMLEditRules::WillRemoveList(Selection* aSelection, bool aOrdered, bool* aCancel, bool* aHandled) { if (!aSelection || !aCancel || !aHandled) { return NS_ERROR_NULL_POINTER; } // initialize out param *aCancel = false; *aHandled = true; nsresult rv = NormalizeSelection(aSelection); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor); nsTArray<RefPtr<nsRange>> arrayOfRanges; GetPromotedRanges(*aSelection, arrayOfRanges, EditAction::makeList); // use these ranges to contruct a list of nodes to act on. nsTArray<OwningNonNull<nsINode>> arrayOfNodes; rv = GetListActionNodes(arrayOfNodes, EntireList::no); NS_ENSURE_SUCCESS(rv, rv); // Remove all non-editable nodes. Leave them be. for (int32_t i = arrayOfNodes.Length() - 1; i >= 0; i--) { OwningNonNull<nsINode> testNode = arrayOfNodes[i]; NS_ENSURE_STATE(mHTMLEditor); if (!mHTMLEditor->IsEditable(testNode)) { arrayOfNodes.RemoveElementAt(i); } } // Only act on lists or list items in the array for (auto& curNode : arrayOfNodes) { // here's where we actually figure out what to do if (HTMLEditUtils::IsListItem(curNode)) { // unlist this listitem bool bOutOfList; do { rv = PopListItem(GetAsDOMNode(curNode), &bOutOfList); NS_ENSURE_SUCCESS(rv, rv); } while (!bOutOfList); // keep popping it out until it's not in a list anymore } else if (HTMLEditUtils::IsList(curNode)) { // node is a list, move list items out rv = RemoveListStructure(*curNode->AsElement()); NS_ENSURE_SUCCESS(rv, rv); } } return NS_OK; } nsresult HTMLEditRules::WillMakeDefListItem(Selection* aSelection, const nsAString *aItemType, bool aEntireList, bool* aCancel, bool* aHandled) { // for now we let WillMakeList handle this NS_NAMED_LITERAL_STRING(listType, "dl"); return WillMakeList(aSelection, &listType, aEntireList, nullptr, aCancel, aHandled, aItemType); } nsresult HTMLEditRules::WillMakeBasicBlock(Selection& aSelection, const nsAString& aBlockType, bool* aCancel, bool* aHandled) { MOZ_ASSERT(aCancel && aHandled); NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); OwningNonNull<nsIAtom> blockType = NS_Atomize(aBlockType); WillInsert(aSelection, aCancel); // We want to ignore result of WillInsert() *aCancel = false; *aHandled = false; nsresult rv = NormalizeSelection(&aSelection); NS_ENSURE_SUCCESS(rv, rv); AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor); AutoTransactionsConserveSelection dontSpazMySelection(htmlEditor); *aHandled = true; // Contruct a list of nodes to act on. nsTArray<OwningNonNull<nsINode>> arrayOfNodes; rv = GetNodesFromSelection(aSelection, EditAction::makeBasicBlock, arrayOfNodes); NS_ENSURE_SUCCESS(rv, rv); // Remove all non-editable nodes. Leave them be. for (int32_t i = arrayOfNodes.Length() - 1; i >= 0; i--) { if (!htmlEditor->IsEditable(arrayOfNodes[i])) { arrayOfNodes.RemoveElementAt(i); } } // If nothing visible in list, make an empty block if (ListIsEmptyLine(arrayOfNodes)) { // Get selection location NS_ENSURE_STATE(aSelection.GetRangeAt(0) && aSelection.GetRangeAt(0)->GetStartParent()); OwningNonNull<nsINode> parent = *aSelection.GetRangeAt(0)->GetStartParent(); int32_t offset = aSelection.GetRangeAt(0)->StartOffset(); if (blockType == nsGkAtoms::normal || blockType == nsGkAtoms::_empty) { // We are removing blocks (going to "body text") NS_ENSURE_TRUE(htmlEditor->GetBlock(parent), NS_ERROR_NULL_POINTER); OwningNonNull<Element> curBlock = *htmlEditor->GetBlock(parent); if (HTMLEditUtils::IsFormatNode(curBlock)) { // If the first editable node after selection is a br, consume it. // Otherwise it gets pushed into a following block after the split, // which is visually bad. nsCOMPtr<nsIContent> brNode = htmlEditor->GetNextHTMLNode(parent, offset); if (brNode && brNode->IsHTMLElement(nsGkAtoms::br)) { rv = htmlEditor->DeleteNode(brNode); NS_ENSURE_SUCCESS(rv, rv); } // Do the splits! offset = htmlEditor->SplitNodeDeep(curBlock, *parent->AsContent(), offset, HTMLEditor::EmptyContainers::no); NS_ENSURE_STATE(offset != -1); // Put a br at the split point brNode = htmlEditor->CreateBR(curBlock->GetParentNode(), offset); NS_ENSURE_STATE(brNode); // Put selection at the split point *aHandled = true; rv = aSelection.Collapse(curBlock->GetParentNode(), offset); // Don't restore the selection selectionRestorer.Abort(); NS_ENSURE_SUCCESS(rv, rv); } // Else nothing to do! } else { // We are making a block. Consume a br, if needed. nsCOMPtr<nsIContent> brNode = htmlEditor->GetNextHTMLNode(parent, offset, true); if (brNode && brNode->IsHTMLElement(nsGkAtoms::br)) { rv = htmlEditor->DeleteNode(brNode); NS_ENSURE_SUCCESS(rv, rv); // We don't need to act on this node any more arrayOfNodes.RemoveElement(brNode); } // Make sure we can put a block here rv = SplitAsNeeded(blockType, parent, offset); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr<Element> block = htmlEditor->CreateNode(blockType, parent, offset); NS_ENSURE_STATE(block); // Remember our new block for postprocessing mNewBlock = block; // Delete anything that was in the list of nodes while (!arrayOfNodes.IsEmpty()) { OwningNonNull<nsINode> curNode = arrayOfNodes[0]; rv = htmlEditor->DeleteNode(curNode); NS_ENSURE_SUCCESS(rv, rv); arrayOfNodes.RemoveElementAt(0); } // Put selection in new block *aHandled = true; rv = aSelection.Collapse(block, 0); // Don't restore the selection selectionRestorer.Abort(); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } // Okay, now go through all the nodes and make the right kind of blocks, or // whatever is approriate. Woohoo! Note: blockquote is handled a little // differently. if (blockType == nsGkAtoms::blockquote) { rv = MakeBlockquote(arrayOfNodes); NS_ENSURE_SUCCESS(rv, rv); } else if (blockType == nsGkAtoms::normal || blockType == nsGkAtoms::_empty) { rv = RemoveBlockStyle(arrayOfNodes); NS_ENSURE_SUCCESS(rv, rv); } else { rv = ApplyBlockStyle(arrayOfNodes, blockType); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } nsresult HTMLEditRules::DidMakeBasicBlock(Selection* aSelection, RulesInfo* aInfo, nsresult aResult) { NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER); // check for empty block. if so, put a moz br in it. if (!aSelection->Collapsed()) { return NS_OK; } NS_ENSURE_STATE(aSelection->GetRangeAt(0) && aSelection->GetRangeAt(0)->GetStartParent()); nsresult rv = InsertMozBRIfNeeded(*aSelection->GetRangeAt(0)->GetStartParent()); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult HTMLEditRules::WillIndent(Selection* aSelection, bool* aCancel, bool* aHandled) { NS_ENSURE_STATE(mHTMLEditor); if (mHTMLEditor->IsCSSEnabled()) { nsresult rv = WillCSSIndent(aSelection, aCancel, aHandled); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { nsresult rv = WillHTMLIndent(aSelection, aCancel, aHandled); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } return NS_OK; } nsresult HTMLEditRules::WillCSSIndent(Selection* aSelection, bool* aCancel, bool* aHandled) { if (!aSelection || !aCancel || !aHandled) { return NS_ERROR_NULL_POINTER; } WillInsert(*aSelection, aCancel); // initialize out param // we want to ignore result of WillInsert() *aCancel = false; *aHandled = true; nsresult rv = NormalizeSelection(aSelection); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor); nsTArray<OwningNonNull<nsRange>> arrayOfRanges; nsTArray<OwningNonNull<nsINode>> arrayOfNodes; // short circuit: detect case of collapsed selection inside an <li>. // just sublist that <li>. This prevents bug 97797. nsCOMPtr<Element> liNode; if (aSelection->Collapsed()) { nsCOMPtr<nsINode> node; int32_t offset; NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetStartNodeAndOffset(aSelection, getter_AddRefs(node), &offset); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr<Element> block = mHTMLEditor->GetBlock(*node); if (block && HTMLEditUtils::IsListItem(block)) { liNode = block; } } if (liNode) { arrayOfNodes.AppendElement(*liNode); } else { // convert the selection ranges into "promoted" selection ranges: // this basically just expands the range to include the immediate // block parent, and then further expands to include any ancestors // whose children are all in the range rv = GetNodesFromSelection(*aSelection, EditAction::indent, arrayOfNodes); NS_ENSURE_SUCCESS(rv, rv); } // if nothing visible in list, make an empty block if (ListIsEmptyLine(arrayOfNodes)) { // get selection location NS_ENSURE_STATE(aSelection->RangeCount()); nsCOMPtr<nsINode> parent = aSelection->GetRangeAt(0)->GetStartParent(); int32_t offset = aSelection->GetRangeAt(0)->StartOffset(); NS_ENSURE_STATE(parent); // make sure we can put a block here rv = SplitAsNeeded(*nsGkAtoms::div, parent, offset); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<Element> theBlock = mHTMLEditor->CreateNode(nsGkAtoms::div, parent, offset); NS_ENSURE_STATE(theBlock); // remember our new block for postprocessing mNewBlock = theBlock; ChangeIndentation(*theBlock, Change::plus); // delete anything that was in the list of nodes while (!arrayOfNodes.IsEmpty()) { OwningNonNull<nsINode> curNode = arrayOfNodes[0]; NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteNode(curNode); NS_ENSURE_SUCCESS(rv, rv); arrayOfNodes.RemoveElementAt(0); } // put selection in new block *aHandled = true; rv = aSelection->Collapse(theBlock, 0); // Don't restore the selection selectionRestorer.Abort(); return rv; } // Ok, now go through all the nodes and put them in a blockquote, // or whatever is appropriate. Wohoo! nsCOMPtr<nsINode> curParent; nsCOMPtr<Element> curList, curQuote; nsCOMPtr<nsIContent> sibling; int32_t listCount = arrayOfNodes.Length(); for (int32_t i = 0; i < listCount; i++) { // here's where we actually figure out what to do NS_ENSURE_STATE(arrayOfNodes[i]->IsContent()); nsCOMPtr<nsIContent> curNode = arrayOfNodes[i]->AsContent(); // Ignore all non-editable nodes. Leave them be. NS_ENSURE_STATE(mHTMLEditor); if (!mHTMLEditor->IsEditable(curNode)) { continue; } curParent = curNode->GetParentNode(); int32_t offset = curParent ? curParent->IndexOf(curNode) : -1; // some logic for putting list items into nested lists... if (HTMLEditUtils::IsList(curParent)) { sibling = nullptr; // Check for whether we should join a list that follows curNode. // We do this if the next element is a list, and the list is of the // same type (li/ol) as curNode was a part it. NS_ENSURE_STATE(mHTMLEditor); sibling = mHTMLEditor->GetNextHTMLSibling(curNode); if (sibling && HTMLEditUtils::IsList(sibling) && curParent->NodeInfo()->NameAtom() == sibling->NodeInfo()->NameAtom() && curParent->NodeInfo()->NamespaceID() == sibling->NodeInfo()->NamespaceID()) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->MoveNode(curNode, sibling, 0); NS_ENSURE_SUCCESS(rv, rv); continue; } // Check for whether we should join a list that preceeds curNode. // We do this if the previous element is a list, and the list is of // the same type (li/ol) as curNode was a part of. NS_ENSURE_STATE(mHTMLEditor); sibling = mHTMLEditor->GetPriorHTMLSibling(curNode); if (sibling && HTMLEditUtils::IsList(sibling) && curParent->NodeInfo()->NameAtom() == sibling->NodeInfo()->NameAtom() && curParent->NodeInfo()->NamespaceID() == sibling->NodeInfo()->NamespaceID()) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->MoveNode(curNode, sibling, -1); NS_ENSURE_SUCCESS(rv, rv); continue; } sibling = nullptr; // check to see if curList is still appropriate. Which it is if // curNode is still right after it in the same list. if (curList) { NS_ENSURE_STATE(mHTMLEditor); sibling = mHTMLEditor->GetPriorHTMLSibling(curNode); } if (!curList || (sibling && sibling != curList)) { // create a new nested list of correct type rv = SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, offset); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); curList = mHTMLEditor->CreateNode(curParent->NodeInfo()->NameAtom(), curParent, offset); NS_ENSURE_STATE(curList); // curList is now the correct thing to put curNode in // remember our new block for postprocessing mNewBlock = curList; } // tuck the node into the end of the active list uint32_t listLen = curList->Length(); NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->MoveNode(curNode, curList, listLen); NS_ENSURE_SUCCESS(rv, rv); } // Not a list item. else { if (curNode && IsBlockNode(*curNode)) { ChangeIndentation(*curNode->AsElement(), Change::plus); curQuote = nullptr; } else { if (!curQuote) { // First, check that our element can contain a div. if (!mTextEditor->CanContainTag(*curParent, *nsGkAtoms::div)) { return NS_OK; // cancelled } rv = SplitAsNeeded(*nsGkAtoms::div, curParent, offset); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); curQuote = mHTMLEditor->CreateNode(nsGkAtoms::div, curParent, offset); NS_ENSURE_STATE(curQuote); ChangeIndentation(*curQuote, Change::plus); // remember our new block for postprocessing mNewBlock = curQuote; // curQuote is now the correct thing to put curNode in } // tuck the node into the end of the active blockquote uint32_t quoteLen = curQuote->Length(); NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->MoveNode(curNode, curQuote, quoteLen); NS_ENSURE_SUCCESS(rv, rv); } } } return NS_OK; } nsresult HTMLEditRules::WillHTMLIndent(Selection* aSelection, bool* aCancel, bool* aHandled) { if (!aSelection || !aCancel || !aHandled) { return NS_ERROR_NULL_POINTER; } WillInsert(*aSelection, aCancel); // initialize out param // we want to ignore result of WillInsert() *aCancel = false; *aHandled = true; nsresult rv = NormalizeSelection(aSelection); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor); // convert the selection ranges into "promoted" selection ranges: // this basically just expands the range to include the immediate // block parent, and then further expands to include any ancestors // whose children are all in the range nsTArray<RefPtr<nsRange>> arrayOfRanges; GetPromotedRanges(*aSelection, arrayOfRanges, EditAction::indent); // use these ranges to contruct a list of nodes to act on. nsTArray<OwningNonNull<nsINode>> arrayOfNodes; rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes, EditAction::indent); NS_ENSURE_SUCCESS(rv, rv); // if nothing visible in list, make an empty block if (ListIsEmptyLine(arrayOfNodes)) { // get selection location NS_ENSURE_STATE(aSelection->RangeCount()); nsCOMPtr<nsINode> parent = aSelection->GetRangeAt(0)->GetStartParent(); int32_t offset = aSelection->GetRangeAt(0)->StartOffset(); NS_ENSURE_STATE(parent); // make sure we can put a block here rv = SplitAsNeeded(*nsGkAtoms::blockquote, parent, offset); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<Element> theBlock = mHTMLEditor->CreateNode(nsGkAtoms::blockquote, parent, offset); NS_ENSURE_STATE(theBlock); // remember our new block for postprocessing mNewBlock = theBlock; // delete anything that was in the list of nodes while (!arrayOfNodes.IsEmpty()) { OwningNonNull<nsINode> curNode = arrayOfNodes[0]; NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteNode(curNode); NS_ENSURE_SUCCESS(rv, rv); arrayOfNodes.RemoveElementAt(0); } // put selection in new block *aHandled = true; rv = aSelection->Collapse(theBlock, 0); // Don't restore the selection selectionRestorer.Abort(); return rv; } // Ok, now go through all the nodes and put them in a blockquote, // or whatever is appropriate. Wohoo! nsCOMPtr<nsINode> curParent; nsCOMPtr<nsIContent> sibling; nsCOMPtr<Element> curList, curQuote, indentedLI; int32_t listCount = arrayOfNodes.Length(); for (int32_t i = 0; i < listCount; i++) { // here's where we actually figure out what to do NS_ENSURE_STATE(arrayOfNodes[i]->IsContent()); nsCOMPtr<nsIContent> curNode = arrayOfNodes[i]->AsContent(); // Ignore all non-editable nodes. Leave them be. NS_ENSURE_STATE(mHTMLEditor); if (!mHTMLEditor->IsEditable(curNode)) { continue; } curParent = curNode->GetParentNode(); int32_t offset = curParent ? curParent->IndexOf(curNode) : -1; // some logic for putting list items into nested lists... if (HTMLEditUtils::IsList(curParent)) { sibling = nullptr; // Check for whether we should join a list that follows curNode. // We do this if the next element is a list, and the list is of the // same type (li/ol) as curNode was a part it. NS_ENSURE_STATE(mHTMLEditor); sibling = mHTMLEditor->GetNextHTMLSibling(curNode); if (sibling && HTMLEditUtils::IsList(sibling) && curParent->NodeInfo()->NameAtom() == sibling->NodeInfo()->NameAtom() && curParent->NodeInfo()->NamespaceID() == sibling->NodeInfo()->NamespaceID()) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->MoveNode(curNode, sibling, 0); NS_ENSURE_SUCCESS(rv, rv); continue; } // Check for whether we should join a list that preceeds curNode. // We do this if the previous element is a list, and the list is of // the same type (li/ol) as curNode was a part of. NS_ENSURE_STATE(mHTMLEditor); sibling = mHTMLEditor->GetPriorHTMLSibling(curNode); if (sibling && HTMLEditUtils::IsList(sibling) && curParent->NodeInfo()->NameAtom() == sibling->NodeInfo()->NameAtom() && curParent->NodeInfo()->NamespaceID() == sibling->NodeInfo()->NamespaceID()) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->MoveNode(curNode, sibling, -1); NS_ENSURE_SUCCESS(rv, rv); continue; } sibling = nullptr; // check to see if curList is still appropriate. Which it is if // curNode is still right after it in the same list. if (curList) { NS_ENSURE_STATE(mHTMLEditor); sibling = mHTMLEditor->GetPriorHTMLSibling(curNode); } if (!curList || (sibling && sibling != curList)) { // create a new nested list of correct type rv = SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, offset); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); curList = mHTMLEditor->CreateNode(curParent->NodeInfo()->NameAtom(), curParent, offset); NS_ENSURE_STATE(curList); // curList is now the correct thing to put curNode in // remember our new block for postprocessing mNewBlock = curList; } // tuck the node into the end of the active list NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->MoveNode(curNode, curList, -1); NS_ENSURE_SUCCESS(rv, rv); // forget curQuote, if any curQuote = nullptr; } // Not a list item, use blockquote? else { // if we are inside a list item, we don't want to blockquote, we want // to sublist the list item. We may have several nodes listed in the // array of nodes to act on, that are in the same list item. Since // we only want to indent that li once, we must keep track of the most // recent indented list item, and not indent it if we find another node // to act on that is still inside the same li. nsCOMPtr<Element> listItem = IsInListItem(curNode); if (listItem) { if (indentedLI == listItem) { // already indented this list item continue; } curParent = listItem->GetParentNode(); offset = curParent ? curParent->IndexOf(listItem) : -1; // check to see if curList is still appropriate. Which it is if // curNode is still right after it in the same list. if (curList) { sibling = nullptr; NS_ENSURE_STATE(mHTMLEditor); sibling = mHTMLEditor->GetPriorHTMLSibling(curNode); } if (!curList || (sibling && sibling != curList)) { // create a new nested list of correct type rv = SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, offset); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); curList = mHTMLEditor->CreateNode(curParent->NodeInfo()->NameAtom(), curParent, offset); NS_ENSURE_STATE(curList); } NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->MoveNode(listItem, curList, -1); NS_ENSURE_SUCCESS(rv, rv); // remember we indented this li indentedLI = listItem; } else { // need to make a blockquote to put things in if we haven't already, // or if this node doesn't go in blockquote we used earlier. // One reason it might not go in prio blockquote is if we are now // in a different table cell. if (curQuote && InDifferentTableElements(curQuote, curNode)) { curQuote = nullptr; } if (!curQuote) { // First, check that our element can contain a blockquote. if (!mTextEditor->CanContainTag(*curParent, *nsGkAtoms::blockquote)) { return NS_OK; // cancelled } rv = SplitAsNeeded(*nsGkAtoms::blockquote, curParent, offset); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); curQuote = mHTMLEditor->CreateNode(nsGkAtoms::blockquote, curParent, offset); NS_ENSURE_STATE(curQuote); // remember our new block for postprocessing mNewBlock = curQuote; // curQuote is now the correct thing to put curNode in } // tuck the node into the end of the active blockquote NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->MoveNode(curNode, curQuote, -1); NS_ENSURE_SUCCESS(rv, rv); // forget curList, if any curList = nullptr; } } } return NS_OK; } nsresult HTMLEditRules::WillOutdent(Selection& aSelection, bool* aCancel, bool* aHandled) { MOZ_ASSERT(aCancel && aHandled); *aCancel = false; *aHandled = true; nsCOMPtr<nsIContent> rememberedLeftBQ, rememberedRightBQ; NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); bool useCSS = htmlEditor->IsCSSEnabled(); nsresult rv = NormalizeSelection(&aSelection); NS_ENSURE_SUCCESS(rv, rv); // Some scoping for selection resetting - we may need to tweak it { AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor); // Convert the selection ranges into "promoted" selection ranges: this // basically just expands the range to include the immediate block parent, // and then further expands to include any ancestors whose children are all // in the range nsTArray<OwningNonNull<nsINode>> arrayOfNodes; rv = GetNodesFromSelection(aSelection, EditAction::outdent, arrayOfNodes); NS_ENSURE_SUCCESS(rv, rv); // Okay, now go through all the nodes and remove a level of blockquoting, // or whatever is appropriate. Wohoo! nsCOMPtr<Element> curBlockQuote; nsCOMPtr<nsIContent> firstBQChild, lastBQChild; bool curBlockQuoteIsIndentedWithCSS = false; for (uint32_t i = 0; i < arrayOfNodes.Length(); i++) { if (!arrayOfNodes[i]->IsContent()) { continue; } OwningNonNull<nsIContent> curNode = *arrayOfNodes[i]->AsContent(); // Here's where we actually figure out what to do nsCOMPtr<nsINode> curParent = curNode->GetParentNode(); int32_t offset = curParent ? curParent->IndexOf(curNode) : -1; // Is it a blockquote? if (curNode->IsHTMLElement(nsGkAtoms::blockquote)) { // If it is a blockquote, remove it. So we need to finish up dealng // with any curBlockQuote first. if (curBlockQuote) { rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild, curBlockQuoteIsIndentedWithCSS, getter_AddRefs(rememberedLeftBQ), getter_AddRefs(rememberedRightBQ)); NS_ENSURE_SUCCESS(rv, rv); curBlockQuote = nullptr; firstBQChild = nullptr; lastBQChild = nullptr; curBlockQuoteIsIndentedWithCSS = false; } rv = htmlEditor->RemoveBlockContainer(curNode); NS_ENSURE_SUCCESS(rv, rv); continue; } // Is it a block with a 'margin' property? if (useCSS && IsBlockNode(curNode)) { nsIAtom& marginProperty = MarginPropertyAtomForIndent(*htmlEditor->mCSSEditUtils, curNode); nsAutoString value; htmlEditor->mCSSEditUtils->GetSpecifiedProperty(curNode, marginProperty, value); float f; nsCOMPtr<nsIAtom> unit; NS_ENSURE_STATE(htmlEditor); htmlEditor->mCSSEditUtils->ParseLength(value, &f, getter_AddRefs(unit)); if (f > 0) { ChangeIndentation(*curNode->AsElement(), Change::minus); continue; } } // Is it a list item? if (HTMLEditUtils::IsListItem(curNode)) { // If it is a list item, that means we are not outdenting whole list. // So we need to finish up dealing with any curBlockQuote, and then pop // this list item. if (curBlockQuote) { rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild, curBlockQuoteIsIndentedWithCSS, getter_AddRefs(rememberedLeftBQ), getter_AddRefs(rememberedRightBQ)); NS_ENSURE_SUCCESS(rv, rv); curBlockQuote = nullptr; firstBQChild = nullptr; lastBQChild = nullptr; curBlockQuoteIsIndentedWithCSS = false; } bool unused; rv = PopListItem(GetAsDOMNode(curNode), &unused); NS_ENSURE_SUCCESS(rv, rv); continue; } // Do we have a blockquote that we are already committed to removing? if (curBlockQuote) { // If so, is this node a descendant? if (EditorUtils::IsDescendantOf(curNode, curBlockQuote)) { lastBQChild = curNode; // Then we don't need to do anything different for this node continue; } // Otherwise, we have progressed beyond end of curBlockQuote, so // let's handle it now. We need to remove the portion of // curBlockQuote that contains [firstBQChild - lastBQChild]. rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild, curBlockQuoteIsIndentedWithCSS, getter_AddRefs(rememberedLeftBQ), getter_AddRefs(rememberedRightBQ)); NS_ENSURE_SUCCESS(rv, rv); curBlockQuote = nullptr; firstBQChild = nullptr; lastBQChild = nullptr; curBlockQuoteIsIndentedWithCSS = false; // Fall out and handle curNode } // Are we inside a blockquote? OwningNonNull<nsINode> n = curNode; curBlockQuoteIsIndentedWithCSS = false; // Keep looking up the hierarchy as long as we don't hit the body or the // active editing host or a table element (other than an entire table) while (!n->IsHTMLElement(nsGkAtoms::body) && htmlEditor->IsDescendantOfEditorRoot(n) && (n->IsHTMLElement(nsGkAtoms::table) || !HTMLEditUtils::IsTableElement(n))) { if (!n->GetParentNode()) { break; } n = *n->GetParentNode(); if (n->IsHTMLElement(nsGkAtoms::blockquote)) { // If so, remember it and the first node we are taking out of it. curBlockQuote = n->AsElement(); firstBQChild = curNode; lastBQChild = curNode; break; } else if (useCSS) { nsIAtom& marginProperty = MarginPropertyAtomForIndent(*htmlEditor->mCSSEditUtils, curNode); nsAutoString value; htmlEditor->mCSSEditUtils->GetSpecifiedProperty(*n, marginProperty, value); float f; nsCOMPtr<nsIAtom> unit; htmlEditor->mCSSEditUtils->ParseLength(value, &f, getter_AddRefs(unit)); if (f > 0 && !(HTMLEditUtils::IsList(curParent) && HTMLEditUtils::IsList(curNode))) { curBlockQuote = n->AsElement(); firstBQChild = curNode; lastBQChild = curNode; curBlockQuoteIsIndentedWithCSS = true; break; } } } if (!curBlockQuote) { // Couldn't find enclosing blockquote. Handle list cases. if (HTMLEditUtils::IsList(curParent)) { // Move node out of list if (HTMLEditUtils::IsList(curNode)) { // Just unwrap this sublist rv = htmlEditor->RemoveBlockContainer(curNode); NS_ENSURE_SUCCESS(rv, rv); } // handled list item case above } else if (HTMLEditUtils::IsList(curNode)) { // node is a list, but parent is non-list: move list items out nsCOMPtr<nsIContent> child = curNode->GetLastChild(); while (child) { if (HTMLEditUtils::IsListItem(child)) { bool unused; rv = PopListItem(GetAsDOMNode(child), &unused); NS_ENSURE_SUCCESS(rv, rv); } else if (HTMLEditUtils::IsList(child)) { // We have an embedded list, so move it out from under the parent // list. Be sure to put it after the parent list because this // loop iterates backwards through the parent's list of children. rv = htmlEditor->MoveNode(child, curParent, offset + 1); NS_ENSURE_SUCCESS(rv, rv); } else { // Delete any non-list items for now rv = htmlEditor->DeleteNode(child); NS_ENSURE_SUCCESS(rv, rv); } child = curNode->GetLastChild(); } // Delete the now-empty list rv = htmlEditor->RemoveBlockContainer(curNode); NS_ENSURE_SUCCESS(rv, rv); } else if (useCSS) { nsCOMPtr<Element> element; if (curNode->GetAsText()) { // We want to outdent the parent of text nodes element = curNode->GetParentElement(); } else if (curNode->IsElement()) { element = curNode->AsElement(); } if (element) { ChangeIndentation(*element, Change::minus); } } } } if (curBlockQuote) { // We have a blockquote we haven't finished handling rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild, curBlockQuoteIsIndentedWithCSS, getter_AddRefs(rememberedLeftBQ), getter_AddRefs(rememberedRightBQ)); NS_ENSURE_SUCCESS(rv, rv); } } // Make sure selection didn't stick to last piece of content in old bq (only // a problem for collapsed selections) if (rememberedLeftBQ || rememberedRightBQ) { if (aSelection.Collapsed()) { // Push selection past end of rememberedLeftBQ NS_ENSURE_TRUE(aSelection.GetRangeAt(0), NS_OK); nsCOMPtr<nsINode> startNode = aSelection.GetRangeAt(0)->GetStartParent(); int32_t startOffset = aSelection.GetRangeAt(0)->StartOffset(); if (rememberedLeftBQ && (startNode == rememberedLeftBQ || EditorUtils::IsDescendantOf(startNode, rememberedLeftBQ))) { // Selection is inside rememberedLeftBQ - push it past it. startNode = rememberedLeftBQ->GetParentNode(); startOffset = startNode ? 1 + startNode->IndexOf(rememberedLeftBQ) : 0; aSelection.Collapse(startNode, startOffset); } // And pull selection before beginning of rememberedRightBQ startNode = aSelection.GetRangeAt(0)->GetStartParent(); startOffset = aSelection.GetRangeAt(0)->StartOffset(); if (rememberedRightBQ && (startNode == rememberedRightBQ || EditorUtils::IsDescendantOf(startNode, rememberedRightBQ))) { // Selection is inside rememberedRightBQ - push it before it. startNode = rememberedRightBQ->GetParentNode(); startOffset = startNode ? startNode->IndexOf(rememberedRightBQ) : -1; aSelection.Collapse(startNode, startOffset); } } return NS_OK; } return NS_OK; } /** * RemovePartOfBlock() splits aBlock and move aStartChild to aEndChild out of * aBlock. */ nsresult HTMLEditRules::RemovePartOfBlock(Element& aBlock, nsIContent& aStartChild, nsIContent& aEndChild) { SplitBlock(aBlock, aStartChild, aEndChild); // Get rid of part of blockquote we are outdenting NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->RemoveBlockContainer(aBlock); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } void HTMLEditRules::SplitBlock(Element& aBlock, nsIContent& aStartChild, nsIContent& aEndChild, nsIContent** aOutLeftNode, nsIContent** aOutRightNode, nsIContent** aOutMiddleNode) { // aStartChild and aEndChild must be exclusive descendants of aBlock MOZ_ASSERT(EditorUtils::IsDescendantOf(&aStartChild, &aBlock) && EditorUtils::IsDescendantOf(&aEndChild, &aBlock)); NS_ENSURE_TRUE_VOID(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); // Get split point location OwningNonNull<nsIContent> startParent = *aStartChild.GetParent(); int32_t startOffset = startParent->IndexOf(&aStartChild); // Do the splits! nsCOMPtr<nsIContent> newMiddleNode1; htmlEditor->SplitNodeDeep(aBlock, startParent, startOffset, HTMLEditor::EmptyContainers::no, aOutLeftNode, getter_AddRefs(newMiddleNode1)); // Get split point location OwningNonNull<nsIContent> endParent = *aEndChild.GetParent(); // +1 because we want to be after the child int32_t endOffset = 1 + endParent->IndexOf(&aEndChild); // Do the splits! nsCOMPtr<nsIContent> newMiddleNode2; htmlEditor->SplitNodeDeep(aBlock, endParent, endOffset, HTMLEditor::EmptyContainers::no, getter_AddRefs(newMiddleNode2), aOutRightNode); if (aOutMiddleNode) { if (newMiddleNode2) { newMiddleNode2.forget(aOutMiddleNode); } else { newMiddleNode1.forget(aOutMiddleNode); } } } nsresult HTMLEditRules::OutdentPartOfBlock(Element& aBlock, nsIContent& aStartChild, nsIContent& aEndChild, bool aIsBlockIndentedWithCSS, nsIContent** aOutLeftNode, nsIContent** aOutRightNode) { MOZ_ASSERT(aOutLeftNode && aOutRightNode); nsCOMPtr<nsIContent> middleNode; SplitBlock(aBlock, aStartChild, aEndChild, aOutLeftNode, aOutRightNode, getter_AddRefs(middleNode)); NS_ENSURE_STATE(middleNode); if (!aIsBlockIndentedWithCSS) { NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->RemoveBlockContainer(*middleNode); NS_ENSURE_SUCCESS(rv, rv); } else if (middleNode->IsElement()) { // We do nothing if middleNode isn't an element nsresult rv = ChangeIndentation(*middleNode->AsElement(), Change::minus); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } /** * ConvertListType() converts list type and list item type. */ nsresult HTMLEditRules::ConvertListType(Element* aList, Element** aOutList, nsIAtom* aListType, nsIAtom* aItemType) { MOZ_ASSERT(aList); MOZ_ASSERT(aOutList); MOZ_ASSERT(aListType); MOZ_ASSERT(aItemType); nsCOMPtr<nsINode> child = aList->GetFirstChild(); while (child) { if (child->IsElement()) { dom::Element* element = child->AsElement(); if (HTMLEditUtils::IsListItem(element) && !element->IsHTMLElement(aItemType)) { child = mHTMLEditor->ReplaceContainer(element, aItemType); NS_ENSURE_STATE(child); } else if (HTMLEditUtils::IsList(element) && !element->IsHTMLElement(aListType)) { nsCOMPtr<dom::Element> temp; nsresult rv = ConvertListType(child->AsElement(), getter_AddRefs(temp), aListType, aItemType); NS_ENSURE_SUCCESS(rv, rv); child = temp.forget(); } } child = child->GetNextSibling(); } if (aList->IsHTMLElement(aListType)) { nsCOMPtr<dom::Element> list = aList->AsElement(); list.forget(aOutList); return NS_OK; } *aOutList = mHTMLEditor->ReplaceContainer(aList, aListType).take(); NS_ENSURE_STATE(aOutList); return NS_OK; } /** * CreateStyleForInsertText() takes care of clearing and setting appropriate * style nodes for text insertion. */ nsresult HTMLEditRules::CreateStyleForInsertText(Selection& aSelection, nsIDocument& aDoc) { MOZ_ASSERT(mHTMLEditor->mTypeInState); bool weDidSomething = false; NS_ENSURE_STATE(aSelection.GetRangeAt(0)); nsCOMPtr<nsINode> node = aSelection.GetRangeAt(0)->GetStartParent(); int32_t offset = aSelection.GetRangeAt(0)->StartOffset(); // next examine our present style and make sure default styles are either // present or explicitly overridden. If neither, add the default style to // the TypeInState int32_t length = mHTMLEditor->mDefaultStyles.Length(); for (int32_t j = 0; j < length; j++) { PropItem* propItem = mHTMLEditor->mDefaultStyles[j]; MOZ_ASSERT(propItem); bool bFirst, bAny, bAll; // GetInlineProperty also examine TypeInState. The only gotcha here is // that a cleared property looks like an unset property. For now I'm // assuming that's not a problem: that default styles will always be // multivalue styles (like font face or size) where clearing the style // means we want to go back to the default. If we ever wanted a "toggle" // style like bold for a default, though, I'll have to add code to detect // the difference between unset and explicitly cleared, else user would // never be able to unbold, for instance. nsAutoString curValue; NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetInlinePropertyBase(*propItem->tag, &propItem->attr, nullptr, &bFirst, &bAny, &bAll, &curValue, false); NS_ENSURE_SUCCESS(rv, rv); if (!bAny) { // no style set for this prop/attr mHTMLEditor->mTypeInState->SetProp(propItem->tag, propItem->attr, propItem->value); } } nsCOMPtr<Element> rootElement = aDoc.GetRootElement(); NS_ENSURE_STATE(rootElement); // process clearing any styles first nsAutoPtr<PropItem> item(mHTMLEditor->mTypeInState->TakeClearProperty()); while (item && node != rootElement) { NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->ClearStyle(address_of(node), &offset, item->tag, &item->attr); NS_ENSURE_SUCCESS(rv, rv); item = mHTMLEditor->mTypeInState->TakeClearProperty(); weDidSomething = true; } // then process setting any styles int32_t relFontSize = mHTMLEditor->mTypeInState->TakeRelativeFontSize(); item = mHTMLEditor->mTypeInState->TakeSetProperty(); if (item || relFontSize) { // we have at least one style to add; make a new text node to insert style // nodes above. if (RefPtr<Text> text = node->GetAsText()) { // if we are in a text node, split it NS_ENSURE_STATE(mHTMLEditor); offset = mHTMLEditor->SplitNodeDeep(*text, *text, offset); NS_ENSURE_STATE(offset != -1); node = node->GetParentNode(); } if (!mHTMLEditor->IsContainer(node)) { return NS_OK; } OwningNonNull<Text> newNode = aDoc.CreateTextNode(EmptyString()); NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->InsertNode(*newNode, *node, offset); NS_ENSURE_SUCCESS(rv, rv); node = newNode; offset = 0; weDidSomething = true; if (relFontSize) { // dir indicated bigger versus smaller. 1 = bigger, -1 = smaller HTMLEditor::FontSize dir = relFontSize > 0 ? HTMLEditor::FontSize::incr : HTMLEditor::FontSize::decr; for (int32_t j = 0; j < DeprecatedAbs(relFontSize); j++) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->RelativeFontChangeOnTextNode(dir, newNode, 0, -1); NS_ENSURE_SUCCESS(rv, rv); } } while (item) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->SetInlinePropertyOnNode(*node->AsContent(), *item->tag, &item->attr, item->value); NS_ENSURE_SUCCESS(rv, rv); item = mHTMLEditor->mTypeInState->TakeSetProperty(); } } if (weDidSomething) { return aSelection.Collapse(node, offset); } return NS_OK; } /** * Figure out if aNode is (or is inside) an empty block. A block can have * children and still be considered empty, if the children are empty or * non-editable. */ nsresult HTMLEditRules::IsEmptyBlock(Element& aNode, bool* aOutIsEmptyBlock, MozBRCounts aMozBRCounts) { MOZ_ASSERT(aOutIsEmptyBlock); *aOutIsEmptyBlock = true; NS_ENSURE_TRUE(IsBlockNode(aNode), NS_ERROR_NULL_POINTER); return mHTMLEditor->IsEmptyNode(aNode.AsDOMNode(), aOutIsEmptyBlock, aMozBRCounts == MozBRCounts::yes ? false : true); } nsresult HTMLEditRules::WillAlign(Selection& aSelection, const nsAString& aAlignType, bool* aCancel, bool* aHandled) { MOZ_ASSERT(aCancel && aHandled); NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); WillInsert(aSelection, aCancel); // Initialize out param. We want to ignore result of WillInsert(). *aCancel = false; *aHandled = false; nsresult rv = NormalizeSelection(&aSelection); NS_ENSURE_SUCCESS(rv, rv); AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor); // Convert the selection ranges into "promoted" selection ranges: This // basically just expands the range to include the immediate block parent, // and then further expands to include any ancestors whose children are all // in the range *aHandled = true; nsTArray<OwningNonNull<nsINode>> nodeArray; rv = GetNodesFromSelection(aSelection, EditAction::align, nodeArray); NS_ENSURE_SUCCESS(rv, rv); // If we don't have any nodes, or we have only a single br, then we are // creating an empty alignment div. We have to do some different things for // these. bool emptyDiv = nodeArray.IsEmpty(); if (nodeArray.Length() == 1) { OwningNonNull<nsINode> node = nodeArray[0]; if (HTMLEditUtils::SupportsAlignAttr(GetAsDOMNode(node))) { // The node is a table element, an hr, a paragraph, a div or a section // header; in HTML 4, it can directly carry the ALIGN attribute and we // don't need to make a div! If we are in CSS mode, all the work is done // in AlignBlock rv = AlignBlock(*node->AsElement(), aAlignType, ContentsOnly::yes); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } if (TextEditUtils::IsBreak(node)) { // The special case emptyDiv code (below) that consumes BRs can cause // tables to split if the start node of the selection is not in a table // cell or caption, for example parent is a <tr>. Avoid this unnecessary // splitting if possible by leaving emptyDiv FALSE so that we fall // through to the normal case alignment code. // // XXX: It seems a little error prone for the emptyDiv special case code // to assume that the start node of the selection is the parent of the // single node in the nodeArray, as the paragraph above points out. Do we // rely on the selection start node because of the fact that nodeArray // can be empty? We should probably revisit this issue. - kin NS_ENSURE_STATE(aSelection.GetRangeAt(0) && aSelection.GetRangeAt(0)->GetStartParent()); OwningNonNull<nsINode> parent = *aSelection.GetRangeAt(0)->GetStartParent(); emptyDiv = !HTMLEditUtils::IsTableElement(parent) || HTMLEditUtils::IsTableCellOrCaption(parent); } } if (emptyDiv) { nsCOMPtr<nsINode> parent = aSelection.GetRangeAt(0) ? aSelection.GetRangeAt(0)->GetStartParent() : nullptr; NS_ENSURE_STATE(parent); int32_t offset = aSelection.GetRangeAt(0)->StartOffset(); rv = SplitAsNeeded(*nsGkAtoms::div, parent, offset); NS_ENSURE_SUCCESS(rv, rv); // Consume a trailing br, if any. This is to keep an alignment from // creating extra lines, if possible. nsCOMPtr<nsIContent> brContent = htmlEditor->GetNextHTMLNode(parent, offset); if (brContent && TextEditUtils::IsBreak(brContent)) { // Making use of html structure... if next node after where we are // putting our div is not a block, then the br we found is in same block // we are, so it's safe to consume it. nsCOMPtr<nsIContent> sibling = htmlEditor->GetNextHTMLSibling(parent, offset); if (sibling && !IsBlockNode(*sibling)) { rv = htmlEditor->DeleteNode(brContent); NS_ENSURE_SUCCESS(rv, rv); } } nsCOMPtr<Element> div = htmlEditor->CreateNode(nsGkAtoms::div, parent, offset); NS_ENSURE_STATE(div); // Remember our new block for postprocessing mNewBlock = div; // Set up the alignment on the div, using HTML or CSS rv = AlignBlock(*div, aAlignType, ContentsOnly::yes); NS_ENSURE_SUCCESS(rv, rv); *aHandled = true; // Put in a moz-br so that it won't get deleted rv = CreateMozBR(div->AsDOMNode(), 0); NS_ENSURE_SUCCESS(rv, rv); rv = aSelection.Collapse(div, 0); // Don't restore the selection selectionRestorer.Abort(); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } // Next we detect all the transitions in the array, where a transition // means that adjacent nodes in the array don't have the same parent. nsTArray<bool> transitionList; MakeTransitionList(nodeArray, transitionList); // Okay, now go through all the nodes and give them an align attrib or put // them in a div, or whatever is appropriate. Woohoo! nsCOMPtr<Element> curDiv; bool useCSS = htmlEditor->IsCSSEnabled(); for (size_t i = 0; i < nodeArray.Length(); i++) { auto& curNode = nodeArray[i]; // Here's where we actually figure out what to do // Ignore all non-editable nodes. Leave them be. if (!htmlEditor->IsEditable(curNode)) { continue; } // The node is a table element, an hr, a paragraph, a div or a section // header; in HTML 4, it can directly carry the ALIGN attribute and we // don't need to nest it, just set the alignment. In CSS, assign the // corresponding CSS styles in AlignBlock if (HTMLEditUtils::SupportsAlignAttr(GetAsDOMNode(curNode))) { rv = AlignBlock(*curNode->AsElement(), aAlignType, ContentsOnly::no); NS_ENSURE_SUCCESS(rv, rv); // Clear out curDiv so that we don't put nodes after this one into it curDiv = nullptr; continue; } nsCOMPtr<nsINode> curParent = curNode->GetParentNode(); int32_t offset = curParent ? curParent->IndexOf(curNode) : -1; // Skip insignificant formatting text nodes to prevent unnecessary // structure splitting! bool isEmptyTextNode = false; if (curNode->GetAsText() && ((HTMLEditUtils::IsTableElement(curParent) && !HTMLEditUtils::IsTableCellOrCaption(*curParent)) || HTMLEditUtils::IsList(curParent) || (NS_SUCCEEDED(htmlEditor->IsEmptyNode(curNode, &isEmptyTextNode)) && isEmptyTextNode))) { continue; } // If it's a list item, or a list inside a list, forget any "current" div, // and instead put divs inside the appropriate block (td, li, etc.) if (HTMLEditUtils::IsListItem(curNode) || HTMLEditUtils::IsList(curNode)) { rv = RemoveAlignment(GetAsDOMNode(curNode), aAlignType, true); NS_ENSURE_SUCCESS(rv, rv); if (useCSS) { htmlEditor->mCSSEditUtils->SetCSSEquivalentToHTMLStyle( curNode->AsElement(), nullptr, &NS_LITERAL_STRING("align"), &aAlignType, false); curDiv = nullptr; continue; } if (HTMLEditUtils::IsList(curParent)) { // If we don't use CSS, add a contraint to list element: they have to // be inside another list, i.e., >= second level of nesting rv = AlignInnerBlocks(*curNode, &aAlignType); NS_ENSURE_SUCCESS(rv, rv); curDiv = nullptr; continue; } // Clear out curDiv so that we don't put nodes after this one into it } // Need to make a div to put things in if we haven't already, or if this // node doesn't go in div we used earlier. if (!curDiv || transitionList[i]) { // First, check that our element can contain a div. if (!mTextEditor->CanContainTag(*curParent, *nsGkAtoms::div)) { // Cancelled return NS_OK; } rv = SplitAsNeeded(*nsGkAtoms::div, curParent, offset); NS_ENSURE_SUCCESS(rv, rv); curDiv = htmlEditor->CreateNode(nsGkAtoms::div, curParent, offset); NS_ENSURE_STATE(curDiv); // Remember our new block for postprocessing mNewBlock = curDiv; // Set up the alignment on the div rv = AlignBlock(*curDiv, aAlignType, ContentsOnly::yes); } NS_ENSURE_STATE(curNode->IsContent()); // Tuck the node into the end of the active div rv = htmlEditor->MoveNode(curNode->AsContent(), curDiv, -1); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } /** * AlignInnerBlocks() aligns inside table cells or list items. */ nsresult HTMLEditRules::AlignInnerBlocks(nsINode& aNode, const nsAString* alignType) { NS_ENSURE_TRUE(alignType, NS_ERROR_NULL_POINTER); // Gather list of table cells or list items nsTArray<OwningNonNull<nsINode>> nodeArray; TableCellAndListItemFunctor functor; DOMIterator iter(aNode); iter.AppendList(functor, nodeArray); // Now that we have the list, align their contents as requested for (auto& node : nodeArray) { nsresult rv = AlignBlockContents(GetAsDOMNode(node), alignType); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } /** * AlignBlockContents() aligns contents of a block element. */ nsresult HTMLEditRules::AlignBlockContents(nsIDOMNode* aNode, const nsAString* alignType) { nsCOMPtr<nsINode> node = do_QueryInterface(aNode); NS_ENSURE_TRUE(node && alignType, NS_ERROR_NULL_POINTER); nsCOMPtr<nsIContent> firstChild, lastChild; nsCOMPtr<Element> divNode; bool useCSS = mHTMLEditor->IsCSSEnabled(); NS_ENSURE_STATE(mHTMLEditor); firstChild = mHTMLEditor->GetFirstEditableChild(*node); NS_ENSURE_STATE(mHTMLEditor); lastChild = mHTMLEditor->GetLastEditableChild(*node); NS_NAMED_LITERAL_STRING(attr, "align"); if (!firstChild) { // this cell has no content, nothing to align } else if (firstChild == lastChild && firstChild->IsHTMLElement(nsGkAtoms::div)) { // the cell already has a div containing all of its content: just // act on this div. nsCOMPtr<nsIDOMElement> divElem = do_QueryInterface(firstChild); if (useCSS) { NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->SetAttributeOrEquivalent(divElem, attr, *alignType, false); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->SetAttribute(divElem, attr, *alignType); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } else { // else we need to put in a div, set the alignment, and toss in all the children NS_ENSURE_STATE(mHTMLEditor); divNode = mHTMLEditor->CreateNode(nsGkAtoms::div, node, 0); NS_ENSURE_STATE(divNode); // set up the alignment on the div nsCOMPtr<nsIDOMElement> divElem = do_QueryInterface(divNode); if (useCSS) { NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->SetAttributeOrEquivalent(divElem, attr, *alignType, false); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->SetAttribute(divElem, attr, *alignType); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } // tuck the children into the end of the active div while (lastChild && (lastChild != divNode)) { NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->MoveNode(lastChild, divNode, 0); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); lastChild = mHTMLEditor->GetLastEditableChild(*node); } } return NS_OK; } /** * CheckForEmptyBlock() is called by WillDeleteSelection() to detect and handle * case of deleting from inside an empty block. */ nsresult HTMLEditRules::CheckForEmptyBlock(nsINode* aStartNode, Element* aBodyNode, Selection* aSelection, nsIEditor::EDirection aAction, bool* aHandled) { // If the editing host is an inline element, bail out early. if (aBodyNode && IsInlineNode(*aBodyNode)) { return NS_OK; } NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); // If we are inside an empty block, delete it. Note: do NOT delete table // elements this way. nsCOMPtr<Element> block = htmlEditor->GetBlock(*aStartNode); bool bIsEmptyNode; nsCOMPtr<Element> emptyBlock; if (block && block != aBodyNode) { // Efficiency hack, avoiding IsEmptyNode() call when in body nsresult rv = htmlEditor->IsEmptyNode(block, &bIsEmptyNode, true, false); NS_ENSURE_SUCCESS(rv, rv); while (block && bIsEmptyNode && !HTMLEditUtils::IsTableElement(block) && block != aBodyNode) { emptyBlock = block; block = htmlEditor->GetBlockNodeParent(emptyBlock); if (block) { rv = htmlEditor->IsEmptyNode(block, &bIsEmptyNode, true, false); NS_ENSURE_SUCCESS(rv, rv); } } } if (emptyBlock && emptyBlock->IsEditable()) { nsCOMPtr<nsINode> blockParent = emptyBlock->GetParentNode(); NS_ENSURE_TRUE(blockParent, NS_ERROR_FAILURE); int32_t offset = blockParent->IndexOf(emptyBlock); if (HTMLEditUtils::IsListItem(emptyBlock)) { // Are we the first list item in the list? bool bIsFirst; NS_ENSURE_STATE(htmlEditor); nsresult rv = htmlEditor->IsFirstEditableChild(GetAsDOMNode(emptyBlock), &bIsFirst); NS_ENSURE_SUCCESS(rv, rv); if (bIsFirst) { nsCOMPtr<nsINode> listParent = blockParent->GetParentNode(); NS_ENSURE_TRUE(listParent, NS_ERROR_FAILURE); int32_t listOffset = listParent->IndexOf(blockParent); // If we are a sublist, skip the br creation if (!HTMLEditUtils::IsList(listParent)) { // Create a br before list NS_ENSURE_STATE(htmlEditor); nsCOMPtr<Element> br = htmlEditor->CreateBR(listParent, listOffset); NS_ENSURE_STATE(br); // Adjust selection to be right before it rv = aSelection->Collapse(listParent, listOffset); NS_ENSURE_SUCCESS(rv, rv); } // Else just let selection percolate up. We'll adjust it in // AfterEdit() } } else { if (aAction == nsIEditor::eNext || aAction == nsIEditor::eNextWord || aAction == nsIEditor::eToEndOfLine) { // Move to the start of the next node, if any nsCOMPtr<nsIContent> nextNode = htmlEditor->GetNextNode(blockParent, offset + 1, true); if (nextNode) { EditorDOMPoint pt = GetGoodSelPointForNode(*nextNode, aAction); nsresult rv = aSelection->Collapse(pt.node, pt.offset); NS_ENSURE_SUCCESS(rv, rv); } else { // Adjust selection to be right after it. nsresult rv = aSelection->Collapse(blockParent, offset + 1); NS_ENSURE_SUCCESS(rv, rv); } } else if (aAction == nsIEditor::ePrevious || aAction == nsIEditor::ePreviousWord || aAction == nsIEditor::eToBeginningOfLine) { // Move to the end of the previous node nsCOMPtr<nsIContent> priorNode = htmlEditor->GetPriorNode(blockParent, offset, true); if (priorNode) { EditorDOMPoint pt = GetGoodSelPointForNode(*priorNode, aAction); nsresult rv = aSelection->Collapse(pt.node, pt.offset); NS_ENSURE_SUCCESS(rv, rv); } else { nsresult rv = aSelection->Collapse(blockParent, offset + 1); NS_ENSURE_SUCCESS(rv, rv); } } else if (aAction != nsIEditor::eNone) { NS_RUNTIMEABORT("CheckForEmptyBlock doesn't support this action yet"); } } NS_ENSURE_STATE(htmlEditor); *aHandled = true; nsresult rv = htmlEditor->DeleteNode(emptyBlock); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } Element* HTMLEditRules::CheckForInvisibleBR(Element& aBlock, BRLocation aWhere, int32_t aOffset) { nsCOMPtr<nsINode> testNode; int32_t testOffset = 0; if (aWhere == BRLocation::blockEnd) { // No block crossing nsCOMPtr<nsIContent> rightmostNode = mHTMLEditor->GetRightmostChild(&aBlock, true); if (!rightmostNode) { return nullptr; } testNode = rightmostNode->GetParentNode(); // Use offset + 1, so last node is included in our evaluation testOffset = testNode->IndexOf(rightmostNode) + 1; } else if (aOffset) { testNode = &aBlock; // We'll check everything to the left of the input position testOffset = aOffset; } else { return nullptr; } WSRunObject wsTester(mHTMLEditor, testNode, testOffset); if (WSType::br == wsTester.mStartReason) { return wsTester.mStartReasonNode->AsElement(); } return nullptr; } /** * aLists and aTables allow the caller to specify what kind of content to * "look inside". If aTables is Tables::yes, look inside any table content, * and insert the inner content into the supplied issupportsarray at offset * aIndex. Similarly with aLists and list content. aIndex is updated to * point past inserted elements. */ void HTMLEditRules::GetInnerContent( nsINode& aNode, nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes, int32_t* aIndex, Lists aLists, Tables aTables) { MOZ_ASSERT(aIndex); for (nsCOMPtr<nsIContent> node = mHTMLEditor->GetFirstEditableChild(aNode); node; node = node->GetNextSibling()) { if ((aLists == Lists::yes && (HTMLEditUtils::IsList(node) || HTMLEditUtils::IsListItem(node))) || (aTables == Tables::yes && HTMLEditUtils::IsTableElement(node))) { GetInnerContent(*node, aOutArrayOfNodes, aIndex, aLists, aTables); } else { aOutArrayOfNodes.InsertElementAt(*aIndex, *node); (*aIndex)++; } } } /** * Promotes selection to include blocks that have all their children selected. */ nsresult HTMLEditRules::ExpandSelectionForDeletion(Selection& aSelection) { // Don't need to touch collapsed selections if (aSelection.Collapsed()) { return NS_OK; } // We don't need to mess with cell selections, and we assume multirange // selections are those. if (aSelection.RangeCount() != 1) { return NS_OK; } // Find current sel start and end NS_ENSURE_TRUE(aSelection.GetRangeAt(0), NS_ERROR_NULL_POINTER); OwningNonNull<nsRange> range = *aSelection.GetRangeAt(0); nsCOMPtr<nsINode> selStartNode = range->GetStartParent(); int32_t selStartOffset = range->StartOffset(); nsCOMPtr<nsINode> selEndNode = range->GetEndParent(); int32_t selEndOffset = range->EndOffset(); // Find current selection common block parent nsCOMPtr<Element> selCommon = HTMLEditor::GetBlock(*range->GetCommonAncestor()); NS_ENSURE_STATE(selCommon); // Set up for loops and cache our root element nsCOMPtr<nsINode> firstBRParent; nsCOMPtr<nsINode> unused; int32_t visOffset = 0, firstBROffset = 0; WSType wsType; nsCOMPtr<Element> root = mHTMLEditor->GetActiveEditingHost(); NS_ENSURE_TRUE(root, NS_ERROR_FAILURE); // Find previous visible thingy before start of selection if (selStartNode != selCommon && selStartNode != root) { while (true) { WSRunObject wsObj(mHTMLEditor, selStartNode, selStartOffset); wsObj.PriorVisibleNode(selStartNode, selStartOffset, address_of(unused), &visOffset, &wsType); if (wsType != WSType::thisBlock) { break; } // We want to keep looking up. But stop if we are crossing table // element boundaries, or if we hit the root. if (HTMLEditUtils::IsTableElement(wsObj.mStartReasonNode) || selCommon == wsObj.mStartReasonNode || root == wsObj.mStartReasonNode) { break; } selStartNode = wsObj.mStartReasonNode->GetParentNode(); selStartOffset = selStartNode ? selStartNode->IndexOf(wsObj.mStartReasonNode) : -1; } } // Find next visible thingy after end of selection if (selEndNode != selCommon && selEndNode != root) { for (;;) { WSRunObject wsObj(mHTMLEditor, selEndNode, selEndOffset); wsObj.NextVisibleNode(selEndNode, selEndOffset, address_of(unused), &visOffset, &wsType); if (wsType == WSType::br) { if (mHTMLEditor->IsVisBreak(wsObj.mEndReasonNode)) { break; } if (!firstBRParent) { firstBRParent = selEndNode; firstBROffset = selEndOffset; } selEndNode = wsObj.mEndReasonNode->GetParentNode(); selEndOffset = selEndNode ? selEndNode->IndexOf(wsObj.mEndReasonNode) + 1 : 0; } else if (wsType == WSType::thisBlock) { // We want to keep looking up. But stop if we are crossing table // element boundaries, or if we hit the root. if (HTMLEditUtils::IsTableElement(wsObj.mEndReasonNode) || selCommon == wsObj.mEndReasonNode || root == wsObj.mEndReasonNode) { break; } selEndNode = wsObj.mEndReasonNode->GetParentNode(); selEndOffset = 1 + selEndNode->IndexOf(wsObj.mEndReasonNode); } else { break; } } } // Now set the selection to the new range aSelection.Collapse(selStartNode, selStartOffset); // Expand selection endpoint only if we didn't pass a br, or if we really // needed to pass that br (i.e., its block is now totally selected) bool doEndExpansion = true; if (firstBRParent) { // Find block node containing br nsCOMPtr<Element> brBlock = HTMLEditor::GetBlock(*firstBRParent); bool nodeBefore = false, nodeAfter = false; // Create a range that represents expanded selection RefPtr<nsRange> range = new nsRange(selStartNode); nsresult rv = range->SetStart(selStartNode, selStartOffset); NS_ENSURE_SUCCESS(rv, rv); rv = range->SetEnd(selEndNode, selEndOffset); NS_ENSURE_SUCCESS(rv, rv); // Check if block is entirely inside range if (brBlock) { nsRange::CompareNodeToRange(brBlock, range, &nodeBefore, &nodeAfter); } // If block isn't contained, forgo grabbing the br in expanded selection if (nodeBefore || nodeAfter) { doEndExpansion = false; } } if (doEndExpansion) { nsresult rv = aSelection.Extend(selEndNode, selEndOffset); NS_ENSURE_SUCCESS(rv, rv); } else { // Only expand to just before br nsresult rv = aSelection.Extend(firstBRParent, firstBROffset); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } /** * NormalizeSelection() tweaks non-collapsed selections to be more "natural". * Idea here is to adjust selection endpoint so that they do not cross breaks * or block boundaries unless something editable beyond that boundary is also * selected. This adjustment makes it much easier for the various block * operations to determine what nodes to act on. */ nsresult HTMLEditRules::NormalizeSelection(Selection* inSelection) { NS_ENSURE_TRUE(inSelection, NS_ERROR_NULL_POINTER); // don't need to touch collapsed selections if (inSelection->Collapsed()) { return NS_OK; } int32_t rangeCount; nsresult rv = inSelection->GetRangeCount(&rangeCount); NS_ENSURE_SUCCESS(rv, rv); // we don't need to mess with cell selections, and we assume multirange selections are those. if (rangeCount != 1) { return NS_OK; } RefPtr<nsRange> range = inSelection->GetRangeAt(0); NS_ENSURE_TRUE(range, NS_ERROR_NULL_POINTER); nsCOMPtr<nsIDOMNode> startNode, endNode; int32_t startOffset, endOffset; nsCOMPtr<nsIDOMNode> newStartNode, newEndNode; int32_t newStartOffset, newEndOffset; rv = range->GetStartContainer(getter_AddRefs(startNode)); NS_ENSURE_SUCCESS(rv, rv); rv = range->GetStartOffset(&startOffset); NS_ENSURE_SUCCESS(rv, rv); rv = range->GetEndContainer(getter_AddRefs(endNode)); NS_ENSURE_SUCCESS(rv, rv); rv = range->GetEndOffset(&endOffset); NS_ENSURE_SUCCESS(rv, rv); // adjusted values default to original values newStartNode = startNode; newStartOffset = startOffset; newEndNode = endNode; newEndOffset = endOffset; // some locals we need for whitespace code nsCOMPtr<nsINode> unused; int32_t offset; WSType wsType; // let the whitespace code do the heavy lifting WSRunObject wsEndObj(mHTMLEditor, endNode, endOffset); // is there any intervening visible whitespace? if so we can't push selection past that, // it would visibly change maening of users selection nsCOMPtr<nsINode> endNode_(do_QueryInterface(endNode)); wsEndObj.PriorVisibleNode(endNode_, endOffset, address_of(unused), &offset, &wsType); if (wsType != WSType::text && wsType != WSType::normalWS) { // eThisBlock and eOtherBlock conveniently distinquish cases // of going "down" into a block and "up" out of a block. if (wsEndObj.mStartReason == WSType::otherBlock) { // endpoint is just after the close of a block. nsCOMPtr<nsIDOMNode> child = GetAsDOMNode(mHTMLEditor->GetRightmostChild(wsEndObj.mStartReasonNode, true)); if (child) { newEndNode = EditorBase::GetNodeLocation(child, &newEndOffset); ++newEndOffset; // offset *after* child } // else block is empty - we can leave selection alone here, i think. } else if (wsEndObj.mStartReason == WSType::thisBlock) { // endpoint is just after start of this block nsCOMPtr<nsIDOMNode> child; NS_ENSURE_STATE(mHTMLEditor); mHTMLEditor->GetPriorHTMLNode(endNode, endOffset, address_of(child)); if (child) { newEndNode = EditorBase::GetNodeLocation(child, &newEndOffset); ++newEndOffset; // offset *after* child } // else block is empty - we can leave selection alone here, i think. } else if (wsEndObj.mStartReason == WSType::br) { // endpoint is just after break. lets adjust it to before it. newEndNode = EditorBase::GetNodeLocation(GetAsDOMNode(wsEndObj.mStartReasonNode), &newEndOffset); } } // similar dealio for start of range WSRunObject wsStartObj(mHTMLEditor, startNode, startOffset); // is there any intervening visible whitespace? if so we can't push selection past that, // it would visibly change maening of users selection nsCOMPtr<nsINode> startNode_(do_QueryInterface(startNode)); wsStartObj.NextVisibleNode(startNode_, startOffset, address_of(unused), &offset, &wsType); if (wsType != WSType::text && wsType != WSType::normalWS) { // eThisBlock and eOtherBlock conveniently distinquish cases // of going "down" into a block and "up" out of a block. if (wsStartObj.mEndReason == WSType::otherBlock) { // startpoint is just before the start of a block. nsCOMPtr<nsIDOMNode> child = GetAsDOMNode(mHTMLEditor->GetLeftmostChild(wsStartObj.mEndReasonNode, true)); if (child) { newStartNode = EditorBase::GetNodeLocation(child, &newStartOffset); } // else block is empty - we can leave selection alone here, i think. } else if (wsStartObj.mEndReason == WSType::thisBlock) { // startpoint is just before end of this block nsCOMPtr<nsIDOMNode> child; NS_ENSURE_STATE(mHTMLEditor); mHTMLEditor->GetNextHTMLNode(startNode, startOffset, address_of(child)); if (child) { newStartNode = EditorBase::GetNodeLocation(child, &newStartOffset); } // else block is empty - we can leave selection alone here, i think. } else if (wsStartObj.mEndReason == WSType::br) { // startpoint is just before a break. lets adjust it to after it. newStartNode = EditorBase::GetNodeLocation(GetAsDOMNode(wsStartObj.mEndReasonNode), &newStartOffset); ++newStartOffset; // offset *after* break } } // there is a demented possiblity we have to check for. We might have a very strange selection // that is not collapsed and yet does not contain any editable content, and satisfies some of the // above conditions that cause tweaking. In this case we don't want to tweak the selection into // a block it was never in, etc. There are a variety of strategies one might use to try to // detect these cases, but I think the most straightforward is to see if the adjusted locations // "cross" the old values: ie, new end before old start, or new start after old end. If so // then just leave things alone. int16_t comp; comp = nsContentUtils::ComparePoints(startNode, startOffset, newEndNode, newEndOffset); if (comp == 1) { return NS_OK; // New end before old start. } comp = nsContentUtils::ComparePoints(newStartNode, newStartOffset, endNode, endOffset); if (comp == 1) { return NS_OK; // New start after old end. } // otherwise set selection to new values. inSelection->Collapse(newStartNode, newStartOffset); inSelection->Extend(newEndNode, newEndOffset); return NS_OK; } /** * GetPromotedPoint() figures out where a start or end point for a block * operation really is. */ void HTMLEditRules::GetPromotedPoint(RulesEndpoint aWhere, nsIDOMNode* aNode, int32_t aOffset, EditAction actionID, nsCOMPtr<nsIDOMNode>* outNode, int32_t* outOffset) { nsCOMPtr<nsINode> node = do_QueryInterface(aNode); MOZ_ASSERT(node && outNode && outOffset); // default values *outNode = node->AsDOMNode(); *outOffset = aOffset; // we do one thing for text actions, something else entirely for other // actions if (actionID == EditAction::insertText || actionID == EditAction::insertIMEText || actionID == EditAction::insertBreak || actionID == EditAction::deleteText) { bool isSpace, isNBSP; nsCOMPtr<nsIContent> content = do_QueryInterface(node), temp; // for text actions, we want to look backwards (or forwards, as // appropriate) for additional whitespace or nbsp's. We may have to act on // these later even though they are outside of the initial selection. Even // if they are in another node! while (content) { int32_t offset; if (aWhere == kStart) { NS_ENSURE_TRUE_VOID(mHTMLEditor); mHTMLEditor->IsPrevCharInNodeWhitespace(content, *outOffset, &isSpace, &isNBSP, getter_AddRefs(temp), &offset); } else { NS_ENSURE_TRUE_VOID(mHTMLEditor); mHTMLEditor->IsNextCharInNodeWhitespace(content, *outOffset, &isSpace, &isNBSP, getter_AddRefs(temp), &offset); } if (isSpace || isNBSP) { content = temp; *outOffset = offset; } else { break; } } *outNode = content->AsDOMNode(); return; } int32_t offset = aOffset; // else not a text section. In this case we want to see if we should grab // any adjacent inline nodes and/or parents and other ancestors if (aWhere == kStart) { // some special casing for text nodes if (node->IsNodeOfType(nsINode::eTEXT)) { if (!node->GetParentNode()) { // Okay, can't promote any further return; } offset = node->GetParentNode()->IndexOf(node); node = node->GetParentNode(); } // look back through any further inline nodes that aren't across a <br> // from us, and that are enclosed in the same block. NS_ENSURE_TRUE_VOID(mHTMLEditor); nsCOMPtr<nsINode> priorNode = mHTMLEditor->GetPriorHTMLNode(node, offset, true); while (priorNode && priorNode->GetParentNode() && mHTMLEditor && !mHTMLEditor->IsVisBreak(priorNode) && !IsBlockNode(*priorNode)) { offset = priorNode->GetParentNode()->IndexOf(priorNode); node = priorNode->GetParentNode(); NS_ENSURE_TRUE_VOID(mHTMLEditor); priorNode = mHTMLEditor->GetPriorHTMLNode(node, offset, true); } // finding the real start for this point. look up the tree for as long as // we are the first node in the container, and as long as we haven't hit // the body node. NS_ENSURE_TRUE_VOID(mHTMLEditor); nsCOMPtr<nsIContent> nearNode = mHTMLEditor->GetPriorHTMLNode(node, offset, true); while (!nearNode && !node->IsHTMLElement(nsGkAtoms::body) && node->GetParentNode()) { // some cutoffs are here: we don't need to also include them in the // aWhere == kEnd case. as long as they are in one or the other it will // work. special case for outdent: don't keep looking up if we have // found a blockquote element to act on if (actionID == EditAction::outdent && node->IsHTMLElement(nsGkAtoms::blockquote)) { break; } int32_t parentOffset = node->GetParentNode()->IndexOf(node); nsCOMPtr<nsINode> parent = node->GetParentNode(); // Don't walk past the editable section. Note that we need to check // before walking up to a parent because we need to return the parent // object, so the parent itself might not be in the editable area, but // it's OK if we're not performing a block-level action. bool blockLevelAction = actionID == EditAction::indent || actionID == EditAction::outdent || actionID == EditAction::align || actionID == EditAction::makeBasicBlock; NS_ENSURE_TRUE_VOID(mHTMLEditor); if (!mHTMLEditor->IsDescendantOfEditorRoot(parent) && (blockLevelAction || !mHTMLEditor || !mHTMLEditor->IsDescendantOfEditorRoot(node))) { NS_ENSURE_TRUE_VOID(mHTMLEditor); break; } node = parent; offset = parentOffset; NS_ENSURE_TRUE_VOID(mHTMLEditor); nearNode = mHTMLEditor->GetPriorHTMLNode(node, offset, true); } *outNode = node->AsDOMNode(); *outOffset = offset; return; } // aWhere == kEnd // some special casing for text nodes if (node->IsNodeOfType(nsINode::eTEXT)) { if (!node->GetParentNode()) { // Okay, can't promote any further return; } // want to be after the text node offset = 1 + node->GetParentNode()->IndexOf(node); node = node->GetParentNode(); } // look ahead through any further inline nodes that aren't across a <br> from // us, and that are enclosed in the same block. NS_ENSURE_TRUE(mHTMLEditor, /* void */); nsCOMPtr<nsIContent> nextNode = mHTMLEditor->GetNextHTMLNode(node, offset, true); while (nextNode && !IsBlockNode(*nextNode) && nextNode->GetParentNode()) { offset = 1 + nextNode->GetParentNode()->IndexOf(nextNode); node = nextNode->GetParentNode(); NS_ENSURE_TRUE_VOID(mHTMLEditor); if (mHTMLEditor->IsVisBreak(nextNode)) { break; } // Check for newlines in pre-formatted text nodes. bool isPRE; mHTMLEditor->IsPreformatted(nextNode->AsDOMNode(), &isPRE); if (isPRE) { nsCOMPtr<nsIDOMText> textNode = do_QueryInterface(nextNode); if (textNode) { nsAutoString tempString; textNode->GetData(tempString); int32_t newlinePos = tempString.FindChar(nsCRT::LF); if (newlinePos >= 0) { if (static_cast<uint32_t>(newlinePos) + 1 == tempString.Length()) { // No need for special processing if the newline is at the end. break; } *outNode = nextNode->AsDOMNode(); *outOffset = newlinePos + 1; return; } } } NS_ENSURE_TRUE_VOID(mHTMLEditor); nextNode = mHTMLEditor->GetNextHTMLNode(node, offset, true); } // finding the real end for this point. look up the tree for as long as we // are the last node in the container, and as long as we haven't hit the body // node. NS_ENSURE_TRUE_VOID(mHTMLEditor); nsCOMPtr<nsIContent> nearNode = mHTMLEditor->GetNextHTMLNode(node, offset, true); while (!nearNode && !node->IsHTMLElement(nsGkAtoms::body) && node->GetParentNode()) { int32_t parentOffset = node->GetParentNode()->IndexOf(node); nsCOMPtr<nsINode> parent = node->GetParentNode(); // Don't walk past the editable section. Note that we need to check before // walking up to a parent because we need to return the parent object, so // the parent itself might not be in the editable area, but it's OK. if ((!mHTMLEditor || !mHTMLEditor->IsDescendantOfEditorRoot(node)) && (!mHTMLEditor || !mHTMLEditor->IsDescendantOfEditorRoot(parent))) { NS_ENSURE_TRUE_VOID(mHTMLEditor); break; } node = parent; // we want to be AFTER nearNode offset = parentOffset + 1; NS_ENSURE_TRUE_VOID(mHTMLEditor); nearNode = mHTMLEditor->GetNextHTMLNode(node, offset, true); } *outNode = node->AsDOMNode(); *outOffset = offset; } /** * GetPromotedRanges() runs all the selection range endpoint through * GetPromotedPoint(). */ void HTMLEditRules::GetPromotedRanges(Selection& aSelection, nsTArray<RefPtr<nsRange>>& outArrayOfRanges, EditAction inOperationType) { uint32_t rangeCount = aSelection.RangeCount(); for (uint32_t i = 0; i < rangeCount; i++) { RefPtr<nsRange> selectionRange = aSelection.GetRangeAt(i); MOZ_ASSERT(selectionRange); // Clone range so we don't muck with actual selection ranges RefPtr<nsRange> opRange = selectionRange->CloneRange(); // Make a new adjusted range to represent the appropriate block content. // The basic idea is to push out the range endpoints to truly enclose the // blocks that we will affect. This call alters opRange. PromoteRange(*opRange, inOperationType); // Stuff new opRange into array outArrayOfRanges.AppendElement(opRange); } } /** * PromoteRange() expands a range to include any parents for which all editable * children are already in range. */ void HTMLEditRules::PromoteRange(nsRange& aRange, EditAction aOperationType) { NS_ENSURE_TRUE(mHTMLEditor, ); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); nsCOMPtr<nsINode> startNode = aRange.GetStartParent(); nsCOMPtr<nsINode> endNode = aRange.GetEndParent(); int32_t startOffset = aRange.StartOffset(); int32_t endOffset = aRange.EndOffset(); // MOOSE major hack: // GetPromotedPoint doesn't really do the right thing for collapsed ranges // inside block elements that contain nothing but a solo <br>. It's easier // to put a workaround here than to revamp GetPromotedPoint. :-( if (startNode == endNode && startOffset == endOffset) { nsCOMPtr<Element> block = htmlEditor->GetBlock(*startNode); if (block) { bool bIsEmptyNode = false; nsCOMPtr<nsIContent> root = htmlEditor->GetActiveEditingHost(); // Make sure we don't go higher than our root element in the content tree NS_ENSURE_TRUE(root, ); if (!nsContentUtils::ContentIsDescendantOf(root, block)) { htmlEditor->IsEmptyNode(block, &bIsEmptyNode, true, false); } if (bIsEmptyNode) { startNode = block; endNode = block; startOffset = 0; endOffset = block->Length(); } } } // Make a new adjusted range to represent the appropriate block content. // This is tricky. The basic idea is to push out the range endpoints to // truly enclose the blocks that we will affect. nsCOMPtr<nsIDOMNode> opStartNode; nsCOMPtr<nsIDOMNode> opEndNode; int32_t opStartOffset, opEndOffset; GetPromotedPoint(kStart, GetAsDOMNode(startNode), startOffset, aOperationType, address_of(opStartNode), &opStartOffset); GetPromotedPoint(kEnd, GetAsDOMNode(endNode), endOffset, aOperationType, address_of(opEndNode), &opEndOffset); // Make sure that the new range ends up to be in the editable section. if (!htmlEditor->IsDescendantOfEditorRoot( EditorBase::GetNodeAtRangeOffsetPoint(opStartNode, opStartOffset)) || !htmlEditor->IsDescendantOfEditorRoot( EditorBase::GetNodeAtRangeOffsetPoint(opEndNode, opEndOffset - 1))) { return; } DebugOnly<nsresult> rv = aRange.SetStart(opStartNode, opStartOffset); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = aRange.SetEnd(opEndNode, opEndOffset); MOZ_ASSERT(NS_SUCCEEDED(rv)); } class UniqueFunctor final : public BoolDomIterFunctor { public: explicit UniqueFunctor(nsTArray<OwningNonNull<nsINode>>& aArray) : mArray(aArray) { } // Used to build list of all nodes iterator covers. virtual bool operator()(nsINode* aNode) const { return !mArray.Contains(aNode); } private: nsTArray<OwningNonNull<nsINode>>& mArray; }; /** * GetNodesForOperation() runs through the ranges in the array and construct a * new array of nodes to be acted on. */ nsresult HTMLEditRules::GetNodesForOperation( nsTArray<RefPtr<nsRange>>& aArrayOfRanges, nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes, EditAction aOperationType, TouchContent aTouchContent) { NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); int32_t rangeCount = aArrayOfRanges.Length(); if (aTouchContent == TouchContent::yes) { // Split text nodes. This is necessary, since GetPromotedPoint() may return a // range ending in a text node in case where part of a pre-formatted // elements needs to be moved. for (int32_t i = 0; i < rangeCount; i++) { RefPtr<nsRange> r = aArrayOfRanges[i]; nsCOMPtr<nsIContent> endParent = do_QueryInterface(r->GetEndParent()); if (!htmlEditor->IsTextNode(endParent)) { continue; } nsCOMPtr<nsIDOMText> textNode = do_QueryInterface(endParent); if (textNode) { int32_t offset = r->EndOffset(); nsAutoString tempString; textNode->GetData(tempString); if (0 < offset && offset < static_cast<int32_t>(tempString.Length())) { // Split the text node. nsCOMPtr<nsIDOMNode> tempNode; nsresult rv = htmlEditor->SplitNode(endParent->AsDOMNode(), offset, getter_AddRefs(tempNode)); NS_ENSURE_SUCCESS(rv, rv); // Correct the range. // The new end parent becomes the parent node of the text. nsCOMPtr<nsIContent> newParent = endParent->GetParent(); r->SetEnd(newParent, newParent->IndexOf(endParent)); } } } } // Bust up any inlines that cross our range endpoints, but only if we are // allowed to touch content. if (aTouchContent == TouchContent::yes) { nsTArray<OwningNonNull<RangeItem>> rangeItemArray; rangeItemArray.AppendElements(rangeCount); // First register ranges for special editor gravity for (int32_t i = 0; i < rangeCount; i++) { rangeItemArray[i] = new RangeItem(); rangeItemArray[i]->StoreRange(aArrayOfRanges[0]); htmlEditor->mRangeUpdater.RegisterRangeItem(rangeItemArray[i]); aArrayOfRanges.RemoveElementAt(0); } // Now bust up inlines. nsresult rv = NS_OK; for (auto& item : Reversed(rangeItemArray)) { rv = BustUpInlinesAtRangeEndpoints(*item); if (NS_FAILED(rv)) { break; } } // Then unregister the ranges for (auto& item : rangeItemArray) { htmlEditor->mRangeUpdater.DropRangeItem(item); aArrayOfRanges.AppendElement(item->GetRange()); } NS_ENSURE_SUCCESS(rv, rv); } // Gather up a list of all the nodes for (auto& range : aArrayOfRanges) { DOMSubtreeIterator iter; nsresult rv = iter.Init(*range); NS_ENSURE_SUCCESS(rv, rv); if (aOutArrayOfNodes.IsEmpty()) { iter.AppendList(TrivialFunctor(), aOutArrayOfNodes); } else { // We don't want duplicates in aOutArrayOfNodes, so we use an // iterator/functor that only return nodes that are not already in // aOutArrayOfNodes. nsTArray<OwningNonNull<nsINode>> nodes; iter.AppendList(UniqueFunctor(aOutArrayOfNodes), nodes); aOutArrayOfNodes.AppendElements(nodes); } } // Certain operations should not act on li's and td's, but rather inside // them. Alter the list as needed. if (aOperationType == EditAction::makeBasicBlock) { for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) { OwningNonNull<nsINode> node = aOutArrayOfNodes[i]; if (HTMLEditUtils::IsListItem(node)) { int32_t j = i; aOutArrayOfNodes.RemoveElementAt(i); GetInnerContent(*node, aOutArrayOfNodes, &j); } } } // Indent/outdent already do something special for list items, but we still // need to make sure we don't act on table elements else if (aOperationType == EditAction::outdent || aOperationType == EditAction::indent || aOperationType == EditAction::setAbsolutePosition) { for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) { OwningNonNull<nsINode> node = aOutArrayOfNodes[i]; if (HTMLEditUtils::IsTableElementButNotTable(node)) { int32_t j = i; aOutArrayOfNodes.RemoveElementAt(i); GetInnerContent(*node, aOutArrayOfNodes, &j); } } } // Outdent should look inside of divs. if (aOperationType == EditAction::outdent && !htmlEditor->IsCSSEnabled()) { for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) { OwningNonNull<nsINode> node = aOutArrayOfNodes[i]; if (node->IsHTMLElement(nsGkAtoms::div)) { int32_t j = i; aOutArrayOfNodes.RemoveElementAt(i); GetInnerContent(*node, aOutArrayOfNodes, &j, Lists::no, Tables::no); } } } // Post-process the list to break up inline containers that contain br's, but // only for operations that might care, like making lists or paragraphs if (aOperationType == EditAction::makeBasicBlock || aOperationType == EditAction::makeList || aOperationType == EditAction::align || aOperationType == EditAction::setAbsolutePosition || aOperationType == EditAction::indent || aOperationType == EditAction::outdent) { for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) { OwningNonNull<nsINode> node = aOutArrayOfNodes[i]; if (aTouchContent == TouchContent::yes && IsInlineNode(node) && htmlEditor->IsContainer(node) && !htmlEditor->IsTextNode(node)) { nsTArray<OwningNonNull<nsINode>> arrayOfInlines; nsresult rv = BustUpInlinesAtBRs(*node->AsContent(), arrayOfInlines); NS_ENSURE_SUCCESS(rv, rv); // Put these nodes in aOutArrayOfNodes, replacing the current node aOutArrayOfNodes.RemoveElementAt(i); aOutArrayOfNodes.InsertElementsAt(i, arrayOfInlines); } } } return NS_OK; } void HTMLEditRules::GetChildNodesForOperation( nsINode& aNode, nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes) { for (nsCOMPtr<nsIContent> child = aNode.GetFirstChild(); child; child = child->GetNextSibling()) { outArrayOfNodes.AppendElement(*child); } } nsresult HTMLEditRules::GetListActionNodes( nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes, EntireList aEntireList, TouchContent aTouchContent) { NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); RefPtr<Selection> selection = htmlEditor->GetSelection(); NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE); // Added this in so that ui code can ask to change an entire list, even if // selection is only in part of it. used by list item dialog. if (aEntireList == EntireList::yes) { uint32_t rangeCount = selection->RangeCount(); for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) { RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx); for (nsCOMPtr<nsINode> parent = range->GetCommonAncestor(); parent; parent = parent->GetParentNode()) { if (HTMLEditUtils::IsList(parent)) { aOutArrayOfNodes.AppendElement(*parent); break; } } } // If we didn't find any nodes this way, then try the normal way. Perhaps // the selection spans multiple lists but with no common list parent. if (!aOutArrayOfNodes.IsEmpty()) { return NS_OK; } } { // We don't like other people messing with our selection! AutoTransactionsConserveSelection dontSpazMySelection(htmlEditor); // contruct a list of nodes to act on. nsresult rv = GetNodesFromSelection(*selection, EditAction::makeList, aOutArrayOfNodes, aTouchContent); NS_ENSURE_SUCCESS(rv, rv); } // Pre-process our list of nodes for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) { OwningNonNull<nsINode> testNode = aOutArrayOfNodes[i]; // Remove all non-editable nodes. Leave them be. if (!htmlEditor->IsEditable(testNode)) { aOutArrayOfNodes.RemoveElementAt(i); continue; } // Scan for table elements and divs. If we find table elements other than // table, replace it with a list of any editable non-table content. if (HTMLEditUtils::IsTableElementButNotTable(testNode)) { int32_t j = i; aOutArrayOfNodes.RemoveElementAt(i); GetInnerContent(*testNode, aOutArrayOfNodes, &j, Lists::no); } } // If there is only one node in the array, and it is a list, div, or // blockquote, then look inside of it until we find inner list or content. LookInsideDivBQandList(aOutArrayOfNodes); return NS_OK; } void HTMLEditRules::LookInsideDivBQandList( nsTArray<OwningNonNull<nsINode>>& aNodeArray) { NS_ENSURE_TRUE(mHTMLEditor, ); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); // If there is only one node in the array, and it is a list, div, or // blockquote, then look inside of it until we find inner list or content. if (aNodeArray.Length() != 1) { return; } OwningNonNull<nsINode> curNode = aNodeArray[0]; while (curNode->IsHTMLElement(nsGkAtoms::div) || HTMLEditUtils::IsList(curNode) || curNode->IsHTMLElement(nsGkAtoms::blockquote)) { // Dive as long as there's only one child, and it's a list, div, blockquote uint32_t numChildren = htmlEditor->CountEditableChildren(curNode); if (numChildren != 1) { break; } // Keep diving! XXX One would expect to dive into the one editable node. nsCOMPtr<nsIContent> child = curNode->GetFirstChild(); if (!child->IsHTMLElement(nsGkAtoms::div) && !HTMLEditUtils::IsList(child) && !child->IsHTMLElement(nsGkAtoms::blockquote)) { break; } // check editability XXX floppy moose curNode = child; } // We've found innermost list/blockquote/div: replace the one node in the // array with these nodes aNodeArray.RemoveElementAt(0); if (curNode->IsAnyOfHTMLElements(nsGkAtoms::div, nsGkAtoms::blockquote)) { int32_t j = 0; GetInnerContent(*curNode, aNodeArray, &j, Lists::no, Tables::no); return; } aNodeArray.AppendElement(*curNode); } void HTMLEditRules::GetDefinitionListItemTypes(dom::Element* aElement, bool* aDT, bool* aDD) { MOZ_ASSERT(aElement); MOZ_ASSERT(aElement->IsHTMLElement(nsGkAtoms::dl)); MOZ_ASSERT(aDT); MOZ_ASSERT(aDD); *aDT = *aDD = false; for (nsIContent* child = aElement->GetFirstChild(); child; child = child->GetNextSibling()) { if (child->IsHTMLElement(nsGkAtoms::dt)) { *aDT = true; } else if (child->IsHTMLElement(nsGkAtoms::dd)) { *aDD = true; } } } nsresult HTMLEditRules::GetParagraphFormatNodes( nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes, TouchContent aTouchContent) { NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); RefPtr<Selection> selection = htmlEditor->GetSelection(); NS_ENSURE_STATE(selection); // Contruct a list of nodes to act on. nsresult rv = GetNodesFromSelection(*selection, EditAction::makeBasicBlock, outArrayOfNodes, aTouchContent); NS_ENSURE_SUCCESS(rv, rv); // Pre-process our list of nodes for (int32_t i = outArrayOfNodes.Length() - 1; i >= 0; i--) { OwningNonNull<nsINode> testNode = outArrayOfNodes[i]; // Remove all non-editable nodes. Leave them be. if (!htmlEditor->IsEditable(testNode)) { outArrayOfNodes.RemoveElementAt(i); continue; } // Scan for table elements. If we find table elements other than table, // replace it with a list of any editable non-table content. Ditto for // list elements. if (HTMLEditUtils::IsTableElement(testNode) || HTMLEditUtils::IsList(testNode) || HTMLEditUtils::IsListItem(testNode)) { int32_t j = i; outArrayOfNodes.RemoveElementAt(i); GetInnerContent(testNode, outArrayOfNodes, &j); } } return NS_OK; } nsresult HTMLEditRules::BustUpInlinesAtRangeEndpoints(RangeItem& item) { bool isCollapsed = ((item.startNode == item.endNode) && (item.startOffset == item.endOffset)); nsCOMPtr<nsIContent> endInline = GetHighestInlineParent(*item.endNode); // if we have inline parents above range endpoints, split them if (endInline && !isCollapsed) { nsCOMPtr<nsINode> resultEndNode = endInline->GetParentNode(); NS_ENSURE_STATE(mHTMLEditor); // item.endNode must be content if endInline isn't null int32_t resultEndOffset = mHTMLEditor->SplitNodeDeep(*endInline, *item.endNode->AsContent(), item.endOffset, EditorBase::EmptyContainers::no); NS_ENSURE_TRUE(resultEndOffset != -1, NS_ERROR_FAILURE); // reset range item.endNode = resultEndNode; item.endOffset = resultEndOffset; } nsCOMPtr<nsIContent> startInline = GetHighestInlineParent(*item.startNode); if (startInline) { nsCOMPtr<nsINode> resultStartNode = startInline->GetParentNode(); NS_ENSURE_STATE(mHTMLEditor); int32_t resultStartOffset = mHTMLEditor->SplitNodeDeep(*startInline, *item.startNode->AsContent(), item.startOffset, EditorBase::EmptyContainers::no); NS_ENSURE_TRUE(resultStartOffset != -1, NS_ERROR_FAILURE); // reset range item.startNode = resultStartNode; item.startOffset = resultStartOffset; } return NS_OK; } nsresult HTMLEditRules::BustUpInlinesAtBRs( nsIContent& aNode, nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes) { NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); // First build up a list of all the break nodes inside the inline container. nsTArray<OwningNonNull<nsINode>> arrayOfBreaks; BRNodeFunctor functor; DOMIterator iter(aNode); iter.AppendList(functor, arrayOfBreaks); // If there aren't any breaks, just put inNode itself in the array if (arrayOfBreaks.IsEmpty()) { aOutArrayOfNodes.AppendElement(aNode); return NS_OK; } // Else we need to bust up inNode along all the breaks nsCOMPtr<nsINode> inlineParentNode = aNode.GetParentNode(); nsCOMPtr<nsIContent> splitDeepNode = &aNode; nsCOMPtr<nsIContent> leftNode, rightNode; for (uint32_t i = 0; i < arrayOfBreaks.Length(); i++) { OwningNonNull<Element> breakNode = *arrayOfBreaks[i]->AsElement(); NS_ENSURE_TRUE(splitDeepNode, NS_ERROR_NULL_POINTER); NS_ENSURE_TRUE(breakNode->GetParent(), NS_ERROR_NULL_POINTER); OwningNonNull<nsIContent> splitParentNode = *breakNode->GetParent(); int32_t splitOffset = splitParentNode->IndexOf(breakNode); int32_t resultOffset = htmlEditor->SplitNodeDeep(*splitDeepNode, splitParentNode, splitOffset, HTMLEditor::EmptyContainers::yes, getter_AddRefs(leftNode), getter_AddRefs(rightNode)); NS_ENSURE_STATE(resultOffset != -1); // Put left node in node list if (leftNode) { // Might not be a left node. A break might have been at the very // beginning of inline container, in which case SplitNodeDeep would not // actually split anything aOutArrayOfNodes.AppendElement(*leftNode); } // Move break outside of container and also put in node list nsresult rv = htmlEditor->MoveNode(breakNode, inlineParentNode, resultOffset); NS_ENSURE_SUCCESS(rv, rv); aOutArrayOfNodes.AppendElement(*breakNode); // Now rightNode becomes the new node to split splitDeepNode = rightNode; } // Now tack on remaining rightNode, if any, to the list if (rightNode) { aOutArrayOfNodes.AppendElement(*rightNode); } return NS_OK; } nsIContent* HTMLEditRules::GetHighestInlineParent(nsINode& aNode) { if (!aNode.IsContent() || IsBlockNode(aNode)) { return nullptr; } OwningNonNull<nsIContent> node = *aNode.AsContent(); while (node->GetParent() && IsInlineNode(*node->GetParent())) { node = *node->GetParent(); } return node; } /** * GetNodesFromPoint() constructs a list of nodes from a point that will be * operated on. */ nsresult HTMLEditRules::GetNodesFromPoint( EditorDOMPoint aPoint, EditAction aOperation, nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes, TouchContent aTouchContent) { NS_ENSURE_STATE(aPoint.node); RefPtr<nsRange> range = new nsRange(aPoint.node); nsresult rv = range->SetStart(aPoint.node, aPoint.offset); MOZ_ASSERT(NS_SUCCEEDED(rv)); // Expand the range to include adjacent inlines PromoteRange(*range, aOperation); // Make array of ranges nsTArray<RefPtr<nsRange>> arrayOfRanges; // Stuff new opRange into array arrayOfRanges.AppendElement(range); // Use these ranges to contruct a list of nodes to act on rv = GetNodesForOperation(arrayOfRanges, outArrayOfNodes, aOperation, aTouchContent); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } /** * GetNodesFromSelection() constructs a list of nodes from the selection that * will be operated on. */ nsresult HTMLEditRules::GetNodesFromSelection( Selection& aSelection, EditAction aOperation, nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes, TouchContent aTouchContent) { // Promote selection ranges nsTArray<RefPtr<nsRange>> arrayOfRanges; GetPromotedRanges(aSelection, arrayOfRanges, aOperation); // Use these ranges to contruct a list of nodes to act on. nsresult rv = GetNodesForOperation(arrayOfRanges, outArrayOfNodes, aOperation, aTouchContent); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } /** * MakeTransitionList() detects all the transitions in the array, where a * transition means that adjacent nodes in the array don't have the same parent. */ void HTMLEditRules::MakeTransitionList(nsTArray<OwningNonNull<nsINode>>& aNodeArray, nsTArray<bool>& aTransitionArray) { nsCOMPtr<nsINode> prevParent; aTransitionArray.EnsureLengthAtLeast(aNodeArray.Length()); for (uint32_t i = 0; i < aNodeArray.Length(); i++) { if (aNodeArray[i]->GetParentNode() != prevParent) { // Different parents: transition point aTransitionArray[i] = true; } else { // Same parents: these nodes grew up together aTransitionArray[i] = false; } prevParent = aNodeArray[i]->GetParentNode(); } } /** * If aNode is the descendant of a listitem, return that li. But table element * boundaries are stoppers on the search. Also stops on the active editor host * (contenteditable). Also test if aNode is an li itself. */ Element* HTMLEditRules::IsInListItem(nsINode* aNode) { NS_ENSURE_TRUE(aNode, nullptr); if (HTMLEditUtils::IsListItem(aNode)) { return aNode->AsElement(); } Element* parent = aNode->GetParentElement(); while (parent && mHTMLEditor && mHTMLEditor->IsDescendantOfEditorRoot(parent) && !HTMLEditUtils::IsTableElement(parent)) { if (HTMLEditUtils::IsListItem(parent)) { return parent; } parent = parent->GetParentElement(); } return nullptr; } /** * ReturnInHeader: do the right thing for returns pressed in headers */ nsresult HTMLEditRules::ReturnInHeader(Selection& aSelection, Element& aHeader, nsINode& aNode, int32_t aOffset) { NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); // Remember where the header is nsCOMPtr<nsINode> headerParent = aHeader.GetParentNode(); int32_t offset = headerParent ? headerParent->IndexOf(&aHeader) : -1; // Get ws code to adjust any ws nsCOMPtr<nsINode> node = &aNode; nsresult rv = WSRunObject::PrepareToSplitAcrossBlocks(htmlEditor, address_of(node), &aOffset); NS_ENSURE_SUCCESS(rv, rv); // Split the header NS_ENSURE_STATE(node->IsContent()); htmlEditor->SplitNodeDeep(aHeader, *node->AsContent(), aOffset); // If the left-hand heading is empty, put a mozbr in it nsCOMPtr<nsIContent> prevItem = htmlEditor->GetPriorHTMLSibling(&aHeader); if (prevItem && HTMLEditUtils::IsHeader(*prevItem)) { bool isEmptyNode; rv = htmlEditor->IsEmptyNode(prevItem, &isEmptyNode); NS_ENSURE_SUCCESS(rv, rv); if (isEmptyNode) { rv = CreateMozBR(prevItem->AsDOMNode(), 0); NS_ENSURE_SUCCESS(rv, rv); } } // If the new (righthand) header node is empty, delete it bool isEmpty; rv = IsEmptyBlock(aHeader, &isEmpty, MozBRCounts::no); NS_ENSURE_SUCCESS(rv, rv); if (isEmpty) { rv = htmlEditor->DeleteNode(&aHeader); NS_ENSURE_SUCCESS(rv, rv); // Layout tells the caret to blink in a weird place if we don't place a // break after the header. nsCOMPtr<nsIContent> sibling = htmlEditor->GetNextHTMLSibling(headerParent, offset + 1); if (!sibling || !sibling->IsHTMLElement(nsGkAtoms::br)) { ClearCachedStyles(); htmlEditor->mTypeInState->ClearAllProps(); // Create a paragraph nsCOMPtr<Element> pNode = htmlEditor->CreateNode(nsGkAtoms::p, headerParent, offset + 1); NS_ENSURE_STATE(pNode); // Append a <br> to it nsCOMPtr<Element> brNode = htmlEditor->CreateBR(pNode, 0); NS_ENSURE_STATE(brNode); // Set selection to before the break rv = aSelection.Collapse(pNode, 0); NS_ENSURE_SUCCESS(rv, rv); } else { headerParent = sibling->GetParentNode(); offset = headerParent ? headerParent->IndexOf(sibling) : -1; // Put selection after break rv = aSelection.Collapse(headerParent, offset + 1); NS_ENSURE_SUCCESS(rv, rv); } } else { // Put selection at front of righthand heading rv = aSelection.Collapse(&aHeader, 0); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } /** * ReturnInParagraph() does the right thing for returns pressed in paragraphs. */ nsresult HTMLEditRules::ReturnInParagraph(Selection* aSelection, nsIDOMNode* aPara, nsIDOMNode* aNode, int32_t aOffset, bool* aCancel, bool* aHandled) { nsCOMPtr<nsINode> node = do_QueryInterface(aNode); if (!aSelection || !aPara || !node || !aCancel || !aHandled) { return NS_ERROR_NULL_POINTER; } *aCancel = false; *aHandled = false; int32_t offset; nsCOMPtr<nsINode> parent = EditorBase::GetNodeLocation(node, &offset); NS_ENSURE_STATE(mHTMLEditor); bool doesCRCreateNewP = mHTMLEditor->GetReturnInParagraphCreatesNewParagraph(); bool newBRneeded = false; bool newSelNode = false; nsCOMPtr<nsIContent> sibling; nsCOMPtr<nsIDOMNode> selNode = aNode; int32_t selOffset = aOffset; NS_ENSURE_STATE(mHTMLEditor); if (aNode == aPara && doesCRCreateNewP) { // we are at the edges of the block, newBRneeded not needed! sibling = node->AsContent(); } else if (mHTMLEditor->IsTextNode(aNode)) { nsCOMPtr<nsIDOMText> textNode = do_QueryInterface(aNode); uint32_t strLength; nsresult rv = textNode->GetLength(&strLength); NS_ENSURE_SUCCESS(rv, rv); // at beginning of text node? if (!aOffset) { // is there a BR prior to it? NS_ENSURE_STATE(mHTMLEditor); sibling = mHTMLEditor->GetPriorHTMLSibling(node); if (!sibling || !mHTMLEditor || !mHTMLEditor->IsVisBreak(sibling) || TextEditUtils::HasMozAttr(GetAsDOMNode(sibling))) { NS_ENSURE_STATE(mHTMLEditor); newBRneeded = true; } } else if (aOffset == (int32_t)strLength) { // we're at the end of text node... // is there a BR after to it? NS_ENSURE_STATE(mHTMLEditor); sibling = mHTMLEditor->GetNextHTMLSibling(node); if (!sibling || !mHTMLEditor || !mHTMLEditor->IsVisBreak(sibling) || TextEditUtils::HasMozAttr(GetAsDOMNode(sibling))) { NS_ENSURE_STATE(mHTMLEditor); newBRneeded = true; offset++; } } else { if (doesCRCreateNewP) { nsCOMPtr<nsIDOMNode> tmp; rv = mTextEditor->SplitNode(aNode, aOffset, getter_AddRefs(tmp)); NS_ENSURE_SUCCESS(rv, rv); selNode = tmp; } newBRneeded = true; offset++; } } else { // not in a text node. // is there a BR prior to it? nsCOMPtr<nsIContent> nearNode; NS_ENSURE_STATE(mHTMLEditor); nearNode = mHTMLEditor->GetPriorHTMLNode(node, aOffset); NS_ENSURE_STATE(mHTMLEditor); if (!nearNode || !mHTMLEditor->IsVisBreak(nearNode) || TextEditUtils::HasMozAttr(GetAsDOMNode(nearNode))) { // is there a BR after it? NS_ENSURE_STATE(mHTMLEditor); nearNode = mHTMLEditor->GetNextHTMLNode(node, aOffset); NS_ENSURE_STATE(mHTMLEditor); if (!nearNode || !mHTMLEditor->IsVisBreak(nearNode) || TextEditUtils::HasMozAttr(GetAsDOMNode(nearNode))) { newBRneeded = true; parent = node; offset = aOffset; newSelNode = true; } } if (!newBRneeded) { sibling = nearNode; } } if (newBRneeded) { // if CR does not create a new P, default to BR creation NS_ENSURE_TRUE(doesCRCreateNewP, NS_OK); NS_ENSURE_STATE(mHTMLEditor); sibling = mHTMLEditor->CreateBR(parent, offset); if (newSelNode) { // We split the parent after the br we've just inserted. selNode = GetAsDOMNode(parent); selOffset = offset + 1; } } *aHandled = true; return SplitParagraph(aPara, sibling, aSelection, address_of(selNode), &selOffset); } /** * SplitParagraph() splits a paragraph at selection point, possibly deleting a * br. */ nsresult HTMLEditRules::SplitParagraph(nsIDOMNode *aPara, nsIContent* aBRNode, Selection* aSelection, nsCOMPtr<nsIDOMNode>* aSelNode, int32_t* aOffset) { nsCOMPtr<Element> para = do_QueryInterface(aPara); NS_ENSURE_TRUE(para && aBRNode && aSelNode && *aSelNode && aOffset && aSelection, NS_ERROR_NULL_POINTER); // split para // get ws code to adjust any ws nsCOMPtr<nsIContent> leftPara, rightPara; NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<nsINode> selNode(do_QueryInterface(*aSelNode)); nsresult rv = WSRunObject::PrepareToSplitAcrossBlocks(mHTMLEditor, address_of(selNode), aOffset); // XXX When it fails, why do we need to return selection node? (Why can the // caller trust the result even when it returns error?) *aSelNode = GetAsDOMNode(selNode); NS_ENSURE_SUCCESS(rv, rv); // split the paragraph NS_ENSURE_STATE(mHTMLEditor); NS_ENSURE_STATE(selNode->IsContent()); mHTMLEditor->SplitNodeDeep(*para, *selNode->AsContent(), *aOffset, HTMLEditor::EmptyContainers::yes, getter_AddRefs(leftPara), getter_AddRefs(rightPara)); // get rid of the break, if it is visible (otherwise it may be needed to prevent an empty p) NS_ENSURE_STATE(mHTMLEditor); if (mHTMLEditor->IsVisBreak(aBRNode)) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->DeleteNode(aBRNode); NS_ENSURE_SUCCESS(rv, rv); } // remove ID attribute on the paragraph we just created nsCOMPtr<nsIDOMElement> rightElt = do_QueryInterface(rightPara); NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->RemoveAttribute(rightElt, NS_LITERAL_STRING("id")); NS_ENSURE_SUCCESS(rv, rv); // check both halves of para to see if we need mozBR rv = InsertMozBRIfNeeded(*leftPara); NS_ENSURE_SUCCESS(rv, rv); rv = InsertMozBRIfNeeded(*rightPara); NS_ENSURE_SUCCESS(rv, rv); // selection to beginning of right hand para; // look inside any containers that are up front. nsCOMPtr<nsINode> rightParaNode = do_QueryInterface(rightPara); NS_ENSURE_STATE(mHTMLEditor && rightParaNode); nsCOMPtr<nsIDOMNode> child = GetAsDOMNode(mHTMLEditor->GetLeftmostChild(rightParaNode, true)); if (mHTMLEditor->IsTextNode(child) || mHTMLEditor->IsContainer(child)) { aSelection->Collapse(child,0); } else { int32_t offset; nsCOMPtr<nsIDOMNode> parent = EditorBase::GetNodeLocation(child, &offset); aSelection->Collapse(parent,offset); } return NS_OK; } /** * ReturnInListItem: do the right thing for returns pressed in list items */ nsresult HTMLEditRules::ReturnInListItem(Selection& aSelection, Element& aListItem, nsINode& aNode, int32_t aOffset) { MOZ_ASSERT(HTMLEditUtils::IsListItem(&aListItem)); NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); // Get the item parent and the active editing host. nsCOMPtr<Element> root = htmlEditor->GetActiveEditingHost(); nsCOMPtr<Element> list = aListItem.GetParentElement(); int32_t itemOffset = list ? list->IndexOf(&aListItem) : -1; // If we are in an empty item, then we want to pop up out of the list, but // only if prefs say it's okay and if the parent isn't the active editing // host. bool isEmpty; nsresult rv = IsEmptyBlock(aListItem, &isEmpty, MozBRCounts::no); NS_ENSURE_SUCCESS(rv, rv); if (isEmpty && root != list && mReturnInEmptyLIKillsList) { // Get the list offset now -- before we might eventually split the list nsCOMPtr<nsINode> listParent = list->GetParentNode(); int32_t offset = listParent ? listParent->IndexOf(list) : -1; // Are we the last list item in the list? bool isLast; rv = htmlEditor->IsLastEditableChild(aListItem.AsDOMNode(), &isLast); NS_ENSURE_SUCCESS(rv, rv); if (!isLast) { // We need to split the list! ErrorResult rv; htmlEditor->SplitNode(*list, itemOffset, rv); NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult()); } // Are we in a sublist? if (HTMLEditUtils::IsList(listParent)) { // If so, move item out of this list and into the grandparent list rv = htmlEditor->MoveNode(&aListItem, listParent, offset + 1); NS_ENSURE_SUCCESS(rv, rv); rv = aSelection.Collapse(&aListItem, 0); NS_ENSURE_SUCCESS(rv, rv); } else { // Otherwise kill this item rv = htmlEditor->DeleteNode(&aListItem); NS_ENSURE_SUCCESS(rv, rv); // Time to insert a paragraph nsCOMPtr<Element> pNode = htmlEditor->CreateNode(nsGkAtoms::p, listParent, offset + 1); NS_ENSURE_STATE(pNode); // Append a <br> to it nsCOMPtr<Element> brNode = htmlEditor->CreateBR(pNode, 0); NS_ENSURE_STATE(brNode); // Set selection to before the break rv = aSelection.Collapse(pNode, 0); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } // Else we want a new list item at the same list level. Get ws code to // adjust any ws. nsCOMPtr<nsINode> selNode = &aNode; rv = WSRunObject::PrepareToSplitAcrossBlocks(htmlEditor, address_of(selNode), &aOffset); NS_ENSURE_SUCCESS(rv, rv); // Now split list item NS_ENSURE_STATE(selNode->IsContent()); htmlEditor->SplitNodeDeep(aListItem, *selNode->AsContent(), aOffset); // Hack: until I can change the damaged doc range code back to being // extra-inclusive, I have to manually detect certain list items that may be // left empty. nsCOMPtr<nsIContent> prevItem = htmlEditor->GetPriorHTMLSibling(&aListItem); if (prevItem && HTMLEditUtils::IsListItem(prevItem)) { bool isEmptyNode; rv = htmlEditor->IsEmptyNode(prevItem, &isEmptyNode); NS_ENSURE_SUCCESS(rv, rv); if (isEmptyNode) { rv = CreateMozBR(prevItem->AsDOMNode(), 0); NS_ENSURE_SUCCESS(rv, rv); } else { rv = htmlEditor->IsEmptyNode(&aListItem, &isEmptyNode, true); NS_ENSURE_SUCCESS(rv, rv); if (isEmptyNode) { nsCOMPtr<nsIAtom> nodeAtom = aListItem.NodeInfo()->NameAtom(); if (nodeAtom == nsGkAtoms::dd || nodeAtom == nsGkAtoms::dt) { nsCOMPtr<nsINode> list = aListItem.GetParentNode(); int32_t itemOffset = list ? list->IndexOf(&aListItem) : -1; nsIAtom* listAtom = nodeAtom == nsGkAtoms::dt ? nsGkAtoms::dd : nsGkAtoms::dt; nsCOMPtr<Element> newListItem = htmlEditor->CreateNode(listAtom, list, itemOffset + 1); NS_ENSURE_STATE(newListItem); rv = mTextEditor->DeleteNode(&aListItem); NS_ENSURE_SUCCESS(rv, rv); rv = aSelection.Collapse(newListItem, 0); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsCOMPtr<Element> brNode; rv = htmlEditor->CopyLastEditableChildStyles(GetAsDOMNode(prevItem), GetAsDOMNode(&aListItem), getter_AddRefs(brNode)); NS_ENSURE_SUCCESS(rv, rv); if (brNode) { nsCOMPtr<nsINode> brParent = brNode->GetParentNode(); int32_t offset = brParent ? brParent->IndexOf(brNode) : -1; rv = aSelection.Collapse(brParent, offset); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } } else { WSRunObject wsObj(htmlEditor, &aListItem, 0); nsCOMPtr<nsINode> visNode; int32_t visOffset = 0; WSType wsType; wsObj.NextVisibleNode(&aListItem, 0, address_of(visNode), &visOffset, &wsType); if (wsType == WSType::special || wsType == WSType::br || visNode->IsHTMLElement(nsGkAtoms::hr)) { nsCOMPtr<nsINode> parent = visNode->GetParentNode(); int32_t offset = parent ? parent->IndexOf(visNode) : -1; rv = aSelection.Collapse(parent, offset); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } else { rv = aSelection.Collapse(visNode, visOffset); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } } } } rv = aSelection.Collapse(&aListItem, 0); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } /** * MakeBlockquote() puts the list of nodes into one or more blockquotes. */ nsresult HTMLEditRules::MakeBlockquote(nsTArray<OwningNonNull<nsINode>>& aNodeArray) { // The idea here is to put the nodes into a minimal number of blockquotes. // When the user blockquotes something, they expect one blockquote. That may // not be possible (for instance, if they have two table cells selected, you // need two blockquotes inside the cells). nsCOMPtr<Element> curBlock; nsCOMPtr<nsINode> prevParent; for (auto& curNode : aNodeArray) { // Get the node to act on, and its location NS_ENSURE_STATE(curNode->IsContent()); // If the node is a table element or list item, dive inside if (HTMLEditUtils::IsTableElementButNotTable(curNode) || HTMLEditUtils::IsListItem(curNode)) { // Forget any previous block curBlock = nullptr; // Recursion time nsTArray<OwningNonNull<nsINode>> childArray; GetChildNodesForOperation(*curNode, childArray); nsresult rv = MakeBlockquote(childArray); NS_ENSURE_SUCCESS(rv, rv); } // If the node has different parent than previous node, further nodes in a // new parent if (prevParent) { if (prevParent != curNode->GetParentNode()) { // Forget any previous blockquote node we were using curBlock = nullptr; prevParent = curNode->GetParentNode(); } } else { prevParent = curNode->GetParentNode(); } // If no curBlock, make one if (!curBlock) { nsCOMPtr<nsINode> curParent = curNode->GetParentNode(); int32_t offset = curParent ? curParent->IndexOf(curNode) : -1; nsresult rv = SplitAsNeeded(*nsGkAtoms::blockquote, curParent, offset); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); curBlock = mHTMLEditor->CreateNode(nsGkAtoms::blockquote, curParent, offset); NS_ENSURE_STATE(curBlock); // remember our new block for postprocessing mNewBlock = curBlock; // note: doesn't matter if we set mNewBlock multiple times. } NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->MoveNode(curNode->AsContent(), curBlock, -1); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } /** * RemoveBlockStyle() makes the nodes have no special block type. */ nsresult HTMLEditRules::RemoveBlockStyle(nsTArray<OwningNonNull<nsINode>>& aNodeArray) { NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); // Intent of this routine is to be used for converting to/from headers, // paragraphs, pre, and address. Those blocks that pretty much just contain // inline things... nsCOMPtr<Element> curBlock; nsCOMPtr<nsIContent> firstNode, lastNode; for (auto& curNode : aNodeArray) { // If curNode is a address, p, header, address, or pre, remove it if (HTMLEditUtils::IsFormatNode(curNode)) { // Process any partial progress saved if (curBlock) { nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode); NS_ENSURE_SUCCESS(rv, rv); firstNode = lastNode = curBlock = nullptr; } // Remove current block nsresult rv = htmlEditor->RemoveBlockContainer(*curNode->AsContent()); NS_ENSURE_SUCCESS(rv, rv); } else if (curNode->IsAnyOfHTMLElements(nsGkAtoms::table, nsGkAtoms::tr, nsGkAtoms::tbody, nsGkAtoms::td, nsGkAtoms::li, nsGkAtoms::blockquote, nsGkAtoms::div) || HTMLEditUtils::IsList(curNode)) { // Process any partial progress saved if (curBlock) { nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode); NS_ENSURE_SUCCESS(rv, rv); firstNode = lastNode = curBlock = nullptr; } // Recursion time nsTArray<OwningNonNull<nsINode>> childArray; GetChildNodesForOperation(*curNode, childArray); nsresult rv = RemoveBlockStyle(childArray); NS_ENSURE_SUCCESS(rv, rv); } else if (IsInlineNode(curNode)) { if (curBlock) { // If so, is this node a descendant? if (EditorUtils::IsDescendantOf(curNode, curBlock)) { // Then we don't need to do anything different for this node lastNode = curNode->AsContent(); continue; } // Otherwise, we have progressed beyond end of curBlock, so let's // handle it now. We need to remove the portion of curBlock that // contains [firstNode - lastNode]. nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode); NS_ENSURE_SUCCESS(rv, rv); firstNode = lastNode = curBlock = nullptr; // Fall out and handle curNode } curBlock = htmlEditor->GetBlockNodeParent(curNode); if (curBlock && HTMLEditUtils::IsFormatNode(curBlock)) { firstNode = lastNode = curNode->AsContent(); } else { // Not a block kind that we care about. curBlock = nullptr; } } else if (curBlock) { // Some node that is already sans block style. Skip over it and process // any partial progress saved. nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode); NS_ENSURE_SUCCESS(rv, rv); firstNode = lastNode = curBlock = nullptr; } } // Process any partial progress saved if (curBlock) { nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode); NS_ENSURE_SUCCESS(rv, rv); firstNode = lastNode = curBlock = nullptr; } return NS_OK; } /** * ApplyBlockStyle() does whatever it takes to make the list of nodes into one * or more blocks of type aBlockTag. */ nsresult HTMLEditRules::ApplyBlockStyle(nsTArray<OwningNonNull<nsINode>>& aNodeArray, nsIAtom& aBlockTag) { // Intent of this routine is to be used for converting to/from headers, // paragraphs, pre, and address. Those blocks that pretty much just contain // inline things... NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); // Remove all non-editable nodes. Leave them be. for (int32_t i = aNodeArray.Length() - 1; i >= 0; i--) { if (!htmlEditor->IsEditable(aNodeArray[i])) { aNodeArray.RemoveElementAt(i); } } nsCOMPtr<Element> newBlock; nsCOMPtr<Element> curBlock; for (auto& curNode : aNodeArray) { nsCOMPtr<nsINode> curParent = curNode->GetParentNode(); int32_t offset = curParent ? curParent->IndexOf(curNode) : -1; // Is it already the right kind of block? if (curNode->IsHTMLElement(&aBlockTag)) { // Forget any previous block used for previous inline nodes curBlock = nullptr; // Do nothing to this block continue; } // If curNode is a address, p, header, address, or pre, replace it with a // new block of correct type. // XXX: pre can't hold everything the others can if (HTMLEditUtils::IsMozDiv(curNode) || HTMLEditUtils::IsFormatNode(curNode)) { // Forget any previous block used for previous inline nodes curBlock = nullptr; newBlock = htmlEditor->ReplaceContainer(curNode->AsElement(), &aBlockTag, nullptr, nullptr, EditorBase::eCloneAttributes); NS_ENSURE_STATE(newBlock); } else if (HTMLEditUtils::IsTable(curNode) || HTMLEditUtils::IsList(curNode) || curNode->IsAnyOfHTMLElements(nsGkAtoms::tbody, nsGkAtoms::tr, nsGkAtoms::td, nsGkAtoms::li, nsGkAtoms::blockquote, nsGkAtoms::div)) { // Forget any previous block used for previous inline nodes curBlock = nullptr; // Recursion time nsTArray<OwningNonNull<nsINode>> childArray; GetChildNodesForOperation(*curNode, childArray); if (!childArray.IsEmpty()) { nsresult rv = ApplyBlockStyle(childArray, aBlockTag); NS_ENSURE_SUCCESS(rv, rv); } else { // Make sure we can put a block here nsresult rv = SplitAsNeeded(aBlockTag, curParent, offset); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr<Element> theBlock = htmlEditor->CreateNode(&aBlockTag, curParent, offset); NS_ENSURE_STATE(theBlock); // Remember our new block for postprocessing mNewBlock = theBlock; } } else if (curNode->IsHTMLElement(nsGkAtoms::br)) { // If the node is a break, we honor it by putting further nodes in a new // parent if (curBlock) { // Forget any previous block used for previous inline nodes curBlock = nullptr; nsresult rv = htmlEditor->DeleteNode(curNode); NS_ENSURE_SUCCESS(rv, rv); } else { // The break is the first (or even only) node we encountered. Create a // block for it. nsresult rv = SplitAsNeeded(aBlockTag, curParent, offset); NS_ENSURE_SUCCESS(rv, rv); curBlock = htmlEditor->CreateNode(&aBlockTag, curParent, offset); NS_ENSURE_STATE(curBlock); // Remember our new block for postprocessing mNewBlock = curBlock; // Note: doesn't matter if we set mNewBlock multiple times. rv = htmlEditor->MoveNode(curNode->AsContent(), curBlock, -1); NS_ENSURE_SUCCESS(rv, rv); } } else if (IsInlineNode(curNode)) { // If curNode is inline, pull it into curBlock. Note: it's assumed that // consecutive inline nodes in aNodeArray are actually members of the // same block parent. This happens to be true now as a side effect of // how aNodeArray is contructed, but some additional logic should be // added here if that should change // // If curNode is a non editable, drop it if we are going to <pre>. if (&aBlockTag == nsGkAtoms::pre && !htmlEditor->IsEditable(curNode)) { // Do nothing to this block continue; } // If no curBlock, make one if (!curBlock) { nsresult rv = SplitAsNeeded(aBlockTag, curParent, offset); NS_ENSURE_SUCCESS(rv, rv); curBlock = htmlEditor->CreateNode(&aBlockTag, curParent, offset); NS_ENSURE_STATE(curBlock); // Remember our new block for postprocessing mNewBlock = curBlock; // Note: doesn't matter if we set mNewBlock multiple times. } // XXX If curNode is a br, replace it with a return if going to <pre> // This is a continuation of some inline nodes that belong together in // the same block item. Use curBlock. nsresult rv = htmlEditor->MoveNode(curNode->AsContent(), curBlock, -1); NS_ENSURE_SUCCESS(rv, rv); } } return NS_OK; } /** * Given a tag name, split inOutParent up to the point where we can insert the * tag. Adjust inOutParent and inOutOffset to point to new location for tag. */ nsresult HTMLEditRules::SplitAsNeeded(nsIAtom& aTag, OwningNonNull<nsINode>& aInOutParent, int32_t& aInOutOffset) { // XXX Is there a better way to do this? nsCOMPtr<nsINode> parent = aInOutParent.forget(); nsresult rv = SplitAsNeeded(aTag, parent, aInOutOffset); aInOutParent = parent.forget(); return rv; } nsresult HTMLEditRules::SplitAsNeeded(nsIAtom& aTag, nsCOMPtr<nsINode>& inOutParent, int32_t& inOutOffset) { NS_ENSURE_TRUE(inOutParent, NS_ERROR_NULL_POINTER); // Check that we have a place that can legally contain the tag nsCOMPtr<nsINode> tagParent, splitNode; for (nsCOMPtr<nsINode> parent = inOutParent; parent; parent = parent->GetParentNode()) { // Sniffing up the parent tree until we find a legal place for the block // Don't leave the active editing host NS_ENSURE_STATE(mHTMLEditor); if (!mHTMLEditor->IsDescendantOfEditorRoot(parent)) { // XXX Why do we need to check mHTMLEditor again here? NS_ENSURE_STATE(mHTMLEditor); if (parent != mHTMLEditor->GetActiveEditingHost()) { return NS_ERROR_FAILURE; } } NS_ENSURE_STATE(mHTMLEditor); if (mHTMLEditor->CanContainTag(*parent, aTag)) { // Success tagParent = parent; break; } splitNode = parent; } if (!tagParent) { // Could not find a place to build tag! return NS_ERROR_FAILURE; } if (splitNode && splitNode->IsContent() && inOutParent->IsContent()) { // We found a place for block, but above inOutParent. We need to split. NS_ENSURE_STATE(mHTMLEditor); int32_t offset = mHTMLEditor->SplitNodeDeep(*splitNode->AsContent(), *inOutParent->AsContent(), inOutOffset); NS_ENSURE_STATE(offset != -1); inOutParent = tagParent; inOutOffset = offset; } return NS_OK; } /** * JoinNodesSmart: Join two nodes, doing whatever makes sense for their * children (which often means joining them, too). aNodeLeft & aNodeRight must * be same type of node. * * Returns the point where they're merged, or (nullptr, -1) on failure. */ EditorDOMPoint HTMLEditRules::JoinNodesSmart(nsIContent& aNodeLeft, nsIContent& aNodeRight) { // Caller responsible for left and right node being the same type nsCOMPtr<nsINode> parent = aNodeLeft.GetParentNode(); NS_ENSURE_TRUE(parent, EditorDOMPoint()); int32_t parOffset = parent->IndexOf(&aNodeLeft); nsCOMPtr<nsINode> rightParent = aNodeRight.GetParentNode(); // If they don't have the same parent, first move the right node to after the // left one if (parent != rightParent) { NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint()); nsresult rv = mHTMLEditor->MoveNode(&aNodeRight, parent, parOffset); NS_ENSURE_SUCCESS(rv, EditorDOMPoint()); } EditorDOMPoint ret(&aNodeRight, aNodeLeft.Length()); // Separate join rules for differing blocks if (HTMLEditUtils::IsList(&aNodeLeft) || aNodeLeft.GetAsText()) { // For lists, merge shallow (wouldn't want to combine list items) nsresult rv = mHTMLEditor->JoinNodes(aNodeLeft, aNodeRight); NS_ENSURE_SUCCESS(rv, EditorDOMPoint()); return ret; } // Remember the last left child, and first right child NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint()); nsCOMPtr<nsIContent> lastLeft = mHTMLEditor->GetLastEditableChild(aNodeLeft); NS_ENSURE_TRUE(lastLeft, EditorDOMPoint()); NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint()); nsCOMPtr<nsIContent> firstRight = mHTMLEditor->GetFirstEditableChild(aNodeRight); NS_ENSURE_TRUE(firstRight, EditorDOMPoint()); // For list items, divs, etc., merge smart NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint()); nsresult rv = mHTMLEditor->JoinNodes(aNodeLeft, aNodeRight); NS_ENSURE_SUCCESS(rv, EditorDOMPoint()); if (lastLeft && firstRight && mHTMLEditor && mHTMLEditor->AreNodesSameType(lastLeft, firstRight) && (lastLeft->GetAsText() || !mHTMLEditor || (lastLeft->IsElement() && firstRight->IsElement() && mHTMLEditor->mCSSEditUtils->ElementsSameStyle(lastLeft->AsElement(), firstRight->AsElement())))) { NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint()); return JoinNodesSmart(*lastLeft, *firstRight); } return ret; } Element* HTMLEditRules::GetTopEnclosingMailCite(nsINode& aNode) { nsCOMPtr<Element> ret; for (nsCOMPtr<nsINode> node = &aNode; node; node = node->GetParentNode()) { if ((IsPlaintextEditor() && node->IsHTMLElement(nsGkAtoms::pre)) || HTMLEditUtils::IsMailCite(node)) { ret = node->AsElement(); } if (node->IsHTMLElement(nsGkAtoms::body)) { break; } } return ret; } nsresult HTMLEditRules::CacheInlineStyles(nsIDOMNode* aNode) { NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER); NS_ENSURE_STATE(mHTMLEditor); bool useCSS = mHTMLEditor->IsCSSEnabled(); for (int32_t j = 0; j < SIZE_STYLE_TABLE; ++j) { // If type-in state is set, don't intervene bool typeInSet, unused; if (NS_WARN_IF(!mHTMLEditor)) { return NS_ERROR_UNEXPECTED; } mHTMLEditor->mTypeInState->GetTypingState(typeInSet, unused, mCachedStyles[j].tag, mCachedStyles[j].attr, nullptr); if (typeInSet) { continue; } bool isSet = false; nsAutoString outValue; // Don't use CSS for <font size>, we don't support it usefully (bug 780035) if (!useCSS || (mCachedStyles[j].tag == nsGkAtoms::font && mCachedStyles[j].attr.EqualsLiteral("size"))) { NS_ENSURE_STATE(mHTMLEditor); mHTMLEditor->IsTextPropertySetByContent(aNode, mCachedStyles[j].tag, &(mCachedStyles[j].attr), nullptr, isSet, &outValue); } else { NS_ENSURE_STATE(mHTMLEditor); mHTMLEditor->mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(aNode, mCachedStyles[j].tag, &(mCachedStyles[j].attr), isSet, outValue, CSSEditUtils::eComputed); } if (isSet) { mCachedStyles[j].mPresent = true; mCachedStyles[j].value.Assign(outValue); } } return NS_OK; } nsresult HTMLEditRules::ReapplyCachedStyles() { // The idea here is to examine our cached list of styles and see if any have // been removed. If so, add typeinstate for them, so that they will be // reinserted when new content is added. // remember if we are in css mode NS_ENSURE_STATE(mHTMLEditor); bool useCSS = mHTMLEditor->IsCSSEnabled(); // get selection point; if it doesn't exist, we have nothing to do NS_ENSURE_STATE(mHTMLEditor); RefPtr<Selection> selection = mHTMLEditor->GetSelection(); if (!selection) { // If the document is removed from its parent document during executing an // editor operation with DOMMutationEvent or something, there may be no // selection. return NS_OK; } if (!selection->RangeCount()) { // Nothing to do return NS_OK; } nsCOMPtr<nsIContent> selNode = do_QueryInterface(selection->GetRangeAt(0)->GetStartParent()); if (!selNode) { // Nothing to do return NS_OK; } for (int32_t i = 0; i < SIZE_STYLE_TABLE; ++i) { if (mCachedStyles[i].mPresent) { bool bFirst, bAny, bAll; bFirst = bAny = bAll = false; nsAutoString curValue; if (useCSS) { // check computed style first in css case NS_ENSURE_STATE(mHTMLEditor); bAny = mHTMLEditor->mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet( selNode, mCachedStyles[i].tag, &(mCachedStyles[i].attr), curValue, CSSEditUtils::eComputed); } if (!bAny) { // then check typeinstate and html style NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetInlinePropertyBase(*mCachedStyles[i].tag, &(mCachedStyles[i].attr), &(mCachedStyles[i].value), &bFirst, &bAny, &bAll, &curValue, false); NS_ENSURE_SUCCESS(rv, rv); } // this style has disappeared through deletion. Add to our typeinstate: if (!bAny || IsStyleCachePreservingAction(mTheAction)) { NS_ENSURE_STATE(mHTMLEditor); mHTMLEditor->mTypeInState->SetProp(mCachedStyles[i].tag, mCachedStyles[i].attr, mCachedStyles[i].value); } } } return NS_OK; } void HTMLEditRules::ClearCachedStyles() { // clear the mPresent bits in mCachedStyles array for (uint32_t j = 0; j < SIZE_STYLE_TABLE; j++) { mCachedStyles[j].mPresent = false; mCachedStyles[j].value.Truncate(); } } void HTMLEditRules::AdjustSpecialBreaks() { NS_ENSURE_TRUE_VOID(mHTMLEditor); // Gather list of empty nodes nsTArray<OwningNonNull<nsINode>> nodeArray; EmptyEditableFunctor functor(mHTMLEditor); DOMIterator iter; if (NS_WARN_IF(NS_FAILED(iter.Init(*mDocChangeRange)))) { return; } iter.AppendList(functor, nodeArray); // Put moz-br's into these empty li's and td's for (auto& node : nodeArray) { // Need to put br at END of node. It may have empty containers in it and // still pass the "IsEmptyNode" test, and we want the br's to be after // them. Also, we want the br to be after the selection if the selection // is in this node. nsresult rv = CreateMozBR(node->AsDOMNode(), (int32_t)node->Length()); if (NS_WARN_IF(NS_FAILED(rv))) { return; } } } nsresult HTMLEditRules::AdjustWhitespace(Selection* aSelection) { // get selection point nsCOMPtr<nsIDOMNode> selNode; int32_t selOffset; NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetStartNodeAndOffset(aSelection, getter_AddRefs(selNode), &selOffset); NS_ENSURE_SUCCESS(rv, rv); // ask whitespace object to tweak nbsp's NS_ENSURE_STATE(mHTMLEditor); return WSRunObject(mHTMLEditor, selNode, selOffset).AdjustWhitespace(); } nsresult HTMLEditRules::PinSelectionToNewBlock(Selection* aSelection) { NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER); if (!aSelection->Collapsed()) { return NS_OK; } // get the (collapsed) selection location nsCOMPtr<nsIDOMNode> selNode, temp; int32_t selOffset; NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetStartNodeAndOffset(aSelection, getter_AddRefs(selNode), &selOffset); NS_ENSURE_SUCCESS(rv, rv); temp = selNode; // use ranges and sRangeHelper to compare sel point to new block nsCOMPtr<nsINode> node = do_QueryInterface(selNode); NS_ENSURE_STATE(node); RefPtr<nsRange> range = new nsRange(node); rv = range->SetStart(selNode, selOffset); NS_ENSURE_SUCCESS(rv, rv); rv = range->SetEnd(selNode, selOffset); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr<nsIContent> block = mNewBlock.get(); NS_ENSURE_TRUE(block, NS_ERROR_NO_INTERFACE); bool nodeBefore, nodeAfter; rv = nsRange::CompareNodeToRange(block, range, &nodeBefore, &nodeAfter); NS_ENSURE_SUCCESS(rv, rv); if (nodeBefore && nodeAfter) { return NS_OK; // selection is inside block } else if (nodeBefore) { // selection is after block. put at end of block. nsCOMPtr<nsIDOMNode> tmp = GetAsDOMNode(mNewBlock); NS_ENSURE_STATE(mHTMLEditor); tmp = GetAsDOMNode(mHTMLEditor->GetLastEditableChild(*block)); uint32_t endPoint; if (mHTMLEditor->IsTextNode(tmp) || mHTMLEditor->IsContainer(tmp)) { rv = EditorBase::GetLengthOfDOMNode(tmp, endPoint); NS_ENSURE_SUCCESS(rv, rv); } else { tmp = EditorBase::GetNodeLocation(tmp, (int32_t*)&endPoint); endPoint++; // want to be after this node } return aSelection->Collapse(tmp, (int32_t)endPoint); } else { // selection is before block. put at start of block. nsCOMPtr<nsIDOMNode> tmp = GetAsDOMNode(mNewBlock); NS_ENSURE_STATE(mHTMLEditor); tmp = GetAsDOMNode(mHTMLEditor->GetFirstEditableChild(*block)); int32_t offset; if (mHTMLEditor->IsTextNode(tmp) || mHTMLEditor->IsContainer(tmp)) { tmp = EditorBase::GetNodeLocation(tmp, &offset); } return aSelection->Collapse(tmp, 0); } } void HTMLEditRules::CheckInterlinePosition(Selection& aSelection) { // If the selection isn't collapsed, do nothing. if (!aSelection.Collapsed()) { return; } NS_ENSURE_TRUE_VOID(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); // Get the (collapsed) selection location NS_ENSURE_TRUE_VOID(aSelection.GetRangeAt(0) && aSelection.GetRangeAt(0)->GetStartParent()); OwningNonNull<nsINode> selNode = *aSelection.GetRangeAt(0)->GetStartParent(); int32_t selOffset = aSelection.GetRangeAt(0)->StartOffset(); // First, let's check to see if we are after a <br>. We take care of this // special-case first so that we don't accidentally fall through into one of // the other conditionals. nsCOMPtr<nsIContent> node = htmlEditor->GetPriorHTMLNode(selNode, selOffset, true); if (node && node->IsHTMLElement(nsGkAtoms::br)) { aSelection.SetInterlinePosition(true); return; } // Are we after a block? If so try set caret to following content node = htmlEditor->GetPriorHTMLSibling(selNode, selOffset); if (node && IsBlockNode(*node)) { aSelection.SetInterlinePosition(true); return; } // Are we before a block? If so try set caret to prior content node = htmlEditor->GetNextHTMLSibling(selNode, selOffset); if (node && IsBlockNode(*node)) { aSelection.SetInterlinePosition(false); } } nsresult HTMLEditRules::AdjustSelection(Selection* aSelection, nsIEditor::EDirection aAction) { NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER); // if the selection isn't collapsed, do nothing. // moose: one thing to do instead is check for the case of // only a single break selected, and collapse it. Good thing? Beats me. if (!aSelection->Collapsed()) { return NS_OK; } // get the (collapsed) selection location nsCOMPtr<nsINode> selNode, temp; int32_t selOffset; NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetStartNodeAndOffset(aSelection, getter_AddRefs(selNode), &selOffset); NS_ENSURE_SUCCESS(rv, rv); temp = selNode; // are we in an editable node? NS_ENSURE_STATE(mHTMLEditor); while (!mHTMLEditor->IsEditable(selNode)) { // scan up the tree until we find an editable place to be selNode = EditorBase::GetNodeLocation(temp, &selOffset); NS_ENSURE_TRUE(selNode, NS_ERROR_FAILURE); temp = selNode; NS_ENSURE_STATE(mHTMLEditor); } // make sure we aren't in an empty block - user will see no cursor. If this // is happening, put a <br> in the block if allowed. NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<Element> theblock = mHTMLEditor->GetBlock(*selNode); if (theblock && mHTMLEditor->IsEditable(theblock)) { bool bIsEmptyNode; NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->IsEmptyNode(theblock, &bIsEmptyNode, false, false); NS_ENSURE_SUCCESS(rv, rv); // check if br can go into the destination node NS_ENSURE_STATE(mHTMLEditor); if (bIsEmptyNode && mHTMLEditor->CanContainTag(*selNode, *nsGkAtoms::br)) { NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<Element> rootNode = mHTMLEditor->GetRoot(); NS_ENSURE_TRUE(rootNode, NS_ERROR_FAILURE); if (selNode == rootNode) { // Our root node is completely empty. Don't add a <br> here. // AfterEditInner() will add one for us when it calls // CreateBogusNodeIfNeeded()! return NS_OK; } // we know we can skip the rest of this routine given the cirumstance return CreateMozBR(GetAsDOMNode(selNode), selOffset); } } // are we in a text node? nsCOMPtr<nsIDOMCharacterData> textNode = do_QueryInterface(selNode); if (textNode) return NS_OK; // we LIKE it when we are in a text node. that RULZ // do we need to insert a special mozBR? We do if we are: // 1) prior node is in same block where selection is AND // 2) prior node is a br AND // 3) that br is not visible NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<nsIContent> nearNode = mHTMLEditor->GetPriorHTMLNode(selNode, selOffset); if (nearNode) { // is nearNode also a descendant of same block? NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<Element> block = mHTMLEditor->GetBlock(*selNode); nsCOMPtr<Element> nearBlock = mHTMLEditor->GetBlockNodeParent(nearNode); if (block && block == nearBlock) { if (nearNode && TextEditUtils::IsBreak(nearNode)) { NS_ENSURE_STATE(mHTMLEditor); if (!mHTMLEditor->IsVisBreak(nearNode)) { // need to insert special moz BR. Why? Because if we don't // the user will see no new line for the break. Also, things // like table cells won't grow in height. nsCOMPtr<nsIDOMNode> brNode; rv = CreateMozBR(GetAsDOMNode(selNode), selOffset, getter_AddRefs(brNode)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr<nsIDOMNode> brParent = EditorBase::GetNodeLocation(brNode, &selOffset); // selection stays *before* moz-br, sticking to it aSelection->SetInterlinePosition(true); rv = aSelection->Collapse(brParent, selOffset); NS_ENSURE_SUCCESS(rv, rv); } else { NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<nsIContent> nextNode = mHTMLEditor->GetNextHTMLNode(nearNode, true); if (nextNode && TextEditUtils::IsMozBR(nextNode)) { // selection between br and mozbr. make it stick to mozbr // so that it will be on blank line. aSelection->SetInterlinePosition(true); } } } } } // we aren't in a textnode: are we adjacent to text or a break or an image? NS_ENSURE_STATE(mHTMLEditor); nearNode = mHTMLEditor->GetPriorHTMLNode(selNode, selOffset, true); if (nearNode && (TextEditUtils::IsBreak(nearNode) || EditorBase::IsTextNode(nearNode) || HTMLEditUtils::IsImage(nearNode) || nearNode->IsHTMLElement(nsGkAtoms::hr))) { // this is a good place for the caret to be return NS_OK; } NS_ENSURE_STATE(mHTMLEditor); nearNode = mHTMLEditor->GetNextHTMLNode(selNode, selOffset, true); if (nearNode && (TextEditUtils::IsBreak(nearNode) || EditorBase::IsTextNode(nearNode) || nearNode->IsAnyOfHTMLElements(nsGkAtoms::img, nsGkAtoms::hr))) { return NS_OK; // this is a good place for the caret to be } // look for a nearby text node. // prefer the correct direction. nsCOMPtr<nsIDOMNode> nearNodeDOM = GetAsDOMNode(nearNode); rv = FindNearSelectableNode(GetAsDOMNode(selNode), selOffset, aAction, address_of(nearNodeDOM)); NS_ENSURE_SUCCESS(rv, rv); nearNode = do_QueryInterface(nearNodeDOM); if (!nearNode) { return NS_OK; } EditorDOMPoint pt = GetGoodSelPointForNode(*nearNode, aAction); rv = aSelection->Collapse(pt.node, pt.offset); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult HTMLEditRules::FindNearSelectableNode(nsIDOMNode* aSelNode, int32_t aSelOffset, nsIEditor::EDirection& aDirection, nsCOMPtr<nsIDOMNode>* outSelectableNode) { NS_ENSURE_TRUE(aSelNode && outSelectableNode, NS_ERROR_NULL_POINTER); *outSelectableNode = nullptr; nsCOMPtr<nsIDOMNode> nearNode, curNode; if (aDirection == nsIEditor::ePrevious) { NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetPriorHTMLNode(aSelNode, aSelOffset, address_of(nearNode)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetNextHTMLNode(aSelNode, aSelOffset, address_of(nearNode)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } // Try the other direction then. if (!nearNode) { if (aDirection == nsIEditor::ePrevious) { aDirection = nsIEditor::eNext; } else { aDirection = nsIEditor::ePrevious; } if (aDirection == nsIEditor::ePrevious) { NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetPriorHTMLNode(aSelNode, aSelOffset, address_of(nearNode)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetNextHTMLNode(aSelNode, aSelOffset, address_of(nearNode)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } // scan in the right direction until we find an eligible text node, // but don't cross any breaks, images, or table elements. NS_ENSURE_STATE(mHTMLEditor); while (nearNode && !(mHTMLEditor->IsTextNode(nearNode) || TextEditUtils::IsBreak(nearNode) || HTMLEditUtils::IsImage(nearNode))) { curNode = nearNode; if (aDirection == nsIEditor::ePrevious) { NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetPriorHTMLNode(curNode, address_of(nearNode)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetNextHTMLNode(curNode, address_of(nearNode)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } NS_ENSURE_STATE(mHTMLEditor); } if (nearNode) { // don't cross any table elements if (InDifferentTableElements(nearNode, aSelNode)) { return NS_OK; } // otherwise, ok, we have found a good spot to put the selection *outSelectableNode = do_QueryInterface(nearNode); } return NS_OK; } bool HTMLEditRules::InDifferentTableElements(nsIDOMNode* aNode1, nsIDOMNode* aNode2) { nsCOMPtr<nsINode> node1 = do_QueryInterface(aNode1); nsCOMPtr<nsINode> node2 = do_QueryInterface(aNode2); return InDifferentTableElements(node1, node2); } bool HTMLEditRules::InDifferentTableElements(nsINode* aNode1, nsINode* aNode2) { MOZ_ASSERT(aNode1 && aNode2); while (aNode1 && !HTMLEditUtils::IsTableElement(aNode1)) { aNode1 = aNode1->GetParentNode(); } while (aNode2 && !HTMLEditUtils::IsTableElement(aNode2)) { aNode2 = aNode2->GetParentNode(); } return aNode1 != aNode2; } nsresult HTMLEditRules::RemoveEmptyNodes() { NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); // Some general notes on the algorithm used here: the goal is to examine all // the nodes in mDocChangeRange, and remove the empty ones. We do this by // using a content iterator to traverse all the nodes in the range, and // placing the empty nodes into an array. After finishing the iteration, we // delete the empty nodes in the array. (They cannot be deleted as we find // them because that would invalidate the iterator.) // // Since checking to see if a node is empty can be costly for nodes with many // descendants, there are some optimizations made. I rely on the fact that // the iterator is post-order: it will visit children of a node before // visiting the parent node. So if I find that a child node is not empty, I // know that its parent is not empty without even checking. So I put the // parent on a "skipList" which is just a voidArray of nodes I can skip the // empty check on. If I encounter a node on the skiplist, i skip the // processing for that node and replace its slot in the skiplist with that // node's parent. // // An interesting idea is to go ahead and regard parent nodes that are NOT on // the skiplist as being empty (without even doing the IsEmptyNode check) on // the theory that if they weren't empty, we would have encountered a // non-empty child earlier and thus put this parent node on the skiplist. // // Unfortunately I can't use that strategy here, because the range may // include some children of a node while excluding others. Thus I could find // all the _examined_ children empty, but still not have an empty parent. // need an iterator nsCOMPtr<nsIContentIterator> iter = NS_NewContentIterator(); nsresult rv = iter->Init(mDocChangeRange); NS_ENSURE_SUCCESS(rv, rv); nsTArray<OwningNonNull<nsINode>> arrayOfEmptyNodes, arrayOfEmptyCites, skipList; // Check for empty nodes while (!iter->IsDone()) { OwningNonNull<nsINode> node = *iter->GetCurrentNode(); nsCOMPtr<nsINode> parent = node->GetParentNode(); size_t idx = skipList.IndexOf(node); if (idx != skipList.NoIndex) { // This node is on our skip list. Skip processing for this node, and // replace its value in the skip list with the value of its parent if (parent) { skipList[idx] = parent; } } else { bool bIsCandidate = false; bool bIsEmptyNode = false; bool bIsMailCite = false; if (node->IsElement()) { if (node->IsHTMLElement(nsGkAtoms::body)) { // Don't delete the body } else if ((bIsMailCite = HTMLEditUtils::IsMailCite(node)) || node->IsHTMLElement(nsGkAtoms::a) || HTMLEditUtils::IsInlineStyle(node) || HTMLEditUtils::IsList(node) || node->IsHTMLElement(nsGkAtoms::div)) { // Only consider certain nodes to be empty for purposes of removal bIsCandidate = true; } else if (HTMLEditUtils::IsFormatNode(node) || HTMLEditUtils::IsListItem(node) || node->IsHTMLElement(nsGkAtoms::blockquote)) { // These node types are candidates if selection is not in them. If // it is one of these, don't delete if selection inside. This is so // we can create empty headings, etc., for the user to type into. bool bIsSelInNode; rv = SelectionEndpointInNode(node, &bIsSelInNode); NS_ENSURE_SUCCESS(rv, rv); if (!bIsSelInNode) { bIsCandidate = true; } } } if (bIsCandidate) { // We delete mailcites even if they have a solo br in them. Other // nodes we require to be empty. rv = htmlEditor->IsEmptyNode(node->AsDOMNode(), &bIsEmptyNode, bIsMailCite, true); NS_ENSURE_SUCCESS(rv, rv); if (bIsEmptyNode) { if (bIsMailCite) { // mailcites go on a separate list from other empty nodes arrayOfEmptyCites.AppendElement(*node); } else { arrayOfEmptyNodes.AppendElement(*node); } } } if (!bIsEmptyNode && parent) { // put parent on skip list skipList.AppendElement(*parent); } } iter->Next(); } // now delete the empty nodes for (auto& delNode : arrayOfEmptyNodes) { if (htmlEditor->IsModifiableNode(delNode)) { rv = htmlEditor->DeleteNode(delNode); NS_ENSURE_SUCCESS(rv, rv); } } // Now delete the empty mailcites. This is a separate step because we want // to pull out any br's and preserve them. for (auto& delNode : arrayOfEmptyCites) { bool bIsEmptyNode; rv = htmlEditor->IsEmptyNode(delNode, &bIsEmptyNode, false, true); NS_ENSURE_SUCCESS(rv, rv); if (!bIsEmptyNode) { // We are deleting a cite that has just a br. We want to delete cite, // but preserve br. nsCOMPtr<nsINode> parent = delNode->GetParentNode(); int32_t offset = parent ? parent->IndexOf(delNode) : -1; nsCOMPtr<Element> br = htmlEditor->CreateBR(parent, offset); NS_ENSURE_STATE(br); } rv = htmlEditor->DeleteNode(delNode); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } nsresult HTMLEditRules::SelectionEndpointInNode(nsINode* aNode, bool* aResult) { NS_ENSURE_TRUE(aNode && aResult, NS_ERROR_NULL_POINTER); nsIDOMNode* node = aNode->AsDOMNode(); *aResult = false; NS_ENSURE_STATE(mHTMLEditor); RefPtr<Selection> selection = mHTMLEditor->GetSelection(); NS_ENSURE_STATE(selection); uint32_t rangeCount = selection->RangeCount(); for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) { RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx); nsCOMPtr<nsIDOMNode> startParent, endParent; range->GetStartContainer(getter_AddRefs(startParent)); if (startParent) { if (node == startParent) { *aResult = true; return NS_OK; } if (EditorUtils::IsDescendantOf(startParent, node)) { *aResult = true; return NS_OK; } } range->GetEndContainer(getter_AddRefs(endParent)); if (startParent == endParent) { continue; } if (endParent) { if (node == endParent) { *aResult = true; return NS_OK; } if (EditorUtils::IsDescendantOf(endParent, node)) { *aResult = true; return NS_OK; } } } return NS_OK; } /** * IsEmptyInline: Return true if aNode is an empty inline container */ bool HTMLEditRules::IsEmptyInline(nsINode& aNode) { NS_ENSURE_TRUE(mHTMLEditor, false); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); if (IsInlineNode(aNode) && htmlEditor->IsContainer(&aNode)) { bool isEmpty = true; htmlEditor->IsEmptyNode(&aNode, &isEmpty); return isEmpty; } return false; } bool HTMLEditRules::ListIsEmptyLine(nsTArray<OwningNonNull<nsINode>>& aArrayOfNodes) { // We have a list of nodes which we are candidates for being moved into a new // block. Determine if it's anything more than a blank line. Look for // editable content above and beyond one single BR. NS_ENSURE_TRUE(aArrayOfNodes.Length(), true); NS_ENSURE_TRUE(mHTMLEditor, false); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); int32_t brCount = 0; for (auto& node : aArrayOfNodes) { if (!htmlEditor->IsEditable(node)) { continue; } if (TextEditUtils::IsBreak(node)) { // First break doesn't count if (brCount) { return false; } brCount++; } else if (IsEmptyInline(node)) { // Empty inline, keep looking } else { return false; } } return true; } nsresult HTMLEditRules::PopListItem(nsIDOMNode* aListItem, bool* aOutOfList) { nsCOMPtr<Element> listItem = do_QueryInterface(aListItem); // check parms NS_ENSURE_TRUE(listItem && aOutOfList, NS_ERROR_NULL_POINTER); // init out params *aOutOfList = false; nsCOMPtr<nsINode> curParent = listItem->GetParentNode(); int32_t offset = curParent ? curParent->IndexOf(listItem) : -1; if (!HTMLEditUtils::IsListItem(listItem)) { return NS_ERROR_FAILURE; } // if it's first or last list item, don't need to split the list // otherwise we do. nsCOMPtr<nsINode> curParPar = curParent->GetParentNode(); int32_t parOffset = curParPar ? curParPar->IndexOf(curParent) : -1; bool bIsFirstListItem; NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->IsFirstEditableChild(aListItem, &bIsFirstListItem); NS_ENSURE_SUCCESS(rv, rv); bool bIsLastListItem; NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->IsLastEditableChild(aListItem, &bIsLastListItem); NS_ENSURE_SUCCESS(rv, rv); if (!bIsFirstListItem && !bIsLastListItem) { // split the list nsCOMPtr<nsIDOMNode> newBlock; NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->SplitNode(GetAsDOMNode(curParent), offset, getter_AddRefs(newBlock)); NS_ENSURE_SUCCESS(rv, rv); } if (!bIsFirstListItem) { parOffset++; } NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->MoveNode(listItem, curParPar, parOffset); NS_ENSURE_SUCCESS(rv, rv); // unwrap list item contents if they are no longer in a list if (!HTMLEditUtils::IsList(curParPar) && HTMLEditUtils::IsListItem(listItem)) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->RemoveBlockContainer(*listItem); NS_ENSURE_SUCCESS(rv, rv); *aOutOfList = true; } return NS_OK; } nsresult HTMLEditRules::RemoveListStructure(Element& aList) { NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); while (aList.GetFirstChild()) { OwningNonNull<nsIContent> child = *aList.GetFirstChild(); if (HTMLEditUtils::IsListItem(child)) { bool isOutOfList; // Keep popping it out until it's not in a list anymore do { nsresult rv = PopListItem(child->AsDOMNode(), &isOutOfList); NS_ENSURE_SUCCESS(rv, rv); } while (!isOutOfList); } else if (HTMLEditUtils::IsList(child)) { nsresult rv = RemoveListStructure(*child->AsElement()); NS_ENSURE_SUCCESS(rv, rv); } else { // Delete any non-list items for now nsresult rv = htmlEditor->DeleteNode(child); NS_ENSURE_SUCCESS(rv, rv); } } // Delete the now-empty list nsresult rv = htmlEditor->RemoveBlockContainer(aList); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult HTMLEditRules::ConfirmSelectionInBody() { // get the body NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<nsIDOMElement> rootElement = do_QueryInterface(mHTMLEditor->GetRoot()); NS_ENSURE_TRUE(rootElement, NS_ERROR_UNEXPECTED); // get the selection NS_ENSURE_STATE(mHTMLEditor); RefPtr<Selection> selection = mHTMLEditor->GetSelection(); NS_ENSURE_STATE(selection); // get the selection start location nsCOMPtr<nsIDOMNode> selNode, temp, parent; int32_t selOffset; NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetStartNodeAndOffset(selection, getter_AddRefs(selNode), &selOffset); if (NS_FAILED(rv)) { return rv; } temp = selNode; // check that selNode is inside body while (temp && !TextEditUtils::IsBody(temp)) { temp->GetParentNode(getter_AddRefs(parent)); temp = parent; } // if we aren't in the body, force the issue if (!temp) { // uncomment this to see when we get bad selections // NS_NOTREACHED("selection not in body"); selection->Collapse(rootElement, 0); } // get the selection end location NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->GetEndNodeAndOffset(selection, getter_AddRefs(selNode), &selOffset); NS_ENSURE_SUCCESS(rv, rv); temp = selNode; // check that selNode is inside body while (temp && !TextEditUtils::IsBody(temp)) { rv = temp->GetParentNode(getter_AddRefs(parent)); temp = parent; } // if we aren't in the body, force the issue if (!temp) { // uncomment this to see when we get bad selections // NS_NOTREACHED("selection not in body"); selection->Collapse(rootElement, 0); } // XXX This is the result of the last call of GetParentNode(), it doesn't // make sense... return rv; } nsresult HTMLEditRules::UpdateDocChangeRange(nsRange* aRange) { // first make sure aRange is in the document. It might not be if // portions of our editting action involved manipulating nodes // prior to placing them in the document (e.g., populating a list item // before placing it in its list) nsCOMPtr<nsIDOMNode> startNode; nsresult rv = aRange->GetStartContainer(getter_AddRefs(startNode)); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); if (!mHTMLEditor->IsDescendantOfRoot(startNode)) { // just return - we don't need to adjust mDocChangeRange in this case return NS_OK; } if (!mDocChangeRange) { // clone aRange. mDocChangeRange = aRange->CloneRange(); } else { int16_t result; // compare starts of ranges rv = mDocChangeRange->CompareBoundaryPoints(nsIDOMRange::START_TO_START, aRange, &result); if (rv == NS_ERROR_NOT_INITIALIZED) { // This will happen is mDocChangeRange is non-null, but the range is // uninitialized. In this case we'll set the start to aRange start. // The same test won't be needed further down since after we've set // the start the range will be collapsed to that point. result = 1; rv = NS_OK; } NS_ENSURE_SUCCESS(rv, rv); // Positive result means mDocChangeRange start is after aRange start. if (result > 0) { int32_t startOffset; rv = aRange->GetStartOffset(&startOffset); NS_ENSURE_SUCCESS(rv, rv); rv = mDocChangeRange->SetStart(startNode, startOffset); NS_ENSURE_SUCCESS(rv, rv); } // compare ends of ranges rv = mDocChangeRange->CompareBoundaryPoints(nsIDOMRange::END_TO_END, aRange, &result); NS_ENSURE_SUCCESS(rv, rv); // Negative result means mDocChangeRange end is before aRange end. if (result < 0) { nsCOMPtr<nsIDOMNode> endNode; int32_t endOffset; rv = aRange->GetEndContainer(getter_AddRefs(endNode)); NS_ENSURE_SUCCESS(rv, rv); rv = aRange->GetEndOffset(&endOffset); NS_ENSURE_SUCCESS(rv, rv); rv = mDocChangeRange->SetEnd(endNode, endOffset); NS_ENSURE_SUCCESS(rv, rv); } } return NS_OK; } nsresult HTMLEditRules::InsertMozBRIfNeeded(nsINode& aNode) { if (!IsBlockNode(aNode)) { return NS_OK; } bool isEmpty; NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->IsEmptyNode(&aNode, &isEmpty); NS_ENSURE_SUCCESS(rv, rv); if (!isEmpty) { return NS_OK; } return CreateMozBR(aNode.AsDOMNode(), 0); } NS_IMETHODIMP HTMLEditRules::WillCreateNode(const nsAString& aTag, nsIDOMNode* aParent, int32_t aPosition) { return NS_OK; } NS_IMETHODIMP HTMLEditRules::DidCreateNode(const nsAString& aTag, nsIDOMNode* aNode, nsIDOMNode* aParent, int32_t aPosition, nsresult aResult) { if (!mListenerEnabled) { return NS_OK; } // assumption that Join keeps the righthand node nsresult rv = mUtilRange->SelectNode(aNode); NS_ENSURE_SUCCESS(rv, rv); return UpdateDocChangeRange(mUtilRange); } NS_IMETHODIMP HTMLEditRules::WillInsertNode(nsIDOMNode* aNode, nsIDOMNode* aParent, int32_t aPosition) { return NS_OK; } NS_IMETHODIMP HTMLEditRules::DidInsertNode(nsIDOMNode* aNode, nsIDOMNode* aParent, int32_t aPosition, nsresult aResult) { if (!mListenerEnabled) { return NS_OK; } nsresult rv = mUtilRange->SelectNode(aNode); NS_ENSURE_SUCCESS(rv, rv); return UpdateDocChangeRange(mUtilRange); } NS_IMETHODIMP HTMLEditRules::WillDeleteNode(nsIDOMNode* aChild) { if (!mListenerEnabled) { return NS_OK; } nsresult rv = mUtilRange->SelectNode(aChild); NS_ENSURE_SUCCESS(rv, rv); return UpdateDocChangeRange(mUtilRange); } NS_IMETHODIMP HTMLEditRules::DidDeleteNode(nsIDOMNode* aChild, nsresult aResult) { return NS_OK; } NS_IMETHODIMP HTMLEditRules::WillSplitNode(nsIDOMNode* aExistingRightNode, int32_t aOffset) { return NS_OK; } NS_IMETHODIMP HTMLEditRules::DidSplitNode(nsIDOMNode* aExistingRightNode, int32_t aOffset, nsIDOMNode* aNewLeftNode, nsresult aResult) { if (!mListenerEnabled) { return NS_OK; } nsresult rv = mUtilRange->SetStart(aNewLeftNode, 0); NS_ENSURE_SUCCESS(rv, rv); rv = mUtilRange->SetEnd(aExistingRightNode, 0); NS_ENSURE_SUCCESS(rv, rv); return UpdateDocChangeRange(mUtilRange); } NS_IMETHODIMP HTMLEditRules::WillJoinNodes(nsIDOMNode* aLeftNode, nsIDOMNode* aRightNode, nsIDOMNode* aParent) { if (!mListenerEnabled) { return NS_OK; } // remember split point return EditorBase::GetLengthOfDOMNode(aLeftNode, mJoinOffset); } NS_IMETHODIMP HTMLEditRules::DidJoinNodes(nsIDOMNode* aLeftNode, nsIDOMNode* aRightNode, nsIDOMNode* aParent, nsresult aResult) { if (!mListenerEnabled) { return NS_OK; } // assumption that Join keeps the righthand node nsresult rv = mUtilRange->SetStart(aRightNode, mJoinOffset); NS_ENSURE_SUCCESS(rv, rv); rv = mUtilRange->SetEnd(aRightNode, mJoinOffset); NS_ENSURE_SUCCESS(rv, rv); return UpdateDocChangeRange(mUtilRange); } NS_IMETHODIMP HTMLEditRules::WillInsertText(nsIDOMCharacterData* aTextNode, int32_t aOffset, const nsAString& aString) { return NS_OK; } NS_IMETHODIMP HTMLEditRules::DidInsertText(nsIDOMCharacterData* aTextNode, int32_t aOffset, const nsAString& aString, nsresult aResult) { if (!mListenerEnabled) { return NS_OK; } int32_t length = aString.Length(); nsCOMPtr<nsIDOMNode> theNode = do_QueryInterface(aTextNode); nsresult rv = mUtilRange->SetStart(theNode, aOffset); NS_ENSURE_SUCCESS(rv, rv); rv = mUtilRange->SetEnd(theNode, aOffset+length); NS_ENSURE_SUCCESS(rv, rv); return UpdateDocChangeRange(mUtilRange); } NS_IMETHODIMP HTMLEditRules::WillDeleteText(nsIDOMCharacterData* aTextNode, int32_t aOffset, int32_t aLength) { return NS_OK; } NS_IMETHODIMP HTMLEditRules::DidDeleteText(nsIDOMCharacterData* aTextNode, int32_t aOffset, int32_t aLength, nsresult aResult) { if (!mListenerEnabled) { return NS_OK; } nsCOMPtr<nsIDOMNode> theNode = do_QueryInterface(aTextNode); nsresult rv = mUtilRange->SetStart(theNode, aOffset); NS_ENSURE_SUCCESS(rv, rv); rv = mUtilRange->SetEnd(theNode, aOffset); NS_ENSURE_SUCCESS(rv, rv); return UpdateDocChangeRange(mUtilRange); } NS_IMETHODIMP HTMLEditRules::WillDeleteSelection(nsISelection* aSelection) { if (!mListenerEnabled) { return NS_OK; } if (NS_WARN_IF(!aSelection)) { return NS_ERROR_INVALID_ARG; } RefPtr<Selection> selection = aSelection->AsSelection(); // get the (collapsed) selection location nsCOMPtr<nsIDOMNode> selNode; int32_t selOffset; NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetStartNodeAndOffset(selection, getter_AddRefs(selNode), &selOffset); NS_ENSURE_SUCCESS(rv, rv); rv = mUtilRange->SetStart(selNode, selOffset); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->GetEndNodeAndOffset(selection, getter_AddRefs(selNode), &selOffset); NS_ENSURE_SUCCESS(rv, rv); rv = mUtilRange->SetEnd(selNode, selOffset); NS_ENSURE_SUCCESS(rv, rv); return UpdateDocChangeRange(mUtilRange); } NS_IMETHODIMP HTMLEditRules::DidDeleteSelection(nsISelection *aSelection) { return NS_OK; } // Let's remove all alignment hints in the children of aNode; it can // be an ALIGN attribute (in case we just remove it) or a CENTER // element (here we have to remove the container and keep its // children). We break on tables and don't look at their children. nsresult HTMLEditRules::RemoveAlignment(nsIDOMNode* aNode, const nsAString& aAlignType, bool aChildrenOnly) { NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER); NS_ENSURE_STATE(mHTMLEditor); if (mHTMLEditor->IsTextNode(aNode) || HTMLEditUtils::IsTable(aNode)) { return NS_OK; } nsCOMPtr<nsIDOMNode> child = aNode,tmp; if (aChildrenOnly) { aNode->GetFirstChild(getter_AddRefs(child)); } NS_ENSURE_STATE(mHTMLEditor); bool useCSS = mHTMLEditor->IsCSSEnabled(); while (child) { if (aChildrenOnly) { // get the next sibling right now because we could have to remove child child->GetNextSibling(getter_AddRefs(tmp)); } else { tmp = nullptr; } bool isBlock; NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->NodeIsBlockStatic(child, &isBlock); NS_ENSURE_SUCCESS(rv, rv); if (EditorBase::NodeIsType(child, nsGkAtoms::center)) { // the current node is a CENTER element // first remove children's alignment rv = RemoveAlignment(child, aAlignType, true); NS_ENSURE_SUCCESS(rv, rv); // we may have to insert BRs in first and last position of element's children // if the nodes before/after are not blocks and not BRs rv = MakeSureElemStartsOrEndsOnCR(child); NS_ENSURE_SUCCESS(rv, rv); // now remove the CENTER container NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<Element> childAsElement = do_QueryInterface(child); NS_ENSURE_STATE(childAsElement); rv = mHTMLEditor->RemoveContainer(childAsElement); NS_ENSURE_SUCCESS(rv, rv); } else if (isBlock || HTMLEditUtils::IsHR(child)) { // the current node is a block element nsCOMPtr<nsIDOMElement> curElem = do_QueryInterface(child); if (HTMLEditUtils::SupportsAlignAttr(child)) { // remove the ALIGN attribute if this element can have it NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->RemoveAttribute(curElem, NS_LITERAL_STRING("align")); NS_ENSURE_SUCCESS(rv, rv); } if (useCSS) { if (HTMLEditUtils::IsTable(child) || HTMLEditUtils::IsHR(child)) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->SetAttributeOrEquivalent(curElem, NS_LITERAL_STRING("align"), aAlignType, false); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { nsAutoString dummyCssValue; NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->mCSSEditUtils->RemoveCSSInlineStyle( child, nsGkAtoms::textAlign, dummyCssValue); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } if (!HTMLEditUtils::IsTable(child)) { // unless this is a table, look at children rv = RemoveAlignment(child, aAlignType, true); NS_ENSURE_SUCCESS(rv, rv); } } child = tmp; } return NS_OK; } // Let's insert a BR as first (resp. last) child of aNode if its // first (resp. last) child is not a block nor a BR, and if the // previous (resp. next) sibling is not a block nor a BR nsresult HTMLEditRules::MakeSureElemStartsOrEndsOnCR(nsIDOMNode* aNode, bool aStarts) { nsCOMPtr<nsINode> node = do_QueryInterface(aNode); NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER); nsCOMPtr<nsIDOMNode> child; if (aStarts) { NS_ENSURE_STATE(mHTMLEditor); child = GetAsDOMNode(mHTMLEditor->GetFirstEditableChild(*node)); } else { NS_ENSURE_STATE(mHTMLEditor); child = GetAsDOMNode(mHTMLEditor->GetLastEditableChild(*node)); } NS_ENSURE_TRUE(child, NS_OK); bool isChildBlock; NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->NodeIsBlockStatic(child, &isChildBlock); NS_ENSURE_SUCCESS(rv, rv); bool foundCR = false; if (isChildBlock || TextEditUtils::IsBreak(child)) { foundCR = true; } else { nsCOMPtr<nsIDOMNode> sibling; if (aStarts) { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->GetPriorHTMLSibling(aNode, address_of(sibling)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->GetNextHTMLSibling(aNode, address_of(sibling)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } if (sibling) { bool isBlock; NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->NodeIsBlockStatic(sibling, &isBlock); NS_ENSURE_SUCCESS(rv, rv); if (isBlock || TextEditUtils::IsBreak(sibling)) { foundCR = true; } } else { foundCR = true; } } if (!foundCR) { int32_t offset = 0; if (!aStarts) { nsCOMPtr<nsINode> node = do_QueryInterface(aNode); NS_ENSURE_STATE(node); offset = node->GetChildCount(); } nsCOMPtr<nsIDOMNode> brNode; NS_ENSURE_STATE(mHTMLEditor); rv = mHTMLEditor->CreateBR(aNode, offset, address_of(brNode)); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } nsresult HTMLEditRules::MakeSureElemStartsOrEndsOnCR(nsIDOMNode* aNode) { nsresult rv = MakeSureElemStartsOrEndsOnCR(aNode, false); NS_ENSURE_SUCCESS(rv, rv); return MakeSureElemStartsOrEndsOnCR(aNode, true); } nsresult HTMLEditRules::AlignBlock(Element& aElement, const nsAString& aAlignType, ContentsOnly aContentsOnly) { if (!IsBlockNode(aElement) && !aElement.IsHTMLElement(nsGkAtoms::hr)) { // We deal only with blocks; early way out return NS_OK; } NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); nsresult rv = RemoveAlignment(aElement.AsDOMNode(), aAlignType, aContentsOnly == ContentsOnly::yes); NS_ENSURE_SUCCESS(rv, rv); NS_NAMED_LITERAL_STRING(attr, "align"); if (htmlEditor->IsCSSEnabled()) { // Let's use CSS alignment; we use margin-left and margin-right for tables // and text-align for other block-level elements rv = htmlEditor->SetAttributeOrEquivalent( static_cast<nsIDOMElement*>(aElement.AsDOMNode()), attr, aAlignType, false); NS_ENSURE_SUCCESS(rv, rv); } else { // HTML case; this code is supposed to be called ONLY if the element // supports the align attribute but we'll never know... if (HTMLEditUtils::SupportsAlignAttr(aElement.AsDOMNode())) { rv = htmlEditor->SetAttribute( static_cast<nsIDOMElement*>(aElement.AsDOMNode()), attr, aAlignType); NS_ENSURE_SUCCESS(rv, rv); } } return NS_OK; } nsresult HTMLEditRules::ChangeIndentation(Element& aElement, Change aChange) { NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); nsIAtom& marginProperty = MarginPropertyAtomForIndent(*htmlEditor->mCSSEditUtils, aElement); nsAutoString value; htmlEditor->mCSSEditUtils->GetSpecifiedProperty(aElement, marginProperty, value); float f; nsCOMPtr<nsIAtom> unit; htmlEditor->mCSSEditUtils->ParseLength(value, &f, getter_AddRefs(unit)); if (!f) { nsAutoString defaultLengthUnit; htmlEditor->mCSSEditUtils->GetDefaultLengthUnit(defaultLengthUnit); unit = NS_Atomize(defaultLengthUnit); } int8_t multiplier = aChange == Change::plus ? +1 : -1; if (nsGkAtoms::in == unit) { f += NS_EDITOR_INDENT_INCREMENT_IN * multiplier; } else if (nsGkAtoms::cm == unit) { f += NS_EDITOR_INDENT_INCREMENT_CM * multiplier; } else if (nsGkAtoms::mm == unit) { f += NS_EDITOR_INDENT_INCREMENT_MM * multiplier; } else if (nsGkAtoms::pt == unit) { f += NS_EDITOR_INDENT_INCREMENT_PT * multiplier; } else if (nsGkAtoms::pc == unit) { f += NS_EDITOR_INDENT_INCREMENT_PC * multiplier; } else if (nsGkAtoms::em == unit) { f += NS_EDITOR_INDENT_INCREMENT_EM * multiplier; } else if (nsGkAtoms::ex == unit) { f += NS_EDITOR_INDENT_INCREMENT_EX * multiplier; } else if (nsGkAtoms::px == unit) { f += NS_EDITOR_INDENT_INCREMENT_PX * multiplier; } else if (nsGkAtoms::percentage == unit) { f += NS_EDITOR_INDENT_INCREMENT_PERCENT * multiplier; } if (0 < f) { nsAutoString newValue; newValue.AppendFloat(f); newValue.Append(nsDependentAtomString(unit)); htmlEditor->mCSSEditUtils->SetCSSProperty(aElement, marginProperty, newValue); return NS_OK; } htmlEditor->mCSSEditUtils->RemoveCSSProperty(aElement, marginProperty, value); // Remove unnecessary divs if (!aElement.IsHTMLElement(nsGkAtoms::div) || &aElement == htmlEditor->GetActiveEditingHost() || !htmlEditor->IsDescendantOfEditorRoot(&aElement) || HTMLEditor::HasAttributes(&aElement)) { return NS_OK; } nsresult rv = htmlEditor->RemoveContainer(&aElement); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult HTMLEditRules::WillAbsolutePosition(Selection& aSelection, bool* aCancel, bool* aHandled) { MOZ_ASSERT(aCancel && aHandled); NS_ENSURE_STATE(mHTMLEditor); RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); WillInsert(aSelection, aCancel); // We want to ignore result of WillInsert() *aCancel = false; *aHandled = true; nsCOMPtr<Element> focusElement = htmlEditor->GetSelectionContainer(); if (focusElement && HTMLEditUtils::IsImage(focusElement)) { mNewBlock = focusElement; return NS_OK; } nsresult rv = NormalizeSelection(&aSelection); NS_ENSURE_SUCCESS(rv, rv); AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor); // Convert the selection ranges into "promoted" selection ranges: this // basically just expands the range to include the immediate block parent, // and then further expands to include any ancestors whose children are all // in the range. nsTArray<RefPtr<nsRange>> arrayOfRanges; GetPromotedRanges(aSelection, arrayOfRanges, EditAction::setAbsolutePosition); // Use these ranges to contruct a list of nodes to act on. nsTArray<OwningNonNull<nsINode>> arrayOfNodes; rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes, EditAction::setAbsolutePosition); NS_ENSURE_SUCCESS(rv, rv); // If nothing visible in list, make an empty block if (ListIsEmptyLine(arrayOfNodes)) { // Get selection location NS_ENSURE_STATE(aSelection.GetRangeAt(0) && aSelection.GetRangeAt(0)->GetStartParent()); OwningNonNull<nsINode> parent = *aSelection.GetRangeAt(0)->GetStartParent(); int32_t offset = aSelection.GetRangeAt(0)->StartOffset(); // Make sure we can put a block here rv = SplitAsNeeded(*nsGkAtoms::div, parent, offset); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr<Element> positionedDiv = htmlEditor->CreateNode(nsGkAtoms::div, parent, offset); NS_ENSURE_STATE(positionedDiv); // Remember our new block for postprocessing mNewBlock = positionedDiv; // Delete anything that was in the list of nodes while (!arrayOfNodes.IsEmpty()) { OwningNonNull<nsINode> curNode = arrayOfNodes[0]; rv = htmlEditor->DeleteNode(curNode); NS_ENSURE_SUCCESS(rv, rv); arrayOfNodes.RemoveElementAt(0); } // Put selection in new block *aHandled = true; rv = aSelection.Collapse(positionedDiv, 0); // Don't restore the selection selectionRestorer.Abort(); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } // Okay, now go through all the nodes and put them in a blockquote, or // whatever is appropriate. Woohoo! nsCOMPtr<Element> curList, curPositionedDiv, indentedLI; for (uint32_t i = 0; i < arrayOfNodes.Length(); i++) { // Here's where we actually figure out what to do NS_ENSURE_STATE(arrayOfNodes[i]->IsContent()); OwningNonNull<nsIContent> curNode = *arrayOfNodes[i]->AsContent(); // Ignore all non-editable nodes. Leave them be. if (!htmlEditor->IsEditable(curNode)) { continue; } nsCOMPtr<nsIContent> sibling; nsCOMPtr<nsINode> curParent = curNode->GetParentNode(); int32_t offset = curParent ? curParent->IndexOf(curNode) : -1; // Some logic for putting list items into nested lists... if (HTMLEditUtils::IsList(curParent)) { // Check to see if curList is still appropriate. Which it is if curNode // is still right after it in the same list. if (curList) { sibling = htmlEditor->GetPriorHTMLSibling(curNode); } if (!curList || (sibling && sibling != curList)) { // Create a new nested list of correct type rv = SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, offset); NS_ENSURE_SUCCESS(rv, rv); if (!curPositionedDiv) { nsCOMPtr<nsINode> curParentParent = curParent->GetParentNode(); int32_t parentOffset = curParentParent ? curParentParent->IndexOf(curParent) : -1; curPositionedDiv = htmlEditor->CreateNode(nsGkAtoms::div, curParentParent, parentOffset); mNewBlock = curPositionedDiv; } curList = htmlEditor->CreateNode(curParent->NodeInfo()->NameAtom(), curPositionedDiv, -1); NS_ENSURE_STATE(curList); // curList is now the correct thing to put curNode in. Remember our // new block for postprocessing. } // Tuck the node into the end of the active list rv = htmlEditor->MoveNode(curNode, curList, -1); NS_ENSURE_SUCCESS(rv, rv); } else { // Not a list item, use blockquote? If we are inside a list item, we // don't want to blockquote, we want to sublist the list item. We may // have several nodes listed in the array of nodes to act on, that are in // the same list item. Since we only want to indent that li once, we // must keep track of the most recent indented list item, and not indent // it if we find another node to act on that is still inside the same li. nsCOMPtr<Element> listItem = IsInListItem(curNode); if (listItem) { if (indentedLI == listItem) { // Already indented this list item continue; } curParent = listItem->GetParentNode(); offset = curParent ? curParent->IndexOf(listItem) : -1; // Check to see if curList is still appropriate. Which it is if // curNode is still right after it in the same list. if (curList) { sibling = htmlEditor->GetPriorHTMLSibling(curNode); } if (!curList || (sibling && sibling != curList)) { // Create a new nested list of correct type rv = SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, offset); NS_ENSURE_SUCCESS(rv, rv); if (!curPositionedDiv) { nsCOMPtr<nsINode> curParentParent = curParent->GetParentNode(); int32_t parentOffset = curParentParent ? curParentParent->IndexOf(curParent) : -1; curPositionedDiv = htmlEditor->CreateNode(nsGkAtoms::div, curParentParent, parentOffset); mNewBlock = curPositionedDiv; } curList = htmlEditor->CreateNode(curParent->NodeInfo()->NameAtom(), curPositionedDiv, -1); NS_ENSURE_STATE(curList); } rv = htmlEditor->MoveNode(listItem, curList, -1); NS_ENSURE_SUCCESS(rv, rv); // Remember we indented this li indentedLI = listItem; } else { // Need to make a div to put things in if we haven't already if (!curPositionedDiv) { if (curNode->IsHTMLElement(nsGkAtoms::div)) { curPositionedDiv = curNode->AsElement(); mNewBlock = curPositionedDiv; curList = nullptr; continue; } rv = SplitAsNeeded(*nsGkAtoms::div, curParent, offset); NS_ENSURE_SUCCESS(rv, rv); curPositionedDiv = htmlEditor->CreateNode(nsGkAtoms::div, curParent, offset); NS_ENSURE_STATE(curPositionedDiv); // Remember our new block for postprocessing mNewBlock = curPositionedDiv; // curPositionedDiv is now the correct thing to put curNode in } // Tuck the node into the end of the active blockquote rv = htmlEditor->MoveNode(curNode, curPositionedDiv, -1); NS_ENSURE_SUCCESS(rv, rv); // Forget curList, if any curList = nullptr; } } } return NS_OK; } nsresult HTMLEditRules::DidAbsolutePosition() { NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<nsIHTMLAbsPosEditor> absPosHTMLEditor = mHTMLEditor; nsCOMPtr<nsIDOMElement> elt = static_cast<nsIDOMElement*>(GetAsDOMNode(mNewBlock)); return absPosHTMLEditor->AbsolutelyPositionElement(elt, true); } nsresult HTMLEditRules::WillRemoveAbsolutePosition(Selection* aSelection, bool* aCancel, bool* aHandled) { if (!aSelection || !aCancel || !aHandled) { return NS_ERROR_NULL_POINTER; } WillInsert(*aSelection, aCancel); // initialize out param // we want to ignore aCancel from WillInsert() *aCancel = false; *aHandled = true; nsCOMPtr<nsIDOMElement> elt; NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetAbsolutelyPositionedSelectionContainer(getter_AddRefs(elt)); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor); NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<nsIHTMLAbsPosEditor> absPosHTMLEditor = mHTMLEditor; return absPosHTMLEditor->AbsolutelyPositionElement(elt, false); } nsresult HTMLEditRules::WillRelativeChangeZIndex(Selection* aSelection, int32_t aChange, bool* aCancel, bool* aHandled) { if (!aSelection || !aCancel || !aHandled) { return NS_ERROR_NULL_POINTER; } WillInsert(*aSelection, aCancel); // initialize out param // we want to ignore aCancel from WillInsert() *aCancel = false; *aHandled = true; nsCOMPtr<nsIDOMElement> elt; NS_ENSURE_STATE(mHTMLEditor); nsresult rv = mHTMLEditor->GetAbsolutelyPositionedSelectionContainer(getter_AddRefs(elt)); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_STATE(mHTMLEditor); AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor); NS_ENSURE_STATE(mHTMLEditor); nsCOMPtr<nsIHTMLAbsPosEditor> absPosHTMLEditor = mHTMLEditor; int32_t zIndex; return absPosHTMLEditor->RelativeChangeElementZIndex(elt, aChange, &zIndex); } NS_IMETHODIMP HTMLEditRules::DocumentModified() { nsContentUtils::AddScriptRunner( NewRunnableMethod(this, &HTMLEditRules::DocumentModifiedWorker)); return NS_OK; } void HTMLEditRules::DocumentModifiedWorker() { if (!mHTMLEditor) { return; } // DeleteNode below may cause a flush, which could destroy the editor nsAutoScriptBlockerSuppressNodeRemoved scriptBlocker; RefPtr<HTMLEditor> htmlEditor(mHTMLEditor); RefPtr<Selection> selection = htmlEditor->GetSelection(); if (!selection) { return; } // Delete our bogus node, if we have one, since the document might not be // empty any more. if (mBogusNode) { mTextEditor->DeleteNode(mBogusNode); mBogusNode = nullptr; } // Try to recreate the bogus node if needed. CreateBogusNodeIfNeeded(selection); } } // namespace mozilla