Bug 1540029 - part 8: Replace HTMLEditRules::GetParagraphState() with new stack only class r=m_kato

Differential Revision: https://phabricator.services.mozilla.com/D45790

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Masayuki Nakano 2019-09-18 08:20:29 +00:00
parent 75e563c076
commit 8b7e7ba3d1
7 changed files with 221 additions and 154 deletions

View File

@ -84,6 +84,7 @@ class InsertTextTransaction;
class JoinNodeTransaction;
class ListElementSelectionState;
class ListItemElementSelectionState;
class ParagraphStateAtSelection;
class PlaceholderTransaction;
class PresShell;
class SplitNodeResult;
@ -2675,6 +2676,7 @@ class EditorBase : public nsIEditor,
friend class JoinNodeTransaction;
friend class ListElementSelectionState;
friend class ListItemElementSelectionState;
friend class ParagraphStateAtSelection;
friend class SplitNodeTransaction;
friend class TextEditRules;
friend class TypeInState;

View File

@ -1064,142 +1064,185 @@ static nsStaticAtom& MarginPropertyAtomForIndent(nsINode& aNode) {
: *nsGkAtoms::marginLeft;
}
nsresult HTMLEditRules::GetParagraphState(bool* aMixed, nsAString& outFormat) {
if (NS_WARN_IF(!aMixed)) {
return NS_ERROR_INVALID_ARG;
ParagraphStateAtSelection::ParagraphStateAtSelection(HTMLEditor& aHTMLEditor,
ErrorResult& aRv) {
if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
aRv = EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
return;
}
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
// XXX Should we create another constructor which won't create
// AutoEditActionDataSetter? Or should we create another
// AutoEditActionDataSetter which won't nest edit action?
EditorBase::AutoEditActionDataSetter editActionData(aHTMLEditor,
EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
aRv = EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
return;
}
// This routine is *heavily* tied to our ui choices in the paragraph
// style popup. I can't see a way around that.
*aMixed = true;
outFormat.Truncate(0);
AutoSafeEditorData setData(*this, *mHTMLEditor);
bool bMixed = false;
// using "x" as an uninitialized value, since "" is meaningful
nsAutoString formatStr(NS_LITERAL_STRING("x"));
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
nsresult rv = GetParagraphFormatNodes(arrayOfNodes);
AutoTArray<OwningNonNull<nsINode>, 64> arrayOfNodes;
nsresult rv =
CollectEditableFormatNodesInSelection(aHTMLEditor, arrayOfNodes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
aRv.Throw(rv);
return;
}
// post process list. We need to replace any block nodes that are not format
// nodes with their content. This is so we only have to look "up" the
// hierarchy to find format nodes, instead of both up and down.
// We need to append descendant format block if block nodes are not format
// block. This is so we only have to look "up" the hierarchy to find
// format nodes, instead of both up and down.
for (int32_t i = arrayOfNodes.Length() - 1; i >= 0; i--) {
auto& curNode = arrayOfNodes[i];
auto& node = arrayOfNodes[i];
nsAutoString format;
// if it is a known format node we have it easy
if (HTMLEditor::NodeIsBlockStatic(curNode) &&
!HTMLEditUtils::IsFormatNode(curNode)) {
// arrayOfNodes.RemoveObject(curNode);
rv = AppendInnerFormatNodes(arrayOfNodes, curNode);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (HTMLEditor::NodeIsBlockStatic(node) &&
!HTMLEditUtils::IsFormatNode(node)) {
// XXX This RemoveObject() call has already been commented out and
// the above comment explained we're trying to replace non-format
// block nodes in the array. According to the following blocks and
// `AppendDescendantFormatNodesAndFirstInlineNode()`, replacing
// non-format block with descendants format blocks makes sense.
// arrayOfNodes.RemoveObject(node);
ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
arrayOfNodes, *node->AsElement());
}
}
// we might have an empty node list. if so, find selection parent
// We might have an empty node list. if so, find selection parent
// and put that on the list
if (arrayOfNodes.IsEmpty()) {
EditorRawDOMPoint atCaret(EditorBase::GetStartPoint(*SelectionRefPtr()));
EditorRawDOMPoint atCaret(
EditorBase::GetStartPoint(*aHTMLEditor.SelectionRefPtr()));
if (NS_WARN_IF(!atCaret.IsSet())) {
return NS_ERROR_FAILURE;
aRv.Throw(NS_ERROR_FAILURE);
return;
}
arrayOfNodes.AppendElement(*atCaret.GetContainer());
}
// remember root node
Element* rootElement = HTMLEditorRef().GetRoot();
if (NS_WARN_IF(!rootElement)) {
return NS_ERROR_FAILURE;
Element* bodyOrDocumentElement = aHTMLEditor.GetRoot();
if (NS_WARN_IF(!bodyOrDocumentElement)) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
// loop through the nodes in selection and examine their paragraph format
for (auto& curNode : Reversed(arrayOfNodes)) {
nsAutoString format;
// if it is a known format node we have it easy
if (HTMLEditUtils::IsFormatNode(curNode)) {
GetFormatString(curNode, format);
} else if (HTMLEditor::NodeIsBlockStatic(curNode)) {
// this is a div or some other non-format block.
// we should ignore it. Its children were appended to this list
// by AppendInnerFormatNodes() call above. We will get needed
// info when we examine them instead.
for (auto& node : Reversed(arrayOfNodes)) {
nsAtom* paragraphStateOfNode = nsGkAtoms::_empty;
if (HTMLEditUtils::IsFormatNode(node)) {
MOZ_ASSERT(node->NodeInfo()->NameAtom());
paragraphStateOfNode = node->NodeInfo()->NameAtom();
}
// Ignore non-format block node since its children have been appended
// the list above so that we'll handle this descendants later.
else if (HTMLEditor::NodeIsBlockStatic(node)) {
continue;
} else {
nsINode* node = curNode->GetParentNode();
while (node) {
if (node == rootElement) {
format.Truncate(0);
break;
} else if (HTMLEditUtils::IsFormatNode(node)) {
GetFormatString(node, format);
}
// If we meet an inline node, let's get its parent format.
else {
for (nsINode* parentNode = node->GetParentNode(); parentNode;
parentNode = parentNode->GetParentNode()) {
// If we reach `HTMLDocument.body` or `Document.documentElement`,
// there is no format.
if (parentNode == bodyOrDocumentElement) {
break;
}
if (HTMLEditUtils::IsFormatNode(parentNode)) {
MOZ_ASSERT(parentNode->NodeInfo()->NameAtom());
paragraphStateOfNode = parentNode->NodeInfo()->NameAtom();
break;
}
// else keep looking up
node = node->GetParentNode();
}
}
// if this is the first node, we've found, remember it as the format
if (formatStr.EqualsLiteral("x")) {
formatStr = format;
if (!mFirstParagraphState) {
mFirstParagraphState = paragraphStateOfNode;
continue;
}
// else make sure it matches previously found format
else if (format != formatStr) {
bMixed = true;
if (mFirstParagraphState != paragraphStateOfNode) {
mIsMixed = true;
break;
}
}
*aMixed = bMixed;
outFormat = formatStr;
return NS_OK;
}
nsresult HTMLEditRules::AppendInnerFormatNodes(
nsTArray<OwningNonNull<nsINode>>& aArray, nsINode* aNode) {
MOZ_ASSERT(aNode);
// static
void ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
nsTArray<OwningNonNull<nsINode>>& aArrayOfNodes,
Element& aNonFormatBlockElement) {
MOZ_ASSERT(HTMLEditor::NodeIsBlockStatic(aNonFormatBlockElement));
MOZ_ASSERT(!HTMLEditUtils::IsFormatNode(&aNonFormatBlockElement));
// we only need to place any one inline inside this node onto
// We only need to place any one inline inside this node onto
// the list. They are all the same for purposes of determining
// paragraph style. We use foundInline to track this as we are
// going through the children in the loop below.
bool foundInline = false;
for (nsIContent* child = aNode->GetFirstChild(); child;
child = child->GetNextSibling()) {
bool isBlock = HTMLEditor::NodeIsBlockStatic(*child);
bool isFormat = HTMLEditUtils::IsFormatNode(child);
for (nsIContent* childContent = aNonFormatBlockElement.GetFirstChild();
childContent; childContent = childContent->GetNextSibling()) {
bool isBlock = HTMLEditor::NodeIsBlockStatic(*childContent);
bool isFormat = HTMLEditUtils::IsFormatNode(childContent);
// If the child is a non-format block element, let's check its children
// recursively.
if (isBlock && !isFormat) {
// if it's a div, etc., recurse
AppendInnerFormatNodes(aArray, child);
} else if (isFormat) {
aArray.AppendElement(*child);
} else if (!foundInline) {
// if this is the first inline we've found, use it
ParagraphStateAtSelection::AppendDescendantFormatNodesAndFirstInlineNode(
aArrayOfNodes, *childContent->AsElement());
continue;
}
// If it's a format block, append it.
if (isFormat) {
aArrayOfNodes.AppendElement(*childContent);
continue;
}
MOZ_ASSERT(!isBlock);
// If we haven't found inline node, append only this first inline node.
// XXX I think that this makes sense if caller of this removes
// aNonFormatBlockElement from aArrayOfNodes because the last loop of
// the constructor can check parent format block with
// aNonFormatBlockElement.
if (!foundInline) {
foundInline = true;
aArray.AppendElement(*child);
aArrayOfNodes.AppendElement(*childContent);
continue;
}
}
return NS_OK;
}
nsresult HTMLEditRules::GetFormatString(nsINode* aNode, nsAString& outFormat) {
NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER);
// static
nsresult ParagraphStateAtSelection::CollectEditableFormatNodesInSelection(
HTMLEditor& aHTMLEditor, nsTArray<OwningNonNull<nsINode>>& aArrayOfNodes) {
nsresult rv = aHTMLEditor.CollectEditTargetNodesInExtendedSelectionRanges(
aArrayOfNodes, EditSubAction::eCreateOrRemoveBlock,
HTMLEditor::CollectNonEditableNodes::Yes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (HTMLEditUtils::IsFormatNode(aNode)) {
aNode->NodeInfo()->NameAtom()->ToString(outFormat);
} else {
outFormat.Truncate();
// Pre-process our list of nodes
for (int32_t i = aArrayOfNodes.Length() - 1; i >= 0; i--) {
OwningNonNull<nsINode> node = aArrayOfNodes[i];
// Remove all non-editable nodes. Leave them be.
if (!aHTMLEditor.IsEditable(node)) {
aArrayOfNodes.RemoveElementAt(i);
continue;
}
// Scan for table elements. If we find table elements other than table,
// replace it with a list of any editable non-table content. Ditto for
// list elements.
if (HTMLEditUtils::IsTableElement(node) || HTMLEditUtils::IsList(node) ||
HTMLEditUtils::IsListItem(node)) {
aArrayOfNodes.RemoveElementAt(i);
aHTMLEditor.CollectChildren(node, aArrayOfNodes, i,
HTMLEditor::CollectListChildren::Yes,
HTMLEditor::CollectTableChildren::Yes,
HTMLEditor::CollectNonEditableNodes::Yes);
}
}
return NS_OK;
}
@ -7962,43 +8005,6 @@ Element* HTMLEditor::GetDeepestEditableOnlyChildDivBlockquoteOrListElement(
return parentElement;
}
nsresult HTMLEditRules::GetParagraphFormatNodes(
nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes) {
MOZ_ASSERT(IsEditorDataAvailable());
nsresult rv = HTMLEditorRef().CollectEditTargetNodesInExtendedSelectionRanges(
outArrayOfNodes, EditSubAction::eCreateOrRemoveBlock,
HTMLEditor::CollectNonEditableNodes::Yes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Pre-process our list of nodes
for (int32_t i = outArrayOfNodes.Length() - 1; i >= 0; i--) {
OwningNonNull<nsINode> testNode = outArrayOfNodes[i];
// Remove all non-editable nodes. Leave them be.
if (!HTMLEditorRef().IsEditable(testNode)) {
outArrayOfNodes.RemoveElementAt(i);
continue;
}
// Scan for table elements. If we find table elements other than table,
// replace it with a list of any editable non-table content. Ditto for
// list elements.
if (HTMLEditUtils::IsTableElement(testNode) ||
HTMLEditUtils::IsList(testNode) ||
HTMLEditUtils::IsListItem(testNode)) {
outArrayOfNodes.RemoveElementAt(i);
HTMLEditorRef().CollectChildren(testNode, outArrayOfNodes, i,
HTMLEditor::CollectListChildren::Yes,
HTMLEditor::CollectTableChildren::Yes,
HTMLEditor::CollectNonEditableNodes::Yes);
}
}
return NS_OK;
}
nsresult HTMLEditor::SplitParentInlineElementsAtRangeEdges(
RangeItem& aRangeItem) {
MOZ_ASSERT(IsEditActionDataAvailable());

View File

@ -59,9 +59,6 @@ class HTMLEditRules : public TextEditRules {
virtual nsresult Init(TextEditor* aTextEditor) override;
virtual nsresult DetachEditor() override;
MOZ_CAN_RUN_SCRIPT
nsresult GetParagraphState(bool* aMixed, nsAString& outFormat);
protected:
virtual ~HTMLEditRules() = default;
@ -70,14 +67,6 @@ class HTMLEditRules : public TextEditRules {
return mData->HTMLEditorRef();
}
nsresult AppendInnerFormatNodes(nsTArray<OwningNonNull<nsINode>>& aArray,
nsINode* aNode);
nsresult GetFormatString(nsINode* aNode, nsAString& outFormat);
MOZ_CAN_RUN_SCRIPT
nsresult GetParagraphFormatNodes(
nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes);
protected:
HTMLEditor* mHTMLEditor;
bool mInitialized;

View File

@ -1832,21 +1832,28 @@ nsresult HTMLEditor::SetParagraphFormatAsAction(
}
NS_IMETHODIMP
HTMLEditor::GetParagraphState(bool* aMixed, nsAString& outFormat) {
HTMLEditor::GetParagraphState(bool* aMixed, nsAString& aFirstParagraphState) {
if (NS_WARN_IF(!aMixed)) {
return NS_ERROR_INVALID_ARG;
}
if (!mRules) {
if (!mInitSucceeded) {
return NS_ERROR_NOT_INITIALIZED;
}
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
ErrorResult error;
ParagraphStateAtSelection state(*this, error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
RefPtr<HTMLEditRules> htmlRules(mRules->AsHTMLEditRules());
return htmlRules->GetParagraphState(aMixed, outFormat);
*aMixed = state.IsMixed();
if (NS_WARN_IF(!state.GetFirstParagraphStateAtSelection())) {
// XXX Odd result, but keep this behavior for now...
aFirstParagraphState.AssignASCII("x");
} else {
state.GetFirstParagraphStateAtSelection()->ToString(aFirstParagraphState);
}
return NS_OK;
}
NS_IMETHODIMP

View File

@ -48,6 +48,7 @@ class EmptyEditableFunctor;
class ListElementSelectionState;
class ListItemElementSelectionState;
class MoveNodeResult;
class ParagraphStateAtSelection;
class ResizerSelectionListener;
class SplitRangeOffFromNodeResult;
class WSRunObject;
@ -4370,6 +4371,7 @@ class HTMLEditor final : public TextEditor,
friend class HTMLEditRules;
friend class ListElementSelectionState;
friend class ListItemElementSelectionState;
friend class ParagraphStateAtSelection;
friend class SlurpBlobEventListener;
friend class TextEditor;
friend class WSRunObject;
@ -4441,6 +4443,63 @@ class MOZ_STACK_CLASS AlignStateAtSelection final {
nsIHTMLEditor::EAlignment mFirstAlign = nsIHTMLEditor::eLeft;
};
/**
* ParagraphStateAtSelection class gets format block types around selection.
*/
class MOZ_STACK_CLASS ParagraphStateAtSelection final {
public:
ParagraphStateAtSelection() = delete;
ParagraphStateAtSelection(HTMLEditor& aHTMLEditor, ErrorResult& aRv);
/**
* GetFirstParagraphStateAtSelection() returns:
* - nullptr if there is no format blocks nor inline nodes.
* - nsGkAtoms::_empty if first node is not in any format block.
* - a tag name of format block at first node.
* XXX See the private method explanations. If selection ranges contains
* non-format block first, it'll be check after its siblings. Therefore,
* this may return non-first paragraph state.
*/
nsAtom* GetFirstParagraphStateAtSelection() const {
return mFirstParagraphState;
}
/**
* If selected nodes are not in same format node nor only in no-format blocks,
* this returns true.
*/
bool IsMixed() const { return mIsMixed; }
private:
/**
* AppendDescendantFormatNodesAndFirstInlineNode() appends descendant
* format blocks and first inline child node in aNonFormatBlockElement to
* the last of the array (not inserting where aNonFormatBlockElement is,
* so that the node order becomes randomly).
*
* @param aArrayOfNodes [in/out] Found descendant format blocks
* and first inline node in each non-format
* block will be appended to this.
* @param aNonFormatBlockElement Must be a non-format block element.
*/
static void AppendDescendantFormatNodesAndFirstInlineNode(
nsTArray<OwningNonNull<nsINode>>& aArrayOfNodes,
Element& aNonFormatBlockElement);
/**
* CollectEditableFormatNodesInSelection() collects only editable nodes
* around selection ranges (with
* `HTMLEditor::CollectEditTargetNodesInExtendedSelectionRanges()`, see its
* document for the detail). If it includes list, list item or table
* related elements, they will be replaced their children.
*/
static nsresult CollectEditableFormatNodesInSelection(
HTMLEditor& aHTMLEditor, nsTArray<OwningNonNull<nsINode>>& aArrayOfNodes);
RefPtr<nsAtom> mFirstParagraphState;
bool mIsMixed = false;
};
} // namespace mozilla
mozilla::HTMLEditor* nsIEditor::AsHTMLEditor() {

View File

@ -558,16 +558,21 @@ nsresult ParagraphStateCommand::GetCurrentState(
return NS_ERROR_INVALID_ARG;
}
bool outMixed;
nsAutoString outStateString;
nsresult rv = aHTMLEditor->GetParagraphState(&outMixed, outStateString);
if (NS_SUCCEEDED(rv)) {
nsAutoCString tOutStateString;
LossyCopyUTF16toASCII(outStateString, tOutStateString);
aParams.SetBool(STATE_MIXED, outMixed);
aParams.SetCString(STATE_ATTRIBUTE, tOutStateString);
ErrorResult error;
ParagraphStateAtSelection state(*aHTMLEditor, error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
return rv;
aParams.SetBool(STATE_MIXED, state.IsMixed());
if (NS_WARN_IF(!state.GetFirstParagraphStateAtSelection())) {
// XXX This is odd behavior, we should fix this later.
aParams.SetCString(STATE_ATTRIBUTE, NS_LITERAL_CSTRING("x"));
} else {
nsCString paragraphState; // Don't use `nsAutoCString` for avoiding copy.
state.GetFirstParagraphStateAtSelection()->ToUTF8String(paragraphState);
aParams.SetCString(STATE_ATTRIBUTE, paragraphState);
}
return NS_OK;
}
nsresult ParagraphStateCommand::SetState(HTMLEditor* aHTMLEditor,

View File

@ -219,7 +219,6 @@ interface nsIHTMLEditor : nsISupports
* @param aMixed True if there is more than one format
* @return Name of block tag. "" is returned for none.
*/
[can_run_script]
AString getParagraphState(out boolean aMixed);
/**