/* -*- 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 "HTMLEditUtils.h"
#include "AutoClonedRangeArray.h" // for AutoClonedRangeArray
#include "CSSEditUtils.h" // for CSSEditUtils
#include "EditAction.h" // for EditAction
#include "EditorBase.h" // for EditorBase, EditorType
#include "EditorDOMPoint.h" // for EditorDOMPoint, etc.
#include "EditorForwards.h" // for CollectChildrenOptions
#include "EditorUtils.h" // for EditorUtils
#include "HTMLEditHelpers.h" // for EditorInlineStyle
#include "WSRunScanner.h" // for WSRunScanner
#include "mozilla/ArrayUtils.h" // for ArrayLength
#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc.
#include "mozilla/Attributes.h"
#include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_
#include "mozilla/RangeUtils.h" // for RangeUtils
#include "mozilla/dom/DocumentInlines.h" // for GetBodyElement()
#include "mozilla/dom/Element.h" // for Element, nsINode
#include "mozilla/dom/ElementInlines.h" // for IsContentEditablePlainTextOnly()
#include "mozilla/dom/HTMLAnchorElement.h"
#include "mozilla/dom/HTMLBodyElement.h"
#include "mozilla/dom/HTMLInputElement.h"
#include "mozilla/ServoCSSParser.h" // for ServoCSSParser
#include "mozilla/dom/StaticRange.h"
#include "mozilla/dom/Text.h" // for Text
#include "nsAString.h" // for nsAString::IsEmpty
#include "nsAtom.h" // for nsAtom
#include "nsAttrValue.h" // nsAttrValue
#include "nsCaseTreatment.h"
#include "nsCOMPtr.h" // for nsCOMPtr, operator==, etc.
#include "nsComputedDOMStyle.h" // for nsComputedDOMStyle
#include "nsDebug.h" // for NS_ASSERTION, etc.
#include "nsElementTable.h" // for nsHTMLElement
#include "nsError.h" // for NS_SUCCEEDED
#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::a, etc.
#include "nsHTMLTags.h"
#include "nsIContentInlines.h" // for nsIContent::IsInDesignMode(), etc.
#include "nsIFrameInlines.h" // for nsIFrame::IsFlexOrGridItem()
#include "nsLiteralString.h" // for NS_LITERAL_STRING
#include "nsNameSpaceManager.h" // for kNameSpaceID_None
#include "nsPrintfCString.h" // nsPringfCString
#include "nsString.h" // for nsAutoString
#include "nsStyledElement.h"
#include "nsStyleStruct.h" // for StyleDisplay
#include "nsStyleUtil.h" // for nsStyleUtil
#include "nsTextFragment.h" // for nsTextFragment
#include "nsTextFrame.h" // for nsTextFrame
namespace mozilla {
using namespace dom;
using EditorType = EditorBase::EditorType;
template nsIContent* HTMLEditUtils::GetPreviousContent(
const EditorDOMPoint& aPoint,
const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck,
const Element* aAncestorLimiter);
template nsIContent* HTMLEditUtils::GetPreviousContent(
const EditorRawDOMPoint& aPoint,
const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck,
const Element* aAncestorLimiter);
template nsIContent* HTMLEditUtils::GetPreviousContent(
const EditorDOMPointInText& aPoint,
const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck,
const Element* aAncestorLimiter);
template nsIContent* HTMLEditUtils::GetPreviousContent(
const EditorRawDOMPointInText& aPoint,
const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck,
const Element* aAncestorLimiter);
template nsIContent* HTMLEditUtils::GetNextContent(
const EditorDOMPoint& aPoint,
const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck,
const Element* aAncestorLimiter);
template nsIContent* HTMLEditUtils::GetNextContent(
const EditorRawDOMPoint& aPoint,
const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck,
const Element* aAncestorLimiter);
template nsIContent* HTMLEditUtils::GetNextContent(
const EditorDOMPointInText& aPoint,
const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck,
const Element* aAncestorLimiter);
template nsIContent* HTMLEditUtils::GetNextContent(
const EditorRawDOMPointInText& aPoint,
const WalkTreeOptions& aOptions,
BlockInlineCheck aBlockInlineCheck,
const Element* aAncestorLimiter);
template EditorDOMPoint HTMLEditUtils::GetPreviousEditablePoint(
nsIContent& aContent,
const Element* aAncestorLimiter,
InvisibleWhiteSpaces aInvisibleWhiteSpaces,
TableBoundary aHowToTreatTableBoundary);
template EditorRawDOMPoint HTMLEditUtils::GetPreviousEditablePoint(
nsIContent& aContent,
const Element* aAncestorLimiter,
InvisibleWhiteSpaces aInvisibleWhiteSpaces,
TableBoundary aHowToTreatTableBoundary);
template EditorDOMPoint HTMLEditUtils::GetNextEditablePoint(
nsIContent& aContent,
const Element* aAncestorLimiter,
InvisibleWhiteSpaces aInvisibleWhiteSpaces,
TableBoundary aHowToTreatTableBoundary);
template EditorRawDOMPoint HTMLEditUtils::GetNextEditablePoint(
nsIContent& aContent,
const Element* aAncestorLimiter,
InvisibleWhiteSpaces aInvisibleWhiteSpaces,
TableBoundary aHowToTreatTableBoundary);
template EditorDOMPoint HTMLEditUtils::LineRequiresPaddingLineBreakToBeVisible(
const EditorDOMPoint& aPoint,
const Element& aEditingHost);
template EditorDOMPoint HTMLEditUtils::LineRequiresPaddingLineBreakToBeVisible(
const EditorRawDOMPoint& aPoint,
const Element& aEditingHost);
template EditorDOMPoint HTMLEditUtils::LineRequiresPaddingLineBreakToBeVisible(
const EditorDOMPointInText& aPoint,
const Element& aEditingHost);
template EditorDOMPoint HTMLEditUtils::LineRequiresPaddingLineBreakToBeVisible(
const EditorRawDOMPointInText& aPoint,
const Element& aEditingHost);
template nsIContent* HTMLEditUtils::GetContentToPreserveInlineStyles(
const EditorDOMPoint& aPoint,
const Element& aEditingHost);
template nsIContent* HTMLEditUtils::GetContentToPreserveInlineStyles(
const EditorRawDOMPoint& aPoint,
const Element& aEditingHost);
template EditorDOMPoint HTMLEditUtils::GetBetterInsertionPointFor(
const nsIContent& aContentToInsert,
const EditorDOMPoint& aPointToInsert);
template EditorRawDOMPoint HTMLEditUtils::GetBetterInsertionPointFor(
const nsIContent& aContentToInsert,
const EditorRawDOMPoint& aPointToInsert);
template EditorDOMPoint HTMLEditUtils::GetBetterInsertionPointFor(
const nsIContent& aContentToInsert,
const EditorRawDOMPoint& aPointToInsert);
template EditorRawDOMPoint HTMLEditUtils::GetBetterInsertionPointFor(
const nsIContent& aContentToInsert,
const EditorDOMPoint& aPointToInsert);
template EditorDOMPoint HTMLEditUtils::GetBetterCaretPositionToInsertText(
const EditorDOMPoint& aPoint);
template EditorDOMPoint HTMLEditUtils::GetBetterCaretPositionToInsertText(
const EditorRawDOMPoint& aPoint);
template EditorRawDOMPoint HTMLEditUtils::GetBetterCaretPositionToInsertText(
const EditorDOMPoint& aPoint);
template EditorRawDOMPoint HTMLEditUtils::GetBetterCaretPositionToInsertText(
const EditorRawDOMPoint& aPoint);
template Result<EditorDOMPoint, nsresult>
HTMLEditUtils::ComputePointToPutCaretInElementIfOutside(
const Element& aElement,
const EditorDOMPoint& aCurrentPoint);
template Result<EditorRawDOMPoint, nsresult>
HTMLEditUtils::ComputePointToPutCaretInElementIfOutside(
const Element& aElement,
const EditorDOMPoint& aCurrentPoint);
template Result<EditorDOMPoint, nsresult>
HTMLEditUtils::ComputePointToPutCaretInElementIfOutside(
const Element& aElement,
const EditorRawDOMPoint& aCurrentPoint);
template Result<EditorRawDOMPoint, nsresult>
HTMLEditUtils::ComputePointToPutCaretInElementIfOutside(
const Element& aElement,
const EditorRawDOMPoint& aCurrentPoint);
template bool HTMLEditUtils::IsSameCSSColorValue(
const nsAString& aColorA,
const nsAString& aColorB);
template bool HTMLEditUtils::IsSameCSSColorValue(
const nsACString& aColorA,
const nsACString& aColorB);
template Maybe<EditorLineBreak> HTMLEditUtils::GetFollowingUnnecessaryLineBreak(
const EditorDOMPoint& aPoint);
template Maybe<EditorLineBreak> HTMLEditUtils::GetFollowingUnnecessaryLineBreak(
const EditorRawDOMPoint& aPoint);
template Maybe<EditorLineBreak> HTMLEditUtils::GetFollowingUnnecessaryLineBreak(
const EditorDOMPointInText& aPoint);
template Maybe<EditorLineBreak> HTMLEditUtils::GetFollowingUnnecessaryLineBreak(
const EditorRawDOMPointInText& aPoint);
template Maybe<EditorRawLineBreak>
HTMLEditUtils::GetFollowingUnnecessaryLineBreak(
const EditorDOMPoint& aPoint);
template Maybe<EditorRawLineBreak>
HTMLEditUtils::GetFollowingUnnecessaryLineBreak(
const EditorRawDOMPoint& aPoint);
template Maybe<EditorRawLineBreak>
HTMLEditUtils::GetFollowingUnnecessaryLineBreak(
const EditorDOMPointInText& aPoint);
template Maybe<EditorRawLineBreak>
HTMLEditUtils::GetFollowingUnnecessaryLineBreak(
const EditorRawDOMPointInText& aPoint);
template bool HTMLEditUtils::PointIsImmediatelyBeforeCurrentBlockBoundary(
const EditorDOMPoint& aPoint,
IgnoreInvisibleLineBreak aIgnoreInvisibleLineBreak);
template bool HTMLEditUtils::PointIsImmediatelyBeforeCurrentBlockBoundary(
const EditorRawDOMPoint& aPoint,
IgnoreInvisibleLineBreak aIgnoreInvisibleLineBreak);
template bool HTMLEditUtils::PointIsImmediatelyBeforeCurrentBlockBoundary(
const EditorDOMPointInText& aPoint,
IgnoreInvisibleLineBreak aIgnoreInvisibleLineBreak);
template bool HTMLEditUtils::PointIsImmediatelyBeforeCurrentBlockBoundary(
const EditorRawDOMPointInText& aPoint,
IgnoreInvisibleLineBreak aIgnoreInvisibleLineBreak);
template Maybe<EditorLineBreak> HTMLEditUtils::GetUnnecessaryLineBreak(
const Element& aBlockElement, ScanLineBreak aScanLineBreak);
template Maybe<EditorRawLineBreak> HTMLEditUtils::GetUnnecessaryLineBreak(
const Element& aBlockElement, ScanLineBreak aScanLineBreak);
bool HTMLEditUtils::ElementIsEditableRoot(
const Element& aElement) {
MOZ_ASSERT(!aElement.IsInNativeAnonymousSubtree());
if (NS_WARN_IF(!aElement.IsEditable()) ||
NS_WARN_IF(!aElement.IsInComposedDoc())) {
return false;
}
return !aElement.GetParent() ||
// root element
!aElement.GetParent()->IsEditable() ||
// editing host
aElement.OwnerDoc()->GetBody() == &aElement;
// the <body>
}
bool HTMLEditUtils::CanContentsBeJoined(
const nsIContent& aLeftContent,
const nsIContent& aRightContent) {
if (aLeftContent.NodeInfo()->NameAtom() !=
aRightContent.NodeInfo()->NameAtom()) {
return false;
}
if (!aLeftContent.IsElement()) {
return true;
// can join text nodes, etc
}
MOZ_ASSERT(aRightContent.IsElement());
if (aLeftContent.NodeInfo()->NameAtom() == nsGkAtoms::font) {
const nsAttrValue*
const leftSize =
aLeftContent.AsElement()->GetParsedAttr(nsGkAtoms::size);
const nsAttrValue*
const rightSize =
aRightContent.AsElement()->GetParsedAttr(nsGkAtoms::size);
if (!leftSize ^ !rightSize || (leftSize && !leftSize->Equals(*rightSize))) {
return false;
}
const nsAttrValue*
const leftColor =
aLeftContent.AsElement()->GetParsedAttr(nsGkAtoms::color);
const nsAttrValue*
const rightColor =
aRightContent.AsElement()->GetParsedAttr(nsGkAtoms::color);
if (!leftColor ^ !rightColor ||
(leftColor && !leftColor->Equals(*rightColor))) {
return false;
}
const nsAttrValue*
const leftFace =
aLeftContent.AsElement()->GetParsedAttr(nsGkAtoms::face);
const nsAttrValue*
const rightFace =
aRightContent.AsElement()->GetParsedAttr(nsGkAtoms::face);
if (!leftFace ^ !rightFace || (leftFace && !leftFace->Equals(*rightFace))) {
return false;
}
}
nsStyledElement* leftStyledElement =
nsStyledElement::FromNode(
const_cast<nsIContent*>(&aLeftContent));
if (!leftStyledElement) {
return false;
}
nsStyledElement* rightStyledElement =
nsStyledElement::FromNode(
const_cast<nsIContent*>(&aRightContent));
if (!rightStyledElement) {
return false;
}
return CSSEditUtils::DoStyledElementsHaveSameStyle(*leftStyledElement,
*rightStyledElement);
}
static bool IsHTMLBlockElementByDefault(
const nsIContent& aContent) {
if (!aContent.IsHTMLElement()) {
return false;
}
if (aContent.IsHTMLElement(nsGkAtoms::br)) {
// shortcut for TextEditor
MOZ_ASSERT(!nsHTMLElement::IsBlock(
nsHTMLTags::CaseSensitiveAtomTagToId(nsGkAtoms::br)));
return false;
}
// We want to treat these as block nodes even though nsHTMLElement says
// they're not.
if (aContent.IsAnyOfHTMLElements(
nsGkAtoms::body, nsGkAtoms::head, nsGkAtoms::tbody, nsGkAtoms::thead,
nsGkAtoms::tfoot, nsGkAtoms::tr, nsGkAtoms::th, nsGkAtoms::td,
nsGkAtoms::dt, nsGkAtoms::dd)) {
return true;
}
return nsHTMLElement::IsBlock(
nsHTMLTags::CaseSensitiveAtomTagToId(aContent.NodeInfo()->NameAtom()));
}
bool HTMLEditUtils::IsBlockElement(
const nsIContent& aContent,
BlockInlineCheck aBlockInlineCheck) {
MOZ_ASSERT(aBlockInlineCheck != BlockInlineCheck::Unused);
if (MOZ_UNLIKELY(!aContent.IsElement())) {
return false;
}
// If it's a <br>, we should always treat it as an inline element because
// its preceding collapse white-spaces and another <br> works same as usual
// even if you set its style to `display:block`.
if (aContent.IsHTMLElement(nsGkAtoms::br)) {
return false;
}
if (aBlockInlineCheck == BlockInlineCheck::UseHTMLDefaultStyle) {
return IsHTMLBlockElementByDefault(aContent);
}
// Let's treat the document element and the body element is a block to avoid
// complicated things which may be detected by fuzzing.
if (aContent.OwnerDoc()->GetDocumentElement() == &aContent ||
(aContent.IsHTMLElement(nsGkAtoms::body) &&
aContent.OwnerDoc()->GetBodyElement() == &aContent)) {
return true;
}
RefPtr<
const ComputedStyle> elementStyle =
nsComputedDOMStyle::GetComputedStyleNoFlush(aContent.AsElement());
if (MOZ_UNLIKELY(!elementStyle)) {
// If aContent is not in the composed tree
return IsHTMLBlockElementByDefault(aContent);
}
const nsStyleDisplay* styleDisplay = elementStyle->StyleDisplay();
if (MOZ_UNLIKELY(styleDisplay->mDisplay == StyleDisplay::None)) {
// Typically, we should not keep handling editing in invisible nodes, but if
// we reach here, let's fallback to the default style for protecting the
// structure as far as possible.
return IsHTMLBlockElementByDefault(aContent);
}
// Both Blink and WebKit treat ruby style as a block, see IsEnclosingBlock()
// in Chromium or isBlock() in WebKit.
if (styleDisplay->IsRubyDisplayType()) {
return true;
}
// If the outside is not inline, treat it as block.
if (!styleDisplay->IsInlineOutsideStyle()) {
return true;
}
// If we're checking display-inside, inline-block, etc should be a block too.
return aBlockInlineCheck == BlockInlineCheck::UseComputedDisplayStyle &&
styleDisplay->DisplayInside() == StyleDisplayInside::FlowRoot &&
// Treat widgets as inline since they won't hide collapsible
// white-spaces around them.
styleDisplay->EffectiveAppearance() == StyleAppearance::None;
}
bool HTMLEditUtils::IsInlineContent(
const nsIContent& aContent,
BlockInlineCheck aBlockInlineCheck) {
MOZ_ASSERT(aBlockInlineCheck != BlockInlineCheck::Unused);
if (!aContent.IsElement()) {
return true;
}
// If it's a <br>, we should always treat it as an inline element because
// its preceding collapse white-spaces and another <br> works same as usual
// even if you set its style to `display:block`.
if (aContent.IsHTMLElement(nsGkAtoms::br)) {
return true;
}
if (aBlockInlineCheck == BlockInlineCheck::UseHTMLDefaultStyle) {
return !IsHTMLBlockElementByDefault(aContent);
}
// Let's treat the document element and the body element is a block to avoid
// complicated things which may be detected by fuzzing.
if (aContent.OwnerDoc()->GetDocumentElement() == &aContent ||
(aContent.IsHTMLElement(nsGkAtoms::body) &&
aContent.OwnerDoc()->GetBodyElement() == &aContent)) {
return false;
}
RefPtr<
const ComputedStyle> elementStyle =
nsComputedDOMStyle::GetComputedStyleNoFlush(aContent.AsElement());
if (MOZ_UNLIKELY(!elementStyle)) {
// If aContent is not in the composed tree
return !IsHTMLBlockElementByDefault(aContent);
}
const nsStyleDisplay* styleDisplay = elementStyle->StyleDisplay();
if (MOZ_UNLIKELY(styleDisplay->mDisplay == StyleDisplay::None)) {
// Similar to IsBlockElement, let's fallback to refer the default style.
// Note that if you change here, you may need to check the parent element
// style if aContent.
return !IsHTMLBlockElementByDefault(aContent);
}
// Different block IsBlockElement, when the display-outside is inline, it's
// simply an inline element.
return styleDisplay->IsInlineOutsideStyle() ||
styleDisplay->IsRubyDisplayType();
}
bool HTMLEditUtils::IsFlexOrGridItem(
const Element& aElement) {
nsIFrame* frame = aElement.GetPrimaryFrame();
return frame && frame->IsFlexOrGridItem();
}
bool HTMLEditUtils::IsInclusiveAncestorCSSDisplayNone(
const nsIContent& aContent) {
if (NS_WARN_IF(!aContent.IsInComposedDoc())) {
return true;
}
for (
const Element* element :
aContent.InclusiveFlatTreeAncestorsOfType<Element>()) {
RefPtr<
const ComputedStyle> elementStyle =
nsComputedDOMStyle::GetComputedStyleNoFlush(element);
if (NS_WARN_IF(!elementStyle)) {
continue;
}
const nsStyleDisplay* styleDisplay = elementStyle->StyleDisplay();
if (MOZ_UNLIKELY(styleDisplay->mDisplay == StyleDisplay::None)) {
return true;
}
}
return false;
}
bool HTMLEditUtils::IsVisibleElementEvenIfLeafNode(
const nsIContent& aContent) {
if (!aContent.IsElement()) {
return false;
}
// Assume non-HTML element is visible.
if (!aContent.IsHTMLElement()) {
return true;
}
// XXX Should we return false if the element is display:none?
if (HTMLEditUtils::IsBlockElement(
aContent, BlockInlineCheck::UseComputedDisplayStyle)) {
return true;
}
if (aContent.IsAnyOfHTMLElements(nsGkAtoms::applet, nsGkAtoms::iframe,
nsGkAtoms::img, nsGkAtoms::meter,
nsGkAtoms::progress, nsGkAtoms::select,
nsGkAtoms::textarea)) {
return true;
}
if (
const HTMLInputElement* inputElement =
HTMLInputElement::FromNode(&aContent)) {
return inputElement->ControlType() != FormControlType::InputHidden;
}
// If the element has a primary frame and it's not empty, the element is
// visible.
// XXX This method does not guarantee that the layout has already been
// updated. Therefore, this check might be wrong in the edge cases.
// However, basically, editor apps should not depend on this path, this
// is required if last <br> before a block boundary becomes visible because
// of followed by empty but styled frame like <span style=padding:1px></span>.
if (aContent.GetPrimaryFrame() &&
!aContent.GetPrimaryFrame()->GetSize().IsEmpty()) {
return true;
}
// Maybe, empty inline element such as <span>.
return false;
}
/**
* IsInlineStyle() returns true if aNode is an inline style.
*/
bool HTMLEditUtils::IsInlineStyle(nsINode* aNode) {
MOZ_ASSERT(aNode);
return aNode->IsAnyOfHTMLElements(
nsGkAtoms::b, nsGkAtoms::i, nsGkAtoms::u, nsGkAtoms::tt, nsGkAtoms::s,
nsGkAtoms::strike, nsGkAtoms::big, nsGkAtoms::small, nsGkAtoms::sub,
nsGkAtoms::sup, nsGkAtoms::font);
}
bool HTMLEditUtils::IsDisplayOutsideInline(
const Element& aElement) {
RefPtr<
const ComputedStyle> elementStyle =
nsComputedDOMStyle::GetComputedStyleNoFlush(&aElement);
if (!elementStyle) {
return false;
}
return elementStyle->StyleDisplay()->DisplayOutside() ==
StyleDisplayOutside::
Inline;
}
bool HTMLEditUtils::IsDisplayInsideFlowRoot(
const Element& aElement) {
RefPtr<
const ComputedStyle> elementStyle =
nsComputedDOMStyle::GetComputedStyleNoFlush(&aElement);
if (!elementStyle) {
return false;
}
return elementStyle->StyleDisplay()->DisplayInside() ==
StyleDisplayInside::FlowRoot;
}
bool HTMLEditUtils::IsRemovableInlineStyleElement(Element& aElement) {
if (!aElement.IsHTMLElement()) {
return false;
}
// https://w3c.github.io/editing/execCommand.html#removeformat-candidate
if (aElement.IsAnyOfHTMLElements(
nsGkAtoms::abbr,
// Chrome ignores, but does not make sense.
nsGkAtoms::acronym, nsGkAtoms::b,
nsGkAtoms::bdi,
// Chrome ignores, but does not make sense.
nsGkAtoms::bdo, nsGkAtoms::big, nsGkAtoms::cite, nsGkAtoms::code,
// nsGkAtoms::del, Chrome ignores, but does not make sense but
// execCommand unofficial draft excludes this. Spec issue:
// https://github.com/w3c/editing/issues/192
nsGkAtoms::dfn, nsGkAtoms::em, nsGkAtoms::font, nsGkAtoms::i,
nsGkAtoms::ins, nsGkAtoms::kbd,
nsGkAtoms::mark,
// Chrome ignores, but does not make sense.
nsGkAtoms::nobr, nsGkAtoms::q, nsGkAtoms::s, nsGkAtoms::samp,
nsGkAtoms::small, nsGkAtoms::span, nsGkAtoms::strike,
nsGkAtoms::strong, nsGkAtoms::sub, nsGkAtoms::sup, nsGkAtoms::tt,
nsGkAtoms::u, nsGkAtoms::var)) {
return true;
}
// If it's a <blink> element, we can remove it.
nsAutoString tagName;
aElement.GetTagName(tagName);
return tagName.LowerCaseEqualsASCII(
"blink");
}
/**
* IsNodeThatCanOutdent() returns true if aNode is a list, list item or
* blockquote.
*/
bool HTMLEditUtils::IsNodeThatCanOutdent(nsINode* aNode) {
MOZ_ASSERT(aNode);
return aNode->IsAnyOfHTMLElements(nsGkAtoms::ul, nsGkAtoms::ol, nsGkAtoms::dl,
nsGkAtoms::li, nsGkAtoms::dd, nsGkAtoms::dt,
nsGkAtoms::blockquote);
}
/**
* IsHeader() returns true if aNode is an html header.
*/
bool HTMLEditUtils::IsHeader(nsINode& aNode) {
return aNode.IsAnyOfHTMLElements(nsGkAtoms::h1, nsGkAtoms::h2, nsGkAtoms::h3,
nsGkAtoms::h4, nsGkAtoms::h5, nsGkAtoms::h6);
}
/**
* IsListItem() returns true if aNode is an html list item.
*/
bool HTMLEditUtils::IsListItem(
const nsINode* aNode) {
MOZ_ASSERT(aNode);
return aNode->IsAnyOfHTMLElements(nsGkAtoms::li, nsGkAtoms::dd,
nsGkAtoms::dt);
}
/**
* IsAnyTableElement() returns true if aNode is an html table, td, tr, ...
*/
bool HTMLEditUtils::IsAnyTableElement(
const nsINode* aNode) {
MOZ_ASSERT(aNode);
return aNode->IsAnyOfHTMLElements(
nsGkAtoms::table, nsGkAtoms::tr, nsGkAtoms::td, nsGkAtoms::th,
nsGkAtoms::thead, nsGkAtoms::tfoot, nsGkAtoms::tbody, nsGkAtoms::caption);
}
/**
* IsAnyTableElementButNotTable() returns true if aNode is an html td, tr, ...
* (doesn't include table)
*/
bool HTMLEditUtils::IsAnyTableElementButNotTable(nsINode* aNode) {
MOZ_ASSERT(aNode);
return aNode->IsAnyOfHTMLElements(nsGkAtoms::tr, nsGkAtoms::td, nsGkAtoms::th,
nsGkAtoms::thead, nsGkAtoms::tfoot,
nsGkAtoms::tbody, nsGkAtoms::caption);
}
/**
* IsTable() returns true if aNode is an html table.
*/
bool HTMLEditUtils::IsTable(nsINode* aNode) {
return aNode && aNode->IsHTMLElement(nsGkAtoms::table);
}
/**
* IsTableRow() returns true if aNode is an html tr.
*/
bool HTMLEditUtils::IsTableRow(nsINode* aNode) {
return aNode && aNode->IsHTMLElement(nsGkAtoms::tr);
}
/**
* IsTableCell() returns true if aNode is an html td or th.
*/
bool HTMLEditUtils::IsTableCell(
const nsINode* aNode) {
MOZ_ASSERT(aNode);
return aNode->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th);
}
/**
* IsTableCellOrCaption() returns true if aNode is an html td or th or caption.
*/
bool HTMLEditUtils::IsTableCellOrCaption(nsINode& aNode) {
return aNode.IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th,
nsGkAtoms::caption);
}
/**
* IsAnyListElement() returns true if aNode is an html list.
*/
bool HTMLEditUtils::IsAnyListElement(
const nsINode* aNode) {
MOZ_ASSERT(aNode);
return aNode->IsAnyOfHTMLElements(nsGkAtoms::ul, nsGkAtoms::ol,
nsGkAtoms::dl);
}
/**
* IsPre() returns true if aNode is an html pre node.
*/
bool HTMLEditUtils::IsPre(
const nsINode* aNode) {
return aNode && aNode->IsHTMLElement(nsGkAtoms::pre);
}
/**
* IsImage() returns true if aNode is an html image node.
*/
bool HTMLEditUtils::IsImage(nsINode* aNode) {
return aNode && aNode->IsHTMLElement(nsGkAtoms::img);
}
bool HTMLEditUtils::IsLink(
const nsINode* aNode) {
MOZ_ASSERT(aNode);
if (!aNode->IsContent()) {
return false;
}
RefPtr<
const dom::HTMLAnchorElement> anchor =
dom::HTMLAnchorElement::FromNodeOrNull(aNode->AsContent());
if (!anchor) {
return false;
}
nsAutoCString tmpText;
anchor->GetHref(tmpText);
return !tmpText.IsEmpty();
}
bool HTMLEditUtils::IsNamedAnchor(
const nsINode* aNode) {
MOZ_ASSERT(aNode);
if (!aNode->IsHTMLElement(nsGkAtoms::a)) {
return false;
}
nsAutoString text;
return aNode->AsElement()->GetAttr(nsGkAtoms::name, text) && !text.IsEmpty();
}
/**
* IsMozDiv() returns true if aNode is an html div node with |type = _moz|.
*/
bool HTMLEditUtils::IsMozDiv(nsINode* aNode) {
MOZ_ASSERT(aNode);
return aNode->IsHTMLElement(nsGkAtoms::div) &&
aNode->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
u
"_moz"_ns, eIgnoreCase);
}
/**
* IsMailCite() returns true if aNode is an html blockquote with |type=cite|.
*/
bool HTMLEditUtils::IsMailCite(
const Element& aElement) {
// don't ask me why, but our html mailcites are id'd by "type=cite"...
if (aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, u
"cite"_ns,
eIgnoreCase)) {
return true;
}
// ... but our plaintext mailcites by "_moz_quote=true". go figure.
if (aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::mozquote, u
"true"_ns,
eIgnoreCase)) {
return true;
}
return false;
}
/**
* IsFormWidget() returns true if aNode is a form widget of some kind.
*/
bool HTMLEditUtils::IsFormWidget(
const nsINode* aNode) {
MOZ_ASSERT(aNode);
return aNode->IsAnyOfHTMLElements(nsGkAtoms::textarea, nsGkAtoms::select,
nsGkAtoms::button, nsGkAtoms::output,
nsGkAtoms::progress, nsGkAtoms::meter,
nsGkAtoms::input);
}
bool HTMLEditUtils::SupportsAlignAttr(nsINode& aNode) {
return aNode.IsAnyOfHTMLElements(
nsGkAtoms::hr, nsGkAtoms::table, nsGkAtoms::tbody, nsGkAtoms::tfoot,
nsGkAtoms::thead, nsGkAtoms::tr, nsGkAtoms::td, nsGkAtoms::th,
nsGkAtoms::div, nsGkAtoms::p, nsGkAtoms::h1, nsGkAtoms::h2, nsGkAtoms::h3,
nsGkAtoms::h4, nsGkAtoms::h5, nsGkAtoms::h6);
}
bool HTMLEditUtils::IsVisibleTextNode(
const Text& aText) {
if (!aText.TextDataLength()) {
return false;
}
Maybe<uint32_t> visibleCharOffset =
HTMLEditUtils::GetInclusiveNextNonCollapsibleCharOffset(
EditorDOMPointInText(&aText, 0));
if (visibleCharOffset.isSome()) {
return true;
}
// Now, all characters in aText is collapsible white-spaces. The node is
// invisible if next to block boundary.
return !HTMLEditUtils::GetElementOfImmediateBlockBoundary(
aText, WalkTreeDirection::Forward) &&
!HTMLEditUtils::GetElementOfImmediateBlockBoundary(
aText, WalkTreeDirection::Backward);
}
bool HTMLEditUtils::IsInVisibleTextFrames(nsPresContext* aPresContext,
const Text& aText) {
// TODO(dholbert): aPresContext is now unused; maybe we can remove it, here
// and in IsEmptyNode? We do use it as a signal (implicitly here,
// more-explicitly in IsEmptyNode) that we are in a "SafeToAskLayout" case...
// If/when we remove it, we should be sure we're not losing that signal of
// strictness, since this function here does absolutely need to query layout.
MOZ_ASSERT(aPresContext);
if (!aText.TextDataLength()) {
return false;
}
nsTextFrame* textFrame = do_QueryFrame(aText.GetPrimaryFrame());
if (!textFrame) {
return false;
}
return textFrame->HasVisibleText();
}
template <
typename PT,
typename CT>
EditorDOMPoint HTMLEditUtils::LineRequiresPaddingLineBreakToBeVisible(
const EditorDOMPointBase<PT, CT>& aPoint,
const Element& aEditingHost) {
if (MOZ_UNLIKELY(!aPoint.IsInContentNode())) {
return EditorDOMPoint();
}
// First, if the container is an element node, get the next deepest point.
EditorRawDOMPoint point = aPoint.
template To<EditorRawDOMPoint>();
if (point.IsContainerElement()) {
for (nsIContent* child = point.GetChild(); child;
child = child->GetFirstChild()) {
if (child->IsHTMLElement(nsGkAtoms::br)) {
return EditorDOMPoint();
}
if (!HTMLEditUtils::IsSimplyEditableNode(*child) ||
HTMLEditUtils::IsBlockElement(
*child, BlockInlineCheck::UseComputedDisplayOutsideStyle) ||
(child->IsElement() && !HTMLEditUtils::IsContainerNode(*child))) {
break;
}
point.Set(child, 0);
}
}
// If the point is in a Text, check the next character in it to skip the
// expensive check below.
if (point.IsInTextNode()) {
if (!point.IsStartOfContainer() &&
!point.IsPreviousCharCollapsibleASCIISpace()) {
return EditorDOMPoint();
// Not following collapsible white-space
}
if (!point.IsEndOfContainer()) {
if (!point.IsCharCollapsibleASCIISpace()) {
return EditorDOMPoint();
}
const bool linefeedPreformatted = EditorUtils::IsNewLinePreformatted(
*point.
template ContainerAs<Text>());
const nsTextFragment& fragment =
point.
template ContainerAs<Text>()->TextFragment();
for (uint32_t i : IntegerRange(point.Offset(), fragment.GetLength())) {
const char16_t ch = fragment.CharAt(i);
if (linefeedPreformatted && ch == HTMLEditUtils::kNewLine) {
return EditorDOMPoint();
// Followed by a preformatted line break.
}
if (!nsCRT::IsAsciiSpace(ch)) {
return EditorDOMPoint();
// Followed by a visible character.
}
}
}
}
const auto AdjustPointToInsertPaddingLineBreak =
[](EditorDOMPoint& aPointToInsertLineBreak,
const Element* aParentBlockElement,
const Element& aEditingHost) {
if (MOZ_UNLIKELY(!aPointToInsertLineBreak.IsInContentNode())) {
aPointToInsertLineBreak.Clear();
return;
}
while (MOZ_UNLIKELY(!HTMLEditUtils::CanNodeContain(
*aPointToInsertLineBreak.GetContainer(), *nsGkAtoms::br))) {
if (MOZ_UNLIKELY(aPointToInsertLineBreak.GetContainer() ==
aParentBlockElement ||
aPointToInsertLineBreak.GetContainer() ==
&aEditingHost)) {
aPointToInsertLineBreak.Clear();
return;
}
aPointToInsertLineBreak.SetAfterContainer();
if (MOZ_UNLIKELY(!aPointToInsertLineBreak.IsInContentNode())) {
aPointToInsertLineBreak.Clear();
return;
}
}
};
// If the point is in an empty block, we can skip the expensive check below
// too.
const Element* maybeNonEditableBlock =
HTMLEditUtils::GetInclusiveAncestorElement(
*point.ContainerAs<nsIContent>(), ClosestBlockElement,
BlockInlineCheck::UseComputedDisplayStyle);
if (maybeNonEditableBlock &&
HTMLEditUtils::IsEmptyNode(
*maybeNonEditableBlock,
{EmptyCheckOption::TreatSingleBRElementAsVisible})) {
EditorDOMPoint pointToInsertLineBreak =
HTMLEditUtils::GetDeepestEditableEndPointOf<EditorDOMPoint>(
*maybeNonEditableBlock);
if (pointToInsertLineBreak.IsInTextNode()) {
pointToInsertLineBreak.SetAfterContainer();
}
AdjustPointToInsertPaddingLineBreak(pointToInsertLineBreak,
maybeNonEditableBlock, aEditingHost);
return pointToInsertLineBreak;
}
EditorDOMPoint preferredPaddingLineBreakPoint;
const bool followedByBlockBoundary = [&]() {
if (point.GetContainer() == maybeNonEditableBlock &&
point.IsEndOfContainer()) {
preferredPaddingLineBreakPoint = point.To<EditorDOMPoint>();
return true;
}
if (point.GetContainer() == &aEditingHost && point.IsEndOfContainer()) {
return false;
}
const WSScanResult nextThing =
WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::EditableNodes, point,
BlockInlineCheck::UseComputedDisplayOutsideStyle);
if (nextThing.ReachedBlockBoundary()) {
if (nextThing.ReachedCurrentBlockBoundary()) {
preferredPaddingLineBreakPoint = point.AfterContainer<EditorDOMPoint>();
}
else {
preferredPaddingLineBreakPoint = point.To<EditorDOMPoint>();
}
return true;
}
return false;
}();
if (!followedByBlockBoundary) {
return EditorDOMPoint();
}
const bool followingBlockBoundaryOrCollapsibleWhiteSpace = [&]() {
if (point.GetContainer() == maybeNonEditableBlock &&
point.IsStartOfContainer()) {
return true;
}
const WSScanResult previousThing =
WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::EditableNodes, preferredPaddingLineBreakPoint,
BlockInlineCheck::UseComputedDisplayOutsideStyle);
if (previousThing.ContentIsText()) {
if (MOZ_UNLIKELY(!previousThing.TextPtr()->TextDataLength())) {
return false;
}
auto atLastChar = EditorRawDOMPointInText(
previousThing.TextPtr(),
previousThing.TextPtr()->TextDataLength() - 1);
if (atLastChar.IsCharCollapsibleASCIISpace()) {
preferredPaddingLineBreakPoint.SetAfter(previousThing.TextPtr());
return true;
}
return false;
}
return previousThing.ReachedBlockBoundary();
}();
if (!followingBlockBoundaryOrCollapsibleWhiteSpace) {
return EditorDOMPoint();
}
AdjustPointToInsertPaddingLineBreak(preferredPaddingLineBreakPoint,
maybeNonEditableBlock, aEditingHost);
return preferredPaddingLineBreakPoint;
}
Element* HTMLEditUtils::GetElementOfImmediateBlockBoundary(
const nsIContent& aContent,
const WalkTreeDirection aDirection) {
MOZ_ASSERT(aContent.IsHTMLElement(nsGkAtoms::br) || aContent.IsText());
// First, we get a block container. This is not designed for reaching
// no block boundaries in the tree.
Element* maybeNonEditableAncestorBlock = HTMLEditUtils::GetAncestorElement(
aContent, HTMLEditUtils::ClosestBlockElement,
BlockInlineCheck::UseComputedDisplayStyle);
if (NS_WARN_IF(!maybeNonEditableAncestorBlock)) {
return nullptr;
}
auto getNextContent = [&aDirection, &maybeNonEditableAncestorBlock](
const nsIContent& aContent) -> nsIContent* {
return aDirection == WalkTreeDirection::Forward
? HTMLEditUtils::GetNextContent(
aContent,
{WalkTreeOption::IgnoreDataNodeExceptText,
WalkTreeOption::StopAtBlockBoundary},
BlockInlineCheck::UseComputedDisplayStyle,
maybeNonEditableAncestorBlock)
: HTMLEditUtils::GetPreviousContent(
aContent,
{WalkTreeOption::IgnoreDataNodeExceptText,
WalkTreeOption::StopAtBlockBoundary},
BlockInlineCheck::UseComputedDisplayStyle,
maybeNonEditableAncestorBlock);
};
// Then, scan block element boundary while we don't see visible things.
const bool isBRElement = aContent.IsHTMLElement(nsGkAtoms::br);
for (nsIContent* nextContent = getNextContent(aContent); nextContent;
nextContent = getNextContent(*nextContent)) {
if (nextContent->IsElement()) {
// Break is right before a child block, it's not visible
if (HTMLEditUtils::IsBlockElement(
*nextContent, BlockInlineCheck::UseComputedDisplayStyle)) {
return nextContent->AsElement();
}
// XXX How about other non-HTML elements? Assume they are styled as
// blocks for now.
if (!nextContent->IsHTMLElement()) {
return nextContent->AsElement();
}
// If there is a visible content which generates something visible,
// stop scanning.
if (HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*nextContent)) {
return nullptr;
}
if (nextContent->IsHTMLElement(nsGkAtoms::br)) {
// If aContent is a <br> element, another <br> element prevents the
// block boundary special handling.
if (isBRElement) {
return nullptr;
}
MOZ_ASSERT(aContent.IsText());
// Following <br> element always hides its following block boundary.
// I.e., white-spaces is at end of the text node is visible.
if (aDirection == WalkTreeDirection::Forward) {
return nullptr;
}
// Otherwise, if text node follows <br> element, its white-spaces at
// start of the text node are invisible. In this case, we return
// the found <br> element.
return nextContent->AsElement();
}
continue;
}
switch (nextContent->NodeType()) {
case nsINode::TEXT_NODE:
case nsINode::CDATA_SECTION_NODE:
break;
default:
continue;
}
Text* textNode = Text::FromNode(nextContent);
MOZ_ASSERT(textNode);
if (!textNode->TextLength()) {
continue;
// empty invisible text node, keep scanning next one.
}
if (HTMLEditUtils::IsInclusiveAncestorCSSDisplayNone(*textNode)) {
continue;
// Styled as invisible.
}
if (!textNode->TextIsOnlyWhitespace()) {
return nullptr;
// found a visible text node.
}
const nsTextFragment& textFragment = textNode->TextFragment();
const bool isWhiteSpacePreformatted =
EditorUtils::IsWhiteSpacePreformatted(*textNode);
const bool isNewLinePreformatted =
EditorUtils::IsNewLinePreformatted(*textNode);
if (!isWhiteSpacePreformatted && !isNewLinePreformatted) {
// if the white-space only text node is not preformatted, ignore it.
continue;
}
for (uint32_t i = 0; i < textFragment.GetLength(); i++) {
if (textFragment.CharAt(i) == HTMLEditUtils::kNewLine) {
if (isNewLinePreformatted) {
return nullptr;
// found a visible text node.
}
continue;
}
if (isWhiteSpacePreformatted) {
return nullptr;
// found a visible text node.
}
}
// All white-spaces in the text node is invisible, keep scanning next one.
}
// There is no visible content and reached current block boundary. Then,
// the <br> element is the last content in the block and invisible.
// XXX Should we treat it visible if it's the only child of a block?
return maybeNonEditableAncestorBlock;
}
template <
typename PT,
typename CT>
bool HTMLEditUtils::PointIsImmediatelyBeforeCurrentBlockBoundary(
const EditorDOMPointBase<PT, CT>& aPoint,
IgnoreInvisibleLineBreak aIgnoreInvisibleLineBreak) {
MOZ_ASSERT(aPoint.IsSetAndValidInComposedDoc());
if (MOZ_UNLIKELY(!aPoint.IsInContentNode())) {
return false;
}
const WSScanResult nextThing =
WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::EditableNodes, aPoint,
BlockInlineCheck::UseComputedDisplayOutsideStyle);
if (nextThing.ReachedCurrentBlockBoundary()) {
return true;
}
if (nextThing.ReachedInvisibleBRElement()) {
if (aIgnoreInvisibleLineBreak == IgnoreInvisibleLineBreak::No) {
return false;
}
const WSScanResult afterInvisibleBRThing =
WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::EditableNodes,
nextThing.PointAfterReachedContent<EditorRawDOMPoint>(),
BlockInlineCheck::UseComputedDisplayOutsideStyle);
return afterInvisibleBRThing.ReachedCurrentBlockBoundary();
}
if (nextThing.ReachedPreformattedLineBreak()) {
if (aIgnoreInvisibleLineBreak == IgnoreInvisibleLineBreak::No) {
return false;
}
const WSScanResult afterPreformattedLineBreakThing =
WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::EditableNodes,
nextThing.PointAfterReachedContent<EditorRawDOMPoint>(),
BlockInlineCheck::UseComputedDisplayOutsideStyle);
return afterPreformattedLineBreakThing.ReachedCurrentBlockBoundary();
}
return false;
}
template <
typename EditorLineBreakType>
Maybe<EditorLineBreakType> HTMLEditUtils::GetUnnecessaryLineBreak(
const Element& aBlockElement, ScanLineBreak aScanLineBreak) {
auto* lastLineBreakContent = [&]() -> nsIContent* {
const LeafNodeTypes leafNodeOrNonEditableNode{
LeafNodeType::LeafNodeOrNonEditableNode};
const WalkTreeOptions onlyPrecedingLine{
WalkTreeOption::StopAtBlockBoundary};
for (nsIContent* content =
aScanLineBreak == ScanLineBreak::AtEndOfBlock
? HTMLEditUtils::GetLastLeafContent(aBlockElement,
leafNodeOrNonEditableNode)
: HTMLEditUtils::GetPreviousContent(
aBlockElement, onlyPrecedingLine,
BlockInlineCheck::UseComputedDisplayStyle,
aBlockElement.GetParentElement());
content;
content =
aScanLineBreak == ScanLineBreak::AtEndOfBlock
? HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
*content, leafNodeOrNonEditableNode,
BlockInlineCheck::UseComputedDisplayStyle,
&aBlockElement)
: HTMLEditUtils::GetPreviousContent(
*content, onlyPrecedingLine,
BlockInlineCheck::UseComputedDisplayStyle,
aBlockElement.GetParentElement())) {
// If we're scanning preceding <br> element of aBlockElement, we don't
// need to look for a line break in another block because the caller
// needs to handle only preceding <br> element of aBlockElement.
if (aScanLineBreak == ScanLineBreak::BeforeBlock &&
HTMLEditUtils::IsBlockElement(
*content, BlockInlineCheck::UseComputedDisplayStyle)) {
return nullptr;
}
if (Text* textNode = Text::FromNode(content)) {
if (!textNode->TextLength()) {
continue;
// ignore empty text node
}
const nsTextFragment& textFragment = textNode->TextFragment();
if (EditorUtils::IsNewLinePreformatted(*textNode) &&
textFragment.CharAt(textFragment.GetLength() - 1u) ==
HTMLEditUtils::kNewLine) {
// If the text node ends with a preserved line break, it's unnecessary
// unless it follows another preformatted line break.
if (textFragment.GetLength() == 1u) {
return textNode;
// Need to scan previous leaf.
}
return textFragment.CharAt(textFragment.GetLength() - 2u) ==
HTMLEditUtils::kNewLine
? nullptr
: textNode;
}
if (HTMLEditUtils::IsVisibleTextNode(*textNode)) {
return nullptr;
}
continue;
}
if (content->IsCharacterData()) {
continue;
// ignore hidden character data nodes like comment
}
if (content->IsHTMLElement(nsGkAtoms::br)) {
return content;
}
if (HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*content)) {
return nullptr;
}
// Otherwise, e.g., empty <b>, we should keep scanning.
}
return nullptr;
}();
if (!lastLineBreakContent) {
return Nothing();
}
// If the found node is a text node and contains only one preformatted new
// line break, we need to keep scanning previous one, but if it has 2 or more
// characters, we know it has redundant line break.
Text*
const lastLineBreakText = Text::FromNode(lastLineBreakContent);
if (lastLineBreakText && lastLineBreakText->TextDataLength() != 1u) {
return Some(EditorLineBreakType::AtLastChar(*lastLineBreakText));
}
HTMLBRElement*
const lastBRElement =
lastLineBreakText ? nullptr
: HTMLBRElement::FromNode(lastLineBreakContent);
MOZ_ASSERT_IF(!lastLineBreakText, lastBRElement);
// Scan previous leaf content, but now, we can stop at child block boundary.
const LeafNodeTypes leafNodeOrNonEditableNodeOrChildBlock{
LeafNodeType::LeafNodeOrNonEditableNode,
LeafNodeType::LeafNodeOrChildBlock};
const Element* blockElement = HTMLEditUtils::GetAncestorElement(
*lastLineBreakContent, HTMLEditUtils::ClosestBlockElement,
BlockInlineCheck::UseComputedDisplayStyle);
for (nsIContent* content =
HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
*lastLineBreakContent, leafNodeOrNonEditableNodeOrChildBlock,
BlockInlineCheck::UseComputedDisplayStyle, blockElement);
content;
content = HTMLEditUtils::GetPreviousLeafContentOrPreviousBlockElement(
*content, leafNodeOrNonEditableNodeOrChildBlock,
BlockInlineCheck::UseComputedDisplayStyle, blockElement)) {
if (HTMLEditUtils::IsBlockElement(
*content, BlockInlineCheck::UseComputedDisplayStyle) ||
(content->IsElement() && !content->IsHTMLElement())) {
// Now, must found <div>...<div>...</div><br></div>
// ^^^^
// In this case, the <br> element is necessary to make a following empty
// line of the inner <div> visible.
return Nothing();
}
if (Text* textNode = Text::FromNode(content)) {
if (!textNode->TextDataLength()) {
continue;
// ignore empty text node
}
const nsTextFragment& textFragment = textNode->TextFragment();
if (EditorUtils::IsNewLinePreformatted(*textNode) &&
textFragment.CharAt(textFragment.GetLength() - 1u) ==
HTMLEditUtils::kNewLine) {
// So, we are here because the preformatted line break is followed by
// lastLineBreakContent which is <br> or a text node containing only
// one. In this case, even if their parents are different,
// lastLineBreakContent is necessary to make the last line visible.
return Nothing();
}
if (!HTMLEditUtils::IsVisibleTextNode(*textNode)) {
continue;
}
if (EditorUtils::IsWhiteSpacePreformatted(*textNode)) {
// If the white-space is preserved, neither following <br> nor a
// preformatted line break is not necessary.
return Some(lastLineBreakText
? EditorLineBreakType::AtLastChar(*lastLineBreakText)
: EditorLineBreakType(*lastBRElement));
}
// Otherwise, only if the last character is a collapsible white-space,
// we need lastLineBreakContent to make the trailing white-space visible.
switch (textFragment.CharAt(textFragment.GetLength() - 1u)) {
case HTMLEditUtils::kSpace:
case HTMLEditUtils::kNewLine:
case HTMLEditUtils::kCarriageReturn:
case HTMLEditUtils::kTab:
return Nothing();
default:
return Some(lastLineBreakText
? EditorLineBreakType::AtLastChar(*lastLineBreakText)
: EditorLineBreakType(*lastBRElement));
}
}
if (content->IsCharacterData()) {
continue;
// ignore hidden character data nodes like comment
}
// If lastLineBreakContent follows a <br> element in same block, it's
// necessary to make the empty last line visible.
if (content->IsHTMLElement(nsGkAtoms::br)) {
return Nothing();
}
if (HTMLEditUtils::IsVisibleElementEvenIfLeafNode(*content)) {
return Some(lastLineBreakText
? EditorLineBreakType::AtLastChar(*lastLineBreakText)
: EditorLineBreakType(*lastBRElement));
}
// Otherwise, ignore empty inline elements such as <b>.
}
// If the block is empty except invisible data nodes and lastLineBreakContent,
// lastLineBreakContent is necessary to make the block visible.
return Nothing();
}
template <
typename EditorLineBreakType,
typename EditorDOMPointType>
Maybe<EditorLineBreakType> HTMLEditUtils::GetFollowingUnnecessaryLineBreak(
const EditorDOMPointType& aPoint) {
MOZ_ASSERT(aPoint.IsSetAndValid());
MOZ_ASSERT(aPoint.IsInContentNode());
const WSScanResult nextThing =
WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::EditableNodes, aPoint,
BlockInlineCheck::UseComputedDisplayStyle);
if (!nextThing.ReachedBRElement() &&
!(nextThing.ReachedPreformattedLineBreak() &&
nextThing.PointAtReachedContent<EditorRawDOMPoint>()
.IsAtLastContent())) {
return Nothing();
// no line break next to aPoint
}
const WSScanResult nextThingOfLineBreak =
WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
WSRunScanner::Scan::EditableNodes,
nextThing.PointAfterReachedContent<EditorRawDOMPoint>(),
BlockInlineCheck::UseComputedDisplayStyle);
const Element*
const blockElement =
nextThingOfLineBreak.ReachedBlockBoundary()
? nextThingOfLineBreak.ElementPtr()
: HTMLEditUtils::GetAncestorElement(
*nextThing.GetContent(), {AncestorType::ClosestBlockElement},
BlockInlineCheck::UseComputedDisplayStyle);
if (MOZ_UNLIKELY(!blockElement)) {
return Nothing();
}
Maybe<EditorLineBreakType> unnecessaryLineBreak =
GetUnnecessaryLineBreak<EditorLineBreakType>(
*blockElement, nextThingOfLineBreak.ReachedOtherBlockElement()
? ScanLineBreak::BeforeBlock
: ScanLineBreak::AtEndOfBlock);
// If the line break content is different from the found line break
// immediately after aPoint, it's too far. So, the caller should not touch it.
if (unnecessaryLineBreak.isSome() &&
&unnecessaryLineBreak->ContentRef() != nextThing.GetContent()) {
unnecessaryLineBreak.reset();
}
return unnecessaryLineBreak;
}
bool HTMLEditUtils::IsEmptyNode(nsPresContext* aPresContext,
const nsINode& aNode,
const EmptyCheckOptions& aOptions
/* = {} */,
bool* aSeenBR
/* = nullptr */) {
MOZ_ASSERT_IF(aOptions.contains(EmptyCheckOption::SafeToAskLayout),
aPresContext);
if (aSeenBR) {
*aSeenBR =
false;
}
if (
const Text* text = Text::FromNode(&aNode)) {
return aOptions.contains(EmptyCheckOption::SafeToAskLayout)
? !IsInVisibleTextFrames(aPresContext, *text)
: !IsVisibleTextNode(*text);
}
if (!aNode.IsElement()) {
return false;
}
if (
// If it's not a container such as an <hr> or <br>, etc, it should be
// treated as not empty.
!IsContainerNode(*aNode.AsContent()) ||
// If it's a named anchor, we shouldn't treat it as empty because it
// has special meaning even if invisible.
IsNamedAnchor(&aNode) ||
// Form widgets should be treated as not empty because they have special
// meaning even if invisible.
IsFormWidget(&aNode)) {
return false;
}
const auto [isListItem, isTableCell, hasAppearance] =
[&]() MOZ_NEVER_INLINE_DEBUG -> std::tuple<
bool,
bool,
bool> {
// Let's stop treating the document element and the <body> as a list item
// nor a table cell to avoid tricky cases.
if (aNode.OwnerDoc()->GetDocumentElement() == &aNode ||
(aNode.IsHTMLElement(nsGkAtoms::body) &&
aNode.OwnerDoc()->GetBodyElement() == &aNode)) {
return {
false,
false,
false};
}
RefPtr<
const ComputedStyle> elementStyle =
nsComputedDOMStyle::GetComputedStyleNoFlush(aNode.AsElement());
// If there is no style information like in a document fragment, let's refer
// the default style.
if (MOZ_UNLIKELY(!elementStyle)) {
return {IsListItem(&aNode), IsTableCell(&aNode),
false};
}
const nsStyleDisplay* styleDisplay = elementStyle->StyleDisplay();
if (NS_WARN_IF(!styleDisplay)) {
return {IsListItem(&aNode), IsTableCell(&aNode),
false};
}
if (styleDisplay->mDisplay != StyleDisplay::None &&
styleDisplay->HasAppearance()) {
return {
false,
false,
true};
}
if (styleDisplay->IsListItem()) {
return {
true,
false,
false};
}
if (styleDisplay->mDisplay == StyleDisplay::TableCell) {
return {
false,
true,
false};
}
// The default display of <dt> and <dd> is block. Therefore, we need
// special handling for them.
return {styleDisplay->mDisplay == StyleDisplay::Block &&
aNode.IsAnyOfHTMLElements(nsGkAtoms::dd, nsGkAtoms::dt),
false,
false};
}();
// The web author created native widget without form control elements. Let's
// treat it as visible.
if (hasAppearance) {
return false;
}
if (isListItem &&
aOptions.contains(EmptyCheckOption::TreatListItemAsVisible)) {
return false;
}
if (isTableCell &&
aOptions.contains(EmptyCheckOption::TreatTableCellAsVisible)) {
return false;
}
bool seenBR = aSeenBR && *aSeenBR;
for (nsIContent* childContent = aNode.GetFirstChild(); childContent;
childContent = childContent->GetNextSibling()) {
// Is the child editable and non-empty? if so, return false
if (aOptions.contains(
EmptyCheckOption::TreatNonEditableContentAsInvisible) &&
!EditorUtils::IsEditableContent(*childContent, EditorType::HTML)) {
continue;
}
if (Text* text = Text::FromNode(childContent)) {
// break out if we find we aren't empty
if (aOptions.contains(EmptyCheckOption::SafeToAskLayout)
? IsInVisibleTextFrames(aPresContext, *text)
: IsVisibleTextNode(*text)) {
return false;
}
continue;
}
MOZ_ASSERT(childContent != &aNode);
if (!aOptions.contains(EmptyCheckOption::TreatSingleBRElementAsVisible) &&
!seenBR && childContent->IsHTMLElement(nsGkAtoms::br)) {
// Ignore first <br> element in it if caller wants so because it's
// typically a padding <br> element of for a parent block.
seenBR =
true;
if (aSeenBR) {
*aSeenBR =
true;
}
continue;
}
if (aOptions.contains(EmptyCheckOption::TreatBlockAsVisible) &&
HTMLEditUtils::IsBlockElement(
*childContent, BlockInlineCheck::UseComputedDisplayOutsideStyle)) {
return false;
}
// Note: list items or table cells are not considered empty
// if they contain other lists or tables
EmptyCheckOptions options(aOptions);
if (childContent->IsElement() && (isListItem || isTableCell)) {
options += {EmptyCheckOption::TreatListItemAsVisible,
EmptyCheckOption::TreatTableCellAsVisible};
}
if (!IsEmptyNode(aPresContext, *childContent, options, &seenBR)) {
if (aSeenBR) {
*aSeenBR = seenBR;
}
return false;
}
}
if (aSeenBR) {
*aSeenBR = seenBR;
}
return true;
}
bool HTMLEditUtils::ShouldInsertLinefeedCharacter(
const EditorDOMPoint& aPointToInsert,
const Element& aEditingHost) {
MOZ_ASSERT(aPointToInsert.IsSetAndValid());
if (!aPointToInsert.IsInContentNode()) {
return false;
}
// If in contenteditable=plaintext-only, we should use linefeed when it's
// preformatted.
if (aEditingHost.IsContentEditablePlainTextOnly()) {
return EditorUtils::IsNewLinePreformatted(
*aPointToInsert.ContainerAs<nsIContent>());
}
// closestEditableBlockElement can be nullptr if aEditingHost is an inline
// element.
Element* closestEditableBlockElement =
HTMLEditUtils::GetInclusiveAncestorElement(
*aPointToInsert.ContainerAs<nsIContent>(),
HTMLEditUtils::ClosestEditableBlockElement,
BlockInlineCheck::UseComputedDisplayOutsideStyle);
// If and only if the nearest block is the editing host or its parent,
// and new line character is preformatted, we should insert a linefeed.
return (!closestEditableBlockElement ||
closestEditableBlockElement == &aEditingHost) &&
EditorUtils::IsNewLinePreformatted(
*aPointToInsert.ContainerAs<nsIContent>());
}
// We use bitmasks to test containment of elements. Elements are marked to be
// in certain groups by setting the mGroup member of the `ElementInfo` struct
// to the corresponding GROUP_ values (OR'ed together). Similarly, elements are
// marked to allow containment of certain groups by setting the
// mCanContainGroups member of the `ElementInfo` struct to the corresponding
// GROUP_ values (OR'ed together).
// Testing containment then simply consists of checking whether the
// mCanContainGroups bitmask of an element and the mGroup bitmask of a
// potential child overlap.
#define GROUP_NONE 0
// body, head, html
#define GROUP_TOPLEVEL (1 << 1)
// base, link, meta, script, style, title
#define GROUP_HEAD_CONTENT (1 << 2)
// b, big, i, s, small, strike, tt, u
#define GROUP_FONTSTYLE (1 << 3)
// abbr, acronym, cite, code, datalist, del, dfn, em, ins, kbd, mark, rb, rp
// rt, rtc, ruby, samp, strong, var
#define GROUP_PHRASE (1 << 4)
// a, applet, basefont, bdi, bdo, br, font, iframe, img, map, meter, object,
// output, picture, progress, q, script, span, sub, sup
#define GROUP_SPECIAL (1 << 5)
// button, form, input, label, select, textarea
#define GROUP_FORMCONTROL (1 << 6)
// address, applet, article, aside, blockquote, button, center, del, details,
// dialog, dir, div, dl, fieldset, figure, footer, form, h1, h2, h3, h4, h5,
// h6, header, hgroup, hr, iframe, ins, main, map, menu, nav, noframes,
// noscript, object, ol, p, pre, table, search, section, summary, ul
#define GROUP_BLOCK (1 << 7)
// frame, frameset
#define GROUP_FRAME (1 << 8)
// col, tbody
#define GROUP_TABLE_CONTENT (1 << 9)
// tr
#define GROUP_TBODY_CONTENT (1 << 10)
// td, th
#define GROUP_TR_CONTENT (1 << 11)
// col
#define GROUP_COLGROUP_CONTENT (1 << 12)
// param
#define GROUP_OBJECT_CONTENT (1 << 13)
// li
#define GROUP_LI (1 << 14)
// area
#define GROUP_MAP_CONTENT (1 << 15)
// optgroup, option
#define GROUP_SELECT_CONTENT (1 << 16)
// option
#define GROUP_OPTIONS (1 << 17)
// dd, dt
#define GROUP_DL_CONTENT (1 << 18)
// p
#define GROUP_P (1 << 19)
// text, white-space, newline, comment
#define GROUP_LEAF (1 << 20)
// XXX This is because the editor does sublists illegally.
// ol, ul
#define GROUP_OL_UL (1 << 21)
// h1, h2, h3, h4, h5, h6
#define GROUP_HEADING (1 << 22)
// figcaption
#define GROUP_FIGCAPTION (1 << 23)
// picture members (img, source)
#define GROUP_PICTURE_CONTENT (1 << 24)
#define GROUP_INLINE_ELEMENT \
(GROUP_FONTSTYLE | GROUP_PHRASE | GROUP_SPECIAL | GROUP_FORMCONTROL | \
GROUP_LEAF)
#define GROUP_FLOW_ELEMENT (GROUP_INLINE_ELEMENT | GROUP_BLOCK)
struct ElementInfo final {
#ifdef DEBUG
nsHTMLTag mTag;
#endif
// See `GROUP_NONE`'s comment.
uint32_t mGroup;
// See `GROUP_NONE`'s comment.
uint32_t mCanContainGroups;
bool mIsContainer;
bool mCanContainSelf;
};
#ifdef DEBUG
# define ELEM(_tag, _isContainer, _canContainSelf, _group, _canContainGroups) \
{eHTMLTag_
##_tag, _group, _canContainGroups, _isContainer, _canContainSelf}
#else
# define ELEM(_tag, _isContainer, _canContainSelf, _group, _canContainGroups) \
{_group, _canContainGroups, _isContainer, _canContainSelf}
#endif
static const ElementInfo kElements[eHTMLTag_userdefined] = {
ELEM(a,
true,
false, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
ELEM(abbr,
true,
true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
ELEM(acronym,
true,
true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
ELEM(address,
true,
true, GROUP_BLOCK, GROUP_INLINE_ELEMENT | GROUP_P),
// While applet is no longer a valid tag, removing it here breaks the editor
// (compiles, but causes many tests to fail in odd ways). This list is
// tracked against the main HTML Tag list, so any changes will require more
// than just removing entries.
ELEM(applet,
true,
true, GROUP_SPECIAL | GROUP_BLOCK,
GROUP_FLOW_ELEMENT | GROUP_OBJECT_CONTENT),
ELEM(area,
false,
false, GROUP_MAP_CONTENT, GROUP_NONE),
ELEM(article,
true,
true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
ELEM(aside,
true,
true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
ELEM(audio,
false,
false, GROUP_NONE, GROUP_NONE),
ELEM(b,
true,
true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
ELEM(base,
false,
false, GROUP_HEAD_CONTENT, GROUP_NONE),
ELEM(basefont,
false,
false, GROUP_SPECIAL, GROUP_NONE),
ELEM(bdi,
true,
true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
ELEM(bdo,
true,
true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
ELEM(bgsound,
false,
false, GROUP_NONE, GROUP_NONE),
ELEM(big,
true,
true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
ELEM(blockquote,
true,
true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
ELEM(body,
true,
true, GROUP_TOPLEVEL, GROUP_FLOW_ELEMENT),
ELEM(br,
false,
false, GROUP_SPECIAL, GROUP_NONE),
ELEM(button,
true,
true, GROUP_FORMCONTROL | GROUP_BLOCK,
GROUP_FLOW_ELEMENT),
ELEM(canvas,
false,
false, GROUP_NONE, GROUP_NONE),
ELEM(caption,
true,
true, GROUP_NONE, GROUP_INLINE_ELEMENT),
ELEM(center,
true,
true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
ELEM(cite,
true,
true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
ELEM(code,
true,
true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
ELEM(col,
false,
false, GROUP_TABLE_CONTENT | GROUP_COLGROUP_CONTENT,
GROUP_NONE),
ELEM(colgroup,
true,
false, GROUP_NONE, GROUP_COLGROUP_CONTENT),
ELEM(data,
true,
false, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
ELEM(datalist,
true,
false, GROUP_PHRASE,
GROUP_OPTIONS | GROUP_INLINE_ELEMENT),
ELEM(dd,
true,
false, GROUP_DL_CONTENT, GROUP_FLOW_ELEMENT),
ELEM(del,
true,
true, GROUP_PHRASE | GROUP_BLOCK, GROUP_FLOW_ELEMENT),
ELEM(details,
true,
true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
ELEM(dfn,
true,
true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
ELEM(dialog,
true,
true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
ELEM(dir,
true,
false, GROUP_BLOCK, GROUP_LI),
ELEM(div,
true,
true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
ELEM(dl,
true,
false, GROUP_BLOCK, GROUP_DL_CONTENT),
ELEM(dt,
true,
true, GROUP_DL_CONTENT, GROUP_INLINE_ELEMENT),
ELEM(em,
true,
true, GROUP_PHRASE, GROUP_INLINE_ELEMENT),
ELEM(embed,
false,
false, GROUP_NONE, GROUP_NONE),
ELEM(fieldset,
true,
true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
ELEM(figcaption,
true,
false, GROUP_FIGCAPTION, GROUP_FLOW_ELEMENT),
ELEM(figure,
true,
true, GROUP_BLOCK,
GROUP_FLOW_ELEMENT | GROUP_FIGCAPTION),
ELEM(font,
true,
true, GROUP_SPECIAL, GROUP_INLINE_ELEMENT),
ELEM(footer,
true,
true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
ELEM(form,
true,
true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
ELEM(frame,
false,
false, GROUP_FRAME, GROUP_NONE),
ELEM(frameset,
true,
true, GROUP_FRAME, GROUP_FRAME),
ELEM(h1,
true,
false, GROUP_BLOCK | GROUP_HEADING, GROUP_INLINE_ELEMENT),
ELEM(h2,
true,
false, GROUP_BLOCK | GROUP_HEADING, GROUP_INLINE_ELEMENT),
ELEM(h3,
true,
false, GROUP_BLOCK | GROUP_HEADING, GROUP_INLINE_ELEMENT),
ELEM(h4,
true,
false, GROUP_BLOCK | GROUP_HEADING, GROUP_INLINE_ELEMENT),
ELEM(h5,
true,
false, GROUP_BLOCK | GROUP_HEADING, GROUP_INLINE_ELEMENT),
ELEM(h6,
true,
false, GROUP_BLOCK | GROUP_HEADING, GROUP_INLINE_ELEMENT),
ELEM(head,
true,
false, GROUP_TOPLEVEL, GROUP_HEAD_CONTENT),
ELEM(header,
true,
true, GROUP_BLOCK, GROUP_FLOW_ELEMENT),
ELEM(hgroup,
true,
false, GROUP_BLOCK, GROUP_HEADING),
ELEM(hr,
false,
false, GROUP_BLOCK, GROUP_NONE),
ELEM(html,
true,
false, GROUP_TOPLEVEL, GROUP_TOPLEVEL),
ELEM(i,
true,
true, GROUP_FONTSTYLE, GROUP_INLINE_ELEMENT),
ELEM(iframe,
true,
true, GROUP_SPECIAL | GROUP_BLOCK, GROUP_FLOW_ELEMENT),
ELEM(image,
false,
false, GROUP_NONE, GROUP_NONE),
--> --------------------
--> maximum size reached
--> --------------------