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/StaticPrefs_editor.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 (IsInPlaintextMode()) {
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) {
for (const EditorInlineStyleAndValue& styleToSet : aStylesToSet) {
if (!StaticPrefs::
editor_inline_style_range_compatible_with_the_other_browsers() &&
!aRanges.IsCollapsed()) {
MOZ_ALWAYS_TRUE(aRanges.SaveAndTrackRanges(*this));
}
AutoInlineStyleSetter inlineStyleSetter(styleToSet);
for (OwningNonNull<nsRange>& domRange : aRanges.Ranges()) {
inlineStyleSetter.Reset();
auto rangeOrError =
[&]() MOZ_CAN_RUN_SCRIPT -> Result<EditorDOMRange, nsresult> {
if (aRanges.HasSavedRanges()) {
return EditorDOMRange(
GetExtendedRangeWrappingEntirelySelectedElements(
EditorRawDOMRange(domRange)));
}
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 (aRanges.HasSavedRanges()) {
return;
}
// 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();
}
if (aRanges.HasSavedRanges()) {
aRanges.RestoreFromSavedRanges();
}
}
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(kNameSpaceID_None, 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(kNameSpaceID_None, 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())) {
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,
aHTMLEditor.GetSplitNodeDirection());
}
// 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,
aHTMLEditor.GetSplitNodeDirection());
}
// 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()) ||
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)
? nextContentInRange
: nullptr;
}
// static
nsIContent* HTMLEditor::AutoInlineStyleSetter::GetPreviousEditableInlineContent(
const nsIContent& aContent,