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 "HTMLEditor.h"
#include "EditAction.h"
#include "EditorDOMAPIWrapper.h"
#include "EditorUtils.h"
#include "HTMLEditorNestedClasses.h"
#include "mozilla/Assertions.h"
#include "mozilla/Attributes.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/IMEStateManager.h"
#include "mozilla/Logging.h"
#include "mozilla/Maybe.h"
#include "mozilla/RefPtr.h"
#include "mozilla/dom/AncestorIterator.h"
#include "mozilla/dom/Element.h"
#include "mozInlineSpellChecker.h"
#include "nsContentUtils.h"
#include "nsIContent.h"
#include "nsIContentInlines.h"
#include "nsIMutationObserver.h"
#include "nsINode.h"
#include "nsRange.h"
#include "nsThreadUtils.h"
namespace mozilla {
using namespace dom;
/******************************************************************************
* DOM mutation logger
******************************************************************************/
// - HTMLEditorMutation:3: Logging only mutations in editable containers which
// is not expected.
// - HTMLEditorMutation:4: Logging only mutations in editable containers which
// is either expected or not expected.
// - HTMLEditorMutation:5: Logging any mutations including in
// non-editable containers.
LazyLogModule gHTMLEditorMutationLog("HTMLEditorMutation");
LogLevel HTMLEditor::MutationLogLevelOf(
nsIContent* aContent,
const CharacterDataChangeInfo* aCharacterDataChangeInfo,
DOMMutationType aDOMMutationType) const {
// Should be called only when the "info" level is enabled at least since we
// shouldn't add any new unnecessary calls in the hot paths when the logging
// is disabled.
MOZ_ASSERT(MOZ_LOG_TEST(gHTMLEditorMutationLog, LogLevel::Info));
if (MOZ_UNLIKELY(!aContent->IsInComposedDoc())) {
return LogLevel::Disabled;
}
Element* const containerElement = aContent->GetAsElementOrParentElement();
if (!containerElement || !containerElement->IsEditable()) {
return MOZ_LOG_TEST(gHTMLEditorMutationLog, LogLevel::Verbose)
? LogLevel::Verbose
: LogLevel::Disabled;
}
if (!mRunningDOMAPIWrapper) {
return LogLevel::Info;
}
switch (aDOMMutationType) {
case DOMMutationType::ContentAppended:
return mRunningDOMAPIWrapper->IsExpectedContentAppended(aContent)
? LogLevel::Debug
: LogLevel::Info;
case DOMMutationType::ContentInserted:
return mRunningDOMAPIWrapper->IsExpectedContentInserted(aContent)
? LogLevel::Debug
: LogLevel::Info;
case DOMMutationType::ContentWillBeRemoved:
return mRunningDOMAPIWrapper->IsExpectedContentWillBeRemoved(aContent)
? LogLevel::Debug
: LogLevel::Info;
case DOMMutationType::CharacterDataChanged:
MOZ_ASSERT(aCharacterDataChangeInfo);
return mRunningDOMAPIWrapper->IsExpectedCharacterDataChanged(
aContent, *aCharacterDataChangeInfo)
? LogLevel::Debug
: LogLevel::Info;
default:
MOZ_ASSERT_UNREACHABLE("Invalid DOMMutationType value");
return LogLevel::Disabled;
}
}
// - HTMLEditorAttrMutation:3: Logging only mutations of editable element which
// is not expected.
// - HTMLEditorAttrMutation:4: Logging only mutations of editable element which
// is either expected or not expected.
// - HTMLEditorAttrMutation:5: Logging any mutations including non-editable
// elements' attributes.
LazyLogModule gHTMLEditorAttrMutationLog("HTMLEditorAttrMutation");
LogLevel HTMLEditor::AttrMutationLogLevelOf(
Element* aElement, int32_t aNameSpaceID, nsAtom* aAttribute,
AttrModType aModType, const nsAttrValue* aOldValue) const {
// Should be called only when the "info" level is enabled at least since we
// shouldn't add any new unnecessary calls in the hot paths when the logging
// is disabled.
MOZ_ASSERT(MOZ_LOG_TEST(gHTMLEditorAttrMutationLog, LogLevel::Info));
if (MOZ_UNLIKELY(!aElement->IsInComposedDoc())) {
return LogLevel::Disabled;
}
if (!aElement->IsEditable()) {
return MOZ_LOG_TEST(gHTMLEditorAttrMutationLog, LogLevel::Verbose)
? LogLevel::Verbose
: LogLevel::Disabled;
}
if (!mRunningDOMAPIWrapper) {
return LogLevel::Info;
}
return mRunningDOMAPIWrapper->IsExpectedAttributeChanged(
aElement, aNameSpaceID, aAttribute, aModType, aOldValue)
? LogLevel::Debug
: LogLevel::Info;
}
void HTMLEditor::MaybeLogContentAppended(nsIContent* aFirstNewContent) const {
const LogLevel logLevel = MutationLogLevelOf(
aFirstNewContent, nullptr, DOMMutationType::ContentAppended);
if (logLevel == LogLevel::Disabled) {
return;
}
MOZ_LOG(
gHTMLEditorMutationLog, logLevel,
("%p %s ContentAppended: %s (previousSibling=%s, nextSibling=%s)", this,
logLevel == LogLevel::Debug ? "HTMLEditor " : "SomebodyElse",
NodeToString(aFirstNewContent).get(),
NodeToString(aFirstNewContent ? aFirstNewContent->GetPreviousSibling()
: nullptr)
.get(),
NodeToString(aFirstNewContent ? aFirstNewContent->GetNextSibling()
: nullptr)
.get()));
}
void HTMLEditor::MaybeLogContentInserted(nsIContent* aChild) const {
const LogLevel logLevel =
MutationLogLevelOf(aChild, nullptr, DOMMutationType::ContentInserted);
if (logLevel == LogLevel::Disabled) {
return;
}
MOZ_LOG(gHTMLEditorMutationLog, logLevel,
("%p %s ContentInserted: %s (previousSibling=%s, nextSibling=%s)",
this, logLevel == LogLevel::Debug ? "HTMLEditor " : "SomebodyElse",
NodeToString(aChild).get(),
NodeToString(aChild ? aChild->GetPreviousSibling() : nullptr).get(),
NodeToString(aChild ? aChild->GetNextSibling() : nullptr).get()));
}
void HTMLEditor::MaybeLogContentWillBeRemoved(nsIContent* aChild) const {
const LogLevel logLevel = MutationLogLevelOf(
aChild, nullptr, DOMMutationType::ContentWillBeRemoved);
if (logLevel == LogLevel::Disabled) {
return;
}
MOZ_LOG(
gHTMLEditorMutationLog, logLevel,
("%p %s ContentWillBeRemoved: %s (previousSibling=%s, nextSibling=%s)",
this, logLevel == LogLevel::Debug ? "HTMLEditor " : "SomebodyElse",
NodeToString(aChild).get(),
NodeToString(aChild ? aChild->GetPreviousSibling() : nullptr).get(),
NodeToString(aChild ? aChild->GetNextSibling() : nullptr).get()));
}
void HTMLEditor::MaybeLogCharacterDataChanged(
nsIContent* aContent, const CharacterDataChangeInfo& aInfo) const {
const LogLevel logLevel = MutationLogLevelOf(
aContent, &aInfo, DOMMutationType::CharacterDataChanged);
if (logLevel == LogLevel::Disabled) {
return;
}
nsAutoString data;
aContent->GetCharacterDataBuffer()->AppendTo(data);
MarkSelectionAndShrinkLongString shrunkenData(
data, aInfo.mChangeStart, aInfo.mChangeStart + aInfo.mReplaceLength);
MakeHumanFriendly(shrunkenData);
MOZ_LOG(
gHTMLEditorMutationLog, logLevel,
("%p %s CharacterDataChanged: %s, data=\"%s\", (length=%u), aInfo=%s",
this, logLevel == LogLevel::Debug ? "HTMLEditor " : "SomebodyElse",
ToString(*aContent).c_str(), NS_ConvertUTF16toUTF8(shrunkenData).get(),
aContent->Length(), ToString(aInfo).c_str()));
}
void HTMLEditor::MaybeLogAttributeChanged(Element* aElement,
int32_t aNameSpaceID,
nsAtom* aAttribute,
AttrModType aModType,
const nsAttrValue* aOldValue) const {
const LogLevel logLevel = AttrMutationLogLevelOf(
aElement, aNameSpaceID, aAttribute, aModType, aOldValue);
if (logLevel == LogLevel::Disabled) {
return;
}
nsAutoString value;
aElement->GetAttr(aAttribute, value);
MOZ_LOG(
gHTMLEditorAttrMutationLog, logLevel,
("%p %s AttributeChanged: %s of %s %s", this,
logLevel == LogLevel::Debug ? "HTMLEditor " : "SomebodyElse",
nsAutoAtomCString(aAttribute).get(), NodeToString(aElement).get(),
aModType == AttrModType::Removal
? "Removed"
: nsPrintfCString("to \"%s\"", NS_ConvertUTF16toUTF8(value).get())
.get()));
}
/******************************************************************************
* mozilla::HTMLEditor - Start/end of a DOM API call to modify the DOM
******************************************************************************/
const AutoDOMAPIWrapperBase* HTMLEditor::OnDOMAPICallStart(
const AutoDOMAPIWrapperBase& aWrapperBase) {
const AutoDOMAPIWrapperBase* const prevRunner = mRunningDOMAPIWrapper;
mRunningDOMAPIWrapper = &aWrapperBase;
MOZ_LOG(gHTMLEditorMutationLog, LogLevel::Warning,
(">>>> %p Calling DOM API: %s", this,
ToString(*mRunningDOMAPIWrapper).c_str()));
return prevRunner;
}
void HTMLEditor::OnDOMAPICallEnd(const AutoDOMAPIWrapperBase* aPrevWrapper) {
MOZ_LOG(gHTMLEditorMutationLog, LogLevel::Warning,
("<<<< %p Called DOM API: %s", this,
ToString(mRunningDOMAPIWrapper->Type()).c_str()));
mRunningDOMAPIWrapper = aPrevWrapper;
}
/******************************************************************************
* mozilla::HTMLEditor - mutation observers/handlers
******************************************************************************/
void HTMLEditor::NotifyRootChanged() {
MOZ_ASSERT(mPendingRootElementUpdatedRunner,
"HTMLEditor::NotifyRootChanged() should be called via a runner");
mPendingRootElementUpdatedRunner = nullptr;
nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return;
}
RemoveEventListeners();
nsresult rv = InstallEventListeners();
if (NS_FAILED(rv)) {
NS_WARNING("HTMLEditor::InstallEventListeners() failed, but ignored");
return;
}
UpdateRootElement();
if (MOZ_LIKELY(mRootElement)) {
rv = MaybeCollapseSelectionAtFirstEditableNode(false);
if (NS_FAILED(rv)) {
NS_WARNING(
"HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(false) "
"failed, "
"but ignored");
return;
}
// When this editor has focus, we need to reset the selection limiter to
// new root. Otherwise, that is going to be done when this gets focus.
nsCOMPtr<nsINode> node = GetFocusedNode();
if (node) {
DebugOnly<nsresult> rvIgnored = InitializeSelection(*node);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"EditorBase::InitializeSelection() failed, but ignored");
}
SyncRealTimeSpell();
}
RefPtr<Element> newRootElement(mRootElement);
IMEStateManager::OnUpdateHTMLEditorRootElement(*this, newRootElement);
}
// nsStubMutationObserver::ContentAppended override
MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentAppended(
nsIContent* aFirstNewContent, const ContentAppendInfo&) {
DoContentInserted(aFirstNewContent, ContentNodeIs::Appended);
}
// nsStubMutationObserver::ContentInserted override
MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentInserted(
nsIContent* aChild, const ContentInsertInfo&) {
DoContentInserted(aChild, ContentNodeIs::Inserted);
}
bool HTMLEditor::IsInObservedSubtree(nsIContent* aChild) {
if (!aChild) {
return false;
}
// FIXME(emilio, bug 1596856): This should probably work if the root is in the
// same shadow tree as the child, probably? I don't know what the
// contenteditable-in-shadow-dom situation is.
if (Element* root = GetRoot()) {
// To be super safe here, check both ChromeOnlyAccess and NAC / Shadow DOM.
// That catches (also unbound) native anonymous content and ShadowDOM.
if (root->ChromeOnlyAccess() != aChild->ChromeOnlyAccess() ||
root->IsInNativeAnonymousSubtree() !=
aChild->IsInNativeAnonymousSubtree() ||
root->IsInShadowTree() != aChild->IsInShadowTree()) {
return false;
}
}
return !aChild->ChromeOnlyAccess() && !aChild->IsInShadowTree() &&
!aChild->IsInNativeAnonymousSubtree();
}
bool HTMLEditor::ShouldReplaceRootElement() const {
if (!mRootElement) {
// If we don't know what is our root element, we should find our root.
return true;
}
// If we temporary set document root element to mRootElement, but there is
// body element now, we should replace the root element by the body element.
return mRootElement != GetBodyElement();
}
void HTMLEditor::DoContentInserted(nsIContent* aChild,
ContentNodeIs aContentNodeIs) {
MOZ_ASSERT(aChild);
nsINode* container = aChild->GetParentNode();
MOZ_ASSERT(container);
if (!IsInObservedSubtree(aChild)) {
return;
}
if (MOZ_LOG_TEST(gHTMLEditorMutationLog, LogLevel::Info)) {
if (aContentNodeIs == ContentNodeIs::Appended) {
MaybeLogContentAppended(aChild);
} else {
MOZ_ASSERT(aContentNodeIs == ContentNodeIs::Inserted);
MaybeLogContentInserted(aChild);
}
}
// XXX Why do we need this? This method is a helper of mutation observer.
// So, the callers of mutation observer should guarantee that this won't
// be deleted at least during the call.
RefPtr<HTMLEditor> kungFuDeathGrip(this);
// Do not create AutoEditActionDataSetter here because it grabs `Selection`,
// but that appear in the profile. If you need to create to it in some cases,
// you should do it in the minimum scope.
if (ShouldReplaceRootElement()) {
// Forget maybe disconnected root element right now because nobody should
// work with it.
mRootElement = nullptr;
if (mPendingRootElementUpdatedRunner) {
return;
}
mPendingRootElementUpdatedRunner = NewRunnableMethod(
"HTMLEditor::NotifyRootChanged", this, &HTMLEditor::NotifyRootChanged);
nsContentUtils::AddScriptRunner(
do_AddRef(mPendingRootElementUpdatedRunner));
return;
}
// We don't need to handle our own modifications
if (!GetTopLevelEditSubAction() && container->IsEditable()) {
if (EditorUtils::IsPaddingBRElementForEmptyEditor(*aChild)) {
// Ignore insertion of the padding <br> element.
return;
}
nsresult rv = RunOrScheduleOnModifyDocument();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return;
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::RunOrScheduleOnModifyDocument() failed, but ignored");
// Update spellcheck for only the newly-inserted node (bug 743819)
if (mInlineSpellChecker) {
nsIContent* endContent = aChild;
if (aContentNodeIs == ContentNodeIs::Appended) {
nsIContent* child = nullptr;
for (child = aChild; child; child = child->GetNextSibling()) {
if (child->InclusiveDescendantMayNeedSpellchecking(this)) {
break;
}
}
if (!child) {
// No child needed spellchecking, return.
return;
}
// Maybe more than 1 child was appended.
endContent = container->GetLastChild();
} else if (!aChild->InclusiveDescendantMayNeedSpellchecking(this)) {
return;
}
RefPtr<nsRange> range = nsRange::Create(aChild);
range->SelectNodesInContainer(container, aChild, endContent);
DebugOnly<nsresult> rvIgnored =
mInlineSpellChecker->SpellCheckRange(range);
NS_WARNING_ASSERTION(
rvIgnored == NS_ERROR_NOT_INITIALIZED || NS_SUCCEEDED(rvIgnored),
"mozInlineSpellChecker::SpellCheckRange() failed, but ignored");
}
}
}
// nsStubMutationObserver::ContentWillBeRemoved override
MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentWillBeRemoved(
nsIContent* aChild, const ContentRemoveInfo&) {
if (mLastCollapsibleWhiteSpaceAppendedTextNode == aChild) {
mLastCollapsibleWhiteSpaceAppendedTextNode = nullptr;
}
if (!IsInObservedSubtree(aChild)) {
return;
}
if (MOZ_LOG_TEST(gHTMLEditorMutationLog, LogLevel::Info)) {
MaybeLogContentWillBeRemoved(aChild);
}
// XXX Why do we need to do this? This method is a mutation observer's
// method. Therefore, the caller should guarantee that this won't be
// deleted during the call.
RefPtr<HTMLEditor> kungFuDeathGrip(this);
// Do not create AutoEditActionDataSetter here because it grabs `Selection`,
// but that appear in the profile. If you need to create to it in some cases,
// you should do it in the minimum scope.
// FYI: mRootElement may be the <body> of the document or the root element.
// Therefore, we don't need to check it across shadow DOM boundaries.
if (mRootElement && mRootElement->IsInclusiveDescendantOf(aChild)) {
// Forget the disconnected root element right now because nobody should work
// with it.
mRootElement = nullptr;
if (mPendingRootElementUpdatedRunner) {
return;
}
mPendingRootElementUpdatedRunner = NewRunnableMethod(
"HTMLEditor::NotifyRootChanged", this, &HTMLEditor::NotifyRootChanged);
nsContentUtils::AddScriptRunner(
do_AddRef(mPendingRootElementUpdatedRunner));
return;
}
// We don't need to handle our own modifications
if (!GetTopLevelEditSubAction() && aChild->GetParentNode()->IsEditable()) {
if (aChild && EditorUtils::IsPaddingBRElementForEmptyEditor(*aChild)) {
// Ignore removal of the padding <br> element for empty editor.
return;
}
nsresult rv = RunOrScheduleOnModifyDocument(aChild);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return;
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"HTMLEditor::RunOrScheduleOnModifyDocument() failed, but ignored");
}
}
// nsStubMutationObserver::CharacterDataChanged override
MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::CharacterDataChanged(
nsIContent* aContent, const CharacterDataChangeInfo& aInfo) {
if (!IsInObservedSubtree(aContent)) {
return;
}
if (MOZ_LOG_TEST(gHTMLEditorMutationLog, LogLevel::Info)) {
MaybeLogCharacterDataChanged(aContent, aInfo);
}
if (!mInlineSpellChecker || !aContent->IsEditable() ||
GetTopLevelEditSubAction() != EditSubAction::eNone) {
return;
}
nsIContent* parent = aContent->GetParent();
if (!parent || !parent->InclusiveDescendantMayNeedSpellchecking(this)) {
return;
}
RefPtr<nsRange> range = nsRange::Create(aContent);
range->SelectNodesInContainer(parent, aContent, aContent);
DebugOnly<nsresult> rvIgnored = mInlineSpellChecker->SpellCheckRange(range);
}
nsresult HTMLEditor::RunOrScheduleOnModifyDocument(
const nsIContent* aContentWillBeRemoved /* = nullptr */) {
if (mPendingDocumentModifiedRunner) {
return NS_OK; // We've already posted same runnable into the queue.
}
mPendingDocumentModifiedRunner = new DocumentModifiedEvent(*this);
nsContentUtils::AddScriptRunner(do_AddRef(mPendingDocumentModifiedRunner));
// Be aware, if OnModifyDocument() may be called synchronously, the
// editor might have been destroyed here.
return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
}
// nsStubMutationObserver::AttributeChanged override
MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::AttributeChanged(
Element* aElement, int32_t aNameSpaceID, nsAtom* aAttribute,
AttrModType aModType, const nsAttrValue* aOldValue) {
if (MOZ_LOG_TEST(gHTMLEditorAttrMutationLog, LogLevel::Info) &&
IsInObservedSubtree(aElement)) {
MaybeLogAttributeChanged(aElement, aNameSpaceID, aAttribute, aModType,
aOldValue);
}
}
nsresult HTMLEditor::OnModifyDocument(const DocumentModifiedEvent& aRunner) {
MOZ_ASSERT(mPendingDocumentModifiedRunner,
"HTMLEditor::OnModifyDocument() should be called via a runner");
MOZ_ASSERT(&aRunner == mPendingDocumentModifiedRunner);
mPendingDocumentModifiedRunner = nullptr;
Maybe<AutoEditActionDataSetter> editActionData;
if (!IsEditActionDataAvailable()) {
editActionData.emplace(*this,
EditAction::eCreatePaddingBRElementForEmptyEditor);
if (NS_WARN_IF(!editActionData->CanHandle())) {
return NS_ERROR_NOT_AVAILABLE;
}
}
// EnsureNoPaddingBRElementForEmptyEditor() below may cause a flush, which
// could destroy the editor
nsAutoScriptBlockerSuppressNodeRemoved scriptBlocker;
// Delete our padding <br> element for empty editor, if we have one, since
// the document might not be empty any more.
nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return rv;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
"failed, but ignored");
// Try to recreate the padding <br> element for empty editor if needed.
rv = MaybeCreatePaddingBRElementForEmptyEditor();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"EditorBase::MaybeCreatePaddingBRElementForEmptyEditor() failed");
return rv;
}
} // namespace mozilla