Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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 "ErrorList.h"
#include "HTMLEditor.h"
#include "HTMLEditorInlines.h"
#include "HTMLEditorNestedClasses.h"
#include "AutoRangeArray.h"
#include "CSSEditUtils.h"
#include "EditAction.h"
#include "EditorUtils.h"
#include "HTMLEditHelpers.h"
#include "HTMLEditUtils.h"
#include "PendingStyles.h"
#include "SelectionState.h"
#include "WSRunObject.h"
#include "mozilla/Assertions.h"
#include "mozilla/ContentIterator.h"
#include "mozilla/EditorForwards.h"
#include "mozilla/mozalloc.h"
#include "mozilla/SelectionState.h"
#include "mozilla/dom/AncestorIterator.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/HTMLBRElement.h"
#include "mozilla/dom/NameSpaceConstants.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/dom/Text.h"
#include "nsAString.h"
#include "nsAtom.h"
#include "nsAttrName.h"
#include "nsAttrValue.h"
#include "nsCaseTreatment.h"
#include "nsColor.h"
#include "nsComponentManagerUtils.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsGkAtoms.h"
#include "nsIContent.h"
#include "nsINode.h"
#include "nsIPrincipal.h"
#include "nsISupportsImpl.h"
#include "nsLiteralString.h"
#include "nsNameSpaceManager.h"
#include "nsRange.h"
#include "nsReadableUtils.h"
#include "nsString.h"
#include "nsStringFwd.h"
#include "nsStyledElement.h"
#include "nsTArray.h"
#include "nsTextNode.h"
#include "nsUnicharUtils.h"
namespace mozilla {
using namespace dom;
using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
using LeafNodeType = HTMLEditUtils::LeafNodeType;
using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes;
using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
template nsresult HTMLEditor::SetInlinePropertiesAsSubAction(
const AutoTArray<EditorInlineStyleAndValue, 1>& aStylesToSet);
template nsresult HTMLEditor::SetInlinePropertiesAsSubAction(
const AutoTArray<EditorInlineStyleAndValue, 32>& aStylesToSet);
template nsresult HTMLEditor::SetInlinePropertiesAroundRanges(
AutoRangeArray& aRanges,
const AutoTArray<EditorInlineStyleAndValue, 1>& aStylesToSet,
const Element& aEditingHost);
template nsresult HTMLEditor::SetInlinePropertiesAroundRanges(
AutoRangeArray& aRanges,
const AutoTArray<EditorInlineStyleAndValue, 32>& aStylesToSet,
const Element& aEditingHost);
nsresult HTMLEditor::SetInlinePropertyAsAction(nsStaticAtom& aProperty,
nsStaticAtom* aAttribute,
const nsAString& aValue,
nsIPrincipal* aPrincipal) {
AutoEditActionDataSetter editActionData(
*this,
HTMLEditUtils::GetEditActionForFormatText(aProperty, aAttribute, true),
aPrincipal);
switch (editActionData.GetEditAction()) {
case EditAction::eSetFontFamilyProperty:
MOZ_ASSERT(!aValue.IsVoid());
// XXX Should we trim unnecessary white-spaces?
editActionData.SetData(aValue);
break;
case EditAction::eSetColorProperty:
case EditAction::eSetBackgroundColorPropertyInline:
editActionData.SetColorData(aValue);
break;
default:
break;
}
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
// XXX Due to bug 1659276 and bug 1659924, we should not scroll selection
// into view after setting the new style.
AutoPlaceholderBatch treatAsOneTransaction(*this, ScrollSelectionIntoView::No,
__FUNCTION__);
nsStaticAtom* property = &aProperty;
nsStaticAtom* attribute = aAttribute;
nsString value(aValue);
if (attribute == nsGkAtoms::color || attribute == nsGkAtoms::bgcolor) {
if (!IsCSSEnabled()) {
// We allow CSS style color value even in the HTML mode. In the cases,
// we will apply the style with CSS. For considering it in the value
// as-is if it's a known CSS keyboard, `rgb()` or `rgba()` style.
// NOTE: It may be later that we set the color into the DOM tree and at
// that time, IsCSSEnabled() may return true. E.g., setting color value
// to collapsed selection, then, change the CSS enabled, finally, user
// types text there.
if (!HTMLEditUtils::MaybeCSSSpecificColorValue(value)) {
HTMLEditUtils::GetNormalizedHTMLColorValue(value, value);
}
} else {
HTMLEditUtils::GetNormalizedCSSColorValue(
value, HTMLEditUtils::ZeroAlphaColor::RGBAValue, value);
}
}
AutoTArray<EditorInlineStyle, 1> stylesToRemove;
if (&aProperty == nsGkAtoms::sup) {
// Superscript and Subscript styles are mutually exclusive.
stylesToRemove.AppendElement(EditorInlineStyle(*nsGkAtoms::sub));
} else if (&aProperty == nsGkAtoms::sub) {
// Superscript and Subscript styles are mutually exclusive.
stylesToRemove.AppendElement(EditorInlineStyle(*nsGkAtoms::sup));
}
// Handling `<tt>` element code was implemented for composer (bug 115922).
// This shouldn't work with `Document.execCommand()`. Currently, aPrincipal
// is set only when the root caller is Document::ExecCommand() so that
// we should handle `<tt>` element only when aPrincipal is nullptr that
// must be only when XUL command is executed on composer.
else if (!aPrincipal) {
if (&aProperty == nsGkAtoms::tt) {
stylesToRemove.AppendElement(
EditorInlineStyle(*nsGkAtoms::font, nsGkAtoms::face));
} else if (&aProperty == nsGkAtoms::font && aAttribute == nsGkAtoms::face) {
if (!value.LowerCaseEqualsASCII("tt")) {
stylesToRemove.AppendElement(EditorInlineStyle(*nsGkAtoms::tt));
} else {
stylesToRemove.AppendElement(
EditorInlineStyle(*nsGkAtoms::font, nsGkAtoms::face));
// Override property, attribute and value if the new font face value is
// "tt".
property = nsGkAtoms::tt;
attribute = nullptr;
value.Truncate();
}
}
}
if (!stylesToRemove.IsEmpty()) {
nsresult rv = RemoveInlinePropertiesAsSubAction(stylesToRemove);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::RemoveInlinePropertiesAsSubAction() failed");
return rv;
}
}
AutoTArray<EditorInlineStyleAndValue, 1> styleToSet;
styleToSet.AppendElement(
attribute
? EditorInlineStyleAndValue(*property, *attribute, std::move(value))
: EditorInlineStyleAndValue(*property));
rv = SetInlinePropertiesAsSubAction(styleToSet);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::SetInlinePropertiesAsSubAction() failed");
return EditorBase::ToGenericNSResult(rv);
}
NS_IMETHODIMP HTMLEditor::SetInlineProperty(const nsAString& aProperty,
const nsAString& aAttribute,
const nsAString& aValue) {
nsStaticAtom* property = NS_GetStaticAtom(aProperty);
if (NS_WARN_IF(!property)) {
return NS_ERROR_INVALID_ARG;
}
nsStaticAtom* attribute = EditorUtils::GetAttributeAtom(aAttribute);
AutoEditActionDataSetter editActionData(
*this,
HTMLEditUtils::GetEditActionForFormatText(*property, attribute, true));
switch (editActionData.GetEditAction()) {
case EditAction::eSetFontFamilyProperty:
MOZ_ASSERT(!aValue.IsVoid());
// XXX Should we trim unnecessary white-spaces?
editActionData.SetData(aValue);
break;
case EditAction::eSetColorProperty:
case EditAction::eSetBackgroundColorPropertyInline:
editActionData.SetColorData(aValue);
break;
default:
break;
}
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
AutoTArray<EditorInlineStyleAndValue, 1> styleToSet;
styleToSet.AppendElement(
attribute ? EditorInlineStyleAndValue(*property, *attribute, aValue)
: EditorInlineStyleAndValue(*property));
rv = SetInlinePropertiesAsSubAction(styleToSet);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::SetInlinePropertiesAsSubAction() failed");
return EditorBase::ToGenericNSResult(rv);
}
template <size_t N>
nsresult HTMLEditor::SetInlinePropertiesAsSubAction(
const AutoTArray<EditorInlineStyleAndValue, N>& aStylesToSet) {
MOZ_ASSERT(IsEditActionDataAvailable());
MOZ_ASSERT(!aStylesToSet.IsEmpty());
DebugOnly<nsresult> rvIgnored = CommitComposition();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"EditorBase::CommitComposition() failed, but ignored");
if (SelectionRef().IsCollapsed()) {
// Manipulating text attributes on a collapsed selection only sets state
// for the next text insertion
mPendingStylesToApplyToNewContent->PreserveStyles(aStylesToSet);
return NS_OK;
}
// XXX Shouldn't we return before calling `CommitComposition()`?
if (IsPlaintextMailComposer()) {
return NS_OK;
}
{
Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
if (MOZ_UNLIKELY(result.isErr())) {
NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
return result.unwrapErr();
}
if (result.inspect().Canceled()) {
return NS_OK;
}
}
RefPtr<Element> const editingHost =
ComputeEditingHost(LimitInBodyElement::No);
if (NS_WARN_IF(!editingHost)) {
return NS_ERROR_FAILURE;
}
AutoPlaceholderBatch treatAsOneTransaction(
*this, ScrollSelectionIntoView::Yes, __FUNCTION__);
IgnoredErrorResult ignoredError;
AutoEditSubActionNotifier startToHandleEditSubAction(
*this, EditSubAction::eInsertElement, nsIEditor::eNext, ignoredError);
if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
return ignoredError.StealNSResult();
}
NS_WARNING_ASSERTION(
!ignoredError.Failed(),
"HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
// TODO: We don't need AutoTransactionsConserveSelection here in the normal
// cases, but removing this may cause the behavior with the legacy
// mutation event listeners. We should try to delete this in a bug.
AutoTransactionsConserveSelection dontChangeMySelection(*this);
AutoRangeArray selectionRanges(SelectionRef());
nsresult rv = SetInlinePropertiesAroundRanges(selectionRanges, aStylesToSet,
*editingHost);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::SetInlinePropertiesAroundRanges() failed");
return rv;
}
MOZ_ASSERT(!selectionRanges.HasSavedRanges());
rv = selectionRanges.ApplyTo(SelectionRef());
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::ApplyTo() failed");
return rv;
}
template <size_t N>
nsresult HTMLEditor::SetInlinePropertiesAroundRanges(
AutoRangeArray& aRanges,
const AutoTArray<EditorInlineStyleAndValue, N>& aStylesToSet,
const Element& aEditingHost) {
MOZ_ASSERT(!aRanges.HasSavedRanges());
for (const EditorInlineStyleAndValue& styleToSet : aStylesToSet) {
AutoInlineStyleSetter inlineStyleSetter(styleToSet);
for (OwningNonNull<nsRange>& domRange : aRanges.Ranges()) {
inlineStyleSetter.Reset();
auto rangeOrError =
[&]() MOZ_CAN_RUN_SCRIPT -> Result<EditorDOMRange, nsresult> {
EditorDOMRange range(domRange);
// If we're setting <font>, we want to remove ancestors which set
// `font-size` or <font size="..."> recursively. Therefore, for
// extending the ranges to contain all ancestors in the range, we need
// to split ancestors first.
// XXX: Blink and WebKit inserts <font> elements to inner most
// elements, however, we cannot do it under current design because
// once we contain ancestors which have `font-size` or are
// <font size="...">, we lost the original ranges which we wanted to
// apply the style. For fixing this, we need to manage both ranges, but
// it's too expensive especially we allow to run script when we touch
// the DOM tree. Additionally, font-size value affects the height
// of the element, but does not affect the height of ancestor inline
// elements. Therefore, following the behavior may cause similar issue
// as bug 1808906. So at least for now, we should not do this big work.
if (styleToSet.IsStyleOfFontElement()) {
Result<SplitRangeOffResult, nsresult> splitAncestorsResult =
SplitAncestorStyledInlineElementsAtRangeEdges(
range, styleToSet, SplitAtEdges::eDoNotCreateEmptyContainer);
if (MOZ_UNLIKELY(splitAncestorsResult.isErr())) {
NS_WARNING(
"HTMLEditor::SplitAncestorStyledInlineElementsAtRangeEdges() "
"failed");
return splitAncestorsResult.propagateErr();
}
SplitRangeOffResult unwrappedResult = splitAncestorsResult.unwrap();
unwrappedResult.IgnoreCaretPointSuggestion();
range = unwrappedResult.RangeRef();
if (NS_WARN_IF(!range.IsPositionedAndValid())) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
}
Result<EditorRawDOMRange, nsresult> rangeOrError =
inlineStyleSetter.ExtendOrShrinkRangeToApplyTheStyle(*this, range,
aEditingHost);
if (MOZ_UNLIKELY(rangeOrError.isErr())) {
NS_WARNING(
"HTMLEditor::ExtendOrShrinkRangeToApplyTheStyle() failed, but "
"ignored");
return EditorDOMRange();
}
return EditorDOMRange(rangeOrError.unwrap());
}();
if (MOZ_UNLIKELY(rangeOrError.isErr())) {
return rangeOrError.unwrapErr();
}
const EditorDOMRange range = rangeOrError.unwrap();
if (!range.IsPositioned()) {
continue;
}
// If the range is collapsed, we should insert new element there.
if (range.Collapsed()) {
Result<RefPtr<Text>, nsresult> emptyTextNodeOrError =
AutoInlineStyleSetter::GetEmptyTextNodeToApplyNewStyle(
*this, range.StartRef(), aEditingHost);
if (MOZ_UNLIKELY(emptyTextNodeOrError.isErr())) {
NS_WARNING(
"AutoInlineStyleSetter::GetEmptyTextNodeToApplyNewStyle() "
"failed");
return emptyTextNodeOrError.unwrapErr();
}
if (MOZ_UNLIKELY(!emptyTextNodeOrError.inspect())) {
continue; // Couldn't insert text node there
}
RefPtr<Text> emptyTextNode = emptyTextNodeOrError.unwrap();
Result<CaretPoint, nsresult> caretPointOrError =
inlineStyleSetter
.ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(
*this, *emptyTextNode);
if (MOZ_UNLIKELY(caretPointOrError.isErr())) {
NS_WARNING(
"AutoInlineStyleSetter::"
"ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed");
return caretPointOrError.unwrapErr();
}
DebugOnly<nsresult> rvIgnored = domRange->CollapseTo(emptyTextNode, 0);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"nsRange::CollapseTo() failed, but ignored");
continue;
}
// Use const_cast hack here for preventing the others to update the range.
AutoTrackDOMRange trackRange(RangeUpdaterRef(),
const_cast<EditorDOMRange*>(&range));
auto UpdateSelectionRange = [&]() MOZ_CAN_RUN_SCRIPT {
// If inlineStyleSetter creates elements or setting styles, we should
// select between start of first element and end of last element.
if (inlineStyleSetter.FirstHandledPointRef().IsInContentNode()) {
MOZ_ASSERT(inlineStyleSetter.LastHandledPointRef().IsInContentNode());
const auto startPoint =
!inlineStyleSetter.FirstHandledPointRef().IsStartOfContainer()
? inlineStyleSetter.FirstHandledPointRef()
.To<EditorRawDOMPoint>()
: HTMLEditUtils::GetDeepestEditableStartPointOf<
EditorRawDOMPoint>(
*inlineStyleSetter.FirstHandledPointRef()
.ContainerAs<nsIContent>());
const auto endPoint =
!inlineStyleSetter.LastHandledPointRef().IsEndOfContainer()
? inlineStyleSetter.LastHandledPointRef()
.To<EditorRawDOMPoint>()
: HTMLEditUtils::GetDeepestEditableEndPointOf<
EditorRawDOMPoint>(
*inlineStyleSetter.LastHandledPointRef()
.ContainerAs<nsIContent>());
nsresult rv = domRange->SetStartAndEnd(
startPoint.ToRawRangeBoundary(), endPoint.ToRawRangeBoundary());
if (NS_SUCCEEDED(rv)) {
trackRange.StopTracking();
return;
}
}
// Otherwise, use the range computed with the tracking original range.
trackRange.FlushAndStopTracking();
domRange->SetStartAndEnd(range.StartRef().ToRawRangeBoundary(),
range.EndRef().ToRawRangeBoundary());
};
// If range is in a text node, apply new style simply.
if (range.InSameContainer() && range.StartRef().IsInTextNode()) {
// MOZ_KnownLive(...ContainerAs<Text>()) because of grabbed by `range`.
// MOZ_KnownLive(styleToSet.*) due to bug 1622253.
Result<SplitRangeOffFromNodeResult, nsresult>
wrapTextInStyledElementResult =
inlineStyleSetter.SplitTextNodeAndApplyStyleToMiddleNode(
*this, MOZ_KnownLive(*range.StartRef().ContainerAs<Text>()),
range.StartRef().Offset(), range.EndRef().Offset());
if (MOZ_UNLIKELY(wrapTextInStyledElementResult.isErr())) {
NS_WARNING("HTMLEditor::SetInlinePropertyOnTextNode() failed");
return wrapTextInStyledElementResult.unwrapErr();
}
// The caller should handle the ranges as Selection if necessary, and we
// don't want to update aRanges with this result.
wrapTextInStyledElementResult.inspect().IgnoreCaretPointSuggestion();
UpdateSelectionRange();
continue;
}
// Collect editable nodes which are entirely contained in the range.
AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContentsAroundRange;
{
ContentSubtreeIterator subtreeIter;
// If there is no node which is entirely in the range,
// `ContentSubtreeIterator::Init()` fails, but this is possible case,
// don't warn it.
if (NS_SUCCEEDED(
subtreeIter.Init(range.StartRef().ToRawRangeBoundary(),
range.EndRef().ToRawRangeBoundary()))) {
for (; !subtreeIter.IsDone(); subtreeIter.Next()) {
nsINode* node = subtreeIter.GetCurrentNode();
if (NS_WARN_IF(!node)) {
return NS_ERROR_FAILURE;
}
if (MOZ_UNLIKELY(!node->IsContent())) {
continue;
}
// We don't need to wrap non-editable node in new inline element
// nor shouldn't modify `style` attribute of non-editable element.
if (!EditorUtils::IsEditableContent(*node->AsContent(),
EditorType::HTML)) {
continue;
}
// We shouldn't wrap invisible text node in new inline element.
if (node->IsText() &&
!HTMLEditUtils::IsVisibleTextNode(*node->AsText())) {
continue;
}
arrayOfContentsAroundRange.AppendElement(*node->AsContent());
}
}
}
// If start node is a text node, apply new style to a part of it.
if (range.StartRef().IsInTextNode() &&
EditorUtils::IsEditableContent(*range.StartRef().ContainerAs<Text>(),
EditorType::HTML)) {
// MOZ_KnownLive(...ContainerAs<Text>()) because of grabbed by `range`.
// MOZ_KnownLive(styleToSet.*) due to bug 1622253.
Result<SplitRangeOffFromNodeResult, nsresult>
wrapTextInStyledElementResult =
inlineStyleSetter.SplitTextNodeAndApplyStyleToMiddleNode(
*this, MOZ_KnownLive(*range.StartRef().ContainerAs<Text>()),
range.StartRef().Offset(),
range.StartRef().ContainerAs<Text>()->TextDataLength());
if (MOZ_UNLIKELY(wrapTextInStyledElementResult.isErr())) {
NS_WARNING("HTMLEditor::SetInlinePropertyOnTextNode() failed");
return wrapTextInStyledElementResult.unwrapErr();
}
// The caller should handle the ranges as Selection if necessary, and we
// don't want to update aRanges with this result.
wrapTextInStyledElementResult.inspect().IgnoreCaretPointSuggestion();
}
// Then, apply new style to all nodes in the range entirely.
for (auto& content : arrayOfContentsAroundRange) {
// MOZ_KnownLive due to bug 1622253.
Result<CaretPoint, nsresult> pointToPutCaretOrError =
inlineStyleSetter
.ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(
*this, MOZ_KnownLive(*content));
if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
NS_WARNING(
"AutoInlineStyleSetter::"
"ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed");
return pointToPutCaretOrError.unwrapErr();
}
// The caller should handle the ranges as Selection if necessary, and we
// don't want to update aRanges with this result.
pointToPutCaretOrError.inspect().IgnoreCaretPointSuggestion();
}
// Finally, if end node is a text node, apply new style to a part of it.
if (range.EndRef().IsInTextNode() &&
EditorUtils::IsEditableContent(*range.EndRef().ContainerAs<Text>(),
EditorType::HTML)) {
// MOZ_KnownLive(...ContainerAs<Text>()) because of grabbed by `range`.
// MOZ_KnownLive(styleToSet.mAttribute) due to bug 1622253.
Result<SplitRangeOffFromNodeResult, nsresult>
wrapTextInStyledElementResult =
inlineStyleSetter.SplitTextNodeAndApplyStyleToMiddleNode(
*this, MOZ_KnownLive(*range.EndRef().ContainerAs<Text>()),
0, range.EndRef().Offset());
if (MOZ_UNLIKELY(wrapTextInStyledElementResult.isErr())) {
NS_WARNING("HTMLEditor::SetInlinePropertyOnTextNode() failed");
return wrapTextInStyledElementResult.unwrapErr();
}
// The caller should handle the ranges as Selection if necessary, and we
// don't want to update aRanges with this result.
wrapTextInStyledElementResult.inspect().IgnoreCaretPointSuggestion();
}
UpdateSelectionRange();
}
}
return NS_OK;
}
// static
Result<RefPtr<Text>, nsresult>
HTMLEditor::AutoInlineStyleSetter::GetEmptyTextNodeToApplyNewStyle(
HTMLEditor& aHTMLEditor, const EditorDOMPoint& aCandidatePointToInsert,
const Element& aEditingHost) {
auto pointToInsertNewText =
HTMLEditUtils::GetBetterCaretPositionToInsertText<EditorDOMPoint>(
aCandidatePointToInsert, aEditingHost);
if (MOZ_UNLIKELY(!pointToInsertNewText.IsSet())) {
return RefPtr<Text>(); // cannot insert text there
}
auto pointToInsertNewStyleOrError =
[&]() MOZ_CAN_RUN_SCRIPT -> Result<EditorDOMPoint, nsresult> {
if (!pointToInsertNewText.IsInTextNode()) {
return pointToInsertNewText;
}
if (!pointToInsertNewText.ContainerAs<Text>()->TextDataLength()) {
return pointToInsertNewText; // Use it
}
if (pointToInsertNewText.IsStartOfContainer()) {
return pointToInsertNewText.ParentPoint();
}
if (pointToInsertNewText.IsEndOfContainer()) {
return EditorDOMPoint::After(*pointToInsertNewText.ContainerAs<Text>());
}
Result<SplitNodeResult, nsresult> splitTextNodeResult =
aHTMLEditor.SplitNodeWithTransaction(pointToInsertNewText);
if (MOZ_UNLIKELY(splitTextNodeResult.isErr())) {
NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
return splitTextNodeResult.propagateErr();
}
SplitNodeResult unwrappedSplitTextNodeResult = splitTextNodeResult.unwrap();
unwrappedSplitTextNodeResult.IgnoreCaretPointSuggestion();
return unwrappedSplitTextNodeResult.AtSplitPoint<EditorDOMPoint>();
}();
if (MOZ_UNLIKELY(pointToInsertNewStyleOrError.isErr())) {
return pointToInsertNewStyleOrError.propagateErr();
}
// If we already have empty text node which is available for placeholder in
// new styled element, let's use it.
if (pointToInsertNewStyleOrError.inspect().IsInTextNode()) {
return RefPtr<Text>(
pointToInsertNewStyleOrError.inspect().ContainerAs<Text>());
}
// Otherwise, we need an empty text node to create new inline style.
RefPtr<Text> newEmptyTextNode = aHTMLEditor.CreateTextNode(u""_ns);
if (MOZ_UNLIKELY(!newEmptyTextNode)) {
NS_WARNING("EditorBase::CreateTextNode() failed");
return Err(NS_ERROR_FAILURE);
}
Result<CreateTextResult, nsresult> insertNewTextNodeResult =
aHTMLEditor.InsertNodeWithTransaction<Text>(
*newEmptyTextNode, pointToInsertNewStyleOrError.inspect());
if (MOZ_UNLIKELY(insertNewTextNodeResult.isErr())) {
NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
return insertNewTextNodeResult.propagateErr();
}
insertNewTextNodeResult.inspect().IgnoreCaretPointSuggestion();
return newEmptyTextNode;
}
Result<bool, nsresult>
HTMLEditor::AutoInlineStyleSetter::ElementIsGoodContainerForTheStyle(
HTMLEditor& aHTMLEditor, Element& aElement) const {
// If the editor is in the CSS mode and the style can be specified with CSS,
// we should not use existing HTML element as a new container.
const bool isCSSEditable = IsCSSSettable(aElement);
if (!aHTMLEditor.IsCSSEnabled() || !isCSSEditable) {
// First check for <b>, <i>, etc.
if (aElement.IsHTMLElement(&HTMLPropertyRef()) &&
!HTMLEditUtils::ElementHasAttribute(aElement) && !mAttribute) {
return true;
}
// Now look for things like <font>
if (mAttribute) {
nsString attrValue;
if (aElement.IsHTMLElement(&HTMLPropertyRef()) &&
!HTMLEditUtils::ElementHasAttributeExcept(aElement, *mAttribute) &&
aElement.GetAttr(mAttribute, attrValue)) {
if (attrValue.Equals(mAttributeValue,
nsCaseInsensitiveStringComparator)) {
return true;
}
if (mAttribute == nsGkAtoms::color ||
mAttribute == nsGkAtoms::bgcolor) {
if (aHTMLEditor.IsCSSEnabled()) {
if (HTMLEditUtils::IsSameCSSColorValue(mAttributeValue,
attrValue)) {
return true;
}
} else if (HTMLEditUtils::IsSameHTMLColorValue(
mAttributeValue, attrValue,
HTMLEditUtils::TransparentKeyword::Allowed)) {
return true;
}
}
}
}
if (!isCSSEditable) {
return false;
}
}
// No luck so far. Now we check for a <span> with a single style=""
// attribute that sets only the style we're looking for, if this type of
// style supports it
if (!aElement.IsHTMLElement(nsGkAtoms::span) ||
!aElement.HasAttr(nsGkAtoms::style) ||
HTMLEditUtils::ElementHasAttributeExcept(aElement, *nsGkAtoms::style)) {
return false;
}
nsStyledElement* styledElement = nsStyledElement::FromNode(&aElement);
if (MOZ_UNLIKELY(!styledElement)) {
return false;
}
// Some CSS styles are not so simple. For instance, underline is
// "text-decoration: underline", which decomposes into four different text-*
// properties. So for now, we just create a span, add the desired style, and
// see if it matches.
RefPtr<Element> newSpanElement =
aHTMLEditor.CreateHTMLContent(nsGkAtoms::span);
if (MOZ_UNLIKELY(!newSpanElement)) {
NS_WARNING("EditorBase::CreateHTMLContent(nsGkAtoms::span) failed");
return false;
}
nsStyledElement* styledNewSpanElement =
nsStyledElement::FromNode(newSpanElement);
if (MOZ_UNLIKELY(!styledNewSpanElement)) {
return false;
}
// MOZ_KnownLive(*styledNewSpanElement): It's newSpanElement whose type is
// RefPtr.
Result<size_t, nsresult> result = CSSEditUtils::SetCSSEquivalentToStyle(
WithTransaction::No, aHTMLEditor, MOZ_KnownLive(*styledNewSpanElement),
*this, &mAttributeValue);
if (MOZ_UNLIKELY(result.isErr())) {
// The call shouldn't return destroyed error because it must be
// impossible to run script with modifying the new orphan node.
MOZ_ASSERT_UNREACHABLE("How did you destroy this editor?");
if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) {
return Err(NS_ERROR_EDITOR_DESTROYED);
}
return false;
}
return CSSEditUtils::DoStyledElementsHaveSameStyle(*styledNewSpanElement,
*styledElement);
}
bool HTMLEditor::AutoInlineStyleSetter::ElementIsGoodContainerToSetStyle(
nsStyledElement& aStyledElement) const {
if (!HTMLEditUtils::IsContainerNode(aStyledElement) ||
!EditorUtils::IsEditableContent(aStyledElement, EditorType::HTML)) {
return false;
}
// If it has `style` attribute, let's use it.
if (aStyledElement.HasAttr(nsGkAtoms::style)) {
return true;
}
// If it has `class` or `id` attribute, the element may have specific rule.
// For applying the new style, we may need to set `style` attribute to it
// to override the specified rule.
if (aStyledElement.HasAttr(nsGkAtoms::id) ||
aStyledElement.HasAttr(nsGkAtoms::_class)) {
return true;
}
// If we're setting text-decoration and the element represents a value of
// text-decoration, <ins> or <del>, let's use it.
if (IsStyleOfTextDecoration(IgnoreSElement::No) &&
aStyledElement.IsAnyOfHTMLElements(nsGkAtoms::u, nsGkAtoms::s,
nsGkAtoms::strike, nsGkAtoms::ins,
nsGkAtoms::del)) {
return true;
}
// If we're setting font-size, color or background-color, we should use <font>
// for compatibility with the other browsers.
if (&HTMLPropertyRef() == nsGkAtoms::font &&
aStyledElement.IsHTMLElement(nsGkAtoms::font)) {
return true;
}
// If the element has one or more <br> (even if it's invisible), we don't
// want to use the <span> for compatibility with the other browsers.
if (aStyledElement.QuerySelector("br"_ns, IgnoreErrors())) {
return false;
}
// NOTE: The following code does not match with the other browsers not
// completely. Blink considers this with relation with the range.
// However, we cannot do it now. We should fix here after or at
// fixing bug 1792386.
// If it's only visible element child of parent block, let's use it.
// E.g., we don't want to create new <span> when
// `<p>{ <span>abc</span> }</p>`.
if (aStyledElement.GetParentElement() &&
HTMLEditUtils::IsBlockElement(
*aStyledElement.GetParentElement(),
BlockInlineCheck::UseComputedDisplayStyle)) {
for (nsIContent* previousSibling = aStyledElement.GetPreviousSibling();
previousSibling;
previousSibling = previousSibling->GetPreviousSibling()) {
if (previousSibling->IsElement()) {
return false; // Assume any elements visible.
}
if (Text* text = Text::FromNode(previousSibling)) {
if (HTMLEditUtils::IsVisibleTextNode(*text)) {
return false;
}
continue;
}
}
for (nsIContent* nextSibling = aStyledElement.GetNextSibling(); nextSibling;
nextSibling = nextSibling->GetNextSibling()) {
if (nextSibling->IsElement()) {
if (!HTMLEditUtils::IsInvisibleBRElement(*nextSibling)) {
return false;
}
continue; // The invisible <br> element may be followed by a child
// block, let's continue to check it.
}
if (Text* text = Text::FromNode(nextSibling)) {
if (HTMLEditUtils::IsVisibleTextNode(*text)) {
return false;
}
continue;
}
}
return true;
}
// Otherwise, wrap it into new <span> for making
// `<span>[abc</span> <span>def]</span>` become
// `<span style="..."><span>abc</span> <span>def</span></span>` rather
// than `<span style="...">abc <span>def</span></span>`.
return false;
}
Result<SplitRangeOffFromNodeResult, nsresult>
HTMLEditor::AutoInlineStyleSetter::SplitTextNodeAndApplyStyleToMiddleNode(
HTMLEditor& aHTMLEditor, Text& aText, uint32_t aStartOffset,
uint32_t aEndOffset) {
const RefPtr<Element> element = aText.GetParentElement();
if (!element || !HTMLEditUtils::CanNodeContain(*element, HTMLPropertyRef())) {
OnHandled(EditorDOMPoint(&aText, aStartOffset),
EditorDOMPoint(&aText, aEndOffset));
return SplitRangeOffFromNodeResult(nullptr, &aText, nullptr);
}
// Don't need to do anything if no characters actually selected
if (aStartOffset == aEndOffset) {
OnHandled(EditorDOMPoint(&aText, aStartOffset),
EditorDOMPoint(&aText, aEndOffset));
return SplitRangeOffFromNodeResult(nullptr, &aText, nullptr);
}
// Don't need to do anything if property already set on node
if (IsCSSSettable(*element)) {
// The HTML styles defined by this have a CSS equivalence for node;
// let's check if it carries those CSS styles
nsAutoString value(mAttributeValue);
Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
CSSEditUtils::IsComputedCSSEquivalentTo(aHTMLEditor, *element, *this,
value);
if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) {
NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed");
return isComputedCSSEquivalentToStyleOrError.propagateErr();
}
if (isComputedCSSEquivalentToStyleOrError.unwrap()) {
OnHandled(EditorDOMPoint(&aText, aStartOffset),
EditorDOMPoint(&aText, aEndOffset));
return SplitRangeOffFromNodeResult(nullptr, &aText, nullptr);
}
} else if (HTMLEditUtils::IsInlineStyleSetByElement(aText, *this,
&mAttributeValue)) {
OnHandled(EditorDOMPoint(&aText, aStartOffset),
EditorDOMPoint(&aText, aEndOffset));
return SplitRangeOffFromNodeResult(nullptr, &aText, nullptr);
}
// Make the range an independent node.
auto splitAtEndResult =
[&]() MOZ_CAN_RUN_SCRIPT -> Result<SplitNodeResult, nsresult> {
EditorDOMPoint atEnd(&aText, aEndOffset);
if (atEnd.IsEndOfContainer()) {
return SplitNodeResult::NotHandled(atEnd);
}
// We need to split off back of text node
Result<SplitNodeResult, nsresult> splitNodeResult =
aHTMLEditor.SplitNodeWithTransaction(atEnd);
if (splitNodeResult.isErr()) {
NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
return splitNodeResult;
}
if (MOZ_UNLIKELY(!splitNodeResult.inspect().HasCaretPointSuggestion())) {
NS_WARNING(
"HTMLEditor::SplitNodeWithTransaction() didn't suggest caret "
"point");
return Err(NS_ERROR_FAILURE);
}
return splitNodeResult;
}();
if (MOZ_UNLIKELY(splitAtEndResult.isErr())) {
return splitAtEndResult.propagateErr();
}
SplitNodeResult unwrappedSplitAtEndResult = splitAtEndResult.unwrap();
EditorDOMPoint pointToPutCaret = unwrappedSplitAtEndResult.UnwrapCaretPoint();
auto splitAtStartResult =
[&]() MOZ_CAN_RUN_SCRIPT -> Result<SplitNodeResult, nsresult> {
EditorDOMPoint atStart(unwrappedSplitAtEndResult.DidSplit()
? unwrappedSplitAtEndResult.GetPreviousContent()
: &aText,
aStartOffset);
if (atStart.IsStartOfContainer()) {
return SplitNodeResult::NotHandled(atStart);
}
// We need to split off front of text node
Result<SplitNodeResult, nsresult> splitNodeResult =
aHTMLEditor.SplitNodeWithTransaction(atStart);
if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
return splitNodeResult;
}
if (MOZ_UNLIKELY(!splitNodeResult.inspect().HasCaretPointSuggestion())) {
NS_WARNING(
"HTMLEditor::SplitNodeWithTransaction() didn't suggest caret "
"point");
return Err(NS_ERROR_FAILURE);
}
return splitNodeResult;
}();
if (MOZ_UNLIKELY(splitAtStartResult.isErr())) {
return splitAtStartResult.propagateErr();
}
SplitNodeResult unwrappedSplitAtStartResult = splitAtStartResult.unwrap();
if (unwrappedSplitAtStartResult.HasCaretPointSuggestion()) {
pointToPutCaret = unwrappedSplitAtStartResult.UnwrapCaretPoint();
}
MOZ_ASSERT_IF(unwrappedSplitAtStartResult.DidSplit(),
unwrappedSplitAtStartResult.GetPreviousContent()->IsText());
MOZ_ASSERT_IF(unwrappedSplitAtStartResult.DidSplit(),
unwrappedSplitAtStartResult.GetNextContent()->IsText());
MOZ_ASSERT_IF(unwrappedSplitAtEndResult.DidSplit(),
unwrappedSplitAtEndResult.GetPreviousContent()->IsText());
MOZ_ASSERT_IF(unwrappedSplitAtEndResult.DidSplit(),
unwrappedSplitAtEndResult.GetNextContent()->IsText());
// Note that those text nodes are grabbed by unwrappedSplitAtStartResult,
// unwrappedSplitAtEndResult or the callers. Therefore, we don't need to make
// them strong pointer.
Text* const leftTextNode =
unwrappedSplitAtStartResult.DidSplit()
? unwrappedSplitAtStartResult.GetPreviousContentAs<Text>()
: nullptr;
Text* const middleTextNode =
unwrappedSplitAtStartResult.DidSplit()
? unwrappedSplitAtStartResult.GetNextContentAs<Text>()
: (unwrappedSplitAtEndResult.DidSplit()
? unwrappedSplitAtEndResult.GetPreviousContentAs<Text>()
: &aText);
Text* const rightTextNode =
unwrappedSplitAtEndResult.DidSplit()
? unwrappedSplitAtEndResult.GetNextContentAs<Text>()
: nullptr;
if (mAttribute) {
// Look for siblings that are correct type of node
nsIContent* sibling = HTMLEditUtils::GetPreviousSibling(
*middleTextNode, {WalkTreeOption::IgnoreNonEditableNode});
if (sibling && sibling->IsElement()) {
OwningNonNull<Element> element(*sibling->AsElement());
Result<bool, nsresult> result =
ElementIsGoodContainerForTheStyle(aHTMLEditor, element);
if (MOZ_UNLIKELY(result.isErr())) {
NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed");
return result.propagateErr();
}
if (result.inspect()) {
// Previous sib is already right kind of inline node; slide this over
Result<MoveNodeResult, nsresult> moveTextNodeResult =
aHTMLEditor.MoveNodeToEndWithTransaction(
MOZ_KnownLive(*middleTextNode), element);
if (MOZ_UNLIKELY(moveTextNodeResult.isErr())) {
NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
return moveTextNodeResult.propagateErr();
}
MoveNodeResult unwrappedMoveTextNodeResult =
moveTextNodeResult.unwrap();
unwrappedMoveTextNodeResult.MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
OnHandled(*middleTextNode);
return SplitRangeOffFromNodeResult(leftTextNode, middleTextNode,
rightTextNode,
std::move(pointToPutCaret));
}
}
sibling = HTMLEditUtils::GetNextSibling(
*middleTextNode, {WalkTreeOption::IgnoreNonEditableNode});
if (sibling && sibling->IsElement()) {
OwningNonNull<Element> element(*sibling->AsElement());
Result<bool, nsresult> result =
ElementIsGoodContainerForTheStyle(aHTMLEditor, element);
if (MOZ_UNLIKELY(result.isErr())) {
NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed");
return result.propagateErr();
}
if (result.inspect()) {
// Following sib is already right kind of inline node; slide this over
Result<MoveNodeResult, nsresult> moveTextNodeResult =
aHTMLEditor.MoveNodeWithTransaction(MOZ_KnownLive(*middleTextNode),
EditorDOMPoint(sibling, 0u));
if (MOZ_UNLIKELY(moveTextNodeResult.isErr())) {
NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
return moveTextNodeResult.propagateErr();
}
MoveNodeResult unwrappedMoveTextNodeResult =
moveTextNodeResult.unwrap();
unwrappedMoveTextNodeResult.MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
OnHandled(*middleTextNode);
return SplitRangeOffFromNodeResult(leftTextNode, middleTextNode,
rightTextNode,
std::move(pointToPutCaret));
}
}
}
// Wrap the node inside inline node.
Result<CaretPoint, nsresult> setStyleResult =
ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(
aHTMLEditor, MOZ_KnownLive(*middleTextNode));
if (MOZ_UNLIKELY(setStyleResult.isErr())) {
NS_WARNING(
"AutoInlineStyleSetter::"
"ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed");
return setStyleResult.propagateErr();
}
return SplitRangeOffFromNodeResult(
leftTextNode, middleTextNode, rightTextNode,
setStyleResult.unwrap().UnwrapCaretPoint());
}
Result<CaretPoint, nsresult> HTMLEditor::AutoInlineStyleSetter::ApplyStyle(
HTMLEditor& aHTMLEditor, nsIContent& aContent) {
// If this is an element that can't be contained in a span, we have to
// recurse to its children.
if (!HTMLEditUtils::CanNodeContain(*nsGkAtoms::span, aContent)) {
if (!aContent.HasChildren()) {
return CaretPoint(EditorDOMPoint());
}
AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfContents;
HTMLEditUtils::CollectChildren(
aContent, arrayOfContents,
{CollectChildrenOption::IgnoreNonEditableChildren,
CollectChildrenOption::IgnoreInvisibleTextNodes});
// Then loop through the list, set the property on each node.
EditorDOMPoint pointToPutCaret;
for (const OwningNonNull<nsIContent>& content : arrayOfContents) {
// MOZ_KnownLive because 'arrayOfContents' is guaranteed to
// keep it alive.
Result<CaretPoint, nsresult> setInlinePropertyResult =
ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(
aHTMLEditor, MOZ_KnownLive(content));
if (MOZ_UNLIKELY(setInlinePropertyResult.isErr())) {
NS_WARNING(
"AutoInlineStyleSetter::"
"ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed");
return setInlinePropertyResult;
}
setInlinePropertyResult.unwrap().MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
}
return CaretPoint(std::move(pointToPutCaret));
}
// First check if there's an adjacent sibling we can put our node into.
nsCOMPtr<nsIContent> previousSibling = HTMLEditUtils::GetPreviousSibling(
aContent, {WalkTreeOption::IgnoreNonEditableNode});
nsCOMPtr<nsIContent> nextSibling = HTMLEditUtils::GetNextSibling(
aContent, {WalkTreeOption::IgnoreNonEditableNode});
if (RefPtr<Element> previousElement =
Element::FromNodeOrNull(previousSibling)) {
Result<bool, nsresult> canMoveIntoPreviousSibling =
ElementIsGoodContainerForTheStyle(aHTMLEditor, *previousElement);
if (MOZ_UNLIKELY(canMoveIntoPreviousSibling.isErr())) {
NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed");
return canMoveIntoPreviousSibling.propagateErr();
}
if (canMoveIntoPreviousSibling.inspect()) {
Result<MoveNodeResult, nsresult> moveNodeResult =
aHTMLEditor.MoveNodeToEndWithTransaction(aContent, *previousSibling);
if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
return moveNodeResult.propagateErr();
}
MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
RefPtr<Element> nextElement = Element::FromNodeOrNull(nextSibling);
if (!nextElement) {
OnHandled(aContent);
return CaretPoint(unwrappedMoveNodeResult.UnwrapCaretPoint());
}
Result<bool, nsresult> canMoveIntoNextSibling =
ElementIsGoodContainerForTheStyle(aHTMLEditor, *nextElement);
if (MOZ_UNLIKELY(canMoveIntoNextSibling.isErr())) {
NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed");
unwrappedMoveNodeResult.IgnoreCaretPointSuggestion();
return canMoveIntoNextSibling.propagateErr();
}
if (!canMoveIntoNextSibling.inspect()) {
OnHandled(aContent);
return CaretPoint(unwrappedMoveNodeResult.UnwrapCaretPoint());
}
unwrappedMoveNodeResult.IgnoreCaretPointSuggestion();
// JoinNodesWithTransaction (DoJoinNodes) tries to collapse selection to
// the joined point and we want to skip updating `Selection` here.
AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
Result<JoinNodesResult, nsresult> joinNodesResult =
aHTMLEditor.JoinNodesWithTransaction(*previousElement, *nextElement);
if (MOZ_UNLIKELY(joinNodesResult.isErr())) {
NS_WARNING("HTMLEditor::JoinNodesWithTransaction() failed");
return joinNodesResult.propagateErr();
}
// So, let's take it.
OnHandled(aContent);
return CaretPoint(
joinNodesResult.inspect().AtJoinedPoint<EditorDOMPoint>());
}
}
if (RefPtr<Element> nextElement = Element::FromNodeOrNull(nextSibling)) {
Result<bool, nsresult> canMoveIntoNextSibling =
ElementIsGoodContainerForTheStyle(aHTMLEditor, *nextElement);
if (MOZ_UNLIKELY(canMoveIntoNextSibling.isErr())) {
NS_WARNING("HTMLEditor::ElementIsGoodContainerForTheStyle() failed");
return canMoveIntoNextSibling.propagateErr();
}
if (canMoveIntoNextSibling.inspect()) {
Result<MoveNodeResult, nsresult> moveNodeResult =
aHTMLEditor.MoveNodeWithTransaction(aContent,
EditorDOMPoint(nextElement, 0u));
if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
return moveNodeResult.propagateErr();
}
OnHandled(aContent);
return CaretPoint(moveNodeResult.unwrap().UnwrapCaretPoint());
}
}
// Don't need to do anything if property already set on node
if (const RefPtr<Element> element = aContent.GetAsElementOrParentElement()) {
if (IsCSSSettable(*element)) {
nsAutoString value(mAttributeValue);
// MOZ_KnownLive(element) because it's aContent.
Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
CSSEditUtils::IsComputedCSSEquivalentTo(aHTMLEditor, *element, *this,
value);
if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) {
NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed");
return isComputedCSSEquivalentToStyleOrError.propagateErr();
}
if (isComputedCSSEquivalentToStyleOrError.unwrap()) {
OnHandled(aContent);
return CaretPoint(EditorDOMPoint());
}
} else if (HTMLEditUtils::IsInlineStyleSetByElement(*element, *this,
&mAttributeValue)) {
OnHandled(aContent);
return CaretPoint(EditorDOMPoint());
}
}
auto ShouldUseCSS = [&]() {
if (aHTMLEditor.IsCSSEnabled() && aContent.GetAsElementOrParentElement() &&
IsCSSSettable(*aContent.GetAsElementOrParentElement())) {
return true;
}
// bgcolor is always done using CSS
if (mAttribute == nsGkAtoms::bgcolor) {
return true;
}
// called for removing parent style, we should use CSS with <span> element.
if (IsStyleToInvert()) {
return true;
}
// If we set color value, the value may be able to specified only with CSS.
// In that case, we need to use CSS even in the HTML mode.
if (mAttribute == nsGkAtoms::color) {
return mAttributeValue.First() != '#' &&
!HTMLEditUtils::CanConvertToHTMLColorValue(mAttributeValue);
}
return false;
};
if (ShouldUseCSS()) {
// We need special handlings for text-decoration.
if (IsStyleOfTextDecoration(IgnoreSElement::No)) {
Result<CaretPoint, nsresult> result =
ApplyCSSTextDecoration(aHTMLEditor, aContent);
NS_WARNING_ASSERTION(
result.isOk(),
"AutoInlineStyleSetter::ApplyCSSTextDecoration() failed");
return result;
}
EditorDOMPoint pointToPutCaret;
RefPtr<nsStyledElement> styledElement = [&]() -> nsStyledElement* {
auto* const styledElement = nsStyledElement::FromNode(&aContent);
return styledElement && ElementIsGoodContainerToSetStyle(*styledElement)
? styledElement
: nullptr;
}();
// If we don't have good element to set the style, let's create new <span>.
if (!styledElement) {
Result<CreateElementResult, nsresult> wrapInSpanElementResult =
aHTMLEditor.InsertContainerWithTransaction(aContent,
*nsGkAtoms::span);
if (MOZ_UNLIKELY(wrapInSpanElementResult.isErr())) {
NS_WARNING(
"HTMLEditor::InsertContainerWithTransaction(nsGkAtoms::span) "
"failed");
return wrapInSpanElementResult.propagateErr();
}
CreateElementResult unwrappedWrapInSpanElementResult =
wrapInSpanElementResult.unwrap();
MOZ_ASSERT(unwrappedWrapInSpanElementResult.GetNewNode());
unwrappedWrapInSpanElementResult.MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
styledElement = nsStyledElement::FromNode(
unwrappedWrapInSpanElementResult.GetNewNode());
MOZ_ASSERT(styledElement);
if (MOZ_UNLIKELY(!styledElement)) {
// Don't return error to avoid creating new path to throwing error.
OnHandled(aContent);
return CaretPoint(pointToPutCaret);
}
}
// Add the CSS styles corresponding to the HTML style request
if (IsCSSSettable(*styledElement)) {
Result<size_t, nsresult> result = CSSEditUtils::SetCSSEquivalentToStyle(
WithTransaction::Yes, aHTMLEditor, *styledElement, *this,
&mAttributeValue);
if (MOZ_UNLIKELY(result.isErr())) {
if (NS_WARN_IF(result.inspectErr() == NS_ERROR_EDITOR_DESTROYED)) {
return Err(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING(
"CSSEditUtils::SetCSSEquivalentToStyle() failed, but ignored");
}
}
OnHandled(aContent);
return CaretPoint(pointToPutCaret);
}
nsAutoString attributeValue(mAttributeValue);
if (mAttribute == nsGkAtoms::color && mAttributeValue.First() != '#') {
// At here, all color values should be able to be parsed as a CSS color
// value. Therefore, we need to convert it to normalized HTML color value.
HTMLEditUtils::ConvertToNormalizedHTMLColorValue(attributeValue,
attributeValue);
}
// is it already the right kind of node, but with wrong attribute?
if (aContent.IsHTMLElement(&HTMLPropertyRef())) {
if (NS_WARN_IF(!mAttribute)) {
return Err(NS_ERROR_INVALID_ARG);
}
// Just set the attribute on it.
nsresult rv = aHTMLEditor.SetAttributeWithTransaction(
MOZ_KnownLive(*aContent.AsElement()), *mAttribute, attributeValue);
if (NS_WARN_IF(aHTMLEditor.Destroyed())) {
return Err(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::SetAttributeWithTransaction() failed");
return Err(rv);
}
OnHandled(aContent);
return CaretPoint(EditorDOMPoint());
}
// ok, chuck it in its very own container
Result<CreateElementResult, nsresult> wrapWithNewElementToFormatResult =
aHTMLEditor.InsertContainerWithTransaction(
aContent, MOZ_KnownLive(HTMLPropertyRef()),
!mAttribute ? HTMLEditor::DoNothingForNewElement
// MOZ_CAN_RUN_SCRIPT_BOUNDARY due to bug 1758868
: [&](HTMLEditor& aHTMLEditor, Element& aNewElement,
const EditorDOMPoint&) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
nsresult rv =
aNewElement.SetAttr(kNameSpaceID_None, mAttribute,
attributeValue, false);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Element::SetAttr() failed");
return rv;
});
if (MOZ_UNLIKELY(wrapWithNewElementToFormatResult.isErr())) {
NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed");
return wrapWithNewElementToFormatResult.propagateErr();
}
OnHandled(aContent);
MOZ_ASSERT(wrapWithNewElementToFormatResult.inspect().GetNewNode());
return CaretPoint(
wrapWithNewElementToFormatResult.unwrap().UnwrapCaretPoint());
}
Result<CaretPoint, nsresult>
HTMLEditor::AutoInlineStyleSetter::ApplyCSSTextDecoration(
HTMLEditor& aHTMLEditor, nsIContent& aContent) {
MOZ_ASSERT(IsStyleOfTextDecoration(IgnoreSElement::No));
EditorDOMPoint pointToPutCaret;
RefPtr<nsStyledElement> styledElement = nsStyledElement::FromNode(aContent);
nsAutoString newTextDecorationValue;
if (&HTMLPropertyRef() == nsGkAtoms::u) {
newTextDecorationValue.AssignLiteral(u"underline");
} else if (&HTMLPropertyRef() == nsGkAtoms::s ||
&HTMLPropertyRef() == nsGkAtoms::strike) {
newTextDecorationValue.AssignLiteral(u"line-through");
} else {
MOZ_ASSERT_UNREACHABLE(
"Was new value added in "
"IsStyleOfTextDecoration(IgnoreSElement::No))?");
}
if (styledElement && IsCSSSettable(*styledElement) &&
ElementIsGoodContainerToSetStyle(*styledElement)) {
nsAutoString textDecorationValue;
nsresult rv = CSSEditUtils::GetSpecifiedProperty(
*styledElement, *nsGkAtoms::text_decoration, textDecorationValue);
if (NS_FAILED(rv)) {
NS_WARNING(
"CSSEditUtils::GetSpecifiedProperty(nsGkAtoms::text_decoration) "
"failed");
return Err(rv);
}
// However, if the element is an element to style the text-decoration,
// replace it with new <span>.
if (styledElement && styledElement->IsAnyOfHTMLElements(
nsGkAtoms::u, nsGkAtoms::s, nsGkAtoms::strike)) {
Result<CreateElementResult, nsresult> replaceResult =
aHTMLEditor.ReplaceContainerAndCloneAttributesWithTransaction(
*styledElement, *nsGkAtoms::span);
if (MOZ_UNLIKELY(replaceResult.isErr())) {
NS_WARNING(
"HTMLEditor::ReplaceContainerAndCloneAttributesWithTransaction() "
"failed");
return replaceResult.propagateErr();
}
CreateElementResult unwrappedReplaceResult = replaceResult.unwrap();
MOZ_ASSERT(unwrappedReplaceResult.GetNewNode());
unwrappedReplaceResult.MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
// The new <span> needs to specify the original element's text-decoration
// style unless it's specified explicitly.
if (textDecorationValue.IsEmpty()) {
if (!newTextDecorationValue.IsEmpty()) {
newTextDecorationValue.Append(HTMLEditUtils::kSpace);
}
if (styledElement->IsHTMLElement(nsGkAtoms::u)) {
newTextDecorationValue.AppendLiteral(u"underline");
} else {
newTextDecorationValue.AppendLiteral(u"line-through");
}
}
styledElement =
nsStyledElement::FromNode(unwrappedReplaceResult.GetNewNode());
if (NS_WARN_IF(!styledElement)) {
OnHandled(aContent);
return CaretPoint(pointToPutCaret);
}
}
// If the element has default style, we need to keep it after specifying
// text-decoration.
else if (textDecorationValue.IsEmpty() &&
styledElement->IsAnyOfHTMLElements(nsGkAtoms::u, nsGkAtoms::ins)) {
if (!newTextDecorationValue.IsEmpty()) {
newTextDecorationValue.Append(HTMLEditUtils::kSpace);
}
newTextDecorationValue.AppendLiteral(u"underline");
} else if (textDecorationValue.IsEmpty() &&
styledElement->IsAnyOfHTMLElements(
nsGkAtoms::s, nsGkAtoms::strike, nsGkAtoms::del)) {
if (!newTextDecorationValue.IsEmpty()) {
newTextDecorationValue.Append(HTMLEditUtils::kSpace);
}
newTextDecorationValue.AppendLiteral(u"line-through");
}
}
// Otherwise, use new <span> element.
else {
Result<CreateElementResult, nsresult> wrapInSpanElementResult =
aHTMLEditor.InsertContainerWithTransaction(aContent, *nsGkAtoms::span);
if (MOZ_UNLIKELY(wrapInSpanElementResult.isErr())) {
NS_WARNING(
"HTMLEditor::InsertContainerWithTransaction(nsGkAtoms::span) failed");
return wrapInSpanElementResult.propagateErr();
}
CreateElementResult unwrappedWrapInSpanElementResult =
wrapInSpanElementResult.unwrap();
MOZ_ASSERT(unwrappedWrapInSpanElementResult.GetNewNode());
unwrappedWrapInSpanElementResult.MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
styledElement = nsStyledElement::FromNode(
unwrappedWrapInSpanElementResult.GetNewNode());
if (NS_WARN_IF(!styledElement)) {
OnHandled(aContent);
return CaretPoint(pointToPutCaret);
}
}
nsresult rv = CSSEditUtils::SetCSSPropertyWithTransaction(
aHTMLEditor, *styledElement, *nsGkAtoms::text_decoration,
newTextDecorationValue);
if (NS_FAILED(rv)) {
NS_WARNING("CSSEditUtils::SetCSSPropertyWithTransaction() failed");
return Err(rv);
}
OnHandled(aContent);
return CaretPoint(pointToPutCaret);
}
Result<CaretPoint, nsresult> HTMLEditor::AutoInlineStyleSetter::
ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(HTMLEditor& aHTMLEditor,
nsIContent& aContent) {
if (NS_WARN_IF(!aContent.GetParentNode())) {
return Err(NS_ERROR_FAILURE);
}
OwningNonNull<nsINode> parent = *aContent.GetParentNode();
nsCOMPtr<nsIContent> previousSibling = aContent.GetPreviousSibling(),
nextSibling = aContent.GetNextSibling();
EditorDOMPoint pointToPutCaret;
if (aContent.IsElement()) {
Result<EditorDOMPoint, nsresult> removeStyleResult =
aHTMLEditor.RemoveStyleInside(MOZ_KnownLive(*aContent.AsElement()),
*this, SpecifiedStyle::Preserve);
if (MOZ_UNLIKELY(removeStyleResult.isErr())) {
NS_WARNING("HTMLEditor::RemoveStyleInside() failed");
return removeStyleResult.propagateErr();
}
if (removeStyleResult.inspect().IsSet()) {
pointToPutCaret = removeStyleResult.unwrap();
}
if (nsStaticAtom* similarElementNameAtom = GetSimilarElementNameAtom()) {
Result<EditorDOMPoint, nsresult> removeStyleResult =
aHTMLEditor.RemoveStyleInside(
MOZ_KnownLive(*aContent.AsElement()),
EditorInlineStyle(*similarElementNameAtom),
SpecifiedStyle::Preserve);
if (MOZ_UNLIKELY(removeStyleResult.isErr())) {
NS_WARNING("HTMLEditor::RemoveStyleInside() failed");
return removeStyleResult.propagateErr();
}
if (removeStyleResult.inspect().IsSet()) {
pointToPutCaret = removeStyleResult.unwrap();
}
}
}
if (aContent.GetParentNode()) {
// The node is still where it was
Result<CaretPoint, nsresult> pointToPutCaretOrError =
ApplyStyle(aHTMLEditor, aContent);
NS_WARNING_ASSERTION(pointToPutCaretOrError.isOk(),
"AutoInlineStyleSetter::ApplyStyle() failed");
return pointToPutCaretOrError;
}
// It's vanished. Use the old siblings for reference to construct a
// list. But first, verify that the previous/next siblings are still
// where we expect them; otherwise we have to give up.
if (NS_WARN_IF(previousSibling &&
previousSibling->GetParentNode() != parent) ||
NS_WARN_IF(nextSibling && nextSibling->GetParentNode() != parent) ||
NS_WARN_IF(!parent->IsInComposedDoc())) {
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
AutoTArray<OwningNonNull<nsIContent>, 24> nodesToSet;
for (nsIContent* content = previousSibling ? previousSibling->GetNextSibling()
: parent->GetFirstChild();
content && content != nextSibling; content = content->GetNextSibling()) {
if (EditorUtils::IsEditableContent(*content, EditorType::HTML)) {
nodesToSet.AppendElement(*content);
}
}
for (OwningNonNull<nsIContent>& content : nodesToSet) {
// MOZ_KnownLive because 'nodesToSet' is guaranteed to
// keep it alive.
Result<CaretPoint, nsresult> pointToPutCaretOrError =
ApplyStyle(aHTMLEditor, MOZ_KnownLive(content));
if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
NS_WARNING("AutoInlineStyleSetter::ApplyStyle() failed");
return pointToPutCaretOrError;
}
pointToPutCaretOrError.unwrap().MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
}
return CaretPoint(pointToPutCaret);
}
bool HTMLEditor::AutoInlineStyleSetter::ContentIsElementSettingTheStyle(
const HTMLEditor& aHTMLEditor, nsIContent& aContent) const {
Element* const element = Element::FromNode(&aContent);
if (!element) {
return false;
}
if (IsRepresentedBy(*element)) {
return true;
}
Result<bool, nsresult> specified = IsSpecifiedBy(aHTMLEditor, *element);
NS_WARNING_ASSERTION(specified.isOk(),
"EditorInlineStyle::IsSpecified() failed, but ignored");
return specified.unwrapOr(false);
}
// static
nsIContent* HTMLEditor::AutoInlineStyleSetter::GetNextEditableInlineContent(
const nsIContent& aContent, const nsINode* aLimiter) {
auto* const nextContentInRange = [&]() -> nsIContent* {
for (nsIContent* parent : aContent.InclusiveAncestorsOfType<nsIContent>()) {
if (parent == aLimiter ||
!EditorUtils::IsEditableContent(*parent, EditorType::HTML) ||
(parent->IsElement() &&
(HTMLEditUtils::IsBlockElement(
*parent->AsElement(),
BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
HTMLEditUtils::IsDisplayInsideFlowRoot(*parent->AsElement())))) {
return nullptr;
}
if (nsIContent* nextSibling = parent->GetNextSibling()) {
return nextSibling;
}
}
return nullptr;
}();
return nextContentInRange &&
EditorUtils::IsEditableContent(*nextContentInRange,
EditorType::HTML) &&
!HTMLEditUtils::IsBlockElement(
*nextContentInRange,
BlockInlineCheck::UseComputedDisplayOutsideStyle)
? nextContentInRange
: nullptr;
}
// static
nsIContent* HTMLEditor::AutoInlineStyleSetter::GetPreviousEditableInlineContent(
const nsIContent& aContent, const nsINode* aLimiter) {
auto* const previousContentInRange = [&]() -> nsIContent* {
for (nsIContent* parent : aContent.InclusiveAncestorsOfType<nsIContent>()) {
if (parent == aLimiter ||
!EditorUtils::IsEditableContent(*parent, EditorType::HTML) ||
(parent->IsElement() &&
(HTMLEditUtils::IsBlockElement(
*parent->AsElement(),
BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
HTMLEditUtils::IsDisplayInsideFlowRoot(*parent->AsElement())))) {
return nullptr;
}
if (nsIContent* previousSibling = parent->GetPreviousSibling()) {
return previousSibling;
}
}
return nullptr;
}();
return previousContentInRange &&
EditorUtils::IsEditableContent(*previousContentInRange,
EditorType::HTML) &&
!HTMLEditUtils::IsBlockElement(
*previousContentInRange,
BlockInlineCheck::UseComputedDisplayOutsideStyle)
? previousContentInRange
: nullptr;
}
EditorRawDOMPoint HTMLEditor::AutoInlineStyleSetter::GetShrunkenRangeStart(
const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
const nsINode& aCommonAncestorOfRange,
const nsIContent* aFirstEntirelySelectedContentNodeInRange) const {
const EditorDOMPoint& startRef = aRange.StartRef();
// <a> cannot be nested and it should be represented with one element as far
// as possible. Therefore, we don't need to shrink the range.
if (IsStyleOfAnchorElement()) {
return startRef.To<EditorRawDOMPoint>();
}
// If the start boundary is at end of a node, we need to shrink the range
// to next content, e.g., `abc[<b>def` should be `abc<b>[def` unless the
// <b> is not entirely selected.
auto* const nextContentOrStartContainer = [&]() -> nsIContent* {
if (!startRef.IsInContentNode()) {
return nullptr;
}
if (!startRef.IsEndOfContainer()) {
return startRef.ContainerAs<nsIContent>();
}
nsIContent* const nextContent =
AutoInlineStyleSetter::GetNextEditableInlineContent(
*startRef.ContainerAs<nsIContent>(), &aCommonAncestorOfRange);
return nextContent ? nextContent : startRef.ContainerAs<nsIContent>();
}();
if (MOZ_UNLIKELY(!nextContentOrStartContainer)) {
return startRef.To<EditorRawDOMPoint>();
}
EditorRawDOMPoint startPoint =
nextContentOrStartContainer != startRef.ContainerAs<nsIContent>()
? EditorRawDOMPoint(nextContentOrStartContainer)
: startRef.To<EditorRawDOMPoint>();
MOZ_ASSERT(startPoint.IsSet());
// If the start point points a content node, let's try to move it down to
// start of the child recursively.
while (nsIContent* child = startPoint.GetChild()) {
// We shouldn't cross editable and block boundary.
if (!EditorUtils::IsEditableContent(*child, EditorType::HTML) ||
HTMLEditUtils::IsBlockElement(
*child, BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
break;
}
// If we reach a text node, the minimized range starts from start of it.
if (child->IsText()) {
startPoint.Set(child, 0u);
break;
}
// Don't shrink the range into element which applies the style to children
// because we want to update the element. E.g., if we are setting
// background color, we want to update style attribute of an element which
// specifies background color with `style` attribute.
if (child == aFirstEntirelySelectedContentNodeInRange) {
break;
}
// We should not start from an atomic element such as <br>, <img>, etc.
if (!HTMLEditUtils::IsContainerNode(*child)) {
break;
}
// If the element specifies the style, we should update it. Therefore, we
// need to wrap it in the range.
if (ContentIsElementSettingTheStyle(aHTMLEditor, *child)) {
break;
}
// If the child is an `<a>`, we should not shrink the range into it
// because user may not want to keep editing in the link except when user
// tries to update selection into it obviously.
if (child->IsHTMLElement(nsGkAtoms::a)) {
break;
}
startPoint.Set(child, 0u);
}
return startPoint;
}
EditorRawDOMPoint HTMLEditor::AutoInlineStyleSetter::GetShrunkenRangeEnd(
const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
const nsINode& aCommonAncestorOfRange,
const nsIContent* aLastEntirelySelectedContentNodeInRange) const {
const EditorDOMPoint& endRef = aRange.EndRef();
// <a> cannot be nested and it should be represented with one element as far
// as possible. Therefore, we don't need to shrink the range.
if (IsStyleOfAnchorElement()) {
return endRef.To<EditorRawDOMPoint>();
}
// If the end boundary is at start of a node, we need to shrink the range
// to previous content, e.g., `abc</b>]def` should be `abc]</b>def` unless
// the <b> is not entirely selected.
auto* const previousContentOrEndContainer = [&]() -> nsIContent* {
if (!endRef.IsInContentNode()) {
return nullptr;
}
if (!endRef.IsStartOfContainer()) {
return endRef.ContainerAs<nsIContent>();
}
nsIContent* const previousContent =
AutoInlineStyleSetter::GetPreviousEditableInlineContent(
*endRef.ContainerAs<nsIContent>(), &aCommonAncestorOfRange);
return previousContent ? previousContent : endRef.ContainerAs<nsIContent>();
}();
if (MOZ_UNLIKELY(!previousContentOrEndContainer)) {
return endRef.To<EditorRawDOMPoint>();
}
EditorRawDOMPoint endPoint =
previousContentOrEndContainer != endRef.ContainerAs<nsIContent>()
? EditorRawDOMPoint::After(*previousContentOrEndContainer)
: endRef.To<EditorRawDOMPoint>();
MOZ_ASSERT(endPoint.IsSet());
// If the end point points after a content node, let's try to move it down
// to end of the child recursively.
while (nsIContent* child = endPoint.GetPreviousSiblingOfChild()) {
// We shouldn't cross editable and block boundary.
if (!EditorUtils::IsEditableContent(*child, EditorType::HTML) ||
HTMLEditUtils::IsBlockElement(
*child, BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
break;
}
// If we reach a text node, the minimized range starts from start of it.
if (child->IsText()) {
endPoint.SetToEndOf(child);
break;
}
// Don't shrink the range into element which applies the style to children
// because we want to update the element. E.g., if we are setting
// background color, we want to update style attribute of an element which
// specifies background color with `style` attribute.
if (child == aLastEntirelySelectedContentNodeInRange) {
break;
}
// We should not end in an atomic element such as <br>, <img>, etc.
if (!HTMLEditUtils::IsContainerNode(*child)) {
break;
}
// If the element specifies the style, we should update it. Therefore, we
// need to wrap it in the range.
if (ContentIsElementSettingTheStyle(aHTMLEditor, *child)) {
break;
}
// If the child is an `<a>`, we should not shrink the range into it
// because user may not want to keep editing in the link except when user
// tries to update selection into it obviously.
if (child->IsHTMLElement(nsGkAtoms::a)) {
break;
}
endPoint.SetToEndOf(child);
}
return endPoint;
}
EditorRawDOMPoint HTMLEditor::AutoInlineStyleSetter::
GetExtendedRangeStartToWrapAncestorApplyingSameStyle(
const HTMLEditor& aHTMLEditor,
const EditorRawDOMPoint& aStartPoint) const {
MOZ_ASSERT(aStartPoint.IsSetAndValid());
EditorRawDOMPoint startPoint = aStartPoint;
// FIXME: This should handle ignore invisible white-spaces before the position
// if it's in a text node or invisible white-spaces.
if (!startPoint.IsStartOfContainer() ||
startPoint.GetContainer()->GetPreviousSibling()) {
return startPoint;
}
// FYI: Currently, we don't support setting `font-size` even in the CSS mode.
// Therefore, if the style is <font size="...">, we always set a <font>.
const bool isSettingFontElement =
IsStyleOfFontSize() ||
(!aHTMLEditor.IsCSSEnabled() && IsStyleOfFontElement());
Element* mostDistantStartParentHavingStyle = nullptr;
for (Element* parent :
startPoint.GetContainer()->InclusiveAncestorsOfType<Element>()) {
if (!EditorUtils::IsEditableContent(*parent, EditorType::HTML) ||
HTMLEditUtils::IsBlockElement(
*parent, BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
HTMLEditUtils::IsDisplayInsideFlowRoot(*parent)) {
break;
}
if (ContentIsElementSettingTheStyle(aHTMLEditor, *parent)) {
mostDistantStartParentHavingStyle = parent;
}
// If we're setting <font> element and there is a <font> element which is
// entirely selected, we should use it.
else if (isSettingFontElement && parent->IsHTMLElement(nsGkAtoms::font)) {
mostDistantStartParentHavingStyle = parent;
}
if (parent->GetPreviousSibling()) {
break; // The parent is not first element in its parent, stop climbing.
}
}
if (mostDistantStartParentHavingStyle) {
startPoint.Set(mostDistantStartParentHavingStyle);
}
return startPoint;
}
EditorRawDOMPoint HTMLEditor::AutoInlineStyleSetter::
GetExtendedRangeEndToWrapAncestorApplyingSameStyle(
const HTMLEditor& aHTMLEditor,
const EditorRawDOMPoint& aEndPoint) const {
MOZ_ASSERT(aEndPoint.IsSetAndValid());
EditorRawDOMPoint endPoint = aEndPoint;
// FIXME: This should ignore invisible white-spaces after the position if it's
// in a text node, invisible <br> or following invisible text nodes.
if (!endPoint.IsEndOfContainer() ||
endPoint.GetContainer()->GetNextSibling()) {
return endPoint;
}
// FYI: Currently, we don't support setting `font-size` even in the CSS mode.
// Therefore, if the style is <font size="...">, we always set a <font>.
const bool isSettingFontElement =
IsStyleOfFontSize() ||
(!aHTMLEditor.IsCSSEnabled() && IsStyleOfFontElement());
Element* mostDistantEndParentHavingStyle = nullptr;
for (Element* parent :
endPoint.GetContainer()->InclusiveAncestorsOfType<Element>()) {
if (!EditorUtils::IsEditableContent(*parent, EditorType::HTML) ||
HTMLEditUtils::IsBlockElement(
*parent, BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
HTMLEditUtils::IsDisplayInsideFlowRoot(*parent)) {
break;
}
if (ContentIsElementSettingTheStyle(aHTMLEditor, *parent)) {
mostDistantEndParentHavingStyle = parent;
}
// If we're setting <font> element and there is a <font> element which is
// entirely selected, we should use it.
else if (isSettingFontElement && parent->IsHTMLElement(nsGkAtoms::font)) {
mostDistantEndParentHavingStyle = parent;
}
if (parent->GetNextSibling()) {
break; // The parent is not last element in its parent, stop climbing.
}
}
if (mostDistantEndParentHavingStyle) {
endPoint.SetAfter(mostDistantEndParentHavingStyle);
}
return endPoint;
}
EditorRawDOMRange HTMLEditor::AutoInlineStyleSetter::
GetExtendedRangeToMinimizeTheNumberOfNewElements(
const HTMLEditor& aHTMLEditor, const nsINode& aCommonAncestor,
EditorRawDOMPoint&& aStartPoint, EditorRawDOMPoint&& aEndPoint) const {
MOZ_ASSERT(aStartPoint.IsSet());
MOZ_ASSERT(aEndPoint.IsSet());
// For minimizing the number of new elements, we should extend the range as
// far as possible. E.g., `<span>[abc</span> <span>def]</span>` should be
// styled as `<b><span>abc</span> <span>def</span></b>`.
// Similarly, if the range crosses a block boundary, we should do same thing.
// I.e., `<p><span>[abc</span></p><p><span>def]</span></p>` should become
// `<p><b><span>abc</span></b></p><p><b><span>def</span></b></p>`.
if (aStartPoint.GetContainer() != aEndPoint.GetContainer()) {
while (aStartPoint.GetContainer() != &aCommonAncestor &&
aStartPoint.IsInContentNode() && aStartPoint.GetContainerParent() &&
aStartPoint.IsStartOfContainer()) {
if (!EditorUtils::IsEditableContent(
*aStartPoint.ContainerAs<nsIContent>(), EditorType::HTML) ||
(aStartPoint.ContainerAs<nsIContent>()->IsElement() &&
(HTMLEditUtils::IsBlockElement(
*aStartPoint.ContainerAs<Element>(),
BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
HTMLEditUtils::IsDisplayInsideFlowRoot(
*aStartPoint.ContainerAs<Element>())))) {
break;
}
aStartPoint = aStartPoint.ParentPoint();
}
while (aEndPoint.GetContainer() != &aCommonAncestor &&
aEndPoint.IsInContentNode() && aEndPoint.GetContainerParent() &&
aEndPoint.IsEndOfContainer()) {
if (!EditorUtils::IsEditableContent(*aEndPoint.ContainerAs<nsIContent>(),
EditorType::HTML) ||
(aEndPoint.ContainerAs<nsIContent>()->IsElement() &&
(HTMLEditUtils::IsBlockElement(
*aEndPoint.ContainerAs<Element>(),
BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
HTMLEditUtils::IsDisplayInsideFlowRoot(
*aEndPoint.ContainerAs<Element>())))) {
break;
}
aEndPoint.SetAfter(aEndPoint.ContainerAs<nsIContent>());
}
}
// Additionally, if we'll set a CSS style, we want to wrap elements which
// should have the new style into the range to avoid creating new <span>
// element.
if (!IsRepresentableWithHTML() ||
(aHTMLEditor.IsCSSEnabled() && IsCSSSettable(*nsGkAtoms::span))) {
// First, if pointing in a text node, use parent point.
if (aStartPoint.IsInContentNode() && aStartPoint.IsStartOfContainer() &&
aStartPoint.GetContainerParentAs<nsIContent>() &&
EditorUtils::IsEditableContent(
*aStartPoint.ContainerParentAs<nsIContent>(), EditorType::HTML) &&
(!aStartPoint.GetContainerAs<Element>() ||
!HTMLEditUtils::IsContainerNode(
*aStartPoint.ContainerAs<nsIContent>())) &&
EditorUtils::IsEditableContent(*aStartPoint.ContainerAs<nsIContent>(),
EditorType::HTML)) {
aStartPoint = aStartPoint.ParentPoint();
MOZ_ASSERT(aStartPoint.IsSet());
}
if (aEndPoint.IsInContentNode() && aEndPoint.IsEndOfContainer() &&
aEndPoint.GetContainerParentAs<nsIContent>() &&
EditorUtils::IsEditableContent(
*aEndPoint.ContainerParentAs<nsIContent>(), EditorType::HTML) &&
(!aEndPoint.GetContainerAs<Element>() ||
!HTMLEditUtils::IsContainerNode(
*aEndPoint.ContainerAs<nsIContent>())) &&
EditorUtils::IsEditableContent(*aEndPoint.ContainerAs<nsIContent>(),
EditorType::HTML)) {
aEndPoint.SetAfter(aEndPoint.GetContainer());
MOZ_ASSERT(aEndPoint.IsSet());
}
// Then, wrap the container if it's a good element to set a CSS property.
if (aStartPoint.IsInContentNode() && aStartPoint.GetContainerParent() &&
// The point must be start of the container
aStartPoint.IsStartOfContainer() &&
// only if the pointing first child node cannot have `style` attribute
(!aStartPoint.GetChildAs<nsStyledElement>() ||
!ElementIsGoodContainerToSetStyle(
*aStartPoint.ChildAs<nsStyledElement>())) &&
// but don't cross block boundary at climbing up the tree
!HTMLEditUtils::IsBlockElement(
*aStartPoint.ContainerAs<nsIContent>(),
BlockInlineCheck::UseComputedDisplayOutsideStyle) &&
// and the container is a good editable element to set CSS style
aStartPoint.GetContainerAs<nsStyledElement>() &&
ElementIsGoodContainerToSetStyle(
*aStartPoint.ContainerAs<nsStyledElement>())) {
aStartPoint = aStartPoint.ParentPoint();
MOZ_ASSERT(aStartPoint.IsSet());
}
if (aEndPoint.IsInContentNode() && aEndPoint.GetContainerParent() &&
// The point must be end of the container
aEndPoint.IsEndOfContainer() &&
// only if the pointing last child node cannot have `style` attribute
(aEndPoint.IsStartOfContainer() ||
!aEndPoint.GetPreviousSiblingOfChildAs<nsStyledElement>() ||
!ElementIsGoodContainerToSetStyle(
*aEndPoint.GetPreviousSiblingOfChildAs<nsStyledElement>())) &&
// but don't cross block boundary at climbing up the tree
!HTMLEditUtils::IsBlockElement(
*aEndPoint.ContainerAs<nsIContent>(),
BlockInlineCheck::UseComputedDisplayOutsideStyle) &&
// and the container is a good editable element to set CSS style
aEndPoint.GetContainerAs<nsStyledElement>() &&
ElementIsGoodContainerToSetStyle(
*aEndPoint.ContainerAs<nsStyledElement>())) {
aEndPoint.SetAfter(aEndPoint.GetContainer());
MOZ_ASSERT(aEndPoint.IsSet());
}
}
return EditorRawDOMRange(std::move(aStartPoint), std::move(aEndPoint));
}
Result<EditorRawDOMRange, nsresult>
HTMLEditor::AutoInlineStyleSetter::ExtendOrShrinkRangeToApplyTheStyle(
const HTMLEditor& aHTMLEditor, const EditorDOMRange& aRange,
const Element& aEditingHost) const {
if (NS_WARN_IF(!aRange.IsPositioned())) {
return Err(NS_ERROR_FAILURE);
}
// For avoiding assertion hits in the utility methods, check whether the
// range is in same subtree, first. Even if the range crosses a subtree
// boundary, it's not a bug of this module.
nsINode* commonAncestor = aRange.GetClosestCommonInclusiveAncestor();
if (NS_WARN_IF(!commonAncestor)) {
return Err(NS_ERROR_FAILURE);
}
// If the range does not select only invisible <br> element, let's extend the
// range to contain the <br> element.
EditorDOMRange range(aRange);
if (range.EndRef().IsInContentNode()) {
WSScanResult nextContentData =
WSRunScanner::ScanNextVisibleNodeOrBlockBoundary(
&aEditingHost, range.EndRef(),
BlockInlineCheck::UseComputedDisplayOutsideStyle);
if (nextContentData.ReachedInvisibleBRElement() &&
nextContentData.BRElementPtr()->GetParentElement() &&
HTMLEditUtils::IsInlineContent(
*nextContentData.BRElementPtr()->GetParentElement(),
BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
range.SetEnd(EditorDOMPoint::After(*nextContentData.BRElementPtr()));
MOZ_ASSERT(range.EndRef().IsSet());
commonAncestor = range.GetClosestCommonInclusiveAncestor();
if (NS_WARN_IF(!commonAncestor)) {
return Err(NS_ERROR_FAILURE);
}
}
}
// If the range is collapsed, we don't want to replace ancestors unless it's
// in an empty element.
if (range.Collapsed() && range.StartRef().GetContainer()->Length()) {
return EditorRawDOMRange(range);
}
// First, shrink the given range to minimize new style applied contents.
// However, we should not shrink the range into entirely selected element.
// E.g., if `abc[<i>def</i>]ghi`, shouldn't shrink it as
// `abc<i>[def]</i>ghi`.
EditorRawDOMPoint startPoint, endPoint;
if (range.Collapsed()) {
startPoint = endPoint = range.StartRef().To<EditorRawDOMPoint>();
} else {
ContentSubtreeIterator iter;
if (NS_FAILED(iter.Init(range.StartRef().ToRawRangeBoundary(),
range.EndRef().ToRawRangeBoundary()))) {
NS_WARNING("ContentSubtreeIterator::Init() failed");
return Err(NS_ERROR_FAILURE);
}
nsIContent* const firstContentEntirelyInRange =
nsIContent::FromNodeOrNull(iter.GetCurrentNode());
nsIContent* const lastContentEntirelyInRange = [&]() {
iter.Last();
return nsIContent::FromNodeOrNull(iter.GetCurrentNode());
}();
// Compute the shrunken range boundaries.
startPoint = GetShrunkenRangeStart(aHTMLEditor, range, *commonAncestor,
firstContentEntirelyInRange);
MOZ_ASSERT(startPoint.IsSet());
endPoint = GetShrunkenRangeEnd(aHTMLEditor, range, *commonAncestor,
lastContentEntirelyInRange);
MOZ_ASSERT(endPoint.IsSet());
// If shrunken range is swapped, it could like this case:
// `abc[</span><span>]def`, starts at very end of a node and ends at
// very start of immediately next node. In this case, we should use
// the original range instead.
if (MOZ_UNLIKELY(!startPoint.EqualsOrIsBefore(endPoint))) {
startPoint = range.StartRef().To<EditorRawDOMPoint>();
endPoint = range.EndRef().To<EditorRawDOMPoint>();
}
}
// Then, we may need to extend the range to wrap parent inline elements
// which specify same style since we need to remove same style elements to
// apply new value. E.g., abc
// <span style="background-color: red">
// <span style="background-color: blue">[def]</span>
// </span>
// ghi
// In this case, we need to wrap the other <span> element if setting
// background color. Then, the inner <span> element is removed and the
// other <span> element's style attribute will be updated rather than
// inserting new <span> element.
startPoint = GetExtendedRangeStartToWrapAncestorApplyingSameStyle(aHTMLEditor,
startPoint);
MOZ_ASSERT(startPoint.IsSet());
endPoint =
GetExtendedRangeEndToWrapAncestorApplyingSameStyle(aHTMLEditor, endPoint);
MOZ_ASSERT(endPoint.IsSet());
// Finally, we need to extend the range unless the range is in an element to
// reduce the number of creating new elements. E.g., if now selects
// `<span>[abc</span><span>def]</span>`, we should make it
// `<b><span>abc</span><span>def</span></b>` rather than
// `<span><b>abc</b></span><span><b>def</b></span>`.
EditorRawDOMRange finalRange =
GetExtendedRangeToMinimizeTheNumberOfNewElements(
aHTMLEditor, *commonAncestor, std::move(startPoint),
std::move(endPoint));
#if 0
fprintf(stderr,
"ExtendOrShrinkRangeToApplyTheStyle:\n"
" Result: {(\n %s\n ) - (\n %s\n )},\n"
" Input: {(\n %s\n ) - (\n %s\n )}\n",
ToString(finalRange.StartRef()).c_str(),
ToString(finalRange.EndRef()).c_str(),
ToString(aRange.StartRef()).c_str(),
ToString(aRange.EndRef()).c_str());
#endif
return finalRange;
}
Result<SplitRangeOffResult, nsresult>
HTMLEditor::SplitAncestorStyledInlineElementsAtRangeEdges(
const EditorDOMRange& aRange, const EditorInlineStyle& aStyle,
SplitAtEdges aSplitAtEdges) {
MOZ_ASSERT(IsEditActionDataAvailable());
if (NS_WARN_IF(!aRange.IsPositioned())) {
return Err(NS_ERROR_FAILURE);
}
EditorDOMRange range(aRange);
// split any matching style nodes above the start of range
auto resultAtStart =
[&]() MOZ_CAN_RUN_SCRIPT -> Result<SplitNodeResult, nsresult> {
AutoTrackDOMRange tracker(RangeUpdaterRef(), &range);
Result<SplitNodeResult, nsresult> result =
SplitAncestorStyledInlineElementsAt(range.StartRef(), aStyle,
aSplitAtEdges);
if (MOZ_UNLIKELY(result.isErr())) {
NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed");
return result;
}
tracker.FlushAndStopTracking();
if (result.inspect().Handled()) {
auto startOfRange = result.inspect().AtSplitPoint<EditorDOMPoint>();
if (!startOfRange.IsSet()) {
result.inspect().IgnoreCaretPointSuggestion();
NS_WARNING(
"HTMLEditor::SplitAncestorStyledInlineElementsAt() didn't return "
"split point");
return Err(NS_ERROR_FAILURE);
}
range.SetStart(std::move(startOfRange));
} else if (MOZ_UNLIKELY(!range.IsPositioned())) {
NS_WARNING(
"HTMLEditor::SplitAncestorStyledInlineElementsAt() caused unexpected "
"DOM tree");
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
return result;
}();
if (MOZ_UNLIKELY(resultAtStart.isErr())) {
return resultAtStart.propagateErr();
}
SplitNodeResult unwrappedResultAtStart = resultAtStart.unwrap();
// second verse, same as the first...
auto resultAtEnd =
[&]() MOZ_CAN_RUN_SCRIPT -> Result<SplitNodeResult, nsresult> {
AutoTrackDOMRange tracker(RangeUpdaterRef(), &range);
Result<SplitNodeResult, nsresult> result =
SplitAncestorStyledInlineElementsAt(range.EndRef(), aStyle,
aSplitAtEdges);
if (MOZ_UNLIKELY(result.isErr())) {
NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed");
return result;
}
tracker.FlushAndStopTracking();
if (result.inspect().Handled()) {
auto endOfRange = result.inspect().AtSplitPoint<EditorDOMPoint>();
if (!endOfRange.IsSet()) {
result.inspect().IgnoreCaretPointSuggestion();
NS_WARNING(
"HTMLEditor::SplitAncestorStyledInlineElementsAt() didn't return "
"split point");
return Err(NS_ERROR_FAILURE);
}
range.SetEnd(std::move(endOfRange));
} else if (MOZ_UNLIKELY(!range.IsPositioned())) {
NS_WARNING(
"HTMLEditor::SplitAncestorStyledInlineElementsAt() caused unexpected "
"DOM tree");
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
}
return result;
}();
if (MOZ_UNLIKELY(resultAtEnd.isErr())) {
unwrappedResultAtStart.IgnoreCaretPointSuggestion();
return resultAtEnd.propagateErr();
}
return SplitRangeOffResult(std::move(range),
std::move(unwrappedResultAtStart),
resultAtEnd.unwrap());
}
Result<SplitNodeResult, nsresult>
HTMLEditor::SplitAncestorStyledInlineElementsAt(
const EditorDOMPoint& aPointToSplit, const EditorInlineStyle& aStyle,
SplitAtEdges aSplitAtEdges) {
// If the point is in a non-content node, e.g., in the document node, we
// should split nothing.
if (MOZ_UNLIKELY(!aPointToSplit.IsInContentNode())) {
return SplitNodeResult::NotHandled(aPointToSplit);
}
// We assume that this method is called only when we're removing style(s).
// Even if we're in HTML mode and there is no presentation element in the
// block, we may need to overwrite the block's style with `<span>` element
// and CSS. For example, `<h1>` element has `font-weight: bold;` as its
// default style. If `Document.execCommand("bold")` is called for its
// text, we should make it unbold. Therefore, we shouldn't check
// IsCSSEnabled() in most cases. However, there is an exception.
// FontFaceStateCommand::SetState() calls RemoveInlinePropertyAsAction()
// with nsGkAtoms::tt before calling SetInlinePropertyAsAction() if we
// are handling a XUL command. Only in that case, we need to check
// IsCSSEnabled().
const bool handleCSS =
aStyle.mHTMLProperty != nsGkAtoms::tt || IsCSSEnabled();
AutoTArray<OwningNonNull<Element>, 24> arrayOfParents;
for (Element* element :
aPointToSplit.GetContainer()->InclusiveAncestorsOfType<Element>()) {
if (element->IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::head,
nsGkAtoms::html) ||
HTMLEditUtils::IsBlockElement(
*element, BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
!element->GetParent() ||
!EditorUtils::IsEditableContent(*element->GetParent(),
EditorType::HTML) ||
NS_WARN_IF(!HTMLEditUtils::IsSplittableNode(*element))) {
break;
}
arrayOfParents.AppendElement(*element);
}
// Split any matching style nodes above the point.
SplitNodeResult result = SplitNodeResult::NotHandled(aPointToSplit);
MOZ_ASSERT(!result.Handled());
EditorDOMPoint pointToPutCaret;
for (OwningNonNull<Element>& element : arrayOfParents) {
auto isSetByCSSOrError = [&]() -> Result<bool, nsresult> {
if (!handleCSS) {
return false;
}
// The HTML style defined by aStyle has a CSS equivalence in this
// implementation for the node; let's check if it carries those CSS
// styles
if (aStyle.IsCSSRemovable(*element)) {
nsAutoString firstValue;
Result<bool, nsresult> isSpecifiedByCSSOrError =
CSSEditUtils::IsSpecifiedCSSEquivalentTo(*this, *element, aStyle,
firstValue);
if (MOZ_UNLIKELY(isSpecifiedByCSSOrError.isErr())) {
result.IgnoreCaretPointSuggestion();
NS_WARNING("CSSEditUtils::IsSpecifiedCSSEquivalentTo() failed");
return isSpecifiedByCSSOrError;
}
if (isSpecifiedByCSSOrError.unwrap()) {
return true;
}
}
// If this is <sub> or <sup>, we won't use vertical-align CSS property
// because <sub>/<sup> changes font size but neither `vertical-align:
// sub` nor `vertical-align: super` changes it (bug 394304 comment 2).
// Therefore, they are not equivalents. However, they're obviously
// conflict with vertical-align style. Thus, we need to remove ancestor
// elements having vertical-align style.
if (aStyle.IsStyleConflictingWithVerticalAlign()) {
nsAutoString value;
nsresult rv = CSSEditUtils::GetSpecifiedProperty(
*element, *nsGkAtoms::vertical_align, value);
if (NS_FAILED(rv)) {
NS_WARNING("CSSEditUtils::GetSpecifiedProperty() failed");
result.IgnoreCaretPointSuggestion();
return Err(rv);
}
if (!value.IsEmpty()) {
return true;
}
}
return false;
}();
if (MOZ_UNLIKELY(isSetByCSSOrError.isErr())) {
return isSetByCSSOrError.propagateErr();
}
if (!isSetByCSSOrError.inspect()) {
if (!aStyle.IsStyleToClearAllInlineStyles()) {
// If we're removing a link style and the element is an <a href>, we
// need to split it.
if (aStyle.mHTMLProperty == nsGkAtoms::href &&
HTMLEditUtils::IsLink(element)) {
}
// If we're removing HTML style, we should split only the element
// which represents the style.
else if (!element->IsHTMLElement(aStyle.mHTMLProperty) ||
(aStyle.mAttribute && !element->HasAttr(aStyle.mAttribute))) {
continue;
}
// If we're setting <font> related styles, it means that we're not
// toggling the style. In this case, we need to remove parent <font>
// elements and/or update parent <font> elements if there are some
// elements which have the attribute. However, we should not touch if
// the value is same as what the caller setting to keep the DOM tree
// as-is as far as possible.
if (aStyle.IsStyleOfFontElement() && aStyle.MaybeHasValue()) {
const nsAttrValue* const attrValue =
element->GetParsedAttr(aStyle.mAttribute);
if (attrValue) {
if (aStyle.mAttribute == nsGkAtoms::size) {
if (nsContentUtils::ParseLegacyFontSize(
aStyle.AsInlineStyleAndValue().mAttributeValue) ==
attrValue->GetIntegerValue()) {
continue;
}
} else if (aStyle.mAttribute == nsGkAtoms::color) {
nsAttrValue newValue;
nscolor oldColor, newColor;
if (attrValue->GetColorValue(oldColor) &&
newValue.ParseColor(
aStyle.AsInlineStyleAndValue().mAttributeValue) &&
newValue.GetColorValue(newColor) && oldColor == newColor) {
continue;
}
} else if (attrValue->Equals(
aStyle.AsInlineStyleAndValue().mAttributeValue,
eIgnoreCase)) {
continue;
}
}
}
}
// If aProperty is nullptr, we need to split any style.
else if (!EditorUtils::IsEditableContent(element, EditorType::HTML) ||
!HTMLEditUtils::IsRemovableInlineStyleElement(*element)) {
continue;
}
}
// Found a style node we need to split.
// XXX If first content is a text node and CSS is enabled, we call this
// with text node but in such case, this does nothing, but returns
// as handled with setting only previous or next node. If its parent
// is a block, we do nothing but return as handled.
AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), &pointToPutCaret);
Result<SplitNodeResult, nsresult> splitNodeResult =
SplitNodeDeepWithTransaction(MOZ_KnownLive(element),
result.AtSplitPoint<EditorDOMPoint>(),
aSplitAtEdges);
if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
NS_WARNING("HTMLEditor::SplitNodeDeepWithTransaction() failed");
return splitNodeResult;
}
SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap();
trackPointToPutCaret.FlushAndStopTracking();
unwrappedSplitNodeResult.MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
// If it's not handled, it means that `content` is not a splittable node
// like a void element even if it has some children, and the split point
// is middle of it.
if (!unwrappedSplitNodeResult.Handled()) {
continue;
}
// Respect the last split result which actually did it.
if (!result.DidSplit() || unwrappedSplitNodeResult.DidSplit()) {
result = unwrappedSplitNodeResult.ToHandledResult();
}
MOZ_ASSERT(result.Handled());
}
return pointToPutCaret.IsSet()
? SplitNodeResult(std::move(result), std::move(pointToPutCaret))
: std::move(result);
}
Result<EditorDOMPoint, nsresult> HTMLEditor::ClearStyleAt(
const EditorDOMPoint& aPoint, const EditorInlineStyle& aStyleToRemove,
SpecifiedStyle aSpecifiedStyle) {
MOZ_ASSERT(IsEditActionDataAvailable());
if (NS_WARN_IF(!aPoint.IsSet())) {
return Err(NS_ERROR_INVALID_ARG);
}
// TODO: We should rewrite this to stop unnecessary element creation and
// deleting it later because it causes the original element may be
// removed from the DOM tree even if same element is still in the
// DOM tree from point of view of users.
// First, split inline elements at the point.
// E.g., if aStyleToRemove.mHTMLProperty is nsGkAtoms::b and
// `<p><b><i>a[]bc</i></b></p>`, we want to make it as
// `<p><b><i>a</i></b><b><i>bc</i></b></p>`.
EditorDOMPoint pointToPutCaret(aPoint);
AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), &pointToPutCaret);
Result<SplitNodeResult, nsresult> splitNodeResult =
SplitAncestorStyledInlineElementsAt(
aPoint, aStyleToRemove, SplitAtEdges::eAllowToCreateEmptyContainer);
if (MOZ_UNLIKELY(splitNodeResult.isErr())) {
NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed");
return splitNodeResult.propagateErr();
}
trackPointToPutCaret.FlushAndStopTracking();
SplitNodeResult unwrappedSplitNodeResult = splitNodeResult.unwrap();
unwrappedSplitNodeResult.MoveCaretPointTo(
pointToPutCaret, *this,
{SuggestCaret::OnlyIfHasSuggestion,
SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
// If there is no styled inline elements of aStyleToRemove, we just return the
// given point.
// E.g., `<p><i>a[]bc</i></p>` for nsGkAtoms::b.
if (!unwrappedSplitNodeResult.Handled()) {
return pointToPutCaret;
}
// If it did split nodes, but topmost ancestor inline element is split
// at start of it, we don't need the empty inline element. Let's remove
// it now. Then, we'll get the following DOM tree if there is no "a" in the
// above case:
// <p><b><i>bc</i></b></p>
// ^^
if (unwrappedSplitNodeResult.GetPreviousContent() &&
HTMLEditUtils::IsEmptyNode(
*unwrappedSplitNodeResult.GetPreviousContent(),
{EmptyCheckOption::TreatSingleBRElementAsVisible,
EmptyCheckOption::TreatListItemAsVisible,
EmptyCheckOption::TreatTableCellAsVisible})) {
AutoTrackDOMPoint trackPointToPutCaret(RangeUpdaterRef(), &pointToPutCaret);
// Delete previous node if it's empty.
// MOZ_KnownLive(unwrappedSplitNodeResult.GetPreviousContent()):
// It's grabbed by unwrappedSplitNodeResult.
nsresult rv = DeleteNodeWithTransaction(
MOZ_KnownLive(*unwrappedSplitNodeResult.GetPreviousContent()));
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
return Err(rv);
}
}
// If we reached block from end of a text node, we can do nothing here.
// E.g., `<p style="font-weight: bold;">a[]bc</p>` for nsGkAtoms::b and
// we're in CSS mode.
// XXX Chrome resets block style and creates `<span>` elements for each
// line in this case.
if (!unwrappedSplitNodeResult.GetNextContent()) {
return pointToPutCaret;
}
// Otherwise, the next node is topmost ancestor inline element which has
// the style. We want to put caret between the split nodes, but we need
// to keep other styles. Therefore, next, we need to split at start of
// the next node. The first example should become
// `<p><b><i>a</i></b><b><i></i></b><b><i>bc</i></b></p>`.
// ^^^^^^^^^^^^^^
nsIContent* firstLeafChildOfNextNode = HTMLEditUtils::GetFirstLeafContent(
*unwrappedSplitNodeResult.GetNextContent(), {LeafNodeType::OnlyLeafNode});
EditorDOMPoint atStartOfNextNode(
firstLeafChildOfNextNode ? firstLeafChildOfNextNode
: unwrappedSplitNodeResult.GetNextContent(),
0);
RefPtr<HTMLBRElement> brElement;
// But don't try to split non-containers like `<br>`, `<hr>` and `<img>`
// element.
if (!atStartOfNextNode.IsInContentNode() ||
!HTMLEditUtils::IsContainerNode(
*atStartOfNextNode.ContainerAs<nsIContent>())) {
// If it's a `<br>` element, let's move it into new node later.
brElement = HTMLBRElement::FromNode(atStartOfNextNode.GetContainer());
if (!atStartOfNextNode.GetContainerParentAs<nsIContent>()) {
NS_WARNING("atStartOfNextNode was in an orphan node");
return Err(NS_ERROR_FAILURE);
}
atStartOfNextNode.Set(atStartOfNextNode.GetContainerParent(), 0);
}
AutoTrackDOMPoint trackPointToPutCaret2(RangeUpdaterRef(), &pointToPutCaret);
Result<SplitNodeResult, nsresult> splitResultAtStartOfNextNode =
SplitAncestorStyledInlineElementsAt(
atStartOfNextNode, aStyleToRemove,
SplitAtEdges::eAllowToCreateEmptyContainer);
if (MOZ_UNLIKELY(splitResultAtStartOfNextNode.isErr())) {
NS_WARNING("HTMLEditor::SplitAncestorStyledInlineElementsAt() failed");
return splitResultAtStartOfNextNode.propagateErr();
}
trackPointToPutCaret2.FlushAndStopTracking();
SplitNodeResult unwrappedSplitResultAtStartOfNextNode =
splitResultAtStartOfNextNode.unwrap();
unwrappedSplitResultAtStartOfNextNode.MoveCaretPointTo(
pointToPutCaret, *this,
{SuggestCaret::OnlyIfHasSuggestion,
SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
if (unwrappedSplitResultAtStartOfNextNode.Handled() &&
unwrappedSplitResultAtStartOfNextNode.GetNextContent()) {
// If the right inline elements are empty, we should remove them. E.g.,
// if the split point is at end of a text node (or end of an inline
// element), e.g., <div><b><i>abc[]</i></b></div>, then now, it's been
// changed to:
// <div><b><i>abc</i></b><b><i>[]</i></b><b><i></i></b></div>
// ^^^^^^^^^^^^^^
// We will change it to:
// <div><b><i>abc</i></b><b><i>[]</i></b></div>
// ^^
// And if it has only padding <br> element, we should move it into the
// previous <i> which will have new content.
bool seenBR = false;
if (HTMLEditUtils::IsEmptyNode(
*unwrappedSplitResultAtStartOfNextNode.GetNextContent(),
{EmptyCheckOption::TreatListItemAsVisible,
EmptyCheckOption::TreatTableCellAsVisible},
&seenBR)) {
if (seenBR && !brElement) {
brElement = HTMLEditUtils::GetFirstBRElement(
*unwrappedSplitResultAtStartOfNextNode.GetNextContentAs<Element>());
}
// Once we remove <br> element's parent, we lose the rights to remove it
// from the parent because the parent becomes not editable. Therefore, we
// need to delete the <br> element before removing its parents for reusing
// it later.
if (brElement) {
nsresult rv = DeleteNodeWithTransaction(*brElement);
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
return Err(rv);
}
}
// Delete next node if it's empty.
// MOZ_KnownLive because of grabbed by
// unwrappedSplitResultAtStartOfNextNode.
nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(
*unwrappedSplitResultAtStartOfNextNode.GetNextContent()));
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
return Err(rv);
}
}
}
if (!unwrappedSplitResultAtStartOfNextNode.Handled()) {
return std::move(pointToPutCaret);
}
// If there is no content, we should return here.
// XXX Is this possible case without mutation event listener?
if (!unwrappedSplitResultAtStartOfNextNode.GetPreviousContent()) {
// XXX This is really odd, but we return this value...
const auto splitPoint =
unwrappedSplitNodeResult.AtSplitPoint<EditorRawDOMPoint>();
const auto splitPointAtStartOfNextNode =
unwrappedSplitResultAtStartOfNextNode.AtSplitPoint<EditorRawDOMPoint>();
return EditorDOMPoint(splitPoint.GetContainer(),
splitPointAtStartOfNextNode.Offset());
}
// Now, we want to put `<br>` element into the empty split node if
// it was in next node of the first split.
// E.g., `<p><b><i>a</i></b><b><i><br></i></b><b><i>bc</i></b></p>`
nsIContent* firstLeafChildOfPreviousNode = HTMLEditUtils::GetFirstLeafContent(
*unwrappedSplitResultAtStartOfNextNode.GetPreviousContent(),
{LeafNodeType::OnlyLeafNode});
pointToPutCaret.Set(
firstLeafChildOfPreviousNode
? firstLeafChildOfPreviousNode
: unwrappedSplitResultAtStartOfNextNode.GetPreviousContent(),
0);
// If the right node starts with a `<br>`, suck it out of right node and into
// the left node left node. This is so we you don't revert back to the
// previous style if you happen to click at the end of a line.
if (brElement) {
if (brElement->GetParentNode()) {
Result<MoveNodeResult, nsresult> moveBRElementResult =
MoveNodeWithTransaction(*brElement, pointToPutCaret);
if (MOZ_UNLIKELY(moveBRElementResult.isErr())) {
NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
return moveBRElementResult.propagateErr();
}
moveBRElementResult.unwrap().MoveCaretPointTo(
pointToPutCaret, *this,
{SuggestCaret::OnlyIfHasSuggestion,
SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
} else {
Result<CreateElementResult, nsresult> insertBRElementResult =
InsertNodeWithTransaction<Element>(*brElement, pointToPutCaret);
if (MOZ_UNLIKELY(insertBRElementResult.isErr())) {
NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
return insertBRElementResult.propagateErr();
}
insertBRElementResult.unwrap().MoveCaretPointTo(
pointToPutCaret, *this,
{SuggestCaret::OnlyIfHasSuggestion,
SuggestCaret::OnlyIfTransactionsAllowedToDoIt});
}
if (unwrappedSplitResultAtStartOfNextNode.GetNextContent() &&
unwrappedSplitResultAtStartOfNextNode.GetNextContent()
->IsInComposedDoc()) {
// If we split inline elements at immediately before <br> element which is
// the last visible content in the right element, we don't need the right
// element anymore. Otherwise, we'll create the following DOM tree:
// - <b>abc</b>{}<br><b></b>
// ^^^^^^^
// - <b><i>abc</i></b><i><br></i><b></b>
// ^^^^^^^
if (HTMLEditUtils::IsEmptyNode(
*unwrappedSplitResultAtStartOfNextNode.GetNextContent(),
{EmptyCheckOption::TreatSingleBRElementAsVisible,
EmptyCheckOption::TreatListItemAsVisible,
EmptyCheckOption::TreatTableCellAsVisible})) {
// MOZ_KnownLive because the result is grabbed by
// unwrappedSplitResultAtStartOfNextNode.
nsresult rv = DeleteNodeWithTransaction(MOZ_KnownLive(
*unwrappedSplitResultAtStartOfNextNode.GetNextContent()));
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
return Err(rv);
}
}
// If the next content has only one <br> element, there may be empty
// inline elements around it. We don't need them anymore because user
// cannot put caret into them. E.g., <b><i>abc[]<br></i><br></b> has
// been changed to <b><i>abc</i></b><i>{}<br></i><b><i></i><br></b> now.
// ^^^^^^^^^^^^^^^^^^
// We don't need the empty <i>.
else if (HTMLEditUtils::IsEmptyNode(
*unwrappedSplitResultAtStartOfNextNode.GetNextContent(),
{EmptyCheckOption::TreatListItemAsVisible,
EmptyCheckOption::TreatTableCellAsVisible})) {
AutoTArray<OwningNonNull<nsIContent>, 4> emptyInlineContainerElements;
HTMLEditUtils::CollectEmptyInlineContainerDescendants(
*unwrappedSplitResultAtStartOfNextNode.GetNextContentAs<Element>(),
emptyInlineContainerElements,
{EmptyCheckOption::TreatSingleBRElementAsVisible,
EmptyCheckOption::TreatListItemAsVisible,
EmptyCheckOption::TreatTableCellAsVisible},
BlockInlineCheck::UseComputedDisplayOutsideStyle);
for (const OwningNonNull<nsIContent>& emptyInlineContainerElement :
emptyInlineContainerElements) {
// MOZ_KnownLive(emptyInlineContainerElement) due to bug 1622253.
nsresult rv = DeleteNodeWithTransaction(
MOZ_KnownLive(emptyInlineContainerElement));
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::DeleteNodeWithTransaction() failed");
return Err(rv);
}
}
}
}
// Update the child.
pointToPutCaret.Set(pointToPutCaret.GetContainer(), 0);
}
// Finally, remove the specified style in the previous node at the
// second split and tells good insertion point to the caller. I.e., we
// want to make the first example as:
// `<p><b><i>a</i></b><i>[]</i><b><i>bc</i></b></p>`
// ^^^^^^^^^
if (auto* const previousElementOfSplitPoint =
unwrappedSplitResultAtStartOfNextNode
.GetPreviousContentAs<Element>()) {
// Track the point at the new hierarchy. This is so we can know where
// to put the selection after we call RemoveStyleInside().
// RemoveStyleInside() could remove any and all of those nodes, so I
// have to use the range tracking system to find the right spot to put
// selection.
AutoTrackDOMPoint tracker(RangeUpdaterRef(), &pointToPutCaret);
// MOZ_KnownLive(previousElementOfSplitPoint):
// It's grabbed by unwrappedSplitResultAtStartOfNextNode.
Result<EditorDOMPoint, nsresult> removeStyleResult =
RemoveStyleInside(MOZ_KnownLive(*previousElementOfSplitPoint),
aStyleToRemove, aSpecifiedStyle);
if (MOZ_UNLIKELY(removeStyleResult.isErr())) {
NS_WARNING("HTMLEditor::RemoveStyleInside() failed");
return removeStyleResult;
}
// We've already computed a suggested caret position at start of first leaf
// which is stored in pointToPutCaret, so we don't need to update it here.
}
return pointToPutCaret;
}
Result<EditorDOMPoint, nsresult> HTMLEditor::RemoveStyleInside(
Element& aElement, const EditorInlineStyle& aStyleToRemove,
SpecifiedStyle aSpecifiedStyle) {
// First, handle all descendants.
AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfChildContents;
HTMLEditUtils::CollectAllChildren(aElement, arrayOfChildContents);
EditorDOMPoint pointToPutCaret;
for (const OwningNonNull<nsIContent>& child : arrayOfChildContents) {
if (!child->IsElement()) {
continue;
}
Result<EditorDOMPoint, nsresult> removeStyleResult = RemoveStyleInside(
MOZ_KnownLive(*child->AsElement()), aStyleToRemove, aSpecifiedStyle);
if (MOZ_UNLIKELY(removeStyleResult.isErr())) {
NS_WARNING("HTMLEditor::RemoveStyleInside() failed");
return removeStyleResult;
}
if (removeStyleResult.inspect().IsSet()) {
pointToPutCaret = removeStyleResult.unwrap();
}
}
// TODO: It seems that if aElement is not editable, we should insert new
// container to remove the style if possible.
if (!EditorUtils::IsEditableContent(aElement, EditorType::HTML)) {
return pointToPutCaret;
}
// Next, remove CSS style first. Then, `style` attribute will be removed if
// the corresponding CSS property is last one.
auto isStyleSpecifiedOrError = [&]() -> Result<bool, nsresult> {
if (!aStyleToRemove.IsCSSRemovable(aElement)) {
return false;
}
MOZ_ASSERT(!aStyleToRemove.IsStyleToClearAllInlineStyles());
Result<bool, nsresult> elementHasSpecifiedCSSEquivalentStylesOrError =
CSSEditUtils::HaveSpecifiedCSSEquivalentStyles(*this, aElement,
aStyleToRemove);
NS_WARNING_ASSERTION(
elementHasSpecifiedCSSEquivalentStylesOrError.isOk(),
"CSSEditUtils::HaveSpecifiedCSSEquivalentStyles() failed");
return elementHasSpecifiedCSSEquivalentStylesOrError;
}();
if (MOZ_UNLIKELY(isStyleSpecifiedOrError.isErr())) {
return isStyleSpecifiedOrError.propagateErr();
}
bool styleSpecified = isStyleSpecifiedOrError.unwrap();
if (nsStyledElement* styledElement = nsStyledElement::FromNode(&aElement)) {
if (styleSpecified) {
// MOZ_KnownLive(*styledElement) because it's an alias of aElement.
nsresult rv = CSSEditUtils::RemoveCSSEquivalentToStyle(
WithTransaction::Yes, *this, MOZ_KnownLive(*styledElement),
aStyleToRemove, nullptr);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return Err(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"CSSEditUtils::RemoveCSSEquivalentToStyle() failed, but ignored");
}
// If the style is <sub> or <sup>, we won't use vertical-align CSS
// property because <sub>/<sup> changes font size but neither
// `vertical-align: sub` nor `vertical-align: super` changes it
// (bug 394304 comment 2). Therefore, they are not equivalents. However,
// they're obviously conflict with vertical-align style. Thus, we need to
// remove the vertical-align style from elements.
if (aStyleToRemove.IsStyleConflictingWithVerticalAlign()) {
nsAutoString value;
nsresult rv = CSSEditUtils::GetSpecifiedProperty(
aElement, *nsGkAtoms::vertical_align, value);
if (NS_FAILED(rv)) {
NS_WARNING("CSSEditUtils::GetSpecifiedProperty() failed");
return Err(rv);
}
if (!value.IsEmpty()) {
// MOZ_KnownLive(*styledElement) because it's an alias of aElement.
nsresult rv = CSSEditUtils::RemoveCSSPropertyWithTransaction(
*this, MOZ_KnownLive(*styledElement), *nsGkAtoms::vertical_align,
value);
if (NS_FAILED(rv)) {
NS_WARNING("CSSEditUtils::RemoveCSSPropertyWithTransaction() failed");
return Err(rv);
}
styleSpecified = true;
}
}
}
// Then, if we could and should remove or replace aElement, let's do it. Or
// just remove attribute.
const bool isStyleRepresentedByElement =
!aStyleToRemove.IsStyleToClearAllInlineStyles() &&
aStyleToRemove.IsRepresentedBy(aElement);
auto ShouldUpdateDOMTree = [&]() {
// If we're removing any inline styles and aElement is an inline style
// element, we can remove or replace it.
if (aStyleToRemove.IsStyleToClearAllInlineStyles() &&
HTMLEditUtils::IsRemovableInlineStyleElement(aElement)) {
return true;
}
// If we're a specific style and aElement represents it, we can remove or
// replace the element or remove the corresponding attribute.
if (isStyleRepresentedByElement) {
return true;
}
// If we've removed a CSS style from the `style` attribute of aElement, we
// could remove the element.
return aElement.IsHTMLElement(nsGkAtoms::span) && styleSpecified;
};
if (!ShouldUpdateDOMTree()) {
return pointToPutCaret;
}
const bool elementHasNecessaryAttributes = [&]() {
// If we're not removing nor replacing aElement itself, we don't need to
// take care of its `style` and `class` attributes even if aSpecifiedStyle
// is `Discard` because aSpecifiedStyle is not intended to be used in this
// case.
if (!isStyleRepresentedByElement) {
return HTMLEditUtils::ElementHasAttributeExcept(aElement,
*nsGkAtoms::_empty);
}
// If we're removing links, we don't need to keep <a> even if it has some
// specific attributes because it cannot be nested. However, if and only if
// it has `style` attribute and aSpecifiedStyle is not `Discard`, we need to
// replace it with new <span> to keep the style.
if (aStyleToRemove.IsStyleOfAnchorElement()) {
return aSpecifiedStyle == SpecifiedStyle::Preserve &&
(aElement.HasNonEmptyAttr(nsGkAtoms::style) ||
aElement.HasNonEmptyAttr(nsGkAtoms::_class));
}
nsAtom& attrKeepStaying = aStyleToRemove.mAttribute
? *aStyleToRemove.mAttribute
: *nsGkAtoms::_empty;
return aSpecifiedStyle == SpecifiedStyle::Preserve
// If we're try to remove the element but the caller wants to
// preserve the style, check whether aElement has attributes
// except the removing attribute since `style` and `class` should
// keep existing to preserve the style.
? HTMLEditUtils::ElementHasAttributeExcept(aElement,
attrKeepStaying)
// If we're try to remove the element and the caller wants to
// discard the style specified to the element, check whether
// aElement has attributes except the removing attribute, `style`
// and `class` since we don't want to keep these attributes.
: HTMLEditUtils::ElementHasAttributeExcept(
aElement, attrKeepStaying, *nsGkAtoms::style,
*nsGkAtoms::_class);
}();
// If the element is not a <span> and still has some attributes, we should
// replace it with new <span>.
auto ReplaceWithNewSpan = [&]() {
if (aStyleToRemove.IsStyleToClearAllInlineStyles()) {
return false; // Remove it even if it has attributes.
}
if (aElement.IsHTMLElement(nsGkAtoms::span)) {
return false; // Don't replace <span> with new <span>.
}
if (!isStyleRepresentedByElement) {
return false; // Keep non-related element as-is.
}
if (!elementHasNecessaryAttributes) {
return false; // Should remove it instead of replacing it.
}
if (aElement.IsHTMLElement(nsGkAtoms::font)) {
// Replace <font> if it won't have its specific attributes.
return (aStyleToRemove.mHTMLProperty == nsGkAtoms::color ||
!aElement.HasAttr(nsGkAtoms::color)) &&
(aStyleToRemove.mHTMLProperty == nsGkAtoms::face ||
!aElement.HasAttr(nsGkAtoms::face)) &&
(aStyleToRemove.mHTMLProperty == nsGkAtoms::size ||
!aElement.HasAttr(nsGkAtoms::size));
}
// The styled element has only global attributes, let's replace it with new
// <span> with cloning the attributes.
return true;
};
if (ReplaceWithNewSpan()) {
// Before cloning the attribute to new element, let's remove it.
if (aStyleToRemove.mAttribute) {
nsresult rv =
RemoveAttributeWithTransaction(aElement, *aStyleToRemove.mAttribute);
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::RemoveAttributeWithTransaction() failed");
return Err(rv);
}
}
if (aSpecifiedStyle == SpecifiedStyle::Discard) {
nsresult rv = RemoveAttributeWithTransaction(aElement, *nsGkAtoms::style);
if (NS_FAILED(rv)) {
NS_WARNING(
"EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::style) "
"failed");
return Err(rv);
}
rv = RemoveAttributeWithTransaction(aElement, *nsGkAtoms::_class);
if (NS_FAILED(rv)) {
NS_WARNING(
"EditorBase::RemoveAttributeWithTransaction(nsGkAtoms::_class) "
"failed");
return Err(rv);
}
}
// Move `style` attribute and `class` element to span element before
// removing aElement from the tree.
auto replaceWithSpanResult =
[&]() MOZ_CAN_RUN_SCRIPT -> Result<CreateElementResult, nsresult> {
if (!aStyleToRemove.IsStyleOfAnchorElement()) {
return ReplaceContainerAndCloneAttributesWithTransaction(
aElement, *nsGkAtoms::span);
}
nsString styleValue; // Use nsString to avoid copying the buffer at
// setting the attribute.
aElement.GetAttr(nsGkAtoms::style, styleValue);
return ReplaceContainerWithTransaction(aElement, *nsGkAtoms::span,
*nsGkAtoms::style, styleValue);
}();
if (MOZ_UNLIKELY(replaceWithSpanResult.isErr())) {
NS_WARNING(
"HTMLEditor::ReplaceContainerWithTransaction(nsGkAtoms::span) "
"failed");
return replaceWithSpanResult.propagateErr();
}
CreateElementResult unwrappedReplaceWithSpanResult =
replaceWithSpanResult.unwrap();
if (AllowsTransactionsToChangeSelection()) {
unwrappedReplaceWithSpanResult.MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
} else {
unwrappedReplaceWithSpanResult.IgnoreCaretPointSuggestion();
}
return pointToPutCaret;
}
auto RemoveElement = [&]() {
if (aStyleToRemove.IsStyleToClearAllInlineStyles()) {
MOZ_ASSERT(HTMLEditUtils::IsRemovableInlineStyleElement(aElement));
return true;
}
// If the element still has some attributes, we should not remove it to keep
// current presentation and/or semantics.
if (elementHasNecessaryAttributes) {
return false;
}
// If the style is represented by the element, let's remove it.
if (isStyleRepresentedByElement) {
return true;
}
// If we've removed a CSS style and that made the <span> element have no
// attributes, we can delete it.
if (styleSpecified && aElement.IsHTMLElement(nsGkAtoms::span)) {
return true;
}
return false;
};
if (RemoveElement()) {
Result<EditorDOMPoint, nsresult> unwrapElementResult =
RemoveContainerWithTransaction(aElement);
if (MOZ_UNLIKELY(unwrapElementResult.isErr())) {
NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed");
return unwrapElementResult.propagateErr();
}
if (AllowsTransactionsToChangeSelection() &&
unwrapElementResult.inspect().IsSet()) {
pointToPutCaret = unwrapElementResult.unwrap();
}
return pointToPutCaret;
}
// If the element needs to keep having some attributes, just remove the
// attribute. Note that we don't need to remove `style` attribute here when
// aSpecifiedStyle is `Discard` because we've already removed unnecessary
// CSS style above.
if (isStyleRepresentedByElement && aStyleToRemove.mAttribute) {
nsresult rv =
RemoveAttributeWithTransaction(aElement, *aStyleToRemove.mAttribute);
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::RemoveAttributeWithTransaction() failed");
return Err(rv);
}
}
return pointToPutCaret;
}
EditorRawDOMRange HTMLEditor::GetExtendedRangeWrappingNamedAnchor(
const EditorRawDOMRange& aRange) const {
MOZ_ASSERT(aRange.StartRef().IsSet());
MOZ_ASSERT(aRange.EndRef().IsSet());
// FYI: We don't want to stop at ancestor block boundaries to extend the range
// because <a name> can have block elements with low level DOM API. We want
// to remove any <a name> ancestors to remove the style.
EditorRawDOMRange newRange(aRange);
for (Element* element :
aRange.StartRef().GetContainer()->InclusiveAncestorsOfType<Element>()) {
if (!HTMLEditUtils::IsNamedAnchor(element)) {
continue;
}
newRange.SetStart(EditorRawDOMPoint(element));
}
for (Element* element :
aRange.EndRef().GetContainer()->InclusiveAncestorsOfType<Element>()) {
if (!HTMLEditUtils::IsNamedAnchor(element)) {
continue;
}
newRange.SetEnd(EditorRawDOMPoint::After(*element));
}
return newRange;
}
EditorRawDOMRange HTMLEditor::GetExtendedRangeWrappingEntirelySelectedElements(
const EditorRawDOMRange& aRange) const {
MOZ_ASSERT(aRange.StartRef().IsSet());
MOZ_ASSERT(aRange.EndRef().IsSet());
// FYI: We don't want to stop at ancestor block boundaries to extend the range
// because the style may come from inline parents of block elements which may
// occur in invalid DOM tree. We want to split any (even invalid) ancestors
// at removing the styles.
EditorRawDOMRange newRange(aRange);
while (newRange.StartRef().IsInContentNode() &&
newRange.StartRef().IsStartOfContainer()) {
if (!EditorUtils::IsEditableContent(
*newRange.StartRef().ContainerAs<nsIContent>(), EditorType::HTML)) {
break;
}
newRange.SetStart(newRange.StartRef().ParentPoint());
}
while (newRange.EndRef().IsInContentNode() &&
newRange.EndRef().IsEndOfContainer()) {
if (!EditorUtils::IsEditableContent(
*newRange.EndRef().ContainerAs<nsIContent>(), EditorType::HTML)) {
break;
}
newRange.SetEnd(
EditorRawDOMPoint::After(*newRange.EndRef().ContainerAs<nsIContent>()));
}
return newRange;
}
nsresult HTMLEditor::GetInlinePropertyBase(const EditorInlineStyle& aStyle,
const nsAString* aValue,
bool* aFirst, bool* aAny, bool* aAll,
nsAString* outValue) const {
MOZ_ASSERT(!aStyle.IsStyleToClearAllInlineStyles());
MOZ_ASSERT(IsEditActionDataAvailable());
*aAny = false;
*aAll = true;
*aFirst = false;
bool first = true;
const bool isCollapsed = SelectionRef().IsCollapsed();
RefPtr<nsRange> range = SelectionRef().GetRangeAt(0);
// XXX: Should be a while loop, to get each separate range
// XXX: ERROR_HANDLING can currentItem be null?
if (range) {
// For each range, set a flag
bool firstNodeInRange = true;
if (isCollapsed) {
if (NS_WARN_IF(!range->GetStartContainer())) {
return NS_ERROR_FAILURE;
}
nsString tOutString;
const PendingStyleState styleState = [&]() {
if (aStyle.mAttribute) {
auto state = mPendingStylesToApplyToNewContent->GetStyleState(
*aStyle.mHTMLProperty, aStyle.mAttribute, &tOutString);
if (outValue) {
outValue->Assign(tOutString);
}
return state;
}
return mPendingStylesToApplyToNewContent->GetStyleState(
*aStyle.mHTMLProperty);
}();
if (styleState != PendingStyleState::NotUpdated) {
*aFirst = *aAny = *aAll =
(styleState == PendingStyleState::BeingPreserved);
return NS_OK;
}
nsIContent* const collapsedContent =
nsIContent::FromNode(range->GetStartContainer());
if (MOZ_LIKELY(collapsedContent &&
collapsedContent->GetAsElementOrParentElement()) &&
aStyle.IsCSSSettable(
*collapsedContent->GetAsElementOrParentElement())) {
if (aValue) {
tOutString.Assign(*aValue);
}
Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
CSSEditUtils::IsComputedCSSEquivalentTo(
*this, MOZ_KnownLive(*collapsedContent), aStyle, tOutString);
if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) {
NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed");
return isComputedCSSEquivalentToStyleOrError.unwrapErr();
}
*aFirst = *aAny = *aAll =
isComputedCSSEquivalentToStyleOrError.unwrap();
if (outValue) {
outValue->Assign(tOutString);
}
return NS_OK;
}
*aFirst = *aAny = *aAll =
collapsedContent && HTMLEditUtils::IsInlineStyleSetByElement(
*collapsedContent, aStyle, aValue, outValue);
return NS_OK;
}
// Non-collapsed selection
nsAutoString firstValue, theValue;
nsCOMPtr<nsINode> endNode = range->GetEndContainer();
uint32_t endOffset = range->EndOffset();
PostContentIterator postOrderIter;
DebugOnly<nsresult> rvIgnored = postOrderIter.Init(range);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to initialize post-order content iterator");
for (; !postOrderIter.IsDone(); postOrderIter.Next()) {
if (postOrderIter.GetCurrentNode()->IsHTMLElement(nsGkAtoms::body)) {
break;
}
RefPtr<Text> textNode = Text::FromNode(postOrderIter.GetCurrentNode());
if (!textNode) {
continue;
}
// just ignore any non-editable nodes
if (!EditorUtils::IsEditableContent(*textNode, EditorType::HTML) ||
!HTMLEditUtils::IsVisibleTextNode(*textNode)) {
continue;
}
if (!isCollapsed && first && firstNodeInRange) {
firstNodeInRange = false;
if (range->StartOffset() == textNode->TextDataLength()) {
continue;
}
} else if (textNode == endNode && !endOffset) {
continue;
}
const RefPtr<Element> element = textNode->GetParentElement();
bool isSet = false;
if (first) {
if (element) {
if (aStyle.IsCSSSettable(*element)) {
// The HTML styles defined by aHTMLProperty/aAttribute have a CSS
// equivalence in this implementation for node; let's check if it
// carries those CSS styles
if (aValue) {
firstValue.Assign(*aValue);
}
Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
CSSEditUtils::IsComputedCSSEquivalentTo(*this, *element, aStyle,
firstValue);
if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) {
NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed");
return isComputedCSSEquivalentToStyleOrError.unwrapErr();
}
isSet = isComputedCSSEquivalentToStyleOrError.unwrap();
} else {
isSet = HTMLEditUtils::IsInlineStyleSetByElement(
*element, aStyle, aValue, &firstValue);
}
}
*aFirst = isSet;
first = false;
if (outValue) {
*outValue = firstValue;
}
} else {
if (element) {
if (aStyle.IsCSSSettable(*element)) {
// The HTML styles defined by aHTMLProperty/aAttribute have a CSS
// equivalence in this implementation for node; let's check if it
// carries those CSS styles
if (aValue) {
theValue.Assign(*aValue);
}
Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
CSSEditUtils::IsComputedCSSEquivalentTo(*this, *element, aStyle,
theValue);
if (MOZ_UNLIKELY(isComputedCSSEquivalentToStyleOrError.isErr())) {
NS_WARNING("CSSEditUtils::IsComputedCSSEquivalentTo() failed");
return isComputedCSSEquivalentToStyleOrError.unwrapErr();
}
isSet = isComputedCSSEquivalentToStyleOrError.unwrap();
} else {
isSet = HTMLEditUtils::IsInlineStyleSetByElement(*element, aStyle,
aValue, &theValue);
}
}
if (firstValue != theValue &&
// For text-decoration related HTML properties, i.e. <u> and
// <strike>, we have to also check |isSet| because text-decoration
// is a shorthand property, and it may contains other unrelated
// longhand components, e.g. text-decoration-color, so we have to do
// an extra check before setting |*aAll| to false.
// e.g.
// firstValue: "underline rgb(0, 0, 0)"
// theValue: "underline rgb(0, 0, 238)" // <a> uses blue color
// These two values should be the same if we are checking `<u>`.
// That's why we need to check |*aFirst| and |isSet|.
//
// This is a work-around for text-decoration.
// Once this spec issue is resolved, we could drop this work-around
// check.
(!aStyle.IsStyleOfTextDecoration(
EditorInlineStyle::IgnoreSElement::Yes) ||
*aFirst != isSet)) {
*aAll = false;
}
}
if (isSet) {
*aAny = true;
} else {
*aAll = false;
}
}
}
if (!*aAny) {
// make sure that if none of the selection is set, we don't report all is
// set
*aAll = false;
}
return NS_OK;
}
nsresult HTMLEditor::GetInlineProperty(nsStaticAtom& aHTMLProperty,
nsAtom* aAttribute,
const nsAString& aValue, bool* aFirst,
bool* aAny, bool* aAll) const {
if (NS_WARN_IF(!aFirst) || NS_WARN_IF(!aAny) || NS_WARN_IF(!aAll)) {
return NS_ERROR_INVALID_ARG;
}
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
}
const nsAString* val = !aValue.IsEmpty() ? &aValue : nullptr;
nsresult rv =
GetInlinePropertyBase(EditorInlineStyle(aHTMLProperty, aAttribute), val,
aFirst, aAny, aAll, nullptr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::GetInlinePropertyBase() failed");
return EditorBase::ToGenericNSResult(rv);
}
NS_IMETHODIMP HTMLEditor::GetInlinePropertyWithAttrValue(
const nsAString& aHTMLProperty, const nsAString& aAttribute,
const nsAString& aValue, bool* aFirst, bool* aAny, bool* aAll,
nsAString& outValue) {
nsStaticAtom* property = NS_GetStaticAtom(aHTMLProperty);
if (NS_WARN_IF(!property)) {
return NS_ERROR_INVALID_ARG;
}
nsStaticAtom* attribute = EditorUtils::GetAttributeAtom(aAttribute);
// MOZ_KnownLive because nsStaticAtom is available until shutting down.
nsresult rv = GetInlinePropertyWithAttrValue(MOZ_KnownLive(*property),
MOZ_KnownLive(attribute), aValue,
aFirst, aAny, aAll, outValue);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::GetInlinePropertyWithAttrValue() failed");
return rv;
}
nsresult HTMLEditor::GetInlinePropertyWithAttrValue(
nsStaticAtom& aHTMLProperty, nsAtom* aAttribute, const nsAString& aValue,
bool* aFirst, bool* aAny, bool* aAll, nsAString& outValue) {
if (NS_WARN_IF(!aFirst) || NS_WARN_IF(!aAny) || NS_WARN_IF(!aAll)) {
return NS_ERROR_INVALID_ARG;
}
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
}
const nsAString* val = !aValue.IsEmpty() ? &aValue : nullptr;
nsresult rv =
GetInlinePropertyBase(EditorInlineStyle(aHTMLProperty, aAttribute), val,
aFirst, aAny, aAll, &outValue);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::GetInlinePropertyBase() failed");
return EditorBase::ToGenericNSResult(rv);
}
nsresult HTMLEditor::RemoveAllInlinePropertiesAsAction(
nsIPrincipal* aPrincipal) {
AutoEditActionDataSetter editActionData(
*this, EditAction::eRemoveAllInlineStyleProperties, aPrincipal);
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
AutoPlaceholderBatch treatAsOneTransaction(
*this, ScrollSelectionIntoView::Yes, __FUNCTION__);
IgnoredErrorResult ignoredError;
AutoEditSubActionNotifier startToHandleEditSubAction(
*this, EditSubAction::eRemoveAllTextProperties, nsIEditor::eNext,
ignoredError);
if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
return EditorBase::ToGenericNSResult(ignoredError.StealNSResult());
}
NS_WARNING_ASSERTION(
!ignoredError.Failed(),
"HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
AutoTArray<EditorInlineStyle, 1> removeAllInlineStyles;
removeAllInlineStyles.AppendElement(EditorInlineStyle::RemoveAllStyles());
rv = RemoveInlinePropertiesAsSubAction(removeAllInlineStyles);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::RemoveInlinePropertiesAsSubAction() failed");
return EditorBase::ToGenericNSResult(rv);
}
nsresult HTMLEditor::RemoveInlinePropertyAsAction(nsStaticAtom& aHTMLProperty,
nsStaticAtom* aAttribute,
nsIPrincipal* aPrincipal) {
AutoEditActionDataSetter editActionData(
*this,
HTMLEditUtils::GetEditActionForFormatText(aHTMLProperty, aAttribute,
false),
aPrincipal);
switch (editActionData.GetEditAction()) {
case EditAction::eRemoveFontFamilyProperty:
MOZ_ASSERT(!u""_ns.IsVoid());
editActionData.SetData(u""_ns);
break;
case EditAction::eRemoveColorProperty:
case EditAction::eRemoveBackgroundColorPropertyInline:
editActionData.SetColorData(u""_ns);
break;
default:
break;
}
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
AutoTArray<EditorInlineStyle, 8> removeInlineStyleAndRelatedElements;
AppendInlineStyleAndRelatedStyle(EditorInlineStyle(aHTMLProperty, aAttribute),
removeInlineStyleAndRelatedElements);
rv = RemoveInlinePropertiesAsSubAction(removeInlineStyleAndRelatedElements);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::RemoveInlinePropertiesAsSubAction() failed");
return EditorBase::ToGenericNSResult(rv);
}
NS_IMETHODIMP HTMLEditor::RemoveInlineProperty(const nsAString& aProperty,
const nsAString& aAttribute) {
nsStaticAtom* property = NS_GetStaticAtom(aProperty);
nsStaticAtom* attribute = EditorUtils::GetAttributeAtom(aAttribute);
AutoEditActionDataSetter editActionData(
*this,
HTMLEditUtils::GetEditActionForFormatText(*property, attribute, false));
switch (editActionData.GetEditAction()) {
case EditAction::eRemoveFontFamilyProperty:
MOZ_ASSERT(!u""_ns.IsVoid());
editActionData.SetData(u""_ns);
break;
case EditAction::eRemoveColorProperty:
case EditAction::eRemoveBackgroundColorPropertyInline:
editActionData.SetColorData(u""_ns);
break;
default:
break;
}
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
AutoTArray<EditorInlineStyle, 1> removeOneInlineStyle;
removeOneInlineStyle.AppendElement(EditorInlineStyle(*property, attribute));
rv = RemoveInlinePropertiesAsSubAction(removeOneInlineStyle);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::RemoveInlinePropertiesAsSubAction() failed");
return EditorBase::ToGenericNSResult(rv);
}
void HTMLEditor::AppendInlineStyleAndRelatedStyle(
const EditorInlineStyle& aStyleToRemove,
nsTArray<EditorInlineStyle>& aStylesToRemove) const {
if (nsStaticAtom* similarElementName =
aStyleToRemove.GetSimilarElementNameAtom()) {
EditorInlineStyle anotherStyle(*similarElementName);
if (!aStylesToRemove.Contains(anotherStyle)) {
aStylesToRemove.AppendElement(std::move(anotherStyle));
}
} else if (aStyleToRemove.mHTMLProperty == nsGkAtoms::font) {
if (aStyleToRemove.mAttribute == nsGkAtoms::size) {
EditorInlineStyle big(*nsGkAtoms::big), small(*nsGkAtoms::small);
if (!aStylesToRemove.Contains(big)) {
aStylesToRemove.AppendElement(std::move(big));
}
if (!aStylesToRemove.Contains(small)) {
aStylesToRemove.AppendElement(std::move(small));
}
}
// Handling <tt> element code was implemented for composer (bug 115922).
// This shouldn't work with Document.execCommand() for compatibility with
// the other browsers. Currently, edit action principal is set only when
// the root caller is Document::ExecCommand() so that we should handle <tt>
// element only when the principal is nullptr that must be only when XUL
// command is executed on composer.
else if (aStyleToRemove.mAttribute == nsGkAtoms::face &&
!GetEditActionPrincipal()) {
EditorInlineStyle tt(*nsGkAtoms::tt);
if (!aStylesToRemove.Contains(tt)) {
aStylesToRemove.AppendElement(std::move(tt));
}
}
}
if (!aStylesToRemove.Contains(aStyleToRemove)) {
aStylesToRemove.AppendElement(aStyleToRemove);
}
}
nsresult HTMLEditor::RemoveInlinePropertiesAsSubAction(
const nsTArray<EditorInlineStyle>& aStylesToRemove) {
MOZ_ASSERT(IsEditActionDataAvailable());
MOZ_ASSERT(!aStylesToRemove.IsEmpty());
DebugOnly<nsresult> rvIgnored = CommitComposition();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"EditorBase::CommitComposition() failed, but ignored");
if (SelectionRef().IsCollapsed()) {
// Manipulating text attributes on a collapsed selection only sets state
// for the next text insertion
mPendingStylesToApplyToNewContent->ClearStyles(aStylesToRemove);
return NS_OK;
}
// XXX Shouldn't we quit before calling `CommitComposition()`?
if (IsPlaintextMailComposer()) {
return NS_OK;
}
{
Result<EditActionResult, nsresult> result = CanHandleHTMLEditSubAction();
if (MOZ_UNLIKELY(result.isErr())) {
NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
return result.unwrapErr();
}
if (result.inspect().Canceled()) {
return NS_OK;
}
}
AutoPlaceholderBatch treatAsOneTransaction(
*this, ScrollSelectionIntoView::Yes, __FUNCTION__);
IgnoredErrorResult ignoredError;
AutoEditSubActionNotifier startToHandleEditSubAction(
*this, EditSubAction::eRemoveTextProperty, nsIEditor::eNext,
ignoredError);
if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
return ignoredError.StealNSResult();
}
NS_WARNING_ASSERTION(
!ignoredError.Failed(),
"HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
// TODO: We don't need AutoTransactionsConserveSelection here in the normal
// cases, but removing this may cause the behavior with the legacy
// mutation event listeners. We should try to delete this in a bug.
AutoTransactionsConserveSelection dontChangeMySelection(*this);
AutoRangeArray selectionRanges(SelectionRef());
for (const EditorInlineStyle& styleToRemove : aStylesToRemove) {
// The ranges may be updated by changing the DOM tree. In strictly
// speaking, we should save and restore the ranges at every range loop,
// but we've never done so and it may be expensive if there are a lot of
// ranges. Therefore, we should do it for every style handling for now.
// TODO: We should collect everything required for removing the style before
// touching the DOM tree. Then, we need to save and restore the
// ranges only once.
Maybe<AutoInlineStyleSetter> styleInverter;
if (styleToRemove.IsInvertibleWithCSS()) {
styleInverter.emplace(EditorInlineStyleAndValue::ToInvert(styleToRemove));
}
for (OwningNonNull<nsRange>& selectionRange : selectionRanges.Ranges()) {
AutoTrackDOMRange trackSelectionRange(RangeUpdaterRef(), &selectionRange);
// If we're removing <a name>, we don't want to split ancestors because
// the split fragment will keep working as named anchor. Therefore, we
// need to remove all <a name> elements which the selection range even
// partially contains.
const EditorDOMRange range(
styleToRemove.mHTMLProperty == nsGkAtoms::name
? GetExtendedRangeWrappingNamedAnchor(
EditorRawDOMRange(selectionRange))
: GetExtendedRangeWrappingEntirelySelectedElements(
EditorRawDOMRange(selectionRange)));
if (NS_WARN_IF(!range.IsPositioned())) {
continue;
}
// Remove this style from ancestors of our range endpoints, splitting
// them as appropriate
Result<SplitRangeOffResult, nsresult> splitRangeOffResult =
SplitAncestorStyledInlineElementsAtRangeEdges(
range, styleToRemove, SplitAtEdges::eAllowToCreateEmptyContainer);
if (MOZ_UNLIKELY(splitRangeOffResult.isErr())) {
NS_WARNING(
"HTMLEditor::SplitAncestorStyledInlineElementsAtRangeEdges() "
"failed");
return splitRangeOffResult.unwrapErr();
}
// There is AutoTransactionsConserveSelection, so we don't need to
// update selection here.
splitRangeOffResult.inspect().IgnoreCaretPointSuggestion();
// XXX Modifying `range` means that we may modify ranges in `Selection`.
// Is this intentional? Note that the range may be not in
// `Selection` too. It seems that at least one of them is not
// an unexpected case.
const EditorDOMRange& splitRange =
splitRangeOffResult.inspect().RangeRef();
if (NS_WARN_IF(!splitRange.IsPositioned())) {
continue;
}
AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContentsToInvertStyle;
{
// Collect top level children in the range first.
// TODO: Perhaps, HTMLEditUtils::IsSplittableNode should be used here
// instead of EditorUtils::IsEditableContent.
AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContentsAroundRange;
if (splitRange.InSameContainer() &&
splitRange.StartRef().IsInTextNode()) {
if (!EditorUtils::IsEditableContent(
*splitRange.StartRef().ContainerAs<Text>(),
EditorType::HTML)) {
continue;
}
arrayOfContentsAroundRange.AppendElement(
*splitRange.StartRef().ContainerAs<Text>());
} else if (splitRange.IsInTextNodes() &&
splitRange.InAdjacentSiblings()) {
// Adjacent siblings are in a same element, so the editable state of
// both text nodes are always same.
if (!EditorUtils::IsEditableContent(
*splitRange.StartRef().ContainerAs<Text>(),
EditorType::HTML)) {
continue;
}
arrayOfContentsAroundRange.AppendElement(
*splitRange.StartRef().ContainerAs<Text>());
arrayOfContentsAroundRange.AppendElement(
*splitRange.EndRef().ContainerAs<Text>());
} else {
// Append first node if it's a text node but selected not entirely.
if (splitRange.StartRef().IsInTextNode() &&
!splitRange.StartRef().IsStartOfContainer() &&
EditorUtils::IsEditableContent(
*splitRange.StartRef().ContainerAs<Text>(),
EditorType::HTML)) {
arrayOfContentsAroundRange.AppendElement(
*splitRange.StartRef().ContainerAs<Text>());
}
// Append all entirely selected nodes.
ContentSubtreeIterator subtreeIter;
if (NS_SUCCEEDED(
subtreeIter.Init(splitRange.StartRef().ToRawRangeBoundary(),
splitRange.EndRef().ToRawRangeBoundary()))) {
for (; !subtreeIter.IsDone(); subtreeIter.Next()) {
nsCOMPtr<nsINode> node = subtreeIter.GetCurrentNode();
if (NS_WARN_IF(!node)) {
return NS_ERROR_FAILURE;
}
if (node->IsContent() &&
EditorUtils::IsEditableContent(*node->AsContent(),
EditorType::HTML)) {
arrayOfContentsAroundRange.AppendElement(*node->AsContent());
}
}
}
// Append last node if it's a text node but selected not entirely.
if (!splitRange.InSameContainer() &&
splitRange.EndRef().IsInTextNode() &&
!splitRange.EndRef().IsEndOfContainer() &&
EditorUtils::IsEditableContent(
*splitRange.EndRef().ContainerAs<Text>(), EditorType::HTML)) {
arrayOfContentsAroundRange.AppendElement(
*splitRange.EndRef().ContainerAs<Text>());
}
}
if (styleToRemove.IsInvertibleWithCSS()) {
arrayOfContentsToInvertStyle.SetCapacity(
arrayOfContentsAroundRange.Length());
}
for (OwningNonNull<nsIContent>& content : arrayOfContentsAroundRange) {
// We should remove style from the element and its descendants.
if (content->IsElement()) {
Result<EditorDOMPoint, nsresult> removeStyleResult =
RemoveStyleInside(MOZ_KnownLive(*content->AsElement()),
styleToRemove, SpecifiedStyle::Preserve);
if (MOZ_UNLIKELY(removeStyleResult.isErr())) {
NS_WARNING("HTMLEditor::RemoveStyleInside() failed");
return removeStyleResult.unwrapErr();
}
// There is AutoTransactionsConserveSelection, so we don't need to
// update selection here.
// If the element was removed from the DOM tree by
// RemoveStyleInside, we need to do nothing for it anymore.
if (!content->GetParentNode()) {
continue;
}
}
if (styleToRemove.IsInvertibleWithCSS()) {
arrayOfContentsToInvertStyle.AppendElement(content);
}
} // for-loop for arrayOfContentsAroundRange
}
auto FlushAndStopTrackingAndShrinkSelectionRange =
[&]() MOZ_CAN_RUN_SCRIPT {
trackSelectionRange.FlushAndStopTracking();
if (NS_WARN_IF(!selectionRange->IsPositioned())) {
return;
}
EditorRawDOMRange range(selectionRange);
nsINode* const commonAncestor =
range.GetClosestCommonInclusiveAncestor();
// Shrink range for compatibility between browsers.
nsIContent* const maybeNextContent =
range.StartRef().IsInContentNode() &&
range.StartRef().IsEndOfContainer()
? AutoInlineStyleSetter::GetNextEditableInlineContent(
*range.StartRef().ContainerAs<nsIContent>(),
commonAncestor)
: nullptr;
nsIContent* const maybePreviousContent =
range.EndRef().IsInContentNode() &&
range.EndRef().IsStartOfContainer()
? AutoInlineStyleSetter::GetPreviousEditableInlineContent(
*range.EndRef().ContainerAs<nsIContent>(),
commonAncestor)
: nullptr;
if (!maybeNextContent && !maybePreviousContent) {
return;
}
const auto startPoint =
maybeNextContent &&
maybeNextContent != selectionRange->GetStartContainer()
? HTMLEditUtils::GetDeepestEditableStartPointOf<
EditorRawDOMPoint>(*maybeNextContent)
: range.StartRef();
const auto endPoint =
maybePreviousContent && maybePreviousContent !=
selectionRange->GetEndContainer()
? HTMLEditUtils::GetDeepestEditableEndPointOf<
EditorRawDOMPoint>(*maybePreviousContent)
: range.EndRef();
DebugOnly<nsresult> rvIgnored = selectionRange->SetStartAndEnd(
startPoint.ToRawRangeBoundary(), endPoint.ToRawRangeBoundary());
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"nsRange::SetStartAndEnd() failed, but ignored");
};
if (arrayOfContentsToInvertStyle.IsEmpty()) {
FlushAndStopTrackingAndShrinkSelectionRange();
continue;
}
MOZ_ASSERT(styleToRemove.IsInvertibleWithCSS());
// If the style is specified in parent block and we can remove the
// style with inserting new <span> element, we should do it.
for (OwningNonNull<nsIContent>& content : arrayOfContentsToInvertStyle) {
if (Element* element = Element::FromNode(content)) {
// XXX Do we need to call this even when data node or something? If
// so, for what?
// MOZ_KnownLive because 'arrayOfContents' is guaranteed to
// keep it alive.
nsresult rv = styleInverter->InvertStyleIfApplied(
*this, MOZ_KnownLive(*element));
if (NS_FAILED(rv)) {
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
NS_WARNING(
"AutoInlineStyleSetter::InvertStyleIfApplied() failed");
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING(
"AutoInlineStyleSetter::InvertStyleIfApplied() failed, but "
"ignored");
}
continue;
}
// Unfortunately, all browsers don't join text nodes when removing a
// style. Therefore, there may be multiple text nodes as adjacent
// siblings. That's the reason why we need to handle text nodes in this
// loop.
if (Text* textNode = Text::FromNode(content)) {
const uint32_t startOffset =
content == splitRange.StartRef().GetContainer()
? splitRange.StartRef().Offset()
: 0u;
const uint32_t endOffset =
content == splitRange.EndRef().GetContainer()
? splitRange.EndRef().Offset()
: textNode->TextDataLength();
Result<SplitRangeOffFromNodeResult, nsresult>
wrapTextInStyledElementResult =
styleInverter->InvertStyleIfApplied(
*this, MOZ_KnownLive(*textNode), startOffset, endOffset);
if (MOZ_UNLIKELY(wrapTextInStyledElementResult.isErr())) {
NS_WARNING("AutoInlineStyleSetter::InvertStyleIfApplied() failed");
return wrapTextInStyledElementResult.unwrapErr();
}
SplitRangeOffFromNodeResult unwrappedWrapTextInStyledElementResult =
wrapTextInStyledElementResult.unwrap();
// There is AutoTransactionsConserveSelection, so we don't need to
// update selection here.
unwrappedWrapTextInStyledElementResult.IgnoreCaretPointSuggestion();
// If we've split the content, let's swap content in
// arrayOfContentsToInvertStyle with the text node which is applied
// the style.
if (unwrappedWrapTextInStyledElementResult.DidSplit() &&
styleToRemove.IsInvertibleWithCSS()) {
MOZ_ASSERT(unwrappedWrapTextInStyledElementResult
.GetMiddleContentAs<Text>());
if (Text* styledTextNode = unwrappedWrapTextInStyledElementResult
.GetMiddleContentAs<Text>()) {
if (styledTextNode != content) {
arrayOfContentsToInvertStyle.ReplaceElementAt(
arrayOfContentsToInvertStyle.Length() - 1,
OwningNonNull<nsIContent>(*styledTextNode));
}
}
}
continue;
}
// If the node is not an element nor a text node, it's invisible.
// In this case, we don't need to make it wrapped in new element.
}
// Finally, we should remove the style from all leaf text nodes if
// they still have the style.
AutoTArray<OwningNonNull<Text>, 32> leafTextNodes;
for (const OwningNonNull<nsIContent>& content :
arrayOfContentsToInvertStyle) {
// XXX Should we ignore content which has already removed from the
// DOM tree by the previous for-loop?
if (content->IsElement()) {
CollectEditableLeafTextNodes(*content->AsElement(), leafTextNodes);
}
}
for (const OwningNonNull<Text>& textNode : leafTextNodes) {
Result<SplitRangeOffFromNodeResult, nsresult>
wrapTextInStyledElementResult = styleInverter->InvertStyleIfApplied(
*this, MOZ_KnownLive(*textNode), 0, textNode->TextLength());
if (MOZ_UNLIKELY(wrapTextInStyledElementResult.isErr())) {
NS_WARNING(
"AutoInlineStyleSetter::SplitTextNodeAndApplyStyleToMiddleNode() "
"failed");
return wrapTextInStyledElementResult.unwrapErr();
}
// There is AutoTransactionsConserveSelection, so we don't need to
// update selection here.
wrapTextInStyledElementResult.inspect().IgnoreCaretPointSuggestion();
} // for-loop of leafTextNodes
// styleInverter may have touched a part of the range. Therefore, we
// cannot adjust the range without comparing DOM node position and
// first/last touched positions, but it may be too expensive. I think
// that shrinking only the tracked range boundaries must be enough in most
// cases.
FlushAndStopTrackingAndShrinkSelectionRange();
} // for-loop of selectionRanges
} // for-loop of styles
MOZ_ASSERT(!selectionRanges.HasSavedRanges());
nsresult rv = selectionRanges.ApplyTo(SelectionRef());
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::ApplyTo() failed");
return rv;
}
nsresult HTMLEditor::AutoInlineStyleSetter::InvertStyleIfApplied(
HTMLEditor& aHTMLEditor, Element& aElement) {
MOZ_ASSERT(IsStyleToInvert());
Result<bool, nsresult> isRemovableParentStyleOrError =
aHTMLEditor.IsRemovableParentStyleWithNewSpanElement(aElement, *this);
if (MOZ_UNLIKELY(isRemovableParentStyleOrError.isErr())) {
NS_WARNING("HTMLEditor::IsRemovableParentStyleWithNewSpanElement() failed");
return isRemovableParentStyleOrError.unwrapErr();
}
if (!isRemovableParentStyleOrError.unwrap()) {
// E.g., text-decoration cannot be override visually in children.
// In such cases, we can do nothing.
return NS_OK;
}
// Wrap it into a new element, move it into direct child which has same style,
// or specify the style to its parent.
Result<CaretPoint, nsresult> pointToPutCaretOrError =
ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle(aHTMLEditor, aElement);
if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
NS_WARNING(
"AutoInlineStyleSetter::"
"ApplyStyleToNodeOrChildrenAndRemoveNestedSameStyle() failed");
return pointToPutCaretOrError.unwrapErr();
}
// The caller must update `Selection` later so that we don't need this.
pointToPutCaretOrError.unwrap().IgnoreCaretPointSuggestion();
return NS_OK;
}
Result<SplitRangeOffFromNodeResult, nsresult>
HTMLEditor::AutoInlineStyleSetter::InvertStyleIfApplied(HTMLEditor& aHTMLEditor,
Text& aTextNode,
uint32_t aStartOffset,
uint32_t aEndOffset) {
MOZ_ASSERT(IsStyleToInvert());
Result<bool, nsresult> isRemovableParentStyleOrError =
aHTMLEditor.IsRemovableParentStyleWithNewSpanElement(aTextNode, *this);
if (MOZ_UNLIKELY(isRemovableParentStyleOrError.isErr())) {
NS_WARNING("HTMLEditor::IsRemovableParentStyleWithNewSpanElement() failed");
return isRemovableParentStyleOrError.propagateErr();
}
if (!isRemovableParentStyleOrError.unwrap()) {
// E.g., text-decoration cannot be override visually in children.
// In such cases, we can do nothing.
return SplitRangeOffFromNodeResult(nullptr, &aTextNode, nullptr);
}
// We need to use new `<span>` element or existing element if it's available
// to overwrite parent style.
Result<SplitRangeOffFromNodeResult, nsresult> wrapTextInStyledElementResult =
SplitTextNodeAndApplyStyleToMiddleNode(aHTMLEditor, aTextNode,
aStartOffset, aEndOffset);
NS_WARNING_ASSERTION(
wrapTextInStyledElementResult.isOk(),
"AutoInlineStyleSetter::SplitTextNodeAndApplyStyleToMiddleNode() failed");
return wrapTextInStyledElementResult;
}
Result<bool, nsresult> HTMLEditor::IsRemovableParentStyleWithNewSpanElement(
nsIContent& aContent, const EditorInlineStyle& aStyle) const {
// We don't support to remove all inline styles with this path.
if (aStyle.IsStyleToClearAllInlineStyles()) {
return false;
}
// First check whether the style is invertible since this is the fastest
// check.
if (!aStyle.IsInvertibleWithCSS()) {
return false;
}
// If aContent is not an element and it's not in an element, it means that
// aContent is disconnected non-element node. In this case, it's never
// applied any styles which are invertible.
const RefPtr<Element> element = aContent.GetAsElementOrParentElement();
if (MOZ_UNLIKELY(!element)) {
return false;
}
// If parent block has invertible style, we should remove the style with
// creating new `<span>` element even in HTML mode because Chrome does it.
if (!aStyle.IsCSSSettable(*element)) {
return false;
}
nsAutoString emptyString;
Result<bool, nsresult> isComputedCSSEquivalentToStyleOrError =
CSSEditUtils::IsComputedCSSEquivalentTo(*this, *element, aStyle,
emptyString);
NS_WARNING_ASSERTION(isComputedCSSEquivalentToStyleOrError.isOk(),
"CSSEditUtils::IsComputedCSSEquivalentTo() failed");
return isComputedCSSEquivalentToStyleOrError;
}
void HTMLEditor::CollectEditableLeafTextNodes(
Element& aElement, nsTArray<OwningNonNull<Text>>& aLeafTextNodes) const {
for (nsIContent* child = aElement.GetFirstChild(); child;
child = child->GetNextSibling()) {
if (child->IsElement()) {
CollectEditableLeafTextNodes(*child->AsElement(), aLeafTextNodes);
continue;
}
if (child->IsText()) {
aLeafTextNodes.AppendElement(*child->AsText());
}
}
}
nsresult HTMLEditor::IncreaseFontSizeAsAction(nsIPrincipal* aPrincipal) {
AutoEditActionDataSetter editActionData(*this, EditAction::eIncrementFontSize,
aPrincipal);
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
rv = IncrementOrDecrementFontSizeAsSubAction(FontSize::incr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::IncrementOrDecrementFontSizeAsSubAction("
"FontSize::incr) failed");
return EditorBase::ToGenericNSResult(rv);
}
nsresult HTMLEditor::DecreaseFontSizeAsAction(nsIPrincipal* aPrincipal) {
AutoEditActionDataSetter editActionData(*this, EditAction::eDecrementFontSize,
aPrincipal);
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent(), failed");
return EditorBase::ToGenericNSResult(rv);
}
rv = IncrementOrDecrementFontSizeAsSubAction(FontSize::decr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditor::IncrementOrDecrementFontSizeAsSubAction("
"FontSize::decr) failed");
return EditorBase::ToGenericNSResult(rv);
}
nsresult HTMLEditor::IncrementOrDecrementFontSizeAsSubAction(
FontSize aIncrementOrDecrement) {
MOZ_ASSERT(IsEditActionDataAvailable());
// Committing composition and changing font size should be undone together.
AutoPlaceholderBatch treatAsOneTransaction(
*this, ScrollSelectionIntoView::Yes, __FUNCTION__);
DebugOnly<nsresult> rvIgnored = CommitComposition();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"EditorBase::CommitComposition() failed, but ignored");
// If selection is collapsed, set typing state
if (SelectionRef().IsCollapsed()) {
nsStaticAtom& bigOrSmallTagName = aIncrementOrDecrement == FontSize::incr
? *nsGkAtoms::big
: *nsGkAtoms::small;
// Let's see in what kind of element the selection is
if (!SelectionRef().RangeCount()) {
return NS_OK;
}
const auto firstRangeStartPoint =
EditorBase::GetFirstSelectionStartPoint<EditorRawDOMPoint>();
if (NS_WARN_IF(!firstRangeStartPoint.IsSet())) {
return NS_OK;
}
Element* element =
firstRangeStartPoint.GetContainerOrContainerParentElement();
if (NS_WARN_IF(!element)) {
return NS_OK;
}
if (!HTMLEditUtils::CanNodeContain(*element, bigOrSmallTagName)) {
return NS_OK;
}
// Manipulating text attributes on a collapsed selection only sets state
// for the next text insertion
mPendingStylesToApplyToNewContent->PreserveStyle(bigOrSmallTagName, nullptr,
u""_ns);
return NS_OK;
}
IgnoredErrorResult ignoredError;
AutoEditSubActionNotifier startToHandleEditSubAction(
*this, EditSubAction::eSetTextProperty, nsIEditor::eNext, ignoredError);
if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
return ignoredError.StealNSResult();
}
NS_WARNING_ASSERTION(
!ignoredError.Failed(),
"HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
// TODO: We don't need AutoTransactionsConserveSelection here in the normal
// cases, but removing this may cause the behavior with the legacy
// mutation event listeners. We should try to delete this in a bug.
AutoTransactionsConserveSelection dontChangeMySelection(*this);
AutoRangeArray selectionRanges(SelectionRef());
MOZ_ALWAYS_TRUE(selectionRanges.SaveAndTrackRanges(*this));
for (const OwningNonNull<nsRange>& domRange : selectionRanges.Ranges()) {
// TODO: We should stop extending the range outside ancestor blocks because
// we don't need to do it for setting inline styles. However, here is
// chrome only handling path. Therefore, we don't need to fix here
// soon.
const EditorDOMRange range(GetExtendedRangeWrappingEntirelySelectedElements(
EditorRawDOMRange(domRange)));
if (NS_WARN_IF(!range.IsPositioned())) {
continue;
}
if (range.InSameContainer() && range.StartRef().IsInTextNode()) {
Result<CreateElementResult, nsresult> wrapInBigOrSmallElementResult =
SetFontSizeOnTextNode(
MOZ_KnownLive(*range.StartRef().ContainerAs<Text>()),
range.StartRef().Offset(), range.EndRef().Offset(),
aIncrementOrDecrement);
if (MOZ_UNLIKELY(wrapInBigOrSmallElementResult.isErr())) {
NS_WARNING("HTMLEditor::SetFontSizeOnTextNode() failed");
return wrapInBigOrSmallElementResult.unwrapErr();
}
// There is an AutoTransactionsConserveSelection instance so that we don't
// need to update selection for this change.
wrapInBigOrSmallElementResult.inspect().IgnoreCaretPointSuggestion();
continue;
}
// Not the easy case. Range not contained in single text node. There
// are up to three phases here. There are all the nodes reported by the
// subtree iterator to be processed. And there are potentially a
// starting textnode and an ending textnode which are only partially
// contained by the range.
// Let's handle the nodes reported by the iterator. These nodes are
// entirely contained in the selection range. We build up a list of them
// (since doing operations on the document during iteration would perturb
// the iterator).
// Iterate range and build up array
ContentSubtreeIterator subtreeIter;
if (NS_SUCCEEDED(subtreeIter.Init(range.StartRef().ToRawRangeBoundary(),
range.EndRef().ToRawRangeBoundary()))) {
nsTArray<OwningNonNull<nsIContent>> arrayOfContents;
for (; !subtreeIter.IsDone(); subtreeIter.Next()) {
if (NS_WARN_IF(!subtreeIter.GetCurrentNode()->IsContent())) {
return NS_ERROR_FAILURE;
}
OwningNonNull<nsIContent> content =
*subtreeIter.GetCurrentNode()->AsContent();
if (EditorUtils::IsEditableContent(content, EditorType::HTML)) {
arrayOfContents.AppendElement(content);
}
}
// Now that we have the list, do the font size change on each node
for (OwningNonNull<nsIContent>& content : arrayOfContents) {
// MOZ_KnownLive because of bug 1622253
Result<EditorDOMPoint, nsresult> fontChangeOnNodeResult =
SetFontSizeWithBigOrSmallElement(MOZ_KnownLive(content),
aIncrementOrDecrement);
if (MOZ_UNLIKELY(fontChangeOnNodeResult.isErr())) {
NS_WARNING("HTMLEditor::SetFontSizeWithBigOrSmallElement() failed");
return fontChangeOnNodeResult.unwrapErr();
}
// There is an AutoTransactionsConserveSelection, so we don't need to
// update selection here.
}
}
// Now check the start and end parents of the range to see if they need
// to be separately handled (they do if they are text nodes, due to how
// the subtree iterator works - it will not have reported them).
if (range.StartRef().IsInTextNode() &&
!range.StartRef().IsEndOfContainer() &&
EditorUtils::IsEditableContent(*range.StartRef().ContainerAs<Text>(),
EditorType::HTML)) {
Result<CreateElementResult, nsresult> wrapInBigOrSmallElementResult =
SetFontSizeOnTextNode(
MOZ_KnownLive(*range.StartRef().ContainerAs<Text>()),
range.StartRef().Offset(),
range.StartRef().ContainerAs<Text>()->TextDataLength(),
aIncrementOrDecrement);
if (MOZ_UNLIKELY(wrapInBigOrSmallElementResult.isErr())) {
NS_WARNING("HTMLEditor::SetFontSizeOnTextNode() failed");
return wrapInBigOrSmallElementResult.unwrapErr();
}
// There is an AutoTransactionsConserveSelection instance so that we
// don't need to update selection for this change.
wrapInBigOrSmallElementResult.inspect().IgnoreCaretPointSuggestion();
}
if (range.EndRef().IsInTextNode() && !range.EndRef().IsStartOfContainer() &&
EditorUtils::IsEditableContent(*range.EndRef().ContainerAs<Text>(),
EditorType::HTML)) {
Result<CreateElementResult, nsresult> wrapInBigOrSmallElementResult =
SetFontSizeOnTextNode(
MOZ_KnownLive(*range.EndRef().ContainerAs<Text>()), 0u,
range.EndRef().Offset(), aIncrementOrDecrement);
if (MOZ_UNLIKELY(wrapInBigOrSmallElementResult.isErr())) {
NS_WARNING("HTMLEditor::SetFontSizeOnTextNode() failed");
return wrapInBigOrSmallElementResult.unwrapErr();
}
// There is an AutoTransactionsConserveSelection instance so that we
// don't need to update selection for this change.
wrapInBigOrSmallElementResult.inspect().IgnoreCaretPointSuggestion();
}
}
MOZ_ASSERT(selectionRanges.HasSavedRanges());
selectionRanges.RestoreFromSavedRanges();
nsresult rv = selectionRanges.ApplyTo(SelectionRef());
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "AutoRangeArray::ApplyTo() failed");
return rv;
}
Result<CreateElementResult, nsresult> HTMLEditor::SetFontSizeOnTextNode(
Text& aTextNode, uint32_t aStartOffset, uint32_t aEndOffset,
FontSize aIncrementOrDecrement) {
// Don't need to do anything if no characters actually selected
if (aStartOffset == aEndOffset) {
return CreateElementResult::NotHandled();
}
if (!aTextNode.GetParentNode() ||
!HTMLEditUtils::CanNodeContain(*aTextNode.GetParentNode(),
*nsGkAtoms::big)) {
return CreateElementResult::NotHandled();
}
aEndOffset = std::min(aTextNode.Length(), aEndOffset);
// Make the range an independent node.
RefPtr<Text> textNodeForTheRange = &aTextNode;
EditorDOMPoint pointToPutCaret;
{
auto pointToPutCaretOrError =
[&]() MOZ_CAN_RUN_SCRIPT -> Result<EditorDOMPoint, nsresult> {
EditorDOMPoint pointToPutCaret;
// Split at the end of the range.
EditorDOMPoint atEnd(textNodeForTheRange, aEndOffset);
if (!atEnd.IsEndOfContainer()) {
// We need to split off back of text node
Result<SplitNodeResult, nsresult> splitAtEndResult =
SplitNodeWithTransaction(atEnd);
if (MOZ_UNLIKELY(splitAtEndResult.isErr())) {
NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
return splitAtEndResult.propagateErr();
}
SplitNodeResult unwrappedSplitAtEndResult = splitAtEndResult.unwrap();
if (MOZ_UNLIKELY(
!unwrappedSplitAtEndResult.HasCaretPointSuggestion())) {
NS_WARNING(
"HTMLEditor::SplitNodeWithTransaction() didn't suggest caret "
"point");
return Err(NS_ERROR_FAILURE);
}
unwrappedSplitAtEndResult.MoveCaretPointTo(pointToPutCaret, *this, {});
MOZ_ASSERT_IF(AllowsTransactionsToChangeSelection(),
pointToPutCaret.IsSet());
textNodeForTheRange =
unwrappedSplitAtEndResult.GetPreviousContentAs<Text>();
MOZ_DIAGNOSTIC_ASSERT(textNodeForTheRange);
}
// Split at the start of the range.
EditorDOMPoint atStart(textNodeForTheRange, aStartOffset);
if (!atStart.IsStartOfContainer()) {
// We need to split off front of text node
Result<SplitNodeResult, nsresult> splitAtStartResult =
SplitNodeWithTransaction(atStart);
if (MOZ_UNLIKELY(splitAtStartResult.isErr())) {
NS_WARNING("HTMLEditor::SplitNodeWithTransaction() failed");
return splitAtStartResult.propagateErr();
}
SplitNodeResult unwrappedSplitAtStartResult =
splitAtStartResult.unwrap();
if (MOZ_UNLIKELY(
!unwrappedSplitAtStartResult.HasCaretPointSuggestion())) {
NS_WARNING(
"HTMLEditor::SplitNodeWithTransaction() didn't suggest caret "
"point");
return Err(NS_ERROR_FAILURE);
}
unwrappedSplitAtStartResult.MoveCaretPointTo(pointToPutCaret, *this,
{});
MOZ_ASSERT_IF(AllowsTransactionsToChangeSelection(),
pointToPutCaret.IsSet());
textNodeForTheRange =
unwrappedSplitAtStartResult.GetNextContentAs<Text>();
MOZ_DIAGNOSTIC_ASSERT(textNodeForTheRange);
}
return pointToPutCaret;
}();
if (MOZ_UNLIKELY(pointToPutCaretOrError.isErr())) {
// Don't warn here since it should be done in the lambda.
return pointToPutCaretOrError.propagateErr();
}
pointToPutCaret = pointToPutCaretOrError.unwrap();
}
// Look for siblings that are correct type of node
nsStaticAtom* const bigOrSmallTagName =
aIncrementOrDecrement == FontSize::incr ? nsGkAtoms::big
: nsGkAtoms::small;
nsCOMPtr<nsIContent> sibling = HTMLEditUtils::GetPreviousSibling(
*textNodeForTheRange, {WalkTreeOption::IgnoreNonEditableNode});
if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) {
// Previous sib is already right kind of inline node; slide this over
Result<MoveNodeResult, nsresult> moveTextNodeResult =
MoveNodeToEndWithTransaction(*textNodeForTheRange, *sibling);
if (MOZ_UNLIKELY(moveTextNodeResult.isErr())) {
NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
return moveTextNodeResult.propagateErr();
}
MoveNodeResult unwrappedMoveTextNodeResult = moveTextNodeResult.unwrap();
unwrappedMoveTextNodeResult.MoveCaretPointTo(
pointToPutCaret, *this, {SuggestCaret::OnlyIfHasSuggestion});
// XXX Should we return the new container?
return CreateElementResult::NotHandled(std::move(pointToPutCaret));
}
sibling = HTMLEditUtils::GetNextSibling(
*textNodeForTheRange, {WalkTreeOption::IgnoreNonEditableNode});
if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) {
// Following sib is already right kind of inline node; slide this over
Result<MoveNodeResult, nsresult> moveTextNodeResult =
MoveNodeWithTransaction(*textNodeForTheRange,
EditorDOMPoint(sibling, 0u));
if (MOZ_UNLIKELY(moveTextNodeResult.isErr())) {
NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
return moveTextNodeResult.propagateErr();
}
MoveNodeResult unwrappedMoveTextNodeResult = moveTextNodeResult.unwrap();
unwrappedMoveTextNodeResult.MoveCaretPointTo(
pointToPutCaret, *this, {SuggestCaret::OnlyIfHasSuggestion});
// XXX Should we return the new container?
return CreateElementResult::NotHandled(std::move(pointToPutCaret));
}
// Else wrap the node inside font node with appropriate relative size
Result<CreateElementResult, nsresult> wrapTextInBigOrSmallElementResult =
InsertContainerWithTransaction(*textNodeForTheRange,
MOZ_KnownLive(*bigOrSmallTagName));
if (wrapTextInBigOrSmallElementResult.isErr()) {
NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed");
return wrapTextInBigOrSmallElementResult;
}
CreateElementResult unwrappedWrapTextInBigOrSmallElementResult =
wrapTextInBigOrSmallElementResult.unwrap();
unwrappedWrapTextInBigOrSmallElementResult.MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
return CreateElementResult(
unwrappedWrapTextInBigOrSmallElementResult.UnwrapNewNode(),
std::move(pointToPutCaret));
}
Result<EditorDOMPoint, nsresult> HTMLEditor::SetFontSizeOfFontElementChildren(
nsIContent& aContent, FontSize aIncrementOrDecrement) {
// This routine looks for all the font nodes in the tree rooted by aNode,
// including aNode itself, looking for font nodes that have the size attr
// set. Any such nodes need to have big or small put inside them, since
// they override any big/small that are above them.
// If this is a font node with size, put big/small inside it.
if (aContent.IsHTMLElement(nsGkAtoms::font) &&
aContent.AsElement()->HasAttr(nsGkAtoms::size)) {
EditorDOMPoint pointToPutCaret;
// Cycle through children and adjust relative font size.
AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfContents;
HTMLEditUtils::CollectAllChildren(aContent, arrayOfContents);
for (const auto& child : arrayOfContents) {
// MOZ_KnownLive because of bug 1622253
Result<EditorDOMPoint, nsresult> setFontSizeOfChildResult =
SetFontSizeWithBigOrSmallElement(MOZ_KnownLive(child),
aIncrementOrDecrement);
if (MOZ_UNLIKELY(setFontSizeOfChildResult.isErr())) {
NS_WARNING("HTMLEditor::WrapContentInBigOrSmallElement() failed");
return setFontSizeOfChildResult;
}
if (setFontSizeOfChildResult.inspect().IsSet()) {
pointToPutCaret = setFontSizeOfChildResult.unwrap();
}
}
// WrapContentInBigOrSmallElement already calls us recursively,
// so we don't need to check our children again.
return pointToPutCaret;
}
// Otherwise cycle through the children.
EditorDOMPoint pointToPutCaret;
AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfContents;
HTMLEditUtils::CollectAllChildren(aContent, arrayOfContents);
for (const auto& child : arrayOfContents) {
// MOZ_KnownLive because of bug 1622253
Result<EditorDOMPoint, nsresult> fontSizeChangeResult =
SetFontSizeOfFontElementChildren(MOZ_KnownLive(child),
aIncrementOrDecrement);
if (MOZ_UNLIKELY(fontSizeChangeResult.isErr())) {
NS_WARNING("HTMLEditor::SetFontSizeOfFontElementChildren() failed");
return fontSizeChangeResult;
}
if (fontSizeChangeResult.inspect().IsSet()) {
pointToPutCaret = fontSizeChangeResult.unwrap();
}
}
return pointToPutCaret;
}
Result<EditorDOMPoint, nsresult> HTMLEditor::SetFontSizeWithBigOrSmallElement(
nsIContent& aContent, FontSize aIncrementOrDecrement) {
nsStaticAtom* const bigOrSmallTagName =
aIncrementOrDecrement == FontSize::incr ? nsGkAtoms::big
: nsGkAtoms::small;
// Is aContent the opposite of what we want?
if ((aIncrementOrDecrement == FontSize::incr &&
aContent.IsHTMLElement(nsGkAtoms::small)) ||
(aIncrementOrDecrement == FontSize::decr &&
aContent.IsHTMLElement(nsGkAtoms::big))) {
// First, populate any nested font elements that have the size attr set
Result<EditorDOMPoint, nsresult> fontSizeChangeOfDescendantsResult =
SetFontSizeOfFontElementChildren(aContent, aIncrementOrDecrement);
if (MOZ_UNLIKELY(fontSizeChangeOfDescendantsResult.isErr())) {
NS_WARNING("HTMLEditor::SetFontSizeOfFontElementChildren() failed");
return fontSizeChangeOfDescendantsResult;
}
EditorDOMPoint pointToPutCaret = fontSizeChangeOfDescendantsResult.unwrap();
// In that case, just unwrap the <big> or <small> element.
Result<EditorDOMPoint, nsresult> unwrapBigOrSmallElementResult =
RemoveContainerWithTransaction(MOZ_KnownLive(*aContent.AsElement()));
if (MOZ_UNLIKELY(unwrapBigOrSmallElementResult.isErr())) {
NS_WARNING("HTMLEditor::RemoveContainerWithTransaction() failed");
return unwrapBigOrSmallElementResult;
}
if (unwrapBigOrSmallElementResult.inspect().IsSet()) {
pointToPutCaret = unwrapBigOrSmallElementResult.unwrap();
}
return pointToPutCaret;
}
if (HTMLEditUtils::CanNodeContain(*bigOrSmallTagName, aContent)) {
// First, populate any nested font tags that have the size attr set
Result<EditorDOMPoint, nsresult> fontSizeChangeOfDescendantsResult =
SetFontSizeOfFontElementChildren(aContent, aIncrementOrDecrement);
if (MOZ_UNLIKELY(fontSizeChangeOfDescendantsResult.isErr())) {
NS_WARNING("HTMLEditor::SetFontSizeOfFontElementChildren() failed");
return fontSizeChangeOfDescendantsResult;
}
EditorDOMPoint pointToPutCaret = fontSizeChangeOfDescendantsResult.unwrap();
// Next, if next or previous is <big> or <small>, move aContent into it.
nsCOMPtr<nsIContent> sibling = HTMLEditUtils::GetPreviousSibling(
aContent, {WalkTreeOption::IgnoreNonEditableNode});
if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) {
Result<MoveNodeResult, nsresult> moveNodeResult =
MoveNodeToEndWithTransaction(aContent, *sibling);
if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
NS_WARNING("HTMLEditor::MoveNodeToEndWithTransaction() failed");
return moveNodeResult.propagateErr();
}
MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
unwrappedMoveNodeResult.MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
return pointToPutCaret;
}
sibling = HTMLEditUtils::GetNextSibling(
aContent, {WalkTreeOption::IgnoreNonEditableNode});
if (sibling && sibling->IsHTMLElement(bigOrSmallTagName)) {
Result<MoveNodeResult, nsresult> moveNodeResult =
MoveNodeWithTransaction(aContent, EditorDOMPoint(sibling, 0u));
if (MOZ_UNLIKELY(moveNodeResult.isErr())) {
NS_WARNING("HTMLEditor::MoveNodeWithTransaction() failed");
return moveNodeResult.propagateErr();
}
MoveNodeResult unwrappedMoveNodeResult = moveNodeResult.unwrap();
unwrappedMoveNodeResult.MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
return pointToPutCaret;
}
// Otherwise, wrap aContent in new <big> or <small>
Result<CreateElementResult, nsresult> wrapInBigOrSmallElementResult =
InsertContainerWithTransaction(aContent,
MOZ_KnownLive(*bigOrSmallTagName));
if (MOZ_UNLIKELY(wrapInBigOrSmallElementResult.isErr())) {
NS_WARNING("HTMLEditor::InsertContainerWithTransaction() failed");
return Err(wrapInBigOrSmallElementResult.unwrapErr());
}
CreateElementResult unwrappedWrapInBigOrSmallElementResult =
wrapInBigOrSmallElementResult.unwrap();
MOZ_ASSERT(unwrappedWrapInBigOrSmallElementResult.GetNewNode());
unwrappedWrapInBigOrSmallElementResult.MoveCaretPointTo(
pointToPutCaret, {SuggestCaret::OnlyIfHasSuggestion});
return pointToPutCaret;
}
// none of the above? then cycle through the children.
// MOOSE: we should group the children together if possible
// into a single "big" or "small". For the moment they are
// each getting their own.
EditorDOMPoint pointToPutCaret;
AutoTArray<OwningNonNull<nsIContent>, 32> arrayOfContents;
HTMLEditUtils::CollectAllChildren(aContent, arrayOfContents);
for (const auto& child : arrayOfContents) {
// MOZ_KnownLive because of bug 1622253
Result<EditorDOMPoint, nsresult> setFontSizeOfChildResult =
SetFontSizeWithBigOrSmallElement(MOZ_KnownLive(child),
aIncrementOrDecrement);
if (MOZ_UNLIKELY(setFontSizeOfChildResult.isErr())) {
NS_WARNING("HTMLEditor::SetFontSizeWithBigOrSmallElement() failed");
return setFontSizeOfChildResult;
}
if (setFontSizeOfChildResult.inspect().IsSet()) {
pointToPutCaret = setFontSizeOfChildResult.unwrap();
}
}
return pointToPutCaret;
}
NS_IMETHODIMP HTMLEditor::GetFontFaceState(bool* aMixed, nsAString& outFace) {
if (NS_WARN_IF(!aMixed)) {
return NS_ERROR_INVALID_ARG;
}
*aMixed = true;
outFace.Truncate();
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
}
bool first, any, all;
nsresult rv = GetInlinePropertyBase(
EditorInlineStyle(*nsGkAtoms::font, nsGkAtoms::face), nullptr, &first,
&any, &all, &outFace);
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::GetInlinePropertyBase(nsGkAtoms::font, nsGkAtoms::face) "
"failed");
return EditorBase::ToGenericNSResult(rv);
}
if (any && !all) {
return NS_OK; // mixed
}
if (all) {
*aMixed = false;
return NS_OK;
}
// if there is no font face, check for tt
rv = GetInlinePropertyBase(EditorInlineStyle(*nsGkAtoms::tt), nullptr, &first,
&any, &all, nullptr);
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::GetInlinePropertyBase(nsGkAtoms::tt) failed");
return EditorBase::ToGenericNSResult(rv);
}
if (any && !all) {
return NS_OK; // mixed
}
if (all) {
*aMixed = false;
outFace.AssignLiteral("tt");
}
if (!any) {
// there was no font face attrs of any kind. We are in normal font.
outFace.Truncate();
*aMixed = false;
}
return NS_OK;
}
nsresult HTMLEditor::GetFontColorState(bool* aMixed, nsAString& aOutColor) {
if (NS_WARN_IF(!aMixed)) {
return NS_ERROR_INVALID_ARG;
}
*aMixed = true;
aOutColor.Truncate();
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
}
bool first, any, all;
nsresult rv = GetInlinePropertyBase(
EditorInlineStyle(*nsGkAtoms::font, nsGkAtoms::color), nullptr, &first,
&any, &all, &aOutColor);
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::GetInlinePropertyBase(nsGkAtoms::font, nsGkAtoms::color) "
"failed");
return EditorBase::ToGenericNSResult(rv);
}
if (any && !all) {
return NS_OK; // mixed
}
if (all) {
*aMixed = false;
return NS_OK;
}
if (!any) {
// there was no font color attrs of any kind..
aOutColor.Truncate();
*aMixed = false;
}
return NS_OK;
}
// The return value is true only if the instance of the HTML editor we created
// can handle CSS styles and if the CSS preference is checked.
NS_IMETHODIMP HTMLEditor::GetIsCSSEnabled(bool* aIsCSSEnabled) {
*aIsCSSEnabled = IsCSSEnabled();
return NS_OK;
}
bool HTMLEditor::HasStyleOrIdOrClassAttribute(Element& aElement) {
return aElement.HasNonEmptyAttr(nsGkAtoms::style) ||
aElement.HasNonEmptyAttr(nsGkAtoms::_class) ||
aElement.HasNonEmptyAttr(nsGkAtoms::id);
}
} // namespace mozilla