1 /*
2  * Copyright (C) 2006, 2008 Apple Inc. All rights reserved.
3  * Copyright (C) 2010 Google Inc. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25  */
26 
27 #include "third_party/blink/renderer/core/editing/commands/apply_block_element_command.h"
28 
29 #include "third_party/blink/renderer/core/dom/node_computed_style.h"
30 #include "third_party/blink/renderer/core/dom/text.h"
31 #include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
32 #include "third_party/blink/renderer/core/editing/editing_utilities.h"
33 #include "third_party/blink/renderer/core/editing/selection_template.h"
34 #include "third_party/blink/renderer/core/editing/visible_position.h"
35 #include "third_party/blink/renderer/core/editing/visible_selection.h"
36 #include "third_party/blink/renderer/core/editing/visible_units.h"
37 #include "third_party/blink/renderer/core/html/html_br_element.h"
38 #include "third_party/blink/renderer/core/html/html_element.h"
39 #include "third_party/blink/renderer/core/html_names.h"
40 #include "third_party/blink/renderer/core/style/computed_style.h"
41 #include "third_party/blink/renderer/platform/bindings/exception_state.h"
42 #include "third_party/blink/renderer/platform/heap/heap.h"
43 
44 namespace blink {
45 
ApplyBlockElementCommand(Document & document,const QualifiedName & tag_name,const AtomicString & inline_style)46 ApplyBlockElementCommand::ApplyBlockElementCommand(
47     Document& document,
48     const QualifiedName& tag_name,
49     const AtomicString& inline_style)
50     : CompositeEditCommand(document),
51       tag_name_(tag_name),
52       inline_style_(inline_style) {}
53 
ApplyBlockElementCommand(Document & document,const QualifiedName & tag_name)54 ApplyBlockElementCommand::ApplyBlockElementCommand(
55     Document& document,
56     const QualifiedName& tag_name)
57     : CompositeEditCommand(document), tag_name_(tag_name) {}
58 
DoApply(EditingState * editing_state)59 void ApplyBlockElementCommand::DoApply(EditingState* editing_state) {
60   // ApplyBlockElementCommands are only created directly by editor commands'
61   // execution, which updates layout before entering doApply().
62   DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
63 
64   if (!RootEditableElementOf(EndingSelection().Base()))
65     return;
66 
67   VisiblePosition visible_end = EndingVisibleSelection().VisibleEnd();
68   VisiblePosition visible_start = EndingVisibleSelection().VisibleStart();
69   if (visible_start.IsNull() || visible_start.IsOrphan() ||
70       visible_end.IsNull() || visible_end.IsOrphan())
71     return;
72 
73   // When a selection ends at the start of a paragraph, we rarely paint
74   // the selection gap before that paragraph, because there often is no gap.
75   // In a case like this, it's not obvious to the user that the selection
76   // ends "inside" that paragraph, so it would be confusing if Indent/Outdent
77   // operated on that paragraph.
78   // FIXME: We paint the gap before some paragraphs that are indented with left
79   // margin/padding, but not others.  We should make the gap painting more
80   // consistent and then use a left margin/padding rule here.
81   if (visible_end.DeepEquivalent() != visible_start.DeepEquivalent() &&
82       IsStartOfParagraph(visible_end)) {
83     const Position& new_end =
84         PreviousPositionOf(visible_end, kCannotCrossEditingBoundary)
85             .DeepEquivalent();
86     SelectionInDOMTree::Builder builder;
87     builder.Collapse(visible_start.ToPositionWithAffinity());
88     if (new_end.IsNotNull())
89       builder.Extend(new_end);
90     SetEndingSelection(SelectionForUndoStep::From(builder.Build()));
91     ABORT_EDITING_COMMAND_IF(EndingVisibleSelection().VisibleStart().IsNull());
92     ABORT_EDITING_COMMAND_IF(EndingVisibleSelection().VisibleEnd().IsNull());
93   }
94 
95   VisibleSelection selection =
96       SelectionForParagraphIteration(EndingVisibleSelection());
97   VisiblePosition start_of_selection = selection.VisibleStart();
98   ABORT_EDITING_COMMAND_IF(start_of_selection.IsNull());
99   VisiblePosition end_of_selection = selection.VisibleEnd();
100   ABORT_EDITING_COMMAND_IF(end_of_selection.IsNull());
101   ContainerNode* start_scope = nullptr;
102   int start_index = IndexForVisiblePosition(start_of_selection, start_scope);
103   ContainerNode* end_scope = nullptr;
104   int end_index = IndexForVisiblePosition(end_of_selection, end_scope);
105 
106   FormatSelection(start_of_selection, end_of_selection, editing_state);
107   if (editing_state->IsAborted())
108     return;
109 
110   GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
111 
112   DCHECK_EQ(start_scope, end_scope);
113   DCHECK_GE(start_index, 0);
114   DCHECK_LE(start_index, end_index);
115   if (start_scope == end_scope && start_index >= 0 &&
116       start_index <= end_index) {
117     VisiblePosition start(VisiblePositionForIndex(start_index, start_scope));
118     VisiblePosition end(VisiblePositionForIndex(end_index, end_scope));
119     if (start.IsNotNull() && end.IsNotNull()) {
120       SetEndingSelection(SelectionForUndoStep::From(
121           SelectionInDOMTree::Builder()
122               .Collapse(start.ToPositionWithAffinity())
123               .Extend(end.DeepEquivalent())
124               .Build()));
125     }
126   }
127 }
128 
IsAtUnsplittableElement(const Position & pos)129 static bool IsAtUnsplittableElement(const Position& pos) {
130   Node* node = pos.AnchorNode();
131   return node == RootEditableElementOf(pos) ||
132          node == EnclosingNodeOfType(pos, &IsTableCell);
133 }
134 
FormatSelection(const VisiblePosition & start_of_selection,const VisiblePosition & end_of_selection,EditingState * editing_state)135 void ApplyBlockElementCommand::FormatSelection(
136     const VisiblePosition& start_of_selection,
137     const VisiblePosition& end_of_selection,
138     EditingState* editing_state) {
139   // Special case empty unsplittable elements because there's nothing to split
140   // and there's nothing to move.
141   const Position& caret_position =
142       MostForwardCaretPosition(start_of_selection.DeepEquivalent());
143   if (IsAtUnsplittableElement(caret_position)) {
144     HTMLElement* blockquote = CreateBlockElement();
145     InsertNodeAt(blockquote, caret_position, editing_state);
146     if (editing_state->IsAborted())
147       return;
148     auto* placeholder = MakeGarbageCollected<HTMLBRElement>(GetDocument());
149     AppendNode(placeholder, blockquote, editing_state);
150     if (editing_state->IsAborted())
151       return;
152     SetEndingSelection(SelectionForUndoStep::From(
153         SelectionInDOMTree::Builder()
154             .Collapse(Position::BeforeNode(*placeholder))
155             .Build()));
156     return;
157   }
158 
159   HTMLElement* blockquote_for_next_indent = nullptr;
160   VisiblePosition end_of_current_paragraph = EndOfParagraph(start_of_selection);
161   const VisiblePosition& visible_end_of_last_paragraph =
162       EndOfParagraph(end_of_selection);
163   const Position& end_of_next_last_paragraph =
164       EndOfParagraph(NextPositionOf(visible_end_of_last_paragraph))
165           .DeepEquivalent();
166   Position end_of_last_paragraph =
167       visible_end_of_last_paragraph.DeepEquivalent();
168 
169   bool at_end = false;
170   while (end_of_current_paragraph.DeepEquivalent() !=
171              end_of_next_last_paragraph &&
172          !at_end) {
173     if (end_of_current_paragraph.DeepEquivalent() == end_of_last_paragraph)
174       at_end = true;
175 
176     Position start, end;
177     RangeForParagraphSplittingTextNodesIfNeeded(
178         end_of_current_paragraph, end_of_last_paragraph, start, end);
179     end_of_current_paragraph = CreateVisiblePosition(end);
180 
181     Node* enclosing_cell = EnclosingNodeOfType(start, &IsTableCell);
182     PositionWithAffinity end_of_next_paragraph =
183         EndOfNextParagrahSplittingTextNodesIfNeeded(
184             end_of_current_paragraph, end_of_last_paragraph, start, end)
185             .ToPositionWithAffinity();
186 
187     FormatRange(start, end, end_of_last_paragraph, blockquote_for_next_indent,
188                 editing_state);
189     if (editing_state->IsAborted())
190       return;
191 
192     // Don't put the next paragraph in the blockquote we just created for this
193     // paragraph unless the next paragraph is in the same cell.
194     if (enclosing_cell &&
195         enclosing_cell !=
196             EnclosingNodeOfType(end_of_next_paragraph.GetPosition(),
197                                 &IsTableCell))
198       blockquote_for_next_indent = nullptr;
199 
200     // indentIntoBlockquote could move more than one paragraph if the paragraph
201     // is in a list item or a table. As a result,
202     // |endOfNextLastParagraph| could refer to a position no longer in the
203     // document.
204     if (end_of_next_last_paragraph.IsNotNull() &&
205         !end_of_next_last_paragraph.IsConnected())
206       break;
207     // Sanity check: Make sure our moveParagraph calls didn't remove
208     // endOfNextParagraph.anchorNode() If somehow, e.g. mutation
209     // event handler, we did, return to prevent crashes.
210     if (end_of_next_paragraph.IsNotNull() &&
211         !end_of_next_paragraph.IsConnected())
212       return;
213 
214     GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
215     end_of_current_paragraph = CreateVisiblePosition(end_of_next_paragraph);
216   }
217 }
218 
IsNewLineAtPosition(const Position & position)219 static bool IsNewLineAtPosition(const Position& position) {
220   auto* text_node = DynamicTo<Text>(position.ComputeContainerNode());
221   int offset = position.OffsetInContainerNode();
222   if (!text_node || offset < 0 ||
223       offset >= static_cast<int>(text_node->length()))
224     return false;
225 
226   DummyExceptionStateForTesting exception_state;
227   String text_at_position =
228       text_node->substringData(offset, 1, exception_state);
229   if (exception_state.HadException())
230     return false;
231 
232   return text_at_position[0] == '\n';
233 }
234 
ComputedStyleOfEnclosingTextNode(const Position & position)235 static const ComputedStyle* ComputedStyleOfEnclosingTextNode(
236     const Position& position) {
237   if (!position.IsOffsetInAnchor() || !position.ComputeContainerNode() ||
238       !position.ComputeContainerNode()->IsTextNode())
239     return nullptr;
240   return position.ComputeContainerNode()->GetComputedStyle();
241 }
242 
RangeForParagraphSplittingTextNodesIfNeeded(const VisiblePosition & end_of_current_paragraph,Position & end_of_last_paragraph,Position & start,Position & end)243 void ApplyBlockElementCommand::RangeForParagraphSplittingTextNodesIfNeeded(
244     const VisiblePosition& end_of_current_paragraph,
245     Position& end_of_last_paragraph,
246     Position& start,
247     Position& end) {
248   start = StartOfParagraph(end_of_current_paragraph).DeepEquivalent();
249   end = end_of_current_paragraph.DeepEquivalent();
250 
251   bool is_start_and_end_on_same_node = false;
252   if (const ComputedStyle* start_style =
253           ComputedStyleOfEnclosingTextNode(start)) {
254     is_start_and_end_on_same_node =
255         ComputedStyleOfEnclosingTextNode(end) &&
256         start.ComputeContainerNode() == end.ComputeContainerNode();
257     bool is_start_and_end_of_last_paragraph_on_same_node =
258         ComputedStyleOfEnclosingTextNode(end_of_last_paragraph) &&
259         start.ComputeContainerNode() ==
260             end_of_last_paragraph.ComputeContainerNode();
261 
262     // Avoid obtanining the start of next paragraph for start
263     // TODO(yosin) We should use |PositionMoveType::CodePoint| for
264     // |previousPositionOf()|.
265     if (start_style->PreserveNewline() && IsNewLineAtPosition(start) &&
266         !IsNewLineAtPosition(
267             PreviousPositionOf(start, PositionMoveType::kCodeUnit)) &&
268         start.OffsetInContainerNode() > 0)
269       start = StartOfParagraph(CreateVisiblePosition(PreviousPositionOf(
270                                    end, PositionMoveType::kCodeUnit)))
271                   .DeepEquivalent();
272 
273     // If start is in the middle of a text node, split.
274     if (!start_style->CollapseWhiteSpace() &&
275         start.OffsetInContainerNode() > 0) {
276       int start_offset = start.OffsetInContainerNode();
277       auto* start_text = To<Text>(start.ComputeContainerNode());
278       SplitTextNode(start_text, start_offset);
279       GetDocument().UpdateStyleAndLayoutTree();
280 
281       start = Position::FirstPositionInNode(*start_text);
282       if (is_start_and_end_on_same_node) {
283         DCHECK_GE(end.OffsetInContainerNode(), start_offset);
284         end = Position(start_text, end.OffsetInContainerNode() - start_offset);
285       }
286       if (is_start_and_end_of_last_paragraph_on_same_node) {
287         DCHECK_GE(end_of_last_paragraph.OffsetInContainerNode(), start_offset);
288         end_of_last_paragraph =
289             Position(start_text, end_of_last_paragraph.OffsetInContainerNode() -
290                                      start_offset);
291       }
292     }
293   }
294 
295   if (const ComputedStyle* end_style = ComputedStyleOfEnclosingTextNode(end)) {
296     bool is_end_and_end_of_last_paragraph_on_same_node =
297         ComputedStyleOfEnclosingTextNode(end_of_last_paragraph) &&
298         end.AnchorNode() == end_of_last_paragraph.AnchorNode();
299     // Include \n at the end of line if we're at an empty paragraph
300     if (end_style->PreserveNewline() && start == end &&
301         end.OffsetInContainerNode() <
302             static_cast<int>(To<Text>(end.ComputeContainerNode())->length())) {
303       int end_offset = end.OffsetInContainerNode();
304       // TODO(yosin) We should use |PositionMoveType::CodePoint| for
305       // |previousPositionOf()|.
306       if (!IsNewLineAtPosition(
307               PreviousPositionOf(end, PositionMoveType::kCodeUnit)) &&
308           IsNewLineAtPosition(end))
309         end = Position(end.ComputeContainerNode(), end_offset + 1);
310       if (is_end_and_end_of_last_paragraph_on_same_node &&
311           end.OffsetInContainerNode() >=
312               end_of_last_paragraph.OffsetInContainerNode())
313         end_of_last_paragraph = end;
314     }
315 
316     // If end is in the middle of a text node, split.
317     if (end_style->UserModify() != EUserModify::kReadOnly &&
318         !end_style->CollapseWhiteSpace() && end.OffsetInContainerNode() &&
319         end.OffsetInContainerNode() <
320             static_cast<int>(To<Text>(end.ComputeContainerNode())->length())) {
321       auto* end_container = To<Text>(end.ComputeContainerNode());
322       SplitTextNode(end_container, end.OffsetInContainerNode());
323       GetDocument().UpdateStyleAndLayoutTree();
324 
325       const Node* const previous_sibling_of_end =
326           end_container->previousSibling();
327       DCHECK(previous_sibling_of_end);
328       if (is_start_and_end_on_same_node) {
329         start = FirstPositionInOrBeforeNode(*previous_sibling_of_end);
330       }
331       if (is_end_and_end_of_last_paragraph_on_same_node) {
332         if (end_of_last_paragraph.OffsetInContainerNode() ==
333             end.OffsetInContainerNode()) {
334           end_of_last_paragraph =
335               LastPositionInOrAfterNode(*previous_sibling_of_end);
336         } else {
337           end_of_last_paragraph = Position(
338               end_container, end_of_last_paragraph.OffsetInContainerNode() -
339                                  end.OffsetInContainerNode());
340         }
341       }
342       end = Position::LastPositionInNode(*previous_sibling_of_end);
343     }
344   }
345 }
346 
347 VisiblePosition
EndOfNextParagrahSplittingTextNodesIfNeeded(VisiblePosition & end_of_current_paragraph,Position & end_of_last_paragraph,Position & start,Position & end)348 ApplyBlockElementCommand::EndOfNextParagrahSplittingTextNodesIfNeeded(
349     VisiblePosition& end_of_current_paragraph,
350     Position& end_of_last_paragraph,
351     Position& start,
352     Position& end) {
353   const VisiblePosition& end_of_next_paragraph =
354       EndOfParagraph(NextPositionOf(end_of_current_paragraph));
355   const Position& end_of_next_paragraph_position =
356       end_of_next_paragraph.DeepEquivalent();
357   const ComputedStyle* style =
358       ComputedStyleOfEnclosingTextNode(end_of_next_paragraph_position);
359   if (!style)
360     return end_of_next_paragraph;
361 
362   auto* const end_of_next_paragraph_text =
363       To<Text>(end_of_next_paragraph_position.ComputeContainerNode());
364   if (!style->PreserveNewline() ||
365       !end_of_next_paragraph_position.OffsetInContainerNode() ||
366       !IsNewLineAtPosition(
367           Position::FirstPositionInNode(*end_of_next_paragraph_text)))
368     return end_of_next_paragraph;
369 
370   // \n at the beginning of the text node immediately following the current
371   // paragraph is trimmed by moveParagraphWithClones. If endOfNextParagraph was
372   // pointing at this same text node, endOfNextParagraph will be shifted by one
373   // paragraph. Avoid this by splitting "\n"
374   SplitTextNode(end_of_next_paragraph_text, 1);
375   GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
376   Text* const previous_text =
377       DynamicTo<Text>(end_of_next_paragraph_text->previousSibling());
378   if (end_of_next_paragraph_text == start.ComputeContainerNode() &&
379       previous_text) {
380     DCHECK_LT(start.OffsetInContainerNode(),
381               end_of_next_paragraph_position.OffsetInContainerNode());
382     start = Position(previous_text, start.OffsetInContainerNode());
383   }
384   if (end_of_next_paragraph_text == end.ComputeContainerNode() &&
385       previous_text) {
386     DCHECK_LT(end.OffsetInContainerNode(),
387               end_of_next_paragraph_position.OffsetInContainerNode());
388     end = Position(previous_text, end.OffsetInContainerNode());
389   }
390   if (end_of_next_paragraph_text ==
391       end_of_last_paragraph.ComputeContainerNode()) {
392     if (end_of_last_paragraph.OffsetInContainerNode() <
393         end_of_next_paragraph_position.OffsetInContainerNode()) {
394       // We can only fix endOfLastParagraph if the previous node was still text
395       // and hasn't been modified by script.
396       if (previous_text && static_cast<unsigned>(
397                                end_of_last_paragraph.OffsetInContainerNode()) <=
398                                previous_text->length()) {
399         end_of_last_paragraph = Position(
400             previous_text, end_of_last_paragraph.OffsetInContainerNode());
401       }
402     } else {
403       end_of_last_paragraph =
404           Position(end_of_next_paragraph_text,
405                    end_of_last_paragraph.OffsetInContainerNode() - 1);
406     }
407   }
408 
409   return CreateVisiblePosition(
410       Position(end_of_next_paragraph_text,
411                end_of_next_paragraph_position.OffsetInContainerNode() - 1));
412 }
413 
CreateBlockElement() const414 HTMLElement* ApplyBlockElementCommand::CreateBlockElement() const {
415   HTMLElement* element = CreateHTMLElement(GetDocument(), tag_name_);
416   if (inline_style_.length())
417     element->setAttribute(html_names::kStyleAttr, inline_style_);
418   return element;
419 }
420 
421 }  // namespace blink
422