/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 sw=2 et tw=78: */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is mozilla.org code. * * The Initial Developer of the Original Code is * Netscape Communications Corporation. * Portions created by the Initial Developer are Copyright (C) 1998 * the Initial Developer. All Rights Reserved. * * Contributor(s): * * Alternatively, the contents of this file may be used under the terms of * either of the GNU General Public License Version 2 or later (the "GPL"), * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ #include "nsHTMLEditor.h" #include "nsHTMLEditRules.h" #include "nsTextEditUtils.h" #include "nsHTMLEditUtils.h" #include "nsWSRunObject.h" #include "nsEditorEventListeners.h" #include "nsIDOMText.h" #include "nsIDOMNodeList.h" #include "nsIDOMDocument.h" #include "nsIDOMAttr.h" #include "nsIDocument.h" #include "nsIDOMEventTarget.h" #include "nsIDOMNSEvent.h" #include "nsIDOMKeyEvent.h" #include "nsIDOMKeyListener.h" #include "nsIDOMMouseListener.h" #include "nsIDOMMouseEvent.h" #include "nsIDOMComment.h" #include "nsISelection.h" #include "nsISelectionPrivate.h" #include "nsIDOMHTMLAnchorElement.h" #include "nsIDOMHTMLImageElement.h" #include "nsISelectionController.h" #include "nsIFileChannel.h" #include "nsICSSLoader.h" #include "nsICSSStyleSheet.h" #include "nsIDocumentObserver.h" #include "nsIDocumentStateListener.h" #include "nsIEnumerator.h" #include "nsIContent.h" #include "nsIContentIterator.h" #include "nsIDOMRange.h" #include "nsIDOMNSRange.h" #include "nsCOMArray.h" #include "nsIFile.h" #include "nsIURL.h" #include "nsIComponentManager.h" #include "nsIServiceManager.h" #include "nsIDocumentEncoder.h" #include "nsIDOMDocumentFragment.h" #include "nsIPresShell.h" #include "nsPresContext.h" #include "nsIParser.h" #include "nsParserCIID.h" #include "nsXPCOM.h" #include "nsISupportsPrimitives.h" #include "nsLinebreakConverter.h" #include "nsIFragmentContentSink.h" #include "nsIContentSink.h" // netwerk #include "nsIURI.h" #include "nsNetUtil.h" // Drag & Drop, Clipboard #include "nsIClipboard.h" #include "nsITransferable.h" #include "nsIDragService.h" #include "nsIDOMNSUIEvent.h" #include "nsIOutputStream.h" #include "nsIInputStream.h" #include "nsDirectoryServiceDefs.h" // for relativization #include "nsUnicharUtils.h" #include "nsIDOMDocumentTraversal.h" #include "nsIDOMTreeWalker.h" #include "nsIDOMNodeFilter.h" #include "nsIDOMNamedNodeMap.h" #include "nsIDOMHTMLLinkElement.h" #include "nsIDOMHTMLObjectElement.h" #include "nsIDOMHTMLFrameElement.h" #include "nsIDOMHTMLIFrameElement.h" #include "nsIDOMHTMLInputElement.h" #include "nsIDOMHTMLScriptElement.h" #include "nsIDOMHTMLEmbedElement.h" #include "nsIDOMHTMLTableCellElement.h" #include "nsIDOMHTMLTableRowElement.h" #include "nsIDOMHTMLTableElement.h" #include "nsIDOMHTMLBodyElement.h" // Misc #include "TextEditorTest.h" #include "nsEditorUtils.h" #include "nsIPrefBranch.h" #include "nsIPrefService.h" #include "nsIContentFilter.h" #include "nsEventDispatcher.h" const PRUnichar nbsp = 160; static NS_DEFINE_CID(kCParserCID, NS_PARSER_CID); // private clipboard data flavors for html copy/paste #define kHTMLContext "text/_moz_htmlcontext" #define kHTMLInfo "text/_moz_htmlinfo" #define kInsertCookie "_moz_Insert Here_moz_" #define NS_FOUND_TARGET NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_EDITOR, 3) // some little helpers static PRInt32 FindPositiveIntegerAfterString(const char *aLeadingString, nsCString &aCStr); static nsresult RemoveFragComments(nsCString &theStr); static void RemoveBodyAndHead(nsIDOMNode *aNode); static nsresult FindTargetNode(nsIDOMNode *aStart, nsCOMPtr &aResult); static nsCOMPtr GetListParent(nsIDOMNode* aNode) { if (!aNode) return nsnull; nsCOMPtr parent, tmp; aNode->GetParentNode(getter_AddRefs(parent)); while (parent) { if (nsHTMLEditUtils::IsList(parent)) return parent; parent->GetParentNode(getter_AddRefs(tmp)); parent = tmp; } return nsnull; } static nsCOMPtr GetTableParent(nsIDOMNode* aNode) { if (!aNode) return nsnull; nsCOMPtr parent, tmp; aNode->GetParentNode(getter_AddRefs(parent)); while (parent) { if (nsHTMLEditUtils::IsTable(parent)) return parent; parent->GetParentNode(getter_AddRefs(tmp)); parent = tmp; } return nsnull; } NS_IMETHODIMP nsHTMLEditor::LoadHTML(const nsAString & aInputString) { if (!mRules) return NS_ERROR_NOT_INITIALIZED; // force IME commit; set up rules sniffing and batching ForceCompositionEnd(); nsAutoEditBatch beginBatching(this); nsAutoRules beginRulesSniffing(this, kOpLoadHTML, nsIEditor::eNext); // Get selection nsCOMPtrselection; nsresult res = GetSelection(getter_AddRefs(selection)); if (NS_FAILED(res)) return res; nsTextRulesInfo ruleInfo(nsTextEditRules::kLoadHTML); PRBool cancel, handled; res = mRules->WillDoAction(selection, &ruleInfo, &cancel, &handled); if (NS_FAILED(res)) return res; if (cancel) return NS_OK; // rules canceled the operation if (!handled) { PRBool isCollapsed; res = selection->GetIsCollapsed(&isCollapsed); if (NS_FAILED(res)) return res; // Delete Selection, but only if it isn't collapsed, see bug #106269 if (!isCollapsed) { res = DeleteSelection(eNone); if (NS_FAILED(res)) return res; } // Get the first range in the selection, for context: nsCOMPtr range; res = selection->GetRangeAt(0, getter_AddRefs(range)); NS_ENSURE_SUCCESS(res, res); if (!range) return NS_ERROR_NULL_POINTER; nsCOMPtr nsrange (do_QueryInterface(range)); if (!nsrange) return NS_ERROR_NO_INTERFACE; // create fragment for pasted html nsCOMPtr docfrag; { res = nsrange->CreateContextualFragment(aInputString, getter_AddRefs(docfrag)); NS_ENSURE_SUCCESS(res, res); } // put the fragment into the document nsCOMPtr parent, junk; res = range->GetStartContainer(getter_AddRefs(parent)); NS_ENSURE_SUCCESS(res, res); if (!parent) return NS_ERROR_NULL_POINTER; PRInt32 childOffset; res = range->GetStartOffset(&childOffset); NS_ENSURE_SUCCESS(res, res); nsCOMPtr nodeToInsert; docfrag->GetFirstChild(getter_AddRefs(nodeToInsert)); while (nodeToInsert) { res = InsertNode(nodeToInsert, parent, childOffset++); NS_ENSURE_SUCCESS(res, res); docfrag->GetFirstChild(getter_AddRefs(nodeToInsert)); } } return mRules->DidDoAction(selection, &ruleInfo, res); } NS_IMETHODIMP nsHTMLEditor::InsertHTML(const nsAString & aInString) { const nsAFlatString& empty = EmptyString(); return InsertHTMLWithContext(aInString, empty, empty, empty, nsnull, nsnull, 0, PR_TRUE); } nsresult nsHTMLEditor::InsertHTMLWithContext(const nsAString & aInputString, const nsAString & aContextStr, const nsAString & aInfoStr, const nsAString & aFlavor, nsIDOMDocument *aSourceDoc, nsIDOMNode *aDestNode, PRInt32 aDestOffset, PRBool aDeleteSelection) { if (!mRules) return NS_ERROR_NOT_INITIALIZED; // force IME commit; set up rules sniffing and batching ForceCompositionEnd(); nsAutoEditBatch beginBatching(this); nsAutoRules beginRulesSniffing(this, kOpHTMLPaste, nsIEditor::eNext); // Get selection nsresult res; nsCOMPtrselection; res = GetSelection(getter_AddRefs(selection)); if (NS_FAILED(res)) return res; // create a dom document fragment that represents the structure to paste nsCOMPtr fragmentAsNode, streamStartParent, streamEndParent; PRInt32 streamStartOffset = 0, streamEndOffset = 0; res = CreateDOMFragmentFromPaste(aInputString, aContextStr, aInfoStr, address_of(fragmentAsNode), address_of(streamStartParent), address_of(streamEndParent), &streamStartOffset, &streamEndOffset); NS_ENSURE_SUCCESS(res, res); nsCOMPtr targetNode, tempNode; PRInt32 targetOffset=0; if (!aDestNode) { // if caller didn't provide the destination/target node, // fetch the paste insertion point from our selection res = GetStartNodeAndOffset(selection, address_of(targetNode), &targetOffset); if (!targetNode) res = NS_ERROR_FAILURE; if (NS_FAILED(res)) return res; } else { targetNode = aDestNode; targetOffset = aDestOffset; } PRBool doContinue = PR_TRUE; res = DoContentFilterCallback(aFlavor, aSourceDoc, aDeleteSelection, (nsIDOMNode **)address_of(fragmentAsNode), (nsIDOMNode **)address_of(streamStartParent), &streamStartOffset, (nsIDOMNode **)address_of(streamEndParent), &streamEndOffset, (nsIDOMNode **)address_of(targetNode), &targetOffset, &doContinue); NS_ENSURE_SUCCESS(res, res); if (!doContinue) return NS_OK; // if we have a destination / target node, we want to insert there // rather than in place of the selection // ignore aDeleteSelection here if no aDestNode since deletion will // also occur later; this block is intended to cover the various // scenarios where we are dropping in an editor (and may want to delete // the selection before collapsing the selection in the new destination) if (aDestNode) { if (aDeleteSelection) { // Use an auto tracker so that our drop point is correctly // positioned after the delete. nsAutoTrackDOMPoint tracker(mRangeUpdater, &targetNode, &targetOffset); res = DeleteSelection(eNone); NS_ENSURE_SUCCESS(res, res); } res = selection->Collapse(targetNode, targetOffset); NS_ENSURE_SUCCESS(res, res); } // we need to recalculate various things based on potentially new offsets // this is work to be completed at a later date (probably by jfrancis) // make a list of what nodes in docFrag we need to move nsCOMArray nodeList; res = CreateListOfNodesToPaste(fragmentAsNode, nodeList, streamStartParent, streamStartOffset, streamEndParent, streamEndOffset); NS_ENSURE_SUCCESS(res, res); if (nodeList.Count() == 0) return NS_OK; // walk list of nodes; perform surgery on nodes (relativize) with _mozattr res = RelativizeURIInFragmentList(nodeList, aFlavor, aSourceDoc, targetNode); // ignore results from this call, try to paste/insert anyways // are there any table elements in the list? // node and offset for insertion nsCOMPtr parentNode; PRInt32 offsetOfNewNode; // check for table cell selection mode PRBool cellSelectionMode = PR_FALSE; nsCOMPtr cell; res = GetFirstSelectedCell(nsnull, getter_AddRefs(cell)); if (NS_SUCCEEDED(res) && cell) { cellSelectionMode = PR_TRUE; } if (cellSelectionMode) { // do we have table content to paste? If so, we want to delete // the selected table cells and replace with new table elements; // but if not we want to delete _contents_ of cells and replace // with non-table elements. Use cellSelectionMode bool to // indicate results. nsIDOMNode* firstNode = nodeList[0]; if (!nsHTMLEditUtils::IsTableElement(firstNode)) cellSelectionMode = PR_FALSE; } if (!cellSelectionMode) { res = DeleteSelectionAndPrepareToCreateNode(parentNode, offsetOfNewNode); NS_ENSURE_SUCCESS(res, res); // pasting does not inherit local inline styles res = RemoveAllInlineProperties(); NS_ENSURE_SUCCESS(res, res); } else { // delete whole cells: we will replace with new table content if (1) { // Save current selection since DeleteTableCell perturbs it nsAutoSelectionReset selectionResetter(selection, this); res = DeleteTableCell(1); NS_ENSURE_SUCCESS(res, res); } // collapse selection to beginning of deleted table content selection->CollapseToStart(); } // give rules a chance to handle or cancel nsTextRulesInfo ruleInfo(nsTextEditRules::kInsertElement); PRBool cancel, handled; res = mRules->WillDoAction(selection, &ruleInfo, &cancel, &handled); if (NS_FAILED(res)) return res; if (cancel) return NS_OK; // rules canceled the operation if (!handled) { // The rules code (WillDoAction above) might have changed the selection. // refresh our memory... res = GetStartNodeAndOffset(selection, address_of(parentNode), &offsetOfNewNode); if (!parentNode) res = NS_ERROR_FAILURE; if (NS_FAILED(res)) return res; // Adjust position based on the first node we are going to insert. NormalizeEOLInsertPosition(nodeList[0], address_of(parentNode), &offsetOfNewNode); // if there are any invisible br's after our insertion point, remove them. // this is because if there is a br at end of what we paste, it will make // the invisible br visible. nsWSRunObject wsObj(this, parentNode, offsetOfNewNode); if (nsTextEditUtils::IsBreak(wsObj.mEndReasonNode) && !IsVisBreak(wsObj.mEndReasonNode) ) { res = DeleteNode(wsObj.mEndReasonNode); if (NS_FAILED(res)) return res; } // remeber if we are in a link. PRBool bStartedInLink = IsInLink(parentNode); // are we in a text node? If so, split it. if (IsTextNode(parentNode)) { nsCOMPtr temp; res = SplitNodeDeep(parentNode, parentNode, offsetOfNewNode, &offsetOfNewNode); if (NS_FAILED(res)) return res; res = parentNode->GetParentNode(getter_AddRefs(temp)); if (NS_FAILED(res)) return res; parentNode = temp; } // build up list of parents of first node in list that are either // lists or tables. First examine front of paste node list. nsCOMArray startListAndTableArray; res = GetListAndTableParents(PR_FALSE, nodeList, startListAndTableArray); NS_ENSURE_SUCCESS(res, res); // remember number of lists and tables above us PRInt32 highWaterMark = -1; if (startListAndTableArray.Count() > 0) { res = DiscoverPartialListsAndTables(nodeList, startListAndTableArray, &highWaterMark); NS_ENSURE_SUCCESS(res, res); } // if we have pieces of tables or lists to be inserted, let's force the paste // to deal with table elements right away, so that it doesn't orphan some // table or list contents outside the table or list. if (highWaterMark >= 0) { res = ReplaceOrphanedStructure(PR_FALSE, nodeList, startListAndTableArray, highWaterMark); NS_ENSURE_SUCCESS(res, res); } // Now go through the same process again for the end of the paste node list. nsCOMArray endListAndTableArray; res = GetListAndTableParents(PR_TRUE, nodeList, endListAndTableArray); NS_ENSURE_SUCCESS(res, res); highWaterMark = -1; // remember number of lists and tables above us if (endListAndTableArray.Count() > 0) { res = DiscoverPartialListsAndTables(nodeList, endListAndTableArray, &highWaterMark); NS_ENSURE_SUCCESS(res, res); } // don't orphan partial list or table structure if (highWaterMark >= 0) { res = ReplaceOrphanedStructure(PR_TRUE, nodeList, endListAndTableArray, highWaterMark); NS_ENSURE_SUCCESS(res, res); } // Loop over the node list and paste the nodes: nsCOMPtr parentBlock, lastInsertNode, insertedContextParent; PRInt32 listCount = nodeList.Count(); PRInt32 j; if (IsBlockNode(parentNode)) parentBlock = parentNode; else parentBlock = GetBlockNodeParent(parentNode); for (j=0; j curNode = nodeList[j]; NS_ENSURE_TRUE(curNode, NS_ERROR_FAILURE); NS_ENSURE_TRUE(curNode != fragmentAsNode, NS_ERROR_FAILURE); NS_ENSURE_TRUE(!nsTextEditUtils::IsBody(curNode), NS_ERROR_FAILURE); if (insertedContextParent) { // if we had to insert something higher up in the paste hierarchy, we want to // skip any further paste nodes that descend from that. Else we will paste twice. if (nsEditorUtils::IsDescendantOf(curNode, insertedContextParent)) continue; } // give the user a hand on table element insertion. if they have // a table or table row on the clipboard, and are trying to insert // into a table or table row, insert the appropriate children instead. if ( (nsHTMLEditUtils::IsTableRow(curNode) && nsHTMLEditUtils::IsTableRow(parentNode)) && (nsHTMLEditUtils::IsTable(curNode) || nsHTMLEditUtils::IsTable(parentNode)) ) { nsCOMPtr child; curNode->GetFirstChild(getter_AddRefs(child)); while (child) { res = InsertNodeAtPoint(child, address_of(parentNode), &offsetOfNewNode, PR_TRUE); if (NS_FAILED(res)) break; bDidInsert = PR_TRUE; lastInsertNode = child; offsetOfNewNode++; curNode->GetFirstChild(getter_AddRefs(child)); } } // give the user a hand on list insertion. if they have // a list on the clipboard, and are trying to insert // into a list or list item, insert the appropriate children instead, // ie, merge the lists instead of pasting in a sublist. else if (nsHTMLEditUtils::IsList(curNode) && (nsHTMLEditUtils::IsList(parentNode) || nsHTMLEditUtils::IsListItem(parentNode)) ) { nsCOMPtr child, tmp; curNode->GetFirstChild(getter_AddRefs(child)); while (child) { if (nsHTMLEditUtils::IsListItem(child) || nsHTMLEditUtils::IsList(child)) { // check if we are pasting into empty list item. If so // delete it and paste into parent list instead. if (nsHTMLEditUtils::IsListItem(parentNode)) { PRBool isEmpty; res = IsEmptyNode(parentNode, &isEmpty, PR_TRUE); if ((NS_SUCCEEDED(res)) && isEmpty) { nsCOMPtr listNode; PRInt32 newOffset; GetNodeLocation(parentNode, address_of(listNode), &newOffset); if (listNode) { DeleteNode(parentNode); parentNode = listNode; offsetOfNewNode = newOffset; } } } res = InsertNodeAtPoint(child, address_of(parentNode), &offsetOfNewNode, PR_TRUE); if (NS_FAILED(res)) break; bDidInsert = PR_TRUE; lastInsertNode = child; offsetOfNewNode++; } else { curNode->RemoveChild(child, getter_AddRefs(tmp)); } curNode->GetFirstChild(getter_AddRefs(child)); } } // check for pre's going into pre's. else if (nsHTMLEditUtils::IsPre(parentBlock) && nsHTMLEditUtils::IsPre(curNode)) { nsCOMPtr child, tmp; curNode->GetFirstChild(getter_AddRefs(child)); while (child) { res = InsertNodeAtPoint(child, address_of(parentNode), &offsetOfNewNode, PR_TRUE); if (NS_FAILED(res)) break; bDidInsert = PR_TRUE; lastInsertNode = child; offsetOfNewNode++; curNode->GetFirstChild(getter_AddRefs(child)); } } if (!bDidInsert || NS_FAILED(res)) { // try to insert res = InsertNodeAtPoint(curNode, address_of(parentNode), &offsetOfNewNode, PR_TRUE); if (NS_SUCCEEDED(res)) { bDidInsert = PR_TRUE; lastInsertNode = curNode; } // Assume failure means no legal parent in the document hierarchy, // try again with the parent of curNode in the paste hierarchy. nsCOMPtr parent; while (NS_FAILED(res) && curNode) { curNode->GetParentNode(getter_AddRefs(parent)); if (parent && !nsTextEditUtils::IsBody(parent)) { res = InsertNodeAtPoint(parent, address_of(parentNode), &offsetOfNewNode, PR_TRUE); if (NS_SUCCEEDED(res)) { bDidInsert = PR_TRUE; insertedContextParent = parent; lastInsertNode = parent; } } curNode = parent; } } if (lastInsertNode) { res = GetNodeLocation(lastInsertNode, address_of(parentNode), &offsetOfNewNode); NS_ENSURE_SUCCESS(res, res); offsetOfNewNode++; } } // Now collapse the selection to the end of what we just inserted: if (lastInsertNode) { // set selection to the end of what we just pasted. nsCOMPtr selNode, tmp, visNode, highTable; PRInt32 selOffset; // but don't cross tables if (!nsHTMLEditUtils::IsTable(lastInsertNode)) { res = GetLastEditableLeaf(lastInsertNode, address_of(selNode)); if (NS_FAILED(res)) return res; tmp = selNode; while (tmp && (tmp != lastInsertNode)) { if (nsHTMLEditUtils::IsTable(tmp)) highTable = tmp; nsCOMPtr parent = tmp; tmp->GetParentNode(getter_AddRefs(parent)); tmp = parent; } if (highTable) selNode = highTable; } if (!selNode) selNode = lastInsertNode; if (IsTextNode(selNode) || (IsContainer(selNode) && !nsHTMLEditUtils::IsTable(selNode))) { res = GetLengthOfDOMNode(selNode, (PRUint32&)selOffset); if (NS_FAILED(res)) return res; } else // we need to find a container for selection. Look up. { tmp = selNode; res = GetNodeLocation(tmp, address_of(selNode), &selOffset); ++selOffset; // want to be *after* last leaf node in paste if (NS_FAILED(res)) return res; } // make sure we don't end up with selection collapsed after an invisible break node nsWSRunObject wsRunObj(this, selNode, selOffset); PRInt32 outVisOffset=0; PRInt16 visType=0; res = wsRunObj.PriorVisibleNode(selNode, selOffset, address_of(visNode), &outVisOffset, &visType); if (NS_FAILED(res)) return res; if (visType == nsWSRunObject::eBreak) { // we are after a break. Is it visible? Despite the name, // PriorVisibleNode does not make that determination for breaks. // It also may not return the break in visNode. We have to pull it // out of the nsWSRunObject's state. if (!IsVisBreak(wsRunObj.mStartReasonNode)) { // don't leave selection past an invisible break; // reset {selNode,selOffset} to point before break res = GetNodeLocation(wsRunObj.mStartReasonNode, address_of(selNode), &selOffset); // we want to be inside any inline style prior to break nsWSRunObject wsRunObj(this, selNode, selOffset); res = wsRunObj.PriorVisibleNode(selNode, selOffset, address_of(visNode), &outVisOffset, &visType); if (NS_FAILED(res)) return res; if (visType == nsWSRunObject::eText || visType == nsWSRunObject::eNormalWS) { selNode = visNode; selOffset = outVisOffset; // PriorVisibleNode already set offset to _after_ the text or ws } else if (visType == nsWSRunObject::eSpecial) { // prior visible thing is an image or some other non-text thingy. // We want to be right after it. res = GetNodeLocation(wsRunObj.mStartReasonNode, address_of(selNode), &selOffset); ++selOffset; } } } selection->Collapse(selNode, selOffset); // if we just pasted a link, discontinue link style nsCOMPtr link; if (!bStartedInLink && IsInLink(selNode, address_of(link))) { // so, if we just pasted a link, I split it. Why do that instead of just // nudging selection point beyond it? Because it might have ended in a BR // that is not visible. If so, the code above just placed selection // inside that. So I split it instead. nsCOMPtr leftLink; PRInt32 linkOffset; res = SplitNodeDeep(link, selNode, selOffset, &linkOffset, PR_TRUE, address_of(leftLink)); if (NS_FAILED(res)) return res; res = GetNodeLocation(leftLink, address_of(selNode), &selOffset); if (NS_FAILED(res)) return res; selection->Collapse(selNode, selOffset+1); } } } res = mRules->DidDoAction(selection, &ruleInfo, res); return res; } // returns empty string if nothing to modify on node nsresult nsHTMLEditor::GetAttributeToModifyOnNode(nsIDOMNode *aNode, nsAString &aAttr) { aAttr.Truncate(); NS_NAMED_LITERAL_STRING(srcStr, "src"); nsCOMPtr nodeAsImage = do_QueryInterface(aNode); if (nodeAsImage) { aAttr = srcStr; return NS_OK; } nsCOMPtr nodeAsAnchor = do_QueryInterface(aNode); if (nodeAsAnchor) { aAttr.AssignLiteral("href"); return NS_OK; } NS_NAMED_LITERAL_STRING(bgStr, "background"); nsCOMPtr nodeAsBody = do_QueryInterface(aNode); if (nodeAsBody) { aAttr = bgStr; return NS_OK; } nsCOMPtr nodeAsTable = do_QueryInterface(aNode); if (nodeAsTable) { aAttr = bgStr; return NS_OK; } nsCOMPtr nodeAsTableRow = do_QueryInterface(aNode); if (nodeAsTableRow) { aAttr = bgStr; return NS_OK; } nsCOMPtr nodeAsTableCell = do_QueryInterface(aNode); if (nodeAsTableCell) { aAttr = bgStr; return NS_OK; } nsCOMPtr nodeAsScript = do_QueryInterface(aNode); if (nodeAsScript) { aAttr = srcStr; return NS_OK; } nsCOMPtr nodeAsEmbed = do_QueryInterface(aNode); if (nodeAsEmbed) { aAttr = srcStr; return NS_OK; } nsCOMPtr nodeAsObject = do_QueryInterface(aNode); if (nodeAsObject) { aAttr.AssignLiteral("data"); return NS_OK; } nsCOMPtr nodeAsLink = do_QueryInterface(aNode); if (nodeAsLink) { // Test if the link has a rel value indicating it to be a stylesheet nsAutoString linkRel; if (NS_SUCCEEDED(nodeAsLink->GetRel(linkRel)) && !linkRel.IsEmpty()) { nsReadingIterator start; nsReadingIterator end; nsReadingIterator current; linkRel.BeginReading(start); linkRel.EndReading(end); // Walk through space delimited string looking for "stylesheet" for (current = start; current != end; ++current) { // Ignore whitespace if (nsCRT::IsAsciiSpace(*current)) continue; // Grab the next space delimited word nsReadingIterator startWord = current; do { ++current; } while (current != end && !nsCRT::IsAsciiSpace(*current)); // Store the link for fix up if it says "stylesheet" if (Substring(startWord, current).LowerCaseEqualsLiteral("stylesheet")) { aAttr.AssignLiteral("href"); return NS_OK; } if (current == end) break; } } return NS_OK; } nsCOMPtr nodeAsFrame = do_QueryInterface(aNode); if (nodeAsFrame) { aAttr = srcStr; return NS_OK; } nsCOMPtr nodeAsIFrame = do_QueryInterface(aNode); if (nodeAsIFrame) { aAttr = srcStr; return NS_OK; } nsCOMPtr nodeAsInput = do_QueryInterface(aNode); if (nodeAsInput) { aAttr = srcStr; return NS_OK; } return NS_OK; } nsresult nsHTMLEditor::RelativizeURIForNode(nsIDOMNode *aNode, nsIURL *aDestURL) { nsAutoString attributeToModify; GetAttributeToModifyOnNode(aNode, attributeToModify); if (attributeToModify.IsEmpty()) return NS_OK; nsCOMPtr attrMap; nsresult rv = aNode->GetAttributes(getter_AddRefs(attrMap)); NS_ENSURE_SUCCESS(rv, NS_OK); if (!attrMap) return NS_OK; // assume errors here shouldn't cancel insertion nsCOMPtr attrNode; rv = attrMap->GetNamedItem(attributeToModify, getter_AddRefs(attrNode)); NS_ENSURE_SUCCESS(rv, NS_OK); // assume errors here shouldn't cancel insertion if (attrNode) { nsAutoString oldValue; attrNode->GetNodeValue(oldValue); if (!oldValue.IsEmpty()) { NS_ConvertUTF16toUTF8 oldCValue(oldValue); nsCOMPtr currentNodeURI; rv = NS_NewURI(getter_AddRefs(currentNodeURI), oldCValue); if (NS_SUCCEEDED(rv)) { nsCAutoString newRelativePath; aDestURL->GetRelativeSpec(currentNodeURI, newRelativePath); if (!newRelativePath.IsEmpty()) { NS_ConvertUTF8toUTF16 newCValue(newRelativePath); attrNode->SetNodeValue(newCValue); } } } } return NS_OK; } nsresult nsHTMLEditor::RelativizeURIInFragmentList(const nsCOMArray &aNodeList, const nsAString &aFlavor, nsIDOMDocument *aSourceDoc, nsIDOMNode *aTargetNode) { // determine destination URL nsCOMPtr domDoc; GetDocument(getter_AddRefs(domDoc)); nsCOMPtr destDoc = do_QueryInterface(domDoc); if (!destDoc) return NS_ERROR_FAILURE; nsCOMPtr destURL = do_QueryInterface(destDoc->GetDocumentURI()); if (!destURL) return NS_ERROR_FAILURE; // brade: eventually should look for a base url in the document if present nsresult rv; nsCOMPtr trav = do_QueryInterface(domDoc, &rv); NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); PRInt32 listCount = aNodeList.Count(); PRInt32 j; for (j = 0; j < listCount; j++) { nsIDOMNode* somenode = aNodeList[j]; nsCOMPtr walker; rv = trav->CreateTreeWalker(somenode, nsIDOMNodeFilter::SHOW_ELEMENT, nsnull, PR_TRUE, getter_AddRefs(walker)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr currentNode; walker->GetCurrentNode(getter_AddRefs(currentNode)); while (currentNode) { rv = RelativizeURIForNode(currentNode, destURL); NS_ENSURE_SUCCESS(rv, rv); walker->NextNode(getter_AddRefs(currentNode)); } } return NS_OK; } nsresult nsHTMLEditor::AddInsertionListener(nsIContentFilter *aListener) { if (!aListener) return NS_ERROR_NULL_POINTER; // don't let a listener be added more than once if (mContentFilters.IndexOfObject(aListener) == -1) { if (!mContentFilters.AppendObject(aListener)) return NS_ERROR_FAILURE; } return NS_OK; } nsresult nsHTMLEditor::RemoveInsertionListener(nsIContentFilter *aListener) { if (!aListener) return NS_ERROR_FAILURE; if (!mContentFilters.RemoveObject(aListener)) return NS_ERROR_FAILURE; return NS_OK; } nsresult nsHTMLEditor::DoContentFilterCallback(const nsAString &aFlavor, nsIDOMDocument *sourceDoc, PRBool aWillDeleteSelection, nsIDOMNode **aFragmentAsNode, nsIDOMNode **aFragStartNode, PRInt32 *aFragStartOffset, nsIDOMNode **aFragEndNode, PRInt32 *aFragEndOffset, nsIDOMNode **aTargetNode, PRInt32 *aTargetOffset, PRBool *aDoContinue) { *aDoContinue = PR_TRUE; PRInt32 i; nsIContentFilter *listener; for (i=0; i < mContentFilters.Count() && *aDoContinue; i++) { listener = (nsIContentFilter *)mContentFilters[i]; if (listener) listener->NotifyOfInsertion(aFlavor, nsnull, sourceDoc, aWillDeleteSelection, aFragmentAsNode, aFragStartNode, aFragStartOffset, aFragEndNode, aFragEndOffset, aTargetNode, aTargetOffset, aDoContinue); } return NS_OK; } PRBool nsHTMLEditor::IsInLink(nsIDOMNode *aNode, nsCOMPtr *outLink) { if (!aNode) return PR_FALSE; if (outLink) *outLink = nsnull; nsCOMPtr tmp, node = aNode; while (node) { if (nsHTMLEditUtils::IsLink(node)) { if (outLink) *outLink = node; return PR_TRUE; } tmp = node; tmp->GetParentNode(getter_AddRefs(node)); } return PR_FALSE; } nsresult nsHTMLEditor::StripFormattingNodes(nsIDOMNode *aNode, PRBool aListOnly) { NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER); nsresult res = NS_OK; nsCOMPtr content = do_QueryInterface(aNode); if (content->TextIsOnlyWhitespace()) { nsCOMPtr parent, ignored; aNode->GetParentNode(getter_AddRefs(parent)); if (parent) { if (!aListOnly || nsHTMLEditUtils::IsList(parent)) res = parent->RemoveChild(aNode, getter_AddRefs(ignored)); return res; } } if (!nsHTMLEditUtils::IsPre(aNode)) { nsCOMPtr child; aNode->GetLastChild(getter_AddRefs(child)); while (child) { nsCOMPtr tmp; child->GetPreviousSibling(getter_AddRefs(tmp)); res = StripFormattingNodes(child, aListOnly); NS_ENSURE_SUCCESS(res, res); child = tmp; } } return res; } NS_IMETHODIMP nsHTMLEditor::PrepareTransferable(nsITransferable **transferable) { return NS_OK; } NS_IMETHODIMP nsHTMLEditor::PrepareHTMLTransferable(nsITransferable **aTransferable, PRBool aHavePrivFlavor) { // Create generic Transferable for getting the data nsresult rv = CallCreateInstance("@mozilla.org/widget/transferable;1", aTransferable); if (NS_FAILED(rv)) return rv; // Get the nsITransferable interface for getting the data from the clipboard if (aTransferable) { // Create the desired DataFlavor for the type of data // we want to get out of the transferable if ((mFlags & eEditorPlaintextMask) == 0) // This should only happen in html editors, not plaintext { if (!aHavePrivFlavor) { (*aTransferable)->AddDataFlavor(kNativeHTMLMime); } (*aTransferable)->AddDataFlavor(kHTMLMime); (*aTransferable)->AddDataFlavor(kFileMime); nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); PRInt32 clipboardPasteOrder = 1; // order of image-encoding preference if (prefs) { prefs->GetIntPref("clipboard.paste_image_type", &clipboardPasteOrder); switch (clipboardPasteOrder) { case 0: // prefer JPEG over PNG over GIF encoding (*aTransferable)->AddDataFlavor(kJPEGImageMime); (*aTransferable)->AddDataFlavor(kPNGImageMime); (*aTransferable)->AddDataFlavor(kGIFImageMime); break; case 1: // prefer PNG over JPEG over GIF encoding (default) default: (*aTransferable)->AddDataFlavor(kPNGImageMime); (*aTransferable)->AddDataFlavor(kJPEGImageMime); (*aTransferable)->AddDataFlavor(kGIFImageMime); break; case 2: // prefer GIF over JPEG over PNG encoding (*aTransferable)->AddDataFlavor(kGIFImageMime); (*aTransferable)->AddDataFlavor(kJPEGImageMime); (*aTransferable)->AddDataFlavor(kPNGImageMime); break; } } } (*aTransferable)->AddDataFlavor(kUnicodeMime); (*aTransferable)->AddDataFlavor(kMozTextInternal); } return NS_OK; } PRInt32 FindPositiveIntegerAfterString(const char *aLeadingString, nsCString &aCStr) { // first obtain offsets from cfhtml str PRInt32 numFront = aCStr.Find(aLeadingString); if (numFront == -1) return -1; numFront += strlen(aLeadingString); PRInt32 numBack = aCStr.FindCharInSet(CRLF, numFront); if (numBack == -1) return -1; nsCAutoString numStr(Substring(aCStr, numFront, numBack-numFront)); PRInt32 errorCode; return numStr.ToInteger(&errorCode); } nsresult RemoveFragComments(nsCString & aStr) { // remove the StartFragment/EndFragment comments from the str, if present PRInt32 startCommentIndx = aStr.Find("", PR_FALSE, startCommentIndx); if (startCommentEnd > startCommentIndx) aStr.Cut(startCommentIndx, (startCommentEnd+3)-startCommentIndx); } PRInt32 endCommentIndx = aStr.Find("", PR_FALSE, endCommentIndx); if (endCommentEnd > endCommentIndx) aStr.Cut(endCommentIndx, (endCommentEnd+3)-endCommentIndx); } return NS_OK; } nsresult nsHTMLEditor::ParseCFHTML(nsCString & aCfhtml, PRUnichar **aStuffToPaste, PRUnichar **aCfcontext) { // first obtain offsets from cfhtml str PRInt32 startHTML = FindPositiveIntegerAfterString("StartHTML:", aCfhtml); PRInt32 endHTML = FindPositiveIntegerAfterString("EndHTML:", aCfhtml); PRInt32 startFragment = FindPositiveIntegerAfterString("StartFragment:", aCfhtml); PRInt32 endFragment = FindPositiveIntegerAfterString("EndFragment:", aCfhtml); if ((startHTML<0) || (endHTML<0) || (startFragment<0) || (endFragment<0)) return NS_ERROR_FAILURE; // create context string nsCAutoString contextUTF8(Substring(aCfhtml, startHTML, startFragment - startHTML) + NS_LITERAL_CSTRING("") + Substring(aCfhtml, endFragment, endHTML - endFragment)); // validate startFragment // make sure it's not in the middle of a HTML tag // see bug #228879 for more details PRInt32 curPos = startFragment; while (curPos > startHTML) { if (aCfhtml[curPos] == '>') { // working backwards, the first thing we see is the end of a tag // so StartFragment is good, so do nothing. break; } else if (aCfhtml[curPos] == '<') { // if we are at the start, then we want to see the '<' if (curPos != startFragment) { // working backwards, the first thing we see is the start of a tag // so StartFragment is bad, so we need to update it. NS_ERROR("StartFragment byte count in the clipboard looks bad, see bug #228879"); startFragment = curPos - 1; } break; } else { curPos--; } } // create fragment string nsCAutoString fragmentUTF8(Substring(aCfhtml, startFragment, endFragment-startFragment)); // remove the StartFragment/EndFragment comments from the fragment, if present RemoveFragComments(fragmentUTF8); // remove the StartFragment/EndFragment comments from the context, if present RemoveFragComments(contextUTF8); // convert both strings to usc2 const nsAFlatString& fragUcs2Str = NS_ConvertUTF8toUTF16(fragmentUTF8); const nsAFlatString& cntxtUcs2Str = NS_ConvertUTF8toUTF16(contextUTF8); // translate platform linebreaks for fragment PRInt32 oldLengthInChars = fragUcs2Str.Length() + 1; // +1 to include null terminator PRInt32 newLengthInChars = 0; *aStuffToPaste = nsLinebreakConverter::ConvertUnicharLineBreaks(fragUcs2Str.get(), nsLinebreakConverter::eLinebreakAny, nsLinebreakConverter::eLinebreakContent, oldLengthInChars, &newLengthInChars); if (!aStuffToPaste) { return NS_ERROR_FAILURE; } // translate platform linebreaks for context oldLengthInChars = cntxtUcs2Str.Length() + 1; // +1 to include null terminator newLengthInChars = 0; *aCfcontext = nsLinebreakConverter::ConvertUnicharLineBreaks(cntxtUcs2Str.get(), nsLinebreakConverter::eLinebreakAny, nsLinebreakConverter::eLinebreakContent, oldLengthInChars, &newLengthInChars); // it's ok for context to be empty. frag might be whole doc and contain all it's context. // we're done! return NS_OK; } NS_IMETHODIMP nsHTMLEditor::InsertFromTransferable(nsITransferable *transferable, nsIDOMDocument *aSourceDoc, const nsAString & aContextStr, const nsAString & aInfoStr, nsIDOMNode *aDestinationNode, PRInt32 aDestOffset, PRBool aDoDeleteSelection) { nsresult rv = NS_OK; nsXPIDLCString bestFlavor; nsCOMPtr genericDataObj; PRUint32 len = 0; if ( NS_SUCCEEDED(transferable->GetAnyTransferData(getter_Copies(bestFlavor), getter_AddRefs(genericDataObj), &len)) ) { nsAutoTxnsConserveSelection dontSpazMySelection(this); nsAutoString flavor; flavor.AssignWithConversion(bestFlavor); nsAutoString stuffToPaste; #ifdef DEBUG_clipboard printf("Got flavor [%s]\n", bestFlavor); #endif if (0 == nsCRT::strcmp(bestFlavor, kNativeHTMLMime)) { // note cf_html uses utf8, hence use length = len, not len/2 as in flavors below nsCOMPtr textDataObj(do_QueryInterface(genericDataObj)); if (textDataObj && len > 0) { nsCAutoString cfhtml; textDataObj->GetData(cfhtml); NS_ASSERTION(cfhtml.Length() <= (len), "Invalid length!"); nsXPIDLString cfcontext, cffragment, cfselection; // cfselection left emtpy for now rv = ParseCFHTML(cfhtml, getter_Copies(cffragment), getter_Copies(cfcontext)); if (NS_SUCCEEDED(rv) && !cffragment.IsEmpty()) { nsAutoEditBatch beginBatching(this); rv = InsertHTMLWithContext(cffragment, cfcontext, cfselection, flavor, aSourceDoc, aDestinationNode, aDestOffset, aDoDeleteSelection); } } } else if (0 == nsCRT::strcmp(bestFlavor, kHTMLMime)) { nsCOMPtr textDataObj(do_QueryInterface(genericDataObj)); if (textDataObj && len > 0) { nsAutoString text; textDataObj->GetData(text); NS_ASSERTION(text.Length() <= (len/2), "Invalid length!"); stuffToPaste.Assign(text.get(), len / 2); nsAutoEditBatch beginBatching(this); rv = InsertHTMLWithContext(stuffToPaste, aContextStr, aInfoStr, flavor, aSourceDoc, aDestinationNode, aDestOffset, aDoDeleteSelection); } } else if (0 == nsCRT::strcmp(bestFlavor, kUnicodeMime) || 0 == nsCRT::strcmp(bestFlavor, kMozTextInternal)) { nsCOMPtr textDataObj(do_QueryInterface(genericDataObj)); if (textDataObj && len > 0) { nsAutoString text; textDataObj->GetData(text); NS_ASSERTION(text.Length() <= (len/2), "Invalid length!"); stuffToPaste.Assign(text.get(), len / 2); nsAutoEditBatch beginBatching(this); // need to provide a hook from this point rv = InsertTextAt(stuffToPaste, aDestinationNode, aDestOffset, aDoDeleteSelection); } } else if (0 == nsCRT::strcmp(bestFlavor, kFileMime)) { nsCOMPtr fileObj(do_QueryInterface(genericDataObj)); if (fileObj && len > 0) { nsCOMPtr uri; rv = NS_NewFileURI(getter_AddRefs(uri), fileObj); if (NS_FAILED(rv)) return rv; nsCOMPtr fileURL(do_QueryInterface(uri)); if (fileURL) { PRBool insertAsImage = PR_FALSE; nsCAutoString fileextension; rv = fileURL->GetFileExtension(fileextension); if (NS_SUCCEEDED(rv) && !fileextension.IsEmpty()) { if ( (nsCRT::strcasecmp(fileextension.get(), "jpg") == 0 ) || (nsCRT::strcasecmp(fileextension.get(), "jpeg") == 0 ) || (nsCRT::strcasecmp(fileextension.get(), "gif") == 0 ) || (nsCRT::strcasecmp(fileextension.get(), "png") == 0 ) ) { insertAsImage = PR_TRUE; } } nsCAutoString urltext; rv = fileURL->GetSpec(urltext); if (NS_SUCCEEDED(rv) && !urltext.IsEmpty()) { if (insertAsImage) { stuffToPaste.AssignLiteral("\"\""); } else /* insert as link */ { stuffToPaste.AssignLiteral(""); AppendUTF8toUTF16(urltext, stuffToPaste); stuffToPaste.AppendLiteral(""); } nsAutoEditBatch beginBatching(this); const nsAFlatString& empty = EmptyString(); rv = InsertHTMLWithContext(stuffToPaste, empty, empty, flavor, aSourceDoc, aDestinationNode, aDestOffset, aDoDeleteSelection); } } } } else if (0 == nsCRT::strcmp(bestFlavor, kJPEGImageMime) || 0 == nsCRT::strcmp(bestFlavor, kPNGImageMime) || 0 == nsCRT::strcmp(bestFlavor, kGIFImageMime)) { nsCOMPtr imageStream(do_QueryInterface(genericDataObj)); NS_ENSURE_TRUE(imageStream, NS_ERROR_FAILURE); nsCOMPtr fileToUse; NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(fileToUse)); if (0 == nsCRT::strcmp(bestFlavor, kJPEGImageMime)) fileToUse->Append(NS_LITERAL_STRING("moz-screenshot.jpg")); else if (0 == nsCRT::strcmp(bestFlavor, kPNGImageMime)) fileToUse->Append(NS_LITERAL_STRING("moz-screenshot.png")); else if (0 == nsCRT::strcmp(bestFlavor, kGIFImageMime)) fileToUse->Append(NS_LITERAL_STRING("moz-screenshot.gif")); nsCOMPtr path = do_QueryInterface(fileToUse); path->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); nsCOMPtr outputStream; rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream), fileToUse); NS_ENSURE_SUCCESS(rv, rv); PRUint32 length; imageStream->Available(&length); nsCOMPtr bufferedOutputStream; rv = NS_NewBufferedOutputStream(getter_AddRefs(bufferedOutputStream), outputStream, length); NS_ENSURE_SUCCESS(rv, rv); PRUint32 numWritten; rv = bufferedOutputStream->WriteFrom(imageStream, length, &numWritten); NS_ENSURE_SUCCESS(rv, rv); // force the stream close before we try to insert the image // into the document. rv = bufferedOutputStream->Close(); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr uri; rv = NS_NewFileURI(getter_AddRefs(uri), fileToUse); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr fileURL(do_QueryInterface(uri)); if (fileURL) { nsCAutoString urltext; rv = fileURL->GetSpec(urltext); if (NS_SUCCEEDED(rv) && !urltext.IsEmpty()) { stuffToPaste.AssignLiteral("\"\""); nsAutoEditBatch beginBatching(this); rv = InsertHTMLWithContext(stuffToPaste, EmptyString(), EmptyString(), NS_LITERAL_STRING(kFileMime), aSourceDoc, aDestinationNode, aDestOffset, aDoDeleteSelection); } } } } // Try to scroll the selection into view if the paste/drop succeeded // After ScrollSelectionIntoView(), the pending notifications might be // flushed and PresShell/PresContext/Frames may be dead. See bug 418470. if (NS_SUCCEEDED(rv)) ScrollSelectionIntoView(PR_FALSE); return rv; } NS_IMETHODIMP nsHTMLEditor::InsertFromDrop(nsIDOMEvent* aDropEvent) { ForceCompositionEnd(); nsresult rv; nsCOMPtr dragService = do_GetService("@mozilla.org/widget/dragservice;1", &rv); if (NS_FAILED(rv)) return rv; nsCOMPtr dragSession; dragService->GetCurrentSession(getter_AddRefs(dragSession)); if (!dragSession) return NS_OK; // transferable hooks here nsCOMPtr domdoc; GetDocument(getter_AddRefs(domdoc)); // find out if we have our internal html flavor on the clipboard. We don't want to mess // around with cfhtml if we do. PRBool bHavePrivateHTMLFlavor = PR_FALSE; rv = dragSession->IsDataFlavorSupported(kHTMLContext, &bHavePrivateHTMLFlavor); if (NS_FAILED(rv)) return rv; // Get the nsITransferable interface for getting the data from the drop nsCOMPtr trans; rv = PrepareHTMLTransferable(getter_AddRefs(trans), bHavePrivateHTMLFlavor); if (NS_FAILED(rv)) return rv; if (!trans) return NS_OK; // NS_ERROR_FAILURE; SHOULD WE FAIL? PRUint32 numItems = 0; rv = dragSession->GetNumDropItems(&numItems); if (NS_FAILED(rv)) return rv; // Combine any deletion and drop insertion into one transaction nsAutoEditBatch beginBatching(this); // We never have to delete if selection is already collapsed PRBool deleteSelection = PR_FALSE; nsCOMPtr newSelectionParent; PRInt32 newSelectionOffset = 0; // Source doc is null if source is *not* the current editor document nsCOMPtr srcdomdoc; rv = dragSession->GetSourceDocument(getter_AddRefs(srcdomdoc)); if (NS_FAILED(rv)) return rv; PRUint32 i; PRBool doPlaceCaret = PR_TRUE; for (i = 0; i < numItems; ++i) { rv = dragSession->GetData(trans, i); if (NS_FAILED(rv)) return rv; if (!trans) return NS_OK; // NS_ERROR_FAILURE; Should we fail? // get additional html copy hints, if present nsAutoString contextStr, infoStr; nsCOMPtr contextDataObj, infoDataObj; PRUint32 contextLen, infoLen; nsCOMPtr textDataObj; nsCOMPtr contextTrans = do_CreateInstance("@mozilla.org/widget/transferable;1"); NS_ENSURE_TRUE(contextTrans, NS_ERROR_NULL_POINTER); contextTrans->AddDataFlavor(kHTMLContext); dragSession->GetData(contextTrans, i); contextTrans->GetTransferData(kHTMLContext, getter_AddRefs(contextDataObj), &contextLen); nsCOMPtr infoTrans = do_CreateInstance("@mozilla.org/widget/transferable;1"); NS_ENSURE_TRUE(infoTrans, NS_ERROR_NULL_POINTER); infoTrans->AddDataFlavor(kHTMLInfo); dragSession->GetData(infoTrans, i); infoTrans->GetTransferData(kHTMLInfo, getter_AddRefs(infoDataObj), &infoLen); if (contextDataObj) { nsAutoString text; textDataObj = do_QueryInterface(contextDataObj); textDataObj->GetData(text); NS_ASSERTION(text.Length() <= (contextLen/2), "Invalid length!"); contextStr.Assign(text.get(), contextLen / 2); } if (infoDataObj) { nsAutoString text; textDataObj = do_QueryInterface(infoDataObj); textDataObj->GetData(text); NS_ASSERTION(text.Length() <= (infoLen/2), "Invalid length!"); infoStr.Assign(text.get(), infoLen / 2); } if (doPlaceCaret) { // check if the user pressed the key to force a copy rather than a move // if we run into problems here, we'll just assume the user doesn't want a copy PRBool userWantsCopy = PR_FALSE; nsCOMPtr nsuiEvent(do_QueryInterface(aDropEvent)); if (!nsuiEvent) return NS_ERROR_FAILURE; nsCOMPtr mouseEvent(do_QueryInterface(aDropEvent)); if (mouseEvent) #if defined(XP_MAC) || defined(XP_MACOSX) mouseEvent->GetAltKey(&userWantsCopy); #else mouseEvent->GetCtrlKey(&userWantsCopy); #endif // Current doc is destination nsCOMPtrdestdomdoc; rv = GetDocument(getter_AddRefs(destdomdoc)); if (NS_FAILED(rv)) return rv; nsCOMPtr selection; rv = GetSelection(getter_AddRefs(selection)); if (NS_FAILED(rv)) return rv; if (!selection) return NS_ERROR_FAILURE; PRBool isCollapsed; rv = selection->GetIsCollapsed(&isCollapsed); if (NS_FAILED(rv)) return rv; // Parent and offset under the mouse cursor rv = nsuiEvent->GetRangeParent(getter_AddRefs(newSelectionParent)); if (NS_FAILED(rv)) return rv; if (!newSelectionParent) return NS_ERROR_FAILURE; rv = nsuiEvent->GetRangeOffset(&newSelectionOffset); if (NS_FAILED(rv)) return rv; // XXX: This userSelectNode code is a workaround for bug 195957. // // Check to see if newSelectionParent is part of a "-moz-user-select: all" // subtree. If it is, we need to make sure we don't drop into it! nsCOMPtr userSelectNode = FindUserSelectAllNode(newSelectionParent); if (userSelectNode) { // The drop is happening over a "-moz-user-select: all" // subtree so make sure the content we insert goes before // the root of the subtree. // // XXX: Note that inserting before the subtree matches the // current behavior when dropping on top of an image. // The decision for dropping before or after the // subtree should really be done based on coordinates. rv = GetNodeLocation(userSelectNode, address_of(newSelectionParent), &newSelectionOffset); if (NS_FAILED(rv)) return rv; if (!newSelectionParent) return NS_ERROR_FAILURE; } // We never have to delete if selection is already collapsed PRBool cursorIsInSelection = PR_FALSE; // Check if mouse is in the selection if (!isCollapsed) { PRInt32 rangeCount; rv = selection->GetRangeCount(&rangeCount); if (NS_FAILED(rv)) return rv; for (PRInt32 j = 0; j < rangeCount; j++) { nsCOMPtr range; rv = selection->GetRangeAt(j, getter_AddRefs(range)); if (NS_FAILED(rv) || !range) continue;//don't bail yet, iterate through them all nsCOMPtr nsrange(do_QueryInterface(range)); if (NS_FAILED(rv) || !nsrange) continue;//don't bail yet, iterate through them all rv = nsrange->IsPointInRange(newSelectionParent, newSelectionOffset, &cursorIsInSelection); if(cursorIsInSelection) break; } if (cursorIsInSelection) { // Dragging within same doc can't drop on itself -- leave! // (We shouldn't get here - drag event shouldn't have started if over selection) if (srcdomdoc == destdomdoc) return NS_OK; // Dragging from another window onto a selection // XXX Decision made to NOT do this, // note that 4.x does replace if dropped on //deleteSelection = PR_TRUE; } else { // We are NOT over the selection if (srcdomdoc == destdomdoc) { // Within the same doc: delete if user doesn't want to copy deleteSelection = !userWantsCopy; } else { // Different source doc: Don't delete deleteSelection = PR_FALSE; } } } // We have to figure out whether to delete/relocate caret only once doPlaceCaret = PR_FALSE; } // handle transferable hooks if (!nsEditorHookUtils::DoInsertionHook(domdoc, aDropEvent, trans)) return NS_OK; // Beware! This may flush notifications via synchronous // ScrollSelectionIntoView. rv = InsertFromTransferable(trans, srcdomdoc, contextStr, infoStr, newSelectionParent, newSelectionOffset, deleteSelection); } return rv; } NS_IMETHODIMP nsHTMLEditor::CanDrag(nsIDOMEvent *aDragEvent, PRBool *aCanDrag) { return nsPlaintextEditor::CanDrag(aDragEvent, aCanDrag); } nsresult nsHTMLEditor::PutDragDataInTransferable(nsITransferable **aTransferable) { NS_ENSURE_ARG_POINTER(aTransferable); *aTransferable = nsnull; nsCOMPtr docEncoder; nsresult rv = SetupDocEncoder(getter_AddRefs(docEncoder)); if (NS_FAILED(rv)) return rv; NS_ENSURE_TRUE(docEncoder, NS_ERROR_FAILURE); // grab a string nsAutoString buffer, parents, info; // find out if we're a plaintext control or not PRUint32 editorFlags = 0; rv = GetFlags(&editorFlags); if (NS_FAILED(rv)) return rv; PRBool bIsPlainTextControl = ((editorFlags & eEditorPlaintextMask) != 0); if (!bIsPlainTextControl) { // encode the selection as html with contextual info rv = docEncoder->EncodeToStringWithContext(parents, info, buffer); if (NS_FAILED(rv)) return rv; } else { // encode the selection rv = docEncoder->EncodeToString(buffer); if (NS_FAILED(rv)) return rv; } // if we have an empty string, we're done; otherwise continue if ( buffer.IsEmpty() ) return NS_OK; nsCOMPtr dataWrapper, contextWrapper, infoWrapper; dataWrapper = do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); rv = dataWrapper->SetData(buffer); if (NS_FAILED(rv)) return rv; /* create html flavor transferable */ nsCOMPtr trans = do_CreateInstance("@mozilla.org/widget/transferable;1"); NS_ENSURE_TRUE(trans, NS_ERROR_FAILURE); if (bIsPlainTextControl) { // Add the unicode flavor to the transferable rv = trans->AddDataFlavor(kUnicodeMime); if (NS_FAILED(rv)) return rv; // QI the data object an |nsISupports| so that when the transferable holds // onto it, it will addref the correct interface. nsCOMPtr genericDataObj(do_QueryInterface(dataWrapper)); rv = trans->SetTransferData(kUnicodeMime, genericDataObj, buffer.Length() * sizeof(PRUnichar)); if (NS_FAILED(rv)) return rv; } else { contextWrapper = do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID); NS_ENSURE_TRUE(contextWrapper, NS_ERROR_FAILURE); infoWrapper = do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID); NS_ENSURE_TRUE(infoWrapper, NS_ERROR_FAILURE); contextWrapper->SetData(parents); infoWrapper->SetData(info); rv = trans->AddDataFlavor(kHTMLMime); if (NS_FAILED(rv)) return rv; nsCOMPtr htmlConverter = do_CreateInstance("@mozilla.org/widget/htmlformatconverter;1"); NS_ENSURE_TRUE(htmlConverter, NS_ERROR_FAILURE); rv = trans->SetConverter(htmlConverter); if (NS_FAILED(rv)) return rv; nsCOMPtr genericDataObj(do_QueryInterface(dataWrapper)); rv = trans->SetTransferData(kHTMLMime, genericDataObj, buffer.Length() * sizeof(PRUnichar)); if (NS_FAILED(rv)) return rv; if (!parents.IsEmpty()) { // Add the htmlcontext DataFlavor to the transferable trans->AddDataFlavor(kHTMLContext); genericDataObj = do_QueryInterface(contextWrapper); trans->SetTransferData(kHTMLContext, genericDataObj, parents.Length() * sizeof(PRUnichar)); } if (!info.IsEmpty()) { // Add the htmlinfo DataFlavor to the transferable trans->AddDataFlavor(kHTMLInfo); genericDataObj = do_QueryInterface(infoWrapper); trans->SetTransferData(kHTMLInfo, genericDataObj, info.Length() * sizeof(PRUnichar)); } } *aTransferable = trans; NS_ADDREF(*aTransferable); return NS_OK; } NS_IMETHODIMP nsHTMLEditor::DoDrag(nsIDOMEvent *aDragEvent) { return nsPlaintextEditor::DoDrag(aDragEvent); } PRBool nsHTMLEditor::HavePrivateHTMLFlavor(nsIClipboard *aClipboard) { // check the clipboard for our special kHTMLContext flavor. If that is there, we know // we have our own internal html format on clipboard. if (!aClipboard) return PR_FALSE; PRBool bHavePrivateHTMLFlavor = PR_FALSE; const char* flavArray[] = { kHTMLContext }; if (NS_SUCCEEDED(aClipboard->HasDataMatchingFlavors(flavArray, NS_ARRAY_LENGTH(flavArray), nsIClipboard::kGlobalClipboard, &bHavePrivateHTMLFlavor ))) return bHavePrivateHTMLFlavor; return PR_FALSE; } NS_IMETHODIMP nsHTMLEditor::Paste(PRInt32 aSelectionType) { ForceCompositionEnd(); PRBool preventDefault; nsresult rv = FireClipboardEvent(NS_PASTE, &preventDefault); if (NS_FAILED(rv) || preventDefault) return rv; // Get Clipboard Service nsCOMPtr clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv)); if (NS_FAILED(rv)) return rv; // find out if we have our internal html flavor on the clipboard. We don't want to mess // around with cfhtml if we do. PRBool bHavePrivateHTMLFlavor = HavePrivateHTMLFlavor(clipboard); // Get the nsITransferable interface for getting the data from the clipboard nsCOMPtr trans; rv = PrepareHTMLTransferable(getter_AddRefs(trans), bHavePrivateHTMLFlavor); if (NS_SUCCEEDED(rv) && trans) { // Get the Data from the clipboard if (NS_SUCCEEDED(clipboard->GetData(trans, aSelectionType)) && IsModifiable()) { // also get additional html copy hints, if present nsAutoString contextStr, infoStr; // also get additional html copy hints, if present if (bHavePrivateHTMLFlavor) { nsCOMPtr contextDataObj, infoDataObj; PRUint32 contextLen, infoLen; nsCOMPtr textDataObj; nsCOMPtr contextTrans = do_CreateInstance("@mozilla.org/widget/transferable;1"); NS_ENSURE_TRUE(contextTrans, NS_ERROR_NULL_POINTER); contextTrans->AddDataFlavor(kHTMLContext); clipboard->GetData(contextTrans, aSelectionType); contextTrans->GetTransferData(kHTMLContext, getter_AddRefs(contextDataObj), &contextLen); nsCOMPtr infoTrans = do_CreateInstance("@mozilla.org/widget/transferable;1"); NS_ENSURE_TRUE(infoTrans, NS_ERROR_NULL_POINTER); infoTrans->AddDataFlavor(kHTMLInfo); clipboard->GetData(infoTrans, aSelectionType); infoTrans->GetTransferData(kHTMLInfo, getter_AddRefs(infoDataObj), &infoLen); if (contextDataObj) { nsAutoString text; textDataObj = do_QueryInterface(contextDataObj); textDataObj->GetData(text); NS_ASSERTION(text.Length() <= (contextLen/2), "Invalid length!"); contextStr.Assign(text.get(), contextLen / 2); } if (infoDataObj) { nsAutoString text; textDataObj = do_QueryInterface(infoDataObj); textDataObj->GetData(text); NS_ASSERTION(text.Length() <= (infoLen/2), "Invalid length!"); infoStr.Assign(text.get(), infoLen / 2); } } // handle transferable hooks nsCOMPtr domdoc; GetDocument(getter_AddRefs(domdoc)); if (!nsEditorHookUtils::DoInsertionHook(domdoc, nsnull, trans)) return NS_OK; // Beware! This may flush notifications via synchronous // ScrollSelectionIntoView. rv = InsertFromTransferable(trans, nsnull, contextStr, infoStr, nsnull, 0, PR_TRUE); } } return rv; } // // HTML PasteNoFormatting. Ignore any HTML styles and formating in paste source // NS_IMETHODIMP nsHTMLEditor::PasteNoFormatting(PRInt32 aSelectionType) { ForceCompositionEnd(); // Get Clipboard Service nsresult rv; nsCOMPtr clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv)); if (NS_FAILED(rv)) return rv; // Get the nsITransferable interface for getting the data from the clipboard. // use nsPlaintextEditor::PrepareTransferable() to force unicode plaintext data. nsCOMPtr trans; rv = nsPlaintextEditor::PrepareTransferable(getter_AddRefs(trans)); if (NS_SUCCEEDED(rv) && trans) { // Get the Data from the clipboard if (NS_SUCCEEDED(clipboard->GetData(trans, aSelectionType)) && IsModifiable()) { const nsAFlatString& empty = EmptyString(); // Beware! This may flush notifications via synchronous // ScrollSelectionIntoView. rv = InsertFromTransferable(trans, nsnull, empty, empty, nsnull, 0, PR_TRUE); } } return rv; } NS_IMETHODIMP nsHTMLEditor::CanPaste(PRInt32 aSelectionType, PRBool *aCanPaste) { NS_ENSURE_ARG_POINTER(aCanPaste); *aCanPaste = PR_FALSE; // can't paste if readonly if (!IsModifiable()) return NS_OK; nsresult rv; nsCOMPtr clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv)); if (NS_FAILED(rv)) return rv; // the flavors that we can deal with (preferred order selectable for k*ImageMime) const char* textEditorFlavors[] = { kUnicodeMime }; const char* textHtmlEditorFlavors[] = { kUnicodeMime, kHTMLMime, kJPEGImageMime, kPNGImageMime, kGIFImageMime }; PRUint32 editorFlags; GetFlags(&editorFlags); PRBool haveFlavors; // Use the flavors depending on the current editor mask if ((editorFlags & eEditorPlaintextMask)) rv = clipboard->HasDataMatchingFlavors(textEditorFlavors, NS_ARRAY_LENGTH(textEditorFlavors), aSelectionType, &haveFlavors); else rv = clipboard->HasDataMatchingFlavors(textHtmlEditorFlavors, NS_ARRAY_LENGTH(textHtmlEditorFlavors), aSelectionType, &haveFlavors); if (NS_FAILED(rv)) return rv; *aCanPaste = haveFlavors; return NS_OK; } // // HTML PasteAsQuotation: Paste in a blockquote type=cite // NS_IMETHODIMP nsHTMLEditor::PasteAsQuotation(PRInt32 aSelectionType) { if (mFlags & eEditorPlaintextMask) return PasteAsPlaintextQuotation(aSelectionType); nsAutoString citation; return PasteAsCitedQuotation(citation, aSelectionType); } NS_IMETHODIMP nsHTMLEditor::PasteAsCitedQuotation(const nsAString & aCitation, PRInt32 aSelectionType) { nsAutoEditBatch beginBatching(this); nsAutoRules beginRulesSniffing(this, kOpInsertQuotation, nsIEditor::eNext); // get selection nsCOMPtr selection; nsresult res = GetSelection(getter_AddRefs(selection)); if (NS_FAILED(res)) return res; if (!selection) return NS_ERROR_NULL_POINTER; // give rules a chance to handle or cancel nsTextRulesInfo ruleInfo(nsTextEditRules::kInsertElement); PRBool cancel, handled; res = mRules->WillDoAction(selection, &ruleInfo, &cancel, &handled); if (NS_FAILED(res)) return res; if (cancel) return NS_OK; // rules canceled the operation if (!handled) { nsCOMPtr newNode; res = DeleteSelectionAndCreateNode(NS_LITERAL_STRING("blockquote"), getter_AddRefs(newNode)); if (NS_FAILED(res)) return res; if (!newNode) return NS_ERROR_NULL_POINTER; // Try to set type=cite. Ignore it if this fails. nsCOMPtr newElement (do_QueryInterface(newNode)); if (newElement) { newElement->SetAttribute(NS_LITERAL_STRING("type"), NS_LITERAL_STRING("cite")); } // Set the selection to the underneath the node we just inserted: res = selection->Collapse(newNode, 0); if (NS_FAILED(res)) { #ifdef DEBUG_akkana printf("Couldn't collapse"); #endif // XXX: error result: should res be returned here? } res = Paste(aSelectionType); } return res; } // // Paste a plaintext quotation // NS_IMETHODIMP nsHTMLEditor::PasteAsPlaintextQuotation(PRInt32 aSelectionType) { // Get Clipboard Service nsresult rv; nsCOMPtr clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv)); if (NS_FAILED(rv)) return rv; // Create generic Transferable for getting the data nsCOMPtr trans = do_CreateInstance("@mozilla.org/widget/transferable;1", &rv); if (NS_SUCCEEDED(rv) && trans) { // We only handle plaintext pastes here trans->AddDataFlavor(kUnicodeMime); // Get the Data from the clipboard clipboard->GetData(trans, aSelectionType); // Now we ask the transferable for the data // it still owns the data, we just have a pointer to it. // If it can't support a "text" output of the data the call will fail nsCOMPtr genericDataObj; PRUint32 len = 0; char* flav = 0; rv = trans->GetAnyTransferData(&flav, getter_AddRefs(genericDataObj), &len); if (NS_FAILED(rv)) { #ifdef DEBUG_akkana printf("PasteAsPlaintextQuotation: GetAnyTransferData failed, %d\n", rv); #endif return rv; } if (flav && 0 == nsCRT::strcmp((flav), kUnicodeMime)) { #ifdef DEBUG_clipboard printf("Got flavor [%s]\n", flav); #endif nsCOMPtr textDataObj(do_QueryInterface(genericDataObj)); if (textDataObj && len > 0) { nsAutoString stuffToPaste; textDataObj->GetData(stuffToPaste); NS_ASSERTION(stuffToPaste.Length() <= (len/2), "Invalid length!"); nsAutoEditBatch beginBatching(this); rv = InsertAsPlaintextQuotation(stuffToPaste, PR_TRUE, 0); } } NS_Free(flav); } return rv; } NS_IMETHODIMP nsHTMLEditor::InsertTextWithQuotations(const nsAString &aStringToInsert) { if (mWrapToWindow) return InsertText(aStringToInsert); // The whole operation should be undoable in one transaction: BeginTransaction(); // We're going to loop over the string, collecting up a "hunk" // that's all the same type (quoted or not), // Whenever the quotedness changes (or we reach the string's end) // we will insert the hunk all at once, quoted or non. static const PRUnichar cite('>'); PRBool curHunkIsQuoted = (aStringToInsert.First() == cite); nsAString::const_iterator hunkStart, strEnd; aStringToInsert.BeginReading(hunkStart); aStringToInsert.EndReading(strEnd); // In the loop below, we only look for DOM newlines (\n), // because we don't have a FindChars method that can look // for both \r and \n. \r is illegal in the dom anyway, // but in debug builds, let's take the time to verify that // there aren't any there: #ifdef DEBUG nsAString::const_iterator dbgStart (hunkStart); if (FindCharInReadable('\r', dbgStart, strEnd)) NS_ASSERTION(PR_FALSE, "Return characters in DOM! InsertTextWithQuotations may be wrong"); #endif /* DEBUG */ // Loop over lines: nsresult rv = NS_OK; nsAString::const_iterator lineStart (hunkStart); while (1) // we will break from inside when we run out of newlines { // Search for the end of this line (dom newlines, see above): PRBool found = FindCharInReadable('\n', lineStart, strEnd); PRBool quoted = PR_FALSE; if (found) { // if there's another newline, lineStart now points there. // Loop over any consecutive newline chars: nsAString::const_iterator firstNewline (lineStart); while (*lineStart == '\n') ++lineStart; quoted = (*lineStart == cite); if (quoted == curHunkIsQuoted) continue; // else we're changing state, so we need to insert // from curHunk to lineStart then loop around. // But if the current hunk is quoted, then we want to make sure // that any extra newlines on the end do not get included in // the quoted section: blank lines flaking a quoted section // should be considered unquoted, so that if the user clicks // there and starts typing, the new text will be outside of // the quoted block. if (curHunkIsQuoted) lineStart = firstNewline; } // If no newline found, lineStart is now strEnd and we can finish up, // inserting from curHunk to lineStart then returning. const nsAString &curHunk = Substring(hunkStart, lineStart); nsCOMPtr dummyNode; #ifdef DEBUG_akkana_verbose printf("==== Inserting text as %squoted: ---\n%s---\n", curHunkIsQuoted ? "" : "non-", NS_LossyConvertUTF16toASCII(curHunk).get()); #endif if (curHunkIsQuoted) rv = InsertAsPlaintextQuotation(curHunk, PR_FALSE, getter_AddRefs(dummyNode)); else rv = InsertText(curHunk); if (!found) break; curHunkIsQuoted = quoted; hunkStart = lineStart; } EndTransaction(); return rv; } NS_IMETHODIMP nsHTMLEditor::InsertAsQuotation(const nsAString & aQuotedText, nsIDOMNode **aNodeInserted) { if (mFlags & eEditorPlaintextMask) return InsertAsPlaintextQuotation(aQuotedText, PR_TRUE, aNodeInserted); nsAutoString citation; return InsertAsCitedQuotation(aQuotedText, citation, PR_FALSE, aNodeInserted); } // Insert plaintext as a quotation, with cite marks (e.g. "> "). // This differs from its corresponding method in nsPlaintextEditor // in that here, quoted material is enclosed in a
 tag
