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