tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

HTMLEditorInsertLineBreakHandler.cpp (18746B)


      1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set ts=2 sw=2 et tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 #include "EditorBase.h"
      8 #include "HTMLEditor.h"
      9 #include "HTMLEditorInlines.h"
     10 #include "HTMLEditorNestedClasses.h"
     11 
     12 #include "CSSEditUtils.h"
     13 #include "EditAction.h"
     14 #include "EditorDOMPoint.h"
     15 #include "EditorLineBreak.h"
     16 #include "EditorUtils.h"
     17 #include "HTMLEditHelpers.h"
     18 #include "HTMLEditUtils.h"
     19 #include "PendingStyles.h"  // for SpecifiedStyle
     20 #include "WhiteSpaceVisibilityKeeper.h"
     21 #include "WSRunScanner.h"
     22 
     23 #include "ErrorList.h"
     24 #include "mozilla/Assertions.h"
     25 #include "mozilla/AutoRestore.h"
     26 #include "mozilla/ContentIterator.h"
     27 #include "mozilla/EditorForwards.h"
     28 #include "mozilla/Maybe.h"
     29 #include "mozilla/PresShell.h"
     30 #include "mozilla/TextComposition.h"
     31 #include "mozilla/dom/RangeBinding.h"
     32 #include "mozilla/dom/Selection.h"
     33 #include "nsContentUtils.h"
     34 #include "nsDebug.h"
     35 #include "nsError.h"
     36 #include "nsFrameSelection.h"
     37 #include "nsGkAtoms.h"
     38 #include "nsIContent.h"
     39 #include "nsINode.h"
     40 #include "nsRange.h"
     41 #include "nsTArray.h"
     42 #include "nsTextNode.h"
     43 
     44 class nsISupports;
     45 
     46 namespace mozilla {
     47 
     48 using namespace dom;
     49 using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
     50 using EmptyCheckOptions = HTMLEditUtils::EmptyCheckOptions;
     51 
     52 nsresult HTMLEditor::InsertLineBreakAsSubAction() {
     53  MOZ_ASSERT(IsEditActionDataAvailable());
     54  MOZ_ASSERT(!IsSelectionRangeContainerNotContent());
     55 
     56  if (NS_WARN_IF(!mInitSucceeded)) {
     57    return NS_ERROR_NOT_INITIALIZED;
     58  }
     59 
     60  {
     61    Result<EditActionResult, nsresult> result =
     62        CanHandleHTMLEditSubAction(CheckSelectionInReplacedElement::No);
     63    if (MOZ_UNLIKELY(result.isErr())) {
     64      NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
     65      return result.unwrapErr();
     66    }
     67    if (result.inspect().Canceled()) {
     68      return NS_OK;
     69    }
     70  }
     71 
     72  // XXX This may be called by execCommand() with "insertLineBreak".
     73  //     In such case, naming the transaction "TypingTxnName" is odd.
     74  AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName,
     75                                             ScrollSelectionIntoView::Yes,
     76                                             __FUNCTION__);
     77 
     78  // calling it text insertion to trigger moz br treatment by rules
     79  // XXX Why do we use EditSubAction::eInsertText here?  Looks like
     80  //     EditSubAction::eInsertLineBreak or EditSubAction::eInsertNode
     81  //     is better.
     82  IgnoredErrorResult ignoredError;
     83  AutoEditSubActionNotifier startToHandleEditSubAction(
     84      *this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError);
     85  if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
     86    return ignoredError.StealNSResult();
     87  }
     88  NS_WARNING_ASSERTION(
     89      !ignoredError.Failed(),
     90      "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
     91 
     92  UndefineCaretBidiLevel();
     93 
     94  // If the selection isn't collapsed, delete it.
     95  if (!SelectionRef().IsCollapsed()) {
     96    nsresult rv =
     97        DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eStrip);
     98    if (NS_FAILED(rv)) {
     99      NS_WARNING(
    100          "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed");
    101      return rv;
    102    }
    103  }
    104 
    105  const RefPtr<Element> editingHost =
    106      ComputeEditingHost(LimitInBodyElement::No);
    107  if (NS_WARN_IF(!editingHost)) {
    108    return NS_ERROR_FAILURE;
    109  }
    110 
    111  AutoInsertLineBreakHandler handler(*this, *editingHost);
    112  nsresult rv = handler.Run();
    113  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
    114                       "AutoInsertLineBreakHandler::Run() failed");
    115  return rv;
    116 }
    117 
    118 nsresult HTMLEditor::AutoInsertLineBreakHandler::Run() {
    119  MOZ_ASSERT(mHTMLEditor.IsEditActionDataAvailable());
    120 
    121  const auto atStartOfSelection =
    122      mHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>();
    123  if (NS_WARN_IF(!atStartOfSelection.IsInContentNode())) {
    124    return NS_ERROR_FAILURE;
    125  }
    126  MOZ_ASSERT(atStartOfSelection.IsSetAndValidInComposedDoc());
    127 
    128  const Maybe<LineBreakType> lineBreakType =
    129      mHTMLEditor.GetPreferredLineBreakType(
    130          *atStartOfSelection.ContainerAs<nsIContent>(), mEditingHost);
    131  if (MOZ_UNLIKELY(!lineBreakType)) {
    132    return NS_SUCCESS_DOM_NO_OPERATION;  // Cannot insert a line break there.
    133  }
    134  if (lineBreakType.value() == LineBreakType::BRElement) {
    135    nsresult rv = HandleInsertBRElement();
    136    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
    137                         "AutoInsertLineBreakHandler::HandleInsertBRElement()");
    138    return rv;
    139  }
    140 
    141  nsresult rv = HandleInsertLinefeed();
    142  NS_WARNING_ASSERTION(
    143      NS_SUCCEEDED(rv),
    144      "AutoInsertLineBreakHandler::HandleInsertLinefeed() failed");
    145  return rv;
    146 }
    147 
    148 nsresult HTMLEditor::AutoInsertLineBreakHandler::HandleInsertBRElement() {
    149  const EditorDOMPoint pointToInsert = [&]() {
    150    const auto atStartOfSelection =
    151        mHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>();
    152    MOZ_ASSERT(atStartOfSelection.IsInContentNode());
    153    return HTMLEditUtils::GetPossiblePointToInsert(
    154        atStartOfSelection, *nsGkAtoms::br, mEditingHost);
    155  }();
    156  if (NS_WARN_IF(!pointToInsert.IsSet())) {
    157    return Err(NS_ERROR_FAILURE);
    158  }
    159  MOZ_ASSERT(pointToInsert.IsInContentNode());
    160 
    161  // XXX Should we check the preferred line break again?
    162 
    163  Result<CreateLineBreakResult, nsresult> insertLineBreakResultOrError =
    164      mHTMLEditor.InsertLineBreak(WithTransaction::Yes,
    165                                  LineBreakType::BRElement, pointToInsert,
    166                                  nsIEditor::eNext);
    167  if (MOZ_UNLIKELY(insertLineBreakResultOrError.isErr())) {
    168    NS_WARNING(
    169        "HTMLEditor::InsertLineBreak(WithTransaction::Yes, "
    170        "LineBreakType::BRElement, eNext) failed");
    171    return insertLineBreakResultOrError.unwrapErr();
    172  }
    173  CreateLineBreakResult insertLineBreakResult =
    174      insertLineBreakResultOrError.unwrap();
    175  MOZ_ASSERT(insertLineBreakResult.Handled());
    176  insertLineBreakResult.IgnoreCaretPointSuggestion();
    177 
    178  auto pointToPutCaret = insertLineBreakResult.UnwrapCaretPoint();
    179  if (MOZ_UNLIKELY(!pointToPutCaret.IsSet())) {
    180    NS_WARNING("Inserted <br> was unexpectedly removed");
    181    return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
    182  }
    183  const WSScanResult backwardScanFromBeforeBRElementResult =
    184      WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary(
    185          {WSRunScanner::Option::OnlyEditableNodes},
    186          insertLineBreakResult.AtLineBreak<EditorDOMPoint>());
    187  if (MOZ_UNLIKELY(backwardScanFromBeforeBRElementResult.Failed())) {
    188    NS_WARNING("WSRunScanner::ScanPreviousVisibleNodeOrBlockBoundary() failed");
    189    return Err(NS_ERROR_FAILURE);
    190  }
    191 
    192  const WSScanResult forwardScanFromAfterBRElementResult =
    193      WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
    194          {WSRunScanner::Option::OnlyEditableNodes}, pointToPutCaret);
    195  if (MOZ_UNLIKELY(forwardScanFromAfterBRElementResult.Failed())) {
    196    NS_WARNING("WSRunScanner::ScanNextVisibleNodeOrBlockBoundary() failed");
    197    return Err(NS_ERROR_FAILURE);
    198  }
    199  const bool brElementIsAfterBlock =
    200      backwardScanFromBeforeBRElementResult.ReachedBlockBoundary() ||
    201      // FIXME: This is wrong considering because the inline editing host may
    202      // be surrounded by visible inline content.  However, WSRunScanner is
    203      // not aware of block boundary around it and stopping this change causes
    204      // starting to fail some WPT.  Therefore, we need to keep doing this for
    205      // now.
    206      backwardScanFromBeforeBRElementResult.ReachedInlineEditingHostBoundary();
    207  const bool brElementIsBeforeBlock =
    208      forwardScanFromAfterBRElementResult.ReachedBlockBoundary() ||
    209      // FIXME: See above comment.
    210      forwardScanFromAfterBRElementResult.ReachedInlineEditingHostBoundary();
    211  const bool isEmptyEditingHost = HTMLEditUtils::IsEmptyNode(
    212      mEditingHost, {EmptyCheckOption::TreatNonEditableContentAsInvisible});
    213  if (brElementIsBeforeBlock &&
    214      (isEmptyEditingHost || !brElementIsAfterBlock)) {
    215    // Empty last line is invisible if it's immediately before either parent
    216    // or another block's boundary so that we need to put invisible <br>
    217    // element here for making it visible.
    218    Result<CreateLineBreakResult, nsresult>
    219        insertPaddingBRElementResultOrError =
    220            WhiteSpaceVisibilityKeeper::InsertLineBreak(
    221                LineBreakType::BRElement, mHTMLEditor, pointToPutCaret);
    222    if (MOZ_UNLIKELY(insertPaddingBRElementResultOrError.isErr())) {
    223      NS_WARNING(
    224          "WhiteSpaceVisibilityKeeper::InsertLineBreak(LineBreakType::"
    225          "BRElement) failed");
    226      return insertPaddingBRElementResultOrError.unwrapErr();
    227    }
    228    CreateLineBreakResult insertPaddingBRElementResult =
    229        insertPaddingBRElementResultOrError.unwrap();
    230    pointToPutCaret =
    231        insertPaddingBRElementResult.AtLineBreak<EditorDOMPoint>();
    232    insertPaddingBRElementResult.IgnoreCaretPointSuggestion();
    233  } else if (forwardScanFromAfterBRElementResult
    234                 .InVisibleOrCollapsibleCharacters()) {
    235    pointToPutCaret = forwardScanFromAfterBRElementResult
    236                          .PointAtReachedContent<EditorDOMPoint>();
    237  } else if (forwardScanFromAfterBRElementResult.ReachedSpecialContent()) {
    238    // Next inserting text should be inserted into styled inline elements if
    239    // they have first visible thing in the new line.
    240    pointToPutCaret = forwardScanFromAfterBRElementResult
    241                          .PointAtReachedContent<EditorDOMPoint>();
    242  }
    243 
    244  nsresult rv = mHTMLEditor.CollapseSelectionTo(pointToPutCaret);
    245  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
    246                       "EditorBase::CollapseSelectionTo() failed");
    247  return rv;
    248 }
    249 
    250 nsresult HTMLEditor::AutoInsertLineBreakHandler::HandleInsertLinefeed() {
    251  nsresult rv = mHTMLEditor.EnsureNoPaddingBRElementForEmptyEditor();
    252  if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    253    return NS_ERROR_EDITOR_DESTROYED;
    254  }
    255  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
    256                       "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
    257                       "failed, but ignored");
    258 
    259  if (NS_SUCCEEDED(rv) && mHTMLEditor.SelectionRef().IsCollapsed()) {
    260    nsresult rv =
    261        mHTMLEditor.EnsureCaretNotAfterInvisibleBRElement(mEditingHost);
    262    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    263      return NS_ERROR_EDITOR_DESTROYED;
    264    }
    265    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
    266                         "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
    267                         "failed, but ignored");
    268    if (NS_SUCCEEDED(rv)) {
    269      nsresult rv = mHTMLEditor.PrepareInlineStylesForCaret();
    270      if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    271        return NS_ERROR_EDITOR_DESTROYED;
    272      }
    273      NS_WARNING_ASSERTION(
    274          NS_SUCCEEDED(rv),
    275          "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
    276    }
    277  }
    278 
    279  const EditorDOMPoint atStartOfSelection =
    280      mHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>();
    281  if (NS_WARN_IF(!atStartOfSelection.IsInContentNode())) {
    282    return NS_ERROR_FAILURE;
    283  }
    284  MOZ_ASSERT(atStartOfSelection.IsSetAndValidInComposedDoc());
    285 
    286  // Do nothing if the node is read-only
    287  if (!HTMLEditUtils::IsSimplyEditableNode(
    288          *atStartOfSelection.GetContainer())) {
    289    return NS_SUCCESS_DOM_NO_OPERATION;
    290  }
    291 
    292  Result<EditorDOMPoint, nsresult> insertLineFeedResult =
    293      AutoInsertLineBreakHandler::InsertLinefeed(
    294          mHTMLEditor, atStartOfSelection, mEditingHost);
    295  if (MOZ_UNLIKELY(insertLineFeedResult.isErr())) {
    296    NS_WARNING("AutoInsertLineBreakHandler::InsertLinefeed() failed");
    297    return insertLineFeedResult.unwrapErr();
    298  }
    299  rv = mHTMLEditor.CollapseSelectionTo(insertLineFeedResult.inspect());
    300  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
    301                       "EditorBase::CollapseSelectionTo() failed");
    302  return rv;
    303 }
    304 
    305 // static
    306 Result<EditorDOMPoint, nsresult>
    307 HTMLEditor::AutoInsertLineBreakHandler::InsertLinefeed(
    308    HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToBreak,
    309    const Element& aEditingHost) {
    310  if (NS_WARN_IF(!aPointToBreak.IsSet())) {
    311    return Err(NS_ERROR_INVALID_ARG);
    312  }
    313 
    314  const RefPtr<Document> document = aHTMLEditor.GetDocument();
    315  MOZ_DIAGNOSTIC_ASSERT(document);
    316  if (NS_WARN_IF(!document)) {
    317    return Err(NS_ERROR_FAILURE);
    318  }
    319 
    320  // TODO: The following code is duplicated from `HandleInsertText`.  They
    321  //       should be merged when we fix bug 92921.
    322 
    323  Result<EditorDOMPoint, nsresult> setStyleResult =
    324      aHTMLEditor.CreateStyleForInsertText(aPointToBreak, aEditingHost);
    325  if (MOZ_UNLIKELY(setStyleResult.isErr())) {
    326    NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed");
    327    return setStyleResult.propagateErr();
    328  }
    329 
    330  EditorDOMPoint pointToInsert = setStyleResult.inspect().IsSet()
    331                                     ? setStyleResult.inspect()
    332                                     : aPointToBreak;
    333  if (NS_WARN_IF(!pointToInsert.IsSetAndValid()) ||
    334      NS_WARN_IF(!pointToInsert.IsInContentNode())) {
    335    return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
    336  }
    337  MOZ_ASSERT(pointToInsert.IsSetAndValid());
    338 
    339  // The node may not be able to have a text node so that we need to check it
    340  // here.
    341  pointToInsert = HTMLEditUtils::GetPossiblePointToInsert(
    342      pointToInsert, *nsGkAtoms::textTagName, aEditingHost);
    343  if (NS_WARN_IF(!pointToInsert.IsSet())) {
    344    return Err(NS_ERROR_FAILURE);
    345  }
    346  MOZ_ASSERT(pointToInsert.IsInContentNode());
    347 
    348  // FIXME: If the computed point does not preformat linefeed, we should switch
    349  // back to inserting a <br>.  However, I think it should be handled before
    350  // calling this.
    351 
    352  AutoRestore<bool> disableListener(
    353      aHTMLEditor.EditSubActionDataRef().mAdjustChangedRangeFromListener);
    354  aHTMLEditor.EditSubActionDataRef().mAdjustChangedRangeFromListener = false;
    355 
    356  // TODO: We don't need AutoTransactionsConserveSelection here in the normal
    357  //       cases, but removing this may cause the behavior with the legacy
    358  //       mutation event listeners.  We should try to delete this in a bug.
    359  AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);
    360 
    361  EditorDOMPoint pointToPutCaret;
    362  {
    363    AutoTrackDOMPoint trackingInsertingPosition(aHTMLEditor.RangeUpdaterRef(),
    364                                                &pointToInsert);
    365    Result<CreateLineBreakResult, nsresult> insertLinefeedResultOrError =
    366        aHTMLEditor.InsertLineBreak(WithTransaction::Yes,
    367                                    LineBreakType::Linefeed, pointToInsert,
    368                                    eNext);
    369    if (MOZ_UNLIKELY(insertLinefeedResultOrError.isErr())) {
    370      NS_WARNING(
    371          "HTMLEditor::InsertLineBreak(WithTransaction::Yes, "
    372          "LineBreakType::Linefeed, eNext) failed");
    373      return insertLinefeedResultOrError.propagateErr();
    374    }
    375    pointToPutCaret = insertLinefeedResultOrError.unwrap().UnwrapCaretPoint();
    376  }
    377 
    378  // Insert a padding <br> if the inserted linefeed is followed by a block
    379  // boundary.  Note that it should always be <br> for avoiding padding line
    380  // breaks appear in `.textContent` value.
    381  if (pointToPutCaret.IsInContentNode() && pointToPutCaret.IsEndOfContainer()) {
    382    const WSRunScanner scannerAtCaret({}, pointToPutCaret, &aEditingHost);
    383    const WSScanResult prevVisibleThing =
    384        scannerAtCaret.ScanPreviousVisibleNodeOrBlockBoundaryFrom(
    385            pointToPutCaret);
    386    const WSScanResult nextVisibleThing =
    387        scannerAtCaret.ScanInclusiveNextVisibleNodeOrBlockBoundaryFrom(
    388            pointToPutCaret);
    389    if (prevVisibleThing.ReachedPreformattedLineBreak() &&
    390        (nextVisibleThing.ReachedCurrentBlockBoundary() ||
    391         nextVisibleThing.ReachedInlineEditingHostBoundary()) &&
    392        HTMLEditUtils::CanNodeContain(*nextVisibleThing.ElementPtr(),
    393                                      *nsGkAtoms::br)) {
    394      AutoTrackDOMPoint trackingInsertedPosition(aHTMLEditor.RangeUpdaterRef(),
    395                                                 &pointToInsert);
    396      AutoTrackDOMPoint trackingNewCaretPosition(aHTMLEditor.RangeUpdaterRef(),
    397                                                 &pointToPutCaret);
    398      Result<CreateLineBreakResult, nsresult> insertBRElementResultOrError =
    399          aHTMLEditor.InsertLineBreak(
    400              WithTransaction::Yes, LineBreakType::BRElement, pointToPutCaret);
    401      if (MOZ_UNLIKELY(insertBRElementResultOrError.isErr())) {
    402        NS_WARNING(
    403            "HTMLEditor::InsertLineBreak(WithTransaction::Yes, "
    404            "LineBreakType::BRElement) failed");
    405        return insertBRElementResultOrError.propagateErr();
    406      }
    407      CreateLineBreakResult insertBRElementResult =
    408          insertBRElementResultOrError.unwrap();
    409      MOZ_ASSERT(insertBRElementResult.Handled());
    410      insertBRElementResult.IgnoreCaretPointSuggestion();
    411    }
    412  }
    413 
    414  // manually update the doc changed range so that
    415  // OnEndHandlingTopLevelEditSubActionInternal will clean up the correct
    416  // portion of the document.
    417  MOZ_ASSERT(pointToPutCaret.IsSet());
    418  if (NS_WARN_IF(!pointToPutCaret.IsSet())) {
    419    // XXX Here is odd.  We did mChangedRange->SetStartAndEnd(pointToInsert,
    420    //     pointToPutCaret), but it always fails because of the latter is unset.
    421    //     Therefore, always returning NS_ERROR_FAILURE from here is the
    422    //     traditional behavior...
    423    // TODO: Stop updating the interline position of Selection with fixing here
    424    //       and returning expected point.
    425    DebugOnly<nsresult> rvIgnored =
    426        aHTMLEditor.SelectionRef().SetInterlinePosition(
    427            InterlinePosition::EndOfLine);
    428    NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
    429                         "Selection::SetInterlinePosition(InterlinePosition::"
    430                         "EndOfLine) failed, but ignored");
    431    if (NS_FAILED(aHTMLEditor.TopLevelEditSubActionDataRef()
    432                      .mChangedRange->CollapseTo(pointToInsert))) {
    433      NS_WARNING("nsRange::CollapseTo() failed");
    434      return Err(NS_ERROR_FAILURE);
    435    }
    436    NS_WARNING(
    437        "We always return NS_ERROR_FAILURE here because of a failure of "
    438        "updating mChangedRange");
    439    return Err(NS_ERROR_FAILURE);
    440  }
    441 
    442  if (NS_FAILED(aHTMLEditor.TopLevelEditSubActionDataRef()
    443                    .mChangedRange->SetStartAndEnd(
    444                        pointToInsert.ToRawRangeBoundary(),
    445                        pointToPutCaret.ToRawRangeBoundary()))) {
    446    NS_WARNING("nsRange::SetStartAndEnd() failed");
    447    return Err(NS_ERROR_FAILURE);
    448  }
    449 
    450  pointToPutCaret.SetInterlinePosition(InterlinePosition::EndOfLine);
    451  return pointToPutCaret;
    452 }
    453 
    454 }  // namespace mozilla