// in order to preserve the original line wrapping.
NS_IMETHODIMP
nsHTMLEditor::InsertAsPlaintextQuotation(const nsAString & aQuotedText,
                                         PRBool aAddCites,
                                         nsIDOMNode **aNodeInserted)
{
  if (mWrapToWindow)
    return nsPlaintextEditor::InsertAsQuotation(aQuotedText, aNodeInserted);

  nsresult rv;

  // The quotesPreformatted pref is a temporary measure. See bug 69638.
  // Eventually we'll pick one way or the other.
  PRBool quotesInPre = PR_FALSE;
  nsCOMPtr prefBranch =
    do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
  if (NS_SUCCEEDED(rv) && prefBranch)
    prefBranch->GetBoolPref("editor.quotesPreformatted", "esInPre);

  nsCOMPtr preNode;
  // get selection
  nsCOMPtr selection;
  rv = GetSelection(getter_AddRefs(selection));
  if (NS_FAILED(rv)) return rv;
  if (!selection) return NS_ERROR_NULL_POINTER;
  else
  {
    nsAutoEditBatch beginBatching(this);
    nsAutoRules beginRulesSniffing(this, kOpInsertQuotation, nsIEditor::eNext);

    // give rules a chance to handle or cancel
    nsTextRulesInfo ruleInfo(nsTextEditRules::kInsertElement);
    PRBool cancel, handled;
    rv = mRules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
    if (NS_FAILED(rv)) return rv;
    if (cancel) return NS_OK; // rules canceled the operation
    if (!handled)
    {
      // Wrap the inserted quote in a 
 so it won't be wrapped:
      nsAutoString tag;
      if (quotesInPre)
        tag.AssignLiteral("pre");
      else
        tag.AssignLiteral("span");

      rv = DeleteSelectionAndCreateNode(tag, getter_AddRefs(preNode));
      
      // If this succeeded, then set selection inside the pre
      // so the inserted text will end up there.
      // If it failed, we don't care what the return value was,
      // but we'll fall through and try to insert the text anyway.
      if (NS_SUCCEEDED(rv) && preNode)
      {
        // Add an attribute on the pre node so we'll know it's a quotation.
        // Do this after the insertion, so that 
        nsCOMPtr preElement (do_QueryInterface(preNode));
        if (preElement)
        {
          preElement->SetAttribute(NS_LITERAL_STRING("_moz_quote"),
                                   NS_LITERAL_STRING("true"));
          if (quotesInPre)
          {
            // set style to not have unwanted vertical margins
            preElement->SetAttribute(NS_LITERAL_STRING("style"),
                                     NS_LITERAL_STRING("margin: 0 0 0 0px;"));
          }
          else
          {
            // turn off wrapping on spans
            preElement->SetAttribute(NS_LITERAL_STRING("style"),
                                     NS_LITERAL_STRING("white-space: pre;"));
          }
        }

        // and set the selection inside it:
        selection->Collapse(preNode, 0);
      }

      if (aAddCites)
        rv = nsPlaintextEditor::InsertAsQuotation(aQuotedText, aNodeInserted);
      else
        rv = nsPlaintextEditor::InsertText(aQuotedText);
      // Note that if !aAddCites, aNodeInserted isn't set.
      // That's okay because the routines that use aAddCites
      // don't need to know the inserted node.

      if (aNodeInserted && NS_SUCCEEDED(rv))
      {
        *aNodeInserted = preNode;
        NS_IF_ADDREF(*aNodeInserted);
      }
    }
  }
    
  // Set the selection to just after the inserted node:
  if (NS_SUCCEEDED(rv) && preNode)
  {
    nsCOMPtr parent;
    PRInt32 offset;
    if (NS_SUCCEEDED(GetNodeLocation(preNode, address_of(parent), &offset)) && parent)
      selection->Collapse(parent, offset+1);
  }
  return rv;
}

NS_IMETHODIMP    
nsHTMLEditor::StripCites()
{
  return nsPlaintextEditor::StripCites();
}

NS_IMETHODIMP    
nsHTMLEditor::Rewrap(PRBool aRespectNewlines)
{
  return nsPlaintextEditor::Rewrap(aRespectNewlines);
}

NS_IMETHODIMP
nsHTMLEditor::InsertAsCitedQuotation(const nsAString & aQuotedText,
                                     const nsAString & aCitation,
                                     PRBool aInsertHTML,
                                     nsIDOMNode **aNodeInserted)
{
  // Don't let anyone insert html into a "plaintext" editor:
  if (mFlags & eEditorPlaintextMask)
  {
    NS_ASSERTION(!aInsertHTML, "InsertAsCitedQuotation: trying to insert html into plaintext editor");
    return InsertAsPlaintextQuotation(aQuotedText, PR_TRUE, aNodeInserted);
  }

  nsCOMPtr newNode;
  nsresult res = NS_OK;

  // get selection
  nsCOMPtr selection;
  res = GetSelection(getter_AddRefs(selection));
  if (NS_FAILED(res)) return res;
  if (!selection) return NS_ERROR_NULL_POINTER;
  else
  {
    nsAutoEditBatch beginBatching(this);
    nsAutoRules beginRulesSniffing(this, kOpInsertQuotation, nsIEditor::eNext);

    // give rules a chance to handle or cancel
    nsTextRulesInfo ruleInfo(nsTextEditRules::kInsertElement);
    PRBool cancel, handled;
    res = mRules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
    if (NS_FAILED(res)) return res;
    if (cancel) return NS_OK; // rules canceled the operation
    if (!handled)
    {
      res = DeleteSelectionAndCreateNode(NS_LITERAL_STRING("blockquote"), getter_AddRefs(newNode));
      if (NS_FAILED(res)) return res;
      if (!newNode) return NS_ERROR_NULL_POINTER;

      // Try to set type=cite.  Ignore it if this fails.
      nsCOMPtr newElement (do_QueryInterface(newNode));
      if (newElement)
      {
        NS_NAMED_LITERAL_STRING(citestr, "cite");
        newElement->SetAttribute(NS_LITERAL_STRING("type"), citestr);

        if (!aCitation.IsEmpty())
          newElement->SetAttribute(citestr, aCitation);

        // Set the selection inside the blockquote so aQuotedText will go there:
        selection->Collapse(newNode, 0);
      }

      if (aInsertHTML)
        res = LoadHTML(aQuotedText);

      else
        res = InsertText(aQuotedText);  // XXX ignore charset

      if (aNodeInserted)
      {
        if (NS_SUCCEEDED(res))
        {
          *aNodeInserted = newNode;
          NS_IF_ADDREF(*aNodeInserted);
        }
      }
    }
  }

  // Set the selection to just after the inserted node:
  if (NS_SUCCEEDED(res) && newNode)
  {
    nsCOMPtr parent;
    PRInt32 offset;
    if (NS_SUCCEEDED(GetNodeLocation(newNode, address_of(parent), &offset)) && parent)
      selection->Collapse(parent, offset+1);
  }
  return res;
}


void RemoveBodyAndHead(nsIDOMNode *aNode)
{
  if (!aNode) 
    return;
    
  nsCOMPtr tmp, child, body, head;  
  // find the body and head nodes if any.
  // look only at immediate children of aNode.
  aNode->GetFirstChild(getter_AddRefs(child));
  while (child)
  {
    if (nsTextEditUtils::IsBody(child))
    {
      body = child;
    }
    else if (nsEditor::NodeIsType(child, nsEditProperty::head))
    {
      head = child;
    }
    child->GetNextSibling(getter_AddRefs(tmp));
    child = tmp;
  }
  if (head) 
  {
    aNode->RemoveChild(head, getter_AddRefs(tmp));
  }
  if (body)
  {
    body->GetFirstChild(getter_AddRefs(child));
    while (child)
    {
      aNode->InsertBefore(child, body, getter_AddRefs(tmp));
      body->GetFirstChild(getter_AddRefs(child));
    }
    aNode->RemoveChild(body, getter_AddRefs(tmp));
  }
}

/**
 * This function finds the target node that we will be pasting into. aStart is
 * the context that we're given and aResult will be the target. Initially,
 * *aResult must be NULL.
 *
 * The target for a paste is found by either finding the node that contains
 * the magical comment node containing kInsertCookie or, failing that, the
 * firstChild of the firstChild (until we reach a leaf).
 */
nsresult FindTargetNode(nsIDOMNode *aStart, nsCOMPtr &aResult)
{
  if (!aStart)
    return NS_OK;

  nsCOMPtr child, tmp;

  nsresult rv = aStart->GetFirstChild(getter_AddRefs(child));
  NS_ENSURE_SUCCESS(rv, rv);

  if (!child)
  {
    // If the current result is NULL, then aStart is a leaf, and is the
    // fallback result.
    if (!aResult)
      aResult = aStart;

    return NS_OK;
  }

  do
  {
    // Is this child the magical cookie?
    nsCOMPtr comment = do_QueryInterface(child);
    if (comment)
    {
      nsAutoString data;
      rv = comment->GetData(data);
      NS_ENSURE_SUCCESS(rv, rv);

      if (data.EqualsLiteral(kInsertCookie))
      {
        // Yes it is! Return an error so we bubble out and short-circuit the
        // search.
        aResult = aStart;

        // Note: it doesn't matter if this fails.
        aStart->RemoveChild(child, getter_AddRefs(tmp));

        return NS_FOUND_TARGET;
      }
    }

    // Note: Don't use NS_ENSURE_* here since we return a failure result to
    // inicate that we found the magical cookie and we don't want to spam the
    // console.
    rv = FindTargetNode(child, aResult);
    if (NS_FAILED(rv))
      return rv;

    rv = child->GetNextSibling(getter_AddRefs(tmp));
    NS_ENSURE_SUCCESS(rv, rv);

    child = tmp;
  } while (child);

  return NS_OK;
}

nsresult nsHTMLEditor::CreateDOMFragmentFromPaste(const nsAString &aInputString,
                                                  const nsAString & aContextStr,
                                                  const nsAString & aInfoStr,
                                                  nsCOMPtr *outFragNode,
                                                  nsCOMPtr *outStartNode,
                                                  nsCOMPtr *outEndNode,
                                                  PRInt32 *outStartOffset,
                                                  PRInt32 *outEndOffset)
{
  if (!outFragNode || !outStartNode || !outEndNode) 
    return NS_ERROR_NULL_POINTER;
  nsCOMPtr docfrag;
  nsCOMPtr contextAsNode, tmp;  
  nsresult res = NS_OK;

  nsCOMPtr domDoc;
  GetDocument(getter_AddRefs(domDoc));

  nsCOMPtr doc = do_QueryInterface(domDoc);
  NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
  
  // if we have context info, create a fragment for that
  nsAutoTArray tagStack;
  nsCOMPtr contextfrag;
  nsCOMPtr contextLeaf, junk;
  if (!aContextStr.IsEmpty())
  {
    res = ParseFragment(aContextStr, tagStack, doc, address_of(contextAsNode));
    NS_ENSURE_SUCCESS(res, res);
    NS_ENSURE_TRUE(contextAsNode, NS_ERROR_FAILURE);

    res = StripFormattingNodes(contextAsNode);
    NS_ENSURE_SUCCESS(res, res);

    RemoveBodyAndHead(contextAsNode);

    res = FindTargetNode(contextAsNode, contextLeaf);
    if (res == NS_FOUND_TARGET)
      res = NS_OK;
    NS_ENSURE_SUCCESS(res, res);
  }

  // get the tagstack for the context
  res = CreateTagStack(tagStack, contextLeaf);
  NS_ENSURE_SUCCESS(res, res);

  // create fragment for pasted html
  res = ParseFragment(aInputString, tagStack, doc, outFragNode);
  NS_ENSURE_SUCCESS(res, res);
  NS_ENSURE_TRUE(*outFragNode, NS_ERROR_FAILURE);

  RemoveBodyAndHead(*outFragNode);

  if (contextAsNode)
  {
    // unite the two trees
    contextLeaf->AppendChild(*outFragNode, getter_AddRefs(junk));
    *outFragNode = contextAsNode;
  }

  res = StripFormattingNodes(*outFragNode, PR_TRUE);
  NS_ENSURE_SUCCESS(res, res);

  // If there was no context, then treat all of the data we did get as the
  // pasted data.
  if (contextLeaf)
    *outEndNode = *outStartNode = contextLeaf;
  else
    *outEndNode = *outStartNode = *outFragNode;

  *outStartOffset = 0;

  // get the infoString contents
  nsAutoString numstr1, numstr2;
  if (!aInfoStr.IsEmpty())
  {
    PRInt32 err, sep, num;
    sep = aInfoStr.FindChar((PRUnichar)',');
    numstr1 = Substring(aInfoStr, 0, sep);
    numstr2 = Substring(aInfoStr, sep+1, aInfoStr.Length() - (sep+1));

    // Move the start and end children.
    num = numstr1.ToInteger(&err);
    while (num--)
    {
      (*outStartNode)->GetFirstChild(getter_AddRefs(tmp));
      if (!tmp)
        return NS_ERROR_FAILURE;
      tmp.swap(*outStartNode);
    }

    num = numstr2.ToInteger(&err);
    while (num--)
    {
      (*outEndNode)->GetLastChild(getter_AddRefs(tmp));
      if (!tmp)
        return NS_ERROR_FAILURE;
      tmp.swap(*outEndNode);
    }
  }

  GetLengthOfDOMNode(*outEndNode, (PRUint32&)*outEndOffset);
  return res;
}


nsresult nsHTMLEditor::ParseFragment(const nsAString & aFragStr,
                                     nsTArray &aTagStack,
                                     nsIDocument* aTargetDocument,
                                     nsCOMPtr *outNode)
{
  // figure out if we are parsing full context or not
  PRBool bContext = aTagStack.IsEmpty();

  // create the parser to do the conversion.
  nsresult res;
  nsCOMPtr parser = do_CreateInstance(kCParserCID, &res);
  NS_ENSURE_SUCCESS(res, res);
  NS_ENSURE_TRUE(parser, NS_ERROR_FAILURE);

  // create the html fragment sink
  nsCOMPtr sink;
  if (bContext)
    sink = do_CreateInstance(NS_HTMLFRAGMENTSINK2_CONTRACTID);
  else
    sink = do_CreateInstance(NS_HTMLFRAGMENTSINK_CONTRACTID);

  NS_ENSURE_TRUE(sink, NS_ERROR_FAILURE);
  nsCOMPtr fragSink(do_QueryInterface(sink));
  NS_ENSURE_TRUE(fragSink, NS_ERROR_FAILURE);

  fragSink->SetTargetDocument(aTargetDocument);

  // parse the fragment
  parser->SetContentSink(sink);
  if (bContext)
    parser->Parse(aFragStr, (void*)0, NS_LITERAL_CSTRING("text/html"), PR_TRUE, eDTDMode_fragment);
  else
    parser->ParseFragment(aFragStr, 0, aTagStack, PR_FALSE, NS_LITERAL_CSTRING("text/html"), eDTDMode_quirks);
  // get the fragment node
  nsCOMPtr contextfrag;
  res = fragSink->GetFragment(PR_TRUE, getter_AddRefs(contextfrag));
  NS_ENSURE_SUCCESS(res, res);
  *outNode = do_QueryInterface(contextfrag);
  
  return res;
}

nsresult nsHTMLEditor::CreateTagStack(nsTArray &aTagStack, nsIDOMNode *aNode)
{
  nsresult res = NS_OK;
  nsCOMPtr node= aNode;
  PRBool bSeenBody = PR_FALSE;
  
  while (node) 
  {
    if (nsTextEditUtils::IsBody(node))
      bSeenBody = PR_TRUE;
    nsCOMPtr temp = node;
    PRUint16 nodeType;
    
    node->GetNodeType(&nodeType);
    if (nsIDOMNode::ELEMENT_NODE == nodeType)
    {
      nsString* tagName = aTagStack.AppendElement();
      NS_ENSURE_TRUE(tagName, NS_ERROR_OUT_OF_MEMORY);

      node->GetNodeName(*tagName);
      // printf("%s\n",NS_LossyConvertUTF16toASCII(tagName).get());
    }

    res = temp->GetParentNode(getter_AddRefs(node));
    NS_ENSURE_SUCCESS(res, res);  
  }
  
  if (!bSeenBody)
  {
      aTagStack.AppendElement(NS_LITERAL_STRING("BODY"));
  }
  return res;
}


nsresult nsHTMLEditor::CreateListOfNodesToPaste(nsIDOMNode  *aFragmentAsNode,
                                                nsCOMArray& outNodeList,
                                                nsIDOMNode *aStartNode,
                                                PRInt32 aStartOffset,
                                                nsIDOMNode *aEndNode,
                                                PRInt32 aEndOffset)
{
  if (!aFragmentAsNode) 
    return NS_ERROR_NULL_POINTER;

  nsresult res;

  // if no info was provided about the boundary between context and stream,
  // then assume all is stream.
  if (!aStartNode)
  {
    PRInt32 fragLen;
    res = GetLengthOfDOMNode(aFragmentAsNode, (PRUint32&)fragLen);
    NS_ENSURE_SUCCESS(res, res);

    aStartNode = aFragmentAsNode;
    aStartOffset = 0;
    aEndNode = aFragmentAsNode;
    aEndOffset = fragLen;
  }

  nsCOMPtr docFragRange =
                          do_CreateInstance("@mozilla.org/content/range;1");
  if (!docFragRange) return NS_ERROR_OUT_OF_MEMORY;

  res = docFragRange->SetStart(aStartNode, aStartOffset);
  NS_ENSURE_SUCCESS(res, res);
  res = docFragRange->SetEnd(aEndNode, aEndOffset);
  NS_ENSURE_SUCCESS(res, res);

  // now use a subtree iterator over the range to create a list of nodes
  nsTrivialFunctor functor;
  nsDOMSubtreeIterator iter;
  res = iter.Init(docFragRange);
  NS_ENSURE_SUCCESS(res, res);
  res = iter.AppendList(functor, outNodeList);

  return res;
}

nsresult 
nsHTMLEditor::GetListAndTableParents(PRBool aEnd, 
                                     nsCOMArray& aListOfNodes,
                                     nsCOMArray& outArray)
{
  PRInt32 listCount = aListOfNodes.Count();
  if (listCount <= 0)
    return NS_ERROR_FAILURE;  // no empty lists, please
    
  // build up list of parents of first (or last) node in list 
  // that are either lists, or tables.  
  PRInt32 idx = 0;
  if (aEnd) idx = listCount-1;
  
  nsCOMPtr  pNode = aListOfNodes[idx];
  while (pNode)
  {
    if (nsHTMLEditUtils::IsList(pNode) || nsHTMLEditUtils::IsTable(pNode))
    {
      if (!outArray.AppendObject(pNode))
      {
        return NS_ERROR_FAILURE;
      }
    }
    nsCOMPtr parent;
    pNode->GetParentNode(getter_AddRefs(parent));
    pNode = parent;
  }
  return NS_OK;
}

nsresult
nsHTMLEditor::DiscoverPartialListsAndTables(nsCOMArray& aPasteNodes,
                                            nsCOMArray& aListsAndTables,
                                            PRInt32 *outHighWaterMark)
{
  NS_ENSURE_TRUE(outHighWaterMark, NS_ERROR_NULL_POINTER);
  
  *outHighWaterMark = -1;
  PRInt32 listAndTableParents = aListsAndTables.Count();
  
  // scan insertion list for table elements (other than table).
  PRInt32 listCount = aPasteNodes.Count();
  PRInt32 j;  
  for (j=0; j curNode = aPasteNodes[j];

    NS_ENSURE_TRUE(curNode, NS_ERROR_FAILURE);
    if (nsHTMLEditUtils::IsTableElement(curNode) && !nsHTMLEditUtils::IsTable(curNode))
    {
      nsCOMPtr theTable = GetTableParent(curNode);
      if (theTable)
      {
        PRInt32 indexT = aListsAndTables.IndexOf(theTable);
        if (indexT >= 0)
        {
          *outHighWaterMark = indexT;
          if (*outHighWaterMark == listAndTableParents-1) break;
        }
        else
        {
          break;
        }
      }
    }
    if (nsHTMLEditUtils::IsListItem(curNode))
    {
      nsCOMPtr theList = GetListParent(curNode);
      if (theList)
      {
        PRInt32 indexL = aListsAndTables.IndexOf(theList);
        if (indexL >= 0)
        {
          *outHighWaterMark = indexL;
          if (*outHighWaterMark == listAndTableParents-1) break;
        }
        else
        {
          break;
        }
      }
    }
  }
  return NS_OK;
}

nsresult
nsHTMLEditor::ScanForListAndTableStructure( PRBool aEnd,
                                            nsCOMArray& aNodes,
                                            nsIDOMNode *aListOrTable,
                                            nsCOMPtr *outReplaceNode)
{
  NS_ENSURE_TRUE(aListOrTable, NS_ERROR_NULL_POINTER);
  NS_ENSURE_TRUE(outReplaceNode, NS_ERROR_NULL_POINTER);

  *outReplaceNode = 0;
  
  // look upward from first/last paste node for a piece of this list/table
  PRInt32 listCount = aNodes.Count(), idx = 0;
  if (aEnd) idx = listCount-1;
  PRBool bList = nsHTMLEditUtils::IsList(aListOrTable);
  
  nsCOMPtr  pNode = aNodes[idx];
  nsCOMPtr  originalNode = pNode;
  while (pNode)
  {
    if ( (bList && nsHTMLEditUtils::IsListItem(pNode)) ||
         (!bList && (nsHTMLEditUtils::IsTableElement(pNode) && !nsHTMLEditUtils::IsTable(pNode))) )
    {
      nsCOMPtr structureNode;
      if (bList) structureNode = GetListParent(pNode);
      else structureNode = GetTableParent(pNode);
      if (structureNode == aListOrTable)
      {
        if (bList)
          *outReplaceNode = structureNode;
        else
          *outReplaceNode = pNode;
        break;
      }
    }
    nsCOMPtr parent;
    pNode->GetParentNode(getter_AddRefs(parent));
    pNode = parent;
  }
  return NS_OK;
}    

nsresult
nsHTMLEditor::ReplaceOrphanedStructure(PRBool aEnd,
                                       nsCOMArray& aNodeArray,
                                       nsCOMArray& aListAndTableArray,
                                       PRInt32 aHighWaterMark)
{
  nsCOMPtr curNode = aListAndTableArray[aHighWaterMark];
  NS_ENSURE_TRUE(curNode, NS_ERROR_NULL_POINTER);
  
  nsCOMPtr replaceNode, originalNode;
  
  // find substructure of list or table that must be included in paste.
  nsresult res = ScanForListAndTableStructure(aEnd, aNodeArray, 
                                 curNode, address_of(replaceNode));
  NS_ENSURE_SUCCESS(res, res);
  
  // if we found substructure, paste it instead of it's descendants
  if (replaceNode)
  {
    // postprocess list to remove any descendants of this node
    // so that we don't insert them twice.
    nsCOMPtr endpoint;
    do
    {
      endpoint = GetArrayEndpoint(aEnd, aNodeArray);
      if (!endpoint) break;
      if (nsEditorUtils::IsDescendantOf(endpoint, replaceNode))
        aNodeArray.RemoveObject(endpoint);
      else
        break;
    } while(endpoint);
    
    // now replace the removed nodes with the structural parent
    if (aEnd) aNodeArray.AppendObject(replaceNode);
    else aNodeArray.InsertObjectAt(replaceNode, 0);
  }
  return NS_OK;
}

nsIDOMNode* nsHTMLEditor::GetArrayEndpoint(PRBool aEnd,
                                           nsCOMArray& aNodeArray)
{
  PRInt32 listCount = aNodeArray.Count();
  if (listCount <= 0) 
    return nsnull;

  if (aEnd)
  {
    return aNodeArray[listCount-1];
  }
  
  return aNodeArray[0];
}