/* -*- 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 {
usingnamespace dom; using EditorType = EditorBase::EditorType;
if (MOZ_UNLIKELY(!aContent.IsElement())) { returnfalse;
} // 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)) { returnfalse;
} 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)) { returntrue;
}
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()) { returntrue;
} // If the outside is not inline, treat it as block. if (!styleDisplay->IsInlineOutsideStyle()) { returntrue;
} // 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;
}
if (!aContent.IsElement()) { returntrue;
} // 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)) { returntrue;
} 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)) { returnfalse;
}
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::IsInclusiveAncestorCSSDisplayNone( const nsIContent& aContent) { if (NS_WARN_IF(!aContent.IsInComposedDoc())) { returntrue;
} 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)) { returntrue;
}
} returnfalse;
}
bool HTMLEditUtils::IsVisibleElementEvenIfLeafNode(const nsIContent& aContent) { if (!aContent.IsElement()) { returnfalse;
} // Assume non-HTML element is visible. if (!aContent.IsHTMLElement()) { returntrue;
} // XXX Should we return false if the element is display:none? if (HTMLEditUtils::IsBlockElement(
aContent, BlockInlineCheck::UseComputedDisplayStyle)) { returntrue;
} if (aContent.IsAnyOfHTMLElements(nsGkAtoms::applet, nsGkAtoms::iframe,
nsGkAtoms::img, nsGkAtoms::meter,
nsGkAtoms::progress, nsGkAtoms::select,
nsGkAtoms::textarea)) { returntrue;
} 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()) { returntrue;
} // Maybe, empty inline element such as <span>. returnfalse;
}
bool HTMLEditUtils::IsRemovableInlineStyleElement(Element& aElement) { if (!aElement.IsHTMLElement()) { returnfalse;
} // 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)) { returntrue;
} // 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);
}
/** * 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)) { returntrue;
}
// ... but our plaintext mailcites by "_moz_quote=true". go figure. if (aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::mozquote, u"true"_ns,
eIgnoreCase)) { returntrue;
}
returnfalse;
}
/** * 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);
}
// 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()) { returnfalse;
}
nsTextFrame* textFrame = do_QueryFrame(aText.GetPrimaryFrame()); if (!textFrame) { returnfalse;
}
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();
} constbool 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.
}
}
}
}
// 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;
}
// 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;
}
// Then, scan block element boundary while we don't see visible things. constbool 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(); constbool isWhiteSpacePreformatted =
EditorUtils::IsWhiteSpacePreformatted(*textNode); constbool 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 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();
}
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;
}
if (const Text* text = Text::FromNode(&aNode)) { return aOptions.contains(EmptyCheckOption::SafeToAskLayout)
? !IsInVisibleTextFrames(aPresContext, *text)
: !IsVisibleTextNode(*text);
}
if (!aNode.IsElement()) { returnfalse;
}
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)) { returnfalse;
}
constauto [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) { returnfalse;
}
if (isListItem &&
aOptions.contains(EmptyCheckOption::TreatListItemAsVisible)) { returnfalse;
} if (isTableCell &&
aOptions.contains(EmptyCheckOption::TreatTableCellAsVisible)) { returnfalse;
}
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)) { returnfalse;
} 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)) { returnfalse;
}
// 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;
} returnfalse;
}
}
if (!aPointToInsert.IsInContentNode()) { returnfalse;
}
// 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)
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.