/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "WSRunObject.h" #include "TextEditUtils.h" #include "mozilla/Assertions.h" #include "mozilla/Casting.h" #include "mozilla/EditorUtils.h" #include "mozilla/HTMLEditor.h" #include "mozilla/mozalloc.h" #include "mozilla/OwningNonNull.h" #include "mozilla/SelectionState.h" #include "nsAString.h" #include "nsCRT.h" #include "nsContentUtils.h" #include "nsDebug.h" #include "nsError.h" #include "nsIContent.h" #include "nsIDOMDocument.h" #include "nsIDOMNode.h" #include "nsISupportsImpl.h" #include "nsRange.h" #include "nsString.h" #include "nsTextFragment.h" namespace mozilla { using namespace dom; const char16_t nbsp = 160; WSRunObject::WSRunObject(HTMLEditor* aHTMLEditor, nsINode* aNode, int32_t aOffset) : mNode(aNode) , mOffset(aOffset) , mPRE(false) , mStartOffset(0) , mEndOffset(0) , mFirstNBSPOffset(0) , mLastNBSPOffset(0) , mStartRun(nullptr) , mEndRun(nullptr) , mHTMLEditor(aHTMLEditor) { GetWSNodes(); GetRuns(); } WSRunObject::WSRunObject(HTMLEditor* aHTMLEditor, nsIDOMNode* aNode, int32_t aOffset) : mNode(do_QueryInterface(aNode)) , mOffset(aOffset) , mPRE(false) , mStartOffset(0) , mEndOffset(0) , mFirstNBSPOffset(0) , mLastNBSPOffset(0) , mStartRun(nullptr) , mEndRun(nullptr) , mHTMLEditor(aHTMLEditor) { GetWSNodes(); GetRuns(); } WSRunObject::~WSRunObject() { ClearRuns(); } nsresult WSRunObject::ScrubBlockBoundary(HTMLEditor* aHTMLEditor, BlockBoundary aBoundary, nsINode* aBlock, int32_t aOffset) { NS_ENSURE_TRUE(aHTMLEditor && aBlock, NS_ERROR_NULL_POINTER); int32_t offset; if (aBoundary == kBlockStart) { offset = 0; } else if (aBoundary == kBlockEnd) { offset = aBlock->Length(); } else { // Else we are scrubbing an outer boundary - just before or after a block // element. NS_ENSURE_STATE(aOffset >= 0); offset = aOffset; } WSRunObject theWSObj(aHTMLEditor, aBlock, offset); return theWSObj.Scrub(); } nsresult WSRunObject::PrepareToJoinBlocks(HTMLEditor* aHTMLEditor, Element* aLeftBlock, Element* aRightBlock) { NS_ENSURE_TRUE(aLeftBlock && aRightBlock && aHTMLEditor, NS_ERROR_NULL_POINTER); WSRunObject leftWSObj(aHTMLEditor, aLeftBlock, aLeftBlock->Length()); WSRunObject rightWSObj(aHTMLEditor, aRightBlock, 0); return leftWSObj.PrepareToDeleteRangePriv(&rightWSObj); } nsresult WSRunObject::PrepareToDeleteRange(HTMLEditor* aHTMLEditor, nsCOMPtr<nsINode>* aStartNode, int32_t* aStartOffset, nsCOMPtr<nsINode>* aEndNode, int32_t* aEndOffset) { NS_ENSURE_TRUE(aHTMLEditor && aStartNode && *aStartNode && aStartOffset && aEndNode && *aEndNode && aEndOffset, NS_ERROR_NULL_POINTER); AutoTrackDOMPoint trackerStart(aHTMLEditor->mRangeUpdater, aStartNode, aStartOffset); AutoTrackDOMPoint trackerEnd(aHTMLEditor->mRangeUpdater, aEndNode, aEndOffset); WSRunObject leftWSObj(aHTMLEditor, *aStartNode, *aStartOffset); WSRunObject rightWSObj(aHTMLEditor, *aEndNode, *aEndOffset); return leftWSObj.PrepareToDeleteRangePriv(&rightWSObj); } nsresult WSRunObject::PrepareToDeleteNode(HTMLEditor* aHTMLEditor, nsIContent* aContent) { NS_ENSURE_TRUE(aContent && aHTMLEditor, NS_ERROR_NULL_POINTER); nsCOMPtr<nsINode> parent = aContent->GetParentNode(); NS_ENSURE_STATE(parent); int32_t offset = parent->IndexOf(aContent); WSRunObject leftWSObj(aHTMLEditor, parent, offset); WSRunObject rightWSObj(aHTMLEditor, parent, offset + 1); return leftWSObj.PrepareToDeleteRangePriv(&rightWSObj); } nsresult WSRunObject::PrepareToSplitAcrossBlocks(HTMLEditor* aHTMLEditor, nsCOMPtr<nsINode>* aSplitNode, int32_t* aSplitOffset) { NS_ENSURE_TRUE(aHTMLEditor && aSplitNode && *aSplitNode && aSplitOffset, NS_ERROR_NULL_POINTER); AutoTrackDOMPoint tracker(aHTMLEditor->mRangeUpdater, aSplitNode, aSplitOffset); WSRunObject wsObj(aHTMLEditor, *aSplitNode, *aSplitOffset); return wsObj.PrepareToSplitAcrossBlocksPriv(); } already_AddRefed<Element> WSRunObject::InsertBreak(nsCOMPtr<nsINode>* aInOutParent, int32_t* aInOutOffset, nsIEditor::EDirection aSelect) { // MOOSE: for now, we always assume non-PRE formatting. Fix this later. // meanwhile, the pre case is handled in WillInsertText in // HTMLEditRules.cpp NS_ENSURE_TRUE(aInOutParent && aInOutOffset, nullptr); WSFragment *beforeRun, *afterRun; FindRun(*aInOutParent, *aInOutOffset, &beforeRun, false); FindRun(*aInOutParent, *aInOutOffset, &afterRun, true); { // Some scoping for AutoTrackDOMPoint. This will track our insertion // point while we tweak any surrounding whitespace AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater, aInOutParent, aInOutOffset); // Handle any changes needed to ws run after inserted br if (!afterRun || (afterRun->mType & WSType::trailingWS)) { // Don't need to do anything. Just insert break. ws won't change. } else if (afterRun->mType & WSType::leadingWS) { // Delete the leading ws that is after insertion point. We don't // have to (it would still not be significant after br), but it's // just more aesthetically pleasing to. nsresult rv = DeleteChars(*aInOutParent, *aInOutOffset, afterRun->mEndNode, afterRun->mEndOffset, eOutsideUserSelectAll); NS_ENSURE_SUCCESS(rv, nullptr); } else if (afterRun->mType == WSType::normalWS) { // Need to determine if break at front of non-nbsp run. If so, convert // run to nbsp. WSPoint thePoint = GetCharAfter(*aInOutParent, *aInOutOffset); if (thePoint.mTextNode && nsCRT::IsAsciiSpace(thePoint.mChar)) { WSPoint prevPoint = GetCharBefore(thePoint); if (prevPoint.mTextNode && !nsCRT::IsAsciiSpace(prevPoint.mChar)) { // We are at start of non-nbsps. Convert to a single nbsp. nsresult rv = ConvertToNBSP(thePoint); NS_ENSURE_SUCCESS(rv, nullptr); } } } // Handle any changes needed to ws run before inserted br if (!beforeRun || (beforeRun->mType & WSType::leadingWS)) { // Don't need to do anything. Just insert break. ws won't change. } else if (beforeRun->mType & WSType::trailingWS) { // Need to delete the trailing ws that is before insertion point, because it // would become significant after break inserted. nsresult rv = DeleteChars(beforeRun->mStartNode, beforeRun->mStartOffset, *aInOutParent, *aInOutOffset, eOutsideUserSelectAll); NS_ENSURE_SUCCESS(rv, nullptr); } else if (beforeRun->mType == WSType::normalWS) { // Try to change an nbsp to a space, just to prevent nbsp proliferation nsresult rv = CheckTrailingNBSP(beforeRun, *aInOutParent, *aInOutOffset); NS_ENSURE_SUCCESS(rv, nullptr); } } // ready, aim, fire! return mHTMLEditor->CreateBRImpl(aInOutParent, aInOutOffset, aSelect); } nsresult WSRunObject::InsertText(const nsAString& aStringToInsert, nsCOMPtr<nsINode>* aInOutParent, int32_t* aInOutOffset, nsIDocument* aDoc) { // MOOSE: for now, we always assume non-PRE formatting. Fix this later. // meanwhile, the pre case is handled in WillInsertText in // HTMLEditRules.cpp // MOOSE: for now, just getting the ws logic straight. This implementation // is very slow. Will need to replace edit rules impl with a more efficient // text sink here that does the minimal amount of searching/replacing/copying NS_ENSURE_TRUE(aInOutParent && aInOutOffset && aDoc, NS_ERROR_NULL_POINTER); if (aStringToInsert.IsEmpty()) { return NS_OK; } nsAutoString theString(aStringToInsert); WSFragment *beforeRun, *afterRun; FindRun(*aInOutParent, *aInOutOffset, &beforeRun, false); FindRun(*aInOutParent, *aInOutOffset, &afterRun, true); { // Some scoping for AutoTrackDOMPoint. This will track our insertion // point while we tweak any surrounding whitespace AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater, aInOutParent, aInOutOffset); // Handle any changes needed to ws run after inserted text if (!afterRun || afterRun->mType & WSType::trailingWS) { // Don't need to do anything. Just insert text. ws won't change. } else if (afterRun->mType & WSType::leadingWS) { // Delete the leading ws that is after insertion point, because it // would become significant after text inserted. nsresult rv = DeleteChars(*aInOutParent, *aInOutOffset, afterRun->mEndNode, afterRun->mEndOffset, eOutsideUserSelectAll); NS_ENSURE_SUCCESS(rv, rv); } else if (afterRun->mType == WSType::normalWS) { // Try to change an nbsp to a space, if possible, just to prevent nbsp // proliferation nsresult rv = CheckLeadingNBSP(afterRun, *aInOutParent, *aInOutOffset); NS_ENSURE_SUCCESS(rv, rv); } // Handle any changes needed to ws run before inserted text if (!beforeRun || beforeRun->mType & WSType::leadingWS) { // Don't need to do anything. Just insert text. ws won't change. } else if (beforeRun->mType & WSType::trailingWS) { // Need to delete the trailing ws that is before insertion point, because // it would become significant after text inserted. nsresult rv = DeleteChars(beforeRun->mStartNode, beforeRun->mStartOffset, *aInOutParent, *aInOutOffset, eOutsideUserSelectAll); NS_ENSURE_SUCCESS(rv, rv); } else if (beforeRun->mType == WSType::normalWS) { // Try to change an nbsp to a space, if possible, just to prevent nbsp // proliferation nsresult rv = CheckTrailingNBSP(beforeRun, *aInOutParent, *aInOutOffset); NS_ENSURE_SUCCESS(rv, rv); } } // Next up, tweak head and tail of string as needed. First the head: there // are a variety of circumstances that would require us to convert a leading // ws char into an nbsp: if (nsCRT::IsAsciiSpace(theString[0])) { // We have a leading space if (beforeRun) { if (beforeRun->mType & WSType::leadingWS) { theString.SetCharAt(nbsp, 0); } else if (beforeRun->mType & WSType::normalWS) { WSPoint wspoint = GetCharBefore(*aInOutParent, *aInOutOffset); if (wspoint.mTextNode && nsCRT::IsAsciiSpace(wspoint.mChar)) { theString.SetCharAt(nbsp, 0); } } } else if (mStartReason & WSType::block || mStartReason == WSType::br) { theString.SetCharAt(nbsp, 0); } } // Then the tail uint32_t lastCharIndex = theString.Length() - 1; if (nsCRT::IsAsciiSpace(theString[lastCharIndex])) { // We have a leading space if (afterRun) { if (afterRun->mType & WSType::trailingWS) { theString.SetCharAt(nbsp, lastCharIndex); } else if (afterRun->mType & WSType::normalWS) { WSPoint wspoint = GetCharAfter(*aInOutParent, *aInOutOffset); if (wspoint.mTextNode && nsCRT::IsAsciiSpace(wspoint.mChar)) { theString.SetCharAt(nbsp, lastCharIndex); } } } else if (mEndReason & WSType::block) { theString.SetCharAt(nbsp, lastCharIndex); } } // Next, scan string for adjacent ws and convert to nbsp/space combos // MOOSE: don't need to convert tabs here since that is done by // WillInsertText() before we are called. Eventually, all that logic will be // pushed down into here and made more efficient. bool prevWS = false; for (uint32_t i = 0; i <= lastCharIndex; i++) { if (nsCRT::IsAsciiSpace(theString[i])) { if (prevWS) { // i - 1 can't be negative because prevWS starts out false theString.SetCharAt(nbsp, i - 1); } else { prevWS = true; } } else { prevWS = false; } } // Ready, aim, fire! mHTMLEditor->InsertTextImpl(theString, aInOutParent, aInOutOffset, aDoc); return NS_OK; } nsresult WSRunObject::DeleteWSBackward() { WSPoint point = GetCharBefore(mNode, mOffset); NS_ENSURE_TRUE(point.mTextNode, NS_OK); // nothing to delete // Easy case, preformatted ws. if (mPRE && (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == nbsp)) { return DeleteChars(point.mTextNode, point.mOffset, point.mTextNode, point.mOffset + 1); } // Caller's job to ensure that previous char is really ws. If it is normal // ws, we need to delete the whole run. if (nsCRT::IsAsciiSpace(point.mChar)) { RefPtr<Text> startNodeText, endNodeText; int32_t startOffset, endOffset; GetAsciiWSBounds(eBoth, point.mTextNode, point.mOffset + 1, getter_AddRefs(startNodeText), &startOffset, getter_AddRefs(endNodeText), &endOffset); // adjust surrounding ws nsCOMPtr<nsINode> startNode = startNodeText.get(); nsCOMPtr<nsINode> endNode = endNodeText.get(); nsresult rv = WSRunObject::PrepareToDeleteRange(mHTMLEditor, address_of(startNode), &startOffset, address_of(endNode), &endOffset); NS_ENSURE_SUCCESS(rv, rv); // finally, delete that ws return DeleteChars(startNode, startOffset, endNode, endOffset); } if (point.mChar == nbsp) { nsCOMPtr<nsINode> node(point.mTextNode); // adjust surrounding ws int32_t startOffset = point.mOffset; int32_t endOffset = point.mOffset + 1; nsresult rv = WSRunObject::PrepareToDeleteRange(mHTMLEditor, address_of(node), &startOffset, address_of(node), &endOffset); NS_ENSURE_SUCCESS(rv, rv); // finally, delete that ws return DeleteChars(node, startOffset, node, endOffset); } return NS_OK; } nsresult WSRunObject::DeleteWSForward() { WSPoint point = GetCharAfter(mNode, mOffset); NS_ENSURE_TRUE(point.mTextNode, NS_OK); // nothing to delete // Easy case, preformatted ws. if (mPRE && (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == nbsp)) { return DeleteChars(point.mTextNode, point.mOffset, point.mTextNode, point.mOffset + 1); } // Caller's job to ensure that next char is really ws. If it is normal ws, // we need to delete the whole run. if (nsCRT::IsAsciiSpace(point.mChar)) { RefPtr<Text> startNodeText, endNodeText; int32_t startOffset, endOffset; GetAsciiWSBounds(eBoth, point.mTextNode, point.mOffset + 1, getter_AddRefs(startNodeText), &startOffset, getter_AddRefs(endNodeText), &endOffset); // Adjust surrounding ws nsCOMPtr<nsINode> startNode(startNodeText), endNode(endNodeText); nsresult rv = WSRunObject::PrepareToDeleteRange(mHTMLEditor, address_of(startNode), &startOffset, address_of(endNode), &endOffset); NS_ENSURE_SUCCESS(rv, rv); // Finally, delete that ws return DeleteChars(startNode, startOffset, endNode, endOffset); } if (point.mChar == nbsp) { nsCOMPtr<nsINode> node(point.mTextNode); // Adjust surrounding ws int32_t startOffset = point.mOffset; int32_t endOffset = point.mOffset+1; nsresult rv = WSRunObject::PrepareToDeleteRange(mHTMLEditor, address_of(node), &startOffset, address_of(node), &endOffset); NS_ENSURE_SUCCESS(rv, rv); // Finally, delete that ws return DeleteChars(node, startOffset, node, endOffset); } return NS_OK; } void WSRunObject::PriorVisibleNode(nsINode* aNode, int32_t aOffset, nsCOMPtr<nsINode>* outVisNode, int32_t* outVisOffset, WSType* outType) { // Find first visible thing before the point. Position // outVisNode/outVisOffset just _after_ that thing. If we don't find // anything return start of ws. MOZ_ASSERT(aNode && outVisNode && outVisOffset && outType); WSFragment* run; FindRun(aNode, aOffset, &run, false); // Is there a visible run there or earlier? for (; run; run = run->mLeft) { if (run->mType == WSType::normalWS) { WSPoint point = GetCharBefore(aNode, aOffset); // When it's a non-empty text node, return it. if (point.mTextNode && point.mTextNode->Length()) { *outVisNode = point.mTextNode; *outVisOffset = point.mOffset + 1; if (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == nbsp) { *outType = WSType::normalWS; } else { *outType = WSType::text; } return; } // If no text node, keep looking. We should eventually fall out of loop } } // If we get here, then nothing in ws data to find. Return start reason. *outVisNode = mStartReasonNode; // This really isn't meaningful if mStartReasonNode != mStartNode *outVisOffset = mStartOffset; *outType = mStartReason; } void WSRunObject::NextVisibleNode(nsINode* aNode, int32_t aOffset, nsCOMPtr<nsINode>* outVisNode, int32_t* outVisOffset, WSType* outType) { // Find first visible thing after the point. Position // outVisNode/outVisOffset just _before_ that thing. If we don't find // anything return end of ws. MOZ_ASSERT(aNode && outVisNode && outVisOffset && outType); WSFragment* run; FindRun(aNode, aOffset, &run, true); // Is there a visible run there or later? for (; run; run = run->mRight) { if (run->mType == WSType::normalWS) { WSPoint point = GetCharAfter(aNode, aOffset); // When it's a non-empty text node, return it. if (point.mTextNode && point.mTextNode->Length()) { *outVisNode = point.mTextNode; *outVisOffset = point.mOffset; if (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == nbsp) { *outType = WSType::normalWS; } else { *outType = WSType::text; } return; } // If no text node, keep looking. We should eventually fall out of loop } } // If we get here, then nothing in ws data to find. Return end reason *outVisNode = mEndReasonNode; // This really isn't meaningful if mEndReasonNode != mEndNode *outVisOffset = mEndOffset; *outType = mEndReason; } nsresult WSRunObject::AdjustWhitespace() { // this routine examines a run of ws and tries to get rid of some unneeded nbsp's, // replacing them with regualr ascii space if possible. Keeping things simple // for now and just trying to fix up the trailing ws in the run. if (!mLastNBSPNode) { // nothing to do! return NS_OK; } WSFragment *curRun = mStartRun; while (curRun) { // look for normal ws run if (curRun->mType == WSType::normalWS) { nsresult rv = CheckTrailingNBSPOfRun(curRun); if (NS_FAILED(rv)) { return rv; } } curRun = curRun->mRight; } return NS_OK; } //-------------------------------------------------------------------------------------------- // protected methods //-------------------------------------------------------------------------------------------- nsINode* WSRunObject::GetWSBoundingParent() { NS_ENSURE_TRUE(mNode, nullptr); OwningNonNull<nsINode> wsBoundingParent = *mNode; while (!IsBlockNode(wsBoundingParent)) { nsCOMPtr<nsINode> parent = wsBoundingParent->GetParentNode(); if (!parent || !mHTMLEditor->IsEditable(parent)) { break; } wsBoundingParent = parent; } return wsBoundingParent; } nsresult WSRunObject::GetWSNodes() { // collect up an array of nodes that are contiguous with the insertion point // and which contain only whitespace. Stop if you reach non-ws text or a new // block boundary. EditorDOMPoint start(mNode, mOffset), end(mNode, mOffset); nsCOMPtr<nsINode> wsBoundingParent = GetWSBoundingParent(); // first look backwards to find preceding ws nodes if (RefPtr<Text> textNode = mNode->GetAsText()) { const nsTextFragment* textFrag = textNode->GetText(); mNodeArray.InsertElementAt(0, textNode); if (mOffset) { for (int32_t pos = mOffset - 1; pos >= 0; pos--) { // sanity bounds check the char position. bug 136165 if (uint32_t(pos) >= textFrag->GetLength()) { NS_NOTREACHED("looking beyond end of text fragment"); continue; } char16_t theChar = textFrag->CharAt(pos); if (!nsCRT::IsAsciiSpace(theChar)) { if (theChar != nbsp) { mStartNode = textNode; mStartOffset = pos + 1; mStartReason = WSType::text; mStartReasonNode = textNode; break; } // as we look backwards update our earliest found nbsp mFirstNBSPNode = textNode; mFirstNBSPOffset = pos; // also keep track of latest nbsp so far if (!mLastNBSPNode) { mLastNBSPNode = textNode; mLastNBSPOffset = pos; } } start.node = textNode; start.offset = pos; } } } while (!mStartNode) { // we haven't found the start of ws yet. Keep looking nsCOMPtr<nsIContent> priorNode = GetPreviousWSNode(start, wsBoundingParent); if (priorNode) { if (IsBlockNode(priorNode)) { mStartNode = start.node; mStartOffset = start.offset; mStartReason = WSType::otherBlock; mStartReasonNode = priorNode; } else if (RefPtr<Text> textNode = priorNode->GetAsText()) { mNodeArray.InsertElementAt(0, textNode); const nsTextFragment *textFrag; if (!textNode || !(textFrag = textNode->GetText())) { return NS_ERROR_NULL_POINTER; } uint32_t len = textNode->TextLength(); if (len < 1) { // Zero length text node. Set start point to it // so we can get past it! start.SetPoint(priorNode, 0); } else { for (int32_t pos = len - 1; pos >= 0; pos--) { // sanity bounds check the char position. bug 136165 if (uint32_t(pos) >= textFrag->GetLength()) { NS_NOTREACHED("looking beyond end of text fragment"); continue; } char16_t theChar = textFrag->CharAt(pos); if (!nsCRT::IsAsciiSpace(theChar)) { if (theChar != nbsp) { mStartNode = textNode; mStartOffset = pos + 1; mStartReason = WSType::text; mStartReasonNode = textNode; break; } // as we look backwards update our earliest found nbsp mFirstNBSPNode = textNode; mFirstNBSPOffset = pos; // also keep track of latest nbsp so far if (!mLastNBSPNode) { mLastNBSPNode = textNode; mLastNBSPOffset = pos; } } start.SetPoint(textNode, pos); } } } else { // it's a break or a special node, like <img>, that is not a block and not // a break but still serves as a terminator to ws runs. mStartNode = start.node; mStartOffset = start.offset; if (TextEditUtils::IsBreak(priorNode)) { mStartReason = WSType::br; } else { mStartReason = WSType::special; } mStartReasonNode = priorNode; } } else { // no prior node means we exhausted wsBoundingParent mStartNode = start.node; mStartOffset = start.offset; mStartReason = WSType::thisBlock; mStartReasonNode = wsBoundingParent; } } // then look ahead to find following ws nodes if (RefPtr<Text> textNode = mNode->GetAsText()) { // don't need to put it on list. it already is from code above const nsTextFragment *textFrag = textNode->GetText(); uint32_t len = textNode->TextLength(); if (uint16_t(mOffset)<len) { for (uint32_t pos = mOffset; pos < len; pos++) { // sanity bounds check the char position. bug 136165 if (pos >= textFrag->GetLength()) { NS_NOTREACHED("looking beyond end of text fragment"); continue; } char16_t theChar = textFrag->CharAt(pos); if (!nsCRT::IsAsciiSpace(theChar)) { if (theChar != nbsp) { mEndNode = textNode; mEndOffset = pos; mEndReason = WSType::text; mEndReasonNode = textNode; break; } // as we look forwards update our latest found nbsp mLastNBSPNode = textNode; mLastNBSPOffset = pos; // also keep track of earliest nbsp so far if (!mFirstNBSPNode) { mFirstNBSPNode = textNode; mFirstNBSPOffset = pos; } } end.SetPoint(textNode, pos + 1); } } } while (!mEndNode) { // we haven't found the end of ws yet. Keep looking nsCOMPtr<nsIContent> nextNode = GetNextWSNode(end, wsBoundingParent); if (nextNode) { if (IsBlockNode(nextNode)) { // we encountered a new block. therefore no more ws. mEndNode = end.node; mEndOffset = end.offset; mEndReason = WSType::otherBlock; mEndReasonNode = nextNode; } else if (RefPtr<Text> textNode = nextNode->GetAsText()) { mNodeArray.AppendElement(textNode); const nsTextFragment *textFrag; if (!textNode || !(textFrag = textNode->GetText())) { return NS_ERROR_NULL_POINTER; } uint32_t len = textNode->TextLength(); if (len < 1) { // Zero length text node. Set end point to it // so we can get past it! end.SetPoint(textNode, 0); } else { for (uint32_t pos = 0; pos < len; pos++) { // sanity bounds check the char position. bug 136165 if (pos >= textFrag->GetLength()) { NS_NOTREACHED("looking beyond end of text fragment"); continue; } char16_t theChar = textFrag->CharAt(pos); if (!nsCRT::IsAsciiSpace(theChar)) { if (theChar != nbsp) { mEndNode = textNode; mEndOffset = pos; mEndReason = WSType::text; mEndReasonNode = textNode; break; } // as we look forwards update our latest found nbsp mLastNBSPNode = textNode; mLastNBSPOffset = pos; // also keep track of earliest nbsp so far if (!mFirstNBSPNode) { mFirstNBSPNode = textNode; mFirstNBSPOffset = pos; } } end.SetPoint(textNode, pos + 1); } } } else { // we encountered a break or a special node, like <img>, // that is not a block and not a break but still // serves as a terminator to ws runs. mEndNode = end.node; mEndOffset = end.offset; if (TextEditUtils::IsBreak(nextNode)) { mEndReason = WSType::br; } else { mEndReason = WSType::special; } mEndReasonNode = nextNode; } } else { // no next node means we exhausted wsBoundingParent mEndNode = end.node; mEndOffset = end.offset; mEndReason = WSType::thisBlock; mEndReasonNode = wsBoundingParent; } } return NS_OK; } void WSRunObject::GetRuns() { ClearRuns(); // handle some easy cases first mHTMLEditor->IsPreformatted(GetAsDOMNode(mNode), &mPRE); // if it's preformatedd, or if we are surrounded by text or special, it's all one // big normal ws run if (mPRE || ((mStartReason == WSType::text || mStartReason == WSType::special) && (mEndReason == WSType::text || mEndReason == WSType::special || mEndReason == WSType::br))) { MakeSingleWSRun(WSType::normalWS); return; } // if we are before or after a block (or after a break), and there are no nbsp's, // then it's all non-rendering ws. if (!mFirstNBSPNode && !mLastNBSPNode && ((mStartReason & WSType::block) || mStartReason == WSType::br || (mEndReason & WSType::block))) { WSType wstype; if ((mStartReason & WSType::block) || mStartReason == WSType::br) { wstype = WSType::leadingWS; } if (mEndReason & WSType::block) { wstype |= WSType::trailingWS; } MakeSingleWSRun(wstype); return; } // otherwise a little trickier. shucks. mStartRun = new WSFragment(); mStartRun->mStartNode = mStartNode; mStartRun->mStartOffset = mStartOffset; if (mStartReason & WSType::block || mStartReason == WSType::br) { // set up mStartRun mStartRun->mType = WSType::leadingWS; mStartRun->mEndNode = mFirstNBSPNode; mStartRun->mEndOffset = mFirstNBSPOffset; mStartRun->mLeftType = mStartReason; mStartRun->mRightType = WSType::normalWS; // set up next run WSFragment *normalRun = new WSFragment(); mStartRun->mRight = normalRun; normalRun->mType = WSType::normalWS; normalRun->mStartNode = mFirstNBSPNode; normalRun->mStartOffset = mFirstNBSPOffset; normalRun->mLeftType = WSType::leadingWS; normalRun->mLeft = mStartRun; if (mEndReason != WSType::block) { // then no trailing ws. this normal run ends the overall ws run. normalRun->mRightType = mEndReason; normalRun->mEndNode = mEndNode; normalRun->mEndOffset = mEndOffset; mEndRun = normalRun; } else { // we might have trailing ws. // it so happens that *if* there is an nbsp at end, {mEndNode,mEndOffset-1} // will point to it, even though in general start/end points not // guaranteed to be in text nodes. if (mLastNBSPNode == mEndNode && mLastNBSPOffset == mEndOffset - 1) { // normal ws runs right up to adjacent block (nbsp next to block) normalRun->mRightType = mEndReason; normalRun->mEndNode = mEndNode; normalRun->mEndOffset = mEndOffset; mEndRun = normalRun; } else { normalRun->mEndNode = mLastNBSPNode; normalRun->mEndOffset = mLastNBSPOffset+1; normalRun->mRightType = WSType::trailingWS; // set up next run WSFragment *lastRun = new WSFragment(); lastRun->mType = WSType::trailingWS; lastRun->mStartNode = mLastNBSPNode; lastRun->mStartOffset = mLastNBSPOffset+1; lastRun->mEndNode = mEndNode; lastRun->mEndOffset = mEndOffset; lastRun->mLeftType = WSType::normalWS; lastRun->mLeft = normalRun; lastRun->mRightType = mEndReason; mEndRun = lastRun; normalRun->mRight = lastRun; } } } else { // mStartReason is not WSType::block or WSType::br; set up mStartRun mStartRun->mType = WSType::normalWS; mStartRun->mEndNode = mLastNBSPNode; mStartRun->mEndOffset = mLastNBSPOffset+1; mStartRun->mLeftType = mStartReason; // we might have trailing ws. // it so happens that *if* there is an nbsp at end, {mEndNode,mEndOffset-1} // will point to it, even though in general start/end points not // guaranteed to be in text nodes. if (mLastNBSPNode == mEndNode && mLastNBSPOffset == (mEndOffset - 1)) { mStartRun->mRightType = mEndReason; mStartRun->mEndNode = mEndNode; mStartRun->mEndOffset = mEndOffset; mEndRun = mStartRun; } else { // set up next run WSFragment *lastRun = new WSFragment(); lastRun->mType = WSType::trailingWS; lastRun->mStartNode = mLastNBSPNode; lastRun->mStartOffset = mLastNBSPOffset+1; lastRun->mLeftType = WSType::normalWS; lastRun->mLeft = mStartRun; lastRun->mRightType = mEndReason; mEndRun = lastRun; mStartRun->mRight = lastRun; mStartRun->mRightType = WSType::trailingWS; } } } void WSRunObject::ClearRuns() { WSFragment *tmp, *run; run = mStartRun; while (run) { tmp = run->mRight; delete run; run = tmp; } mStartRun = 0; mEndRun = 0; } void WSRunObject::MakeSingleWSRun(WSType aType) { mStartRun = new WSFragment(); mStartRun->mStartNode = mStartNode; mStartRun->mStartOffset = mStartOffset; mStartRun->mType = aType; mStartRun->mEndNode = mEndNode; mStartRun->mEndOffset = mEndOffset; mStartRun->mLeftType = mStartReason; mStartRun->mRightType = mEndReason; mEndRun = mStartRun; } nsIContent* WSRunObject::GetPreviousWSNodeInner(nsINode* aStartNode, nsINode* aBlockParent) { // Can't really recycle various getnext/prior routines because we have // special needs here. Need to step into inline containers but not block // containers. MOZ_ASSERT(aStartNode && aBlockParent); nsCOMPtr<nsIContent> priorNode = aStartNode->GetPreviousSibling(); OwningNonNull<nsINode> curNode = *aStartNode; while (!priorNode) { // We have exhausted nodes in parent of aStartNode. nsCOMPtr<nsINode> curParent = curNode->GetParentNode(); NS_ENSURE_TRUE(curParent, nullptr); if (curParent == aBlockParent) { // We have exhausted nodes in the block parent. The convention here is // to return null. return nullptr; } // We have a parent: look for previous sibling priorNode = curParent->GetPreviousSibling(); curNode = curParent; } // We have a prior node. If it's a block, return it. if (IsBlockNode(priorNode)) { return priorNode; } if (mHTMLEditor->IsContainer(priorNode)) { // Else if it's a container, get deep rightmost child nsCOMPtr<nsIContent> child = mHTMLEditor->GetRightmostChild(priorNode); if (child) { return child; } } // Else return the node itself return priorNode; } nsIContent* WSRunObject::GetPreviousWSNode(EditorDOMPoint aPoint, nsINode* aBlockParent) { // Can't really recycle various getnext/prior routines because we // have special needs here. Need to step into inline containers but // not block containers. MOZ_ASSERT(aPoint.node && aBlockParent); if (aPoint.node->NodeType() == nsIDOMNode::TEXT_NODE) { return GetPreviousWSNodeInner(aPoint.node, aBlockParent); } if (!mHTMLEditor->IsContainer(aPoint.node)) { return GetPreviousWSNodeInner(aPoint.node, aBlockParent); } if (!aPoint.offset) { if (aPoint.node == aBlockParent) { // We are at start of the block. return nullptr; } // We are at start of non-block container return GetPreviousWSNodeInner(aPoint.node, aBlockParent); } nsCOMPtr<nsIContent> startContent = do_QueryInterface(aPoint.node); NS_ENSURE_TRUE(startContent, nullptr); nsCOMPtr<nsIContent> priorNode = startContent->GetChildAt(aPoint.offset - 1); NS_ENSURE_TRUE(priorNode, nullptr); // We have a prior node. If it's a block, return it. if (IsBlockNode(priorNode)) { return priorNode; } if (mHTMLEditor->IsContainer(priorNode)) { // Else if it's a container, get deep rightmost child nsCOMPtr<nsIContent> child = mHTMLEditor->GetRightmostChild(priorNode); if (child) { return child; } } // Else return the node itself return priorNode; } nsIContent* WSRunObject::GetNextWSNodeInner(nsINode* aStartNode, nsINode* aBlockParent) { // Can't really recycle various getnext/prior routines because we have // special needs here. Need to step into inline containers but not block // containers. MOZ_ASSERT(aStartNode && aBlockParent); nsCOMPtr<nsIContent> nextNode = aStartNode->GetNextSibling(); nsCOMPtr<nsINode> curNode = aStartNode; while (!nextNode) { // We have exhausted nodes in parent of aStartNode. nsCOMPtr<nsINode> curParent = curNode->GetParentNode(); NS_ENSURE_TRUE(curParent, nullptr); if (curParent == aBlockParent) { // We have exhausted nodes in the block parent. The convention here is // to return null. return nullptr; } // We have a parent: look for next sibling nextNode = curParent->GetNextSibling(); curNode = curParent; } // We have a next node. If it's a block, return it. if (IsBlockNode(nextNode)) { return nextNode; } if (mHTMLEditor->IsContainer(nextNode)) { // Else if it's a container, get deep leftmost child nsCOMPtr<nsIContent> child = mHTMLEditor->GetLeftmostChild(nextNode); if (child) { return child; } } // Else return the node itself return nextNode; } nsIContent* WSRunObject::GetNextWSNode(EditorDOMPoint aPoint, nsINode* aBlockParent) { // Can't really recycle various getnext/prior routines because we have // special needs here. Need to step into inline containers but not block // containers. MOZ_ASSERT(aPoint.node && aBlockParent); if (aPoint.node->NodeType() == nsIDOMNode::TEXT_NODE) { return GetNextWSNodeInner(aPoint.node, aBlockParent); } if (!mHTMLEditor->IsContainer(aPoint.node)) { return GetNextWSNodeInner(aPoint.node, aBlockParent); } nsCOMPtr<nsIContent> startContent = do_QueryInterface(aPoint.node); NS_ENSURE_TRUE(startContent, nullptr); nsCOMPtr<nsIContent> nextNode = startContent->GetChildAt(aPoint.offset); if (!nextNode) { if (aPoint.node == aBlockParent) { // We are at end of the block. return nullptr; } // We are at end of non-block container return GetNextWSNodeInner(aPoint.node, aBlockParent); } // We have a next node. If it's a block, return it. if (IsBlockNode(nextNode)) { return nextNode; } if (mHTMLEditor->IsContainer(nextNode)) { // else if it's a container, get deep leftmost child nsCOMPtr<nsIContent> child = mHTMLEditor->GetLeftmostChild(nextNode); if (child) { return child; } } // Else return the node itself return nextNode; } nsresult WSRunObject::PrepareToDeleteRangePriv(WSRunObject* aEndObject) { // this routine adjust whitespace before *this* and after aEndObject // in preperation for the two areas to become adjacent after the // intervening content is deleted. It's overly agressive right // now. There might be a block boundary remaining between them after // the deletion, in which case these adjstments are unneeded (though // I don't think they can ever be harmful?) NS_ENSURE_TRUE(aEndObject, NS_ERROR_NULL_POINTER); // get the runs before and after selection WSFragment *beforeRun, *afterRun; FindRun(mNode, mOffset, &beforeRun, false); aEndObject->FindRun(aEndObject->mNode, aEndObject->mOffset, &afterRun, true); // trim after run of any leading ws if (afterRun && (afterRun->mType & WSType::leadingWS)) { nsresult rv = aEndObject->DeleteChars(aEndObject->mNode, aEndObject->mOffset, afterRun->mEndNode, afterRun->mEndOffset, eOutsideUserSelectAll); NS_ENSURE_SUCCESS(rv, rv); } // adjust normal ws in afterRun if needed if (afterRun && afterRun->mType == WSType::normalWS && !aEndObject->mPRE) { if ((beforeRun && (beforeRun->mType & WSType::leadingWS)) || (!beforeRun && ((mStartReason & WSType::block) || mStartReason == WSType::br))) { // make sure leading char of following ws is an nbsp, so that it will show up WSPoint point = aEndObject->GetCharAfter(aEndObject->mNode, aEndObject->mOffset); if (point.mTextNode && nsCRT::IsAsciiSpace(point.mChar)) { nsresult rv = aEndObject->ConvertToNBSP(point, eOutsideUserSelectAll); NS_ENSURE_SUCCESS(rv, rv); } } } // trim before run of any trailing ws if (beforeRun && (beforeRun->mType & WSType::trailingWS)) { nsresult rv = DeleteChars(beforeRun->mStartNode, beforeRun->mStartOffset, mNode, mOffset, eOutsideUserSelectAll); NS_ENSURE_SUCCESS(rv, rv); } else if (beforeRun && beforeRun->mType == WSType::normalWS && !mPRE) { if ((afterRun && (afterRun->mType & WSType::trailingWS)) || (afterRun && afterRun->mType == WSType::normalWS) || (!afterRun && (aEndObject->mEndReason & WSType::block))) { // make sure trailing char of starting ws is an nbsp, so that it will show up WSPoint point = GetCharBefore(mNode, mOffset); if (point.mTextNode && nsCRT::IsAsciiSpace(point.mChar)) { RefPtr<Text> wsStartNode, wsEndNode; int32_t wsStartOffset, wsEndOffset; GetAsciiWSBounds(eBoth, mNode, mOffset, getter_AddRefs(wsStartNode), &wsStartOffset, getter_AddRefs(wsEndNode), &wsEndOffset); point.mTextNode = wsStartNode; point.mOffset = wsStartOffset; nsresult rv = ConvertToNBSP(point, eOutsideUserSelectAll); NS_ENSURE_SUCCESS(rv, rv); } } } return NS_OK; } nsresult WSRunObject::PrepareToSplitAcrossBlocksPriv() { // used to prepare ws to be split across two blocks. The main issue // here is make sure normalWS doesn't end up becoming non-significant // leading or trailing ws after the split. // get the runs before and after selection WSFragment *beforeRun, *afterRun; FindRun(mNode, mOffset, &beforeRun, false); FindRun(mNode, mOffset, &afterRun, true); // adjust normal ws in afterRun if needed if (afterRun && afterRun->mType == WSType::normalWS) { // make sure leading char of following ws is an nbsp, so that it will show up WSPoint point = GetCharAfter(mNode, mOffset); if (point.mTextNode && nsCRT::IsAsciiSpace(point.mChar)) { nsresult rv = ConvertToNBSP(point); NS_ENSURE_SUCCESS(rv, rv); } } // adjust normal ws in beforeRun if needed if (beforeRun && beforeRun->mType == WSType::normalWS) { // make sure trailing char of starting ws is an nbsp, so that it will show up WSPoint point = GetCharBefore(mNode, mOffset); if (point.mTextNode && nsCRT::IsAsciiSpace(point.mChar)) { RefPtr<Text> wsStartNode, wsEndNode; int32_t wsStartOffset, wsEndOffset; GetAsciiWSBounds(eBoth, mNode, mOffset, getter_AddRefs(wsStartNode), &wsStartOffset, getter_AddRefs(wsEndNode), &wsEndOffset); point.mTextNode = wsStartNode; point.mOffset = wsStartOffset; nsresult rv = ConvertToNBSP(point); NS_ENSURE_SUCCESS(rv, rv); } } return NS_OK; } nsresult WSRunObject::DeleteChars(nsINode* aStartNode, int32_t aStartOffset, nsINode* aEndNode, int32_t aEndOffset, AreaRestriction aAR) { // MOOSE: this routine needs to be modified to preserve the integrity of the // wsFragment info. NS_ENSURE_TRUE(aStartNode && aEndNode, NS_ERROR_NULL_POINTER); if (aAR == eOutsideUserSelectAll) { nsCOMPtr<nsIDOMNode> san = mHTMLEditor->FindUserSelectAllNode(GetAsDOMNode(aStartNode)); if (san) { return NS_OK; } if (aStartNode != aEndNode) { san = mHTMLEditor->FindUserSelectAllNode(GetAsDOMNode(aEndNode)); if (san) { return NS_OK; } } } if (aStartNode == aEndNode && aStartOffset == aEndOffset) { // Nothing to delete return NS_OK; } int32_t idx = mNodeArray.IndexOf(aStartNode); if (idx == -1) { // If our strarting point wasn't one of our ws text nodes, then just go // through them from the beginning. idx = 0; } if (aStartNode == aEndNode && aStartNode->GetAsText()) { return mHTMLEditor->DeleteText(*aStartNode->GetAsText(), static_cast<uint32_t>(aStartOffset), static_cast<uint32_t>(aEndOffset - aStartOffset)); } RefPtr<nsRange> range; int32_t count = mNodeArray.Length(); for (; idx < count; idx++) { RefPtr<Text> node = mNodeArray[idx]; if (!node) { // We ran out of ws nodes; must have been deleting to end return NS_OK; } if (node == aStartNode) { uint32_t len = node->Length(); if (uint32_t(aStartOffset) < len) { nsresult rv = mHTMLEditor->DeleteText(*node, AssertedCast<uint32_t>(aStartOffset), len - aStartOffset); NS_ENSURE_SUCCESS(rv, rv); } } else if (node == aEndNode) { if (aEndOffset) { nsresult rv = mHTMLEditor->DeleteText(*node, 0, AssertedCast<uint32_t>(aEndOffset)); NS_ENSURE_SUCCESS(rv, rv); } break; } else { if (!range) { range = new nsRange(aStartNode); nsresult rv = range->Set(aStartNode, aStartOffset, aEndNode, aEndOffset); NS_ENSURE_SUCCESS(rv, rv); } bool nodeBefore, nodeAfter; nsresult rv = nsRange::CompareNodeToRange(node, range, &nodeBefore, &nodeAfter); NS_ENSURE_SUCCESS(rv, rv); if (nodeAfter) { break; } if (!nodeBefore) { rv = mHTMLEditor->DeleteNode(node); NS_ENSURE_SUCCESS(rv, rv); mNodeArray.RemoveElement(node); --count; --idx; } } } return NS_OK; } WSRunObject::WSPoint WSRunObject::GetCharAfter(nsINode* aNode, int32_t aOffset) { MOZ_ASSERT(aNode); int32_t idx = mNodeArray.IndexOf(aNode); if (idx == -1) { // Use range comparisons to get right ws node return GetWSPointAfter(aNode, aOffset); } // Use WSPoint version of GetCharAfter() return GetCharAfter(WSPoint(mNodeArray[idx], aOffset, 0)); } WSRunObject::WSPoint WSRunObject::GetCharBefore(nsINode* aNode, int32_t aOffset) { MOZ_ASSERT(aNode); int32_t idx = mNodeArray.IndexOf(aNode); if (idx == -1) { // Use range comparisons to get right ws node return GetWSPointBefore(aNode, aOffset); } // Use WSPoint version of GetCharBefore() return GetCharBefore(WSPoint(mNodeArray[idx], aOffset, 0)); } WSRunObject::WSPoint WSRunObject::GetCharAfter(const WSPoint &aPoint) { MOZ_ASSERT(aPoint.mTextNode); WSPoint outPoint; outPoint.mTextNode = nullptr; outPoint.mOffset = 0; outPoint.mChar = 0; int32_t idx = mNodeArray.IndexOf(aPoint.mTextNode); if (idx == -1) { // Can't find point, but it's not an error return outPoint; } if (static_cast<uint16_t>(aPoint.mOffset) < aPoint.mTextNode->TextLength()) { outPoint = aPoint; outPoint.mChar = GetCharAt(aPoint.mTextNode, aPoint.mOffset); return outPoint; } int32_t numNodes = mNodeArray.Length(); if (idx + 1 < numNodes) { outPoint.mTextNode = mNodeArray[idx + 1]; MOZ_ASSERT(outPoint.mTextNode); outPoint.mOffset = 0; outPoint.mChar = GetCharAt(outPoint.mTextNode, 0); } return outPoint; } WSRunObject::WSPoint WSRunObject::GetCharBefore(const WSPoint &aPoint) { MOZ_ASSERT(aPoint.mTextNode); WSPoint outPoint; outPoint.mTextNode = nullptr; outPoint.mOffset = 0; outPoint.mChar = 0; int32_t idx = mNodeArray.IndexOf(aPoint.mTextNode); if (idx == -1) { // Can't find point, but it's not an error return outPoint; } if (aPoint.mOffset) { outPoint = aPoint; outPoint.mOffset--; outPoint.mChar = GetCharAt(aPoint.mTextNode, aPoint.mOffset - 1); return outPoint; } if (idx) { outPoint.mTextNode = mNodeArray[idx - 1]; uint32_t len = outPoint.mTextNode->TextLength(); if (len) { outPoint.mOffset = len - 1; outPoint.mChar = GetCharAt(outPoint.mTextNode, len - 1); } } return outPoint; } nsresult WSRunObject::ConvertToNBSP(WSPoint aPoint, AreaRestriction aAR) { // MOOSE: this routine needs to be modified to preserve the integrity of the // wsFragment info. NS_ENSURE_TRUE(aPoint.mTextNode, NS_ERROR_NULL_POINTER); if (aAR == eOutsideUserSelectAll) { nsCOMPtr<nsIDOMNode> san = mHTMLEditor->FindUserSelectAllNode(GetAsDOMNode(aPoint.mTextNode)); if (san) { return NS_OK; } } // First, insert an nbsp AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor); nsAutoString nbspStr(nbsp); nsresult rv = mHTMLEditor->InsertTextIntoTextNodeImpl(nbspStr, *aPoint.mTextNode, aPoint.mOffset, true); NS_ENSURE_SUCCESS(rv, rv); // Next, find range of ws it will replace RefPtr<Text> startNode, endNode; int32_t startOffset = 0, endOffset = 0; GetAsciiWSBounds(eAfter, aPoint.mTextNode, aPoint.mOffset + 1, getter_AddRefs(startNode), &startOffset, getter_AddRefs(endNode), &endOffset); // Finally, delete that replaced ws, if any if (startNode) { rv = DeleteChars(startNode, startOffset, endNode, endOffset); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } void WSRunObject::GetAsciiWSBounds(int16_t aDir, nsINode* aNode, int32_t aOffset, Text** outStartNode, int32_t* outStartOffset, Text** outEndNode, int32_t* outEndOffset) { MOZ_ASSERT(aNode && outStartNode && outStartOffset && outEndNode && outEndOffset); RefPtr<Text> startNode, endNode; int32_t startOffset = 0, endOffset = 0; if (aDir & eAfter) { WSPoint point = GetCharAfter(aNode, aOffset); if (point.mTextNode) { // We found a text node, at least startNode = endNode = point.mTextNode; startOffset = endOffset = point.mOffset; // Scan ahead to end of ASCII ws for (; nsCRT::IsAsciiSpace(point.mChar) && point.mTextNode; point = GetCharAfter(point)) { endNode = point.mTextNode; // endOffset is _after_ ws point.mOffset++; endOffset = point.mOffset; } } } if (aDir & eBefore) { WSPoint point = GetCharBefore(aNode, aOffset); if (point.mTextNode) { // We found a text node, at least startNode = point.mTextNode; startOffset = point.mOffset + 1; if (!endNode) { endNode = startNode; endOffset = startOffset; } // Scan back to start of ASCII ws for (; nsCRT::IsAsciiSpace(point.mChar) && point.mTextNode; point = GetCharBefore(point)) { startNode = point.mTextNode; startOffset = point.mOffset; } } } startNode.forget(outStartNode); *outStartOffset = startOffset; endNode.forget(outEndNode); *outEndOffset = endOffset; } /** * Given a dompoint, find the ws run that is before or after it, as caller * needs */ void WSRunObject::FindRun(nsINode* aNode, int32_t aOffset, WSFragment** outRun, bool after) { MOZ_ASSERT(aNode && outRun); *outRun = nullptr; for (WSFragment* run = mStartRun; run; run = run->mRight) { int32_t comp = run->mStartNode ? nsContentUtils::ComparePoints(aNode, aOffset, run->mStartNode, run->mStartOffset) : -1; if (comp <= 0) { if (after) { *outRun = run; } else { // before *outRun = nullptr; } return; } comp = run->mEndNode ? nsContentUtils::ComparePoints(aNode, aOffset, run->mEndNode, run->mEndOffset) : -1; if (comp < 0) { *outRun = run; return; } else if (!comp) { if (after) { *outRun = run->mRight; } else { // before *outRun = run; } return; } if (!run->mRight) { if (after) { *outRun = nullptr; } else { // before *outRun = run; } return; } } } char16_t WSRunObject::GetCharAt(Text* aTextNode, int32_t aOffset) { // return 0 if we can't get a char, for whatever reason NS_ENSURE_TRUE(aTextNode, 0); int32_t len = int32_t(aTextNode->TextLength()); if (aOffset < 0 || aOffset >= len) { return 0; } return aTextNode->GetText()->CharAt(aOffset); } WSRunObject::WSPoint WSRunObject::GetWSPointAfter(nsINode* aNode, int32_t aOffset) { // Note: only to be called if aNode is not a ws node. // Binary search on wsnodes uint32_t numNodes = mNodeArray.Length(); if (!numNodes) { // Do nothing if there are no nodes to search WSPoint outPoint; return outPoint; } uint32_t firstNum = 0, curNum = numNodes/2, lastNum = numNodes; int16_t cmp = 0; RefPtr<Text> curNode; // Begin binary search. We do this because we need to minimize calls to // ComparePoints(), which is expensive. while (curNum != lastNum) { curNode = mNodeArray[curNum]; cmp = nsContentUtils::ComparePoints(aNode, aOffset, curNode, 0); if (cmp < 0) { lastNum = curNum; } else { firstNum = curNum + 1; } curNum = (lastNum - firstNum)/2 + firstNum; MOZ_ASSERT(firstNum <= curNum && curNum <= lastNum, "Bad binary search"); } // When the binary search is complete, we always know that the current node // is the same as the end node, which is always past our range. Therefore, // we've found the node immediately after the point of interest. if (curNum == mNodeArray.Length()) { // hey asked for past our range (it's after the last node). GetCharAfter // will do the work for us when we pass it the last index of the last node. RefPtr<Text> textNode(mNodeArray[curNum - 1]); WSPoint point(textNode, textNode->TextLength(), 0); return GetCharAfter(point); } else { // The char after the point is the first character of our range. RefPtr<Text> textNode(mNodeArray[curNum]); WSPoint point(textNode, 0, 0); return GetCharAfter(point); } } WSRunObject::WSPoint WSRunObject::GetWSPointBefore(nsINode* aNode, int32_t aOffset) { // Note: only to be called if aNode is not a ws node. // Binary search on wsnodes uint32_t numNodes = mNodeArray.Length(); if (!numNodes) { // Do nothing if there are no nodes to search WSPoint outPoint; return outPoint; } uint32_t firstNum = 0, curNum = numNodes/2, lastNum = numNodes; int16_t cmp = 0; RefPtr<Text> curNode; // Begin binary search. We do this because we need to minimize calls to // ComparePoints(), which is expensive. while (curNum != lastNum) { curNode = mNodeArray[curNum]; cmp = nsContentUtils::ComparePoints(aNode, aOffset, curNode, 0); if (cmp < 0) { lastNum = curNum; } else { firstNum = curNum + 1; } curNum = (lastNum - firstNum)/2 + firstNum; MOZ_ASSERT(firstNum <= curNum && curNum <= lastNum, "Bad binary search"); } // When the binary search is complete, we always know that the current node // is the same as the end node, which is always past our range. Therefore, // we've found the node immediately after the point of interest. if (curNum == mNodeArray.Length()) { // Get the point before the end of the last node, we can pass the length of // the node into GetCharBefore, and it will return the last character. RefPtr<Text> textNode(mNodeArray[curNum - 1]); WSPoint point(textNode, textNode->TextLength(), 0); return GetCharBefore(point); } else { // We can just ask the current node for the point immediately before it, // it will handle moving to the previous node (if any) and returning the // appropriate character RefPtr<Text> textNode(mNodeArray[curNum]); WSPoint point(textNode, 0, 0); return GetCharBefore(point); } } nsresult WSRunObject::CheckTrailingNBSPOfRun(WSFragment *aRun) { // Try to change an nbsp to a space, if possible, just to prevent nbsp // proliferation. Examine what is before and after the trailing nbsp, if // any. NS_ENSURE_TRUE(aRun, NS_ERROR_NULL_POINTER); bool leftCheck = false; bool spaceNBSP = false; bool rightCheck = false; // confirm run is normalWS if (aRun->mType != WSType::normalWS) { return NS_ERROR_FAILURE; } // first check for trailing nbsp WSPoint thePoint = GetCharBefore(aRun->mEndNode, aRun->mEndOffset); if (thePoint.mTextNode && thePoint.mChar == nbsp) { // now check that what is to the left of it is compatible with replacing nbsp with space WSPoint prevPoint = GetCharBefore(thePoint); if (prevPoint.mTextNode) { if (!nsCRT::IsAsciiSpace(prevPoint.mChar)) { leftCheck = true; } else { spaceNBSP = true; } } else if (aRun->mLeftType == WSType::text || aRun->mLeftType == WSType::special) { leftCheck = true; } if (leftCheck || spaceNBSP) { // now check that what is to the right of it is compatible with replacing // nbsp with space if (aRun->mRightType == WSType::text || aRun->mRightType == WSType::special || aRun->mRightType == WSType::br) { rightCheck = true; } if ((aRun->mRightType & WSType::block) && IsBlockNode(GetWSBoundingParent())) { // We are at a block boundary. Insert a <br>. Why? Well, first note // that the br will have no visible effect since it is up against a // block boundary. |foo<br><p>bar| renders like |foo<p>bar| and // similarly |<p>foo<br></p>bar| renders like |<p>foo</p>bar|. What // this <br> addition gets us is the ability to convert a trailing nbsp // to a space. Consider: |<body>foo. '</body>|, where ' represents // selection. User types space attempting to put 2 spaces after the // end of their sentence. We used to do this as: |<body>foo. //  </body>| This caused problems with soft wrapping: the nbsp // would wrap to the next line, which looked attrocious. If you try to // do: |<body>foo.  </body>| instead, the trailing space is // invisible because it is against a block boundary. If you do: // |<body>foo.  </body>| then you get an even uglier soft // wrapping problem, where foo is on one line until you type the final // space, and then "foo " jumps down to the next line. Ugh. The best // way I can find out of this is to throw in a harmless <br> here, // which allows us to do: |<body>foo.  <br></body>|, which doesn't // cause foo to jump lines, doesn't cause spaces to show up at the // beginning of soft wrapped lines, and lets the user see 2 spaces when // they type 2 spaces. nsCOMPtr<Element> brNode = mHTMLEditor->CreateBR(aRun->mEndNode, aRun->mEndOffset); NS_ENSURE_TRUE(brNode, NS_ERROR_FAILURE); // Refresh thePoint, prevPoint thePoint = GetCharBefore(aRun->mEndNode, aRun->mEndOffset); prevPoint = GetCharBefore(thePoint); rightCheck = true; } } if (leftCheck && rightCheck) { // Now replace nbsp with space. First, insert a space AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor); nsAutoString spaceStr(char16_t(32)); nsresult rv = mHTMLEditor->InsertTextIntoTextNodeImpl(spaceStr, *thePoint.mTextNode, thePoint.mOffset, true); NS_ENSURE_SUCCESS(rv, rv); // Finally, delete that nbsp rv = DeleteChars(thePoint.mTextNode, thePoint.mOffset + 1, thePoint.mTextNode, thePoint.mOffset + 2); NS_ENSURE_SUCCESS(rv, rv); } else if (!mPRE && spaceNBSP && rightCheck) { // Don't mess with this preformatted for now. We have a run of ASCII // whitespace (which will render as one space) followed by an nbsp (which // is at the end of the whitespace run). Let's switch their order. This // will ensure that if someone types two spaces after a sentence, and the // editor softwraps at this point, the spaces won't be split across lines, // which looks ugly and is bad for the moose. RefPtr<Text> startNode, endNode; int32_t startOffset, endOffset; GetAsciiWSBounds(eBoth, prevPoint.mTextNode, prevPoint.mOffset + 1, getter_AddRefs(startNode), &startOffset, getter_AddRefs(endNode), &endOffset); // Delete that nbsp nsresult rv = DeleteChars(thePoint.mTextNode, thePoint.mOffset, thePoint.mTextNode, thePoint.mOffset + 1); NS_ENSURE_SUCCESS(rv, rv); // Finally, insert that nbsp before the ASCII ws run AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor); nsAutoString nbspStr(nbsp); rv = mHTMLEditor->InsertTextIntoTextNodeImpl(nbspStr, *startNode, startOffset, true); NS_ENSURE_SUCCESS(rv, rv); } } return NS_OK; } nsresult WSRunObject::CheckTrailingNBSP(WSFragment* aRun, nsINode* aNode, int32_t aOffset) { // Try to change an nbsp to a space, if possible, just to prevent nbsp // proliferation. This routine is called when we are about to make this // point in the ws abut an inserted break or text, so we don't have to worry // about what is after it. What is after it now will end up after the // inserted object. NS_ENSURE_TRUE(aRun && aNode, NS_ERROR_NULL_POINTER); bool canConvert = false; WSPoint thePoint = GetCharBefore(aNode, aOffset); if (thePoint.mTextNode && thePoint.mChar == nbsp) { WSPoint prevPoint = GetCharBefore(thePoint); if (prevPoint.mTextNode) { if (!nsCRT::IsAsciiSpace(prevPoint.mChar)) { canConvert = true; } } else if (aRun->mLeftType == WSType::text || aRun->mLeftType == WSType::special) { canConvert = true; } } if (canConvert) { // First, insert a space AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor); nsAutoString spaceStr(char16_t(32)); nsresult rv = mHTMLEditor->InsertTextIntoTextNodeImpl(spaceStr, *thePoint.mTextNode, thePoint.mOffset, true); NS_ENSURE_SUCCESS(rv, rv); // Finally, delete that nbsp rv = DeleteChars(thePoint.mTextNode, thePoint.mOffset + 1, thePoint.mTextNode, thePoint.mOffset + 2); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } nsresult WSRunObject::CheckLeadingNBSP(WSFragment* aRun, nsINode* aNode, int32_t aOffset) { // Try to change an nbsp to a space, if possible, just to prevent nbsp // proliferation This routine is called when we are about to make this point // in the ws abut an inserted text, so we don't have to worry about what is // before it. What is before it now will end up before the inserted text. bool canConvert = false; WSPoint thePoint = GetCharAfter(aNode, aOffset); if (thePoint.mChar == nbsp) { WSPoint tmp = thePoint; // we want to be after thePoint tmp.mOffset++; WSPoint nextPoint = GetCharAfter(tmp); if (nextPoint.mTextNode) { if (!nsCRT::IsAsciiSpace(nextPoint.mChar)) { canConvert = true; } } else if (aRun->mRightType == WSType::text || aRun->mRightType == WSType::special || aRun->mRightType == WSType::br) { canConvert = true; } } if (canConvert) { // First, insert a space AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor); nsAutoString spaceStr(char16_t(32)); nsresult rv = mHTMLEditor->InsertTextIntoTextNodeImpl(spaceStr, *thePoint.mTextNode, thePoint.mOffset, true); NS_ENSURE_SUCCESS(rv, rv); // Finally, delete that nbsp rv = DeleteChars(thePoint.mTextNode, thePoint.mOffset + 1, thePoint.mTextNode, thePoint.mOffset + 2); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } nsresult WSRunObject::Scrub() { WSFragment *run = mStartRun; while (run) { if (run->mType & (WSType::leadingWS | WSType::trailingWS)) { nsresult rv = DeleteChars(run->mStartNode, run->mStartOffset, run->mEndNode, run->mEndOffset); NS_ENSURE_SUCCESS(rv, rv); } run = run->mRight; } return NS_OK; } bool WSRunObject::IsBlockNode(nsINode* aNode) { return aNode && aNode->IsElement() && HTMLEditor::NodeIsBlockStatic(aNode->AsElement()); } } // namespace mozilla