зеркало из https://github.com/mozilla/gecko-dev.git
2029 строки
68 KiB
C++
2029 строки
68 KiB
C++
/* -*- 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/EditorDOMPoint.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 kNBSP = 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->ComputeIndexOf(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(Selection& aSelection,
|
|
const EditorRawDOMPoint& aPointToInsert,
|
|
nsIEditor::EDirection aSelect)
|
|
{
|
|
if (NS_WARN_IF(!aPointToInsert.IsSet())) {
|
|
return nullptr;
|
|
}
|
|
|
|
// MOOSE: for now, we always assume non-PRE formatting. Fix this later.
|
|
// meanwhile, the pre case is handled in WillInsertText in
|
|
// HTMLEditRules.cpp
|
|
|
|
WSFragment* beforeRun = FindNearestRun(aPointToInsert, false);
|
|
WSFragment* afterRun = FindNearestRun(aPointToInsert, true);
|
|
|
|
EditorDOMPoint pointToInsert(aPointToInsert);
|
|
{
|
|
// Some scoping for AutoTrackDOMPoint. This will track our insertion
|
|
// point while we tweak any surrounding whitespace
|
|
AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater, &pointToInsert);
|
|
|
|
// 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 = DeleteRange(pointToInsert.AsRaw(), afterRun->EndPoint());
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return 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 = GetNextCharPoint(pointToInsert.AsRaw());
|
|
if (thePoint.mTextNode && nsCRT::IsAsciiSpace(thePoint.mChar)) {
|
|
WSPoint prevPoint = GetPreviousCharPoint(thePoint);
|
|
if (!prevPoint.mTextNode ||
|
|
(prevPoint.mTextNode && !nsCRT::IsAsciiSpace(prevPoint.mChar))) {
|
|
// We are at start of non-nbsps. Convert to a single nbsp.
|
|
nsresult rv = InsertNBSPAndRemoveFollowingASCIIWhitespaces(thePoint);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return 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 = DeleteRange(beforeRun->StartPoint(), pointToInsert.AsRaw());
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return nullptr;
|
|
}
|
|
} else if (beforeRun->mType == WSType::normalWS) {
|
|
// Try to change an nbsp to a space, just to prevent nbsp proliferation
|
|
nsresult rv =
|
|
ReplacePreviousNBSPIfUnncessary(beforeRun, pointToInsert.AsRaw());
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
RefPtr<Element> newBRElement =
|
|
mHTMLEditor->CreateBRImpl(aSelection, pointToInsert.AsRaw(), aSelect);
|
|
if (NS_WARN_IF(!newBRElement)) {
|
|
return nullptr;
|
|
}
|
|
return newBRElement.forget();
|
|
}
|
|
|
|
nsresult
|
|
WSRunObject::InsertText(nsIDocument& aDocument,
|
|
const nsAString& aStringToInsert,
|
|
const EditorRawDOMPoint& aPointToInsert,
|
|
EditorRawDOMPoint* aPointAfterInsertedString)
|
|
{
|
|
// 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
|
|
|
|
if (NS_WARN_IF(!aPointToInsert.IsSet())) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
MOZ_ASSERT(aPointToInsert.IsSet());
|
|
|
|
|
|
if (aStringToInsert.IsEmpty()) {
|
|
if (aPointAfterInsertedString) {
|
|
*aPointAfterInsertedString = aPointToInsert;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
WSFragment* beforeRun = FindNearestRun(aPointToInsert, false);
|
|
WSFragment* afterRun = FindNearestRun(aPointToInsert, true);
|
|
|
|
EditorDOMPoint pointToInsert(aPointToInsert);
|
|
nsAutoString theString(aStringToInsert);
|
|
{
|
|
// Some scoping for AutoTrackDOMPoint. This will track our insertion
|
|
// point while we tweak any surrounding whitespace
|
|
AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater, &pointToInsert);
|
|
|
|
// 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 = DeleteRange(pointToInsert.AsRaw(), afterRun->EndPoint());
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return 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, pointToInsert.GetContainer(),
|
|
pointToInsert.Offset());
|
|
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 = DeleteRange(beforeRun->StartPoint(), pointToInsert.AsRaw());
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
} else if (beforeRun->mType == WSType::normalWS) {
|
|
// Try to change an nbsp to a space, if possible, just to prevent nbsp
|
|
// proliferation
|
|
nsresult rv =
|
|
ReplacePreviousNBSPIfUnncessary(beforeRun, pointToInsert.AsRaw());
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
}
|
|
|
|
// After this block, pointToInsert is modified by AutoTrackDOMPoint.
|
|
}
|
|
|
|
// 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(kNBSP, 0);
|
|
} else if (beforeRun->mType & WSType::normalWS) {
|
|
WSPoint wspoint = GetPreviousCharPoint(pointToInsert.AsRaw());
|
|
if (wspoint.mTextNode && nsCRT::IsAsciiSpace(wspoint.mChar)) {
|
|
theString.SetCharAt(kNBSP, 0);
|
|
}
|
|
}
|
|
} else if (mStartReason & WSType::block || mStartReason == WSType::br) {
|
|
theString.SetCharAt(kNBSP, 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(kNBSP, lastCharIndex);
|
|
} else if (afterRun->mType & WSType::normalWS) {
|
|
WSPoint wspoint = GetNextCharPoint(pointToInsert.AsRaw());
|
|
if (wspoint.mTextNode && nsCRT::IsAsciiSpace(wspoint.mChar)) {
|
|
theString.SetCharAt(kNBSP, lastCharIndex);
|
|
}
|
|
}
|
|
} else if (mEndReason & WSType::block) {
|
|
theString.SetCharAt(kNBSP, 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(kNBSP, i - 1);
|
|
} else {
|
|
prevWS = true;
|
|
}
|
|
} else {
|
|
prevWS = false;
|
|
}
|
|
}
|
|
|
|
// Ready, aim, fire!
|
|
nsresult rv =
|
|
mHTMLEditor->InsertTextImpl(aDocument, theString, pointToInsert.AsRaw(),
|
|
aPointAfterInsertedString);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return NS_OK;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
WSRunObject::DeleteWSBackward()
|
|
{
|
|
WSPoint point = GetPreviousCharPoint(Point());
|
|
NS_ENSURE_TRUE(point.mTextNode, NS_OK); // nothing to delete
|
|
|
|
// Easy case, preformatted ws.
|
|
if (mPRE && (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == kNBSP)) {
|
|
nsresult rv =
|
|
DeleteRange(EditorRawDOMPoint(point.mTextNode, point.mOffset),
|
|
EditorRawDOMPoint(point.mTextNode, point.mOffset + 1));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
// 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;
|
|
GetASCIIWhitespacesBounds(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
|
|
rv = DeleteRange(EditorRawDOMPoint(startNode, startOffset),
|
|
EditorRawDOMPoint(endNode, endOffset));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
if (point.mChar == kNBSP) {
|
|
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
|
|
rv = DeleteRange(EditorRawDOMPoint(node, startOffset),
|
|
EditorRawDOMPoint(node, endOffset));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
WSRunObject::DeleteWSForward()
|
|
{
|
|
WSPoint point = GetNextCharPoint(Point());
|
|
NS_ENSURE_TRUE(point.mTextNode, NS_OK); // nothing to delete
|
|
|
|
// Easy case, preformatted ws.
|
|
if (mPRE && (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == kNBSP)) {
|
|
nsresult rv =
|
|
DeleteRange(EditorRawDOMPoint(point.mTextNode, point.mOffset),
|
|
EditorRawDOMPoint(point.mTextNode, point.mOffset + 1));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
// 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;
|
|
GetASCIIWhitespacesBounds(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
|
|
rv = DeleteRange(EditorRawDOMPoint(startNode, startOffset),
|
|
EditorRawDOMPoint(endNode, endOffset));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
if (point.mChar == kNBSP) {
|
|
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
|
|
rv = DeleteRange(EditorRawDOMPoint(node, startOffset),
|
|
EditorRawDOMPoint(node, endOffset));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
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 = FindNearestRun(EditorRawDOMPoint(aNode, aOffset), false);
|
|
|
|
// Is there a visible run there or earlier?
|
|
for (; run; run = run->mLeft) {
|
|
if (run->mType == WSType::normalWS) {
|
|
WSPoint point = GetPreviousCharPoint(EditorRawDOMPoint(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 == kNBSP) {
|
|
*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 = FindNearestRun(EditorRawDOMPoint(aNode, aOffset), true);
|
|
|
|
// Is there a visible run there or later?
|
|
for (; run; run = run->mRight) {
|
|
if (run->mType == WSType::normalWS) {
|
|
WSPoint point = GetNextCharPoint(EditorRawDOMPoint(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 == kNBSP) {
|
|
*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 != kNBSP) {
|
|
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.Set(textNode, 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.GetContainer();
|
|
mStartOffset = start.Offset();
|
|
mStartReason = WSType::otherBlock;
|
|
mStartReasonNode = priorNode;
|
|
} else if (priorNode->IsNodeOfType(nsINode::eTEXT) &&
|
|
priorNode->IsEditable()) {
|
|
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.Set(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 != kNBSP) {
|
|
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.Set(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.GetContainer();
|
|
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.GetContainer();
|
|
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 != kNBSP) {
|
|
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.Set(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.GetContainer();
|
|
mEndOffset = end.Offset();
|
|
mEndReason = WSType::otherBlock;
|
|
mEndReasonNode = nextNode;
|
|
} else if (nextNode->IsNodeOfType(nsINode::eTEXT) &&
|
|
nextNode->IsEditable()) {
|
|
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.Set(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 != kNBSP) {
|
|
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.Set(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.GetContainer();
|
|
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.GetContainer();
|
|
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(const 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.IsSet() && aBlockParent);
|
|
|
|
if (aPoint.IsInTextNode()) {
|
|
return GetPreviousWSNodeInner(aPoint.GetContainer(), aBlockParent);
|
|
}
|
|
if (!mHTMLEditor->IsContainer(aPoint.GetContainer())) {
|
|
return GetPreviousWSNodeInner(aPoint.GetContainer(), aBlockParent);
|
|
}
|
|
|
|
if (!aPoint.Offset()) {
|
|
if (aPoint.GetContainer() == aBlockParent) {
|
|
// We are at start of the block.
|
|
return nullptr;
|
|
}
|
|
|
|
// We are at start of non-block container
|
|
return GetPreviousWSNodeInner(aPoint.GetContainer(), aBlockParent);
|
|
}
|
|
|
|
if (NS_WARN_IF(!aPoint.GetContainerAsContent())) {
|
|
return nullptr;
|
|
}
|
|
|
|
nsCOMPtr<nsIContent> priorNode = aPoint.GetPreviousSiblingOfChild();
|
|
if (NS_WARN_IF(!priorNode)) {
|
|
return 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(const 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.IsSet() && aBlockParent);
|
|
|
|
if (aPoint.IsInTextNode()) {
|
|
return GetNextWSNodeInner(aPoint.GetContainer(), aBlockParent);
|
|
}
|
|
if (!mHTMLEditor->IsContainer(aPoint.GetContainer())) {
|
|
return GetNextWSNodeInner(aPoint.GetContainer(), aBlockParent);
|
|
}
|
|
|
|
if (NS_WARN_IF(!aPoint.GetContainerAsContent())) {
|
|
return nullptr;
|
|
}
|
|
|
|
nsCOMPtr<nsIContent> nextNode = aPoint.GetChild();
|
|
if (!nextNode) {
|
|
if (aPoint.GetContainer() == aBlockParent) {
|
|
// We are at end of the block.
|
|
return nullptr;
|
|
}
|
|
|
|
// We are at end of non-block container
|
|
return GetNextWSNodeInner(aPoint.GetContainer(), 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 = FindNearestRun(Point(), false);
|
|
WSFragment* afterRun = aEndObject->FindNearestRun(aEndObject->Point(), true);
|
|
|
|
// trim after run of any leading ws
|
|
if (afterRun && (afterRun->mType & WSType::leadingWS)) {
|
|
nsresult rv =
|
|
aEndObject->DeleteRange(aEndObject->Point(), afterRun->EndPoint());
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return 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->GetNextCharPoint(aEndObject->Point());
|
|
if (point.mTextNode && nsCRT::IsAsciiSpace(point.mChar)) {
|
|
nsresult rv =
|
|
aEndObject->InsertNBSPAndRemoveFollowingASCIIWhitespaces(point);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// trim before run of any trailing ws
|
|
if (beforeRun && (beforeRun->mType & WSType::trailingWS)) {
|
|
nsresult rv = DeleteRange(beforeRun->StartPoint(), Point());
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return 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 = GetPreviousCharPoint(Point());
|
|
if (point.mTextNode && nsCRT::IsAsciiSpace(point.mChar)) {
|
|
RefPtr<Text> wsStartNode, wsEndNode;
|
|
int32_t wsStartOffset, wsEndOffset;
|
|
GetASCIIWhitespacesBounds(eBoth, mNode, mOffset,
|
|
getter_AddRefs(wsStartNode), &wsStartOffset,
|
|
getter_AddRefs(wsEndNode), &wsEndOffset);
|
|
point.mTextNode = wsStartNode;
|
|
point.mOffset = wsStartOffset;
|
|
nsresult rv = InsertNBSPAndRemoveFollowingASCIIWhitespaces(point);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return 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 = FindNearestRun(Point(), false);
|
|
WSFragment* afterRun = FindNearestRun(Point(), 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 = GetNextCharPoint(Point());
|
|
if (point.mTextNode && nsCRT::IsAsciiSpace(point.mChar)) {
|
|
nsresult rv = InsertNBSPAndRemoveFollowingASCIIWhitespaces(point);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return 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 = GetPreviousCharPoint(Point());
|
|
if (point.mTextNode && nsCRT::IsAsciiSpace(point.mChar)) {
|
|
RefPtr<Text> wsStartNode, wsEndNode;
|
|
int32_t wsStartOffset, wsEndOffset;
|
|
GetASCIIWhitespacesBounds(eBoth, mNode, mOffset,
|
|
getter_AddRefs(wsStartNode), &wsStartOffset,
|
|
getter_AddRefs(wsEndNode), &wsEndOffset);
|
|
point.mTextNode = wsStartNode;
|
|
point.mOffset = wsStartOffset;
|
|
nsresult rv = InsertNBSPAndRemoveFollowingASCIIWhitespaces(point);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
}
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
WSRunObject::DeleteRange(const EditorRawDOMPoint& aStartPoint,
|
|
const EditorRawDOMPoint& aEndPoint)
|
|
{
|
|
if (NS_WARN_IF(!aStartPoint.IsSet()) ||
|
|
NS_WARN_IF(!aEndPoint.IsSet())) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
MOZ_ASSERT(aStartPoint.IsSetAndValid());
|
|
MOZ_ASSERT(aEndPoint.IsSetAndValid());
|
|
|
|
// MOOSE: this routine needs to be modified to preserve the integrity of the
|
|
// wsFragment info.
|
|
|
|
if (aStartPoint == aEndPoint) {
|
|
// Nothing to delete
|
|
return NS_OK;
|
|
}
|
|
|
|
if (aStartPoint.GetContainer() == aEndPoint.GetContainer() &&
|
|
aStartPoint.IsInTextNode()) {
|
|
return mHTMLEditor->DeleteText(*aStartPoint.GetContainerAsText(),
|
|
aStartPoint.Offset(),
|
|
aEndPoint.Offset() - aStartPoint.Offset());
|
|
}
|
|
|
|
RefPtr<nsRange> range;
|
|
int32_t count = mNodeArray.Length();
|
|
int32_t idx = mNodeArray.IndexOf(aStartPoint.GetContainer());
|
|
if (idx == -1) {
|
|
// If our starting point wasn't one of our ws text nodes, then just go
|
|
// through them from the beginning.
|
|
idx = 0;
|
|
}
|
|
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 == aStartPoint.GetContainer()) {
|
|
if (!aStartPoint.IsEndOfContainer()) {
|
|
nsresult rv =
|
|
mHTMLEditor->DeleteText(*node, aStartPoint.Offset(),
|
|
aStartPoint.GetContainer()->Length() -
|
|
aStartPoint.Offset());
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
}
|
|
} else if (node == aEndPoint.GetContainer()) {
|
|
if (!aEndPoint.IsStartOfContainer()) {
|
|
nsresult rv = mHTMLEditor->DeleteText(*node, 0, aEndPoint.Offset());
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
}
|
|
break;
|
|
} else {
|
|
if (!range) {
|
|
range = new nsRange(aStartPoint.GetContainer());
|
|
nsresult rv = range->SetStartAndEnd(aStartPoint, aEndPoint);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
}
|
|
bool nodeBefore, nodeAfter;
|
|
nsresult rv =
|
|
nsRange::CompareNodeToRange(node, range, &nodeBefore, &nodeAfter);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
if (nodeAfter) {
|
|
break;
|
|
}
|
|
if (!nodeBefore) {
|
|
rv = mHTMLEditor->DeleteNode(node);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
mNodeArray.RemoveElement(node);
|
|
--count;
|
|
--idx;
|
|
}
|
|
}
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
WSRunObject::WSPoint
|
|
WSRunObject::GetNextCharPoint(const EditorRawDOMPoint& aPoint)
|
|
{
|
|
MOZ_ASSERT(aPoint.IsSetAndValid());
|
|
|
|
int32_t idx = mNodeArray.IndexOf(aPoint.GetContainer());
|
|
if (idx == -1) {
|
|
// Use range comparisons to get next text node which is in mNodeArray.
|
|
return GetNextCharPointInternal(aPoint);
|
|
}
|
|
// Use WSPoint version of GetNextCharPoint()
|
|
return GetNextCharPoint(WSPoint(mNodeArray[idx], aPoint.Offset(), 0));
|
|
}
|
|
|
|
WSRunObject::WSPoint
|
|
WSRunObject::GetPreviousCharPoint(const EditorRawDOMPoint& aPoint)
|
|
{
|
|
MOZ_ASSERT(aPoint.IsSetAndValid());
|
|
|
|
int32_t idx = mNodeArray.IndexOf(aPoint.GetContainer());
|
|
if (idx == -1) {
|
|
// Use range comparisons to get previous text node which is in mNodeArray.
|
|
return GetPreviousCharPointInternal(aPoint);
|
|
}
|
|
// Use WSPoint version of GetPreviousCharPoint()
|
|
return GetPreviousCharPoint(WSPoint(mNodeArray[idx], aPoint.Offset(), 0));
|
|
}
|
|
|
|
WSRunObject::WSPoint
|
|
WSRunObject::GetNextCharPoint(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::GetPreviousCharPoint(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::InsertNBSPAndRemoveFollowingASCIIWhitespaces(WSPoint aPoint)
|
|
{
|
|
// MOOSE: this routine needs to be modified to preserve the integrity of the
|
|
// wsFragment info.
|
|
if (NS_WARN_IF(!aPoint.mTextNode)) {
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
// First, insert an NBSP.
|
|
AutoTransactionsConserveSelection dontChangeMySelection(mHTMLEditor);
|
|
nsresult rv =
|
|
mHTMLEditor->InsertTextIntoTextNodeImpl(nsDependentSubstring(&kNBSP, 1),
|
|
*aPoint.mTextNode, aPoint.mOffset,
|
|
true);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
|
|
// Now, the text node may have been modified by mutation observer.
|
|
// So, the NBSP may have gone.
|
|
if (aPoint.mTextNode->TextDataLength() <= aPoint.mOffset ||
|
|
aPoint.mTextNode->GetText()->CharAt(aPoint.mOffset) != kNBSP) {
|
|
// This is just preparation of an edit action. Let's return NS_OK.
|
|
// XXX Perhaps, we should return another success code which indicates
|
|
// mutation observer touched the DOM tree. However, that should
|
|
// be returned from each transaction's DoTransaction.
|
|
return NS_OK;
|
|
}
|
|
|
|
// Next, find range of whitespaces it will be replaced.
|
|
RefPtr<Text> startNode, endNode;
|
|
int32_t startOffset = 0, endOffset = 0;
|
|
|
|
GetASCIIWhitespacesBounds(eAfter, aPoint.mTextNode, aPoint.mOffset + 1,
|
|
getter_AddRefs(startNode), &startOffset,
|
|
getter_AddRefs(endNode), &endOffset);
|
|
|
|
// Finally, delete that replaced ws, if any
|
|
if (startNode) {
|
|
rv = DeleteRange(EditorRawDOMPoint(startNode, startOffset),
|
|
EditorRawDOMPoint(endNode, endOffset));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
WSRunObject::GetASCIIWhitespacesBounds(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 = GetNextCharPoint(EditorRawDOMPoint(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 = GetNextCharPoint(point)) {
|
|
endNode = point.mTextNode;
|
|
// endOffset is _after_ ws
|
|
point.mOffset++;
|
|
endOffset = point.mOffset;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (aDir & eBefore) {
|
|
WSPoint point = GetPreviousCharPoint(EditorRawDOMPoint(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 = GetPreviousCharPoint(point)) {
|
|
startNode = point.mTextNode;
|
|
startOffset = point.mOffset;
|
|
}
|
|
}
|
|
}
|
|
|
|
startNode.forget(outStartNode);
|
|
*outStartOffset = startOffset;
|
|
endNode.forget(outEndNode);
|
|
*outEndOffset = endOffset;
|
|
}
|
|
|
|
WSRunObject::WSFragment*
|
|
WSRunObject::FindNearestRun(const EditorRawDOMPoint& aPoint,
|
|
bool aForward)
|
|
{
|
|
MOZ_ASSERT(aPoint.IsSetAndValid());
|
|
|
|
for (WSFragment* run = mStartRun; run; run = run->mRight) {
|
|
int32_t comp = run->mStartNode ?
|
|
nsContentUtils::ComparePoints(aPoint, run->StartPoint()) : -1;
|
|
if (comp <= 0) {
|
|
// aPoint equals or before start of the run. Return the run if we're
|
|
// scanning forward, otherwise, nullptr.
|
|
return aForward ? run : nullptr;
|
|
}
|
|
|
|
comp = run->mEndNode ?
|
|
nsContentUtils::ComparePoints(aPoint, run->EndPoint()) : -1;
|
|
if (comp < 0) {
|
|
// If aPoint is in the run, return the run.
|
|
return run;
|
|
}
|
|
|
|
if (!comp) {
|
|
// If aPoint is at end of the run, return next run if we're scanning
|
|
// forward, otherwise, return the run.
|
|
return aForward ? run->mRight : run;
|
|
}
|
|
|
|
if (!run->mRight) {
|
|
// If the run is the last run and aPoint is after end of the last run,
|
|
// return nullptr if we're scanning forward, otherwise, return this
|
|
// last run.
|
|
return aForward ? nullptr : run;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
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::GetNextCharPointInternal(const EditorRawDOMPoint& aPoint)
|
|
{
|
|
// Note: only to be called if aPoint.GetContainer() 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;
|
|
}
|
|
|
|
// Begin binary search. We do this because we need to minimize calls to
|
|
// ComparePoints(), which is expensive.
|
|
uint32_t firstNum = 0, curNum = numNodes / 2, lastNum = numNodes;
|
|
while (curNum != lastNum) {
|
|
RefPtr<Text> curNode = mNodeArray[curNum];
|
|
int16_t cmp =
|
|
nsContentUtils::ComparePoints(aPoint, EditorRawDOMPoint(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).
|
|
// GetNextCharPoint() 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 GetNextCharPoint(point);
|
|
}
|
|
|
|
// The char after the point is the first character of our range.
|
|
RefPtr<Text> textNode(mNodeArray[curNum]);
|
|
WSPoint point(textNode, 0, 0);
|
|
return GetNextCharPoint(point);
|
|
}
|
|
|
|
WSRunObject::WSPoint
|
|
WSRunObject::GetPreviousCharPointInternal(const EditorRawDOMPoint& aPoint)
|
|
{
|
|
// 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(aPoint, EditorRawDOMPoint(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 GetPreviousCharPoint(), and it will return the last
|
|
// character.
|
|
RefPtr<Text> textNode(mNodeArray[curNum - 1]);
|
|
WSPoint point(textNode, textNode->TextLength(), 0);
|
|
return GetPreviousCharPoint(point);
|
|
}
|
|
|
|
// 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 GetPreviousCharPoint(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;
|
|
}
|
|
|
|
if (NS_WARN_IF(!mHTMLEditor)) {
|
|
return NS_ERROR_NOT_INITIALIZED;
|
|
}
|
|
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
|
|
|
|
// first check for trailing nbsp
|
|
WSPoint thePoint = GetPreviousCharPoint(aRun->EndPoint());
|
|
if (thePoint.mTextNode && thePoint.mChar == kNBSP) {
|
|
// now check that what is to the left of it is compatible with replacing nbsp with space
|
|
WSPoint prevPoint = GetPreviousCharPoint(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.
|
|
|
|
RefPtr<Element> brNode = htmlEditor->CreateBR(aRun->EndPoint());
|
|
if (NS_WARN_IF(!brNode)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
// Refresh thePoint, prevPoint
|
|
thePoint = GetPreviousCharPoint(aRun->EndPoint());
|
|
prevPoint = GetPreviousCharPoint(thePoint);
|
|
rightCheck = true;
|
|
}
|
|
}
|
|
if (leftCheck && rightCheck) {
|
|
// Now replace nbsp with space. First, insert a space
|
|
AutoTransactionsConserveSelection dontChangeMySelection(htmlEditor);
|
|
nsAutoString spaceStr(char16_t(32));
|
|
nsresult rv =
|
|
htmlEditor->InsertTextIntoTextNodeImpl(spaceStr, *thePoint.mTextNode,
|
|
thePoint.mOffset, true);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
// Finally, delete that nbsp
|
|
rv = DeleteRange(EditorRawDOMPoint(thePoint.mTextNode,
|
|
thePoint.mOffset + 1),
|
|
EditorRawDOMPoint(thePoint.mTextNode,
|
|
thePoint.mOffset + 2));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return 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;
|
|
GetASCIIWhitespacesBounds(eBoth, prevPoint.mTextNode,
|
|
prevPoint.mOffset + 1,
|
|
getter_AddRefs(startNode), &startOffset,
|
|
getter_AddRefs(endNode), &endOffset);
|
|
|
|
// Delete that nbsp
|
|
nsresult rv = DeleteRange(EditorRawDOMPoint(thePoint.mTextNode,
|
|
thePoint.mOffset),
|
|
EditorRawDOMPoint(thePoint.mTextNode,
|
|
thePoint.mOffset + 1));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
|
|
// Finally, insert that nbsp before the ASCII ws run
|
|
AutoTransactionsConserveSelection dontChangeMySelection(htmlEditor);
|
|
rv =
|
|
htmlEditor->InsertTextIntoTextNodeImpl(nsDependentSubstring(&kNBSP, 1),
|
|
*startNode, startOffset, true);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
WSRunObject::ReplacePreviousNBSPIfUnncessary(WSFragment* aRun,
|
|
const EditorRawDOMPoint& aPoint)
|
|
{
|
|
if (NS_WARN_IF(!aRun) ||
|
|
NS_WARN_IF(!aPoint.IsSet())) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
MOZ_ASSERT(aPoint.IsSetAndValid());
|
|
|
|
// 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.
|
|
bool canConvert = false;
|
|
WSPoint thePoint = GetPreviousCharPoint(aPoint);
|
|
if (thePoint.mTextNode && thePoint.mChar == kNBSP) {
|
|
WSPoint prevPoint = GetPreviousCharPoint(thePoint);
|
|
if (prevPoint.mTextNode) {
|
|
if (!nsCRT::IsAsciiSpace(prevPoint.mChar)) {
|
|
// If previous character is a NBSP and its previous character isn't
|
|
// ASCII space, we can replace the NBSP with ASCII space.
|
|
canConvert = true;
|
|
}
|
|
} else if (aRun->mLeftType == WSType::text ||
|
|
aRun->mLeftType == WSType::special) {
|
|
// If previous character is a NBSP and it's the first character of the
|
|
// text node, additionally, if its previous node is a text node including
|
|
// non-whitespace characters or <img> node or something inline
|
|
// non-container element node, we can replace the NBSP with ASCII space.
|
|
canConvert = true;
|
|
}
|
|
}
|
|
|
|
if (!canConvert) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// First, insert a space before the previous NBSP.
|
|
AutoTransactionsConserveSelection dontChangeMySelection(mHTMLEditor);
|
|
nsAutoString spaceStr(char16_t(32));
|
|
nsresult rv =
|
|
mHTMLEditor->InsertTextIntoTextNodeImpl(spaceStr, *thePoint.mTextNode,
|
|
thePoint.mOffset, true);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
|
|
// Finally, delete the previous NBSP.
|
|
rv = DeleteRange(EditorRawDOMPoint(thePoint.mTextNode,
|
|
thePoint.mOffset + 1),
|
|
EditorRawDOMPoint(thePoint.mTextNode,
|
|
thePoint.mOffset + 2));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return 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 = GetNextCharPoint(EditorRawDOMPoint(aNode, aOffset));
|
|
if (thePoint.mChar == kNBSP) {
|
|
WSPoint tmp = thePoint;
|
|
// we want to be after thePoint
|
|
tmp.mOffset++;
|
|
WSPoint nextPoint = GetNextCharPoint(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 dontChangeMySelection(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 = DeleteRange(EditorRawDOMPoint(thePoint.mTextNode,
|
|
thePoint.mOffset + 1),
|
|
EditorRawDOMPoint(thePoint.mTextNode,
|
|
thePoint.mOffset + 2));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
|
|
nsresult
|
|
WSRunObject::Scrub()
|
|
{
|
|
WSFragment *run = mStartRun;
|
|
while (run) {
|
|
if (run->mType & (WSType::leadingWS | WSType::trailingWS)) {
|
|
nsresult rv = DeleteRange(run->StartPoint(), run->EndPoint());
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
}
|
|
run = run->mRight;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
bool
|
|
WSRunObject::IsBlockNode(nsINode* aNode)
|
|
{
|
|
return aNode && aNode->IsElement() &&
|
|
HTMLEditor::NodeIsBlockStatic(aNode->AsElement());
|
|
}
|
|
|
|
} // namespace mozilla
|