summaryrefslogtreecommitdiff
path: root/chromium/third_party/blink/renderer/core/editing
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2018-05-15 10:20:33 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2018-05-15 10:28:57 +0000
commitd17ea114e5ef69ad5d5d7413280a13e6428098aa (patch)
tree2c01a75df69f30d27b1432467cfe7c1467a498da /chromium/third_party/blink/renderer/core/editing
parent8c5c43c7b138c9b4b0bf56d946e61d3bbc111bec (diff)
downloadqtwebengine-chromium-d17ea114e5ef69ad5d5d7413280a13e6428098aa.tar.gz
BASELINE: Update Chromium to 67.0.3396.47
Change-Id: Idcb1341782e417561a2473eeecc82642dafda5b7 Reviewed-by: Michal Klocek <michal.klocek@qt.io>
Diffstat (limited to 'chromium/third_party/blink/renderer/core/editing')
-rw-r--r--chromium/third_party/blink/renderer/core/editing/BUILD.gn435
-rw-r--r--chromium/third_party/blink/renderer/core/editing/OWNERS8
-rw-r--r--chromium/third_party/blink/renderer/core/editing/caret_display_item_client.cc307
-rw-r--r--chromium/third_party/blink/renderer/core/editing/caret_display_item_client.h113
-rw-r--r--chromium/third_party/blink/renderer/core/editing/caret_display_item_client_test.cc484
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/append_node_command.cc62
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/append_node_command.h53
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/apply_block_element_command.cc423
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/apply_block_element_command.h76
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/apply_block_element_command_test.cc127
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/apply_style_command.cc2085
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/apply_style_command.h218
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/apply_style_command_test.cc98
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/break_blockquote_command.cc297
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/break_blockquote_command.h46
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/clipboard_commands.cc488
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/clipboard_commands.h125
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/composite_edit_command.cc2066
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/composite_edit_command.h239
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/composite_edit_command_test.cc149
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/create_link_command.cc68
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/create_link_command.h49
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/delete_from_text_node_command.cc75
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/delete_from_text_node_command.h59
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/delete_selection_command.cc1277
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/delete_selection_command.h123
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/delete_selection_command_test.cc78
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/delete_selection_options.cc75
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/delete_selection_options.h62
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/document_exec_command.cc159
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/drag_and_drop_command.cc29
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/drag_and_drop_command.h35
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/edit_command.cc108
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/edit_command.h102
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/editing_command_test.cc80
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/editing_commands_utilities.cc709
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h177
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/editing_commands_utilities_test.cc92
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/editing_state.cc36
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/editing_state.h99
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/editor_command.cc2006
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/editor_command.h84
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/editor_command_names.h155
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/format_block_command.cc206
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/format_block_command.h67
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/indent_outdent_command.cc455
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/indent_outdent_command.h71
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_commands.cc208
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_commands.h114
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_incremental_text_command.cc171
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_incremental_text_command.h29
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_incremental_text_command_test.cc72
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_into_text_node_command.cc81
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_into_text_node_command.h58
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_line_break_command.cc237
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_line_break_command.h51
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_list_command.cc710
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_list_command.h79
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_list_command_test.cc133
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_node_before_command.cc80
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_node_before_command.h62
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command.cc615
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command.h79
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command_test.cc58
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_text_command.cc351
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_text_command.h72
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/insert_text_command_test.cc283
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/merge_identical_elements_command.cc94
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/merge_identical_elements_command.h55
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/move_commands.cc595
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/move_commands.h264
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/remove_css_property_command.cc75
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/remove_css_property_command.h61
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/remove_format_command.cc89
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/remove_format_command.h48
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/remove_node_command.cc84
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/remove_node_command.h59
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/remove_node_preserving_children_command.cc72
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/remove_node_preserving_children_command.h57
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/replace_node_with_span_command.cc90
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/replace_node_with_span_command.h65
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/replace_selection_command.cc2071
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/replace_selection_command.h170
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/replace_selection_command_test.cc190
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/selection_for_undo_step.cc125
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/selection_for_undo_step.h99
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/set_character_data_command.cc73
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/set_character_data_command.h42
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/set_character_data_command_test.cc120
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/set_node_attribute_command.cc58
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/set_node_attribute_command.h60
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/simplify_markup_command.cc163
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/simplify_markup_command.h58
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/smart_replace.h40
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/smart_replace_cf.cc96
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/smart_replace_icu.cc115
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/split_element_command.cc112
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/split_element_command.h57
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/split_text_node_command.cc111
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/split_text_node_command.h58
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/split_text_node_command_test.cc98
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/split_text_node_containing_element_command.cc72
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/split_text_node_containing_element_command.h53
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/style_commands.cc609
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/style_commands.h215
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/typing_command.cc1108
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/typing_command.h206
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/typing_command_test.cc96
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/undo_stack.cc109
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/undo_stack.h90
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/undo_step.cc158
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/undo_step.h97
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/unlink_command.cc46
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/unlink_command.h47
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/wrap_contents_in_dummy_span_command.cc86
-rw-r--r--chromium/third_party/blink/renderer/core/editing/commands/wrap_contents_in_dummy_span_command.h57
-rw-r--r--chromium/third_party/blink/renderer/core/editing/dom_selection.cc847
-rw-r--r--chromium/third_party/blink/renderer/core/editing/dom_selection.h139
-rw-r--r--chromium/third_party/blink/renderer/core/editing/drag_caret.cc117
-rw-r--r--chromium/third_party/blink/renderer/core/editing/drag_caret.h87
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editing_behavior.cc301
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editing_behavior.h136
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editing_behavior_types.h49
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editing_boundary.h39
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editing_strategy.cc74
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editing_strategy.h43
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editing_strategy_test.cc35
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editing_style.cc1876
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editing_style.h274
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editing_style_test.cc38
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editing_style_utilities.cc239
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editing_style_utilities.h76
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editing_tri_state.h35
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editing_utilities.cc1764
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editing_utilities.h388
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editing_utilities_test.cc892
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editor.cc944
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editor.h292
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editor_key_bindings.cc92
-rw-r--r--chromium/third_party/blink/renderer/core/editing/editor_test.cc60
-rw-r--r--chromium/third_party/blink/renderer/core/editing/ephemeral_range.cc220
-rw-r--r--chromium/third_party/blink/renderer/core/editing/ephemeral_range.h158
-rw-r--r--chromium/third_party/blink/renderer/core/editing/ephemeral_range_test.cc229
-rw-r--r--chromium/third_party/blink/renderer/core/editing/finder/find_in_page_coordinates.cc162
-rw-r--r--chromium/third_party/blink/renderer/core/editing/finder/find_in_page_coordinates.h63
-rw-r--r--chromium/third_party/blink/renderer/core/editing/finder/find_options.h51
-rw-r--r--chromium/third_party/blink/renderer/core/editing/finder/text_finder.cc925
-rw-r--r--chromium/third_party/blink/renderer/core/editing/finder/text_finder.h298
-rw-r--r--chromium/third_party/blink/renderer/core/editing/finder/text_finder_test.cc638
-rw-r--r--chromium/third_party/blink/renderer/core/editing/forward.h58
-rw-r--r--chromium/third_party/blink/renderer/core/editing/frame_caret.cc237
-rw-r--r--chromium/third_party/blink/renderer/core/editing/frame_caret.h118
-rw-r--r--chromium/third_party/blink/renderer/core/editing/frame_caret_test.cc99
-rw-r--r--chromium/third_party/blink/renderer/core/editing/frame_selection.cc1247
-rw-r--r--chromium/third_party/blink/renderer/core/editing/frame_selection.h290
-rw-r--r--chromium/third_party/blink/renderer/core/editing/frame_selection_test.cc1086
-rw-r--r--chromium/third_party/blink/renderer/core/editing/granularity_strategy.cc290
-rw-r--r--chromium/third_party/blink/renderer/core/editing/granularity_strategy.h129
-rw-r--r--chromium/third_party/blink/renderer/core/editing/granularity_strategy_test.cc771
-rw-r--r--chromium/third_party/blink/renderer/core/editing/ime/ime_text_span.cc71
-rw-r--r--chromium/third_party/blink/renderer/core/editing/ime/ime_text_span.h81
-rw-r--r--chromium/third_party/blink/renderer/core/editing/ime/ime_text_span_test.cc76
-rw-r--r--chromium/third_party/blink/renderer/core/editing/ime/ime_text_span_vector_builder.cc45
-rw-r--r--chromium/third_party/blink/renderer/core/editing/ime/ime_text_span_vector_builder.h55
-rw-r--r--chromium/third_party/blink/renderer/core/editing/ime/input_method_controller.cc1489
-rw-r--r--chromium/third_party/blink/renderer/core/editing/ime/input_method_controller.h192
-rw-r--r--chromium/third_party/blink/renderer/core/editing/ime/input_method_controller_test.cc3234
-rw-r--r--chromium/third_party/blink/renderer/core/editing/inline_box_position.cc433
-rw-r--r--chromium/third_party/blink/renderer/core/editing/inline_box_position.h101
-rw-r--r--chromium/third_party/blink/renderer/core/editing/inline_box_position_test.cc52
-rw-r--r--chromium/third_party/blink/renderer/core/editing/inline_box_traversal.cc137
-rw-r--r--chromium/third_party/blink/renderer/core/editing/inline_box_traversal.h50
-rw-r--r--chromium/third_party/blink/renderer/core/editing/input_mode_names.json517
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/backwards_character_iterator.cc105
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/backwards_character_iterator.h68
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/backwards_text_buffer.cc25
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/backwards_text_buffer.h29
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/backwards_text_buffer_test.cc64
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/bit_stack.cc71
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/bit_stack.h55
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/character_iterator.cc232
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/character_iterator.h116
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/character_iterator_test.cc75
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/forwards_text_buffer.cc18
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/forwards_text_buffer.h28
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/forwards_text_buffer_test.cc64
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/fully_clipped_state_stack.cc99
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/fully_clipped_state_stack.h36
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/search_buffer.cc433
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/search_buffer.h109
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/search_buffer_test.cc84
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator.cc442
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator.h137
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator_test.cc337
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/text_buffer_base.cc37
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/text_buffer_base.h65
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/text_iterator.cc1003
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/text_iterator.h255
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_behavior.cc191
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_behavior.h140
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_behavior_test.cc107
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_test.cc1091
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_text_node_handler.cc580
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_text_node_handler.h114
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_text_state.cc232
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_text_state.h116
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/text_searcher_icu.cc169
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/text_searcher_icu.h44
-rw-r--r--chromium/third_party/blink/renderer/core/editing/iterators/text_searcher_icu_test.cc101
-rw-r--r--chromium/third_party/blink/renderer/core/editing/keyboard_test.cc186
-rw-r--r--chromium/third_party/blink/renderer/core/editing/layout_selection.cc891
-rw-r--r--chromium/third_party/blink/renderer/core/editing/layout_selection.h135
-rw-r--r--chromium/third_party/blink/renderer/core/editing/layout_selection_test.cc857
-rw-r--r--chromium/third_party/blink/renderer/core/editing/link_selection_test.cc358
-rw-r--r--chromium/third_party/blink/renderer/core/editing/local_caret_rect.cc285
-rw-r--r--chromium/third_party/blink/renderer/core/editing/local_caret_rect.h57
-rw-r--r--chromium/third_party/blink/renderer/core/editing/local_caret_rect_bidi_test.cc1644
-rw-r--r--chromium/third_party/blink/renderer/core/editing/local_caret_rect_test.cc949
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker.cc25
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker.h38
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker_list_impl.cc75
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker_list_impl.h55
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker_list_impl_test.cc43
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker_test.cc41
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/composition_marker.cc24
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/composition_marker.h40
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/composition_marker_list_impl.cc72
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/composition_marker_list_impl.h52
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/composition_marker_list_impl_test.cc77
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/composition_marker_test.cc41
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/document_marker.cc104
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/document_marker.h171
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/document_marker_controller.cc847
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/document_marker_controller.h176
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/document_marker_controller_test.cc477
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/document_marker_list.cc13
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/document_marker_list.h75
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/document_marker_test.cc219
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/grammar_marker.cc20
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/grammar_marker.h31
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/grammar_marker_list_impl.cc13
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/grammar_marker_list_impl.h26
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/grammar_marker_list_impl_test.cc29
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/grammar_marker_test.cc30
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/marker_test_utilities.h21
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor.cc211
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor.h64
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor_test.cc600
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/spell_check_marker.cc21
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/spell_check_marker.h40
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/spell_check_marker_list_impl.cc127
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/spell_check_marker_list_impl.h65
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/spelling_marker.cc20
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/spelling_marker.h31
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/spelling_marker_list_impl.cc13
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/spelling_marker_list_impl.h26
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/spelling_marker_list_impl_test.cc160
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/spelling_marker_test.cc30
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/styleable_marker.cc53
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/styleable_marker.h49
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker.cc59
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker.h63
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_list_impl.cc210
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_list_impl.h70
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_list_impl_test.cc346
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_properties.cc63
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_properties.h69
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_replacement_scope.cc25
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_replacement_scope.h31
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_test.cc73
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/text_match_marker.cc61
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/text_match_marker.h80
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/text_match_marker_list_impl.cc128
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/text_match_marker_list_impl.h66
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/text_match_marker_list_impl_test.cc43
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/unsorted_document_marker_list_editor.cc124
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/unsorted_document_marker_list_editor.h58
-rw-r--r--chromium/third_party/blink/renderer/core/editing/markers/unsorted_document_marker_list_editor_test.cc472
-rw-r--r--chromium/third_party/blink/renderer/core/editing/ng_flat_tree_shorthands.cc28
-rw-r--r--chromium/third_party/blink/renderer/core/editing/ng_flat_tree_shorthands.h26
-rw-r--r--chromium/third_party/blink/renderer/core/editing/plain_text_range.cc214
-rw-r--r--chromium/third_party/blink/renderer/core/editing/plain_text_range.h86
-rw-r--r--chromium/third_party/blink/renderer/core/editing/plain_text_range_test.cc32
-rw-r--r--chromium/third_party/blink/renderer/core/editing/position.cc783
-rw-r--r--chromium/third_party/blink/renderer/core/editing/position.h307
-rw-r--r--chromium/third_party/blink/renderer/core/editing/position_iterator.cc384
-rw-r--r--chromium/third_party/blink/renderer/core/editing/position_iterator.h104
-rw-r--r--chromium/third_party/blink/renderer/core/editing/position_iterator_test.cc247
-rw-r--r--chromium/third_party/blink/renderer/core/editing/position_test.cc259
-rw-r--r--chromium/third_party/blink/renderer/core/editing/position_with_affinity.cc54
-rw-r--r--chromium/third_party/blink/renderer/core/editing/position_with_affinity.h94
-rw-r--r--chromium/third_party/blink/renderer/core/editing/position_with_affinity_test.cc20
-rw-r--r--chromium/third_party/blink/renderer/core/editing/relocatable_position.cc27
-rw-r--r--chromium/third_party/blink/renderer/core/editing/relocatable_position.h35
-rw-r--r--chromium/third_party/blink/renderer/core/editing/relocatable_position_test.cc33
-rw-r--r--chromium/third_party/blink/renderer/core/editing/rendered_position.cc451
-rw-r--r--chromium/third_party/blink/renderer/core/editing/rendered_position.h127
-rw-r--r--chromium/third_party/blink/renderer/core/editing/rendered_position_test.cc275
-rw-r--r--chromium/third_party/blink/renderer/core/editing/reveal_selection_scope.cc60
-rw-r--r--chromium/third_party/blink/renderer/core/editing/reveal_selection_scope.h60
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection.idl72
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_adjuster.cc769
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_adjuster.h40
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_adjuster_test.cc40
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_controller.cc1362
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_controller.h154
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_controller_test.cc205
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_editor.cc432
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_editor.h122
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_modifier.cc892
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_modifier.h144
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_modifier_character.cc441
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_modifier_test.cc96
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_modifier_word.cc437
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_strategy.h20
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_template.cc438
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_template.h171
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_template_test.cc122
-rw-r--r--chromium/third_party/blink/renderer/core/editing/selection_type.h35
-rw-r--r--chromium/third_party/blink/renderer/core/editing/serializers/html_interchange.cc101
-rw-r--r--chromium/third_party/blink/renderer/core/editing/serializers/html_interchange.h46
-rw-r--r--chromium/third_party/blink/renderer/core/editing/serializers/markup_accumulator.cc243
-rw-r--r--chromium/third_party/blink/renderer/core/editing/serializers/markup_accumulator.h102
-rw-r--r--chromium/third_party/blink/renderer/core/editing/serializers/markup_formatter.cc510
-rw-r--r--chromium/third_party/blink/renderer/core/editing/serializers/markup_formatter.h129
-rw-r--r--chromium/third_party/blink/renderer/core/editing/serializers/serialization.cc782
-rw-r--r--chromium/third_party/blink/renderer/core/editing/serializers/serialization.h112
-rw-r--r--chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_accumulator.cc241
-rw-r--r--chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_accumulator.h100
-rw-r--r--chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_serializer.cc560
-rw-r--r--chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_serializer.h76
-rw-r--r--chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_serializer_test.cc306
-rw-r--r--chromium/third_party/blink/renderer/core/editing/serializers/text_offset.cc25
-rw-r--r--chromium/third_party/blink/renderer/core/editing/serializers/text_offset.h36
-rw-r--r--chromium/third_party/blink/renderer/core/editing/set_selection_options.cc82
-rw-r--r--chromium/third_party/blink/renderer/core/editing/set_selection_options.h83
-rw-r--r--chromium/third_party/blink/renderer/core/editing/set_selection_options_test.cc54
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/cold_mode_spell_check_requester.cc182
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/cold_mode_spell_check_requester.h66
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/hot_mode_spell_check_requester.cc128
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/hot_mode_spell_check_requester.h34
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_callback.cc240
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_callback.h109
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_callback_test.cc184
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/spell_check_requester.cc326
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/spell_check_requester.h143
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/spell_check_test_base.cc41
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/spell_check_test_base.h23
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/spell_checker.cc792
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/spell_checker.h126
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/spell_checker_test.cc376
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/text_checking.h64
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/text_checking_paragraph.cc153
-rw-r--r--chromium/third_party/blink/renderer/core/editing/spellcheck/text_checking_paragraph.h86
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/backspace_state_machine.cc255
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/backspace_state_machine.h72
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/backspace_state_machine_test.cc993
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/backward_code_point_state_machine.cc69
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/backward_code_point_state_machine.h53
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/backward_code_point_state_machine_test.cc79
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/backward_grapheme_boundary_state_machine.cc242
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/backward_grapheme_boundary_state_machine.h75
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/backward_grapheme_boundary_state_machine_test.cc509
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/forward_code_point_state_machine.cc69
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/forward_code_point_state_machine.h53
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/forward_code_point_state_machine_test.cc75
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/forward_grapheme_boundary_state_machine.cc257
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/forward_grapheme_boundary_state_machine.h78
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/forward_grapheme_boundary_state_machine_test.cc718
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_test_util.cc112
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_test_util.h59
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_util.cc131
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_util.h23
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_util_test.cc169
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.cc23
-rw-r--r--chromium/third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.h30
-rw-r--r--chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_backend_impl.cc63
-rw-r--r--chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_backend_impl.h39
-rw-r--r--chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller.cc638
-rw-r--r--chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller.h81
-rw-r--r--chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller_test.cc442
-rw-r--r--chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_info.h26
-rw-r--r--chromium/third_party/blink/renderer/core/editing/testing/editing_test_base.cc113
-rw-r--r--chromium/third_party/blink/renderer/core/editing/testing/editing_test_base.h75
-rw-r--r--chromium/third_party/blink/renderer/core/editing/testing/editing_test_base_test.cc37
-rw-r--r--chromium/third_party/blink/renderer/core/editing/testing/selection_sample.cc373
-rw-r--r--chromium/third_party/blink/renderer/core/editing/testing/selection_sample.h51
-rw-r--r--chromium/third_party/blink/renderer/core/editing/testing/selection_sample_test.cc414
-rw-r--r--chromium/third_party/blink/renderer/core/editing/text_affinity.cc27
-rw-r--r--chromium/third_party/blink/renderer/core/editing/text_affinity.h54
-rw-r--r--chromium/third_party/blink/renderer/core/editing/text_granularity.h47
-rw-r--r--chromium/third_party/blink/renderer/core/editing/text_offset_mapping.cc179
-rw-r--r--chromium/third_party/blink/renderer/core/editing/text_offset_mapping.h81
-rw-r--r--chromium/third_party/blink/renderer/core/editing/text_offset_mapping_test.cc239
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_position.cc225
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_position.h151
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_position_test.cc112
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_selection.cc493
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_selection.h183
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_selection_test.cc694
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_units.cc1490
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_units.h334
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_units_line.cc804
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_units_line_test.cc626
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_units_paragraph.cc378
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_units_paragraph_test.cc262
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_units_sentence.cc168
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_units_sentence_test.cc162
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_units_test.cc783
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_units_word.cc206
-rw-r--r--chromium/third_party/blink/renderer/core/editing/visible_units_word_test.cc455
-rw-r--r--chromium/third_party/blink/renderer/core/editing/web_substring_util.mm220
-rw-r--r--chromium/third_party/blink/renderer/core/editing/writing_direction.h35
413 files changed, 98473 insertions, 0 deletions
diff --git a/chromium/third_party/blink/renderer/core/editing/BUILD.gn b/chromium/third_party/blink/renderer/core/editing/BUILD.gn
new file mode 100644
index 00000000000..e5dcc13f92e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/BUILD.gn
@@ -0,0 +1,435 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//third_party/blink/renderer/core/core.gni")
+
+blink_core_sources("editing") {
+ split_count = 5
+ sources = [
+ "caret_display_item_client.cc",
+ "caret_display_item_client.h",
+ "commands/append_node_command.cc",
+ "commands/append_node_command.h",
+ "commands/apply_block_element_command.cc",
+ "commands/apply_block_element_command.h",
+ "commands/apply_style_command.cc",
+ "commands/apply_style_command.h",
+ "commands/break_blockquote_command.cc",
+ "commands/break_blockquote_command.h",
+ "commands/clipboard_commands.cc",
+ "commands/clipboard_commands.h",
+ "commands/composite_edit_command.cc",
+ "commands/composite_edit_command.h",
+ "commands/create_link_command.cc",
+ "commands/create_link_command.h",
+ "commands/delete_from_text_node_command.cc",
+ "commands/delete_from_text_node_command.h",
+ "commands/delete_selection_command.cc",
+ "commands/delete_selection_command.h",
+ "commands/delete_selection_options.cc",
+ "commands/delete_selection_options.h",
+ "commands/document_exec_command.cc",
+ "commands/drag_and_drop_command.cc",
+ "commands/drag_and_drop_command.h",
+ "commands/edit_command.cc",
+ "commands/edit_command.h",
+ "commands/editing_commands_utilities.cc",
+ "commands/editing_commands_utilities.h",
+ "commands/editing_state.cc",
+ "commands/editing_state.h",
+ "commands/editor_command.cc",
+ "commands/editor_command.h",
+ "commands/editor_command_names.h",
+ "commands/format_block_command.cc",
+ "commands/format_block_command.h",
+ "commands/indent_outdent_command.cc",
+ "commands/indent_outdent_command.h",
+ "commands/insert_commands.cc",
+ "commands/insert_commands.h",
+ "commands/insert_incremental_text_command.cc",
+ "commands/insert_incremental_text_command.h",
+ "commands/insert_into_text_node_command.cc",
+ "commands/insert_into_text_node_command.h",
+ "commands/insert_line_break_command.cc",
+ "commands/insert_line_break_command.h",
+ "commands/insert_list_command.cc",
+ "commands/insert_list_command.h",
+ "commands/insert_node_before_command.cc",
+ "commands/insert_node_before_command.h",
+ "commands/insert_paragraph_separator_command.cc",
+ "commands/insert_paragraph_separator_command.h",
+ "commands/insert_text_command.cc",
+ "commands/insert_text_command.h",
+ "commands/merge_identical_elements_command.cc",
+ "commands/merge_identical_elements_command.h",
+ "commands/move_commands.cc",
+ "commands/move_commands.h",
+ "commands/remove_css_property_command.cc",
+ "commands/remove_css_property_command.h",
+ "commands/remove_format_command.cc",
+ "commands/remove_format_command.h",
+ "commands/remove_node_command.cc",
+ "commands/remove_node_command.h",
+ "commands/remove_node_preserving_children_command.cc",
+ "commands/remove_node_preserving_children_command.h",
+ "commands/replace_node_with_span_command.cc",
+ "commands/replace_node_with_span_command.h",
+ "commands/replace_selection_command.cc",
+ "commands/replace_selection_command.h",
+ "commands/selection_for_undo_step.cc",
+ "commands/selection_for_undo_step.h",
+ "commands/set_character_data_command.cc",
+ "commands/set_character_data_command.h",
+ "commands/set_node_attribute_command.cc",
+ "commands/set_node_attribute_command.h",
+ "commands/simplify_markup_command.cc",
+ "commands/simplify_markup_command.h",
+ "commands/smart_replace.h",
+ "commands/smart_replace_icu.cc",
+ "commands/split_element_command.cc",
+ "commands/split_element_command.h",
+ "commands/split_text_node_command.cc",
+ "commands/split_text_node_command.h",
+ "commands/split_text_node_containing_element_command.cc",
+ "commands/split_text_node_containing_element_command.h",
+ "commands/style_commands.cc",
+ "commands/style_commands.h",
+ "commands/typing_command.cc",
+ "commands/typing_command.h",
+ "commands/undo_stack.cc",
+ "commands/undo_stack.h",
+ "commands/undo_step.cc",
+ "commands/undo_step.h",
+ "commands/unlink_command.cc",
+ "commands/unlink_command.h",
+ "commands/wrap_contents_in_dummy_span_command.cc",
+ "commands/wrap_contents_in_dummy_span_command.h",
+ "dom_selection.cc",
+ "dom_selection.h",
+ "drag_caret.cc",
+ "drag_caret.h",
+ "editing_behavior.cc",
+ "editing_behavior.h",
+ "editing_behavior_types.h",
+ "editing_boundary.h",
+ "editing_strategy.cc",
+ "editing_strategy.h",
+ "editing_style.cc",
+ "editing_style.h",
+ "editing_style_utilities.cc",
+ "editing_style_utilities.h",
+ "editing_tri_state.h",
+ "editing_utilities.cc",
+ "editing_utilities.h",
+ "editor.cc",
+ "editor.h",
+ "editor_key_bindings.cc",
+ "ephemeral_range.cc",
+ "ephemeral_range.h",
+ "finder/find_in_page_coordinates.cc",
+ "finder/find_in_page_coordinates.h",
+ "finder/text_finder.cc",
+ "finder/text_finder.h",
+ "forward.h",
+ "frame_caret.cc",
+ "frame_caret.h",
+ "frame_selection.cc",
+ "frame_selection.h",
+ "granularity_strategy.cc",
+ "granularity_strategy.h",
+ "ime/ime_text_span.cc",
+ "ime/ime_text_span.h",
+ "ime/ime_text_span_vector_builder.cc",
+ "ime/ime_text_span_vector_builder.h",
+ "ime/input_method_controller.cc",
+ "ime/input_method_controller.h",
+ "inline_box_position.cc",
+ "inline_box_position.h",
+ "inline_box_traversal.cc",
+ "inline_box_traversal.h",
+ "iterators/backwards_character_iterator.cc",
+ "iterators/backwards_character_iterator.h",
+ "iterators/backwards_text_buffer.cc",
+ "iterators/backwards_text_buffer.h",
+ "iterators/bit_stack.cc",
+ "iterators/bit_stack.h",
+ "iterators/character_iterator.cc",
+ "iterators/character_iterator.h",
+ "iterators/forwards_text_buffer.cc",
+ "iterators/forwards_text_buffer.h",
+ "iterators/fully_clipped_state_stack.cc",
+ "iterators/fully_clipped_state_stack.h",
+ "iterators/search_buffer.cc",
+ "iterators/search_buffer.h",
+ "iterators/simplified_backwards_text_iterator.cc",
+ "iterators/simplified_backwards_text_iterator.h",
+ "iterators/text_buffer_base.cc",
+ "iterators/text_buffer_base.h",
+ "iterators/text_iterator.cc",
+ "iterators/text_iterator.h",
+ "iterators/text_iterator_behavior.cc",
+ "iterators/text_iterator_behavior.h",
+ "iterators/text_iterator_text_node_handler.cc",
+ "iterators/text_iterator_text_node_handler.h",
+ "iterators/text_iterator_text_state.cc",
+ "iterators/text_iterator_text_state.h",
+ "iterators/text_searcher_icu.cc",
+ "iterators/text_searcher_icu.h",
+ "layout_selection.cc",
+ "layout_selection.h",
+ "local_caret_rect.cc",
+ "local_caret_rect.h",
+ "markers/active_suggestion_marker.cc",
+ "markers/active_suggestion_marker.h",
+ "markers/active_suggestion_marker_list_impl.cc",
+ "markers/active_suggestion_marker_list_impl.h",
+ "markers/composition_marker.cc",
+ "markers/composition_marker.h",
+ "markers/composition_marker_list_impl.cc",
+ "markers/composition_marker_list_impl.h",
+ "markers/document_marker.cc",
+ "markers/document_marker.h",
+ "markers/document_marker_controller.cc",
+ "markers/document_marker_controller.h",
+ "markers/document_marker_list.cc",
+ "markers/document_marker_list.h",
+ "markers/grammar_marker.cc",
+ "markers/grammar_marker.h",
+ "markers/grammar_marker_list_impl.cc",
+ "markers/grammar_marker_list_impl.h",
+ "markers/sorted_document_marker_list_editor.cc",
+ "markers/sorted_document_marker_list_editor.h",
+ "markers/spell_check_marker.cc",
+ "markers/spell_check_marker.h",
+ "markers/spell_check_marker_list_impl.cc",
+ "markers/spell_check_marker_list_impl.h",
+ "markers/spelling_marker.cc",
+ "markers/spelling_marker.h",
+ "markers/spelling_marker_list_impl.cc",
+ "markers/spelling_marker_list_impl.h",
+ "markers/styleable_marker.cc",
+ "markers/styleable_marker.h",
+ "markers/suggestion_marker.cc",
+ "markers/suggestion_marker.h",
+ "markers/suggestion_marker_list_impl.cc",
+ "markers/suggestion_marker_list_impl.h",
+ "markers/suggestion_marker_properties.cc",
+ "markers/suggestion_marker_properties.h",
+ "markers/suggestion_marker_replacement_scope.cc",
+ "markers/suggestion_marker_replacement_scope.h",
+ "markers/text_match_marker.cc",
+ "markers/text_match_marker.h",
+ "markers/text_match_marker_list_impl.cc",
+ "markers/text_match_marker_list_impl.h",
+ "markers/unsorted_document_marker_list_editor.cc",
+ "markers/unsorted_document_marker_list_editor.h",
+ "ng_flat_tree_shorthands.cc",
+ "ng_flat_tree_shorthands.h",
+ "plain_text_range.cc",
+ "plain_text_range.h",
+ "position.cc",
+ "position.h",
+ "position_iterator.cc",
+ "position_iterator.h",
+ "position_with_affinity.cc",
+ "position_with_affinity.h",
+ "relocatable_position.cc",
+ "relocatable_position.h",
+ "rendered_position.cc",
+ "rendered_position.h",
+ "reveal_selection_scope.cc",
+ "reveal_selection_scope.h",
+ "selection_adjuster.cc",
+ "selection_adjuster.h",
+ "selection_controller.cc",
+ "selection_controller.h",
+ "selection_editor.cc",
+ "selection_editor.h",
+ "selection_modifier.cc",
+ "selection_modifier.h",
+ "selection_modifier_character.cc",
+ "selection_modifier_word.cc",
+ "selection_strategy.h",
+ "selection_template.cc",
+ "selection_template.h",
+ "selection_type.h",
+ "serializers/html_interchange.cc",
+ "serializers/html_interchange.h",
+ "serializers/markup_accumulator.cc",
+ "serializers/markup_accumulator.h",
+ "serializers/markup_formatter.cc",
+ "serializers/markup_formatter.h",
+ "serializers/serialization.cc",
+ "serializers/serialization.h",
+ "serializers/styled_markup_accumulator.cc",
+ "serializers/styled_markup_accumulator.h",
+ "serializers/styled_markup_serializer.cc",
+ "serializers/styled_markup_serializer.h",
+ "serializers/text_offset.cc",
+ "serializers/text_offset.h",
+ "set_selection_options.cc",
+ "set_selection_options.h",
+ "spellcheck/cold_mode_spell_check_requester.cc",
+ "spellcheck/cold_mode_spell_check_requester.h",
+ "spellcheck/hot_mode_spell_check_requester.cc",
+ "spellcheck/hot_mode_spell_check_requester.h",
+ "spellcheck/idle_spell_check_callback.cc",
+ "spellcheck/idle_spell_check_callback.h",
+ "spellcheck/spell_check_requester.cc",
+ "spellcheck/spell_check_requester.h",
+ "spellcheck/spell_checker.cc",
+ "spellcheck/spell_checker.h",
+ "spellcheck/text_checking.h",
+ "spellcheck/text_checking_paragraph.cc",
+ "spellcheck/text_checking_paragraph.h",
+ "state_machines/backspace_state_machine.cc",
+ "state_machines/backspace_state_machine.h",
+ "state_machines/backward_code_point_state_machine.cc",
+ "state_machines/backward_code_point_state_machine.h",
+ "state_machines/backward_grapheme_boundary_state_machine.cc",
+ "state_machines/backward_grapheme_boundary_state_machine.h",
+ "state_machines/forward_code_point_state_machine.cc",
+ "state_machines/forward_code_point_state_machine.h",
+ "state_machines/forward_grapheme_boundary_state_machine.cc",
+ "state_machines/forward_grapheme_boundary_state_machine.h",
+ "state_machines/state_machine_util.cc",
+ "state_machines/state_machine_util.h",
+ "state_machines/text_segmentation_machine_state.cc",
+ "state_machines/text_segmentation_machine_state.h",
+ "suggestion/text_suggestion_backend_impl.cc",
+ "suggestion/text_suggestion_backend_impl.h",
+ "suggestion/text_suggestion_controller.cc",
+ "suggestion/text_suggestion_controller.h",
+ "suggestion/text_suggestion_info.h",
+ "text_affinity.cc",
+ "text_affinity.h",
+ "text_granularity.h",
+ "text_offset_mapping.cc",
+ "text_offset_mapping.h",
+ "visible_position.cc",
+ "visible_position.h",
+ "visible_selection.cc",
+ "visible_selection.h",
+ "visible_units.cc",
+ "visible_units.h",
+ "visible_units_line.cc",
+ "visible_units_paragraph.cc",
+ "visible_units_sentence.cc",
+ "visible_units_word.cc",
+ "web_substring_util.mm",
+ "writing_direction.h",
+ ]
+
+ if (is_mac) {
+ sources += [ "commands/smart_replace_cf.cc" ]
+ }
+}
+
+jumbo_source_set("unit_tests") {
+ testonly = true
+ sources = [
+ "commands/apply_block_element_command_test.cc",
+ "commands/apply_style_command_test.cc",
+ "commands/composite_edit_command_test.cc",
+ "commands/delete_selection_command_test.cc",
+ "commands/editing_command_test.cc",
+ "commands/editing_commands_utilities_test.cc",
+ "commands/insert_incremental_text_command_test.cc",
+ "commands/insert_list_command_test.cc",
+ "commands/insert_paragraph_separator_command_test.cc",
+ "commands/insert_text_command_test.cc",
+ "commands/replace_selection_command_test.cc",
+ "commands/set_character_data_command_test.cc",
+ "commands/split_text_node_command_test.cc",
+ "commands/typing_command_test.cc",
+ "editing_strategy_test.cc",
+ "editing_style_test.cc",
+ "editing_utilities_test.cc",
+ "editor_test.cc",
+ "ephemeral_range_test.cc",
+ "frame_caret_test.cc",
+ "frame_selection_test.cc",
+ "granularity_strategy_test.cc",
+ "ime/ime_text_span_test.cc",
+ "ime/input_method_controller_test.cc",
+ "inline_box_position_test.cc",
+ "iterators/backwards_text_buffer_test.cc",
+ "iterators/character_iterator_test.cc",
+ "iterators/forwards_text_buffer_test.cc",
+ "iterators/search_buffer_test.cc",
+ "iterators/simplified_backwards_text_iterator_test.cc",
+ "iterators/text_iterator_behavior_test.cc",
+ "iterators/text_iterator_test.cc",
+ "iterators/text_searcher_icu_test.cc",
+ "layout_selection_test.cc",
+ "local_caret_rect_bidi_test.cc",
+ "local_caret_rect_test.cc",
+ "markers/active_suggestion_marker_list_impl_test.cc",
+ "markers/active_suggestion_marker_test.cc",
+ "markers/composition_marker_list_impl_test.cc",
+ "markers/composition_marker_test.cc",
+ "markers/document_marker_controller_test.cc",
+ "markers/document_marker_test.cc",
+ "markers/grammar_marker_list_impl_test.cc",
+ "markers/grammar_marker_test.cc",
+ "markers/sorted_document_marker_list_editor_test.cc",
+ "markers/spelling_marker_list_impl_test.cc",
+ "markers/spelling_marker_test.cc",
+ "markers/suggestion_marker_list_impl_test.cc",
+ "markers/suggestion_marker_test.cc",
+ "markers/text_match_marker_list_impl_test.cc",
+ "markers/unsorted_document_marker_list_editor_test.cc",
+ "plain_text_range_test.cc",
+ "position_iterator_test.cc",
+ "position_test.cc",
+ "position_with_affinity_test.cc",
+ "relocatable_position_test.cc",
+ "rendered_position_test.cc",
+ "selection_adjuster_test.cc",
+ "selection_controller_test.cc",
+ "selection_modifier_test.cc",
+ "selection_template_test.cc",
+ "serializers/styled_markup_serializer_test.cc",
+ "set_selection_options_test.cc",
+ "spellcheck/idle_spell_check_callback_test.cc",
+ "spellcheck/spell_check_test_base.cc",
+ "spellcheck/spell_check_test_base.h",
+ "spellcheck/spell_checker_test.cc",
+ "state_machines/backspace_state_machine_test.cc",
+ "state_machines/backward_code_point_state_machine_test.cc",
+ "state_machines/backward_grapheme_boundary_state_machine_test.cc",
+ "state_machines/forward_code_point_state_machine_test.cc",
+ "state_machines/forward_grapheme_boundary_state_machine_test.cc",
+ "state_machines/state_machine_test_util.cc",
+ "state_machines/state_machine_test_util.h",
+ "state_machines/state_machine_util_test.cc",
+ "suggestion/text_suggestion_controller_test.cc",
+ "testing/editing_test_base.cc",
+ "testing/editing_test_base.h",
+ "testing/editing_test_base_test.cc",
+ "testing/selection_sample.cc",
+ "testing/selection_sample.h",
+ "testing/selection_sample_test.cc",
+ "text_offset_mapping_test.cc",
+ "visible_position_test.cc",
+ "visible_selection_test.cc",
+ "visible_units_line_test.cc",
+ "visible_units_paragraph_test.cc",
+ "visible_units_sentence_test.cc",
+ "visible_units_test.cc",
+ "visible_units_word_test.cc",
+ ]
+
+ configs += [
+ "//third_party/blink/renderer/core:blink_core_pch",
+ "//third_party/blink/renderer:config",
+ "//third_party/blink/renderer:inside_blink",
+ ]
+
+ deps = [
+ "//testing/gmock",
+ "//testing/gtest",
+ "//third_party/blink/renderer/core",
+ ]
+}
diff --git a/chromium/third_party/blink/renderer/core/editing/OWNERS b/chromium/third_party/blink/renderer/core/editing/OWNERS
new file mode 100644
index 00000000000..fcd162db0be
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/OWNERS
@@ -0,0 +1,8 @@
+yosin@chromium.org
+xiaochengh@chromium.org
+
+# IME-related changes
+changwan@chromium.org
+
+# TEAM: editing-dev@chromium.org
+# COMPONENT: Blink>Editing
diff --git a/chromium/third_party/blink/renderer/core/editing/caret_display_item_client.cc b/chromium/third_party/blink/renderer/core/editing/caret_display_item_client.cc
new file mode 100644
index 00000000000..af432495ae1
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/caret_display_item_client.cc
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2004, 2008, 2009, 2010 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/caret_display_item_client.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/local_caret_rect.h"
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/layout/layout_block.h"
+#include "third_party/blink/renderer/core/layout/layout_view.h"
+#include "third_party/blink/renderer/core/paint/find_paint_offset_and_visual_rect_needing_update.h"
+#include "third_party/blink/renderer/core/paint/object_paint_invalidator.h"
+#include "third_party/blink/renderer/core/paint/paint_info.h"
+#include "third_party/blink/renderer/core/paint/paint_invalidator.h"
+#include "third_party/blink/renderer/core/paint/paint_layer.h"
+#include "third_party/blink/renderer/platform/graphics/graphics_context.h"
+#include "third_party/blink/renderer/platform/graphics/paint/drawing_recorder.h"
+
+namespace blink {
+
+CaretDisplayItemClient::CaretDisplayItemClient() = default;
+CaretDisplayItemClient::~CaretDisplayItemClient() = default;
+
+static inline bool CaretRendersInsideNode(const Node* node) {
+ return node && !IsDisplayInsideTable(node) && !EditingIgnoresContent(*node);
+}
+
+LayoutBlock* CaretDisplayItemClient::CaretLayoutBlock(const Node* node) {
+ if (!node)
+ return nullptr;
+
+ LayoutObject* layout_object = node->GetLayoutObject();
+ if (!layout_object)
+ return nullptr;
+
+ // if caretNode is a block and caret is inside it then caret should be painted
+ // by that block
+ bool painted_by_block =
+ layout_object->IsLayoutBlock() && CaretRendersInsideNode(node);
+ // TODO(yoichio): This function is called at least
+ // DocumentLifeCycle::LayoutClean but caretRendersInsideNode above can
+ // layout. Thus |node->layoutObject()| can be changed then this is bad
+ // design. We should make caret painting algorithm clean.
+ CHECK_EQ(layout_object, node->GetLayoutObject())
+ << "Layout tree should not changed";
+ return painted_by_block ? ToLayoutBlock(layout_object)
+ : layout_object->ContainingBlock();
+}
+
+static LayoutRect MapCaretRectToCaretPainter(const LayoutBlock* caret_block,
+ const LocalCaretRect& caret_rect) {
+ // FIXME: This shouldn't be called on un-rooted subtrees.
+ // FIXME: This should probably just use mapLocalToAncestor.
+ // Compute an offset between the caretLayoutItem and the caretPainterItem.
+
+ LayoutObject* caret_layout_object =
+ const_cast<LayoutObject*>(caret_rect.layout_object);
+ DCHECK(caret_layout_object->IsDescendantOf(caret_block));
+
+ LayoutRect result_rect = caret_rect.rect;
+ caret_block->FlipForWritingMode(result_rect);
+ while (caret_layout_object != caret_block) {
+ LayoutObject* container_object = caret_layout_object->Container();
+ if (!container_object)
+ return LayoutRect();
+ result_rect.Move(
+ caret_layout_object->OffsetFromContainer(container_object));
+ caret_layout_object = container_object;
+ }
+ return result_rect;
+}
+
+LayoutRect CaretDisplayItemClient::ComputeCaretRect(
+ const PositionWithAffinity& caret_position) {
+ if (caret_position.IsNull())
+ return LayoutRect();
+
+ DCHECK(caret_position.AnchorNode()->GetLayoutObject());
+
+ // First compute a rect local to the layoutObject at the selection start.
+ const LocalCaretRect& caret_rect = LocalCaretRectOfPosition(caret_position);
+
+ // Get the layoutObject that will be responsible for painting the caret
+ // (which is either the layoutObject we just found, or one of its containers).
+ const LayoutBlock* caret_block =
+ CaretLayoutBlock(caret_position.AnchorNode());
+ return MapCaretRectToCaretPainter(caret_block, caret_rect);
+}
+
+void CaretDisplayItemClient::ClearPreviousVisualRect(const LayoutBlock& block) {
+ if (block == layout_block_)
+ visual_rect_ = LayoutRect();
+ if (block == previous_layout_block_)
+ visual_rect_in_previous_layout_block_ = LayoutRect();
+}
+
+void CaretDisplayItemClient::LayoutBlockWillBeDestroyed(
+ const LayoutBlock& block) {
+ if (block == layout_block_)
+ layout_block_ = nullptr;
+ if (block == previous_layout_block_)
+ previous_layout_block_ = nullptr;
+}
+
+void CaretDisplayItemClient::UpdateStyleAndLayoutIfNeeded(
+ const PositionWithAffinity& caret_position) {
+ // This method may be called multiple times (e.g. in partial lifecycle
+ // updates) before a paint invalidation. We should save m_previousLayoutBlock
+ // and m_visualRectInPreviousLayoutBlock only if they have not been saved
+ // since the last paint invalidation to ensure the caret painted in the
+ // previous paint invalidated block will be invalidated. We don't care about
+ // intermediate changes of layoutBlock because they are not painted.
+ if (!previous_layout_block_) {
+ previous_layout_block_ = layout_block_;
+ visual_rect_in_previous_layout_block_ = visual_rect_;
+ }
+
+ LayoutBlock* new_layout_block = CaretLayoutBlock(caret_position.AnchorNode());
+ if (new_layout_block != layout_block_) {
+ if (layout_block_)
+ layout_block_->SetMayNeedPaintInvalidation();
+ layout_block_ = new_layout_block;
+ visual_rect_ = LayoutRect();
+ if (new_layout_block) {
+ needs_paint_invalidation_ = true;
+ if (new_layout_block == previous_layout_block_) {
+ // The caret has disappeared and is reappearing in the same block,
+ // since the last paint invalidation. Set m_visualRect as if the caret
+ // has always been there as paint invalidation doesn't care about the
+ // intermediate changes.
+ visual_rect_ = visual_rect_in_previous_layout_block_;
+ }
+ }
+ }
+
+ if (!new_layout_block) {
+ color_ = Color();
+ local_rect_ = LayoutRect();
+ return;
+ }
+
+ Color new_color;
+ if (caret_position.AnchorNode()) {
+ new_color = caret_position.AnchorNode()->GetLayoutObject()->ResolveColor(
+ GetCSSPropertyCaretColor());
+ }
+ if (new_color != color_) {
+ needs_paint_invalidation_ = true;
+ color_ = new_color;
+ }
+
+ LayoutRect new_local_rect = ComputeCaretRect(caret_position);
+ if (new_local_rect != local_rect_) {
+ needs_paint_invalidation_ = true;
+ local_rect_ = new_local_rect;
+ }
+
+ if (needs_paint_invalidation_)
+ new_layout_block->SetMayNeedPaintInvalidation();
+}
+
+void CaretDisplayItemClient::InvalidatePaint(
+ const LayoutBlock& block,
+ const PaintInvalidatorContext& context) {
+ if (block == layout_block_) {
+ InvalidatePaintInCurrentLayoutBlock(context);
+ return;
+ }
+
+ if (block == previous_layout_block_)
+ InvalidatePaintInPreviousLayoutBlock(context);
+}
+
+void CaretDisplayItemClient::InvalidatePaintInPreviousLayoutBlock(
+ const PaintInvalidatorContext& context) {
+ DCHECK(previous_layout_block_);
+
+ ObjectPaintInvalidatorWithContext object_invalidator(*previous_layout_block_,
+ context);
+ // For SPv175 raster invalidation will be done in PaintController.
+ if (!RuntimeEnabledFeatures::SlimmingPaintV175Enabled() &&
+ !IsImmediateFullPaintInvalidationReason(
+ previous_layout_block_->FullPaintInvalidationReason())) {
+ object_invalidator.InvalidatePaintRectangleWithContext(
+ visual_rect_in_previous_layout_block_, PaintInvalidationReason::kCaret);
+ }
+
+ context.painting_layer->SetNeedsRepaint();
+ object_invalidator.InvalidateDisplayItemClient(
+ *this, PaintInvalidationReason::kCaret);
+ previous_layout_block_ = nullptr;
+}
+
+void CaretDisplayItemClient::InvalidatePaintInCurrentLayoutBlock(
+ const PaintInvalidatorContext& context) {
+ DCHECK(layout_block_);
+
+ LayoutRect new_visual_rect;
+#if DCHECK_IS_ON()
+ FindVisualRectNeedingUpdateScope finder(*layout_block_, context, visual_rect_,
+ new_visual_rect);
+#endif
+ if (context.NeedsVisualRectUpdate(*layout_block_)) {
+ if (!local_rect_.IsEmpty()) {
+ new_visual_rect = local_rect_;
+ context.MapLocalRectToVisualRectInBacking(*layout_block_,
+ new_visual_rect);
+
+ if (layout_block_->UsesCompositedScrolling()) {
+ // The caret should use scrolling coordinate space.
+ DCHECK(layout_block_ == context.paint_invalidation_container);
+ new_visual_rect.Move(
+ LayoutSize(layout_block_->ScrolledContentOffset()));
+ }
+ }
+ } else {
+ new_visual_rect = visual_rect_;
+ }
+
+ if (layout_block_ == previous_layout_block_)
+ previous_layout_block_ = nullptr;
+
+ ObjectPaintInvalidatorWithContext object_invalidator(*layout_block_, context);
+ if (!needs_paint_invalidation_ && new_visual_rect == visual_rect_) {
+ // The caret may change paint offset without changing visual rect, and we
+ // need to invalidate the display item client if the block is doing full
+ // paint invalidation.
+ if (IsImmediateFullPaintInvalidationReason(
+ layout_block_->FullPaintInvalidationReason()) ||
+ // For SPv1, kSubtreeInvalidationChecking may hint change of
+ // paint offset. See ObjectPaintInvalidatorWithContext::
+ // invalidatePaintIfNeededWithComputedReason().
+ (!RuntimeEnabledFeatures::SlimmingPaintV175Enabled() &&
+ (context.subtree_flags &
+ PaintInvalidatorContext::kSubtreeInvalidationChecking))) {
+ object_invalidator.InvalidateDisplayItemClient(
+ *this, PaintInvalidationReason::kCaret);
+ }
+ return;
+ }
+
+ needs_paint_invalidation_ = false;
+
+ if (!RuntimeEnabledFeatures::SlimmingPaintV175Enabled() &&
+ !IsImmediateFullPaintInvalidationReason(
+ layout_block_->FullPaintInvalidationReason())) {
+ object_invalidator.FullyInvalidatePaint(PaintInvalidationReason::kCaret,
+ visual_rect_, new_visual_rect);
+ }
+
+ context.painting_layer->SetNeedsRepaint();
+ object_invalidator.InvalidateDisplayItemClient(
+ *this, PaintInvalidationReason::kCaret);
+
+ visual_rect_ = new_visual_rect;
+}
+
+void CaretDisplayItemClient::PaintCaret(
+ GraphicsContext& context,
+ const LayoutPoint& paint_offset,
+ DisplayItem::Type display_item_type) const {
+ if (DrawingRecorder::UseCachedDrawingIfPossible(context, *this,
+ display_item_type))
+ return;
+
+ LayoutRect drawing_rect = local_rect_;
+ drawing_rect.MoveBy(paint_offset);
+
+ DrawingRecorder recorder(context, *this, display_item_type);
+ IntRect paint_rect = PixelSnappedIntRect(drawing_rect);
+ context.FillRect(paint_rect, color_);
+}
+
+String CaretDisplayItemClient::DebugName() const {
+ return "Caret";
+}
+
+LayoutRect CaretDisplayItemClient::VisualRect() const {
+ return visual_rect_;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/caret_display_item_client.h b/chromium/third_party/blink/renderer/core/editing/caret_display_item_client.h
new file mode 100644
index 00000000000..c2c8ca5332f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/caret_display_item_client.h
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_CARET_DISPLAY_ITEM_CLIENT_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_CARET_DISPLAY_ITEM_CLIENT_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/geometry/int_rect.h"
+#include "third_party/blink/renderer/platform/geometry/layout_rect.h"
+#include "third_party/blink/renderer/platform/graphics/color.h"
+#include "third_party/blink/renderer/platform/graphics/paint/display_item.h"
+
+namespace blink {
+
+class Node;
+class GraphicsContext;
+class LayoutBlock;
+struct PaintInvalidatorContext;
+
+class CaretDisplayItemClient final : public DisplayItemClient {
+ public:
+ CaretDisplayItemClient();
+ virtual ~CaretDisplayItemClient();
+
+ // TODO(yosin,wangxianzhu): Make these two static functions private or
+ // combine them into updateForPaintInvalidation() when the callsites in
+ // FrameCaret are removed.
+
+ // Creating VisiblePosition causes synchronous layout so we should use the
+ // PositionWithAffinity version if possible.
+ // A position in HTMLTextFromControlElement is a typical example.
+ static LayoutRect ComputeCaretRect(
+ const PositionWithAffinity& caret_position);
+ static LayoutBlock* CaretLayoutBlock(const Node*);
+
+ // Called indirectly from LayoutBlock::clearPreviousVisualRects().
+ void ClearPreviousVisualRect(const LayoutBlock&);
+
+ // Called indirectly from LayoutBlock::willBeDestroyed().
+ void LayoutBlockWillBeDestroyed(const LayoutBlock&);
+
+ // Called when a FrameView finishes layout. Updates style and geometry of the
+ // caret for paint invalidation and painting.
+ void UpdateStyleAndLayoutIfNeeded(const PositionWithAffinity& caret_position);
+
+ // Called during LayoutBlock paint invalidation.
+ void InvalidatePaint(const LayoutBlock&, const PaintInvalidatorContext&);
+
+ bool ShouldPaintCaret(const LayoutBlock& block) const {
+ return &block == layout_block_;
+ }
+ void PaintCaret(GraphicsContext&,
+ const LayoutPoint& paint_offset,
+ DisplayItem::Type) const;
+
+ // DisplayItemClient methods.
+ LayoutRect VisualRect() const final;
+ String DebugName() const final;
+
+ private:
+ friend class CaretDisplayItemClientTest;
+
+ void InvalidatePaintInCurrentLayoutBlock(const PaintInvalidatorContext&);
+ void InvalidatePaintInPreviousLayoutBlock(const PaintInvalidatorContext&);
+
+ // These are updated by updateStyleAndLayoutIfNeeded().
+ Color color_;
+ LayoutRect local_rect_;
+ LayoutBlock* layout_block_ = nullptr;
+
+ // Visual rect of the caret in m_layoutBlock. This is updated by
+ // invalidatePaintIfNeeded().
+ LayoutRect visual_rect_;
+
+ // These are set to the previous value of m_layoutBlock and m_visualRect
+ // during updateStyleAndLayoutIfNeeded() if they haven't been set since the
+ // last paint invalidation. They can only be used in invalidatePaintIfNeeded()
+ // to invalidate the caret in the previous layout block.
+ const LayoutBlock* previous_layout_block_ = nullptr;
+ LayoutRect visual_rect_in_previous_layout_block_;
+
+ bool needs_paint_invalidation_ = false;
+
+ DISALLOW_COPY_AND_ASSIGN(CaretDisplayItemClient);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_CARET_DISPLAY_ITEM_CLIENT_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/caret_display_item_client_test.cc b/chromium/third_party/blink/renderer/core/editing/caret_display_item_client_test.cc
new file mode 100644
index 00000000000..a27f802ded0
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/caret_display_item_client_test.cc
@@ -0,0 +1,484 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/caret_display_item_client.h"
+
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/layout/layout_view.h"
+#include "third_party/blink/renderer/core/page/focus_controller.h"
+#include "third_party/blink/renderer/core/paint/paint_layer.h"
+#include "third_party/blink/renderer/core/testing/core_unit_test_helper.h"
+#include "third_party/blink/renderer/platform/graphics/graphics_layer.h"
+#include "third_party/blink/renderer/platform/graphics/paint/raster_invalidation_tracking.h"
+#include "third_party/blink/renderer/platform/testing/paint_test_configurations.h"
+
+namespace blink {
+
+class CaretDisplayItemClientTest : public PaintTestConfigurations,
+ public RenderingTest {
+ protected:
+ void SetUp() override {
+ RenderingTest::SetUp();
+ EnableCompositing();
+ Selection().SetCaretBlinkingSuspended(true);
+ }
+
+ const RasterInvalidationTracking* GetRasterInvalidationTracking() const {
+ // TODO(wangxianzhu): Test SPv2.
+ DCHECK(!RuntimeEnabledFeatures::SlimmingPaintV2Enabled());
+ return GetLayoutView()
+ .Layer()
+ ->GraphicsLayerBacking()
+ ->GetRasterInvalidationTracking();
+ }
+
+ FrameSelection& Selection() const {
+ return GetDocument().View()->GetFrame().Selection();
+ }
+
+ const DisplayItemClient& GetCaretDisplayItemClient() const {
+ return Selection().CaretDisplayItemClientForTesting();
+ }
+
+ const LayoutBlock* CaretLayoutBlock() const {
+ return static_cast<const CaretDisplayItemClient&>(
+ GetCaretDisplayItemClient())
+ .layout_block_;
+ }
+
+ const LayoutBlock* PreviousCaretLayoutBlock() const {
+ return static_cast<const CaretDisplayItemClient&>(
+ GetCaretDisplayItemClient())
+ .previous_layout_block_;
+ }
+
+ Text* AppendTextNode(const String& data) {
+ Text* text = GetDocument().createTextNode(data);
+ GetDocument().body()->AppendChild(text);
+ return text;
+ }
+
+ Element* AppendBlock(const String& data) {
+ Element* block = GetDocument().CreateRawElement(HTMLNames::divTag);
+ Text* text = GetDocument().createTextNode(data);
+ block->AppendChild(text);
+ GetDocument().body()->AppendChild(block);
+ return block;
+ }
+
+ void UpdateAllLifecyclePhases() {
+ // Partial lifecycle updates should not affect caret paint invalidation.
+ GetDocument().View()->UpdateLifecycleToLayoutClean();
+ GetDocument().View()->UpdateAllLifecyclePhases();
+ // Partial lifecycle updates should not affect caret paint invalidation.
+ GetDocument().View()->UpdateLifecycleToLayoutClean();
+ }
+};
+
+INSTANTIATE_TEST_CASE_P(All,
+ CaretDisplayItemClientTest,
+ testing::ValuesIn(kAllSlimmingPaintTestConfigurations));
+
+TEST_P(CaretDisplayItemClientTest, CaretPaintInvalidation) {
+ GetDocument().body()->setContentEditable("true", ASSERT_NO_EXCEPTION);
+ GetDocument().GetPage()->GetFocusController().SetActive(true);
+ GetDocument().GetPage()->GetFocusController().SetFocused(true);
+
+ Text* text = AppendTextNode("Hello, World!");
+ UpdateAllLifecyclePhases();
+ const auto* block = ToLayoutBlock(GetDocument().body()->GetLayoutObject());
+
+ // Focus the body. Should invalidate the new caret.
+ GetDocument().View()->SetTracksPaintInvalidations(true);
+ GetDocument().body()->focus();
+ UpdateAllLifecyclePhases();
+ EXPECT_TRUE(block->ShouldPaintCursorCaret());
+
+ LayoutRect caret_visual_rect = GetCaretDisplayItemClient().VisualRect();
+ EXPECT_EQ(1, caret_visual_rect.Width());
+ EXPECT_EQ(block->Location(), caret_visual_rect.Location());
+
+ const Vector<RasterInvalidationInfo>* raster_invalidations;
+ if (!RuntimeEnabledFeatures::SlimmingPaintV2Enabled()) {
+ raster_invalidations = &GetRasterInvalidationTracking()->Invalidations();
+ ASSERT_EQ(1u, raster_invalidations->size());
+ EXPECT_EQ(EnclosingIntRect(caret_visual_rect),
+ (*raster_invalidations)[0].rect);
+ if (RuntimeEnabledFeatures::SlimmingPaintV175Enabled()) {
+ EXPECT_EQ(&GetCaretDisplayItemClient(),
+ (*raster_invalidations)[0].client);
+ EXPECT_EQ(PaintInvalidationReason::kAppeared,
+ (*raster_invalidations)[0].reason);
+ } else {
+ EXPECT_EQ(block, (*raster_invalidations)[0].client);
+ EXPECT_EQ(PaintInvalidationReason::kCaret,
+ (*raster_invalidations)[0].reason);
+ }
+ }
+
+ std::unique_ptr<JSONArray> object_invalidations =
+ GetDocument().View()->TrackedObjectPaintInvalidationsAsJSON();
+ ASSERT_EQ(1u, object_invalidations->size());
+ String s;
+ JSONObject::Cast(object_invalidations->at(0))->Get("object")->AsString(&s);
+ EXPECT_EQ("Caret", s);
+ GetDocument().View()->SetTracksPaintInvalidations(false);
+
+ // Move the caret to the end of the text. Should invalidate both the old and
+ // new carets.
+ GetDocument().View()->SetTracksPaintInvalidations(true);
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder().Collapse(Position(text, 5)).Build());
+ UpdateAllLifecyclePhases();
+ EXPECT_TRUE(block->ShouldPaintCursorCaret());
+
+ LayoutRect new_caret_visual_rect = GetCaretDisplayItemClient().VisualRect();
+ EXPECT_EQ(caret_visual_rect.Size(), new_caret_visual_rect.Size());
+ EXPECT_EQ(caret_visual_rect.Y(), new_caret_visual_rect.Y());
+ EXPECT_LT(caret_visual_rect.X(), new_caret_visual_rect.X());
+
+ if (!RuntimeEnabledFeatures::SlimmingPaintV2Enabled()) {
+ raster_invalidations = &GetRasterInvalidationTracking()->Invalidations();
+ ASSERT_EQ(2u, raster_invalidations->size());
+ EXPECT_EQ(EnclosingIntRect(caret_visual_rect),
+ (*raster_invalidations)[0].rect);
+ if (RuntimeEnabledFeatures::SlimmingPaintV175Enabled()) {
+ EXPECT_EQ(&GetCaretDisplayItemClient(),
+ (*raster_invalidations)[0].client);
+ } else {
+ EXPECT_EQ(block, (*raster_invalidations)[0].client);
+ }
+ EXPECT_EQ(PaintInvalidationReason::kCaret,
+ (*raster_invalidations)[0].reason);
+ EXPECT_EQ(EnclosingIntRect(new_caret_visual_rect),
+ (*raster_invalidations)[1].rect);
+ if (RuntimeEnabledFeatures::SlimmingPaintV175Enabled()) {
+ EXPECT_EQ(&GetCaretDisplayItemClient(),
+ (*raster_invalidations)[1].client);
+ } else {
+ EXPECT_EQ(block, (*raster_invalidations)[1].client);
+ }
+ EXPECT_EQ(PaintInvalidationReason::kCaret,
+ (*raster_invalidations)[1].reason);
+ }
+
+ object_invalidations =
+ GetDocument().View()->TrackedObjectPaintInvalidationsAsJSON();
+ ASSERT_EQ(1u, object_invalidations->size());
+ JSONObject::Cast(object_invalidations->at(0))->Get("object")->AsString(&s);
+ EXPECT_EQ("Caret", s);
+ GetDocument().View()->SetTracksPaintInvalidations(false);
+
+ // Remove selection. Should invalidate the old caret.
+ LayoutRect old_caret_visual_rect = new_caret_visual_rect;
+ GetDocument().View()->SetTracksPaintInvalidations(true);
+ Selection().SetSelectionAndEndTyping(SelectionInDOMTree());
+ UpdateAllLifecyclePhases();
+ EXPECT_FALSE(block->ShouldPaintCursorCaret());
+ EXPECT_EQ(LayoutRect(), GetCaretDisplayItemClient().VisualRect());
+
+ if (!RuntimeEnabledFeatures::SlimmingPaintV2Enabled()) {
+ raster_invalidations = &GetRasterInvalidationTracking()->Invalidations();
+ ASSERT_EQ(1u, raster_invalidations->size());
+ EXPECT_EQ(EnclosingIntRect(old_caret_visual_rect),
+ (*raster_invalidations)[0].rect);
+ if (RuntimeEnabledFeatures::SlimmingPaintV175Enabled()) {
+ EXPECT_EQ(&GetCaretDisplayItemClient(),
+ (*raster_invalidations)[0].client);
+ } else {
+ EXPECT_EQ(block, (*raster_invalidations)[0].client);
+ }
+ }
+
+ object_invalidations =
+ GetDocument().View()->TrackedObjectPaintInvalidationsAsJSON();
+ ASSERT_EQ(1u, object_invalidations->size());
+ JSONObject::Cast(object_invalidations->at(0))->Get("object")->AsString(&s);
+ EXPECT_EQ("Caret", s);
+ GetDocument().View()->SetTracksPaintInvalidations(false);
+}
+
+TEST_P(CaretDisplayItemClientTest, CaretMovesBetweenBlocks) {
+ GetDocument().body()->setContentEditable("true", ASSERT_NO_EXCEPTION);
+ GetDocument().GetPage()->GetFocusController().SetActive(true);
+ GetDocument().GetPage()->GetFocusController().SetFocused(true);
+ auto* block_element1 = AppendBlock("Block1");
+ auto* block_element2 = AppendBlock("Block2");
+ UpdateAllLifecyclePhases();
+ auto* block1 = ToLayoutBlock(block_element1->GetLayoutObject());
+ auto* block2 = ToLayoutBlock(block_element2->GetLayoutObject());
+
+ // Focus the body.
+ GetDocument().body()->focus();
+ UpdateAllLifecyclePhases();
+ LayoutRect caret_visual_rect1 = GetCaretDisplayItemClient().VisualRect();
+ EXPECT_EQ(1, caret_visual_rect1.Width());
+ EXPECT_EQ(block1->FirstFragment().VisualRect().Location(),
+ caret_visual_rect1.Location());
+ EXPECT_TRUE(block1->ShouldPaintCursorCaret());
+ EXPECT_FALSE(block2->ShouldPaintCursorCaret());
+
+ // Move the caret into block2. Should invalidate both the old and new carets.
+ GetDocument().View()->SetTracksPaintInvalidations(true);
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(block_element2, 0))
+ .Build());
+ UpdateAllLifecyclePhases();
+
+ LayoutRect caret_visual_rect2 = GetCaretDisplayItemClient().VisualRect();
+ EXPECT_EQ(1, caret_visual_rect2.Width());
+ EXPECT_EQ(block2->FirstFragment().VisualRect().Location(),
+ caret_visual_rect2.Location());
+ EXPECT_FALSE(block1->ShouldPaintCursorCaret());
+ EXPECT_TRUE(block2->ShouldPaintCursorCaret());
+
+ const Vector<RasterInvalidationInfo>* raster_invalidations;
+ if (!RuntimeEnabledFeatures::SlimmingPaintV2Enabled()) {
+ raster_invalidations = &GetRasterInvalidationTracking()->Invalidations();
+ ASSERT_EQ(2u, raster_invalidations->size());
+ if (RuntimeEnabledFeatures::SlimmingPaintV175Enabled()) {
+ EXPECT_EQ(EnclosingIntRect(caret_visual_rect1),
+ (*raster_invalidations)[0].rect);
+ EXPECT_EQ(&GetCaretDisplayItemClient(),
+ (*raster_invalidations)[0].client);
+ EXPECT_EQ(PaintInvalidationReason::kCaret,
+ (*raster_invalidations)[0].reason);
+ EXPECT_EQ(EnclosingIntRect(caret_visual_rect2),
+ (*raster_invalidations)[1].rect);
+ EXPECT_EQ(&GetCaretDisplayItemClient(),
+ (*raster_invalidations)[1].client);
+ EXPECT_EQ(PaintInvalidationReason::kCaret,
+ (*raster_invalidations)[1].reason);
+ } else {
+ EXPECT_EQ(EnclosingIntRect(caret_visual_rect1),
+ (*raster_invalidations)[0].rect);
+ EXPECT_EQ(block1, (*raster_invalidations)[0].client);
+ EXPECT_EQ(PaintInvalidationReason::kCaret,
+ (*raster_invalidations)[0].reason);
+ EXPECT_EQ(EnclosingIntRect(caret_visual_rect2),
+ (*raster_invalidations)[1].rect);
+ EXPECT_EQ(block2, (*raster_invalidations)[1].client);
+ EXPECT_EQ(PaintInvalidationReason::kCaret,
+ (*raster_invalidations)[1].reason);
+ }
+ }
+
+ std::unique_ptr<JSONArray> object_invalidations =
+ GetDocument().View()->TrackedObjectPaintInvalidationsAsJSON();
+ ASSERT_EQ(2u, object_invalidations->size());
+ GetDocument().View()->SetTracksPaintInvalidations(false);
+
+ // Move the caret back into block1.
+ GetDocument().View()->SetTracksPaintInvalidations(true);
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(block_element1, 0))
+ .Build());
+ UpdateAllLifecyclePhases();
+
+ EXPECT_EQ(caret_visual_rect1, GetCaretDisplayItemClient().VisualRect());
+ EXPECT_TRUE(block1->ShouldPaintCursorCaret());
+ EXPECT_FALSE(block2->ShouldPaintCursorCaret());
+
+ if (!RuntimeEnabledFeatures::SlimmingPaintV2Enabled()) {
+ raster_invalidations = &GetRasterInvalidationTracking()->Invalidations();
+ ASSERT_EQ(2u, raster_invalidations->size());
+ if (RuntimeEnabledFeatures::SlimmingPaintV175Enabled()) {
+ EXPECT_EQ(EnclosingIntRect(caret_visual_rect2),
+ (*raster_invalidations)[0].rect);
+ EXPECT_EQ(&GetCaretDisplayItemClient(),
+ (*raster_invalidations)[0].client);
+ EXPECT_EQ(PaintInvalidationReason::kCaret,
+ (*raster_invalidations)[0].reason);
+ EXPECT_EQ(EnclosingIntRect(caret_visual_rect1),
+ (*raster_invalidations)[1].rect);
+ EXPECT_EQ(&GetCaretDisplayItemClient(),
+ (*raster_invalidations)[1].client);
+ EXPECT_EQ(PaintInvalidationReason::kCaret,
+ (*raster_invalidations)[1].reason);
+ } else {
+ EXPECT_EQ(EnclosingIntRect(caret_visual_rect1),
+ (*raster_invalidations)[0].rect);
+ EXPECT_EQ(block1, (*raster_invalidations)[0].client);
+ EXPECT_EQ(PaintInvalidationReason::kCaret,
+ (*raster_invalidations)[0].reason);
+ EXPECT_EQ(EnclosingIntRect(caret_visual_rect2),
+ (*raster_invalidations)[1].rect);
+ EXPECT_EQ(block2, (*raster_invalidations)[1].client);
+ EXPECT_EQ(PaintInvalidationReason::kCaret,
+ (*raster_invalidations)[1].reason);
+ }
+ }
+
+ object_invalidations =
+ GetDocument().View()->TrackedObjectPaintInvalidationsAsJSON();
+ ASSERT_EQ(2u, object_invalidations->size());
+ GetDocument().View()->SetTracksPaintInvalidations(false);
+}
+
+TEST_P(CaretDisplayItemClientTest, UpdatePreviousLayoutBlock) {
+ GetDocument().body()->setContentEditable("true", ASSERT_NO_EXCEPTION);
+ GetDocument().GetPage()->GetFocusController().SetActive(true);
+ GetDocument().GetPage()->GetFocusController().SetFocused(true);
+ auto* block_element1 = AppendBlock("Block1");
+ auto* block_element2 = AppendBlock("Block2");
+ UpdateAllLifecyclePhases();
+ auto* block1 = ToLayoutBlock(block_element1->GetLayoutObject());
+ auto* block2 = ToLayoutBlock(block_element2->GetLayoutObject());
+
+ // Set caret into block2.
+ GetDocument().body()->focus();
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(block_element2, 0))
+ .Build());
+ GetDocument().View()->UpdateLifecycleToLayoutClean();
+ EXPECT_TRUE(block2->ShouldPaintCursorCaret());
+ EXPECT_EQ(block2, CaretLayoutBlock());
+ EXPECT_FALSE(block1->ShouldPaintCursorCaret());
+ EXPECT_FALSE(PreviousCaretLayoutBlock());
+
+ // Move caret into block1. Should set previousCaretLayoutBlock to block2.
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(block_element1, 0))
+ .Build());
+ GetDocument().View()->UpdateLifecycleToLayoutClean();
+ EXPECT_TRUE(block1->ShouldPaintCursorCaret());
+ EXPECT_EQ(block1, CaretLayoutBlock());
+ EXPECT_FALSE(block2->ShouldPaintCursorCaret());
+ EXPECT_EQ(block2, PreviousCaretLayoutBlock());
+
+ // Move caret into block2. Partial update should not change
+ // previousCaretLayoutBlock.
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(block_element2, 0))
+ .Build());
+ GetDocument().View()->UpdateLifecycleToLayoutClean();
+ EXPECT_TRUE(block2->ShouldPaintCursorCaret());
+ EXPECT_EQ(block2, CaretLayoutBlock());
+ EXPECT_FALSE(block1->ShouldPaintCursorCaret());
+ EXPECT_EQ(block2, PreviousCaretLayoutBlock());
+
+ // Remove block2. Should clear caretLayoutBlock and previousCaretLayoutBlock.
+ block_element2->parentNode()->RemoveChild(block_element2);
+ EXPECT_FALSE(CaretLayoutBlock());
+ EXPECT_FALSE(PreviousCaretLayoutBlock());
+
+ // Set caret into block1.
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(block_element1, 0))
+ .Build());
+ UpdateAllLifecyclePhases();
+ // Remove selection.
+ Selection().SetSelectionAndEndTyping(SelectionInDOMTree());
+ GetDocument().View()->UpdateLifecycleToLayoutClean();
+ EXPECT_EQ(block1, PreviousCaretLayoutBlock());
+}
+
+TEST_P(CaretDisplayItemClientTest, CaretHideMoveAndShow) {
+ GetDocument().body()->setContentEditable("true", ASSERT_NO_EXCEPTION);
+ GetDocument().GetPage()->GetFocusController().SetActive(true);
+ GetDocument().GetPage()->GetFocusController().SetFocused(true);
+
+ Text* text = AppendTextNode("Hello, World!");
+ GetDocument().body()->focus();
+ UpdateAllLifecyclePhases();
+ const auto* block = ToLayoutBlock(GetDocument().body()->GetLayoutObject());
+
+ LayoutRect caret_visual_rect = GetCaretDisplayItemClient().VisualRect();
+ EXPECT_EQ(1, caret_visual_rect.Width());
+ EXPECT_EQ(block->Location(), caret_visual_rect.Location());
+
+ GetDocument().View()->SetTracksPaintInvalidations(true);
+ // Simulate that the blinking cursor becomes invisible.
+ Selection().SetCaretVisible(false);
+ // Move the caret to the end of the text.
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder().Collapse(Position(text, 5)).Build());
+ // Simulate that the cursor blinking is restarted.
+ Selection().SetCaretVisible(true);
+ UpdateAllLifecyclePhases();
+
+ LayoutRect new_caret_visual_rect = GetCaretDisplayItemClient().VisualRect();
+ EXPECT_EQ(caret_visual_rect.Size(), new_caret_visual_rect.Size());
+ EXPECT_EQ(caret_visual_rect.Y(), new_caret_visual_rect.Y());
+ EXPECT_LT(caret_visual_rect.X(), new_caret_visual_rect.X());
+
+ if (!RuntimeEnabledFeatures::SlimmingPaintV2Enabled()) {
+ const auto& raster_invalidations =
+ GetRasterInvalidationTracking()->Invalidations();
+ ASSERT_EQ(2u, raster_invalidations.size());
+ EXPECT_EQ(EnclosingIntRect(caret_visual_rect),
+ raster_invalidations[0].rect);
+ if (RuntimeEnabledFeatures::SlimmingPaintV175Enabled())
+ EXPECT_EQ(&GetCaretDisplayItemClient(), raster_invalidations[0].client);
+ else
+ EXPECT_EQ(block, raster_invalidations[0].client);
+ EXPECT_EQ(PaintInvalidationReason::kCaret, raster_invalidations[0].reason);
+ EXPECT_EQ(EnclosingIntRect(new_caret_visual_rect),
+ raster_invalidations[1].rect);
+ if (RuntimeEnabledFeatures::SlimmingPaintV175Enabled())
+ EXPECT_EQ(&GetCaretDisplayItemClient(), raster_invalidations[1].client);
+ else
+ EXPECT_EQ(block, raster_invalidations[1].client);
+ EXPECT_EQ(PaintInvalidationReason::kCaret, raster_invalidations[1].reason);
+ }
+
+ auto object_invalidations =
+ GetDocument().View()->TrackedObjectPaintInvalidationsAsJSON();
+ ASSERT_EQ(1u, object_invalidations->size());
+ String s;
+ JSONObject::Cast(object_invalidations->at(0))->Get("object")->AsString(&s);
+ EXPECT_EQ("Caret", s);
+ GetDocument().View()->SetTracksPaintInvalidations(false);
+}
+
+TEST_P(CaretDisplayItemClientTest, CompositingChange) {
+ if (RuntimeEnabledFeatures::SlimmingPaintV2Enabled())
+ return;
+
+ EnableCompositing();
+ SetBodyInnerHTML(
+ "<style>"
+ " body { margin: 0 }"
+ " #container { position: absolute; top: 55px; left: 66px; }"
+ "</style>"
+ "<div id='container'>"
+ " <div id='editor' contenteditable style='padding: 50px'>ABCDE</div>"
+ "</div>");
+
+ GetDocument().GetPage()->GetFocusController().SetActive(true);
+ GetDocument().GetPage()->GetFocusController().SetFocused(true);
+ auto* container = GetDocument().getElementById("container");
+ auto* editor = GetDocument().getElementById("editor");
+ auto* editor_block = ToLayoutBlock(editor->GetLayoutObject());
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder().Collapse(Position(editor, 0)).Build());
+ UpdateAllLifecyclePhases();
+
+ EXPECT_TRUE(editor_block->ShouldPaintCursorCaret());
+ EXPECT_EQ(editor_block, CaretLayoutBlock());
+ EXPECT_EQ(LayoutRect(116, 105, 1, 1),
+ GetCaretDisplayItemClient().VisualRect());
+
+ // Composite container.
+ container->setAttribute(HTMLNames::styleAttr, "will-change: transform");
+ UpdateAllLifecyclePhases();
+ EXPECT_EQ(LayoutRect(50, 50, 1, 1), GetCaretDisplayItemClient().VisualRect());
+
+ // Uncomposite container.
+ container->setAttribute(HTMLNames::styleAttr, "");
+ UpdateAllLifecyclePhases();
+ EXPECT_EQ(LayoutRect(116, 105, 1, 1),
+ GetCaretDisplayItemClient().VisualRect());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/append_node_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/append_node_command.cc
new file mode 100644
index 00000000000..f4ba2617395
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/append_node_command.cc
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/append_node_command.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+
+namespace blink {
+
+AppendNodeCommand::AppendNodeCommand(ContainerNode* parent, Node* node)
+ : SimpleEditCommand(parent->GetDocument()), parent_(parent), node_(node) {
+ DCHECK(parent_);
+ DCHECK(node_);
+ DCHECK(!node_->parentNode()) << node_;
+
+ DCHECK(HasEditableStyle(*parent_) || !parent_->InActiveDocument()) << parent_;
+}
+
+void AppendNodeCommand::DoApply(EditingState*) {
+ if (!HasEditableStyle(*parent_) && parent_->InActiveDocument())
+ return;
+
+ parent_->AppendChild(node_.Get(), IGNORE_EXCEPTION_FOR_TESTING);
+}
+
+void AppendNodeCommand::DoUnapply() {
+ if (!HasEditableStyle(*parent_))
+ return;
+
+ node_->remove(IGNORE_EXCEPTION_FOR_TESTING);
+}
+
+void AppendNodeCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(parent_);
+ visitor->Trace(node_);
+ SimpleEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/append_node_command.h b/chromium/third_party/blink/renderer/core/editing/commands/append_node_command.h
new file mode 100644
index 00000000000..d5880a698df
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/append_node_command.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_APPEND_NODE_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_APPEND_NODE_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/edit_command.h"
+
+namespace blink {
+
+class AppendNodeCommand final : public SimpleEditCommand {
+ public:
+ static AppendNodeCommand* Create(ContainerNode* parent, Node* node) {
+ return new AppendNodeCommand(parent, node);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ AppendNodeCommand(ContainerNode* parent, Node*);
+
+ void DoApply(EditingState*) override;
+ void DoUnapply() override;
+
+ Member<ContainerNode> parent_;
+ Member<Node> node_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_APPEND_NODE_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/apply_block_element_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/apply_block_element_command.cc
new file mode 100644
index 00000000000..492ac08996a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/apply_block_element_command.cc
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2006, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/apply_block_element_command.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/node_computed_style.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/html/html_br_element.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/style/computed_style.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+ApplyBlockElementCommand::ApplyBlockElementCommand(
+ Document& document,
+ const QualifiedName& tag_name,
+ const AtomicString& inline_style)
+ : CompositeEditCommand(document),
+ tag_name_(tag_name),
+ inline_style_(inline_style) {}
+
+ApplyBlockElementCommand::ApplyBlockElementCommand(
+ Document& document,
+ const QualifiedName& tag_name)
+ : CompositeEditCommand(document), tag_name_(tag_name) {}
+
+void ApplyBlockElementCommand::DoApply(EditingState* editing_state) {
+ // ApplyBlockElementCommands are only created directly by editor commands'
+ // execution, which updates layout before entering doApply().
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+
+ if (!RootEditableElementOf(EndingSelection().Base()))
+ return;
+
+ VisiblePosition visible_end = EndingVisibleSelection().VisibleEnd();
+ VisiblePosition visible_start = EndingVisibleSelection().VisibleStart();
+ if (visible_start.IsNull() || visible_start.IsOrphan() ||
+ visible_end.IsNull() || visible_end.IsOrphan())
+ return;
+
+ // When a selection ends at the start of a paragraph, we rarely paint
+ // the selection gap before that paragraph, because there often is no gap.
+ // In a case like this, it's not obvious to the user that the selection
+ // ends "inside" that paragraph, so it would be confusing if Indent/Outdent
+ // operated on that paragraph.
+ // FIXME: We paint the gap before some paragraphs that are indented with left
+ // margin/padding, but not others. We should make the gap painting more
+ // consistent and then use a left margin/padding rule here.
+ if (visible_end.DeepEquivalent() != visible_start.DeepEquivalent() &&
+ IsStartOfParagraph(visible_end)) {
+ const Position& new_end =
+ PreviousPositionOf(visible_end, kCannotCrossEditingBoundary)
+ .DeepEquivalent();
+ SelectionInDOMTree::Builder builder;
+ builder.Collapse(visible_start.ToPositionWithAffinity());
+ if (new_end.IsNotNull())
+ builder.Extend(new_end);
+ SetEndingSelection(SelectionForUndoStep::From(builder.Build()));
+ }
+
+ VisibleSelection selection =
+ SelectionForParagraphIteration(EndingVisibleSelection());
+ VisiblePosition start_of_selection = selection.VisibleStart();
+ VisiblePosition end_of_selection = selection.VisibleEnd();
+ DCHECK(!start_of_selection.IsNull());
+ DCHECK(!end_of_selection.IsNull());
+ ContainerNode* start_scope = nullptr;
+ int start_index = IndexForVisiblePosition(start_of_selection, start_scope);
+ ContainerNode* end_scope = nullptr;
+ int end_index = IndexForVisiblePosition(end_of_selection, end_scope);
+
+ FormatSelection(start_of_selection, end_of_selection, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ DCHECK_EQ(start_scope, end_scope);
+ DCHECK_GE(start_index, 0);
+ DCHECK_LE(start_index, end_index);
+ if (start_scope == end_scope && start_index >= 0 &&
+ start_index <= end_index) {
+ VisiblePosition start(VisiblePositionForIndex(start_index, start_scope));
+ VisiblePosition end(VisiblePositionForIndex(end_index, end_scope));
+ if (start.IsNotNull() && end.IsNotNull()) {
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(start.ToPositionWithAffinity())
+ .Extend(end.DeepEquivalent())
+ .Build()));
+ }
+ }
+}
+
+static bool IsAtUnsplittableElement(const Position& pos) {
+ Node* node = pos.AnchorNode();
+ return node == RootEditableElementOf(pos) ||
+ node == EnclosingNodeOfType(pos, &IsTableCell);
+}
+
+void ApplyBlockElementCommand::FormatSelection(
+ const VisiblePosition& start_of_selection,
+ const VisiblePosition& end_of_selection,
+ EditingState* editing_state) {
+ // Special case empty unsplittable elements because there's nothing to split
+ // and there's nothing to move.
+ const Position& caret_position =
+ MostForwardCaretPosition(start_of_selection.DeepEquivalent());
+ if (IsAtUnsplittableElement(caret_position)) {
+ HTMLElement* blockquote = CreateBlockElement();
+ InsertNodeAt(blockquote, caret_position, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ HTMLBRElement* placeholder = HTMLBRElement::Create(GetDocument());
+ AppendNode(placeholder, blockquote, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::BeforeNode(*placeholder))
+ .Build()));
+ return;
+ }
+
+ HTMLElement* blockquote_for_next_indent = nullptr;
+ VisiblePosition end_of_current_paragraph = EndOfParagraph(start_of_selection);
+ const VisiblePosition& visible_end_of_last_paragraph =
+ EndOfParagraph(end_of_selection);
+ const Position& end_of_next_last_paragraph =
+ EndOfParagraph(NextPositionOf(visible_end_of_last_paragraph))
+ .DeepEquivalent();
+ Position end_of_last_paragraph =
+ visible_end_of_last_paragraph.DeepEquivalent();
+
+ bool at_end = false;
+ while (end_of_current_paragraph.DeepEquivalent() !=
+ end_of_next_last_paragraph &&
+ !at_end) {
+ if (end_of_current_paragraph.DeepEquivalent() == end_of_last_paragraph)
+ at_end = true;
+
+ Position start, end;
+ RangeForParagraphSplittingTextNodesIfNeeded(
+ end_of_current_paragraph, end_of_last_paragraph, start, end);
+ end_of_current_paragraph = CreateVisiblePosition(end);
+
+ Node* enclosing_cell = EnclosingNodeOfType(start, &IsTableCell);
+ PositionWithAffinity end_of_next_paragraph =
+ EndOfNextParagrahSplittingTextNodesIfNeeded(
+ end_of_current_paragraph, end_of_last_paragraph, start, end)
+ .ToPositionWithAffinity();
+
+ FormatRange(start, end, end_of_last_paragraph, blockquote_for_next_indent,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ // Don't put the next paragraph in the blockquote we just created for this
+ // paragraph unless the next paragraph is in the same cell.
+ if (enclosing_cell &&
+ enclosing_cell !=
+ EnclosingNodeOfType(end_of_next_paragraph.GetPosition(),
+ &IsTableCell))
+ blockquote_for_next_indent = nullptr;
+
+ // indentIntoBlockquote could move more than one paragraph if the paragraph
+ // is in a list item or a table. As a result,
+ // |endOfNextLastParagraph| could refer to a position no longer in the
+ // document.
+ if (end_of_next_last_paragraph.IsNotNull() &&
+ !end_of_next_last_paragraph.IsConnected())
+ break;
+ // Sanity check: Make sure our moveParagraph calls didn't remove
+ // endOfNextParagraph.anchorNode() If somehow, e.g. mutation
+ // event handler, we did, return to prevent crashes.
+ if (end_of_next_paragraph.IsNotNull() &&
+ !end_of_next_paragraph.IsConnected())
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ end_of_current_paragraph = CreateVisiblePosition(end_of_next_paragraph);
+ }
+}
+
+static bool IsNewLineAtPosition(const Position& position) {
+ Node* text_node = position.ComputeContainerNode();
+ int offset = position.OffsetInContainerNode();
+ if (!text_node || !text_node->IsTextNode() || offset < 0 ||
+ offset >= static_cast<int>(ToText(text_node)->length()))
+ return false;
+
+ DummyExceptionStateForTesting exception_state;
+ String text_at_position =
+ ToText(text_node)->substringData(offset, 1, exception_state);
+ if (exception_state.HadException())
+ return false;
+
+ return text_at_position[0] == '\n';
+}
+
+static const ComputedStyle* ComputedStyleOfEnclosingTextNode(
+ const Position& position) {
+ if (!position.IsOffsetInAnchor() || !position.ComputeContainerNode() ||
+ !position.ComputeContainerNode()->IsTextNode())
+ return nullptr;
+ return position.ComputeContainerNode()->GetComputedStyle();
+}
+
+void ApplyBlockElementCommand::RangeForParagraphSplittingTextNodesIfNeeded(
+ const VisiblePosition& end_of_current_paragraph,
+ Position& end_of_last_paragraph,
+ Position& start,
+ Position& end) {
+ start = StartOfParagraph(end_of_current_paragraph).DeepEquivalent();
+ end = end_of_current_paragraph.DeepEquivalent();
+
+ bool is_start_and_end_on_same_node = false;
+ if (const ComputedStyle* start_style =
+ ComputedStyleOfEnclosingTextNode(start)) {
+ is_start_and_end_on_same_node =
+ ComputedStyleOfEnclosingTextNode(end) &&
+ start.ComputeContainerNode() == end.ComputeContainerNode();
+ bool is_start_and_end_of_last_paragraph_on_same_node =
+ ComputedStyleOfEnclosingTextNode(end_of_last_paragraph) &&
+ start.ComputeContainerNode() ==
+ end_of_last_paragraph.ComputeContainerNode();
+
+ // Avoid obtanining the start of next paragraph for start
+ // TODO(yosin) We should use |PositionMoveType::CodePoint| for
+ // |previousPositionOf()|.
+ if (start_style->PreserveNewline() && IsNewLineAtPosition(start) &&
+ !IsNewLineAtPosition(
+ PreviousPositionOf(start, PositionMoveType::kCodeUnit)) &&
+ start.OffsetInContainerNode() > 0)
+ start = StartOfParagraph(CreateVisiblePosition(PreviousPositionOf(
+ end, PositionMoveType::kCodeUnit)))
+ .DeepEquivalent();
+
+ // If start is in the middle of a text node, split.
+ if (!start_style->CollapseWhiteSpace() &&
+ start.OffsetInContainerNode() > 0) {
+ int start_offset = start.OffsetInContainerNode();
+ Text* start_text = ToText(start.ComputeContainerNode());
+ SplitTextNode(start_text, start_offset);
+ GetDocument().UpdateStyleAndLayoutTree();
+
+ start = Position::FirstPositionInNode(*start_text);
+ if (is_start_and_end_on_same_node) {
+ DCHECK_GE(end.OffsetInContainerNode(), start_offset);
+ end = Position(start_text, end.OffsetInContainerNode() - start_offset);
+ }
+ if (is_start_and_end_of_last_paragraph_on_same_node) {
+ DCHECK_GE(end_of_last_paragraph.OffsetInContainerNode(), start_offset);
+ end_of_last_paragraph =
+ Position(start_text, end_of_last_paragraph.OffsetInContainerNode() -
+ start_offset);
+ }
+ }
+ }
+
+ if (const ComputedStyle* end_style = ComputedStyleOfEnclosingTextNode(end)) {
+ bool is_end_and_end_of_last_paragraph_on_same_node =
+ ComputedStyleOfEnclosingTextNode(end_of_last_paragraph) &&
+ end.AnchorNode() == end_of_last_paragraph.AnchorNode();
+ // Include \n at the end of line if we're at an empty paragraph
+ if (end_style->PreserveNewline() && start == end &&
+ end.OffsetInContainerNode() <
+ static_cast<int>(ToText(end.ComputeContainerNode())->length())) {
+ int end_offset = end.OffsetInContainerNode();
+ // TODO(yosin) We should use |PositionMoveType::CodePoint| for
+ // |previousPositionOf()|.
+ if (!IsNewLineAtPosition(
+ PreviousPositionOf(end, PositionMoveType::kCodeUnit)) &&
+ IsNewLineAtPosition(end))
+ end = Position(end.ComputeContainerNode(), end_offset + 1);
+ if (is_end_and_end_of_last_paragraph_on_same_node &&
+ end.OffsetInContainerNode() >=
+ end_of_last_paragraph.OffsetInContainerNode())
+ end_of_last_paragraph = end;
+ }
+
+ // If end is in the middle of a text node, split.
+ if (end_style->UserModify() != EUserModify::kReadOnly &&
+ !end_style->CollapseWhiteSpace() && end.OffsetInContainerNode() &&
+ end.OffsetInContainerNode() <
+ static_cast<int>(ToText(end.ComputeContainerNode())->length())) {
+ Text* end_container = ToText(end.ComputeContainerNode());
+ SplitTextNode(end_container, end.OffsetInContainerNode());
+ GetDocument().UpdateStyleAndLayoutTree();
+
+ const Node* const previous_sibling_of_end =
+ end_container->previousSibling();
+ DCHECK(previous_sibling_of_end);
+ if (is_start_and_end_on_same_node) {
+ start = FirstPositionInOrBeforeNode(*previous_sibling_of_end);
+ }
+ if (is_end_and_end_of_last_paragraph_on_same_node) {
+ if (end_of_last_paragraph.OffsetInContainerNode() ==
+ end.OffsetInContainerNode()) {
+ end_of_last_paragraph =
+ LastPositionInOrAfterNode(*previous_sibling_of_end);
+ } else {
+ end_of_last_paragraph = Position(
+ end_container, end_of_last_paragraph.OffsetInContainerNode() -
+ end.OffsetInContainerNode());
+ }
+ }
+ end = Position::LastPositionInNode(*previous_sibling_of_end);
+ }
+ }
+}
+
+VisiblePosition
+ApplyBlockElementCommand::EndOfNextParagrahSplittingTextNodesIfNeeded(
+ VisiblePosition& end_of_current_paragraph,
+ Position& end_of_last_paragraph,
+ Position& start,
+ Position& end) {
+ const VisiblePosition& end_of_next_paragraph =
+ EndOfParagraph(NextPositionOf(end_of_current_paragraph));
+ const Position& end_of_next_paragraph_position =
+ end_of_next_paragraph.DeepEquivalent();
+ const ComputedStyle* style =
+ ComputedStyleOfEnclosingTextNode(end_of_next_paragraph_position);
+ if (!style)
+ return end_of_next_paragraph;
+
+ Text* const end_of_next_paragraph_text =
+ ToText(end_of_next_paragraph_position.ComputeContainerNode());
+ if (!style->PreserveNewline() ||
+ !end_of_next_paragraph_position.OffsetInContainerNode() ||
+ !IsNewLineAtPosition(
+ Position::FirstPositionInNode(*end_of_next_paragraph_text)))
+ return end_of_next_paragraph;
+
+ // \n at the beginning of the text node immediately following the current
+ // paragraph is trimmed by moveParagraphWithClones. If endOfNextParagraph was
+ // pointing at this same text node, endOfNextParagraph will be shifted by one
+ // paragraph. Avoid this by splitting "\n"
+ SplitTextNode(end_of_next_paragraph_text, 1);
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ Text* const previous_text =
+ end_of_next_paragraph_text->previousSibling() &&
+ end_of_next_paragraph_text->previousSibling()->IsTextNode()
+ ? ToText(end_of_next_paragraph_text->previousSibling())
+ : nullptr;
+ if (end_of_next_paragraph_text == start.ComputeContainerNode() &&
+ previous_text) {
+ DCHECK_LT(start.OffsetInContainerNode(),
+ end_of_next_paragraph_position.OffsetInContainerNode());
+ start = Position(previous_text, start.OffsetInContainerNode());
+ }
+ if (end_of_next_paragraph_text == end.ComputeContainerNode() &&
+ previous_text) {
+ DCHECK_LT(end.OffsetInContainerNode(),
+ end_of_next_paragraph_position.OffsetInContainerNode());
+ end = Position(previous_text, end.OffsetInContainerNode());
+ }
+ if (end_of_next_paragraph_text ==
+ end_of_last_paragraph.ComputeContainerNode()) {
+ if (end_of_last_paragraph.OffsetInContainerNode() <
+ end_of_next_paragraph_position.OffsetInContainerNode()) {
+ // We can only fix endOfLastParagraph if the previous node was still text
+ // and hasn't been modified by script.
+ if (previous_text && static_cast<unsigned>(
+ end_of_last_paragraph.OffsetInContainerNode()) <=
+ previous_text->length()) {
+ end_of_last_paragraph = Position(
+ previous_text, end_of_last_paragraph.OffsetInContainerNode());
+ }
+ } else {
+ end_of_last_paragraph =
+ Position(end_of_next_paragraph_text,
+ end_of_last_paragraph.OffsetInContainerNode() - 1);
+ }
+ }
+
+ return CreateVisiblePosition(
+ Position(end_of_next_paragraph_text,
+ end_of_next_paragraph_position.OffsetInContainerNode() - 1));
+}
+
+HTMLElement* ApplyBlockElementCommand::CreateBlockElement() const {
+ HTMLElement* element = CreateHTMLElement(GetDocument(), tag_name_);
+ if (inline_style_.length())
+ element->setAttribute(styleAttr, inline_style_);
+ return element;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/apply_block_element_command.h b/chromium/third_party/blink/renderer/core/editing/commands/apply_block_element_command.h
new file mode 100644
index 00000000000..5bf1faeca9e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/apply_block_element_command.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_APPLY_BLOCK_ELEMENT_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_APPLY_BLOCK_ELEMENT_COMMAND_H_
+
+#include "third_party/blink/renderer/core/dom/qualified_name.h"
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+namespace blink {
+
+class CORE_EXPORT ApplyBlockElementCommand : public CompositeEditCommand {
+ protected:
+ ApplyBlockElementCommand(Document&,
+ const QualifiedName& tag_name,
+ const AtomicString& inline_style);
+ ApplyBlockElementCommand(Document&, const QualifiedName& tag_name);
+
+ virtual void FormatSelection(const VisiblePosition& start_of_selection,
+ const VisiblePosition& end_of_selection,
+ EditingState*);
+ HTMLElement* CreateBlockElement() const;
+ const QualifiedName& TagName() const { return tag_name_; }
+
+ private:
+ void DoApply(EditingState*) final;
+ virtual void FormatRange(const Position& start,
+ const Position& end,
+ const Position& end_of_selection,
+ HTMLElement*&,
+ EditingState*) = 0;
+ void RangeForParagraphSplittingTextNodesIfNeeded(
+ const VisiblePosition& end_of_current_paragraph,
+ Position& end_of_last_paragraph,
+ Position& start,
+ Position& end);
+ VisiblePosition EndOfNextParagrahSplittingTextNodesIfNeeded(
+ VisiblePosition& end_of_current_paragraph,
+ Position& end_of_last_paragraph,
+ Position& start,
+ Position& end);
+
+ QualifiedName tag_name_;
+ AtomicString inline_style_;
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/apply_block_element_command_test.cc b/chromium/third_party/blink/renderer/core/editing/commands/apply_block_element_command_test.cc
new file mode 100644
index 00000000000..67482c7d9aa
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/apply_block_element_command_test.cc
@@ -0,0 +1,127 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/qualified_name.h"
+#include "third_party/blink/renderer/core/editing/commands/format_block_command.h"
+#include "third_party/blink/renderer/core/editing/commands/indent_outdent_command.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/html/html_head_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+
+#include <memory>
+
+namespace blink {
+
+class ApplyBlockElementCommandTest : public EditingTestBase {};
+
+// This is a regression test for https://crbug.com/639534
+TEST_F(ApplyBlockElementCommandTest, selectionCrossingOverBody) {
+ GetDocument().head()->insertAdjacentHTML(
+ "afterbegin",
+ "<style> .CLASS13 { -webkit-user-modify: read-write; }</style></head>",
+ ASSERT_NO_EXCEPTION);
+ GetDocument().body()->insertAdjacentHTML(
+ "afterbegin",
+ "\n<pre><var id='va' class='CLASS13'>\nC\n</var></pre><input />",
+ ASSERT_NO_EXCEPTION);
+ GetDocument().body()->insertAdjacentText("beforebegin", "foo",
+ ASSERT_NO_EXCEPTION);
+
+ GetDocument().body()->setContentEditable("false", ASSERT_NO_EXCEPTION);
+ GetDocument().setDesignMode("on");
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(
+ Position(GetDocument().documentElement(), 1),
+ Position(GetDocument().getElementById("va")->firstChild(), 2))
+ .Build());
+
+ FormatBlockCommand* command =
+ FormatBlockCommand::Create(GetDocument(), HTMLNames::footerTag);
+ command->Apply();
+
+ EXPECT_EQ(
+ "<body contenteditable=\"false\">\n"
+ "<pre><var id=\"va\" class=\"CLASS13\">\nC\n</var></pre><input></body>",
+ GetDocument().documentElement()->InnerHTMLAsString());
+}
+
+// This is a regression test for https://crbug.com/660801
+TEST_F(ApplyBlockElementCommandTest, visibilityChangeDuringCommand) {
+ GetDocument().head()->insertAdjacentHTML(
+ "afterbegin", "<style>li:first-child { visibility:visible; }</style>",
+ ASSERT_NO_EXCEPTION);
+ SetBodyContent("<ul style='visibility:hidden'><li>xyz</li></ul>");
+ GetDocument().setDesignMode("on");
+
+ UpdateAllLifecyclePhases();
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(GetDocument().QuerySelector("li"), 0))
+ .Build());
+
+ IndentOutdentCommand* command = IndentOutdentCommand::Create(
+ GetDocument(), IndentOutdentCommand::kIndent);
+ command->Apply();
+
+ EXPECT_EQ(
+ "<head><style>li:first-child { visibility:visible; }</style></head>"
+ "<body><ul style=\"visibility:hidden\"><ul></ul><li>xyz</li></ul></body>",
+ GetDocument().documentElement()->InnerHTMLAsString());
+}
+
+// This is a regression test for https://crbug.com/712510
+TEST_F(ApplyBlockElementCommandTest, IndentHeadingIntoBlockquote) {
+ SetBodyContent(
+ "<div contenteditable=\"true\">"
+ "<h6><button><table></table></button></h6>"
+ "<object></object>"
+ "</div>");
+ Element* button = GetDocument().QuerySelector("button");
+ Element* object = GetDocument().QuerySelector("object");
+ Selection().SetSelectionAndEndTyping(SelectionInDOMTree::Builder()
+ .Collapse(Position(button, 0))
+ .Extend(Position(object, 0))
+ .Build());
+
+ IndentOutdentCommand* command = IndentOutdentCommand::Create(
+ GetDocument(), IndentOutdentCommand::kIndent);
+ command->Apply();
+
+ // This only records the current behavior, which can be wrong.
+ EXPECT_EQ(
+ "<div contenteditable=\"true\">"
+ "<blockquote style=\"margin: 0 0 0 40px; border: none; padding: 0px;\">"
+ "<h6><button></button></h6>"
+ "<h6><button><table></table></button></h6>"
+ "</blockquote>"
+ "<h6><button></button></h6><br>"
+ "<object></object>"
+ "</div>",
+ GetDocument().body()->InnerHTMLAsString());
+}
+
+// This is a regression test for https://crbug.com/806525
+TEST_F(ApplyBlockElementCommandTest, InsertPlaceHolderAtDisconnectedPosition) {
+ GetDocument().setDesignMode("on");
+ InsertStyleElement(".input:nth-of-type(2n+1) { visibility:collapse; }");
+ Selection().SetSelectionAndEndTyping(SetSelectionTextToBody(
+ "^<input><input class=\"input\" style=\"position:absolute\">|"));
+ FormatBlockCommand* command =
+ FormatBlockCommand::Create(GetDocument(), HTMLNames::preTag);
+ // Crash happens here.
+ EXPECT_FALSE(command->Apply());
+ EXPECT_EQ(
+ "<pre>|<input></pre><input class=\"input\" style=\"position:absolute\">",
+ GetSelectionTextFromBody());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/apply_style_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/apply_style_command.cc
new file mode 100644
index 00000000000..22a44de79d8
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/apply_style_command.cc
@@ -0,0 +1,2085 @@
+/*
+ * Copyright (C) 2005, 2006, 2008, 2009 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/apply_style_command.h"
+
+#include "third_party/blink/renderer/core/css/css_computed_style_declaration.h"
+#include "third_party/blink/renderer/core/css/css_primitive_value.h"
+#include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/css_property_names.h"
+#include "third_party/blink/renderer/core/css_value_keywords.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/node_list.h"
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/editing_style.h"
+#include "third_party/blink/renderer/core/editing/editing_style_utilities.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/plain_text_range.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/serializers/html_interchange.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/editing/writing_direction.h"
+#include "third_party/blink/renderer/core/frame/use_counter.h"
+#include "third_party/blink/renderer/core/html/html_font_element.h"
+#include "third_party/blink/renderer/core/html/html_span_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/core/layout/layout_text.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+static bool HasNoAttributeOrOnlyStyleAttribute(
+ const HTMLElement* element,
+ ShouldStyleAttributeBeEmpty should_style_attribute_be_empty) {
+ AttributeCollection attributes = element->Attributes();
+ if (attributes.IsEmpty())
+ return true;
+
+ unsigned matched_attributes = 0;
+ if (element->hasAttribute(styleAttr) &&
+ (should_style_attribute_be_empty == kAllowNonEmptyStyleAttribute ||
+ !element->InlineStyle() || element->InlineStyle()->IsEmpty()))
+ matched_attributes++;
+
+ DCHECK_LE(matched_attributes, attributes.size());
+ return matched_attributes == attributes.size();
+}
+
+bool IsStyleSpanOrSpanWithOnlyStyleAttribute(const Element* element) {
+ if (auto* span = ToHTMLSpanElementOrNull(element)) {
+ return HasNoAttributeOrOnlyStyleAttribute(span,
+ kAllowNonEmptyStyleAttribute);
+ }
+ return false;
+}
+
+static inline bool IsSpanWithoutAttributesOrUnstyledStyleSpan(
+ const Node* node) {
+ if (auto* span = ToHTMLSpanElementOrNull(node)) {
+ return HasNoAttributeOrOnlyStyleAttribute(span,
+ kStyleAttributeShouldBeEmpty);
+ }
+ return false;
+}
+
+bool IsEmptyFontTag(
+ const Element* element,
+ ShouldStyleAttributeBeEmpty should_style_attribute_be_empty) {
+ if (auto* font = ToHTMLFontElementOrNull(element)) {
+ return HasNoAttributeOrOnlyStyleAttribute(font,
+ should_style_attribute_be_empty);
+ }
+ return false;
+}
+
+static bool OffsetIsBeforeLastNodeOffset(int offset, Node* anchor_node) {
+ if (anchor_node->IsCharacterDataNode())
+ return offset < static_cast<int>(ToCharacterData(anchor_node)->length());
+ int current_offset = 0;
+ for (Node* node = NodeTraversal::FirstChild(*anchor_node);
+ node && current_offset < offset;
+ node = NodeTraversal::NextSibling(*node))
+ current_offset++;
+ return offset < current_offset;
+}
+
+ApplyStyleCommand::ApplyStyleCommand(Document& document,
+ const EditingStyle* style,
+ InputEvent::InputType input_type,
+ EPropertyLevel property_level)
+ : CompositeEditCommand(document),
+ style_(style->Copy()),
+ input_type_(input_type),
+ property_level_(property_level),
+ start_(MostForwardCaretPosition(EndingSelection().Start())),
+ end_(MostBackwardCaretPosition(EndingSelection().End())),
+ use_ending_selection_(true),
+ styled_inline_element_(nullptr),
+ remove_only_(false),
+ is_inline_element_to_remove_function_(nullptr) {}
+
+ApplyStyleCommand::ApplyStyleCommand(Document& document,
+ const EditingStyle* style,
+ const Position& start,
+ const Position& end)
+ : CompositeEditCommand(document),
+ style_(style->Copy()),
+ input_type_(InputEvent::InputType::kNone),
+ property_level_(kPropertyDefault),
+ start_(start),
+ end_(end),
+ use_ending_selection_(false),
+ styled_inline_element_(nullptr),
+ remove_only_(false),
+ is_inline_element_to_remove_function_(nullptr) {}
+
+ApplyStyleCommand::ApplyStyleCommand(Element* element, bool remove_only)
+ : CompositeEditCommand(element->GetDocument()),
+ style_(EditingStyle::Create()),
+ input_type_(InputEvent::InputType::kNone),
+ property_level_(kPropertyDefault),
+ start_(MostForwardCaretPosition(EndingSelection().Start())),
+ end_(MostBackwardCaretPosition(EndingSelection().End())),
+ use_ending_selection_(true),
+ styled_inline_element_(element),
+ remove_only_(remove_only),
+ is_inline_element_to_remove_function_(nullptr) {}
+
+ApplyStyleCommand::ApplyStyleCommand(
+ Document& document,
+ const EditingStyle* style,
+ IsInlineElementToRemoveFunction is_inline_element_to_remove_function,
+ InputEvent::InputType input_type)
+ : CompositeEditCommand(document),
+ style_(style->Copy()),
+ input_type_(input_type),
+ property_level_(kPropertyDefault),
+ start_(MostForwardCaretPosition(EndingSelection().Start())),
+ end_(MostBackwardCaretPosition(EndingSelection().End())),
+ use_ending_selection_(true),
+ styled_inline_element_(nullptr),
+ remove_only_(true),
+ is_inline_element_to_remove_function_(
+ is_inline_element_to_remove_function) {}
+
+void ApplyStyleCommand::UpdateStartEnd(const EphemeralRange& range) {
+ if (!use_ending_selection_ &&
+ (range.StartPosition() != start_ || range.EndPosition() != end_))
+ use_ending_selection_ = true;
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ const bool was_base_first =
+ StartingSelection().IsBaseFirst() || !SelectionIsDirectional();
+ SelectionInDOMTree::Builder builder;
+ if (was_base_first)
+ builder.SetAsForwardSelection(range);
+ else
+ builder.SetAsBackwardSelection(range);
+ const VisibleSelection& visible_selection =
+ CreateVisibleSelection(builder.Build());
+ SetEndingSelection(
+ SelectionForUndoStep::From(visible_selection.AsSelection()));
+ start_ = range.StartPosition();
+ end_ = range.EndPosition();
+}
+
+Position ApplyStyleCommand::StartPosition() {
+ if (use_ending_selection_)
+ return EndingSelection().Start();
+
+ return start_;
+}
+
+Position ApplyStyleCommand::EndPosition() {
+ if (use_ending_selection_)
+ return EndingSelection().End();
+
+ return end_;
+}
+
+void ApplyStyleCommand::DoApply(EditingState* editing_state) {
+ DCHECK(StartPosition().IsNotNull());
+ DCHECK(EndPosition().IsNotNull());
+ switch (property_level_) {
+ case kPropertyDefault: {
+ // Apply the block-centric properties of the style.
+ EditingStyle* block_style = style_->ExtractAndRemoveBlockProperties();
+ if (!block_style->IsEmpty()) {
+ ApplyBlockStyle(block_style, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ // Apply any remaining styles to the inline elements.
+ if (!style_->IsEmpty() || styled_inline_element_ ||
+ is_inline_element_to_remove_function_) {
+ ApplyRelativeFontStyleChange(style_.Get(), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ ApplyInlineStyle(style_.Get(), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ break;
+ }
+ case kForceBlockProperties:
+ // Force all properties to be applied as block styles.
+ ApplyBlockStyle(style_.Get(), editing_state);
+ break;
+ }
+}
+
+InputEvent::InputType ApplyStyleCommand::GetInputType() const {
+ return input_type_;
+}
+
+void ApplyStyleCommand::ApplyBlockStyle(EditingStyle* style,
+ EditingState* editing_state) {
+ // update document layout once before removing styles
+ // so that we avoid the expense of updating before each and every call
+ // to check a computed style
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // get positions we want to use for applying style
+ Position start = StartPosition();
+ Position end = EndPosition();
+ if (ComparePositions(end, start) < 0) {
+ Position swap = start;
+ start = end;
+ end = swap;
+ }
+
+ VisiblePosition visible_start = CreateVisiblePosition(start);
+ VisiblePosition visible_end = CreateVisiblePosition(end);
+
+ if (visible_start.IsNull() || visible_start.IsOrphan() ||
+ visible_end.IsNull() || visible_end.IsOrphan())
+ return;
+
+ // Save and restore the selection endpoints using their indices in the
+ // document, since addBlockStyleIfNeeded may moveParagraphs, which can remove
+ // these endpoints. Calculate start and end indices from the start of the tree
+ // that they're in.
+ const Node& scope = NodeTraversal::HighestAncestorOrSelf(
+ *visible_start.DeepEquivalent().AnchorNode());
+ const EphemeralRange start_range(
+ Position::FirstPositionInNode(scope),
+ visible_start.DeepEquivalent().ParentAnchoredEquivalent());
+ const EphemeralRange end_range(
+ Position::FirstPositionInNode(scope),
+ visible_end.DeepEquivalent().ParentAnchoredEquivalent());
+
+ const TextIteratorBehavior behavior =
+ TextIteratorBehavior::AllVisiblePositionsRangeLengthBehavior();
+
+ const int start_index = TextIterator::RangeLength(start_range, behavior);
+ const int end_index = TextIterator::RangeLength(end_range, behavior);
+
+ VisiblePosition paragraph_start(StartOfParagraph(visible_start));
+ VisiblePosition next_paragraph_start(
+ NextPositionOf(EndOfParagraph(paragraph_start)));
+ Position beyond_end =
+ NextPositionOf(EndOfParagraph(visible_end)).DeepEquivalent();
+ // TODO(editing-dev): Use a saner approach (e.g., temporary Ranges) to keep
+ // these positions in document instead of iteratively performing orphan checks
+ // and recalculating them when they become orphans.
+ while (paragraph_start.IsNotNull() &&
+ paragraph_start.DeepEquivalent() != beyond_end) {
+ DCHECK(!paragraph_start.IsOrphan()) << paragraph_start;
+ StyleChange style_change(style, paragraph_start.DeepEquivalent());
+ if (style_change.CssStyle().length() || remove_only_) {
+ Element* block =
+ EnclosingBlock(paragraph_start.DeepEquivalent().AnchorNode());
+ const Position& paragraph_start_to_move =
+ paragraph_start.DeepEquivalent();
+ if (!remove_only_ && IsEditablePosition(paragraph_start_to_move)) {
+ HTMLElement* new_block = MoveParagraphContentsToNewBlockIfNecessary(
+ paragraph_start_to_move, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ if (new_block) {
+ block = new_block;
+ if (paragraph_start.IsOrphan()) {
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ paragraph_start = CreateVisiblePosition(
+ Position::FirstPositionInNode(*new_block));
+ }
+ }
+ DCHECK(!paragraph_start.IsOrphan()) << paragraph_start;
+ }
+ if (block && block->IsHTMLElement()) {
+ RemoveCSSStyle(style, ToHTMLElement(block), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ DCHECK(!paragraph_start.IsOrphan()) << paragraph_start;
+ if (!remove_only_) {
+ AddBlockStyle(style_change, ToHTMLElement(block));
+ DCHECK(!paragraph_start.IsOrphan()) << paragraph_start;
+ }
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Make the VisiblePositions valid again after style changes.
+ // TODO(editing-dev): We shouldn't store VisiblePositions and inspect
+ // their properties after they have been invalidated by mutations. See
+ // crbug.com/648949 for details.
+ DCHECK(!paragraph_start.IsOrphan()) << paragraph_start;
+ paragraph_start =
+ CreateVisiblePosition(paragraph_start.ToPositionWithAffinity());
+ if (next_paragraph_start.IsOrphan()) {
+ next_paragraph_start = NextPositionOf(EndOfParagraph(paragraph_start));
+ } else {
+ next_paragraph_start = CreateVisiblePosition(
+ next_paragraph_start.ToPositionWithAffinity());
+ }
+ }
+
+ DCHECK(!next_paragraph_start.IsOrphan()) << next_paragraph_start;
+ paragraph_start = next_paragraph_start;
+ next_paragraph_start = NextPositionOf(EndOfParagraph(paragraph_start));
+ }
+
+ // Update style and layout again, since added or removed styles could have
+ // affected the layout. We need clean layout in order to compute
+ // plain-text ranges below.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ EphemeralRange start_ephemeral_range =
+ PlainTextRange(start_index)
+ .CreateRangeForSelection(ToContainerNode(scope));
+ if (start_ephemeral_range.IsNull())
+ return;
+ EphemeralRange end_ephemeral_range =
+ PlainTextRange(end_index).CreateRangeForSelection(ToContainerNode(scope));
+ if (end_ephemeral_range.IsNull())
+ return;
+ UpdateStartEnd(EphemeralRange(start_ephemeral_range.StartPosition(),
+ end_ephemeral_range.StartPosition()));
+}
+
+static MutableCSSPropertyValueSet* CopyStyleOrCreateEmpty(
+ const CSSPropertyValueSet* style) {
+ if (!style)
+ return MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ return style->MutableCopy();
+}
+
+void ApplyStyleCommand::ApplyRelativeFontStyleChange(
+ EditingStyle* style,
+ EditingState* editing_state) {
+ static const float kMinimumFontSize = 0.1f;
+
+ if (!style || !style->HasFontSizeDelta())
+ return;
+
+ Position start = StartPosition();
+ Position end = EndPosition();
+ if (ComparePositions(end, start) < 0) {
+ Position swap = start;
+ start = end;
+ end = swap;
+ }
+
+ // Join up any adjacent text nodes.
+ if (start.AnchorNode()->IsTextNode()) {
+ JoinChildTextNodes(start.AnchorNode()->parentNode(), start, end);
+ start = StartPosition();
+ end = EndPosition();
+ }
+
+ if (start.IsNull() || end.IsNull())
+ return;
+
+ if (end.AnchorNode()->IsTextNode() &&
+ start.AnchorNode()->parentNode() != end.AnchorNode()->parentNode()) {
+ JoinChildTextNodes(end.AnchorNode()->parentNode(), start, end);
+ start = StartPosition();
+ end = EndPosition();
+ }
+
+ if (start.IsNull() || end.IsNull())
+ return;
+
+ // Split the start text nodes if needed to apply style.
+ if (IsValidCaretPositionInTextNode(start)) {
+ SplitTextAtStart(start, end);
+ start = StartPosition();
+ end = EndPosition();
+ }
+
+ if (IsValidCaretPositionInTextNode(end)) {
+ SplitTextAtEnd(start, end);
+ start = StartPosition();
+ end = EndPosition();
+ }
+
+ DCHECK(start.AnchorNode());
+ DCHECK(end.AnchorNode());
+ // Calculate loop end point.
+ // If the end node is before the start node (can only happen if the end node
+ // is an ancestor of the start node), we gather nodes up to the next sibling
+ // of the end node
+ const Node* const beyond_end = end.NodeAsRangePastLastNode();
+ // Move upstream to ensure we do not add redundant spans.
+ start = MostBackwardCaretPosition(start);
+ Node* start_node = start.AnchorNode();
+ DCHECK(start_node);
+
+ // Make sure we're not already at the end or the next NodeTraversal::next()
+ // will traverse past it.
+ if (start_node == beyond_end)
+ return;
+
+ if (start_node->IsTextNode() &&
+ start.ComputeOffsetInContainerNode() >= CaretMaxOffset(start_node)) {
+ // Move out of text node if range does not include its characters.
+ start_node = NodeTraversal::Next(*start_node);
+ if (!start_node)
+ return;
+ }
+
+ // Store away font size before making any changes to the document.
+ // This ensures that changes to one node won't effect another.
+ HeapHashMap<Member<Node>, float> starting_font_sizes;
+ for (Node* node = start_node; node != beyond_end;
+ node = NodeTraversal::Next(*node)) {
+ DCHECK(node);
+ starting_font_sizes.Set(node, ComputedFontSize(node));
+ }
+
+ // These spans were added by us. If empty after font size changes, they can be
+ // removed.
+ HeapVector<Member<HTMLElement>> unstyled_spans;
+
+ Node* last_styled_node = nullptr;
+ Node* node = start_node;
+ while (node != beyond_end) {
+ DCHECK(node);
+ Node* const next_node = NodeTraversal::Next(*node);
+ HTMLElement* element = nullptr;
+ if (node->IsHTMLElement()) {
+ // Only work on fully selected nodes.
+ if (!ElementFullySelected(ToHTMLElement(*node), start, end)) {
+ node = next_node;
+ continue;
+ }
+ element = ToHTMLElement(node);
+ } else if (node->IsTextNode() && node->GetLayoutObject() &&
+ node->parentNode() != last_styled_node) {
+ // Last styled node was not parent node of this text node, but we wish to
+ // style this text node. To make this possible, add a style span to
+ // surround this text node.
+ HTMLSpanElement* span = HTMLSpanElement::Create(GetDocument());
+ SurroundNodeRangeWithElement(node, node, span, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ element = span;
+ } else {
+ node = next_node;
+ // Only handle HTML elements and text nodes.
+ continue;
+ }
+ last_styled_node = node;
+
+ MutableCSSPropertyValueSet* inline_style =
+ CopyStyleOrCreateEmpty(element->InlineStyle());
+ float current_font_size = ComputedFontSize(node);
+ float desired_font_size =
+ max(kMinimumFontSize,
+ starting_font_sizes.at(node) + style->FontSizeDelta());
+ const CSSValue* value =
+ inline_style->GetPropertyCSSValue(CSSPropertyFontSize);
+ if (value) {
+ element->RemoveInlineStyleProperty(CSSPropertyFontSize);
+ current_font_size = ComputedFontSize(node);
+ }
+ if (current_font_size != desired_font_size) {
+ inline_style->SetProperty(
+ CSSPropertyFontSize,
+ *CSSPrimitiveValue::Create(desired_font_size,
+ CSSPrimitiveValue::UnitType::kPixels),
+ false);
+ SetNodeAttribute(element, styleAttr,
+ AtomicString(inline_style->AsText()));
+ }
+ if (inline_style->IsEmpty()) {
+ RemoveElementAttribute(element, styleAttr);
+ if (IsSpanWithoutAttributesOrUnstyledStyleSpan(element))
+ unstyled_spans.push_back(element);
+ }
+ node = next_node;
+ }
+
+ for (const auto& unstyled_span : unstyled_spans) {
+ RemoveNodePreservingChildren(unstyled_span, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+}
+
+static ContainerNode* DummySpanAncestorForNode(const Node* node) {
+ while (node && (!node->IsElementNode() ||
+ !IsStyleSpanOrSpanWithOnlyStyleAttribute(ToElement(node))))
+ node = node->parentNode();
+
+ return node ? node->parentNode() : nullptr;
+}
+
+void ApplyStyleCommand::CleanupUnstyledAppleStyleSpans(
+ ContainerNode* dummy_span_ancestor,
+ EditingState* editing_state) {
+ if (!dummy_span_ancestor)
+ return;
+
+ // Dummy spans are created when text node is split, so that style information
+ // can be propagated, which can result in more splitting. If a dummy span gets
+ // cloned/split, the new node is always a sibling of it. Therefore, we scan
+ // all the children of the dummy's parent
+ Node* next;
+ for (Node* node = dummy_span_ancestor->firstChild(); node; node = next) {
+ next = node->nextSibling();
+ if (IsSpanWithoutAttributesOrUnstyledStyleSpan(node)) {
+ RemoveNodePreservingChildren(node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+}
+
+HTMLElement* ApplyStyleCommand::SplitAncestorsWithUnicodeBidi(
+ Node* node,
+ bool before,
+ WritingDirection allowed_direction) {
+ // We are allowed to leave the highest ancestor with unicode-bidi unsplit if
+ // it is unicode-bidi: embed and direction: allowedDirection. In that case, we
+ // return the unsplit ancestor. Otherwise, we return 0.
+ Element* block = EnclosingBlock(node);
+ if (!block)
+ return nullptr;
+
+ ContainerNode* highest_ancestor_with_unicode_bidi = nullptr;
+ ContainerNode* next_highest_ancestor_with_unicode_bidi = nullptr;
+ int highest_ancestor_unicode_bidi = 0;
+ for (Node& runner : NodeTraversal::AncestorsOf(*node)) {
+ if (runner == block)
+ break;
+ int unicode_bidi = GetIdentifierValue(
+ CSSComputedStyleDeclaration::Create(&runner), CSSPropertyUnicodeBidi);
+ if (unicode_bidi && unicode_bidi != CSSValueNormal) {
+ highest_ancestor_unicode_bidi = unicode_bidi;
+ next_highest_ancestor_with_unicode_bidi =
+ highest_ancestor_with_unicode_bidi;
+ highest_ancestor_with_unicode_bidi = static_cast<ContainerNode*>(&runner);
+ }
+ }
+
+ if (!highest_ancestor_with_unicode_bidi)
+ return nullptr;
+
+ HTMLElement* unsplit_ancestor = nullptr;
+
+ WritingDirection highest_ancestor_direction;
+ if (allowed_direction != WritingDirection::kNatural &&
+ highest_ancestor_unicode_bidi != CSSValueBidiOverride &&
+ highest_ancestor_with_unicode_bidi->IsHTMLElement() &&
+ EditingStyle::Create(highest_ancestor_with_unicode_bidi,
+ EditingStyle::kAllProperties)
+ ->GetTextDirection(highest_ancestor_direction) &&
+ highest_ancestor_direction == allowed_direction) {
+ if (!next_highest_ancestor_with_unicode_bidi)
+ return ToHTMLElement(highest_ancestor_with_unicode_bidi);
+
+ unsplit_ancestor = ToHTMLElement(highest_ancestor_with_unicode_bidi);
+ highest_ancestor_with_unicode_bidi =
+ next_highest_ancestor_with_unicode_bidi;
+ }
+
+ // Split every ancestor through highest ancestor with embedding.
+ Node* current_node = node;
+ while (current_node) {
+ Element* parent = ToElement(current_node->parentNode());
+ if (before ? current_node->previousSibling() : current_node->nextSibling())
+ SplitElement(parent, before ? current_node : current_node->nextSibling());
+ if (parent == highest_ancestor_with_unicode_bidi)
+ break;
+ current_node = parent;
+ }
+ return unsplit_ancestor;
+}
+
+void ApplyStyleCommand::RemoveEmbeddingUpToEnclosingBlock(
+ Node* node,
+ HTMLElement* unsplit_ancestor,
+ EditingState* editing_state) {
+ Element* block = EnclosingBlock(node);
+ if (!block)
+ return;
+
+ for (Node& runner : NodeTraversal::AncestorsOf(*node)) {
+ if (runner == block || runner == unsplit_ancestor)
+ break;
+ if (!runner.IsStyledElement())
+ continue;
+
+ Element* element = ToElement(&runner);
+ int unicode_bidi = GetIdentifierValue(
+ CSSComputedStyleDeclaration::Create(element), CSSPropertyUnicodeBidi);
+ if (!unicode_bidi || unicode_bidi == CSSValueNormal)
+ continue;
+
+ // FIXME: This code should really consider the mapped attribute 'dir', the
+ // inline style declaration, and all matching style rules in order to
+ // determine how to best set the unicode-bidi property to 'normal'. For now,
+ // it assumes that if the 'dir' attribute is present, then removing it will
+ // suffice, and otherwise it sets the property in the inline style
+ // declaration.
+ if (element->hasAttribute(dirAttr)) {
+ // FIXME: If this is a BDO element, we should probably just remove it if
+ // it has no other attributes, like we (should) do with B and I elements.
+ RemoveElementAttribute(element, dirAttr);
+ } else {
+ MutableCSSPropertyValueSet* inline_style =
+ CopyStyleOrCreateEmpty(element->InlineStyle());
+ inline_style->SetProperty(CSSPropertyUnicodeBidi, CSSValueNormal);
+ inline_style->RemoveProperty(CSSPropertyDirection);
+ SetNodeAttribute(element, styleAttr,
+ AtomicString(inline_style->AsText()));
+ if (IsSpanWithoutAttributesOrUnstyledStyleSpan(element)) {
+ RemoveNodePreservingChildren(element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+ }
+}
+
+static HTMLElement* HighestEmbeddingAncestor(Node* start_node,
+ Node* enclosing_node) {
+ for (Node* n = start_node; n && n != enclosing_node; n = n->parentNode()) {
+ if (n->IsHTMLElement() &&
+ EditingStyleUtilities::IsEmbedOrIsolate(GetIdentifierValue(
+ CSSComputedStyleDeclaration::Create(n), CSSPropertyUnicodeBidi))) {
+ return ToHTMLElement(n);
+ }
+ }
+
+ return nullptr;
+}
+
+void ApplyStyleCommand::ApplyInlineStyle(EditingStyle* style,
+ EditingState* editing_state) {
+ ContainerNode* start_dummy_span_ancestor = nullptr;
+ ContainerNode* end_dummy_span_ancestor = nullptr;
+
+ // update document layout once before removing styles
+ // so that we avoid the expense of updating before each and every call
+ // to check a computed style
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // adjust to the positions we want to use for applying style
+ Position start = StartPosition();
+ Position end = EndPosition();
+
+ if (start.IsNull() || end.IsNull())
+ return;
+
+ if (ComparePositions(end, start) < 0) {
+ Position swap = start;
+ start = end;
+ end = swap;
+ }
+
+ // split the start node and containing element if the selection starts inside
+ // of it
+ bool split_start = IsValidCaretPositionInTextNode(start);
+ if (split_start) {
+ if (ShouldSplitTextElement(start.AnchorNode()->parentElement(), style))
+ SplitTextElementAtStart(start, end);
+ else
+ SplitTextAtStart(start, end);
+ start = StartPosition();
+ end = EndPosition();
+ if (start.IsNull() || end.IsNull())
+ return;
+ start_dummy_span_ancestor = DummySpanAncestorForNode(start.AnchorNode());
+ }
+
+ // split the end node and containing element if the selection ends inside of
+ // it
+ bool split_end = IsValidCaretPositionInTextNode(end);
+ if (split_end) {
+ if (ShouldSplitTextElement(end.AnchorNode()->parentElement(), style))
+ SplitTextElementAtEnd(start, end);
+ else
+ SplitTextAtEnd(start, end);
+ start = StartPosition();
+ end = EndPosition();
+ if (start.IsNull() || end.IsNull())
+ return;
+ end_dummy_span_ancestor = DummySpanAncestorForNode(end.AnchorNode());
+ }
+
+ // Remove style from the selection.
+ // Use the upstream position of the start for removing style.
+ // This will ensure we remove all traces of the relevant styles from the
+ // selection and prevent us from adding redundant ones, as described in:
+ // <rdar://problem/3724344> Bolding and unbolding creates extraneous tags
+ Position remove_start = MostBackwardCaretPosition(start);
+ WritingDirection text_direction = WritingDirection::kNatural;
+ bool has_text_direction = style->GetTextDirection(text_direction);
+ EditingStyle* style_without_embedding = nullptr;
+ EditingStyle* embedding_style = nullptr;
+ if (has_text_direction) {
+ // Leave alone an ancestor that provides the desired single level embedding,
+ // if there is one.
+ HTMLElement* start_unsplit_ancestor =
+ SplitAncestorsWithUnicodeBidi(start.AnchorNode(), true, text_direction);
+ HTMLElement* end_unsplit_ancestor =
+ SplitAncestorsWithUnicodeBidi(end.AnchorNode(), false, text_direction);
+ RemoveEmbeddingUpToEnclosingBlock(start.AnchorNode(),
+ start_unsplit_ancestor, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ RemoveEmbeddingUpToEnclosingBlock(end.AnchorNode(), end_unsplit_ancestor,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ // Avoid removing the dir attribute and the unicode-bidi and direction
+ // properties from the unsplit ancestors.
+ Position embedding_remove_start = remove_start;
+ if (start_unsplit_ancestor &&
+ ElementFullySelected(*start_unsplit_ancestor, remove_start, end))
+ embedding_remove_start =
+ Position::InParentAfterNode(*start_unsplit_ancestor);
+
+ Position embedding_remove_end = end;
+ if (end_unsplit_ancestor &&
+ ElementFullySelected(*end_unsplit_ancestor, remove_start, end))
+ embedding_remove_end = MostForwardCaretPosition(
+ Position::InParentBeforeNode(*end_unsplit_ancestor));
+
+ if (embedding_remove_end != remove_start || embedding_remove_end != end) {
+ style_without_embedding = style->Copy();
+ embedding_style = style_without_embedding->ExtractAndRemoveTextDirection(
+ GetDocument().GetSecureContextMode());
+
+ if (ComparePositions(embedding_remove_start, embedding_remove_end) <= 0) {
+ RemoveInlineStyle(
+ embedding_style,
+ EphemeralRange(embedding_remove_start, embedding_remove_end),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+ }
+
+ RemoveInlineStyle(style_without_embedding ? style_without_embedding : style,
+ EphemeralRange(remove_start, end), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ start = StartPosition();
+ end = EndPosition();
+ if (start.IsNull() || start.IsOrphan() || end.IsNull() || end.IsOrphan())
+ return;
+
+ if (split_start) {
+ bool merge_result =
+ MergeStartWithPreviousIfIdentical(start, end, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ if (split_start && merge_result) {
+ start = StartPosition();
+ end = EndPosition();
+ }
+ }
+
+ if (split_end) {
+ MergeEndWithNextIfIdentical(start, end, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ start = StartPosition();
+ end = EndPosition();
+ }
+
+ // update document layout once before running the rest of the function
+ // so that we avoid the expense of updating before each and every call
+ // to check a computed style
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ EditingStyle* style_to_apply = style;
+ if (has_text_direction) {
+ // Avoid applying the unicode-bidi and direction properties beneath
+ // ancestors that already have them.
+ HTMLElement* embedding_start_element = HighestEmbeddingAncestor(
+ start.AnchorNode(), EnclosingBlock(start.AnchorNode()));
+ HTMLElement* embedding_end_element = HighestEmbeddingAncestor(
+ end.AnchorNode(), EnclosingBlock(end.AnchorNode()));
+
+ if (embedding_start_element || embedding_end_element) {
+ Position embedding_apply_start =
+ embedding_start_element
+ ? Position::InParentAfterNode(*embedding_start_element)
+ : start;
+ Position embedding_apply_end =
+ embedding_end_element
+ ? Position::InParentBeforeNode(*embedding_end_element)
+ : end;
+ DCHECK(embedding_apply_start.IsNotNull());
+ DCHECK(embedding_apply_end.IsNotNull());
+
+ if (!embedding_style) {
+ style_without_embedding = style->Copy();
+ embedding_style =
+ style_without_embedding->ExtractAndRemoveTextDirection(
+ GetDocument().GetSecureContextMode());
+ }
+ FixRangeAndApplyInlineStyle(embedding_style, embedding_apply_start,
+ embedding_apply_end, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ style_to_apply = style_without_embedding;
+ }
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ FixRangeAndApplyInlineStyle(style_to_apply, start, end, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ // Remove dummy style spans created by splitting text elements.
+ CleanupUnstyledAppleStyleSpans(start_dummy_span_ancestor, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ if (end_dummy_span_ancestor != start_dummy_span_ancestor)
+ CleanupUnstyledAppleStyleSpans(end_dummy_span_ancestor, editing_state);
+}
+
+void ApplyStyleCommand::FixRangeAndApplyInlineStyle(
+ EditingStyle* style,
+ const Position& start,
+ const Position& end,
+ EditingState* editing_state) {
+ Node* start_node = start.AnchorNode();
+ DCHECK(start_node);
+
+ if (start.ComputeEditingOffset() >= CaretMaxOffset(start.AnchorNode())) {
+ start_node = NodeTraversal::Next(*start_node);
+ if (!start_node ||
+ ComparePositions(end, FirstPositionInOrBeforeNode(*start_node)) < 0)
+ return;
+ }
+
+ Node* past_end_node = end.AnchorNode();
+ if (end.ComputeEditingOffset() >= CaretMaxOffset(end.AnchorNode()))
+ past_end_node = NodeTraversal::NextSkippingChildren(*end.AnchorNode());
+
+ // FIXME: Callers should perform this operation on a Range that includes the
+ // br if they want style applied to the empty line.
+ if (start == end && IsHTMLBRElement(*start.AnchorNode()))
+ past_end_node = NodeTraversal::Next(*start.AnchorNode());
+
+ // Start from the highest fully selected ancestor so that we can modify the
+ // fully selected node. e.g. When applying font-size: large on <font
+ // color="blue">hello</font>, we need to include the font element in our run
+ // to generate <font color="blue" size="4">hello</font> instead of <font
+ // color="blue"><font size="4">hello</font></font>
+ Element* editable_root = RootEditableElement(*start_node);
+ if (start_node != editable_root) {
+ // TODO(editing-dev): Investigate why |start| can be after |end| here in
+ // some cases. For example, in LayoutTest
+ // editing/style/make-text-writing-direction-inline-{mac,win}.html
+ // blink::Range object will collapse to end in this case but EphemeralRange
+ // will trigger DCHECK, so we have to explicitly handle this.
+ const EphemeralRange& range =
+ start <= end ? EphemeralRange(start, end) : EphemeralRange(end, start);
+ while (editable_root && start_node->parentNode() != editable_root &&
+ IsNodeVisiblyContainedWithin(*start_node->parentNode(), range))
+ start_node = start_node->parentNode();
+ }
+
+ ApplyInlineStyleToNodeRange(style, start_node, past_end_node, editing_state);
+}
+
+static bool ContainsNonEditableRegion(Node& node) {
+ if (!HasEditableStyle(node))
+ return true;
+
+ Node* sibling = NodeTraversal::NextSkippingChildren(node);
+ for (Node* descendent = node.firstChild();
+ descendent && descendent != sibling;
+ descendent = NodeTraversal::Next(*descendent)) {
+ if (!HasEditableStyle(*descendent))
+ return true;
+ }
+
+ return false;
+}
+
+class InlineRunToApplyStyle {
+ DISALLOW_NEW_EXCEPT_PLACEMENT_NEW();
+
+ public:
+ InlineRunToApplyStyle(Node* start, Node* end, Node* past_end_node)
+ : start(start), end(end), past_end_node(past_end_node) {
+ DCHECK_EQ(start->parentNode(), end->parentNode());
+ }
+
+ bool StartAndEndAreStillInDocument() {
+ return start && end && start->isConnected() && end->isConnected();
+ }
+
+ void Trace(blink::Visitor* visitor) {
+ visitor->Trace(start);
+ visitor->Trace(end);
+ visitor->Trace(past_end_node);
+ visitor->Trace(position_for_style_computation);
+ visitor->Trace(dummy_element);
+ }
+
+ Member<Node> start;
+ Member<Node> end;
+ Member<Node> past_end_node;
+ Position position_for_style_computation;
+ Member<HTMLSpanElement> dummy_element;
+ StyleChange change;
+};
+
+} // namespace blink
+
+WTF_ALLOW_INIT_WITH_MEM_FUNCTIONS(blink::InlineRunToApplyStyle);
+
+namespace blink {
+
+void ApplyStyleCommand::ApplyInlineStyleToNodeRange(
+ EditingStyle* style,
+ Node* start_node,
+ Node* past_end_node,
+ EditingState* editing_state) {
+ if (remove_only_)
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ HeapVector<InlineRunToApplyStyle> runs;
+ Node* node = start_node;
+ for (Node* next; node && node != past_end_node; node = next) {
+ next = NodeTraversal::Next(*node);
+
+ if (!node->GetLayoutObject() || !HasEditableStyle(*node))
+ continue;
+
+ if (!HasRichlyEditableStyle(*node) && node->IsHTMLElement()) {
+ HTMLElement* element = ToHTMLElement(node);
+ // This is a plaintext-only region. Only proceed if it's fully selected.
+ // pastEndNode is the node after the last fully selected node, so if it's
+ // inside node then node isn't fully selected.
+ if (past_end_node && past_end_node->IsDescendantOf(element))
+ break;
+ // Add to this element's inline style and skip over its contents.
+ next = NodeTraversal::NextSkippingChildren(*node);
+ if (!style->Style())
+ continue;
+ MutableCSSPropertyValueSet* inline_style =
+ CopyStyleOrCreateEmpty(element->InlineStyle());
+ inline_style->MergeAndOverrideOnConflict(style->Style());
+ SetNodeAttribute(element, styleAttr,
+ AtomicString(inline_style->AsText()));
+ continue;
+ }
+
+ if (IsEnclosingBlock(node))
+ continue;
+
+ if (node->hasChildren()) {
+ if (node->contains(past_end_node) || ContainsNonEditableRegion(*node) ||
+ !HasEditableStyle(*node->parentNode()))
+ continue;
+ if (EditingIgnoresContent(*node)) {
+ next = NodeTraversal::NextSkippingChildren(*node);
+ continue;
+ }
+ }
+
+ Node* run_start = node;
+ Node* run_end = node;
+ Node* sibling = node->nextSibling();
+ while (sibling && sibling != past_end_node &&
+ !sibling->contains(past_end_node) &&
+ (!IsEnclosingBlock(sibling) || IsHTMLBRElement(*sibling)) &&
+ !ContainsNonEditableRegion(*sibling)) {
+ run_end = sibling;
+ sibling = run_end->nextSibling();
+ }
+ DCHECK(run_end);
+ next = NodeTraversal::NextSkippingChildren(*run_end);
+
+ Node* past_end_node = NodeTraversal::NextSkippingChildren(*run_end);
+ if (!ShouldApplyInlineStyleToRun(style, run_start, past_end_node))
+ continue;
+
+ runs.push_back(InlineRunToApplyStyle(run_start, run_end, past_end_node));
+ }
+
+ for (auto& run : runs) {
+ RemoveConflictingInlineStyleFromRun(style, run.start, run.end,
+ run.past_end_node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ if (run.StartAndEndAreStillInDocument()) {
+ run.position_for_style_computation = PositionToComputeInlineStyleChange(
+ run.start, run.dummy_element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ for (auto& run : runs) {
+ if (run.position_for_style_computation.IsNotNull())
+ run.change = StyleChange(style, run.position_for_style_computation);
+ }
+
+ for (auto& run : runs) {
+ if (run.dummy_element) {
+ RemoveNode(run.dummy_element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ if (run.StartAndEndAreStillInDocument()) {
+ ApplyInlineStyleChange(run.start.Release(), run.end.Release(), run.change,
+ kAddStyledElement, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+}
+
+bool ApplyStyleCommand::IsStyledInlineElementToRemove(Element* element) const {
+ return (styled_inline_element_ &&
+ element->HasTagName(styled_inline_element_->TagQName())) ||
+ (is_inline_element_to_remove_function_ &&
+ is_inline_element_to_remove_function_(element));
+}
+
+bool ApplyStyleCommand::ShouldApplyInlineStyleToRun(EditingStyle* style,
+ Node* run_start,
+ Node* past_end_node) {
+ DCHECK(style);
+ DCHECK(run_start);
+
+ for (Node* node = run_start; node && node != past_end_node;
+ node = NodeTraversal::Next(*node)) {
+ if (node->hasChildren())
+ continue;
+ // We don't consider m_isInlineElementToRemoveFunction here because we never
+ // apply style when m_isInlineElementToRemoveFunction is specified
+ if (!style->StyleIsPresentInComputedStyleOfNode(node))
+ return true;
+ if (styled_inline_element_ &&
+ !EnclosingElementWithTag(Position::BeforeNode(*node),
+ styled_inline_element_->TagQName()))
+ return true;
+ }
+ return false;
+}
+
+void ApplyStyleCommand::RemoveConflictingInlineStyleFromRun(
+ EditingStyle* style,
+ Member<Node>& run_start,
+ Member<Node>& run_end,
+ Node* past_end_node,
+ EditingState* editing_state) {
+ DCHECK(run_start);
+ DCHECK(run_end);
+ Node* next = run_start;
+ for (Node* node = next; node && node->isConnected() && node != past_end_node;
+ node = next) {
+ if (EditingIgnoresContent(*node)) {
+ DCHECK(!node->contains(past_end_node)) << node << " " << past_end_node;
+ next = NodeTraversal::NextSkippingChildren(*node);
+ } else {
+ next = NodeTraversal::Next(*node);
+ }
+ if (!node->IsHTMLElement())
+ continue;
+
+ HTMLElement& element = ToHTMLElement(*node);
+ Node* previous_sibling = element.previousSibling();
+ Node* next_sibling = element.nextSibling();
+ ContainerNode* parent = element.parentNode();
+ RemoveInlineStyleFromElement(style, &element, editing_state, kRemoveAlways);
+ if (editing_state->IsAborted())
+ return;
+ if (!element.isConnected()) {
+ // FIXME: We might need to update the start and the end of current
+ // selection here but need a test.
+ if (run_start == element)
+ run_start = previous_sibling ? previous_sibling->nextSibling()
+ : parent->firstChild();
+ if (run_end == element)
+ run_end = next_sibling ? next_sibling->previousSibling()
+ : parent->lastChild();
+ }
+ }
+}
+
+bool ApplyStyleCommand::RemoveInlineStyleFromElement(
+ EditingStyle* style,
+ HTMLElement* element,
+ EditingState* editing_state,
+ InlineStyleRemovalMode mode,
+ EditingStyle* extracted_style) {
+ DCHECK(element);
+ GetDocument().UpdateStyleAndLayoutTree();
+ if (!element->parentNode() || !HasEditableStyle(*element->parentNode()))
+ return false;
+
+ if (IsStyledInlineElementToRemove(element)) {
+ if (mode == kRemoveNone)
+ return true;
+ if (extracted_style)
+ extracted_style->MergeInlineStyleOfElement(element,
+ EditingStyle::kOverrideValues);
+ RemoveNodePreservingChildren(element, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ return true;
+ }
+
+ bool removed = RemoveImplicitlyStyledElement(style, element, mode,
+ extracted_style, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+
+ if (!element->isConnected())
+ return removed;
+
+ // If the node was converted to a span, the span may still contain relevant
+ // styles which must be removed (e.g. <b style='font-weight: bold'>)
+ if (RemoveCSSStyle(style, element, editing_state, mode, extracted_style))
+ removed = true;
+ if (editing_state->IsAborted())
+ return false;
+
+ return removed;
+}
+
+void ApplyStyleCommand::ReplaceWithSpanOrRemoveIfWithoutAttributes(
+ HTMLElement* elem,
+ EditingState* editing_state) {
+ if (HasNoAttributeOrOnlyStyleAttribute(elem, kStyleAttributeShouldBeEmpty))
+ RemoveNodePreservingChildren(elem, editing_state);
+ else
+ ReplaceElementWithSpanPreservingChildrenAndAttributes(elem);
+}
+
+bool ApplyStyleCommand::RemoveImplicitlyStyledElement(
+ EditingStyle* style,
+ HTMLElement* element,
+ InlineStyleRemovalMode mode,
+ EditingStyle* extracted_style,
+ EditingState* editing_state) {
+ DCHECK(style);
+ if (mode == kRemoveNone) {
+ DCHECK(!extracted_style);
+ return style->ConflictsWithImplicitStyleOfElement(element) ||
+ style->ConflictsWithImplicitStyleOfAttributes(element);
+ }
+
+ DCHECK(mode == kRemoveIfNeeded || mode == kRemoveAlways);
+ if (style->ConflictsWithImplicitStyleOfElement(
+ element, extracted_style,
+ mode == kRemoveAlways ? EditingStyle::kExtractMatchingStyle
+ : EditingStyle::kDoNotExtractMatchingStyle)) {
+ ReplaceWithSpanOrRemoveIfWithoutAttributes(element, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ return true;
+ }
+
+ // unicode-bidi and direction are pushed down separately so don't push down
+ // with other styles
+ Vector<QualifiedName> attributes;
+ if (!style->ExtractConflictingImplicitStyleOfAttributes(
+ element,
+ extracted_style ? EditingStyle::kPreserveWritingDirection
+ : EditingStyle::kDoNotPreserveWritingDirection,
+ extracted_style, attributes,
+ mode == kRemoveAlways ? EditingStyle::kExtractMatchingStyle
+ : EditingStyle::kDoNotExtractMatchingStyle))
+ return false;
+
+ for (const auto& attribute : attributes)
+ RemoveElementAttribute(element, attribute);
+
+ if (IsEmptyFontTag(element) ||
+ IsSpanWithoutAttributesOrUnstyledStyleSpan(element)) {
+ RemoveNodePreservingChildren(element, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ }
+
+ return true;
+}
+
+bool ApplyStyleCommand::RemoveCSSStyle(EditingStyle* style,
+ HTMLElement* element,
+ EditingState* editing_state,
+ InlineStyleRemovalMode mode,
+ EditingStyle* extracted_style) {
+ DCHECK(style);
+ DCHECK(element);
+
+ if (mode == kRemoveNone)
+ return style->ConflictsWithInlineStyleOfElement(element);
+
+ Vector<CSSPropertyID> properties;
+ if (!style->ConflictsWithInlineStyleOfElement(element, extracted_style,
+ properties))
+ return false;
+
+ // FIXME: We should use a mass-removal function here but we don't have an
+ // undoable one yet.
+ for (const auto& property : properties)
+ RemoveCSSProperty(element, property);
+
+ if (IsSpanWithoutAttributesOrUnstyledStyleSpan(element))
+ RemoveNodePreservingChildren(element, editing_state);
+
+ return true;
+}
+
+// Finds the enclosing element until which the tree can be split.
+// When a user hits ENTER, they won't expect this element to be split into two.
+// You may pass it as the second argument of splitTreeToNode.
+static Element* UnsplittableElementForPosition(const Position& p) {
+ // Since enclosingNodeOfType won't search beyond the highest root editable
+ // node, this code works even if the closest table cell was outside of the
+ // root editable node.
+ Element* enclosing_cell = ToElement(EnclosingNodeOfType(p, &IsTableCell));
+ if (enclosing_cell)
+ return enclosing_cell;
+
+ return RootEditableElementOf(p);
+}
+
+HTMLElement* ApplyStyleCommand::HighestAncestorWithConflictingInlineStyle(
+ EditingStyle* style,
+ Node* node) {
+ if (!node)
+ return nullptr;
+
+ HTMLElement* result = nullptr;
+ Node* unsplittable_element =
+ UnsplittableElementForPosition(FirstPositionInOrBeforeNode(*node));
+
+ for (Node* n = node; n; n = n->parentNode()) {
+ if (n->IsHTMLElement() &&
+ ShouldRemoveInlineStyleFromElement(style, ToHTMLElement(n)))
+ result = ToHTMLElement(n);
+ // Should stop at the editable root (cannot cross editing boundary) and
+ // also stop at the unsplittable element to be consistent with other UAs
+ if (n == unsplittable_element)
+ break;
+ }
+
+ return result;
+}
+
+void ApplyStyleCommand::ApplyInlineStyleToPushDown(
+ Node* node,
+ EditingStyle* style,
+ EditingState* editing_state) {
+ DCHECK(node);
+
+ node->GetDocument().UpdateStyleAndLayoutTree();
+
+ if (!style || style->IsEmpty() || !node->GetLayoutObject() ||
+ IsHTMLIFrameElement(*node))
+ return;
+
+ EditingStyle* new_inline_style = style;
+ if (node->IsHTMLElement() && ToHTMLElement(node)->InlineStyle()) {
+ new_inline_style = style->Copy();
+ new_inline_style->MergeInlineStyleOfElement(ToHTMLElement(node),
+ EditingStyle::kOverrideValues);
+ }
+
+ // Since addInlineStyleIfNeeded can't add styles to block-flow layout objects,
+ // add style attribute instead.
+ // FIXME: applyInlineStyleToRange should be used here instead.
+ if ((node->GetLayoutObject()->IsLayoutBlockFlow() || node->hasChildren()) &&
+ node->IsHTMLElement()) {
+ SetNodeAttribute(ToHTMLElement(node), styleAttr,
+ AtomicString(new_inline_style->Style()->AsText()));
+ return;
+ }
+
+ if (node->GetLayoutObject()->IsText() &&
+ ToLayoutText(node->GetLayoutObject())->IsAllCollapsibleWhitespace())
+ return;
+
+ // We can't wrap node with the styled element here because new styled element
+ // will never be removed if we did. If we modified the child pointer in
+ // pushDownInlineStyleAroundNode to point to new style element then we fall
+ // into an infinite loop where we keep removing and adding styled element
+ // wrapping node.
+ AddInlineStyleIfNeeded(new_inline_style, node, node, editing_state);
+}
+
+void ApplyStyleCommand::PushDownInlineStyleAroundNode(
+ EditingStyle* style,
+ Node* target_node,
+ EditingState* editing_state) {
+ HTMLElement* highest_ancestor =
+ HighestAncestorWithConflictingInlineStyle(style, target_node);
+ if (!highest_ancestor)
+ return;
+
+ // The outer loop is traversing the tree vertically from highestAncestor to
+ // targetNode
+ Node* current = highest_ancestor;
+ // Along the way, styled elements that contain targetNode are removed and
+ // accumulated into elementsToPushDown. Each child of the removed element,
+ // exclusing ancestors of targetNode, is then wrapped by clones of elements in
+ // elementsToPushDown.
+ HeapVector<Member<Element>> elements_to_push_down;
+ while (current && current != target_node && current->contains(target_node)) {
+ NodeVector current_children;
+ GetChildNodes(ToContainerNode(*current), current_children);
+ Element* styled_element = nullptr;
+ if (current->IsStyledElement() &&
+ IsStyledInlineElementToRemove(ToElement(current))) {
+ styled_element = ToElement(current);
+ elements_to_push_down.push_back(styled_element);
+ }
+
+ EditingStyle* style_to_push_down = EditingStyle::Create();
+ if (current->IsHTMLElement()) {
+ RemoveInlineStyleFromElement(style, ToHTMLElement(current), editing_state,
+ kRemoveIfNeeded, style_to_push_down);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ // The inner loop will go through children on each level
+ // FIXME: we should aggregate inline child elements together so that we
+ // don't wrap each child separately.
+ for (const auto& current_child : current_children) {
+ Node* child = current_child;
+ if (!child->parentNode())
+ continue;
+ if (!child->contains(target_node) && elements_to_push_down.size()) {
+ for (const auto& element : elements_to_push_down) {
+ Element* wrapper = element->CloneWithoutChildren();
+ wrapper->removeAttribute(styleAttr);
+ // Delete id attribute from the second element because the same id
+ // cannot be used for more than one element
+ element->removeAttribute(HTMLNames::idAttr);
+ if (IsHTMLAnchorElement(element))
+ element->removeAttribute(HTMLNames::nameAttr);
+ SurroundNodeRangeWithElement(child, child, wrapper, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+
+ // Apply style to all nodes containing targetNode and their siblings but
+ // NOT to targetNode But if we've removed styledElement then go ahead and
+ // always apply the style.
+ if (child != target_node || styled_element) {
+ ApplyInlineStyleToPushDown(child, style_to_push_down, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ // We found the next node for the outer loop (contains targetNode)
+ // When reached targetNode, stop the outer loop upon the completion of the
+ // current inner loop
+ if (child == target_node || child->contains(target_node))
+ current = child;
+ }
+ }
+}
+
+void ApplyStyleCommand::RemoveInlineStyle(EditingStyle* style,
+ const EphemeralRange& range,
+ EditingState* editing_state) {
+ const Position& start = range.StartPosition();
+ const Position& end = range.EndPosition();
+ DCHECK(Position::CommonAncestorTreeScope(start, end)) << start << " " << end;
+ // FIXME: We should assert that start/end are not in the middle of a text
+ // node.
+
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ Position push_down_start = MostForwardCaretPosition(start);
+ // If the pushDownStart is at the end of a text node, then this node is not
+ // fully selected. Move it to the next deep quivalent position to avoid
+ // removing the style from this node. e.g. if pushDownStart was at
+ // Position("hello", 5) in <b>hello<div>world</div></b>, we want
+ // Position("world", 0) instead.
+ const unsigned push_down_start_offset =
+ push_down_start.ComputeOffsetInContainerNode();
+ Node* push_down_start_container = push_down_start.ComputeContainerNode();
+ if (push_down_start_container && push_down_start_container->IsTextNode() &&
+ push_down_start_offset ==
+ ToText(push_down_start_container)->length())
+ push_down_start = NextVisuallyDistinctCandidate(push_down_start);
+
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ Position push_down_end = MostBackwardCaretPosition(end);
+ // If pushDownEnd is at the start of a text node, then this node is not fully
+ // selected. Move it to the previous deep equivalent position to avoid
+ // removing the style from this node.
+ Node* push_down_end_container = push_down_end.ComputeContainerNode();
+ if (push_down_end_container && push_down_end_container->IsTextNode() &&
+ !push_down_end.ComputeOffsetInContainerNode())
+ push_down_end = PreviousVisuallyDistinctCandidate(push_down_end);
+
+ PushDownInlineStyleAroundNode(style, push_down_start.AnchorNode(),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ PushDownInlineStyleAroundNode(style, push_down_end.AnchorNode(),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ // The s and e variables store the positions used to set the ending selection
+ // after style removal takes place. This will help callers to recognize when
+ // either the start node or the end node are removed from the document during
+ // the work of this function.
+ // If pushDownInlineStyleAroundNode has pruned start.anchorNode() or
+ // end.anchorNode(), use pushDownStart or pushDownEnd instead, which
+ // pushDownInlineStyleAroundNode won't prune.
+ Position s = start.IsNull() || start.IsOrphan() ? push_down_start : start;
+ Position e = end.IsNull() || end.IsOrphan() ? push_down_end : end;
+
+ // Current ending selection resetting algorithm assumes |start| and |end|
+ // are in a same DOM tree even if they are not in document.
+ if (!Position::CommonAncestorTreeScope(start, end))
+ return;
+
+ Node* node = start.AnchorNode();
+ while (node) {
+ Node* next = nullptr;
+ if (EditingIgnoresContent(*node)) {
+ DCHECK(node == end.AnchorNode() || !node->contains(end.AnchorNode()))
+ << node << " " << end;
+ next = NodeTraversal::NextSkippingChildren(*node);
+ } else {
+ next = NodeTraversal::Next(*node);
+ }
+ if (node->IsHTMLElement() &&
+ ElementFullySelected(ToHTMLElement(*node), start, end)) {
+ HTMLElement* elem = ToHTMLElement(node);
+ Node* prev = NodeTraversal::PreviousPostOrder(*elem);
+ Node* next = NodeTraversal::Next(*elem);
+ EditingStyle* style_to_push_down = nullptr;
+ Node* child_node = nullptr;
+ if (IsStyledInlineElementToRemove(elem)) {
+ style_to_push_down = EditingStyle::Create();
+ child_node = elem->firstChild();
+ }
+
+ RemoveInlineStyleFromElement(style, elem, editing_state, kRemoveIfNeeded,
+ style_to_push_down);
+ if (editing_state->IsAborted())
+ return;
+ if (!elem->isConnected()) {
+ if (s.AnchorNode() == elem) {
+ // Since elem must have been fully selected, and it is at the start
+ // of the selection, it is clear we can set the new s offset to 0.
+ DCHECK(s.IsBeforeAnchor() || s.IsBeforeChildren() ||
+ s.OffsetInContainerNode() <= 0)
+ << s;
+ s = next ? FirstPositionInOrBeforeNode(*next) : Position();
+ }
+ if (e.AnchorNode() == elem) {
+ // Since elem must have been fully selected, and it is at the end
+ // of the selection, it is clear we can set the new e offset to
+ // the max range offset of prev.
+ DCHECK(s.IsAfterAnchor() ||
+ !OffsetIsBeforeLastNodeOffset(s.OffsetInContainerNode(),
+ s.ComputeContainerNode()))
+ << s;
+ e = prev ? LastPositionInOrAfterNode(*prev) : Position();
+ }
+ }
+
+ if (style_to_push_down) {
+ for (; child_node; child_node = child_node->nextSibling()) {
+ ApplyInlineStyleToPushDown(child_node, style_to_push_down,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+ }
+ if (node == end.AnchorNode())
+ break;
+ node = next;
+ }
+
+ UpdateStartEnd(EphemeralRange(s, e));
+}
+
+bool ApplyStyleCommand::ElementFullySelected(const HTMLElement& element,
+ const Position& start,
+ const Position& end) const {
+ // The tree may have changed and Position::upstream() relies on an up-to-date
+ // layout.
+ element.GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ return ComparePositions(FirstPositionInOrBeforeNode(element), start) >= 0 &&
+ ComparePositions(
+ MostBackwardCaretPosition(LastPositionInOrAfterNode(element)),
+ end) <= 0;
+}
+
+void ApplyStyleCommand::SplitTextAtStart(const Position& start,
+ const Position& end) {
+ DCHECK(start.ComputeContainerNode()->IsTextNode()) << start;
+
+ Position new_end;
+ if (end.IsOffsetInAnchor() &&
+ start.ComputeContainerNode() == end.ComputeContainerNode())
+ new_end =
+ Position(end.ComputeContainerNode(),
+ end.OffsetInContainerNode() - start.OffsetInContainerNode());
+ else
+ new_end = end;
+
+ Text* text = ToText(start.ComputeContainerNode());
+ SplitTextNode(text, start.OffsetInContainerNode());
+ UpdateStartEnd(EphemeralRange(Position::FirstPositionInNode(*text), new_end));
+}
+
+void ApplyStyleCommand::SplitTextAtEnd(const Position& start,
+ const Position& end) {
+ DCHECK(end.ComputeContainerNode()->IsTextNode()) << end;
+
+ bool should_update_start =
+ start.IsOffsetInAnchor() &&
+ start.ComputeContainerNode() == end.ComputeContainerNode();
+ Text* text = ToText(end.AnchorNode());
+ SplitTextNode(text, end.OffsetInContainerNode());
+
+ Node* prev_node = text->previousSibling();
+ if (!prev_node || !prev_node->IsTextNode())
+ return;
+
+ Position new_start =
+ should_update_start
+ ? Position(ToText(prev_node), start.OffsetInContainerNode())
+ : start;
+ UpdateStartEnd(
+ EphemeralRange(new_start, Position::LastPositionInNode(*prev_node)));
+}
+
+void ApplyStyleCommand::SplitTextElementAtStart(const Position& start,
+ const Position& end) {
+ DCHECK(start.ComputeContainerNode()->IsTextNode()) << start;
+
+ Position new_end;
+ if (start.ComputeContainerNode() == end.ComputeContainerNode())
+ new_end =
+ Position(end.ComputeContainerNode(),
+ end.OffsetInContainerNode() - start.OffsetInContainerNode());
+ else
+ new_end = end;
+
+ SplitTextNodeContainingElement(ToText(start.ComputeContainerNode()),
+ start.OffsetInContainerNode());
+ UpdateStartEnd(EphemeralRange(
+ Position::BeforeNode(*start.ComputeContainerNode()), new_end));
+}
+
+void ApplyStyleCommand::SplitTextElementAtEnd(const Position& start,
+ const Position& end) {
+ DCHECK(end.ComputeContainerNode()->IsTextNode()) << end;
+
+ bool should_update_start =
+ start.ComputeContainerNode() == end.ComputeContainerNode();
+ SplitTextNodeContainingElement(ToText(end.ComputeContainerNode()),
+ end.OffsetInContainerNode());
+
+ Node* parent_element = end.ComputeContainerNode()->parentNode();
+ if (!parent_element || !parent_element->previousSibling())
+ return;
+ Node* first_text_node = parent_element->previousSibling()->lastChild();
+ if (!first_text_node || !first_text_node->IsTextNode())
+ return;
+
+ Position new_start =
+ should_update_start
+ ? Position(ToText(first_text_node), start.OffsetInContainerNode())
+ : start;
+ UpdateStartEnd(
+ EphemeralRange(new_start, Position::AfterNode(*first_text_node)));
+}
+
+bool ApplyStyleCommand::ShouldSplitTextElement(Element* element,
+ EditingStyle* style) {
+ if (!element || !element->IsHTMLElement())
+ return false;
+
+ return ShouldRemoveInlineStyleFromElement(style, ToHTMLElement(element));
+}
+
+bool ApplyStyleCommand::IsValidCaretPositionInTextNode(
+ const Position& position) {
+ DCHECK(position.IsNotNull());
+
+ Node* node = position.ComputeContainerNode();
+ if (!position.IsOffsetInAnchor() || !node->IsTextNode())
+ return false;
+ int offset_in_text = position.OffsetInContainerNode();
+ return offset_in_text > CaretMinOffset(node) &&
+ offset_in_text < CaretMaxOffset(node);
+}
+
+bool ApplyStyleCommand::MergeStartWithPreviousIfIdentical(
+ const Position& start,
+ const Position& end,
+ EditingState* editing_state) {
+ Node* start_node = start.ComputeContainerNode();
+ int start_offset = start.ComputeOffsetInContainerNode();
+ if (start_offset)
+ return false;
+
+ if (IsAtomicNode(start_node)) {
+ // note: prior siblings could be unrendered elements. it's silly to miss the
+ // merge opportunity just for that.
+ if (start_node->previousSibling())
+ return false;
+
+ start_node = start_node->parentNode();
+ }
+
+ if (!start_node->IsElementNode())
+ return false;
+
+ Node* previous_sibling = start_node->previousSibling();
+
+ if (previous_sibling &&
+ AreIdenticalElements(*start_node, *previous_sibling)) {
+ Element* previous_element = ToElement(previous_sibling);
+ Element* element = ToElement(start_node);
+ Node* start_child = element->firstChild();
+ DCHECK(start_child);
+ MergeIdenticalElements(previous_element, element, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+
+ int start_offset_adjustment = start_child->NodeIndex();
+ int end_offset_adjustment =
+ start_node == end.AnchorNode() ? start_offset_adjustment : 0;
+ UpdateStartEnd(EphemeralRange(
+ Position(start_node, start_offset_adjustment),
+ Position(end.AnchorNode(),
+ end.ComputeEditingOffset() + end_offset_adjustment)));
+ return true;
+ }
+
+ return false;
+}
+
+bool ApplyStyleCommand::MergeEndWithNextIfIdentical(
+ const Position& start,
+ const Position& end,
+ EditingState* editing_state) {
+ Node* end_node = end.ComputeContainerNode();
+
+ if (IsAtomicNode(end_node)) {
+ int end_offset = end.ComputeOffsetInContainerNode();
+ if (OffsetIsBeforeLastNodeOffset(end_offset, end_node))
+ return false;
+
+ if (end.AnchorNode()->nextSibling())
+ return false;
+
+ end_node = end.AnchorNode()->parentNode();
+ }
+
+ if (!end_node->IsElementNode() || IsHTMLBRElement(*end_node))
+ return false;
+
+ Node* next_sibling = end_node->nextSibling();
+ if (next_sibling && AreIdenticalElements(*end_node, *next_sibling)) {
+ Element* next_element = ToElement(next_sibling);
+ Element* element = ToElement(end_node);
+ Node* next_child = next_element->firstChild();
+
+ MergeIdenticalElements(element, next_element, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+
+ bool should_update_start = start.ComputeContainerNode() == end_node;
+ int end_offset = next_child ? next_child->NodeIndex()
+ : next_element->childNodes()->length();
+ UpdateStartEnd(EphemeralRange(
+ should_update_start
+ ? Position(next_element, start.OffsetInContainerNode())
+ : start,
+ Position(next_element, end_offset)));
+ return true;
+ }
+
+ return false;
+}
+
+void ApplyStyleCommand::SurroundNodeRangeWithElement(
+ Node* passed_start_node,
+ Node* end_node,
+ Element* element_to_insert,
+ EditingState* editing_state) {
+ DCHECK(passed_start_node);
+ DCHECK(end_node);
+ DCHECK(element_to_insert);
+ Node* node = passed_start_node;
+ Element* element = element_to_insert;
+
+ InsertNodeBefore(element, node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ GetDocument().UpdateStyleAndLayoutTree();
+ while (node) {
+ Node* next = node->nextSibling();
+ if (HasEditableStyle(*node)) {
+ RemoveNode(node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ AppendNode(node, element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ if (node == end_node)
+ break;
+ node = next;
+ }
+
+ Node* next_sibling = element->nextSibling();
+ Node* previous_sibling = element->previousSibling();
+ if (next_sibling && next_sibling->IsElementNode() &&
+ HasEditableStyle(*next_sibling) &&
+ AreIdenticalElements(*element, ToElement(*next_sibling))) {
+ MergeIdenticalElements(element, ToElement(next_sibling), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ if (previous_sibling && previous_sibling->IsElementNode() &&
+ HasEditableStyle(*previous_sibling)) {
+ Node* merged_element = previous_sibling->nextSibling();
+ if (merged_element->IsElementNode() && HasEditableStyle(*merged_element) &&
+ AreIdenticalElements(ToElement(*previous_sibling),
+ ToElement(*merged_element))) {
+ MergeIdenticalElements(ToElement(previous_sibling),
+ ToElement(merged_element), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+
+ // FIXME: We should probably call updateStartEnd if the start or end was in
+ // the node range so that the endingSelection() is canonicalized. See the
+ // comments at the end of VisibleSelection::validate().
+}
+
+void ApplyStyleCommand::AddBlockStyle(const StyleChange& style_change,
+ HTMLElement* block) {
+ // Do not check for legacy styles here. Those styles, like <B> and <I>, only
+ // apply for inline content.
+ if (!block)
+ return;
+
+ String css_style = style_change.CssStyle();
+ StringBuilder css_text;
+ css_text.Append(css_style);
+ if (const CSSPropertyValueSet* decl = block->InlineStyle()) {
+ if (!css_style.IsEmpty())
+ css_text.Append(' ');
+ css_text.Append(decl->AsText());
+ }
+ SetNodeAttribute(block, styleAttr, css_text.ToAtomicString());
+}
+
+void ApplyStyleCommand::AddInlineStyleIfNeeded(EditingStyle* style,
+ Node* passed_start,
+ Node* passed_end,
+ EditingState* editing_state) {
+ if (!passed_start || !passed_end || !passed_start->isConnected() ||
+ !passed_end->isConnected())
+ return;
+
+ Node* start = passed_start;
+ Member<HTMLSpanElement> dummy_element = nullptr;
+ StyleChange style_change(style, PositionToComputeInlineStyleChange(
+ start, dummy_element, editing_state));
+ if (editing_state->IsAborted())
+ return;
+
+ if (dummy_element) {
+ RemoveNode(dummy_element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ ApplyInlineStyleChange(start, passed_end, style_change,
+ kDoNotAddStyledElement, editing_state);
+}
+
+Position ApplyStyleCommand::PositionToComputeInlineStyleChange(
+ Node* start_node,
+ Member<HTMLSpanElement>& dummy_element,
+ EditingState* editing_state) {
+ DCHECK(start_node);
+ // It's okay to obtain the style at the startNode because we've removed all
+ // relevant styles from the current run.
+ if (!start_node->IsElementNode()) {
+ dummy_element = HTMLSpanElement::Create(GetDocument());
+ InsertNodeAt(dummy_element, Position::BeforeNode(*start_node),
+ editing_state);
+ if (editing_state->IsAborted())
+ return Position();
+ return Position::BeforeNode(*dummy_element);
+ }
+
+ return FirstPositionInOrBeforeNode(*start_node);
+}
+
+void ApplyStyleCommand::ApplyInlineStyleChange(
+ Node* passed_start,
+ Node* passed_end,
+ StyleChange& style_change,
+ EAddStyledElement add_styled_element,
+ EditingState* editing_state) {
+ Node* start_node = passed_start;
+ Node* end_node = passed_end;
+ DCHECK(start_node->isConnected()) << start_node;
+ DCHECK(end_node->isConnected()) << end_node;
+
+ // Find appropriate font and span elements top-down.
+ HTMLFontElement* font_container = nullptr;
+ HTMLElement* style_container = nullptr;
+ for (Node* container = start_node; container && start_node == end_node;
+ container = container->firstChild()) {
+ if (auto* font = ToHTMLFontElementOrNull(container))
+ font_container = font;
+ bool style_container_is_not_span = !IsHTMLSpanElement(style_container);
+ if (container->IsHTMLElement()) {
+ HTMLElement* container_element = ToHTMLElement(container);
+ if (IsHTMLSpanElement(*container_element) ||
+ (style_container_is_not_span && container_element->HasChildren()))
+ style_container = ToHTMLElement(container);
+ }
+ if (!container->hasChildren())
+ break;
+ start_node = container->firstChild();
+ end_node = container->lastChild();
+ }
+
+ // Font tags need to go outside of CSS so that CSS font sizes override leagcy
+ // font sizes.
+ if (style_change.ApplyFontColor() || style_change.ApplyFontFace() ||
+ style_change.ApplyFontSize()) {
+ if (font_container) {
+ if (style_change.ApplyFontColor())
+ SetNodeAttribute(font_container, colorAttr,
+ AtomicString(style_change.FontColor()));
+ if (style_change.ApplyFontFace())
+ SetNodeAttribute(font_container, faceAttr,
+ AtomicString(style_change.FontFace()));
+ if (style_change.ApplyFontSize())
+ SetNodeAttribute(font_container, sizeAttr,
+ AtomicString(style_change.FontSize()));
+ } else {
+ HTMLFontElement* font_element = HTMLFontElement::Create(GetDocument());
+ if (style_change.ApplyFontColor())
+ font_element->setAttribute(colorAttr,
+ AtomicString(style_change.FontColor()));
+ if (style_change.ApplyFontFace())
+ font_element->setAttribute(faceAttr,
+ AtomicString(style_change.FontFace()));
+ if (style_change.ApplyFontSize())
+ font_element->setAttribute(sizeAttr,
+ AtomicString(style_change.FontSize()));
+ SurroundNodeRangeWithElement(start_node, end_node, font_element,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+
+ if (style_change.CssStyle().length()) {
+ if (style_container) {
+ if (const CSSPropertyValueSet* existing_style =
+ style_container->InlineStyle()) {
+ String existing_text = existing_style->AsText();
+ StringBuilder css_text;
+ css_text.Append(existing_text);
+ if (!existing_text.IsEmpty())
+ css_text.Append(' ');
+ css_text.Append(style_change.CssStyle());
+ SetNodeAttribute(style_container, styleAttr, css_text.ToAtomicString());
+ } else {
+ SetNodeAttribute(style_container, styleAttr,
+ AtomicString(style_change.CssStyle()));
+ }
+ } else {
+ HTMLSpanElement* style_element = HTMLSpanElement::Create(GetDocument());
+ style_element->setAttribute(styleAttr,
+ AtomicString(style_change.CssStyle()));
+ SurroundNodeRangeWithElement(start_node, end_node, style_element,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+
+ if (style_change.ApplyBold()) {
+ SurroundNodeRangeWithElement(start_node, end_node,
+ HTMLElement::Create(bTag, GetDocument()),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ if (style_change.ApplyItalic()) {
+ SurroundNodeRangeWithElement(start_node, end_node,
+ HTMLElement::Create(iTag, GetDocument()),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ if (style_change.ApplyUnderline()) {
+ SurroundNodeRangeWithElement(start_node, end_node,
+ HTMLElement::Create(uTag, GetDocument()),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ if (style_change.ApplyLineThrough()) {
+ SurroundNodeRangeWithElement(start_node, end_node,
+ HTMLElement::Create(strikeTag, GetDocument()),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ if (style_change.ApplySubscript()) {
+ SurroundNodeRangeWithElement(start_node, end_node,
+ HTMLElement::Create(subTag, GetDocument()),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ } else if (style_change.ApplySuperscript()) {
+ SurroundNodeRangeWithElement(start_node, end_node,
+ HTMLElement::Create(supTag, GetDocument()),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ if (styled_inline_element_ && add_styled_element == kAddStyledElement) {
+ SurroundNodeRangeWithElement(start_node, end_node,
+ styled_inline_element_->CloneWithoutChildren(),
+ editing_state);
+ }
+}
+
+float ApplyStyleCommand::ComputedFontSize(Node* node) {
+ if (!node)
+ return 0;
+
+ CSSComputedStyleDeclaration* style =
+ CSSComputedStyleDeclaration::Create(node);
+ if (!style)
+ return 0;
+
+ const CSSPrimitiveValue* value =
+ ToCSSPrimitiveValue(style->GetPropertyCSSValue(GetCSSPropertyFontSize()));
+ if (!value)
+ return 0;
+
+ // TODO(yosin): We should have printer for |CSSPrimitiveValue::UnitType|.
+ DCHECK(value->TypeWithCalcResolved() == CSSPrimitiveValue::UnitType::kPixels);
+ return value->GetFloatValue();
+}
+
+void ApplyStyleCommand::JoinChildTextNodes(ContainerNode* node,
+ const Position& start,
+ const Position& end) {
+ if (!node)
+ return;
+
+ Position new_start = start;
+ Position new_end = end;
+
+ HeapVector<Member<Text>> text_nodes;
+ for (Node* curr = node->firstChild(); curr; curr = curr->nextSibling()) {
+ if (!curr->IsTextNode())
+ continue;
+
+ text_nodes.push_back(ToText(curr));
+ }
+
+ for (const auto& text_node : text_nodes) {
+ Text* child_text = text_node;
+ Node* next = child_text->nextSibling();
+ if (!next || !next->IsTextNode())
+ continue;
+
+ Text* next_text = ToText(next);
+ if (start.IsOffsetInAnchor() && next == start.ComputeContainerNode())
+ new_start = Position(
+ child_text, child_text->length() + start.OffsetInContainerNode());
+ if (end.IsOffsetInAnchor() && next == end.ComputeContainerNode())
+ new_end = Position(child_text,
+ child_text->length() + end.OffsetInContainerNode());
+ String text_to_move = next_text->data();
+ InsertTextIntoNode(child_text, child_text->length(), text_to_move);
+ // Removing a Text node doesn't dispatch synchronous events.
+ RemoveNode(next, ASSERT_NO_EDITING_ABORT);
+ // don't move child node pointer. it may want to merge with more text nodes.
+ }
+
+ UpdateStartEnd(EphemeralRange(new_start, new_end));
+}
+
+void ApplyStyleCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(style_);
+ visitor->Trace(start_);
+ visitor->Trace(end_);
+ visitor->Trace(styled_inline_element_);
+ CompositeEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/apply_style_command.h b/chromium/third_party/blink/renderer/core/editing/commands/apply_style_command.h
new file mode 100644
index 00000000000..6cb46a48822
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/apply_style_command.h
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2005, 2006, 2008, 2009 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_APPLY_STYLE_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_APPLY_STYLE_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+
+namespace blink {
+
+class EditingStyle;
+class HTMLSpanElement;
+class StyleChange;
+
+enum ShouldIncludeTypingStyle { kIncludeTypingStyle, kIgnoreTypingStyle };
+
+enum class WritingDirection;
+
+class CORE_EXPORT ApplyStyleCommand final : public CompositeEditCommand {
+ public:
+ enum EPropertyLevel { kPropertyDefault, kForceBlockProperties };
+ enum InlineStyleRemovalMode { kRemoveIfNeeded, kRemoveAlways, kRemoveNone };
+ enum EAddStyledElement { kAddStyledElement, kDoNotAddStyledElement };
+ typedef bool (*IsInlineElementToRemoveFunction)(const Element*);
+
+ static ApplyStyleCommand* Create(Document& document,
+ const EditingStyle* style,
+ InputEvent::InputType input_type,
+ EPropertyLevel level = kPropertyDefault) {
+ return new ApplyStyleCommand(document, style, input_type, level);
+ }
+ static ApplyStyleCommand* Create(Document& document,
+ const EditingStyle* style,
+ const Position& start,
+ const Position& end) {
+ return new ApplyStyleCommand(document, style, start, end);
+ }
+ static ApplyStyleCommand* Create(Element* element, bool remove_only) {
+ return new ApplyStyleCommand(element, remove_only);
+ }
+ static ApplyStyleCommand* Create(
+ Document& document,
+ const EditingStyle* style,
+ IsInlineElementToRemoveFunction is_inline_element_to_remove_function,
+ InputEvent::InputType input_type) {
+ return new ApplyStyleCommand(
+ document, style, is_inline_element_to_remove_function, input_type);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ ApplyStyleCommand(Document&,
+ const EditingStyle*,
+ InputEvent::InputType,
+ EPropertyLevel);
+ ApplyStyleCommand(Document&,
+ const EditingStyle*,
+ const Position& start,
+ const Position& end);
+ ApplyStyleCommand(Element*, bool remove_only);
+ ApplyStyleCommand(Document&,
+ const EditingStyle*,
+ bool (*is_inline_element_to_remove)(const Element*),
+ InputEvent::InputType);
+
+ void DoApply(EditingState*) override;
+ InputEvent::InputType GetInputType() const override;
+
+ // style-removal helpers
+ bool IsStyledInlineElementToRemove(Element*) const;
+ bool ShouldApplyInlineStyleToRun(EditingStyle*,
+ Node* run_start,
+ Node* past_end_node);
+ void RemoveConflictingInlineStyleFromRun(EditingStyle*,
+ Member<Node>& run_start,
+ Member<Node>& run_end,
+ Node* past_end_node,
+ EditingState*);
+ bool RemoveInlineStyleFromElement(EditingStyle*,
+ HTMLElement*,
+ EditingState*,
+ InlineStyleRemovalMode = kRemoveIfNeeded,
+ EditingStyle* extracted_style = nullptr);
+ inline bool ShouldRemoveInlineStyleFromElement(EditingStyle* style,
+ HTMLElement* element) {
+ return RemoveInlineStyleFromElement(style, element, ASSERT_NO_EDITING_ABORT,
+ kRemoveNone);
+ }
+ void ReplaceWithSpanOrRemoveIfWithoutAttributes(HTMLElement*, EditingState*);
+ bool RemoveImplicitlyStyledElement(EditingStyle*,
+ HTMLElement*,
+ InlineStyleRemovalMode,
+ EditingStyle* extracted_style,
+ EditingState*);
+ bool RemoveCSSStyle(EditingStyle*,
+ HTMLElement*,
+ EditingState*,
+ InlineStyleRemovalMode = kRemoveIfNeeded,
+ EditingStyle* extracted_style = nullptr);
+ HTMLElement* HighestAncestorWithConflictingInlineStyle(EditingStyle*, Node*);
+ void ApplyInlineStyleToPushDown(Node*, EditingStyle*, EditingState*);
+ void PushDownInlineStyleAroundNode(EditingStyle*, Node*, EditingState*);
+ void RemoveInlineStyle(EditingStyle*,
+ const EphemeralRange& range,
+ EditingState*);
+ bool ElementFullySelected(const HTMLElement&,
+ const Position& start,
+ const Position& end) const;
+
+ // style-application helpers
+ void ApplyBlockStyle(EditingStyle*, EditingState*);
+ void ApplyRelativeFontStyleChange(EditingStyle*, EditingState*);
+ void ApplyInlineStyle(EditingStyle*, EditingState*);
+ void FixRangeAndApplyInlineStyle(EditingStyle*,
+ const Position& start,
+ const Position& end,
+ EditingState*);
+ void ApplyInlineStyleToNodeRange(EditingStyle*,
+ Node* start_node,
+ Node* past_end_node,
+ EditingState*);
+ void AddBlockStyle(const StyleChange&, HTMLElement*);
+ void AddInlineStyleIfNeeded(EditingStyle*,
+ Node* start,
+ Node* end,
+ EditingState*);
+ Position PositionToComputeInlineStyleChange(
+ Node*,
+ Member<HTMLSpanElement>& dummy_element,
+ EditingState*);
+ void ApplyInlineStyleChange(Node* start_node,
+ Node* end_node,
+ StyleChange&,
+ EAddStyledElement,
+ EditingState*);
+ void SplitTextAtStart(const Position& start, const Position& end);
+ void SplitTextAtEnd(const Position& start, const Position& end);
+ void SplitTextElementAtStart(const Position& start, const Position& end);
+ void SplitTextElementAtEnd(const Position& start, const Position& end);
+ bool ShouldSplitTextElement(Element*, EditingStyle*);
+ bool IsValidCaretPositionInTextNode(const Position&);
+ bool MergeStartWithPreviousIfIdentical(const Position& start,
+ const Position& end,
+ EditingState*);
+ bool MergeEndWithNextIfIdentical(const Position& start,
+ const Position& end,
+ EditingState*);
+ void CleanupUnstyledAppleStyleSpans(ContainerNode* dummy_span_ancestor,
+ EditingState*);
+
+ void SurroundNodeRangeWithElement(Node* start,
+ Node* end,
+ Element*,
+ EditingState*);
+ float ComputedFontSize(Node*);
+ void JoinChildTextNodes(ContainerNode*,
+ const Position& start,
+ const Position& end);
+
+ HTMLElement* SplitAncestorsWithUnicodeBidi(
+ Node*,
+ bool before,
+ WritingDirection allowed_direction);
+ void RemoveEmbeddingUpToEnclosingBlock(Node*,
+ HTMLElement* unsplit_ancestor,
+ EditingState*);
+
+ void UpdateStartEnd(const EphemeralRange&);
+ Position StartPosition();
+ Position EndPosition();
+
+ const Member<EditingStyle> style_;
+ const InputEvent::InputType input_type_;
+ const EPropertyLevel property_level_;
+ Position start_;
+ Position end_;
+ bool use_ending_selection_;
+ const Member<Element> styled_inline_element_;
+ const bool remove_only_;
+ IsInlineElementToRemoveFunction const is_inline_element_to_remove_function_;
+};
+
+enum ShouldStyleAttributeBeEmpty {
+ kAllowNonEmptyStyleAttribute,
+ kStyleAttributeShouldBeEmpty
+};
+bool IsEmptyFontTag(const Element*,
+ ShouldStyleAttributeBeEmpty = kStyleAttributeShouldBeEmpty);
+bool IsLegacyAppleHTMLSpanElement(const Node*);
+bool IsStyleSpanOrSpanWithOnlyStyleAttribute(const Element*);
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/apply_style_command_test.cc b/chromium/third_party/blink/renderer/core/editing/commands/apply_style_command_test.cc
new file mode 100644
index 00000000000..93e43d87f64
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/apply_style_command_test.cc
@@ -0,0 +1,98 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/apply_style_command.h"
+
+#include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/editing/editing_style.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+
+namespace blink {
+
+class ApplyStyleCommandTest : public EditingTestBase {};
+
+// This is a regression test for https://crbug.com/675727
+TEST_F(ApplyStyleCommandTest, RemoveRedundantBlocksWithStarEditableStyle) {
+ // The second <div> below is redundant from Blink's perspective (no siblings
+ // && no attributes) and will be removed by
+ // |DeleteSelectionCommand::removeRedundantBlocks()|.
+ SetBodyContent(
+ "<div><div>"
+ "<div></div>"
+ "<ul>"
+ "<li>"
+ "<div></div>"
+ "<input>"
+ "<style> * {-webkit-user-modify: read-write;}</style><div></div>"
+ "</li>"
+ "</ul></div></div>");
+
+ Element* li = GetDocument().QuerySelector("li");
+
+ LocalFrame* frame = GetDocument().GetFrame();
+ frame->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(li, PositionAnchorType::kBeforeAnchor))
+ .Build());
+
+ MutableCSSPropertyValueSet* style =
+ MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ style->SetProperty(CSSPropertyTextAlign, "center", /* important */ false,
+ SecureContextMode::kInsecureContext);
+ ApplyStyleCommand::Create(GetDocument(), EditingStyle::Create(style),
+ InputEvent::InputType::kFormatJustifyCenter,
+ ApplyStyleCommand::kForceBlockProperties)
+ ->Apply();
+ // Shouldn't crash.
+}
+
+// This is a regression test for https://crbug.com/761280
+TEST_F(ApplyStyleCommandTest, JustifyRightDetachesDestination) {
+ SetBodyContent(
+ "<style>"
+ ".CLASS1{visibility:visible;}"
+ "*:last-child{visibility:collapse;display:list-item;}"
+ "</style>"
+ "<input class=CLASS1>"
+ "<ruby>"
+ "<button class=CLASS1></button>"
+ "<button></button>"
+ "</ruby");
+ Element* body = GetDocument().body();
+ // The bug does't reproduce with a contenteditable <div> as container.
+ body->setAttribute(HTMLNames::contenteditableAttr, "true");
+ GetDocument().UpdateStyleAndLayout();
+ Selection().SelectAll();
+
+ MutableCSSPropertyValueSet* style =
+ MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ style->SetProperty(CSSPropertyTextAlign, "right", /* important */ false,
+ SecureContextMode::kInsecureContext);
+ ApplyStyleCommand::Create(GetDocument(), EditingStyle::Create(style),
+ InputEvent::InputType::kFormatJustifyCenter,
+ ApplyStyleCommand::kForceBlockProperties)
+ ->Apply();
+ // Shouldn't crash.
+}
+
+// This is a regression test for https://crbug.com/726992
+TEST_F(ApplyStyleCommandTest, FontSizeDeltaWithSpanElement) {
+ Selection().SetSelectionAndEndTyping(SetSelectionTextToBody(
+ "<div contenteditable>^<div></div>a<span></span>|</div>"));
+
+ MutableCSSPropertyValueSet* style =
+ MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ style->SetProperty(CSSPropertyWebkitFontSizeDelta, "3", /* important */ false,
+ GetDocument().GetSecureContextMode());
+ ApplyStyleCommand::Create(GetDocument(), EditingStyle::Create(style),
+ InputEvent::InputType::kNone)
+ ->Apply();
+ EXPECT_EQ("<div contenteditable><div></div><span>^a|</span></div>",
+ GetSelectionTextFromBody());
+}
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/break_blockquote_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/break_blockquote_command.cc
new file mode 100644
index 00000000000..9e534c0e31a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/break_blockquote_command.cc
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2005 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/break_blockquote_command.h"
+
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/commands/delete_selection_options.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/html/html_br_element.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/html/html_quote_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/layout/layout_list_item.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+namespace {
+
+bool IsFirstVisiblePositionInNode(const VisiblePosition& visible_position,
+ const ContainerNode* node) {
+ if (visible_position.IsNull())
+ return false;
+
+ if (!visible_position.DeepEquivalent().ComputeContainerNode()->IsDescendantOf(
+ node))
+ return false;
+
+ VisiblePosition previous = PreviousPositionOf(visible_position);
+ return previous.IsNull() ||
+ !previous.DeepEquivalent().AnchorNode()->IsDescendantOf(node);
+}
+
+bool IsLastVisiblePositionInNode(const VisiblePosition& visible_position,
+ const ContainerNode* node) {
+ if (visible_position.IsNull())
+ return false;
+
+ if (!visible_position.DeepEquivalent().ComputeContainerNode()->IsDescendantOf(
+ node))
+ return false;
+
+ VisiblePosition next = NextPositionOf(visible_position);
+ return next.IsNull() ||
+ !next.DeepEquivalent().AnchorNode()->IsDescendantOf(node);
+}
+
+} // namespace
+
+BreakBlockquoteCommand::BreakBlockquoteCommand(Document& document)
+ : CompositeEditCommand(document) {}
+
+static HTMLQuoteElement* TopBlockquoteOf(const Position& start) {
+ // This is a position equivalent to the caret. We use |downstream()| so that
+ // |position| will be in the first node that we need to move (there are a few
+ // exceptions to this, see |doApply|).
+ const Position& position = MostForwardCaretPosition(start);
+ return ToHTMLQuoteElement(
+ HighestEnclosingNodeOfType(position, IsMailHTMLBlockquoteElement));
+}
+
+void BreakBlockquoteCommand::DoApply(EditingState* editing_state) {
+ if (EndingSelection().IsNone())
+ return;
+
+ if (!TopBlockquoteOf(EndingVisibleSelection().Start()))
+ return;
+
+ // Delete the current selection.
+ if (EndingSelection().IsRange()) {
+ if (!DeleteSelection(editing_state, DeleteSelectionOptions::Builder()
+ .SetExpandForSpecialElements(true)
+ .SetSanitizeMarkup(true)
+ .Build()))
+ return;
+ }
+
+ // This is a scenario that should never happen, but we want to
+ // make sure we don't dereference a null pointer below.
+
+ DCHECK(!EndingSelection().IsNone());
+
+ if (EndingSelection().IsNone())
+ return;
+
+ const VisibleSelection& visible_selection = EndingVisibleSelection();
+ VisiblePosition visible_pos = visible_selection.VisibleStart();
+
+ // pos is a position equivalent to the caret. We use downstream() so that pos
+ // will be in the first node that we need to move (there are a few exceptions
+ // to this, see below).
+ Position pos = MostForwardCaretPosition(visible_selection.Start());
+
+ // Find the top-most blockquote from the start.
+ HTMLQuoteElement* const top_blockquote =
+ TopBlockquoteOf(visible_selection.Start());
+ if (!top_blockquote || !top_blockquote->parentNode())
+ return;
+
+ HTMLBRElement* break_element = HTMLBRElement::Create(GetDocument());
+
+ bool is_last_vis_pos_in_node =
+ IsLastVisiblePositionInNode(visible_pos, top_blockquote);
+
+ // If the position is at the beginning of the top quoted content, we don't
+ // need to break the quote. Instead, insert the break before the blockquote,
+ // unless the position is as the end of the the quoted content.
+ if (IsFirstVisiblePositionInNode(visible_pos, top_blockquote) &&
+ !is_last_vis_pos_in_node) {
+ InsertNodeBefore(break_element, top_blockquote, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::BeforeNode(*break_element))
+ .Build()));
+ RebalanceWhitespace();
+ return;
+ }
+
+ // Insert a break after the top blockquote.
+ InsertNodeAfter(break_element, top_blockquote, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // If we're inserting the break at the end of the quoted content, we don't
+ // need to break the quote.
+ if (is_last_vis_pos_in_node) {
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::BeforeNode(*break_element))
+ .Build()));
+ RebalanceWhitespace();
+ return;
+ }
+
+ // Don't move a line break just after the caret. Doing so would create an
+ // extra, empty paragraph in the new blockquote.
+ if (LineBreakExistsAtVisiblePosition(visible_pos)) {
+ pos = NextPositionOf(pos, PositionMoveType::kGraphemeCluster);
+ }
+
+ // Adjust the position so we don't split at the beginning of a quote.
+ while (IsFirstVisiblePositionInNode(CreateVisiblePosition(pos),
+ ToHTMLQuoteElement(EnclosingNodeOfType(
+ pos, IsMailHTMLBlockquoteElement)))) {
+ pos = PreviousPositionOf(pos, PositionMoveType::kGraphemeCluster);
+ }
+
+ // startNode is the first node that we need to move to the new blockquote.
+ Node* start_node = pos.AnchorNode();
+ DCHECK(start_node);
+
+ // Split at pos if in the middle of a text node.
+ if (start_node->IsTextNode()) {
+ Text* text_node = ToText(start_node);
+ int text_offset = pos.ComputeOffsetInContainerNode();
+ if ((unsigned)text_offset >= text_node->length()) {
+ start_node = NodeTraversal::Next(*start_node);
+ DCHECK(start_node);
+ } else if (text_offset > 0) {
+ SplitTextNode(text_node, text_offset);
+ }
+ } else if (pos.ComputeEditingOffset() > 0) {
+ Node* child_at_offset =
+ NodeTraversal::ChildAt(*start_node, pos.ComputeEditingOffset());
+ start_node =
+ child_at_offset ? child_at_offset : NodeTraversal::Next(*start_node);
+ DCHECK(start_node);
+ }
+
+ // If there's nothing inside topBlockquote to move, we're finished.
+ if (!start_node->IsDescendantOf(top_blockquote)) {
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(FirstPositionInOrBeforeNode(*start_node))
+ .Build()));
+ return;
+ }
+
+ // Build up list of ancestors in between the start node and the top
+ // blockquote.
+ HeapVector<Member<Element>> ancestors;
+ for (Element* node = start_node->parentElement();
+ node && node != top_blockquote; node = node->parentElement())
+ ancestors.push_back(node);
+
+ // Insert a clone of the top blockquote after the break.
+ Element* cloned_blockquote = top_blockquote->CloneWithoutChildren();
+ InsertNodeAfter(cloned_blockquote, break_element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ // Clone startNode's ancestors into the cloned blockquote.
+ // On exiting this loop, clonedAncestor is the lowest ancestor
+ // that was cloned (i.e. the clone of either ancestors.last()
+ // or clonedBlockquote if ancestors is empty).
+ Element* cloned_ancestor = cloned_blockquote;
+ for (size_t i = ancestors.size(); i != 0; --i) {
+ Element* cloned_child = ancestors[i - 1]->CloneWithoutChildren();
+ // Preserve list item numbering in cloned lists.
+ if (IsHTMLOListElement(*cloned_child)) {
+ Node* list_child_node = i > 1 ? ancestors[i - 2].Get() : start_node;
+ // The first child of the cloned list might not be a list item element,
+ // find the first one so that we know where to start numbering.
+ while (list_child_node && !IsHTMLLIElement(*list_child_node))
+ list_child_node = list_child_node->nextSibling();
+ if (IsListItem(list_child_node))
+ SetNodeAttribute(
+ cloned_child, startAttr,
+ AtomicString::Number(
+ ToLayoutListItem(list_child_node->GetLayoutObject())->Value()));
+ }
+
+ AppendNode(cloned_child, cloned_ancestor, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ cloned_ancestor = cloned_child;
+ }
+
+ MoveRemainingSiblingsToNewParent(start_node, nullptr, cloned_ancestor,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ if (!ancestors.IsEmpty()) {
+ // Split the tree up the ancestor chain until the topBlockquote
+ // Throughout this loop, clonedParent is the clone of ancestor's parent.
+ // This is so we can clone ancestor's siblings and place the clones
+ // into the clone corresponding to the ancestor's parent.
+ Element* ancestor = nullptr;
+ Element* cloned_parent = nullptr;
+ for (ancestor = ancestors.front(),
+ cloned_parent = cloned_ancestor->parentElement();
+ ancestor && ancestor != top_blockquote;
+ ancestor = ancestor->parentElement(),
+ cloned_parent = cloned_parent->parentElement()) {
+ MoveRemainingSiblingsToNewParent(ancestor->nextSibling(), nullptr,
+ cloned_parent, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ // If the startNode's original parent is now empty, remove it
+ Element* original_parent = ancestors.front().Get();
+ if (!original_parent->HasChildren()) {
+ RemoveNode(original_parent, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+
+ // Make sure the cloned block quote renders.
+ AddBlockPlaceholderIfNeeded(cloned_blockquote, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ // Put the selection right before the break.
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::BeforeNode(*break_element))
+ .Build()));
+ RebalanceWhitespace();
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/break_blockquote_command.h b/chromium/third_party/blink/renderer/core/editing/commands/break_blockquote_command.h
new file mode 100644
index 00000000000..583676737b7
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/break_blockquote_command.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_BREAK_BLOCKQUOTE_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_BREAK_BLOCKQUOTE_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+namespace blink {
+
+class BreakBlockquoteCommand final : public CompositeEditCommand {
+ public:
+ static BreakBlockquoteCommand* Create(Document& document) {
+ return new BreakBlockquoteCommand(document);
+ }
+
+ private:
+ explicit BreakBlockquoteCommand(Document&);
+ void DoApply(EditingState*) override;
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/clipboard_commands.cc b/chromium/third_party/blink/renderer/core/editing/commands/clipboard_commands.cc
new file mode 100644
index 00000000000..fab38800fac
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/clipboard_commands.cc
@@ -0,0 +1,488 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
+ * Copyright (C) 2009 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/clipboard_commands.h"
+
+#include "third_party/blink/renderer/core/clipboard/data_transfer_access_policy.h"
+#include "third_party/blink/renderer/core/clipboard/pasteboard.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/serializers/serialization.h"
+#include "third_party/blink/renderer/core/events/clipboard_event.h"
+#include "third_party/blink/renderer/core/events/text_event.h"
+#include "third_party/blink/renderer/core/frame/content_settings_client.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+#include "third_party/blink/renderer/core/html/html_image_element.h"
+#include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h"
+#include "third_party/blink/renderer/platform/paste_mode.h"
+
+namespace blink {
+
+bool ClipboardCommands::CanReadClipboard(LocalFrame& frame,
+ EditorCommandSource source) {
+ if (source == EditorCommandSource::kMenuOrKeyBinding)
+ return true;
+ Settings* const settings = frame.GetSettings();
+ const bool default_value = settings &&
+ settings->GetJavaScriptCanAccessClipboard() &&
+ settings->GetDOMPasteAllowed();
+ if (!frame.GetContentSettingsClient())
+ return default_value;
+ return frame.GetContentSettingsClient()->AllowReadFromClipboard(
+ default_value);
+}
+
+bool ClipboardCommands::CanWriteClipboard(LocalFrame& frame,
+ EditorCommandSource source) {
+ if (source == EditorCommandSource::kMenuOrKeyBinding)
+ return true;
+ Settings* const settings = frame.GetSettings();
+ const bool default_value =
+ (settings && settings->GetJavaScriptCanAccessClipboard()) ||
+ Frame::HasTransientUserActivation(&frame);
+ if (!frame.GetContentSettingsClient())
+ return default_value;
+ return frame.GetContentSettingsClient()->AllowWriteToClipboard(default_value);
+}
+
+bool ClipboardCommands::CanSmartReplaceWithPasteboard(LocalFrame& frame,
+ Pasteboard* pasteboard) {
+ return frame.GetEditor().SmartInsertDeleteEnabled() &&
+ pasteboard->CanSmartReplace();
+}
+
+Element* ClipboardCommands::FindEventTargetForClipboardEvent(
+ LocalFrame& frame,
+ EditorCommandSource source) {
+ // https://www.w3.org/TR/clipboard-apis/#fire-a-clipboard-event says:
+ // "Set target to be the element that contains the start of the selection in
+ // document order, or the body element if there is no selection or cursor."
+ // We treat hidden selections as "no selection or cursor".
+ if (source == EditorCommandSource::kMenuOrKeyBinding &&
+ frame.Selection().IsHidden())
+ return frame.Selection().GetDocument().body();
+
+ return FindEventTargetFrom(
+ frame, frame.Selection().ComputeVisibleSelectionInDOMTree());
+}
+
+// Returns true if Editor should continue with default processing.
+bool ClipboardCommands::DispatchClipboardEvent(LocalFrame& frame,
+ const AtomicString& event_type,
+ DataTransferAccessPolicy policy,
+ EditorCommandSource source,
+ PasteMode paste_mode) {
+ Element* const target = FindEventTargetForClipboardEvent(frame, source);
+ if (!target)
+ return true;
+
+ DataTransfer* const data_transfer =
+ DataTransfer::Create(DataTransfer::kCopyAndPaste, policy,
+ policy == DataTransferAccessPolicy::kWritable
+ ? DataObject::Create()
+ : DataObject::CreateFromPasteboard(paste_mode));
+
+ Event* const evt = ClipboardEvent::Create(event_type, data_transfer);
+ target->DispatchEvent(evt);
+ const bool no_default_processing = evt->defaultPrevented();
+ if (no_default_processing && policy == DataTransferAccessPolicy::kWritable) {
+ Pasteboard::GeneralPasteboard()->WriteDataObject(
+ data_transfer->GetDataObject());
+ }
+
+ // Invalidate clipboard here for security.
+ data_transfer->SetAccessPolicy(DataTransferAccessPolicy::kNumb);
+ return !no_default_processing;
+}
+
+bool ClipboardCommands::DispatchCopyOrCutEvent(LocalFrame& frame,
+ EditorCommandSource source,
+ const AtomicString& event_type) {
+ // TODO(editing-dev): The use of UpdateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (IsInPasswordField(
+ frame.Selection().ComputeVisibleSelectionInDOMTree().Start()))
+ return true;
+
+ return DispatchClipboardEvent(frame, event_type,
+ DataTransferAccessPolicy::kWritable, source,
+ PasteMode::kAllMimeTypes);
+}
+
+bool ClipboardCommands::DispatchPasteEvent(LocalFrame& frame,
+ PasteMode paste_mode,
+ EditorCommandSource source) {
+ return DispatchClipboardEvent(frame, EventTypeNames::paste,
+ DataTransferAccessPolicy::kReadable, source,
+ paste_mode);
+}
+
+// WinIE uses onbeforecut and onbeforepaste to enables the cut and paste menu
+// items. They also send onbeforecopy, apparently for symmetry, but it doesn't
+// affect the menu items. We need to use onbeforecopy as a real menu enabler
+// because we allow elements that are not normally selectable to implement
+// copy/paste (like divs, or a document body).
+
+bool ClipboardCommands::EnabledCopy(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source) {
+ if (!CanWriteClipboard(frame, source))
+ return false;
+ return !DispatchCopyOrCutEvent(frame, source, EventTypeNames::beforecopy) ||
+ frame.GetEditor().CanCopy();
+}
+
+bool ClipboardCommands::EnabledCut(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source) {
+ if (!CanWriteClipboard(frame, source))
+ return false;
+ if (source == EditorCommandSource::kMenuOrKeyBinding &&
+ !frame.Selection().SelectionHasFocus())
+ return false;
+ return !DispatchCopyOrCutEvent(frame, source, EventTypeNames::beforecut) ||
+ frame.GetEditor().CanCut();
+}
+
+bool ClipboardCommands::EnabledPaste(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source) {
+ if (!CanReadClipboard(frame, source))
+ return false;
+ if (source == EditorCommandSource::kMenuOrKeyBinding &&
+ !frame.Selection().SelectionHasFocus())
+ return false;
+ return frame.GetEditor().CanPaste();
+}
+
+static Pasteboard::SmartReplaceOption GetSmartReplaceOption(
+ const LocalFrame& frame) {
+ if (frame.GetEditor().SmartInsertDeleteEnabled() &&
+ frame.Selection().Granularity() == TextGranularity::kWord)
+ return Pasteboard::kCanSmartReplace;
+ return Pasteboard::kCannotSmartReplace;
+}
+
+void ClipboardCommands::WriteSelectionToPasteboard(LocalFrame& frame) {
+ const KURL& url = frame.GetDocument()->Url();
+ const String html = frame.Selection().SelectedHTMLForClipboard();
+ const String plain_text = frame.SelectedTextForClipboard();
+ Pasteboard::GeneralPasteboard()->WriteHTML(html, url, plain_text,
+ GetSmartReplaceOption(frame));
+}
+
+bool ClipboardCommands::PasteSupported(LocalFrame* frame) {
+ const Settings* const settings = frame->GetSettings();
+ const bool default_value = settings &&
+ settings->GetJavaScriptCanAccessClipboard() &&
+ settings->GetDOMPasteAllowed();
+ if (!frame->GetContentSettingsClient())
+ return default_value;
+ return frame->GetContentSettingsClient()->AllowReadFromClipboard(
+ default_value);
+}
+
+bool ClipboardCommands::ExecuteCopy(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ if (!DispatchCopyOrCutEvent(frame, source, EventTypeNames::copy))
+ return true;
+ if (!frame.GetEditor().CanCopy())
+ return true;
+
+ // Since copy is a read-only operation it succeeds anytime a selection
+ // is *visible*. In contrast to cut or paste, the selection does not
+ // need to be focused - being visible is enough.
+ if (source == EditorCommandSource::kMenuOrKeyBinding &&
+ frame.Selection().IsHidden())
+ return true;
+
+ // TODO(editing-dev): The use of UpdateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ // A 'copy' event handler might have dirtied the layout so we need to update
+ // before we obtain the selection.
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (EnclosingTextControl(
+ frame.Selection().ComputeVisibleSelectionInDOMTree().Start())) {
+ Pasteboard::GeneralPasteboard()->WritePlainText(
+ frame.SelectedTextForClipboard(), GetSmartReplaceOption(frame));
+ return true;
+ }
+ const Document* const document = frame.GetDocument();
+ if (HTMLImageElement* image_element =
+ ImageElementFromImageDocument(document)) {
+ WriteImageNodeToPasteboard(Pasteboard::GeneralPasteboard(), *image_element,
+ document->title());
+ return true;
+ }
+ WriteSelectionToPasteboard(frame);
+ return true;
+}
+
+bool ClipboardCommands::CanDeleteRange(const EphemeralRange& range) {
+ if (range.IsCollapsed())
+ return false;
+
+ const Node& start_container = *range.StartPosition().ComputeContainerNode();
+ const Node& end_container = *range.EndPosition().ComputeContainerNode();
+
+ return HasEditableStyle(start_container) && HasEditableStyle(end_container);
+}
+
+static DeleteMode ConvertSmartReplaceOptionToDeleteMode(
+ Pasteboard::SmartReplaceOption smart_replace_option) {
+ if (smart_replace_option == Pasteboard::kCanSmartReplace)
+ return DeleteMode::kSmart;
+ DCHECK_EQ(smart_replace_option, Pasteboard::kCannotSmartReplace);
+ return DeleteMode::kSimple;
+}
+
+bool ClipboardCommands::ExecuteCut(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ if (!DispatchCopyOrCutEvent(frame, source, EventTypeNames::cut))
+ return true;
+ if (!frame.GetEditor().CanCut())
+ return true;
+
+ // TODO(editing-dev): The use of UpdateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ // A 'cut' event handler might have dirtied the layout so we need to update
+ // before we obtain the selection.
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (source == EditorCommandSource::kMenuOrKeyBinding &&
+ !frame.Selection().SelectionHasFocus())
+ return true;
+
+ if (!CanDeleteRange(frame.GetEditor().SelectedRange()))
+ return true;
+ if (EnclosingTextControl(
+ frame.Selection().ComputeVisibleSelectionInDOMTree().Start())) {
+ const String plain_text = frame.SelectedTextForClipboard();
+ Pasteboard::GeneralPasteboard()->WritePlainText(
+ plain_text, GetSmartReplaceOption(frame));
+ } else {
+ WriteSelectionToPasteboard(frame);
+ }
+
+ if (source == EditorCommandSource::kMenuOrKeyBinding) {
+ if (DispatchBeforeInputDataTransfer(
+ FindEventTargetForClipboardEvent(frame, source),
+ InputEvent::InputType::kDeleteByCut,
+ nullptr) != DispatchEventResult::kNotCanceled)
+ return true;
+ // 'beforeinput' event handler may destroy target frame.
+ if (frame.GetDocument()->GetFrame() != frame)
+ return true;
+ }
+ frame.GetEditor().DeleteSelectionWithSmartDelete(
+ ConvertSmartReplaceOptionToDeleteMode(GetSmartReplaceOption(frame)),
+ InputEvent::InputType::kDeleteByCut);
+
+ return true;
+}
+
+void ClipboardCommands::PasteAsFragment(LocalFrame& frame,
+ DocumentFragment* pasting_fragment,
+ bool smart_replace,
+ bool match_style,
+ EditorCommandSource source) {
+ Element* const target = FindEventTargetForClipboardEvent(frame, source);
+ if (!target)
+ return;
+ target->DispatchEvent(TextEvent::CreateForFragmentPaste(
+ frame.DomWindow(), pasting_fragment, smart_replace, match_style));
+}
+
+void ClipboardCommands::PasteAsPlainTextWithPasteboard(
+ LocalFrame& frame,
+ Pasteboard* pasteboard,
+ EditorCommandSource source) {
+ Element* const target = FindEventTargetForClipboardEvent(frame, source);
+ if (!target)
+ return;
+ target->DispatchEvent(TextEvent::CreateForPlainTextPaste(
+ frame.DomWindow(), pasteboard->PlainText(),
+ CanSmartReplaceWithPasteboard(frame, pasteboard)));
+}
+
+ClipboardCommands::FragmentAndPlainText
+ClipboardCommands::GetFragmentFromClipboard(LocalFrame& frame,
+ Pasteboard* pasteboard) {
+ DocumentFragment* fragment = nullptr;
+ if (pasteboard->IsHTMLAvailable()) {
+ unsigned fragment_start = 0;
+ unsigned fragment_end = 0;
+ KURL url;
+ const String markup =
+ pasteboard->ReadHTML(url, fragment_start, fragment_end);
+ if (!markup.IsEmpty()) {
+ DCHECK(frame.GetDocument());
+ fragment = CreateFragmentFromMarkupWithContext(
+ *frame.GetDocument(), markup, fragment_start, fragment_end, url,
+ kDisallowScriptingAndPluginContent);
+ }
+ }
+ if (fragment)
+ return std::make_pair(fragment, false);
+
+ const String text = pasteboard->PlainText();
+ if (text.IsEmpty())
+ return std::make_pair(fragment, false);
+
+ // TODO(editing-dev): Use of UpdateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ // |SelectedRange| requires clean layout for visible selection
+ // normalization.
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+ fragment = CreateFragmentFromText(frame.GetEditor().SelectedRange(), text);
+ return std::make_pair(fragment, true);
+}
+
+void ClipboardCommands::PasteWithPasteboard(LocalFrame& frame,
+ Pasteboard* pasteboard,
+ EditorCommandSource source) {
+ const ClipboardCommands::FragmentAndPlainText fragment_and_plain_text =
+ GetFragmentFromClipboard(frame, pasteboard);
+
+ if (!fragment_and_plain_text.first)
+ return;
+
+ PasteAsFragment(frame, fragment_and_plain_text.first,
+ CanSmartReplaceWithPasteboard(frame, pasteboard),
+ fragment_and_plain_text.second, source);
+}
+
+void ClipboardCommands::Paste(LocalFrame& frame, EditorCommandSource source) {
+ DCHECK(frame.GetDocument());
+ if (!DispatchPasteEvent(frame, PasteMode::kAllMimeTypes, source))
+ return;
+ if (!frame.GetEditor().CanPaste())
+ return;
+
+ // TODO(editing-dev): The use of UpdateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ // A 'paste' event handler might have dirtied the layout so we need to update
+ // before we obtain the selection.
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (source == EditorCommandSource::kMenuOrKeyBinding &&
+ !frame.Selection().SelectionHasFocus())
+ return;
+
+ ResourceFetcher* const loader = frame.GetDocument()->Fetcher();
+ ResourceCacheValidationSuppressor validation_suppressor(loader);
+
+ const PasteMode paste_mode = frame.GetEditor().CanEditRichly()
+ ? PasteMode::kAllMimeTypes
+ : PasteMode::kPlainTextOnly;
+
+ if (source == EditorCommandSource::kMenuOrKeyBinding) {
+ DataTransfer* data_transfer = DataTransfer::Create(
+ DataTransfer::kCopyAndPaste, DataTransferAccessPolicy::kReadable,
+ DataObject::CreateFromPasteboard(paste_mode));
+
+ if (DispatchBeforeInputDataTransfer(
+ FindEventTargetForClipboardEvent(frame, source),
+ InputEvent::InputType::kInsertFromPaste,
+ data_transfer) != DispatchEventResult::kNotCanceled)
+ return;
+ // 'beforeinput' event handler may destroy target frame.
+ if (frame.GetDocument()->GetFrame() != frame)
+ return;
+ }
+
+ if (paste_mode == PasteMode::kAllMimeTypes) {
+ PasteWithPasteboard(frame, Pasteboard::GeneralPasteboard(), source);
+ return;
+ }
+ PasteAsPlainTextWithPasteboard(frame, Pasteboard::GeneralPasteboard(),
+ source);
+}
+
+bool ClipboardCommands::ExecutePaste(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ Paste(frame, source);
+ return true;
+}
+
+bool ClipboardCommands::ExecutePasteGlobalSelection(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ if (!frame.GetEditor().Behavior().SupportsGlobalSelection())
+ return false;
+ DCHECK_EQ(source, EditorCommandSource::kMenuOrKeyBinding);
+
+ const bool old_selection_mode =
+ Pasteboard::GeneralPasteboard()->IsSelectionMode();
+ Pasteboard::GeneralPasteboard()->SetSelectionMode(true);
+ Paste(frame, source);
+ Pasteboard::GeneralPasteboard()->SetSelectionMode(old_selection_mode);
+ return true;
+}
+
+bool ClipboardCommands::ExecutePasteAndMatchStyle(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ if (!DispatchPasteEvent(frame, PasteMode::kPlainTextOnly, source))
+ return false;
+ if (!frame.GetEditor().CanPaste())
+ return false;
+
+ // TODO(editing-dev): The use of UpdateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ // A 'paste' event handler might have dirtied the layout so we need to update
+ // before we obtain the selection.
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (source == EditorCommandSource::kMenuOrKeyBinding &&
+ !frame.Selection().SelectionHasFocus())
+ return false;
+
+ PasteAsPlainTextWithPasteboard(frame, Pasteboard::GeneralPasteboard(),
+ source);
+ return true;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/clipboard_commands.h b/chromium/third_party/blink/renderer/core/editing/commands/clipboard_commands.h
new file mode 100644
index 00000000000..582214b565f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/clipboard_commands.h
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
+ * Copyright (C) 2009 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_CLIPBOARD_COMMANDS_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_CLIPBOARD_COMMANDS_H_
+
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+
+namespace blink {
+
+class DocumentFragment;
+class Element;
+class Event;
+class LocalFrame;
+class Pasteboard;
+
+enum class DataTransferAccessPolicy;
+enum class EditorCommandSource;
+enum class PasteMode;
+
+// This class provides static functions about commands related to clipboard.
+class ClipboardCommands {
+ STATIC_ONLY(ClipboardCommands);
+
+ public:
+ static bool EnabledCopy(LocalFrame&, Event*, EditorCommandSource);
+ static bool EnabledCut(LocalFrame&, Event*, EditorCommandSource);
+ static bool EnabledPaste(LocalFrame&, Event*, EditorCommandSource);
+
+ // Returns |bool| value for Document#execCommand().
+ static bool ExecuteCopy(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteCut(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecutePaste(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecutePasteGlobalSelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecutePasteAndMatchStyle(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+
+ static bool PasteSupported(LocalFrame*);
+
+ static bool CanReadClipboard(LocalFrame&, EditorCommandSource);
+ static bool CanWriteClipboard(LocalFrame&, EditorCommandSource);
+
+ private:
+ static bool CanSmartReplaceWithPasteboard(LocalFrame&, Pasteboard*);
+ static bool CanDeleteRange(const EphemeralRange&);
+ static Element* FindEventTargetForClipboardEvent(LocalFrame&,
+ EditorCommandSource);
+
+ // Returns true if Editor should continue with default processing.
+ static bool DispatchClipboardEvent(LocalFrame&,
+ const AtomicString&,
+ DataTransferAccessPolicy,
+ EditorCommandSource,
+ PasteMode);
+ static bool DispatchCopyOrCutEvent(LocalFrame&,
+ EditorCommandSource,
+ const AtomicString&);
+ static bool DispatchPasteEvent(LocalFrame&, PasteMode, EditorCommandSource);
+
+ static void WriteSelectionToPasteboard(LocalFrame&);
+ static void Paste(LocalFrame&, EditorCommandSource);
+ static void PasteAsFragment(LocalFrame&,
+ DocumentFragment*,
+ bool smart_replace,
+ bool match_style,
+ EditorCommandSource);
+ static void PasteAsPlainTextWithPasteboard(LocalFrame&,
+ Pasteboard*,
+ EditorCommandSource);
+ static void PasteWithPasteboard(LocalFrame&,
+ Pasteboard*,
+ EditorCommandSource);
+
+ using FragmentAndPlainText = std::pair<DocumentFragment*, const bool>;
+ static FragmentAndPlainText GetFragmentFromClipboard(LocalFrame&,
+ Pasteboard*);
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/composite_edit_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/composite_edit_command.cc
new file mode 100644
index 00000000000..6298bb28bfa
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/composite_edit_command.cc
@@ -0,0 +1,2066 @@
+/*
+ * Copyright (C) 2005, 2006, 2007, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+#include <algorithm>
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/document_fragment.h"
+#include "third_party/blink/renderer/core/dom/element_traversal.h"
+#include "third_party/blink/renderer/core/dom/events/scoped_event_queue.h"
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/commands/append_node_command.h"
+#include "third_party/blink/renderer/core/editing/commands/apply_style_command.h"
+#include "third_party/blink/renderer/core/editing/commands/delete_from_text_node_command.h"
+#include "third_party/blink/renderer/core/editing/commands/delete_selection_command.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/commands/insert_into_text_node_command.h"
+#include "third_party/blink/renderer/core/editing/commands/insert_line_break_command.h"
+#include "third_party/blink/renderer/core/editing/commands/insert_node_before_command.h"
+#include "third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command.h"
+#include "third_party/blink/renderer/core/editing/commands/merge_identical_elements_command.h"
+#include "third_party/blink/renderer/core/editing/commands/remove_css_property_command.h"
+#include "third_party/blink/renderer/core/editing/commands/remove_node_command.h"
+#include "third_party/blink/renderer/core/editing/commands/remove_node_preserving_children_command.h"
+#include "third_party/blink/renderer/core/editing/commands/replace_node_with_span_command.h"
+#include "third_party/blink/renderer/core/editing/commands/replace_selection_command.h"
+#include "third_party/blink/renderer/core/editing/commands/set_character_data_command.h"
+#include "third_party/blink/renderer/core/editing/commands/set_node_attribute_command.h"
+#include "third_party/blink/renderer/core/editing/commands/split_element_command.h"
+#include "third_party/blink/renderer/core/editing/commands/split_text_node_command.h"
+#include "third_party/blink/renderer/core/editing/commands/split_text_node_containing_element_command.h"
+#include "third_party/blink/renderer/core/editing/commands/undo_stack.h"
+#include "third_party/blink/renderer/core/editing/commands/wrap_contents_in_dummy_span_command.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+#include "third_party/blink/renderer/core/editing/plain_text_range.h"
+#include "third_party/blink/renderer/core/editing/relocatable_position.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/serializers/serialization.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/html/html_br_element.h"
+#include "third_party/blink/renderer/core/html/html_div_element.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/html/html_li_element.h"
+#include "third_party/blink/renderer/core/html/html_quote_element.h"
+#include "third_party/blink/renderer/core/html/html_span_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/layout/layout_block.h"
+#include "third_party/blink/renderer/core/layout/layout_list_item.h"
+#include "third_party/blink/renderer/core/layout/layout_text.h"
+#include "third_party/blink/renderer/core/layout/line/inline_text_box.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+CompositeEditCommand::CompositeEditCommand(Document& document)
+ : EditCommand(document) {
+ const VisibleSelection& visible_selection =
+ document.GetFrame()
+ ->Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated();
+ SetStartingSelection(
+ SelectionForUndoStep::From(visible_selection.AsSelection()));
+ SetEndingSelection(starting_selection_);
+}
+
+CompositeEditCommand::~CompositeEditCommand() {
+ DCHECK(IsTopLevelCommand() || !undo_step_);
+}
+
+VisibleSelection CompositeEditCommand::EndingVisibleSelection() const {
+ // TODO(editing-dev): The use of
+ // |Document::UpdateStyleAndLayoutIgnorePendingStylesheets()|
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ return CreateVisibleSelection(ending_selection_);
+}
+
+bool CompositeEditCommand::Apply() {
+ DCHECK(!IsCommandGroupWrapper());
+ if (!IsRichlyEditablePosition(EndingVisibleSelection().Base())) {
+ switch (GetInputType()) {
+ case InputEvent::InputType::kInsertText:
+ case InputEvent::InputType::kInsertLineBreak:
+ case InputEvent::InputType::kInsertParagraph:
+ case InputEvent::InputType::kInsertFromPaste:
+ case InputEvent::InputType::kInsertFromDrop:
+ case InputEvent::InputType::kInsertFromYank:
+ case InputEvent::InputType::kInsertTranspose:
+ case InputEvent::InputType::kInsertReplacementText:
+ case InputEvent::InputType::kInsertCompositionText:
+ case InputEvent::InputType::kDeleteWordBackward:
+ case InputEvent::InputType::kDeleteWordForward:
+ case InputEvent::InputType::kDeleteSoftLineBackward:
+ case InputEvent::InputType::kDeleteSoftLineForward:
+ case InputEvent::InputType::kDeleteHardLineBackward:
+ case InputEvent::InputType::kDeleteHardLineForward:
+ case InputEvent::InputType::kDeleteContentBackward:
+ case InputEvent::InputType::kDeleteContentForward:
+ case InputEvent::InputType::kDeleteByCut:
+ case InputEvent::InputType::kDeleteByDrag:
+ case InputEvent::InputType::kNone:
+ break;
+ default:
+ NOTREACHED() << "Not supported input type on plain-text only element:"
+ << static_cast<int>(GetInputType());
+ return false;
+ }
+ }
+ EnsureUndoStep();
+
+ // Changes to the document may have been made since the last editing operation
+ // that require a layout, as in <rdar://problem/5658603>. Low level
+ // operations, like RemoveNodeCommand, don't require a layout because the high
+ // level operations that use them perform one if one is necessary (like for
+ // the creation of VisiblePositions).
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ LocalFrame* frame = GetDocument().GetFrame();
+ DCHECK(frame);
+ // directional is stored at the top level command, so that before and after
+ // executing command same directional will be there.
+ SetSelectionIsDirectional(frame->Selection().IsDirectional());
+ GetUndoStep()->SetSelectionIsDirectional(SelectionIsDirectional());
+
+ EditingState editing_state;
+ EventQueueScope event_queue_scope;
+ DoApply(&editing_state);
+
+ // Only need to call appliedEditing for top-level commands, and TypingCommands
+ // do it on their own (see TypingCommand::typingAddedToOpenCommand).
+ if (!IsTypingCommand())
+ AppliedEditing();
+ return !editing_state.IsAborted();
+}
+
+UndoStep* CompositeEditCommand::EnsureUndoStep() {
+ CompositeEditCommand* command = this;
+ while (command && command->Parent())
+ command = command->Parent();
+ if (!command->undo_step_) {
+ command->undo_step_ = UndoStep::Create(&GetDocument(), StartingSelection(),
+ EndingSelection(), GetInputType());
+ }
+ return command->undo_step_.Get();
+}
+
+bool CompositeEditCommand::PreservesTypingStyle() const {
+ return false;
+}
+
+bool CompositeEditCommand::IsTypingCommand() const {
+ return false;
+}
+
+bool CompositeEditCommand::IsCommandGroupWrapper() const {
+ return false;
+}
+
+bool CompositeEditCommand::IsDragAndDropCommand() const {
+ return false;
+}
+
+bool CompositeEditCommand::IsReplaceSelectionCommand() const {
+ return false;
+}
+
+//
+// sugary-sweet convenience functions to help create and apply edit commands in
+// composite commands
+//
+void CompositeEditCommand::ApplyCommandToComposite(
+ EditCommand* command,
+ EditingState* editing_state) {
+ command->SetParent(this);
+ command->SetSelectionIsDirectional(SelectionIsDirectional());
+ command->DoApply(editing_state);
+ if (editing_state->IsAborted()) {
+ command->SetParent(nullptr);
+ return;
+ }
+ if (command->IsSimpleEditCommand()) {
+ command->SetParent(nullptr);
+ EnsureUndoStep()->Append(ToSimpleEditCommand(command));
+ }
+ commands_.push_back(command);
+}
+
+void CompositeEditCommand::AppendCommandToUndoStep(
+ CompositeEditCommand* command) {
+ EnsureUndoStep()->Append(command->EnsureUndoStep());
+ command->undo_step_ = nullptr;
+ command->SetParent(this);
+ commands_.push_back(command);
+}
+
+void CompositeEditCommand::ApplyStyle(const EditingStyle* style,
+ EditingState* editing_state) {
+ ApplyCommandToComposite(
+ ApplyStyleCommand::Create(GetDocument(), style,
+ InputEvent::InputType::kNone),
+ editing_state);
+}
+
+void CompositeEditCommand::ApplyStyle(const EditingStyle* style,
+ const Position& start,
+ const Position& end,
+ EditingState* editing_state) {
+ ApplyCommandToComposite(
+ ApplyStyleCommand::Create(GetDocument(), style, start, end),
+ editing_state);
+}
+
+void CompositeEditCommand::ApplyStyledElement(Element* element,
+ EditingState* editing_state) {
+ ApplyCommandToComposite(ApplyStyleCommand::Create(element, false),
+ editing_state);
+}
+
+void CompositeEditCommand::RemoveStyledElement(Element* element,
+ EditingState* editing_state) {
+ ApplyCommandToComposite(ApplyStyleCommand::Create(element, true),
+ editing_state);
+}
+
+void CompositeEditCommand::InsertParagraphSeparator(
+ EditingState* editing_state,
+ bool use_default_paragraph_element,
+ bool paste_blockqutoe_into_unquoted_area) {
+ ApplyCommandToComposite(InsertParagraphSeparatorCommand::Create(
+ GetDocument(), use_default_paragraph_element,
+ paste_blockqutoe_into_unquoted_area),
+ editing_state);
+}
+
+bool CompositeEditCommand::IsRemovableBlock(const Node* node) {
+ DCHECK(node);
+ if (!IsHTMLDivElement(*node))
+ return false;
+
+ const HTMLDivElement& element = ToHTMLDivElement(*node);
+ ContainerNode* parent_node = element.parentNode();
+ if (parent_node && parent_node->firstChild() != parent_node->lastChild())
+ return false;
+
+ if (!element.hasAttributes())
+ return true;
+
+ return false;
+}
+
+void CompositeEditCommand::InsertNodeBefore(
+ Node* insert_child,
+ Node* ref_child,
+ EditingState* editing_state,
+ ShouldAssumeContentIsAlwaysEditable
+ should_assume_content_is_always_editable) {
+ DCHECK_NE(GetDocument().body(), ref_child);
+ ABORT_EDITING_COMMAND_IF(!ref_child->parentNode());
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ ABORT_EDITING_COMMAND_IF(!HasEditableStyle(*ref_child->parentNode()) &&
+ ref_child->parentNode()->InActiveDocument());
+ ApplyCommandToComposite(
+ InsertNodeBeforeCommand::Create(insert_child, ref_child,
+ should_assume_content_is_always_editable),
+ editing_state);
+}
+
+void CompositeEditCommand::InsertNodeAfter(Node* insert_child,
+ Node* ref_child,
+ EditingState* editing_state) {
+ DCHECK(insert_child);
+ DCHECK(ref_child);
+ DCHECK_NE(GetDocument().body(), ref_child);
+ ContainerNode* parent = ref_child->parentNode();
+ DCHECK(parent);
+ DCHECK(!parent->IsShadowRoot()) << parent;
+ if (parent->lastChild() == ref_child) {
+ AppendNode(insert_child, parent, editing_state);
+ } else {
+ DCHECK(ref_child->nextSibling()) << ref_child;
+ InsertNodeBefore(insert_child, ref_child->nextSibling(), editing_state);
+ }
+}
+
+void CompositeEditCommand::InsertNodeAt(Node* insert_child,
+ const Position& editing_position,
+ EditingState* editing_state) {
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ ABORT_EDITING_COMMAND_IF(!IsEditablePosition(editing_position));
+ // For editing positions like [table, 0], insert before the table,
+ // likewise for replaced elements, brs, etc.
+ Position p = editing_position.ParentAnchoredEquivalent();
+ Node* ref_child = p.AnchorNode();
+ int offset = p.OffsetInContainerNode();
+
+ if (CanHaveChildrenForEditing(ref_child)) {
+ Node* child = ref_child->firstChild();
+ for (int i = 0; child && i < offset; i++)
+ child = child->nextSibling();
+ if (child)
+ InsertNodeBefore(insert_child, child, editing_state);
+ else
+ AppendNode(insert_child, ToContainerNode(ref_child), editing_state);
+ } else if (CaretMinOffset(ref_child) >= offset) {
+ InsertNodeBefore(insert_child, ref_child, editing_state);
+ } else if (ref_child->IsTextNode() && CaretMaxOffset(ref_child) > offset) {
+ SplitTextNode(ToText(ref_child), offset);
+
+ // Mutation events (bug 22634) from the text node insertion may have removed
+ // the refChild
+ if (!ref_child->isConnected())
+ return;
+ InsertNodeBefore(insert_child, ref_child, editing_state);
+ } else {
+ InsertNodeAfter(insert_child, ref_child, editing_state);
+ }
+}
+
+void CompositeEditCommand::AppendNode(Node* node,
+ ContainerNode* parent,
+ EditingState* editing_state) {
+ // When cloneParagraphUnderNewElement() clones the fallback content
+ // of an OBJECT element, the ASSERT below may fire since the return
+ // value of canHaveChildrenForEditing is not reliable until the layout
+ // object of the OBJECT is created. Hence we ignore this check for OBJECTs.
+ // TODO(yosin): We should move following |ABORT_EDITING_COMMAND_IF|s to
+ // |AppendNodeCommand|.
+ // TODO(yosin): We should get rid of |canHaveChildrenForEditing()|, since
+ // |cloneParagraphUnderNewElement()| attempt to clone non-well-formed HTML,
+ // produced by JavaScript.
+ ABORT_EDITING_COMMAND_IF(
+ !CanHaveChildrenForEditing(parent) &&
+ !(parent->IsElementNode() && ToElement(parent)->TagQName() == objectTag));
+ ABORT_EDITING_COMMAND_IF(!HasEditableStyle(*parent) &&
+ parent->InActiveDocument());
+ ApplyCommandToComposite(AppendNodeCommand::Create(parent, node),
+ editing_state);
+}
+
+void CompositeEditCommand::RemoveChildrenInRange(Node* node,
+ unsigned from,
+ unsigned to,
+ EditingState* editing_state) {
+ HeapVector<Member<Node>> children;
+ Node* child = NodeTraversal::ChildAt(*node, from);
+ for (unsigned i = from; child && i < to; i++, child = child->nextSibling())
+ children.push_back(child);
+
+ size_t size = children.size();
+ for (size_t i = 0; i < size; ++i) {
+ RemoveNode(children[i].Release(), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+}
+
+void CompositeEditCommand::RemoveNode(
+ Node* node,
+ EditingState* editing_state,
+ ShouldAssumeContentIsAlwaysEditable
+ should_assume_content_is_always_editable) {
+ if (!node || !node->NonShadowBoundaryParentNode())
+ return;
+ ABORT_EDITING_COMMAND_IF(!node->GetDocument().GetFrame());
+ ApplyCommandToComposite(
+ RemoveNodeCommand::Create(node, should_assume_content_is_always_editable),
+ editing_state);
+}
+
+void CompositeEditCommand::RemoveNodePreservingChildren(
+ Node* node,
+ EditingState* editing_state,
+ ShouldAssumeContentIsAlwaysEditable
+ should_assume_content_is_always_editable) {
+ ABORT_EDITING_COMMAND_IF(!node->GetDocument().GetFrame());
+ ApplyCommandToComposite(RemoveNodePreservingChildrenCommand::Create(
+ node, should_assume_content_is_always_editable),
+ editing_state);
+}
+
+void CompositeEditCommand::RemoveNodeAndPruneAncestors(
+ Node* node,
+ EditingState* editing_state,
+ Node* exclude_node) {
+ DCHECK_NE(node, exclude_node);
+ ContainerNode* parent = node->parentNode();
+ RemoveNode(node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ Prune(parent, editing_state, exclude_node);
+}
+
+void CompositeEditCommand::MoveRemainingSiblingsToNewParent(
+ Node* node,
+ Node* past_last_node_to_move,
+ Element* new_parent,
+ EditingState* editing_state) {
+ NodeVector nodes_to_remove;
+
+ for (; node && node != past_last_node_to_move; node = node->nextSibling())
+ nodes_to_remove.push_back(node);
+
+ for (unsigned i = 0; i < nodes_to_remove.size(); i++) {
+ RemoveNode(nodes_to_remove[i], editing_state);
+ if (editing_state->IsAborted())
+ return;
+ AppendNode(nodes_to_remove[i], new_parent, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+}
+
+void CompositeEditCommand::UpdatePositionForNodeRemovalPreservingChildren(
+ Position& position,
+ Node& node) {
+ int offset =
+ position.IsOffsetInAnchor() ? position.OffsetInContainerNode() : 0;
+ position = ComputePositionForNodeRemoval(position, node);
+ if (offset == 0)
+ return;
+ position = Position::CreateWithoutValidationDeprecated(
+ *position.ComputeContainerNode(), offset);
+}
+
+HTMLSpanElement*
+CompositeEditCommand::ReplaceElementWithSpanPreservingChildrenAndAttributes(
+ HTMLElement* node) {
+ // It would also be possible to implement all of ReplaceNodeWithSpanCommand
+ // as a series of existing smaller edit commands. Someone who wanted to
+ // reduce the number of edit commands could do so here.
+ ReplaceNodeWithSpanCommand* command =
+ ReplaceNodeWithSpanCommand::Create(node);
+ // ReplaceNodeWithSpanCommand is never aborted.
+ ApplyCommandToComposite(command, ASSERT_NO_EDITING_ABORT);
+ // Returning a raw pointer here is OK because the command is retained by
+ // applyCommandToComposite (thus retaining the span), and the span is also
+ // in the DOM tree, and thus alive whie it has a parent.
+ DCHECK(command->SpanElement()->isConnected()) << command->SpanElement();
+ return command->SpanElement();
+}
+
+void CompositeEditCommand::Prune(Node* node,
+ EditingState* editing_state,
+ Node* exclude_node) {
+ if (Node* highest_node_to_remove =
+ HighestNodeToRemoveInPruning(node, exclude_node))
+ RemoveNode(highest_node_to_remove, editing_state);
+}
+
+void CompositeEditCommand::SplitTextNode(Text* node, unsigned offset) {
+ // SplitTextNodeCommand is never aborted.
+ ApplyCommandToComposite(SplitTextNodeCommand::Create(node, offset),
+ ASSERT_NO_EDITING_ABORT);
+}
+
+void CompositeEditCommand::SplitElement(Element* element, Node* at_child) {
+ // SplitElementCommand is never aborted.
+ ApplyCommandToComposite(SplitElementCommand::Create(element, at_child),
+ ASSERT_NO_EDITING_ABORT);
+}
+
+void CompositeEditCommand::MergeIdenticalElements(Element* first,
+ Element* second,
+ EditingState* editing_state) {
+ DCHECK(!first->IsDescendantOf(second)) << first << " " << second;
+ DCHECK_NE(second, first);
+ if (first->nextSibling() != second) {
+ RemoveNode(second, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ InsertNodeAfter(second, first, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ ApplyCommandToComposite(MergeIdenticalElementsCommand::Create(first, second),
+ editing_state);
+}
+
+void CompositeEditCommand::WrapContentsInDummySpan(Element* element) {
+ // WrapContentsInDummySpanCommand is never aborted.
+ ApplyCommandToComposite(WrapContentsInDummySpanCommand::Create(element),
+ ASSERT_NO_EDITING_ABORT);
+}
+
+void CompositeEditCommand::SplitTextNodeContainingElement(Text* text,
+ unsigned offset) {
+ // SplitTextNodeContainingElementCommand is never aborted.
+ ApplyCommandToComposite(
+ SplitTextNodeContainingElementCommand::Create(text, offset),
+ ASSERT_NO_EDITING_ABORT);
+}
+
+void CompositeEditCommand::InsertTextIntoNode(Text* node,
+ unsigned offset,
+ const String& text) {
+ // InsertIntoTextNodeCommand is never aborted.
+ if (!text.IsEmpty())
+ ApplyCommandToComposite(
+ InsertIntoTextNodeCommand::Create(node, offset, text),
+ ASSERT_NO_EDITING_ABORT);
+}
+
+void CompositeEditCommand::DeleteTextFromNode(Text* node,
+ unsigned offset,
+ unsigned count) {
+ // DeleteFromTextNodeCommand is never aborted.
+ ApplyCommandToComposite(
+ DeleteFromTextNodeCommand::Create(node, offset, count),
+ ASSERT_NO_EDITING_ABORT);
+}
+
+void CompositeEditCommand::ReplaceTextInNode(Text* node,
+ unsigned offset,
+ unsigned count,
+ const String& replacement_text) {
+ // SetCharacterDataCommand is never aborted.
+ ApplyCommandToComposite(
+ SetCharacterDataCommand::Create(node, offset, count, replacement_text),
+ ASSERT_NO_EDITING_ABORT);
+}
+
+Position CompositeEditCommand::ReplaceSelectedTextInNode(const String& text) {
+ const Position& start = EndingSelection().Start();
+ const Position& end = EndingSelection().End();
+ if (start.ComputeContainerNode() != end.ComputeContainerNode() ||
+ !start.ComputeContainerNode()->IsTextNode() ||
+ IsTabHTMLSpanElementTextNode(start.ComputeContainerNode()))
+ return Position();
+
+ Text* text_node = ToText(start.ComputeContainerNode());
+ ReplaceTextInNode(text_node, start.OffsetInContainerNode(),
+ end.OffsetInContainerNode() - start.OffsetInContainerNode(),
+ text);
+
+ return Position(text_node, start.OffsetInContainerNode() + text.length());
+}
+
+Position CompositeEditCommand::PositionOutsideTabSpan(const Position& pos) {
+ if (!IsTabHTMLSpanElementTextNode(pos.AnchorNode()))
+ return pos;
+
+ switch (pos.AnchorType()) {
+ case PositionAnchorType::kBeforeChildren:
+ case PositionAnchorType::kAfterChildren:
+ NOTREACHED();
+ return pos;
+ case PositionAnchorType::kOffsetInAnchor:
+ break;
+ case PositionAnchorType::kBeforeAnchor:
+ return Position::InParentBeforeNode(*pos.AnchorNode());
+ case PositionAnchorType::kAfterAnchor:
+ return Position::InParentAfterNode(*pos.AnchorNode());
+ }
+
+ HTMLSpanElement* tab_span = TabSpanElement(pos.ComputeContainerNode());
+ DCHECK(tab_span);
+
+ // TODO(editing-dev): Hoist this UpdateStyleAndLayoutIgnorePendingStylesheets
+ // to the callers. See crbug.com/590369 for details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (pos.OffsetInContainerNode() <= CaretMinOffset(pos.ComputeContainerNode()))
+ return Position::InParentBeforeNode(*tab_span);
+
+ if (pos.OffsetInContainerNode() >= CaretMaxOffset(pos.ComputeContainerNode()))
+ return Position::InParentAfterNode(*tab_span);
+
+ SplitTextNodeContainingElement(ToText(pos.ComputeContainerNode()),
+ pos.OffsetInContainerNode());
+ return Position::InParentBeforeNode(*tab_span);
+}
+
+void CompositeEditCommand::InsertNodeAtTabSpanPosition(
+ Node* node,
+ const Position& pos,
+ EditingState* editing_state) {
+ // insert node before, after, or at split of tab span
+ InsertNodeAt(node, PositionOutsideTabSpan(pos), editing_state);
+}
+
+bool CompositeEditCommand::DeleteSelection(
+ EditingState* editing_state,
+ const DeleteSelectionOptions& options) {
+ if (!EndingSelection().IsRange())
+ return true;
+
+ ApplyCommandToComposite(
+ DeleteSelectionCommand::Create(GetDocument(), options), editing_state);
+ if (editing_state->IsAborted())
+ return false;
+
+ if (!EndingSelection().IsValidFor(GetDocument())) {
+ editing_state->Abort();
+ return false;
+ }
+ return true;
+}
+
+void CompositeEditCommand::RemoveCSSProperty(Element* element,
+ CSSPropertyID property) {
+ // RemoveCSSPropertyCommand is never aborted.
+ ApplyCommandToComposite(
+ RemoveCSSPropertyCommand::Create(GetDocument(), element, property),
+ ASSERT_NO_EDITING_ABORT);
+}
+
+void CompositeEditCommand::RemoveElementAttribute(
+ Element* element,
+ const QualifiedName& attribute) {
+ SetNodeAttribute(element, attribute, AtomicString());
+}
+
+void CompositeEditCommand::SetNodeAttribute(Element* element,
+ const QualifiedName& attribute,
+ const AtomicString& value) {
+ // SetNodeAttributeCommand is never aborted.
+ ApplyCommandToComposite(
+ SetNodeAttributeCommand::Create(element, attribute, value),
+ ASSERT_NO_EDITING_ABORT);
+}
+
+bool CompositeEditCommand::CanRebalance(const Position& position) const {
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets()
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ Node* node = position.ComputeContainerNode();
+ if (!position.IsOffsetInAnchor() || !node || !node->IsTextNode() ||
+ !HasRichlyEditableStyle(*node))
+ return false;
+
+ Text* text_node = ToText(node);
+ if (text_node->length() == 0)
+ return false;
+
+ LayoutText* layout_text = text_node->GetLayoutObject();
+ if (layout_text && !layout_text->Style()->CollapseWhiteSpace())
+ return false;
+
+ return true;
+}
+
+// FIXME: Doesn't go into text nodes that contribute adjacent text (siblings,
+// cousins, etc).
+void CompositeEditCommand::RebalanceWhitespaceAt(const Position& position) {
+ Node* node = position.ComputeContainerNode();
+ if (!CanRebalance(position))
+ return;
+
+ // If the rebalance is for the single offset, and neither text[offset] nor
+ // text[offset - 1] are some form of whitespace, do nothing.
+ int offset = position.ComputeOffsetInContainerNode();
+ String text = ToText(node)->data();
+ if (!IsWhitespace(text[offset])) {
+ offset--;
+ if (offset < 0 || !IsWhitespace(text[offset]))
+ return;
+ }
+
+ RebalanceWhitespaceOnTextSubstring(ToText(node),
+ position.OffsetInContainerNode(),
+ position.OffsetInContainerNode());
+}
+
+void CompositeEditCommand::RebalanceWhitespaceOnTextSubstring(Text* text_node,
+ int start_offset,
+ int end_offset) {
+ String text = text_node->data();
+ DCHECK(!text.IsEmpty());
+
+ // Set upstream and downstream to define the extent of the whitespace
+ // surrounding text[offset].
+ int upstream = start_offset;
+ while (upstream > 0 && IsWhitespace(text[upstream - 1]))
+ upstream--;
+
+ int downstream = end_offset;
+ while ((unsigned)downstream < text.length() && IsWhitespace(text[downstream]))
+ downstream++;
+
+ int length = downstream - upstream;
+ if (!length)
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ VisiblePosition visible_upstream_pos =
+ CreateVisiblePosition(Position(text_node, upstream));
+ VisiblePosition visible_downstream_pos =
+ CreateVisiblePosition(Position(text_node, downstream));
+
+ String string = text.Substring(upstream, length);
+ // FIXME: Because of the problem mentioned at the top of this function, we
+ // must also use nbsps at the start/end of the string because this function
+ // doesn't get all surrounding whitespace, just the whitespace in the
+ // current text node. However, if the next sibling node is a text node
+ // (not empty, see http://crbug.com/632300), we should use a plain space.
+ // See http://crbug.com/310149
+ const bool next_sibling_is_text_node =
+ text_node->nextSibling() && text_node->nextSibling()->IsTextNode() &&
+ ToText(text_node->nextSibling())->data().length() &&
+ !IsWhitespace(ToText(text_node->nextSibling())->data()[0]);
+ const bool should_emit_nbs_pbefore_end =
+ (IsEndOfParagraph(visible_downstream_pos) ||
+ (unsigned)downstream == text.length()) &&
+ !next_sibling_is_text_node;
+ String rebalanced_string = StringWithRebalancedWhitespace(
+ string, IsStartOfParagraph(visible_upstream_pos) || !upstream,
+ should_emit_nbs_pbefore_end);
+
+ if (string != rebalanced_string)
+ ReplaceTextInNode(text_node, upstream, length, rebalanced_string);
+}
+
+void CompositeEditCommand::PrepareWhitespaceAtPositionForSplit(
+ Position& position) {
+ if (!IsRichlyEditablePosition(position))
+ return;
+ Node* node = position.AnchorNode();
+ if (!node || !node->IsTextNode())
+ return;
+ Text* text_node = ToText(node);
+
+ if (text_node->length() == 0)
+ return;
+ LayoutText* layout_text = text_node->GetLayoutObject();
+ if (layout_text && !layout_text->Style()->CollapseWhiteSpace())
+ return;
+
+ // Delete collapsed whitespace so that inserting nbsps doesn't uncollapse it.
+ Position upstream_pos = MostBackwardCaretPosition(position);
+ DeleteInsignificantText(upstream_pos, MostForwardCaretPosition(position));
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ position = MostForwardCaretPosition(upstream_pos);
+ VisiblePosition visible_pos = CreateVisiblePosition(position);
+ VisiblePosition previous_visible_pos = PreviousPositionOf(visible_pos);
+ ReplaceCollapsibleWhitespaceWithNonBreakingSpaceIfNeeded(
+ previous_visible_pos);
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ ReplaceCollapsibleWhitespaceWithNonBreakingSpaceIfNeeded(
+ CreateVisiblePosition(position));
+}
+
+void CompositeEditCommand::
+ ReplaceCollapsibleWhitespaceWithNonBreakingSpaceIfNeeded(
+ const VisiblePosition& visible_position) {
+ if (!IsCollapsibleWhitespace(CharacterAfter(visible_position)))
+ return;
+ Position pos = MostForwardCaretPosition(visible_position.DeepEquivalent());
+ if (!pos.ComputeContainerNode() || !pos.ComputeContainerNode()->IsTextNode())
+ return;
+ ReplaceTextInNode(ToText(pos.ComputeContainerNode()),
+ pos.OffsetInContainerNode(), 1, NonBreakingSpaceString());
+}
+
+void CompositeEditCommand::RebalanceWhitespace() {
+ VisibleSelection selection = EndingVisibleSelection();
+ if (selection.IsNone())
+ return;
+
+ RebalanceWhitespaceAt(selection.Start());
+ if (selection.IsRange())
+ RebalanceWhitespaceAt(selection.End());
+}
+
+void CompositeEditCommand::DeleteInsignificantText(Text* text_node,
+ unsigned start,
+ unsigned end) {
+ if (!text_node || start >= end)
+ return;
+
+ GetDocument().UpdateStyleAndLayout();
+
+ LayoutText* text_layout_object = text_node->GetLayoutObject();
+ if (!text_layout_object)
+ return;
+
+ Vector<InlineTextBox*> sorted_text_boxes;
+ size_t sorted_text_boxes_position = 0;
+
+ for (InlineTextBox* text_box : text_layout_object->TextBoxes())
+ sorted_text_boxes.push_back(text_box);
+
+ // If there is mixed directionality text, the boxes can be out of order,
+ // (like Arabic with embedded LTR), so sort them first.
+ if (text_layout_object->ContainsReversedText())
+ std::sort(sorted_text_boxes.begin(), sorted_text_boxes.end(),
+ InlineTextBox::CompareByStart);
+ InlineTextBox* box = sorted_text_boxes.IsEmpty()
+ ? 0
+ : sorted_text_boxes[sorted_text_boxes_position];
+
+ if (!box) {
+ // whole text node is empty
+ // Removing a Text node won't dispatch synchronous events.
+ RemoveNode(text_node, ASSERT_NO_EDITING_ABORT);
+ return;
+ }
+
+ unsigned length = text_node->length();
+ if (start >= length || end > length)
+ return;
+
+ unsigned removed = 0;
+ InlineTextBox* prev_box = nullptr;
+ String str;
+
+ // This loop structure works to process all gaps preceding a box,
+ // and also will look at the gap after the last box.
+ while (prev_box || box) {
+ unsigned gap_start = prev_box ? prev_box->Start() + prev_box->Len() : 0;
+ if (end < gap_start) {
+ // No more chance for any intersections
+ break;
+ }
+
+ unsigned gap_end = box ? box->Start() : length;
+ bool indices_intersect = start <= gap_end && end >= gap_start;
+ int gap_len = gap_end - gap_start;
+ if (indices_intersect && gap_len > 0) {
+ gap_start = std::max(gap_start, start);
+ if (str.IsNull())
+ str = text_node->data().Substring(start, end - start);
+ // remove text in the gap
+ str.Remove(gap_start - start - removed, gap_len);
+ removed += gap_len;
+ }
+
+ prev_box = box;
+ if (box) {
+ if (++sorted_text_boxes_position < sorted_text_boxes.size())
+ box = sorted_text_boxes[sorted_text_boxes_position];
+ else
+ box = nullptr;
+ }
+ }
+
+ if (!str.IsNull()) {
+ // Replace the text between start and end with our pruned version.
+ if (!str.IsEmpty()) {
+ ReplaceTextInNode(text_node, start, end - start, str);
+ } else {
+ // Assert that we are not going to delete all of the text in the node.
+ // If we were, that should have been done above with the call to
+ // removeNode and return.
+ DCHECK(start > 0 || end - start < text_node->length());
+ DeleteTextFromNode(text_node, start, end - start);
+ }
+ }
+}
+
+void CompositeEditCommand::DeleteInsignificantText(const Position& start,
+ const Position& end) {
+ if (start.IsNull() || end.IsNull())
+ return;
+
+ if (ComparePositions(start, end) >= 0)
+ return;
+
+ HeapVector<Member<Text>> nodes;
+ for (Node& node : NodeTraversal::StartsAt(*start.AnchorNode())) {
+ if (node.IsTextNode())
+ nodes.push_back(ToText(&node));
+ if (&node == end.AnchorNode())
+ break;
+ }
+
+ for (const auto& node : nodes) {
+ Text* text_node = node;
+ int start_offset = text_node == start.AnchorNode()
+ ? start.ComputeOffsetInContainerNode()
+ : 0;
+ int end_offset = text_node == end.AnchorNode()
+ ? end.ComputeOffsetInContainerNode()
+ : static_cast<int>(text_node->length());
+ DeleteInsignificantText(text_node, start_offset, end_offset);
+ }
+}
+
+void CompositeEditCommand::DeleteInsignificantTextDownstream(
+ const Position& pos) {
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+ Position end = MostForwardCaretPosition(
+ NextPositionOf(CreateVisiblePosition(pos)).DeepEquivalent());
+ DeleteInsignificantText(pos, end);
+}
+
+HTMLBRElement* CompositeEditCommand::AppendBlockPlaceholder(
+ Element* container,
+ EditingState* editing_state) {
+ if (!container)
+ return nullptr;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Should assert isLayoutBlockFlow || isInlineFlow when deletion improves. See
+ // 4244964.
+ DCHECK(container->GetLayoutObject()) << container;
+
+ HTMLBRElement* placeholder = HTMLBRElement::Create(GetDocument());
+ AppendNode(placeholder, container, editing_state);
+ if (editing_state->IsAborted())
+ return nullptr;
+ return placeholder;
+}
+
+HTMLBRElement* CompositeEditCommand::InsertBlockPlaceholder(
+ const Position& pos,
+ EditingState* editing_state) {
+ if (pos.IsNull())
+ return nullptr;
+
+ // Should assert isLayoutBlockFlow || isInlineFlow when deletion improves. See
+ // 4244964.
+ DCHECK(pos.AnchorNode()->GetLayoutObject()) << pos;
+
+ HTMLBRElement* placeholder = HTMLBRElement::Create(GetDocument());
+ InsertNodeAt(placeholder, pos, editing_state);
+ if (editing_state->IsAborted())
+ return nullptr;
+ return placeholder;
+}
+
+HTMLBRElement* CompositeEditCommand::AddBlockPlaceholderIfNeeded(
+ Element* container,
+ EditingState* editing_state) {
+ if (!container)
+ return nullptr;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ LayoutObject* layout_object = container->GetLayoutObject();
+ if (!layout_object || !layout_object->IsLayoutBlockFlow())
+ return nullptr;
+
+ // append the placeholder to make sure it follows
+ // any unrendered blocks
+ LayoutBlockFlow* block = ToLayoutBlockFlow(layout_object);
+ if (block->Size().Height() == 0 ||
+ (block->IsListItem() && ToLayoutListItem(block)->IsEmpty()))
+ return AppendBlockPlaceholder(container, editing_state);
+
+ return nullptr;
+}
+
+// Assumes that the position is at a placeholder and does the removal without
+// much checking.
+void CompositeEditCommand::RemovePlaceholderAt(const Position& p) {
+ DCHECK(LineBreakExistsAtPosition(p)) << p;
+
+ // We are certain that the position is at a line break, but it may be a br or
+ // a preserved newline.
+ if (IsHTMLBRElement(*p.AnchorNode())) {
+ // Removing a BR element won't dispatch synchronous events.
+ RemoveNode(p.AnchorNode(), ASSERT_NO_EDITING_ABORT);
+ return;
+ }
+
+ DeleteTextFromNode(ToText(p.AnchorNode()), p.OffsetInContainerNode(), 1);
+}
+
+HTMLElement* CompositeEditCommand::InsertNewDefaultParagraphElementAt(
+ const Position& position,
+ EditingState* editing_state) {
+ HTMLElement* paragraph_element = CreateDefaultParagraphElement(GetDocument());
+ paragraph_element->AppendChild(HTMLBRElement::Create(GetDocument()));
+ InsertNodeAt(paragraph_element, position, editing_state);
+ if (editing_state->IsAborted())
+ return nullptr;
+ return paragraph_element;
+}
+
+// If the paragraph is not entirely within it's own block, create one and move
+// the paragraph into it, and return that block. Otherwise return 0.
+HTMLElement* CompositeEditCommand::MoveParagraphContentsToNewBlockIfNecessary(
+ const Position& pos,
+ EditingState* editing_state) {
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+ DCHECK(IsEditablePosition(pos)) << pos;
+
+ // It's strange that this function is responsible for verifying that pos has
+ // not been invalidated by an earlier call to this function. The caller,
+ // applyBlockStyle, should do this.
+ VisiblePosition visible_pos = CreateVisiblePosition(pos);
+ VisiblePosition visible_paragraph_start = StartOfParagraph(visible_pos);
+ VisiblePosition visible_paragraph_end = EndOfParagraph(visible_pos);
+ VisiblePosition next = NextPositionOf(visible_paragraph_end);
+ VisiblePosition visible_end = next.IsNotNull() ? next : visible_paragraph_end;
+
+ Position upstream_start =
+ MostBackwardCaretPosition(visible_paragraph_start.DeepEquivalent());
+ Position upstream_end =
+ MostBackwardCaretPosition(visible_end.DeepEquivalent());
+
+ // If there are no VisiblePositions in the same block as pos then
+ // upstreamStart will be outside the paragraph
+ if (ComparePositions(pos, upstream_start) < 0)
+ return nullptr;
+
+ // Perform some checks to see if we need to perform work in this function.
+ if (IsEnclosingBlock(upstream_start.AnchorNode())) {
+ // If the block is the root editable element, always move content to a new
+ // block, since it is illegal to modify attributes on the root editable
+ // element for editing.
+ if (upstream_start.AnchorNode() == RootEditableElementOf(upstream_start)) {
+ // If the block is the root editable element and it contains no visible
+ // content, create a new block but don't try and move content into it,
+ // since there's nothing for moveParagraphs to move.
+ if (!HasRenderedNonAnonymousDescendantsWithHeight(
+ upstream_start.AnchorNode()->GetLayoutObject()))
+ return InsertNewDefaultParagraphElementAt(upstream_start,
+ editing_state);
+ } else if (IsEnclosingBlock(upstream_end.AnchorNode())) {
+ if (!upstream_end.AnchorNode()->IsDescendantOf(
+ upstream_start.AnchorNode())) {
+ // If the paragraph end is a descendant of paragraph start, then we need
+ // to run the rest of this function. If not, we can bail here.
+ return nullptr;
+ }
+ } else if (EnclosingBlock(upstream_end.AnchorNode()) !=
+ upstream_start.AnchorNode()) {
+ // It should be an ancestor of the paragraph start.
+ // We can bail as we have a full block to work with.
+ return nullptr;
+ } else if (IsEndOfEditableOrNonEditableContent(visible_end)) {
+ // At the end of the editable region. We can bail here as well.
+ return nullptr;
+ }
+ }
+
+ if (visible_paragraph_end.IsNull())
+ return nullptr;
+
+ HTMLElement* const new_block =
+ InsertNewDefaultParagraphElementAt(upstream_start, editing_state);
+ if (editing_state->IsAborted())
+ return nullptr;
+ DCHECK(new_block);
+
+ bool end_was_br =
+ IsHTMLBRElement(*visible_paragraph_end.DeepEquivalent().AnchorNode());
+
+ // Inserting default paragraph element can change visible position. We
+ // should update visible positions before use them.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ const VisiblePosition& destination =
+ VisiblePosition::FirstPositionInNode(*new_block);
+ if (destination.IsNull()) {
+ // Reached by CompositeEditingCommandTest
+ // .MoveParagraphContentsToNewBlockWithNonEditableStyle.
+ editing_state->Abort();
+ return nullptr;
+ }
+
+ visible_pos = CreateVisiblePosition(pos);
+ visible_paragraph_start = StartOfParagraph(visible_pos);
+ visible_paragraph_end = EndOfParagraph(visible_pos);
+ MoveParagraphs(visible_paragraph_start, visible_paragraph_end, destination,
+ editing_state);
+ if (editing_state->IsAborted())
+ return nullptr;
+
+ if (new_block->lastChild() && IsHTMLBRElement(*new_block->lastChild()) &&
+ !end_was_br) {
+ RemoveNode(new_block->lastChild(), editing_state);
+ if (editing_state->IsAborted())
+ return nullptr;
+ }
+
+ return new_block;
+}
+
+void CompositeEditCommand::PushAnchorElementDown(Element* anchor_node,
+ EditingState* editing_state) {
+ if (!anchor_node)
+ return;
+
+ DCHECK(anchor_node->IsLink()) << anchor_node;
+
+ const VisibleSelection& visible_selection = CreateVisibleSelection(
+ SelectionInDOMTree::Builder().SelectAllChildren(*anchor_node).Build());
+ SetEndingSelection(
+ SelectionForUndoStep::From(visible_selection.AsSelection()));
+ ApplyStyledElement(anchor_node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ // Clones of anchorNode have been pushed down, now remove it.
+ if (anchor_node->isConnected())
+ RemoveNodePreservingChildren(anchor_node, editing_state);
+}
+
+// Clone the paragraph between start and end under blockElement,
+// preserving the hierarchy up to outerNode.
+
+void CompositeEditCommand::CloneParagraphUnderNewElement(
+ const Position& start,
+ const Position& end,
+ Node* passed_outer_node,
+ Element* block_element,
+ EditingState* editing_state) {
+ DCHECK_LE(start, end);
+ DCHECK(passed_outer_node);
+ DCHECK(block_element);
+
+ // First we clone the outerNode
+ Node* last_node = nullptr;
+ Node* outer_node = passed_outer_node;
+
+ if (IsRootEditableElement(*outer_node)) {
+ last_node = block_element;
+ } else {
+ last_node = outer_node->cloneNode(IsDisplayInsideTable(outer_node));
+ AppendNode(last_node, block_element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ if (start.AnchorNode() != outer_node && last_node->IsElementNode() &&
+ start.AnchorNode()->IsDescendantOf(outer_node)) {
+ HeapVector<Member<Node>> ancestors;
+
+ // Insert each node from innerNode to outerNode (excluded) in a list.
+ for (Node& runner :
+ NodeTraversal::InclusiveAncestorsOf(*start.AnchorNode())) {
+ if (runner == outer_node)
+ break;
+ ancestors.push_back(runner);
+ }
+
+ // Clone every node between start.anchorNode() and outerBlock.
+
+ for (size_t i = ancestors.size(); i != 0; --i) {
+ Node* item = ancestors[i - 1].Get();
+ Node* child = item->cloneNode(IsDisplayInsideTable(item));
+ AppendNode(child, ToElement(last_node), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ last_node = child;
+ }
+ }
+
+ // Scripts specified in javascript protocol may remove |outerNode|
+ // during insertion, e.g. <iframe src="javascript:...">
+ if (!outer_node->isConnected())
+ return;
+
+ // Handle the case of paragraphs with more than one node,
+ // cloning all the siblings until end.anchorNode() is reached.
+
+ if (start.AnchorNode() != end.AnchorNode() &&
+ !start.AnchorNode()->IsDescendantOf(end.AnchorNode())) {
+ // If end is not a descendant of outerNode we need to
+ // find the first common ancestor to increase the scope
+ // of our nextSibling traversal.
+ while (outer_node && !end.AnchorNode()->IsDescendantOf(outer_node)) {
+ outer_node = outer_node->parentNode();
+ }
+
+ if (!outer_node)
+ return;
+
+ Node* start_node = start.AnchorNode();
+ for (Node* node =
+ NodeTraversal::NextSkippingChildren(*start_node, outer_node);
+ node; node = NodeTraversal::NextSkippingChildren(*node, outer_node)) {
+ // Move lastNode up in the tree as much as node was moved up in the tree
+ // by NodeTraversal::nextSkippingChildren, so that the relative depth
+ // between node and the original start node is maintained in the clone.
+ while (start_node && last_node &&
+ start_node->parentNode() != node->parentNode()) {
+ start_node = start_node->parentNode();
+ last_node = last_node->parentNode();
+ }
+
+ if (!last_node || !last_node->parentNode())
+ return;
+
+ Node* cloned_node = node->cloneNode(true);
+ InsertNodeAfter(cloned_node, last_node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ last_node = cloned_node;
+ if (node == end.AnchorNode() || end.AnchorNode()->IsDescendantOf(node))
+ break;
+ }
+ }
+}
+
+// There are bugs in deletion when it removes a fully selected table/list.
+// It expands and removes the entire table/list, but will let content
+// before and after the table/list collapse onto one line.
+// Deleting a paragraph will leave a placeholder. Remove it (and prune
+// empty or unrendered parents).
+
+void CompositeEditCommand::CleanupAfterDeletion(EditingState* editing_state) {
+ CleanupAfterDeletion(editing_state, VisiblePosition());
+}
+
+void CompositeEditCommand::CleanupAfterDeletion(EditingState* editing_state,
+ VisiblePosition destination) {
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ VisiblePosition caret_after_delete = EndingVisibleSelection().VisibleStart();
+ Node* destination_node = destination.DeepEquivalent().AnchorNode();
+ if (caret_after_delete.DeepEquivalent() != destination.DeepEquivalent() &&
+ IsStartOfParagraph(caret_after_delete) &&
+ IsEndOfParagraph(caret_after_delete)) {
+ // Note: We want the rightmost candidate.
+ Position position =
+ MostForwardCaretPosition(caret_after_delete.DeepEquivalent());
+ Node* node = position.AnchorNode();
+
+ // InsertListCommandTest.CleanupNodeSameAsDestinationNode reaches here.
+ ABORT_EDITING_COMMAND_IF(destination_node == node);
+ // Bail if we'd remove an ancestor of our destination.
+ if (destination_node && destination_node->IsDescendantOf(node))
+ return;
+
+ // Normally deletion will leave a br as a placeholder.
+ if (IsHTMLBRElement(*node)) {
+ RemoveNodeAndPruneAncestors(node, editing_state, destination_node);
+
+ // If the selection to move was empty and in an empty block that
+ // doesn't require a placeholder to prop itself open (like a bordered
+ // div or an li), remove it during the move (the list removal code
+ // expects this behavior).
+ } else if (IsEnclosingBlock(node)) {
+ // If caret position after deletion and destination position coincides,
+ // node should not be removed.
+ if (!RendersInDifferentPosition(position, destination.DeepEquivalent())) {
+ Prune(node, editing_state, destination_node);
+ return;
+ }
+ RemoveNodeAndPruneAncestors(node, editing_state, destination_node);
+ } else if (LineBreakExistsAtPosition(position)) {
+ // There is a preserved '\n' at caretAfterDelete.
+ // We can safely assume this is a text node.
+ Text* text_node = ToText(node);
+ if (text_node->length() == 1)
+ RemoveNodeAndPruneAncestors(node, editing_state, destination_node);
+ else
+ DeleteTextFromNode(text_node, position.ComputeOffsetInContainerNode(),
+ 1);
+ }
+ }
+}
+
+// This is a version of moveParagraph that preserves style by keeping the
+// original markup. It is currently used only by IndentOutdentCommand but it is
+// meant to be used in the future by several other commands such as InsertList
+// and the align commands.
+// The blockElement parameter is the element to move the paragraph to, outerNode
+// is the top element of the paragraph hierarchy.
+
+void CompositeEditCommand::MoveParagraphWithClones(
+ const VisiblePosition& start_of_paragraph_to_move,
+ const VisiblePosition& end_of_paragraph_to_move,
+ HTMLElement* block_element,
+ Node* outer_node,
+ EditingState* editing_state) {
+ // InsertListCommandTest.InsertListWithCollapsedVisibility reaches here.
+ ABORT_EDITING_COMMAND_IF(start_of_paragraph_to_move.IsNull());
+ ABORT_EDITING_COMMAND_IF(end_of_paragraph_to_move.IsNull());
+ DCHECK(outer_node);
+ DCHECK(block_element);
+
+ RelocatablePosition relocatable_before_paragraph(
+ PreviousPositionOf(start_of_paragraph_to_move).DeepEquivalent());
+ RelocatablePosition relocatable_after_paragraph(
+ NextPositionOf(end_of_paragraph_to_move).DeepEquivalent());
+
+ // We upstream() the end and downstream() the start so that we don't include
+ // collapsed whitespace in the move. When we paste a fragment, spaces after
+ // the end and before the start are treated as though they were rendered.
+ Position start =
+ MostForwardCaretPosition(start_of_paragraph_to_move.DeepEquivalent());
+ Position end = start_of_paragraph_to_move.DeepEquivalent() ==
+ end_of_paragraph_to_move.DeepEquivalent()
+ ? start
+ : MostBackwardCaretPosition(
+ end_of_paragraph_to_move.DeepEquivalent());
+ if (ComparePositions(start, end) > 0)
+ end = start;
+
+ CloneParagraphUnderNewElement(start, end, outer_node, block_element,
+ editing_state);
+
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder().Collapse(start).Extend(end).Build()));
+ if (!DeleteSelection(
+ editing_state,
+ DeleteSelectionOptions::Builder().SetSanitizeMarkup(true).Build()))
+ return;
+
+ // There are bugs in deletion when it removes a fully selected table/list.
+ // It expands and removes the entire table/list, but will let content
+ // before and after the table/list collapse onto one line.
+
+ CleanupAfterDeletion(editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Add a br if pruning an empty block level element caused a collapse. For
+ // example:
+ // foo^
+ // <div>bar</div>
+ // baz
+ // Imagine moving 'bar' to ^. 'bar' will be deleted and its div pruned. That
+ // would cause 'baz' to collapse onto the line with 'foobar' unless we insert
+ // a br. Must recononicalize these two VisiblePositions after the pruning
+ // above.
+ const VisiblePosition& before_paragraph =
+ CreateVisiblePosition(relocatable_before_paragraph.GetPosition());
+ const VisiblePosition& after_paragraph =
+ CreateVisiblePosition(relocatable_after_paragraph.GetPosition());
+
+ if (before_paragraph.IsNotNull() &&
+ !IsDisplayInsideTable(before_paragraph.DeepEquivalent().AnchorNode()) &&
+ ((!IsEndOfParagraph(before_paragraph) &&
+ !IsStartOfParagraph(before_paragraph)) ||
+ before_paragraph.DeepEquivalent() == after_paragraph.DeepEquivalent())) {
+ // FIXME: Trim text between beforeParagraph and afterParagraph if they
+ // aren't equal.
+ InsertNodeAt(HTMLBRElement::Create(GetDocument()),
+ before_paragraph.DeepEquivalent(), editing_state);
+ }
+}
+
+void CompositeEditCommand::MoveParagraph(
+ const VisiblePosition& start_of_paragraph_to_move,
+ const VisiblePosition& end_of_paragraph_to_move,
+ const VisiblePosition& destination,
+ EditingState* editing_state,
+ ShouldPreserveSelection should_preserve_selection,
+ ShouldPreserveStyle should_preserve_style,
+ Node* constraining_ancestor) {
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+ DCHECK(IsStartOfParagraph(start_of_paragraph_to_move))
+ << start_of_paragraph_to_move;
+ DCHECK(IsEndOfParagraph(end_of_paragraph_to_move))
+ << end_of_paragraph_to_move;
+ MoveParagraphs(start_of_paragraph_to_move, end_of_paragraph_to_move,
+ destination, editing_state, should_preserve_selection,
+ should_preserve_style, constraining_ancestor);
+}
+
+void CompositeEditCommand::MoveParagraphs(
+ const VisiblePosition& start_of_paragraph_to_move,
+ const VisiblePosition& end_of_paragraph_to_move,
+ const VisiblePosition& destination,
+ EditingState* editing_state,
+ ShouldPreserveSelection should_preserve_selection,
+ ShouldPreserveStyle should_preserve_style,
+ Node* constraining_ancestor) {
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+ DCHECK(start_of_paragraph_to_move.IsNotNull());
+ DCHECK(end_of_paragraph_to_move.IsNotNull());
+ DCHECK(destination.IsNotNull());
+
+ if (start_of_paragraph_to_move.DeepEquivalent() ==
+ destination.DeepEquivalent() ||
+ start_of_paragraph_to_move.IsNull())
+ return;
+
+ // Can't move the range to a destination inside itself.
+ if (destination.DeepEquivalent() >=
+ start_of_paragraph_to_move.DeepEquivalent() &&
+ destination.DeepEquivalent() <=
+ end_of_paragraph_to_move.DeepEquivalent()) {
+ // Reached by unit test TypingCommandTest.insertLineBreakWithIllFormedHTML
+ // and ApplyStyleCommandTest.JustifyRightDetachesDestination
+ editing_state->Abort();
+ return;
+ }
+
+ int start_index = -1;
+ int end_index = -1;
+ int destination_index = -1;
+ if (should_preserve_selection == kPreserveSelection &&
+ !EndingSelection().IsNone()) {
+ VisiblePosition visible_start = EndingVisibleSelection().VisibleStart();
+ VisiblePosition visible_end = EndingVisibleSelection().VisibleEnd();
+
+ bool start_after_paragraph =
+ ComparePositions(visible_start, end_of_paragraph_to_move) > 0;
+ bool end_before_paragraph =
+ ComparePositions(visible_end, start_of_paragraph_to_move) < 0;
+
+ if (!start_after_paragraph && !end_before_paragraph) {
+ bool start_in_paragraph =
+ ComparePositions(visible_start, start_of_paragraph_to_move) >= 0;
+ bool end_in_paragraph =
+ ComparePositions(visible_end, end_of_paragraph_to_move) <= 0;
+
+ const TextIteratorBehavior behavior =
+ TextIteratorBehavior::AllVisiblePositionsRangeLengthBehavior();
+
+ start_index = 0;
+ if (start_in_paragraph) {
+ start_index = TextIterator::RangeLength(
+ start_of_paragraph_to_move.ToParentAnchoredPosition(),
+ visible_start.ToParentAnchoredPosition(), behavior);
+ }
+
+ end_index = 0;
+ if (end_in_paragraph) {
+ end_index = TextIterator::RangeLength(
+ start_of_paragraph_to_move.ToParentAnchoredPosition(),
+ visible_end.ToParentAnchoredPosition(), behavior);
+ }
+ }
+ }
+
+ RelocatablePosition before_paragraph_position(
+ PreviousPositionOf(start_of_paragraph_to_move,
+ kCannotCrossEditingBoundary)
+ .DeepEquivalent());
+ RelocatablePosition after_paragraph_position(
+ NextPositionOf(end_of_paragraph_to_move, kCannotCrossEditingBoundary)
+ .DeepEquivalent());
+
+ // We upstream() the end and downstream() the start so that we don't include
+ // collapsed whitespace in the move. When we paste a fragment, spaces after
+ // the end and before the start are treated as though they were rendered.
+ Position start =
+ MostForwardCaretPosition(start_of_paragraph_to_move.DeepEquivalent());
+ Position end =
+ MostBackwardCaretPosition(end_of_paragraph_to_move.DeepEquivalent());
+
+ // FIXME: This is an inefficient way to preserve style on nodes in the
+ // paragraph to move. It shouldn't matter though, since moved paragraphs will
+ // usually be quite small.
+ DocumentFragment* fragment =
+ start_of_paragraph_to_move.DeepEquivalent() !=
+ end_of_paragraph_to_move.DeepEquivalent()
+ ? CreateFragmentFromMarkup(
+ GetDocument(),
+ CreateMarkup(start.ParentAnchoredEquivalent(),
+ end.ParentAnchoredEquivalent(),
+ kDoNotAnnotateForInterchange,
+ ConvertBlocksToInlines::kConvert,
+ kDoNotResolveURLs, constraining_ancestor),
+ "")
+ : nullptr;
+
+ // A non-empty paragraph's style is moved when we copy and move it. We don't
+ // move anything if we're given an empty paragraph, but an empty paragraph can
+ // have style too, <div><b><br></b></div> for example. Save it so that we can
+ // preserve it later.
+ EditingStyle* style_in_empty_paragraph = nullptr;
+ if (start_of_paragraph_to_move.DeepEquivalent() ==
+ end_of_paragraph_to_move.DeepEquivalent() &&
+ should_preserve_style == kPreserveStyle) {
+ style_in_empty_paragraph =
+ EditingStyle::Create(start_of_paragraph_to_move.DeepEquivalent());
+ style_in_empty_paragraph->MergeTypingStyle(&GetDocument());
+ // The moved paragraph should assume the block style of the destination.
+ style_in_empty_paragraph->RemoveBlockProperties();
+ }
+
+ // FIXME (5098931): We should add a new insert action
+ // "WebViewInsertActionMoved" and call shouldInsertFragment here.
+
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+
+ const VisibleSelection& selection_to_delete = CreateVisibleSelection(
+ SelectionInDOMTree::Builder().Collapse(start).Extend(end).Build());
+ SetEndingSelection(
+ SelectionForUndoStep::From(selection_to_delete.AsSelection()));
+ if (!DeleteSelection(
+ editing_state,
+ DeleteSelectionOptions::Builder().SetSanitizeMarkup(true).Build()))
+ return;
+
+ DCHECK(destination.DeepEquivalent().IsConnected()) << destination;
+ CleanupAfterDeletion(editing_state, destination);
+ if (editing_state->IsAborted())
+ return;
+ DCHECK(destination.DeepEquivalent().IsConnected()) << destination;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Add a br if pruning an empty block level element caused a collapse. For
+ // example:
+ // foo^
+ // <div>bar</div>
+ // baz
+ // Imagine moving 'bar' to ^. 'bar' will be deleted and its div pruned. That
+ // would cause 'baz' to collapse onto the line with 'foobar' unless we insert
+ // a br. Must recononicalize these two VisiblePositions after the pruning
+ // above.
+ VisiblePosition before_paragraph =
+ CreateVisiblePosition(before_paragraph_position.GetPosition());
+ VisiblePosition after_paragraph =
+ CreateVisiblePosition(after_paragraph_position.GetPosition());
+ if (before_paragraph.IsNotNull() &&
+ ((!IsStartOfParagraph(before_paragraph) &&
+ !IsEndOfParagraph(before_paragraph)) ||
+ before_paragraph.DeepEquivalent() == after_paragraph.DeepEquivalent())) {
+ // FIXME: Trim text between beforeParagraph and afterParagraph if they
+ // aren't equal.
+ InsertNodeAt(HTMLBRElement::Create(GetDocument()),
+ before_paragraph.DeepEquivalent(), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ // TextIterator::rangeLength requires clean layout.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ destination_index = TextIterator::RangeLength(
+ Position::FirstPositionInNode(*GetDocument().documentElement()),
+ destination.ToParentAnchoredPosition(),
+ TextIteratorBehavior::AllVisiblePositionsRangeLengthBehavior());
+
+ const VisibleSelection& destination_selection =
+ CreateVisibleSelection(SelectionInDOMTree::Builder()
+ .Collapse(destination.ToPositionWithAffinity())
+ .Build());
+ if (EndingSelection().IsNone()) {
+ // We abort executing command since |destination| becomes invisible.
+ editing_state->Abort();
+ return;
+ }
+ SetEndingSelection(
+ SelectionForUndoStep::From(destination_selection.AsSelection()));
+ ReplaceSelectionCommand::CommandOptions options =
+ ReplaceSelectionCommand::kSelectReplacement |
+ ReplaceSelectionCommand::kMovingParagraph;
+ if (should_preserve_style == kDoNotPreserveStyle)
+ options |= ReplaceSelectionCommand::kMatchStyle;
+ ApplyCommandToComposite(
+ ReplaceSelectionCommand::Create(GetDocument(), fragment, options),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ ABORT_EDITING_COMMAND_IF(!EndingSelection().IsValidFor(GetDocument()));
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // If the selection is in an empty paragraph, restore styles from the old
+ // empty paragraph to the new empty paragraph.
+ bool selection_is_empty_paragraph =
+ EndingSelection().IsCaret() &&
+ IsStartOfParagraph(EndingVisibleSelection().VisibleStart()) &&
+ IsEndOfParagraph(EndingVisibleSelection().VisibleStart());
+ if (style_in_empty_paragraph && selection_is_empty_paragraph) {
+ ApplyStyle(style_in_empty_paragraph, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ if (should_preserve_selection == kDoNotPreserveSelection || start_index == -1)
+ return;
+ Element* document_element = GetDocument().documentElement();
+ if (!document_element)
+ return;
+
+ // We need clean layout in order to compute plain-text ranges below.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Fragment creation (using createMarkup) incorrectly uses regular spaces
+ // instead of nbsps for some spaces that were rendered (11475), which causes
+ // spaces to be collapsed during the move operation. This results in a call
+ // to rangeFromLocationAndLength with a location past the end of the
+ // document (which will return null).
+ EphemeralRange start_range = PlainTextRange(destination_index + start_index)
+ .CreateRangeForSelection(*document_element);
+ if (start_range.IsNull())
+ return;
+ EphemeralRange end_range = PlainTextRange(destination_index + end_index)
+ .CreateRangeForSelection(*document_element);
+ if (end_range.IsNull())
+ return;
+ const VisibleSelection& visible_selection =
+ CreateVisibleSelection(SelectionInDOMTree::Builder()
+ .Collapse(start_range.StartPosition())
+ .Extend(end_range.StartPosition())
+ .Build());
+ SetEndingSelection(
+ SelectionForUndoStep::From(visible_selection.AsSelection()));
+}
+
+// FIXME: Send an appropriate shouldDeleteRange call.
+bool CompositeEditCommand::BreakOutOfEmptyListItem(
+ EditingState* editing_state) {
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+ Node* empty_list_item =
+ EnclosingEmptyListItem(EndingVisibleSelection().VisibleStart());
+ if (!empty_list_item)
+ return false;
+
+ EditingStyle* style = EditingStyle::Create(EndingSelection().Start());
+ style->MergeTypingStyle(&GetDocument());
+
+ ContainerNode* list_node = empty_list_item->parentNode();
+ // FIXME: Can't we do something better when the immediate parent wasn't a list
+ // node?
+ if (!list_node ||
+ (!IsHTMLUListElement(*list_node) && !IsHTMLOListElement(*list_node)) ||
+ !HasEditableStyle(*list_node) ||
+ list_node == RootEditableElement(*empty_list_item))
+ return false;
+
+ HTMLElement* new_block = nullptr;
+ if (ContainerNode* block_enclosing_list = list_node->parentNode()) {
+ if (IsHTMLLIElement(
+ *block_enclosing_list)) { // listNode is inside another list item
+ if (VisiblePositionAfterNode(*block_enclosing_list).DeepEquivalent() ==
+ VisiblePositionAfterNode(*list_node).DeepEquivalent()) {
+ // If listNode appears at the end of the outer list item, then move
+ // listNode outside of this list item, e.g.
+ // <ul><li>hello <ul><li><br></li></ul> </li></ul>
+ // should become
+ // <ul><li>hello</li> <ul><li><br></li></ul> </ul>
+ // after this section.
+ //
+ // If listNode does NOT appear at the end, then we should consider it as
+ // a regular paragraph, e.g.
+ // <ul><li> <ul><li><br></li></ul> hello</li></ul>
+ // should become
+ // <ul><li> <div><br></div> hello</li></ul>
+ // at the end
+ SplitElement(ToElement(block_enclosing_list), list_node);
+ RemoveNodePreservingChildren(list_node->parentNode(), editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ new_block = HTMLLIElement::Create(GetDocument());
+ }
+ // If listNode does NOT appear at the end of the outer list item, then
+ // behave as if in a regular paragraph.
+ } else if (IsHTMLOListElement(*block_enclosing_list) ||
+ IsHTMLUListElement(*block_enclosing_list)) {
+ new_block = HTMLLIElement::Create(GetDocument());
+ }
+ }
+ if (!new_block)
+ new_block = CreateDefaultParagraphElement(GetDocument());
+
+ Node* previous_list_node =
+ empty_list_item->IsElementNode()
+ ? ElementTraversal::PreviousSibling(*empty_list_item)
+ : empty_list_item->previousSibling();
+ Node* next_list_node = empty_list_item->IsElementNode()
+ ? ElementTraversal::NextSibling(*empty_list_item)
+ : empty_list_item->nextSibling();
+ if (IsListItem(next_list_node) || IsHTMLListElement(next_list_node)) {
+ // If emptyListItem follows another list item or nested list, split the list
+ // node.
+ if (IsListItem(previous_list_node) || IsHTMLListElement(previous_list_node))
+ SplitElement(ToElement(list_node), empty_list_item);
+
+ // If emptyListItem is followed by other list item or nested list, then
+ // insert newBlock before the list node. Because we have splitted the
+ // element, emptyListItem is the first element in the list node.
+ // i.e. insert newBlock before ul or ol whose first element is emptyListItem
+ InsertNodeBefore(new_block, list_node, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ RemoveNode(empty_list_item, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ } else {
+ // When emptyListItem does not follow any list item or nested list, insert
+ // newBlock after the enclosing list node. Remove the enclosing node if
+ // emptyListItem is the only child; otherwise just remove emptyListItem.
+ InsertNodeAfter(new_block, list_node, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ RemoveNode(
+ IsListItem(previous_list_node) || IsHTMLListElement(previous_list_node)
+ ? empty_list_item
+ : list_node,
+ editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ }
+
+ AppendBlockPlaceholder(new_block, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::FirstPositionInNode(*new_block))
+ .Build()));
+
+ style->PrepareToApplyAt(EndingSelection().Start());
+ if (!style->IsEmpty()) {
+ ApplyStyle(style, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ }
+
+ return true;
+}
+
+// If the caret is in an empty quoted paragraph, and either there is nothing
+// before that paragraph, or what is before is unquoted, and the user presses
+// delete, unquote that paragraph.
+bool CompositeEditCommand::BreakOutOfEmptyMailBlockquotedParagraph(
+ EditingState* editing_state) {
+ if (!EndingSelection().IsCaret())
+ return false;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ VisiblePosition caret = EndingVisibleSelection().VisibleStart();
+ HTMLQuoteElement* highest_blockquote =
+ ToHTMLQuoteElement(HighestEnclosingNodeOfType(
+ caret.DeepEquivalent(), &IsMailHTMLBlockquoteElement));
+ if (!highest_blockquote)
+ return false;
+
+ if (!IsStartOfParagraph(caret) || !IsEndOfParagraph(caret))
+ return false;
+
+ VisiblePosition previous =
+ PreviousPositionOf(caret, kCannotCrossEditingBoundary);
+ // Only move forward if there's nothing before the caret, or if there's
+ // unquoted content before it.
+ if (EnclosingNodeOfType(previous.DeepEquivalent(),
+ &IsMailHTMLBlockquoteElement))
+ return false;
+
+ HTMLBRElement* br = HTMLBRElement::Create(GetDocument());
+ // We want to replace this quoted paragraph with an unquoted one, so insert a
+ // br to hold the caret before the highest blockquote.
+ InsertNodeBefore(br, highest_blockquote, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ VisiblePosition at_br = VisiblePosition::BeforeNode(*br);
+ // If the br we inserted collapsed, for example:
+ // foo<br><blockquote>...</blockquote>
+ // insert a second one.
+ if (!IsStartOfParagraph(at_br)) {
+ InsertNodeBefore(HTMLBRElement::Create(GetDocument()), br, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ }
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(at_br.ToPositionWithAffinity())
+ .Build()));
+
+ // If this is an empty paragraph there must be a line break here.
+ if (!LineBreakExistsAtVisiblePosition(caret))
+ return false;
+
+ Position caret_pos(MostForwardCaretPosition(caret.DeepEquivalent()));
+ // A line break is either a br or a preserved newline.
+ DCHECK(
+ IsHTMLBRElement(caret_pos.AnchorNode()) ||
+ (caret_pos.AnchorNode()->IsTextNode() &&
+ caret_pos.AnchorNode()->GetLayoutObject()->Style()->PreserveNewline()))
+ << caret_pos;
+
+ if (IsHTMLBRElement(*caret_pos.AnchorNode())) {
+ RemoveNodeAndPruneAncestors(caret_pos.AnchorNode(), editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ } else if (caret_pos.AnchorNode()->IsTextNode()) {
+ DCHECK_EQ(caret_pos.ComputeOffsetInContainerNode(), 0);
+ Text* text_node = ToText(caret_pos.AnchorNode());
+ ContainerNode* parent_node = text_node->parentNode();
+ // The preserved newline must be the first thing in the node, since
+ // otherwise the previous paragraph would be quoted, and we verified that it
+ // wasn't above.
+ DeleteTextFromNode(text_node, 0, 1);
+ Prune(parent_node, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ }
+
+ return true;
+}
+
+// Operations use this function to avoid inserting content into an anchor when
+// at the start or the end of that anchor, as in NSTextView.
+// FIXME: This is only an approximation of NSTextViews insertion behavior, which
+// varies depending on how the caret was made.
+Position CompositeEditCommand::PositionAvoidingSpecialElementBoundary(
+ const Position& original,
+ EditingState* editing_state) {
+ if (original.IsNull())
+ return original;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ VisiblePosition visible_pos = CreateVisiblePosition(original);
+ Element* enclosing_anchor = EnclosingAnchorElement(original);
+ Position result = original;
+
+ if (!enclosing_anchor)
+ return result;
+
+ // Don't avoid block level anchors, because that would insert content into the
+ // wrong paragraph.
+ if (enclosing_anchor && !IsEnclosingBlock(enclosing_anchor)) {
+ VisiblePosition first_in_anchor =
+ VisiblePosition::FirstPositionInNode(*enclosing_anchor);
+ VisiblePosition last_in_anchor =
+ VisiblePosition::LastPositionInNode(*enclosing_anchor);
+ // If visually just after the anchor, insert *inside* the anchor unless it's
+ // the last VisiblePosition in the document, to match NSTextView.
+ if (visible_pos.DeepEquivalent() == last_in_anchor.DeepEquivalent()) {
+ // Make sure anchors are pushed down before avoiding them so that we don't
+ // also avoid structural elements like lists and blocks (5142012).
+ if (original.AnchorNode() != enclosing_anchor &&
+ original.AnchorNode()->parentNode() != enclosing_anchor) {
+ PushAnchorElementDown(enclosing_anchor, editing_state);
+ if (editing_state->IsAborted())
+ return original;
+ enclosing_anchor = EnclosingAnchorElement(original);
+ if (!enclosing_anchor)
+ return original;
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Don't insert outside an anchor if doing so would skip over a line
+ // break. It would probably be safe to move the line break so that we
+ // could still avoid the anchor here.
+ Position downstream(
+ MostForwardCaretPosition(visible_pos.DeepEquivalent()));
+ if (LineBreakExistsAtVisiblePosition(visible_pos) &&
+ downstream.AnchorNode()->IsDescendantOf(enclosing_anchor))
+ return original;
+
+ result = Position::InParentAfterNode(*enclosing_anchor);
+ }
+
+ // If visually just before an anchor, insert *outside* the anchor unless
+ // it's the first VisiblePosition in a paragraph, to match NSTextView.
+ if (visible_pos.DeepEquivalent() == first_in_anchor.DeepEquivalent()) {
+ // Make sure anchors are pushed down before avoiding them so that we don't
+ // also avoid structural elements like lists and blocks (5142012).
+ if (original.AnchorNode() != enclosing_anchor &&
+ original.AnchorNode()->parentNode() != enclosing_anchor) {
+ PushAnchorElementDown(enclosing_anchor, editing_state);
+ if (editing_state->IsAborted())
+ return original;
+ enclosing_anchor = EnclosingAnchorElement(original);
+ }
+ if (!enclosing_anchor)
+ return original;
+
+ result = Position::InParentBeforeNode(*enclosing_anchor);
+ }
+ }
+
+ if (result.IsNull() || !RootEditableElementOf(result))
+ result = original;
+
+ return result;
+}
+
+// Splits the tree parent by parent until we reach the specified ancestor. We
+// use VisiblePositions to determine if the split is necessary. Returns the last
+// split node.
+Node* CompositeEditCommand::SplitTreeToNode(Node* start,
+ Node* end,
+ bool should_split_ancestor) {
+ DCHECK(start);
+ DCHECK(end);
+ DCHECK_NE(start, end);
+
+ if (should_split_ancestor && end->parentNode())
+ end = end->parentNode();
+ if (!start->IsDescendantOf(end))
+ return end;
+
+ Node* end_node = end;
+ Node* node = nullptr;
+ for (node = start; node->parentNode() != end_node;
+ node = node->parentNode()) {
+ Element* parent_element = node->parentElement();
+ if (!parent_element)
+ break;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Do not split a node when doing so introduces an empty node.
+ VisiblePosition position_in_parent =
+ VisiblePosition::FirstPositionInNode(*parent_element);
+ VisiblePosition position_in_node =
+ CreateVisiblePosition(FirstPositionInOrBeforeNode(*node));
+ if (position_in_parent.DeepEquivalent() !=
+ position_in_node.DeepEquivalent())
+ SplitElement(parent_element, node);
+ }
+
+ return node;
+}
+
+void CompositeEditCommand::SetStartingSelection(
+ const SelectionForUndoStep& selection) {
+ for (CompositeEditCommand* command = this;; command = command->Parent()) {
+ if (UndoStep* undo_step = command->GetUndoStep()) {
+ DCHECK(command->IsTopLevelCommand());
+ undo_step->SetStartingSelection(selection);
+ }
+ command->starting_selection_ = selection;
+ if (!command->Parent() || command->Parent()->IsFirstCommand(command))
+ break;
+ }
+}
+
+void CompositeEditCommand::SetEndingSelection(
+ const SelectionForUndoStep& selection) {
+ for (CompositeEditCommand* command = this; command;
+ command = command->Parent()) {
+ if (UndoStep* undo_step = command->GetUndoStep()) {
+ DCHECK(command->IsTopLevelCommand());
+ undo_step->SetEndingSelection(selection);
+ }
+ command->ending_selection_ = selection;
+ }
+}
+
+void CompositeEditCommand::SetParent(CompositeEditCommand* parent) {
+ EditCommand::SetParent(parent);
+ if (!parent)
+ return;
+ starting_selection_ = parent->ending_selection_;
+ ending_selection_ = parent->ending_selection_;
+}
+
+// Determines whether a node is inside a range or visibly starts and ends at the
+// boundaries of the range. Call this function to determine whether a node is
+// visibly fit inside selectedRange
+bool CompositeEditCommand::IsNodeVisiblyContainedWithin(
+ Node& node,
+ const EphemeralRange& selected_range) {
+ DCHECK(!NeedsLayoutTreeUpdate(node));
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ node.GetDocument().Lifecycle());
+
+ if (IsNodeFullyContained(selected_range, node))
+ return true;
+
+ bool start_is_visually_same =
+ VisiblePositionBeforeNode(node).DeepEquivalent() ==
+ CreateVisiblePosition(selected_range.StartPosition()).DeepEquivalent();
+ if (start_is_visually_same &&
+ ComparePositions(Position::InParentAfterNode(node),
+ selected_range.EndPosition()) < 0)
+ return true;
+
+ bool end_is_visually_same =
+ VisiblePositionAfterNode(node).DeepEquivalent() ==
+ CreateVisiblePosition(selected_range.EndPosition()).DeepEquivalent();
+ if (end_is_visually_same &&
+ ComparePositions(selected_range.StartPosition(),
+ Position::InParentBeforeNode(node)) < 0)
+ return true;
+
+ return start_is_visually_same && end_is_visually_same;
+}
+
+void CompositeEditCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(commands_);
+ visitor->Trace(starting_selection_);
+ visitor->Trace(ending_selection_);
+ visitor->Trace(undo_step_);
+ EditCommand::Trace(visitor);
+}
+
+void CompositeEditCommand::AppliedEditing() {
+ DCHECK(!IsCommandGroupWrapper());
+ EventQueueScope scope;
+
+ const UndoStep& undo_step = *GetUndoStep();
+ DispatchEditableContentChangedEvents(undo_step.StartingRootEditableElement(),
+ undo_step.EndingRootEditableElement());
+ LocalFrame* const frame = GetDocument().GetFrame();
+ Editor& editor = frame->GetEditor();
+ // TODO(chongz): Filter empty InputType after spec is finalized.
+ DispatchInputEventEditableContentChanged(
+ undo_step.StartingRootEditableElement(),
+ undo_step.EndingRootEditableElement(), GetInputType(),
+ TextDataForInputEvent(), IsComposingFromCommand(this));
+
+ const SelectionInDOMTree& new_selection =
+ CorrectedSelectionAfterCommand(EndingSelection(), &GetDocument());
+
+ // Don't clear the typing style with this selection change. We do those things
+ // elsewhere if necessary.
+ ChangeSelectionAfterCommand(frame, new_selection,
+ SetSelectionOptions::Builder()
+ .SetIsDirectional(SelectionIsDirectional())
+ .Build());
+
+ if (!PreservesTypingStyle())
+ editor.ClearTypingStyle();
+
+ CompositeEditCommand* const last_edit_command = editor.LastEditCommand();
+ // Command will be equal to last edit command only in the case of typing
+ if (last_edit_command == this) {
+ DCHECK(IsTypingCommand());
+ } else if (last_edit_command && last_edit_command->IsDragAndDropCommand() &&
+ (GetInputType() == InputEvent::InputType::kDeleteByDrag ||
+ GetInputType() == InputEvent::InputType::kInsertFromDrop)) {
+ // Only register undo entry when combined with other commands.
+ if (!last_edit_command->GetUndoStep()) {
+ editor.GetUndoStack().RegisterUndoStep(
+ last_edit_command->EnsureUndoStep());
+ }
+ last_edit_command->EnsureUndoStep()->SetEndingSelection(
+ EnsureUndoStep()->EndingSelection());
+ last_edit_command->GetUndoStep()->SetSelectionIsDirectional(
+ GetUndoStep()->SelectionIsDirectional());
+ last_edit_command->AppendCommandToUndoStep(this);
+ } else {
+ // Only register a new undo command if the command passed in is
+ // different from the last command
+ editor.SetLastEditCommand(this);
+ editor.GetUndoStack().RegisterUndoStep(EnsureUndoStep());
+ }
+
+ editor.RespondToChangedContents(new_selection.Base());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/composite_edit_command.h b/chromium/third_party/blink/renderer/core/editing/commands/composite_edit_command.h
new file mode 100644
index 00000000000..9666b64a940
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/composite_edit_command.h
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_COMPOSITE_EDIT_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_COMPOSITE_EDIT_COMMAND_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/css_property_names.h"
+#include "third_party/blink/renderer/core/editing/commands/edit_command.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_state.h"
+#include "third_party/blink/renderer/core/editing/commands/undo_step.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/wtf/vector.h"
+
+namespace blink {
+
+class DeleteSelectionOptions;
+class EditingStyle;
+class Element;
+class HTMLBRElement;
+class HTMLElement;
+class HTMLSpanElement;
+class Text;
+
+class CORE_EXPORT CompositeEditCommand : public EditCommand {
+ public:
+ enum ShouldPreserveSelection { kPreserveSelection, kDoNotPreserveSelection };
+ enum ShouldPreserveStyle { kPreserveStyle, kDoNotPreserveStyle };
+
+ ~CompositeEditCommand() override;
+
+ const SelectionForUndoStep& StartingSelection() const {
+ return starting_selection_;
+ }
+ const SelectionForUndoStep& EndingSelection() const {
+ return ending_selection_;
+ }
+
+ void SetStartingSelection(const SelectionForUndoStep&);
+ void SetEndingSelection(const SelectionForUndoStep&);
+
+ void SetParent(CompositeEditCommand*) override;
+
+ // Returns |false| if the command failed. e.g. It's aborted.
+ bool Apply();
+ bool IsFirstCommand(EditCommand* command) {
+ return !commands_.IsEmpty() && commands_.front() == command;
+ }
+ UndoStep* GetUndoStep() { return undo_step_.Get(); }
+ UndoStep* EnsureUndoStep();
+ // Append undo step from an already applied command.
+ void AppendCommandToUndoStep(CompositeEditCommand*);
+
+ virtual bool IsReplaceSelectionCommand() const;
+ virtual bool IsTypingCommand() const;
+ virtual bool IsCommandGroupWrapper() const;
+ virtual bool IsDragAndDropCommand() const;
+ virtual bool PreservesTypingStyle() const;
+
+ virtual void AppliedEditing();
+
+ virtual void Trace(blink::Visitor*);
+
+ protected:
+ explicit CompositeEditCommand(Document&);
+
+ VisibleSelection EndingVisibleSelection() const;
+ //
+ // sugary-sweet convenience functions to help create and apply edit commands
+ // in composite commands
+ //
+ void AppendNode(Node*, ContainerNode* parent, EditingState*);
+ void ApplyCommandToComposite(EditCommand*, EditingState*);
+ void ApplyStyle(const EditingStyle*, EditingState*);
+ void ApplyStyle(const EditingStyle*,
+ const Position& start,
+ const Position& end,
+ EditingState*);
+ void ApplyStyledElement(Element*, EditingState*);
+ void RemoveStyledElement(Element*, EditingState*);
+ // Returns |false| if the EditingState has been aborted.
+ bool DeleteSelection(EditingState*, const DeleteSelectionOptions&);
+ virtual void DeleteTextFromNode(Text*, unsigned offset, unsigned count);
+ bool IsRemovableBlock(const Node*);
+ void InsertNodeAfter(Node*, Node* ref_child, EditingState*);
+ void InsertNodeAt(Node*, const Position&, EditingState*);
+ void InsertNodeAtTabSpanPosition(Node*, const Position&, EditingState*);
+ void InsertNodeBefore(Node*,
+ Node* ref_child,
+ EditingState*,
+ ShouldAssumeContentIsAlwaysEditable =
+ kDoNotAssumeContentIsAlwaysEditable);
+ void InsertParagraphSeparator(
+ EditingState*,
+ bool use_default_paragraph_element = false,
+ bool paste_blockqutoe_into_unquoted_area = false);
+ void InsertTextIntoNode(Text*, unsigned offset, const String& text);
+ void MergeIdenticalElements(Element*, Element*, EditingState*);
+ void RebalanceWhitespace();
+ void RebalanceWhitespaceAt(const Position&);
+ void RebalanceWhitespaceOnTextSubstring(Text*,
+ int start_offset,
+ int end_offset);
+ void PrepareWhitespaceAtPositionForSplit(Position&);
+ void ReplaceCollapsibleWhitespaceWithNonBreakingSpaceIfNeeded(
+ const VisiblePosition&);
+ bool CanRebalance(const Position&) const;
+ void RemoveCSSProperty(Element*, CSSPropertyID);
+ void RemoveElementAttribute(Element*, const QualifiedName& attribute);
+ void RemoveChildrenInRange(Node*, unsigned from, unsigned to, EditingState*);
+ virtual void RemoveNode(Node*,
+ EditingState*,
+ ShouldAssumeContentIsAlwaysEditable =
+ kDoNotAssumeContentIsAlwaysEditable);
+ HTMLSpanElement* ReplaceElementWithSpanPreservingChildrenAndAttributes(
+ HTMLElement*);
+ void RemoveNodePreservingChildren(Node*,
+ EditingState*,
+ ShouldAssumeContentIsAlwaysEditable =
+ kDoNotAssumeContentIsAlwaysEditable);
+ void RemoveNodeAndPruneAncestors(Node*,
+ EditingState*,
+ Node* exclude_node = nullptr);
+ void MoveRemainingSiblingsToNewParent(Node*,
+ Node* past_last_node_to_move,
+ Element* new_parent,
+ EditingState*);
+ void UpdatePositionForNodeRemovalPreservingChildren(Position&, Node&);
+ void Prune(Node*, EditingState*, Node* exclude_node = nullptr);
+ void ReplaceTextInNode(Text*,
+ unsigned offset,
+ unsigned count,
+ const String& replacement_text);
+ Position ReplaceSelectedTextInNode(const String&);
+ Position PositionOutsideTabSpan(const Position&);
+ void SetNodeAttribute(Element*,
+ const QualifiedName& attribute,
+ const AtomicString& value);
+ void SplitElement(Element*, Node* at_child);
+ void SplitTextNode(Text*, unsigned offset);
+ void SplitTextNodeContainingElement(Text*, unsigned offset);
+ void WrapContentsInDummySpan(Element*);
+
+ void DeleteInsignificantText(Text*, unsigned start, unsigned end);
+ void DeleteInsignificantText(const Position& start, const Position& end);
+ void DeleteInsignificantTextDownstream(const Position&);
+
+ HTMLBRElement* AppendBlockPlaceholder(Element*, EditingState*);
+ HTMLBRElement* InsertBlockPlaceholder(const Position&, EditingState*);
+ HTMLBRElement* AddBlockPlaceholderIfNeeded(Element*, EditingState*);
+ void RemovePlaceholderAt(const Position&);
+
+ HTMLElement* InsertNewDefaultParagraphElementAt(const Position&,
+ EditingState*);
+
+ HTMLElement* MoveParagraphContentsToNewBlockIfNecessary(const Position&,
+ EditingState*);
+
+ void PushAnchorElementDown(Element*, EditingState*);
+
+ void MoveParagraph(const VisiblePosition&,
+ const VisiblePosition&,
+ const VisiblePosition&,
+ EditingState*,
+ ShouldPreserveSelection = kDoNotPreserveSelection,
+ ShouldPreserveStyle = kPreserveStyle,
+ Node* constraining_ancestor = nullptr);
+ void MoveParagraphs(const VisiblePosition&,
+ const VisiblePosition&,
+ const VisiblePosition&,
+ EditingState*,
+ ShouldPreserveSelection = kDoNotPreserveSelection,
+ ShouldPreserveStyle = kPreserveStyle,
+ Node* constraining_ancestor = nullptr);
+ void MoveParagraphWithClones(
+ const VisiblePosition& start_of_paragraph_to_move,
+ const VisiblePosition& end_of_paragraph_to_move,
+ HTMLElement* block_element,
+ Node* outer_node,
+ EditingState*);
+ void CloneParagraphUnderNewElement(const Position& start,
+ const Position& end,
+ Node* outer_node,
+ Element* block_element,
+ EditingState*);
+ void CleanupAfterDeletion(EditingState*, VisiblePosition destination);
+ void CleanupAfterDeletion(EditingState*);
+
+ bool BreakOutOfEmptyListItem(EditingState*);
+ bool BreakOutOfEmptyMailBlockquotedParagraph(EditingState*);
+
+ Position PositionAvoidingSpecialElementBoundary(const Position&,
+ EditingState*);
+
+ Node* SplitTreeToNode(Node*, Node*, bool split_ancestor = false);
+
+ static bool IsNodeVisiblyContainedWithin(Node&, const EphemeralRange&);
+
+ HeapVector<Member<EditCommand>> commands_;
+
+ private:
+ bool IsCompositeEditCommand() const final { return true; }
+
+ SelectionForUndoStep starting_selection_;
+ SelectionForUndoStep ending_selection_;
+ Member<UndoStep> undo_step_;
+};
+
+DEFINE_TYPE_CASTS(CompositeEditCommand,
+ EditCommand,
+ command,
+ command->IsCompositeEditCommand(),
+ command.IsCompositeEditCommand());
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_COMPOSITE_EDIT_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/composite_edit_command_test.cc b/chromium/third_party/blink/renderer/core/editing/commands/composite_edit_command_test.cc
new file mode 100644
index 00000000000..5cf9da21411
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/composite_edit_command_test.cc
@@ -0,0 +1,149 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+#include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+
+namespace blink {
+
+namespace {
+
+class SampleCommand final : public CompositeEditCommand {
+ public:
+ SampleCommand(Document&);
+
+ void InsertNodeBefore(Node*,
+ Node* ref_child,
+ EditingState*,
+ ShouldAssumeContentIsAlwaysEditable =
+ kDoNotAssumeContentIsAlwaysEditable);
+
+ void MoveParagraphContentsToNewBlockIfNecessary(const Position&,
+ EditingState*);
+
+ // CompositeEditCommand member implementations
+ void DoApply(EditingState*) final {}
+ InputEvent::InputType GetInputType() const final {
+ return InputEvent::InputType::kNone;
+ }
+};
+
+SampleCommand::SampleCommand(Document& document)
+ : CompositeEditCommand(document) {}
+
+void SampleCommand::InsertNodeBefore(
+ Node* insert_child,
+ Node* ref_child,
+ EditingState* editing_state,
+ ShouldAssumeContentIsAlwaysEditable
+ should_assume_content_is_always_editable) {
+ CompositeEditCommand::InsertNodeBefore(
+ insert_child, ref_child, editing_state,
+ should_assume_content_is_always_editable);
+}
+
+void SampleCommand::MoveParagraphContentsToNewBlockIfNecessary(
+ const Position& position,
+ EditingState* editing_state) {
+ CompositeEditCommand::MoveParagraphContentsToNewBlockIfNecessary(
+ position, editing_state);
+}
+
+} // namespace
+
+class CompositeEditCommandTest : public EditingTestBase {};
+
+TEST_F(CompositeEditCommandTest, insertNodeBefore) {
+ SetBodyContent("<div contenteditable><b></b></div>");
+ SampleCommand& sample = *new SampleCommand(GetDocument());
+ Node* insert_child = GetDocument().createTextNode("foo");
+ Element* ref_child = GetDocument().QuerySelector("b");
+ Element* div = GetDocument().QuerySelector("div");
+
+ EditingState editing_state;
+ sample.InsertNodeBefore(insert_child, ref_child, &editing_state);
+ EXPECT_FALSE(editing_state.IsAborted());
+ EXPECT_EQ("foo<b></b>", div->InnerHTMLAsString());
+}
+
+TEST_F(CompositeEditCommandTest, insertNodeBeforeInUneditable) {
+ SetBodyContent("<div><b></b></div>");
+ SampleCommand& sample = *new SampleCommand(GetDocument());
+ Node* insert_child = GetDocument().createTextNode("foo");
+ Element* ref_child = GetDocument().QuerySelector("b");
+
+ EditingState editing_state;
+ sample.InsertNodeBefore(insert_child, ref_child, &editing_state);
+ EXPECT_TRUE(editing_state.IsAborted());
+}
+
+TEST_F(CompositeEditCommandTest, insertNodeBeforeDisconnectedNode) {
+ SetBodyContent("<div><b></b></div>");
+ SampleCommand& sample = *new SampleCommand(GetDocument());
+ Node* insert_child = GetDocument().createTextNode("foo");
+ Element* ref_child = GetDocument().QuerySelector("b");
+ Element* div = GetDocument().QuerySelector("div");
+ div->remove();
+
+ EditingState editing_state;
+ sample.InsertNodeBefore(insert_child, ref_child, &editing_state);
+ EXPECT_FALSE(editing_state.IsAborted());
+ EXPECT_EQ("<b></b>", div->InnerHTMLAsString())
+ << "InsertNodeBeforeCommand does nothing for disconnected node";
+}
+
+TEST_F(CompositeEditCommandTest, insertNodeBeforeWithDirtyLayoutTree) {
+ SetBodyContent("<div><b></b></div>");
+ SampleCommand& sample = *new SampleCommand(GetDocument());
+ Node* insert_child = GetDocument().createTextNode("foo");
+ Element* ref_child = GetDocument().QuerySelector("b");
+ Element* div = GetDocument().QuerySelector("div");
+ div->setAttribute(HTMLNames::contenteditableAttr, "true");
+
+ EditingState editing_state;
+ sample.InsertNodeBefore(insert_child, ref_child, &editing_state);
+ EXPECT_FALSE(editing_state.IsAborted());
+ EXPECT_EQ("foo<b></b>", div->InnerHTMLAsString());
+}
+
+TEST_F(CompositeEditCommandTest,
+ MoveParagraphContentsToNewBlockWithNonEditableStyle) {
+ SetBodyContent(
+ "<style>div{-webkit-user-modify:read-only;user-select:none;}</style>"
+ "foo");
+ SampleCommand& sample = *new SampleCommand(GetDocument());
+ Element* body = GetDocument().body();
+ Node* text = body->lastChild();
+ body->setAttribute(HTMLNames::contenteditableAttr, "true");
+ GetDocument().UpdateStyleAndLayout();
+
+ EditingState editing_state;
+ sample.MoveParagraphContentsToNewBlockIfNecessary(Position(text, 0),
+ &editing_state);
+ EXPECT_TRUE(editing_state.IsAborted());
+ EXPECT_EQ(
+ "<div><br></div>"
+ "<style>div{-webkit-user-modify:read-only;user-select:none;}</style>"
+ "foo",
+ body->InnerHTMLAsString());
+}
+
+TEST_F(CompositeEditCommandTest, InsertNodeOnDisconnectedParent) {
+ SetBodyContent("<p><b></b></p>");
+ SampleCommand& sample = *new SampleCommand(GetDocument());
+ Node* insert_child = GetDocument().QuerySelector("b");
+ Element* ref_child = GetDocument().QuerySelector("p");
+ ref_child->remove();
+ EditingState editing_state;
+ // editing state should abort here.
+ sample.InsertNodeBefore(insert_child, ref_child, &editing_state);
+ EXPECT_TRUE(editing_state.IsAborted());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/create_link_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/create_link_command.cc
new file mode 100644
index 00000000000..a33e03bfd4e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/create_link_command.cc
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2006 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/create_link_command.h"
+
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/html/html_anchor_element.h"
+
+namespace blink {
+
+CreateLinkCommand::CreateLinkCommand(Document& document, const String& url)
+ : CompositeEditCommand(document) {
+ url_ = url;
+}
+
+void CreateLinkCommand::DoApply(EditingState* editing_state) {
+ if (EndingSelection().IsNone())
+ return;
+
+ HTMLAnchorElement* anchor_element = HTMLAnchorElement::Create(GetDocument());
+ anchor_element->SetHref(AtomicString(url_));
+
+ if (EndingSelection().IsRange()) {
+ ApplyStyledElement(anchor_element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ } else {
+ InsertNodeAt(anchor_element, EndingVisibleSelection().Start(),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ Text* text_node = Text::Create(GetDocument(), url_);
+ AppendNode(text_node, anchor_element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::InParentBeforeNode(*anchor_element))
+ .Extend(Position::InParentAfterNode(*anchor_element))
+ .Build()));
+ }
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/create_link_command.h b/chromium/third_party/blink/renderer/core/editing/commands/create_link_command.h
new file mode 100644
index 00000000000..27c695f045b
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/create_link_command.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_CREATE_LINK_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_CREATE_LINK_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+namespace blink {
+
+class CreateLinkCommand final : public CompositeEditCommand {
+ public:
+ static CreateLinkCommand* Create(Document& document, const String& link_url) {
+ return new CreateLinkCommand(document, link_url);
+ }
+
+ private:
+ CreateLinkCommand(Document&, const String& link_url);
+
+ void DoApply(EditingState*) override;
+
+ String url_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_CREATE_LINK_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/delete_from_text_node_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/delete_from_text_node_command.cc
new file mode 100644
index 00000000000..337d0910b8d
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/delete_from_text_node_command.cc
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2005, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/delete_from_text_node_command.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+
+namespace blink {
+
+DeleteFromTextNodeCommand::DeleteFromTextNodeCommand(Text* node,
+ unsigned offset,
+ unsigned count)
+ : SimpleEditCommand(node->GetDocument()),
+ node_(node),
+ offset_(offset),
+ count_(count) {
+ DCHECK(node_);
+ DCHECK_LE(offset_, node_->length());
+ DCHECK_LE(offset_ + count_, node_->length());
+}
+
+void DeleteFromTextNodeCommand::DoApply(EditingState*) {
+ DCHECK(node_);
+
+ GetDocument().UpdateStyleAndLayoutTree();
+ if (!HasEditableStyle(*node_))
+ return;
+
+ DummyExceptionStateForTesting exception_state;
+ text_ = node_->substringData(offset_, count_, exception_state);
+ if (exception_state.HadException())
+ return;
+
+ node_->deleteData(offset_, count_, exception_state);
+}
+
+void DeleteFromTextNodeCommand::DoUnapply() {
+ DCHECK(node_);
+
+ if (!HasEditableStyle(*node_))
+ return;
+
+ node_->insertData(offset_, text_, IGNORE_EXCEPTION_FOR_TESTING);
+}
+
+void DeleteFromTextNodeCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(node_);
+ SimpleEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/delete_from_text_node_command.h b/chromium/third_party/blink/renderer/core/editing/commands/delete_from_text_node_command.h
new file mode 100644
index 00000000000..a667f29d770
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/delete_from_text_node_command.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_DELETE_FROM_TEXT_NODE_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_DELETE_FROM_TEXT_NODE_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/edit_command.h"
+
+namespace blink {
+
+class Text;
+
+class DeleteFromTextNodeCommand final : public SimpleEditCommand {
+ public:
+ static DeleteFromTextNodeCommand* Create(Text* node,
+ unsigned offset,
+ unsigned count) {
+ return new DeleteFromTextNodeCommand(node, offset, count);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ DeleteFromTextNodeCommand(Text*, unsigned offset, unsigned count);
+
+ void DoApply(EditingState*) override;
+ void DoUnapply() override;
+
+ Member<Text> node_;
+ unsigned offset_;
+ unsigned count_;
+ String text_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_DELETE_FROM_TEXT_NODE_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/delete_selection_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/delete_selection_command.cc
new file mode 100644
index 00000000000..5b58183c1b2
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/delete_selection_command.cc
@@ -0,0 +1,1277 @@
+/*
+ * Copyright (C) 2005 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/delete_selection_command.h"
+
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/editing_boundary.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/local_caret_rect.h"
+#include "third_party/blink/renderer/core/editing/relocatable_position.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
+#include "third_party/blink/renderer/core/html/html_br_element.h"
+#include "third_party/blink/renderer/core/html/html_style_element.h"
+#include "third_party/blink/renderer/core/html/html_table_row_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/layout/layout_table_cell.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+static bool IsTableCellEmpty(Node* cell) {
+ DCHECK(cell);
+ DCHECK(IsTableCell(cell)) << cell;
+ return VisiblePosition::FirstPositionInNode(*cell).DeepEquivalent() ==
+ VisiblePosition::LastPositionInNode(*cell).DeepEquivalent();
+}
+
+static bool IsTableRowEmpty(Node* row) {
+ if (!IsHTMLTableRowElement(row))
+ return false;
+
+ row->GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ for (Node* child = row->firstChild(); child; child = child->nextSibling()) {
+ if (IsTableCell(child) && !IsTableCellEmpty(child))
+ return false;
+ }
+ return true;
+}
+
+static bool CanMergeListElements(Element* first_list, Element* second_list) {
+ if (!first_list || !second_list || first_list == second_list)
+ return false;
+
+ return CanMergeLists(*first_list, *second_list);
+}
+
+DeleteSelectionCommand::DeleteSelectionCommand(
+ Document& document,
+ const DeleteSelectionOptions& options,
+ InputEvent::InputType input_type,
+ const Position& reference_move_position)
+ : CompositeEditCommand(document),
+ options_(options),
+ has_selection_to_delete_(false),
+ merge_blocks_after_delete_(options.IsMergeBlocksAfterDelete()),
+ input_type_(input_type),
+ reference_move_position_(reference_move_position) {}
+
+DeleteSelectionCommand::DeleteSelectionCommand(
+ const VisibleSelection& selection,
+ const DeleteSelectionOptions& options,
+ InputEvent::InputType input_type)
+ : CompositeEditCommand(*selection.Start().GetDocument()),
+ options_(options),
+ has_selection_to_delete_(true),
+ merge_blocks_after_delete_(options.IsMergeBlocksAfterDelete()),
+ input_type_(input_type),
+ selection_to_delete_(selection) {}
+
+void DeleteSelectionCommand::InitializeStartEnd(Position& start,
+ Position& end) {
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ GetDocument().Lifecycle());
+
+ HTMLElement* start_special_container = nullptr;
+ HTMLElement* end_special_container = nullptr;
+
+ start = selection_to_delete_.Start();
+ end = selection_to_delete_.End();
+
+ // For HRs, we'll get a position at (HR,1) when hitting delete from the
+ // beginning of the previous line, or (HR,0) when forward deleting, but in
+ // these cases, we want to delete it, so manually expand the selection
+ if (IsHTMLHRElement(*start.AnchorNode()))
+ start = Position::BeforeNode(*start.AnchorNode());
+ else if (IsHTMLHRElement(*end.AnchorNode()))
+ end = Position::AfterNode(*end.AnchorNode());
+
+ // FIXME: This is only used so that moveParagraphs can avoid the bugs in
+ // special element expansion.
+ if (!options_.IsExpandForSpecialElements())
+ return;
+
+ while (1) {
+ start_special_container = nullptr;
+ end_special_container = nullptr;
+
+ Position s =
+ PositionBeforeContainingSpecialElement(start, &start_special_container);
+ Position e =
+ PositionAfterContainingSpecialElement(end, &end_special_container);
+
+ if (!start_special_container && !end_special_container)
+ break;
+
+ if (CreateVisiblePosition(start).DeepEquivalent() !=
+ selection_to_delete_.VisibleStart().DeepEquivalent() ||
+ CreateVisiblePosition(end).DeepEquivalent() !=
+ selection_to_delete_.VisibleEnd().DeepEquivalent())
+ break;
+
+ // If we're going to expand to include the startSpecialContainer, it must be
+ // fully selected.
+ if (start_special_container && !end_special_container &&
+ ComparePositions(Position::InParentAfterNode(*start_special_container),
+ end) > -1)
+ break;
+
+ // If we're going to expand to include the endSpecialContainer, it must be
+ // fully selected.
+ if (end_special_container && !start_special_container &&
+ ComparePositions(
+ start, Position::InParentBeforeNode(*end_special_container)) > -1)
+ break;
+
+ if (start_special_container &&
+ start_special_container->IsDescendantOf(end_special_container)) {
+ // Don't adjust the end yet, it is the end of a special element that
+ // contains the start special element (which may or may not be fully
+ // selected).
+ start = s;
+ } else if (end_special_container &&
+ end_special_container->IsDescendantOf(start_special_container)) {
+ // Don't adjust the start yet, it is the start of a special element that
+ // contains the end special element (which may or may not be fully
+ // selected).
+ end = e;
+ } else {
+ start = s;
+ end = e;
+ }
+ }
+}
+
+void DeleteSelectionCommand::SetStartingSelectionOnSmartDelete(
+ const Position& start,
+ const Position& end) {
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ GetDocument().Lifecycle());
+
+ const bool is_base_first = StartingSelection().IsBaseFirst();
+ // TODO(yosin): We should not call |createVisiblePosition()| here and use
+ // |start| and |end| as base/extent since |VisibleSelection| also calls
+ // |createVisiblePosition()| during construction.
+ // Because of |newBase.affinity()| can be |Upstream|, we can't simply
+ // use |start| and |end| here.
+ VisiblePosition new_base = CreateVisiblePosition(is_base_first ? start : end);
+ VisiblePosition new_extent =
+ CreateVisiblePosition(is_base_first ? end : start);
+ SelectionInDOMTree::Builder builder;
+ builder.SetAffinity(new_base.Affinity())
+ .SetBaseAndExtentDeprecated(new_base.DeepEquivalent(),
+ new_extent.DeepEquivalent());
+ const VisibleSelection& visible_selection =
+ CreateVisibleSelection(builder.Build());
+ SetStartingSelection(
+ SelectionForUndoStep::From(visible_selection.AsSelection()));
+}
+
+// This assumes that it starts in editable content.
+static Position TrailingWhitespacePosition(const Position& position,
+ WhitespacePositionOption option) {
+ DCHECK(!NeedsLayoutTreeUpdate(position));
+ DCHECK(IsEditablePosition(position)) << position;
+ if (position.IsNull())
+ return Position();
+
+ const VisiblePosition visible_position = CreateVisiblePosition(position);
+ const UChar character_after_visible_position =
+ CharacterAfter(visible_position);
+ const bool is_space =
+ option == kConsiderNonCollapsibleWhitespace
+ ? (IsSpaceOrNewline(character_after_visible_position) ||
+ character_after_visible_position == kNoBreakSpaceCharacter)
+ : IsCollapsibleWhitespace(character_after_visible_position);
+ // The space must not be in another paragraph and it must be editable.
+ if (is_space && !IsEndOfParagraph(visible_position) &&
+ NextPositionOf(visible_position, kCannotCrossEditingBoundary).IsNotNull())
+ return position;
+ return Position();
+}
+
+void DeleteSelectionCommand::InitializePositionData(
+ EditingState* editing_state) {
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ GetDocument().Lifecycle());
+
+ Position start, end;
+ InitializeStartEnd(start, end);
+ DCHECK(start.IsNotNull());
+ DCHECK(end.IsNotNull());
+ if (!IsEditablePosition(start)) {
+ editing_state->Abort();
+ return;
+ }
+ if (!IsEditablePosition(end)) {
+ Node* highest_root = HighestEditableRoot(start);
+ DCHECK(highest_root);
+ end = LastEditablePositionBeforePositionInRoot(end, *highest_root);
+ }
+
+ upstream_start_ = MostBackwardCaretPosition(start);
+ downstream_start_ = MostForwardCaretPosition(start);
+ upstream_end_ = MostBackwardCaretPosition(end);
+ downstream_end_ = MostForwardCaretPosition(end);
+
+ start_root_ = RootEditableElementOf(start);
+ end_root_ = RootEditableElementOf(end);
+
+ start_table_row_ =
+ ToHTMLTableRowElement(EnclosingNodeOfType(start, &IsHTMLTableRowElement));
+ end_table_row_ =
+ ToHTMLTableRowElement(EnclosingNodeOfType(end, &IsHTMLTableRowElement));
+
+ // Don't move content out of a table cell.
+ // If the cell is non-editable, enclosingNodeOfType won't return it by
+ // default, so tell that function that we don't care if it returns
+ // non-editable nodes.
+ Node* start_cell = EnclosingNodeOfType(upstream_start_, &IsTableCell,
+ kCanCrossEditingBoundary);
+ Node* end_cell = EnclosingNodeOfType(downstream_end_, &IsTableCell,
+ kCanCrossEditingBoundary);
+ // FIXME: This isn't right. A borderless table with two rows and a single
+ // column would appear as two paragraphs.
+ if (end_cell && end_cell != start_cell)
+ merge_blocks_after_delete_ = false;
+
+ // Usually the start and the end of the selection to delete are pulled
+ // together as a result of the deletion. Sometimes they aren't (like when no
+ // merge is requested), so we must choose one position to hold the caret
+ // and receive the placeholder after deletion.
+ VisiblePosition visible_end = CreateVisiblePosition(downstream_end_);
+ if (merge_blocks_after_delete_ && !IsEndOfParagraph(visible_end))
+ ending_position_ = downstream_end_;
+ else
+ ending_position_ = downstream_start_;
+
+ // We don't want to merge into a block if it will mean changing the quote
+ // level of content after deleting selections that contain a whole number
+ // paragraphs plus a line break, since it is unclear to most users that such a
+ // selection actually ends at the start of the next paragraph. This matches
+ // TextEdit behavior for indented paragraphs.
+ // Only apply this rule if the endingSelection is a range selection. If it is
+ // a caret, then other operations have created the selection we're deleting
+ // (like the process of creating a selection to delete during a backspace),
+ // and the user isn't in the situation described above.
+ if (NumEnclosingMailBlockquotes(start) != NumEnclosingMailBlockquotes(end) &&
+ IsStartOfParagraph(visible_end) &&
+ IsStartOfParagraph(CreateVisiblePosition(start)) &&
+ EndingSelection().IsRange()) {
+ merge_blocks_after_delete_ = false;
+ prune_start_block_if_necessary_ = true;
+ }
+
+ // Handle leading and trailing whitespace, as well as smart delete adjustments
+ // to the selection
+ leading_whitespace_ = LeadingCollapsibleWhitespacePosition(
+ upstream_start_, selection_to_delete_.Affinity());
+ trailing_whitespace_ = TrailingWhitespacePosition(
+ downstream_end_, kNotConsiderNonCollapsibleWhitespace);
+
+ if (options_.IsSmartDelete()) {
+ // skip smart delete if the selection to delete already starts or ends with
+ // whitespace
+ Position pos =
+ CreateVisiblePosition(upstream_start_, selection_to_delete_.Affinity())
+ .DeepEquivalent();
+ bool skip_smart_delete =
+ TrailingWhitespacePosition(pos, kConsiderNonCollapsibleWhitespace)
+ .IsNotNull();
+ if (!skip_smart_delete) {
+ skip_smart_delete = LeadingCollapsibleWhitespacePosition(
+ downstream_end_, TextAffinity::kDefault,
+ kConsiderNonCollapsibleWhitespace)
+ .IsNotNull();
+ }
+
+ // extend selection upstream if there is whitespace there
+ bool has_leading_whitespace_before_adjustment =
+ LeadingCollapsibleWhitespacePosition(upstream_start_,
+ selection_to_delete_.Affinity(),
+ kConsiderNonCollapsibleWhitespace)
+ .IsNotNull();
+ if (!skip_smart_delete && has_leading_whitespace_before_adjustment) {
+ VisiblePosition visible_pos =
+ PreviousPositionOf(CreateVisiblePosition(upstream_start_));
+ pos = visible_pos.DeepEquivalent();
+ // Expand out one character upstream for smart delete and recalculate
+ // positions based on this change.
+ upstream_start_ = MostBackwardCaretPosition(pos);
+ downstream_start_ = MostForwardCaretPosition(pos);
+ leading_whitespace_ = LeadingCollapsibleWhitespacePosition(
+ upstream_start_, visible_pos.Affinity());
+
+ SetStartingSelectionOnSmartDelete(upstream_start_, upstream_end_);
+ }
+
+ // trailing whitespace is only considered for smart delete if there is no
+ // leading whitespace, as in the case where you double-click the first word
+ // of a paragraph.
+ if (!skip_smart_delete && !has_leading_whitespace_before_adjustment &&
+ TrailingWhitespacePosition(downstream_end_,
+ kConsiderNonCollapsibleWhitespace)
+ .IsNotNull()) {
+ // Expand out one character downstream for smart delete and recalculate
+ // positions based on this change.
+ pos = NextPositionOf(CreateVisiblePosition(downstream_end_))
+ .DeepEquivalent();
+ upstream_end_ = MostBackwardCaretPosition(pos);
+ downstream_end_ = MostForwardCaretPosition(pos);
+ trailing_whitespace_ = TrailingWhitespacePosition(
+ downstream_end_, kNotConsiderNonCollapsibleWhitespace);
+
+ SetStartingSelectionOnSmartDelete(downstream_start_, downstream_end_);
+ }
+ }
+
+ // We must pass call parentAnchoredEquivalent on the positions since some
+ // editing positions that appear inside their nodes aren't really inside them.
+ // [hr, 0] is one example.
+ // FIXME: parentAnchoredEquivalent should eventually be moved into enclosing
+ // element getters like the one below, since editing functions should
+ // obviously accept editing positions.
+ // FIXME: Passing false to enclosingNodeOfType tells it that it's OK to return
+ // a non-editable node. This was done to match existing behavior, but it
+ // seems wrong.
+ start_block_ =
+ EnclosingNodeOfType(downstream_start_.ParentAnchoredEquivalent(),
+ &IsEnclosingBlock, kCanCrossEditingBoundary);
+ end_block_ = EnclosingNodeOfType(upstream_end_.ParentAnchoredEquivalent(),
+ &IsEnclosingBlock, kCanCrossEditingBoundary);
+}
+
+// We don't want to inherit style from an element which can't have contents.
+static bool ShouldNotInheritStyleFrom(const Node& node) {
+ return !node.CanContainRangeEndPoint();
+}
+
+void DeleteSelectionCommand::SaveTypingStyleState() {
+ // A common case is deleting characters that are all from the same text node.
+ // In that case, the style at the start of the selection before deletion will
+ // be the same as the style at the start of the selection after deletion
+ // (since those two positions will be identical). Therefore there is no need
+ // to save the typing style at the start of the selection, nor is there a
+ // reason to compute the style at the start of the selection after deletion
+ // (see the early return in calculateTypingStyleAfterDelete).
+ if (upstream_start_.AnchorNode() == downstream_end_.AnchorNode() &&
+ upstream_start_.AnchorNode()->IsTextNode())
+ return;
+
+ if (ShouldNotInheritStyleFrom(*selection_to_delete_.Start().AnchorNode()))
+ return;
+
+ // Figure out the typing style in effect before the delete is done.
+ typing_style_ = EditingStyle::Create(
+ selection_to_delete_.Start(), EditingStyle::kEditingPropertiesInEffect);
+ typing_style_->RemoveStyleAddedByElement(
+ EnclosingAnchorElement(selection_to_delete_.Start()));
+
+ // If we're deleting into a Mail blockquote, save the style at end() instead
+ // of start(). We'll use this later in computeTypingStyleAfterDelete if we end
+ // up outside of a Mail blockquote
+ if (EnclosingNodeOfType(selection_to_delete_.Start(),
+ IsMailHTMLBlockquoteElement)) {
+ delete_into_blockquote_style_ =
+ EditingStyle::Create(selection_to_delete_.End());
+ return;
+ }
+ delete_into_blockquote_style_ = nullptr;
+}
+
+bool DeleteSelectionCommand::HandleSpecialCaseBRDelete(
+ EditingState* editing_state) {
+ Node* node_after_upstream_start = upstream_start_.ComputeNodeAfterPosition();
+ Node* node_after_downstream_start =
+ downstream_start_.ComputeNodeAfterPosition();
+ // Upstream end will appear before BR due to canonicalization
+ Node* node_after_upstream_end = upstream_end_.ComputeNodeAfterPosition();
+
+ if (!node_after_upstream_start || !node_after_downstream_start)
+ return false;
+
+ // Check for special-case where the selection contains only a BR on a line by
+ // itself after another BR.
+ bool upstream_start_is_br = IsHTMLBRElement(*node_after_upstream_start);
+ bool downstream_start_is_br = IsHTMLBRElement(*node_after_downstream_start);
+ bool is_br_on_line_by_itself =
+ upstream_start_is_br && downstream_start_is_br &&
+ node_after_downstream_start == node_after_upstream_end;
+ if (is_br_on_line_by_itself) {
+ RemoveNode(node_after_downstream_start, editing_state);
+ return true;
+ }
+
+ // FIXME: This code doesn't belong in here.
+ // We detect the case where the start is an empty line consisting of BR not
+ // wrapped in a block element.
+ if (upstream_start_is_br && downstream_start_is_br) {
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (!(IsStartOfBlock(
+ VisiblePosition::BeforeNode(*node_after_upstream_start)) &&
+ IsEndOfBlock(
+ VisiblePosition::AfterNode(*node_after_upstream_start)))) {
+ starts_at_empty_line_ = true;
+ ending_position_ = downstream_end_;
+ }
+ }
+
+ return false;
+}
+
+static Position FirstEditablePositionInNode(Node* node) {
+ DCHECK(node);
+ Node* next = node;
+ while (next && !HasEditableStyle(*next))
+ next = NodeTraversal::Next(*next, node);
+ return next ? FirstPositionInOrBeforeNode(*next) : Position();
+}
+
+void DeleteSelectionCommand::RemoveNode(
+ Node* node,
+ EditingState* editing_state,
+ ShouldAssumeContentIsAlwaysEditable
+ should_assume_content_is_always_editable) {
+ if (!node)
+ return;
+
+ if (start_root_ != end_root_ && !(node->IsDescendantOf(start_root_.Get()) &&
+ node->IsDescendantOf(end_root_.Get()))) {
+ // If a node is not in both the start and end editable roots, remove it only
+ // if its inside an editable region.
+ if (!HasEditableStyle(*node->parentNode())) {
+ // Don't remove non-editable atomic nodes.
+ if (!node->hasChildren())
+ return;
+ // Search this non-editable region for editable regions to empty.
+ Node* child = node->firstChild();
+ while (child) {
+ Node* next_child = child->nextSibling();
+ RemoveNode(child, editing_state,
+ should_assume_content_is_always_editable);
+ if (editing_state->IsAborted())
+ return;
+ // Bail if nextChild is no longer node's child.
+ if (next_child && next_child->parentNode() != node)
+ return;
+ child = next_child;
+ }
+
+ // Don't remove editable regions that are inside non-editable ones, just
+ // clear them.
+ return;
+ }
+ }
+
+ if (IsTableStructureNode(node) || IsRootEditableElement(*node)) {
+ // Do not remove an element of table structure; remove its contents.
+ // Likewise for the root editable element.
+ Node* child = node->firstChild();
+ while (child) {
+ Node* remove = child;
+ child = child->nextSibling();
+ RemoveNode(remove, editing_state,
+ should_assume_content_is_always_editable);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ // Make sure empty cell has some height, if a placeholder can be inserted.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ LayoutObject* r = node->GetLayoutObject();
+ if (r && r->IsTableCell() && ToLayoutTableCell(r)->ContentHeight() <= 0) {
+ Position first_editable_position = FirstEditablePositionInNode(node);
+ if (first_editable_position.IsNotNull())
+ InsertBlockPlaceholder(first_editable_position, editing_state);
+ }
+ return;
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (node == start_block_) {
+ VisiblePosition previous = PreviousPositionOf(
+ VisiblePosition::FirstPositionInNode(*start_block_.Get()));
+ if (previous.IsNotNull() && !IsEndOfBlock(previous))
+ need_placeholder_ = true;
+ }
+ if (node == end_block_) {
+ VisiblePosition next =
+ NextPositionOf(VisiblePosition::LastPositionInNode(*end_block_.Get()));
+ if (next.IsNotNull() && !IsStartOfBlock(next))
+ need_placeholder_ = true;
+ }
+
+ // FIXME: Update the endpoints of the range being deleted.
+ ending_position_ = ComputePositionForNodeRemoval(ending_position_, *node);
+ leading_whitespace_ =
+ ComputePositionForNodeRemoval(leading_whitespace_, *node);
+ trailing_whitespace_ =
+ ComputePositionForNodeRemoval(trailing_whitespace_, *node);
+
+ CompositeEditCommand::RemoveNode(node, editing_state,
+ should_assume_content_is_always_editable);
+}
+
+static void UpdatePositionForTextRemoval(Text* node,
+ int offset,
+ int count,
+ Position& position) {
+ if (!position.IsOffsetInAnchor() || position.ComputeContainerNode() != node)
+ return;
+
+ if (position.OffsetInContainerNode() > offset + count)
+ position = Position(position.ComputeContainerNode(),
+ position.OffsetInContainerNode() - count);
+ else if (position.OffsetInContainerNode() > offset)
+ position = Position(position.ComputeContainerNode(), offset);
+}
+
+void DeleteSelectionCommand::DeleteTextFromNode(Text* node,
+ unsigned offset,
+ unsigned count) {
+ // FIXME: Update the endpoints of the range being deleted.
+ UpdatePositionForTextRemoval(node, offset, count, ending_position_);
+ UpdatePositionForTextRemoval(node, offset, count, leading_whitespace_);
+ UpdatePositionForTextRemoval(node, offset, count, trailing_whitespace_);
+ UpdatePositionForTextRemoval(node, offset, count, downstream_end_);
+
+ CompositeEditCommand::DeleteTextFromNode(node, offset, count);
+}
+
+void DeleteSelectionCommand::
+ MakeStylingElementsDirectChildrenOfEditableRootToPreventStyleLoss(
+ EditingState* editing_state) {
+ Range* range = CreateRange(selection_to_delete_.ToNormalizedEphemeralRange());
+ Node* node = range->FirstNode();
+ while (node && node != range->PastLastNode()) {
+ Node* next_node = NodeTraversal::Next(*node);
+ if (IsHTMLStyleElement(*node) || IsHTMLLinkElement(*node)) {
+ next_node = NodeTraversal::NextSkippingChildren(*node);
+ Element* element = RootEditableElement(*node);
+ if (element) {
+ RemoveNode(node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ AppendNode(node, element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+ node = next_node;
+ }
+}
+
+void DeleteSelectionCommand::HandleGeneralDelete(EditingState* editing_state) {
+ if (upstream_start_.IsNull())
+ return;
+
+ int start_offset = upstream_start_.ComputeEditingOffset();
+ Node* start_node = upstream_start_.AnchorNode();
+ DCHECK(start_node);
+
+ MakeStylingElementsDirectChildrenOfEditableRootToPreventStyleLoss(
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ // Never remove the start block unless it's a table, in which case we won't
+ // merge content in.
+ if (start_node == start_block_.Get() && !start_offset &&
+ CanHaveChildrenForEditing(start_node) &&
+ !IsHTMLTableElement(*start_node)) {
+ start_offset = 0;
+ start_node = NodeTraversal::Next(*start_node);
+ if (!start_node)
+ return;
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (start_offset >= CaretMaxOffset(start_node) && start_node->IsTextNode()) {
+ Text* text = ToText(start_node);
+ if (text->length() > (unsigned)CaretMaxOffset(start_node))
+ DeleteTextFromNode(text, CaretMaxOffset(start_node),
+ text->length() - CaretMaxOffset(start_node));
+ }
+
+ if (start_offset >= EditingStrategy::LastOffsetForEditing(start_node)) {
+ start_node = NodeTraversal::NextSkippingChildren(*start_node);
+ start_offset = 0;
+ }
+
+ // Done adjusting the start. See if we're all done.
+ if (!start_node)
+ return;
+
+ if (start_node == downstream_end_.AnchorNode()) {
+ if (downstream_end_.ComputeEditingOffset() - start_offset > 0) {
+ if (start_node->IsTextNode()) {
+ // in a text node that needs to be trimmed
+ Text* text = ToText(start_node);
+ DeleteTextFromNode(
+ text, start_offset,
+ downstream_end_.ComputeOffsetInContainerNode() - start_offset);
+ } else {
+ RemoveChildrenInRange(start_node, start_offset,
+ downstream_end_.ComputeEditingOffset(),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ ending_position_ = upstream_start_;
+ }
+ // We should update layout to associate |start_node| to layout object.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ }
+
+ // The selection to delete is all in one node.
+ if (!start_node->GetLayoutObject() ||
+ (!start_offset && downstream_end_.AtLastEditingPositionForNode())) {
+ RemoveNode(start_node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ } else {
+ bool start_node_was_descendant_of_end_node =
+ upstream_start_.AnchorNode()->IsDescendantOf(
+ downstream_end_.AnchorNode());
+ // The selection to delete spans more than one node.
+ Node* node(start_node);
+
+ if (start_offset > 0) {
+ if (start_node->IsTextNode()) {
+ // in a text node that needs to be trimmed
+ Text* text = ToText(node);
+ DeleteTextFromNode(text, start_offset, text->length() - start_offset);
+ node = NodeTraversal::Next(*node);
+ } else {
+ node = NodeTraversal::ChildAt(*start_node, start_offset);
+ }
+ } else if (start_node == upstream_end_.AnchorNode() &&
+ start_node->IsTextNode()) {
+ Text* text = ToText(upstream_end_.AnchorNode());
+ DeleteTextFromNode(text, 0, upstream_end_.ComputeOffsetInContainerNode());
+ }
+
+ // handle deleting all nodes that are completely selected
+ while (node && node != downstream_end_.AnchorNode()) {
+ if (ComparePositions(FirstPositionInOrBeforeNode(*node),
+ downstream_end_) >= 0) {
+ // NodeTraversal::nextSkippingChildren just blew past the end position,
+ // so stop deleting
+ node = nullptr;
+ } else if (!downstream_end_.AnchorNode()->IsDescendantOf(node)) {
+ Node* next_node = NodeTraversal::NextSkippingChildren(*node);
+ // if we just removed a node from the end container, update end position
+ // so the check above will work
+ downstream_end_ = ComputePositionForNodeRemoval(downstream_end_, *node);
+ RemoveNode(node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ node = next_node;
+ } else {
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ Node& n = NodeTraversal::LastWithinOrSelf(*node);
+ if (downstream_end_.AnchorNode() == n &&
+ downstream_end_.ComputeEditingOffset() >= CaretMaxOffset(&n)) {
+ RemoveNode(node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ node = nullptr;
+ } else {
+ node = NodeTraversal::Next(*node);
+ }
+ }
+ }
+
+ // TODO(editing-dev): Hoist updateStyleAndLayoutIgnorePendingStylesheets
+ // to caller. See http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (downstream_end_.AnchorNode() != start_node &&
+ !upstream_start_.AnchorNode()->IsDescendantOf(
+ downstream_end_.AnchorNode()) &&
+ downstream_end_.IsConnected() &&
+ downstream_end_.ComputeEditingOffset() >=
+ CaretMinOffset(downstream_end_.AnchorNode())) {
+ if (downstream_end_.AtLastEditingPositionForNode() &&
+ !CanHaveChildrenForEditing(downstream_end_.AnchorNode())) {
+ // The node itself is fully selected, not just its contents. Delete it.
+ RemoveNode(downstream_end_.AnchorNode(), editing_state);
+ } else {
+ if (downstream_end_.AnchorNode()->IsTextNode()) {
+ // in a text node that needs to be trimmed
+ Text* text = ToText(downstream_end_.AnchorNode());
+ if (downstream_end_.ComputeEditingOffset() > 0) {
+ DeleteTextFromNode(text, 0, downstream_end_.ComputeEditingOffset());
+ }
+ // Remove children of m_downstreamEnd.anchorNode() that come after
+ // m_upstreamStart. Don't try to remove children if m_upstreamStart
+ // was inside m_downstreamEnd.anchorNode() and m_upstreamStart has
+ // been removed from the document, because then we don't know how many
+ // children to remove.
+ // FIXME: Make m_upstreamStart a position we update as we remove
+ // content, then we can always know which children to remove.
+ } else if (!(start_node_was_descendant_of_end_node &&
+ !upstream_start_.IsConnected())) {
+ int offset = 0;
+ if (upstream_start_.AnchorNode()->IsDescendantOf(
+ downstream_end_.AnchorNode())) {
+ Node* n = upstream_start_.AnchorNode();
+ while (n && n->parentNode() != downstream_end_.AnchorNode())
+ n = n->parentNode();
+ if (n)
+ offset = n->NodeIndex() + 1;
+ }
+ RemoveChildrenInRange(downstream_end_.AnchorNode(), offset,
+ downstream_end_.ComputeEditingOffset(),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ downstream_end_ =
+ Position::EditingPositionOf(downstream_end_.AnchorNode(), offset);
+ }
+ }
+ }
+ }
+}
+
+void DeleteSelectionCommand::FixupWhitespace() {
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (leading_whitespace_.IsNotNull() &&
+ !IsRenderedCharacter(leading_whitespace_) &&
+ leading_whitespace_.AnchorNode()->IsTextNode()) {
+ Text* text_node = ToText(leading_whitespace_.AnchorNode());
+ DCHECK(!text_node->GetLayoutObject() ||
+ text_node->GetLayoutObject()->Style()->CollapseWhiteSpace())
+ << text_node;
+ ReplaceTextInNode(text_node,
+ leading_whitespace_.ComputeOffsetInContainerNode(), 1,
+ NonBreakingSpaceString());
+ }
+ if (trailing_whitespace_.IsNotNull() &&
+ !IsRenderedCharacter(trailing_whitespace_) &&
+ trailing_whitespace_.AnchorNode()->IsTextNode()) {
+ Text* text_node = ToText(trailing_whitespace_.AnchorNode());
+ DCHECK(!text_node->GetLayoutObject() ||
+ text_node->GetLayoutObject()->Style()->CollapseWhiteSpace())
+ << text_node;
+ ReplaceTextInNode(text_node,
+ trailing_whitespace_.ComputeOffsetInContainerNode(), 1,
+ NonBreakingSpaceString());
+ }
+}
+
+// If a selection starts in one block and ends in another, we have to merge to
+// bring content before the start together with content after the end.
+void DeleteSelectionCommand::MergeParagraphs(EditingState* editing_state) {
+ if (!merge_blocks_after_delete_) {
+ if (prune_start_block_if_necessary_) {
+ // We aren't going to merge into the start block, so remove it if it's
+ // empty.
+ Prune(start_block_, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ // Removing the start block during a deletion is usually an indication
+ // that we need a placeholder, but not in this case.
+ need_placeholder_ = false;
+ }
+ return;
+ }
+
+ // It shouldn't have been asked to both try and merge content into the start
+ // block and prune it.
+ DCHECK(!prune_start_block_if_necessary_);
+
+ // FIXME: Deletion should adjust selection endpoints as it removes nodes so
+ // that we never get into this state (4099839).
+ if (!downstream_end_.IsConnected() || !upstream_start_.IsConnected())
+ return;
+
+ // FIXME: The deletion algorithm shouldn't let this happen.
+ if (ComparePositions(upstream_start_, downstream_end_) > 0)
+ return;
+
+ // There's nothing to merge.
+ if (upstream_start_ == downstream_end_)
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ VisiblePosition start_of_paragraph_to_move =
+ CreateVisiblePosition(downstream_end_);
+ VisiblePosition merge_destination = CreateVisiblePosition(upstream_start_);
+
+ // m_downstreamEnd's block has been emptied out by deletion. There is no
+ // content inside of it to move, so just remove it.
+ Element* end_block = EnclosingBlock(downstream_end_.AnchorNode());
+ if (!end_block ||
+ !end_block->contains(
+ start_of_paragraph_to_move.DeepEquivalent().AnchorNode()) ||
+ !start_of_paragraph_to_move.DeepEquivalent().AnchorNode()) {
+ RemoveNode(EnclosingBlock(downstream_end_.AnchorNode()), editing_state);
+ return;
+ }
+
+ RelocatablePosition relocatable_start(
+ start_of_paragraph_to_move.DeepEquivalent());
+
+ // We need to merge into m_upstreamStart's block, but it's been emptied out
+ // and collapsed by deletion.
+ if (!merge_destination.DeepEquivalent().AnchorNode() ||
+ (!merge_destination.DeepEquivalent().AnchorNode()->IsDescendantOf(
+ EnclosingBlock(upstream_start_.ComputeContainerNode())) &&
+ (!merge_destination.DeepEquivalent().AnchorNode()->hasChildren() ||
+ !upstream_start_.ComputeContainerNode()->hasChildren())) ||
+ (starts_at_empty_line_ &&
+ merge_destination.DeepEquivalent() !=
+ start_of_paragraph_to_move.DeepEquivalent())) {
+ InsertNodeAt(HTMLBRElement::Create(GetDocument()), upstream_start_,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ merge_destination = CreateVisiblePosition(upstream_start_);
+ start_of_paragraph_to_move =
+ CreateVisiblePosition(relocatable_start.GetPosition());
+ }
+
+ if (merge_destination.DeepEquivalent() ==
+ start_of_paragraph_to_move.DeepEquivalent())
+ return;
+
+ VisiblePosition end_of_paragraph_to_move =
+ EndOfParagraph(start_of_paragraph_to_move, kCanSkipOverEditingBoundary);
+
+ if (merge_destination.DeepEquivalent() ==
+ end_of_paragraph_to_move.DeepEquivalent())
+ return;
+
+ // If the merge destination and source to be moved are both list items of
+ // different lists, merge them into single list.
+ Node* list_item_in_first_paragraph =
+ EnclosingNodeOfType(upstream_start_, IsListItem);
+ Node* list_item_in_second_paragraph =
+ EnclosingNodeOfType(downstream_end_, IsListItem);
+ if (list_item_in_first_paragraph && list_item_in_second_paragraph &&
+ CanMergeListElements(list_item_in_first_paragraph->parentElement(),
+ list_item_in_second_paragraph->parentElement())) {
+ MergeIdenticalElements(list_item_in_first_paragraph->parentElement(),
+ list_item_in_second_paragraph->parentElement(),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ ending_position_ = merge_destination.DeepEquivalent();
+ return;
+ }
+
+ // The rule for merging into an empty block is: only do so if its farther to
+ // the right.
+ // FIXME: Consider RTL.
+ if (!starts_at_empty_line_ && IsStartOfParagraph(merge_destination) &&
+ AbsoluteCaretBoundsOf(start_of_paragraph_to_move).X() >
+ AbsoluteCaretBoundsOf(merge_destination).X()) {
+ if (IsHTMLBRElement(
+ *MostForwardCaretPosition(merge_destination.DeepEquivalent())
+ .AnchorNode())) {
+ RemoveNodeAndPruneAncestors(
+ MostForwardCaretPosition(merge_destination.DeepEquivalent())
+ .AnchorNode(),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ ending_position_ = relocatable_start.GetPosition();
+ return;
+ }
+ }
+
+ // Block images, tables and horizontal rules cannot be made inline with
+ // content at mergeDestination. If there is any
+ // (!isStartOfParagraph(mergeDestination)), don't merge, just move
+ // the caret to just before the selection we deleted. See
+ // https://bugs.webkit.org/show_bug.cgi?id=25439
+ if (IsRenderedAsNonInlineTableImageOrHR(
+ start_of_paragraph_to_move.DeepEquivalent().AnchorNode()) &&
+ !IsStartOfParagraph(merge_destination)) {
+ ending_position_ = upstream_start_;
+ return;
+ }
+
+ // moveParagraphs will insert placeholders if it removes blocks that would
+ // require their use, don't let block removals that it does cause the
+ // insertion of *another* placeholder.
+ bool need_placeholder = need_placeholder_;
+ bool paragraph_to_merge_is_empty =
+ start_of_paragraph_to_move.DeepEquivalent() ==
+ end_of_paragraph_to_move.DeepEquivalent();
+ MoveParagraph(
+ start_of_paragraph_to_move, end_of_paragraph_to_move, merge_destination,
+ editing_state, kDoNotPreserveSelection,
+ paragraph_to_merge_is_empty ? kDoNotPreserveStyle : kPreserveStyle);
+ if (editing_state->IsAborted())
+ return;
+ need_placeholder_ = need_placeholder;
+ // The endingPosition was likely clobbered by the move, so recompute it
+ // (moveParagraph selects the moved paragraph).
+ ending_position_ = EndingVisibleSelection().Start();
+}
+
+void DeleteSelectionCommand::RemovePreviouslySelectedEmptyTableRows(
+ EditingState* editing_state) {
+ if (end_table_row_ && end_table_row_->isConnected() &&
+ end_table_row_ != start_table_row_) {
+ Node* row = end_table_row_->previousSibling();
+ while (row && row != start_table_row_) {
+ Node* previous_row = row->previousSibling();
+ if (IsTableRowEmpty(row)) {
+ // Use a raw removeNode, instead of DeleteSelectionCommand's,
+ // because that won't remove rows, it only empties them in
+ // preparation for this function.
+ CompositeEditCommand::RemoveNode(row, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ row = previous_row;
+ }
+ }
+
+ // Remove empty rows after the start row.
+ if (start_table_row_ && start_table_row_->isConnected() &&
+ start_table_row_ != end_table_row_) {
+ Node* row = start_table_row_->nextSibling();
+ while (row && row != end_table_row_) {
+ Node* next_row = row->nextSibling();
+ if (IsTableRowEmpty(row)) {
+ CompositeEditCommand::RemoveNode(row, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ row = next_row;
+ }
+ }
+
+ if (end_table_row_ && end_table_row_->isConnected() &&
+ end_table_row_ != start_table_row_) {
+ if (IsTableRowEmpty(end_table_row_.Get())) {
+ // Don't remove m_endTableRow if it's where we're putting the ending
+ // selection.
+ if (!ending_position_.AnchorNode()->IsDescendantOf(
+ end_table_row_.Get())) {
+ // FIXME: We probably shouldn't remove m_endTableRow unless it's
+ // fully selected, even if it is empty. We'll need to start
+ // adjusting the selection endpoints during deletion to know
+ // whether or not m_endTableRow was fully selected here.
+ CompositeEditCommand::RemoveNode(end_table_row_.Get(), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+ }
+}
+
+void DeleteSelectionCommand::CalculateTypingStyleAfterDelete() {
+ // Clearing any previously set typing style and doing an early return.
+ if (!typing_style_) {
+ GetDocument().GetFrame()->GetEditor().ClearTypingStyle();
+ return;
+ }
+
+ // Compute the difference between the style before the delete and the style
+ // now after the delete has been done. Set this style on the frame, so other
+ // editing commands being composed with this one will work, and also cache it
+ // on the command, so the LocalFrame::appliedEditing can set it after the
+ // whole composite command has completed.
+
+ // If we deleted into a blockquote, but are now no longer in a blockquote, use
+ // the alternate typing style
+ if (delete_into_blockquote_style_ &&
+ !EnclosingNodeOfType(ending_position_, IsMailHTMLBlockquoteElement,
+ kCanCrossEditingBoundary))
+ typing_style_ = delete_into_blockquote_style_;
+ delete_into_blockquote_style_ = nullptr;
+
+ typing_style_->PrepareToApplyAt(ending_position_);
+ if (typing_style_->IsEmpty())
+ typing_style_ = nullptr;
+ // This is where we've deleted all traces of a style but not a whole paragraph
+ // (that's handled above). In this case if we start typing, the new characters
+ // should have the same style as the just deleted ones, but, if we change the
+ // selection, come back and start typing that style should be lost. Also see
+ // preserveTypingStyle() below.
+ GetDocument().GetFrame()->GetEditor().SetTypingStyle(typing_style_);
+}
+
+void DeleteSelectionCommand::ClearTransientState() {
+ selection_to_delete_ = VisibleSelection();
+ upstream_start_ = Position();
+ downstream_start_ = Position();
+ upstream_end_ = Position();
+ downstream_end_ = Position();
+ ending_position_ = Position();
+ leading_whitespace_ = Position();
+ trailing_whitespace_ = Position();
+ reference_move_position_ = Position();
+}
+
+// This method removes div elements with no attributes that have only one child
+// or no children at all.
+void DeleteSelectionCommand::RemoveRedundantBlocks(
+ EditingState* editing_state) {
+ Node* node = ending_position_.ComputeContainerNode();
+ Element* root_element = RootEditableElement(*node);
+
+ while (node != root_element) {
+ ABORT_EDITING_COMMAND_IF(!node);
+ if (IsRemovableBlock(node)) {
+ if (node == ending_position_.AnchorNode())
+ UpdatePositionForNodeRemovalPreservingChildren(ending_position_, *node);
+
+ CompositeEditCommand::RemoveNodePreservingChildren(node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ node = ending_position_.AnchorNode();
+ } else {
+ node = node->parentNode();
+ }
+ }
+}
+
+void DeleteSelectionCommand::DoApply(EditingState* editing_state) {
+ // If selection has not been set to a custom selection when the command was
+ // created, use the current ending selection.
+ if (!has_selection_to_delete_)
+ selection_to_delete_ = EndingVisibleSelection();
+
+ if (!selection_to_delete_.IsValidFor(GetDocument()) ||
+ !selection_to_delete_.IsRange() ||
+ !selection_to_delete_.IsContentEditable()) {
+ // editing/execCommand/delete-non-editable-range-crash.html reaches here.
+ return;
+ }
+
+ RelocatablePosition relocatable_reference_position(reference_move_position_);
+
+ // save this to later make the selection with
+ TextAffinity affinity = selection_to_delete_.Affinity();
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ Position downstream_end =
+ MostForwardCaretPosition(selection_to_delete_.End());
+ bool root_will_stay_open_without_placeholder =
+ downstream_end.ComputeContainerNode() ==
+ RootEditableElement(*downstream_end.ComputeContainerNode()) ||
+ (downstream_end.ComputeContainerNode()->IsTextNode() &&
+ downstream_end.ComputeContainerNode()->parentNode() ==
+ RootEditableElement(*downstream_end.ComputeContainerNode()));
+ bool line_break_at_end_of_selection_to_delete =
+ LineBreakExistsAtVisiblePosition(selection_to_delete_.VisibleEnd());
+ need_placeholder_ = !root_will_stay_open_without_placeholder &&
+ IsStartOfParagraph(selection_to_delete_.VisibleStart(),
+ kCanCrossEditingBoundary) &&
+ IsEndOfParagraph(selection_to_delete_.VisibleEnd(),
+ kCanCrossEditingBoundary) &&
+ !line_break_at_end_of_selection_to_delete;
+ if (need_placeholder_) {
+ // Don't need a placeholder when deleting a selection that starts just
+ // before a table and ends inside it (we do need placeholders to hold
+ // open empty cells, but that's handled elsewhere).
+ if (Element* table =
+ TableElementJustAfter(selection_to_delete_.VisibleStart())) {
+ if (selection_to_delete_.End().AnchorNode()->IsDescendantOf(table))
+ need_placeholder_ = false;
+ }
+ }
+
+ // set up our state
+ InitializePositionData(editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ bool line_break_before_start = LineBreakExistsAtVisiblePosition(
+ PreviousPositionOf(CreateVisiblePosition(upstream_start_)));
+
+ // Delete any text that may hinder our ability to fixup whitespace after the
+ // delete
+ DeleteInsignificantTextDownstream(trailing_whitespace_);
+
+ SaveTypingStyleState();
+
+ // deleting just a BR is handled specially, at least because we do not
+ // want to replace it with a placeholder BR!
+ bool br_result = HandleSpecialCaseBRDelete(editing_state);
+ if (editing_state->IsAborted())
+ return;
+ if (br_result) {
+ CalculateTypingStyleAfterDelete();
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ SelectionInDOMTree::Builder builder;
+ builder.SetAffinity(affinity);
+ if (ending_position_.IsNotNull())
+ builder.Collapse(ending_position_);
+ const VisibleSelection& visible_selection =
+ CreateVisibleSelection(builder.Build());
+ SetEndingSelection(
+ SelectionForUndoStep::From(visible_selection.AsSelection()));
+ ClearTransientState();
+ RebalanceWhitespace();
+ return;
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ HandleGeneralDelete(editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ FixupWhitespace();
+
+ MergeParagraphs(editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ RemovePreviouslySelectedEmptyTableRows(editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ if (!need_placeholder_ && root_will_stay_open_without_placeholder) {
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ VisiblePosition visual_ending = CreateVisiblePosition(ending_position_);
+ bool has_placeholder =
+ LineBreakExistsAtVisiblePosition(visual_ending) &&
+ NextPositionOf(visual_ending, kCannotCrossEditingBoundary).IsNull();
+ need_placeholder_ = has_placeholder && line_break_before_start &&
+ !line_break_at_end_of_selection_to_delete;
+ }
+
+ HTMLBRElement* placeholder =
+ need_placeholder_ ? HTMLBRElement::Create(GetDocument()) : nullptr;
+
+ if (placeholder) {
+ if (options_.IsSanitizeMarkup()) {
+ RemoveRedundantBlocks(editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ // handleGeneralDelete cause DOM mutation events so |m_endingPosition|
+ // can be out of document.
+ if (ending_position_.IsConnected()) {
+ InsertNodeAt(placeholder, ending_position_, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+
+ RebalanceWhitespaceAt(ending_position_);
+
+ CalculateTypingStyleAfterDelete();
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ SelectionInDOMTree::Builder builder;
+ builder.SetAffinity(affinity);
+ if (ending_position_.IsNotNull())
+ builder.Collapse(ending_position_);
+ const VisibleSelection& visible_selection =
+ CreateVisibleSelection(builder.Build());
+ SetEndingSelection(
+ SelectionForUndoStep::From(visible_selection.AsSelection()));
+
+ if (relocatable_reference_position.GetPosition().IsNull()) {
+ ClearTransientState();
+ return;
+ }
+
+ // This deletion command is part of a move operation, we need to cleanup after
+ // deletion.
+ reference_move_position_ = relocatable_reference_position.GetPosition();
+ // If the node for the destination has been removed as a result of the
+ // deletion, set the destination to the ending point after the deletion.
+ // Fixes: <rdar://problem/3910425> REGRESSION (Mail): Crash in
+ // ReplaceSelectionCommand; selection is empty, leading to null deref
+ if (!reference_move_position_.IsConnected())
+ reference_move_position_ = EndingVisibleSelection().Start();
+
+ // Move selection shouldn't left empty <li> block.
+ CleanupAfterDeletion(editing_state,
+ CreateVisiblePosition(reference_move_position_));
+ if (editing_state->IsAborted())
+ return;
+
+ ClearTransientState();
+}
+
+InputEvent::InputType DeleteSelectionCommand::GetInputType() const {
+ // |DeleteSelectionCommand| could be used with Cut, Menu Bar deletion and
+ // |TypingCommand|.
+ // 1. Cut and Menu Bar deletion should rely on correct |m_inputType|.
+ // 2. |TypingCommand| will supply the |inputType()|, so |m_inputType| could
+ // default to |InputType::None|.
+ return input_type_;
+}
+
+// Normally deletion doesn't preserve the typing style that was present before
+// it. For example, type a character, Bold, then delete the character and start
+// typing. The Bold typing style shouldn't stick around. Deletion should
+// preserve a typing style that *it* sets, however.
+bool DeleteSelectionCommand::PreservesTypingStyle() const {
+ return typing_style_;
+}
+
+void DeleteSelectionCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(selection_to_delete_);
+ visitor->Trace(upstream_start_);
+ visitor->Trace(downstream_start_);
+ visitor->Trace(upstream_end_);
+ visitor->Trace(downstream_end_);
+ visitor->Trace(ending_position_);
+ visitor->Trace(leading_whitespace_);
+ visitor->Trace(trailing_whitespace_);
+ visitor->Trace(reference_move_position_);
+ visitor->Trace(start_block_);
+ visitor->Trace(end_block_);
+ visitor->Trace(typing_style_);
+ visitor->Trace(delete_into_blockquote_style_);
+ visitor->Trace(start_root_);
+ visitor->Trace(end_root_);
+ visitor->Trace(start_table_row_);
+ visitor->Trace(end_table_row_);
+ visitor->Trace(temporary_placeholder_);
+ CompositeEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/delete_selection_command.h b/chromium/third_party/blink/renderer/core/editing/commands/delete_selection_command.h
new file mode 100644
index 00000000000..5f3f4f093a2
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/delete_selection_command.h
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_DELETE_SELECTION_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_DELETE_SELECTION_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+#include "third_party/blink/renderer/core/editing/commands/delete_selection_options.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+
+namespace blink {
+
+class EditingStyle;
+class HTMLTableRowElement;
+
+class CORE_EXPORT DeleteSelectionCommand final : public CompositeEditCommand {
+ public:
+ static DeleteSelectionCommand* Create(
+ Document& document,
+ const DeleteSelectionOptions& options,
+ InputEvent::InputType input_type = InputEvent::InputType::kNone,
+ const Position& reference_move_position = Position()) {
+ return new DeleteSelectionCommand(document, options, input_type,
+ reference_move_position);
+ }
+ static DeleteSelectionCommand* Create(
+ const VisibleSelection& selection,
+ const DeleteSelectionOptions& options,
+ InputEvent::InputType input_type = InputEvent::InputType::kNone) {
+ return new DeleteSelectionCommand(selection, options, input_type);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ DeleteSelectionCommand(Document&,
+ const DeleteSelectionOptions&,
+ InputEvent::InputType,
+ const Position& reference_move_position);
+ DeleteSelectionCommand(const VisibleSelection&,
+ const DeleteSelectionOptions&,
+ InputEvent::InputType);
+
+ void DoApply(EditingState*) override;
+ InputEvent::InputType GetInputType() const override;
+
+ bool PreservesTypingStyle() const override;
+
+ void InitializeStartEnd(Position&, Position&);
+ void SetStartingSelectionOnSmartDelete(const Position&, const Position&);
+ void InitializePositionData(EditingState*);
+ void SaveTypingStyleState();
+ bool HandleSpecialCaseBRDelete(EditingState*);
+ void HandleGeneralDelete(EditingState*);
+ void FixupWhitespace();
+ void MergeParagraphs(EditingState*);
+ void RemovePreviouslySelectedEmptyTableRows(EditingState*);
+ void CalculateTypingStyleAfterDelete();
+ void ClearTransientState();
+ void MakeStylingElementsDirectChildrenOfEditableRootToPreventStyleLoss(
+ EditingState*);
+ void RemoveNode(Node*,
+ EditingState*,
+ ShouldAssumeContentIsAlwaysEditable =
+ kDoNotAssumeContentIsAlwaysEditable) override;
+ void DeleteTextFromNode(Text*, unsigned, unsigned) override;
+ void RemoveRedundantBlocks(EditingState*);
+
+ const DeleteSelectionOptions options_;
+ const bool has_selection_to_delete_;
+ bool merge_blocks_after_delete_;
+ bool need_placeholder_ = false;
+ bool prune_start_block_if_necessary_ = false;
+ bool starts_at_empty_line_ = false;
+ const InputEvent::InputType input_type_;
+
+ // This data is transient and should be cleared at the end of the doApply
+ // function.
+ VisibleSelection selection_to_delete_;
+ Position upstream_start_;
+ Position downstream_start_;
+ Position upstream_end_;
+ Position downstream_end_;
+ Position ending_position_;
+ Position leading_whitespace_;
+ Position trailing_whitespace_;
+ Position reference_move_position_;
+ Member<Node> start_block_;
+ Member<Node> end_block_;
+ Member<EditingStyle> typing_style_;
+ Member<EditingStyle> delete_into_blockquote_style_;
+ Member<Element> start_root_;
+ Member<Element> end_root_;
+ Member<HTMLTableRowElement> start_table_row_;
+ Member<HTMLTableRowElement> end_table_row_;
+ Member<Node> temporary_placeholder_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_DELETE_SELECTION_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/delete_selection_command_test.cc b/chromium/third_party/blink/renderer/core/editing/commands/delete_selection_command_test.cc
new file mode 100644
index 00000000000..40ecab83091
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/delete_selection_command_test.cc
@@ -0,0 +1,78 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/delete_selection_command.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
+
+#include <memory>
+
+namespace blink {
+
+class DeleteSelectionCommandTest : public EditingTestBase {};
+
+// This is a regression test for https://crbug.com/668765
+TEST_F(DeleteSelectionCommandTest, deleteListFromTable) {
+ SetBodyContent(
+ "<div contenteditable=true>"
+ "<table><tr><td><ol>"
+ "<li><br></li>"
+ "<li>foo</li>"
+ "</ol></td></tr></table>"
+ "</div>");
+
+ Element* div = GetDocument().QuerySelector("div");
+ Element* table = GetDocument().QuerySelector("table");
+ Element* br = GetDocument().QuerySelector("br");
+
+ LocalFrame* frame = GetDocument().GetFrame();
+ frame->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(br, PositionAnchorType::kBeforeAnchor))
+ .Extend(Position(table, PositionAnchorType::kAfterAnchor))
+ .Build());
+
+ DeleteSelectionCommand* command =
+ DeleteSelectionCommand::Create(GetDocument(),
+ DeleteSelectionOptions::Builder()
+ .SetMergeBlocksAfterDelete(true)
+ .SetSanitizeMarkup(true)
+ .Build(),
+ InputEvent::InputType::kDeleteByCut);
+
+ EXPECT_TRUE(command->Apply()) << "the delete command should have succeeded";
+ EXPECT_EQ("<div contenteditable=\"true\"><br></div>",
+ GetDocument().body()->InnerHTMLAsString());
+ EXPECT_TRUE(frame->Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_EQ(Position(div, 0), frame->Selection()
+ .ComputeVisibleSelectionInDOMTree()
+ .Base()
+ .ToOffsetInAnchor());
+}
+
+TEST_F(DeleteSelectionCommandTest, ForwardDeleteWithFirstLetter) {
+ InsertStyleElement("p::first-letter {font-size:200%;}");
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<p contenteditable>a^b|c</p>"));
+
+ DeleteSelectionCommand& command = *DeleteSelectionCommand::Create(
+ GetDocument(), DeleteSelectionOptions::Builder()
+ .SetMergeBlocksAfterDelete(true)
+ .SetSanitizeMarkup(true)
+ .Build());
+ EXPECT_TRUE(command.Apply()) << "the delete command should have succeeded";
+ EXPECT_EQ("<p contenteditable>a|c</p>", GetSelectionTextFromBody());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/delete_selection_options.cc b/chromium/third_party/blink/renderer/core/editing/commands/delete_selection_options.cc
new file mode 100644
index 00000000000..bebf5ddea0e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/delete_selection_options.cc
@@ -0,0 +1,75 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/delete_selection_options.h"
+
+namespace blink {
+
+DeleteSelectionOptions::DeleteSelectionOptions(const DeleteSelectionOptions&) =
+ default;
+DeleteSelectionOptions::DeleteSelectionOptions() = default;
+
+bool DeleteSelectionOptions::IsExpandForSpecialElements() const {
+ return is_expand_for_special_elements_;
+}
+bool DeleteSelectionOptions::IsMergeBlocksAfterDelete() const {
+ return is_merge_blocks_after_delete_;
+}
+bool DeleteSelectionOptions::IsSanitizeMarkup() const {
+ return is_sanitize_markup_;
+}
+bool DeleteSelectionOptions::IsSmartDelete() const {
+ return is_smart_delete_;
+}
+
+// static
+DeleteSelectionOptions DeleteSelectionOptions::NormalDelete() {
+ return Builder()
+ .SetMergeBlocksAfterDelete(true)
+ .SetExpandForSpecialElements(true)
+ .SetSanitizeMarkup(true)
+ .Build();
+}
+
+DeleteSelectionOptions DeleteSelectionOptions::SmartDelete() {
+ return Builder()
+ .SetSmartDelete(true)
+ .SetMergeBlocksAfterDelete(true)
+ .SetExpandForSpecialElements(true)
+ .SetSanitizeMarkup(true)
+ .Build();
+}
+
+// ----
+DeleteSelectionOptions::Builder::Builder() = default;
+
+DeleteSelectionOptions DeleteSelectionOptions::Builder::Build() const {
+ return options_;
+}
+
+DeleteSelectionOptions::Builder&
+DeleteSelectionOptions::Builder::SetExpandForSpecialElements(bool value) {
+ options_.is_expand_for_special_elements_ = value;
+ return *this;
+}
+
+DeleteSelectionOptions::Builder&
+DeleteSelectionOptions::Builder::SetMergeBlocksAfterDelete(bool value) {
+ options_.is_merge_blocks_after_delete_ = value;
+ return *this;
+}
+
+DeleteSelectionOptions::Builder&
+DeleteSelectionOptions::Builder::SetSanitizeMarkup(bool value) {
+ options_.is_sanitize_markup_ = value;
+ return *this;
+}
+
+DeleteSelectionOptions::Builder&
+DeleteSelectionOptions::Builder::SetSmartDelete(bool value) {
+ options_.is_smart_delete_ = value;
+ return *this;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/delete_selection_options.h b/chromium/third_party/blink/renderer/core/editing/commands/delete_selection_options.h
new file mode 100644
index 00000000000..ef1038c5904
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/delete_selection_options.h
@@ -0,0 +1,62 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_DELETE_SELECTION_OPTIONS_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_DELETE_SELECTION_OPTIONS_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+
+namespace blink {
+
+// DeleteSelectionOptions of |DeleteSelectionCommand|.
+class CORE_EXPORT DeleteSelectionOptions final {
+ DISALLOW_NEW();
+
+ public:
+ class Builder;
+
+ DeleteSelectionOptions(const DeleteSelectionOptions&);
+
+ bool IsExpandForSpecialElements() const;
+ bool IsMergeBlocksAfterDelete() const;
+ bool IsSanitizeMarkup() const;
+ bool IsSmartDelete() const;
+
+ static DeleteSelectionOptions NormalDelete();
+ static DeleteSelectionOptions SmartDelete();
+
+ private:
+ DeleteSelectionOptions();
+
+ bool is_expand_for_special_elements_ = false;
+ bool is_merge_blocks_after_delete_ = false;
+ bool is_sanitize_markup_ = false;
+ bool is_smart_delete_ = false;
+};
+
+// Build |DeleteSelectionCommand::Options|.
+class CORE_EXPORT DeleteSelectionOptions::Builder final {
+ DISALLOW_NEW();
+
+ public:
+ Builder();
+
+ DeleteSelectionOptions Build() const;
+
+ Builder& SetExpandForSpecialElements(bool);
+ Builder& SetMergeBlocksAfterDelete(bool);
+ Builder& SetSanitizeMarkup(bool);
+ Builder& SetSmartDelete(bool);
+
+ private:
+ DeleteSelectionOptions options_;
+
+ DISALLOW_COPY_AND_ASSIGN(Builder);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_DELETE_SELECTION_OPTIONS_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/document_exec_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/document_exec_command.cc
new file mode 100644
index 00000000000..f35d0380bc3
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/document_exec_command.cc
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
+ * (C) 1999 Antti Koivisto (koivisto@kde.org)
+ * (C) 2001 Dirk Mueller (mueller@kde.org)
+ * (C) 2006 Alexey Proskuryakov (ap@webkit.org)
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2011, 2012 Apple Inc. All
+ * rights reserved.
+ * Copyright (C) 2008, 2009 Torch Mobile Inc. All rights reserved.
+ * (http://www.torchmobile.com/)
+ * Copyright (C) 2008, 2009, 2011, 2012 Google Inc. All rights reserved.
+ * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies)
+ * Copyright (C) Research In Motion Limited 2010-2011. All rights reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "third_party/blink/renderer/core/dom/document.h"
+
+#include "third_party/blink/renderer/core/dom/events/scoped_event_queue.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/commands/editor_command.h"
+#include "third_party/blink/renderer/core/editing/editing_tri_state.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/frame/use_counter.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+#include "third_party/blink/renderer/core/inspector/console_message.h"
+#include "third_party/blink/renderer/platform/histogram.h"
+#include "third_party/blink/renderer/platform/wtf/auto_reset.h"
+#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
+
+namespace blink {
+
+namespace {
+
+EditorCommand GetCommand(Document* document, const String& command_name) {
+ LocalFrame* frame = document->GetFrame();
+ if (!frame || frame->GetDocument() != document)
+ return EditorCommand();
+
+ document->UpdateStyleAndLayoutTree();
+ return frame->GetEditor().CreateCommand(command_name,
+ EditorCommandSource::kDOM);
+}
+
+} // namespace
+
+bool Document::execCommand(const String& command_name,
+ bool,
+ const String& value,
+ ExceptionState& exception_state) {
+ if (!IsHTMLDocument() && !IsXHTMLDocument()) {
+ exception_state.ThrowDOMException(
+ kInvalidStateError, "execCommand is only supported on HTML documents.");
+ return false;
+ }
+ if (FocusedElement() && IsTextControl(*FocusedElement()))
+ UseCounter::Count(*this, WebFeature::kExecCommandOnInputOrTextarea);
+
+ // We don't allow recursive |execCommand()| to protect against attack code.
+ // Recursive call of |execCommand()| could be happened by moving iframe
+ // with script triggered by insertion, e.g. <iframe src="javascript:...">
+ // <iframe onload="...">. This usage is valid as of the specification
+ // although, it isn't common use case, rather it is used as attack code.
+ if (is_running_exec_command_) {
+ String message =
+ "We don't execute document.execCommand() this time, because it is "
+ "called recursively.";
+ AddConsoleMessage(ConsoleMessage::Create(kJSMessageSource,
+ kWarningMessageLevel, message));
+ return false;
+ }
+ AutoReset<bool> execute_scope(&is_running_exec_command_, true);
+
+ // Postpone DOM mutation events, which can execute scripts and change
+ // DOM tree against implementation assumption.
+ EventQueueScope event_queue_scope;
+ TidyUpHTMLStructure(*this);
+ const EditorCommand editor_command = GetCommand(this, command_name);
+
+ DEFINE_STATIC_LOCAL(SparseHistogram, editor_command_histogram,
+ ("WebCore.Document.execCommand"));
+ editor_command_histogram.Sample(editor_command.IdForHistogram());
+ return editor_command.Execute(value);
+}
+
+bool Document::queryCommandEnabled(const String& command_name,
+ ExceptionState& exception_state) {
+ if (!IsHTMLDocument() && !IsXHTMLDocument()) {
+ exception_state.ThrowDOMException(
+ kInvalidStateError,
+ "queryCommandEnabled is only supported on HTML documents.");
+ return false;
+ }
+
+ return GetCommand(this, command_name).IsEnabled();
+}
+
+bool Document::queryCommandIndeterm(const String& command_name,
+ ExceptionState& exception_state) {
+ if (!IsHTMLDocument() && !IsXHTMLDocument()) {
+ exception_state.ThrowDOMException(
+ kInvalidStateError,
+ "queryCommandIndeterm is only supported on HTML documents.");
+ return false;
+ }
+
+ return GetCommand(this, command_name).GetState() == EditingTriState::kMixed;
+}
+
+bool Document::queryCommandState(const String& command_name,
+ ExceptionState& exception_state) {
+ if (!IsHTMLDocument() && !IsXHTMLDocument()) {
+ exception_state.ThrowDOMException(
+ kInvalidStateError,
+ "queryCommandState is only supported on HTML documents.");
+ return false;
+ }
+
+ return GetCommand(this, command_name).GetState() == EditingTriState::kTrue;
+}
+
+bool Document::queryCommandSupported(const String& command_name,
+ ExceptionState& exception_state) {
+ if (!IsHTMLDocument() && !IsXHTMLDocument()) {
+ exception_state.ThrowDOMException(
+ kInvalidStateError,
+ "queryCommandSupported is only supported on HTML documents.");
+ return false;
+ }
+
+ return GetCommand(this, command_name).IsSupported();
+}
+
+String Document::queryCommandValue(const String& command_name,
+ ExceptionState& exception_state) {
+ if (!IsHTMLDocument() && !IsXHTMLDocument()) {
+ exception_state.ThrowDOMException(
+ kInvalidStateError,
+ "queryCommandValue is only supported on HTML documents.");
+ return "";
+ }
+
+ return GetCommand(this, command_name).Value();
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/drag_and_drop_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/drag_and_drop_command.cc
new file mode 100644
index 00000000000..113b3f56c9f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/drag_and_drop_command.cc
@@ -0,0 +1,29 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/drag_and_drop_command.h"
+
+namespace blink {
+
+DragAndDropCommand::DragAndDropCommand(Document& document)
+ : CompositeEditCommand(document) {}
+
+bool DragAndDropCommand::IsCommandGroupWrapper() const {
+ return true;
+}
+
+bool DragAndDropCommand::IsDragAndDropCommand() const {
+ return true;
+}
+
+void DragAndDropCommand::DoApply(EditingState*) {
+ // Do nothing. Should only register undo entry after combined with other
+ // commands.
+}
+
+InputEvent::InputType DragAndDropCommand::GetInputType() const {
+ return InputEvent::InputType::kNone;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/drag_and_drop_command.h b/chromium/third_party/blink/renderer/core/editing/commands/drag_and_drop_command.h
new file mode 100644
index 00000000000..baa7a83c80f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/drag_and_drop_command.h
@@ -0,0 +1,35 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_DRAG_AND_DROP_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_DRAG_AND_DROP_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+namespace blink {
+
+// |DragAndDropCommand| is a dummy command. It doesn't do anything by itself,
+// but will act as a catcher for the following |DeleteByDrag| and
+// |InsertFromDrop| commands, and combine them into a single undo entry.
+// In the future when necessary, this mechanism can be generalized into a common
+// command wrapper to achieve undo group.
+class DragAndDropCommand final : public CompositeEditCommand {
+ public:
+ static DragAndDropCommand* Create(Document& document) {
+ return new DragAndDropCommand(document);
+ }
+
+ bool IsCommandGroupWrapper() const override;
+ bool IsDragAndDropCommand() const override;
+
+ private:
+ explicit DragAndDropCommand(Document&);
+
+ void DoApply(EditingState*) override;
+ InputEvent::InputType GetInputType() const override;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_DRAG_AND_DROP_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/edit_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/edit_command.cc
new file mode 100644
index 00000000000..a449514fb06
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/edit_command.cc
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2005, 2006, 2007 Apple, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/edit_command.h"
+
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/layout/layout_text.h"
+#include "third_party/blink/renderer/core/layout/line/inline_text_box.h"
+#include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h"
+
+namespace blink {
+
+EditCommand::EditCommand(Document& document)
+ : document_(&document), parent_(nullptr) {
+ DCHECK(document_);
+ DCHECK(document_->GetFrame());
+}
+
+EditCommand::~EditCommand() = default;
+
+InputEvent::InputType EditCommand::GetInputType() const {
+ return InputEvent::InputType::kNone;
+}
+
+String EditCommand::TextDataForInputEvent() const {
+ return g_null_atom;
+}
+
+bool EditCommand::IsRenderedCharacter(const Position& position) {
+ if (position.IsNull())
+ return false;
+ DCHECK(position.IsOffsetInAnchor()) << position;
+
+ const Node& node = *position.AnchorNode();
+ if (!node.IsTextNode())
+ return false;
+
+ LayoutObject* layout_object = node.GetLayoutObject();
+ if (!layout_object || !layout_object->IsText())
+ return false;
+
+ // Use NG offset mapping when LayoutNG is enabled.
+ if (auto* mapping = NGOffsetMapping::GetFor(position)) {
+ return mapping->IsBeforeNonCollapsedContent(position);
+ }
+
+ // TODO(editing-dev): This doesn't handle first-letter correctly. Fix it.
+ const LayoutText* layout_text = ToLayoutText(layout_object);
+ const int offset_in_node = position.OffsetInContainerNode();
+ for (InlineTextBox* box : layout_text->TextBoxes()) {
+ if (offset_in_node < static_cast<int>(box->Start()) &&
+ !layout_text->ContainsReversedText()) {
+ // The offset we're looking for is before this node this means the offset
+ // must be in content that is not laid out. Return false.
+ return false;
+ }
+ if (offset_in_node >= static_cast<int>(box->Start()) &&
+ offset_in_node < static_cast<int>(box->Start() + box->Len()))
+ return true;
+ }
+
+ return false;
+}
+
+void EditCommand::SetParent(CompositeEditCommand* parent) {
+ DCHECK((parent && !parent_) || (!parent && parent_));
+ DCHECK(!parent || !IsCompositeEditCommand() ||
+ !ToCompositeEditCommand(this)->GetUndoStep());
+ parent_ = parent;
+}
+
+void SimpleEditCommand::DoReapply() {
+ EditingState editing_state;
+ DoApply(&editing_state);
+}
+
+void EditCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(document_);
+ visitor->Trace(parent_);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/edit_command.h b/chromium/third_party/blink/renderer/core/editing/commands/edit_command.h
new file mode 100644
index 00000000000..d545651062c
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/edit_command.h
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_EDIT_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_EDIT_COMMAND_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/events/input_event.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+class CompositeEditCommand;
+class Document;
+class EditingState;
+
+class CORE_EXPORT EditCommand : public GarbageCollectedFinalized<EditCommand> {
+ public:
+ virtual ~EditCommand();
+
+ virtual void SetParent(CompositeEditCommand*);
+
+ virtual InputEvent::InputType GetInputType() const;
+
+ virtual bool IsSimpleEditCommand() const { return false; }
+ virtual bool IsCompositeEditCommand() const { return false; }
+ bool IsTopLevelCommand() const { return !parent_; }
+
+ // The |EditingState*| argument must not be nullptr.
+ virtual void DoApply(EditingState*) = 0;
+
+ // |TypingCommand| will return the text of the last |m_commands|.
+ virtual String TextDataForInputEvent() const;
+
+ virtual void Trace(blink::Visitor*);
+ bool SelectionIsDirectional() const { return selection_is_directional_; }
+ void SetSelectionIsDirectional(bool is_directional) {
+ selection_is_directional_ = is_directional;
+ }
+
+ protected:
+ explicit EditCommand(Document&);
+
+ Document& GetDocument() const { return *document_.Get(); }
+ CompositeEditCommand* Parent() const { return parent_; }
+
+ static bool IsRenderedCharacter(const Position&);
+
+ private:
+ Member<Document> document_;
+ Member<CompositeEditCommand> parent_;
+ bool selection_is_directional_ = false;
+};
+
+enum ShouldAssumeContentIsAlwaysEditable {
+ kAssumeContentIsAlwaysEditable,
+ kDoNotAssumeContentIsAlwaysEditable,
+};
+
+class CORE_EXPORT SimpleEditCommand : public EditCommand {
+ public:
+ virtual void DoUnapply() = 0;
+ virtual void DoReapply(); // calls doApply()
+
+ protected:
+ explicit SimpleEditCommand(Document& document) : EditCommand(document) {}
+
+ private:
+ bool IsSimpleEditCommand() const final { return true; }
+};
+
+DEFINE_TYPE_CASTS(SimpleEditCommand,
+ EditCommand,
+ command,
+ command->IsSimpleEditCommand(),
+ command.IsSimpleEditCommand());
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_EDIT_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/editing_command_test.cc b/chromium/third_party/blink/renderer/core/editing/commands/editing_command_test.cc
new file mode 100644
index 00000000000..ece012f6632
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/editing_command_test.cc
@@ -0,0 +1,80 @@
+// Copyright (c) 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/public/platform/web_editing_command_type.h"
+#include "third_party/blink/renderer/core/editing/commands/editor_command.h"
+#include "third_party/blink/renderer/core/editing/commands/editor_command_names.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/platform/wtf/string_extras.h"
+
+namespace blink {
+
+namespace {
+
+struct CommandNameEntry {
+ const char* name;
+ WebEditingCommandType type;
+};
+
+const CommandNameEntry kCommandNameEntries[] = {
+#define V(name) {#name, WebEditingCommandType::k##name},
+ FOR_EACH_BLINK_EDITING_COMMAND_NAME(V)
+#undef V
+};
+// Test all commands except WebEditingCommandType::Invalid.
+static_assert(
+ arraysize(kCommandNameEntries) + 1 ==
+ static_cast<size_t>(WebEditingCommandType::kNumberOfCommandTypes),
+ "must test all valid WebEditingCommandType");
+
+} // anonymous namespace
+
+class EditingCommandTest : public EditingTestBase {};
+
+TEST_F(EditingCommandTest, EditorCommandOrder) {
+ for (size_t i = 1; i < arraysize(kCommandNameEntries); ++i) {
+ EXPECT_GT(0, strcasecmp(kCommandNameEntries[i - 1].name,
+ kCommandNameEntries[i].name))
+ << "EDITOR_COMMAND_MAP must be case-folding ordered. Incorrect index:"
+ << i;
+ }
+}
+
+TEST_F(EditingCommandTest, CreateCommandFromString) {
+ Editor& dummy_editor = GetDocument().GetFrame()->GetEditor();
+ for (const auto& entry : kCommandNameEntries) {
+ const EditorCommand command = dummy_editor.CreateCommand(entry.name);
+ EXPECT_EQ(static_cast<int>(entry.type), command.IdForHistogram())
+ << entry.name;
+ }
+}
+
+TEST_F(EditingCommandTest, CreateCommandFromStringCaseFolding) {
+ Editor& dummy_editor = GetDocument().GetFrame()->GetEditor();
+ for (const auto& entry : kCommandNameEntries) {
+ const EditorCommand lower_name_command =
+ dummy_editor.CreateCommand(String(entry.name).DeprecatedLower());
+ EXPECT_EQ(static_cast<int>(entry.type), lower_name_command.IdForHistogram())
+ << entry.name;
+ const EditorCommand upper_name_command =
+ dummy_editor.CreateCommand(String(entry.name).UpperASCII());
+ EXPECT_EQ(static_cast<int>(entry.type), upper_name_command.IdForHistogram())
+ << entry.name;
+ }
+}
+
+TEST_F(EditingCommandTest, CreateCommandFromInvalidString) {
+ const String kInvalidCommandName[] = {
+ "", "iNvAlId", "12345",
+ };
+ Editor& dummy_editor = GetDocument().GetFrame()->GetEditor();
+ for (const auto& command_name : kInvalidCommandName) {
+ const EditorCommand command = dummy_editor.CreateCommand(command_name);
+ EXPECT_EQ(0, command.IdForHistogram());
+ }
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/editing_commands_utilities.cc b/chromium/third_party/blink/renderer/core/editing/commands/editing_commands_utilities.cc
new file mode 100644
index 00000000000..942ba3c0ac3
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/editing_commands_utilities.cc
@@ -0,0 +1,709 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+
+#include "third_party/blink/renderer/core/dom/node_computed_style.h"
+#include "third_party/blink/renderer/core/editing/commands/selection_for_undo_step.h"
+#include "third_party/blink/renderer/core/editing/commands/typing_command.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/frame/local_frame_client.h"
+#include "third_party/blink/renderer/core/frame/use_counter.h"
+#include "third_party/blink/renderer/core/frame/web_feature_forward.h"
+#include "third_party/blink/renderer/core/html/html_body_element.h"
+#include "third_party/blink/renderer/core/html/html_html_element.h"
+#include "third_party/blink/renderer/core/inspector/console_message.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+
+namespace blink {
+
+static bool HasARenderedDescendant(const Node* node,
+ const Node* excluded_node) {
+ for (const Node* n = node->firstChild(); n;) {
+ if (n == excluded_node) {
+ n = NodeTraversal::NextSkippingChildren(*n, node);
+ continue;
+ }
+ if (n->GetLayoutObject())
+ return true;
+ n = NodeTraversal::Next(*n, node);
+ }
+ return false;
+}
+
+Node* HighestNodeToRemoveInPruning(Node* node, const Node* exclude_node) {
+ Node* previous_node = nullptr;
+ Element* element = node ? RootEditableElement(*node) : nullptr;
+ for (; node; node = node->parentNode()) {
+ if (LayoutObject* layout_object = node->GetLayoutObject()) {
+ if (!layout_object->CanHaveChildren() ||
+ HasARenderedDescendant(node, previous_node) || element == node ||
+ exclude_node == node)
+ return previous_node;
+ }
+ previous_node = node;
+ }
+ return nullptr;
+}
+
+Element* EnclosingTableCell(const Position& p) {
+ return ToElement(EnclosingNodeOfType(p, IsTableCell));
+}
+
+bool IsTableStructureNode(const Node* node) {
+ LayoutObject* layout_object = node->GetLayoutObject();
+ return (layout_object &&
+ (layout_object->IsTableCell() || layout_object->IsTableRow() ||
+ layout_object->IsTableSection() ||
+ layout_object->IsLayoutTableCol()));
+}
+
+bool IsNodeRendered(const Node& node) {
+ LayoutObject* layout_object = node.GetLayoutObject();
+ if (!layout_object)
+ return false;
+
+ return layout_object->Style()->Visibility() == EVisibility::kVisible;
+}
+
+bool IsInline(const Node* node) {
+ if (!node)
+ return false;
+
+ const ComputedStyle* style = node->GetComputedStyle();
+ return style && style->Display() == EDisplay::kInline;
+}
+
+// FIXME: This method should not need to call
+// isStartOfParagraph/isEndOfParagraph
+Node* EnclosingEmptyListItem(const VisiblePosition& visible_pos) {
+ DCHECK(visible_pos.IsValid());
+
+ // Check that position is on a line by itself inside a list item
+ Node* list_child_node =
+ EnclosingListChild(visible_pos.DeepEquivalent().AnchorNode());
+ if (!list_child_node || !IsStartOfParagraph(visible_pos) ||
+ !IsEndOfParagraph(visible_pos))
+ return nullptr;
+
+ VisiblePosition first_in_list_child =
+ CreateVisiblePosition(FirstPositionInOrBeforeNode(*list_child_node));
+ VisiblePosition last_in_list_child =
+ CreateVisiblePosition(LastPositionInOrAfterNode(*list_child_node));
+
+ if (first_in_list_child.DeepEquivalent() != visible_pos.DeepEquivalent() ||
+ last_in_list_child.DeepEquivalent() != visible_pos.DeepEquivalent())
+ return nullptr;
+
+ return list_child_node;
+}
+
+bool AreIdenticalElements(const Node& first, const Node& second) {
+ if (!first.IsElementNode() || !second.IsElementNode())
+ return false;
+
+ const Element& first_element = ToElement(first);
+ const Element& second_element = ToElement(second);
+ if (!first_element.HasTagName(second_element.TagQName()))
+ return false;
+
+ if (!first_element.HasEquivalentAttributes(&second_element))
+ return false;
+
+ return HasEditableStyle(first_element) && HasEditableStyle(second_element);
+}
+
+// FIXME: need to dump this
+static bool IsSpecialHTMLElement(const Node& n) {
+ if (!n.IsHTMLElement())
+ return false;
+
+ if (n.IsLink())
+ return true;
+
+ LayoutObject* layout_object = n.GetLayoutObject();
+ if (!layout_object)
+ return false;
+
+ if (layout_object->Style()->Display() == EDisplay::kTable ||
+ layout_object->Style()->Display() == EDisplay::kInlineTable)
+ return true;
+
+ if (layout_object->Style()->IsFloating())
+ return true;
+
+ return false;
+}
+
+static HTMLElement* FirstInSpecialElement(const Position& pos) {
+ DCHECK(!NeedsLayoutTreeUpdate(pos));
+ Element* element = RootEditableElement(*pos.ComputeContainerNode());
+ for (Node& runner : NodeTraversal::InclusiveAncestorsOf(*pos.AnchorNode())) {
+ if (RootEditableElement(runner) != element)
+ break;
+ if (IsSpecialHTMLElement(runner)) {
+ HTMLElement* special_element = ToHTMLElement(&runner);
+ VisiblePosition v_pos = CreateVisiblePosition(pos);
+ VisiblePosition first_in_element =
+ CreateVisiblePosition(FirstPositionInOrBeforeNode(*special_element));
+ if (IsDisplayInsideTable(special_element) &&
+ !IsListItem(v_pos.DeepEquivalent().ComputeContainerNode()) &&
+ v_pos.DeepEquivalent() ==
+ NextPositionOf(first_in_element).DeepEquivalent())
+ return special_element;
+ if (v_pos.DeepEquivalent() == first_in_element.DeepEquivalent())
+ return special_element;
+ }
+ }
+ return nullptr;
+}
+
+static HTMLElement* LastInSpecialElement(const Position& pos) {
+ DCHECK(!NeedsLayoutTreeUpdate(pos));
+ Element* element = RootEditableElement(*pos.ComputeContainerNode());
+ for (Node& runner : NodeTraversal::InclusiveAncestorsOf(*pos.AnchorNode())) {
+ if (RootEditableElement(runner) != element)
+ break;
+ if (IsSpecialHTMLElement(runner)) {
+ HTMLElement* special_element = ToHTMLElement(&runner);
+ VisiblePosition v_pos = CreateVisiblePosition(pos);
+ VisiblePosition last_in_element =
+ CreateVisiblePosition(LastPositionInOrAfterNode(*special_element));
+ if (IsDisplayInsideTable(special_element) &&
+ v_pos.DeepEquivalent() ==
+ PreviousPositionOf(last_in_element).DeepEquivalent())
+ return special_element;
+ if (v_pos.DeepEquivalent() == last_in_element.DeepEquivalent())
+ return special_element;
+ }
+ }
+ return nullptr;
+}
+
+Position PositionBeforeContainingSpecialElement(
+ const Position& pos,
+ HTMLElement** containing_special_element) {
+ DCHECK(!NeedsLayoutTreeUpdate(pos));
+ HTMLElement* n = FirstInSpecialElement(pos);
+ if (!n)
+ return pos;
+ Position result = Position::InParentBeforeNode(*n);
+ if (result.IsNull() || RootEditableElement(*result.AnchorNode()) !=
+ RootEditableElement(*pos.AnchorNode()))
+ return pos;
+ if (containing_special_element)
+ *containing_special_element = n;
+ return result;
+}
+
+Position PositionAfterContainingSpecialElement(
+ const Position& pos,
+ HTMLElement** containing_special_element) {
+ DCHECK(!NeedsLayoutTreeUpdate(pos));
+ HTMLElement* n = LastInSpecialElement(pos);
+ if (!n)
+ return pos;
+ Position result = Position::InParentAfterNode(*n);
+ if (result.IsNull() || RootEditableElement(*result.AnchorNode()) !=
+ RootEditableElement(*pos.AnchorNode()))
+ return pos;
+ if (containing_special_element)
+ *containing_special_element = n;
+ return result;
+}
+
+bool LineBreakExistsAtPosition(const Position& position) {
+ if (position.IsNull())
+ return false;
+
+ if (IsHTMLBRElement(*position.AnchorNode()) &&
+ position.AtFirstEditingPositionForNode())
+ return true;
+
+ if (!position.AnchorNode()->GetLayoutObject())
+ return false;
+
+ if (!position.AnchorNode()->IsTextNode() ||
+ !position.AnchorNode()->GetLayoutObject()->Style()->PreserveNewline())
+ return false;
+
+ const Text* text_node = ToText(position.AnchorNode());
+ unsigned offset = position.OffsetInContainerNode();
+ return offset < text_node->length() && text_node->data()[offset] == '\n';
+}
+
+// return first preceding DOM position rendered at a different location, or
+// "this"
+static Position PreviousCharacterPosition(const Position& position,
+ TextAffinity affinity) {
+ DCHECK(!NeedsLayoutTreeUpdate(position));
+ if (position.IsNull())
+ return Position();
+
+ Element* from_root_editable_element =
+ RootEditableElement(*position.AnchorNode());
+
+ bool at_start_of_line =
+ IsStartOfLine(CreateVisiblePosition(position, affinity));
+ bool rendered = IsVisuallyEquivalentCandidate(position);
+
+ Position current_pos = position;
+ while (!current_pos.AtStartOfTree()) {
+ // TODO(yosin) When we use |previousCharacterPosition()| other than
+ // finding leading whitespace, we should use |Character| instead of
+ // |CodePoint|.
+ current_pos = PreviousPositionOf(current_pos, PositionMoveType::kCodeUnit);
+
+ if (RootEditableElement(*current_pos.AnchorNode()) !=
+ from_root_editable_element)
+ return position;
+
+ if (at_start_of_line || !rendered) {
+ if (IsVisuallyEquivalentCandidate(current_pos))
+ return current_pos;
+ } else if (RendersInDifferentPosition(position, current_pos)) {
+ return current_pos;
+ }
+ }
+
+ return position;
+}
+
+// This assumes that it starts in editable content.
+Position LeadingCollapsibleWhitespacePosition(const Position& position,
+ TextAffinity affinity,
+ WhitespacePositionOption option) {
+ DCHECK(!NeedsLayoutTreeUpdate(position));
+ DCHECK(IsEditablePosition(position)) << position;
+ if (position.IsNull())
+ return Position();
+
+ if (IsHTMLBRElement(*MostBackwardCaretPosition(position).AnchorNode()))
+ return Position();
+
+ const Position& prev = PreviousCharacterPosition(position, affinity);
+ if (prev == position)
+ return Position();
+ const Node* const anchor_node = prev.AnchorNode();
+ if (!anchor_node || !anchor_node->IsTextNode())
+ return Position();
+ if (EnclosingBlockFlowElement(*anchor_node) !=
+ EnclosingBlockFlowElement(*position.AnchorNode()))
+ return Position();
+ if (option == kNotConsiderNonCollapsibleWhitespace &&
+ anchor_node->GetLayoutObject() &&
+ !anchor_node->GetLayoutObject()->Style()->CollapseWhiteSpace())
+ return Position();
+ const String& string = ToText(anchor_node)->data();
+ const UChar previous_character = string[prev.ComputeOffsetInContainerNode()];
+ const bool is_space = option == kConsiderNonCollapsibleWhitespace
+ ? (IsSpaceOrNewline(previous_character) ||
+ previous_character == kNoBreakSpaceCharacter)
+ : IsCollapsibleWhitespace(previous_character);
+ if (!is_space || !IsEditablePosition(prev))
+ return Position();
+ return prev;
+}
+
+unsigned NumEnclosingMailBlockquotes(const Position& p) {
+ unsigned num = 0;
+ for (const Node* n = p.AnchorNode(); n; n = n->parentNode()) {
+ if (IsMailHTMLBlockquoteElement(n))
+ num++;
+ }
+ return num;
+}
+
+bool LineBreakExistsAtVisiblePosition(const VisiblePosition& visible_position) {
+ return LineBreakExistsAtPosition(
+ MostForwardCaretPosition(visible_position.DeepEquivalent()));
+}
+
+HTMLElement* CreateHTMLElement(Document& document, const QualifiedName& name) {
+ DCHECK_EQ(name.NamespaceURI(), HTMLNames::xhtmlNamespaceURI)
+ << "Unexpected namespace: " << name;
+ return ToHTMLElement(document.CreateElement(
+ name, CreateElementFlags::ByCloneNode(), g_null_atom));
+}
+
+HTMLElement* EnclosingList(const Node* node) {
+ if (!node)
+ return nullptr;
+
+ ContainerNode* root = HighestEditableRoot(FirstPositionInOrBeforeNode(*node));
+
+ for (Node& runner : NodeTraversal::AncestorsOf(*node)) {
+ if (IsHTMLUListElement(runner) || IsHTMLOListElement(runner))
+ return ToHTMLElement(&runner);
+ if (runner == root)
+ return nullptr;
+ }
+
+ return nullptr;
+}
+
+Node* EnclosingListChild(const Node* node) {
+ if (!node)
+ return nullptr;
+ // Check for a list item element, or for a node whose parent is a list
+ // element. Such a node will appear visually as a list item (but without a
+ // list marker)
+ ContainerNode* root = HighestEditableRoot(FirstPositionInOrBeforeNode(*node));
+
+ // FIXME: This function is inappropriately named if it starts with node
+ // instead of node->parentNode()
+ for (Node* n = const_cast<Node*>(node); n && n->parentNode();
+ n = n->parentNode()) {
+ if (IsHTMLLIElement(*n) ||
+ (IsHTMLListElement(n->parentNode()) && n != root))
+ return n;
+ if (n == root || IsTableCell(n))
+ return nullptr;
+ }
+
+ return nullptr;
+}
+
+HTMLElement* OutermostEnclosingList(const Node* node,
+ const HTMLElement* root_list) {
+ HTMLElement* list = EnclosingList(node);
+ if (!list)
+ return nullptr;
+
+ while (HTMLElement* next_list = EnclosingList(list)) {
+ if (next_list == root_list)
+ break;
+ list = next_list;
+ }
+
+ return list;
+}
+
+// Determines whether two positions are visibly next to each other (first then
+// second) while ignoring whitespaces and unrendered nodes
+static bool IsVisiblyAdjacent(const Position& first, const Position& second) {
+ return CreateVisiblePosition(first).DeepEquivalent() ==
+ CreateVisiblePosition(MostBackwardCaretPosition(second))
+ .DeepEquivalent();
+}
+
+bool CanMergeLists(const Element& first_list, const Element& second_list) {
+ if (!first_list.IsHTMLElement() || !second_list.IsHTMLElement())
+ return false;
+
+ DCHECK(!NeedsLayoutTreeUpdate(first_list));
+ DCHECK(!NeedsLayoutTreeUpdate(second_list));
+ return first_list.HasTagName(
+ second_list
+ .TagQName()) // make sure the list types match (ol vs. ul)
+ && HasEditableStyle(first_list) &&
+ HasEditableStyle(second_list) // both lists are editable
+ &&
+ RootEditableElement(first_list) ==
+ RootEditableElement(second_list) // don't cross editing boundaries
+ && IsVisiblyAdjacent(Position::InParentAfterNode(first_list),
+ Position::InParentBeforeNode(second_list));
+ // Make sure there is no visible content between this li and the previous list
+}
+
+// Modifies selections that have an end point at the edge of a table
+// that contains the other endpoint so that they don't confuse
+// code that iterates over selected paragraphs.
+VisibleSelection SelectionForParagraphIteration(
+ const VisibleSelection& original) {
+ VisibleSelection new_selection(original);
+ VisiblePosition start_of_selection(new_selection.VisibleStart());
+ VisiblePosition end_of_selection(new_selection.VisibleEnd());
+
+ // If the end of the selection to modify is just after a table, and if the
+ // start of the selection is inside that table, then the last paragraph that
+ // we'll want modify is the last one inside the table, not the table itself (a
+ // table is itself a paragraph).
+ if (Element* table = TableElementJustBefore(end_of_selection)) {
+ if (start_of_selection.DeepEquivalent().AnchorNode()->IsDescendantOf(
+ table)) {
+ const VisiblePosition& new_end =
+ PreviousPositionOf(end_of_selection, kCannotCrossEditingBoundary);
+ if (new_end.IsNotNull()) {
+ new_selection = CreateVisibleSelection(
+ SelectionInDOMTree::Builder()
+ .Collapse(start_of_selection.ToPositionWithAffinity())
+ .Extend(new_end.DeepEquivalent())
+ .Build());
+ } else {
+ new_selection = CreateVisibleSelection(
+ SelectionInDOMTree::Builder()
+ .Collapse(start_of_selection.ToPositionWithAffinity())
+ .Build());
+ }
+ }
+ }
+
+ // If the start of the selection to modify is just before a table, and if the
+ // end of the selection is inside that table, then the first paragraph we'll
+ // want to modify is the first one inside the table, not the paragraph
+ // containing the table itself.
+ if (Element* table = TableElementJustAfter(start_of_selection)) {
+ if (end_of_selection.DeepEquivalent().AnchorNode()->IsDescendantOf(table)) {
+ const VisiblePosition new_start =
+ NextPositionOf(start_of_selection, kCannotCrossEditingBoundary);
+ if (new_start.IsNotNull()) {
+ new_selection = CreateVisibleSelection(
+ SelectionInDOMTree::Builder()
+ .Collapse(new_start.ToPositionWithAffinity())
+ .Extend(end_of_selection.DeepEquivalent())
+ .Build());
+ } else {
+ new_selection = CreateVisibleSelection(
+ SelectionInDOMTree::Builder()
+ .Collapse(end_of_selection.ToPositionWithAffinity())
+ .Build());
+ }
+ }
+ }
+
+ return new_selection;
+}
+
+const String& NonBreakingSpaceString() {
+ DEFINE_STATIC_LOCAL(String, non_breaking_space_string,
+ (&kNoBreakSpaceCharacter, 1));
+ return non_breaking_space_string;
+}
+
+// TODO(tkent): This is a workaround of some crash bugs in the editing code,
+// which assumes a document has a valid HTML structure. We should make the
+// editing code more robust, and should remove this hack. crbug.com/580941.
+void TidyUpHTMLStructure(Document& document) {
+ // hasEditableStyle() needs up-to-date ComputedStyle.
+ document.UpdateStyleAndLayoutTree();
+ const bool needs_valid_structure =
+ HasEditableStyle(document) ||
+ (document.documentElement() &&
+ HasEditableStyle(*document.documentElement()));
+ if (!needs_valid_structure)
+ return;
+
+ Element* const current_root = document.documentElement();
+ if (current_root && IsHTMLHtmlElement(current_root))
+ return;
+ Element* const existing_head =
+ current_root && IsHTMLHeadElement(current_root) ? current_root : nullptr;
+ Element* const existing_body =
+ current_root && (IsHTMLBodyElement(current_root) ||
+ IsHTMLFrameSetElement(current_root))
+ ? current_root
+ : nullptr;
+ // We ensure only "the root is <html>."
+ // documentElement as rootEditableElement is problematic. So we move
+ // non-<html> root elements under <body>, and the <body> works as
+ // rootEditableElement.
+ document.AddConsoleMessage(ConsoleMessage::Create(
+ kJSMessageSource, kWarningMessageLevel,
+ "document.execCommand() doesn't work with an invalid HTML structure. It "
+ "is corrected automatically."));
+ UseCounter::Count(document, WebFeature::kExecCommandAltersHTMLStructure);
+
+ Element* const root = HTMLHtmlElement::Create(document);
+ if (existing_head)
+ root->AppendChild(existing_head);
+ Element* const body =
+ existing_body ? existing_body : HTMLBodyElement::Create(document);
+ if (document.documentElement() && body != document.documentElement())
+ body->AppendChild(document.documentElement());
+ root->AppendChild(body);
+ DCHECK(!document.documentElement());
+ document.AppendChild(root);
+
+ // TODO(tkent): Should we check and move Text node children of <html>?
+}
+
+InputEvent::InputType DeletionInputTypeFromTextGranularity(
+ DeleteDirection direction,
+ TextGranularity granularity) {
+ using InputType = InputEvent::InputType;
+ switch (direction) {
+ case DeleteDirection::kForward:
+ if (granularity == TextGranularity::kWord)
+ return InputType::kDeleteWordForward;
+ if (granularity == TextGranularity::kLineBoundary)
+ return InputType::kDeleteSoftLineForward;
+ if (granularity == TextGranularity::kParagraphBoundary)
+ return InputType::kDeleteHardLineForward;
+ return InputType::kDeleteContentForward;
+ case DeleteDirection::kBackward:
+ if (granularity == TextGranularity::kWord)
+ return InputType::kDeleteWordBackward;
+ if (granularity == TextGranularity::kLineBoundary)
+ return InputType::kDeleteSoftLineBackward;
+ if (granularity == TextGranularity::kParagraphBoundary)
+ return InputType::kDeleteHardLineBackward;
+ return InputType::kDeleteContentBackward;
+ default:
+ return InputType::kNone;
+ }
+}
+
+void DispatchEditableContentChangedEvents(Element* start_root,
+ Element* end_root) {
+ if (start_root) {
+ start_root->DispatchEvent(
+ Event::Create(EventTypeNames::webkitEditableContentChanged));
+ }
+ if (end_root && end_root != start_root) {
+ end_root->DispatchEvent(
+ Event::Create(EventTypeNames::webkitEditableContentChanged));
+ }
+}
+
+static void DispatchInputEvent(Element* target,
+ InputEvent::InputType input_type,
+ const String& data,
+ InputEvent::EventIsComposing is_composing) {
+ if (!target)
+ return;
+ // TODO(chongz): Pass appreciate |ranges| after it's defined on spec.
+ // http://w3c.github.io/editing/input-events.html#dom-inputevent-inputtype
+ InputEvent* const input_event =
+ InputEvent::CreateInput(input_type, data, is_composing, nullptr);
+ target->DispatchScopedEvent(input_event);
+}
+
+void DispatchInputEventEditableContentChanged(
+ Element* start_root,
+ Element* end_root,
+ InputEvent::InputType input_type,
+ const String& data,
+ InputEvent::EventIsComposing is_composing) {
+ if (start_root)
+ DispatchInputEvent(start_root, input_type, data, is_composing);
+ if (end_root && end_root != start_root)
+ DispatchInputEvent(end_root, input_type, data, is_composing);
+}
+
+SelectionInDOMTree CorrectedSelectionAfterCommand(
+ const SelectionForUndoStep& passed_selection,
+ const Document* document) {
+ if (!passed_selection.Base().IsConnected() ||
+ !passed_selection.Extent().IsConnected() ||
+ passed_selection.Base().GetDocument() != document ||
+ passed_selection.Base().GetDocument() !=
+ passed_selection.Extent().GetDocument())
+ return SelectionInDOMTree();
+ return passed_selection.AsSelection();
+}
+
+void ChangeSelectionAfterCommand(LocalFrame* frame,
+ const SelectionInDOMTree& new_selection,
+ const SetSelectionOptions& options) {
+ if (new_selection.IsNone())
+ return;
+ // See <rdar://problem/5729315> Some shouldChangeSelectedDOMRange contain
+ // Ranges for selections that are no longer valid
+ const bool selection_did_not_change_dom_position =
+ new_selection == frame->Selection().GetSelectionInDOMTree() &&
+ options.IsDirectional() == frame->Selection().IsDirectional();
+ const bool handle_visible =
+ frame->Selection().IsHandleVisible() && new_selection.IsRange();
+ frame->Selection().SetSelection(new_selection,
+ SetSelectionOptions::Builder(options)
+ .SetShouldShowHandle(handle_visible)
+ .SetIsDirectional(options.IsDirectional())
+ .Build());
+
+ // Some editing operations change the selection visually without affecting its
+ // position within the DOM. For example when you press return in the following
+ // (the caret is marked by ^):
+ // <div contentEditable="true"><div>^Hello</div></div>
+ // WebCore inserts <div><br></div> *before* the current block, which correctly
+ // moves the paragraph down but which doesn't change the caret's DOM position
+ // (["hello", 0]). In these situations the above FrameSelection::setSelection
+ // call does not call LocalFrameClient::DidChangeSelection(), which, on the
+ // Mac, sends selection change notifications and starts a new kill ring
+ // sequence, but we want to do these things (matches AppKit).
+ if (!selection_did_not_change_dom_position)
+ return;
+ frame->Client()->DidChangeSelection(
+ frame->Selection().GetSelectionInDOMTree().Type() != kRangeSelection);
+}
+
+InputEvent::EventIsComposing IsComposingFromCommand(
+ const CompositeEditCommand* command) {
+ if (command->IsTypingCommand() &&
+ ToTypingCommand(command)->CompositionType() !=
+ TypingCommand::kTextCompositionNone)
+ return InputEvent::EventIsComposing::kIsComposing;
+ return InputEvent::EventIsComposing::kNotComposing;
+}
+
+// ---------
+
+VisiblePosition StartOfBlock(const VisiblePosition& visible_position,
+ EditingBoundaryCrossingRule rule) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ Position position = visible_position.DeepEquivalent();
+ Element* start_block =
+ position.ComputeContainerNode()
+ ? EnclosingBlock(position.ComputeContainerNode(), rule)
+ : nullptr;
+ return start_block ? VisiblePosition::FirstPositionInNode(*start_block)
+ : VisiblePosition();
+}
+
+VisiblePosition EndOfBlock(const VisiblePosition& visible_position,
+ EditingBoundaryCrossingRule rule) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ Position position = visible_position.DeepEquivalent();
+ Element* end_block =
+ position.ComputeContainerNode()
+ ? EnclosingBlock(position.ComputeContainerNode(), rule)
+ : nullptr;
+ return end_block ? VisiblePosition::LastPositionInNode(*end_block)
+ : VisiblePosition();
+}
+
+bool IsStartOfBlock(const VisiblePosition& pos) {
+ DCHECK(pos.IsValid()) << pos;
+ return pos.IsNotNull() &&
+ pos.DeepEquivalent() ==
+ StartOfBlock(pos, kCanCrossEditingBoundary).DeepEquivalent();
+}
+
+bool IsEndOfBlock(const VisiblePosition& pos) {
+ DCHECK(pos.IsValid()) << pos;
+ return pos.IsNotNull() &&
+ pos.DeepEquivalent() ==
+ EndOfBlock(pos, kCanCrossEditingBoundary).DeepEquivalent();
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h b/chromium/third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h
new file mode 100644
index 00000000000..f97d04b6bd6
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2004, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_EDITING_COMMANDS_UTILITIES_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_EDITING_COMMANDS_UTILITIES_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/text_granularity.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/events/input_event.h"
+#include "third_party/blink/renderer/platform/wtf/text/character_names.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+
+namespace blink {
+
+enum class DeleteDirection {
+ kForward,
+ kBackward,
+};
+
+class CompositeEditCommand;
+class Document;
+class Element;
+class HTMLElement;
+class Node;
+class QualifiedName;
+class SelectionForUndoStep;
+class SetSelectionOptions;
+
+// -------------------------------------------------------------------------
+// Node
+// -------------------------------------------------------------------------
+
+Node* HighestNodeToRemoveInPruning(Node*, const Node* exclude_node = nullptr);
+
+Element* EnclosingTableCell(const Position&);
+Node* EnclosingEmptyListItem(const VisiblePosition&);
+
+bool IsTableStructureNode(const Node*);
+bool IsNodeRendered(const Node&);
+bool IsInline(const Node*);
+// Returns true if specified nodes are elements, have identical tag names,
+// have identical attributes, and are editable.
+CORE_EXPORT bool AreIdenticalElements(const Node&, const Node&);
+
+// -------------------------------------------------------------------------
+// Position
+// -------------------------------------------------------------------------
+
+Position PositionBeforeContainingSpecialElement(
+ const Position&,
+ HTMLElement** containing_special_element = nullptr);
+Position PositionAfterContainingSpecialElement(
+ const Position&,
+ HTMLElement** containing_special_element = nullptr);
+
+bool LineBreakExistsAtPosition(const Position&);
+
+// miscellaneous functions on Position
+
+enum WhitespacePositionOption {
+ kNotConsiderNonCollapsibleWhitespace,
+ kConsiderNonCollapsibleWhitespace
+};
+
+// |leadingCollapsibleWhitespacePosition(position)| returns a previous position
+// of |position| if it is at collapsible whitespace, otherwise it returns null
+// position. When it is called with |NotConsiderNonCollapsibleWhitespace| and
+// a previous position in a element which has CSS property "white-space:pre",
+// or its variant, |leadingCollapsibleWhitespacePosition()| returns null
+// position.
+Position LeadingCollapsibleWhitespacePosition(
+ const Position&,
+ TextAffinity,
+ WhitespacePositionOption = kNotConsiderNonCollapsibleWhitespace);
+
+unsigned NumEnclosingMailBlockquotes(const Position&);
+
+// -------------------------------------------------------------------------
+// VisiblePosition
+// -------------------------------------------------------------------------
+
+bool LineBreakExistsAtVisiblePosition(const VisiblePosition&);
+
+// -------------------------------------------------------------------------
+// HTMLElement
+// -------------------------------------------------------------------------
+
+HTMLElement* CreateHTMLElement(Document&, const QualifiedName&);
+HTMLElement* EnclosingList(const Node*);
+HTMLElement* OutermostEnclosingList(const Node*,
+ const HTMLElement* root_list = nullptr);
+Node* EnclosingListChild(const Node*);
+
+// -------------------------------------------------------------------------
+// Element
+// -------------------------------------------------------------------------
+
+bool CanMergeLists(const Element& first_list, const Element& second_list);
+
+// -------------------------------------------------------------------------
+// VisibleSelection
+// -------------------------------------------------------------------------
+
+// Functions returning VisibleSelection
+VisibleSelection SelectionForParagraphIteration(const VisibleSelection&);
+
+const String& NonBreakingSpaceString();
+
+CORE_EXPORT void TidyUpHTMLStructure(Document&);
+
+SelectionInDOMTree CorrectedSelectionAfterCommand(const SelectionForUndoStep&,
+ const Document*);
+void ChangeSelectionAfterCommand(LocalFrame*,
+ const SelectionInDOMTree&,
+ const SetSelectionOptions&);
+
+// -------------------------------------------------------------------------
+// Events
+// -------------------------------------------------------------------------
+
+void DispatchEditableContentChangedEvents(Element* start_root,
+ Element* end_root);
+void DispatchInputEventEditableContentChanged(Element* start_root,
+ Element* end_root,
+ InputEvent::InputType,
+ const String&,
+ InputEvent::EventIsComposing);
+InputEvent::EventIsComposing IsComposingFromCommand(
+ const CompositeEditCommand*);
+
+InputEvent::InputType DeletionInputTypeFromTextGranularity(DeleteDirection,
+ TextGranularity);
+
+// -------------------------------------------------------------------------
+// Blocks (true paragraphs; line break elements don't break blocks)
+// -------------------------------------------------------------------------
+//
+VisiblePosition StartOfBlock(
+ const VisiblePosition&,
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary);
+VisiblePosition EndOfBlock(
+ const VisiblePosition&,
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary);
+bool IsStartOfBlock(const VisiblePosition&);
+bool IsEndOfBlock(const VisiblePosition&);
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/editing_commands_utilities_test.cc b/chromium/third_party/blink/renderer/core/editing/commands/editing_commands_utilities_test.cc
new file mode 100644
index 00000000000..2c6bc8e5a6f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/editing_commands_utilities_test.cc
@@ -0,0 +1,92 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+
+#include "third_party/blink/renderer/core/dom/static_node_list.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/html/html_body_element.h"
+#include "third_party/blink/renderer/core/html/html_div_element.h"
+#include "third_party/blink/renderer/core/html/html_head_element.h"
+
+namespace blink {
+
+class EditingCommandsUtilitiesTest : public EditingTestBase {
+ protected:
+ void MakeDocumentEmpty();
+};
+
+void EditingCommandsUtilitiesTest::MakeDocumentEmpty() {
+ while (GetDocument().firstChild())
+ GetDocument().RemoveChild(GetDocument().firstChild());
+}
+
+TEST_F(EditingCommandsUtilitiesTest, AreaIdenticalElements) {
+ SetBodyContent(
+ "<style>li:nth-child(even) { -webkit-user-modify: read-write; "
+ "}</style><ul><li>first item</li><li>second item</li><li "
+ "class=foo>third</li><li>fourth</li></ul>");
+ StaticElementList* items =
+ GetDocument().QuerySelectorAll("li", ASSERT_NO_EXCEPTION);
+ DCHECK_EQ(items->length(), 4u);
+
+ EXPECT_FALSE(AreIdenticalElements(*items->item(0)->firstChild(),
+ *items->item(1)->firstChild()))
+ << "Can't merge non-elements. e.g. Text nodes";
+
+ // Compare a LI and a UL.
+ EXPECT_FALSE(
+ AreIdenticalElements(*items->item(0), *items->item(0)->parentNode()))
+ << "Can't merge different tag names.";
+
+ EXPECT_FALSE(AreIdenticalElements(*items->item(0), *items->item(2)))
+ << "Can't merge a element with no attributes and another element with an "
+ "attribute.";
+
+ // We can't use contenteditable attribute to make editability difference
+ // because the hasEquivalentAttributes check is done earier.
+ EXPECT_FALSE(AreIdenticalElements(*items->item(0), *items->item(1)))
+ << "Can't merge non-editable nodes.";
+
+ EXPECT_TRUE(AreIdenticalElements(*items->item(1), *items->item(3)));
+}
+
+TEST_F(EditingCommandsUtilitiesTest, TidyUpHTMLStructureFromBody) {
+ Element* body = HTMLBodyElement::Create(GetDocument());
+ MakeDocumentEmpty();
+ GetDocument().setDesignMode("on");
+ GetDocument().AppendChild(body);
+ TidyUpHTMLStructure(GetDocument());
+
+ EXPECT_TRUE(IsHTMLHtmlElement(GetDocument().documentElement()));
+ EXPECT_EQ(body, GetDocument().body());
+ EXPECT_EQ(GetDocument().documentElement(), body->parentNode());
+}
+
+TEST_F(EditingCommandsUtilitiesTest, TidyUpHTMLStructureFromDiv) {
+ Element* div = HTMLDivElement::Create(GetDocument());
+ MakeDocumentEmpty();
+ GetDocument().setDesignMode("on");
+ GetDocument().AppendChild(div);
+ TidyUpHTMLStructure(GetDocument());
+
+ EXPECT_TRUE(IsHTMLHtmlElement(GetDocument().documentElement()));
+ EXPECT_TRUE(IsHTMLBodyElement(GetDocument().body()));
+ EXPECT_EQ(GetDocument().body(), div->parentNode());
+}
+
+TEST_F(EditingCommandsUtilitiesTest, TidyUpHTMLStructureFromHead) {
+ Element* head = HTMLHeadElement::Create(GetDocument());
+ MakeDocumentEmpty();
+ GetDocument().setDesignMode("on");
+ GetDocument().AppendChild(head);
+ TidyUpHTMLStructure(GetDocument());
+
+ EXPECT_TRUE(IsHTMLHtmlElement(GetDocument().documentElement()));
+ EXPECT_TRUE(IsHTMLBodyElement(GetDocument().body()));
+ EXPECT_EQ(GetDocument().documentElement(), head->parentNode());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/editing_state.cc b/chromium/third_party/blink/renderer/core/editing/commands/editing_state.cc
new file mode 100644
index 00000000000..8c20b9f5a12
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/editing_state.cc
@@ -0,0 +1,36 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/editing_state.h"
+
+namespace blink {
+
+EditingState::EditingState() = default;
+
+EditingState::~EditingState() = default;
+
+void EditingState::Abort() {
+ DCHECK(!is_aborted_);
+ is_aborted_ = true;
+}
+
+// ---
+IgnorableEditingAbortState::IgnorableEditingAbortState() = default;
+
+IgnorableEditingAbortState::~IgnorableEditingAbortState() = default;
+
+#if DCHECK_IS_ON()
+// ---
+
+NoEditingAbortChecker::NoEditingAbortChecker(const char* file, int line)
+ : file_(file), line_(line) {}
+
+NoEditingAbortChecker::~NoEditingAbortChecker() {
+ DCHECK_AT(!editing_state_.IsAborted(), file_, line_)
+ << "The operation should not have been aborted.";
+}
+
+#endif
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/editing_state.h b/chromium/third_party/blink/renderer/core/editing/commands/editing_state.h
new file mode 100644
index 00000000000..e1840d17c0d
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/editing_state.h
@@ -0,0 +1,99 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_EDITING_STATE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_EDITING_STATE_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+
+namespace blink {
+
+// EditingState represents current editing command running state for propagating
+// DOM tree mutation operation failure to callers.
+//
+// Example usage:
+// EditingState editingState;
+// ...
+// functionMutatesDOMTree(..., &editingState);
+// if (editingState.isAborted())
+// return;
+//
+class CORE_EXPORT EditingState final {
+ STACK_ALLOCATED();
+
+ public:
+ EditingState();
+ ~EditingState();
+
+ void Abort();
+ bool IsAborted() const { return is_aborted_; }
+
+ private:
+ bool is_aborted_ = false;
+
+ DISALLOW_COPY_AND_ASSIGN(EditingState);
+};
+
+// TODO(yosin): Once all commands aware |EditingState|, we get rid of
+// |IgnorableEditingAbortState | class
+class IgnorableEditingAbortState final {
+ STACK_ALLOCATED();
+
+ public:
+ IgnorableEditingAbortState();
+ ~IgnorableEditingAbortState();
+
+ EditingState* GetEditingState() { return &editing_state_; }
+
+ private:
+ EditingState editing_state_;
+
+ DISALLOW_COPY_AND_ASSIGN(IgnorableEditingAbortState);
+};
+
+// Abort the editing command if the specified expression is true.
+#define ABORT_EDITING_COMMAND_IF(expr) \
+ do { \
+ if (expr) { \
+ editing_state->Abort(); \
+ return; \
+ } \
+ } while (false)
+
+#if DCHECK_IS_ON()
+// This class is inspired by |NoExceptionStateAssertionChecker|.
+class NoEditingAbortChecker final {
+ STACK_ALLOCATED();
+
+ public:
+ NoEditingAbortChecker(const char* file, int line);
+ ~NoEditingAbortChecker();
+
+ EditingState* GetEditingState() { return &editing_state_; }
+
+ private:
+ EditingState editing_state_;
+ const char* const file_;
+ int const line_;
+
+ DISALLOW_COPY_AND_ASSIGN(NoEditingAbortChecker);
+};
+
+// If a function with EditingState* argument should not be aborted,
+// ASSERT_NO_EDITING_ABORT should be specified.
+// fooFunc(...., ASSERT_NO_EDITING_ABORT);
+// It causes an assertion failure If DCHECK_IS_ON() and the function was aborted
+// unexpectedly.
+#define ASSERT_NO_EDITING_ABORT \
+ (NoEditingAbortChecker(__FILE__, __LINE__).GetEditingState())
+#else
+#define ASSERT_NO_EDITING_ABORT (IgnorableEditingAbortState().GetEditingState())
+#endif
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_EDITING_STATE_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/editor_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/editor_command.cc
new file mode 100644
index 00000000000..9125c0ce988
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/editor_command.cc
@@ -0,0 +1,2006 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
+ * Copyright (C) 2009 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/editor_command.h"
+
+#include "third_party/blink/public/platform/platform.h"
+#include "third_party/blink/public/platform/web_editing_command_type.h"
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/css_property_names.h"
+#include "third_party/blink/renderer/core/css_value_keywords.h"
+#include "third_party/blink/renderer/core/dom/events/event.h"
+#include "third_party/blink/renderer/core/dom/tag_collection.h"
+#include "third_party/blink/renderer/core/editing/commands/clipboard_commands.h"
+#include "third_party/blink/renderer/core/editing/commands/create_link_command.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/commands/editor_command_names.h"
+#include "third_party/blink/renderer/core/editing/commands/format_block_command.h"
+#include "third_party/blink/renderer/core/editing/commands/indent_outdent_command.h"
+#include "third_party/blink/renderer/core/editing/commands/insert_commands.h"
+#include "third_party/blink/renderer/core/editing/commands/move_commands.h"
+#include "third_party/blink/renderer/core/editing/commands/remove_format_command.h"
+#include "third_party/blink/renderer/core/editing/commands/style_commands.h"
+#include "third_party/blink/renderer/core/editing/commands/typing_command.h"
+#include "third_party/blink/renderer/core/editing/commands/unlink_command.h"
+#include "third_party/blink/renderer/core/editing/editing_tri_state.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/selection_modifier.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/set_selection_options.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_checker.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/input/event_handler.h"
+#include "third_party/blink/renderer/core/page/chrome_client.h"
+#include "third_party/blink/renderer/core/page/page.h"
+#include "third_party/blink/renderer/platform/histogram.h"
+#include "third_party/blink/renderer/platform/kill_ring.h"
+#include "third_party/blink/renderer/platform/scroll/scrollbar.h"
+#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
+
+#include <iterator>
+
+namespace blink {
+
+using namespace HTMLNames;
+
+namespace {
+
+struct CommandNameEntry {
+ const char* name;
+ WebEditingCommandType type;
+};
+
+const CommandNameEntry kCommandNameEntries[] = {
+#define V(name) {#name, WebEditingCommandType::k##name},
+ FOR_EACH_BLINK_EDITING_COMMAND_NAME(V)
+#undef V
+};
+// Handles all commands except WebEditingCommandType::Invalid.
+static_assert(
+ arraysize(kCommandNameEntries) + 1 ==
+ static_cast<size_t>(WebEditingCommandType::kNumberOfCommandTypes),
+ "must handle all valid WebEditingCommandType");
+
+WebEditingCommandType WebEditingCommandTypeFromCommandName(
+ const String& command_name) {
+ const CommandNameEntry* result = std::lower_bound(
+ std::begin(kCommandNameEntries), std::end(kCommandNameEntries),
+ command_name, [](const CommandNameEntry& entry, const String& needle) {
+ return CodePointCompareIgnoringASCIICase(needle, entry.name) > 0;
+ });
+ if (result != std::end(kCommandNameEntries) &&
+ CodePointCompareIgnoringASCIICase(command_name, result->name) == 0)
+ return result->type;
+ return WebEditingCommandType::kInvalid;
+}
+
+// |frame| is only used for |InsertNewline| due to how |executeInsertNewline()|
+// works.
+InputEvent::InputType InputTypeFromCommandType(
+ WebEditingCommandType command_type,
+ LocalFrame& frame) {
+ // We only handle InputType on spec for 'beforeinput'.
+ // http://w3c.github.io/editing/input-events.html
+ using CommandType = WebEditingCommandType;
+ using InputType = InputEvent::InputType;
+
+ // |executeInsertNewline()| could do two things but we have no other ways to
+ // predict.
+ if (command_type == CommandType::kInsertNewline)
+ return frame.GetEditor().CanEditRichly() ? InputType::kInsertParagraph
+ : InputType::kInsertLineBreak;
+
+ switch (command_type) {
+ // Insertion.
+ case CommandType::kInsertBacktab:
+ case CommandType::kInsertText:
+ return InputType::kInsertText;
+ case CommandType::kInsertLineBreak:
+ return InputType::kInsertLineBreak;
+ case CommandType::kInsertParagraph:
+ case CommandType::kInsertNewlineInQuotedContent:
+ return InputType::kInsertParagraph;
+ case CommandType::kInsertHorizontalRule:
+ return InputType::kInsertHorizontalRule;
+ case CommandType::kInsertOrderedList:
+ return InputType::kInsertOrderedList;
+ case CommandType::kInsertUnorderedList:
+ return InputType::kInsertUnorderedList;
+
+ // Deletion.
+ case CommandType::kDelete:
+ case CommandType::kDeleteBackward:
+ case CommandType::kDeleteBackwardByDecomposingPreviousCharacter:
+ return InputType::kDeleteContentBackward;
+ case CommandType::kDeleteForward:
+ return InputType::kDeleteContentForward;
+ case CommandType::kDeleteToBeginningOfLine:
+ return InputType::kDeleteSoftLineBackward;
+ case CommandType::kDeleteToEndOfLine:
+ return InputType::kDeleteSoftLineForward;
+ case CommandType::kDeleteWordBackward:
+ return InputType::kDeleteWordBackward;
+ case CommandType::kDeleteWordForward:
+ return InputType::kDeleteWordForward;
+ case CommandType::kDeleteToBeginningOfParagraph:
+ return InputType::kDeleteHardLineBackward;
+ case CommandType::kDeleteToEndOfParagraph:
+ return InputType::kDeleteHardLineForward;
+ // TODO(chongz): Find appreciate InputType for following commands.
+ case CommandType::kDeleteToMark:
+ return InputType::kNone;
+
+ // Command.
+ case CommandType::kUndo:
+ return InputType::kHistoryUndo;
+ case CommandType::kRedo:
+ return InputType::kHistoryRedo;
+ // Cut and Paste will be handled in |Editor::dispatchCPPEvent()|.
+
+ // Styling.
+ case CommandType::kBold:
+ case CommandType::kToggleBold:
+ return InputType::kFormatBold;
+ case CommandType::kItalic:
+ case CommandType::kToggleItalic:
+ return InputType::kFormatItalic;
+ case CommandType::kUnderline:
+ case CommandType::kToggleUnderline:
+ return InputType::kFormatUnderline;
+ case CommandType::kStrikethrough:
+ return InputType::kFormatStrikeThrough;
+ case CommandType::kSuperscript:
+ return InputType::kFormatSuperscript;
+ case CommandType::kSubscript:
+ return InputType::kFormatSubscript;
+ default:
+ return InputType::kNone;
+ }
+}
+
+StaticRangeVector* RangesFromCurrentSelectionOrExtendCaret(
+ const LocalFrame& frame,
+ SelectionModifyDirection direction,
+ TextGranularity granularity) {
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+ SelectionModifier selection_modifier(
+ frame, frame.Selection().GetSelectionInDOMTree());
+ selection_modifier.SetSelectionIsDirectional(
+ frame.Selection().IsDirectional());
+ if (selection_modifier.Selection().IsCaret())
+ selection_modifier.Modify(SelectionModifyAlteration::kExtend, direction,
+ granularity);
+ StaticRangeVector* ranges = new StaticRangeVector;
+ // We only supports single selections.
+ if (selection_modifier.Selection().IsNone())
+ return ranges;
+ ranges->push_back(StaticRange::Create(
+ FirstEphemeralRangeOf(selection_modifier.Selection())));
+ return ranges;
+}
+
+EphemeralRange ComputeRangeForTranspose(LocalFrame& frame) {
+ const VisibleSelection& selection =
+ frame.Selection().ComputeVisibleSelectionInDOMTree();
+ if (!selection.IsCaret())
+ return EphemeralRange();
+
+ // Make a selection that goes back one character and forward two characters.
+ const VisiblePosition& caret = selection.VisibleStart();
+ const VisiblePosition& next =
+ IsEndOfParagraph(caret) ? caret : NextPositionOf(caret);
+ const VisiblePosition& previous = PreviousPositionOf(next);
+ if (next.DeepEquivalent() == previous.DeepEquivalent())
+ return EphemeralRange();
+ const VisiblePosition& previous_of_previous = PreviousPositionOf(previous);
+ if (!InSameParagraph(next, previous_of_previous))
+ return EphemeralRange();
+ return MakeRange(previous_of_previous, next);
+}
+
+} // anonymous namespace
+
+class EditorInternalCommand {
+ public:
+ WebEditingCommandType command_type;
+ bool (*execute)(LocalFrame&, Event*, EditorCommandSource, const String&);
+ bool (*is_supported_from_dom)(LocalFrame*);
+ bool (*is_enabled)(LocalFrame&, Event*, EditorCommandSource);
+ EditingTriState (*state)(LocalFrame&, Event*);
+ String (*value)(const EditorInternalCommand&, LocalFrame&, Event*);
+ bool is_text_insertion;
+ bool (*can_execute)(LocalFrame&, EditorCommandSource);
+};
+
+static const bool kNotTextInsertion = false;
+static const bool kIsTextInsertion = true;
+
+static bool ExecuteApplyParagraphStyle(LocalFrame& frame,
+ EditorCommandSource source,
+ InputEvent::InputType input_type,
+ CSSPropertyID property_id,
+ const String& property_value) {
+ MutableCSSPropertyValueSet* style =
+ MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ style->SetProperty(property_id, property_value, /* important */ false,
+ frame.GetDocument()->GetSecureContextMode());
+ // FIXME: We don't call shouldApplyStyle when the source is DOM; is there a
+ // good reason for that?
+ switch (source) {
+ case EditorCommandSource::kMenuOrKeyBinding:
+ frame.GetEditor().ApplyParagraphStyleToSelection(style, input_type);
+ return true;
+ case EditorCommandSource::kDOM:
+ frame.GetEditor().ApplyParagraphStyle(style, input_type);
+ return true;
+ }
+ NOTREACHED();
+ return false;
+}
+
+bool ExpandSelectionToGranularity(LocalFrame& frame,
+ TextGranularity granularity) {
+ const VisibleSelection& selection = CreateVisibleSelectionWithGranularity(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(
+ frame.Selection().ComputeVisibleSelectionInDOMTree().Base(),
+ frame.Selection().ComputeVisibleSelectionInDOMTree().Extent())
+ .Build(),
+ granularity);
+ const EphemeralRange new_range = selection.ToNormalizedEphemeralRange();
+ if (new_range.IsNull())
+ return false;
+ if (new_range.IsCollapsed())
+ return false;
+ frame.Selection().SetSelection(
+ SelectionInDOMTree::Builder().SetBaseAndExtent(new_range).Build(),
+ SetSelectionOptions::Builder().SetShouldCloseTyping(true).Build());
+ return true;
+}
+
+static bool HasChildTags(Element& element, const QualifiedName& tag_name) {
+ return !element.getElementsByTagName(tag_name.LocalName())->IsEmpty();
+}
+
+static EditingTriState SelectionListState(const FrameSelection& selection,
+ const QualifiedName& tag_name) {
+ if (selection.ComputeVisibleSelectionInDOMTreeDeprecated().IsCaret()) {
+ if (EnclosingElementWithTag(
+ selection.ComputeVisibleSelectionInDOMTreeDeprecated().Start(),
+ tag_name))
+ return EditingTriState::kTrue;
+ } else if (selection.ComputeVisibleSelectionInDOMTreeDeprecated().IsRange()) {
+ Element* start_element = EnclosingElementWithTag(
+ selection.ComputeVisibleSelectionInDOMTreeDeprecated().Start(),
+ tag_name);
+ Element* end_element = EnclosingElementWithTag(
+ selection.ComputeVisibleSelectionInDOMTreeDeprecated().End(), tag_name);
+
+ if (start_element && end_element && start_element == end_element) {
+ // If the selected list has the different type of list as child, return
+ // |FalseTriState|.
+ // See http://crbug.com/385374
+ if (HasChildTags(*start_element, tag_name.Matches(ulTag) ? olTag : ulTag))
+ return EditingTriState::kFalse;
+ return EditingTriState::kTrue;
+ }
+ }
+
+ return EditingTriState::kFalse;
+}
+
+static EphemeralRange UnionEphemeralRanges(const EphemeralRange& range1,
+ const EphemeralRange& range2) {
+ const Position start_position =
+ range1.StartPosition().CompareTo(range2.StartPosition()) <= 0
+ ? range1.StartPosition()
+ : range2.StartPosition();
+ const Position end_position =
+ range1.EndPosition().CompareTo(range2.EndPosition()) <= 0
+ ? range1.EndPosition()
+ : range2.EndPosition();
+ return EphemeralRange(start_position, end_position);
+}
+
+// Execute command functions
+
+static bool CanSmartCopyOrDelete(LocalFrame& frame) {
+ return frame.GetEditor().SmartInsertDeleteEnabled() &&
+ frame.Selection().Granularity() == TextGranularity::kWord;
+}
+
+static bool ExecuteCreateLink(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String& value) {
+ if (value.IsEmpty())
+ return false;
+ DCHECK(frame.GetDocument());
+ return CreateLinkCommand::Create(*frame.GetDocument(), value)->Apply();
+}
+
+static bool ExecuteDefaultParagraphSeparator(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String& value) {
+ if (DeprecatedEqualIgnoringCase(value, "div")) {
+ frame.GetEditor().SetDefaultParagraphSeparator(
+ EditorParagraphSeparator::kIsDiv);
+ return true;
+ }
+ if (DeprecatedEqualIgnoringCase(value, "p")) {
+ frame.GetEditor().SetDefaultParagraphSeparator(
+ EditorParagraphSeparator::kIsP);
+ }
+ return true;
+}
+
+static void PerformDelete(LocalFrame& frame) {
+ if (!frame.GetEditor().CanDelete())
+ return;
+
+ // TODO(editing-dev): The use of UpdateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ // |SelectedRange| requires clean layout for visible selection normalization.
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ frame.GetEditor().AddToKillRing(frame.GetEditor().SelectedRange());
+ // TODO(chongz): |Editor::performDelete()| has no direction.
+ // https://github.com/w3c/editing/issues/130
+ frame.GetEditor().DeleteSelectionWithSmartDelete(
+ CanSmartCopyOrDelete(frame) ? DeleteMode::kSmart : DeleteMode::kSimple,
+ InputEvent::InputType::kDeleteContentBackward);
+
+ // clear the "start new kill ring sequence" setting, because it was set to
+ // true when the selection was updated by deleting the range
+ frame.GetEditor().SetStartNewKillRingSequence(false);
+}
+
+static bool ExecuteDelete(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ switch (source) {
+ case EditorCommandSource::kMenuOrKeyBinding: {
+ // Doesn't modify the text if the current selection isn't a range.
+ PerformDelete(frame);
+ return true;
+ }
+ case EditorCommandSource::kDOM:
+ // If the current selection is a caret, delete the preceding character. IE
+ // performs forwardDelete, but we currently side with Firefox. Doesn't
+ // scroll to make the selection visible, or modify the kill ring (this
+ // time, siding with IE, not Firefox).
+ DCHECK(frame.GetDocument());
+ TypingCommand::DeleteKeyPressed(
+ *frame.GetDocument(),
+ frame.Selection().Granularity() == TextGranularity::kWord
+ ? TypingCommand::kSmartDelete
+ : 0);
+ return true;
+ }
+ NOTREACHED();
+ return false;
+}
+
+static bool DeleteWithDirection(LocalFrame& frame,
+ DeleteDirection direction,
+ TextGranularity granularity,
+ bool kill_ring,
+ bool is_typing_action) {
+ Editor& editor = frame.GetEditor();
+ if (!editor.CanEdit())
+ return false;
+
+ EditingState editing_state;
+ if (frame.Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .IsRange()) {
+ if (is_typing_action) {
+ DCHECK(frame.GetDocument());
+ TypingCommand::DeleteKeyPressed(
+ *frame.GetDocument(),
+ CanSmartCopyOrDelete(frame) ? TypingCommand::kSmartDelete : 0,
+ granularity);
+ editor.RevealSelectionAfterEditingOperation();
+ } else {
+ if (kill_ring)
+ editor.AddToKillRing(editor.SelectedRange());
+ editor.DeleteSelectionWithSmartDelete(
+ CanSmartCopyOrDelete(frame) ? DeleteMode::kSmart
+ : DeleteMode::kSimple,
+ DeletionInputTypeFromTextGranularity(direction, granularity));
+ // Implicitly calls revealSelectionAfterEditingOperation().
+ }
+ } else {
+ TypingCommand::Options options = 0;
+ if (CanSmartCopyOrDelete(frame))
+ options |= TypingCommand::kSmartDelete;
+ if (kill_ring)
+ options |= TypingCommand::kKillRing;
+ switch (direction) {
+ case DeleteDirection::kForward:
+ DCHECK(frame.GetDocument());
+ TypingCommand::ForwardDeleteKeyPressed(
+ *frame.GetDocument(), &editing_state, options, granularity);
+ if (editing_state.IsAborted())
+ return false;
+ break;
+ case DeleteDirection::kBackward:
+ DCHECK(frame.GetDocument());
+ TypingCommand::DeleteKeyPressed(*frame.GetDocument(), options,
+ granularity);
+ break;
+ }
+ editor.RevealSelectionAfterEditingOperation();
+ }
+
+ // FIXME: We should to move this down into deleteKeyPressed.
+ // clear the "start new kill ring sequence" setting, because it was set to
+ // true when the selection was updated by deleting the range
+ if (kill_ring)
+ editor.SetStartNewKillRingSequence(false);
+
+ return true;
+}
+
+static bool ExecuteDeleteBackward(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ DeleteWithDirection(frame, DeleteDirection::kBackward,
+ TextGranularity::kCharacter, false, true);
+ return true;
+}
+
+static bool ExecuteDeleteBackwardByDecomposingPreviousCharacter(
+ LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ DLOG(ERROR) << "DeleteBackwardByDecomposingPreviousCharacter is not "
+ "implemented, doing DeleteBackward instead";
+ DeleteWithDirection(frame, DeleteDirection::kBackward,
+ TextGranularity::kCharacter, false, true);
+ return true;
+}
+
+static bool ExecuteDeleteForward(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ DeleteWithDirection(frame, DeleteDirection::kForward,
+ TextGranularity::kCharacter, false, true);
+ return true;
+}
+
+static bool ExecuteDeleteToBeginningOfLine(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ DeleteWithDirection(frame, DeleteDirection::kBackward,
+ TextGranularity::kLineBoundary, true, false);
+ return true;
+}
+
+static bool ExecuteDeleteToBeginningOfParagraph(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ DeleteWithDirection(frame, DeleteDirection::kBackward,
+ TextGranularity::kParagraphBoundary, true, false);
+ return true;
+}
+
+static bool ExecuteDeleteToEndOfLine(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ // Despite its name, this command should delete the newline at the end of a
+ // paragraph if you are at the end of a paragraph (like
+ // DeleteToEndOfParagraph).
+ DeleteWithDirection(frame, DeleteDirection::kForward,
+ TextGranularity::kLineBoundary, true, false);
+ return true;
+}
+
+static bool ExecuteDeleteToEndOfParagraph(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ // Despite its name, this command should delete the newline at the end of
+ // a paragraph if you are at the end of a paragraph.
+ DeleteWithDirection(frame, DeleteDirection::kForward,
+ TextGranularity::kParagraphBoundary, true, false);
+ return true;
+}
+
+static bool ExecuteDeleteToMark(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ const EphemeralRange mark =
+ frame.GetEditor().Mark().ToNormalizedEphemeralRange();
+ if (mark.IsNotNull()) {
+ frame.Selection().SetSelection(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(
+ UnionEphemeralRanges(mark, frame.GetEditor().SelectedRange()))
+ .Build(),
+ SetSelectionOptions::Builder().SetShouldCloseTyping(true).Build());
+ }
+ PerformDelete(frame);
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+ frame.GetEditor().SetMark();
+ return true;
+}
+
+static bool ExecuteDeleteWordBackward(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ DeleteWithDirection(frame, DeleteDirection::kBackward, TextGranularity::kWord,
+ true, false);
+ return true;
+}
+
+static bool ExecuteDeleteWordForward(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ DeleteWithDirection(frame, DeleteDirection::kForward, TextGranularity::kWord,
+ true, false);
+ return true;
+}
+
+static bool ExecuteFindString(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String& value) {
+ return Editor::FindString(frame, value, kCaseInsensitive | kWrapAround);
+}
+
+static bool ExecuteFormatBlock(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String& value) {
+ String tag_name = value.DeprecatedLower();
+ if (tag_name[0] == '<' && tag_name[tag_name.length() - 1] == '>')
+ tag_name = tag_name.Substring(1, tag_name.length() - 2);
+
+ AtomicString local_name, prefix;
+ if (!Document::ParseQualifiedName(AtomicString(tag_name), prefix, local_name,
+ IGNORE_EXCEPTION_FOR_TESTING))
+ return false;
+ QualifiedName qualified_tag_name(prefix, local_name, xhtmlNamespaceURI);
+
+ DCHECK(frame.GetDocument());
+ FormatBlockCommand* command =
+ FormatBlockCommand::Create(*frame.GetDocument(), qualified_tag_name);
+ command->Apply();
+ return command->DidApply();
+}
+
+static bool ExecuteForwardDelete(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ EditingState editing_state;
+ switch (source) {
+ case EditorCommandSource::kMenuOrKeyBinding:
+ DeleteWithDirection(frame, DeleteDirection::kForward,
+ TextGranularity::kCharacter, false, true);
+ return true;
+ case EditorCommandSource::kDOM:
+ // Doesn't scroll to make the selection visible, or modify the kill ring.
+ // ForwardDelete is not implemented in IE or Firefox, so this behavior is
+ // only needed for backward compatibility with ourselves, and for
+ // consistency with Delete.
+ DCHECK(frame.GetDocument());
+ TypingCommand::ForwardDeleteKeyPressed(*frame.GetDocument(),
+ &editing_state);
+ if (editing_state.IsAborted())
+ return false;
+ return true;
+ }
+ NOTREACHED();
+ return false;
+}
+
+static bool ExecuteIgnoreSpelling(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.GetSpellChecker().IgnoreSpelling();
+ return true;
+}
+
+static bool ExecuteIndent(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ DCHECK(frame.GetDocument());
+ return IndentOutdentCommand::Create(*frame.GetDocument(),
+ IndentOutdentCommand::kIndent)
+ ->Apply();
+}
+
+static bool ExecuteJustifyCenter(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ return ExecuteApplyParagraphStyle(frame, source,
+ InputEvent::InputType::kFormatJustifyCenter,
+ CSSPropertyTextAlign, "center");
+}
+
+static bool ExecuteJustifyFull(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ return ExecuteApplyParagraphStyle(frame, source,
+ InputEvent::InputType::kFormatJustifyFull,
+ CSSPropertyTextAlign, "justify");
+}
+
+static bool ExecuteJustifyLeft(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ return ExecuteApplyParagraphStyle(frame, source,
+ InputEvent::InputType::kFormatJustifyLeft,
+ CSSPropertyTextAlign, "left");
+}
+
+static bool ExecuteJustifyRight(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ return ExecuteApplyParagraphStyle(frame, source,
+ InputEvent::InputType::kFormatJustifyRight,
+ CSSPropertyTextAlign, "right");
+}
+
+static bool ExecuteOutdent(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ DCHECK(frame.GetDocument());
+ return IndentOutdentCommand::Create(*frame.GetDocument(),
+ IndentOutdentCommand::kOutdent)
+ ->Apply();
+}
+
+static bool ExecuteToggleOverwrite(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.GetEditor().ToggleOverwriteModeEnabled();
+ return true;
+}
+
+static bool ExecutePrint(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ Page* page = frame.GetPage();
+ if (!page)
+ return false;
+ return page->GetChromeClient().Print(&frame);
+}
+
+static bool ExecuteRedo(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.GetEditor().Redo();
+ return true;
+}
+
+static bool ExecuteRemoveFormat(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ DCHECK(frame.GetDocument());
+ RemoveFormatCommand::Create(*frame.GetDocument())->Apply();
+
+ return true;
+}
+
+static bool ExecuteScrollPageBackward(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ return frame.GetEventHandler().BubblingScroll(kScrollBlockDirectionBackward,
+ kScrollByPage);
+}
+
+static bool ExecuteScrollPageForward(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ return frame.GetEventHandler().BubblingScroll(kScrollBlockDirectionForward,
+ kScrollByPage);
+}
+
+static bool ExecuteScrollLineUp(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ return frame.GetEventHandler().BubblingScroll(kScrollUpIgnoringWritingMode,
+ kScrollByLine);
+}
+
+static bool ExecuteScrollLineDown(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ return frame.GetEventHandler().BubblingScroll(kScrollDownIgnoringWritingMode,
+ kScrollByLine);
+}
+
+static bool ExecuteScrollToBeginningOfDocument(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ return frame.GetEventHandler().BubblingScroll(kScrollBlockDirectionBackward,
+ kScrollByDocument);
+}
+
+static bool ExecuteScrollToEndOfDocument(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ return frame.GetEventHandler().BubblingScroll(kScrollBlockDirectionForward,
+ kScrollByDocument);
+}
+
+static bool ExecuteSelectAll(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ const SetSelectionBy set_selection_by =
+ source == EditorCommandSource::kMenuOrKeyBinding
+ ? SetSelectionBy::kUser
+ : SetSelectionBy::kSystem;
+ frame.Selection().SelectAll(set_selection_by);
+ return true;
+}
+
+static bool ExecuteSelectLine(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ return ExpandSelectionToGranularity(frame, TextGranularity::kLine);
+}
+
+static bool ExecuteSelectParagraph(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ return ExpandSelectionToGranularity(frame, TextGranularity::kParagraph);
+}
+
+static bool ExecuteSelectSentence(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ return ExpandSelectionToGranularity(frame, TextGranularity::kSentence);
+}
+
+static bool ExecuteSelectToMark(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ const EphemeralRange mark =
+ frame.GetEditor().Mark().ToNormalizedEphemeralRange();
+ EphemeralRange selection = frame.GetEditor().SelectedRange();
+ if (mark.IsNull() || selection.IsNull())
+ return false;
+ frame.Selection().SetSelection(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(UnionEphemeralRanges(mark, selection))
+ .Build(),
+ SetSelectionOptions::Builder().SetShouldCloseTyping(true).Build());
+ return true;
+}
+
+static bool ExecuteSelectWord(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ return ExpandSelectionToGranularity(frame, TextGranularity::kWord);
+}
+
+static bool ExecuteSetMark(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.GetEditor().SetMark();
+ return true;
+}
+
+static bool ExecuteSwapWithMark(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ const VisibleSelection mark(frame.GetEditor().Mark());
+ const VisibleSelection& selection =
+ frame.Selection().ComputeVisibleSelectionInDOMTreeDeprecated();
+ const bool mark_is_directional = frame.GetEditor().MarkIsDirectional();
+ if (mark.IsNone() || selection.IsNone())
+ return false;
+
+ frame.GetEditor().SetMark();
+ frame.Selection().SetSelection(mark.AsSelection(),
+ SetSelectionOptions::Builder()
+ .SetIsDirectional(mark_is_directional)
+ .Build());
+ return true;
+}
+
+static bool ExecuteTranspose(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ Editor& editor = frame.GetEditor();
+ if (!editor.CanEdit())
+ return false;
+
+ Document* const document = frame.GetDocument();
+
+ // TODO(editing-dev): The use of UpdateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ document->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ const EphemeralRange& range = ComputeRangeForTranspose(frame);
+ if (range.IsNull())
+ return false;
+
+ // Transpose the two characters.
+ const String& text = PlainText(range);
+ if (text.length() != 2)
+ return false;
+ const String& transposed = text.Right(1) + text.Left(1);
+
+ if (DispatchBeforeInputInsertText(
+ EventTargetNodeForDocument(document), transposed,
+ InputEvent::InputType::kInsertTranspose,
+ new StaticRangeVector(1, StaticRange::Create(range))) !=
+ DispatchEventResult::kNotCanceled)
+ return false;
+
+ // 'beforeinput' event handler may destroy document->
+ if (frame.GetDocument() != document)
+ return false;
+
+ // TODO(editing-dev): The use of UpdateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ document->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // 'beforeinput' event handler may change selection, we need to re-calculate
+ // range.
+ const EphemeralRange& new_range = ComputeRangeForTranspose(frame);
+ if (new_range.IsNull())
+ return false;
+
+ const String& new_text = PlainText(new_range);
+ if (new_text.length() != 2)
+ return false;
+ const String& new_transposed = new_text.Right(1) + new_text.Left(1);
+
+ const SelectionInDOMTree& new_selection =
+ SelectionInDOMTree::Builder().SetBaseAndExtent(new_range).Build();
+
+ // Select the two characters.
+ if (CreateVisibleSelection(new_selection) !=
+ frame.Selection().ComputeVisibleSelectionInDOMTree())
+ frame.Selection().SetSelectionAndEndTyping(new_selection);
+
+ // Insert the transposed characters.
+ editor.ReplaceSelectionWithText(new_transposed, false, false,
+ InputEvent::InputType::kInsertTranspose);
+ return true;
+}
+
+static bool ExecuteUndo(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.GetEditor().Undo();
+ return true;
+}
+
+static bool ExecuteUnlink(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ DCHECK(frame.GetDocument());
+ return UnlinkCommand::Create(*frame.GetDocument())->Apply();
+}
+
+static bool ExecuteUnselect(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Clear();
+ return true;
+}
+
+static bool ExecuteYank(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ const String& yank_string = frame.GetEditor().GetKillRing().Yank();
+ if (DispatchBeforeInputInsertText(
+ EventTargetNodeForDocument(frame.GetDocument()), yank_string,
+ InputEvent::InputType::kInsertFromYank) !=
+ DispatchEventResult::kNotCanceled)
+ return true;
+
+ // 'beforeinput' event handler may destroy document.
+ if (frame.GetDocument()->GetFrame() != &frame)
+ return false;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ frame.GetEditor().InsertTextWithoutSendingTextEvent(
+ yank_string, false, nullptr, InputEvent::InputType::kInsertFromYank);
+ frame.GetEditor().GetKillRing().SetToYankedState();
+ return true;
+}
+
+static bool ExecuteYankAndSelect(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ const String& yank_string = frame.GetEditor().GetKillRing().Yank();
+ if (DispatchBeforeInputInsertText(
+ EventTargetNodeForDocument(frame.GetDocument()), yank_string,
+ InputEvent::InputType::kInsertFromYank) !=
+ DispatchEventResult::kNotCanceled)
+ return true;
+
+ // 'beforeinput' event handler may destroy document.
+ if (frame.GetDocument()->GetFrame() != &frame)
+ return false;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ frame.GetEditor().InsertTextWithoutSendingTextEvent(
+ frame.GetEditor().GetKillRing().Yank(), true, nullptr,
+ InputEvent::InputType::kInsertFromYank);
+ frame.GetEditor().GetKillRing().SetToYankedState();
+ return true;
+}
+
+// Supported functions
+
+static bool Supported(LocalFrame*) {
+ return true;
+}
+
+static bool SupportedFromMenuOrKeyBinding(LocalFrame*) {
+ return false;
+}
+
+// Enabled functions
+
+static bool Enabled(LocalFrame&, Event*, EditorCommandSource) {
+ return true;
+}
+
+static bool EnabledVisibleSelection(LocalFrame& frame,
+ Event* event,
+ EditorCommandSource source) {
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (source == EditorCommandSource::kMenuOrKeyBinding &&
+ !frame.Selection().SelectionHasFocus())
+ return false;
+
+ // The term "visible" here includes a caret in editable text or a range in any
+ // text.
+ const VisibleSelection& selection =
+ CreateVisibleSelection(frame.GetEditor().SelectionForCommand(event));
+ return (selection.IsCaret() && selection.IsContentEditable()) ||
+ selection.IsRange();
+}
+
+static bool EnabledVisibleSelectionAndMark(LocalFrame& frame,
+ Event* event,
+ EditorCommandSource source) {
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (source == EditorCommandSource::kMenuOrKeyBinding &&
+ !frame.Selection().SelectionHasFocus())
+ return false;
+
+ const VisibleSelection& selection =
+ CreateVisibleSelection(frame.GetEditor().SelectionForCommand(event));
+ return ((selection.IsCaret() && selection.IsContentEditable()) ||
+ selection.IsRange()) &&
+ !frame.GetEditor().Mark().IsNone();
+}
+
+static bool EnableCaretInEditableText(LocalFrame& frame,
+ Event* event,
+ EditorCommandSource source) {
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (source == EditorCommandSource::kMenuOrKeyBinding &&
+ !frame.Selection().SelectionHasFocus())
+ return false;
+ const VisibleSelection& selection =
+ CreateVisibleSelection(frame.GetEditor().SelectionForCommand(event));
+ return selection.IsCaret() && selection.IsContentEditable();
+}
+
+static bool EnabledInEditableText(LocalFrame& frame,
+ Event* event,
+ EditorCommandSource source) {
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (source == EditorCommandSource::kMenuOrKeyBinding &&
+ !frame.Selection().SelectionHasFocus())
+ return false;
+ const SelectionInDOMTree selection =
+ frame.GetEditor().SelectionForCommand(event);
+ return RootEditableElementOf(
+ CreateVisiblePosition(selection.Base()).DeepEquivalent());
+}
+
+static bool EnabledDelete(LocalFrame& frame,
+ Event* event,
+ EditorCommandSource source) {
+ switch (source) {
+ case EditorCommandSource::kMenuOrKeyBinding:
+ return frame.Selection().SelectionHasFocus() &&
+ frame.GetEditor().CanDelete();
+ case EditorCommandSource::kDOM:
+ // "Delete" from DOM is like delete/backspace keypress, affects selected
+ // range if non-empty, otherwise removes a character
+ return EnabledInEditableText(frame, event, source);
+ }
+ NOTREACHED();
+ return false;
+}
+
+static bool EnabledInRichlyEditableText(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source) {
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (source == EditorCommandSource::kMenuOrKeyBinding &&
+ !frame.Selection().SelectionHasFocus())
+ return false;
+ const VisibleSelection& selection =
+ frame.Selection().ComputeVisibleSelectionInDOMTree();
+ return !selection.IsNone() && IsRichlyEditablePosition(selection.Base()) &&
+ selection.RootEditableElement();
+}
+
+static bool EnabledRangeInEditableText(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source) {
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (source == EditorCommandSource::kMenuOrKeyBinding &&
+ !frame.Selection().SelectionHasFocus())
+ return false;
+ return frame.Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .IsRange() &&
+ frame.Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .IsContentEditable();
+}
+
+static bool EnabledRangeInRichlyEditableText(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source) {
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (source == EditorCommandSource::kMenuOrKeyBinding &&
+ !frame.Selection().SelectionHasFocus())
+ return false;
+ const VisibleSelection& selection =
+ frame.Selection().ComputeVisibleSelectionInDOMTree();
+ return selection.IsRange() && IsRichlyEditablePosition(selection.Base());
+}
+
+static bool EnabledRedo(LocalFrame& frame, Event*, EditorCommandSource) {
+ return frame.GetEditor().CanRedo();
+}
+
+static bool EnabledUndo(LocalFrame& frame, Event*, EditorCommandSource) {
+ return frame.GetEditor().CanUndo();
+}
+
+static bool EnabledUnselect(LocalFrame& frame,
+ Event* event,
+ EditorCommandSource) {
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // The term "visible" here includes a caret in editable text or a range in any
+ // text.
+ const VisibleSelection& selection =
+ CreateVisibleSelection(frame.GetEditor().SelectionForCommand(event));
+ return (selection.IsCaret() && selection.IsContentEditable()) ||
+ selection.IsRange();
+}
+
+static bool EnabledSelectAll(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source) {
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+ const VisibleSelection& selection =
+ frame.Selection().ComputeVisibleSelectionInDOMTree();
+ if (selection.IsNone())
+ return true;
+ // Hidden selection appears as no selection to users, in which case user-
+ // triggered SelectAll should be enabled and act as if there is no selection.
+ if (source == EditorCommandSource::kMenuOrKeyBinding &&
+ frame.Selection().IsHidden())
+ return true;
+ if (Node* root = HighestEditableRoot(selection.Start())) {
+ if (!root->hasChildren())
+ return false;
+ // TODO(amaralp): Return false if already fully selected.
+ }
+ // TODO(amaralp): Address user-select handling.
+ return true;
+}
+
+// State functions
+
+static EditingTriState StateNone(LocalFrame&, Event*) {
+ return EditingTriState::kFalse;
+}
+
+EditingTriState StateOrderedList(LocalFrame& frame, Event*) {
+ return SelectionListState(frame.Selection(), olTag);
+}
+
+static EditingTriState StateUnorderedList(LocalFrame& frame, Event*) {
+ return SelectionListState(frame.Selection(), ulTag);
+}
+
+static EditingTriState StateJustifyCenter(LocalFrame& frame, Event*) {
+ return StyleCommands::StateStyle(frame, CSSPropertyTextAlign, "center");
+}
+
+static EditingTriState StateJustifyFull(LocalFrame& frame, Event*) {
+ return StyleCommands::StateStyle(frame, CSSPropertyTextAlign, "justify");
+}
+
+static EditingTriState StateJustifyLeft(LocalFrame& frame, Event*) {
+ return StyleCommands::StateStyle(frame, CSSPropertyTextAlign, "left");
+}
+
+static EditingTriState StateJustifyRight(LocalFrame& frame, Event*) {
+ return StyleCommands::StateStyle(frame, CSSPropertyTextAlign, "right");
+}
+
+// Value functions
+
+static String ValueStateOrNull(const EditorInternalCommand& self,
+ LocalFrame& frame,
+ Event* triggering_event) {
+ if (self.state == StateNone)
+ return String();
+ return self.state(frame, triggering_event) == EditingTriState::kTrue
+ ? "true"
+ : "false";
+}
+
+// The command has no value.
+// https://w3c.github.io/editing/execCommand.html#querycommandvalue()
+// > ... or has no value, return the empty string.
+static String ValueEmpty(const EditorInternalCommand&, LocalFrame&, Event*) {
+ return g_empty_string;
+}
+
+static String ValueDefaultParagraphSeparator(const EditorInternalCommand&,
+ LocalFrame& frame,
+ Event*) {
+ switch (frame.GetEditor().DefaultParagraphSeparator()) {
+ case EditorParagraphSeparator::kIsDiv:
+ return divTag.LocalName();
+ case EditorParagraphSeparator::kIsP:
+ return pTag.LocalName();
+ }
+
+ NOTREACHED();
+ return String();
+}
+
+static String ValueFormatBlock(const EditorInternalCommand&,
+ LocalFrame& frame,
+ Event*) {
+ const VisibleSelection& selection =
+ frame.Selection().ComputeVisibleSelectionInDOMTreeDeprecated();
+ if (selection.IsNone() || !selection.IsValidFor(*(frame.GetDocument())) ||
+ !selection.IsContentEditable())
+ return "";
+ Element* format_block_element =
+ FormatBlockCommand::ElementForFormatBlockCommand(
+ FirstEphemeralRangeOf(selection));
+ if (!format_block_element)
+ return "";
+ return format_block_element->localName();
+}
+
+// CanExectue functions
+
+static bool CanNotExecuteWhenDisabled(LocalFrame&, EditorCommandSource) {
+ return false;
+}
+
+// Map of functions
+
+static const EditorInternalCommand* InternalCommand(
+ const String& command_name) {
+ static const EditorInternalCommand kEditorCommands[] = {
+ // Lists all commands in blink::WebEditingCommandType.
+ // Must be ordered by |commandType| for index lookup.
+ // Covered by unit tests in EditingCommandTest.cpp
+ {WebEditingCommandType::kAlignJustified, ExecuteJustifyFull,
+ SupportedFromMenuOrKeyBinding, EnabledInRichlyEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kAlignLeft, ExecuteJustifyLeft,
+ SupportedFromMenuOrKeyBinding, EnabledInRichlyEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kAlignRight, ExecuteJustifyRight,
+ SupportedFromMenuOrKeyBinding, EnabledInRichlyEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kBackColor, StyleCommands::ExecuteBackColor,
+ Supported, EnabledInRichlyEditableText, StateNone,
+ StyleCommands::ValueBackColor, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ // FIXME: remove BackwardDelete when Safari for Windows stops using it.
+ {WebEditingCommandType::kBackwardDelete, ExecuteDeleteBackward,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kBold, StyleCommands::ExecuteToggleBold,
+ Supported, EnabledInRichlyEditableText, StyleCommands::StateBold,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kCopy, ClipboardCommands::ExecuteCopy, Supported,
+ ClipboardCommands::EnabledCopy, StateNone, ValueStateOrNull,
+ kNotTextInsertion, ClipboardCommands::CanWriteClipboard},
+ {WebEditingCommandType::kCreateLink, ExecuteCreateLink, Supported,
+ EnabledInRichlyEditableText, StateNone, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kCut, ClipboardCommands::ExecuteCut, Supported,
+ ClipboardCommands::EnabledCut, StateNone, ValueStateOrNull,
+ kNotTextInsertion, ClipboardCommands::CanWriteClipboard},
+ {WebEditingCommandType::kDefaultParagraphSeparator,
+ ExecuteDefaultParagraphSeparator, Supported, Enabled, StateNone,
+ ValueDefaultParagraphSeparator, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kDelete, ExecuteDelete, Supported, EnabledDelete,
+ StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kDeleteBackward, ExecuteDeleteBackward,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kDeleteBackwardByDecomposingPreviousCharacter,
+ ExecuteDeleteBackwardByDecomposingPreviousCharacter,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kDeleteForward, ExecuteDeleteForward,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kDeleteToBeginningOfLine,
+ ExecuteDeleteToBeginningOfLine, SupportedFromMenuOrKeyBinding,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kDeleteToBeginningOfParagraph,
+ ExecuteDeleteToBeginningOfParagraph, SupportedFromMenuOrKeyBinding,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kDeleteToEndOfLine, ExecuteDeleteToEndOfLine,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kDeleteToEndOfParagraph,
+ ExecuteDeleteToEndOfParagraph, SupportedFromMenuOrKeyBinding,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kDeleteToMark, ExecuteDeleteToMark,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kDeleteWordBackward, ExecuteDeleteWordBackward,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kDeleteWordForward, ExecuteDeleteWordForward,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kFindString, ExecuteFindString, Supported,
+ Enabled, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kFontName, StyleCommands::ExecuteFontName,
+ Supported, EnabledInRichlyEditableText, StateNone,
+ StyleCommands::ValueFontName, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kFontSize, StyleCommands::ExecuteFontSize,
+ Supported, EnabledInRichlyEditableText, StateNone,
+ StyleCommands::ValueFontSize, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kFontSizeDelta,
+ StyleCommands::ExecuteFontSizeDelta, Supported,
+ EnabledInRichlyEditableText, StateNone,
+ StyleCommands::ValueFontSizeDelta, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kForeColor, StyleCommands::ExecuteForeColor,
+ Supported, EnabledInRichlyEditableText, StateNone,
+ StyleCommands::ValueForeColor, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kFormatBlock, ExecuteFormatBlock, Supported,
+ EnabledInRichlyEditableText, StateNone, ValueFormatBlock,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kForwardDelete, ExecuteForwardDelete, Supported,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kHiliteColor, StyleCommands::ExecuteBackColor,
+ Supported, EnabledInRichlyEditableText, StateNone, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kIgnoreSpelling, ExecuteIgnoreSpelling,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kIndent, ExecuteIndent, Supported,
+ EnabledInRichlyEditableText, StateNone, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kInsertBacktab,
+ InsertCommands::ExecuteInsertBacktab, SupportedFromMenuOrKeyBinding,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kIsTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kInsertHTML, InsertCommands::ExecuteInsertHTML,
+ Supported, EnabledInEditableText, StateNone, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kInsertHorizontalRule,
+ InsertCommands::ExecuteInsertHorizontalRule, Supported,
+ EnabledInRichlyEditableText, StateNone, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kInsertImage, InsertCommands::ExecuteInsertImage,
+ Supported, EnabledInRichlyEditableText, StateNone, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kInsertLineBreak,
+ InsertCommands::ExecuteInsertLineBreak, Supported, EnabledInEditableText,
+ StateNone, ValueStateOrNull, kIsTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kInsertNewline,
+ InsertCommands::ExecuteInsertNewline, SupportedFromMenuOrKeyBinding,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kIsTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kInsertNewlineInQuotedContent,
+ InsertCommands::ExecuteInsertNewlineInQuotedContent, Supported,
+ EnabledInRichlyEditableText, StateNone, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kInsertOrderedList,
+ InsertCommands::ExecuteInsertOrderedList, Supported,
+ EnabledInRichlyEditableText, StateOrderedList, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kInsertParagraph,
+ InsertCommands::ExecuteInsertParagraph, Supported, EnabledInEditableText,
+ StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kInsertTab, InsertCommands::ExecuteInsertTab,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kIsTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kInsertText, InsertCommands::ExecuteInsertText,
+ Supported, EnabledInEditableText, StateNone, ValueStateOrNull,
+ kIsTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kInsertUnorderedList,
+ InsertCommands::ExecuteInsertUnorderedList, Supported,
+ EnabledInRichlyEditableText, StateUnorderedList, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kItalic, StyleCommands::ExecuteToggleItalic,
+ Supported, EnabledInRichlyEditableText, StyleCommands::StateItalic,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kJustifyCenter, ExecuteJustifyCenter, Supported,
+ EnabledInRichlyEditableText, StateJustifyCenter, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kJustifyFull, ExecuteJustifyFull, Supported,
+ EnabledInRichlyEditableText, StateJustifyFull, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kJustifyLeft, ExecuteJustifyLeft, Supported,
+ EnabledInRichlyEditableText, StateJustifyLeft, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kJustifyNone, ExecuteJustifyLeft, Supported,
+ EnabledInRichlyEditableText, StateNone, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kJustifyRight, ExecuteJustifyRight, Supported,
+ EnabledInRichlyEditableText, StateJustifyRight, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMakeTextWritingDirectionLeftToRight,
+ StyleCommands::ExecuteMakeTextWritingDirectionLeftToRight,
+ SupportedFromMenuOrKeyBinding, EnabledInRichlyEditableText,
+ StyleCommands::StateTextWritingDirectionLeftToRight, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMakeTextWritingDirectionNatural,
+ StyleCommands::ExecuteMakeTextWritingDirectionNatural,
+ SupportedFromMenuOrKeyBinding, EnabledInRichlyEditableText,
+ StyleCommands::StateTextWritingDirectionNatural, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMakeTextWritingDirectionRightToLeft,
+ StyleCommands::ExecuteMakeTextWritingDirectionRightToLeft,
+ SupportedFromMenuOrKeyBinding, EnabledInRichlyEditableText,
+ StyleCommands::StateTextWritingDirectionRightToLeft, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveBackward, MoveCommands::ExecuteMoveBackward,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveBackwardAndModifySelection,
+ MoveCommands::ExecuteMoveBackwardAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveDown, MoveCommands::ExecuteMoveDown,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveDownAndModifySelection,
+ MoveCommands::ExecuteMoveDownAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveForward, MoveCommands::ExecuteMoveForward,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveForwardAndModifySelection,
+ MoveCommands::ExecuteMoveForwardAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveLeft, MoveCommands::ExecuteMoveLeft,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveLeftAndModifySelection,
+ MoveCommands::ExecuteMoveLeftAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMovePageDown, MoveCommands::ExecuteMovePageDown,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMovePageDownAndModifySelection,
+ MoveCommands::ExecuteMovePageDownAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMovePageUp, MoveCommands::ExecuteMovePageUp,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMovePageUpAndModifySelection,
+ MoveCommands::ExecuteMovePageUpAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveParagraphBackward,
+ MoveCommands::ExecuteMoveParagraphBackward,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveParagraphBackwardAndModifySelection,
+ MoveCommands::ExecuteMoveParagraphBackwardAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveParagraphForward,
+ MoveCommands::ExecuteMoveParagraphForward, SupportedFromMenuOrKeyBinding,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveParagraphForwardAndModifySelection,
+ MoveCommands::ExecuteMoveParagraphForwardAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveRight, MoveCommands::ExecuteMoveRight,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveRightAndModifySelection,
+ MoveCommands::ExecuteMoveRightAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToBeginningOfDocument,
+ MoveCommands::ExecuteMoveToBeginningOfDocument,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToBeginningOfDocumentAndModifySelection,
+ MoveCommands::ExecuteMoveToBeginningOfDocumentAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToBeginningOfLine,
+ MoveCommands::ExecuteMoveToBeginningOfLine,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToBeginningOfLineAndModifySelection,
+ MoveCommands::ExecuteMoveToBeginningOfLineAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToBeginningOfParagraph,
+ MoveCommands::ExecuteMoveToBeginningOfParagraph,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToBeginningOfParagraphAndModifySelection,
+ MoveCommands::ExecuteMoveToBeginningOfParagraphAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToBeginningOfSentence,
+ MoveCommands::ExecuteMoveToBeginningOfSentence,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToBeginningOfSentenceAndModifySelection,
+ MoveCommands::ExecuteMoveToBeginningOfSentenceAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToEndOfDocument,
+ MoveCommands::ExecuteMoveToEndOfDocument, SupportedFromMenuOrKeyBinding,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToEndOfDocumentAndModifySelection,
+ MoveCommands::ExecuteMoveToEndOfDocumentAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToEndOfLine,
+ MoveCommands::ExecuteMoveToEndOfLine, SupportedFromMenuOrKeyBinding,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToEndOfLineAndModifySelection,
+ MoveCommands::ExecuteMoveToEndOfLineAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToEndOfParagraph,
+ MoveCommands::ExecuteMoveToEndOfParagraph, SupportedFromMenuOrKeyBinding,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToEndOfParagraphAndModifySelection,
+ MoveCommands::ExecuteMoveToEndOfParagraphAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToEndOfSentence,
+ MoveCommands::ExecuteMoveToEndOfSentence, SupportedFromMenuOrKeyBinding,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToEndOfSentenceAndModifySelection,
+ MoveCommands::ExecuteMoveToEndOfSentenceAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToLeftEndOfLine,
+ MoveCommands::ExecuteMoveToLeftEndOfLine, SupportedFromMenuOrKeyBinding,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToLeftEndOfLineAndModifySelection,
+ MoveCommands::ExecuteMoveToLeftEndOfLineAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToRightEndOfLine,
+ MoveCommands::ExecuteMoveToRightEndOfLine, SupportedFromMenuOrKeyBinding,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveToRightEndOfLineAndModifySelection,
+ MoveCommands::ExecuteMoveToRightEndOfLineAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveUp, MoveCommands::ExecuteMoveUp,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveUpAndModifySelection,
+ MoveCommands::ExecuteMoveUpAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveWordBackward,
+ MoveCommands::ExecuteMoveWordBackward, SupportedFromMenuOrKeyBinding,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveWordBackwardAndModifySelection,
+ MoveCommands::ExecuteMoveWordBackwardAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveWordForward,
+ MoveCommands::ExecuteMoveWordForward, SupportedFromMenuOrKeyBinding,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveWordForwardAndModifySelection,
+ MoveCommands::ExecuteMoveWordForwardAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveWordLeft, MoveCommands::ExecuteMoveWordLeft,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveWordLeftAndModifySelection,
+ MoveCommands::ExecuteMoveWordLeftAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveWordRight,
+ MoveCommands::ExecuteMoveWordRight, SupportedFromMenuOrKeyBinding,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kMoveWordRightAndModifySelection,
+ MoveCommands::ExecuteMoveWordRightAndModifySelection,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kOutdent, ExecuteOutdent, Supported,
+ EnabledInRichlyEditableText, StateNone, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kOverWrite, ExecuteToggleOverwrite,
+ SupportedFromMenuOrKeyBinding, EnabledInRichlyEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kPaste, ClipboardCommands::ExecutePaste,
+ ClipboardCommands::PasteSupported, ClipboardCommands::EnabledPaste,
+ StateNone, ValueStateOrNull, kNotTextInsertion,
+ ClipboardCommands::CanReadClipboard},
+ {WebEditingCommandType::kPasteAndMatchStyle,
+ ClipboardCommands::ExecutePasteAndMatchStyle, Supported,
+ ClipboardCommands::EnabledPaste, StateNone, ValueStateOrNull,
+ kNotTextInsertion, ClipboardCommands::CanReadClipboard},
+ {WebEditingCommandType::kPasteGlobalSelection,
+ ClipboardCommands::ExecutePasteGlobalSelection,
+ SupportedFromMenuOrKeyBinding, ClipboardCommands::EnabledPaste,
+ StateNone, ValueStateOrNull, kNotTextInsertion,
+ ClipboardCommands::CanReadClipboard},
+ {WebEditingCommandType::kPrint, ExecutePrint, Supported, Enabled,
+ StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kRedo, ExecuteRedo, Supported, EnabledRedo,
+ StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kRemoveFormat, ExecuteRemoveFormat, Supported,
+ EnabledRangeInEditableText, StateNone, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kScrollPageBackward, ExecuteScrollPageBackward,
+ SupportedFromMenuOrKeyBinding, Enabled, StateNone, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kScrollPageForward, ExecuteScrollPageForward,
+ SupportedFromMenuOrKeyBinding, Enabled, StateNone, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kScrollLineUp, ExecuteScrollLineUp,
+ SupportedFromMenuOrKeyBinding, Enabled, StateNone, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kScrollLineDown, ExecuteScrollLineDown,
+ SupportedFromMenuOrKeyBinding, Enabled, StateNone, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kScrollToBeginningOfDocument,
+ ExecuteScrollToBeginningOfDocument, SupportedFromMenuOrKeyBinding,
+ Enabled, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kScrollToEndOfDocument,
+ ExecuteScrollToEndOfDocument, SupportedFromMenuOrKeyBinding, Enabled,
+ StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kSelectAll, ExecuteSelectAll, Supported,
+ EnabledSelectAll, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kSelectLine, ExecuteSelectLine,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kSelectParagraph, ExecuteSelectParagraph,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kSelectSentence, ExecuteSelectSentence,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kSelectToMark, ExecuteSelectToMark,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelectionAndMark, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kSelectWord, ExecuteSelectWord,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kSetMark, ExecuteSetMark,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelection, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kStrikethrough,
+ StyleCommands::ExecuteStrikethrough, Supported,
+ EnabledInRichlyEditableText, StyleCommands::StateStrikethrough,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kStyleWithCSS, StyleCommands::ExecuteStyleWithCSS,
+ Supported, Enabled, StyleCommands::StateStyleWithCSS, ValueEmpty,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kSubscript, StyleCommands::ExecuteSubscript,
+ Supported, EnabledInRichlyEditableText, StyleCommands::StateSubscript,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kSuperscript, StyleCommands::ExecuteSuperscript,
+ Supported, EnabledInRichlyEditableText, StyleCommands::StateSuperscript,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kSwapWithMark, ExecuteSwapWithMark,
+ SupportedFromMenuOrKeyBinding, EnabledVisibleSelectionAndMark, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kToggleBold, StyleCommands::ExecuteToggleBold,
+ SupportedFromMenuOrKeyBinding, EnabledInRichlyEditableText,
+ StyleCommands::StateBold, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kToggleItalic, StyleCommands::ExecuteToggleItalic,
+ SupportedFromMenuOrKeyBinding, EnabledInRichlyEditableText,
+ StyleCommands::StateItalic, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kToggleUnderline, StyleCommands::ExecuteUnderline,
+ SupportedFromMenuOrKeyBinding, EnabledInRichlyEditableText,
+ StyleCommands::StateUnderline, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kTranspose, ExecuteTranspose, Supported,
+ EnableCaretInEditableText, StateNone, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kUnderline, StyleCommands::ExecuteUnderline,
+ Supported, EnabledInRichlyEditableText, StyleCommands::StateUnderline,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kUndo, ExecuteUndo, Supported, EnabledUndo,
+ StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kUnlink, ExecuteUnlink, Supported,
+ EnabledRangeInRichlyEditableText, StateNone, ValueStateOrNull,
+ kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kUnscript, StyleCommands::ExecuteUnscript,
+ SupportedFromMenuOrKeyBinding, EnabledInRichlyEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kUnselect, ExecuteUnselect, Supported,
+ EnabledUnselect, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kUseCSS, StyleCommands::ExecuteUseCSS, Supported,
+ Enabled, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kYank, ExecuteYank, SupportedFromMenuOrKeyBinding,
+ EnabledInEditableText, StateNone, ValueStateOrNull, kNotTextInsertion,
+ CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kYankAndSelect, ExecuteYankAndSelect,
+ SupportedFromMenuOrKeyBinding, EnabledInEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ {WebEditingCommandType::kAlignCenter, ExecuteJustifyCenter,
+ SupportedFromMenuOrKeyBinding, EnabledInRichlyEditableText, StateNone,
+ ValueStateOrNull, kNotTextInsertion, CanNotExecuteWhenDisabled},
+ };
+ // Handles all commands except WebEditingCommandType::Invalid.
+ static_assert(
+ arraysize(kEditorCommands) + 1 ==
+ static_cast<size_t>(WebEditingCommandType::kNumberOfCommandTypes),
+ "must handle all valid WebEditingCommandType");
+
+ WebEditingCommandType command_type =
+ WebEditingCommandTypeFromCommandName(command_name);
+ if (command_type == WebEditingCommandType::kInvalid)
+ return nullptr;
+
+ int command_index = static_cast<int>(command_type) - 1;
+ DCHECK(command_index >= 0 &&
+ command_index < static_cast<int>(arraysize(kEditorCommands)));
+ return &kEditorCommands[command_index];
+}
+
+EditorCommand Editor::CreateCommand(const String& command_name) const {
+ return EditorCommand(InternalCommand(command_name),
+ EditorCommandSource::kMenuOrKeyBinding, frame_);
+}
+
+EditorCommand Editor::CreateCommand(const String& command_name,
+ EditorCommandSource source) const {
+ return EditorCommand(InternalCommand(command_name), source, frame_);
+}
+
+bool Editor::ExecuteCommand(const String& command_name) {
+ // Specially handling commands that Editor::execCommand does not directly
+ // support.
+ DCHECK(GetFrame().GetDocument()->IsActive());
+ if (command_name == "DeleteToEndOfParagraph") {
+ if (!DeleteWithDirection(GetFrame(), DeleteDirection::kForward,
+ TextGranularity::kParagraphBoundary, true,
+ false)) {
+ DeleteWithDirection(GetFrame(), DeleteDirection::kForward,
+ TextGranularity::kCharacter, true, false);
+ }
+ return true;
+ }
+ if (command_name == "DeleteBackward")
+ return CreateCommand(AtomicString("BackwardDelete")).Execute();
+ if (command_name == "DeleteForward")
+ return CreateCommand(AtomicString("ForwardDelete")).Execute();
+ if (command_name == "AdvanceToNextMisspelling") {
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // We need to pass false here or else the currently selected word will never
+ // be skipped.
+ GetSpellChecker().AdvanceToNextMisspelling(false);
+ return true;
+ }
+ if (command_name == "ToggleSpellPanel") {
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited.
+ // see http://crbug.com/590369 for more details.
+ GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ GetSpellChecker().ShowSpellingGuessPanel();
+ return true;
+ }
+ return CreateCommand(command_name).Execute();
+}
+
+bool Editor::ExecuteCommand(const String& command_name, const String& value) {
+ // moveToBeginningOfDocument and moveToEndfDocument are only handled by WebKit
+ // for editable nodes.
+ DCHECK(GetFrame().GetDocument()->IsActive());
+ if (!CanEdit() && command_name == "moveToBeginningOfDocument")
+ return GetFrame().GetEventHandler().BubblingScroll(
+ kScrollUpIgnoringWritingMode, kScrollByDocument);
+
+ if (!CanEdit() && command_name == "moveToEndOfDocument")
+ return GetFrame().GetEventHandler().BubblingScroll(
+ kScrollDownIgnoringWritingMode, kScrollByDocument);
+
+ if (command_name == "ToggleSpellPanel") {
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ GetSpellChecker().ShowSpellingGuessPanel();
+ return true;
+ }
+
+ return CreateCommand(command_name).Execute(value);
+}
+
+bool Editor::IsCommandEnabled(const String& command_name) const {
+ return CreateCommand(command_name).IsEnabled();
+}
+
+EditorCommand::EditorCommand()
+ : command_(nullptr), source_(EditorCommandSource::kMenuOrKeyBinding) {}
+
+EditorCommand::EditorCommand(const EditorInternalCommand* command,
+ EditorCommandSource source,
+ LocalFrame* frame)
+ : command_(command), source_(source), frame_(command ? frame : nullptr) {
+ // Use separate assertions so we can tell which bad thing happened.
+ if (!command)
+ DCHECK(!frame_);
+ else
+ DCHECK(frame_);
+}
+
+LocalFrame& EditorCommand::GetFrame() const {
+ DCHECK(frame_);
+ return *frame_;
+}
+
+bool EditorCommand::Execute(const String& parameter,
+ Event* triggering_event) const {
+ if (!CanExecute(triggering_event))
+ return false;
+
+ if (source_ == EditorCommandSource::kMenuOrKeyBinding) {
+ InputEvent::InputType input_type =
+ InputTypeFromCommandType(command_->command_type, *frame_);
+ if (input_type != InputEvent::InputType::kNone) {
+ if (DispatchBeforeInputEditorCommand(
+ EventTargetNodeForDocument(frame_->GetDocument()), input_type,
+ GetTargetRanges()) != DispatchEventResult::kNotCanceled)
+ return true;
+ // 'beforeinput' event handler may destroy target frame.
+ if (frame_->GetDocument()->GetFrame() != frame_)
+ return false;
+ }
+ }
+
+ GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+ DEFINE_STATIC_LOCAL(SparseHistogram, command_histogram,
+ ("WebCore.Editing.Commands"));
+ command_histogram.Sample(static_cast<int>(command_->command_type));
+ return command_->execute(*frame_, triggering_event, source_, parameter);
+}
+
+bool EditorCommand::Execute(Event* triggering_event) const {
+ return Execute(String(), triggering_event);
+}
+
+bool EditorCommand::CanExecute(Event* triggering_event) const {
+ if (IsEnabled(triggering_event))
+ return true;
+ return IsSupported() && frame_ && command_->can_execute(*frame_, source_);
+}
+
+bool EditorCommand::IsSupported() const {
+ if (!command_)
+ return false;
+ switch (source_) {
+ case EditorCommandSource::kMenuOrKeyBinding:
+ return true;
+ case EditorCommandSource::kDOM:
+ return command_->is_supported_from_dom(frame_.Get());
+ }
+ NOTREACHED();
+ return false;
+}
+
+bool EditorCommand::IsEnabled(Event* triggering_event) const {
+ if (!IsSupported() || !frame_)
+ return false;
+ return command_->is_enabled(*frame_, triggering_event, source_);
+}
+
+EditingTriState EditorCommand::GetState(Event* triggering_event) const {
+ if (!IsSupported() || !frame_)
+ return EditingTriState::kFalse;
+ return command_->state(*frame_, triggering_event);
+}
+
+String EditorCommand::Value(Event* triggering_event) const {
+ if (!IsSupported() || !frame_)
+ return String();
+ return command_->value(*command_, *frame_, triggering_event);
+}
+
+bool EditorCommand::IsTextInsertion() const {
+ return command_ && command_->is_text_insertion;
+}
+
+int EditorCommand::IdForHistogram() const {
+ return IsSupported() ? static_cast<int>(command_->command_type) : 0;
+}
+
+const StaticRangeVector* EditorCommand::GetTargetRanges() const {
+ const Node* target = EventTargetNodeForDocument(frame_->GetDocument());
+ if (!IsSupported() || !frame_ || !target || !HasRichlyEditableStyle(*target))
+ return nullptr;
+
+ switch (command_->command_type) {
+ case WebEditingCommandType::kDelete:
+ case WebEditingCommandType::kDeleteBackward:
+ return RangesFromCurrentSelectionOrExtendCaret(
+ *frame_, SelectionModifyDirection::kBackward,
+ TextGranularity::kCharacter);
+ case WebEditingCommandType::kDeleteForward:
+ return RangesFromCurrentSelectionOrExtendCaret(
+ *frame_, SelectionModifyDirection::kForward,
+ TextGranularity::kCharacter);
+ case WebEditingCommandType::kDeleteToBeginningOfLine:
+ return RangesFromCurrentSelectionOrExtendCaret(
+ *frame_, SelectionModifyDirection::kBackward, TextGranularity::kLine);
+ case WebEditingCommandType::kDeleteToBeginningOfParagraph:
+ return RangesFromCurrentSelectionOrExtendCaret(
+ *frame_, SelectionModifyDirection::kBackward,
+ TextGranularity::kParagraph);
+ case WebEditingCommandType::kDeleteToEndOfLine:
+ return RangesFromCurrentSelectionOrExtendCaret(
+ *frame_, SelectionModifyDirection::kForward, TextGranularity::kLine);
+ case WebEditingCommandType::kDeleteToEndOfParagraph:
+ return RangesFromCurrentSelectionOrExtendCaret(
+ *frame_, SelectionModifyDirection::kForward,
+ TextGranularity::kParagraph);
+ case WebEditingCommandType::kDeleteWordBackward:
+ return RangesFromCurrentSelectionOrExtendCaret(
+ *frame_, SelectionModifyDirection::kBackward, TextGranularity::kWord);
+ case WebEditingCommandType::kDeleteWordForward:
+ return RangesFromCurrentSelectionOrExtendCaret(
+ *frame_, SelectionModifyDirection::kForward, TextGranularity::kWord);
+ default:
+ return TargetRangesForInputEvent(*target);
+ }
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/editor_command.h b/chromium/third_party/blink/renderer/core/editing/commands/editor_command.h
new file mode 100644
index 00000000000..0cee2e65931
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/editor_command.h
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_EDITOR_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_EDITOR_COMMAND_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/dom/static_range.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+
+namespace blink {
+
+class EditorInternalCommand;
+class Event;
+class LocalFrame;
+
+enum class EditingTriState;
+enum class EditorCommandSource;
+
+class CORE_EXPORT EditorCommand {
+ STACK_ALLOCATED();
+
+ public:
+ EditorCommand();
+ EditorCommand(const EditorInternalCommand*, EditorCommandSource, LocalFrame*);
+
+ bool Execute(const String& parameter = String(),
+ Event* triggering_event = nullptr) const;
+ bool Execute(Event* triggering_event) const;
+
+ bool CanExecute(Event* triggering_event = nullptr) const;
+ bool IsSupported() const;
+ bool IsEnabled(Event* triggering_event = nullptr) const;
+
+ EditingTriState GetState(Event* triggering_event = nullptr) const;
+ String Value(Event* triggering_event = nullptr) const;
+
+ bool IsTextInsertion() const;
+
+ // Returns 0 if this EditorCommand is not supported.
+ int IdForHistogram() const;
+
+ private:
+ LocalFrame& GetFrame() const;
+
+ // Returns target ranges for the command, currently only supports delete
+ // related commands. Used by InputEvent.
+ const StaticRangeVector* GetTargetRanges() const;
+
+ const EditorInternalCommand* command_;
+ const EditorCommandSource source_;
+ const Member<LocalFrame> frame_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_EDITOR_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/editor_command_names.h b/chromium/third_party/blink/renderer/core/editing/commands/editor_command_names.h
new file mode 100644
index 00000000000..6be74053db2
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/editor_command_names.h
@@ -0,0 +1,155 @@
+// Copyright (c) 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_EDITOR_COMMAND_NAMES_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_EDITOR_COMMAND_NAMES_H_
+
+namespace blink {
+
+// Must be ordered in a case-folding manner for binary search. Covered by unit
+// tests in EditingCommandTest.cpp (not able to use static_assert)
+#define FOR_EACH_BLINK_EDITING_COMMAND_NAME(V) \
+ V(AlignCenter) \
+ V(AlignJustified) \
+ V(AlignLeft) \
+ V(AlignRight) \
+ V(BackColor) \
+ V(BackwardDelete) \
+ V(Bold) \
+ V(Copy) \
+ V(CreateLink) \
+ V(Cut) \
+ V(DefaultParagraphSeparator) \
+ V(Delete) \
+ V(DeleteBackward) \
+ V(DeleteBackwardByDecomposingPreviousCharacter) \
+ V(DeleteForward) \
+ V(DeleteToBeginningOfLine) \
+ V(DeleteToBeginningOfParagraph) \
+ V(DeleteToEndOfLine) \
+ V(DeleteToEndOfParagraph) \
+ V(DeleteToMark) \
+ V(DeleteWordBackward) \
+ V(DeleteWordForward) \
+ V(FindString) \
+ V(FontName) \
+ V(FontSize) \
+ V(FontSizeDelta) \
+ V(ForeColor) \
+ V(FormatBlock) \
+ V(ForwardDelete) \
+ V(HiliteColor) \
+ V(IgnoreSpelling) \
+ V(Indent) \
+ V(InsertBacktab) \
+ V(InsertHorizontalRule) \
+ V(InsertHTML) \
+ V(InsertImage) \
+ V(InsertLineBreak) \
+ V(InsertNewline) \
+ V(InsertNewlineInQuotedContent) \
+ V(InsertOrderedList) \
+ V(InsertParagraph) \
+ V(InsertTab) \
+ V(InsertText) \
+ V(InsertUnorderedList) \
+ V(Italic) \
+ V(JustifyCenter) \
+ V(JustifyFull) \
+ V(JustifyLeft) \
+ V(JustifyNone) \
+ V(JustifyRight) \
+ V(MakeTextWritingDirectionLeftToRight) \
+ V(MakeTextWritingDirectionNatural) \
+ V(MakeTextWritingDirectionRightToLeft) \
+ V(MoveBackward) \
+ V(MoveBackwardAndModifySelection) \
+ V(MoveDown) \
+ V(MoveDownAndModifySelection) \
+ V(MoveForward) \
+ V(MoveForwardAndModifySelection) \
+ V(MoveLeft) \
+ V(MoveLeftAndModifySelection) \
+ V(MovePageDown) \
+ V(MovePageDownAndModifySelection) \
+ V(MovePageUp) \
+ V(MovePageUpAndModifySelection) \
+ V(MoveParagraphBackward) \
+ V(MoveParagraphBackwardAndModifySelection) \
+ V(MoveParagraphForward) \
+ V(MoveParagraphForwardAndModifySelection) \
+ V(MoveRight) \
+ V(MoveRightAndModifySelection) \
+ V(MoveToBeginningOfDocument) \
+ V(MoveToBeginningOfDocumentAndModifySelection) \
+ V(MoveToBeginningOfLine) \
+ V(MoveToBeginningOfLineAndModifySelection) \
+ V(MoveToBeginningOfParagraph) \
+ V(MoveToBeginningOfParagraphAndModifySelection) \
+ V(MoveToBeginningOfSentence) \
+ V(MoveToBeginningOfSentenceAndModifySelection) \
+ V(MoveToEndOfDocument) \
+ V(MoveToEndOfDocumentAndModifySelection) \
+ V(MoveToEndOfLine) \
+ V(MoveToEndOfLineAndModifySelection) \
+ V(MoveToEndOfParagraph) \
+ V(MoveToEndOfParagraphAndModifySelection) \
+ V(MoveToEndOfSentence) \
+ V(MoveToEndOfSentenceAndModifySelection) \
+ V(MoveToLeftEndOfLine) \
+ V(MoveToLeftEndOfLineAndModifySelection) \
+ V(MoveToRightEndOfLine) \
+ V(MoveToRightEndOfLineAndModifySelection) \
+ V(MoveUp) \
+ V(MoveUpAndModifySelection) \
+ V(MoveWordBackward) \
+ V(MoveWordBackwardAndModifySelection) \
+ V(MoveWordForward) \
+ V(MoveWordForwardAndModifySelection) \
+ V(MoveWordLeft) \
+ V(MoveWordLeftAndModifySelection) \
+ V(MoveWordRight) \
+ V(MoveWordRightAndModifySelection) \
+ V(Outdent) \
+ V(OverWrite) \
+ V(Paste) \
+ V(PasteAndMatchStyle) \
+ V(PasteGlobalSelection) \
+ V(Print) \
+ V(Redo) \
+ V(RemoveFormat) \
+ V(ScrollLineDown) \
+ V(ScrollLineUp) \
+ V(ScrollPageBackward) \
+ V(ScrollPageForward) \
+ V(ScrollToBeginningOfDocument) \
+ V(ScrollToEndOfDocument) \
+ V(SelectAll) \
+ V(SelectLine) \
+ V(SelectParagraph) \
+ V(SelectSentence) \
+ V(SelectToMark) \
+ V(SelectWord) \
+ V(SetMark) \
+ V(Strikethrough) \
+ V(StyleWithCSS) \
+ V(Subscript) \
+ V(Superscript) \
+ V(SwapWithMark) \
+ V(ToggleBold) \
+ V(ToggleItalic) \
+ V(ToggleUnderline) \
+ V(Transpose) \
+ V(Underline) \
+ V(Undo) \
+ V(Unlink) \
+ V(Unscript) \
+ V(Unselect) \
+ V(UseCSS) \
+ V(Yank) \
+ V(YankAndSelect)
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_EDITOR_COMMAND_NAMES_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/format_block_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/format_block_command.cc
new file mode 100644
index 00000000000..2642dd7f342
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/format_block_command.cc
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2006 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/format_block_command.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/html/html_br_element.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+static Node* EnclosingBlockToSplitTreeTo(Node* start_node);
+static bool IsElementForFormatBlock(const QualifiedName& tag_name);
+static inline bool IsElementForFormatBlock(Node* node) {
+ return node->IsElementNode() &&
+ IsElementForFormatBlock(ToElement(node)->TagQName());
+}
+
+static Element* EnclosingBlockFlowElement(
+ const VisiblePosition& visible_position) {
+ if (visible_position.IsNull())
+ return nullptr;
+ return EnclosingBlockFlowElement(
+ *visible_position.DeepEquivalent().AnchorNode());
+}
+
+FormatBlockCommand::FormatBlockCommand(Document& document,
+ const QualifiedName& tag_name)
+ : ApplyBlockElementCommand(document, tag_name), did_apply_(false) {}
+
+void FormatBlockCommand::FormatSelection(
+ const VisiblePosition& start_of_selection,
+ const VisiblePosition& end_of_selection,
+ EditingState* editing_state) {
+ if (!IsElementForFormatBlock(TagName()))
+ return;
+ ApplyBlockElementCommand::FormatSelection(start_of_selection,
+ end_of_selection, editing_state);
+ did_apply_ = true;
+}
+
+void FormatBlockCommand::FormatRange(const Position& start,
+ const Position& end,
+ const Position& end_of_selection,
+ HTMLElement*& block_element,
+ EditingState* editing_state) {
+ Element* ref_element = EnclosingBlockFlowElement(CreateVisiblePosition(end));
+ Element* root = RootEditableElementOf(start);
+ // Root is null for elements with contenteditable=false.
+ if (!root || !ref_element)
+ return;
+
+ Node* node_to_split_to = EnclosingBlockToSplitTreeTo(start.AnchorNode());
+ Node* outer_block =
+ (start.AnchorNode() == node_to_split_to)
+ ? start.AnchorNode()
+ : SplitTreeToNode(start.AnchorNode(), node_to_split_to);
+ Node* node_after_insertion_position = outer_block;
+ const EphemeralRange range(start, end_of_selection);
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (IsElementForFormatBlock(ref_element->TagQName()) &&
+ CreateVisiblePosition(start).DeepEquivalent() ==
+ StartOfBlock(CreateVisiblePosition(start)).DeepEquivalent() &&
+ (CreateVisiblePosition(end).DeepEquivalent() ==
+ EndOfBlock(CreateVisiblePosition(end)).DeepEquivalent() ||
+ IsNodeVisiblyContainedWithin(*ref_element, range)) &&
+ ref_element != root && !root->IsDescendantOf(ref_element)) {
+ // Already in a block element that only contains the current paragraph
+ if (ref_element->HasTagName(TagName()))
+ return;
+ node_after_insertion_position = ref_element;
+ }
+
+ if (!block_element) {
+ // Create a new blockquote and insert it as a child of the root editable
+ // element. We accomplish this by splitting all parents of the current
+ // paragraph up to that point.
+ block_element = CreateBlockElement();
+ InsertNodeBefore(block_element, node_after_insertion_position,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ }
+
+ Position last_paragraph_in_block_node =
+ block_element->lastChild()
+ ? Position::AfterNode(*block_element->lastChild())
+ : Position();
+ bool was_end_of_paragraph =
+ IsEndOfParagraph(CreateVisiblePosition(last_paragraph_in_block_node));
+
+ const VisiblePosition& start_of_paragraph_to_move =
+ CreateVisiblePosition(start);
+ const VisiblePosition& end_of_paragraph_to_move = CreateVisiblePosition(end);
+ // execCommand/format_block/format_block_with_nth_child_crash.html reaches
+ // here.
+ ABORT_EDITING_COMMAND_IF(start_of_paragraph_to_move.IsNull());
+ ABORT_EDITING_COMMAND_IF(end_of_paragraph_to_move.IsNull());
+ MoveParagraphWithClones(start_of_paragraph_to_move, end_of_paragraph_to_move,
+ block_element, outer_block, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ ABORT_EDITING_COMMAND_IF(
+ !last_paragraph_in_block_node.IsValidFor(GetDocument()));
+
+ // Copy the inline style of the original block element to the newly created
+ // block-style element.
+ if (outer_block != node_after_insertion_position &&
+ ToHTMLElement(node_after_insertion_position)->hasAttribute(styleAttr))
+ block_element->setAttribute(
+ styleAttr,
+ ToHTMLElement(node_after_insertion_position)->getAttribute(styleAttr));
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (was_end_of_paragraph &&
+ !IsEndOfParagraph(CreateVisiblePosition(last_paragraph_in_block_node)) &&
+ !IsStartOfParagraph(CreateVisiblePosition(last_paragraph_in_block_node)))
+ InsertBlockPlaceholder(last_paragraph_in_block_node, editing_state);
+}
+
+Element* FormatBlockCommand::ElementForFormatBlockCommand(
+ const EphemeralRange& range) {
+ Node* common_ancestor = range.CommonAncestorContainer();
+ while (common_ancestor && !IsElementForFormatBlock(common_ancestor))
+ common_ancestor = common_ancestor->parentNode();
+
+ if (!common_ancestor)
+ return nullptr;
+
+ Element* element =
+ RootEditableElement(*range.StartPosition().ComputeContainerNode());
+ if (!element || common_ancestor->contains(element))
+ return nullptr;
+
+ return common_ancestor->IsElementNode() ? ToElement(common_ancestor)
+ : nullptr;
+}
+
+bool IsElementForFormatBlock(const QualifiedName& tag_name) {
+ DEFINE_STATIC_LOCAL(
+ HashSet<QualifiedName>, block_tags,
+ ({
+ addressTag, articleTag, asideTag, blockquoteTag, ddTag, divTag,
+ dlTag, dtTag, footerTag, h1Tag, h2Tag, h3Tag,
+ h4Tag, h5Tag, h6Tag, headerTag, hgroupTag, mainTag,
+ navTag, pTag, preTag, sectionTag,
+ }));
+ return block_tags.Contains(tag_name);
+}
+
+Node* EnclosingBlockToSplitTreeTo(Node* start_node) {
+ DCHECK(start_node);
+ Node* last_block = start_node;
+ for (Node& runner : NodeTraversal::InclusiveAncestorsOf(*start_node)) {
+ if (!HasEditableStyle(runner))
+ return last_block;
+ if (IsTableCell(&runner) || IsHTMLBodyElement(&runner) ||
+ !runner.parentNode() || !HasEditableStyle(*runner.parentNode()) ||
+ IsElementForFormatBlock(&runner))
+ return &runner;
+ if (IsEnclosingBlock(&runner))
+ last_block = &runner;
+ if (IsHTMLListElement(&runner))
+ return HasEditableStyle(*runner.parentNode()) ? runner.parentNode()
+ : &runner;
+ }
+ return last_block;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/format_block_command.h b/chromium/third_party/blink/renderer/core/editing/commands/format_block_command.h
new file mode 100644
index 00000000000..e4c50dc8016
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/format_block_command.h
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_FORMAT_BLOCK_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_FORMAT_BLOCK_COMMAND_H_
+
+#include "third_party/blink/renderer/core/dom/qualified_name.h"
+#include "third_party/blink/renderer/core/editing/commands/apply_block_element_command.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+
+namespace blink {
+
+class Document;
+class Element;
+
+class CORE_EXPORT FormatBlockCommand final : public ApplyBlockElementCommand {
+ public:
+ static FormatBlockCommand* Create(Document& document,
+ const QualifiedName& tag_name) {
+ return new FormatBlockCommand(document, tag_name);
+ }
+
+ bool PreservesTypingStyle() const override { return true; }
+
+ static Element* ElementForFormatBlockCommand(const EphemeralRange&);
+ bool DidApply() const { return did_apply_; }
+
+ private:
+ FormatBlockCommand(Document&, const QualifiedName& tag_name);
+
+ void FormatSelection(const VisiblePosition& start_of_selection,
+ const VisiblePosition& end_of_selection,
+ EditingState*) override;
+ void FormatRange(const Position& start,
+ const Position& end,
+ const Position& end_of_selection,
+ HTMLElement*&,
+ EditingState*) override;
+
+ bool did_apply_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_FORMAT_BLOCK_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/indent_outdent_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/indent_outdent_command.cc
new file mode 100644
index 00000000000..2f50ae0fe65
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/indent_outdent_command.cc
@@ -0,0 +1,455 @@
+/*
+ * Copyright (C) 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/indent_outdent_command.h"
+
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element_traversal.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/commands/insert_list_command.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/html/html_br_element.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+// Returns true if |node| is UL, OL, or BLOCKQUOTE with "display:block".
+// "Outdent" command considers <BLOCKQUOTE style="display:inline"> makes
+// indentation.
+static bool IsHTMLListOrBlockquoteElement(const Node* node) {
+ if (!node || !node->IsHTMLElement())
+ return false;
+ if (!node->GetLayoutObject() || !node->GetLayoutObject()->IsLayoutBlock())
+ return false;
+ const HTMLElement& element = ToHTMLElement(*node);
+ // TODO(yosin): We should check OL/UL element has "list-style-type" CSS
+ // property to make sure they layout contents as list.
+ return IsHTMLUListElement(element) || IsHTMLOListElement(element) ||
+ element.HasTagName(blockquoteTag);
+}
+
+IndentOutdentCommand::IndentOutdentCommand(Document& document,
+ EIndentType type_of_action)
+ : ApplyBlockElementCommand(
+ document,
+ blockquoteTag,
+ "margin: 0 0 0 40px; border: none; padding: 0px;"),
+ type_of_action_(type_of_action) {}
+
+bool IndentOutdentCommand::TryIndentingAsListItem(const Position& start,
+ const Position& end,
+ EditingState* editing_state) {
+ // If our selection is not inside a list, bail out.
+ Node* last_node_in_selected_paragraph = start.AnchorNode();
+ HTMLElement* list_element = EnclosingList(last_node_in_selected_paragraph);
+ if (!list_element)
+ return false;
+
+ // Find the block that we want to indent. If it's not a list item (e.g., a
+ // div inside a list item), we bail out.
+ Element* selected_list_item = EnclosingBlock(last_node_in_selected_paragraph);
+
+ // FIXME: we need to deal with the case where there is no li (malformed HTML)
+ if (!IsHTMLLIElement(selected_list_item))
+ return false;
+
+ // FIXME: previousElementSibling does not ignore non-rendered content like
+ // <span></span>. Should we?
+ Element* previous_list =
+ ElementTraversal::PreviousSibling(*selected_list_item);
+ Element* next_list = ElementTraversal::NextSibling(*selected_list_item);
+
+ // We should calculate visible range in list item because inserting new
+ // list element will change visibility of list item, e.g. :first-child
+ // CSS selector.
+ HTMLElement* new_list = ToHTMLElement(GetDocument().CreateElement(
+ list_element->TagQName(), CreateElementFlags::ByCloneNode(),
+ g_null_atom));
+ InsertNodeBefore(new_list, selected_list_item, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // We should clone all the children of the list item for indenting purposes.
+ // However, in case the current selection does not encompass all its children,
+ // we need to explicitally handle the same. The original list item too would
+ // require proper deletion in that case.
+ const bool should_keep_selected_list =
+ end.AnchorNode() == selected_list_item ||
+ end.AnchorNode()->IsDescendantOf(selected_list_item->lastChild());
+
+ const VisiblePosition& start_of_paragraph_to_move =
+ CreateVisiblePosition(start);
+ const VisiblePosition& end_of_paragraph_to_move =
+ should_keep_selected_list
+ ? CreateVisiblePosition(end)
+ : VisiblePosition::AfterNode(*selected_list_item->lastChild());
+
+ // The insertion of |newList| may change the computed style of other
+ // elements, resulting in failure in visible canonicalization.
+ if (start_of_paragraph_to_move.IsNull() ||
+ end_of_paragraph_to_move.IsNull()) {
+ editing_state->Abort();
+ return false;
+ }
+
+ MoveParagraphWithClones(start_of_paragraph_to_move, end_of_paragraph_to_move,
+ new_list, selected_list_item, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+
+ if (!should_keep_selected_list) {
+ RemoveNode(selected_list_item, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ DCHECK(new_list);
+ if (previous_list && CanMergeLists(*previous_list, *new_list)) {
+ MergeIdenticalElements(previous_list, new_list, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (next_list && CanMergeLists(*new_list, *next_list)) {
+ MergeIdenticalElements(new_list, next_list, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ }
+
+ return true;
+}
+
+void IndentOutdentCommand::IndentIntoBlockquote(const Position& start,
+ const Position& end,
+ HTMLElement*& target_blockquote,
+ EditingState* editing_state) {
+ Element* enclosing_cell = ToElement(EnclosingNodeOfType(start, &IsTableCell));
+ Element* element_to_split_to;
+ if (enclosing_cell)
+ element_to_split_to = enclosing_cell;
+ else if (EnclosingList(start.ComputeContainerNode()))
+ element_to_split_to = EnclosingBlock(start.ComputeContainerNode());
+ else
+ element_to_split_to = RootEditableElementOf(start);
+
+ if (!element_to_split_to)
+ return;
+
+ Node* outer_block =
+ (start.ComputeContainerNode() == element_to_split_to)
+ ? start.ComputeContainerNode()
+ : SplitTreeToNode(start.ComputeContainerNode(), element_to_split_to);
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ VisiblePosition start_of_contents = CreateVisiblePosition(start);
+ if (!target_blockquote) {
+ // Create a new blockquote and insert it as a child of the root editable
+ // element. We accomplish this by splitting all parents of the current
+ // paragraph up to that point.
+ target_blockquote = CreateBlockElement();
+ if (outer_block == start.ComputeContainerNode()) {
+ // When we apply indent to an empty <blockquote>, we should call
+ // insertNodeAfter(). See http://crbug.com/625802 for more details.
+ if (outer_block->HasTagName(blockquoteTag))
+ InsertNodeAfter(target_blockquote, outer_block, editing_state);
+ else
+ InsertNodeAt(target_blockquote, start, editing_state);
+ } else
+ InsertNodeBefore(target_blockquote, outer_block, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ start_of_contents = VisiblePosition::InParentAfterNode(*target_blockquote);
+ }
+
+ VisiblePosition end_of_contents = CreateVisiblePosition(end);
+ if (start_of_contents.IsNull() || end_of_contents.IsNull())
+ return;
+ MoveParagraphWithClones(start_of_contents, end_of_contents, target_blockquote,
+ outer_block, editing_state);
+}
+
+void IndentOutdentCommand::OutdentParagraph(EditingState* editing_state) {
+ VisiblePosition visible_start_of_paragraph =
+ StartOfParagraph(EndingVisibleSelection().VisibleStart());
+ VisiblePosition visible_end_of_paragraph =
+ EndOfParagraph(visible_start_of_paragraph);
+
+ HTMLElement* enclosing_element = ToHTMLElement(
+ EnclosingNodeOfType(visible_start_of_paragraph.DeepEquivalent(),
+ &IsHTMLListOrBlockquoteElement));
+ // We can't outdent if there is no place to go!
+ if (!enclosing_element || !HasEditableStyle(*enclosing_element->parentNode()))
+ return;
+
+ // Use InsertListCommand to remove the selection from the list
+ if (IsHTMLOListElement(*enclosing_element)) {
+ ApplyCommandToComposite(InsertListCommand::Create(
+ GetDocument(), InsertListCommand::kOrderedList),
+ editing_state);
+ return;
+ }
+ if (IsHTMLUListElement(*enclosing_element)) {
+ ApplyCommandToComposite(
+ InsertListCommand::Create(GetDocument(),
+ InsertListCommand::kUnorderedList),
+ editing_state);
+ return;
+ }
+
+ // The selection is inside a blockquote i.e. enclosingNode is a blockquote
+ VisiblePosition position_in_enclosing_block =
+ VisiblePosition::FirstPositionInNode(*enclosing_element);
+ // If the blockquote is inline, the start of the enclosing block coincides
+ // with positionInEnclosingBlock.
+ VisiblePosition start_of_enclosing_block =
+ (enclosing_element->GetLayoutObject() &&
+ enclosing_element->GetLayoutObject()->IsInline())
+ ? position_in_enclosing_block
+ : StartOfBlock(position_in_enclosing_block);
+ VisiblePosition last_position_in_enclosing_block =
+ VisiblePosition::LastPositionInNode(*enclosing_element);
+ VisiblePosition end_of_enclosing_block =
+ EndOfBlock(last_position_in_enclosing_block);
+ if (visible_start_of_paragraph.DeepEquivalent() ==
+ start_of_enclosing_block.DeepEquivalent() &&
+ visible_end_of_paragraph.DeepEquivalent() ==
+ end_of_enclosing_block.DeepEquivalent()) {
+ // The blockquote doesn't contain anything outside the paragraph, so it can
+ // be totally removed.
+ Node* split_point = enclosing_element->nextSibling();
+ RemoveNodePreservingChildren(enclosing_element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ // outdentRegion() assumes it is operating on the first paragraph of an
+ // enclosing blockquote, but if there are multiply nested blockquotes and
+ // we've just removed one, then this assumption isn't true. By splitting the
+ // next containing blockquote after this node, we keep this assumption true
+ if (split_point) {
+ if (Element* split_point_parent = split_point->parentElement()) {
+ // We can't outdent if there is no place to go!
+ if (split_point_parent->HasTagName(blockquoteTag) &&
+ !split_point->HasTagName(blockquoteTag) &&
+ HasEditableStyle(*split_point_parent->parentNode()))
+ SplitElement(split_point_parent, split_point);
+ }
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ visible_start_of_paragraph =
+ CreateVisiblePosition(visible_start_of_paragraph.DeepEquivalent());
+ if (visible_start_of_paragraph.IsNotNull() &&
+ !IsStartOfParagraph(visible_start_of_paragraph)) {
+ InsertNodeAt(HTMLBRElement::Create(GetDocument()),
+ visible_start_of_paragraph.DeepEquivalent(), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ visible_end_of_paragraph =
+ CreateVisiblePosition(visible_end_of_paragraph.DeepEquivalent());
+ if (visible_end_of_paragraph.IsNotNull() &&
+ !IsEndOfParagraph(visible_end_of_paragraph))
+ InsertNodeAt(HTMLBRElement::Create(GetDocument()),
+ visible_end_of_paragraph.DeepEquivalent(), editing_state);
+ return;
+ }
+
+ Node* split_blockquote_node = enclosing_element;
+ if (Element* enclosing_block_flow = EnclosingBlock(
+ visible_start_of_paragraph.DeepEquivalent().AnchorNode())) {
+ if (enclosing_block_flow != enclosing_element) {
+ // We should check if the blockquotes are nested, as nested blockquotes
+ // may be at different indentations.
+ const Position& previous_element =
+ PreviousCandidate(visible_start_of_paragraph.DeepEquivalent());
+ HTMLElement* const previous_element_is_blockquote =
+ ToHTMLElement(EnclosingNodeOfType(previous_element,
+ &IsHTMLListOrBlockquoteElement));
+ const bool is_previous_blockquote_same =
+ !previous_element_is_blockquote ||
+ (enclosing_element == previous_element_is_blockquote);
+ const bool split_ancestor = true;
+ if (is_previous_blockquote_same) {
+ split_blockquote_node = SplitTreeToNode(
+ enclosing_block_flow, enclosing_element, split_ancestor);
+ } else {
+ SplitTreeToNode(
+ visible_start_of_paragraph.DeepEquivalent().AnchorNode(),
+ enclosing_element, split_ancestor);
+ }
+ } else {
+ // We split the blockquote at where we start outdenting.
+ Node* highest_inline_node = HighestEnclosingNodeOfType(
+ visible_start_of_paragraph.DeepEquivalent(), IsInline,
+ kCannotCrossEditingBoundary, enclosing_block_flow);
+ SplitElement(
+ enclosing_element,
+ highest_inline_node
+ ? highest_inline_node
+ : visible_start_of_paragraph.DeepEquivalent().AnchorNode());
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Re-canonicalize visible{Start,End}OfParagraph, make them valid again
+ // after DOM change.
+ // TODO(editing-dev): We should not store a VisiblePosition and later
+ // inspect its properties when it is already invalidated.
+ // See crbug.com/648949 for details.
+ visible_start_of_paragraph = CreateVisiblePosition(
+ visible_start_of_paragraph.ToPositionWithAffinity());
+ visible_end_of_paragraph = CreateVisiblePosition(
+ visible_end_of_paragraph.ToPositionWithAffinity());
+ }
+
+ // TODO(editing-dev): We should not store a VisiblePosition and later
+ // inspect its properties when it is already invalidated.
+ // See crbug.com/648949 for details.
+ VisiblePosition start_of_paragraph_to_move =
+ StartOfParagraph(visible_start_of_paragraph);
+ VisiblePosition end_of_paragraph_to_move =
+ EndOfParagraph(visible_end_of_paragraph);
+ if (start_of_paragraph_to_move.IsNull() || end_of_paragraph_to_move.IsNull())
+ return;
+ HTMLBRElement* placeholder = HTMLBRElement::Create(GetDocument());
+ InsertNodeBefore(placeholder, split_blockquote_node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ start_of_paragraph_to_move = CreateVisiblePosition(
+ start_of_paragraph_to_move.ToPositionWithAffinity());
+ end_of_paragraph_to_move =
+ CreateVisiblePosition(end_of_paragraph_to_move.ToPositionWithAffinity());
+ MoveParagraph(start_of_paragraph_to_move, end_of_paragraph_to_move,
+ VisiblePosition::BeforeNode(*placeholder), editing_state,
+ kPreserveSelection);
+}
+
+// FIXME: We should merge this function with
+// ApplyBlockElementCommand::formatSelection
+void IndentOutdentCommand::OutdentRegion(
+ const VisiblePosition& start_of_selection,
+ const VisiblePosition& end_of_selection,
+ EditingState* editing_state) {
+ VisiblePosition end_of_current_paragraph = EndOfParagraph(start_of_selection);
+ VisiblePosition end_of_last_paragraph = EndOfParagraph(end_of_selection);
+
+ if (end_of_current_paragraph.DeepEquivalent() ==
+ end_of_last_paragraph.DeepEquivalent()) {
+ OutdentParagraph(editing_state);
+ return;
+ }
+
+ Position original_selection_end = EndingVisibleSelection().End();
+ Position end_after_selection =
+ EndOfParagraph(NextPositionOf(end_of_last_paragraph)).DeepEquivalent();
+
+ while (end_of_current_paragraph.DeepEquivalent() != end_after_selection) {
+ PositionWithAffinity end_of_next_paragraph =
+ EndOfParagraph(NextPositionOf(end_of_current_paragraph))
+ .ToPositionWithAffinity();
+ if (end_of_current_paragraph.DeepEquivalent() ==
+ end_of_last_paragraph.DeepEquivalent()) {
+ SelectionInDOMTree::Builder builder;
+ if (original_selection_end.IsNotNull())
+ builder.Collapse(original_selection_end);
+ SetEndingSelection(SelectionForUndoStep::From(builder.Build()));
+ } else {
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(end_of_current_paragraph.DeepEquivalent())
+ .Build()));
+ }
+
+ OutdentParagraph(editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ // outdentParagraph could move more than one paragraph if the paragraph
+ // is in a list item. As a result, endAfterSelection and endOfNextParagraph
+ // could refer to positions no longer in the document.
+ if (end_after_selection.IsNotNull() && !end_after_selection.IsConnected())
+ break;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (end_of_next_paragraph.IsNotNull() &&
+ !end_of_next_paragraph.IsConnected()) {
+ end_of_current_paragraph =
+ CreateVisiblePosition(EndingVisibleSelection().End());
+ end_of_next_paragraph =
+ EndOfParagraph(NextPositionOf(end_of_current_paragraph))
+ .ToPositionWithAffinity();
+ }
+ end_of_current_paragraph = CreateVisiblePosition(end_of_next_paragraph);
+ }
+}
+
+void IndentOutdentCommand::FormatSelection(
+ const VisiblePosition& start_of_selection,
+ const VisiblePosition& end_of_selection,
+ EditingState* editing_state) {
+ if (type_of_action_ == kIndent)
+ ApplyBlockElementCommand::FormatSelection(start_of_selection,
+ end_of_selection, editing_state);
+ else
+ OutdentRegion(start_of_selection, end_of_selection, editing_state);
+}
+
+void IndentOutdentCommand::FormatRange(const Position& start,
+ const Position& end,
+ const Position&,
+ HTMLElement*& blockquote_for_next_indent,
+ EditingState* editing_state) {
+ bool indenting_as_list_item_result =
+ TryIndentingAsListItem(start, end, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ if (indenting_as_list_item_result)
+ blockquote_for_next_indent = nullptr;
+ else
+ IndentIntoBlockquote(start, end, blockquote_for_next_indent, editing_state);
+}
+
+InputEvent::InputType IndentOutdentCommand::GetInputType() const {
+ return type_of_action_ == kIndent ? InputEvent::InputType::kFormatIndent
+ : InputEvent::InputType::kFormatOutdent;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/indent_outdent_command.h b/chromium/third_party/blink/renderer/core/editing/commands/indent_outdent_command.h
new file mode 100644
index 00000000000..302ea868a11
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/indent_outdent_command.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INDENT_OUTDENT_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INDENT_OUTDENT_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/apply_block_element_command.h"
+
+namespace blink {
+
+class CORE_EXPORT IndentOutdentCommand final : public ApplyBlockElementCommand {
+ public:
+ enum EIndentType { kIndent, kOutdent };
+ static IndentOutdentCommand* Create(Document& document, EIndentType type) {
+ return new IndentOutdentCommand(document, type);
+ }
+
+ bool PreservesTypingStyle() const override { return true; }
+
+ private:
+ IndentOutdentCommand(Document&, EIndentType);
+
+ InputEvent::InputType GetInputType() const override;
+
+ void OutdentRegion(const VisiblePosition&,
+ const VisiblePosition&,
+ EditingState*);
+ void OutdentParagraph(EditingState*);
+ bool TryIndentingAsListItem(const Position&, const Position&, EditingState*);
+ void IndentIntoBlockquote(const Position&,
+ const Position&,
+ HTMLElement*&,
+ EditingState*);
+
+ void FormatSelection(const VisiblePosition& start_of_selection,
+ const VisiblePosition& end_of_selection,
+ EditingState*) override;
+ void FormatRange(const Position& start,
+ const Position& end,
+ const Position& end_of_selection,
+ HTMLElement*& blockquote_for_next_indent,
+ EditingState*) override;
+
+ EIndentType type_of_action_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INDENT_OUTDENT_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_commands.cc b/chromium/third_party/blink/renderer/core/editing/commands/insert_commands.cc
new file mode 100644
index 00000000000..7b351d7d7d7
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_commands.cc
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
+ * Copyright (C) 2009 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/insert_commands.h"
+
+#include "third_party/blink/renderer/core/dom/document_fragment.h"
+#include "third_party/blink/renderer/core/editing/commands/insert_list_command.h"
+#include "third_party/blink/renderer/core/editing/commands/replace_selection_command.h"
+#include "third_party/blink/renderer/core/editing/commands/typing_command.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/serializers/serialization.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/html/html_hr_element.h"
+#include "third_party/blink/renderer/core/html/html_image_element.h"
+#include "third_party/blink/renderer/core/input/event_handler.h"
+
+namespace blink {
+
+LocalFrame& InsertCommands::TargetFrame(LocalFrame& frame, Event* event) {
+ if (!event)
+ return frame;
+ const Node* node = event->target()->ToNode();
+ if (!node)
+ return frame;
+ LocalFrame* local_frame = node->GetDocument().GetFrame();
+ DCHECK(local_frame);
+ return *local_frame;
+}
+
+bool InsertCommands::ExecuteInsertFragment(LocalFrame& frame,
+ DocumentFragment* fragment) {
+ DCHECK(frame.GetDocument());
+ return ReplaceSelectionCommand::Create(
+ *frame.GetDocument(), fragment,
+ ReplaceSelectionCommand::kPreventNesting,
+ InputEvent::InputType::kNone)
+ ->Apply();
+}
+
+bool InsertCommands::ExecuteInsertElement(LocalFrame& frame,
+ HTMLElement* content) {
+ DCHECK(frame.GetDocument());
+ DocumentFragment* const fragment =
+ DocumentFragment::Create(*frame.GetDocument());
+ DummyExceptionStateForTesting exception_state;
+ fragment->AppendChild(content, exception_state);
+ if (exception_state.HadException())
+ return false;
+ return ExecuteInsertFragment(frame, fragment);
+}
+
+bool InsertCommands::ExecuteInsertBacktab(LocalFrame& frame,
+ Event* event,
+ EditorCommandSource,
+ const String&) {
+ return TargetFrame(frame, event)
+ .GetEventHandler()
+ .HandleTextInputEvent("\t", event);
+}
+
+bool InsertCommands::ExecuteInsertHorizontalRule(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String& value) {
+ DCHECK(frame.GetDocument());
+ HTMLHRElement* const rule = HTMLHRElement::Create(*frame.GetDocument());
+ if (!value.IsEmpty())
+ rule->SetIdAttribute(AtomicString(value));
+ return ExecuteInsertElement(frame, rule);
+}
+
+bool InsertCommands::ExecuteInsertHTML(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String& value) {
+ DCHECK(frame.GetDocument());
+ return ExecuteInsertFragment(
+ frame, CreateFragmentFromMarkup(*frame.GetDocument(), value, ""));
+}
+
+bool InsertCommands::ExecuteInsertImage(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String& value) {
+ DCHECK(frame.GetDocument());
+ HTMLImageElement* const image =
+ HTMLImageElement::Create(*frame.GetDocument());
+ if (!value.IsEmpty())
+ image->SetSrc(value);
+ return ExecuteInsertElement(frame, image);
+}
+
+bool InsertCommands::ExecuteInsertLineBreak(LocalFrame& frame,
+ Event* event,
+ EditorCommandSource source,
+ const String&) {
+ switch (source) {
+ case EditorCommandSource::kMenuOrKeyBinding:
+ return TargetFrame(frame, event)
+ .GetEventHandler()
+ .HandleTextInputEvent("\n", event, kTextEventInputLineBreak);
+ case EditorCommandSource::kDOM:
+ // Doesn't scroll to make the selection visible, or modify the kill ring.
+ // InsertLineBreak is not implemented in IE or Firefox, so this behavior
+ // is only needed for backward compatibility with ourselves, and for
+ // consistency with other commands.
+ DCHECK(frame.GetDocument());
+ return TypingCommand::InsertLineBreak(*frame.GetDocument());
+ }
+ NOTREACHED();
+ return false;
+}
+
+bool InsertCommands::ExecuteInsertNewline(LocalFrame& frame,
+ Event* event,
+ EditorCommandSource,
+ const String&) {
+ const LocalFrame& target_frame = TargetFrame(frame, event);
+ return target_frame.GetEventHandler().HandleTextInputEvent(
+ "\n", event,
+ target_frame.GetEditor().CanEditRichly() ? kTextEventInputKeyboard
+ : kTextEventInputLineBreak);
+}
+
+bool InsertCommands::ExecuteInsertNewlineInQuotedContent(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ DCHECK(frame.GetDocument());
+ return TypingCommand::InsertParagraphSeparatorInQuotedContent(
+ *frame.GetDocument());
+}
+
+bool InsertCommands::ExecuteInsertOrderedList(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ DCHECK(frame.GetDocument());
+ return InsertListCommand::Create(*frame.GetDocument(),
+ InsertListCommand::kOrderedList)
+ ->Apply();
+}
+
+bool InsertCommands::ExecuteInsertParagraph(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ DCHECK(frame.GetDocument());
+ return TypingCommand::InsertParagraphSeparator(*frame.GetDocument());
+}
+
+bool InsertCommands::ExecuteInsertTab(LocalFrame& frame,
+ Event* event,
+ EditorCommandSource,
+ const String&) {
+ return TargetFrame(frame, event)
+ .GetEventHandler()
+ .HandleTextInputEvent("\t", event);
+}
+
+bool InsertCommands::ExecuteInsertText(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String& value) {
+ DCHECK(frame.GetDocument());
+ TypingCommand::InsertText(*frame.GetDocument(), value, 0);
+ return true;
+}
+
+bool InsertCommands::ExecuteInsertUnorderedList(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ DCHECK(frame.GetDocument());
+ return InsertListCommand::Create(*frame.GetDocument(),
+ InsertListCommand::kUnorderedList)
+ ->Apply();
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_commands.h b/chromium/third_party/blink/renderer/core/editing/commands/insert_commands.h
new file mode 100644
index 00000000000..19a633e0a66
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_commands.h
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
+ * Copyright (C) 2009 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_COMMANDS_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_COMMANDS_H_
+
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+
+namespace blink {
+
+class DocumentFragment;
+class Event;
+class HTMLElement;
+class LocalFrame;
+
+enum class EditorCommandSource;
+
+// This class provides static functions about commands related to insert.
+class InsertCommands {
+ STATIC_ONLY(InsertCommands);
+
+ public:
+ // Returns |bool| value for Document#execCommand().
+ static bool ExecuteInsertBacktab(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteInsertHorizontalRule(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteInsertHTML(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteInsertImage(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteInsertLineBreak(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteInsertNewline(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteInsertNewlineInQuotedContent(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteInsertOrderedList(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteInsertParagraph(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteInsertTab(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteInsertText(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteInsertUnorderedList(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+
+ private:
+ static bool ExecuteInsertFragment(LocalFrame&, DocumentFragment*);
+ static bool ExecuteInsertElement(LocalFrame&, HTMLElement*);
+
+ // Related to Editor::selectionForCommand.
+ // Certain operations continue to use the target control's selection even if
+ // the event handler already moved the selection outside of the text control.
+ static LocalFrame& TargetFrame(LocalFrame&, Event*);
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_incremental_text_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/insert_incremental_text_command.cc
new file mode 100644
index 00000000000..7f7398a41d1
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_incremental_text_command.cc
@@ -0,0 +1,171 @@
+// Copyright (c) 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/insert_incremental_text_command.h"
+
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/iterators/character_iterator.h"
+#include "third_party/blink/renderer/core/editing/plain_text_range.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/state_machines/forward_code_point_state_machine.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/html/html_span_element.h"
+
+namespace blink {
+
+namespace {
+
+size_t ComputeCommonPrefixLength(const String& str1, const String& str2) {
+ const size_t max_common_prefix_length =
+ std::min(str1.length(), str2.length());
+ ForwardCodePointStateMachine code_point_state_machine;
+ size_t result = 0;
+ for (size_t index = 0; index < max_common_prefix_length; ++index) {
+ if (str1[index] != str2[index])
+ return result;
+ code_point_state_machine.FeedFollowingCodeUnit(str1[index]);
+ if (!code_point_state_machine.AtCodePointBoundary())
+ continue;
+ result = index;
+ }
+ return max_common_prefix_length;
+}
+
+size_t ComputeCommonSuffixLength(const String& str1, const String& str2) {
+ const size_t length1 = str1.length();
+ const size_t length2 = str2.length();
+ const size_t max_common_suffix_length = std::min(length1, length2);
+ for (size_t index = 0; index < max_common_suffix_length; ++index) {
+ if (str1[length1 - index - 1] != str2[length2 - index - 1])
+ return index;
+ }
+ return max_common_suffix_length;
+}
+
+size_t ComputeCommonGraphemeClusterPrefixLength(const Position& selection_start,
+ const String& old_text,
+ const String& new_text) {
+ const size_t common_prefix_length =
+ ComputeCommonPrefixLength(old_text, new_text);
+ const int selection_offset = selection_start.ComputeOffsetInContainerNode();
+ const ContainerNode* selection_node =
+ selection_start.ComputeContainerNode()->parentNode();
+
+ // For grapheme cluster, we should adjust it for grapheme boundary.
+ const EphemeralRange& range =
+ PlainTextRange(0, selection_offset + common_prefix_length)
+ .CreateRange(*selection_node);
+ if (range.IsNull())
+ return 0;
+ const Position& position = range.EndPosition();
+ const size_t diff = ComputeDistanceToLeftGraphemeBoundary(position);
+ DCHECK_GE(common_prefix_length, diff);
+ return common_prefix_length - diff;
+}
+
+size_t ComputeCommonGraphemeClusterSuffixLength(const Position& selection_start,
+ const String& old_text,
+ const String& new_text) {
+ const size_t common_suffix_length =
+ ComputeCommonSuffixLength(old_text, new_text);
+ const int selection_offset = selection_start.ComputeOffsetInContainerNode();
+ const ContainerNode* selection_node =
+ selection_start.ComputeContainerNode()->parentNode();
+
+ // For grapheme cluster, we should adjust it for grapheme boundary.
+ const EphemeralRange& range =
+ PlainTextRange(
+ 0, selection_offset + old_text.length() - common_suffix_length)
+ .CreateRange(*selection_node);
+ if (range.IsNull())
+ return 0;
+ const Position& position = range.EndPosition();
+ const size_t diff = ComputeDistanceToRightGraphemeBoundary(position);
+ if (diff > common_suffix_length)
+ return 0;
+ return common_suffix_length - diff;
+}
+
+const String ComputeTextForInsertion(const String& new_text,
+ const size_t common_prefix_length,
+ const size_t common_suffix_length) {
+ return new_text.Substring(
+ common_prefix_length,
+ new_text.length() - common_prefix_length - common_suffix_length);
+}
+
+SelectionInDOMTree ComputeSelectionForInsertion(
+ const EphemeralRange& selection_range,
+ const int offset,
+ const int length) {
+ CharacterIterator char_it(
+ selection_range,
+ TextIteratorBehavior::EmitsObjectReplacementCharacterBehavior());
+ const EphemeralRange& range_for_insertion =
+ char_it.CalculateCharacterSubrange(offset, length);
+ return SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(range_for_insertion)
+ .Build();
+}
+
+} // anonymous namespace
+
+InsertIncrementalTextCommand* InsertIncrementalTextCommand::Create(
+ Document& document,
+ const String& text,
+ RebalanceType rebalance_type) {
+ return new InsertIncrementalTextCommand(document, text, rebalance_type);
+}
+
+InsertIncrementalTextCommand::InsertIncrementalTextCommand(
+ Document& document,
+ const String& text,
+ RebalanceType rebalance_type)
+ : InsertTextCommand(document, text, rebalance_type) {}
+
+void InsertIncrementalTextCommand::DoApply(EditingState* editing_state) {
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+ const Element* element = RootEditableElementOf(EndingSelection().Base());
+ DCHECK(element);
+
+ const VisibleSelection& visible_selection = EndingVisibleSelection();
+ const EphemeralRange selection_range(visible_selection.Start(),
+ visible_selection.End());
+ const String old_text = PlainText(
+ selection_range,
+ TextIteratorBehavior::EmitsObjectReplacementCharacterBehavior());
+ const String& new_text = text_;
+
+ const Position& selection_start = visible_selection.Start();
+ const size_t new_text_length = new_text.length();
+ const size_t old_text_length = old_text.length();
+ const size_t common_prefix_length = ComputeCommonGraphemeClusterPrefixLength(
+ selection_start, old_text, new_text);
+ // We should ignore common prefix when finding common suffix.
+ const size_t common_suffix_length = ComputeCommonGraphemeClusterSuffixLength(
+ selection_start, old_text.Right(old_text_length - common_prefix_length),
+ new_text.Right(new_text_length - common_prefix_length));
+ DCHECK_GE(old_text_length, common_prefix_length + common_suffix_length);
+
+ text_ = ComputeTextForInsertion(text_, common_prefix_length,
+ common_suffix_length);
+
+ const int offset = static_cast<int>(common_prefix_length);
+ const int length = static_cast<int>(old_text_length - common_prefix_length -
+ common_suffix_length);
+ const VisibleSelection& selection_for_insertion = CreateVisibleSelection(
+ ComputeSelectionForInsertion(selection_range, offset, length));
+
+ SetEndingSelectionWithoutValidation(selection_for_insertion.Start(),
+ selection_for_insertion.End());
+
+ InsertTextCommand::DoApply(editing_state);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_incremental_text_command.h b/chromium/third_party/blink/renderer/core/editing/commands/insert_incremental_text_command.h
new file mode 100644
index 00000000000..7c0778c4ea6
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_incremental_text_command.h
@@ -0,0 +1,29 @@
+// Copyright (c) 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_INCREMENTAL_TEXT_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_INCREMENTAL_TEXT_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/insert_text_command.h"
+
+namespace blink {
+
+class CORE_EXPORT InsertIncrementalTextCommand final
+ : public InsertTextCommand {
+ public:
+ static InsertIncrementalTextCommand* Create(
+ Document&,
+ const String&,
+ RebalanceType = kRebalanceLeadingAndTrailingWhitespaces);
+
+ private:
+ InsertIncrementalTextCommand(Document&,
+ const String& text,
+ RebalanceType);
+ void DoApply(EditingState*) override;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_INCREMENTAL_TEXT_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_incremental_text_command_test.cc b/chromium/third_party/blink/renderer/core/editing/commands/insert_incremental_text_command_test.cc
new file mode 100644
index 00000000000..890caebb750
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_incremental_text_command_test.cc
@@ -0,0 +1,72 @@
+// Copyright (c) 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/insert_incremental_text_command.h"
+
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class InsertIncrementalTextCommandTest : public EditingTestBase {};
+
+// http://crbug.com/706166
+TEST_F(InsertIncrementalTextCommandTest, SurrogatePairsReplace) {
+ SetBodyContent("<div id=sample contenteditable><a>a</a>b&#x1F63A;</div>");
+ Element* const sample = GetDocument().getElementById("sample");
+ const String new_text(Vector<UChar>{0xD83D, 0xDE38}); // U+1F638
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(sample->lastChild(), 1))
+ .Extend(Position(sample->lastChild(), 3))
+ .Build());
+ CompositeEditCommand* const command =
+ InsertIncrementalTextCommand::Create(GetDocument(), new_text);
+ command->Apply();
+
+ EXPECT_EQ(String(Vector<UChar>{'b', 0xD83D, 0xDE38}),
+ sample->lastChild()->nodeValue())
+ << "Replace 'U+D83D U+DE3A (U+1F63A) with 'U+D83D U+DE38'(U+1F638)";
+}
+
+TEST_F(InsertIncrementalTextCommandTest, SurrogatePairsNoReplace) {
+ SetBodyContent("<div id=sample contenteditable><a>a</a>b&#x1F63A;</div>");
+ Element* const sample = GetDocument().getElementById("sample");
+ const String new_text(Vector<UChar>{0xD83D, 0xDE3A}); // U+1F63A
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(sample->lastChild(), 1))
+ .Extend(Position(sample->lastChild(), 3))
+ .Build());
+ CompositeEditCommand* const command =
+ InsertIncrementalTextCommand::Create(GetDocument(), new_text);
+ command->Apply();
+
+ EXPECT_EQ(String(Vector<UChar>{'b', 0xD83D, 0xDE3A}),
+ sample->lastChild()->nodeValue())
+ << "Replace 'U+D83D U+DE3A(U+1F63A) with 'U+D83D U+DE3A'(U+1F63A)";
+}
+
+// http://crbug.com/706166
+TEST_F(InsertIncrementalTextCommandTest, SurrogatePairsTwo) {
+ SetBodyContent(
+ "<div id=sample contenteditable><a>a</a>b&#x1F63A;&#x1F63A;</div>");
+ Element* const sample = GetDocument().getElementById("sample");
+ const String new_text(Vector<UChar>{0xD83D, 0xDE38}); // U+1F638
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(sample->lastChild(), 1))
+ .Extend(Position(sample->lastChild(), 5))
+ .Build());
+ CompositeEditCommand* const command =
+ InsertIncrementalTextCommand::Create(GetDocument(), new_text);
+ command->Apply();
+
+ EXPECT_EQ(String(Vector<UChar>{'b', 0xD83D, 0xDE38}),
+ sample->lastChild()->nodeValue())
+ << "Replace 'U+1F63A U+1F63A with U+1F638";
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_into_text_node_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/insert_into_text_node_command.cc
new file mode 100644
index 00000000000..dfbc4660489
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_into_text_node_command.cc
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2005, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/insert_into_text_node_command.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/layout/layout_text.h"
+
+namespace blink {
+
+InsertIntoTextNodeCommand::InsertIntoTextNodeCommand(Text* node,
+ unsigned offset,
+ const String& text)
+ : SimpleEditCommand(node->GetDocument()),
+ node_(node),
+ offset_(offset),
+ text_(text) {
+ DCHECK(node_);
+ DCHECK_LE(offset_, node_->length());
+ DCHECK(!text_.IsEmpty());
+}
+
+void InsertIntoTextNodeCommand::DoApply(EditingState*) {
+ bool password_echo_enabled =
+ GetDocument().GetSettings() &&
+ GetDocument().GetSettings()->GetPasswordEchoEnabled();
+ if (password_echo_enabled)
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (!HasEditableStyle(*node_))
+ return;
+
+ if (password_echo_enabled) {
+ LayoutText* layout_text = node_->GetLayoutObject();
+ if (layout_text && layout_text->IsSecure())
+ layout_text->MomentarilyRevealLastTypedCharacter(offset_ +
+ text_.length() - 1);
+ }
+
+ node_->insertData(offset_, text_, IGNORE_EXCEPTION_FOR_TESTING);
+}
+
+void InsertIntoTextNodeCommand::DoUnapply() {
+ if (!HasEditableStyle(*node_))
+ return;
+
+ node_->deleteData(offset_, text_.length(), IGNORE_EXCEPTION_FOR_TESTING);
+}
+
+void InsertIntoTextNodeCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(node_);
+ SimpleEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_into_text_node_command.h b/chromium/third_party/blink/renderer/core/editing/commands/insert_into_text_node_command.h
new file mode 100644
index 00000000000..ea342d7f008
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_into_text_node_command.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_INTO_TEXT_NODE_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_INTO_TEXT_NODE_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/edit_command.h"
+
+namespace blink {
+
+class Text;
+
+class InsertIntoTextNodeCommand final : public SimpleEditCommand {
+ public:
+ static InsertIntoTextNodeCommand* Create(Text* node,
+ unsigned offset,
+ const String& text) {
+ return new InsertIntoTextNodeCommand(node, offset, text);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ InsertIntoTextNodeCommand(Text* node, unsigned offset, const String& text);
+
+ void DoApply(EditingState*) override;
+ void DoUnapply() override;
+
+ Member<Text> node_;
+ unsigned offset_;
+ String text_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_INTO_TEXT_NODE_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_line_break_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/insert_line_break_command.cc
new file mode 100644
index 00000000000..c7d8e41ac04
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_line_break_command.cc
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2005, 2006 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/insert_line_break_command.h"
+
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/commands/delete_selection_options.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/editing_style.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+#include "third_party/blink/renderer/core/html/html_br_element.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/core/layout/layout_text.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+InsertLineBreakCommand::InsertLineBreakCommand(Document& document)
+ : CompositeEditCommand(document) {}
+
+bool InsertLineBreakCommand::PreservesTypingStyle() const {
+ return true;
+}
+
+// Whether we should insert a break element or a '\n'.
+bool InsertLineBreakCommand::ShouldUseBreakElement(
+ const Position& insertion_pos) {
+ // An editing position like [input, 0] actually refers to the position before
+ // the input element, and in that case we need to check the input element's
+ // parent's layoutObject.
+ Position p(insertion_pos.ParentAnchoredEquivalent());
+ return IsRichlyEditablePosition(p) && p.AnchorNode()->GetLayoutObject() &&
+ !p.AnchorNode()->GetLayoutObject()->Style()->PreserveNewline();
+}
+
+void InsertLineBreakCommand::DoApply(EditingState* editing_state) {
+ if (!DeleteSelection(editing_state, DeleteSelectionOptions::NormalDelete()))
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ VisibleSelection selection = EndingVisibleSelection();
+ if (selection.IsNone() || selection.Start().IsOrphan() ||
+ selection.End().IsOrphan())
+ return;
+
+ // TODO(editing-dev): Stop storing VisiblePositions through mutations.
+ // See crbug.com/648949 for details.
+ VisiblePosition caret(selection.VisibleStart());
+ // FIXME: If the node is hidden, we should still be able to insert text. For
+ // now, we return to avoid a crash.
+ // https://bugs.webkit.org/show_bug.cgi?id=40342
+ if (caret.IsNull())
+ return;
+
+ Position pos(caret.DeepEquivalent());
+
+ pos = PositionAvoidingSpecialElementBoundary(pos, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ pos = PositionOutsideTabSpan(pos);
+
+ Node* node_to_insert = nullptr;
+ if (ShouldUseBreakElement(pos))
+ node_to_insert = HTMLBRElement::Create(GetDocument());
+ else
+ node_to_insert = GetDocument().createTextNode("\n");
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // FIXME: Need to merge text nodes when inserting just after or before text.
+
+ if (IsEndOfParagraph(CreateVisiblePosition(caret.ToPositionWithAffinity())) &&
+ !LineBreakExistsAtVisiblePosition(caret)) {
+ bool need_extra_line_break = !IsHTMLHRElement(*pos.AnchorNode()) &&
+ !IsHTMLTableElement(*pos.AnchorNode());
+
+ InsertNodeAt(node_to_insert, pos, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ if (need_extra_line_break) {
+ Node* extra_node;
+ // TODO(tkent): Can we remove TextControlElement dependency?
+ if (TextControlElement* text_control =
+ EnclosingTextControl(node_to_insert)) {
+ extra_node = text_control->CreatePlaceholderBreakElement();
+ // The placeholder BR should be the last child. There might be
+ // empty Text nodes at |pos|.
+ AppendNode(extra_node, node_to_insert->parentNode(), editing_state);
+ } else {
+ extra_node = node_to_insert->cloneNode(false);
+ InsertNodeAfter(extra_node, node_to_insert, editing_state);
+ }
+ if (editing_state->IsAborted())
+ return;
+ node_to_insert = extra_node;
+ }
+
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::BeforeNode(*node_to_insert))
+ .Build()));
+ } else if (pos.ComputeEditingOffset() <= CaretMinOffset(pos.AnchorNode())) {
+ InsertNodeAt(node_to_insert, pos, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Insert an extra br or '\n' if the just inserted one collapsed.
+ if (!IsStartOfParagraph(VisiblePosition::BeforeNode(*node_to_insert))) {
+ InsertNodeBefore(node_to_insert->cloneNode(false), node_to_insert,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::InParentAfterNode(*node_to_insert))
+ .Build()));
+ // If we're inserting after all of the rendered text in a text node, or into
+ // a non-text node, a simple insertion is sufficient.
+ } else if (!pos.AnchorNode()->IsTextNode() ||
+ pos.ComputeOffsetInContainerNode() >=
+ CaretMaxOffset(pos.AnchorNode())) {
+ InsertNodeAt(node_to_insert, pos, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::InParentAfterNode(*node_to_insert))
+ .Build()));
+ } else if (pos.AnchorNode()->IsTextNode()) {
+ // Split a text node
+ Text* text_node = ToText(pos.AnchorNode());
+ SplitTextNode(text_node, pos.ComputeOffsetInContainerNode());
+ InsertNodeBefore(node_to_insert, text_node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ Position ending_position = Position::FirstPositionInNode(*text_node);
+
+ // Handle whitespace that occurs after the split
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (!IsRenderedCharacter(ending_position)) {
+ Position position_before_text_node(
+ Position::InParentBeforeNode(*text_node));
+ // Clear out all whitespace and insert one non-breaking space
+ DeleteInsignificantTextDownstream(ending_position);
+ // Deleting insignificant whitespace will remove textNode if it contains
+ // nothing but insignificant whitespace.
+ if (text_node->isConnected()) {
+ InsertTextIntoNode(text_node, 0, NonBreakingSpaceString());
+ } else {
+ Text* nbsp_node =
+ GetDocument().createTextNode(NonBreakingSpaceString());
+ InsertNodeAt(nbsp_node, position_before_text_node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ ending_position = Position::FirstPositionInNode(*nbsp_node);
+ }
+ }
+
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(ending_position)
+ .Build()));
+ }
+
+ // Handle the case where there is a typing style.
+
+ EditingStyle* typing_style =
+ GetDocument().GetFrame()->GetEditor().TypingStyle();
+
+ if (typing_style && !typing_style->IsEmpty()) {
+ DCHECK(node_to_insert);
+ // Apply the typing style to the inserted line break, so that if the
+ // selection leaves and then comes back, new input will have the right
+ // style.
+ // FIXME: We shouldn't always apply the typing style to the line break here,
+ // see <rdar://problem/5794462>.
+ ApplyStyle(typing_style, FirstPositionInOrBeforeNode(*node_to_insert),
+ LastPositionInOrAfterNode(*node_to_insert), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ // Even though this applyStyle operates on a Range, it still sets an
+ // endingSelection(). It tries to set a VisibleSelection around the content
+ // it operated on. So, that VisibleSelection will either
+ // (a) select the line break we inserted, or it will
+ // (b) be a caret just before the line break (if the line break is at the
+ // end of a block it isn't selectable).
+ // So, this next call sets the endingSelection() to a caret just after the
+ // line break that we inserted, or just before it if it's at the end of a
+ // block.
+ SetEndingSelection(
+ SelectionForUndoStep::From(SelectionInDOMTree::Builder()
+ .Collapse(EndingVisibleSelection().End())
+ .Build()));
+ }
+
+ RebalanceWhitespace();
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_line_break_command.h b/chromium/third_party/blink/renderer/core/editing/commands/insert_line_break_command.h
new file mode 100644
index 00000000000..bc8f1447906
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_line_break_command.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_LINE_BREAK_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_LINE_BREAK_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+namespace blink {
+
+class InsertLineBreakCommand final : public CompositeEditCommand {
+ public:
+ static InsertLineBreakCommand* Create(Document& document) {
+ return new InsertLineBreakCommand(document);
+ }
+
+ private:
+ explicit InsertLineBreakCommand(Document&);
+
+ void DoApply(EditingState*) override;
+
+ bool PreservesTypingStyle() const override;
+
+ bool ShouldUseBreakElement(const Position&);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_LINE_BREAK_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_list_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/insert_list_command.cc
new file mode 100644
index 00000000000..303cc9240c8
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_list_command.cc
@@ -0,0 +1,710 @@
+/*
+ * Copyright (C) 2006, 2010 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/insert_list_command.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/element_traversal.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/html/html_br_element.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/html/html_li_element.h"
+#include "third_party/blink/renderer/core/html/html_ulist_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+static Node* EnclosingListChild(Node* node, Node* list_node) {
+ Node* list_child = EnclosingListChild(node);
+ while (list_child && EnclosingList(list_child) != list_node)
+ list_child = EnclosingListChild(list_child->parentNode());
+ return list_child;
+}
+
+HTMLUListElement* InsertListCommand::FixOrphanedListChild(
+ Node* node,
+ EditingState* editing_state) {
+ HTMLUListElement* list_element = HTMLUListElement::Create(GetDocument());
+ InsertNodeBefore(list_element, node, editing_state);
+ if (editing_state->IsAborted())
+ return nullptr;
+ RemoveNode(node, editing_state);
+ if (editing_state->IsAborted())
+ return nullptr;
+ AppendNode(node, list_element, editing_state);
+ if (editing_state->IsAborted())
+ return nullptr;
+ return list_element;
+}
+
+HTMLElement* InsertListCommand::MergeWithNeighboringLists(
+ HTMLElement* passed_list,
+ EditingState* editing_state) {
+ DCHECK(passed_list);
+ HTMLElement* list = passed_list;
+ Element* previous_list = ElementTraversal::PreviousSibling(*list);
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (previous_list && CanMergeLists(*previous_list, *list)) {
+ MergeIdenticalElements(previous_list, list, editing_state);
+ if (editing_state->IsAborted())
+ return nullptr;
+ }
+
+ if (!list)
+ return nullptr;
+
+ Element* next_sibling = ElementTraversal::NextSibling(*list);
+ if (!next_sibling || !next_sibling->IsHTMLElement())
+ return list;
+
+ HTMLElement* next_list = ToHTMLElement(next_sibling);
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (CanMergeLists(*list, *next_list)) {
+ MergeIdenticalElements(list, next_list, editing_state);
+ if (editing_state->IsAborted())
+ return nullptr;
+ return next_list;
+ }
+ return list;
+}
+
+bool InsertListCommand::SelectionHasListOfType(
+ const Position& selection_start,
+ const Position& selection_end,
+ const HTMLQualifiedName& list_tag) {
+ DCHECK_LE(selection_start, selection_end);
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ GetDocument().Lifecycle());
+
+ VisiblePosition start = CreateVisiblePosition(selection_start);
+
+ if (!EnclosingList(start.DeepEquivalent().AnchorNode()))
+ return false;
+
+ VisiblePosition end = StartOfParagraph(CreateVisiblePosition(selection_end));
+ while (start.IsNotNull() && start.DeepEquivalent() != end.DeepEquivalent()) {
+ HTMLElement* list_element =
+ EnclosingList(start.DeepEquivalent().AnchorNode());
+ if (!list_element || !list_element->HasTagName(list_tag))
+ return false;
+ start = StartOfNextParagraph(start);
+ }
+
+ return true;
+}
+
+InsertListCommand::InsertListCommand(Document& document, Type type)
+ : CompositeEditCommand(document), type_(type) {}
+
+static bool InSameTreeAndOrdered(const Position& should_be_former,
+ const Position& should_be_later) {
+ // Input positions must be canonical positions.
+ DCHECK_EQ(should_be_former, CanonicalPositionOf(should_be_former))
+ << should_be_former;
+ DCHECK_EQ(should_be_later, CanonicalPositionOf(should_be_later))
+ << should_be_later;
+ return Position::CommonAncestorTreeScope(should_be_former, should_be_later) &&
+ ComparePositions(should_be_former, should_be_later) <= 0;
+}
+
+void InsertListCommand::DoApply(EditingState* editing_state) {
+ // Only entry points are EditorCommand::execute and
+ // IndentOutdentCommand::outdentParagraph, both of which ensure clean layout.
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+
+ const VisibleSelection& visible_selection = EndingVisibleSelection();
+ if (visible_selection.IsNone() || visible_selection.Start().IsOrphan() ||
+ visible_selection.End().IsOrphan())
+ return;
+
+ if (!RootEditableElementOf(EndingSelection().Base()))
+ return;
+
+ VisiblePosition visible_end = visible_selection.VisibleEnd();
+ VisiblePosition visible_start = visible_selection.VisibleStart();
+ // When a selection ends at the start of a paragraph, we rarely paint
+ // the selection gap before that paragraph, because there often is no gap.
+ // In a case like this, it's not obvious to the user that the selection
+ // ends "inside" that paragraph, so it would be confusing if
+ // InsertUn{Ordered}List operated on that paragraph.
+ // FIXME: We paint the gap before some paragraphs that are indented with left
+ // margin/padding, but not others. We should make the gap painting more
+ // consistent and then use a left margin/padding rule here.
+ if (visible_end.DeepEquivalent() != visible_start.DeepEquivalent() &&
+ IsStartOfParagraph(visible_end, kCanSkipOverEditingBoundary)) {
+ const VisiblePosition& new_end =
+ PreviousPositionOf(visible_end, kCannotCrossEditingBoundary);
+ SelectionInDOMTree::Builder builder;
+ builder.Collapse(visible_start.ToPositionWithAffinity());
+ if (new_end.IsNotNull())
+ builder.Extend(new_end.DeepEquivalent());
+ SetEndingSelection(SelectionForUndoStep::From(builder.Build()));
+ if (!RootEditableElementOf(EndingSelection().Base()))
+ return;
+ }
+
+ const HTMLQualifiedName& list_tag = (type_ == kOrderedList) ? olTag : ulTag;
+ if (EndingSelection().IsRange()) {
+ bool force_list_creation = false;
+ VisibleSelection selection =
+ SelectionForParagraphIteration(EndingVisibleSelection());
+ DCHECK(selection.IsRange());
+
+ VisiblePosition visible_start_of_selection = selection.VisibleStart();
+ VisiblePosition visible_end_of_selection = selection.VisibleEnd();
+ PositionWithAffinity start_of_selection =
+ visible_start_of_selection.ToPositionWithAffinity();
+ PositionWithAffinity end_of_selection =
+ visible_end_of_selection.ToPositionWithAffinity();
+ Position start_of_last_paragraph =
+ StartOfParagraph(visible_end_of_selection, kCanSkipOverEditingBoundary)
+ .DeepEquivalent();
+
+ Range* current_selection =
+ CreateRange(FirstEphemeralRangeOf(EndingVisibleSelection()));
+ ContainerNode* scope_for_start_of_selection = nullptr;
+ ContainerNode* scope_for_end_of_selection = nullptr;
+ // FIXME: This is an inefficient way to keep selection alive because
+ // indexForVisiblePosition walks from the beginning of the document to the
+ // visibleEndOfSelection everytime this code is executed. But not using
+ // index is hard because there are so many ways we can lose selection inside
+ // doApplyForSingleParagraph.
+ int index_for_start_of_selection = IndexForVisiblePosition(
+ visible_start_of_selection, scope_for_start_of_selection);
+ int index_for_end_of_selection = IndexForVisiblePosition(
+ visible_end_of_selection, scope_for_end_of_selection);
+
+ if (StartOfParagraph(visible_start_of_selection,
+ kCanSkipOverEditingBoundary)
+ .DeepEquivalent() != start_of_last_paragraph) {
+ force_list_creation =
+ !SelectionHasListOfType(selection.Start(), selection.End(), list_tag);
+
+ VisiblePosition start_of_current_paragraph = visible_start_of_selection;
+ while (InSameTreeAndOrdered(start_of_current_paragraph.DeepEquivalent(),
+ start_of_last_paragraph) &&
+ !InSameParagraph(start_of_current_paragraph,
+ CreateVisiblePosition(start_of_last_paragraph),
+ kCanCrossEditingBoundary)) {
+ // doApply() may operate on and remove the last paragraph of the
+ // selection from the document if it's in the same list item as
+ // startOfCurrentParagraph. Return early to avoid an infinite loop and
+ // because there is no more work to be done.
+ // FIXME(<rdar://problem/5983974>): The endingSelection() may be
+ // incorrect here. Compute the new location of visibleEndOfSelection
+ // and use it as the end of the new selection.
+ if (!start_of_last_paragraph.IsConnected())
+ return;
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(start_of_current_paragraph.DeepEquivalent())
+ .Build()));
+
+ // Save and restore visibleEndOfSelection and startOfLastParagraph when
+ // necessary since moveParagraph and movePragraphWithClones can remove
+ // nodes.
+ bool single_paragraph_result = DoApplyForSingleParagraph(
+ force_list_creation, list_tag, *current_selection, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ if (!single_paragraph_result)
+ break;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Make |visibleEndOfSelection| valid again.
+ if (!end_of_selection.IsConnected() ||
+ !start_of_last_paragraph.IsConnected()) {
+ visible_end_of_selection = VisiblePositionForIndex(
+ index_for_end_of_selection, scope_for_end_of_selection);
+ end_of_selection = visible_end_of_selection.ToPositionWithAffinity();
+ // If visibleEndOfSelection is null, then some contents have been
+ // deleted from the document. This should never happen and if it did,
+ // exit early immediately because we've lost the loop invariant.
+ DCHECK(visible_end_of_selection.IsNotNull());
+ if (visible_end_of_selection.IsNull() ||
+ !RootEditableElementOf(visible_end_of_selection.DeepEquivalent()))
+ return;
+ start_of_last_paragraph =
+ StartOfParagraph(visible_end_of_selection,
+ kCanSkipOverEditingBoundary)
+ .DeepEquivalent();
+ } else {
+ visible_end_of_selection = CreateVisiblePosition(end_of_selection);
+ }
+
+ start_of_current_paragraph =
+ StartOfNextParagraph(EndingVisibleSelection().VisibleStart());
+ }
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(visible_end_of_selection.DeepEquivalent())
+ .Build()));
+ }
+ DoApplyForSingleParagraph(force_list_creation, list_tag, *current_selection,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Fetch the end of the selection, for the reason mentioned above.
+ if (!end_of_selection.IsConnected()) {
+ visible_end_of_selection = VisiblePositionForIndex(
+ index_for_end_of_selection, scope_for_end_of_selection);
+ if (visible_end_of_selection.IsNull())
+ return;
+ } else {
+ visible_end_of_selection = CreateVisiblePosition(end_of_selection);
+ }
+
+ if (!start_of_selection.IsConnected()) {
+ visible_start_of_selection = VisiblePositionForIndex(
+ index_for_start_of_selection, scope_for_start_of_selection);
+ if (visible_start_of_selection.IsNull())
+ return;
+ } else {
+ visible_start_of_selection = CreateVisiblePosition(start_of_selection);
+ }
+
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .SetAffinity(visible_start_of_selection.Affinity())
+ .SetBaseAndExtentDeprecated(
+ visible_start_of_selection.DeepEquivalent(),
+ visible_end_of_selection.DeepEquivalent())
+ .Build()));
+ return;
+ }
+
+ Range* const range =
+ CreateRange(FirstEphemeralRangeOf(EndingVisibleSelection()));
+ DCHECK(range);
+ DoApplyForSingleParagraph(false, list_tag, *range, editing_state);
+}
+
+InputEvent::InputType InsertListCommand::GetInputType() const {
+ return type_ == kOrderedList ? InputEvent::InputType::kInsertOrderedList
+ : InputEvent::InputType::kInsertUnorderedList;
+}
+
+bool InsertListCommand::DoApplyForSingleParagraph(
+ bool force_create_list,
+ const HTMLQualifiedName& list_tag,
+ Range& current_selection,
+ EditingState* editing_state) {
+ // FIXME: This will produce unexpected results for a selection that starts
+ // just before a table and ends inside the first cell,
+ // selectionForParagraphIteration should probably be renamed and deployed
+ // inside setEndingSelection().
+ Node* selection_node = EndingVisibleSelection().Start().AnchorNode();
+ Node* list_child_node = EnclosingListChild(selection_node);
+ bool switch_list_type = false;
+ if (list_child_node) {
+ if (!HasEditableStyle(*list_child_node->parentNode()))
+ return false;
+ // Remove the list child.
+ HTMLElement* list_element = EnclosingList(list_child_node);
+ if (list_element) {
+ if (!HasEditableStyle(*list_element)) {
+ // Since, |listElement| is uneditable, we can't move |listChild|
+ // out from |listElement|.
+ return false;
+ }
+ if (!HasEditableStyle(*list_element->parentNode())) {
+ // Since parent of |listElement| is uneditable, we can not remove
+ // |listElement| for switching list type neither unlistify.
+ return false;
+ }
+ }
+ if (!list_element) {
+ list_element = FixOrphanedListChild(list_child_node, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ list_element = MergeWithNeighboringLists(list_element, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ }
+ DCHECK(HasEditableStyle(*list_element));
+ DCHECK(HasEditableStyle(*list_element->parentNode()));
+ if (!list_element->HasTagName(list_tag)) {
+ // |listChildNode| will be removed from the list and a list of type
+ // |m_type| will be created.
+ switch_list_type = true;
+ }
+
+ // If the list is of the desired type, and we are not removing the list,
+ // then exit early.
+ if (!switch_list_type && force_create_list)
+ return true;
+
+ // If the entire list is selected, then convert the whole list.
+ if (switch_list_type &&
+ IsNodeVisiblyContainedWithin(*list_element,
+ EphemeralRange(&current_selection))) {
+ bool range_start_is_in_list =
+ VisiblePositionBeforeNode(*list_element).DeepEquivalent() ==
+ CreateVisiblePosition(current_selection.StartPosition())
+ .DeepEquivalent();
+ bool range_end_is_in_list =
+ VisiblePositionAfterNode(*list_element).DeepEquivalent() ==
+ CreateVisiblePosition(current_selection.EndPosition())
+ .DeepEquivalent();
+
+ HTMLElement* new_list = CreateHTMLElement(GetDocument(), list_tag);
+ InsertNodeBefore(new_list, list_element, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ Node* first_child_in_list =
+ EnclosingListChild(VisiblePosition::FirstPositionInNode(*list_element)
+ .DeepEquivalent()
+ .AnchorNode(),
+ list_element);
+ Element* outer_block =
+ first_child_in_list && IsBlockFlowElement(*first_child_in_list)
+ ? ToElement(first_child_in_list)
+ : list_element;
+
+ MoveParagraphWithClones(
+ VisiblePosition::FirstPositionInNode(*list_element),
+ VisiblePosition::LastPositionInNode(*list_element), new_list,
+ outer_block, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+
+ // Manually remove listNode because moveParagraphWithClones sometimes
+ // leaves it behind in the document. See the bug 33668 and
+ // editing/execCommand/insert-list-orphaned-item-with-nested-lists.html.
+ // FIXME: This might be a bug in moveParagraphWithClones or
+ // deleteSelection.
+ if (list_element && list_element->isConnected()) {
+ RemoveNode(list_element, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ }
+
+ new_list = MergeWithNeighboringLists(new_list, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+
+ // Restore the start and the end of current selection if they started
+ // inside listNode because moveParagraphWithClones could have removed
+ // them.
+ if (range_start_is_in_list && new_list)
+ current_selection.setStart(new_list, 0, IGNORE_EXCEPTION_FOR_TESTING);
+ if (range_end_is_in_list && new_list) {
+ current_selection.setEnd(new_list,
+ Position::LastOffsetInNode(*new_list),
+ IGNORE_EXCEPTION_FOR_TESTING);
+ }
+
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::FirstPositionInNode(*new_list))
+ .Build()));
+
+ return true;
+ }
+
+ UnlistifyParagraph(EndingVisibleSelection().VisibleStart(), list_element,
+ list_child_node, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ }
+
+ if (!list_child_node || switch_list_type || force_create_list) {
+ ListifyParagraph(EndingVisibleSelection().VisibleStart(), list_tag,
+ editing_state);
+ }
+
+ return true;
+}
+
+void InsertListCommand::UnlistifyParagraph(
+ const VisiblePosition& original_start,
+ HTMLElement* list_element,
+ Node* list_child_node,
+ EditingState* editing_state) {
+ // Since, unlistify paragraph inserts nodes into parent and removes node
+ // from parent, if parent of |listElement| should be editable.
+ DCHECK(HasEditableStyle(*list_element->parentNode()));
+ Node* next_list_child;
+ Node* previous_list_child;
+ VisiblePosition start;
+ VisiblePosition end;
+ DCHECK(list_child_node);
+ if (IsHTMLLIElement(*list_child_node)) {
+ start = VisiblePosition::FirstPositionInNode(*list_child_node);
+ end = VisiblePosition::LastPositionInNode(*list_child_node);
+ next_list_child = list_child_node->nextSibling();
+ previous_list_child = list_child_node->previousSibling();
+ } else {
+ // A paragraph is visually a list item minus a list marker. The paragraph
+ // will be moved.
+ start = StartOfParagraph(original_start, kCanSkipOverEditingBoundary);
+ end = EndOfParagraph(start, kCanSkipOverEditingBoundary);
+ // InsertListCommandTest.UnlistifyParagraphCrashOnRemoveStyle reaches here.
+ ABORT_EDITING_COMMAND_IF(start.DeepEquivalent() == end.DeepEquivalent());
+ next_list_child = EnclosingListChild(
+ NextPositionOf(end).DeepEquivalent().AnchorNode(), list_element);
+ DCHECK_NE(next_list_child, list_child_node);
+ previous_list_child = EnclosingListChild(
+ PreviousPositionOf(start).DeepEquivalent().AnchorNode(), list_element);
+ DCHECK_NE(previous_list_child, list_child_node);
+ }
+
+ // Helpers for making |start| and |end| valid again after DOM changes.
+ PositionWithAffinity start_position = start.ToPositionWithAffinity();
+ PositionWithAffinity end_position = end.ToPositionWithAffinity();
+
+ // When removing a list, we must always create a placeholder to act as a point
+ // of insertion for the list content being removed.
+ HTMLBRElement* placeholder = HTMLBRElement::Create(GetDocument());
+ HTMLElement* element_to_insert = placeholder;
+ // If the content of the list item will be moved into another list, put it in
+ // a list item so that we don't create an orphaned list child.
+ if (EnclosingList(list_element)) {
+ element_to_insert = HTMLLIElement::Create(GetDocument());
+ AppendNode(placeholder, element_to_insert, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ if (next_list_child && previous_list_child) {
+ // We want to pull listChildNode out of listNode, and place it before
+ // nextListChild and after previousListChild, so we split listNode and
+ // insert it between the two lists.
+ // But to split listNode, we must first split ancestors of listChildNode
+ // between it and listNode, if any exist.
+ // FIXME: We appear to split at nextListChild as opposed to listChildNode so
+ // that when we remove listChildNode below in moveParagraphs,
+ // previousListChild will be removed along with it if it is unrendered. But
+ // we ought to remove nextListChild too, if it is unrendered.
+ SplitElement(list_element, SplitTreeToNode(next_list_child, list_element));
+ InsertNodeBefore(element_to_insert, list_element, editing_state);
+ } else if (next_list_child || list_child_node->parentNode() != list_element) {
+ // Just because listChildNode has no previousListChild doesn't mean there
+ // isn't any content in listNode that comes before listChildNode, as
+ // listChildNode could have ancestors between it and listNode. So, we split
+ // up to listNode before inserting the placeholder where we're about to move
+ // listChildNode to.
+ if (list_child_node->parentNode() != list_element)
+ SplitElement(list_element,
+ SplitTreeToNode(list_child_node, list_element));
+ InsertNodeBefore(element_to_insert, list_element, editing_state);
+ } else {
+ InsertNodeAfter(element_to_insert, list_element, editing_state);
+ }
+ if (editing_state->IsAborted())
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Make |start| and |end| valid again.
+ start = CreateVisiblePosition(start_position);
+ end = CreateVisiblePosition(end_position);
+
+ VisiblePosition insertion_point = VisiblePosition::BeforeNode(*placeholder);
+ MoveParagraphs(start, end, insertion_point, editing_state, kPreserveSelection,
+ kPreserveStyle, list_child_node);
+}
+
+static HTMLElement* AdjacentEnclosingList(const VisiblePosition& pos,
+ const VisiblePosition& adjacent_pos,
+ const HTMLQualifiedName& list_tag) {
+ HTMLElement* list_element =
+ OutermostEnclosingList(adjacent_pos.DeepEquivalent().AnchorNode());
+
+ if (!list_element)
+ return nullptr;
+
+ Element* previous_cell = EnclosingTableCell(pos.DeepEquivalent());
+ Element* current_cell = EnclosingTableCell(adjacent_pos.DeepEquivalent());
+
+ if (!list_element->HasTagName(list_tag) ||
+ list_element->contains(pos.DeepEquivalent().AnchorNode()) ||
+ previous_cell != current_cell ||
+ EnclosingList(list_element) !=
+ EnclosingList(pos.DeepEquivalent().AnchorNode()))
+ return nullptr;
+
+ return list_element;
+}
+
+void InsertListCommand::ListifyParagraph(const VisiblePosition& original_start,
+ const HTMLQualifiedName& list_tag,
+ EditingState* editing_state) {
+ const VisiblePosition& start =
+ StartOfParagraph(original_start, kCanSkipOverEditingBoundary);
+ const VisiblePosition& end =
+ EndOfParagraph(start, kCanSkipOverEditingBoundary);
+
+ if (start.IsNull() || end.IsNull())
+ return;
+
+ // Check for adjoining lists.
+ HTMLElement* const previous_list = AdjacentEnclosingList(
+ start, PreviousPositionOf(start, kCannotCrossEditingBoundary), list_tag);
+ HTMLElement* const next_list = AdjacentEnclosingList(
+ start, NextPositionOf(end, kCannotCrossEditingBoundary), list_tag);
+ if (previous_list || next_list) {
+ // Place list item into adjoining lists.
+ HTMLLIElement* list_item_element = HTMLLIElement::Create(GetDocument());
+ if (previous_list)
+ AppendNode(list_item_element, previous_list, editing_state);
+ else
+ InsertNodeAt(list_item_element, Position::BeforeNode(*next_list),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ MoveParagraphOverPositionIntoEmptyListItem(start, list_item_element,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (previous_list && next_list && CanMergeLists(*previous_list, *next_list))
+ MergeIdenticalElements(previous_list, next_list, editing_state);
+
+ return;
+ }
+
+ // Create new list element.
+
+ // Inserting the list into an empty paragraph that isn't held open
+ // by a br or a '\n', will invalidate start and end. Insert
+ // a placeholder and then recompute start and end.
+ Position start_pos = start.DeepEquivalent();
+ if (start.DeepEquivalent() == end.DeepEquivalent() &&
+ IsEnclosingBlock(start.DeepEquivalent().AnchorNode())) {
+ HTMLBRElement* placeholder =
+ InsertBlockPlaceholder(start_pos, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ start_pos = Position::BeforeNode(*placeholder);
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Insert the list at a position visually equivalent to start of the
+ // paragraph that is being moved into the list.
+ // Try to avoid inserting it somewhere where it will be surrounded by
+ // inline ancestors of start, since it is easier for editing to produce
+ // clean markup when inline elements are pushed down as far as possible.
+ Position insertion_pos(MostBackwardCaretPosition(start_pos));
+ // Also avoid the temporary <span> element created by 'unlistifyParagraph'.
+ // This element can be selected by mostBackwardCaretPosition when startPor
+ // points to a element with previous siblings or ancestors with siblings.
+ // |-A
+ // | |-B
+ // | +-C (insertion point)
+ // | |-D (*)
+ if (IsHTMLSpanElement(insertion_pos.AnchorNode())) {
+ insertion_pos =
+ Position::InParentBeforeNode(*insertion_pos.ComputeContainerNode());
+ }
+ // Also avoid the containing list item.
+ Node* const list_child = EnclosingListChild(insertion_pos.AnchorNode());
+ if (IsHTMLLIElement(list_child))
+ insertion_pos = Position::InParentBeforeNode(*list_child);
+
+ HTMLElement* list_element = CreateHTMLElement(GetDocument(), list_tag);
+ InsertNodeAt(list_element, insertion_pos, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ HTMLLIElement* list_item_element = HTMLLIElement::Create(GetDocument());
+ AppendNode(list_item_element, list_element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ // We inserted the list at the start of the content we're about to move.
+ // https://bugs.webkit.org/show_bug.cgi?id=19066: Update the start of content,
+ // so we don't try to move the list into itself.
+ // Layout is necessary since start's node's inline layoutObjects may have been
+ // destroyed by the insertion The end of the content may have changed after
+ // the insertion and layout so update it as well.
+ if (insertion_pos == start_pos) {
+ MoveParagraphOverPositionIntoEmptyListItem(
+ original_start, list_item_element, editing_state);
+ } else {
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ MoveParagraphOverPositionIntoEmptyListItem(
+ CreateVisiblePosition(start_pos), list_item_element, editing_state);
+ }
+ if (editing_state->IsAborted())
+ return;
+
+ MergeWithNeighboringLists(list_element, editing_state);
+}
+
+// TODO(editing-dev): Stop storing VisiblePositions through mutations.
+// See crbug.com/648949 for details.
+void InsertListCommand::MoveParagraphOverPositionIntoEmptyListItem(
+ const VisiblePosition& pos,
+ HTMLLIElement* list_item_element,
+ EditingState* editing_state) {
+ DCHECK(!list_item_element->HasChildren());
+ HTMLBRElement* placeholder = HTMLBRElement::Create(GetDocument());
+ AppendNode(placeholder, list_item_element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ // Inserting list element and list item list may change start of pargraph
+ // to move. We calculate start of paragraph again.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ const VisiblePosition& valid_pos =
+ CreateVisiblePosition(pos.ToPositionWithAffinity());
+ const VisiblePosition& start =
+ StartOfParagraph(valid_pos, kCanSkipOverEditingBoundary);
+ // InsertListCommandTest.InsertListOnEmptyHiddenElements reaches here.
+ ABORT_EDITING_COMMAND_IF(start.IsNull());
+ const VisiblePosition& end =
+ EndOfParagraph(valid_pos, kCanSkipOverEditingBoundary);
+ ABORT_EDITING_COMMAND_IF(end.IsNull());
+ MoveParagraph(start, end, VisiblePosition::BeforeNode(*placeholder),
+ editing_state, kPreserveSelection);
+}
+
+void InsertListCommand::Trace(blink::Visitor* visitor) {
+ CompositeEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_list_command.h b/chromium/third_party/blink/renderer/core/editing/commands/insert_list_command.h
new file mode 100644
index 00000000000..8495b1cf78e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_list_command.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_LIST_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_LIST_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+namespace blink {
+
+class HTMLElement;
+class HTMLUListElement;
+
+class CORE_EXPORT InsertListCommand final : public CompositeEditCommand {
+ public:
+ enum Type { kOrderedList, kUnorderedList };
+
+ static InsertListCommand* Create(Document& document, Type list_type) {
+ return new InsertListCommand(document, list_type);
+ }
+
+ bool PreservesTypingStyle() const override { return true; }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ InsertListCommand(Document&, Type);
+
+ void DoApply(EditingState*) override;
+ InputEvent::InputType GetInputType() const override;
+
+ HTMLUListElement* FixOrphanedListChild(Node*, EditingState*);
+ bool SelectionHasListOfType(const Position& selection_start,
+ const Position& selection_end,
+ const HTMLQualifiedName&);
+ HTMLElement* MergeWithNeighboringLists(HTMLElement*, EditingState*);
+ bool DoApplyForSingleParagraph(bool force_create_list,
+ const HTMLQualifiedName&,
+ Range& current_selection,
+ EditingState*);
+ void UnlistifyParagraph(const VisiblePosition& original_start,
+ HTMLElement* list_node,
+ Node* list_child_node,
+ EditingState*);
+ void ListifyParagraph(const VisiblePosition& original_start,
+ const HTMLQualifiedName& list_tag,
+ EditingState*);
+ void MoveParagraphOverPositionIntoEmptyListItem(const VisiblePosition&,
+ HTMLLIElement*,
+ EditingState*);
+
+ Type type_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_LIST_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_list_command_test.cc b/chromium/third_party/blink/renderer/core/editing/commands/insert_list_command_test.cc
new file mode 100644
index 00000000000..66f7fb68e3a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_list_command_test.cc
@@ -0,0 +1,133 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/insert_list_command.h"
+
+#include "third_party/blink/renderer/core/dom/parent_node.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class InsertListCommandTest : public EditingTestBase {};
+
+TEST_F(InsertListCommandTest, ShouldCleanlyRemoveSpuriousTextNode) {
+ GetDocument().SetCompatibilityMode(Document::kQuirksMode);
+ // Needs to be editable to use InsertListCommand.
+ GetDocument().setDesignMode("on");
+ // Set up the condition:
+ // * Selection is a range, to go down into
+ // InsertListCommand::listifyParagraph.
+ // * The selection needs to have a sibling list to go down into
+ // InsertListCommand::mergeWithNeighboringLists.
+ // * Should be the same type (ordered list) to go into
+ // CompositeEditCommand::mergeIdenticalElements.
+ // * Should have no actual children to fail the listChildNode check
+ // in InsertListCommand::doApplyForSingleParagraph.
+ // * There needs to be an extra text node to trigger its removal in
+ // CompositeEditCommand::mergeIdenticalElements.
+ // The removeNode is what updates document lifecycle to VisualUpdatePending
+ // and makes FrameView::needsLayout return true.
+ SetBodyContent("\nd\n<ol>");
+ Text* empty_text = GetDocument().createTextNode("");
+ GetDocument().body()->InsertBefore(empty_text,
+ GetDocument().body()->firstChild());
+ UpdateAllLifecyclePhases();
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(GetDocument().body(), 0))
+ .Extend(Position(GetDocument().body(), 2))
+ .Build());
+
+ InsertListCommand* command =
+ InsertListCommand::Create(GetDocument(), InsertListCommand::kOrderedList);
+ // This should not DCHECK.
+ EXPECT_TRUE(command->Apply())
+ << "The insert ordered list command should have succeeded";
+ EXPECT_EQ("<ol><li>d</li></ol>", GetDocument().body()->InnerHTMLAsString());
+}
+
+// Refer https://crbug.com/794356
+TEST_F(InsertListCommandTest, UnlistifyParagraphCrashOnVisuallyEmptyParagraph) {
+ GetDocument().setDesignMode("on");
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("^<dl>"
+ "<textarea style='float:left;'></textarea>"
+ "</dl>|"));
+ InsertListCommand* command = InsertListCommand::Create(
+ GetDocument(), InsertListCommand::kUnorderedList);
+ // Crash happens here.
+ EXPECT_FALSE(command->Apply());
+ EXPECT_EQ(
+ "<dl><ul>"
+ "|<textarea style=\"float:left;\"></textarea>"
+ "</ul></dl>",
+ GetSelectionTextFromBody());
+}
+
+// Refer https://crbug.com/798176
+TEST_F(InsertListCommandTest, CleanupNodeSameAsDestinationNode) {
+ GetDocument().setDesignMode("on");
+ InsertStyleElement(
+ "* { -webkit-appearance:checkbox; }"
+ "br { visibility:hidden; }"
+ "colgroup { -webkit-column-count:2; }");
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("^<table><col></table>"
+ "<button></button>|"));
+
+ InsertListCommand* command = InsertListCommand::Create(
+ GetDocument(), InsertListCommand::kUnorderedList);
+
+ // Crash happens here.
+ EXPECT_FALSE(command->Apply());
+ EXPECT_EQ(
+ "<ul><li><br></li></ul>"
+ "<br>"
+ "<table>|<colgroup><col>"
+ "<ul><li><br></li></ul>"
+ "</col></colgroup></table>"
+ "<button></button>",
+ GetSelectionTextFromBody());
+}
+
+TEST_F(InsertListCommandTest, InsertListOnEmptyHiddenElements) {
+ GetDocument().setDesignMode("on");
+ InsertStyleElement("br { visibility:hidden; }");
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("^<button></button>|"));
+ InsertListCommand* command = InsertListCommand::Create(
+ GetDocument(), InsertListCommand::kUnorderedList);
+
+ // Crash happens here.
+ EXPECT_FALSE(command->Apply());
+ EXPECT_EQ(
+ "<button>"
+ "|<ul><li><br></li></ul>"
+ "</button>",
+ GetSelectionTextFromBody());
+}
+
+// Refer https://crbug.com/797520
+TEST_F(InsertListCommandTest, InsertListWithCollapsedVisibility) {
+ GetDocument().setDesignMode("on");
+ InsertStyleElement(
+ "ul { visibility:collapse; }"
+ "dl { visibility:visible; }");
+
+ Selection().SetSelectionAndEndTyping(SetSelectionTextToBody("^<dl>a</dl>|"));
+ InsertListCommand* command =
+ InsertListCommand::Create(GetDocument(), InsertListCommand::kOrderedList);
+
+ // Crash happens here.
+ EXPECT_FALSE(command->Apply());
+ EXPECT_EQ(
+ "<dl>"
+ "<ol></ol><ul>^a|</ul>"
+ "</dl>",
+ GetSelectionTextFromBody());
+}
+}
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_node_before_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/insert_node_before_command.cc
new file mode 100644
index 00000000000..afff31a8389
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_node_before_command.cc
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2005, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/insert_node_before_command.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+
+namespace blink {
+
+InsertNodeBeforeCommand::InsertNodeBeforeCommand(
+ Node* insert_child,
+ Node* ref_child,
+ ShouldAssumeContentIsAlwaysEditable
+ should_assume_content_is_always_editable)
+ : SimpleEditCommand(ref_child->GetDocument()),
+ insert_child_(insert_child),
+ ref_child_(ref_child),
+ should_assume_content_is_always_editable_(
+ should_assume_content_is_always_editable) {
+ DCHECK(insert_child_);
+ DCHECK(!insert_child_->parentNode()) << insert_child_;
+ DCHECK(ref_child_);
+ DCHECK(ref_child_->parentNode()) << ref_child_;
+
+ DCHECK(HasEditableStyle(*ref_child_->parentNode()) ||
+ !ref_child_->parentNode()->InActiveDocument())
+ << ref_child_->parentNode();
+}
+
+void InsertNodeBeforeCommand::DoApply(EditingState*) {
+ ContainerNode* parent = ref_child_->parentNode();
+ GetDocument().UpdateStyleAndLayoutTree();
+ if (!parent || (should_assume_content_is_always_editable_ ==
+ kDoNotAssumeContentIsAlwaysEditable &&
+ !HasEditableStyle(*parent)))
+ return;
+ DCHECK(HasEditableStyle(*parent)) << parent;
+
+ parent->InsertBefore(insert_child_.Get(), ref_child_.Get(),
+ IGNORE_EXCEPTION_FOR_TESTING);
+}
+
+void InsertNodeBeforeCommand::DoUnapply() {
+ GetDocument().UpdateStyleAndLayoutTree();
+ if (!HasEditableStyle(*insert_child_))
+ return;
+
+ insert_child_->remove(IGNORE_EXCEPTION_FOR_TESTING);
+}
+
+void InsertNodeBeforeCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(insert_child_);
+ visitor->Trace(ref_child_);
+ SimpleEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_node_before_command.h b/chromium/third_party/blink/renderer/core/editing/commands/insert_node_before_command.h
new file mode 100644
index 00000000000..2483d05fe70
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_node_before_command.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_NODE_BEFORE_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_NODE_BEFORE_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/edit_command.h"
+
+namespace blink {
+
+class InsertNodeBeforeCommand final : public SimpleEditCommand {
+ public:
+ static InsertNodeBeforeCommand* Create(
+ Node* child_to_insert,
+ Node* child_to_insert_before,
+ ShouldAssumeContentIsAlwaysEditable
+ should_assume_content_is_always_editable) {
+ return new InsertNodeBeforeCommand(
+ child_to_insert, child_to_insert_before,
+ should_assume_content_is_always_editable);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ InsertNodeBeforeCommand(Node* child_to_insert,
+ Node* child_to_insert_before,
+ ShouldAssumeContentIsAlwaysEditable);
+
+ void DoApply(EditingState*) override;
+ void DoUnapply() override;
+
+ Member<Node> insert_child_;
+ Member<Node> ref_child_;
+ ShouldAssumeContentIsAlwaysEditable should_assume_content_is_always_editable_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_NODE_BEFORE_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command.cc
new file mode 100644
index 00000000000..35f6bdddd07
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command.cc
@@ -0,0 +1,615 @@
+/*
+ * Copyright (C) 2005, 2006 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command.h"
+
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/commands/delete_selection_options.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/commands/insert_line_break_command.h"
+#include "third_party/blink/renderer/core/editing/editing_style.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/html/html_br_element.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/html/html_quote_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/core/layout/layout_text.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+// When inserting a new line, we want to avoid nesting empty divs if we can.
+// Otherwise, when pasting, it's easy to have each new line be a div deeper than
+// the previous. E.g., in the case below, we want to insert at ^ instead of |.
+// <div>foo<div>bar</div>|</div>^
+static Element* HighestVisuallyEquivalentDivBelowRoot(Element* start_block) {
+ Element* cur_block = start_block;
+ // We don't want to return a root node (if it happens to be a div, e.g., in a
+ // document fragment) because there are no siblings for us to append to.
+ while (!cur_block->nextSibling() &&
+ IsHTMLDivElement(*cur_block->parentElement()) &&
+ cur_block->parentElement()->parentElement()) {
+ if (cur_block->parentElement()->hasAttributes())
+ break;
+ cur_block = cur_block->parentElement();
+ }
+ return cur_block;
+}
+
+static bool InSameBlock(const VisiblePosition& a, const VisiblePosition& b) {
+ DCHECK(a.IsValid()) << a;
+ DCHECK(b.IsValid()) << b;
+ return !a.IsNull() &&
+ EnclosingBlock(a.DeepEquivalent().ComputeContainerNode()) ==
+ EnclosingBlock(b.DeepEquivalent().ComputeContainerNode());
+}
+
+InsertParagraphSeparatorCommand::InsertParagraphSeparatorCommand(
+ Document& document,
+ bool must_use_default_paragraph_element,
+ bool paste_blockquote_into_unquoted_area)
+ : CompositeEditCommand(document),
+ must_use_default_paragraph_element_(must_use_default_paragraph_element),
+ paste_blockquote_into_unquoted_area_(
+ paste_blockquote_into_unquoted_area) {}
+
+bool InsertParagraphSeparatorCommand::PreservesTypingStyle() const {
+ return true;
+}
+
+void InsertParagraphSeparatorCommand::CalculateStyleBeforeInsertion(
+ const Position& pos) {
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ GetDocument().Lifecycle());
+
+ // It is only important to set a style to apply later if we're at the
+ // boundaries of a paragraph. Otherwise, content that is moved as part of the
+ // work of the command will lend their styles to the new paragraph without any
+ // extra work needed.
+ VisiblePosition visible_pos = CreateVisiblePosition(pos);
+ if (!IsStartOfParagraph(visible_pos) && !IsEndOfParagraph(visible_pos))
+ return;
+
+ DCHECK(pos.IsNotNull());
+ style_ = EditingStyle::Create(pos);
+ style_->MergeTypingStyle(pos.GetDocument());
+}
+
+void InsertParagraphSeparatorCommand::ApplyStyleAfterInsertion(
+ Element* original_enclosing_block,
+ EditingState* editing_state) {
+ // Not only do we break out of header tags, but we also do not preserve the
+ // typing style, in order to match other browsers.
+ if (original_enclosing_block->HasTagName(h1Tag) ||
+ original_enclosing_block->HasTagName(h2Tag) ||
+ original_enclosing_block->HasTagName(h3Tag) ||
+ original_enclosing_block->HasTagName(h4Tag) ||
+ original_enclosing_block->HasTagName(h5Tag)) {
+ return;
+ }
+
+ if (!style_)
+ return;
+
+ style_->PrepareToApplyAt(EndingVisibleSelection().Start());
+ if (!style_->IsEmpty())
+ ApplyStyle(style_.Get(), editing_state);
+}
+
+bool InsertParagraphSeparatorCommand::ShouldUseDefaultParagraphElement(
+ Element* enclosing_block) const {
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+
+ if (must_use_default_paragraph_element_)
+ return true;
+
+ // Assumes that if there was a range selection, it was already deleted.
+ if (!IsEndOfBlock(EndingVisibleSelection().VisibleStart()))
+ return false;
+
+ return enclosing_block->HasTagName(h1Tag) ||
+ enclosing_block->HasTagName(h2Tag) ||
+ enclosing_block->HasTagName(h3Tag) ||
+ enclosing_block->HasTagName(h4Tag) ||
+ enclosing_block->HasTagName(h5Tag);
+}
+
+void InsertParagraphSeparatorCommand::GetAncestorsInsideBlock(
+ const Node* insertion_node,
+ Element* outer_block,
+ HeapVector<Member<Element>>& ancestors) {
+ ancestors.clear();
+
+ // Build up list of ancestors elements between the insertion node and the
+ // outer block.
+ if (insertion_node != outer_block) {
+ for (Element* n = insertion_node->parentElement(); n && n != outer_block;
+ n = n->parentElement())
+ ancestors.push_back(n);
+ }
+}
+
+Element* InsertParagraphSeparatorCommand::CloneHierarchyUnderNewBlock(
+ const HeapVector<Member<Element>>& ancestors,
+ Element* block_to_insert,
+ EditingState* editing_state) {
+ // Make clones of ancestors in between the start node and the start block.
+ Element* parent = block_to_insert;
+ for (size_t i = ancestors.size(); i != 0; --i) {
+ Element* child = ancestors[i - 1]->CloneWithoutChildren();
+ // It should always be okay to remove id from the cloned elements, since the
+ // originals are not deleted.
+ child->removeAttribute(idAttr);
+ AppendNode(child, parent, editing_state);
+ if (editing_state->IsAborted())
+ return nullptr;
+ parent = child;
+ }
+
+ return parent;
+}
+
+void InsertParagraphSeparatorCommand::DoApply(EditingState* editing_state) {
+ // TODO(editing-dev): We shouldn't construct an
+ // InsertParagraphSeparatorCommand with none or invalid selection.
+ const VisibleSelection& visible_selection = EndingVisibleSelection();
+ if (visible_selection.IsNone() ||
+ !visible_selection.IsValidFor(GetDocument()))
+ return;
+
+ Position insertion_position = visible_selection.Start();
+
+ TextAffinity affinity = visible_selection.Affinity();
+
+ // Delete the current selection.
+ if (EndingSelection().IsRange()) {
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ CalculateStyleBeforeInsertion(insertion_position);
+ if (!DeleteSelection(editing_state, DeleteSelectionOptions::NormalDelete()))
+ return;
+ const VisibleSelection& visble_selection_after_delete =
+ EndingVisibleSelection();
+ insertion_position = visble_selection_after_delete.Start();
+ affinity = visble_selection_after_delete.Affinity();
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // FIXME: The parentAnchoredEquivalent conversion needs to be moved into
+ // enclosingBlock.
+ Element* start_block = EnclosingBlock(
+ insertion_position.ParentAnchoredEquivalent().ComputeContainerNode());
+ Node* list_child_node = EnclosingListChild(
+ insertion_position.ParentAnchoredEquivalent().ComputeContainerNode());
+ HTMLElement* list_child = list_child_node && list_child_node->IsHTMLElement()
+ ? ToHTMLElement(list_child_node)
+ : nullptr;
+ Position canonical_pos =
+ CreateVisiblePosition(insertion_position).DeepEquivalent();
+ if (!start_block || !start_block->NonShadowBoundaryParentNode() ||
+ IsTableCell(start_block) ||
+ IsHTMLFormElement(*start_block)
+ // FIXME: If the node is hidden, we don't have a canonical position so we
+ // will do the wrong thing for tables and <hr>.
+ // https://bugs.webkit.org/show_bug.cgi?id=40342
+ || (!canonical_pos.IsNull() &&
+ IsDisplayInsideTable(canonical_pos.AnchorNode())) ||
+ (!canonical_pos.IsNull() &&
+ IsHTMLHRElement(*canonical_pos.AnchorNode()))) {
+ ApplyCommandToComposite(InsertLineBreakCommand::Create(GetDocument()),
+ editing_state);
+ return;
+ }
+
+ // Use the leftmost candidate.
+ insertion_position = MostBackwardCaretPosition(insertion_position);
+ if (!IsVisuallyEquivalentCandidate(insertion_position))
+ insertion_position = MostForwardCaretPosition(insertion_position);
+
+ // Adjust the insertion position after the delete
+ const Position original_insertion_position = insertion_position;
+ const Element* enclosing_anchor =
+ EnclosingAnchorElement(original_insertion_position);
+ insertion_position =
+ PositionAvoidingSpecialElementBoundary(insertion_position, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ // InsertTextCommandTest.AnchorElementWithBlockCrash reaches here.
+ ABORT_EDITING_COMMAND_IF(!start_block->parentNode());
+ if (list_child == enclosing_anchor) {
+ // |positionAvoidingSpecialElementBoundary()| creates new A element and
+ // move to another place.
+ list_child =
+ ToHTMLElement(EnclosingAnchorElement(original_insertion_position));
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ CalculateStyleBeforeInsertion(insertion_position);
+
+ //---------------------------------------------------------------------
+ // Handle special case of typing return on an empty list item
+ if (BreakOutOfEmptyListItem(editing_state) || editing_state->IsAborted())
+ return;
+
+ //---------------------------------------------------------------------
+ // Prepare for more general cases.
+
+ // Create block to be inserted.
+ bool nest_new_block = false;
+ Element* block_to_insert = nullptr;
+ if (IsRootEditableElement(*start_block)) {
+ block_to_insert = CreateDefaultParagraphElement(GetDocument());
+ nest_new_block = true;
+ } else if (ShouldUseDefaultParagraphElement(start_block)) {
+ block_to_insert = CreateDefaultParagraphElement(GetDocument());
+ } else {
+ block_to_insert = start_block->CloneWithoutChildren();
+ }
+
+ VisiblePosition visible_pos =
+ CreateVisiblePosition(insertion_position, affinity);
+ bool is_first_in_block = IsStartOfBlock(visible_pos);
+ bool is_last_in_block = IsEndOfBlock(visible_pos);
+
+ //---------------------------------------------------------------------
+ // Handle case when position is in the last visible position in its block,
+ // including when the block is empty.
+ if (is_last_in_block) {
+ if (nest_new_block) {
+ if (is_first_in_block && !LineBreakExistsAtVisiblePosition(visible_pos)) {
+ // The block is empty. Create an empty block to
+ // represent the paragraph that we're leaving.
+ HTMLElement* extra_block = CreateDefaultParagraphElement(GetDocument());
+ AppendNode(extra_block, start_block, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ AppendBlockPlaceholder(extra_block, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ AppendNode(block_to_insert, start_block, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ } else {
+ // We can get here if we pasted a copied portion of a blockquote with a
+ // newline at the end and are trying to paste it into an unquoted area. We
+ // then don't want the newline within the blockquote or else it will also
+ // be quoted.
+ if (paste_blockquote_into_unquoted_area_) {
+ if (HTMLQuoteElement* highest_blockquote =
+ ToHTMLQuoteElement(HighestEnclosingNodeOfType(
+ canonical_pos, &IsMailHTMLBlockquoteElement)))
+ start_block = highest_blockquote;
+ }
+
+ if (list_child && list_child != start_block) {
+ Element* list_child_to_insert = list_child->CloneWithoutChildren();
+ AppendNode(block_to_insert, list_child_to_insert, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ InsertNodeAfter(list_child_to_insert, list_child, editing_state);
+ } else {
+ // Most of the time we want to stay at the nesting level of the
+ // startBlock (e.g., when nesting within lists). However, for div nodes,
+ // this can result in nested div tags that are hard to break out of.
+ Element* sibling_element = start_block;
+ if (IsHTMLDivElement(*block_to_insert))
+ sibling_element = HighestVisuallyEquivalentDivBelowRoot(start_block);
+ InsertNodeAfter(block_to_insert, sibling_element, editing_state);
+ }
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ // Recreate the same structure in the new paragraph.
+
+ HeapVector<Member<Element>> ancestors;
+ GetAncestorsInsideBlock(
+ PositionOutsideTabSpan(insertion_position).AnchorNode(), start_block,
+ ancestors);
+ Element* parent =
+ CloneHierarchyUnderNewBlock(ancestors, block_to_insert, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ AppendBlockPlaceholder(parent, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::FirstPositionInNode(*parent))
+ .Build()));
+ return;
+ }
+
+ //---------------------------------------------------------------------
+ // Handle case when position is in the first visible position in its block,
+ // and similar case where previous position is in another, presumeably nested,
+ // block.
+ if (is_first_in_block ||
+ !InSameBlock(visible_pos, PreviousPositionOf(visible_pos))) {
+ Node* ref_node = nullptr;
+ insertion_position = PositionOutsideTabSpan(insertion_position);
+
+ if (is_first_in_block && !nest_new_block) {
+ if (list_child && list_child != start_block) {
+ Element* list_child_to_insert = list_child->CloneWithoutChildren();
+ AppendNode(block_to_insert, list_child_to_insert, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ InsertNodeBefore(list_child_to_insert, list_child, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ } else {
+ ref_node = start_block;
+ }
+ } else if (is_first_in_block && nest_new_block) {
+ // startBlock should always have children, otherwise isLastInBlock would
+ // be true and it's handled above.
+ DCHECK(start_block->HasChildren());
+ ref_node = start_block->firstChild();
+ } else if (insertion_position.AnchorNode() == start_block &&
+ nest_new_block) {
+ ref_node = NodeTraversal::ChildAt(
+ *start_block, insertion_position.ComputeEditingOffset());
+ DCHECK(ref_node); // must be true or we'd be in the end of block case
+ } else {
+ ref_node = insertion_position.AnchorNode();
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // find ending selection position easily before inserting the paragraph
+ insertion_position = MostForwardCaretPosition(insertion_position);
+
+ if (ref_node) {
+ InsertNodeBefore(block_to_insert, ref_node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ // Recreate the same structure in the new paragraph.
+
+ HeapVector<Member<Element>> ancestors;
+ insertion_position = PositionAvoidingSpecialElementBoundary(
+ PositionOutsideTabSpan(insertion_position), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ GetAncestorsInsideBlock(insertion_position.AnchorNode(), start_block,
+ ancestors);
+
+ Element* placeholder =
+ CloneHierarchyUnderNewBlock(ancestors, block_to_insert, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ AppendBlockPlaceholder(placeholder, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ // In this case, we need to set the new ending selection.
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(insertion_position)
+ .Build()));
+ return;
+ }
+
+ //---------------------------------------------------------------------
+ // Handle the (more complicated) general case,
+
+ // All of the content in the current block after visiblePos is
+ // about to be wrapped in a new paragraph element. Add a br before
+ // it if visiblePos is at the start of a paragraph so that the
+ // content will move down a line.
+ if (IsStartOfParagraph(visible_pos)) {
+ HTMLBRElement* br = HTMLBRElement::Create(GetDocument());
+ InsertNodeAt(br, insertion_position, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ insertion_position = Position::InParentAfterNode(*br);
+ visible_pos = CreateVisiblePosition(insertion_position);
+ // If the insertion point is a break element, there is nothing else
+ // we need to do.
+ if (visible_pos.IsNotNull() &&
+ visible_pos.DeepEquivalent().AnchorNode()->GetLayoutObject()->IsBR()) {
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(insertion_position)
+ .Build()));
+ return;
+ }
+ }
+
+ // Move downstream. Typing style code will take care of carrying along the
+ // style of the upstream position.
+ insertion_position = MostForwardCaretPosition(insertion_position);
+
+ // At this point, the insertionPosition's node could be a container, and we
+ // want to make sure we include all of the correct nodes when building the
+ // ancestor list. So this needs to be the deepest representation of the
+ // position before we walk the DOM tree.
+ VisiblePosition visible_insertion_position =
+ CreateVisiblePosition(insertion_position);
+ ABORT_EDITING_COMMAND_IF(visible_insertion_position.IsNull());
+
+ insertion_position =
+ PositionOutsideTabSpan(visible_insertion_position.DeepEquivalent());
+ // If the returned position lies either at the end or at the start of an
+ // element that is ignored by editing we should move to its upstream or
+ // downstream position.
+ if (EditingIgnoresContent(*insertion_position.AnchorNode())) {
+ if (insertion_position.AtLastEditingPositionForNode())
+ insertion_position = MostForwardCaretPosition(insertion_position);
+ else if (insertion_position.AtFirstEditingPositionForNode())
+ insertion_position = MostBackwardCaretPosition(insertion_position);
+ }
+
+ ABORT_EDITING_COMMAND_IF(!IsEditablePosition(insertion_position));
+ // Make sure we do not cause a rendered space to become unrendered.
+ // FIXME: We need the affinity for pos, but mostForwardCaretPosition does not
+ // give it
+ Position leading_whitespace = LeadingCollapsibleWhitespacePosition(
+ insertion_position, TextAffinity::kDefault);
+ // FIXME: leadingCollapsibleWhitespacePosition is returning the position
+ // before preserved newlines for positions after the preserved newline,
+ // causing the newline to be turned into a nbsp.
+ if (leading_whitespace.IsNotNull() &&
+ leading_whitespace.AnchorNode()->IsTextNode()) {
+ Text* text_node = ToText(leading_whitespace.AnchorNode());
+ DCHECK(!text_node->GetLayoutObject() ||
+ text_node->GetLayoutObject()->Style()->CollapseWhiteSpace())
+ << text_node;
+ ReplaceTextInNode(text_node,
+ leading_whitespace.ComputeOffsetInContainerNode(), 1,
+ NonBreakingSpaceString());
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ }
+
+ // Split at pos if in the middle of a text node.
+ Position position_after_split;
+ if (insertion_position.IsOffsetInAnchor() &&
+ insertion_position.ComputeContainerNode()->IsTextNode()) {
+ Text* text_node = ToText(insertion_position.ComputeContainerNode());
+ int text_offset = insertion_position.OffsetInContainerNode();
+ bool at_end = static_cast<unsigned>(text_offset) >= text_node->length();
+ if (text_offset > 0 && !at_end) {
+ SplitTextNode(text_node, text_offset);
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ position_after_split = Position::FirstPositionInNode(*text_node);
+ insertion_position = Position(text_node->previousSibling(), text_offset);
+ }
+ }
+
+ // If we got detached due to mutation events, just bail out.
+ if (!start_block->parentNode())
+ return;
+
+ // Put the added block in the tree.
+ if (nest_new_block) {
+ AppendNode(block_to_insert, start_block, editing_state);
+ } else if (list_child && list_child != start_block) {
+ Element* list_child_to_insert = list_child->CloneWithoutChildren();
+ AppendNode(block_to_insert, list_child_to_insert, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ InsertNodeAfter(list_child_to_insert, list_child, editing_state);
+ } else {
+ InsertNodeAfter(block_to_insert, start_block, editing_state);
+ }
+ if (editing_state->IsAborted())
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ visible_pos = CreateVisiblePosition(insertion_position);
+
+ // If the paragraph separator was inserted at the end of a paragraph, an empty
+ // line must be created. All of the nodes, starting at visiblePos, are about
+ // to be added to the new paragraph element. If the first node to be inserted
+ // won't be one that will hold an empty line open, add a br.
+ if (IsEndOfParagraph(visible_pos) &&
+ !LineBreakExistsAtVisiblePosition(visible_pos)) {
+ AppendNode(HTMLBRElement::Create(GetDocument()), block_to_insert,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ }
+
+ // Move the start node and the siblings of the start node.
+ if (CreateVisiblePosition(insertion_position).DeepEquivalent() !=
+ VisiblePosition::BeforeNode(*block_to_insert).DeepEquivalent()) {
+ Node* n;
+ if (insertion_position.ComputeContainerNode() == start_block) {
+ n = insertion_position.ComputeNodeAfterPosition();
+ } else {
+ Node* split_to = insertion_position.ComputeContainerNode();
+ if (split_to->IsTextNode() &&
+ insertion_position.OffsetInContainerNode() >=
+ CaretMaxOffset(split_to))
+ split_to = NodeTraversal::Next(*split_to, start_block);
+ if (split_to)
+ SplitTreeToNode(split_to, start_block);
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ for (n = start_block->firstChild(); n; n = n->nextSibling()) {
+ VisiblePosition before_node_position = VisiblePosition::BeforeNode(*n);
+ if (!before_node_position.IsNull() &&
+ ComparePositions(CreateVisiblePosition(insertion_position),
+ before_node_position) <= 0)
+ break;
+ }
+ }
+
+ MoveRemainingSiblingsToNewParent(n, block_to_insert, block_to_insert,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ // Handle whitespace that occurs after the split
+ if (position_after_split.IsNotNull()) {
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (!IsRenderedCharacter(position_after_split)) {
+ // Clear out all whitespace and insert one non-breaking space
+ DCHECK(!position_after_split.ComputeContainerNode()->GetLayoutObject() ||
+ position_after_split.ComputeContainerNode()
+ ->GetLayoutObject()
+ ->Style()
+ ->CollapseWhiteSpace())
+ << position_after_split;
+ DeleteInsignificantTextDownstream(position_after_split);
+ if (position_after_split.AnchorNode()->IsTextNode())
+ InsertTextIntoNode(ToText(position_after_split.ComputeContainerNode()),
+ 0, NonBreakingSpaceString());
+ }
+ }
+
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::FirstPositionInNode(*block_to_insert))
+ .Build()));
+ ApplyStyleAfterInsertion(start_block, editing_state);
+}
+
+void InsertParagraphSeparatorCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(style_);
+ CompositeEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command.h b/chromium/third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command.h
new file mode 100644
index 00000000000..c4049a68a44
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_PARAGRAPH_SEPARATOR_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_PARAGRAPH_SEPARATOR_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+namespace blink {
+
+class EditingStyle;
+
+class CORE_EXPORT InsertParagraphSeparatorCommand final
+ : public CompositeEditCommand {
+ public:
+ static InsertParagraphSeparatorCommand* Create(
+ Document& document,
+ bool use_default_paragraph_element = false,
+ bool paste_blockquote_into_unquoted_area = false) {
+ return new InsertParagraphSeparatorCommand(
+ document, use_default_paragraph_element,
+ paste_blockquote_into_unquoted_area);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ InsertParagraphSeparatorCommand(Document&,
+ bool use_default_paragraph_element,
+ bool paste_blockquote_into_unquoted_area);
+
+ void DoApply(EditingState*) override;
+
+ void CalculateStyleBeforeInsertion(const Position&);
+ void ApplyStyleAfterInsertion(Element* original_enclosing_block,
+ EditingState*);
+ void GetAncestorsInsideBlock(const Node* insertion_node,
+ Element* outer_block,
+ HeapVector<Member<Element>>& ancestors);
+ Element* CloneHierarchyUnderNewBlock(
+ const HeapVector<Member<Element>>& ancestors,
+ Element* block_to_insert,
+ EditingState*);
+
+ bool ShouldUseDefaultParagraphElement(Element*) const;
+
+ bool PreservesTypingStyle() const override;
+
+ Member<EditingStyle> style_;
+
+ bool must_use_default_paragraph_element_;
+ bool paste_blockquote_into_unquoted_area_;
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command_test.cc b/chromium/third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command_test.cc
new file mode 100644
index 00000000000..a3ebbda1a58
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command_test.cc
@@ -0,0 +1,58 @@
+// Copyright (c) 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command.h"
+
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class InsertParagraphSeparatorCommandTest : public EditingTestBase {};
+
+// http://crbug.com/777378
+TEST_F(InsertParagraphSeparatorCommandTest,
+ CrashWithAppearanceStyleOnEmptyColgroup) {
+ Selection().SetSelectionAndEndTyping(SetSelectionTextToBody(
+ "<table contenteditable>"
+ " <colgroup style='-webkit-appearance:radio;'><!--|--></colgroup>"
+ "</table>"));
+
+ InsertParagraphSeparatorCommand* command =
+ InsertParagraphSeparatorCommand::Create(GetDocument());
+ // Crash should not be observed here.
+ command->Apply();
+
+ EXPECT_EQ(
+ "<table contenteditable>"
+ " <colgroup style=\"-webkit-appearance:radio;\">|<br></colgroup>"
+ "</table>",
+ GetSelectionTextFromBody());
+}
+
+// http://crbug.com/777378
+TEST_F(InsertParagraphSeparatorCommandTest,
+ CrashWithAppearanceStyleOnEmptyColumn) {
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<table contenteditable>"
+ " <colgroup style='-webkit-appearance:radio;'>"
+ " <col><!--|--></col>"
+ " </colgroup>"
+ "</table>"));
+
+ InsertParagraphSeparatorCommand* command =
+ InsertParagraphSeparatorCommand::Create(GetDocument());
+ // Crash should not be observed here.
+ command->Apply();
+ EXPECT_EQ(
+ "<table contenteditable>"
+ " <colgroup style=\"-webkit-appearance:radio;\">|<br>"
+ " <col>"
+ " </colgroup>"
+ "</table>",
+ GetSelectionTextFromBody());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_text_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/insert_text_command.cc
new file mode 100644
index 00000000000..3f47650965f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_text_command.cc
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2005 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/insert_text_command.h"
+
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/commands/delete_selection_options.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/html/html_span_element.h"
+
+namespace blink {
+
+InsertTextCommand::InsertTextCommand(Document& document,
+ const String& text,
+ RebalanceType rebalance_type)
+ : CompositeEditCommand(document),
+ text_(text),
+ rebalance_type_(rebalance_type) {}
+
+String InsertTextCommand::TextDataForInputEvent() const {
+ return text_;
+}
+
+Position InsertTextCommand::PositionInsideTextNode(
+ const Position& p,
+ EditingState* editing_state) {
+ Position pos = p;
+ if (IsTabHTMLSpanElementTextNode(pos.AnchorNode())) {
+ Text* text_node = GetDocument().CreateEditingTextNode("");
+ InsertNodeAtTabSpanPosition(text_node, pos, editing_state);
+ if (editing_state->IsAborted())
+ return Position();
+ return Position::FirstPositionInNode(*text_node);
+ }
+
+ // Prepare for text input by looking at the specified position.
+ // It may be necessary to insert a text node to receive characters.
+ if (!pos.ComputeContainerNode()->IsTextNode()) {
+ Text* text_node = GetDocument().CreateEditingTextNode("");
+ InsertNodeAt(text_node, pos, editing_state);
+ if (editing_state->IsAborted())
+ return Position();
+ return Position::FirstPositionInNode(*text_node);
+ }
+
+ return pos;
+}
+
+void InsertTextCommand::SetEndingSelectionWithoutValidation(
+ const Position& start_position,
+ const Position& end_position) {
+ // We could have inserted a part of composed character sequence,
+ // so we are basically treating ending selection as a range to avoid
+ // validation. <http://bugs.webkit.org/show_bug.cgi?id=15781>
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(start_position)
+ .Extend(end_position)
+ .Build()));
+}
+
+// This avoids the expense of a full fledged delete operation, and avoids a
+// layout that typically results from text removal.
+bool InsertTextCommand::PerformTrivialReplace(const String& text) {
+ // We may need to manipulate neighboring whitespace if we're deleting text.
+ // This case is tested in
+ // InsertTextCommandTest_InsertEmptyTextAfterWhitespaceThatNeedsFixup.
+ if (text.IsEmpty())
+ return false;
+
+ if (!EndingSelection().IsRange())
+ return false;
+
+ if (text.Contains('\t') || text.Contains(' ') || text.Contains('\n'))
+ return false;
+
+ Position start = EndingVisibleSelection().Start();
+ Position end_position = ReplaceSelectedTextInNode(text);
+ if (end_position.IsNull())
+ return false;
+
+ SetEndingSelectionWithoutValidation(start, end_position);
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(EndingVisibleSelection().End())
+ .Build()));
+ return true;
+}
+
+bool InsertTextCommand::PerformOverwrite(const String& text) {
+ Position start = EndingVisibleSelection().Start();
+ if (start.IsNull() || !start.IsOffsetInAnchor() ||
+ !start.ComputeContainerNode()->IsTextNode())
+ return false;
+ Text* text_node = ToText(start.ComputeContainerNode());
+ if (!text_node)
+ return false;
+
+ unsigned count = std::min(
+ text.length(), text_node->length() - start.OffsetInContainerNode());
+ if (!count)
+ return false;
+
+ ReplaceTextInNode(text_node, start.OffsetInContainerNode(), count, text);
+
+ Position end_position =
+ Position(text_node, start.OffsetInContainerNode() + text.length());
+ SetEndingSelectionWithoutValidation(start, end_position);
+ if (EndingSelection().IsNone())
+ return true;
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(EndingVisibleSelection().End())
+ .Build()));
+ return true;
+}
+
+void InsertTextCommand::DoApply(EditingState* editing_state) {
+ DCHECK_EQ(text_.find('\n'), kNotFound);
+
+ // TODO(editing-dev): We shouldn't construct an InsertTextCommand with none or
+ // invalid selection.
+ const VisibleSelection& visible_selection = EndingVisibleSelection();
+ if (visible_selection.IsNone() ||
+ !visible_selection.IsValidFor(GetDocument()))
+ return;
+
+ // Delete the current selection.
+ // FIXME: This delete operation blows away the typing style.
+ if (EndingSelection().IsRange()) {
+ if (PerformTrivialReplace(text_))
+ return;
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ bool end_of_selection_was_at_start_of_block =
+ IsStartOfBlock(EndingVisibleSelection().VisibleEnd());
+ if (!DeleteSelection(editing_state, DeleteSelectionOptions::Builder()
+ .SetMergeBlocksAfterDelete(true)
+ .Build()))
+ return;
+ // deleteSelection eventually makes a new endingSelection out of a Position.
+ // If that Position doesn't have a layoutObject (e.g. it is on a <frameset>
+ // in the DOM), the VisibleSelection cannot be canonicalized to anything
+ // other than NoSelection. The rest of this function requires a real
+ // endingSelection, so bail out.
+ if (EndingSelection().IsNone())
+ return;
+ if (end_of_selection_was_at_start_of_block) {
+ if (EditingStyle* typing_style =
+ GetDocument().GetFrame()->GetEditor().TypingStyle())
+ typing_style->RemoveBlockProperties();
+ }
+ } else if (GetDocument().GetFrame()->GetEditor().IsOverwriteModeEnabled()) {
+ if (PerformOverwrite(text_))
+ return;
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Reached by InsertTextCommandTest.NoVisibleSelectionAfterDeletingSelection
+ ABORT_EDITING_COMMAND_IF(EndingVisibleSelection().IsNone());
+
+ Position start_position(EndingVisibleSelection().Start());
+
+ Position placeholder;
+ // We want to remove preserved newlines and brs that will collapse (and thus
+ // become unnecessary) when content is inserted just before them.
+ // FIXME: We shouldn't really have to do this, but removing placeholders is a
+ // workaround for 9661.
+ // If the caret is just before a placeholder, downstream will normalize the
+ // caret to it.
+ Position downstream(MostForwardCaretPosition(start_position));
+ if (LineBreakExistsAtPosition(downstream)) {
+ // FIXME: This doesn't handle placeholders at the end of anonymous blocks.
+ VisiblePosition caret = CreateVisiblePosition(start_position);
+ if (IsEndOfBlock(caret) && IsStartOfParagraph(caret))
+ placeholder = downstream;
+ // Don't remove the placeholder yet, otherwise the block we're inserting
+ // into would collapse before we get a chance to insert into it. We check
+ // for a placeholder now, though, because doing so requires the creation of
+ // a VisiblePosition, and if we did that post-insertion it would force a
+ // layout.
+ }
+
+ // Insert the character at the leftmost candidate.
+ start_position = MostBackwardCaretPosition(start_position);
+
+ // It is possible for the node that contains startPosition to contain only
+ // unrendered whitespace, and so deleteInsignificantText could remove it.
+ // Save the position before the node in case that happens.
+ DCHECK(start_position.ComputeContainerNode()) << start_position;
+ Position position_before_start_node(
+ Position::InParentBeforeNode(*start_position.ComputeContainerNode()));
+ DeleteInsignificantText(start_position,
+ MostForwardCaretPosition(start_position));
+
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets()
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (!start_position.IsConnected())
+ start_position = position_before_start_node;
+ if (!IsVisuallyEquivalentCandidate(start_position))
+ start_position = MostForwardCaretPosition(start_position);
+
+ start_position =
+ PositionAvoidingSpecialElementBoundary(start_position, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ Position end_position;
+
+ if (text_ == "\t" && IsRichlyEditablePosition(start_position)) {
+ end_position = InsertTab(start_position, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ start_position =
+ PreviousPositionOf(end_position, PositionMoveType::kGraphemeCluster);
+ if (placeholder.IsNotNull())
+ RemovePlaceholderAt(placeholder);
+ } else {
+ // Make sure the document is set up to receive m_text
+ start_position = PositionInsideTextNode(start_position, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ DCHECK(start_position.IsOffsetInAnchor()) << start_position;
+ DCHECK(start_position.ComputeContainerNode()) << start_position;
+ DCHECK(start_position.ComputeContainerNode()->IsTextNode())
+ << start_position;
+ if (placeholder.IsNotNull())
+ RemovePlaceholderAt(placeholder);
+ Text* text_node = ToText(start_position.ComputeContainerNode());
+ const unsigned offset = start_position.OffsetInContainerNode();
+
+ InsertTextIntoNode(text_node, offset, text_);
+ end_position = Position(text_node, offset + text_.length());
+
+ if (rebalance_type_ == kRebalanceLeadingAndTrailingWhitespaces) {
+ // The insertion may require adjusting adjacent whitespace, if it is
+ // present.
+ RebalanceWhitespaceAt(end_position);
+ // Rebalancing on both sides isn't necessary if we've inserted only
+ // spaces.
+ if (!text_.ContainsOnlyWhitespace())
+ RebalanceWhitespaceAt(start_position);
+ } else {
+ DCHECK_EQ(rebalance_type_, kRebalanceAllWhitespaces);
+ if (CanRebalance(start_position) && CanRebalance(end_position))
+ RebalanceWhitespaceOnTextSubstring(
+ text_node, start_position.OffsetInContainerNode(),
+ end_position.OffsetInContainerNode());
+ }
+ }
+
+ SetEndingSelectionWithoutValidation(start_position, end_position);
+
+ // Handle the case where there is a typing style.
+ if (EditingStyle* typing_style =
+ GetDocument().GetFrame()->GetEditor().TypingStyle()) {
+ typing_style->PrepareToApplyAt(end_position,
+ EditingStyle::kPreserveWritingDirection);
+ if (!typing_style->IsEmpty() && !EndingSelection().IsNone()) {
+ ApplyStyle(typing_style, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+
+ SelectionInDOMTree::Builder builder;
+ const VisibleSelection& selection = EndingVisibleSelection();
+ builder.SetAffinity(selection.Affinity());
+ if (selection.End().IsNotNull())
+ builder.Collapse(selection.End());
+ SetEndingSelection(SelectionForUndoStep::From(builder.Build()));
+}
+
+Position InsertTextCommand::InsertTab(const Position& pos,
+ EditingState* editing_state) {
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ Position insert_pos = CreateVisiblePosition(pos).DeepEquivalent();
+ if (insert_pos.IsNull())
+ return pos;
+
+ Node* node = insert_pos.ComputeContainerNode();
+ unsigned offset = node->IsTextNode() ? insert_pos.OffsetInContainerNode() : 0;
+
+ // keep tabs coalesced in tab span
+ if (IsTabHTMLSpanElementTextNode(node)) {
+ Text* text_node = ToText(node);
+ InsertTextIntoNode(text_node, offset, "\t");
+ return Position(text_node, offset + 1);
+ }
+
+ // create new tab span
+ HTMLSpanElement* span_element = CreateTabSpanElement(GetDocument());
+
+ // place it
+ if (!node->IsTextNode()) {
+ InsertNodeAt(span_element, insert_pos, editing_state);
+ } else {
+ Text* text_node = ToText(node);
+ if (offset >= text_node->length()) {
+ InsertNodeAfter(span_element, text_node, editing_state);
+ } else {
+ // split node to make room for the span
+ // NOTE: splitTextNode uses textNode for the
+ // second node in the split, so we need to
+ // insert the span before it.
+ if (offset > 0)
+ SplitTextNode(text_node, offset);
+ InsertNodeBefore(span_element, text_node, editing_state);
+ }
+ }
+ if (editing_state->IsAborted())
+ return Position();
+
+ // return the position following the new tab
+ return Position::LastPositionInNode(*span_element);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_text_command.h b/chromium/third_party/blink/renderer/core/editing/commands/insert_text_command.h
new file mode 100644
index 00000000000..c839cedd3b1
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_text_command.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_TEXT_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_TEXT_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+namespace blink {
+
+class CORE_EXPORT InsertTextCommand : public CompositeEditCommand {
+ public:
+ enum RebalanceType {
+ kRebalanceLeadingAndTrailingWhitespaces,
+ kRebalanceAllWhitespaces
+ };
+
+ static InsertTextCommand* Create(
+ Document& document,
+ const String& text,
+ RebalanceType rebalance_type = kRebalanceLeadingAndTrailingWhitespaces) {
+ return new InsertTextCommand(document, text, rebalance_type);
+ }
+
+ String TextDataForInputEvent() const final;
+
+ protected:
+ InsertTextCommand(Document&,
+ const String& text,
+ RebalanceType);
+
+ void DoApply(EditingState*) override;
+
+ Position PositionInsideTextNode(const Position&, EditingState*);
+ Position InsertTab(const Position&, EditingState*);
+
+ bool PerformTrivialReplace(const String&);
+ bool PerformOverwrite(const String&);
+ void SetEndingSelectionWithoutValidation(const Position& start_position,
+ const Position& end_position);
+
+ friend class TypingCommand;
+
+ String text_;
+ RebalanceType rebalance_type_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_INSERT_TEXT_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/insert_text_command_test.cc b/chromium/third_party/blink/renderer/core/editing/commands/insert_text_command_test.cc
new file mode 100644
index 00000000000..0b712abc381
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/insert_text_command_test.cc
@@ -0,0 +1,283 @@
+// Copyright (c) 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/insert_text_command.h"
+
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/testing/selection_sample.h"
+
+namespace blink {
+
+class InsertTextCommandTest : public EditingTestBase {};
+
+// http://crbug.com/714311
+TEST_F(InsertTextCommandTest, WithTypingStyle) {
+ SetBodyContent("<div contenteditable=true><option id=sample></option></div>");
+ Element* const sample = GetDocument().getElementById("sample");
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder().Collapse(Position(sample, 0)).Build());
+ // Register typing style to make |InsertTextCommand| to attempt to apply
+ // style to inserted text.
+ GetDocument().execCommand("fontSizeDelta", false, "+3", ASSERT_NO_EXCEPTION);
+ CompositeEditCommand* const command =
+ InsertTextCommand::Create(GetDocument(), "x");
+ command->Apply();
+
+ EXPECT_EQ(
+ "<div contenteditable=\"true\"><option id=\"sample\">x</option></div>",
+ GetDocument().body()->InnerHTMLAsString())
+ << "Content of OPTION is distributed into shadow node as text"
+ "without applying typing style.";
+}
+
+// http://crbug.com/741826
+TEST_F(InsertTextCommandTest, InsertChar) {
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<p contenteditable><span>\ta|c</span></p>"));
+ GetDocument().execCommand("insertText", false, "B", ASSERT_NO_EXCEPTION);
+ EXPECT_EQ("<p contenteditable><span>\taB|c</span></p>",
+ GetSelectionTextFromBody())
+ << "We should not split Text node";
+}
+
+// http://crbug.com/741826
+TEST_F(InsertTextCommandTest, InsertCharToWhiteSpacePre) {
+ Selection().SetSelectionAndEndTyping(SetSelectionTextToBody(
+ "<p contenteditable><span style='white-space:pre'>\ta|c</span></p>"));
+ GetDocument().execCommand("insertText", false, "B", ASSERT_NO_EXCEPTION);
+ EXPECT_EQ(
+ "<p contenteditable>"
+ "<span style=\"white-space:pre\">\ta</span>"
+ "B|"
+ "<span style=\"white-space:pre\">c</span>"
+ "</p>",
+ GetSelectionTextFromBody())
+ << "This is a just record current behavior. We should not split SPAN.";
+}
+
+// http://crbug.com/741826
+TEST_F(InsertTextCommandTest, InsertSpace) {
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<p contenteditable><span>\ta|c</span></p>"));
+ GetDocument().execCommand("insertText", false, " ", ASSERT_NO_EXCEPTION);
+ EXPECT_EQ("<p contenteditable><span>\ta\xC2\xA0 |c</span></p>",
+ GetSelectionTextFromBody())
+ << "We should insert U+0020 without splitting SPAN";
+}
+
+// http://crbug.com/741826
+TEST_F(InsertTextCommandTest, InsertSpaceToWhiteSpacePre) {
+ Selection().SetSelectionAndEndTyping(SetSelectionTextToBody(
+ "<p contenteditable><span style='white-space:pre'>\ta|c</span></p>"));
+ GetDocument().execCommand("insertText", false, " ", ASSERT_NO_EXCEPTION);
+ EXPECT_EQ(
+ "<p contenteditable>"
+ "<span style=\"white-space:pre\">\ta</span>"
+ "\xC2\xA0\xC2\xA0|"
+ "<span style=\"white-space:pre\">c</span></p>",
+ GetSelectionTextFromBody())
+ << "We should insert U+0020 without splitting SPAN";
+}
+
+// http://crbug.com/741826
+TEST_F(InsertTextCommandTest, InsertTab) {
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<p contenteditable><span>\ta|c</span></p>"));
+ GetDocument().execCommand("insertText", false, "\t", ASSERT_NO_EXCEPTION);
+ EXPECT_EQ(
+ "<p contenteditable>"
+ "<span>\ta<span style=\"white-space:pre\">\t|</span>c</span>"
+ "</p>",
+ GetSelectionTextFromBody());
+}
+
+// http://crbug.com/741826
+TEST_F(InsertTextCommandTest, InsertTabToWhiteSpacePre) {
+ Selection().SetSelectionAndEndTyping(SetSelectionTextToBody(
+ "<p contenteditable><span style='white-space:pre'>\ta|c</span></p>"));
+ GetDocument().execCommand("insertText", false, "\t", ASSERT_NO_EXCEPTION);
+ EXPECT_EQ(
+ "<p contenteditable><span style=\"white-space:pre\">\ta\t|c</span></p>",
+ GetSelectionTextFromBody());
+}
+
+// http://crbug.com/752860
+TEST_F(InsertTextCommandTest, WhitespaceFixupBeforeParagraph) {
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<div contenteditable>qux ^bar|<p>baz</p>"));
+ GetDocument().execCommand("insertText", false, "", ASSERT_NO_EXCEPTION);
+ // The space after "qux" should have been converted to a no-break space
+ // (U+00A0) to prevent it from being collapsed.
+ EXPECT_EQ("<div contenteditable>qux\xC2\xA0|<p>baz</p></div>",
+ GetSelectionTextFromBody());
+
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<div contenteditable>qux^ bar|<p>baz</p>"));
+ GetDocument().execCommand("insertText", false, " ", ASSERT_NO_EXCEPTION);
+ // The newly-inserted space should have been converted to a no-break space
+ // (U+00A0) to prevent it from being collapsed.
+ EXPECT_EQ("<div contenteditable>qux\xC2\xA0|<p>baz</p></div>",
+ GetSelectionTextFromBody());
+
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<div contenteditable>qux^bar| <p>baz</p>"));
+ GetDocument().execCommand("insertText", false, "", ASSERT_NO_EXCEPTION);
+ // The space after "bar" was already being collapsed before the edit. It
+ // should not have been converted to a no-break space.
+ EXPECT_EQ("<div contenteditable>qux|<p>baz</p></div>",
+ GetSelectionTextFromBody());
+
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<div contenteditable>qux^bar |<p>baz</p>"));
+ GetDocument().execCommand("insertText", false, " ", ASSERT_NO_EXCEPTION);
+ // The newly-inserted space should have been converted to a no-break space
+ // (U+00A0) to prevent it from being collapsed.
+ EXPECT_EQ("<div contenteditable>qux\xC2\xA0|<p>baz</p></div>",
+ GetSelectionTextFromBody());
+
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<div contenteditable>qux\t^bar|<p>baz</p>"));
+ GetDocument().execCommand("insertText", false, "", ASSERT_NO_EXCEPTION);
+ // The tab should have been converted to a no-break space (U+00A0) to prevent
+ // it from being collapsed.
+ EXPECT_EQ("<div contenteditable>qux\xC2\xA0|<p>baz</p></div>",
+ GetSelectionTextFromBody());
+}
+
+TEST_F(InsertTextCommandTest, WhitespaceFixupAfterParagraph) {
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<div contenteditable><p>baz</p>^bar| qux"));
+ GetDocument().execCommand("insertText", false, "", ASSERT_NO_EXCEPTION);
+ // The space before "qux" should have been converted to a no-break space
+ // (U+00A0) to prevent it from being collapsed.
+ EXPECT_EQ("<div contenteditable><p>baz</p>|\xC2\xA0qux</div>",
+ GetSelectionTextFromBody());
+
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<div contenteditable><p>baz</p>^bar |qux"));
+ GetDocument().execCommand("insertText", false, " ", ASSERT_NO_EXCEPTION);
+ // The newly-inserted space should have been converted to a no-break space
+ // (U+00A0) to prevent it from being collapsed.
+ EXPECT_EQ("<div contenteditable><p>baz</p>\xC2\xA0|qux</div>",
+ GetSelectionTextFromBody());
+
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<div contenteditable><p>baz</p> ^bar|qux"));
+ GetDocument().execCommand("insertText", false, "", ASSERT_NO_EXCEPTION);
+ // The space before "bar" was already being collapsed before the edit. It
+ // should not have been converted to a no-break space.
+ EXPECT_EQ("<div contenteditable><p>baz</p>|qux</div>",
+ GetSelectionTextFromBody());
+
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<div contenteditable><p>baz</p>^ bar|qux"));
+ GetDocument().execCommand("insertText", false, " ", ASSERT_NO_EXCEPTION);
+ // The newly-inserted space should have been converted to a no-break space
+ // (U+00A0) to prevent it from being collapsed.
+ EXPECT_EQ("<div contenteditable><p>baz</p>\xC2\xA0|qux</div>",
+ GetSelectionTextFromBody());
+
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<div contenteditable><p>baz</p>^bar|\tqux"));
+ GetDocument().execCommand("insertText", false, "", ASSERT_NO_EXCEPTION);
+ // The tab should have been converted to a no-break space (U+00A0) to prevent
+ // it from being collapsed.
+ EXPECT_EQ("<div contenteditable><p>baz</p>|\xC2\xA0qux</div>",
+ GetSelectionTextFromBody());
+}
+
+// http://crbug.com/779376
+TEST_F(InsertTextCommandTest, NoVisibleSelectionAfterDeletingSelection) {
+ GetDocument().SetCompatibilityMode(Document::kQuirksMode);
+ InsertStyleElement(
+ "ruby {display: inline-block; height: 100%}"
+ "navi {float: left}");
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<div contenteditable>"
+ " <ruby><strike>"
+ " <navi></navi>"
+ " <rtc>^&#xbbc3;&#xff17;&#x8e99;&#x1550;</rtc>"
+ " </strike></ruby>"
+ " <hr>|"
+ "</div>"));
+ // Shouldn't crash inside
+ GetDocument().execCommand("insertText", false, "x", ASSERT_NO_EXCEPTION);
+ // This is only for recording the current behavior, which can be changed.
+ EXPECT_EQ(
+ "<div contenteditable>"
+ " <ruby><strike>"
+ " <navi></navi>"
+ " ^</strike></ruby>"
+ "|</div>",
+ GetSelectionTextFromBody());
+}
+
+// http://crbug.com/778901
+TEST_F(InsertTextCommandTest, CheckTabSpanElementNoCrash) {
+ InsertStyleElement(
+ "head {-webkit-text-stroke-color: black; display: list-item;}");
+ Element* head = GetDocument().QuerySelector("head");
+ Element* style = GetDocument().QuerySelector("style");
+ Element* body = GetDocument().body();
+ body->parentNode()->appendChild(style);
+ GetDocument().setDesignMode("on");
+
+ Selection().SetSelectionAndEndTyping(SelectionInDOMTree::Builder()
+ .Collapse(Position(head, 0))
+ .Extend(Position(body, 0))
+ .Build());
+
+ // Shouldn't crash inside
+ GetDocument().execCommand("insertText", false, "\t", ASSERT_NO_EXCEPTION);
+
+ // This only records the current behavior, which is not necessarily correct.
+ EXPECT_EQ(
+ "<body><span style=\"white-space:pre\">\t|</span></body>"
+ "<style>"
+ "head {-webkit-text-stroke-color: black; display: list-item;}"
+ "</style>",
+ SelectionSample::GetSelectionText(*GetDocument().documentElement(),
+ Selection().GetSelectionInDOMTree()));
+}
+
+// http://crbug.com/792548
+TEST_F(InsertTextCommandTest, AnchorElementWithBlockCrash) {
+ GetDocument().setDesignMode("on");
+ SetBodyContent("<a href=\"www\" style=\"display:block\">");
+ // We need the below DOM with selection.
+ // <a href=\"www\" style=\"display:block\">
+ // <a href=\"www\" style=\"display: inline !important;\">
+ // <i>^home|</i>
+ // </a>
+ // </a>
+ // Since the HTML parser rejects it as there are nested <a> elements.
+ // We are contructing the remaining DOM manually.
+ Element* const anchor = GetDocument().QuerySelector("a");
+ Element* nested_anchor = GetDocument().CreateRawElement(HTMLNames::aTag);
+ Element* iElement = GetDocument().CreateRawElement(HTMLNames::iTag);
+
+ nested_anchor->setAttribute("href", "www");
+ iElement->SetInnerHTMLFromString("home");
+
+ anchor->AppendChild(nested_anchor);
+ nested_anchor->AppendChild(iElement);
+
+ Node* const iElement_text_node = iElement->firstChild();
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(iElement_text_node, 0),
+ Position(iElement_text_node, 4))
+ .Build());
+ // Crash happens here with when '\n' is inserted.
+ GetDocument().execCommand("inserttext", false, "a\n", ASSERT_NO_EXCEPTION);
+ EXPECT_EQ(
+ "<i style=\"display: block;\">"
+ "<a href=\"www\" style=\"display: block;\">a</a>"
+ "</i>|",
+ GetSelectionTextFromBody());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/merge_identical_elements_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/merge_identical_elements_command.cc
new file mode 100644
index 00000000000..af13bce1054
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/merge_identical_elements_command.cc
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2005, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/merge_identical_elements_command.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+
+namespace blink {
+
+MergeIdenticalElementsCommand::MergeIdenticalElementsCommand(Element* first,
+ Element* second)
+ : SimpleEditCommand(first->GetDocument()),
+ element1_(first),
+ element2_(second) {
+ DCHECK(element1_);
+ DCHECK(element2_);
+ DCHECK_EQ(element1_->nextSibling(), element2_);
+}
+
+void MergeIdenticalElementsCommand::DoApply(EditingState*) {
+ if (element1_->nextSibling() != element2_ || !HasEditableStyle(*element1_) ||
+ !HasEditableStyle(*element2_))
+ return;
+
+ at_child_ = element2_->firstChild();
+
+ NodeVector children;
+ GetChildNodes(*element1_, children);
+
+ for (auto& child : children) {
+ element2_->InsertBefore(child.Release(), at_child_.Get(),
+ IGNORE_EXCEPTION_FOR_TESTING);
+ }
+
+ element1_->remove(IGNORE_EXCEPTION_FOR_TESTING);
+}
+
+void MergeIdenticalElementsCommand::DoUnapply() {
+ DCHECK(element1_);
+ DCHECK(element2_);
+
+ Node* at_child = at_child_.Release();
+
+ ContainerNode* parent = element2_->parentNode();
+ if (!parent || !HasEditableStyle(*parent))
+ return;
+
+ DummyExceptionStateForTesting exception_state;
+
+ parent->InsertBefore(element1_.Get(), element2_.Get(), exception_state);
+ if (exception_state.HadException())
+ return;
+
+ HeapVector<Member<Node>> children;
+ for (Node* child = element2_->firstChild(); child && child != at_child;
+ child = child->nextSibling())
+ children.push_back(child);
+
+ for (auto& child : children)
+ element1_->AppendChild(child.Release(), exception_state);
+}
+
+void MergeIdenticalElementsCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(element1_);
+ visitor->Trace(element2_);
+ visitor->Trace(at_child_);
+ SimpleEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/merge_identical_elements_command.h b/chromium/third_party/blink/renderer/core/editing/commands/merge_identical_elements_command.h
new file mode 100644
index 00000000000..88f7c094708
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/merge_identical_elements_command.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_MERGE_IDENTICAL_ELEMENTS_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_MERGE_IDENTICAL_ELEMENTS_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/edit_command.h"
+
+namespace blink {
+
+class MergeIdenticalElementsCommand final : public SimpleEditCommand {
+ public:
+ static MergeIdenticalElementsCommand* Create(Element* element1,
+ Element* element2) {
+ return new MergeIdenticalElementsCommand(element1, element2);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ MergeIdenticalElementsCommand(Element*, Element*);
+
+ void DoApply(EditingState*) override;
+ void DoUnapply() override;
+
+ Member<Element> element1_;
+ Member<Element> element2_;
+ Member<Node> at_child_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_MERGE_IDENTICAL_ELEMENTS_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/move_commands.cc b/chromium/third_party/blink/renderer/core/editing/commands/move_commands.cc
new file mode 100644
index 00000000000..282ebdfdbcd
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/move_commands.cc
@@ -0,0 +1,595 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
+ * Copyright (C) 2009 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/move_commands.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/selection_modifier.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/layout/layout_box.h"
+
+namespace blink {
+
+unsigned MoveCommands::VerticalScrollDistance(LocalFrame& frame) {
+ const Element* const focused_element = frame.GetDocument()->FocusedElement();
+ if (!focused_element)
+ return 0;
+ LayoutObject* const layout_object = focused_element->GetLayoutObject();
+ if (!layout_object || !layout_object->IsBox())
+ return 0;
+ LayoutBox& layout_box = ToLayoutBox(*layout_object);
+ const ComputedStyle* const style = layout_box.Style();
+ if (!style)
+ return 0;
+ if (!(style->OverflowY() == EOverflow::kScroll ||
+ style->OverflowY() == EOverflow::kAuto ||
+ HasEditableStyle(*focused_element)))
+ return 0;
+ const ScrollableArea& scrollable_area =
+ *frame.View()->LayoutViewportScrollableArea();
+ const int height = std::min<int>(layout_box.ClientHeight().ToInt(),
+ scrollable_area.VisibleHeight());
+ return static_cast<unsigned>(
+ max(max<int>(height * ScrollableArea::MinFractionToStepWhenPaging(),
+ height - scrollable_area.MaxOverlapBetweenPages()),
+ 1));
+}
+
+bool MoveCommands::ModifySelectionWithPageGranularity(
+ LocalFrame& frame,
+ SelectionModifyAlteration alter,
+ unsigned vertical_distance,
+ SelectionModifyVerticalDirection direction) {
+ SelectionModifier selection_modifier(
+ frame, frame.Selection().GetSelectionInDOMTree());
+ selection_modifier.SetSelectionIsDirectional(
+ frame.Selection().IsDirectional());
+ if (!selection_modifier.ModifyWithPageGranularity(alter, vertical_distance,
+ direction)) {
+ return false;
+ }
+
+ frame.Selection().SetSelection(
+ selection_modifier.Selection().AsSelection(),
+ SetSelectionOptions::Builder()
+ .SetSetSelectionBy(SetSelectionBy::kUser)
+ .SetShouldCloseTyping(true)
+ .SetShouldClearTypingStyle(true)
+ .SetCursorAlignOnScroll(alter == SelectionModifyAlteration::kMove
+ ? CursorAlignOnScroll::kAlways
+ : CursorAlignOnScroll::kIfNeeded)
+ .SetIsDirectional(alter == SelectionModifyAlteration::kExtend ||
+ frame.GetEditor()
+ .Behavior()
+ .ShouldConsiderSelectionAsDirectional())
+ .Build());
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveBackward(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kMove,
+ SelectionModifyDirection::kBackward,
+ TextGranularity::kCharacter, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveBackwardAndModifySelection(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kBackward,
+ TextGranularity::kCharacter, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveDown(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ return frame.Selection().Modify(
+ SelectionModifyAlteration::kMove, SelectionModifyDirection::kForward,
+ TextGranularity::kLine, SetSelectionBy::kUser);
+}
+
+bool MoveCommands::ExecuteMoveDownAndModifySelection(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kForward,
+ TextGranularity::kLine, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveForward(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kMove,
+ SelectionModifyDirection::kForward,
+ TextGranularity::kCharacter, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveForwardAndModifySelection(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kForward,
+ TextGranularity::kCharacter, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveLeft(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ return frame.Selection().Modify(
+ SelectionModifyAlteration::kMove, SelectionModifyDirection::kLeft,
+ TextGranularity::kCharacter, SetSelectionBy::kUser);
+}
+
+bool MoveCommands::ExecuteMoveLeftAndModifySelection(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kLeft,
+ TextGranularity::kCharacter, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMovePageDown(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ const unsigned distance = VerticalScrollDistance(frame);
+ if (!distance)
+ return false;
+ return ModifySelectionWithPageGranularity(
+ frame, SelectionModifyAlteration::kMove, distance,
+ SelectionModifyVerticalDirection::kDown);
+}
+
+bool MoveCommands::ExecuteMovePageDownAndModifySelection(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ const unsigned distance = VerticalScrollDistance(frame);
+ if (!distance)
+ return false;
+ return ModifySelectionWithPageGranularity(
+ frame, SelectionModifyAlteration::kExtend, distance,
+ SelectionModifyVerticalDirection::kDown);
+}
+
+bool MoveCommands::ExecuteMovePageUp(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ const unsigned distance = VerticalScrollDistance(frame);
+ if (!distance)
+ return false;
+ return ModifySelectionWithPageGranularity(
+ frame, SelectionModifyAlteration::kMove, distance,
+ SelectionModifyVerticalDirection::kUp);
+}
+
+bool MoveCommands::ExecuteMovePageUpAndModifySelection(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ const unsigned distance = VerticalScrollDistance(frame);
+ if (!distance)
+ return false;
+ return ModifySelectionWithPageGranularity(
+ frame, SelectionModifyAlteration::kExtend, distance,
+ SelectionModifyVerticalDirection::kUp);
+}
+
+bool MoveCommands::ExecuteMoveParagraphBackward(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kMove,
+ SelectionModifyDirection::kBackward,
+ TextGranularity::kParagraph, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveParagraphBackwardAndModifySelection(
+ LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kBackward,
+ TextGranularity::kParagraph, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveParagraphForward(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kMove,
+ SelectionModifyDirection::kForward,
+ TextGranularity::kParagraph, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveParagraphForwardAndModifySelection(
+ LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kForward,
+ TextGranularity::kParagraph, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveRight(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ return frame.Selection().Modify(
+ SelectionModifyAlteration::kMove, SelectionModifyDirection::kRight,
+ TextGranularity::kCharacter, SetSelectionBy::kUser);
+}
+
+bool MoveCommands::ExecuteMoveRightAndModifySelection(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kRight,
+ TextGranularity::kCharacter, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToBeginningOfDocument(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kMove, SelectionModifyDirection::kBackward,
+ TextGranularity::kDocumentBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToBeginningOfDocumentAndModifySelection(
+ LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kExtend, SelectionModifyDirection::kBackward,
+ TextGranularity::kDocumentBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToBeginningOfLine(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kMove, SelectionModifyDirection::kBackward,
+ TextGranularity::kLineBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToBeginningOfLineAndModifySelection(
+ LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kExtend, SelectionModifyDirection::kBackward,
+ TextGranularity::kLineBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToBeginningOfParagraph(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kMove, SelectionModifyDirection::kBackward,
+ TextGranularity::kParagraphBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToBeginningOfParagraphAndModifySelection(
+ LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kExtend, SelectionModifyDirection::kBackward,
+ TextGranularity::kParagraphBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToBeginningOfSentence(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kMove, SelectionModifyDirection::kBackward,
+ TextGranularity::kSentenceBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToBeginningOfSentenceAndModifySelection(
+ LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kExtend, SelectionModifyDirection::kBackward,
+ TextGranularity::kSentenceBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToEndOfDocument(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kMove, SelectionModifyDirection::kForward,
+ TextGranularity::kDocumentBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToEndOfDocumentAndModifySelection(
+ LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kExtend, SelectionModifyDirection::kForward,
+ TextGranularity::kDocumentBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToEndOfLine(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kMove, SelectionModifyDirection::kForward,
+ TextGranularity::kLineBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToEndOfLineAndModifySelection(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kExtend, SelectionModifyDirection::kForward,
+ TextGranularity::kLineBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToEndOfParagraph(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kMove, SelectionModifyDirection::kForward,
+ TextGranularity::kParagraphBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToEndOfParagraphAndModifySelection(
+ LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kExtend, SelectionModifyDirection::kForward,
+ TextGranularity::kParagraphBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToEndOfSentence(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kMove, SelectionModifyDirection::kForward,
+ TextGranularity::kSentenceBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToEndOfSentenceAndModifySelection(
+ LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kExtend, SelectionModifyDirection::kForward,
+ TextGranularity::kSentenceBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToLeftEndOfLine(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kMove, SelectionModifyDirection::kLeft,
+ TextGranularity::kLineBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToLeftEndOfLineAndModifySelection(
+ LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kExtend, SelectionModifyDirection::kLeft,
+ TextGranularity::kLineBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToRightEndOfLine(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kMove, SelectionModifyDirection::kRight,
+ TextGranularity::kLineBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveToRightEndOfLineAndModifySelection(
+ LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(
+ SelectionModifyAlteration::kExtend, SelectionModifyDirection::kRight,
+ TextGranularity::kLineBoundary, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveUp(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ return frame.Selection().Modify(
+ SelectionModifyAlteration::kMove, SelectionModifyDirection::kBackward,
+ TextGranularity::kLine, SetSelectionBy::kUser);
+}
+
+bool MoveCommands::ExecuteMoveUpAndModifySelection(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kBackward,
+ TextGranularity::kLine, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveWordBackward(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kMove,
+ SelectionModifyDirection::kBackward,
+ TextGranularity::kWord, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveWordBackwardAndModifySelection(
+ LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kBackward,
+ TextGranularity::kWord, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveWordForward(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kMove,
+ SelectionModifyDirection::kForward,
+ TextGranularity::kWord, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveWordForwardAndModifySelection(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kForward,
+ TextGranularity::kWord, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveWordLeft(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kMove,
+ SelectionModifyDirection::kLeft,
+ TextGranularity::kWord, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveWordLeftAndModifySelection(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kLeft,
+ TextGranularity::kWord, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveWordRight(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kMove,
+ SelectionModifyDirection::kRight,
+ TextGranularity::kWord, SetSelectionBy::kUser);
+ return true;
+}
+
+bool MoveCommands::ExecuteMoveWordRightAndModifySelection(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ frame.Selection().Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kRight,
+ TextGranularity::kWord, SetSelectionBy::kUser);
+ return true;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/move_commands.h b/chromium/third_party/blink/renderer/core/editing/commands/move_commands.h
new file mode 100644
index 00000000000..4eaf2cc9868
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/move_commands.h
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
+ * Copyright (C) 2009 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_MOVE_COMMANDS_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_MOVE_COMMANDS_H_
+
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+
+namespace blink {
+
+class Event;
+class LocalFrame;
+
+enum class EditorCommandSource;
+enum class SelectionModifyAlteration;
+enum class SelectionModifyVerticalDirection;
+
+// This class provides static functions about commands related to move.
+class MoveCommands {
+ STATIC_ONLY(MoveCommands);
+
+ public:
+ // Returns |bool| value for Document#execCommand().
+ static bool ExecuteMoveBackward(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveBackwardAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveDown(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveDownAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveForward(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveForwardAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveLeft(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveLeftAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMovePageDown(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMovePageDownAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMovePageUp(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMovePageUpAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveParagraphBackward(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveParagraphBackwardAndModifySelection(
+ LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveParagraphForward(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveParagraphForwardAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveRight(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveRightAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToBeginningOfDocument(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToBeginningOfDocumentAndModifySelection(
+ LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToBeginningOfLine(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToBeginningOfLineAndModifySelection(
+ LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToBeginningOfParagraph(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToBeginningOfParagraphAndModifySelection(
+ LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToBeginningOfSentence(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToBeginningOfSentenceAndModifySelection(
+ LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToEndOfDocument(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToEndOfDocumentAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToEndOfLine(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToEndOfLineAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToEndOfParagraph(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToEndOfParagraphAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToEndOfSentence(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToEndOfSentenceAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToLeftEndOfLine(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToLeftEndOfLineAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToRightEndOfLine(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveToRightEndOfLineAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveUp(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveUpAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveWordBackward(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveWordBackwardAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveWordForward(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveWordForwardAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveWordLeft(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveWordLeftAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveWordRight(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMoveWordRightAndModifySelection(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+
+ private:
+ static unsigned VerticalScrollDistance(LocalFrame&);
+
+ // Returns true if selection is modified.
+ static bool ModifySelectionWithPageGranularity(
+ LocalFrame&,
+ SelectionModifyAlteration,
+ unsigned,
+ SelectionModifyVerticalDirection);
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/remove_css_property_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/remove_css_property_command.cc
new file mode 100644
index 00000000000..672d976f89a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/remove_css_property_command.cc
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2005, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/remove_css_property_command.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/css/css_style_declaration.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+
+namespace blink {
+
+RemoveCSSPropertyCommand::RemoveCSSPropertyCommand(Document& document,
+ Element* element,
+ CSSPropertyID property)
+ : SimpleEditCommand(document),
+ element_(element),
+ property_(property),
+ important_(false) {
+ DCHECK(element_);
+}
+
+RemoveCSSPropertyCommand::~RemoveCSSPropertyCommand() = default;
+
+void RemoveCSSPropertyCommand::DoApply(EditingState*) {
+ const CSSPropertyValueSet* style = element_->InlineStyle();
+ if (!style)
+ return;
+
+ old_value_ = style->GetPropertyValue(property_);
+ important_ = style->PropertyIsImportant(property_);
+
+ // Mutate using the CSSOM wrapper so we get the same event behavior as a
+ // script. Setting to null string removes the property. We don't have internal
+ // version of removeProperty.
+ element_->style()->SetPropertyInternal(property_, String(), String(), false,
+ GetDocument().GetSecureContextMode(),
+ IGNORE_EXCEPTION_FOR_TESTING);
+}
+
+void RemoveCSSPropertyCommand::DoUnapply() {
+ element_->style()->SetPropertyInternal(
+ property_, String(), old_value_, important_,
+ GetDocument().GetSecureContextMode(), IGNORE_EXCEPTION_FOR_TESTING);
+}
+
+void RemoveCSSPropertyCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(element_);
+ SimpleEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/remove_css_property_command.h b/chromium/third_party/blink/renderer/core/editing/commands/remove_css_property_command.h
new file mode 100644
index 00000000000..147aff65875
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/remove_css_property_command.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REMOVE_CSS_PROPERTY_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REMOVE_CSS_PROPERTY_COMMAND_H_
+
+#include "third_party/blink/renderer/core/css_property_names.h"
+#include "third_party/blink/renderer/core/editing/commands/edit_command.h"
+
+namespace blink {
+
+class Element;
+
+class RemoveCSSPropertyCommand final : public SimpleEditCommand {
+ public:
+ static RemoveCSSPropertyCommand* Create(Document& document,
+ Element* element,
+ CSSPropertyID property) {
+ return new RemoveCSSPropertyCommand(document, element, property);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ RemoveCSSPropertyCommand(Document&, Element*, CSSPropertyID);
+ ~RemoveCSSPropertyCommand() override;
+
+ void DoApply(EditingState*) override;
+ void DoUnapply() override;
+
+ Member<Element> element_;
+ CSSPropertyID property_;
+ String old_value_;
+ bool important_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REMOVE_CSS_PROPERTY_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/remove_format_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/remove_format_command.cc
new file mode 100644
index 00000000000..115c8a9b37a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/remove_format_command.cc
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2007 Apple Computer, Inc. All rights reserved.
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/remove_format_command.h"
+
+#include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/css_value_keywords.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/editing/commands/apply_style_command.h"
+#include "third_party/blink/renderer/core/editing/editing_style.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/html_names.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+RemoveFormatCommand::RemoveFormatCommand(Document& document)
+ : CompositeEditCommand(document) {}
+
+static bool IsElementForRemoveFormatCommand(const Element* element) {
+ DEFINE_STATIC_LOCAL(
+ HashSet<QualifiedName>, elements,
+ ({
+ acronymTag, bTag, bdoTag, bigTag, citeTag, codeTag,
+ dfnTag, emTag, fontTag, iTag, insTag, kbdTag,
+ nobrTag, qTag, sTag, sampTag, smallTag, strikeTag,
+ strongTag, subTag, supTag, ttTag, uTag, varTag,
+ }));
+ return elements.Contains(element->TagQName());
+}
+
+void RemoveFormatCommand::DoApply(EditingState* editing_state) {
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+
+ // TODO(editing-dev): Stop accessing FrameSelection in edit commands.
+ LocalFrame* frame = GetDocument().GetFrame();
+ const VisibleSelection selection =
+ frame->Selection().ComputeVisibleSelectionInDOMTree();
+ if (selection.IsNone() || !selection.IsValidFor(GetDocument()))
+ return;
+
+ // Get the default style for this editable root, it's the style that we'll
+ // give the content that we're operating on.
+ Element* root = selection.RootEditableElement();
+ EditingStyle* default_style = EditingStyle::Create(root);
+
+ // We want to remove everything but transparent background.
+ // FIXME: We shouldn't access style().
+ default_style->Style()->SetProperty(CSSPropertyBackgroundColor,
+ CSSValueTransparent);
+
+ ApplyCommandToComposite(ApplyStyleCommand::Create(
+ GetDocument(), default_style,
+ IsElementForRemoveFormatCommand, GetInputType()),
+ editing_state);
+}
+
+InputEvent::InputType RemoveFormatCommand::GetInputType() const {
+ return InputEvent::InputType::kFormatRemove;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/remove_format_command.h b/chromium/third_party/blink/renderer/core/editing/commands/remove_format_command.h
new file mode 100644
index 00000000000..a80b70d7753
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/remove_format_command.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REMOVE_FORMAT_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REMOVE_FORMAT_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+namespace blink {
+
+class RemoveFormatCommand final : public CompositeEditCommand {
+ public:
+ static RemoveFormatCommand* Create(Document& document) {
+ return new RemoveFormatCommand(document);
+ }
+
+ private:
+ explicit RemoveFormatCommand(Document&);
+
+ void DoApply(EditingState*) override;
+ InputEvent::InputType GetInputType() const;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REMOVE_FORMAT_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/remove_node_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/remove_node_command.cc
new file mode 100644
index 00000000000..e3428b9fd3d
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/remove_node_command.cc
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2005, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/remove_node_command.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/node.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_state.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+
+namespace blink {
+
+RemoveNodeCommand::RemoveNodeCommand(
+ Node* node,
+ ShouldAssumeContentIsAlwaysEditable
+ should_assume_content_is_always_editable)
+ : SimpleEditCommand(node->GetDocument()),
+ node_(node),
+ should_assume_content_is_always_editable_(
+ should_assume_content_is_always_editable) {
+ DCHECK(node_);
+ DCHECK(node_->parentNode());
+}
+
+void RemoveNodeCommand::DoApply(EditingState* editing_state) {
+ ContainerNode* parent = node_->parentNode();
+ GetDocument().UpdateStyleAndLayoutTree();
+ if (!parent || (should_assume_content_is_always_editable_ ==
+ kDoNotAssumeContentIsAlwaysEditable &&
+ !HasEditableStyle(*parent) && parent->InActiveDocument()))
+ return;
+ DCHECK(HasEditableStyle(*parent) || !parent->InActiveDocument()) << parent;
+
+ parent_ = parent;
+ ref_child_ = node_->nextSibling();
+
+ node_->remove(IGNORE_EXCEPTION_FOR_TESTING);
+ // Node::remove dispatch synchronous events such as IFRAME unload events,
+ // and event handlers may break the document. We check the document state
+ // here in order to prevent further processing in bad situation.
+ ABORT_EDITING_COMMAND_IF(!node_->GetDocument().GetFrame());
+ ABORT_EDITING_COMMAND_IF(!node_->GetDocument().documentElement());
+}
+
+void RemoveNodeCommand::DoUnapply() {
+ ContainerNode* parent = parent_.Release();
+ Node* ref_child = ref_child_.Release();
+ if (!parent || !HasEditableStyle(*parent))
+ return;
+
+ parent->InsertBefore(node_.Get(), ref_child, IGNORE_EXCEPTION_FOR_TESTING);
+}
+
+void RemoveNodeCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(node_);
+ visitor->Trace(parent_);
+ visitor->Trace(ref_child_);
+ SimpleEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/remove_node_command.h b/chromium/third_party/blink/renderer/core/editing/commands/remove_node_command.h
new file mode 100644
index 00000000000..e1e35943616
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/remove_node_command.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REMOVE_NODE_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REMOVE_NODE_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/edit_command.h"
+
+namespace blink {
+
+class RemoveNodeCommand final : public SimpleEditCommand {
+ public:
+ static RemoveNodeCommand* Create(
+ Node* node,
+ ShouldAssumeContentIsAlwaysEditable
+ should_assume_content_is_always_editable) {
+ return new RemoveNodeCommand(node,
+ should_assume_content_is_always_editable);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ explicit RemoveNodeCommand(Node*, ShouldAssumeContentIsAlwaysEditable);
+
+ void DoApply(EditingState*) override;
+ void DoUnapply() override;
+
+ Member<Node> node_;
+ Member<ContainerNode> parent_;
+ Member<Node> ref_child_;
+ ShouldAssumeContentIsAlwaysEditable should_assume_content_is_always_editable_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REMOVE_NODE_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/remove_node_preserving_children_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/remove_node_preserving_children_command.cc
new file mode 100644
index 00000000000..02fa23df3e5
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/remove_node_preserving_children_command.cc
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2005, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/remove_node_preserving_children_command.h"
+
+#include "third_party/blink/renderer/core/dom/node.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+
+namespace blink {
+
+RemoveNodePreservingChildrenCommand::RemoveNodePreservingChildrenCommand(
+ Node* node,
+ ShouldAssumeContentIsAlwaysEditable
+ should_assume_content_is_always_editable)
+ : CompositeEditCommand(node->GetDocument()),
+ node_(node),
+ should_assume_content_is_always_editable_(
+ should_assume_content_is_always_editable) {
+ DCHECK(node_);
+}
+
+void RemoveNodePreservingChildrenCommand::DoApply(EditingState* editing_state) {
+ ABORT_EDITING_COMMAND_IF(!node_->parentNode());
+ ABORT_EDITING_COMMAND_IF(!HasEditableStyle(*node_->parentNode()));
+ if (node_->IsContainerNode()) {
+ NodeVector children;
+ GetChildNodes(ToContainerNode(*node_), children);
+
+ for (auto& current_child : children) {
+ Node* child = current_child.Release();
+ RemoveNode(child, editing_state,
+ should_assume_content_is_always_editable_);
+ if (editing_state->IsAborted())
+ return;
+ InsertNodeBefore(child, node_, editing_state,
+ should_assume_content_is_always_editable_);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+ RemoveNode(node_, editing_state, should_assume_content_is_always_editable_);
+}
+
+void RemoveNodePreservingChildrenCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(node_);
+ CompositeEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/remove_node_preserving_children_command.h b/chromium/third_party/blink/renderer/core/editing/commands/remove_node_preserving_children_command.h
new file mode 100644
index 00000000000..db09acda237
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/remove_node_preserving_children_command.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REMOVE_NODE_PRESERVING_CHILDREN_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REMOVE_NODE_PRESERVING_CHILDREN_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+namespace blink {
+
+class RemoveNodePreservingChildrenCommand final : public CompositeEditCommand {
+ public:
+ static RemoveNodePreservingChildrenCommand* Create(
+ Node* node,
+ ShouldAssumeContentIsAlwaysEditable
+ should_assume_content_is_always_editable) {
+ return new RemoveNodePreservingChildrenCommand(
+ node, should_assume_content_is_always_editable);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ RemoveNodePreservingChildrenCommand(Node*,
+ ShouldAssumeContentIsAlwaysEditable);
+
+ void DoApply(EditingState*) override;
+
+ Member<Node> node_;
+ ShouldAssumeContentIsAlwaysEditable should_assume_content_is_always_editable_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REMOVE_NODE_PRESERVING_CHILDREN_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/replace_node_with_span_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/replace_node_with_span_command.cc
new file mode 100644
index 00000000000..0d3f21eb136
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/replace_node_with_span_command.cc
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2009 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/replace_node_with_span_command.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/html/html_span_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+ReplaceNodeWithSpanCommand::ReplaceNodeWithSpanCommand(HTMLElement* element)
+ : SimpleEditCommand(element->GetDocument()), element_to_replace_(element) {
+ DCHECK(element_to_replace_);
+}
+
+static void SwapInNodePreservingAttributesAndChildren(
+ HTMLElement* new_element,
+ HTMLElement& element_to_replace) {
+ DCHECK(element_to_replace.isConnected()) << element_to_replace;
+ ContainerNode* parent_node = element_to_replace.parentNode();
+ parent_node->InsertBefore(new_element, &element_to_replace);
+
+ NodeVector children;
+ GetChildNodes(element_to_replace, children);
+ for (const auto& child : children)
+ new_element->AppendChild(child);
+
+ // FIXME: Fix this to send the proper MutationRecords when MutationObservers
+ // are present.
+ new_element->CloneAttributesFrom(element_to_replace);
+
+ parent_node->RemoveChild(&element_to_replace, ASSERT_NO_EXCEPTION);
+}
+
+void ReplaceNodeWithSpanCommand::DoApply(EditingState*) {
+ if (!element_to_replace_->isConnected())
+ return;
+ if (!span_element_)
+ span_element_ = HTMLSpanElement::Create(element_to_replace_->GetDocument());
+ SwapInNodePreservingAttributesAndChildren(span_element_.Get(),
+ *element_to_replace_);
+}
+
+void ReplaceNodeWithSpanCommand::DoUnapply() {
+ if (!span_element_->isConnected())
+ return;
+ SwapInNodePreservingAttributesAndChildren(element_to_replace_.Get(),
+ *span_element_);
+}
+
+void ReplaceNodeWithSpanCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(element_to_replace_);
+ visitor->Trace(span_element_);
+ SimpleEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/replace_node_with_span_command.h b/chromium/third_party/blink/renderer/core/editing/commands/replace_node_with_span_command.h
new file mode 100644
index 00000000000..43bc31a4e53
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/replace_node_with_span_command.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2009 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REPLACE_NODE_WITH_SPAN_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REPLACE_NODE_WITH_SPAN_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+namespace blink {
+
+class HTMLElement;
+class HTMLSpanElement;
+
+// More accurately, this is
+// ReplaceElementWithSpanPreservingChildrenAndAttributesCommand
+class ReplaceNodeWithSpanCommand final : public SimpleEditCommand {
+ public:
+ static ReplaceNodeWithSpanCommand* Create(HTMLElement* element) {
+ return new ReplaceNodeWithSpanCommand(element);
+ }
+
+ HTMLSpanElement* SpanElement() { return span_element_.Get(); }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ explicit ReplaceNodeWithSpanCommand(HTMLElement*);
+
+ void DoApply(EditingState*) override;
+ void DoUnapply() override;
+
+ Member<HTMLElement> element_to_replace_;
+ Member<HTMLSpanElement> span_element_;
+};
+
+} // namespace blink
+
+#endif // ReplaceNodeWithSpanCommand
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/replace_selection_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/replace_selection_command.cc
new file mode 100644
index 00000000000..3be3469dcd6
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/replace_selection_command.cc
@@ -0,0 +1,2071 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2009, 2010, 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/replace_selection_command.h"
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/css/css_style_declaration.h"
+#include "third_party/blink/renderer/core/css_property_names.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/document_fragment.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/commands/apply_style_command.h"
+#include "third_party/blink/renderer/core/editing/commands/break_blockquote_command.h"
+#include "third_party/blink/renderer/core/editing/commands/delete_selection_options.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/commands/simplify_markup_command.h"
+#include "third_party/blink/renderer/core/editing/commands/smart_replace.h"
+#include "third_party/blink/renderer/core/editing/editing_style.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/serializers/html_interchange.h"
+#include "third_party/blink/renderer/core/editing/serializers/serialization.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/events/before_text_inserted_event.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/use_counter.h"
+#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
+#include "third_party/blink/renderer/core/html/forms/html_select_element.h"
+#include "third_party/blink/renderer/core/html/html_br_element.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/html/html_li_element.h"
+#include "third_party/blink/renderer/core/html/html_quote_element.h"
+#include "third_party/blink/renderer/core/html/html_span_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/input_type_names.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/core/layout/layout_text.h"
+#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
+#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
+#include "third_party/blink/renderer/platform/wtf/vector.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+enum EFragmentType { kEmptyFragment, kSingleTextNodeFragment, kTreeFragment };
+
+// --- ReplacementFragment helper class
+
+class ReplacementFragment final {
+ STACK_ALLOCATED();
+
+ public:
+ ReplacementFragment(Document*, DocumentFragment*, const VisibleSelection&);
+
+ Node* FirstChild() const;
+ Node* LastChild() const;
+
+ bool IsEmpty() const;
+
+ bool HasInterchangeNewlineAtStart() const {
+ return has_interchange_newline_at_start_;
+ }
+ bool HasInterchangeNewlineAtEnd() const {
+ return has_interchange_newline_at_end_;
+ }
+
+ void RemoveNode(Node*);
+ void RemoveNodePreservingChildren(ContainerNode*);
+
+ private:
+ HTMLElement* InsertFragmentForTestRendering(Element* root_editable_element);
+ void RemoveUnrenderedNodes(ContainerNode*);
+ void RestoreAndRemoveTestRenderingNodesToFragment(Element*);
+ void RemoveInterchangeNodes(ContainerNode*);
+
+ void InsertNodeBefore(Node*, Node* ref_node);
+
+ Member<Document> document_;
+ Member<DocumentFragment> fragment_;
+ bool has_interchange_newline_at_start_;
+ bool has_interchange_newline_at_end_;
+
+ DISALLOW_COPY_AND_ASSIGN(ReplacementFragment);
+};
+
+static bool IsInterchangeHTMLBRElement(const Node* node) {
+ DEFINE_STATIC_LOCAL(String, interchange_newline_class_string,
+ (AppleInterchangeNewline));
+ if (!IsHTMLBRElement(node) ||
+ ToHTMLBRElement(node)->getAttribute(classAttr) !=
+ interchange_newline_class_string)
+ return false;
+ UseCounter::Count(node->GetDocument(),
+ WebFeature::kEditingAppleInterchangeNewline);
+ return true;
+}
+
+static Position PositionAvoidingPrecedingNodes(Position pos) {
+ // If we're already on a break, it's probably a placeholder and we shouldn't
+ // change our position.
+ if (EditingIgnoresContent(*pos.AnchorNode()))
+ return pos;
+
+ // We also stop when changing block flow elements because even though the
+ // visual position is the same. E.g.,
+ // <div>foo^</div>^
+ // The two positions above are the same visual position, but we want to stay
+ // in the same block.
+ Element* enclosing_block_element = EnclosingBlock(pos.ComputeContainerNode());
+ for (Position next_position = pos;
+ next_position.ComputeContainerNode() != enclosing_block_element;
+ pos = next_position) {
+ if (LineBreakExistsAtPosition(pos))
+ break;
+
+ if (pos.ComputeContainerNode()->NonShadowBoundaryParentNode())
+ next_position = Position::InParentAfterNode(*pos.ComputeContainerNode());
+
+ if (next_position == pos ||
+ EnclosingBlock(next_position.ComputeContainerNode()) !=
+ enclosing_block_element ||
+ CreateVisiblePosition(pos).DeepEquivalent() !=
+ CreateVisiblePosition(next_position).DeepEquivalent())
+ break;
+ }
+ return pos;
+}
+
+ReplacementFragment::ReplacementFragment(Document* document,
+ DocumentFragment* fragment,
+ const VisibleSelection& selection)
+ : document_(document),
+ fragment_(fragment),
+ has_interchange_newline_at_start_(false),
+ has_interchange_newline_at_end_(false) {
+ if (!document_)
+ return;
+ if (!fragment_ || !fragment_->HasChildren())
+ return;
+
+ TRACE_EVENT0("blink", "ReplacementFragment constructor");
+ Element* editable_root = selection.RootEditableElement();
+ DCHECK(editable_root);
+ if (!editable_root)
+ return;
+
+ document_->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ Element* shadow_ancestor_element;
+ if (editable_root->IsInShadowTree())
+ shadow_ancestor_element = editable_root->OwnerShadowHost();
+ else
+ shadow_ancestor_element = editable_root;
+
+ if (!editable_root->GetAttributeEventListener(
+ EventTypeNames::webkitBeforeTextInserted)
+ // FIXME: Remove these checks once textareas and textfields actually
+ // register an event handler.
+ &&
+ !(shadow_ancestor_element && shadow_ancestor_element->GetLayoutObject() &&
+ shadow_ancestor_element->GetLayoutObject()->IsTextControl()) &&
+ HasRichlyEditableStyle(*editable_root)) {
+ RemoveInterchangeNodes(fragment_.Get());
+ return;
+ }
+
+ if (!HasRichlyEditableStyle(*editable_root)) {
+ bool is_plain_text = true;
+ for (Node& node : NodeTraversal::ChildrenOf(*fragment_)) {
+ if (IsInterchangeHTMLBRElement(&node) && &node == fragment_->lastChild())
+ continue;
+ if (!node.IsTextNode()) {
+ is_plain_text = false;
+ break;
+ }
+ }
+ // We don't need TestRendering for plain-text editing + plain-text
+ // insertion.
+ if (is_plain_text) {
+ RemoveInterchangeNodes(fragment_.Get());
+ String original_text = fragment_->textContent();
+ BeforeTextInsertedEvent* event =
+ BeforeTextInsertedEvent::Create(original_text);
+ editable_root->DispatchEvent(event);
+ if (original_text != event->GetText()) {
+ fragment_ = CreateFragmentFromText(
+ selection.ToNormalizedEphemeralRange(), event->GetText());
+ RemoveInterchangeNodes(fragment_.Get());
+ }
+ return;
+ }
+ }
+
+ HTMLElement* holder = InsertFragmentForTestRendering(editable_root);
+ if (!holder) {
+ RemoveInterchangeNodes(fragment_.Get());
+ return;
+ }
+
+ const EphemeralRange range =
+ CreateVisibleSelection(
+ SelectionInDOMTree::Builder().SelectAllChildren(*holder).Build())
+ .ToNormalizedEphemeralRange();
+ const TextIteratorBehavior& behavior = TextIteratorBehavior::Builder()
+ .SetEmitsOriginalText(true)
+ .SetIgnoresStyleVisibility(true)
+ .Build();
+ const String& text = PlainText(range, behavior);
+
+ RemoveInterchangeNodes(holder);
+ RemoveUnrenderedNodes(holder);
+ RestoreAndRemoveTestRenderingNodesToFragment(holder);
+
+ // Give the root a chance to change the text.
+ BeforeTextInsertedEvent* evt = BeforeTextInsertedEvent::Create(text);
+ editable_root->DispatchEvent(evt);
+ if (text != evt->GetText() || !HasRichlyEditableStyle(*editable_root)) {
+ RestoreAndRemoveTestRenderingNodesToFragment(holder);
+
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ document->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ fragment_ = CreateFragmentFromText(selection.ToNormalizedEphemeralRange(),
+ evt->GetText());
+ if (!fragment_->HasChildren())
+ return;
+
+ holder = InsertFragmentForTestRendering(editable_root);
+ RemoveInterchangeNodes(holder);
+ RemoveUnrenderedNodes(holder);
+ RestoreAndRemoveTestRenderingNodesToFragment(holder);
+ }
+}
+
+bool ReplacementFragment::IsEmpty() const {
+ return (!fragment_ || !fragment_->HasChildren()) &&
+ !has_interchange_newline_at_start_ && !has_interchange_newline_at_end_;
+}
+
+Node* ReplacementFragment::FirstChild() const {
+ return fragment_ ? fragment_->firstChild() : nullptr;
+}
+
+Node* ReplacementFragment::LastChild() const {
+ return fragment_ ? fragment_->lastChild() : nullptr;
+}
+
+void ReplacementFragment::RemoveNodePreservingChildren(ContainerNode* node) {
+ if (!node)
+ return;
+
+ while (Node* n = node->firstChild()) {
+ RemoveNode(n);
+ InsertNodeBefore(n, node);
+ }
+ RemoveNode(node);
+}
+
+void ReplacementFragment::RemoveNode(Node* node) {
+ if (!node)
+ return;
+
+ ContainerNode* parent = node->NonShadowBoundaryParentNode();
+ if (!parent)
+ return;
+
+ parent->RemoveChild(node);
+}
+
+void ReplacementFragment::InsertNodeBefore(Node* node, Node* ref_node) {
+ if (!node || !ref_node)
+ return;
+
+ ContainerNode* parent = ref_node->NonShadowBoundaryParentNode();
+ if (!parent)
+ return;
+
+ parent->InsertBefore(node, ref_node);
+}
+
+HTMLElement* ReplacementFragment::InsertFragmentForTestRendering(
+ Element* root_editable_element) {
+ TRACE_EVENT0("blink", "ReplacementFragment::insertFragmentForTestRendering");
+ DCHECK(document_);
+ HTMLElement* holder = CreateDefaultParagraphElement(*document_.Get());
+
+ holder->AppendChild(fragment_);
+ root_editable_element->AppendChild(holder);
+
+ // TODO(editing-dev): Hoist this call to the call sites.
+ document_->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ return holder;
+}
+
+void ReplacementFragment::RestoreAndRemoveTestRenderingNodesToFragment(
+ Element* holder) {
+ if (!holder)
+ return;
+
+ while (Node* node = holder->firstChild()) {
+ holder->RemoveChild(node);
+ fragment_->AppendChild(node);
+ }
+
+ RemoveNode(holder);
+}
+
+void ReplacementFragment::RemoveUnrenderedNodes(ContainerNode* holder) {
+ HeapVector<Member<Node>> unrendered;
+
+ for (Node& node : NodeTraversal::DescendantsOf(*holder)) {
+ if (!IsNodeRendered(node) && !IsTableStructureNode(&node))
+ unrendered.push_back(&node);
+ }
+
+ for (auto& node : unrendered)
+ RemoveNode(node);
+}
+
+void ReplacementFragment::RemoveInterchangeNodes(ContainerNode* container) {
+ has_interchange_newline_at_start_ = false;
+ has_interchange_newline_at_end_ = false;
+
+ // Interchange newlines at the "start" of the incoming fragment must be
+ // either the first node in the fragment or the first leaf in the fragment.
+ Node* node = container->firstChild();
+ while (node) {
+ if (IsInterchangeHTMLBRElement(node)) {
+ has_interchange_newline_at_start_ = true;
+ RemoveNode(node);
+ break;
+ }
+ node = node->firstChild();
+ }
+ if (!container->HasChildren())
+ return;
+ // Interchange newlines at the "end" of the incoming fragment must be
+ // either the last node in the fragment or the last leaf in the fragment.
+ node = container->lastChild();
+ while (node) {
+ if (IsInterchangeHTMLBRElement(node)) {
+ has_interchange_newline_at_end_ = true;
+ RemoveNode(node);
+ break;
+ }
+ node = node->lastChild();
+ }
+}
+
+inline void ReplaceSelectionCommand::InsertedNodes::RespondToNodeInsertion(
+ Node& node) {
+ if (!first_node_inserted_)
+ first_node_inserted_ = &node;
+
+ last_node_inserted_ = &node;
+}
+
+inline void
+ReplaceSelectionCommand::InsertedNodes::WillRemoveNodePreservingChildren(
+ Node& node) {
+ if (first_node_inserted_.Get() == node)
+ first_node_inserted_ = NodeTraversal::Next(node);
+ if (last_node_inserted_.Get() == node)
+ last_node_inserted_ = node.lastChild()
+ ? node.lastChild()
+ : NodeTraversal::NextSkippingChildren(node);
+ if (ref_node_.Get() == node)
+ ref_node_ = NodeTraversal::Next(node);
+}
+
+inline void ReplaceSelectionCommand::InsertedNodes::WillRemoveNode(Node& node) {
+ if (first_node_inserted_.Get() == node && last_node_inserted_.Get() == node) {
+ first_node_inserted_ = nullptr;
+ last_node_inserted_ = nullptr;
+ } else if (first_node_inserted_.Get() == node) {
+ first_node_inserted_ =
+ NodeTraversal::NextSkippingChildren(*first_node_inserted_);
+ } else if (last_node_inserted_.Get() == node) {
+ last_node_inserted_ =
+ NodeTraversal::PreviousSkippingChildren(*last_node_inserted_);
+ }
+ if (node.contains(ref_node_))
+ ref_node_ = NodeTraversal::NextSkippingChildren(node);
+}
+
+inline void ReplaceSelectionCommand::InsertedNodes::DidReplaceNode(
+ Node& node,
+ Node& new_node) {
+ if (first_node_inserted_.Get() == node)
+ first_node_inserted_ = &new_node;
+ if (last_node_inserted_.Get() == node)
+ last_node_inserted_ = &new_node;
+ if (ref_node_.Get() == node)
+ ref_node_ = &new_node;
+}
+
+ReplaceSelectionCommand::ReplaceSelectionCommand(
+ Document& document,
+ DocumentFragment* fragment,
+ CommandOptions options,
+ InputEvent::InputType input_type)
+ : CompositeEditCommand(document),
+ select_replacement_(options & kSelectReplacement),
+ smart_replace_(options & kSmartReplace),
+ match_style_(options & kMatchStyle),
+ document_fragment_(fragment),
+ prevent_nesting_(options & kPreventNesting),
+ moving_paragraph_(options & kMovingParagraph),
+ input_type_(input_type),
+ sanitize_fragment_(options & kSanitizeFragment),
+ should_merge_end_(false) {}
+
+static bool HasMatchingQuoteLevel(VisiblePosition end_of_existing_content,
+ VisiblePosition end_of_inserted_content) {
+ Position existing = end_of_existing_content.DeepEquivalent();
+ Position inserted = end_of_inserted_content.DeepEquivalent();
+ bool is_inside_mail_blockquote = EnclosingNodeOfType(
+ inserted, IsMailHTMLBlockquoteElement, kCanCrossEditingBoundary);
+ return is_inside_mail_blockquote && (NumEnclosingMailBlockquotes(existing) ==
+ NumEnclosingMailBlockquotes(inserted));
+}
+
+bool ReplaceSelectionCommand::ShouldMergeStart(
+ bool selection_start_was_start_of_paragraph,
+ bool fragment_has_interchange_newline_at_start,
+ bool selection_start_was_inside_mail_blockquote) {
+ if (moving_paragraph_)
+ return false;
+
+ VisiblePosition start_of_inserted_content =
+ PositionAtStartOfInsertedContent();
+ VisiblePosition prev = PreviousPositionOf(start_of_inserted_content,
+ kCannotCrossEditingBoundary);
+ if (prev.IsNull())
+ return false;
+
+ // When we have matching quote levels, its ok to merge more frequently.
+ // For a successful merge, we still need to make sure that the inserted
+ // content starts with the beginning of a paragraph. And we should only merge
+ // here if the selection start was inside a mail blockquote. This prevents
+ // against removing a blockquote from newly pasted quoted content that was
+ // pasted into an unquoted position. If that unquoted position happens to be
+ // right after another blockquote, we don't want to merge and risk stripping a
+ // valid block (and newline) from the pasted content.
+ if (IsStartOfParagraph(start_of_inserted_content) &&
+ selection_start_was_inside_mail_blockquote &&
+ HasMatchingQuoteLevel(prev, PositionAtEndOfInsertedContent()))
+ return true;
+
+ return !selection_start_was_start_of_paragraph &&
+ !fragment_has_interchange_newline_at_start &&
+ IsStartOfParagraph(start_of_inserted_content) &&
+ !IsHTMLBRElement(
+ *start_of_inserted_content.DeepEquivalent().AnchorNode()) &&
+ ShouldMerge(start_of_inserted_content, prev);
+}
+
+bool ReplaceSelectionCommand::ShouldMergeEnd(
+ bool selection_end_was_end_of_paragraph) {
+ VisiblePosition end_of_inserted_content(PositionAtEndOfInsertedContent());
+ VisiblePosition next =
+ NextPositionOf(end_of_inserted_content, kCannotCrossEditingBoundary);
+ if (next.IsNull())
+ return false;
+
+ return !selection_end_was_end_of_paragraph &&
+ IsEndOfParagraph(end_of_inserted_content) &&
+ !IsHTMLBRElement(
+ *end_of_inserted_content.DeepEquivalent().AnchorNode()) &&
+ ShouldMerge(end_of_inserted_content, next);
+}
+
+static bool IsHTMLHeaderElement(const Node* a) {
+ if (!a || !a->IsHTMLElement())
+ return false;
+
+ const HTMLElement& element = ToHTMLElement(*a);
+ return element.HasTagName(h1Tag) || element.HasTagName(h2Tag) ||
+ element.HasTagName(h3Tag) || element.HasTagName(h4Tag) ||
+ element.HasTagName(h5Tag) || element.HasTagName(h6Tag);
+}
+
+static bool HaveSameTagName(Element* a, Element* b) {
+ return a && b && a->tagName() == b->tagName();
+}
+
+bool ReplaceSelectionCommand::ShouldMerge(const VisiblePosition& source,
+ const VisiblePosition& destination) {
+ if (source.IsNull() || destination.IsNull())
+ return false;
+
+ Node* source_node = source.DeepEquivalent().AnchorNode();
+ Node* destination_node = destination.DeepEquivalent().AnchorNode();
+ Element* source_block = EnclosingBlock(source_node);
+ Element* destination_block = EnclosingBlock(destination_node);
+ return source_block &&
+ (!source_block->HasTagName(blockquoteTag) ||
+ IsMailHTMLBlockquoteElement(source_block)) &&
+ EnclosingListChild(source_block) ==
+ EnclosingListChild(destination_node) &&
+ EnclosingTableCell(source.DeepEquivalent()) ==
+ EnclosingTableCell(destination.DeepEquivalent()) &&
+ (!IsHTMLHeaderElement(source_block) ||
+ HaveSameTagName(source_block, destination_block))
+ // Don't merge to or from a position before or after a block because it
+ // would be a no-op and cause infinite recursion.
+ && !IsEnclosingBlock(source_node) &&
+ !IsEnclosingBlock(destination_node);
+}
+
+// Style rules that match just inserted elements could change their appearance,
+// like a div inserted into a document with div { display:inline; }.
+void ReplaceSelectionCommand::RemoveRedundantStylesAndKeepStyleSpanInline(
+ InsertedNodes& inserted_nodes,
+ EditingState* editing_state) {
+ Node* past_end_node = inserted_nodes.PastLastLeaf();
+ Node* next = nullptr;
+ for (Node* node = inserted_nodes.FirstNodeInserted();
+ node && node != past_end_node; node = next) {
+ // FIXME: <rdar://problem/5371536> Style rules that match pasted content can
+ // change it's appearance
+
+ next = NodeTraversal::Next(*node);
+ if (!node->IsStyledElement())
+ continue;
+
+ Element* element = ToElement(node);
+
+ const CSSPropertyValueSet* inline_style = element->InlineStyle();
+ EditingStyle* new_inline_style = EditingStyle::Create(inline_style);
+ if (inline_style) {
+ if (element->IsHTMLElement()) {
+ Vector<QualifiedName> attributes;
+ HTMLElement* html_element = ToHTMLElement(element);
+ DCHECK(html_element);
+
+ if (new_inline_style->ConflictsWithImplicitStyleOfElement(
+ html_element)) {
+ // e.g. <b style="font-weight: normal;"> is converted to <span
+ // style="font-weight: normal;">
+ element = ReplaceElementWithSpanPreservingChildrenAndAttributes(
+ html_element);
+ inline_style = element->InlineStyle();
+ inserted_nodes.DidReplaceNode(*html_element, *element);
+ } else if (new_inline_style
+ ->ExtractConflictingImplicitStyleOfAttributes(
+ html_element,
+ EditingStyle::kPreserveWritingDirection, nullptr,
+ attributes,
+ EditingStyle::kDoNotExtractMatchingStyle)) {
+ // e.g. <font size="3" style="font-size: 20px;"> is converted to <font
+ // style="font-size: 20px;">
+ for (size_t i = 0; i < attributes.size(); i++)
+ RemoveElementAttribute(html_element, attributes[i]);
+ }
+ }
+
+ ContainerNode* context = element->parentNode();
+
+ // If Mail wraps the fragment with a Paste as Quotation blockquote, or if
+ // you're pasting into a quoted region, styles from blockquoteNode are
+ // allowed to override those from the source document, see
+ // <rdar://problem/4930986> and <rdar://problem/5089327>.
+ HTMLQuoteElement* blockquote_element =
+ !context
+ ? ToHTMLQuoteElement(context)
+ : ToHTMLQuoteElement(EnclosingNodeOfType(
+ Position::FirstPositionInNode(*context),
+ IsMailHTMLBlockquoteElement, kCanCrossEditingBoundary));
+
+ // EditingStyle::removeStyleFromRulesAndContext() uses StyleResolver,
+ // which requires clean style.
+ // TODO(editing-dev): There is currently no way to update style without
+ // updating layout. We might want to have updateLifcycleToStyleClean()
+ // similar to FrameView::updateLifecylceToLayoutClean() in Document.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (blockquote_element)
+ new_inline_style->RemoveStyleFromRulesAndContext(
+ element, GetDocument().documentElement());
+
+ new_inline_style->RemoveStyleFromRulesAndContext(element, context);
+ }
+
+ if (!inline_style || new_inline_style->IsEmpty()) {
+ if (IsStyleSpanOrSpanWithOnlyStyleAttribute(element) ||
+ IsEmptyFontTag(element, kAllowNonEmptyStyleAttribute)) {
+ inserted_nodes.WillRemoveNodePreservingChildren(*element);
+ RemoveNodePreservingChildren(element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ continue;
+ }
+ RemoveElementAttribute(element, styleAttr);
+ } else if (new_inline_style->Style()->PropertyCount() !=
+ inline_style->PropertyCount()) {
+ SetNodeAttribute(element, styleAttr,
+ AtomicString(new_inline_style->Style()->AsText()));
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // FIXME: Tolerate differences in id, class, and style attributes.
+ if (element->parentNode() && IsNonTableCellHTMLBlockElement(element) &&
+ AreIdenticalElements(*element, *element->parentNode()) &&
+ VisiblePosition::FirstPositionInNode(*element->parentNode())
+ .DeepEquivalent() ==
+ VisiblePosition::FirstPositionInNode(*element).DeepEquivalent() &&
+ VisiblePosition::LastPositionInNode(*element->parentNode())
+ .DeepEquivalent() ==
+ VisiblePosition::LastPositionInNode(*element).DeepEquivalent()) {
+ inserted_nodes.WillRemoveNodePreservingChildren(*element);
+ RemoveNodePreservingChildren(element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ continue;
+ }
+
+ if (element->parentNode() &&
+ HasRichlyEditableStyle(*element->parentNode()) &&
+ HasRichlyEditableStyle(*element)) {
+ RemoveElementAttribute(element, contenteditableAttr);
+ }
+ }
+}
+
+static bool IsProhibitedParagraphChild(const AtomicString& name) {
+ // https://dvcs.w3.org/hg/editing/raw-file/57abe6d3cb60/editing.html#prohibited-paragraph-child
+ DEFINE_STATIC_LOCAL(HashSet<AtomicString>, elements,
+ ({
+ addressTag.LocalName(), articleTag.LocalName(),
+ asideTag.LocalName(), blockquoteTag.LocalName(),
+ captionTag.LocalName(), centerTag.LocalName(),
+ colTag.LocalName(), colgroupTag.LocalName(),
+ ddTag.LocalName(), detailsTag.LocalName(),
+ dirTag.LocalName(), divTag.LocalName(),
+ dlTag.LocalName(), dtTag.LocalName(),
+ fieldsetTag.LocalName(), figcaptionTag.LocalName(),
+ figureTag.LocalName(), footerTag.LocalName(),
+ formTag.LocalName(), h1Tag.LocalName(),
+ h2Tag.LocalName(), h3Tag.LocalName(),
+ h4Tag.LocalName(), h5Tag.LocalName(),
+ h6Tag.LocalName(), headerTag.LocalName(),
+ hgroupTag.LocalName(), hrTag.LocalName(),
+ liTag.LocalName(), listingTag.LocalName(),
+ mainTag.LocalName(), // Missing in the specification.
+ menuTag.LocalName(), navTag.LocalName(),
+ olTag.LocalName(), pTag.LocalName(),
+ plaintextTag.LocalName(), preTag.LocalName(),
+ sectionTag.LocalName(), summaryTag.LocalName(),
+ tableTag.LocalName(), tbodyTag.LocalName(),
+ tdTag.LocalName(), tfootTag.LocalName(),
+ thTag.LocalName(), theadTag.LocalName(),
+ trTag.LocalName(), ulTag.LocalName(),
+ xmpTag.LocalName(),
+ }));
+ return elements.Contains(name);
+}
+
+void ReplaceSelectionCommand::
+ MakeInsertedContentRoundTrippableWithHTMLTreeBuilder(
+ const InsertedNodes& inserted_nodes,
+ EditingState* editing_state) {
+ Node* past_end_node = inserted_nodes.PastLastLeaf();
+ Node* next = nullptr;
+ for (Node* node = inserted_nodes.FirstNodeInserted();
+ node && node != past_end_node; node = next) {
+ next = NodeTraversal::Next(*node);
+
+ if (!node->IsHTMLElement())
+ continue;
+ // moveElementOutOfAncestor() in a previous iteration might have failed,
+ // and |node| might have been detached from the document tree.
+ if (!node->isConnected())
+ continue;
+
+ HTMLElement& element = ToHTMLElement(*node);
+ if (IsProhibitedParagraphChild(element.localName())) {
+ if (HTMLElement* paragraph_element =
+ ToHTMLElement(EnclosingElementWithTag(
+ Position::InParentBeforeNode(element), pTag))) {
+ MoveElementOutOfAncestor(&element, paragraph_element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+
+ if (IsHTMLHeaderElement(&element)) {
+ if (HTMLElement* header_element = ToHTMLElement(
+ HighestEnclosingNodeOfType(Position::InParentBeforeNode(element),
+ IsHTMLHeaderElement))) {
+ MoveElementOutOfAncestor(&element, header_element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+ }
+}
+
+void ReplaceSelectionCommand::MoveElementOutOfAncestor(
+ Element* element,
+ Element* ancestor,
+ EditingState* editing_state) {
+ DCHECK(element);
+ if (!HasEditableStyle(*ancestor->parentNode()))
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ VisiblePosition position_at_end_of_node =
+ CreateVisiblePosition(LastPositionInOrAfterNode(*element));
+ VisiblePosition last_position_in_paragraph =
+ VisiblePosition::LastPositionInNode(*ancestor);
+ if (position_at_end_of_node.DeepEquivalent() ==
+ last_position_in_paragraph.DeepEquivalent()) {
+ RemoveNode(element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ if (ancestor->nextSibling())
+ InsertNodeBefore(element, ancestor->nextSibling(), editing_state);
+ else
+ AppendNode(element, ancestor->parentNode(), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ } else {
+ Node* node_to_split_to = SplitTreeToNode(element, ancestor, true);
+ RemoveNode(element, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ InsertNodeBefore(element, node_to_split_to, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ if (!ancestor->HasChildren())
+ RemoveNode(ancestor, editing_state);
+}
+
+static inline bool NodeHasVisibleLayoutText(Text& text) {
+ return text.GetLayoutObject() &&
+ text.GetLayoutObject()->ResolvedTextLength() > 0;
+}
+
+void ReplaceSelectionCommand::RemoveUnrenderedTextNodesAtEnds(
+ InsertedNodes& inserted_nodes) {
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ Node* last_leaf_inserted = inserted_nodes.LastLeafInserted();
+ if (last_leaf_inserted && last_leaf_inserted->IsTextNode() &&
+ !NodeHasVisibleLayoutText(ToText(*last_leaf_inserted)) &&
+ !EnclosingElementWithTag(FirstPositionInOrBeforeNode(*last_leaf_inserted),
+ selectTag) &&
+ !EnclosingElementWithTag(FirstPositionInOrBeforeNode(*last_leaf_inserted),
+ scriptTag)) {
+ inserted_nodes.WillRemoveNode(*last_leaf_inserted);
+ // Removing a Text node won't dispatch synchronous events.
+ RemoveNode(last_leaf_inserted, ASSERT_NO_EDITING_ABORT);
+ }
+
+ // We don't have to make sure that firstNodeInserted isn't inside a select or
+ // script element, because it is a top level node in the fragment and the user
+ // can't insert into those elements.
+ Node* first_node_inserted = inserted_nodes.FirstNodeInserted();
+ if (first_node_inserted && first_node_inserted->IsTextNode() &&
+ !NodeHasVisibleLayoutText(ToText(*first_node_inserted))) {
+ inserted_nodes.WillRemoveNode(*first_node_inserted);
+ // Removing a Text node won't dispatch synchronous events.
+ RemoveNode(first_node_inserted, ASSERT_NO_EDITING_ABORT);
+ }
+}
+
+VisiblePosition ReplaceSelectionCommand::PositionAtEndOfInsertedContent()
+ const {
+ // TODO(editing-dev): Hoist the call and change it into a DCHECK.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ // TODO(yosin): We should set |m_endOfInsertedContent| not in SELECT
+ // element, since contents of SELECT elements, e.g. OPTION, OPTGROUP, are
+ // not editable, or SELECT element is an atomic on editing.
+ HTMLSelectElement* enclosing_select = ToHTMLSelectElement(
+ EnclosingElementWithTag(end_of_inserted_content_, selectTag));
+ if (enclosing_select) {
+ return CreateVisiblePosition(LastPositionInOrAfterNode(*enclosing_select));
+ }
+ if (end_of_inserted_content_.IsOrphan())
+ return VisiblePosition();
+ return CreateVisiblePosition(end_of_inserted_content_);
+}
+
+VisiblePosition ReplaceSelectionCommand::PositionAtStartOfInsertedContent()
+ const {
+ // TODO(editing-dev): Hoist the call and change it into a DCHECK.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (start_of_inserted_content_.IsOrphan())
+ return VisiblePosition();
+ return CreateVisiblePosition(start_of_inserted_content_);
+}
+
+static void RemoveHeadContents(ReplacementFragment& fragment) {
+ Node* next = nullptr;
+ for (Node* node = fragment.FirstChild(); node; node = next) {
+ if (IsHTMLBaseElement(*node) || IsHTMLLinkElement(*node) ||
+ IsHTMLMetaElement(*node) || IsHTMLTitleElement(*node)) {
+ next = NodeTraversal::NextSkippingChildren(*node);
+ fragment.RemoveNode(node);
+ } else {
+ next = NodeTraversal::Next(*node);
+ }
+ }
+}
+
+static bool FollowBlockElementStyle(const Node* node) {
+ if (!node->IsHTMLElement())
+ return false;
+
+ const HTMLElement& element = ToHTMLElement(*node);
+ return IsListItem(node) || IsTableCell(node) || element.HasTagName(preTag) ||
+ element.HasTagName(h1Tag) || element.HasTagName(h2Tag) ||
+ element.HasTagName(h3Tag) || element.HasTagName(h4Tag) ||
+ element.HasTagName(h5Tag) || element.HasTagName(h6Tag);
+}
+
+// Remove style spans before insertion if they are unnecessary. It's faster
+// because we'll avoid doing a layout.
+static void HandleStyleSpansBeforeInsertion(ReplacementFragment& fragment,
+ const Position& insertion_pos) {
+ Node* top_node = fragment.FirstChild();
+ if (!IsHTMLSpanElement(top_node))
+ return;
+
+ // Handling the case where we are doing Paste as Quotation or pasting into
+ // quoted content is more complicated (see handleStyleSpans) and doesn't
+ // receive the optimization.
+ if (EnclosingNodeOfType(FirstPositionInOrBeforeNode(*top_node),
+ IsMailHTMLBlockquoteElement,
+ kCanCrossEditingBoundary))
+ return;
+
+ // Remove style spans to follow the styles of parent block element when
+ // |fragment| becomes a part of it. See bugs http://crbug.com/226941 and
+ // http://crbug.com/335955.
+ HTMLSpanElement* wrapping_style_span = ToHTMLSpanElement(top_node);
+ const Node* node = insertion_pos.AnchorNode();
+ // |node| can be an inline element like <br> under <li>
+ // e.g.) editing/execCommand/switch-list-type.html
+ // editing/deleting/backspace-merge-into-block.html
+ if (IsInline(node)) {
+ node = EnclosingBlock(insertion_pos.AnchorNode());
+ if (!node)
+ return;
+ }
+
+ if (FollowBlockElementStyle(node)) {
+ fragment.RemoveNodePreservingChildren(wrapping_style_span);
+ return;
+ }
+
+ EditingStyle* style_at_insertion_pos =
+ EditingStyle::Create(insertion_pos.ParentAnchoredEquivalent());
+ String style_text = style_at_insertion_pos->Style()->AsText();
+
+ // FIXME: This string comparison is a naive way of comparing two styles.
+ // We should be taking the diff and check that the diff is empty.
+ if (style_text != wrapping_style_span->getAttribute(styleAttr))
+ return;
+
+ fragment.RemoveNodePreservingChildren(wrapping_style_span);
+}
+
+void ReplaceSelectionCommand::MergeEndIfNeeded(EditingState* editing_state) {
+ if (!should_merge_end_)
+ return;
+
+ VisiblePosition start_of_inserted_content(PositionAtStartOfInsertedContent());
+ VisiblePosition end_of_inserted_content(PositionAtEndOfInsertedContent());
+
+ // Bail to avoid infinite recursion.
+ if (moving_paragraph_) {
+ NOTREACHED();
+ return;
+ }
+
+ // Merging two paragraphs will destroy the moved one's block styles. Always
+ // move the end of inserted forward to preserve the block style of the
+ // paragraph already in the document, unless the paragraph to move would
+ // include the what was the start of the selection that was pasted into, so
+ // that we preserve that paragraph's block styles.
+ bool merge_forward =
+ !(InSameParagraph(start_of_inserted_content, end_of_inserted_content) &&
+ !IsStartOfParagraph(start_of_inserted_content));
+
+ VisiblePosition destination = merge_forward
+ ? NextPositionOf(end_of_inserted_content)
+ : end_of_inserted_content;
+ // TODO(editing-dev): Stop storing VisiblePositions through mutations.
+ // See crbug.com/648949 for details.
+ VisiblePosition start_of_paragraph_to_move =
+ merge_forward ? StartOfParagraph(end_of_inserted_content)
+ : NextPositionOf(end_of_inserted_content);
+
+ // Merging forward could result in deleting the destination anchor node.
+ // To avoid this, we add a placeholder node before the start of the paragraph.
+ if (EndOfParagraph(start_of_paragraph_to_move).DeepEquivalent() ==
+ destination.DeepEquivalent()) {
+ HTMLBRElement* placeholder = HTMLBRElement::Create(GetDocument());
+ InsertNodeBefore(placeholder,
+ start_of_paragraph_to_move.DeepEquivalent().AnchorNode(),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets()
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ destination = VisiblePosition::BeforeNode(*placeholder);
+ start_of_paragraph_to_move = CreateVisiblePosition(
+ start_of_paragraph_to_move.ToPositionWithAffinity());
+ }
+
+ MoveParagraph(start_of_paragraph_to_move,
+ EndOfParagraph(start_of_paragraph_to_move), destination,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Merging forward will remove m_endOfInsertedContent from the document.
+ if (merge_forward) {
+ const VisibleSelection& visible_selection = EndingVisibleSelection();
+ if (start_of_inserted_content_.IsOrphan()) {
+ start_of_inserted_content_ =
+ visible_selection.VisibleStart().DeepEquivalent();
+ }
+ end_of_inserted_content_ = visible_selection.VisibleEnd().DeepEquivalent();
+ // If we merged text nodes, m_endOfInsertedContent could be null. If
+ // this is the case, we use m_startOfInsertedContent.
+ if (end_of_inserted_content_.IsNull())
+ end_of_inserted_content_ = start_of_inserted_content_;
+ }
+}
+
+static Node* EnclosingInline(Node* node) {
+ while (ContainerNode* parent = node->parentNode()) {
+ if (IsBlockFlowElement(*parent) || IsHTMLBodyElement(*parent))
+ return node;
+ // Stop if any previous sibling is a block.
+ for (Node* sibling = node->previousSibling(); sibling;
+ sibling = sibling->previousSibling()) {
+ if (IsBlockFlowElement(*sibling))
+ return node;
+ }
+ node = parent;
+ }
+ return node;
+}
+
+static bool IsInlineHTMLElementWithStyle(const Node* node) {
+ // We don't want to skip over any block elements.
+ if (IsEnclosingBlock(node))
+ return false;
+
+ if (!node->IsHTMLElement())
+ return false;
+
+ // We can skip over elements whose class attribute is
+ // one of our internal classes.
+ const HTMLElement* element = ToHTMLElement(node);
+ return EditingStyle::ElementIsStyledSpanOrHTMLEquivalent(element);
+}
+
+static inline HTMLElement*
+ElementToSplitToAvoidPastingIntoInlineElementsWithStyle(
+ const Position& insertion_pos) {
+ Element* containing_block =
+ EnclosingBlock(insertion_pos.ComputeContainerNode());
+ return ToHTMLElement(HighestEnclosingNodeOfType(
+ insertion_pos, IsInlineHTMLElementWithStyle, kCannotCrossEditingBoundary,
+ containing_block));
+}
+
+void ReplaceSelectionCommand::SetUpStyle(const VisibleSelection& selection) {
+ // We can skip matching the style if the selection is plain text.
+ // TODO(editing-dev): Use IsEditablePosition instead of using UserModify
+ // directly.
+ if ((selection.Start().AnchorNode()->GetLayoutObject() &&
+ selection.Start()
+ .AnchorNode()
+ ->GetLayoutObject()
+ ->Style()
+ ->UserModify() == EUserModify::kReadWritePlaintextOnly) &&
+ (selection.End().AnchorNode()->GetLayoutObject() &&
+ selection.End().AnchorNode()->GetLayoutObject()->Style()->UserModify() ==
+ EUserModify::kReadWritePlaintextOnly))
+ match_style_ = false;
+
+ if (match_style_) {
+ insertion_style_ = EditingStyle::Create(selection.Start());
+ insertion_style_->MergeTypingStyle(&GetDocument());
+ }
+}
+
+void ReplaceSelectionCommand::InsertParagraphSeparatorIfNeeds(
+ const VisibleSelection& selection,
+ const ReplacementFragment& fragment,
+ EditingState* editing_state) {
+ const VisiblePosition visible_start = selection.VisibleStart();
+ const VisiblePosition visible_end = selection.VisibleEnd();
+
+ const bool selection_end_was_end_of_paragraph = IsEndOfParagraph(visible_end);
+ const bool selection_start_was_start_of_paragraph =
+ IsStartOfParagraph(visible_start);
+
+ Element* const enclosing_block_of_visible_start =
+ EnclosingBlock(visible_start.DeepEquivalent().AnchorNode());
+
+ const bool start_is_inside_mail_blockquote = EnclosingNodeOfType(
+ selection.Start(), IsMailHTMLBlockquoteElement, kCanCrossEditingBoundary);
+ const bool selection_is_plain_text = !IsRichlyEditablePosition(selection.Base());
+ Element* const current_root = selection.RootEditableElement();
+
+ if ((selection_start_was_start_of_paragraph &&
+ selection_end_was_end_of_paragraph &&
+ !start_is_inside_mail_blockquote) ||
+ enclosing_block_of_visible_start == current_root ||
+ IsListItem(enclosing_block_of_visible_start) || selection_is_plain_text) {
+ prevent_nesting_ = false;
+ }
+
+ if (selection.IsRange()) {
+ // When the end of the selection being pasted into is at the end of a
+ // paragraph, and that selection spans multiple blocks, not merging may
+ // leave an empty line.
+ // When the start of the selection being pasted into is at the start of a
+ // block, not merging will leave hanging block(s).
+ // Merge blocks if the start of the selection was in a Mail blockquote,
+ // since we handle that case specially to prevent nesting.
+ bool merge_blocks_after_delete = start_is_inside_mail_blockquote ||
+ IsEndOfParagraph(visible_end) ||
+ IsStartOfBlock(visible_start);
+ // FIXME: We should only expand to include fully selected special elements
+ // if we are copying a selection and pasting it on top of itself.
+ if (!DeleteSelection(editing_state, DeleteSelectionOptions::Builder()
+ .SetMergeBlocksAfterDelete(
+ merge_blocks_after_delete)
+ .SetSanitizeMarkup(true)
+ .Build()))
+ return;
+ if (fragment.HasInterchangeNewlineAtStart()) {
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ VisiblePosition start_after_delete =
+ EndingVisibleSelection().VisibleStart();
+ if (IsEndOfParagraph(start_after_delete) &&
+ !IsStartOfParagraph(start_after_delete) &&
+ !IsEndOfEditableOrNonEditableContent(start_after_delete)) {
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(NextPositionOf(start_after_delete).DeepEquivalent())
+ .Build()));
+ } else {
+ InsertParagraphSeparator(editing_state);
+ }
+ if (editing_state->IsAborted())
+ return;
+ }
+ } else {
+ DCHECK(selection.IsCaret());
+ if (fragment.HasInterchangeNewlineAtStart()) {
+ const VisiblePosition next =
+ NextPositionOf(visible_start, kCannotCrossEditingBoundary);
+ if (IsEndOfParagraph(visible_start) &&
+ !IsStartOfParagraph(visible_start) && next.IsNotNull()) {
+ SetEndingSelection(
+ SelectionForUndoStep::From(SelectionInDOMTree::Builder()
+ .Collapse(next.DeepEquivalent())
+ .Build()));
+ } else {
+ InsertParagraphSeparator(editing_state);
+ if (editing_state->IsAborted())
+ return;
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ }
+ }
+ // We split the current paragraph in two to avoid nesting the blocks from
+ // the fragment inside the current block.
+ //
+ // For example, paste
+ // <div>foo</div><div>bar</div><div>baz</div>
+ // into
+ // <div>x^x</div>
+ // where ^ is the caret.
+ //
+ // As long as the div styles are the same, visually you'd expect:
+ // <div>xbar</div><div>bar</div><div>bazx</div>
+ // not
+ // <div>xbar<div>bar</div><div>bazx</div></div>
+ // Don't do this if the selection started in a Mail blockquote.
+ const VisiblePosition visible_start_position =
+ EndingVisibleSelection().VisibleStart();
+ if (prevent_nesting_ && !start_is_inside_mail_blockquote &&
+ !IsEndOfParagraph(visible_start_position) &&
+ !IsStartOfParagraph(visible_start_position)) {
+ InsertParagraphSeparator(editing_state);
+ if (editing_state->IsAborted())
+ return;
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(
+ PreviousPositionOf(EndingVisibleSelection().VisibleStart())
+ .DeepEquivalent())
+ .Build()));
+ }
+ }
+}
+
+void ReplaceSelectionCommand::DoApply(EditingState* editing_state) {
+ TRACE_EVENT0("blink", "ReplaceSelectionCommand::doApply");
+ const VisibleSelection& selection = EndingVisibleSelection();
+
+ // ReplaceSelectionCommandTest.CrashWithNoSelection hits below abort
+ // condition.
+ ABORT_EDITING_COMMAND_IF(selection.IsNone());
+ ABORT_EDITING_COMMAND_IF(!selection.IsValidFor(GetDocument()));
+
+ if (!selection.RootEditableElement())
+ return;
+
+ ReplacementFragment fragment(&GetDocument(), document_fragment_.Get(),
+ selection);
+ bool trivial_replace_result = PerformTrivialReplace(fragment, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ if (trivial_replace_result)
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ SetUpStyle(selection);
+ Element* const current_root = selection.RootEditableElement();
+ const bool start_is_inside_mail_blockquote = EnclosingNodeOfType(
+ selection.Start(), IsMailHTMLBlockquoteElement, kCanCrossEditingBoundary);
+ const bool selection_is_plain_text =
+ !IsRichlyEditablePosition(selection.Base());
+ const bool selection_end_was_end_of_paragraph =
+ IsEndOfParagraph(selection.VisibleEnd());
+ const bool selection_start_was_start_of_paragraph =
+ IsStartOfParagraph(selection.VisibleStart());
+ InsertParagraphSeparatorIfNeeds(selection, fragment, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ Position insertion_pos = EndingVisibleSelection().Start();
+ // We don't want any of the pasted content to end up nested in a Mail
+ // blockquote, so first break out of any surrounding Mail blockquotes. Unless
+ // we're inserting in a table, in which case breaking the blockquote will
+ // prevent the content from actually being inserted in the table.
+ if (EnclosingNodeOfType(insertion_pos, IsMailHTMLBlockquoteElement,
+ kCanCrossEditingBoundary) &&
+ prevent_nesting_ &&
+ !(EnclosingNodeOfType(insertion_pos, &IsTableStructureNode))) {
+ ApplyCommandToComposite(BreakBlockquoteCommand::Create(GetDocument()),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ // This will leave a br between the split.
+ Node* br = EndingVisibleSelection().Start().AnchorNode();
+ DCHECK(IsHTMLBRElement(br)) << br;
+ // Insert content between the two blockquotes, but remove the br (since it
+ // was just a placeholder).
+ insertion_pos = Position::InParentBeforeNode(*br);
+ RemoveNode(br, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ // Inserting content could cause whitespace to collapse, e.g. inserting
+ // <div>foo</div> into hello^ world.
+ PrepareWhitespaceAtPositionForSplit(insertion_pos);
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // If the downstream node has been removed there's no point in continuing.
+ if (!MostForwardCaretPosition(insertion_pos).AnchorNode())
+ return;
+
+ // NOTE: This would be an incorrect usage of downstream() if downstream() were
+ // changed to mean the last position after p that maps to the same visible
+ // position as p (since in the case where a br is at the end of a block and
+ // collapsed away, there are positions after the br which map to the same
+ // visible position as [br, 0]).
+ HTMLBRElement* end_br = ToHTMLBRElementOrNull(
+ *MostForwardCaretPosition(insertion_pos).AnchorNode());
+ VisiblePosition original_vis_pos_before_end_br;
+ if (end_br) {
+ original_vis_pos_before_end_br =
+ PreviousPositionOf(VisiblePosition::BeforeNode(*end_br));
+ }
+
+ Element* enclosing_block_of_insertion_pos =
+ EnclosingBlock(insertion_pos.AnchorNode());
+
+ // Adjust |enclosingBlockOfInsertionPos| to prevent nesting.
+ // If the start was in a Mail blockquote, we will have already handled
+ // adjusting |enclosingBlockOfInsertionPos| above.
+ if (prevent_nesting_ && enclosing_block_of_insertion_pos &&
+ enclosing_block_of_insertion_pos != current_root &&
+ !IsTableCell(enclosing_block_of_insertion_pos) &&
+ !start_is_inside_mail_blockquote) {
+ VisiblePosition visible_insertion_pos =
+ CreateVisiblePosition(insertion_pos);
+ if (IsEndOfBlock(visible_insertion_pos) &&
+ !(IsStartOfBlock(visible_insertion_pos) &&
+ fragment.HasInterchangeNewlineAtEnd()))
+ insertion_pos =
+ Position::InParentAfterNode(*enclosing_block_of_insertion_pos);
+ else if (IsStartOfBlock(visible_insertion_pos))
+ insertion_pos =
+ Position::InParentBeforeNode(*enclosing_block_of_insertion_pos);
+ }
+
+ // Paste at start or end of link goes outside of link.
+ insertion_pos =
+ PositionAvoidingSpecialElementBoundary(insertion_pos, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ // FIXME: Can this wait until after the operation has been performed? There
+ // doesn't seem to be any work performed after this that queries or uses the
+ // typing style.
+ if (LocalFrame* frame = GetDocument().GetFrame())
+ frame->GetEditor().ClearTypingStyle();
+
+ RemoveHeadContents(fragment);
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // We don't want the destination to end up inside nodes that weren't selected.
+ // To avoid that, we move the position forward without changing the visible
+ // position so we're still at the same visible location, but outside of
+ // preceding tags.
+ insertion_pos = PositionAvoidingPrecedingNodes(insertion_pos);
+
+ // Paste into run of tabs splits the tab span.
+ insertion_pos = PositionOutsideTabSpan(insertion_pos);
+
+ HandleStyleSpansBeforeInsertion(fragment, insertion_pos);
+
+ // We're finished if there is nothing to add.
+ if (fragment.IsEmpty() || !fragment.FirstChild())
+ return;
+
+ // If we are not trying to match the destination style we prefer a position
+ // that is outside inline elements that provide style.
+ // This way we can produce a less verbose markup.
+ // We can skip this optimization for fragments not wrapped in one of
+ // our style spans and for positions inside list items
+ // since insertAsListItems already does the right thing.
+ if (!match_style_ && !EnclosingList(insertion_pos.ComputeContainerNode())) {
+ if (insertion_pos.ComputeContainerNode()->IsTextNode() &&
+ insertion_pos.OffsetInContainerNode() &&
+ !insertion_pos.AtLastEditingPositionForNode()) {
+ SplitTextNode(ToText(insertion_pos.ComputeContainerNode()),
+ insertion_pos.OffsetInContainerNode());
+ insertion_pos =
+ Position::FirstPositionInNode(*insertion_pos.ComputeContainerNode());
+ }
+
+ if (HTMLElement* element_to_split_to =
+ ElementToSplitToAvoidPastingIntoInlineElementsWithStyle(
+ insertion_pos)) {
+ if (insertion_pos.ComputeContainerNode() !=
+ element_to_split_to->parentNode()) {
+ Node* split_start = insertion_pos.ComputeNodeAfterPosition();
+ if (!split_start)
+ split_start = insertion_pos.ComputeContainerNode();
+ Node* node_to_split_to =
+ SplitTreeToNode(split_start, element_to_split_to->parentNode());
+ insertion_pos = Position::InParentBeforeNode(*node_to_split_to);
+ }
+ }
+ }
+
+ // FIXME: When pasting rich content we're often prevented from heading down
+ // the fast path by style spans. Try again here if they've been removed.
+
+ // 1) Insert the content.
+ // 2) Remove redundant styles and style tags, this inner <b> for example:
+ // <b>foo <b>bar</b> baz</b>.
+ // 3) Merge the start of the added content with the content before the
+ // position being pasted into.
+ // 4) Do one of the following:
+ // a) expand the last br if the fragment ends with one and it collapsed,
+ // b) merge the last paragraph of the incoming fragment with the paragraph
+ // that contained the end of the selection that was pasted into, or
+ // c) handle an interchange newline at the end of the incoming fragment.
+ // 5) Add spaces for smart replace.
+ // 6) Select the replacement if requested, and match style if requested.
+
+ InsertedNodes inserted_nodes;
+ inserted_nodes.SetRefNode(fragment.FirstChild());
+ DCHECK(inserted_nodes.RefNode());
+ Node* node = inserted_nodes.RefNode()->nextSibling();
+
+ fragment.RemoveNode(inserted_nodes.RefNode());
+
+ Element* block_start = EnclosingBlock(insertion_pos.AnchorNode());
+ if ((IsHTMLListElement(inserted_nodes.RefNode()) ||
+ (IsHTMLListElement(inserted_nodes.RefNode()->firstChild()))) &&
+ block_start && block_start->GetLayoutObject()->IsListItem() &&
+ HasEditableStyle(*block_start->parentNode())) {
+ inserted_nodes.SetRefNode(
+ InsertAsListItems(ToHTMLElement(inserted_nodes.RefNode()), block_start,
+ insertion_pos, inserted_nodes, editing_state));
+ if (editing_state->IsAborted())
+ return;
+ } else {
+ InsertNodeAt(inserted_nodes.RefNode(), insertion_pos, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ inserted_nodes.RespondToNodeInsertion(*inserted_nodes.RefNode());
+ }
+
+ // Mutation events (bug 22634) may have already removed the inserted content
+ if (!inserted_nodes.RefNode()->isConnected())
+ return;
+
+ bool plain_text_fragment = IsPlainTextMarkup(inserted_nodes.RefNode());
+
+ while (node) {
+ Node* next = node->nextSibling();
+ fragment.RemoveNode(node);
+ InsertNodeAfter(node, inserted_nodes.RefNode(), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ inserted_nodes.RespondToNodeInsertion(*node);
+
+ // Mutation events (bug 22634) may have already removed the inserted content
+ if (!node->isConnected())
+ return;
+
+ inserted_nodes.SetRefNode(node);
+ if (node && plain_text_fragment)
+ plain_text_fragment = IsPlainTextMarkup(node);
+ node = next;
+ }
+
+ if (IsRichlyEditablePosition(insertion_pos)) {
+ RemoveUnrenderedTextNodesAtEnds(inserted_nodes);
+ ABORT_EDITING_COMMAND_IF(!inserted_nodes.RefNode());
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Mutation events (bug 20161) may have already removed the inserted content
+ if (!inserted_nodes.FirstNodeInserted() ||
+ !inserted_nodes.FirstNodeInserted()->isConnected())
+ return;
+
+ // Scripts specified in javascript protocol may remove
+ // |enclosingBlockOfInsertionPos| during insertion, e.g. <iframe
+ // src="javascript:...">
+ if (enclosing_block_of_insertion_pos &&
+ !enclosing_block_of_insertion_pos->isConnected())
+ enclosing_block_of_insertion_pos = nullptr;
+
+ VisiblePosition start_of_inserted_content = CreateVisiblePosition(
+ FirstPositionInOrBeforeNode(*inserted_nodes.FirstNodeInserted()));
+
+ // We inserted before the enclosingBlockOfInsertionPos to prevent nesting, and
+ // the content before the enclosingBlockOfInsertionPos wasn't in its own block
+ // and didn't have a br after it, so the inserted content ended up in the same
+ // paragraph.
+ if (!start_of_inserted_content.IsNull() && enclosing_block_of_insertion_pos &&
+ insertion_pos.AnchorNode() ==
+ enclosing_block_of_insertion_pos->parentNode() &&
+ (unsigned)insertion_pos.ComputeEditingOffset() <
+ enclosing_block_of_insertion_pos->NodeIndex() &&
+ !IsStartOfParagraph(start_of_inserted_content)) {
+ InsertNodeAt(HTMLBRElement::Create(GetDocument()),
+ start_of_inserted_content.DeepEquivalent(), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (end_br &&
+ (plain_text_fragment ||
+ (ShouldRemoveEndBR(end_br, original_vis_pos_before_end_br) &&
+ !(fragment.HasInterchangeNewlineAtEnd() && selection_is_plain_text)))) {
+ ContainerNode* parent = end_br->parentNode();
+ inserted_nodes.WillRemoveNode(*end_br);
+ ABORT_EDITING_COMMAND_IF(!inserted_nodes.RefNode());
+ RemoveNode(end_br, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ if (Node* node_to_remove = HighestNodeToRemoveInPruning(parent)) {
+ inserted_nodes.WillRemoveNode(*node_to_remove);
+ ABORT_EDITING_COMMAND_IF(!inserted_nodes.RefNode());
+ RemoveNode(node_to_remove, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+
+ MakeInsertedContentRoundTrippableWithHTMLTreeBuilder(inserted_nodes,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ {
+ // TODO(dominicc): refNode may not be connected, for example in
+ // LayoutTests/editing/inserting/insert-table-in-paragraph-crash.html .
+ // Refactor this so there's a relationship between the conditions
+ // where refNode is dereferenced and refNode is connected.
+ bool ref_node_was_connected = inserted_nodes.RefNode()->isConnected();
+ RemoveRedundantStylesAndKeepStyleSpanInline(inserted_nodes, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ DCHECK_EQ(inserted_nodes.RefNode()->isConnected(), ref_node_was_connected)
+ << inserted_nodes.RefNode();
+ }
+
+ if (sanitize_fragment_ && inserted_nodes.FirstNodeInserted()) {
+ ApplyCommandToComposite(
+ SimplifyMarkupCommand::Create(GetDocument(),
+ inserted_nodes.FirstNodeInserted(),
+ inserted_nodes.PastLastLeaf()),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ // Setup |start_of_inserted_content_| and |end_of_inserted_content_|.
+ // This should be the last two lines of code that access insertedNodes.
+ // TODO(editing-dev): The {First,Last}NodeInserted() nullptr checks may be
+ // unnecessary. Investigate.
+ start_of_inserted_content_ =
+ inserted_nodes.FirstNodeInserted()
+ ? FirstPositionInOrBeforeNode(*inserted_nodes.FirstNodeInserted())
+ : Position();
+ end_of_inserted_content_ =
+ inserted_nodes.LastLeafInserted()
+ ? LastPositionInOrAfterNode(*inserted_nodes.LastLeafInserted())
+ : Position();
+
+ // Determine whether or not we should merge the end of inserted content with
+ // what's after it before we do the start merge so that the start merge
+ // doesn't effect our decision.
+ should_merge_end_ = ShouldMergeEnd(selection_end_was_end_of_paragraph);
+
+ if (ShouldMergeStart(selection_start_was_start_of_paragraph,
+ fragment.HasInterchangeNewlineAtStart(),
+ start_is_inside_mail_blockquote)) {
+ VisiblePosition start_of_paragraph_to_move =
+ PositionAtStartOfInsertedContent();
+ VisiblePosition destination =
+ PreviousPositionOf(start_of_paragraph_to_move);
+
+ // Helpers for making the VisiblePositions valid again after DOM changes.
+ PositionWithAffinity start_of_paragraph_to_move_position =
+ start_of_paragraph_to_move.ToPositionWithAffinity();
+ PositionWithAffinity destination_position =
+ destination.ToPositionWithAffinity();
+
+ // We need to handle the case where we need to merge the end
+ // but our destination node is inside an inline that is the last in the
+ // block.
+ // We insert a placeholder before the newly inserted content to avoid being
+ // merged into the inline.
+ Node* destination_node = destination.DeepEquivalent().AnchorNode();
+ if (should_merge_end_ &&
+ destination_node != EnclosingInline(destination_node) &&
+ EnclosingInline(destination_node)->nextSibling()) {
+ InsertNodeBefore(HTMLBRElement::Create(GetDocument()),
+ inserted_nodes.RefNode(), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ // Merging the the first paragraph of inserted content with the content that
+ // came before the selection that was pasted into would also move content
+ // after the selection that was pasted into if: only one paragraph was being
+ // pasted, and it was not wrapped in a block, the selection that was pasted
+ // into ended at the end of a block and the next paragraph didn't start at
+ // the start of a block.
+ // Insert a line break just after the inserted content to separate it from
+ // what comes after and prevent that from happening.
+ VisiblePosition end_of_inserted_content = PositionAtEndOfInsertedContent();
+ if (StartOfParagraph(end_of_inserted_content).DeepEquivalent() ==
+ start_of_paragraph_to_move_position.GetPosition()) {
+ InsertNodeAt(HTMLBRElement::Create(GetDocument()),
+ end_of_inserted_content.DeepEquivalent(), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ // Mutation events (bug 22634) triggered by inserting the <br> might have
+ // removed the content we're about to move
+ if (!start_of_paragraph_to_move_position.IsConnected())
+ return;
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Making the two VisiblePositions valid again.
+ start_of_paragraph_to_move =
+ CreateVisiblePosition(start_of_paragraph_to_move_position);
+ destination = CreateVisiblePosition(destination_position);
+
+ // FIXME: Maintain positions for the start and end of inserted content
+ // instead of keeping nodes. The nodes are only ever used to create
+ // positions where inserted content starts/ends.
+ MoveParagraph(start_of_paragraph_to_move,
+ EndOfParagraph(start_of_paragraph_to_move), destination,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ const VisibleSelection& visible_selection_of_insterted_content =
+ EndingVisibleSelection();
+ start_of_inserted_content_ = MostForwardCaretPosition(
+ visible_selection_of_insterted_content.VisibleStart().DeepEquivalent());
+ if (end_of_inserted_content_.IsOrphan()) {
+ end_of_inserted_content_ = MostBackwardCaretPosition(
+ visible_selection_of_insterted_content.VisibleEnd().DeepEquivalent());
+ }
+ }
+
+ Position last_position_to_select;
+ if (fragment.HasInterchangeNewlineAtEnd()) {
+ VisiblePosition end_of_inserted_content = PositionAtEndOfInsertedContent();
+ VisiblePosition next =
+ NextPositionOf(end_of_inserted_content, kCannotCrossEditingBoundary);
+
+ if (selection_end_was_end_of_paragraph ||
+ !IsEndOfParagraph(end_of_inserted_content) || next.IsNull()) {
+ if (TextControlElement* text_control =
+ EnclosingTextControl(current_root)) {
+ if (!inserted_nodes.LastLeafInserted()->nextSibling()) {
+ InsertNodeAfter(text_control->CreatePlaceholderBreakElement(),
+ inserted_nodes.LastLeafInserted(), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(
+ Position::AfterNode(*inserted_nodes.LastLeafInserted()))
+ .Build()));
+ // Select up to the paragraph separator that was added.
+ last_position_to_select =
+ EndingVisibleSelection().VisibleStart().DeepEquivalent();
+ } else if (!IsStartOfParagraph(end_of_inserted_content)) {
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(end_of_inserted_content.DeepEquivalent())
+ .Build()));
+ Element* enclosing_block_element = EnclosingBlock(
+ end_of_inserted_content.DeepEquivalent().AnchorNode());
+ if (IsListItem(enclosing_block_element)) {
+ HTMLLIElement* new_list_item = HTMLLIElement::Create(GetDocument());
+ InsertNodeAfter(new_list_item, enclosing_block_element,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::FirstPositionInNode(*new_list_item))
+ .Build()));
+ } else {
+ // Use a default paragraph element (a plain div) for the empty
+ // paragraph, using the last paragraph block's style seems to annoy
+ // users.
+ InsertParagraphSeparator(
+ editing_state, true,
+ !start_is_inside_mail_blockquote &&
+ HighestEnclosingNodeOfType(
+ end_of_inserted_content.DeepEquivalent(),
+ IsMailHTMLBlockquoteElement, kCannotCrossEditingBoundary,
+ inserted_nodes.FirstNodeInserted()->parentNode()));
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Select up to the paragraph separator that was added.
+ last_position_to_select =
+ EndingVisibleSelection().VisibleStart().DeepEquivalent();
+ UpdateNodesInserted(last_position_to_select.AnchorNode());
+ }
+ } else {
+ // Select up to the beginning of the next paragraph.
+ last_position_to_select = MostForwardCaretPosition(next.DeepEquivalent());
+ }
+ } else {
+ MergeEndIfNeeded(editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ if (ShouldPerformSmartReplace()) {
+ AddSpacesForSmartReplace(editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ // If we are dealing with a fragment created from plain text
+ // no style matching is necessary.
+ if (plain_text_fragment)
+ match_style_ = false;
+
+ CompleteHTMLReplacement(last_position_to_select, editing_state);
+}
+
+bool ReplaceSelectionCommand::ShouldRemoveEndBR(
+ HTMLBRElement* end_br,
+ const VisiblePosition& original_vis_pos_before_end_br) {
+ if (!end_br || !end_br->isConnected())
+ return false;
+
+ VisiblePosition visible_pos = VisiblePosition::BeforeNode(*end_br);
+
+ // Don't remove the br if nothing was inserted.
+ if (PreviousPositionOf(visible_pos).DeepEquivalent() ==
+ original_vis_pos_before_end_br.DeepEquivalent())
+ return false;
+
+ // Remove the br if it is collapsed away and so is unnecessary.
+ if (!GetDocument().InNoQuirksMode() && IsEndOfBlock(visible_pos) &&
+ !IsStartOfParagraph(visible_pos))
+ return true;
+
+ // A br that was originally holding a line open should be displaced by
+ // inserted content or turned into a line break.
+ // A br that was originally acting as a line break should still be acting as a
+ // line break, not as a placeholder.
+ return IsStartOfParagraph(visible_pos) && IsEndOfParagraph(visible_pos);
+}
+
+bool ReplaceSelectionCommand::ShouldPerformSmartReplace() const {
+ if (!smart_replace_)
+ return false;
+
+ TextControlElement* text_control =
+ EnclosingTextControl(PositionAtStartOfInsertedContent().DeepEquivalent());
+ if (IsHTMLInputElement(text_control) &&
+ ToHTMLInputElement(text_control)->type() == InputTypeNames::password)
+ return false; // Disable smart replace for password fields.
+
+ return true;
+}
+
+static bool IsCharacterSmartReplaceExemptConsideringNonBreakingSpace(
+ UChar32 character,
+ bool previous_character) {
+ return IsCharacterSmartReplaceExempt(
+ character == kNoBreakSpaceCharacter ? ' ' : character,
+ previous_character);
+}
+
+void ReplaceSelectionCommand::AddSpacesForSmartReplace(
+ EditingState* editing_state) {
+ VisiblePosition end_of_inserted_content = PositionAtEndOfInsertedContent();
+ Position end_upstream =
+ MostBackwardCaretPosition(end_of_inserted_content.DeepEquivalent());
+ Node* end_node = end_upstream.ComputeNodeBeforePosition();
+ int end_offset =
+ end_node && end_node->IsTextNode() ? ToText(end_node)->length() : 0;
+ if (end_upstream.IsOffsetInAnchor()) {
+ end_node = end_upstream.ComputeContainerNode();
+ end_offset = end_upstream.OffsetInContainerNode();
+ }
+
+ bool needs_trailing_space =
+ !IsEndOfParagraph(end_of_inserted_content) &&
+ !IsCharacterSmartReplaceExemptConsideringNonBreakingSpace(
+ CharacterAfter(end_of_inserted_content), false);
+ if (needs_trailing_space && end_node) {
+ bool collapse_white_space =
+ !end_node->GetLayoutObject() ||
+ end_node->GetLayoutObject()->Style()->CollapseWhiteSpace();
+ if (end_node->IsTextNode()) {
+ InsertTextIntoNode(ToText(end_node), end_offset,
+ collapse_white_space ? NonBreakingSpaceString() : " ");
+ if (end_of_inserted_content_.ComputeContainerNode() == end_node)
+ end_of_inserted_content_ = Position(
+ end_node, end_of_inserted_content_.OffsetInContainerNode() + 1);
+ } else {
+ Text* node = GetDocument().CreateEditingTextNode(
+ collapse_white_space ? NonBreakingSpaceString() : " ");
+ InsertNodeAfter(node, end_node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ // Make sure that |updateNodesInserted| does not change
+ // |m_startOfInsertedContent|.
+ DCHECK(start_of_inserted_content_.IsNotNull());
+ UpdateNodesInserted(node);
+ }
+ }
+
+ GetDocument().UpdateStyleAndLayout();
+
+ VisiblePosition start_of_inserted_content =
+ PositionAtStartOfInsertedContent();
+ Position start_downstream =
+ MostForwardCaretPosition(start_of_inserted_content.DeepEquivalent());
+ Node* start_node = start_downstream.ComputeNodeAfterPosition();
+ unsigned start_offset = 0;
+ if (start_downstream.IsOffsetInAnchor()) {
+ start_node = start_downstream.ComputeContainerNode();
+ start_offset = start_downstream.OffsetInContainerNode();
+ }
+
+ bool needs_leading_space =
+ !IsStartOfParagraph(start_of_inserted_content) &&
+ !IsCharacterSmartReplaceExemptConsideringNonBreakingSpace(
+ CharacterBefore(start_of_inserted_content), true);
+ if (needs_leading_space && start_node) {
+ bool collapse_white_space =
+ !start_node->GetLayoutObject() ||
+ start_node->GetLayoutObject()->Style()->CollapseWhiteSpace();
+ if (start_node->IsTextNode()) {
+ InsertTextIntoNode(ToText(start_node), start_offset,
+ collapse_white_space ? NonBreakingSpaceString() : " ");
+ if (end_of_inserted_content_.ComputeContainerNode() == start_node &&
+ end_of_inserted_content_.OffsetInContainerNode())
+ end_of_inserted_content_ = Position(
+ start_node, end_of_inserted_content_.OffsetInContainerNode() + 1);
+ } else {
+ Text* node = GetDocument().CreateEditingTextNode(
+ collapse_white_space ? NonBreakingSpaceString() : " ");
+ // Don't updateNodesInserted. Doing so would set m_endOfInsertedContent to
+ // be the node containing the leading space, but m_endOfInsertedContent is
+ // supposed to mark the end of pasted content.
+ InsertNodeBefore(node, start_node, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ start_of_inserted_content_ = Position::FirstPositionInNode(*node);
+ }
+ }
+}
+
+void ReplaceSelectionCommand::CompleteHTMLReplacement(
+ const Position& last_position_to_select,
+ EditingState* editing_state) {
+ Position start = PositionAtStartOfInsertedContent().DeepEquivalent();
+ Position end = PositionAtEndOfInsertedContent().DeepEquivalent();
+
+ // Mutation events may have deleted start or end
+ if (start.IsNotNull() && !start.IsOrphan() && end.IsNotNull() &&
+ !end.IsOrphan()) {
+ // FIXME (11475): Remove this and require that the creator of the fragment
+ // to use nbsps.
+ RebalanceWhitespaceAt(start);
+ RebalanceWhitespaceAt(end);
+
+ if (match_style_) {
+ DCHECK(insertion_style_);
+ ApplyStyle(insertion_style_.Get(), start, end, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ if (last_position_to_select.IsNotNull())
+ end = last_position_to_select;
+
+ MergeTextNodesAroundPosition(start, end, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ } else if (last_position_to_select.IsNotNull()) {
+ start = end = last_position_to_select;
+ } else {
+ return;
+ }
+
+ start_of_inserted_range_ = start;
+ end_of_inserted_range_ = end;
+
+ if (select_replacement_) {
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtentDeprecated(start, end)
+ .Build()));
+ return;
+ }
+
+ if (end.IsNotNull()) {
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .Collapse(end)
+ .Build()));
+ return;
+ }
+ SetEndingSelection(SelectionForUndoStep());
+}
+
+void ReplaceSelectionCommand::MergeTextNodesAroundPosition(
+ Position& position,
+ Position& position_only_to_be_updated,
+ EditingState* editing_state) {
+ bool position_is_offset_in_anchor = position.IsOffsetInAnchor();
+ bool position_only_to_be_updated_is_offset_in_anchor =
+ position_only_to_be_updated.IsOffsetInAnchor();
+ Text* text = nullptr;
+ if (position_is_offset_in_anchor && position.ComputeContainerNode() &&
+ position.ComputeContainerNode()->IsTextNode()) {
+ text = ToText(position.ComputeContainerNode());
+ } else {
+ Node* before = position.ComputeNodeBeforePosition();
+ if (before && before->IsTextNode()) {
+ text = ToText(before);
+ } else {
+ Node* after = position.ComputeNodeAfterPosition();
+ if (after && after->IsTextNode())
+ text = ToText(after);
+ }
+ }
+ if (!text)
+ return;
+
+ // Merging Text nodes causes an additional layout. We'd like to skip it if the
+ // editable text is huge.
+ // TODO(tkent): 1024 was chosen by my intuition. We need data.
+ const unsigned kMergeSizeLimit = 1024;
+ bool has_incomplete_surrogate =
+ text->data().length() >= 1 &&
+ (U16_IS_TRAIL(text->data()[0]) ||
+ U16_IS_LEAD(text->data()[text->data().length() - 1]));
+ if (!has_incomplete_surrogate && text->data().length() > kMergeSizeLimit)
+ return;
+ if (text->previousSibling() && text->previousSibling()->IsTextNode()) {
+ Text* previous = ToText(text->previousSibling());
+ if (has_incomplete_surrogate ||
+ previous->data().length() <= kMergeSizeLimit) {
+ InsertTextIntoNode(text, 0, previous->data());
+
+ if (position_is_offset_in_anchor) {
+ position =
+ Position(position.ComputeContainerNode(),
+ previous->length() + position.OffsetInContainerNode());
+ } else {
+ position = ComputePositionForNodeRemoval(position, *previous);
+ }
+
+ if (position_only_to_be_updated_is_offset_in_anchor) {
+ if (position_only_to_be_updated.ComputeContainerNode() == text)
+ position_only_to_be_updated = Position(
+ text, previous->length() +
+ position_only_to_be_updated.OffsetInContainerNode());
+ else if (position_only_to_be_updated.ComputeContainerNode() == previous)
+ position_only_to_be_updated = Position(
+ text, position_only_to_be_updated.OffsetInContainerNode());
+ } else {
+ position_only_to_be_updated = ComputePositionForNodeRemoval(
+ position_only_to_be_updated, *previous);
+ }
+
+ RemoveNode(previous, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+ }
+ if (text->nextSibling() && text->nextSibling()->IsTextNode()) {
+ Text* next = ToText(text->nextSibling());
+ if (!has_incomplete_surrogate && next->data().length() > kMergeSizeLimit)
+ return;
+ unsigned original_length = text->length();
+ InsertTextIntoNode(text, original_length, next->data());
+
+ if (!position_is_offset_in_anchor)
+ position = ComputePositionForNodeRemoval(position, *next);
+
+ if (position_only_to_be_updated_is_offset_in_anchor &&
+ position_only_to_be_updated.ComputeContainerNode() == next) {
+ position_only_to_be_updated = Position(
+ text, original_length +
+ position_only_to_be_updated.OffsetInContainerNode());
+ } else {
+ position_only_to_be_updated =
+ ComputePositionForNodeRemoval(position_only_to_be_updated, *next);
+ }
+
+ RemoveNode(next, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+}
+
+InputEvent::InputType ReplaceSelectionCommand::GetInputType() const {
+ // |ReplaceSelectionCommand| could be used with Paste, Drag&Drop,
+ // InsertFragment and |TypingCommand|.
+ // 1. Paste, Drag&Drop, InsertFragment should rely on correct |m_inputType|.
+ // 2. |TypingCommand| will supply the |inputType()|, so |m_inputType| could
+ // default to |InputType::None|.
+ return input_type_;
+}
+
+// If the user is inserting a list into an existing list, instead of nesting the
+// list, we put the list items into the existing list.
+Node* ReplaceSelectionCommand::InsertAsListItems(HTMLElement* list_element,
+ Element* insertion_block,
+ const Position& insert_pos,
+ InsertedNodes& inserted_nodes,
+ EditingState* editing_state) {
+ while (list_element->HasOneChild() &&
+ IsHTMLListElement(list_element->firstChild()))
+ list_element = ToHTMLElement(list_element->firstChild());
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ bool is_start = IsStartOfParagraph(CreateVisiblePosition(insert_pos));
+ bool is_end = IsEndOfParagraph(CreateVisiblePosition(insert_pos));
+ bool is_middle = !is_start && !is_end;
+ Node* last_node = insertion_block;
+
+ // If we're in the middle of a list item, we should split it into two separate
+ // list items and insert these nodes between them.
+ if (is_middle) {
+ int text_node_offset = insert_pos.OffsetInContainerNode();
+ if (insert_pos.AnchorNode()->IsTextNode() && text_node_offset > 0)
+ SplitTextNode(ToText(insert_pos.AnchorNode()), text_node_offset);
+ SplitTreeToNode(insert_pos.AnchorNode(), last_node, true);
+ }
+
+ while (Node* list_item = list_element->firstChild()) {
+ list_element->RemoveChild(list_item, ASSERT_NO_EXCEPTION);
+ if (is_start || is_middle) {
+ InsertNodeBefore(list_item, last_node, editing_state);
+ if (editing_state->IsAborted())
+ return nullptr;
+ inserted_nodes.RespondToNodeInsertion(*list_item);
+ } else if (is_end) {
+ InsertNodeAfter(list_item, last_node, editing_state);
+ if (editing_state->IsAborted())
+ return nullptr;
+ inserted_nodes.RespondToNodeInsertion(*list_item);
+ last_node = list_item;
+ } else {
+ NOTREACHED();
+ }
+ }
+ if (is_start || is_middle) {
+ if (Node* node = last_node->previousSibling())
+ return node;
+ }
+ return last_node;
+}
+
+void ReplaceSelectionCommand::UpdateNodesInserted(Node* node) {
+ if (!node)
+ return;
+
+ if (start_of_inserted_content_.IsNull())
+ start_of_inserted_content_ = FirstPositionInOrBeforeNode(*node);
+
+ end_of_inserted_content_ =
+ LastPositionInOrAfterNode(NodeTraversal::LastWithinOrSelf(*node));
+}
+
+// During simple pastes, where we're just pasting a text node into a run of
+// text, we insert the text node directly into the text node that holds the
+// selection. This is much faster than the generalized code in
+// ReplaceSelectionCommand, and works around
+// <https://bugs.webkit.org/show_bug.cgi?id=6148> since we don't split text
+// nodes.
+bool ReplaceSelectionCommand::PerformTrivialReplace(
+ const ReplacementFragment& fragment,
+ EditingState* editing_state) {
+ if (!fragment.FirstChild() || fragment.FirstChild() != fragment.LastChild() ||
+ !fragment.FirstChild()->IsTextNode())
+ return false;
+
+ // FIXME: Would be nice to handle smart replace in the fast path.
+ if (smart_replace_ || fragment.HasInterchangeNewlineAtStart() ||
+ fragment.HasInterchangeNewlineAtEnd())
+ return false;
+
+ // e.g. when "bar" is inserted after "foo" in <div><u>foo</u></div>, "bar"
+ // should not be underlined.
+ if (ElementToSplitToAvoidPastingIntoInlineElementsWithStyle(
+ EndingVisibleSelection().Start()))
+ return false;
+
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ Node* node_after_insertion_pos =
+ MostForwardCaretPosition(EndingSelection().End()).AnchorNode();
+ Text* text_node = ToText(fragment.FirstChild());
+ // Our fragment creation code handles tabs, spaces, and newlines, so we don't
+ // have to worry about those here.
+
+ Position start = EndingVisibleSelection().Start();
+ Position end = ReplaceSelectedTextInNode(text_node->data());
+ if (end.IsNull())
+ return false;
+
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (node_after_insertion_pos && node_after_insertion_pos->parentNode() &&
+ IsHTMLBRElement(*node_after_insertion_pos) &&
+ ShouldRemoveEndBR(
+ ToHTMLBRElement(node_after_insertion_pos),
+ VisiblePosition::BeforeNode(*node_after_insertion_pos))) {
+ RemoveNodeAndPruneAncestors(node_after_insertion_pos, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ }
+
+ start_of_inserted_range_ = start;
+ end_of_inserted_range_ = end;
+
+ SetEndingSelection(SelectionForUndoStep::From(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtentDeprecated(select_replacement_ ? start : end, end)
+ .Build()));
+
+ return true;
+}
+
+bool ReplaceSelectionCommand::IsReplaceSelectionCommand() const {
+ return true;
+}
+
+EphemeralRange ReplaceSelectionCommand::InsertedRange() const {
+ return EphemeralRange(start_of_inserted_range_, end_of_inserted_range_);
+}
+
+void ReplaceSelectionCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(start_of_inserted_content_);
+ visitor->Trace(end_of_inserted_content_);
+ visitor->Trace(insertion_style_);
+ visitor->Trace(document_fragment_);
+ visitor->Trace(start_of_inserted_range_);
+ visitor->Trace(end_of_inserted_range_);
+ CompositeEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/replace_selection_command.h b/chromium/third_party/blink/renderer/core/editing/commands/replace_selection_command.h
new file mode 100644
index 00000000000..1943d5a456f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/replace_selection_command.h
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REPLACE_SELECTION_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REPLACE_SELECTION_COMMAND_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+namespace blink {
+
+class DocumentFragment;
+class ReplacementFragment;
+
+class CORE_EXPORT ReplaceSelectionCommand final : public CompositeEditCommand {
+ public:
+ enum CommandOption {
+ kSelectReplacement = 1 << 0,
+ kSmartReplace = 1 << 1,
+ kMatchStyle = 1 << 2,
+ kPreventNesting = 1 << 3,
+ kMovingParagraph = 1 << 4,
+ kSanitizeFragment = 1 << 5
+ };
+
+ typedef unsigned CommandOptions;
+
+ static ReplaceSelectionCommand* Create(
+ Document& document,
+ DocumentFragment* fragment,
+ CommandOptions options,
+ InputEvent::InputType input_type = InputEvent::InputType::kNone) {
+ return new ReplaceSelectionCommand(document, fragment, options, input_type);
+ }
+
+ EphemeralRange InsertedRange() const;
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ ReplaceSelectionCommand(Document&,
+ DocumentFragment*,
+ CommandOptions,
+ InputEvent::InputType);
+
+ void DoApply(EditingState*) override;
+ InputEvent::InputType GetInputType() const override;
+ bool IsReplaceSelectionCommand() const override;
+
+ class InsertedNodes {
+ STACK_ALLOCATED();
+
+ public:
+ void RespondToNodeInsertion(Node&);
+ void WillRemoveNodePreservingChildren(Node&);
+ void WillRemoveNode(Node&);
+ void DidReplaceNode(Node&, Node& new_node);
+
+ Node* FirstNodeInserted() const { return first_node_inserted_.Get(); }
+ Node* LastLeafInserted() const {
+ return last_node_inserted_
+ ? &NodeTraversal::LastWithinOrSelf(*last_node_inserted_)
+ : nullptr;
+ }
+ Node* PastLastLeaf() const {
+ return last_node_inserted_
+ ? NodeTraversal::Next(
+ NodeTraversal::LastWithinOrSelf(*last_node_inserted_))
+ : nullptr;
+ }
+ Node* RefNode() const { return ref_node_.Get(); }
+ void SetRefNode(Node* node) { ref_node_ = node; }
+
+ private:
+ Member<Node> first_node_inserted_;
+ Member<Node> last_node_inserted_;
+ Member<Node> ref_node_;
+ };
+
+ Node* InsertAsListItems(HTMLElement* list_element,
+ Element* insertion_block,
+ const Position&,
+ InsertedNodes&,
+ EditingState*);
+
+ void UpdateNodesInserted(Node*);
+ bool ShouldRemoveEndBR(HTMLBRElement*, const VisiblePosition&);
+
+ bool ShouldMergeStart(bool, bool, bool);
+ bool ShouldMergeEnd(bool selection_end_was_end_of_paragraph);
+ bool ShouldMerge(const VisiblePosition&, const VisiblePosition&);
+
+ void MergeEndIfNeeded(EditingState*);
+
+ void RemoveUnrenderedTextNodesAtEnds(InsertedNodes&);
+
+ void RemoveRedundantStylesAndKeepStyleSpanInline(InsertedNodes&,
+ EditingState*);
+ void MakeInsertedContentRoundTrippableWithHTMLTreeBuilder(
+ const InsertedNodes&,
+ EditingState*);
+ void MoveElementOutOfAncestor(Element*, Element* ancestor, EditingState*);
+ void HandleStyleSpans(InsertedNodes&, EditingState*);
+
+ VisiblePosition PositionAtStartOfInsertedContent() const;
+ VisiblePosition PositionAtEndOfInsertedContent() const;
+
+ bool ShouldPerformSmartReplace() const;
+ void AddSpacesForSmartReplace(EditingState*);
+ void CompleteHTMLReplacement(const Position& last_position_to_select,
+ EditingState*);
+ void MergeTextNodesAroundPosition(Position&,
+ Position& position_only_to_be_updated,
+ EditingState*);
+
+ bool PerformTrivialReplace(const ReplacementFragment&, EditingState*);
+ void SetUpStyle(const VisibleSelection&);
+ void InsertParagraphSeparatorIfNeeds(const VisibleSelection&,
+ const ReplacementFragment&,
+ EditingState*);
+
+ Position start_of_inserted_content_;
+ Position end_of_inserted_content_;
+ Member<EditingStyle> insertion_style_;
+ const bool select_replacement_;
+ const bool smart_replace_;
+ bool match_style_;
+ Member<DocumentFragment> document_fragment_;
+ bool prevent_nesting_;
+ const bool moving_paragraph_;
+ InputEvent::InputType input_type_;
+ const bool sanitize_fragment_;
+ bool should_merge_end_;
+
+ Position start_of_inserted_range_;
+ Position end_of_inserted_range_;
+};
+
+DEFINE_TYPE_CASTS(ReplaceSelectionCommand,
+ CompositeEditCommand,
+ command,
+ command->IsReplaceSelectionCommand(),
+ command.IsReplaceSelectionCommand());
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_REPLACE_SELECTION_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/replace_selection_command_test.cc b/chromium/third_party/blink/renderer/core/editing/commands/replace_selection_command_test.cc
new file mode 100644
index 00000000000..3992c835b16
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/replace_selection_command_test.cc
@@ -0,0 +1,190 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/replace_selection_command.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/document_fragment.h"
+#include "third_party/blink/renderer/core/dom/parser_content_policy.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/layout/layout_view.h"
+#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
+
+#include <memory>
+
+namespace blink {
+
+class ReplaceSelectionCommandTest : public EditingTestBase {};
+
+// This is a regression test for https://crbug.com/619131
+TEST_F(ReplaceSelectionCommandTest, pastingEmptySpan) {
+ GetDocument().setDesignMode("on");
+ SetBodyContent("foo");
+
+ LocalFrame* frame = GetDocument().GetFrame();
+ frame->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(GetDocument().body(), 0))
+ .Build());
+
+ DocumentFragment* fragment = GetDocument().createDocumentFragment();
+ fragment->AppendChild(GetDocument().CreateRawElement(HTMLNames::spanTag));
+
+ // |options| are taken from |Editor::replaceSelectionWithFragment()| with
+ // |selectReplacement| and |smartReplace|.
+ ReplaceSelectionCommand::CommandOptions options =
+ ReplaceSelectionCommand::kPreventNesting |
+ ReplaceSelectionCommand::kSanitizeFragment |
+ ReplaceSelectionCommand::kSelectReplacement |
+ ReplaceSelectionCommand::kSmartReplace;
+ ReplaceSelectionCommand* command =
+ ReplaceSelectionCommand::Create(GetDocument(), fragment, options);
+
+ EXPECT_TRUE(command->Apply()) << "the replace command should have succeeded";
+ EXPECT_EQ("foo", GetDocument().body()->InnerHTMLAsString())
+ << "no DOM tree mutation";
+}
+
+// This is a regression test for https://crbug.com/668808
+TEST_F(ReplaceSelectionCommandTest, pasteSpanInText) {
+ GetDocument().SetCompatibilityMode(Document::kQuirksMode);
+ GetDocument().setDesignMode("on");
+ SetBodyContent("<b>text</b>");
+
+ Element* b_element = GetDocument().QuerySelector("b");
+ LocalFrame* frame = GetDocument().GetFrame();
+ frame->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(b_element->firstChild(), 1))
+ .Build());
+
+ DocumentFragment* fragment = GetDocument().createDocumentFragment();
+ fragment->ParseHTML("<span><div>bar</div></span>", b_element);
+
+ ReplaceSelectionCommand::CommandOptions options = 0;
+ ReplaceSelectionCommand* command =
+ ReplaceSelectionCommand::Create(GetDocument(), fragment, options);
+
+ EXPECT_TRUE(command->Apply()) << "the replace command should have succeeded";
+ EXPECT_EQ("<b>t</b>bar<b>ext</b>", GetDocument().body()->InnerHTMLAsString())
+ << "'bar' should have been inserted";
+}
+
+// This is a regression test for https://crbug.com/121163
+TEST_F(ReplaceSelectionCommandTest, styleTagsInPastedHeadIncludedInContent) {
+ GetDocument().setDesignMode("on");
+ UpdateAllLifecyclePhases();
+ GetDummyPageHolder().GetFrame().Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(GetDocument().body(), 0))
+ .Build());
+
+ DocumentFragment* fragment = GetDocument().createDocumentFragment();
+ fragment->ParseHTML(
+ "<head><style>foo { bar: baz; }</style></head>"
+ "<body><p>Text</p></body>",
+ GetDocument().documentElement(), kDisallowScriptingAndPluginContent);
+
+ ReplaceSelectionCommand::CommandOptions options = 0;
+ ReplaceSelectionCommand* command =
+ ReplaceSelectionCommand::Create(GetDocument(), fragment, options);
+ EXPECT_TRUE(command->Apply()) << "the replace command should have succeeded";
+
+ EXPECT_EQ(
+ "<head><style>foo { bar: baz; }</style></head>"
+ "<body><p>Text</p></body>",
+ GetDocument().body()->InnerHTMLAsString())
+ << "the STYLE and P elements should have been pasted into the body "
+ << "of the document";
+}
+
+// Helper function to set autosizing multipliers on a document.
+bool SetTextAutosizingMultiplier(Document* document, float multiplier) {
+ bool multiplier_set = false;
+ for (LayoutObject* layout_object = document->GetLayoutView(); layout_object;
+ layout_object = layout_object->NextInPreOrder()) {
+ if (layout_object->Style()) {
+ scoped_refptr<ComputedStyle> modified_style =
+ ComputedStyle::Clone(layout_object->StyleRef());
+ modified_style->SetTextAutosizingMultiplier(multiplier);
+ EXPECT_EQ(multiplier, modified_style->TextAutosizingMultiplier());
+ layout_object->SetStyleInternal(std::move(modified_style));
+ multiplier_set = true;
+ }
+ }
+ return multiplier_set;
+}
+
+// This is a regression test for https://crbug.com/768261
+TEST_F(ReplaceSelectionCommandTest, TextAutosizingDoesntInflateText) {
+ GetDocument().GetSettings()->SetTextAutosizingEnabled(true);
+ GetDocument().setDesignMode("on");
+ SetBodyContent("<div><span style='font-size: 12px;'>foo bar</span></div>");
+ SetTextAutosizingMultiplier(&GetDocument(), 2.0);
+
+ Element* div = GetDocument().QuerySelector("div");
+ Element* span = GetDocument().QuerySelector("span");
+
+ // Select "bar".
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(span->firstChild(), 4))
+ .Extend(Position(span->firstChild(), 7))
+ .Build());
+
+ DocumentFragment* fragment = GetDocument().createDocumentFragment();
+ fragment->ParseHTML("baz", span);
+
+ ReplaceSelectionCommand::CommandOptions options =
+ ReplaceSelectionCommand::kMatchStyle;
+
+ ReplaceSelectionCommand* command =
+ ReplaceSelectionCommand::Create(GetDocument(), fragment, options);
+
+ EXPECT_TRUE(command->Apply()) << "the replace command should have succeeded";
+ // The span element should not have been split to increase the font size.
+ EXPECT_EQ(1u, div->CountChildren());
+}
+
+// This is a regression test for https://crbug.com/781282
+TEST_F(ReplaceSelectionCommandTest, TrailingNonVisibleTextCrash) {
+ GetDocument().setDesignMode("on");
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<div>^foo|</div>"));
+
+ DocumentFragment* fragment = GetDocument().createDocumentFragment();
+ fragment->ParseHTML("<div>bar</div> ", GetDocument().QuerySelector("div"));
+ ReplaceSelectionCommand::CommandOptions options = 0;
+ ReplaceSelectionCommand* command =
+ ReplaceSelectionCommand::Create(GetDocument(), fragment, options);
+
+ // Crash should not occur on applying ReplaceSelectionCommand
+ EXPECT_FALSE(command->Apply());
+ EXPECT_EQ("<div>bar</div>|<br>", GetSelectionTextFromBody());
+}
+
+// This is a regression test for https://crbug.com/796840
+TEST_F(ReplaceSelectionCommandTest, CrashWithNoSelection) {
+ GetDocument().setDesignMode("on");
+ SetBodyContent("<div></div>");
+ ReplaceSelectionCommand::CommandOptions options = 0;
+ ReplaceSelectionCommand* command =
+ ReplaceSelectionCommand::Create(GetDocument(), 0, options);
+
+ // Crash should not occur on applying ReplaceSelectionCommand
+ EXPECT_FALSE(command->Apply());
+ EXPECT_EQ("<div></div>", GetSelectionTextFromBody());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/selection_for_undo_step.cc b/chromium/third_party/blink/renderer/core/editing/commands/selection_for_undo_step.cc
new file mode 100644
index 00000000000..792d91fff1c
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/selection_for_undo_step.cc
@@ -0,0 +1,125 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/selection_for_undo_step.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/text_affinity.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+
+namespace blink {
+
+SelectionForUndoStep SelectionForUndoStep::From(
+ const SelectionInDOMTree& selection) {
+ SelectionForUndoStep result;
+ result.base_ = selection.Base();
+ result.extent_ = selection.Extent();
+ result.affinity_ = selection.Affinity();
+ result.is_base_first_ = selection.IsBaseFirst();
+ return result;
+}
+
+SelectionForUndoStep::SelectionForUndoStep(const SelectionForUndoStep& other) =
+ default;
+
+SelectionForUndoStep::SelectionForUndoStep() = default;
+
+SelectionForUndoStep& SelectionForUndoStep::operator=(
+ const SelectionForUndoStep& other) = default;
+
+bool SelectionForUndoStep::operator==(const SelectionForUndoStep& other) const {
+ if (IsNone())
+ return other.IsNone();
+ if (other.IsNone())
+ return false;
+ return base_ == other.base_ && extent_ == other.extent_ &&
+ affinity_ == other.affinity_ && is_base_first_ == other.is_base_first_;
+}
+
+bool SelectionForUndoStep::operator!=(const SelectionForUndoStep& other) const {
+ return !operator==(other);
+}
+
+SelectionInDOMTree SelectionForUndoStep::AsSelection() const {
+ if (IsNone()) {
+ return SelectionInDOMTree();
+ }
+ return SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(base_, extent_)
+ .SetAffinity(affinity_)
+ .Build();
+}
+
+Position SelectionForUndoStep::Start() const {
+ return is_base_first_ ? base_ : extent_;
+}
+
+Position SelectionForUndoStep::End() const {
+ return is_base_first_ ? extent_ : base_;
+}
+
+bool SelectionForUndoStep::IsCaret() const {
+ return base_.IsNotNull() && base_ == extent_;
+}
+
+bool SelectionForUndoStep::IsNone() const {
+ return base_.IsNull();
+}
+
+bool SelectionForUndoStep::IsRange() const {
+ return base_ != extent_;
+}
+
+bool SelectionForUndoStep::IsValidFor(const Document& document) const {
+ if (base_.IsNull())
+ return true;
+ return base_.IsValidFor(document) && extent_.IsValidFor(document);
+}
+
+void SelectionForUndoStep::Trace(blink::Visitor* visitor) {
+ visitor->Trace(base_);
+ visitor->Trace(extent_);
+}
+
+// ---
+SelectionForUndoStep::Builder::Builder() = default;
+
+SelectionForUndoStep::Builder&
+SelectionForUndoStep::Builder::SetBaseAndExtentAsBackwardSelection(
+ const Position& base,
+ const Position& extent) {
+ DCHECK(base.IsNotNull());
+ DCHECK(extent.IsNotNull());
+ DCHECK_NE(base, extent);
+ selection_.base_ = base;
+ selection_.extent_ = extent;
+ selection_.is_base_first_ = false;
+ return *this;
+}
+
+SelectionForUndoStep::Builder&
+SelectionForUndoStep::Builder::SetBaseAndExtentAsForwardSelection(
+ const Position& base,
+ const Position& extent) {
+ DCHECK(base.IsNotNull());
+ DCHECK(extent.IsNotNull());
+ DCHECK_NE(base, extent);
+ selection_.base_ = base;
+ selection_.extent_ = extent;
+ selection_.is_base_first_ = true;
+ return *this;
+}
+
+void SelectionForUndoStep::Builder::Trace(blink::Visitor* visitor) {
+ visitor->Trace(selection_);
+}
+
+// ---
+VisibleSelection CreateVisibleSelection(
+ const SelectionForUndoStep& selection_in_undo_step) {
+ return CreateVisibleSelection(selection_in_undo_step.AsSelection());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/selection_for_undo_step.h b/chromium/third_party/blink/renderer/core/editing/commands/selection_for_undo_step.h
new file mode 100644
index 00000000000..6e3e010f510
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/selection_for_undo_step.h
@@ -0,0 +1,99 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SELECTION_FOR_UNDO_STEP_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SELECTION_FOR_UNDO_STEP_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/text_affinity.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+// This class represents selection in |UndoStep|.
+class SelectionForUndoStep final {
+ DISALLOW_NEW();
+
+ public:
+ class Builder;
+
+ // Returns newly constructed |SelectionForUndoStep| from |SelectionInDOMTree|
+ // with computing direction of selection by base <= extent. Thus, computation
+ // time depends O(depth of tree).
+ static SelectionForUndoStep From(const SelectionInDOMTree&);
+
+ SelectionForUndoStep(const SelectionForUndoStep&);
+ SelectionForUndoStep();
+
+ SelectionForUndoStep& operator=(const SelectionForUndoStep&);
+
+ bool operator==(const SelectionForUndoStep&) const;
+ bool operator!=(const SelectionForUndoStep&) const;
+
+ TextAffinity Affinity() const { return affinity_; }
+ Position Base() const { return base_; }
+ Position Extent() const { return extent_; }
+ bool IsBaseFirst() const { return is_base_first_; }
+
+ SelectionInDOMTree AsSelection() const;
+
+ // Selection type predicates
+ bool IsCaret() const;
+ bool IsNone() const;
+ bool IsRange() const;
+
+ // Returns |base_| if |base_ <= extent| at construction time, otherwise
+ // |extent_|.
+ Position Start() const;
+ // Returns |extent_| if |base_ <= extent| at construction time, otherwise
+ // |base_|.
+ Position End() const;
+
+ bool IsValidFor(const Document&) const;
+
+ void Trace(blink::Visitor*);
+
+ private:
+ // |base_| and |extent_| can be disconnected from document.
+ Position base_;
+ Position extent_;
+ TextAffinity affinity_ = TextAffinity::kDownstream;
+ // Note: We should compute |is_base_first_| as construction otherwise we
+ // fail "backward and forward delete" case in "undo-delete-boundary.html".
+ bool is_base_first_ = true;
+};
+
+// Builds |SelectionForUndoStep| object with disconnected position. You should
+// use |SelectionForUndoStep::From()| if positions are connected.
+class SelectionForUndoStep::Builder final {
+ DISALLOW_NEW();
+
+ public:
+ Builder();
+
+ const SelectionForUndoStep& Build() const { return selection_; }
+
+ // |base| and |extent| can be disconnected.
+ Builder& SetBaseAndExtentAsBackwardSelection(const Position& base,
+ const Position& extent);
+
+ // |base| and |extent| can be disconnected.
+ Builder& SetBaseAndExtentAsForwardSelection(const Position& base,
+ const Position& extent);
+
+ void Trace(blink::Visitor*);
+
+ private:
+ SelectionForUndoStep selection_;
+
+ DISALLOW_COPY_AND_ASSIGN(Builder);
+};
+
+VisibleSelection CreateVisibleSelection(const SelectionForUndoStep&);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SELECTION_FOR_UNDO_STEP_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/set_character_data_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/set_character_data_command.cc
new file mode 100644
index 00000000000..0e89b6c66d3
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/set_character_data_command.cc
@@ -0,0 +1,73 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/set_character_data_command.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/layout/layout_text.h"
+
+namespace blink {
+
+SetCharacterDataCommand::SetCharacterDataCommand(Text* node,
+ unsigned offset,
+ unsigned count,
+ const String& text)
+ : SimpleEditCommand(node->GetDocument()),
+ node_(node),
+ offset_(offset),
+ count_(count),
+ new_text_(text) {
+ DCHECK(node_);
+ DCHECK_LE(offset_, node_->length());
+ DCHECK_LE(offset_ + count_, node_->length());
+ // Callers shouldn't be trying to perform no-op replacements
+ DCHECK(!(count == 0 && text.length() == 0));
+}
+
+void SetCharacterDataCommand::DoApply(EditingState*) {
+ // TODO(editing-dev): The use of updateStyleAndLayoutTree()
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutTree();
+ if (!HasEditableStyle(*node_))
+ return;
+
+ DummyExceptionStateForTesting exception_state;
+ previous_text_for_undo_ =
+ node_->substringData(offset_, count_, exception_state);
+ if (exception_state.HadException())
+ return;
+
+ const bool password_echo_enabled =
+ GetDocument().GetSettings() &&
+ GetDocument().GetSettings()->GetPasswordEchoEnabled();
+
+ if (password_echo_enabled) {
+ LayoutText* layout_text = node_->GetLayoutObject();
+ if (layout_text && layout_text->IsSecure()) {
+ layout_text->MomentarilyRevealLastTypedCharacter(offset_ +
+ new_text_.length() - 1);
+ }
+ }
+
+ node_->replaceData(offset_, count_, new_text_, IGNORE_EXCEPTION_FOR_TESTING);
+}
+
+void SetCharacterDataCommand::DoUnapply() {
+ // TODO(editing-dev): The use of updateStyleAndLayoutTree()
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutTree();
+ if (!HasEditableStyle(*node_))
+ return;
+
+ node_->replaceData(offset_, new_text_.length(), previous_text_for_undo_,
+ IGNORE_EXCEPTION_FOR_TESTING);
+}
+
+void SetCharacterDataCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(node_);
+ SimpleEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/set_character_data_command.h b/chromium/third_party/blink/renderer/core/editing/commands/set_character_data_command.h
new file mode 100644
index 00000000000..8170c0ba506
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/set_character_data_command.h
@@ -0,0 +1,42 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SET_CHARACTER_DATA_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SET_CHARACTER_DATA_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/edit_command.h"
+
+namespace blink {
+
+class CORE_EXPORT SetCharacterDataCommand final : public SimpleEditCommand {
+ public:
+ static SetCharacterDataCommand* Create(Text* node,
+ unsigned offset,
+ unsigned count,
+ const String& text) {
+ return new SetCharacterDataCommand(node, offset, count, text);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ SetCharacterDataCommand(Text* node,
+ unsigned offset,
+ unsigned count,
+ const String& text);
+
+ // EditCommand implementation
+ void DoApply(EditingState*) final;
+ void DoUnapply() final;
+
+ const Member<Text> node_;
+ const unsigned offset_;
+ const unsigned count_;
+ String previous_text_for_undo_;
+ const String new_text_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SET_CHARACTER_DATA_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/set_character_data_command_test.cc b/chromium/third_party/blink/renderer/core/editing/commands/set_character_data_command_test.cc
new file mode 100644
index 00000000000..4b3d0806dec
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/set_character_data_command_test.cc
@@ -0,0 +1,120 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/set_character_data_command.h"
+
+#include "third_party/blink/renderer/core/editing/commands/editing_state.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class SetCharacterDataCommandTest : public EditingTestBase {};
+
+TEST_F(SetCharacterDataCommandTest, replaceTextWithSameLength) {
+ SetBodyContent("<div contenteditable>This is a good test case</div>");
+
+ SimpleEditCommand* command = SetCharacterDataCommand::Create(
+ ToText(GetDocument().body()->firstChild()->firstChild()), 10, 4, "lame");
+
+ command->DoReapply();
+ EXPECT_EQ(
+ "This is a lame test case",
+ ToText(GetDocument().body()->firstChild()->firstChild())->wholeText());
+
+ command->DoUnapply();
+ EXPECT_EQ(
+ "This is a good test case",
+ ToText(GetDocument().body()->firstChild()->firstChild())->wholeText());
+}
+
+TEST_F(SetCharacterDataCommandTest, replaceTextWithLongerText) {
+ SetBodyContent("<div contenteditable>This is a good test case</div>");
+
+ SimpleEditCommand* command = SetCharacterDataCommand::Create(
+ ToText(GetDocument().body()->firstChild()->firstChild()), 10, 4, "lousy");
+
+ command->DoReapply();
+ EXPECT_EQ(
+ "This is a lousy test case",
+ ToText(GetDocument().body()->firstChild()->firstChild())->wholeText());
+
+ command->DoUnapply();
+ EXPECT_EQ(
+ "This is a good test case",
+ ToText(GetDocument().body()->firstChild()->firstChild())->wholeText());
+}
+
+TEST_F(SetCharacterDataCommandTest, replaceTextWithShorterText) {
+ SetBodyContent("<div contenteditable>This is a good test case</div>");
+
+ SimpleEditCommand* command = SetCharacterDataCommand::Create(
+ ToText(GetDocument().body()->firstChild()->firstChild()), 10, 4, "meh");
+
+ command->DoReapply();
+ EXPECT_EQ(
+ "This is a meh test case",
+ ToText(GetDocument().body()->firstChild()->firstChild())->wholeText());
+
+ command->DoUnapply();
+ EXPECT_EQ(
+ "This is a good test case",
+ ToText(GetDocument().body()->firstChild()->firstChild())->wholeText());
+}
+
+TEST_F(SetCharacterDataCommandTest, insertTextIntoEmptyNode) {
+ SetBodyContent("<div contenteditable />");
+
+ GetDocument().body()->firstChild()->appendChild(
+ GetDocument().CreateEditingTextNode(""));
+
+ SimpleEditCommand* command = SetCharacterDataCommand::Create(
+ ToText(GetDocument().body()->firstChild()->firstChild()), 0, 0, "hello");
+
+ command->DoReapply();
+ EXPECT_EQ(
+ "hello",
+ ToText(GetDocument().body()->firstChild()->firstChild())->wholeText());
+
+ command->DoUnapply();
+ EXPECT_EQ(
+ "",
+ ToText(GetDocument().body()->firstChild()->firstChild())->wholeText());
+}
+
+TEST_F(SetCharacterDataCommandTest, insertTextAtEndOfNonEmptyNode) {
+ SetBodyContent("<div contenteditable>Hello</div>");
+
+ SimpleEditCommand* command = SetCharacterDataCommand::Create(
+ ToText(GetDocument().body()->firstChild()->firstChild()), 5, 0,
+ ", world!");
+
+ command->DoReapply();
+ EXPECT_EQ(
+ "Hello, world!",
+ ToText(GetDocument().body()->firstChild()->firstChild())->wholeText());
+
+ command->DoUnapply();
+ EXPECT_EQ(
+ "Hello",
+ ToText(GetDocument().body()->firstChild()->firstChild())->wholeText());
+}
+
+TEST_F(SetCharacterDataCommandTest, replaceEntireNode) {
+ SetBodyContent("<div contenteditable>Hello</div>");
+
+ SimpleEditCommand* command = SetCharacterDataCommand::Create(
+ ToText(GetDocument().body()->firstChild()->firstChild()), 0, 5, "Bye");
+
+ command->DoReapply();
+ EXPECT_EQ(
+ "Bye",
+ ToText(GetDocument().body()->firstChild()->firstChild())->wholeText());
+
+ command->DoUnapply();
+ EXPECT_EQ(
+ "Hello",
+ ToText(GetDocument().body()->firstChild()->firstChild())->wholeText());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/set_node_attribute_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/set_node_attribute_command.cc
new file mode 100644
index 00000000000..470fea0eb04
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/set_node_attribute_command.cc
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2005, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/set_node_attribute_command.h"
+
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+
+namespace blink {
+
+SetNodeAttributeCommand::SetNodeAttributeCommand(Element* element,
+ const QualifiedName& attribute,
+ const AtomicString& value)
+ : SimpleEditCommand(element->GetDocument()),
+ element_(element),
+ attribute_(attribute),
+ value_(value) {
+ DCHECK(element_);
+}
+
+void SetNodeAttributeCommand::DoApply(EditingState*) {
+ old_value_ = element_->getAttribute(attribute_);
+ element_->setAttribute(attribute_, value_);
+}
+
+void SetNodeAttributeCommand::DoUnapply() {
+ element_->setAttribute(attribute_, old_value_);
+ old_value_ = g_null_atom;
+}
+
+void SetNodeAttributeCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(element_);
+ SimpleEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/set_node_attribute_command.h b/chromium/third_party/blink/renderer/core/editing/commands/set_node_attribute_command.h
new file mode 100644
index 00000000000..1c08b640c05
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/set_node_attribute_command.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SET_NODE_ATTRIBUTE_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SET_NODE_ATTRIBUTE_COMMAND_H_
+
+#include "third_party/blink/renderer/core/dom/qualified_name.h"
+#include "third_party/blink/renderer/core/editing/commands/edit_command.h"
+
+namespace blink {
+
+class SetNodeAttributeCommand final : public SimpleEditCommand {
+ public:
+ static SetNodeAttributeCommand* Create(Element* element,
+ const QualifiedName& attribute,
+ const AtomicString& value) {
+ return new SetNodeAttributeCommand(element, attribute, value);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ SetNodeAttributeCommand(Element*,
+ const QualifiedName& attribute,
+ const AtomicString& value);
+
+ void DoApply(EditingState*) override;
+ void DoUnapply() override;
+
+ Member<Element> element_;
+ QualifiedName attribute_;
+ AtomicString value_;
+ AtomicString old_value_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SET_NODE_ATTRIBUTE_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/simplify_markup_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/simplify_markup_command.cc
new file mode 100644
index 00000000000..e5f36993792
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/simplify_markup_command.cc
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2012 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/simplify_markup_command.h"
+
+#include "third_party/blink/renderer/core/dom/node_computed_style.h"
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/core/layout/layout_inline.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/core/style/computed_style.h"
+
+namespace blink {
+
+SimplifyMarkupCommand::SimplifyMarkupCommand(Document& document,
+ Node* first_node,
+ Node* node_after_last)
+ : CompositeEditCommand(document),
+ first_node_(first_node),
+ node_after_last_(node_after_last) {}
+
+void SimplifyMarkupCommand::DoApply(EditingState* editing_state) {
+ ContainerNode* root_node = first_node_->parentNode();
+ HeapVector<Member<ContainerNode>> nodes_to_remove;
+
+ // Walk through the inserted nodes, to see if there are elements that could be
+ // removed without affecting the style. The goal is to produce leaner markup
+ // even when starting from a verbose fragment.
+ // We look at inline elements as well as non top level divs that don't have
+ // attributes.
+ for (Node* node = first_node_.Get(); node && node != node_after_last_;
+ node = NodeTraversal::Next(*node)) {
+ if (node->hasChildren() || (node->IsTextNode() && node->nextSibling()))
+ continue;
+
+ ContainerNode* const starting_node = node->parentNode();
+ if (!starting_node)
+ continue;
+ const ComputedStyle* starting_style = starting_node->GetComputedStyle();
+ if (!starting_style)
+ continue;
+ ContainerNode* current_node = starting_node;
+ ContainerNode* top_node_with_starting_style = nullptr;
+ while (current_node != root_node) {
+ if (current_node->parentNode() != root_node &&
+ IsRemovableBlock(current_node))
+ nodes_to_remove.push_back(current_node);
+
+ current_node = current_node->parentNode();
+ if (!current_node)
+ break;
+
+ if (!current_node->GetLayoutObject() ||
+ !current_node->GetLayoutObject()->IsLayoutInline() ||
+ ToLayoutInline(current_node->GetLayoutObject())
+ ->AlwaysCreateLineBoxes())
+ continue;
+
+ if (current_node->firstChild() != current_node->lastChild()) {
+ top_node_with_starting_style = nullptr;
+ break;
+ }
+
+ if (!current_node->GetComputedStyle()
+ ->VisualInvalidationDiff(GetDocument(), *starting_style)
+ .HasDifference())
+ top_node_with_starting_style = current_node;
+ }
+ if (top_node_with_starting_style) {
+ for (Node& node : NodeTraversal::InclusiveAncestorsOf(*starting_node)) {
+ if (node == top_node_with_starting_style)
+ break;
+ nodes_to_remove.push_back(static_cast<ContainerNode*>(&node));
+ }
+ }
+ }
+
+ // we perform all the DOM mutations at once.
+ for (size_t i = 0; i < nodes_to_remove.size(); ++i) {
+ // FIXME: We can do better by directly moving children from
+ // nodesToRemove[i].
+ int num_pruned_ancestors =
+ PruneSubsequentAncestorsToRemove(nodes_to_remove, i, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ if (num_pruned_ancestors < 0)
+ continue;
+ RemoveNodePreservingChildren(nodes_to_remove[i], editing_state,
+ kAssumeContentIsAlwaysEditable);
+ if (editing_state->IsAborted())
+ return;
+ i += num_pruned_ancestors;
+ }
+}
+
+int SimplifyMarkupCommand::PruneSubsequentAncestorsToRemove(
+ HeapVector<Member<ContainerNode>>& nodes_to_remove,
+ size_t start_node_index,
+ EditingState* editing_state) {
+ size_t past_last_node_to_remove = start_node_index + 1;
+ for (; past_last_node_to_remove < nodes_to_remove.size();
+ ++past_last_node_to_remove) {
+ if (nodes_to_remove[past_last_node_to_remove - 1]->parentNode() !=
+ nodes_to_remove[past_last_node_to_remove])
+ break;
+ DCHECK_EQ(nodes_to_remove[past_last_node_to_remove]->firstChild(),
+ nodes_to_remove[past_last_node_to_remove]->lastChild());
+ }
+
+ ContainerNode* highest_ancestor_to_remove =
+ nodes_to_remove[past_last_node_to_remove - 1].Get();
+ ContainerNode* parent = highest_ancestor_to_remove->parentNode();
+ if (!parent) // Parent has already been removed.
+ return -1;
+
+ if (past_last_node_to_remove == start_node_index + 1)
+ return 0;
+
+ RemoveNode(nodes_to_remove[start_node_index], editing_state,
+ kAssumeContentIsAlwaysEditable);
+ if (editing_state->IsAborted())
+ return -1;
+ InsertNodeBefore(nodes_to_remove[start_node_index],
+ highest_ancestor_to_remove, editing_state,
+ kAssumeContentIsAlwaysEditable);
+ if (editing_state->IsAborted())
+ return -1;
+ RemoveNode(highest_ancestor_to_remove, editing_state,
+ kAssumeContentIsAlwaysEditable);
+ if (editing_state->IsAborted())
+ return -1;
+
+ return past_last_node_to_remove - start_node_index - 1;
+}
+
+void SimplifyMarkupCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(first_node_);
+ visitor->Trace(node_after_last_);
+ CompositeEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/simplify_markup_command.h b/chromium/third_party/blink/renderer/core/editing/commands/simplify_markup_command.h
new file mode 100644
index 00000000000..6d9060e74c1
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/simplify_markup_command.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SIMPLIFY_MARKUP_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SIMPLIFY_MARKUP_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+namespace blink {
+
+class SimplifyMarkupCommand final : public CompositeEditCommand {
+ public:
+ static SimplifyMarkupCommand* Create(Document& document,
+ Node* first_node,
+ Node* node_after_last) {
+ return new SimplifyMarkupCommand(document, first_node, node_after_last);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ SimplifyMarkupCommand(Document&, Node* first_node, Node* node_after_last);
+
+ void DoApply(EditingState*) override;
+ int PruneSubsequentAncestorsToRemove(
+ HeapVector<Member<ContainerNode>>& nodes_to_remove,
+ size_t start_node_index,
+ EditingState*);
+
+ Member<Node> first_node_;
+ Member<Node> node_after_last_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SIMPLIFY_MARKUP_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/smart_replace.h b/chromium/third_party/blink/renderer/core/editing/commands/smart_replace.h
new file mode 100644
index 00000000000..6e8f863a83c
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/smart_replace.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2007 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SMART_REPLACE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SMART_REPLACE_H_
+
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+
+namespace blink {
+
+bool IsCharacterSmartReplaceExempt(UChar32, bool is_previous_character);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SMART_REPLACE_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/smart_replace_cf.cc b/chromium/third_party/blink/renderer/core/editing/commands/smart_replace_cf.cc
new file mode 100644
index 00000000000..0dab650a8f9
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/smart_replace_cf.cc
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2007 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/smart_replace.h"
+
+#include <CoreFoundation/CFCharacterSet.h>
+#include <CoreFoundation/CFString.h>
+
+namespace blink {
+
+static CFMutableCharacterSetRef GetSmartSet(bool is_previous_character) {
+ static CFMutableCharacterSetRef pre_smart_set = nullptr;
+ static CFMutableCharacterSetRef post_smart_set = nullptr;
+ CFMutableCharacterSetRef smart_set =
+ is_previous_character ? pre_smart_set : post_smart_set;
+ if (!smart_set) {
+ smart_set = CFCharacterSetCreateMutable(kCFAllocatorDefault);
+ CFCharacterSetAddCharactersInString(
+ smart_set, is_previous_character ? CFSTR("([\"\'#$/-`{")
+ : CFSTR(")].,;:?\'!\"%*-/}"));
+ CFCharacterSetUnion(smart_set, CFCharacterSetGetPredefined(
+ kCFCharacterSetWhitespaceAndNewline));
+ // Adding CJK ranges
+ CFCharacterSetAddCharactersInRange(
+ smart_set, CFRangeMake(0x1100, 256)); // Hangul Jamo (0x1100 - 0x11FF)
+ CFCharacterSetAddCharactersInRange(
+ smart_set,
+ CFRangeMake(0x2E80, 352)); // CJK & Kangxi Radicals (0x2E80 - 0x2FDF)
+ // Ideograph Descriptions, CJK Symbols, Hiragana, Katakana, Bopomofo, Hangul
+ // Compatibility Jamo, Kanbun, & Bopomofo Ext (0x2FF0 - 0x31BF)
+ CFCharacterSetAddCharactersInRange(smart_set, CFRangeMake(0x2FF0, 464));
+ // Enclosed CJK, CJK Ideographs (Uni Han & Ext A), & Yi (0x3200 - 0xA4CF)
+ CFCharacterSetAddCharactersInRange(smart_set, CFRangeMake(0x3200, 29392));
+ CFCharacterSetAddCharactersInRange(
+ smart_set,
+ CFRangeMake(0xAC00, 11183)); // Hangul Syllables (0xAC00 - 0xD7AF)
+ CFCharacterSetAddCharactersInRange(
+ smart_set,
+ CFRangeMake(0xF900,
+ 352)); // CJK Compatibility Ideographs (0xF900 - 0xFA5F)
+ CFCharacterSetAddCharactersInRange(
+ smart_set,
+ CFRangeMake(0xFE30, 32)); // CJK Compatibility From (0xFE30 - 0xFE4F)
+ CFCharacterSetAddCharactersInRange(
+ smart_set,
+ CFRangeMake(0xFF00, 240)); // Half/Full Width Form (0xFF00 - 0xFFEF)
+ CFCharacterSetAddCharactersInRange(
+ smart_set, CFRangeMake(0x20000, 0xA6D7)); // CJK Ideograph Exntension B
+ CFCharacterSetAddCharactersInRange(
+ smart_set,
+ CFRangeMake(
+ 0x2F800,
+ 0x021E)); // CJK Compatibility Ideographs (0x2F800 - 0x2FA1D)
+
+ if (is_previous_character) {
+ pre_smart_set = smart_set;
+ } else {
+ CFCharacterSetUnion(
+ smart_set, CFCharacterSetGetPredefined(kCFCharacterSetPunctuation));
+ post_smart_set = smart_set;
+ }
+ }
+ return smart_set;
+}
+
+bool IsCharacterSmartReplaceExempt(UChar32 c, bool is_previous_character) {
+ return CFCharacterSetIsLongCharacterMember(GetSmartSet(is_previous_character),
+ c);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/smart_replace_icu.cc b/chromium/third_party/blink/renderer/core/editing/commands/smart_replace_icu.cc
new file mode 100644
index 00000000000..896cdbdfcd9
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/smart_replace_icu.cc
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2007 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Tony Chang <idealisms@gmail.com>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/smart_replace.h"
+
+#include "build/build_config.h"
+
+#if !defined(OS_MACOSX)
+#include <unicode/uset.h>
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+
+namespace blink {
+
+static void AddAllCodePoints(USet* smart_set, const String& string) {
+ for (size_t i = 0; i < string.length(); i++)
+ uset_add(smart_set, string[i]);
+}
+
+// This is mostly a port of the code in WebCore/editing/SmartReplaceCF.cpp
+// except we use icu in place of CoreFoundations character classes.
+static USet* GetSmartSet(bool is_previous_character) {
+ static USet* pre_smart_set = nullptr;
+ static USet* post_smart_set = nullptr;
+ USet* smart_set = is_previous_character ? pre_smart_set : post_smart_set;
+ if (!smart_set) {
+ // Whitespace and newline (kCFCharacterSetWhitespaceAndNewline)
+ static const UChar* kWhitespaceAndNewLine = reinterpret_cast<const UChar*>(
+ u"[[:WSpace:] [\\u000A\\u000B\\u000C\\u000D\\u0085]]");
+ UErrorCode ec = U_ZERO_ERROR;
+ smart_set = uset_openPattern(
+ kWhitespaceAndNewLine,
+ LengthOfNullTerminatedString(kWhitespaceAndNewLine), &ec);
+ DCHECK(U_SUCCESS(ec)) << ec;
+
+ // CJK ranges
+ uset_addRange(smart_set, 0x1100,
+ 0x1100 + 256); // Hangul Jamo (0x1100 - 0x11FF)
+ uset_addRange(smart_set, 0x2E80,
+ 0x2E80 + 352); // CJK & Kangxi Radicals (0x2E80 - 0x2FDF)
+ // Ideograph Descriptions, CJK Symbols, Hiragana, Katakana, Bopomofo, Hangul
+ // Compatibility Jamo, Kanbun, & Bopomofo Ext (0x2FF0 - 0x31BF)
+ uset_addRange(smart_set, 0x2FF0, 0x2FF0 + 464);
+ // Enclosed CJK, CJK Ideographs (Uni Han & Ext A), & Yi (0x3200 - 0xA4CF)
+ uset_addRange(smart_set, 0x3200, 0x3200 + 29392);
+ uset_addRange(smart_set, 0xAC00,
+ 0xAC00 + 11183); // Hangul Syllables (0xAC00 - 0xD7AF)
+ uset_addRange(
+ smart_set, 0xF900,
+ 0xF900 + 352); // CJK Compatibility Ideographs (0xF900 - 0xFA5F)
+ uset_addRange(smart_set, 0xFE30,
+ 0xFE30 + 32); // CJK Compatibility From (0xFE30 - 0xFE4F)
+ uset_addRange(smart_set, 0xFF00,
+ 0xFF00 + 240); // Half/Full Width Form (0xFF00 - 0xFFEF)
+ uset_addRange(smart_set, 0x20000,
+ 0x20000 + 0xA6D7); // CJK Ideograph Exntension B
+ uset_addRange(
+ smart_set, 0x2F800,
+ 0x2F800 + 0x021E); // CJK Compatibility Ideographs (0x2F800 - 0x2FA1D)
+
+ if (is_previous_character) {
+ AddAllCodePoints(smart_set, "([\"\'#$/-`{");
+ pre_smart_set = smart_set;
+ } else {
+ AddAllCodePoints(smart_set, ")].,;:?\'!\"%*-/}");
+
+ // Punctuation (kCFCharacterSetPunctuation)
+ static const UChar* kPunctuationClass =
+ reinterpret_cast<const UChar*>(u"[:P:]");
+ UErrorCode ec = U_ZERO_ERROR;
+ USet* icu_punct = uset_openPattern(
+ kPunctuationClass, LengthOfNullTerminatedString(kPunctuationClass),
+ &ec);
+ DCHECK(U_SUCCESS(ec)) << ec;
+ uset_addAll(smart_set, icu_punct);
+ uset_close(icu_punct);
+
+ post_smart_set = smart_set;
+ }
+ }
+ return smart_set;
+}
+
+bool IsCharacterSmartReplaceExempt(UChar32 c, bool is_previous_character) {
+ return uset_contains(GetSmartSet(is_previous_character), c);
+}
+}
+
+#endif // !defined(OS_MACOSX)
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/split_element_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/split_element_command.cc
new file mode 100644
index 00000000000..3687b185a3f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/split_element_command.cc
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2005, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/split_element_command.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+
+namespace blink {
+
+SplitElementCommand::SplitElementCommand(Element* element, Node* at_child)
+ : SimpleEditCommand(element->GetDocument()),
+ element2_(element),
+ at_child_(at_child) {
+ DCHECK(element2_);
+ DCHECK(at_child_);
+ DCHECK_EQ(at_child_->parentNode(), element2_);
+}
+
+void SplitElementCommand::ExecuteApply() {
+ if (at_child_->parentNode() != element2_)
+ return;
+
+ HeapVector<Member<Node>> children;
+ for (Node* node = element2_->firstChild(); node != at_child_;
+ node = node->nextSibling())
+ children.push_back(node);
+
+ DummyExceptionStateForTesting exception_state;
+
+ ContainerNode* parent = element2_->parentNode();
+ if (!parent || !HasEditableStyle(*parent))
+ return;
+ parent->InsertBefore(element1_.Get(), element2_.Get(), exception_state);
+ if (exception_state.HadException())
+ return;
+
+ // Delete id attribute from the second element because the same id cannot be
+ // used for more than one element
+ element2_->removeAttribute(HTMLNames::idAttr);
+
+ for (const auto& child : children)
+ element1_->AppendChild(child, exception_state);
+}
+
+void SplitElementCommand::DoApply(EditingState*) {
+ element1_ = element2_->CloneWithoutChildren();
+
+ ExecuteApply();
+}
+
+void SplitElementCommand::DoUnapply() {
+ if (!element1_ || !HasEditableStyle(*element1_) ||
+ !HasEditableStyle(*element2_))
+ return;
+
+ NodeVector children;
+ GetChildNodes(*element1_, children);
+
+ Node* ref_child = element2_->firstChild();
+
+ for (const auto& child : children)
+ element2_->InsertBefore(child, ref_child, IGNORE_EXCEPTION_FOR_TESTING);
+
+ // Recover the id attribute of the original element.
+ const AtomicString& id = element1_->getAttribute(HTMLNames::idAttr);
+ if (!id.IsNull())
+ element2_->setAttribute(HTMLNames::idAttr, id);
+
+ element1_->remove(IGNORE_EXCEPTION_FOR_TESTING);
+}
+
+void SplitElementCommand::DoReapply() {
+ if (!element1_)
+ return;
+
+ ExecuteApply();
+}
+
+void SplitElementCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(element1_);
+ visitor->Trace(element2_);
+ visitor->Trace(at_child_);
+ SimpleEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/split_element_command.h b/chromium/third_party/blink/renderer/core/editing/commands/split_element_command.h
new file mode 100644
index 00000000000..a4ca79033f8
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/split_element_command.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SPLIT_ELEMENT_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SPLIT_ELEMENT_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/edit_command.h"
+
+namespace blink {
+
+class SplitElementCommand final : public SimpleEditCommand {
+ public:
+ static SplitElementCommand* Create(Element* element,
+ Node* split_point_child) {
+ return new SplitElementCommand(element, split_point_child);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ SplitElementCommand(Element*, Node* split_point_child);
+
+ void DoApply(EditingState*) override;
+ void DoUnapply() override;
+ void DoReapply() override;
+ void ExecuteApply();
+
+ Member<Element> element1_;
+ Member<Element> element2_;
+ Member<Node> at_child_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SPLIT_ELEMENT_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/split_text_node_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/split_text_node_command.cc
new file mode 100644
index 00000000000..53c6ae850eb
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/split_text_node_command.cc
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2005, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/split_text_node_command.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+
+namespace blink {
+
+SplitTextNodeCommand::SplitTextNodeCommand(Text* text, int offset)
+ : SimpleEditCommand(text->GetDocument()), text2_(text), offset_(offset) {
+ // NOTE: Various callers rely on the fact that the original node becomes
+ // the second node (i.e. the new node is inserted before the existing one).
+ // That is not a fundamental dependency (i.e. it could be re-coded), but
+ // rather is based on how this code happens to work.
+ DCHECK(text2_);
+ DCHECK_GT(text2_->length(), 0u);
+ DCHECK_GT(offset_, 0u);
+ DCHECK_LT(offset_, text2_->length());
+}
+
+void SplitTextNodeCommand::DoApply(EditingState*) {
+ ContainerNode* parent = text2_->parentNode();
+ if (!parent || !HasEditableStyle(*parent))
+ return;
+
+ String prefix_text =
+ text2_->substringData(0, offset_, IGNORE_EXCEPTION_FOR_TESTING);
+ if (prefix_text.IsEmpty())
+ return;
+
+ text1_ = Text::Create(GetDocument(), prefix_text);
+ DCHECK(text1_);
+ GetDocument().Markers().MoveMarkers(text2_.Get(), offset_, text1_.Get());
+
+ InsertText1AndTrimText2();
+}
+
+void SplitTextNodeCommand::DoUnapply() {
+ if (!text1_ || !HasEditableStyle(*text1_))
+ return;
+
+ DCHECK_EQ(text1_->GetDocument(), GetDocument());
+
+ String prefix_text = text1_->data();
+
+ text2_->insertData(0, prefix_text, ASSERT_NO_EXCEPTION);
+ GetDocument().UpdateStyleAndLayout();
+
+ GetDocument().Markers().MoveMarkers(text1_.Get(), prefix_text.length(),
+ text2_.Get());
+ text1_->remove(ASSERT_NO_EXCEPTION);
+}
+
+void SplitTextNodeCommand::DoReapply() {
+ if (!text1_ || !text2_)
+ return;
+
+ ContainerNode* parent = text2_->parentNode();
+ if (!parent || !HasEditableStyle(*parent))
+ return;
+
+ GetDocument().Markers().MoveMarkers(text2_.Get(), offset_, text1_.Get());
+
+ InsertText1AndTrimText2();
+}
+
+void SplitTextNodeCommand::InsertText1AndTrimText2() {
+ DummyExceptionStateForTesting exception_state;
+ text2_->parentNode()->InsertBefore(text1_.Get(), text2_.Get(),
+ exception_state);
+ if (exception_state.HadException())
+ return;
+ text2_->deleteData(0, offset_, exception_state);
+ GetDocument().UpdateStyleAndLayout();
+}
+
+void SplitTextNodeCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(text1_);
+ visitor->Trace(text2_);
+ SimpleEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/split_text_node_command.h b/chromium/third_party/blink/renderer/core/editing/commands/split_text_node_command.h
new file mode 100644
index 00000000000..cbfb6eeee72
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/split_text_node_command.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SPLIT_TEXT_NODE_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SPLIT_TEXT_NODE_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/edit_command.h"
+
+namespace blink {
+
+class Text;
+
+class CORE_EXPORT SplitTextNodeCommand final : public SimpleEditCommand {
+ public:
+ static SplitTextNodeCommand* Create(Text* node, int offset) {
+ return new SplitTextNodeCommand(node, offset);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ SplitTextNodeCommand(Text*, int offset);
+
+ void DoApply(EditingState*) override;
+ void DoUnapply() override;
+ void DoReapply() override;
+ void InsertText1AndTrimText2();
+
+ Member<Text> text1_;
+ Member<Text> text2_;
+ unsigned offset_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SPLIT_TEXT_NODE_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/split_text_node_command_test.cc b/chromium/third_party/blink/renderer/core/editing/commands/split_text_node_command_test.cc
new file mode 100644
index 00000000000..c3b9f0d28ea
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/split_text_node_command_test.cc
@@ -0,0 +1,98 @@
+// Copyright (c) 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/split_text_node_command.h"
+
+#include "third_party/blink/renderer/core/editing/commands/editing_state.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+#include "third_party/blink/renderer/core/editing/plain_text_range.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class SplitTextNodeCommandTest : public EditingTestBase {};
+
+TEST_F(SplitTextNodeCommandTest, splitInMarkerInterior) {
+ SetBodyContent("<div contenteditable>test1 test2 test3</div>");
+
+ ContainerNode* div = ToContainerNode(GetDocument().body()->firstChild());
+
+ EphemeralRange range = PlainTextRange(0, 5).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ range, TextMatchMarker::MatchStatus::kInactive);
+
+ range = PlainTextRange(6, 11).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ range, TextMatchMarker::MatchStatus::kInactive);
+
+ range = PlainTextRange(12, 17).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ range, TextMatchMarker::MatchStatus::kInactive);
+
+ SimpleEditCommand* command = SplitTextNodeCommand::Create(
+ ToText(GetDocument().body()->firstChild()->firstChild()), 8);
+
+ EditingState editingState;
+ command->DoApply(&editingState);
+
+ Node* text1 = ToText(div->firstChild());
+ Node* text2 = ToText(text1->nextSibling());
+
+ // The first marker should end up in text1, the second marker should be
+ // truncated and end up text1, the third marker should end up in text2
+ // and its offset shifted to remain on the same piece of text
+
+ EXPECT_EQ(2u, GetDocument().Markers().MarkersFor(text1).size());
+
+ EXPECT_EQ(0u, GetDocument().Markers().MarkersFor(text1)[0]->StartOffset());
+ EXPECT_EQ(5u, GetDocument().Markers().MarkersFor(text1)[0]->EndOffset());
+
+ EXPECT_EQ(6u, GetDocument().Markers().MarkersFor(text1)[1]->StartOffset());
+ EXPECT_EQ(7u, GetDocument().Markers().MarkersFor(text1)[1]->EndOffset());
+
+ EXPECT_EQ(1u, GetDocument().Markers().MarkersFor(text2).size());
+
+ EXPECT_EQ(4u, GetDocument().Markers().MarkersFor(text2)[0]->StartOffset());
+ EXPECT_EQ(9u, GetDocument().Markers().MarkersFor(text2)[0]->EndOffset());
+
+ // Test undo
+ command->DoUnapply();
+
+ Node* text = ToText(div->firstChild());
+
+ EXPECT_EQ(3u, GetDocument().Markers().MarkersFor(text).size());
+
+ EXPECT_EQ(0u, GetDocument().Markers().MarkersFor(text)[0]->StartOffset());
+ EXPECT_EQ(5u, GetDocument().Markers().MarkersFor(text)[0]->EndOffset());
+
+ // TODO(rlanday): the truncated marker that spanned node boundaries is not
+ // restored properly
+ EXPECT_EQ(6u, GetDocument().Markers().MarkersFor(text)[1]->StartOffset());
+ EXPECT_EQ(7u, GetDocument().Markers().MarkersFor(text)[1]->EndOffset());
+
+ EXPECT_EQ(12u, GetDocument().Markers().MarkersFor(text)[2]->StartOffset());
+ EXPECT_EQ(17u, GetDocument().Markers().MarkersFor(text)[2]->EndOffset());
+
+ // Test redo
+ command->DoReapply();
+
+ text1 = ToText(div->firstChild());
+ text2 = ToText(text1->nextSibling());
+
+ EXPECT_EQ(2u, GetDocument().Markers().MarkersFor(text1).size());
+
+ EXPECT_EQ(0u, GetDocument().Markers().MarkersFor(text1)[0]->StartOffset());
+ EXPECT_EQ(5u, GetDocument().Markers().MarkersFor(text1)[0]->EndOffset());
+
+ EXPECT_EQ(6u, GetDocument().Markers().MarkersFor(text1)[1]->StartOffset());
+ EXPECT_EQ(7u, GetDocument().Markers().MarkersFor(text1)[1]->EndOffset());
+
+ EXPECT_EQ(1u, GetDocument().Markers().MarkersFor(text2).size());
+
+ EXPECT_EQ(4u, GetDocument().Markers().MarkersFor(text2)[0]->StartOffset());
+ EXPECT_EQ(9u, GetDocument().Markers().MarkersFor(text2)[0]->EndOffset());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/split_text_node_containing_element_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/split_text_node_containing_element_command.cc
new file mode 100644
index 00000000000..d841eb31e03
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/split_text_node_containing_element_command.cc
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2005, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/split_text_node_containing_element_command.h"
+
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+
+namespace blink {
+
+SplitTextNodeContainingElementCommand::SplitTextNodeContainingElementCommand(
+ Text* text,
+ int offset)
+ : CompositeEditCommand(text->GetDocument()), text_(text), offset_(offset) {
+ DCHECK(text_);
+ DCHECK_GT(text_->length(), 0u);
+}
+
+void SplitTextNodeContainingElementCommand::DoApply(EditingState*) {
+ DCHECK(text_);
+ DCHECK_GT(offset_, 0);
+
+ SplitTextNode(text_.Get(), offset_);
+
+ Element* parent = text_->parentElement();
+ if (!parent || !parent->parentElement() ||
+ !HasEditableStyle(*parent->parentElement()))
+ return;
+
+ LayoutObject* parent_layout_object = parent->GetLayoutObject();
+ if (!parent_layout_object || !parent_layout_object->IsInline()) {
+ WrapContentsInDummySpan(parent);
+ Node* first_child = parent->firstChild();
+ if (!first_child || !first_child->IsElementNode())
+ return;
+ parent = ToElement(first_child);
+ }
+
+ SplitElement(parent, text_.Get());
+}
+
+void SplitTextNodeContainingElementCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(text_);
+ CompositeEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/split_text_node_containing_element_command.h b/chromium/third_party/blink/renderer/core/editing/commands/split_text_node_containing_element_command.h
new file mode 100644
index 00000000000..7de088c7c4e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/split_text_node_containing_element_command.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SPLIT_TEXT_NODE_CONTAINING_ELEMENT_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SPLIT_TEXT_NODE_CONTAINING_ELEMENT_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+namespace blink {
+
+class SplitTextNodeContainingElementCommand final
+ : public CompositeEditCommand {
+ public:
+ static SplitTextNodeContainingElementCommand* Create(Text* node, int offset) {
+ return new SplitTextNodeContainingElementCommand(node, offset);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ SplitTextNodeContainingElementCommand(Text*, int offset);
+
+ void DoApply(EditingState*) override;
+
+ Member<Text> text_;
+ int offset_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_SPLIT_TEXT_NODE_CONTAINING_ELEMENT_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/style_commands.cc b/chromium/third_party/blink/renderer/core/editing/commands/style_commands.cc
new file mode 100644
index 00000000000..54be08d2d4d
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/style_commands.cc
@@ -0,0 +1,609 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
+ * Copyright (C) 2009 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/style_commands.h"
+
+#include "third_party/blink/renderer/core/css/css_computed_style_declaration.h"
+#include "third_party/blink/renderer/core/css/css_identifier_value.h"
+#include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/css/css_value_list.h"
+#include "third_party/blink/renderer/core/editing/commands/apply_style_command.h"
+#include "third_party/blink/renderer/core/editing/editing_style_utilities.h"
+#include "third_party/blink/renderer/core/editing/editing_tri_state.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/writing_direction.h"
+#include "third_party/blink/renderer/core/html/html_font_element.h"
+
+namespace blink {
+
+void StyleCommands::ApplyStyle(LocalFrame& frame,
+ CSSPropertyValueSet* style,
+ InputEvent::InputType input_type) {
+ const VisibleSelection& selection =
+ frame.Selection().ComputeVisibleSelectionInDOMTreeDeprecated();
+ if (selection.IsNone())
+ return;
+ if (selection.IsCaret()) {
+ frame.GetEditor().ComputeAndSetTypingStyle(style, input_type);
+ return;
+ }
+ DCHECK(selection.IsRange()) << selection;
+ if (!style)
+ return;
+ DCHECK(frame.GetDocument());
+ ApplyStyleCommand::Create(*frame.GetDocument(), EditingStyle::Create(style),
+ input_type)
+ ->Apply();
+}
+
+void StyleCommands::ApplyStyleToSelection(LocalFrame& frame,
+ CSSPropertyValueSet* style,
+ InputEvent::InputType input_type) {
+ if (!style || style->IsEmpty() || !frame.GetEditor().CanEditRichly())
+ return;
+
+ ApplyStyle(frame, style, input_type);
+}
+
+bool StyleCommands::ApplyCommandToFrame(LocalFrame& frame,
+ EditorCommandSource source,
+ InputEvent::InputType input_type,
+ CSSPropertyValueSet* style) {
+ // TODO(editnig-dev): We don't call shouldApplyStyle when the source is DOM;
+ // is there a good reason for that?
+ switch (source) {
+ case EditorCommandSource::kMenuOrKeyBinding:
+ ApplyStyleToSelection(frame, style, input_type);
+ return true;
+ case EditorCommandSource::kDOM:
+ ApplyStyle(frame, style, input_type);
+ return true;
+ }
+ NOTREACHED();
+ return false;
+}
+
+bool StyleCommands::ExecuteApplyStyle(LocalFrame& frame,
+ EditorCommandSource source,
+ InputEvent::InputType input_type,
+ CSSPropertyID property_id,
+ const String& property_value) {
+ DCHECK(frame.GetDocument());
+ MutableCSSPropertyValueSet* const style =
+ MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ style->SetProperty(property_id, property_value, /* important */ false,
+ frame.GetDocument()->GetSecureContextMode());
+ return ApplyCommandToFrame(frame, source, input_type, style);
+}
+
+bool StyleCommands::ExecuteApplyStyle(LocalFrame& frame,
+ EditorCommandSource source,
+ InputEvent::InputType input_type,
+ CSSPropertyID property_id,
+ CSSValueID property_value) {
+ MutableCSSPropertyValueSet* const style =
+ MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ style->SetProperty(property_id, property_value);
+ return ApplyCommandToFrame(frame, source, input_type, style);
+}
+
+bool StyleCommands::ExecuteBackColor(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String& value) {
+ return ExecuteApplyStyle(frame, source, InputEvent::InputType::kNone,
+ CSSPropertyBackgroundColor, value);
+}
+
+bool StyleCommands::ExecuteForeColor(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String& value) {
+ return ExecuteApplyStyle(frame, source, InputEvent::InputType::kNone,
+ CSSPropertyColor, value);
+}
+
+bool StyleCommands::ExecuteFontName(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String& value) {
+ return ExecuteApplyStyle(frame, source, InputEvent::InputType::kNone,
+ CSSPropertyFontFamily, value);
+}
+
+bool StyleCommands::ExecuteFontSize(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String& value) {
+ CSSValueID size;
+ if (!HTMLFontElement::CssValueFromFontSizeNumber(value, size))
+ return false;
+ return ExecuteApplyStyle(frame, source, InputEvent::InputType::kNone,
+ CSSPropertyFontSize, size);
+}
+
+bool StyleCommands::ExecuteFontSizeDelta(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String& value) {
+ return ExecuteApplyStyle(frame, source, InputEvent::InputType::kNone,
+ CSSPropertyWebkitFontSizeDelta, value);
+}
+
+bool StyleCommands::ExecuteMakeTextWritingDirectionLeftToRight(
+ LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ MutableCSSPropertyValueSet* const style =
+ MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ style->SetProperty(CSSPropertyUnicodeBidi, CSSValueIsolate);
+ style->SetProperty(CSSPropertyDirection, CSSValueLtr);
+ ApplyStyle(frame, style, InputEvent::InputType::kFormatSetBlockTextDirection);
+ return true;
+}
+
+bool StyleCommands::ExecuteMakeTextWritingDirectionNatural(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ MutableCSSPropertyValueSet* const style =
+ MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ style->SetProperty(CSSPropertyUnicodeBidi, CSSValueNormal);
+ ApplyStyle(frame, style, InputEvent::InputType::kFormatSetBlockTextDirection);
+ return true;
+}
+
+bool StyleCommands::ExecuteMakeTextWritingDirectionRightToLeft(
+ LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String&) {
+ MutableCSSPropertyValueSet* const style =
+ MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ style->SetProperty(CSSPropertyUnicodeBidi, CSSValueIsolate);
+ style->SetProperty(CSSPropertyDirection, CSSValueRtl);
+ ApplyStyle(frame, style, InputEvent::InputType::kFormatSetBlockTextDirection);
+ return true;
+}
+
+bool StyleCommands::SelectionStartHasStyle(LocalFrame& frame,
+ CSSPropertyID property_id,
+ const String& value) {
+ const SecureContextMode secure_context_mode =
+ frame.GetDocument()->GetSecureContextMode();
+
+ EditingStyle* const style_to_check =
+ EditingStyle::Create(property_id, value, secure_context_mode);
+ EditingStyle* const style_at_start =
+ EditingStyleUtilities::CreateStyleAtSelectionStart(
+ frame.Selection().ComputeVisibleSelectionInDOMTreeDeprecated(),
+ property_id == CSSPropertyBackgroundColor, style_to_check->Style());
+ return style_to_check->TriStateOfStyle(style_at_start, secure_context_mode) !=
+ EditingTriState::kFalse;
+}
+
+bool StyleCommands::ExecuteToggleStyle(LocalFrame& frame,
+ EditorCommandSource source,
+ InputEvent::InputType input_type,
+ CSSPropertyID property_id,
+ const char* off_value,
+ const char* on_value) {
+ // Style is considered present when
+ // Mac: present at the beginning of selection
+ // other: present throughout the selection
+ const bool style_is_present =
+ frame.GetEditor().Behavior().ShouldToggleStyleBasedOnStartOfSelection()
+ ? SelectionStartHasStyle(frame, property_id, on_value)
+ : EditingStyle::SelectionHasStyle(frame, property_id, on_value) ==
+ EditingTriState::kTrue;
+
+ EditingStyle* const style =
+ EditingStyle::Create(property_id, style_is_present ? off_value : on_value,
+ frame.GetDocument()->GetSecureContextMode());
+ return ApplyCommandToFrame(frame, source, input_type, style->Style());
+}
+
+bool StyleCommands::ExecuteToggleBold(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ return ExecuteToggleStyle(frame, source, InputEvent::InputType::kFormatBold,
+ CSSPropertyFontWeight, "normal", "bold");
+}
+
+bool StyleCommands::ExecuteToggleItalic(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ return ExecuteToggleStyle(frame, source, InputEvent::InputType::kFormatItalic,
+ CSSPropertyFontStyle, "normal", "italic");
+}
+
+bool StyleCommands::ExecuteSubscript(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ return ExecuteToggleStyle(frame, source,
+ InputEvent::InputType::kFormatSubscript,
+ CSSPropertyVerticalAlign, "baseline", "sub");
+}
+
+bool StyleCommands::ExecuteSuperscript(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ return ExecuteToggleStyle(frame, source,
+ InputEvent::InputType::kFormatSuperscript,
+ CSSPropertyVerticalAlign, "baseline", "super");
+}
+
+bool StyleCommands::ExecuteUnscript(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ return ExecuteApplyStyle(frame, source, InputEvent::InputType::kNone,
+ CSSPropertyVerticalAlign, "baseline");
+}
+
+String StyleCommands::ComputeToggleStyleInList(EditingStyle& selection_style,
+ CSSPropertyID property_id,
+ const CSSValue& value) {
+ const CSSValue& selected_css_value =
+ *selection_style.Style()->GetPropertyCSSValue(property_id);
+ if (selected_css_value.IsValueList()) {
+ CSSValueList& selected_css_value_list =
+ *ToCSSValueList(selected_css_value).Copy();
+ if (!selected_css_value_list.RemoveAll(value))
+ selected_css_value_list.Append(value);
+ if (selected_css_value_list.length())
+ return selected_css_value_list.CssText();
+ } else if (selected_css_value.CssText() == "none") {
+ return value.CssText();
+ }
+ return "none";
+}
+
+bool StyleCommands::ExecuteToggleStyleInList(LocalFrame& frame,
+ EditorCommandSource source,
+ InputEvent::InputType input_type,
+ CSSPropertyID property_id,
+ const CSSValue& value) {
+ EditingStyle* const selection_style =
+ EditingStyleUtilities::CreateStyleAtSelectionStart(
+ frame.Selection().ComputeVisibleSelectionInDOMTree());
+ if (!selection_style || !selection_style->Style())
+ return false;
+
+ const String new_style =
+ ComputeToggleStyleInList(*selection_style, property_id, value);
+
+ // TODO(editnig-dev): We shouldn't be having to convert new style into text.
+ // We should have setPropertyCSSValue.
+ MutableCSSPropertyValueSet* const new_mutable_style =
+ MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ new_mutable_style->SetProperty(property_id, new_style, /* important */ false,
+ frame.GetDocument()->GetSecureContextMode());
+ return ApplyCommandToFrame(frame, source, input_type, new_mutable_style);
+}
+
+bool StyleCommands::ExecuteStrikethrough(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ const CSSIdentifierValue& line_through =
+ *CSSIdentifierValue::Create(CSSValueLineThrough);
+ return ExecuteToggleStyleInList(
+ frame, source, InputEvent::InputType::kFormatStrikeThrough,
+ CSSPropertyWebkitTextDecorationsInEffect, line_through);
+}
+
+bool StyleCommands::ExecuteUnderline(LocalFrame& frame,
+ Event*,
+ EditorCommandSource source,
+ const String&) {
+ const CSSIdentifierValue& underline =
+ *CSSIdentifierValue::Create(CSSValueUnderline);
+ return ExecuteToggleStyleInList(
+ frame, source, InputEvent::InputType::kFormatUnderline,
+ CSSPropertyWebkitTextDecorationsInEffect, underline);
+}
+
+bool StyleCommands::ExecuteStyleWithCSS(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String& value) {
+ frame.GetEditor().SetShouldStyleWithCSS(
+ !DeprecatedEqualIgnoringCase(value, "false"));
+ return true;
+}
+
+bool StyleCommands::ExecuteUseCSS(LocalFrame& frame,
+ Event*,
+ EditorCommandSource,
+ const String& value) {
+ frame.GetEditor().SetShouldStyleWithCSS(
+ DeprecatedEqualIgnoringCase(value, "false"));
+ return true;
+}
+
+// State functions
+EditingTriState StyleCommands::StateStyle(LocalFrame& frame,
+ CSSPropertyID property_id,
+ const char* desired_value) {
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (frame.GetEditor().Behavior().ShouldToggleStyleBasedOnStartOfSelection()) {
+ return SelectionStartHasStyle(frame, property_id, desired_value)
+ ? EditingTriState::kTrue
+ : EditingTriState::kFalse;
+ }
+ return EditingStyle::SelectionHasStyle(frame, property_id, desired_value);
+}
+
+EditingTriState StyleCommands::StateBold(LocalFrame& frame, Event*) {
+ return StateStyle(frame, CSSPropertyFontWeight, "bold");
+}
+
+EditingTriState StyleCommands::StateItalic(LocalFrame& frame, Event*) {
+ return StateStyle(frame, CSSPropertyFontStyle, "italic");
+}
+
+EditingTriState StyleCommands::StateStrikethrough(LocalFrame& frame, Event*) {
+ return StateStyle(frame, CSSPropertyWebkitTextDecorationsInEffect,
+ "line-through");
+}
+
+EditingTriState StyleCommands::StateStyleWithCSS(LocalFrame& frame, Event*) {
+ return frame.GetEditor().ShouldStyleWithCSS() ? EditingTriState::kTrue
+ : EditingTriState::kFalse;
+}
+
+EditingTriState StyleCommands::StateSubscript(LocalFrame& frame, Event*) {
+ return StateStyle(frame, CSSPropertyVerticalAlign, "sub");
+}
+
+EditingTriState StyleCommands::StateSuperscript(LocalFrame& frame, Event*) {
+ return StateStyle(frame, CSSPropertyVerticalAlign, "super");
+}
+
+bool StyleCommands::IsUnicodeBidiNestedOrMultipleEmbeddings(
+ CSSValueID value_id) {
+ return value_id == CSSValueEmbed || value_id == CSSValueBidiOverride ||
+ value_id == CSSValueWebkitIsolate ||
+ value_id == CSSValueWebkitIsolateOverride ||
+ value_id == CSSValueWebkitPlaintext || value_id == CSSValueIsolate ||
+ value_id == CSSValueIsolateOverride || value_id == CSSValuePlaintext;
+}
+
+WritingDirection StyleCommands::TextDirectionForSelection(
+ const VisibleSelection& selection,
+ EditingStyle* typing_style,
+ bool& has_nested_or_multiple_embeddings) {
+ has_nested_or_multiple_embeddings = true;
+
+ if (selection.IsNone())
+ return WritingDirection::kNatural;
+
+ const Position position = MostForwardCaretPosition(selection.Start());
+
+ const Node* anchor_node = position.AnchorNode();
+ if (!anchor_node)
+ return WritingDirection::kNatural;
+
+ Position end;
+ if (selection.IsRange()) {
+ end = MostBackwardCaretPosition(selection.End());
+
+ DCHECK(end.GetDocument());
+ const EphemeralRange caret_range(position.ParentAnchoredEquivalent(),
+ end.ParentAnchoredEquivalent());
+ for (Node& node : caret_range.Nodes()) {
+ if (!node.IsStyledElement())
+ continue;
+
+ const CSSComputedStyleDeclaration& style =
+ *CSSComputedStyleDeclaration::Create(&node);
+ const CSSValue* unicode_bidi =
+ style.GetPropertyCSSValue(GetCSSPropertyUnicodeBidi());
+ if (!unicode_bidi || !unicode_bidi->IsIdentifierValue())
+ continue;
+
+ const CSSValueID unicode_bidi_value =
+ ToCSSIdentifierValue(unicode_bidi)->GetValueID();
+ if (IsUnicodeBidiNestedOrMultipleEmbeddings(unicode_bidi_value))
+ return WritingDirection::kNatural;
+ }
+ }
+
+ if (selection.IsCaret()) {
+ WritingDirection direction;
+ if (typing_style && typing_style->GetTextDirection(direction)) {
+ has_nested_or_multiple_embeddings = false;
+ return direction;
+ }
+ anchor_node = selection.VisibleStart().DeepEquivalent().AnchorNode();
+ }
+ DCHECK(anchor_node);
+
+ // The selection is either a caret with no typing attributes or a range in
+ // which no embedding is added, so just use the start position to decide.
+ const Node* block = EnclosingBlock(anchor_node);
+ WritingDirection found_direction = WritingDirection::kNatural;
+
+ for (Node& runner : NodeTraversal::InclusiveAncestorsOf(*anchor_node)) {
+ if (runner == block)
+ break;
+ if (!runner.IsStyledElement())
+ continue;
+
+ Element* element = &ToElement(runner);
+ const CSSComputedStyleDeclaration& style =
+ *CSSComputedStyleDeclaration::Create(element);
+ const CSSValue* unicode_bidi =
+ style.GetPropertyCSSValue(GetCSSPropertyUnicodeBidi());
+ if (!unicode_bidi || !unicode_bidi->IsIdentifierValue())
+ continue;
+
+ const CSSValueID unicode_bidi_value =
+ ToCSSIdentifierValue(unicode_bidi)->GetValueID();
+ if (unicode_bidi_value == CSSValueNormal)
+ continue;
+
+ if (unicode_bidi_value == CSSValueBidiOverride)
+ return WritingDirection::kNatural;
+
+ DCHECK(EditingStyleUtilities::IsEmbedOrIsolate(unicode_bidi_value))
+ << unicode_bidi_value;
+ const CSSValue* direction =
+ style.GetPropertyCSSValue(GetCSSPropertyDirection());
+ if (!direction || !direction->IsIdentifierValue())
+ continue;
+
+ const int direction_value = ToCSSIdentifierValue(direction)->GetValueID();
+ if (direction_value != CSSValueLtr && direction_value != CSSValueRtl)
+ continue;
+
+ if (found_direction != WritingDirection::kNatural)
+ return WritingDirection::kNatural;
+
+ // In the range case, make sure that the embedding element persists until
+ // the end of the range.
+ if (selection.IsRange() && !end.AnchorNode()->IsDescendantOf(element))
+ return WritingDirection::kNatural;
+
+ found_direction = direction_value == CSSValueLtr
+ ? WritingDirection::kLeftToRight
+ : WritingDirection::kRightToLeft;
+ }
+ has_nested_or_multiple_embeddings = false;
+ return found_direction;
+}
+
+EditingTriState StyleCommands::StateTextWritingDirection(
+ LocalFrame& frame,
+ WritingDirection direction) {
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ bool has_nested_or_multiple_embeddings;
+ WritingDirection selection_direction = TextDirectionForSelection(
+ frame.Selection().ComputeVisibleSelectionInDOMTreeDeprecated(),
+ frame.GetEditor().TypingStyle(), has_nested_or_multiple_embeddings);
+ // TODO(editnig-dev): We should be returning MixedTriState when
+ // selectionDirection == direction && hasNestedOrMultipleEmbeddings
+ return (selection_direction == direction &&
+ !has_nested_or_multiple_embeddings)
+ ? EditingTriState::kTrue
+ : EditingTriState::kFalse;
+}
+
+EditingTriState StyleCommands::StateTextWritingDirectionLeftToRight(
+ LocalFrame& frame,
+ Event*) {
+ return StateTextWritingDirection(frame, WritingDirection::kLeftToRight);
+}
+
+EditingTriState StyleCommands::StateTextWritingDirectionNatural(
+ LocalFrame& frame,
+ Event*) {
+ return StateTextWritingDirection(frame, WritingDirection::kNatural);
+}
+
+EditingTriState StyleCommands::StateTextWritingDirectionRightToLeft(
+ LocalFrame& frame,
+ Event*) {
+ return StateTextWritingDirection(frame, WritingDirection::kRightToLeft);
+}
+
+EditingTriState StyleCommands::StateUnderline(LocalFrame& frame, Event*) {
+ return StateStyle(frame, CSSPropertyWebkitTextDecorationsInEffect,
+ "underline");
+}
+
+// Value functions
+String StyleCommands::SelectionStartCSSPropertyValue(
+ LocalFrame& frame,
+ CSSPropertyID property_id) {
+ EditingStyle* const selection_style =
+ EditingStyleUtilities::CreateStyleAtSelectionStart(
+ frame.Selection().ComputeVisibleSelectionInDOMTreeDeprecated(),
+ property_id == CSSPropertyBackgroundColor);
+ if (!selection_style || !selection_style->Style())
+ return String();
+
+ if (property_id == CSSPropertyFontSize)
+ return String::Number(selection_style->LegacyFontSize(frame.GetDocument()));
+ return selection_style->Style()->GetPropertyValue(property_id);
+}
+
+String StyleCommands::ValueStyle(LocalFrame& frame, CSSPropertyID property_id) {
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // TODO(editnig-dev): Rather than retrieving the style at the start of the
+ // current selection, we should retrieve the style present throughout the
+ // selection for non-Mac platforms.
+ return SelectionStartCSSPropertyValue(frame, property_id);
+}
+
+String StyleCommands::ValueBackColor(const EditorInternalCommand&,
+ LocalFrame& frame,
+ Event*) {
+ return ValueStyle(frame, CSSPropertyBackgroundColor);
+}
+
+String StyleCommands::ValueForeColor(const EditorInternalCommand&,
+ LocalFrame& frame,
+ Event*) {
+ return ValueStyle(frame, CSSPropertyColor);
+}
+
+String StyleCommands::ValueFontName(const EditorInternalCommand&,
+ LocalFrame& frame,
+ Event*) {
+ return ValueStyle(frame, CSSPropertyFontFamily);
+}
+
+String StyleCommands::ValueFontSize(const EditorInternalCommand&,
+ LocalFrame& frame,
+ Event*) {
+ return ValueStyle(frame, CSSPropertyFontSize);
+}
+
+String StyleCommands::ValueFontSizeDelta(const EditorInternalCommand&,
+ LocalFrame& frame,
+ Event*) {
+ return ValueStyle(frame, CSSPropertyWebkitFontSizeDelta);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/style_commands.h b/chromium/third_party/blink/renderer/core/editing/commands/style_commands.h
new file mode 100644
index 00000000000..cece1e2a9c7
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/style_commands.h
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
+ * Copyright (C) 2009 Igalia S.L.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_STYLE_COMMANDS_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_STYLE_COMMANDS_H_
+
+#include "third_party/blink/renderer/core/events/input_event.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+
+namespace blink {
+
+class CSSPropertyValueSet;
+class EditingStyle;
+class EditorInternalCommand;
+class Event;
+class LocalFrame;
+
+enum class EditingTriState;
+enum class EditorCommandSource;
+enum class WritingDirection;
+
+// This class provides static functions about commands related to style.
+class StyleCommands {
+ STATIC_ONLY(StyleCommands);
+
+ public:
+ // Returns |bool| value for Document#execCommand().
+ static bool ExecuteBackColor(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteForeColor(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteFontName(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteFontSize(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteFontSizeDelta(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteToggleBold(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteToggleItalic(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteSubscript(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteSuperscript(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteUnscript(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteStrikethrough(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteUnderline(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMakeTextWritingDirectionLeftToRight(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMakeTextWritingDirectionNatural(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteMakeTextWritingDirectionRightToLeft(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteStyleWithCSS(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+ static bool ExecuteUseCSS(LocalFrame&,
+ Event*,
+ EditorCommandSource,
+ const String&);
+
+ // State functions
+ static EditingTriState StateStyle(LocalFrame&, CSSPropertyID, const char*);
+ static EditingTriState StateBold(LocalFrame&, Event*);
+ static EditingTriState StateItalic(LocalFrame&, Event*);
+ static EditingTriState StateStrikethrough(LocalFrame&, Event*);
+ static EditingTriState StateStyleWithCSS(LocalFrame&, Event*);
+ static EditingTriState StateSubscript(LocalFrame&, Event*);
+ static EditingTriState StateSuperscript(LocalFrame&, Event*);
+ static EditingTriState StateTextWritingDirectionLeftToRight(LocalFrame&,
+ Event*);
+ static EditingTriState StateTextWritingDirectionNatural(LocalFrame&, Event*);
+ static EditingTriState StateTextWritingDirectionRightToLeft(LocalFrame&,
+ Event*);
+ static EditingTriState StateUnderline(LocalFrame&, Event*);
+
+ // Value functions
+ static String ValueBackColor(const EditorInternalCommand&,
+ LocalFrame&,
+ Event*);
+ static String ValueForeColor(const EditorInternalCommand&,
+ LocalFrame&,
+ Event*);
+ static String ValueFontName(const EditorInternalCommand&,
+ LocalFrame&,
+ Event*);
+ static String ValueFontSize(const EditorInternalCommand&,
+ LocalFrame&,
+ Event*);
+ static String ValueFontSizeDelta(const EditorInternalCommand&,
+ LocalFrame&,
+ Event*);
+
+ private:
+ static void ApplyStyle(LocalFrame&,
+ CSSPropertyValueSet*,
+ InputEvent::InputType);
+ static void ApplyStyleToSelection(LocalFrame&,
+ CSSPropertyValueSet*,
+ InputEvent::InputType);
+ static bool ApplyCommandToFrame(LocalFrame&,
+ EditorCommandSource,
+ InputEvent::InputType,
+ CSSPropertyValueSet*);
+ static bool ExecuteApplyStyle(LocalFrame&,
+ EditorCommandSource,
+ InputEvent::InputType,
+ CSSPropertyID,
+ const String&);
+ static bool ExecuteApplyStyle(LocalFrame&,
+ EditorCommandSource,
+ InputEvent::InputType,
+ CSSPropertyID,
+ CSSValueID);
+ static bool ExecuteToggleStyle(LocalFrame&,
+ EditorCommandSource,
+ InputEvent::InputType,
+ CSSPropertyID,
+ const char* off_value,
+ const char* on_value);
+
+ // FIXME: executeToggleStyleInList does not handle complicated cases such as
+ // <b><u>hello</u>world</b> properly. This function must use
+ // EditingStyle::SelectionHasStyle to determine the current style but we
+ // cannot fix this until https://bugs.webkit.org/show_bug.cgi?id=27818 is
+ // resolved.
+ static bool ExecuteToggleStyleInList(LocalFrame&,
+ EditorCommandSource,
+ InputEvent::InputType,
+ CSSPropertyID,
+ const CSSValue&);
+ static String ComputeToggleStyleInList(EditingStyle&,
+ CSSPropertyID,
+ const CSSValue&);
+ static bool SelectionStartHasStyle(LocalFrame&, CSSPropertyID, const String&);
+ static String SelectionStartCSSPropertyValue(LocalFrame&, CSSPropertyID);
+ static String ValueStyle(LocalFrame&, CSSPropertyID);
+ static bool IsUnicodeBidiNestedOrMultipleEmbeddings(CSSValueID);
+
+ // TODO(editing-dev): We should make |textDirectionForSelection()| to take
+ // |selectionInDOMTree|.
+ static WritingDirection TextDirectionForSelection(const VisibleSelection&,
+ EditingStyle*,
+ bool&);
+ static EditingTriState StateTextWritingDirection(LocalFrame&,
+ WritingDirection);
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/typing_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/typing_command.cc
new file mode 100644
index 00000000000..a1c0ba2e0ae
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/typing_command.cc
@@ -0,0 +1,1108 @@
+/*
+ * Copyright (C) 2005, 2006, 2007, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/typing_command.h"
+
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/element_traversal.h"
+#include "third_party/blink/renderer/core/dom/events/scoped_event_queue.h"
+#include "third_party/blink/renderer/core/editing/commands/break_blockquote_command.h"
+#include "third_party/blink/renderer/core/editing/commands/delete_selection_command.h"
+#include "third_party/blink/renderer/core/editing/commands/delete_selection_options.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/commands/insert_incremental_text_command.h"
+#include "third_party/blink/renderer/core/editing/commands/insert_line_break_command.h"
+#include "third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command.h"
+#include "third_party/blink/renderer/core/editing/commands/insert_text_command.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/plain_text_range.h"
+#include "third_party/blink/renderer/core/editing/selection_modifier.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/events/before_text_inserted_event.h"
+#include "third_party/blink/renderer/core/events/text_event.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/html/html_br_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+
+namespace blink {
+
+namespace {
+
+bool IsValidDocument(const Document& document) {
+ return document.GetFrame() && document.GetFrame()->GetDocument() == &document;
+}
+
+String DispatchBeforeTextInsertedEvent(const String& text,
+ const VisibleSelection& selection,
+ EditingState* editing_state) {
+ Node* start_node = selection.Start().ComputeContainerNode();
+ if (!start_node || !RootEditableElement(*start_node))
+ return text;
+
+ // Send BeforeTextInsertedEvent. The event handler will update text if
+ // necessary.
+ const Document& document = start_node->GetDocument();
+ BeforeTextInsertedEvent* evt = BeforeTextInsertedEvent::Create(text);
+ RootEditableElement(*start_node)->DispatchEvent(evt);
+ if (IsValidDocument(document) && selection.IsValidFor(document))
+ return evt->GetText();
+ // editing/inserting/webkitBeforeTextInserted-removes-frame.html
+ // and
+ // editing/inserting/webkitBeforeTextInserted-disconnects-selection.html
+ // reaches here.
+ editing_state->Abort();
+ return String();
+}
+
+DispatchEventResult DispatchTextInputEvent(LocalFrame* frame,
+ const String& text,
+ EditingState* editing_state) {
+ const Document& document = *frame->GetDocument();
+ Element* target = document.FocusedElement();
+ if (!target)
+ return DispatchEventResult::kCanceledBeforeDispatch;
+
+ // Send TextInputEvent. Unlike BeforeTextInsertedEvent, there is no need to
+ // update text for TextInputEvent as it doesn't have the API to modify text.
+ TextEvent* event = TextEvent::Create(frame->DomWindow(), text,
+ kTextEventInputIncrementalInsertion);
+ event->SetUnderlyingEvent(nullptr);
+ DispatchEventResult result = target->DispatchEvent(event);
+ if (IsValidDocument(document))
+ return result;
+ // editing/inserting/insert-text-remove-iframe-on-textInput-event.html
+ // reaches here.
+ editing_state->Abort();
+ return result;
+}
+
+PlainTextRange GetSelectionOffsets(const SelectionInDOMTree& selection) {
+ const VisibleSelection visible_selection = CreateVisibleSelection(selection);
+ const EphemeralRange range = FirstEphemeralRangeOf(visible_selection);
+ if (range.IsNull())
+ return PlainTextRange();
+ ContainerNode* const editable =
+ RootEditableElementOrTreeScopeRootNodeOf(selection.Base());
+ DCHECK(editable);
+ return PlainTextRange::Create(*editable, range);
+}
+
+SelectionInDOMTree CreateSelection(const size_t start,
+ const size_t end,
+ Element* element) {
+ const EphemeralRange& start_range =
+ PlainTextRange(0, static_cast<int>(start)).CreateRange(*element);
+ DCHECK(start_range.IsNotNull());
+ const Position& start_position = start_range.EndPosition();
+
+ const EphemeralRange& end_range =
+ PlainTextRange(0, static_cast<int>(end)).CreateRange(*element);
+ DCHECK(end_range.IsNotNull());
+ const Position& end_position = end_range.EndPosition();
+
+ const SelectionInDOMTree& selection =
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(start_position, end_position)
+ .Build();
+ return selection;
+}
+
+bool CanAppendNewLineFeedToSelection(const VisibleSelection& selection,
+ EditingState* editing_state) {
+ Element* element = selection.RootEditableElement();
+ if (!element)
+ return false;
+
+ const Document& document = element->GetDocument();
+ BeforeTextInsertedEvent* event =
+ BeforeTextInsertedEvent::Create(String("\n"));
+ element->DispatchEvent(event);
+ // event may invalidate frame or selection
+ if (IsValidDocument(document) && selection.IsValidFor(document))
+ return event->GetText().length();
+ // editing/inserting/webkitBeforeTextInserted-removes-frame.html
+ // and
+ // editing/inserting/webkitBeforeTextInserted-disconnects-selection.html
+ // reaches here.
+ editing_state->Abort();
+ return false;
+}
+
+} // anonymous namespace
+
+using namespace HTMLNames;
+
+TypingCommand::TypingCommand(Document& document,
+ ETypingCommand command_type,
+ const String& text_to_insert,
+ Options options,
+ TextGranularity granularity,
+ TextCompositionType composition_type)
+ : CompositeEditCommand(document),
+ command_type_(command_type),
+ text_to_insert_(text_to_insert),
+ open_for_more_typing_(true),
+ select_inserted_text_(options & kSelectInsertedText),
+ smart_delete_(options & kSmartDelete),
+ granularity_(granularity),
+ composition_type_(composition_type),
+ kill_ring_(options & kKillRing),
+ opened_by_backward_delete_(false) {
+ UpdatePreservesTypingStyle(command_type_);
+}
+
+void TypingCommand::DeleteSelection(Document& document, Options options) {
+ LocalFrame* frame = document.GetFrame();
+ DCHECK(frame);
+
+ if (!frame->Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .IsRange())
+ return;
+
+ if (TypingCommand* last_typing_command =
+ LastTypingCommandIfStillOpenForTyping(frame)) {
+ UpdateSelectionIfDifferentFromCurrentSelection(last_typing_command, frame);
+
+ // InputMethodController uses this function to delete composition
+ // selection. It won't be aborted.
+ last_typing_command->DeleteSelection(options & kSmartDelete,
+ ASSERT_NO_EDITING_ABORT);
+ return;
+ }
+
+ TypingCommand::Create(document, kDeleteSelection, "", options)->Apply();
+}
+
+void TypingCommand::DeleteSelectionIfRange(const VisibleSelection& selection,
+ EditingState* editing_state) {
+ if (!selection.IsRange())
+ return;
+ ApplyCommandToComposite(DeleteSelectionCommand::Create(
+ selection, DeleteSelectionOptions::Builder()
+ .SetSmartDelete(smart_delete_)
+ .SetMergeBlocksAfterDelete(true)
+ .SetExpandForSpecialElements(true)
+ .SetSanitizeMarkup(true)
+ .Build()),
+ editing_state);
+}
+
+void TypingCommand::DeleteKeyPressed(Document& document,
+ Options options,
+ TextGranularity granularity) {
+ if (granularity == TextGranularity::kCharacter) {
+ LocalFrame* frame = document.GetFrame();
+ if (TypingCommand* last_typing_command =
+ LastTypingCommandIfStillOpenForTyping(frame)) {
+ // If the last typing command is not Delete, open a new typing command.
+ // We need to group continuous delete commands alone in a single typing
+ // command.
+ if (last_typing_command->CommandTypeOfOpenCommand() == kDeleteKey) {
+ UpdateSelectionIfDifferentFromCurrentSelection(last_typing_command,
+ frame);
+ EditingState editing_state;
+ last_typing_command->DeleteKeyPressed(granularity, options & kKillRing,
+ &editing_state);
+ return;
+ }
+ }
+ }
+
+ TypingCommand::Create(document, kDeleteKey, "", options, granularity)
+ ->Apply();
+}
+
+void TypingCommand::ForwardDeleteKeyPressed(Document& document,
+ EditingState* editing_state,
+ Options options,
+ TextGranularity granularity) {
+ // FIXME: Forward delete in TextEdit appears to open and close a new typing
+ // command.
+ if (granularity == TextGranularity::kCharacter) {
+ LocalFrame* frame = document.GetFrame();
+ if (TypingCommand* last_typing_command =
+ LastTypingCommandIfStillOpenForTyping(frame)) {
+ UpdateSelectionIfDifferentFromCurrentSelection(last_typing_command,
+ frame);
+ last_typing_command->ForwardDeleteKeyPressed(
+ granularity, options & kKillRing, editing_state);
+ return;
+ }
+ }
+
+ TypingCommand::Create(document, kForwardDeleteKey, "", options, granularity)
+ ->Apply();
+}
+
+String TypingCommand::TextDataForInputEvent() const {
+ if (commands_.IsEmpty() || IsIncrementalInsertion())
+ return text_to_insert_;
+ return commands_.back()->TextDataForInputEvent();
+}
+
+void TypingCommand::UpdateSelectionIfDifferentFromCurrentSelection(
+ TypingCommand* typing_command,
+ LocalFrame* frame) {
+ DCHECK(frame);
+ const SelectionInDOMTree& current_selection =
+ frame->Selection().GetSelectionInDOMTree();
+ if (current_selection == typing_command->EndingSelection().AsSelection())
+ return;
+
+ typing_command->SetStartingSelection(
+ SelectionForUndoStep::From(current_selection));
+ typing_command->SetEndingSelection(
+ SelectionForUndoStep::From(current_selection));
+}
+
+void TypingCommand::InsertText(Document& document,
+ const String& text,
+ Options options,
+ TextCompositionType composition,
+ const bool is_incremental_insertion) {
+ LocalFrame* frame = document.GetFrame();
+ DCHECK(frame);
+ EditingState editing_state;
+ InsertText(document, text, frame->Selection().GetSelectionInDOMTree(),
+ options, &editing_state, composition, is_incremental_insertion);
+}
+
+void TypingCommand::AdjustSelectionAfterIncrementalInsertion(
+ LocalFrame* frame,
+ const size_t selection_start,
+ const size_t text_length,
+ EditingState* editing_state) {
+ if (!IsIncrementalInsertion())
+ return;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ frame->GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ Element* element = frame->Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .RootEditableElement();
+
+ // TODO(editing-dev): The text insertion should probably always leave the
+ // selection in an editable region, but we know of at least one case where it
+ // doesn't (see test case in crbug.com/767599). Return early in this case to
+ // avoid a crash.
+ if (!element) {
+ editing_state->Abort();
+ return;
+ }
+
+ const size_t new_end = selection_start + text_length;
+ const SelectionInDOMTree& selection =
+ CreateSelection(new_end, new_end, element);
+ SetEndingSelection(SelectionForUndoStep::From(selection));
+}
+
+// FIXME: We shouldn't need to take selectionForInsertion. It should be
+// identical to FrameSelection's current selection.
+void TypingCommand::InsertText(
+ Document& document,
+ const String& text,
+ const SelectionInDOMTree& passed_selection_for_insertion,
+ Options options,
+ EditingState* editing_state,
+ TextCompositionType composition_type,
+ const bool is_incremental_insertion,
+ InputEvent::InputType input_type) {
+ DCHECK(!document.NeedsLayoutTreeUpdate());
+ LocalFrame* frame = document.GetFrame();
+ DCHECK(frame);
+
+ const VisibleSelection& current_selection =
+ frame->Selection().ComputeVisibleSelectionInDOMTree();
+ const VisibleSelection& selection_for_insertion =
+ CreateVisibleSelection(passed_selection_for_insertion);
+
+ String new_text = text;
+ if (composition_type != kTextCompositionUpdate) {
+ new_text = DispatchBeforeTextInsertedEvent(text, selection_for_insertion,
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ if (composition_type == kTextCompositionConfirm) {
+ if (DispatchTextInputEvent(frame, new_text, editing_state) !=
+ DispatchEventResult::kNotCanceled)
+ return;
+ // event handler might destroy document.
+ if (editing_state->IsAborted())
+ return;
+ // editing/inserting/insert-text-nodes-disconnect-on-textinput-event.html
+ // hits true for ABORT_EDITING_COMMAND_IF macro.
+ ABORT_EDITING_COMMAND_IF(!selection_for_insertion.IsValidFor(document));
+ }
+
+ // Do nothing if no need to delete and insert.
+ if (selection_for_insertion.IsCaret() && new_text.IsEmpty())
+ return;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ document.UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ const PlainTextRange selection_offsets =
+ GetSelectionOffsets(selection_for_insertion.AsSelection());
+ if (selection_offsets.IsNull())
+ return;
+ const size_t selection_start = selection_offsets.Start();
+
+ // Set the starting and ending selection appropriately if we are using a
+ // selection that is different from the current selection. In the future, we
+ // should change EditCommand to deal with custom selections in a general way
+ // that can be used by all of the commands.
+ if (TypingCommand* last_typing_command =
+ LastTypingCommandIfStillOpenForTyping(frame)) {
+ if (last_typing_command->EndingVisibleSelection() !=
+ selection_for_insertion) {
+ const SelectionForUndoStep& selection_for_insertion_as_undo_step =
+ SelectionForUndoStep::From(selection_for_insertion.AsSelection());
+ last_typing_command->SetStartingSelection(
+ selection_for_insertion_as_undo_step);
+ last_typing_command->SetEndingSelection(
+ selection_for_insertion_as_undo_step);
+ }
+
+ last_typing_command->SetCompositionType(composition_type);
+ last_typing_command->is_incremental_insertion_ = is_incremental_insertion;
+ last_typing_command->selection_start_ = selection_start;
+ last_typing_command->input_type_ = input_type;
+
+ EventQueueScope event_queue_scope;
+ last_typing_command->InsertTextInternal(
+ new_text, options & kSelectInsertedText, editing_state);
+ return;
+ }
+
+ TypingCommand* command = TypingCommand::Create(
+ document, kInsertText, new_text, options, composition_type);
+ bool change_selection = selection_for_insertion != current_selection;
+ if (change_selection) {
+ const SelectionForUndoStep& selection_for_insertion_as_undo_step =
+ SelectionForUndoStep::From(selection_for_insertion.AsSelection());
+ command->SetStartingSelection(selection_for_insertion_as_undo_step);
+ command->SetEndingSelection(selection_for_insertion_as_undo_step);
+ }
+ command->is_incremental_insertion_ = is_incremental_insertion;
+ command->selection_start_ = selection_start;
+ command->input_type_ = input_type;
+ ABORT_EDITING_COMMAND_IF(!command->Apply());
+
+ if (change_selection) {
+ ABORT_EDITING_COMMAND_IF(!current_selection.IsValidFor(document));
+ const SelectionInDOMTree& current_selection_as_dom =
+ current_selection.AsSelection();
+ command->SetEndingSelection(
+ SelectionForUndoStep::From(current_selection_as_dom));
+ frame->Selection().SetSelection(
+ current_selection_as_dom,
+ SetSelectionOptions::Builder()
+ .SetIsDirectional(frame->Selection().IsDirectional())
+ .Build());
+ }
+}
+
+bool TypingCommand::InsertLineBreak(Document& document) {
+ if (TypingCommand* last_typing_command =
+ LastTypingCommandIfStillOpenForTyping(document.GetFrame())) {
+ EditingState editing_state;
+ EventQueueScope event_queue_scope;
+ last_typing_command->InsertLineBreak(&editing_state);
+ return !editing_state.IsAborted();
+ }
+
+ return TypingCommand::Create(document, kInsertLineBreak, "", 0)->Apply();
+}
+
+bool TypingCommand::InsertParagraphSeparatorInQuotedContent(
+ Document& document) {
+ if (TypingCommand* last_typing_command =
+ LastTypingCommandIfStillOpenForTyping(document.GetFrame())) {
+ EditingState editing_state;
+ EventQueueScope event_queue_scope;
+ last_typing_command->InsertParagraphSeparatorInQuotedContent(
+ &editing_state);
+ return !editing_state.IsAborted();
+ }
+
+ return TypingCommand::Create(document,
+ kInsertParagraphSeparatorInQuotedContent)
+ ->Apply();
+}
+
+bool TypingCommand::InsertParagraphSeparator(Document& document) {
+ if (TypingCommand* last_typing_command =
+ LastTypingCommandIfStillOpenForTyping(document.GetFrame())) {
+ EditingState editing_state;
+ EventQueueScope event_queue_scope;
+ last_typing_command->InsertParagraphSeparator(&editing_state);
+ return !editing_state.IsAborted();
+ }
+
+ return TypingCommand::Create(document, kInsertParagraphSeparator, "", 0)
+ ->Apply();
+}
+
+TypingCommand* TypingCommand::LastTypingCommandIfStillOpenForTyping(
+ LocalFrame* frame) {
+ DCHECK(frame);
+
+ CompositeEditCommand* last_edit_command =
+ frame->GetEditor().LastEditCommand();
+ if (!last_edit_command || !last_edit_command->IsTypingCommand() ||
+ !static_cast<TypingCommand*>(last_edit_command)->IsOpenForMoreTyping())
+ return nullptr;
+
+ return static_cast<TypingCommand*>(last_edit_command);
+}
+
+void TypingCommand::CloseTyping(LocalFrame* frame) {
+ if (TypingCommand* last_typing_command =
+ LastTypingCommandIfStillOpenForTyping(frame))
+ last_typing_command->CloseTyping();
+}
+
+void TypingCommand::DoApply(EditingState* editing_state) {
+ if (EndingSelection().IsNone() ||
+ !EndingSelection().IsValidFor(GetDocument()))
+ return;
+
+ if (command_type_ == kDeleteKey) {
+ if (commands_.IsEmpty())
+ opened_by_backward_delete_ = true;
+ }
+
+ switch (command_type_) {
+ case kDeleteSelection:
+ DeleteSelection(smart_delete_, editing_state);
+ return;
+ case kDeleteKey:
+ DeleteKeyPressed(granularity_, kill_ring_, editing_state);
+ return;
+ case kForwardDeleteKey:
+ ForwardDeleteKeyPressed(granularity_, kill_ring_, editing_state);
+ return;
+ case kInsertLineBreak:
+ InsertLineBreak(editing_state);
+ return;
+ case kInsertParagraphSeparator:
+ InsertParagraphSeparator(editing_state);
+ return;
+ case kInsertParagraphSeparatorInQuotedContent:
+ InsertParagraphSeparatorInQuotedContent(editing_state);
+ return;
+ case kInsertText:
+ InsertTextInternal(text_to_insert_, select_inserted_text_, editing_state);
+ return;
+ }
+
+ NOTREACHED();
+}
+
+InputEvent::InputType TypingCommand::GetInputType() const {
+ using InputType = InputEvent::InputType;
+
+ if (composition_type_ != kTextCompositionNone)
+ return InputType::kInsertCompositionText;
+
+ if (input_type_ != InputType::kNone)
+ return input_type_;
+
+ switch (command_type_) {
+ // TODO(chongz): |DeleteSelection| is used by IME but we don't have
+ // direction info.
+ case kDeleteSelection:
+ return InputType::kDeleteContentBackward;
+ case kDeleteKey:
+ return DeletionInputTypeFromTextGranularity(DeleteDirection::kBackward,
+ granularity_);
+ case kForwardDeleteKey:
+ return DeletionInputTypeFromTextGranularity(DeleteDirection::kForward,
+ granularity_);
+ case kInsertText:
+ return InputType::kInsertText;
+ case kInsertLineBreak:
+ return InputType::kInsertLineBreak;
+ case kInsertParagraphSeparator:
+ case kInsertParagraphSeparatorInQuotedContent:
+ return InputType::kInsertParagraph;
+ default:
+ return InputType::kNone;
+ }
+}
+
+void TypingCommand::TypingAddedToOpenCommand(
+ ETypingCommand command_type_for_added_typing) {
+ LocalFrame* frame = GetDocument().GetFrame();
+ if (!frame)
+ return;
+
+ UpdatePreservesTypingStyle(command_type_for_added_typing);
+ UpdateCommandTypeOfOpenCommand(command_type_for_added_typing);
+
+ AppliedEditing();
+}
+
+void TypingCommand::InsertTextInternal(const String& text,
+ bool select_inserted_text,
+ EditingState* editing_state) {
+ text_to_insert_ = text;
+
+ if (text.IsEmpty()) {
+ InsertTextRunWithoutNewlines(text, editing_state);
+ return;
+ }
+ size_t selection_start = selection_start_;
+ unsigned offset = 0;
+ size_t newline;
+ while ((newline = text.find('\n', offset)) != kNotFound) {
+ if (newline > offset) {
+ const size_t insertion_length = newline - offset;
+ InsertTextRunWithoutNewlines(text.Substring(offset, insertion_length),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ AdjustSelectionAfterIncrementalInsertion(GetDocument().GetFrame(),
+ selection_start,
+ insertion_length, editing_state);
+ selection_start += insertion_length;
+ }
+
+ InsertParagraphSeparator(editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ offset = newline + 1;
+ ++selection_start;
+ }
+
+ if (text.length() > offset) {
+ const size_t insertion_length = text.length() - offset;
+ InsertTextRunWithoutNewlines(text.Substring(offset, insertion_length),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ AdjustSelectionAfterIncrementalInsertion(GetDocument().GetFrame(),
+ selection_start, insertion_length,
+ editing_state);
+ }
+
+ if (!select_inserted_text)
+ return;
+
+ // If the caller wants the newly-inserted text to be selected, we select from
+ // the plain text offset corresponding to the beginning of the range (possibly
+ // collapsed) being replaced by the text insert, to wherever the selection was
+ // left after the final run of text was inserted.
+ ContainerNode* const editable =
+ RootEditableElementOrTreeScopeRootNodeOf(EndingSelection().Base());
+
+ const EphemeralRange new_selection_start_collapsed_range =
+ PlainTextRange(selection_start_, selection_start_).CreateRange(*editable);
+ const Position current_selection_end = EndingSelection().End();
+
+ const SelectionInDOMTree& new_selection =
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(new_selection_start_collapsed_range.StartPosition(),
+ current_selection_end)
+ .Build();
+
+ SetEndingSelection(SelectionForUndoStep::From(new_selection));
+}
+
+void TypingCommand::InsertTextRunWithoutNewlines(const String& text,
+ EditingState* editing_state) {
+ CompositeEditCommand* command;
+ if (IsIncrementalInsertion()) {
+ command = InsertIncrementalTextCommand::Create(
+ GetDocument(), text,
+ composition_type_ == kTextCompositionNone
+ ? InsertIncrementalTextCommand::
+ kRebalanceLeadingAndTrailingWhitespaces
+ : InsertIncrementalTextCommand::kRebalanceAllWhitespaces);
+ } else {
+ command = InsertTextCommand::Create(
+ GetDocument(), text,
+ composition_type_ == kTextCompositionNone
+ ? InsertTextCommand::kRebalanceLeadingAndTrailingWhitespaces
+ : InsertTextCommand::kRebalanceAllWhitespaces);
+ }
+
+ command->SetStartingSelection(EndingSelection());
+ command->SetEndingSelection(EndingSelection());
+ ApplyCommandToComposite(command, editing_state);
+ if (editing_state->IsAborted())
+ return;
+
+ TypingAddedToOpenCommand(kInsertText);
+}
+
+void TypingCommand::InsertLineBreak(EditingState* editing_state) {
+ if (!CanAppendNewLineFeedToSelection(EndingVisibleSelection(), editing_state))
+ return;
+
+ ApplyCommandToComposite(InsertLineBreakCommand::Create(GetDocument()),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ TypingAddedToOpenCommand(kInsertLineBreak);
+}
+
+void TypingCommand::InsertParagraphSeparator(EditingState* editing_state) {
+ if (!CanAppendNewLineFeedToSelection(EndingVisibleSelection(), editing_state))
+ return;
+
+ ApplyCommandToComposite(
+ InsertParagraphSeparatorCommand::Create(GetDocument()), editing_state);
+ if (editing_state->IsAborted())
+ return;
+ TypingAddedToOpenCommand(kInsertParagraphSeparator);
+}
+
+void TypingCommand::InsertParagraphSeparatorInQuotedContent(
+ EditingState* editing_state) {
+ // If the selection starts inside a table, just insert the paragraph separator
+ // normally Breaking the blockquote would also break apart the table, which is
+ // unecessary when inserting a newline
+ if (EnclosingNodeOfType(EndingSelection().Start(), &IsTableStructureNode)) {
+ InsertParagraphSeparator(editing_state);
+ return;
+ }
+
+ ApplyCommandToComposite(BreakBlockquoteCommand::Create(GetDocument()),
+ editing_state);
+ if (editing_state->IsAborted())
+ return;
+ TypingAddedToOpenCommand(kInsertParagraphSeparatorInQuotedContent);
+}
+
+bool TypingCommand::MakeEditableRootEmpty(EditingState* editing_state) {
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+ Element* root = RootEditableElementOf(EndingSelection().Base());
+ if (!root || !root->HasChildren())
+ return false;
+
+ if (root->firstChild() == root->lastChild()) {
+ if (IsHTMLBRElement(root->firstChild())) {
+ // If there is a single child and it could be a placeholder, leave it
+ // alone.
+ if (root->GetLayoutObject() &&
+ root->GetLayoutObject()->IsLayoutBlockFlow())
+ return false;
+ }
+ }
+
+ while (Node* child = root->firstChild()) {
+ RemoveNode(child, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ }
+
+ AddBlockPlaceholderIfNeeded(root, editing_state);
+ if (editing_state->IsAborted())
+ return false;
+ const SelectionInDOMTree& selection =
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::FirstPositionInNode(*root))
+ .Build();
+ SetEndingSelection(SelectionForUndoStep::From(selection));
+
+ return true;
+}
+
+// If there are multiple Unicode code points to be deleted, adjust the
+// range to match platform conventions.
+static VisibleSelection AdjustSelectionForBackwardDelete(
+ const VisibleSelection& selection) {
+ if (selection.End().ComputeContainerNode() !=
+ selection.Start().ComputeContainerNode())
+ return selection;
+ if (selection.End().ComputeOffsetInContainerNode() -
+ selection.Start().ComputeOffsetInContainerNode() <=
+ 1)
+ return selection;
+ return VisibleSelection::CreateWithoutValidationDeprecated(
+ selection.End(),
+ PreviousPositionOf(selection.End(), PositionMoveType::kBackwardDeletion),
+ selection.Affinity());
+}
+
+void TypingCommand::DeleteKeyPressed(TextGranularity granularity,
+ bool kill_ring,
+ EditingState* editing_state) {
+ LocalFrame* frame = GetDocument().GetFrame();
+ if (!frame)
+ return;
+
+ if (EndingSelection().IsRange()) {
+ DeleteKeyPressedInternal(EndingVisibleSelection(), EndingSelection(),
+ kill_ring, editing_state);
+ return;
+ }
+
+ if (!EndingSelection().IsCaret()) {
+ NOTREACHED();
+ return;
+ }
+
+ // After breaking out of an empty mail blockquote, we still want continue
+ // with the deletion so actual content will get deleted, and not just the
+ // quote style.
+ const bool break_out_result =
+ BreakOutOfEmptyMailBlockquotedParagraph(editing_state);
+ if (editing_state->IsAborted())
+ return;
+ if (break_out_result)
+ TypingAddedToOpenCommand(kDeleteKey);
+
+ smart_delete_ = false;
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ SelectionModifier selection_modifier(*frame, EndingSelection().AsSelection());
+ selection_modifier.SetSelectionIsDirectional(SelectionIsDirectional());
+ selection_modifier.Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kBackward, granularity);
+ if (kill_ring && selection_modifier.Selection().IsCaret() &&
+ granularity != TextGranularity::kCharacter) {
+ selection_modifier.Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kBackward,
+ TextGranularity::kCharacter);
+ }
+
+ const VisiblePosition& visible_start(EndingVisibleSelection().VisibleStart());
+ const VisiblePosition& previous_position =
+ PreviousPositionOf(visible_start, kCannotCrossEditingBoundary);
+ const Node* enclosing_table_cell =
+ EnclosingNodeOfType(visible_start.DeepEquivalent(), &IsTableCell);
+ const Node* enclosing_table_cell_for_previous_position =
+ EnclosingNodeOfType(previous_position.DeepEquivalent(), &IsTableCell);
+ if (previous_position.IsNull() ||
+ enclosing_table_cell != enclosing_table_cell_for_previous_position) {
+ // When the caret is at the start of the editable area, or cell, in an
+ // empty list item, break out of the list item.
+ const bool break_out_of_empty_list_item_result =
+ BreakOutOfEmptyListItem(editing_state);
+ if (editing_state->IsAborted())
+ return;
+ if (break_out_of_empty_list_item_result) {
+ TypingAddedToOpenCommand(kDeleteKey);
+ return;
+ }
+ }
+ if (previous_position.IsNull()) {
+ // When there are no visible positions in the editing root, delete its
+ // entire contents.
+ if (NextPositionOf(visible_start, kCannotCrossEditingBoundary).IsNull() &&
+ MakeEditableRootEmpty(editing_state)) {
+ TypingAddedToOpenCommand(kDeleteKey);
+ return;
+ }
+ if (editing_state->IsAborted())
+ return;
+ }
+
+ // If we have a caret selection at the beginning of a cell, we have
+ // nothing to do.
+ if (enclosing_table_cell && visible_start.DeepEquivalent() ==
+ VisiblePosition::FirstPositionInNode(
+ *const_cast<Node*>(enclosing_table_cell))
+ .DeepEquivalent())
+ return;
+
+ // If the caret is at the start of a paragraph after a table, move content
+ // into the last table cell (this is done to follows macOS' behavior).
+ if (frame->GetEditor().Behavior().ShouldMergeContentWithTablesOnBackspace() &&
+ IsStartOfParagraph(visible_start) &&
+ TableElementJustBefore(
+ PreviousPositionOf(visible_start, kCannotCrossEditingBoundary))) {
+ // Unless the caret is just before a table. We don't want to move a
+ // table into the last table cell.
+ if (TableElementJustAfter(visible_start))
+ return;
+ // Extend the selection backward into the last cell, then deletion will
+ // handle the move.
+ selection_modifier.Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kBackward, granularity);
+ // If the caret is just after a table, select the table and don't delete
+ // anything.
+ } else if (Element* table = TableElementJustBefore(visible_start)) {
+ const SelectionInDOMTree& selection =
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::BeforeNode(*table))
+ .Extend(EndingSelection().Start())
+ .Build();
+ SetEndingSelection(SelectionForUndoStep::From(selection));
+ TypingAddedToOpenCommand(kDeleteKey);
+ return;
+ }
+
+ const VisibleSelection& selection_to_delete =
+ granularity == TextGranularity::kCharacter
+ ? AdjustSelectionForBackwardDelete(selection_modifier.Selection())
+ : selection_modifier.Selection();
+
+ if (!StartingSelection().IsRange() ||
+ selection_to_delete.Base() != StartingSelection().Start()) {
+ DeleteKeyPressedInternal(
+ selection_to_delete,
+ SelectionForUndoStep::From(selection_to_delete.AsSelection()),
+ kill_ring, editing_state);
+ return;
+ }
+ // Note: |StartingSelection().End()| can be disconnected.
+ // See editing/deleting/delete_list_item.html on MacOS.
+ const SelectionForUndoStep selection_after_undo =
+ SelectionForUndoStep::Builder()
+ .SetBaseAndExtentAsBackwardSelection(
+ StartingSelection().End(),
+ CreateVisiblePosition(selection_to_delete.Extent())
+ .DeepEquivalent())
+ .Build();
+ DeleteKeyPressedInternal(selection_to_delete, selection_after_undo, kill_ring,
+ editing_state);
+}
+
+void TypingCommand::DeleteKeyPressedInternal(
+ const VisibleSelection& selection_to_delete,
+ const SelectionForUndoStep& selection_after_undo,
+ bool kill_ring,
+ EditingState* editing_state) {
+ DCHECK(!selection_to_delete.IsNone());
+ if (selection_to_delete.IsNone())
+ return;
+
+ if (selection_to_delete.IsCaret())
+ return;
+
+ LocalFrame* frame = GetDocument().GetFrame();
+ DCHECK(frame);
+
+ if (kill_ring)
+ frame->GetEditor().AddToKillRing(
+ selection_to_delete.ToNormalizedEphemeralRange());
+ // On Mac, make undo select everything that has been deleted, unless an undo
+ // will undo more than just this deletion.
+ // FIXME: This behaves like TextEdit except for the case where you open with
+ // text insertion and then delete more text than you insert. In that case all
+ // of the text that was around originally should be selected.
+ if (frame->GetEditor().Behavior().ShouldUndoOfDeleteSelectText() &&
+ opened_by_backward_delete_)
+ SetStartingSelection(selection_after_undo);
+ DeleteSelectionIfRange(selection_to_delete, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ SetSmartDelete(false);
+ TypingAddedToOpenCommand(kDeleteKey);
+}
+
+static Position ComputeExtentForForwardDeleteUndo(
+ const VisibleSelection& selection,
+ const Position& extent) {
+ if (extent.ComputeContainerNode() != selection.End().ComputeContainerNode())
+ return selection.Extent();
+ const int extra_characters =
+ selection.Start().ComputeContainerNode() ==
+ selection.End().ComputeContainerNode()
+ ? selection.End().ComputeOffsetInContainerNode() -
+ selection.Start().ComputeOffsetInContainerNode()
+ : selection.End().ComputeOffsetInContainerNode();
+ return Position::CreateWithoutValidation(
+ *extent.ComputeContainerNode(),
+ extent.ComputeOffsetInContainerNode() + extra_characters);
+}
+
+void TypingCommand::ForwardDeleteKeyPressed(TextGranularity granularity,
+ bool kill_ring,
+ EditingState* editing_state) {
+ LocalFrame* frame = GetDocument().GetFrame();
+ if (!frame)
+ return;
+
+ if (EndingSelection().IsRange()) {
+ ForwardDeleteKeyPressedInternal(EndingVisibleSelection(), EndingSelection(),
+ kill_ring, editing_state);
+ return;
+ }
+
+ if (!EndingSelection().IsCaret()) {
+ NOTREACHED();
+ return;
+ }
+
+ smart_delete_ = false;
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Handle delete at beginning-of-block case.
+ // Do nothing in the case that the caret is at the start of a
+ // root editable element or at the start of a document.
+ SelectionModifier selection_modifier(*frame, EndingSelection().AsSelection());
+ selection_modifier.SetSelectionIsDirectional(SelectionIsDirectional());
+ selection_modifier.Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kForward, granularity);
+ if (kill_ring && selection_modifier.Selection().IsCaret() &&
+ granularity != TextGranularity::kCharacter) {
+ selection_modifier.Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kForward,
+ TextGranularity::kCharacter);
+ }
+
+ Position downstream_end = MostForwardCaretPosition(EndingSelection().End());
+ VisiblePosition visible_end = EndingVisibleSelection().VisibleEnd();
+ Node* enclosing_table_cell =
+ EnclosingNodeOfType(visible_end.DeepEquivalent(), &IsTableCell);
+ if (enclosing_table_cell &&
+ visible_end.DeepEquivalent() ==
+ VisiblePosition::LastPositionInNode(*enclosing_table_cell)
+ .DeepEquivalent())
+ return;
+ if (visible_end.DeepEquivalent() ==
+ EndOfParagraph(visible_end).DeepEquivalent()) {
+ downstream_end = MostForwardCaretPosition(
+ NextPositionOf(visible_end, kCannotCrossEditingBoundary)
+ .DeepEquivalent());
+ }
+ // When deleting tables: Select the table first, then perform the deletion
+ if (IsDisplayInsideTable(downstream_end.ComputeContainerNode()) &&
+ downstream_end.ComputeOffsetInContainerNode() <=
+ CaretMinOffset(downstream_end.ComputeContainerNode())) {
+ const SelectionInDOMTree& selection =
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtentDeprecated(
+ EndingSelection().End(),
+ Position::AfterNode(*downstream_end.ComputeContainerNode()))
+ .Build();
+ SetEndingSelection(SelectionForUndoStep::From(selection));
+ TypingAddedToOpenCommand(kForwardDeleteKey);
+ return;
+ }
+
+ // deleting to end of paragraph when at end of paragraph needs to merge
+ // the next paragraph (if any)
+ if (granularity == TextGranularity::kParagraphBoundary &&
+ selection_modifier.Selection().IsCaret() &&
+ IsEndOfParagraph(selection_modifier.Selection().VisibleEnd())) {
+ selection_modifier.Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kForward,
+ TextGranularity::kCharacter);
+ }
+
+ const VisibleSelection& selection_to_delete = selection_modifier.Selection();
+ if (!StartingSelection().IsRange() ||
+ MostBackwardCaretPosition(selection_to_delete.Base()) !=
+ StartingSelection().Start()) {
+ ForwardDeleteKeyPressedInternal(
+ selection_to_delete,
+ SelectionForUndoStep::From(selection_to_delete.AsSelection()),
+ kill_ring, editing_state);
+ return;
+ }
+ // Note: |StartingSelection().Start()| can be disconnected.
+ const SelectionForUndoStep selection_after_undo =
+ SelectionForUndoStep::Builder()
+ .SetBaseAndExtentAsForwardSelection(
+ StartingSelection().Start(),
+ ComputeExtentForForwardDeleteUndo(selection_to_delete,
+ StartingSelection().End()))
+ .Build();
+ ForwardDeleteKeyPressedInternal(selection_to_delete, selection_after_undo,
+ kill_ring, editing_state);
+}
+
+void TypingCommand::ForwardDeleteKeyPressedInternal(
+ const VisibleSelection& selection_to_delete,
+ const SelectionForUndoStep& selection_after_undo,
+ bool kill_ring,
+ EditingState* editing_state) {
+ DCHECK(!selection_to_delete.IsNone());
+ if (selection_to_delete.IsNone())
+ return;
+
+ if (selection_to_delete.IsCaret())
+ return;
+
+ LocalFrame* frame = GetDocument().GetFrame();
+ DCHECK(frame);
+
+ if (kill_ring)
+ frame->GetEditor().AddToKillRing(
+ selection_to_delete.ToNormalizedEphemeralRange());
+ // Make undo select what was deleted on Mac alone
+ if (frame->GetEditor().Behavior().ShouldUndoOfDeleteSelectText())
+ SetStartingSelection(selection_after_undo);
+ DeleteSelectionIfRange(selection_to_delete, editing_state);
+ if (editing_state->IsAborted())
+ return;
+ SetSmartDelete(false);
+ TypingAddedToOpenCommand(kForwardDeleteKey);
+}
+
+void TypingCommand::DeleteSelection(bool smart_delete,
+ EditingState* editing_state) {
+ if (!CompositeEditCommand::DeleteSelection(
+ editing_state, smart_delete ? DeleteSelectionOptions::SmartDelete()
+ : DeleteSelectionOptions::NormalDelete()))
+ return;
+ TypingAddedToOpenCommand(kDeleteSelection);
+}
+
+void TypingCommand::UpdatePreservesTypingStyle(ETypingCommand command_type) {
+ switch (command_type) {
+ case kDeleteSelection:
+ case kDeleteKey:
+ case kForwardDeleteKey:
+ case kInsertParagraphSeparator:
+ case kInsertLineBreak:
+ preserves_typing_style_ = true;
+ return;
+ case kInsertParagraphSeparatorInQuotedContent:
+ case kInsertText:
+ preserves_typing_style_ = false;
+ return;
+ }
+ NOTREACHED();
+ preserves_typing_style_ = false;
+}
+
+bool TypingCommand::IsTypingCommand() const {
+ return true;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/typing_command.h b/chromium/third_party/blink/renderer/core/editing/commands/typing_command.h
new file mode 100644
index 00000000000..b9c97ce5a23
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/typing_command.h
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2005, 2006, 2007, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_TYPING_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_TYPING_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+#include "third_party/blink/renderer/core/editing/text_granularity.h"
+
+namespace blink {
+
+class CORE_EXPORT TypingCommand final : public CompositeEditCommand {
+ public:
+ enum ETypingCommand {
+ kDeleteSelection,
+ kDeleteKey,
+ kForwardDeleteKey,
+ kInsertText,
+ kInsertLineBreak,
+ kInsertParagraphSeparator,
+ kInsertParagraphSeparatorInQuotedContent
+ };
+
+ enum TextCompositionType {
+ kTextCompositionNone,
+ kTextCompositionUpdate,
+ kTextCompositionConfirm,
+ kTextCompositionCancel
+ };
+
+ enum Option {
+ kSelectInsertedText = 1 << 0,
+ kKillRing = 1 << 1,
+ kSmartDelete = 1 << 2
+ };
+ typedef unsigned Options;
+
+ static void DeleteSelection(Document&, Options = 0);
+ static void DeleteKeyPressed(Document&,
+ Options,
+ TextGranularity = TextGranularity::kCharacter);
+ static void ForwardDeleteKeyPressed(
+ Document&,
+ EditingState*,
+ Options = 0,
+ TextGranularity = TextGranularity::kCharacter);
+ static void InsertText(Document&,
+ const String&,
+ Options,
+ TextCompositionType = kTextCompositionNone,
+ const bool is_incremental_insertion = false);
+ static void InsertText(
+ Document&,
+ const String&,
+ const SelectionInDOMTree&,
+ Options,
+ EditingState*,
+ TextCompositionType = kTextCompositionNone,
+ const bool is_incremental_insertion = false,
+ InputEvent::InputType = InputEvent::InputType::kInsertText);
+ static bool InsertLineBreak(Document&);
+ static bool InsertParagraphSeparator(Document&);
+ static bool InsertParagraphSeparatorInQuotedContent(Document&);
+ static void CloseTyping(LocalFrame*);
+
+ static TypingCommand* LastTypingCommandIfStillOpenForTyping(LocalFrame*);
+ static void UpdateSelectionIfDifferentFromCurrentSelection(TypingCommand*,
+ LocalFrame*);
+
+ void InsertTextRunWithoutNewlines(const String& text,
+ EditingState*);
+ void InsertLineBreak(EditingState*);
+ void InsertParagraphSeparatorInQuotedContent(EditingState*);
+ void InsertParagraphSeparator(EditingState*);
+ void DeleteKeyPressed(TextGranularity, bool kill_ring, EditingState*);
+ void ForwardDeleteKeyPressed(TextGranularity, bool kill_ring, EditingState*);
+ void DeleteSelection(bool smart_delete, EditingState*);
+ void SetCompositionType(TextCompositionType type) {
+ composition_type_ = type;
+ }
+ void AdjustSelectionAfterIncrementalInsertion(LocalFrame*,
+ const size_t selection_start,
+ const size_t text_length,
+ EditingState*);
+
+ ETypingCommand CommandTypeOfOpenCommand() const { return command_type_; }
+ TextCompositionType CompositionType() const { return composition_type_; }
+ // |TypingCommand| may contain multiple |InsertTextCommand|, should return
+ // |textDataForInputEvent()| of the last one.
+ String TextDataForInputEvent() const final;
+
+ private:
+ static TypingCommand* Create(
+ Document& document,
+ ETypingCommand command,
+ const String& text = "",
+ Options options = 0,
+ TextGranularity granularity = TextGranularity::kCharacter) {
+ return new TypingCommand(document, command, text, options, granularity,
+ kTextCompositionNone);
+ }
+
+ static TypingCommand* Create(Document& document,
+ ETypingCommand command,
+ const String& text,
+ Options options,
+ TextCompositionType composition_type) {
+ return new TypingCommand(document, command, text, options,
+ TextGranularity::kCharacter, composition_type);
+ }
+
+ TypingCommand(Document&,
+ ETypingCommand,
+ const String& text,
+ Options,
+ TextGranularity,
+ TextCompositionType);
+
+ void SetSmartDelete(bool smart_delete) { smart_delete_ = smart_delete; }
+ bool IsOpenForMoreTyping() const { return open_for_more_typing_; }
+ void CloseTyping() { open_for_more_typing_ = false; }
+
+ void InsertTextInternal(const String& text,
+ bool select_inserted_text,
+ EditingState*);
+
+ void DoApply(EditingState*) override;
+ InputEvent::InputType GetInputType() const override;
+ bool IsTypingCommand() const override;
+ bool PreservesTypingStyle() const override { return preserves_typing_style_; }
+
+ void UpdatePreservesTypingStyle(ETypingCommand);
+ void TypingAddedToOpenCommand(ETypingCommand);
+ bool MakeEditableRootEmpty(EditingState*);
+
+ void UpdateCommandTypeOfOpenCommand(ETypingCommand typing_command) {
+ command_type_ = typing_command;
+ }
+
+ bool IsIncrementalInsertion() const { return is_incremental_insertion_; }
+
+ void DeleteKeyPressedInternal(
+ const VisibleSelection& selection_to_delete,
+ const SelectionForUndoStep& selection_after_undo,
+ bool kill_ring,
+ EditingState*);
+
+ void DeleteSelectionIfRange(const VisibleSelection&, EditingState*);
+
+ void ForwardDeleteKeyPressedInternal(
+ const VisibleSelection& selection_to_delete,
+ const SelectionForUndoStep& selection_after_undo,
+ bool kill_ring,
+ EditingState*);
+
+ ETypingCommand command_type_;
+ String text_to_insert_;
+ bool open_for_more_typing_;
+ const bool select_inserted_text_;
+ bool smart_delete_;
+ const TextGranularity granularity_;
+ TextCompositionType composition_type_;
+ const bool kill_ring_;
+ bool preserves_typing_style_;
+
+ // Undoing a series of backward deletes will restore a selection around all of
+ // the characters that were deleted, but only if the typing command being
+ // undone was opened with a backward delete.
+ bool opened_by_backward_delete_;
+
+ bool is_incremental_insertion_;
+ size_t selection_start_;
+ InputEvent::InputType input_type_;
+};
+
+DEFINE_TYPE_CASTS(TypingCommand,
+ CompositeEditCommand,
+ command,
+ command->IsTypingCommand(),
+ command.IsTypingCommand());
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_TYPING_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/typing_command_test.cc b/chromium/third_party/blink/renderer/core/editing/commands/typing_command_test.cc
new file mode 100644
index 00000000000..fadcfa56225
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/typing_command_test.cc
@@ -0,0 +1,96 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/typing_command.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
+
+#include <memory>
+
+namespace blink {
+
+class TypingCommandTest : public EditingTestBase {};
+
+// This is a regression test for https://crbug.com/585048
+TEST_F(TypingCommandTest, insertLineBreakWithIllFormedHTML) {
+ SetBodyContent("<div contenteditable></div>");
+
+ // <input><form></form></input>
+ Element* input1 = GetDocument().CreateRawElement(HTMLNames::inputTag);
+ Element* form = GetDocument().CreateRawElement(HTMLNames::formTag);
+ input1->AppendChild(form);
+
+ // <tr><input><header></header></input><rbc></rbc></tr>
+ Element* tr = GetDocument().CreateRawElement(HTMLNames::trTag);
+ Element* input2 = GetDocument().CreateRawElement(HTMLNames::inputTag);
+ Element* header = GetDocument().CreateRawElement(HTMLNames::headerTag);
+ Element* rbc = GetDocument().CreateElementForBinding("rbc");
+ input2->AppendChild(header);
+ tr->AppendChild(input2);
+ tr->AppendChild(rbc);
+
+ Element* div = GetDocument().QuerySelector("div");
+ div->AppendChild(input1);
+ div->AppendChild(tr);
+
+ LocalFrame* frame = GetDocument().GetFrame();
+ frame->Selection().SetSelectionAndEndTyping(SelectionInDOMTree::Builder()
+ .Collapse(Position(form, 0))
+ .Extend(Position(header, 0))
+ .Build());
+
+ // Inserting line break should not crash or hit assertion.
+ TypingCommand::InsertLineBreak(GetDocument());
+}
+
+// http://crbug.com/767599
+TEST_F(TypingCommandTest,
+ DontCrashWhenReplaceSelectionCommandLeavesBadSelection) {
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<div contenteditable>^<h1>H1</h1>ello|</div>"));
+
+ // This call shouldn't crash.
+ TypingCommand::InsertText(
+ GetDocument(), " ", 0,
+ TypingCommand::TextCompositionType::kTextCompositionUpdate, true);
+ EXPECT_EQ("<div contenteditable>^<h1></h1>|</div>",
+ GetSelectionTextFromBody());
+}
+
+// crbug.com/794397
+TEST_F(TypingCommandTest, ForwardDeleteInvalidatesSelection) {
+ GetDocument().setDesignMode("on");
+ Selection().SetSelectionAndEndTyping(SetSelectionTextToBody(
+ "<blockquote>^"
+ "<q>"
+ "<table contenteditable=\"false\"><colgroup width=\"-1\">\n</table>|"
+ "</q>"
+ "</blockquote>"
+ "<q>\n<svg></svg></q>"));
+
+ EditingState editing_state;
+ TypingCommand::ForwardDeleteKeyPressed(GetDocument(), &editing_state);
+
+ EXPECT_EQ(
+ "<blockquote>|</blockquote>"
+ "<q>"
+ "<table contenteditable=\"false\">"
+ "<colgroup width=\"-1\"></colgroup>"
+ "</table>\n"
+ "<svg></svg>"
+ "</q>",
+ GetSelectionTextFromBody());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/undo_stack.cc b/chromium/third_party/blink/renderer/core/editing/commands/undo_stack.cc
new file mode 100644
index 00000000000..4a7cf2a3f67
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/undo_stack.cc
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2006, 2007 Apple, Inc. All rights reserved.
+ * Copyright (C) 2012 Google, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/undo_stack.h"
+
+#include "third_party/blink/renderer/core/dom/container_node.h"
+#include "third_party/blink/renderer/core/editing/commands/undo_step.h"
+#include "third_party/blink/renderer/platform/wtf/auto_reset.h"
+
+namespace blink {
+
+// Arbitrary depth limit for the undo stack, to keep it from using
+// unbounded memory. This is the maximum number of distinct undoable
+// actions -- unbroken stretches of typed characters are coalesced
+// into a single action.
+static const size_t kMaximumUndoStackDepth = 1000;
+
+UndoStack::UndoStack() : in_redo_(false) {}
+
+UndoStack* UndoStack::Create() {
+ return new UndoStack();
+}
+
+void UndoStack::RegisterUndoStep(UndoStep* step) {
+ if (undo_stack_.size())
+ DCHECK_GE(step->SequenceNumber(), undo_stack_.back()->SequenceNumber());
+ if (undo_stack_.size() == kMaximumUndoStackDepth)
+ undo_stack_.pop_front(); // drop oldest item off the far end
+ if (!in_redo_)
+ redo_stack_.clear();
+ undo_stack_.push_back(step);
+}
+
+void UndoStack::RegisterRedoStep(UndoStep* step) {
+ redo_stack_.push_back(step);
+}
+
+bool UndoStack::CanUndo() const {
+ return !undo_stack_.IsEmpty();
+}
+
+bool UndoStack::CanRedo() const {
+ return !redo_stack_.IsEmpty();
+}
+
+void UndoStack::Undo() {
+ if (CanUndo()) {
+ UndoStepStack::iterator back = --undo_stack_.end();
+ UndoStep* step(back->Get());
+ undo_stack_.erase(back);
+ step->Unapply();
+ // unapply will call us back to push this command onto the redo stack.
+ }
+}
+
+void UndoStack::Redo() {
+ if (CanRedo()) {
+ UndoStepStack::iterator back = --redo_stack_.end();
+ UndoStep* step(back->Get());
+ redo_stack_.erase(back);
+
+ DCHECK(!in_redo_);
+ AutoReset<bool> redo_scope(&in_redo_, true);
+ step->Reapply();
+ // reapply will call us back to push this command onto the undo stack.
+ }
+}
+
+void UndoStack::Clear() {
+ undo_stack_.clear();
+ redo_stack_.clear();
+}
+
+void UndoStack::Trace(blink::Visitor* visitor) {
+ visitor->Trace(undo_stack_);
+ visitor->Trace(redo_stack_);
+}
+
+UndoStack::UndoStepRange::UndoStepRange(const UndoStepStack& steps)
+ : step_stack_(steps) {}
+
+UndoStack::UndoStepRange UndoStack::UndoSteps() const {
+ return UndoStepRange(undo_stack_);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/undo_stack.h b/chromium/third_party/blink/renderer/core/editing/commands/undo_stack.h
new file mode 100644
index 00000000000..9adcb905bd7
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/undo_stack.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2009 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_UNDO_STACK_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_UNDO_STACK_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/wtf/deque.h"
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+
+namespace blink {
+
+class LocalFrame;
+class UndoStep;
+
+// |UndoStack| is owned by and always 1:1 to |Editor|. Since |Editor| is 1:1 to
+// |LocalFrame|, |UndoStack| is also 1:1 to |LocalFrame|.
+class UndoStack final : public GarbageCollected<UndoStack> {
+ using UndoStepStack = HeapDeque<Member<UndoStep>>;
+
+ public:
+ static UndoStack* Create();
+
+ void RegisterUndoStep(UndoStep*);
+ void RegisterRedoStep(UndoStep*);
+ bool CanUndo() const;
+ bool CanRedo() const;
+ void Undo();
+ void Redo();
+ void Clear();
+
+ class UndoStepRange {
+ STACK_ALLOCATED();
+
+ public:
+ using ConstIterator = UndoStepStack::const_reverse_iterator;
+ ConstIterator begin() { return step_stack_.rbegin(); }
+ ConstIterator end() { return step_stack_.rend(); }
+
+ explicit UndoStepRange(const UndoStepStack&);
+
+ private:
+ const UndoStepStack& step_stack_;
+ };
+
+ UndoStepRange UndoSteps() const;
+
+ void Trace(blink::Visitor*);
+
+ private:
+ UndoStack();
+
+ bool in_redo_;
+ UndoStepStack undo_stack_;
+ UndoStepStack redo_stack_;
+
+ DISALLOW_COPY_AND_ASSIGN(UndoStack);
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/undo_step.cc b/chromium/third_party/blink/renderer/core/editing/commands/undo_step.cc
new file mode 100644
index 00000000000..1a3613e23ea
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/undo_step.cc
@@ -0,0 +1,158 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/commands/undo_step.h"
+
+#include "third_party/blink/renderer/core/dom/events/scoped_event_queue.h"
+#include "third_party/blink/renderer/core/editing/commands/edit_command.h"
+#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
+#include "third_party/blink/renderer/core/editing/commands/undo_stack.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/set_selection_options.h"
+
+namespace blink {
+
+namespace {
+uint64_t g_current_sequence_number = 0;
+}
+
+UndoStep* UndoStep::Create(Document* document,
+ const SelectionForUndoStep& starting_selection,
+ const SelectionForUndoStep& ending_selection,
+ InputEvent::InputType input_type) {
+ return new UndoStep(document, starting_selection, ending_selection,
+ input_type);
+}
+
+UndoStep::UndoStep(Document* document,
+ const SelectionForUndoStep& starting_selection,
+ const SelectionForUndoStep& ending_selection,
+ InputEvent::InputType input_type)
+ : document_(document),
+ starting_selection_(starting_selection),
+ ending_selection_(ending_selection),
+ starting_root_editable_element_(
+ RootEditableElementOf(starting_selection.Base())),
+ ending_root_editable_element_(
+ RootEditableElementOf(ending_selection.Base())),
+ input_type_(input_type),
+ sequence_number_(++g_current_sequence_number) {}
+
+void UndoStep::Unapply() {
+ DCHECK(document_);
+ LocalFrame* frame = document_->GetFrame();
+ DCHECK(frame);
+
+ // Changes to the document may have been made since the last editing operation
+ // that require a layout, as in <rdar://problem/5658603>. Low level
+ // operations, like RemoveNodeCommand, don't require a layout because the high
+ // level operations that use them perform one if one is necessary (like for
+ // the creation of VisiblePositions).
+ document_->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ {
+ size_t size = commands_.size();
+ for (size_t i = size; i; --i)
+ commands_[i - 1]->DoUnapply();
+ }
+
+ EventQueueScope scope;
+
+ DispatchEditableContentChangedEvents(StartingRootEditableElement(),
+ EndingRootEditableElement());
+ DispatchInputEventEditableContentChanged(
+ StartingRootEditableElement(), EndingRootEditableElement(),
+ InputEvent::InputType::kHistoryUndo, g_null_atom,
+ InputEvent::EventIsComposing::kNotComposing);
+
+ const SelectionInDOMTree& new_selection =
+ CorrectedSelectionAfterCommand(StartingSelection(), document_);
+ ChangeSelectionAfterCommand(frame, new_selection,
+ SetSelectionOptions::Builder()
+ .SetShouldCloseTyping(true)
+ .SetShouldClearTypingStyle(true)
+ .SetIsDirectional(SelectionIsDirectional())
+ .Build());
+
+ Editor& editor = frame->GetEditor();
+ editor.SetLastEditCommand(nullptr);
+ editor.GetUndoStack().RegisterRedoStep(this);
+ editor.RespondToChangedContents(new_selection.Base());
+}
+
+void UndoStep::Reapply() {
+ DCHECK(document_);
+ LocalFrame* frame = document_->GetFrame();
+ DCHECK(frame);
+
+ // Changes to the document may have been made since the last editing operation
+ // that require a layout, as in <rdar://problem/5658603>. Low level
+ // operations, like RemoveNodeCommand, don't require a layout because the high
+ // level operations that use them perform one if one is necessary (like for
+ // the creation of VisiblePositions).
+ document_->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ {
+ for (const auto& command : commands_)
+ command->DoReapply();
+ }
+
+ EventQueueScope scope;
+
+ DispatchEditableContentChangedEvents(StartingRootEditableElement(),
+ EndingRootEditableElement());
+ DispatchInputEventEditableContentChanged(
+ StartingRootEditableElement(), EndingRootEditableElement(),
+ InputEvent::InputType::kHistoryRedo, g_null_atom,
+ InputEvent::EventIsComposing::kNotComposing);
+
+ const SelectionInDOMTree& new_selection =
+ CorrectedSelectionAfterCommand(EndingSelection(), document_);
+ ChangeSelectionAfterCommand(frame, new_selection,
+ SetSelectionOptions::Builder()
+ .SetShouldCloseTyping(true)
+ .SetShouldClearTypingStyle(true)
+ .SetIsDirectional(SelectionIsDirectional())
+ .Build());
+
+ Editor& editor = frame->GetEditor();
+ editor.SetLastEditCommand(nullptr);
+ editor.GetUndoStack().RegisterUndoStep(this);
+ editor.RespondToChangedContents(new_selection.Base());
+}
+
+InputEvent::InputType UndoStep::GetInputType() const {
+ return input_type_;
+}
+
+void UndoStep::Append(SimpleEditCommand* command) {
+ commands_.push_back(command);
+}
+
+void UndoStep::Append(UndoStep* undo_step) {
+ commands_.AppendVector(undo_step->commands_);
+}
+
+void UndoStep::SetStartingSelection(const SelectionForUndoStep& selection) {
+ starting_selection_ = selection;
+ starting_root_editable_element_ = RootEditableElementOf(selection.Base());
+}
+
+void UndoStep::SetEndingSelection(const SelectionForUndoStep& selection) {
+ ending_selection_ = selection;
+ ending_root_editable_element_ = RootEditableElementOf(selection.Base());
+}
+
+void UndoStep::Trace(blink::Visitor* visitor) {
+ visitor->Trace(document_);
+ visitor->Trace(starting_selection_);
+ visitor->Trace(ending_selection_);
+ visitor->Trace(commands_);
+ visitor->Trace(starting_root_editable_element_);
+ visitor->Trace(ending_root_editable_element_);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/undo_step.h b/chromium/third_party/blink/renderer/core/editing/commands/undo_step.h
new file mode 100644
index 00000000000..9e0f27ed525
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/undo_step.h
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_UNDO_STEP_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_UNDO_STEP_H_
+
+#include "third_party/blink/renderer/core/editing/commands/selection_for_undo_step.h"
+#include "third_party/blink/renderer/core/events/input_event.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+class SimpleEditCommand;
+
+class UndoStep : public GarbageCollectedFinalized<UndoStep> {
+ public:
+ static UndoStep* Create(Document*,
+ const SelectionForUndoStep&,
+ const SelectionForUndoStep&,
+ InputEvent::InputType);
+
+ void Unapply();
+ void Reapply();
+ InputEvent::InputType GetInputType() const;
+ void Append(SimpleEditCommand*);
+ void Append(UndoStep*);
+
+ const SelectionForUndoStep& StartingSelection() const {
+ return starting_selection_;
+ }
+ const SelectionForUndoStep& EndingSelection() const {
+ return ending_selection_;
+ }
+ bool SelectionIsDirectional() const { return selection_is_directional_; }
+ void SetStartingSelection(const SelectionForUndoStep&);
+ void SetEndingSelection(const SelectionForUndoStep&);
+ void SetSelectionIsDirectional(bool is_directional) {
+ selection_is_directional_ = is_directional;
+ }
+ Element* StartingRootEditableElement() const {
+ return starting_root_editable_element_.Get();
+ }
+ Element* EndingRootEditableElement() const {
+ return ending_root_editable_element_.Get();
+ }
+
+ uint64_t SequenceNumber() const { return sequence_number_; }
+
+ void Trace(blink::Visitor*);
+
+ private:
+ UndoStep(Document*,
+ const SelectionForUndoStep& starting_selection,
+ const SelectionForUndoStep& ending_selection,
+ InputEvent::InputType);
+
+ Member<Document> document_;
+ SelectionForUndoStep starting_selection_;
+ SelectionForUndoStep ending_selection_;
+ HeapVector<Member<SimpleEditCommand>> commands_;
+ Member<Element> starting_root_editable_element_;
+ Member<Element> ending_root_editable_element_;
+ InputEvent::InputType input_type_;
+ const uint64_t sequence_number_;
+ bool selection_is_directional_ = false;
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/unlink_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/unlink_command.cc
new file mode 100644
index 00000000000..80625e359da
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/unlink_command.cc
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2006 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/unlink_command.h"
+
+#include "third_party/blink/renderer/core/html/html_anchor_element.h"
+
+namespace blink {
+
+UnlinkCommand::UnlinkCommand(Document& document)
+ : CompositeEditCommand(document) {}
+
+void UnlinkCommand::DoApply(EditingState* editing_state) {
+ // FIXME: If a caret is inside a link, we should remove it, but currently we
+ // don't.
+ if (!EndingSelection().IsValidFor(GetDocument()))
+ return;
+ if (!EndingSelection().IsRange())
+ return;
+
+ RemoveStyledElement(HTMLAnchorElement::Create(GetDocument()), editing_state);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/unlink_command.h b/chromium/third_party/blink/renderer/core/editing/commands/unlink_command.h
new file mode 100644
index 00000000000..67d2e1bb8a4
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/unlink_command.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_UNLINK_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_UNLINK_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+
+namespace blink {
+
+class UnlinkCommand final : public CompositeEditCommand {
+ public:
+ static UnlinkCommand* Create(Document& document) {
+ return new UnlinkCommand(document);
+ }
+
+ private:
+ explicit UnlinkCommand(Document&);
+
+ void DoApply(EditingState*) override;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_UNLINK_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/wrap_contents_in_dummy_span_command.cc b/chromium/third_party/blink/renderer/core/editing/commands/wrap_contents_in_dummy_span_command.cc
new file mode 100644
index 00000000000..06f79557802
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/wrap_contents_in_dummy_span_command.cc
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2005, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/commands/wrap_contents_in_dummy_span_command.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/editing/commands/apply_style_command.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/html/html_span_element.h"
+
+namespace blink {
+
+WrapContentsInDummySpanCommand::WrapContentsInDummySpanCommand(Element* element)
+ : SimpleEditCommand(element->GetDocument()), element_(element) {
+ DCHECK(element_);
+}
+
+void WrapContentsInDummySpanCommand::ExecuteApply() {
+ NodeVector children;
+ GetChildNodes(*element_, children);
+
+ for (auto& child : children)
+ dummy_span_->AppendChild(child.Release(), IGNORE_EXCEPTION_FOR_TESTING);
+
+ element_->AppendChild(dummy_span_.Get(), IGNORE_EXCEPTION_FOR_TESTING);
+}
+
+void WrapContentsInDummySpanCommand::DoApply(EditingState*) {
+ dummy_span_ = HTMLSpanElement::Create(GetDocument());
+
+ ExecuteApply();
+}
+
+void WrapContentsInDummySpanCommand::DoUnapply() {
+ DCHECK(element_);
+
+ if (!dummy_span_ || !HasEditableStyle(*element_))
+ return;
+
+ NodeVector children;
+ GetChildNodes(*dummy_span_, children);
+
+ for (auto& child : children)
+ element_->AppendChild(child.Release(), IGNORE_EXCEPTION_FOR_TESTING);
+
+ dummy_span_->remove(IGNORE_EXCEPTION_FOR_TESTING);
+}
+
+void WrapContentsInDummySpanCommand::DoReapply() {
+ DCHECK(element_);
+
+ if (!dummy_span_ || !HasEditableStyle(*element_))
+ return;
+
+ ExecuteApply();
+}
+
+void WrapContentsInDummySpanCommand::Trace(blink::Visitor* visitor) {
+ visitor->Trace(element_);
+ visitor->Trace(dummy_span_);
+ SimpleEditCommand::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/commands/wrap_contents_in_dummy_span_command.h b/chromium/third_party/blink/renderer/core/editing/commands/wrap_contents_in_dummy_span_command.h
new file mode 100644
index 00000000000..6b75619e4da
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/commands/wrap_contents_in_dummy_span_command.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_WRAP_CONTENTS_IN_DUMMY_SPAN_COMMAND_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_WRAP_CONTENTS_IN_DUMMY_SPAN_COMMAND_H_
+
+#include "third_party/blink/renderer/core/editing/commands/edit_command.h"
+
+namespace blink {
+
+class HTMLSpanElement;
+
+class WrapContentsInDummySpanCommand final : public SimpleEditCommand {
+ public:
+ static WrapContentsInDummySpanCommand* Create(Element* element) {
+ return new WrapContentsInDummySpanCommand(element);
+ }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ explicit WrapContentsInDummySpanCommand(Element*);
+
+ void DoApply(EditingState*) override;
+ void DoUnapply() override;
+ void DoReapply() override;
+ void ExecuteApply();
+
+ Member<Element> element_;
+ Member<HTMLSpanElement> dummy_span_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_WRAP_CONTENTS_IN_DUMMY_SPAN_COMMAND_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/dom_selection.cc b/chromium/third_party/blink/renderer/core/editing/dom_selection.cc
new file mode 100644
index 00000000000..287857bba25
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/dom_selection.cc
@@ -0,0 +1,847 @@
+/*
+ * Copyright (C) 2007, 2009 Apple Inc. All rights reserved.
+ * Copyright (C) 2012 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/dom_selection.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_messages.h"
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/exception_code.h"
+#include "third_party/blink/renderer/core/dom/node.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/dom/tree_scope.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/selection_modifier.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/set_selection_options.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/frame/deprecation.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/use_counter.h"
+#include "third_party/blink/renderer/core/inspector/console_message.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+
+namespace blink {
+
+static Node* SelectionShadowAncestor(LocalFrame* frame) {
+ Node* node = frame->Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .Base()
+ .AnchorNode();
+ if (!node)
+ return nullptr;
+
+ if (!node->IsInShadowTree())
+ return nullptr;
+
+ return frame->GetDocument()->AncestorInThisScope(node);
+}
+
+DOMSelection::DOMSelection(const TreeScope* tree_scope)
+ : ContextClient(tree_scope->RootNode().GetDocument().GetFrame()),
+ tree_scope_(tree_scope) {}
+
+void DOMSelection::ClearTreeScope() {
+ tree_scope_ = nullptr;
+}
+
+// TODO(editing-dev): The behavior after loosing browsing context is not
+// specified. https://github.com/w3c/selection-api/issues/82
+bool DOMSelection::IsAvailable() const {
+ return GetFrame() && GetFrame()->Selection().IsAvailable();
+}
+
+void DOMSelection::UpdateFrameSelection(
+ const SelectionInDOMTree& selection,
+ Range* new_cached_range,
+ const SetSelectionOptions& passed_options) const {
+ DCHECK(GetFrame());
+ FrameSelection& frame_selection = GetFrame()->Selection();
+ SetSelectionOptions::Builder builder(passed_options);
+ builder.SetShouldCloseTyping(true).SetShouldClearTypingStyle(true);
+ SetSelectionOptions options = builder.Build();
+ // TODO(tkent): Specify FrameSelection::DoNotSetFocus. crbug.com/690272
+ const bool did_set =
+ frame_selection.SetSelectionDeprecated(selection, options);
+ CacheRangeIfSelectionOfDocument(new_cached_range);
+ if (!did_set)
+ return;
+ Element* focused_element = GetFrame()->GetDocument()->FocusedElement();
+ frame_selection.DidSetSelectionDeprecated(options);
+ if (GetFrame() && GetFrame()->GetDocument() &&
+ focused_element != GetFrame()->GetDocument()->FocusedElement())
+ UseCounter::Count(GetFrame(), WebFeature::kSelectionFuncionsChangeFocus);
+}
+
+VisibleSelection DOMSelection::GetVisibleSelection() const {
+ DCHECK(GetFrame());
+ return GetFrame()->Selection().ComputeVisibleSelectionInDOMTreeDeprecated();
+}
+
+bool DOMSelection::IsBaseFirstInSelection() const {
+ DCHECK(GetFrame());
+ const SelectionInDOMTree& selection =
+ GetFrame()->Selection().GetSelectionInDOMTree();
+ return selection.IsBaseFirst();
+}
+
+// TODO(tkent): Following four functions based on VisibleSelection should be
+// removed.
+static Position AnchorPosition(const VisibleSelection& selection) {
+ Position anchor =
+ selection.IsBaseFirst() ? selection.Start() : selection.End();
+ return anchor.ParentAnchoredEquivalent();
+}
+
+static Position FocusPosition(const VisibleSelection& selection) {
+ Position focus =
+ selection.IsBaseFirst() ? selection.End() : selection.Start();
+ return focus.ParentAnchoredEquivalent();
+}
+
+Node* DOMSelection::anchorNode() const {
+ if (Range* range = PrimaryRangeOrNull()) {
+ if (!GetFrame() || IsBaseFirstInSelection())
+ return range->startContainer();
+ return range->endContainer();
+ }
+ return nullptr;
+}
+
+unsigned DOMSelection::anchorOffset() const {
+ if (Range* range = PrimaryRangeOrNull()) {
+ if (!GetFrame() || IsBaseFirstInSelection())
+ return range->startOffset();
+ return range->endOffset();
+ }
+ return 0;
+}
+
+Node* DOMSelection::focusNode() const {
+ if (Range* range = PrimaryRangeOrNull()) {
+ if (!GetFrame() || IsBaseFirstInSelection())
+ return range->endContainer();
+ return range->startContainer();
+ }
+ return nullptr;
+}
+
+unsigned DOMSelection::focusOffset() const {
+ if (Range* range = PrimaryRangeOrNull()) {
+ if (!GetFrame() || IsBaseFirstInSelection())
+ return range->endOffset();
+ return range->startOffset();
+ }
+ return 0;
+}
+
+Node* DOMSelection::baseNode() const {
+ return anchorNode();
+}
+
+unsigned DOMSelection::baseOffset() const {
+ return anchorOffset();
+}
+
+Node* DOMSelection::extentNode() const {
+ return focusNode();
+}
+
+unsigned DOMSelection::extentOffset() const {
+ return focusOffset();
+}
+
+bool DOMSelection::isCollapsed() const {
+ if (!IsAvailable() || SelectionShadowAncestor(GetFrame()))
+ return true;
+ if (Range* range = PrimaryRangeOrNull())
+ return range->collapsed();
+ return true;
+}
+
+String DOMSelection::type() const {
+ if (!IsAvailable())
+ return String();
+ // This is a WebKit DOM extension, incompatible with an IE extension
+ // IE has this same attribute, but returns "none", "text" and "control"
+ // http://msdn.microsoft.com/en-us/library/ms534692(VS.85).aspx
+ if (rangeCount() == 0)
+ return "None";
+ // Do not use isCollapsed() here. We'd like to return "Range" for
+ // range-selection in text control elements.
+ if (GetFrame()->Selection().GetSelectionInDOMTree().IsCaret())
+ return "Caret";
+ return "Range";
+}
+
+unsigned DOMSelection::rangeCount() const {
+ if (!IsAvailable())
+ return 0;
+ if (DocumentCachedRange())
+ return 1;
+ if (GetFrame()
+ ->Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .IsNone())
+ return 0;
+ // Any selection can be adjusted to Range for Document.
+ if (IsSelectionOfDocument())
+ return 1;
+ // In ShadowRoot, we need to try adjustment.
+ if (CreateRangeFromSelectionEditor().IsNotNull())
+ return 1;
+ return 0;
+}
+
+// https://www.w3.org/TR/selection-api/#dom-selection-collapse
+void DOMSelection::collapse(Node* node,
+ unsigned offset,
+ ExceptionState& exception_state) {
+ if (!IsAvailable())
+ return;
+
+ // 1. If node is null, this method must behave identically as
+ // removeAllRanges() and abort these steps.
+ if (!node) {
+ UseCounter::Count(GetFrame(), WebFeature::kSelectionCollapseNull);
+ GetFrame()->Selection().Clear();
+ return;
+ }
+
+ // 2. The method must throw an IndexSizeError exception if offset is longer
+ // than node's length ([DOM4]) and abort these steps.
+ Range::CheckNodeWOffset(node, offset, exception_state);
+ if (exception_state.HadException())
+ return;
+
+ // 3. If node's root is not the document associated with the context object,
+ // abort these steps.
+ if (!IsValidForPosition(node))
+ return;
+
+ // 4. Otherwise, let newRange be a new range.
+ Range* new_range = Range::Create(*GetFrame()->GetDocument());
+
+ // 5. Set ([DOM4]) the start and the end of newRange to (node, offset).
+ new_range->setStart(node, offset, exception_state);
+ if (exception_state.HadException()) {
+ new_range->Dispose();
+ return;
+ }
+ new_range->setEnd(node, offset, exception_state);
+ if (exception_state.HadException()) {
+ new_range->Dispose();
+ return;
+ }
+
+ // 6. Set the context object's range to newRange.
+ UpdateFrameSelection(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(node, offset))
+ .Build(),
+ new_range,
+ SetSelectionOptions::Builder()
+ .SetIsDirectional(GetFrame()->Selection().IsDirectional())
+ .Build());
+}
+
+// https://www.w3.org/TR/selection-api/#dom-selection-collapsetoend
+void DOMSelection::collapseToEnd(ExceptionState& exception_state) {
+ if (!IsAvailable())
+ return;
+
+ // The method must throw InvalidStateError exception if the context object is
+ // empty.
+ if (rangeCount() == 0) {
+ exception_state.ThrowDOMException(kInvalidStateError,
+ "there is no selection.");
+ return;
+ }
+
+ if (Range* current_range = DocumentCachedRange()) {
+ // Otherwise, it must create a new range, set both its start and end to the
+ // end of the context object's range,
+ Range* new_range = current_range->cloneRange();
+ new_range->collapse(false);
+
+ // and then set the context object's range to the newly-created range.
+ SelectionInDOMTree::Builder builder;
+ builder.Collapse(new_range->EndPosition());
+ UpdateFrameSelection(builder.Build(), new_range, SetSelectionOptions());
+ } else {
+ // TODO(tkent): The Selection API doesn't define this behavior. We should
+ // discuss this on https://github.com/w3c/selection-api/issues/83.
+ SelectionInDOMTree::Builder builder;
+ builder.Collapse(
+ GetFrame()->Selection().GetSelectionInDOMTree().ComputeEndPosition());
+ UpdateFrameSelection(builder.Build(), nullptr, SetSelectionOptions());
+ }
+}
+
+// https://www.w3.org/TR/selection-api/#dom-selection-collapsetostart
+void DOMSelection::collapseToStart(ExceptionState& exception_state) {
+ if (!IsAvailable())
+ return;
+
+ // The method must throw InvalidStateError ([DOM4]) exception if the context
+ // object is empty.
+ if (rangeCount() == 0) {
+ exception_state.ThrowDOMException(kInvalidStateError,
+ "there is no selection.");
+ return;
+ }
+
+ if (Range* current_range = DocumentCachedRange()) {
+ // Otherwise, it must create a new range, set both its start and end to the
+ // start of the context object's range,
+ Range* new_range = current_range->cloneRange();
+ new_range->collapse(true);
+
+ // and then set the context object's range to the newly-created range.
+ SelectionInDOMTree::Builder builder;
+ builder.Collapse(new_range->StartPosition());
+ UpdateFrameSelection(builder.Build(), new_range, SetSelectionOptions());
+ } else {
+ // TODO(tkent): The Selection API doesn't define this behavior. We should
+ // discuss this on https://github.com/w3c/selection-api/issues/83.
+ SelectionInDOMTree::Builder builder;
+ builder.Collapse(
+ GetFrame()->Selection().GetSelectionInDOMTree().ComputeStartPosition());
+ UpdateFrameSelection(builder.Build(), nullptr, SetSelectionOptions());
+ }
+}
+
+void DOMSelection::empty() {
+ if (!IsAvailable())
+ return;
+ GetFrame()->Selection().Clear();
+}
+
+void DOMSelection::setBaseAndExtent(Node* base_node,
+ unsigned base_offset,
+ Node* extent_node,
+ unsigned extent_offset,
+ ExceptionState& exception_state) {
+ if (!IsAvailable())
+ return;
+
+ // TODO(editing-dev): Behavior on where base or extent is null is still
+ // under discussion: https://github.com/w3c/selection-api/issues/72
+ if (!base_node) {
+ UseCounter::Count(GetFrame(), WebFeature::kSelectionSetBaseAndExtentNull);
+ GetFrame()->Selection().Clear();
+ return;
+ }
+ if (!extent_node) {
+ UseCounter::Count(GetFrame(), WebFeature::kSelectionSetBaseAndExtentNull);
+ extent_offset = 0;
+ }
+
+ Range::CheckNodeWOffset(base_node, base_offset, exception_state);
+ if (exception_state.HadException())
+ return;
+ if (extent_node) {
+ Range::CheckNodeWOffset(extent_node, extent_offset, exception_state);
+ if (exception_state.HadException())
+ return;
+ }
+
+ if (!IsValidForPosition(base_node) || !IsValidForPosition(extent_node))
+ return;
+
+ ClearCachedRangeIfSelectionOfDocument();
+
+ Position base_position(base_node, base_offset);
+ Position extent_position(extent_node, extent_offset);
+ Range* new_range = Range::Create(base_node->GetDocument());
+ if (extent_position.IsNull()) {
+ new_range->setStart(base_node, base_offset);
+ new_range->setEnd(base_node, base_offset);
+ } else if (base_position < extent_position) {
+ new_range->setStart(base_node, base_offset);
+ new_range->setEnd(extent_node, extent_offset);
+ } else {
+ new_range->setStart(extent_node, extent_offset);
+ new_range->setEnd(base_node, base_offset);
+ }
+ UpdateFrameSelection(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtentDeprecated(base_position, extent_position)
+ .Build(),
+ new_range, SetSelectionOptions::Builder().SetIsDirectional(true).Build());
+}
+
+void DOMSelection::modify(const String& alter_string,
+ const String& direction_string,
+ const String& granularity_string) {
+ if (!IsAvailable())
+ return;
+
+ SelectionModifyAlteration alter;
+ if (DeprecatedEqualIgnoringCase(alter_string, "extend"))
+ alter = SelectionModifyAlteration::kExtend;
+ else if (DeprecatedEqualIgnoringCase(alter_string, "move"))
+ alter = SelectionModifyAlteration::kMove;
+ else
+ return;
+
+ SelectionModifyDirection direction;
+ if (DeprecatedEqualIgnoringCase(direction_string, "forward"))
+ direction = SelectionModifyDirection::kForward;
+ else if (DeprecatedEqualIgnoringCase(direction_string, "backward"))
+ direction = SelectionModifyDirection::kBackward;
+ else if (DeprecatedEqualIgnoringCase(direction_string, "left"))
+ direction = SelectionModifyDirection::kLeft;
+ else if (DeprecatedEqualIgnoringCase(direction_string, "right"))
+ direction = SelectionModifyDirection::kRight;
+ else
+ return;
+
+ TextGranularity granularity;
+ if (DeprecatedEqualIgnoringCase(granularity_string, "character"))
+ granularity = TextGranularity::kCharacter;
+ else if (DeprecatedEqualIgnoringCase(granularity_string, "word"))
+ granularity = TextGranularity::kWord;
+ else if (DeprecatedEqualIgnoringCase(granularity_string, "sentence"))
+ granularity = TextGranularity::kSentence;
+ else if (DeprecatedEqualIgnoringCase(granularity_string, "line"))
+ granularity = TextGranularity::kLine;
+ else if (DeprecatedEqualIgnoringCase(granularity_string, "paragraph"))
+ granularity = TextGranularity::kParagraph;
+ else if (DeprecatedEqualIgnoringCase(granularity_string, "lineboundary"))
+ granularity = TextGranularity::kLineBoundary;
+ else if (DeprecatedEqualIgnoringCase(granularity_string, "sentenceboundary"))
+ granularity = TextGranularity::kSentenceBoundary;
+ else if (DeprecatedEqualIgnoringCase(granularity_string, "paragraphboundary"))
+ granularity = TextGranularity::kParagraphBoundary;
+ else if (DeprecatedEqualIgnoringCase(granularity_string, "documentboundary"))
+ granularity = TextGranularity::kDocumentBoundary;
+ else
+ return;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetFrame()->GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ Element* focused_element = GetFrame()->GetDocument()->FocusedElement();
+ GetFrame()->Selection().Modify(alter, direction, granularity,
+ SetSelectionBy::kSystem);
+ if (GetFrame() && GetFrame()->GetDocument() &&
+ focused_element != GetFrame()->GetDocument()->FocusedElement())
+ UseCounter::Count(GetFrame(), WebFeature::kSelectionFuncionsChangeFocus);
+}
+
+// https://www.w3.org/TR/selection-api/#dom-selection-extend
+void DOMSelection::extend(Node* node,
+ unsigned offset,
+ ExceptionState& exception_state) {
+ DCHECK(node);
+ if (!IsAvailable())
+ return;
+
+ // 1. If node's root is not the document associated with the context object,
+ // abort these steps.
+ if (!IsValidForPosition(node))
+ return;
+
+ // 2. If the context object is empty, throw an InvalidStateError exception and
+ // abort these steps.
+ if (rangeCount() == 0) {
+ exception_state.ThrowDOMException(
+ kInvalidStateError, "This Selection object doesn't have any Ranges.");
+ return;
+ }
+
+ Range::CheckNodeWOffset(node, offset, exception_state);
+ if (exception_state.HadException())
+ return;
+
+ // 3. Let oldAnchor and oldFocus be the context object's anchor and focus, and
+ // let newFocus be the boundary point (node, offset).
+ const Position old_anchor(anchorNode(), anchorOffset());
+ DCHECK(!old_anchor.IsNull());
+ const Position new_focus(node, offset);
+
+ ClearCachedRangeIfSelectionOfDocument();
+
+ // 4. Let newRange be a new range.
+ Range* new_range = Range::Create(*GetFrame()->GetDocument());
+
+ // 5. If node's root is not the same as the context object's range's root, set
+ // newRange's start and end to newFocus.
+ // E.g. oldAnchor might point in shadow Text node in TextControlElement.
+ if (old_anchor.AnchorNode()->TreeRoot() != node->TreeRoot()) {
+ new_range->setStart(node, offset);
+ new_range->setEnd(node, offset);
+
+ } else if (old_anchor <= new_focus) {
+ // 6. Otherwise, if oldAnchor is before or equal to newFocus, set newRange's
+ // start to oldAnchor, then set its end to newFocus.
+ new_range->setStart(old_anchor.AnchorNode(),
+ old_anchor.OffsetInContainerNode());
+ new_range->setEnd(node, offset);
+
+ } else {
+ // 7. Otherwise, set newRange's start to newFocus, then set its end to
+ // oldAnchor.
+ new_range->setStart(node, offset);
+ new_range->setEnd(old_anchor.AnchorNode(),
+ old_anchor.OffsetInContainerNode());
+ }
+
+ // 8. Set the context object's range to newRange.
+ SelectionInDOMTree::Builder builder;
+ if (new_range->collapsed())
+ builder.Collapse(new_focus);
+ else
+ builder.Collapse(old_anchor).Extend(new_focus);
+ UpdateFrameSelection(
+ builder.Build(), new_range,
+ SetSelectionOptions::Builder().SetIsDirectional(true).Build());
+}
+
+Range* DOMSelection::getRangeAt(unsigned index,
+ ExceptionState& exception_state) const {
+ if (!IsAvailable())
+ return nullptr;
+
+ if (index >= rangeCount()) {
+ exception_state.ThrowDOMException(
+ kIndexSizeError, String::Number(index) + " is not a valid index.");
+ return nullptr;
+ }
+
+ // If you're hitting this, you've added broken multi-range selection support
+ DCHECK_EQ(rangeCount(), 1u);
+
+ if (Range* cached_range = DocumentCachedRange())
+ return cached_range;
+
+ Range* range = CreateRange(CreateRangeFromSelectionEditor());
+ CacheRangeIfSelectionOfDocument(range);
+ return range;
+}
+
+Range* DOMSelection::PrimaryRangeOrNull() const {
+ return rangeCount() > 0 ? getRangeAt(0, ASSERT_NO_EXCEPTION) : nullptr;
+}
+
+EphemeralRange DOMSelection::CreateRangeFromSelectionEditor() const {
+ const VisibleSelection& selection = GetVisibleSelection();
+ const Position& anchor = blink::AnchorPosition(selection);
+ if (IsSelectionOfDocument() && !anchor.AnchorNode()->IsInShadowTree())
+ return FirstEphemeralRangeOf(selection);
+
+ Node* const anchor_node = ShadowAdjustedNode(anchor);
+ if (!anchor_node) // crbug.com/595100
+ return EphemeralRange();
+
+ const Position& focus = FocusPosition(selection);
+ const Position shadow_adjusted_focus =
+ Position(ShadowAdjustedNode(focus), ShadowAdjustedOffset(focus));
+ const Position shadow_adjusted_anchor =
+ Position(anchor_node, ShadowAdjustedOffset(anchor));
+ if (selection.IsBaseFirst())
+ return EphemeralRange(shadow_adjusted_anchor, shadow_adjusted_focus);
+ return EphemeralRange(shadow_adjusted_focus, shadow_adjusted_anchor);
+}
+
+bool DOMSelection::IsSelectionOfDocument() const {
+ return tree_scope_ == tree_scope_->GetDocument();
+}
+
+void DOMSelection::CacheRangeIfSelectionOfDocument(Range* range) const {
+ if (!IsSelectionOfDocument())
+ return;
+ if (!GetFrame())
+ return;
+ GetFrame()->Selection().CacheRangeOfDocument(range);
+}
+
+Range* DOMSelection::DocumentCachedRange() const {
+ if (!IsSelectionOfDocument())
+ return nullptr;
+ return GetFrame()->Selection().DocumentCachedRange();
+}
+
+void DOMSelection::ClearCachedRangeIfSelectionOfDocument() {
+ if (!IsSelectionOfDocument())
+ return;
+ GetFrame()->Selection().ClearDocumentCachedRange();
+}
+
+void DOMSelection::removeRange(Range* range) {
+ DCHECK(range);
+ if (!IsAvailable())
+ return;
+ if (range == PrimaryRangeOrNull())
+ GetFrame()->Selection().Clear();
+}
+
+void DOMSelection::removeAllRanges() {
+ if (!IsAvailable())
+ return;
+ GetFrame()->Selection().Clear();
+}
+
+void DOMSelection::addRange(Range* new_range) {
+ DCHECK(new_range);
+
+ if (!IsAvailable())
+ return;
+
+ if (new_range->OwnerDocument() != GetFrame()->GetDocument())
+ return;
+
+ if (!new_range->IsConnected()) {
+ AddConsoleError("The given range isn't in document.");
+ return;
+ }
+
+ FrameSelection& selection = GetFrame()->Selection();
+
+ if (new_range->OwnerDocument() != selection.GetDocument()) {
+ // "editing/selection/selection-in-iframe-removed-crash.html" goes here.
+ return;
+ }
+
+ if (rangeCount() == 0) {
+ UpdateFrameSelection(SelectionInDOMTree::Builder()
+ .Collapse(new_range->StartPosition())
+ .Extend(new_range->EndPosition())
+ .Build(),
+ new_range, SetSelectionOptions());
+ return;
+ }
+
+ Range* original_range = PrimaryRangeOrNull();
+ DCHECK(original_range);
+
+ if (original_range->startContainer()->GetTreeScope() !=
+ new_range->startContainer()->GetTreeScope()) {
+ return;
+ }
+
+ if (original_range->compareBoundaryPoints(Range::kStartToEnd, new_range,
+ ASSERT_NO_EXCEPTION) < 0 ||
+ new_range->compareBoundaryPoints(Range::kStartToEnd, original_range,
+ ASSERT_NO_EXCEPTION) < 0) {
+ return;
+ }
+
+ // TODO(tkent): "Merge the ranges if they intersect" was removed. We show a
+ // warning message for a while, and continue to collect the usage data.
+ // <https://code.google.com/p/chromium/issues/detail?id=353069>.
+ Deprecation::CountDeprecation(GetFrame(),
+ WebFeature::kSelectionAddRangeIntersect);
+}
+
+// https://www.w3.org/TR/selection-api/#dom-selection-deletefromdocument
+void DOMSelection::deleteFromDocument() {
+ if (!IsAvailable())
+ return;
+
+ // The method must invoke deleteContents() ([DOM4]) on the context object's
+ // range if the context object is not empty. Otherwise the method must do
+ // nothing.
+ if (Range* range = DocumentCachedRange()) {
+ range->deleteContents(ASSERT_NO_EXCEPTION);
+ return;
+ }
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetFrame()->GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // The following code is necessary for
+ // editing/selection/deleteFromDocument-crash.html, which assumes
+ // deleteFromDocument() for text selection in a TEXTAREA deletes the TEXTAREA
+ // value.
+
+ FrameSelection& selection = GetFrame()->Selection();
+
+ if (selection.ComputeVisibleSelectionInDOMTree().IsNone())
+ return;
+
+ Range* selected_range =
+ CreateRange(selection.ComputeVisibleSelectionInDOMTree()
+ .ToNormalizedEphemeralRange());
+ if (!selected_range)
+ return;
+
+ // |selectedRange| may point nodes in a different root.
+ selected_range->deleteContents(ASSERT_NO_EXCEPTION);
+}
+
+bool DOMSelection::containsNode(const Node* n, bool allow_partial) const {
+ DCHECK(n);
+
+ if (!IsAvailable())
+ return false;
+
+ if (GetFrame()->GetDocument() != n->GetDocument())
+ return false;
+
+ unsigned node_index = n->NodeIndex();
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ // |VisibleSelection::toNormalizedEphemeralRange| requires clean layout.
+ GetFrame()->GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ FrameSelection& selection = GetFrame()->Selection();
+ const EphemeralRange selected_range =
+ selection.ComputeVisibleSelectionInDOMTreeDeprecated()
+ .ToNormalizedEphemeralRange();
+ if (selected_range.IsNull())
+ return false;
+
+ ContainerNode* parent_node = n->parentNode();
+ if (!parent_node)
+ return false;
+
+ const Position start_position =
+ selected_range.StartPosition().ToOffsetInAnchor();
+ const Position end_position = selected_range.EndPosition().ToOffsetInAnchor();
+ DummyExceptionStateForTesting exception_state;
+ bool node_fully_selected =
+ Range::compareBoundaryPoints(
+ parent_node, node_index, start_position.ComputeContainerNode(),
+ start_position.OffsetInContainerNode(), exception_state) >= 0 &&
+ !exception_state.HadException() &&
+ Range::compareBoundaryPoints(
+ parent_node, node_index + 1, end_position.ComputeContainerNode(),
+ end_position.OffsetInContainerNode(), exception_state) <= 0 &&
+ !exception_state.HadException();
+ if (exception_state.HadException())
+ return false;
+ if (node_fully_selected)
+ return true;
+
+ bool node_fully_unselected =
+ (Range::compareBoundaryPoints(
+ parent_node, node_index, end_position.ComputeContainerNode(),
+ end_position.OffsetInContainerNode(), exception_state) > 0 &&
+ !exception_state.HadException()) ||
+ (Range::compareBoundaryPoints(
+ parent_node, node_index + 1, start_position.ComputeContainerNode(),
+ start_position.OffsetInContainerNode(), exception_state) < 0 &&
+ !exception_state.HadException());
+ DCHECK(!exception_state.HadException());
+ if (node_fully_unselected)
+ return false;
+
+ return allow_partial || n->IsTextNode();
+}
+
+void DOMSelection::selectAllChildren(Node* n, ExceptionState& exception_state) {
+ DCHECK(n);
+
+ // This doesn't (and shouldn't) select text node characters.
+ setBaseAndExtent(n, 0, n, n->CountChildren(), exception_state);
+}
+
+String DOMSelection::toString() {
+ if (!IsAvailable())
+ return String();
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetFrame()->GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ GetFrame()->GetDocument()->Lifecycle());
+
+ const EphemeralRange range = GetFrame()
+ ->Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .ToNormalizedEphemeralRange();
+ return PlainText(
+ range,
+ TextIteratorBehavior::Builder().SetForSelectionToString(true).Build());
+}
+
+Node* DOMSelection::ShadowAdjustedNode(const Position& position) const {
+ if (position.IsNull())
+ return nullptr;
+
+ Node* container_node = position.ComputeContainerNode();
+ Node* adjusted_node = tree_scope_->AncestorInThisScope(container_node);
+
+ if (!adjusted_node)
+ return nullptr;
+
+ if (container_node == adjusted_node)
+ return container_node;
+
+ DCHECK(!adjusted_node->IsShadowRoot()) << adjusted_node;
+ return adjusted_node->ParentOrShadowHostNode();
+}
+
+unsigned DOMSelection::ShadowAdjustedOffset(const Position& position) const {
+ if (position.IsNull())
+ return 0;
+
+ Node* container_node = position.ComputeContainerNode();
+ Node* adjusted_node = tree_scope_->AncestorInThisScope(container_node);
+
+ if (!adjusted_node)
+ return 0;
+
+ if (container_node == adjusted_node)
+ return position.ComputeOffsetInContainerNode();
+
+ return adjusted_node->NodeIndex();
+}
+
+bool DOMSelection::IsValidForPosition(Node* node) const {
+ DCHECK(GetFrame());
+ if (!node)
+ return true;
+ return node->GetDocument() == GetFrame()->GetDocument() &&
+ node->isConnected();
+}
+
+void DOMSelection::AddConsoleError(const String& message) {
+ if (tree_scope_)
+ tree_scope_->GetDocument().AddConsoleMessage(
+ ConsoleMessage::Create(kJSMessageSource, kErrorMessageLevel, message));
+}
+
+void DOMSelection::Trace(blink::Visitor* visitor) {
+ visitor->Trace(tree_scope_);
+ ScriptWrappable::Trace(visitor);
+ ContextClient::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/dom_selection.h b/chromium/third_party/blink/renderer/core/editing/dom_selection.h
new file mode 100644
index 00000000000..985ea2896d6
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/dom_selection.h
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2007 Apple Inc. All rights reserved.
+ * Copyright (C) 2012 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_DOM_SELECTION_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_DOM_SELECTION_H_
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/context_lifecycle_observer.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+
+namespace blink {
+
+class ExceptionState;
+class Node;
+class Range;
+class SetSelectionOptions;
+class TreeScope;
+
+class CORE_EXPORT DOMSelection final : public ScriptWrappable,
+ public ContextClient {
+ DEFINE_WRAPPERTYPEINFO();
+ USING_GARBAGE_COLLECTED_MIXIN(DOMSelection);
+
+ public:
+ static DOMSelection* Create(const TreeScope* tree_scope) {
+ return new DOMSelection(tree_scope);
+ }
+
+ void ClearTreeScope();
+
+ // Safari Selection Object API
+ // These methods return the valid equivalents of internal editing positions.
+ Node* baseNode() const;
+ unsigned baseOffset() const;
+ Node* extentNode() const;
+ unsigned extentOffset() const;
+ String type() const;
+ void setBaseAndExtent(Node* base_node,
+ unsigned base_offset,
+ Node* extent_node,
+ unsigned extent_offset,
+ ExceptionState& = ASSERT_NO_EXCEPTION);
+ void modify(const String& alter,
+ const String& direction,
+ const String& granularity);
+
+ // Mozilla Selection Object API
+ // In Firefox, anchor/focus are the equal to the start/end of the selection,
+ // but reflect the direction in which the selection was made by the user. That
+ // does not mean that they are base/extent, since the base/extent don't
+ // reflect expansion.
+ // These methods return the valid equivalents of internal editing positions.
+ Node* anchorNode() const;
+ unsigned anchorOffset() const;
+ Node* focusNode() const;
+ unsigned focusOffset() const;
+ bool isCollapsed() const;
+ unsigned rangeCount() const;
+ void collapse(Node*, unsigned offset, ExceptionState&);
+ void collapseToEnd(ExceptionState&);
+ void collapseToStart(ExceptionState&);
+ void extend(Node*, unsigned offset, ExceptionState&);
+ Range* getRangeAt(unsigned, ExceptionState&) const;
+ void removeRange(Range*);
+ void removeAllRanges();
+ void addRange(Range*);
+ void deleteFromDocument();
+ bool containsNode(const Node*, bool partly_contained) const;
+ void selectAllChildren(Node*, ExceptionState&);
+
+ String toString();
+
+ // Microsoft Selection Object API
+ void empty();
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ explicit DOMSelection(const TreeScope*);
+
+ bool IsAvailable() const;
+
+ void UpdateFrameSelection(const SelectionInDOMTree&,
+ Range*,
+ const SetSelectionOptions&) const;
+ // Convenience methods for accessors, does not check m_frame present.
+ VisibleSelection GetVisibleSelection() const;
+ bool IsBaseFirstInSelection() const;
+ const Position& AnchorPosition() const;
+
+ Node* ShadowAdjustedNode(const Position&) const;
+ unsigned ShadowAdjustedOffset(const Position&) const;
+
+ bool IsValidForPosition(Node*) const;
+
+ void AddConsoleError(const String& message);
+ Range* PrimaryRangeOrNull() const;
+ EphemeralRange CreateRangeFromSelectionEditor() const;
+
+ bool IsSelectionOfDocument() const;
+ void CacheRangeIfSelectionOfDocument(Range*) const;
+ Range* DocumentCachedRange() const;
+ void ClearCachedRangeIfSelectionOfDocument();
+
+ Member<const TreeScope> tree_scope_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_DOM_SELECTION_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/drag_caret.cc b/chromium/third_party/blink/renderer/core/editing/drag_caret.cc
new file mode 100644
index 00000000000..7860c2ec1cb
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/drag_caret.cc
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2004, 2008, 2009, 2010 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/drag_caret.h"
+
+#include "third_party/blink/renderer/core/editing/caret_display_item_client.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/layout/layout_view.h"
+#include "third_party/blink/renderer/core/paint/paint_layer.h"
+
+namespace blink {
+
+DragCaret::DragCaret() : display_item_client_(new CaretDisplayItemClient()) {}
+
+DragCaret::~DragCaret() = default;
+
+DragCaret* DragCaret::Create() {
+ return new DragCaret;
+}
+
+void DragCaret::ClearPreviousVisualRect(const LayoutBlock& block) {
+ display_item_client_->ClearPreviousVisualRect(block);
+}
+
+void DragCaret::LayoutBlockWillBeDestroyed(const LayoutBlock& block) {
+ display_item_client_->LayoutBlockWillBeDestroyed(block);
+}
+
+void DragCaret::UpdateStyleAndLayoutIfNeeded() {
+ display_item_client_->UpdateStyleAndLayoutIfNeeded(
+ RootEditableElementOf(position_.GetPosition()) ? position_
+ : PositionWithAffinity());
+}
+
+void DragCaret::InvalidatePaint(const LayoutBlock& block,
+ const PaintInvalidatorContext& context) {
+ display_item_client_->InvalidatePaint(block, context);
+}
+
+bool DragCaret::IsContentRichlyEditable() const {
+ return IsRichlyEditablePosition(position_.GetPosition());
+}
+
+void DragCaret::SetCaretPosition(const PositionWithAffinity& position) {
+ position_ = CreateVisiblePosition(position).ToPositionWithAffinity();
+ Document* document = nullptr;
+ if (Node* node = position_.AnchorNode()) {
+ document = &node->GetDocument();
+ SetContext(document);
+ }
+}
+
+void DragCaret::NodeChildrenWillBeRemoved(ContainerNode& container) {
+ if (!HasCaret() || !container.InActiveDocument())
+ return;
+ Node* const anchor_node = position_.GetPosition().AnchorNode();
+ if (!anchor_node || anchor_node == container)
+ return;
+ if (!container.IsShadowIncludingInclusiveAncestorOf(anchor_node))
+ return;
+ Clear();
+}
+
+void DragCaret::NodeWillBeRemoved(Node& node) {
+ if (!HasCaret() || !node.InActiveDocument())
+ return;
+ Node* const anchor_node = position_.GetPosition().AnchorNode();
+ if (!anchor_node)
+ return;
+ if (!node.IsShadowIncludingInclusiveAncestorOf(anchor_node))
+ return;
+ Clear();
+}
+
+void DragCaret::Trace(blink::Visitor* visitor) {
+ visitor->Trace(position_);
+ SynchronousMutationObserver::Trace(visitor);
+}
+
+bool DragCaret::ShouldPaintCaret(const LayoutBlock& block) const {
+ return display_item_client_->ShouldPaintCaret(block);
+}
+
+void DragCaret::PaintDragCaret(const LocalFrame* frame,
+ GraphicsContext& context,
+ const LayoutPoint& paint_offset) const {
+ if (position_.AnchorNode()->GetDocument().GetFrame() != frame)
+ return;
+ display_item_client_->PaintCaret(context, paint_offset,
+ DisplayItem::kDragCaret);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/drag_caret.h b/chromium/third_party/blink/renderer/core/editing/drag_caret.h
new file mode 100644
index 00000000000..8f0cd44a968
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/drag_caret.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_DRAG_CARET_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_DRAG_CARET_H_
+
+#include <memory>
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/dom/synchronous_mutation_observer.h"
+#include "third_party/blink/renderer/core/editing/caret_display_item_client.h"
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+#include "third_party/blink/renderer/platform/graphics/paint_invalidation_reason.h"
+
+namespace blink {
+
+class LayoutBlock;
+struct PaintInvalidatorContext;
+
+class DragCaret final : public GarbageCollectedFinalized<DragCaret>,
+ public SynchronousMutationObserver {
+ USING_GARBAGE_COLLECTED_MIXIN(DragCaret);
+
+ public:
+ static DragCaret* Create();
+
+ virtual ~DragCaret();
+
+ // Paint invalidation methods delegating to CaretDisplayItemClient.
+ void ClearPreviousVisualRect(const LayoutBlock&);
+ void LayoutBlockWillBeDestroyed(const LayoutBlock&);
+ void UpdateStyleAndLayoutIfNeeded();
+ void InvalidatePaint(const LayoutBlock&, const PaintInvalidatorContext&);
+
+ bool ShouldPaintCaret(const LayoutBlock&) const;
+ void PaintDragCaret(const LocalFrame*,
+ GraphicsContext&,
+ const LayoutPoint&) const;
+
+ bool IsContentRichlyEditable() const;
+
+ bool HasCaret() const { return position_.IsNotNull(); }
+ const PositionWithAffinity& CaretPosition() { return position_; }
+ void SetCaretPosition(const PositionWithAffinity&);
+ void Clear() { SetCaretPosition(PositionWithAffinity()); }
+
+ void Trace(blink::Visitor*);
+
+ private:
+ DragCaret();
+
+ // Implementations of |SynchronousMutationObserver|
+ void NodeChildrenWillBeRemoved(ContainerNode&) final;
+ void NodeWillBeRemoved(Node&) final;
+
+ PositionWithAffinity position_;
+ const std::unique_ptr<CaretDisplayItemClient> display_item_client_;
+
+ DISALLOW_COPY_AND_ASSIGN(DragCaret);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_DRAG_CARET_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/editing_behavior.cc b/chromium/third_party/blink/renderer/core/editing/editing_behavior.cc
new file mode 100644
index 00000000000..dc9f9a15216
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editing_behavior.cc
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2006, 2007 Apple, Inc. All rights reserved.
+ * Copyright (C) 2012 Google, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/editing_behavior.h"
+
+#include "build/build_config.h"
+#include "third_party/blink/public/platform/web_input_event.h"
+#include "third_party/blink/public/web/web_settings.h"
+#include "third_party/blink/renderer/core/events/keyboard_event.h"
+#include "third_party/blink/renderer/platform/keyboard_codes.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+
+namespace blink {
+
+namespace {
+
+//
+// The below code was adapted from the WebKit file webview.cpp
+//
+
+const unsigned kCtrlKey = WebInputEvent::kControlKey;
+const unsigned kAltKey = WebInputEvent::kAltKey;
+const unsigned kShiftKey = WebInputEvent::kShiftKey;
+const unsigned kMetaKey = WebInputEvent::kMetaKey;
+#if defined(OS_MACOSX)
+// Aliases for the generic key defintions to make kbd shortcuts definitions more
+// readable on OS X.
+const unsigned kOptionKey = kAltKey;
+
+// Do not use this constant for anything but cursor movement commands. Keys
+// with cmd set have their |isSystemKey| bit set, so chances are the shortcut
+// will not be executed. Another, less important, reason is that shortcuts
+// defined in the layoutObject do not blink the menu item that they triggered.
+// See http://crbug.com/25856 and the bugs linked from there for details.
+const unsigned kCommandKey = kMetaKey;
+#endif
+
+// Keys with special meaning. These will be delegated to the editor using
+// the execCommand() method
+struct KeyboardCodeKeyDownEntry {
+ unsigned virtual_key;
+ unsigned modifiers;
+ const char* name;
+};
+
+struct KeyboardCodeKeyPressEntry {
+ unsigned char_code;
+ unsigned modifiers;
+ const char* name;
+};
+
+// DomKey has a broader range than KeyboardCode, we need DomKey to handle some
+// special keys.
+// Note: We cannot use DomKey for printable keys since it may vary based on
+// locale.
+struct DomKeyKeyDownEntry {
+ const char* key;
+ unsigned modifiers;
+ const char* name;
+};
+
+// Key bindings with command key on Mac and alt key on other platforms are
+// marked as system key events and will be ignored (with the exception
+// of Command-B and Command-I) so they shouldn't be added here.
+const KeyboardCodeKeyDownEntry kKeyboardCodeKeyDownEntries[] = {
+ {VKEY_LEFT, 0, "MoveLeft"},
+ {VKEY_LEFT, kShiftKey, "MoveLeftAndModifySelection"},
+#if defined(OS_MACOSX)
+ {VKEY_LEFT, kOptionKey, "MoveWordLeft"},
+ {VKEY_LEFT, kOptionKey | kShiftKey, "MoveWordLeftAndModifySelection"},
+#else
+ {VKEY_LEFT, kCtrlKey, "MoveWordLeft"},
+ {VKEY_LEFT, kCtrlKey | kShiftKey, "MoveWordLeftAndModifySelection"},
+#endif
+ {VKEY_RIGHT, 0, "MoveRight"},
+ {VKEY_RIGHT, kShiftKey, "MoveRightAndModifySelection"},
+#if defined(OS_MACOSX)
+ {VKEY_RIGHT, kOptionKey, "MoveWordRight"},
+ {VKEY_RIGHT, kOptionKey | kShiftKey, "MoveWordRightAndModifySelection"},
+#else
+ {VKEY_RIGHT, kCtrlKey, "MoveWordRight"},
+ {VKEY_RIGHT, kCtrlKey | kShiftKey, "MoveWordRightAndModifySelection"},
+#endif
+ {VKEY_UP, 0, "MoveUp"},
+ {VKEY_UP, kShiftKey, "MoveUpAndModifySelection"},
+ {VKEY_PRIOR, kShiftKey, "MovePageUpAndModifySelection"},
+ {VKEY_DOWN, 0, "MoveDown"},
+ {VKEY_DOWN, kShiftKey, "MoveDownAndModifySelection"},
+ {VKEY_NEXT, kShiftKey, "MovePageDownAndModifySelection"},
+#if !defined(OS_MACOSX)
+ {VKEY_UP, kCtrlKey, "MoveParagraphBackward"},
+ {VKEY_UP, kCtrlKey | kShiftKey, "MoveParagraphBackwardAndModifySelection"},
+ {VKEY_DOWN, kCtrlKey, "MoveParagraphForward"},
+ {VKEY_DOWN, kCtrlKey | kShiftKey, "MoveParagraphForwardAndModifySelection"},
+ {VKEY_PRIOR, 0, "MovePageUp"},
+ {VKEY_NEXT, 0, "MovePageDown"},
+#endif
+ {VKEY_HOME, 0, "MoveToBeginningOfLine"},
+ {VKEY_HOME, kShiftKey, "MoveToBeginningOfLineAndModifySelection"},
+#if defined(OS_MACOSX)
+ {VKEY_PRIOR, kOptionKey, "MovePageUp"},
+ {VKEY_NEXT, kOptionKey, "MovePageDown"},
+#endif
+#if !defined(OS_MACOSX)
+ {VKEY_HOME, kCtrlKey, "MoveToBeginningOfDocument"},
+ {VKEY_HOME, kCtrlKey | kShiftKey,
+ "MoveToBeginningOfDocumentAndModifySelection"},
+#endif
+ {VKEY_END, 0, "MoveToEndOfLine"},
+ {VKEY_END, kShiftKey, "MoveToEndOfLineAndModifySelection"},
+#if !defined(OS_MACOSX)
+ {VKEY_END, kCtrlKey, "MoveToEndOfDocument"},
+ {VKEY_END, kCtrlKey | kShiftKey, "MoveToEndOfDocumentAndModifySelection"},
+#endif
+ {VKEY_BACK, 0, "DeleteBackward"},
+ {VKEY_BACK, kShiftKey, "DeleteBackward"},
+ {VKEY_DELETE, 0, "DeleteForward"},
+#if defined(OS_MACOSX)
+ {VKEY_BACK, kOptionKey, "DeleteWordBackward"},
+ {VKEY_DELETE, kOptionKey, "DeleteWordForward"},
+#else
+ {VKEY_BACK, kCtrlKey, "DeleteWordBackward"},
+ {VKEY_DELETE, kCtrlKey, "DeleteWordForward"},
+#endif
+#if defined(OS_MACOSX)
+ {'B', kCommandKey, "ToggleBold"},
+ {'I', kCommandKey, "ToggleItalic"},
+#else
+ {'B', kCtrlKey, "ToggleBold"},
+ {'I', kCtrlKey, "ToggleItalic"},
+#endif
+ {'U', kCtrlKey, "ToggleUnderline"},
+ {VKEY_ESCAPE, 0, "Cancel"},
+ {VKEY_OEM_PERIOD, kCtrlKey, "Cancel"},
+ {VKEY_TAB, 0, "InsertTab"},
+ {VKEY_TAB, kShiftKey, "InsertBacktab"},
+ {VKEY_RETURN, 0, "InsertNewline"},
+ {VKEY_RETURN, kCtrlKey, "InsertNewline"},
+ {VKEY_RETURN, kAltKey, "InsertNewline"},
+ {VKEY_RETURN, kAltKey | kShiftKey, "InsertNewline"},
+ {VKEY_RETURN, kShiftKey, "InsertLineBreak"},
+ {VKEY_INSERT, kCtrlKey, "Copy"},
+ {VKEY_INSERT, kShiftKey, "Paste"},
+ {VKEY_DELETE, kShiftKey, "Cut"},
+#if !defined(OS_MACOSX)
+ // On OS X, we pipe these back to the browser, so that it can do menu item
+ // blinking.
+ {'C', kCtrlKey, "Copy"},
+ {'V', kCtrlKey, "Paste"},
+ {'V', kCtrlKey | kShiftKey, "PasteAndMatchStyle"},
+ {'X', kCtrlKey, "Cut"},
+ {'A', kCtrlKey, "SelectAll"},
+ {'Z', kCtrlKey, "Undo"},
+ {'Z', kCtrlKey | kShiftKey, "Redo"},
+ {'Y', kCtrlKey, "Redo"},
+#endif
+ {VKEY_INSERT, 0, "OverWrite"},
+};
+
+const KeyboardCodeKeyPressEntry kKeyboardCodeKeyPressEntries[] = {
+ {'\t', 0, "InsertTab"},
+ {'\t', kShiftKey, "InsertBacktab"},
+ {'\r', 0, "InsertNewline"},
+ {'\r', kShiftKey, "InsertLineBreak"},
+};
+
+const DomKeyKeyDownEntry kDomKeyKeyDownEntries[] = {
+ {"Copy", 0, "Copy"},
+ {"Cut", 0, "Cut"},
+ {"Paste", 0, "Paste"},
+};
+
+const char* LookupCommandNameFromDomKeyKeyDown(const String& key,
+ unsigned modifiers) {
+ // This table is not likely to grow, so sequential search is fine here.
+ for (const auto& entry : kDomKeyKeyDownEntries) {
+ if (key == entry.key && modifiers == entry.modifiers)
+ return entry.name;
+ }
+ return nullptr;
+}
+
+} // anonymous namespace
+
+const char* EditingBehavior::InterpretKeyEvent(
+ const KeyboardEvent& event) const {
+ const WebKeyboardEvent* key_event = event.KeyEvent();
+ if (!key_event)
+ return "";
+
+ static HashMap<int, const char*>* key_down_commands_map = nullptr;
+ static HashMap<int, const char*>* key_press_commands_map = nullptr;
+
+ if (!key_down_commands_map) {
+ key_down_commands_map = new HashMap<int, const char*>;
+ key_press_commands_map = new HashMap<int, const char*>;
+
+ for (const auto& entry : kKeyboardCodeKeyDownEntries) {
+ key_down_commands_map->Set(entry.modifiers << 16 | entry.virtual_key,
+ entry.name);
+ }
+
+ for (const auto& entry : kKeyboardCodeKeyPressEntries) {
+ key_press_commands_map->Set(entry.modifiers << 16 | entry.char_code,
+ entry.name);
+ }
+ }
+
+ unsigned modifiers =
+ key_event->GetModifiers() & (kShiftKey | kAltKey | kCtrlKey | kMetaKey);
+
+ if (key_event->GetType() == WebInputEvent::kRawKeyDown) {
+ int map_key = modifiers << 16 | event.keyCode();
+ const char* name = map_key ? key_down_commands_map->at(map_key) : nullptr;
+ if (!name)
+ name = LookupCommandNameFromDomKeyKeyDown(event.key(), modifiers);
+ return name;
+ }
+
+ int map_key = modifiers << 16 | event.charCode();
+ return map_key ? key_press_commands_map->at(map_key) : nullptr;
+}
+
+bool EditingBehavior::ShouldInsertCharacter(const KeyboardEvent& event) const {
+ if (event.KeyEvent()->text[1] != 0)
+ return true;
+
+ // On Gtk/Linux, it emits key events with ASCII text and ctrl on for ctrl-<x>.
+ // In Webkit, EditorClient::handleKeyboardEvent in
+ // WebKit/gtk/WebCoreSupport/EditorClientGtk.cpp drop such events.
+ // On Mac, it emits key events with ASCII text and meta on for Command-<x>.
+ // These key events should not emit text insert event.
+ // Alt key would be used to insert alternative character, so we should let
+ // through. Also note that Ctrl-Alt combination equals to AltGr key which is
+ // also used to insert alternative character.
+ // http://code.google.com/p/chromium/issues/detail?id=10846
+ // Windows sets both alt and meta are on when "Alt" key pressed.
+ // http://code.google.com/p/chromium/issues/detail?id=2215
+ // Also, we should not rely on an assumption that keyboards don't
+ // send ASCII characters when pressing a control key on Windows,
+ // which may be configured to do it so by user.
+ // See also http://en.wikipedia.org/wiki/Keyboard_Layout
+ // FIXME(ukai): investigate more detail for various keyboard layout.
+ UChar ch = event.KeyEvent()->text[0U];
+
+ // Don't insert null or control characters as they can result in
+ // unexpected behaviour
+ if (ch < ' ')
+ return false;
+#if defined(OS_LINUX)
+ // According to XKB map no keyboard combinations with ctrl key are mapped to
+ // printable characters, however we need the filter as the DomKey/text could
+ // contain printable characters.
+ if (event.ctrlKey())
+ return false;
+#elif !defined(OS_WIN)
+ // Don't insert ASCII character if ctrl w/o alt or meta is on.
+ // On Mac, we should ignore events when meta is on (Command-<x>).
+ if (ch < 0x80) {
+ if (event.ctrlKey() && !event.altKey())
+ return false;
+#if defined(OS_MACOSX)
+ if (event.metaKey())
+ return false;
+#endif
+ }
+#endif
+
+ return true;
+}
+
+STATIC_ASSERT_ENUM(WebSettings::kEditingBehaviorMac, kEditingMacBehavior);
+STATIC_ASSERT_ENUM(WebSettings::kEditingBehaviorWin, kEditingWindowsBehavior);
+STATIC_ASSERT_ENUM(WebSettings::kEditingBehaviorUnix, kEditingUnixBehavior);
+STATIC_ASSERT_ENUM(WebSettings::kEditingBehaviorAndroid,
+ kEditingAndroidBehavior);
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/editing_behavior.h b/chromium/third_party/blink/renderer/core/editing/editing_behavior.h
new file mode 100644
index 00000000000..90f13baf5da
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editing_behavior.h
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies)
+ * Copyright (C) 2010 Apple Inc. All rights reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_BEHAVIOR_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_BEHAVIOR_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/editing_behavior_types.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+
+namespace blink {
+class KeyboardEvent;
+
+class CORE_EXPORT EditingBehavior {
+ STACK_ALLOCATED();
+
+ public:
+ explicit EditingBehavior(EditingBehaviorType type) : type_(type) {}
+
+ // Individual functions for each case where we have more than one style of
+ // editing behavior. Create a new function for any platform difference so we
+ // can control it here.
+
+ // When extending a selection beyond the top or bottom boundary of an editable
+ // area, maintain the horizontal position on Windows and Android but extend it
+ // to the boundary of the editable content on Mac and Linux.
+ bool ShouldMoveCaretToHorizontalBoundaryWhenPastTopOrBottom() const {
+ return type_ != kEditingWindowsBehavior && type_ != kEditingAndroidBehavior;
+ }
+
+ bool ShouldSelectReplacement() const {
+ return type_ == kEditingAndroidBehavior;
+ }
+
+ // On Windows, selections should always be considered as directional,
+ // regardless if it is mouse-based or keyboard-based.
+ bool ShouldConsiderSelectionAsDirectional() const {
+ return type_ != kEditingMacBehavior;
+ }
+
+ // On Mac, when revealing a selection (for example as a result of a Find
+ // operation on the Browser), content should be scrolled such that the
+ // selection gets certer aligned.
+ bool ShouldCenterAlignWhenSelectionIsRevealed() const {
+ return type_ == kEditingMacBehavior;
+ }
+
+ // On Mac, style is considered present when present at the beginning of
+ // selection. On other platforms, style has to be present throughout the
+ // selection.
+ bool ShouldToggleStyleBasedOnStartOfSelection() const {
+ return type_ == kEditingMacBehavior;
+ }
+
+ // Standard Mac behavior when extending to a boundary is grow the selection
+ // rather than leaving the base in place and moving the extent. Matches
+ // NSTextView.
+ bool ShouldAlwaysGrowSelectionWhenExtendingToBoundary() const {
+ return type_ == kEditingMacBehavior;
+ }
+
+ // On Mac, when processing a contextual click, the object being clicked upon
+ // should be selected.
+ bool ShouldSelectOnContextualMenuClick() const {
+ return type_ == kEditingMacBehavior;
+ }
+
+ // On Mac, selecting backwards by word/line from the middle of a word/line,
+ // and then going forward leaves the caret back in the middle with no
+ // selection, instead of directly selecting to the other end of the line/word
+ // (Unix/Windows behavior).
+ bool ShouldExtendSelectionByWordOrLineAcrossCaret() const {
+ return type_ != kEditingMacBehavior;
+ }
+
+ // Based on native behavior, when using ctrl(alt)+arrow to move caret by word,
+ // ctrl(alt)+left arrow moves caret to immediately before the word in all
+ // platforms. For example, the word break positions are:
+ // "|abc |def |hij |opq".
+ // But ctrl+right arrow moves caret to "abc |def |hij |opq" on Windows and
+ // "abc| def| hij| opq|" on Mac and Linux.
+ bool ShouldSkipSpaceWhenMovingRight() const {
+ return type_ == kEditingWindowsBehavior;
+ }
+
+ // On Mac, undo of delete/forward-delete of text should select the deleted
+ // text. On other platforms deleted text should not be selected and the cursor
+ // should be placed where the deletion started.
+ bool ShouldUndoOfDeleteSelectText() const {
+ return type_ == kEditingMacBehavior;
+ }
+
+ // On Mac, backspacing at the start of a blocks merges with the
+ // previous table block, as we do with regular blocks. On other
+ // platforms backspace event does nothing if the block above is a
+ // table, but allows mergin otherwise.
+ bool ShouldMergeContentWithTablesOnBackspace() const {
+ return type_ == kEditingMacBehavior;
+ }
+
+ // Support for global selections, used on platforms like the X Window
+ // System that treat selection as a type of clipboard.
+ bool SupportsGlobalSelection() const {
+ return type_ != kEditingWindowsBehavior && type_ != kEditingMacBehavior;
+ }
+
+ // Convert a KeyboardEvent to a command name like "Copy", "Undo" and so on.
+ // If nothing, return empty string.
+ const char* InterpretKeyEvent(const KeyboardEvent&) const;
+
+ bool ShouldInsertCharacter(const KeyboardEvent&) const;
+
+ private:
+ EditingBehaviorType type_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_BEHAVIOR_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/editing_behavior_types.h b/chromium/third_party/blink/renderer/core/editing/editing_behavior_types.h
new file mode 100644
index 00000000000..3ad689604df
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editing_behavior_types.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies)
+ * Copyright (C) 2010 Apple Inc. All rights reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_BEHAVIOR_TYPES_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_BEHAVIOR_TYPES_H_
+
+namespace blink {
+
+// There are multiple editing details that are different on Windows than
+// Macintosh. We use a single switch for all of them. Some examples:
+//
+// 1) Clicking below the last line of an editable area puts the caret at the
+// end of the last line on Mac, but in the middle of the last line on
+// Windows.
+// 2) Pushing the down arrow key on the last line puts the caret at the end
+// of the last line on Mac, but does nothing on Windows. A similar case
+// exists on the top line.
+//
+// This setting is intended to control these sorts of behaviors. There are some
+// other behaviors with individual function calls on EditorClient (smart copy
+// and paste and selecting the space after a double click) that could be
+// combined with this if if possible in the future.
+enum EditingBehaviorType {
+ kEditingMacBehavior,
+ kEditingWindowsBehavior,
+ kEditingUnixBehavior,
+ kEditingAndroidBehavior
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_BEHAVIOR_TYPES_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/editing_boundary.h b/chromium/third_party/blink/renderer/core/editing/editing_boundary.h
new file mode 100644
index 00000000000..ed614a306eb
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editing_boundary.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2004, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_BOUNDARY_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_BOUNDARY_H_
+
+namespace blink {
+
+enum EditingBoundaryCrossingRule {
+ kCanCrossEditingBoundary,
+ kCannotCrossEditingBoundary,
+ kCanSkipOverEditingBoundary
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_BOUNDARY_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/editing_strategy.cc b/chromium/third_party/blink/renderer/core/editing/editing_strategy.cc
new file mode 100644
index 00000000000..7a7cca69c97
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editing_strategy.cc
@@ -0,0 +1,74 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/editing_strategy.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+
+namespace blink {
+
+// If a node can contain candidates for VisiblePositions, return the offset of
+// the last candidate, otherwise return the number of children for container
+// nodes and the length for unrendered text nodes.
+template <typename Traversal>
+int EditingAlgorithm<Traversal>::CaretMaxOffset(const Node& node) {
+ // For rendered text nodes, return the last position that a caret could
+ // occupy.
+ if (node.IsTextNode() && node.GetLayoutObject())
+ return node.GetLayoutObject()->CaretMaxOffset();
+ // For containers return the number of children. For others do the same as
+ // above.
+ return LastOffsetForEditing(&node);
+}
+
+template <typename Traversal>
+int EditingAlgorithm<Traversal>::LastOffsetForEditing(const Node* node) {
+ DCHECK(node);
+ if (!node)
+ return 0;
+ if (node->IsCharacterDataNode())
+ return static_cast<int>(ToCharacterData(node)->length());
+
+ if (Traversal::HasChildren(*node))
+ return Traversal::CountChildren(*node);
+
+ // FIXME: Try return 0 here.
+
+ if (!EditingIgnoresContent(*node))
+ return 0;
+
+ // editingIgnoresContent uses the same logic in
+ // isEmptyNonEditableNodeInEditable (EditingUtilities.cpp). We don't
+ // understand why this function returns 1 even when the node doesn't have
+ // children.
+ return 1;
+}
+
+template <typename Strategy>
+Node* EditingAlgorithm<Strategy>::RootUserSelectAllForNode(Node* node) {
+ if (!node || UsedValueOfUserSelect(*node) != EUserSelect::kAll)
+ return nullptr;
+ Node* parent = Strategy::Parent(*node);
+ if (!parent)
+ return node;
+
+ Node* candidate_root = node;
+ while (parent) {
+ if (!parent->GetLayoutObject()) {
+ parent = Strategy::Parent(*parent);
+ continue;
+ }
+ if (UsedValueOfUserSelect(*parent) != EUserSelect::kAll)
+ break;
+ candidate_root = parent;
+ parent = Strategy::Parent(*candidate_root);
+ }
+ return candidate_root;
+}
+
+template class CORE_TEMPLATE_EXPORT EditingAlgorithm<NodeTraversal>;
+template class CORE_TEMPLATE_EXPORT EditingAlgorithm<FlatTreeTraversal>;
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/editing_strategy.h b/chromium/third_party/blink/renderer/core/editing/editing_strategy.h
new file mode 100644
index 00000000000..6d016d9858b
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editing_strategy.h
@@ -0,0 +1,43 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_STRATEGY_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_STRATEGY_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/dom/flat_tree_traversal.h"
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+
+namespace blink {
+
+// Editing algorithm defined on node traversal.
+template <typename Traversal>
+class CORE_TEMPLATE_CLASS_EXPORT EditingAlgorithm : public Traversal {
+ STATIC_ONLY(EditingAlgorithm);
+
+ public:
+ static int CaretMaxOffset(const Node&);
+ // This method is used to create positions in the DOM. It returns the
+ // maximum valid offset in a node. It returns 1 for some elements even
+ // though they do not have children, which creates technically invalid DOM
+ // Positions. Be sure to call |parentAnchoredEquivalent()| on a Position
+ // before using it to create a DOM Range, or an exception will be thrown.
+ static int LastOffsetForEditing(const Node*);
+ static Node* RootUserSelectAllForNode(Node*);
+};
+
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ EditingAlgorithm<NodeTraversal>;
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ EditingAlgorithm<FlatTreeTraversal>;
+
+// DOM tree version of editing algorithm
+using EditingStrategy = EditingAlgorithm<NodeTraversal>;
+// Flat tree version of editing algorithm
+using EditingInFlatTreeStrategy = EditingAlgorithm<FlatTreeTraversal>;
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_STRATEGY_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/editing_strategy_test.cc b/chromium/third_party/blink/renderer/core/editing/editing_strategy_test.cc
new file mode 100644
index 00000000000..2234f110a1c
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editing_strategy_test.cc
@@ -0,0 +1,35 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/editing_strategy.h"
+
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class EditingStrategyTest : public EditingTestBase {};
+
+TEST_F(EditingStrategyTest, caretMaxOffset) {
+ const char* body_content =
+ "<p id='host'>00<b id='one'>1</b><b id='two'>22</b>333</p>";
+ const char* shadow_content =
+ "<content select=#two></content><content select=#one></content>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+ Node* host = GetDocument().getElementById("host");
+ Node* one = GetDocument().getElementById("one");
+ Node* two = GetDocument().getElementById("two");
+
+ EXPECT_EQ(4, EditingStrategy::CaretMaxOffset(*host));
+ EXPECT_EQ(1, EditingStrategy::CaretMaxOffset(*one));
+ EXPECT_EQ(1, EditingStrategy::CaretMaxOffset(*one->firstChild()));
+ EXPECT_EQ(2, EditingStrategy::CaretMaxOffset(*two->firstChild()));
+
+ EXPECT_EQ(2, EditingInFlatTreeStrategy::CaretMaxOffset(*host));
+ EXPECT_EQ(1, EditingInFlatTreeStrategy::CaretMaxOffset(*one));
+ EXPECT_EQ(1, EditingInFlatTreeStrategy::CaretMaxOffset(*one->firstChild()));
+ EXPECT_EQ(2, EditingInFlatTreeStrategy::CaretMaxOffset(*two->firstChild()));
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/editing_style.cc b/chromium/third_party/blink/renderer/core/editing/editing_style.cc
new file mode 100644
index 00000000000..2880fdf1b9b
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editing_style.cc
@@ -0,0 +1,1876 @@
+/*
+ * Copyright (C) 2007, 2008, 2009 Apple Computer, Inc.
+ * Copyright (C) 2010, 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/editing_style.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/css/css_color_value.h"
+#include "third_party/blink/renderer/core/css/css_computed_style_declaration.h"
+#include "third_party/blink/renderer/core/css/css_identifier_value.h"
+#include "third_party/blink/renderer/core/css/css_primitive_value.h"
+#include "third_party/blink/renderer/core/css/css_primitive_value_mappings.h"
+#include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/css/css_rule_list.h"
+#include "third_party/blink/renderer/core/css/css_style_rule.h"
+#include "third_party/blink/renderer/core/css/css_value_list.h"
+#include "third_party/blink/renderer/core/css/font_size_functions.h"
+#include "third_party/blink/renderer/core/css/parser/css_parser.h"
+#include "third_party/blink/renderer/core/css/properties/css_property.h"
+#include "third_party/blink/renderer/core/css/resolver/style_resolver.h"
+#include "third_party/blink/renderer/core/css/style_rule.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/node.h"
+#include "third_party/blink/renderer/core/dom/node_computed_style.h"
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/core/dom/qualified_name.h"
+#include "third_party/blink/renderer/core/editing/commands/apply_style_command.h"
+#include "third_party/blink/renderer/core/editing/editing_style_utilities.h"
+#include "third_party/blink/renderer/core/editing/editing_tri_state.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/serializers/html_interchange.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/writing_direction.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/html/html_font_element.h"
+#include "third_party/blink/renderer/core/html/html_span_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/layout/layout_box.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/core/style/computed_style.h"
+#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
+
+namespace blink {
+
+using namespace cssvalue;
+
+// Editing style properties must be preserved during editing operation.
+// e.g. when a user inserts a new paragraph, all properties listed here must be
+// copied to the new paragraph.
+// NOTE: Use either allEditingProperties() or inheritableEditingProperties() to
+// respect runtime enabling of properties.
+static const CSSPropertyID kStaticEditingProperties[] = {
+ CSSPropertyBackgroundColor, CSSPropertyColor, CSSPropertyFontFamily,
+ CSSPropertyFontSize, CSSPropertyFontStyle, CSSPropertyFontVariantLigatures,
+ CSSPropertyFontVariantCaps, CSSPropertyFontWeight, CSSPropertyLetterSpacing,
+ CSSPropertyOrphans, CSSPropertyTextAlign,
+ // FIXME: CSSPropertyTextDecoration needs to be removed when CSS3 Text
+ // Decoration feature is no longer experimental.
+ CSSPropertyTextDecoration, CSSPropertyTextDecorationLine,
+ CSSPropertyTextIndent, CSSPropertyTextTransform, CSSPropertyWhiteSpace,
+ CSSPropertyWidows, CSSPropertyWordSpacing,
+ CSSPropertyWebkitTextDecorationsInEffect, CSSPropertyWebkitTextFillColor,
+ CSSPropertyWebkitTextStrokeColor, CSSPropertyWebkitTextStrokeWidth,
+ CSSPropertyCaretColor};
+
+enum EditingPropertiesType {
+ kOnlyInheritableEditingProperties,
+ kAllEditingProperties
+};
+
+static const Vector<const CSSProperty*>& AllEditingProperties() {
+ DEFINE_STATIC_LOCAL(Vector<const CSSProperty*>, properties, ());
+ if (properties.IsEmpty()) {
+ CSSProperty::FilterEnabledCSSPropertiesIntoVector(
+ kStaticEditingProperties, WTF_ARRAY_LENGTH(kStaticEditingProperties),
+ properties);
+ for (size_t index = 0; index < properties.size(); index++) {
+ if (properties[index]->IDEquals(CSSPropertyTextDecoration)) {
+ properties.EraseAt(index);
+ break;
+ }
+ }
+ }
+ return properties;
+}
+
+static const Vector<const CSSProperty*>& InheritableEditingProperties() {
+ DEFINE_STATIC_LOCAL(Vector<const CSSProperty*>, properties, ());
+ if (properties.IsEmpty()) {
+ CSSProperty::FilterEnabledCSSPropertiesIntoVector(
+ kStaticEditingProperties, WTF_ARRAY_LENGTH(kStaticEditingProperties),
+ properties);
+ for (size_t index = 0; index < properties.size();) {
+ if (!properties[index]->IsInherited()) {
+ properties.EraseAt(index);
+ continue;
+ }
+ ++index;
+ }
+ }
+ return properties;
+}
+
+template <class StyleDeclarationType>
+static MutableCSSPropertyValueSet* CopyEditingProperties(
+ StyleDeclarationType* style,
+ EditingPropertiesType type = kOnlyInheritableEditingProperties) {
+ if (type == kAllEditingProperties)
+ return style->CopyPropertiesInSet(AllEditingProperties());
+ return style->CopyPropertiesInSet(InheritableEditingProperties());
+}
+
+static inline bool IsEditingProperty(CSSPropertyID id) {
+ static const Vector<const CSSProperty*>& properties = AllEditingProperties();
+ for (size_t index = 0; index < properties.size(); index++) {
+ if (properties[index]->IDEquals(id))
+ return true;
+ }
+ return false;
+}
+
+static CSSComputedStyleDeclaration* EnsureComputedStyle(
+ const Position& position) {
+ Element* elem = AssociatedElementOf(position);
+ if (!elem)
+ return nullptr;
+ return CSSComputedStyleDeclaration::Create(elem);
+}
+
+static MutableCSSPropertyValueSet* GetPropertiesNotIn(
+ CSSPropertyValueSet* style_with_redundant_properties,
+ CSSStyleDeclaration* base_style,
+ SecureContextMode);
+enum LegacyFontSizeMode {
+ kAlwaysUseLegacyFontSize,
+ kUseLegacyFontSizeOnlyIfPixelValuesMatch
+};
+static int LegacyFontSizeFromCSSValue(Document*,
+ const CSSValue*,
+ bool,
+ LegacyFontSizeMode);
+
+class HTMLElementEquivalent : public GarbageCollected<HTMLElementEquivalent> {
+ public:
+ static HTMLElementEquivalent* Create(CSSPropertyID property_id,
+ CSSValueID primitive_value,
+ const HTMLQualifiedName& tag_name) {
+ return new HTMLElementEquivalent(property_id, primitive_value, tag_name);
+ }
+
+ virtual bool Matches(const Element* element) const {
+ return !tag_name_ || element->HasTagName(*tag_name_);
+ }
+ virtual bool HasAttribute() const { return false; }
+ virtual bool PropertyExistsInStyle(const CSSPropertyValueSet* style) const {
+ return style->GetPropertyCSSValue(property_id_);
+ }
+ virtual bool ValueIsPresentInStyle(HTMLElement*, CSSPropertyValueSet*) const;
+ virtual void AddToStyle(Element*, EditingStyle*) const;
+
+ virtual void Trace(blink::Visitor* visitor) {
+ visitor->Trace(identifier_value_);
+ }
+
+ protected:
+ HTMLElementEquivalent(CSSPropertyID);
+ HTMLElementEquivalent(CSSPropertyID, const HTMLQualifiedName& tag_name);
+ HTMLElementEquivalent(CSSPropertyID,
+ CSSValueID primitive_value,
+ const HTMLQualifiedName& tag_name);
+ const CSSPropertyID property_id_;
+ const Member<CSSIdentifierValue> identifier_value_;
+ // We can store a pointer because HTML tag names are const global.
+ const HTMLQualifiedName* tag_name_;
+};
+
+HTMLElementEquivalent::HTMLElementEquivalent(CSSPropertyID id)
+ : property_id_(id), tag_name_(nullptr) {}
+
+HTMLElementEquivalent::HTMLElementEquivalent(CSSPropertyID id,
+ const HTMLQualifiedName& tag_name)
+ : property_id_(id), tag_name_(&tag_name) {}
+
+HTMLElementEquivalent::HTMLElementEquivalent(CSSPropertyID id,
+ CSSValueID value_id,
+ const HTMLQualifiedName& tag_name)
+ : property_id_(id),
+ identifier_value_(CSSIdentifierValue::Create(value_id)),
+ tag_name_(&tag_name) {
+ DCHECK_NE(value_id, CSSValueInvalid);
+}
+
+bool HTMLElementEquivalent::ValueIsPresentInStyle(
+ HTMLElement* element,
+ CSSPropertyValueSet* style) const {
+ const CSSValue* value = style->GetPropertyCSSValue(property_id_);
+
+ // TODO: Does this work on style or computed style? The code here, but we
+ // might need to do something here to match CSSPrimitiveValues. if
+ // (property_id_ == CSSPropertyFontWeight &&
+ // identifier_value_->GetValueID() == CSSValueBold) {
+ // if (value->IsPrimitiveValue() &&
+ // ToCSSPrimitiveValue(value)->GetFloatValue() >= BoldThreshold()) {
+ // LOG(INFO) << "weight match in HTMLElementEquivalent for primitive
+ // value"; return true;
+ // } else {
+ // LOG(INFO) << "weight match in HTMLElementEquivalent for identifier
+ // value";
+ // }
+ // }
+
+ return Matches(element) && value && value->IsIdentifierValue() &&
+ ToCSSIdentifierValue(value)->GetValueID() ==
+ identifier_value_->GetValueID();
+}
+
+void HTMLElementEquivalent::AddToStyle(Element* element,
+ EditingStyle* style) const {
+ style->SetProperty(property_id_, identifier_value_->CssText(),
+ /* important */ false,
+ element->GetDocument().GetSecureContextMode());
+}
+
+class HTMLTextDecorationEquivalent final : public HTMLElementEquivalent {
+ public:
+ static HTMLElementEquivalent* Create(CSSValueID primitive_value,
+ const HTMLQualifiedName& tag_name) {
+ return new HTMLTextDecorationEquivalent(primitive_value, tag_name);
+ }
+ bool PropertyExistsInStyle(const CSSPropertyValueSet*) const override;
+ bool ValueIsPresentInStyle(HTMLElement*, CSSPropertyValueSet*) const override;
+
+ virtual void Trace(blink::Visitor* visitor) {
+ HTMLElementEquivalent::Trace(visitor);
+ }
+
+ private:
+ HTMLTextDecorationEquivalent(CSSValueID primitive_value,
+ const HTMLQualifiedName& tag_name);
+};
+
+HTMLTextDecorationEquivalent::HTMLTextDecorationEquivalent(
+ CSSValueID primitive_value,
+ const HTMLQualifiedName& tag_name)
+ : HTMLElementEquivalent(CSSPropertyTextDecorationLine,
+ primitive_value,
+ tag_name)
+// m_propertyID is used in HTMLElementEquivalent::addToStyle
+{}
+
+bool HTMLTextDecorationEquivalent::PropertyExistsInStyle(
+ const CSSPropertyValueSet* style) const {
+ return style->GetPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect) ||
+ style->GetPropertyCSSValue(CSSPropertyTextDecorationLine);
+}
+
+bool HTMLTextDecorationEquivalent::ValueIsPresentInStyle(
+ HTMLElement* element,
+ CSSPropertyValueSet* style) const {
+ const CSSValue* style_value =
+ style->GetPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect);
+ if (!style_value)
+ style_value = style->GetPropertyCSSValue(CSSPropertyTextDecorationLine);
+ return Matches(element) && style_value && style_value->IsValueList() &&
+ ToCSSValueList(style_value)->HasValue(*identifier_value_);
+}
+
+class HTMLAttributeEquivalent : public HTMLElementEquivalent {
+ public:
+ static HTMLAttributeEquivalent* Create(CSSPropertyID property_id,
+ const HTMLQualifiedName& tag_name,
+ const QualifiedName& attr_name) {
+ return new HTMLAttributeEquivalent(property_id, tag_name, attr_name);
+ }
+ static HTMLAttributeEquivalent* Create(CSSPropertyID property_id,
+ const QualifiedName& attr_name) {
+ return new HTMLAttributeEquivalent(property_id, attr_name);
+ }
+
+ bool Matches(const Element* element) const override {
+ return HTMLElementEquivalent::Matches(element) &&
+ element->hasAttribute(attr_name_);
+ }
+ bool HasAttribute() const override { return true; }
+ bool ValueIsPresentInStyle(HTMLElement*, CSSPropertyValueSet*) const override;
+ void AddToStyle(Element*, EditingStyle*) const override;
+ virtual const CSSValue* AttributeValueAsCSSValue(Element*) const;
+ inline const QualifiedName& AttributeName() const { return attr_name_; }
+
+ virtual void Trace(blink::Visitor* visitor) {
+ HTMLElementEquivalent::Trace(visitor);
+ }
+
+ protected:
+ HTMLAttributeEquivalent(CSSPropertyID,
+ const HTMLQualifiedName& tag_name,
+ const QualifiedName& attr_name);
+ HTMLAttributeEquivalent(CSSPropertyID, const QualifiedName& attr_name);
+ // We can store a reference because HTML attribute names are const global.
+ const QualifiedName& attr_name_;
+};
+
+HTMLAttributeEquivalent::HTMLAttributeEquivalent(
+ CSSPropertyID id,
+ const HTMLQualifiedName& tag_name,
+ const QualifiedName& attr_name)
+ : HTMLElementEquivalent(id, tag_name), attr_name_(attr_name) {}
+
+HTMLAttributeEquivalent::HTMLAttributeEquivalent(CSSPropertyID id,
+ const QualifiedName& attr_name)
+ : HTMLElementEquivalent(id), attr_name_(attr_name) {}
+
+bool HTMLAttributeEquivalent::ValueIsPresentInStyle(
+ HTMLElement* element,
+ CSSPropertyValueSet* style) const {
+ const CSSValue* value = AttributeValueAsCSSValue(element);
+ const CSSValue* style_value = style->GetPropertyCSSValue(property_id_);
+
+ return DataEquivalent(value, style_value);
+}
+
+void HTMLAttributeEquivalent::AddToStyle(Element* element,
+ EditingStyle* style) const {
+ if (const CSSValue* value = AttributeValueAsCSSValue(element)) {
+ style->SetProperty(property_id_, value->CssText(), /* important */ false,
+ element->GetDocument().GetSecureContextMode());
+ }
+}
+
+const CSSValue* HTMLAttributeEquivalent::AttributeValueAsCSSValue(
+ Element* element) const {
+ DCHECK(element);
+ const AtomicString& value = element->getAttribute(attr_name_);
+ if (value.IsNull())
+ return nullptr;
+
+ MutableCSSPropertyValueSet* dummy_style = nullptr;
+ dummy_style = MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ dummy_style->SetProperty(property_id_, value, /* important */ false,
+ element->GetDocument().GetSecureContextMode());
+ return dummy_style->GetPropertyCSSValue(property_id_);
+}
+
+class HTMLFontSizeEquivalent final : public HTMLAttributeEquivalent {
+ public:
+ static HTMLFontSizeEquivalent* Create() {
+ return new HTMLFontSizeEquivalent();
+ }
+ const CSSValue* AttributeValueAsCSSValue(Element*) const override;
+
+ virtual void Trace(blink::Visitor* visitor) {
+ HTMLAttributeEquivalent::Trace(visitor);
+ }
+
+ private:
+ HTMLFontSizeEquivalent();
+};
+
+HTMLFontSizeEquivalent::HTMLFontSizeEquivalent()
+ : HTMLAttributeEquivalent(CSSPropertyFontSize,
+ HTMLNames::fontTag,
+ HTMLNames::sizeAttr) {}
+
+const CSSValue* HTMLFontSizeEquivalent::AttributeValueAsCSSValue(
+ Element* element) const {
+ DCHECK(element);
+ const AtomicString& value = element->getAttribute(attr_name_);
+ if (value.IsNull())
+ return nullptr;
+ CSSValueID size;
+ if (!HTMLFontElement::CssValueFromFontSizeNumber(value, size))
+ return nullptr;
+ return CSSIdentifierValue::Create(size);
+}
+
+float EditingStyle::no_font_delta_ = 0.0f;
+
+EditingStyle::EditingStyle(ContainerNode* node,
+ PropertiesToInclude properties_to_include) {
+ Init(node, properties_to_include);
+}
+
+EditingStyle::EditingStyle(const Position& position,
+ PropertiesToInclude properties_to_include) {
+ Init(position.AnchorNode(), properties_to_include);
+}
+
+EditingStyle::EditingStyle(const CSSPropertyValueSet* style)
+ : mutable_style_(style ? style->MutableCopy() : nullptr) {
+ ExtractFontSizeDelta();
+}
+
+EditingStyle::EditingStyle(CSSPropertyID property_id,
+ const String& value,
+ SecureContextMode secure_context_mode)
+ : mutable_style_(nullptr) {
+ SetProperty(property_id, value, /* important */ false, secure_context_mode);
+ is_vertical_align_ = property_id == CSSPropertyVerticalAlign &&
+ (value == "sub" || value == "super");
+}
+
+static Color CssValueToColor(const CSSValue* color_value) {
+ if (!color_value ||
+ (!color_value->IsColorValue() && !color_value->IsPrimitiveValue() &&
+ !color_value->IsIdentifierValue()))
+ return Color::kTransparent;
+
+ if (color_value->IsColorValue())
+ return ToCSSColorValue(color_value)->Value();
+
+ Color color = 0;
+ // FIXME: Why ignore the return value?
+ CSSParser::ParseColor(color, color_value->CssText());
+ return color;
+}
+
+static inline Color GetFontColor(CSSStyleDeclaration* style) {
+ return CssValueToColor(style->GetPropertyCSSValueInternal(CSSPropertyColor));
+}
+
+static inline Color GetFontColor(CSSPropertyValueSet* style) {
+ return CssValueToColor(style->GetPropertyCSSValue(CSSPropertyColor));
+}
+
+static inline Color GetBackgroundColor(CSSStyleDeclaration* style) {
+ return CssValueToColor(
+ style->GetPropertyCSSValueInternal(CSSPropertyBackgroundColor));
+}
+
+static inline Color GetBackgroundColor(CSSPropertyValueSet* style) {
+ return CssValueToColor(
+ style->GetPropertyCSSValue(CSSPropertyBackgroundColor));
+}
+
+static inline Color BackgroundColorInEffect(Node* node) {
+ return CssValueToColor(
+ EditingStyleUtilities::BackgroundColorValueInEffect(node));
+}
+
+static int TextAlignResolvingStartAndEnd(int text_align, int direction) {
+ switch (text_align) {
+ case CSSValueCenter:
+ case CSSValueWebkitCenter:
+ return CSSValueCenter;
+ case CSSValueJustify:
+ return CSSValueJustify;
+ case CSSValueLeft:
+ case CSSValueWebkitLeft:
+ return CSSValueLeft;
+ case CSSValueRight:
+ case CSSValueWebkitRight:
+ return CSSValueRight;
+ case CSSValueStart:
+ return direction != CSSValueRtl ? CSSValueLeft : CSSValueRight;
+ case CSSValueEnd:
+ return direction == CSSValueRtl ? CSSValueRight : CSSValueLeft;
+ }
+ return CSSValueInvalid;
+}
+
+template <typename T>
+static int TextAlignResolvingStartAndEnd(T* style) {
+ return TextAlignResolvingStartAndEnd(
+ GetIdentifierValue(style, CSSPropertyTextAlign),
+ GetIdentifierValue(style, CSSPropertyDirection));
+}
+
+void EditingStyle::Init(Node* node, PropertiesToInclude properties_to_include) {
+ if (IsTabHTMLSpanElementTextNode(node))
+ node = TabSpanElement(node)->parentNode();
+ else if (IsTabHTMLSpanElement(node))
+ node = node->parentNode();
+
+ CSSComputedStyleDeclaration* computed_style_at_position =
+ CSSComputedStyleDeclaration::Create(node);
+ mutable_style_ =
+ properties_to_include == kAllProperties && computed_style_at_position
+ ? computed_style_at_position->CopyProperties()
+ : CopyEditingProperties(computed_style_at_position);
+
+ if (properties_to_include == kEditingPropertiesInEffect) {
+ if (const CSSValue* value =
+ EditingStyleUtilities::BackgroundColorValueInEffect(node)) {
+ mutable_style_->SetProperty(CSSPropertyBackgroundColor, value->CssText(),
+ /* important */ false,
+ node->GetDocument().GetSecureContextMode());
+ }
+ if (const CSSValue* value = computed_style_at_position->GetPropertyCSSValue(
+ GetCSSPropertyWebkitTextDecorationsInEffect())) {
+ mutable_style_->SetProperty(CSSPropertyTextDecoration, value->CssText(),
+ /* important */ false,
+ node->GetDocument().GetSecureContextMode());
+ }
+ }
+
+ if (node && node->EnsureComputedStyle()) {
+ const ComputedStyle* computed_style = node->EnsureComputedStyle();
+
+ // Fix for crbug.com/768261: due to text-autosizing, reading the current
+ // computed font size and re-writing it to an element may actually cause the
+ // font size to become larger (since the autosizer will run again on the new
+ // computed size). The fix is to toss out the computed size property here
+ // and use ComputedStyle::SpecifiedFontSize().
+ if (computed_style->ComputedFontSize() !=
+ computed_style->SpecifiedFontSize()) {
+ // ReplaceSelectionCommandTest_TextAutosizingDoesntInflateText gets here.
+ mutable_style_->SetProperty(
+ CSSPropertyFontSize,
+ CSSPrimitiveValue::Create(computed_style->SpecifiedFontSize(),
+ CSSPrimitiveValue::UnitType::kPixels)
+ ->CssText(),
+ /* important */ false, node->GetDocument().GetSecureContextMode());
+ }
+
+ RemoveInheritedColorsIfNeeded(computed_style);
+ ReplaceFontSizeByKeywordIfPossible(
+ computed_style, node->GetDocument().GetSecureContextMode(),
+ computed_style_at_position);
+ }
+
+ is_monospace_font_ = computed_style_at_position->IsMonospaceFont();
+ ExtractFontSizeDelta();
+}
+
+void EditingStyle::RemoveInheritedColorsIfNeeded(
+ const ComputedStyle* computed_style) {
+ // If a node's text fill color is currentColor, then its children use
+ // their font-color as their text fill color (they don't
+ // inherit it). Likewise for stroke color.
+ // Similar thing happens for caret-color if it's auto or currentColor.
+ if (computed_style->TextFillColor().IsCurrentColor())
+ mutable_style_->RemoveProperty(CSSPropertyWebkitTextFillColor);
+ if (computed_style->TextStrokeColor().IsCurrentColor())
+ mutable_style_->RemoveProperty(CSSPropertyWebkitTextStrokeColor);
+ if (computed_style->CaretColor().IsAutoColor() ||
+ computed_style->CaretColor().IsCurrentColor())
+ mutable_style_->RemoveProperty(CSSPropertyCaretColor);
+}
+
+void EditingStyle::SetProperty(CSSPropertyID property_id,
+ const String& value,
+ bool important,
+ SecureContextMode secure_context_mode) {
+ if (!mutable_style_)
+ mutable_style_ = MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+
+ mutable_style_->SetProperty(property_id, value, important,
+ secure_context_mode);
+}
+
+void EditingStyle::ReplaceFontSizeByKeywordIfPossible(
+ const ComputedStyle* computed_style,
+ SecureContextMode secure_context_mode,
+ CSSComputedStyleDeclaration* css_computed_style) {
+ DCHECK(computed_style);
+ if (computed_style->GetFontDescription().KeywordSize()) {
+ mutable_style_->SetProperty(
+ CSSPropertyFontSize,
+ css_computed_style->GetFontSizeCSSValuePreferringKeyword()->CssText(),
+ /* important */ false, secure_context_mode);
+ }
+}
+
+void EditingStyle::ExtractFontSizeDelta() {
+ if (!mutable_style_)
+ return;
+
+ if (mutable_style_->GetPropertyCSSValue(CSSPropertyFontSize)) {
+ // Explicit font size overrides any delta.
+ mutable_style_->RemoveProperty(CSSPropertyWebkitFontSizeDelta);
+ return;
+ }
+
+ // Get the adjustment amount out of the style.
+ const CSSValue* value =
+ mutable_style_->GetPropertyCSSValue(CSSPropertyWebkitFontSizeDelta);
+ if (!value || !value->IsPrimitiveValue())
+ return;
+
+ const CSSPrimitiveValue* primitive_value = ToCSSPrimitiveValue(value);
+
+ // Only PX handled now. If we handle more types in the future, perhaps
+ // a switch statement here would be more appropriate.
+ if (!primitive_value->IsPx())
+ return;
+
+ font_size_delta_ = primitive_value->GetFloatValue();
+ mutable_style_->RemoveProperty(CSSPropertyWebkitFontSizeDelta);
+}
+
+bool EditingStyle::IsEmpty() const {
+ return (!mutable_style_ || mutable_style_->IsEmpty()) &&
+ font_size_delta_ == no_font_delta_;
+}
+
+bool EditingStyle::GetTextDirection(WritingDirection& writing_direction) const {
+ if (!mutable_style_)
+ return false;
+
+ const CSSValue* unicode_bidi =
+ mutable_style_->GetPropertyCSSValue(CSSPropertyUnicodeBidi);
+ if (!unicode_bidi || !unicode_bidi->IsIdentifierValue())
+ return false;
+
+ CSSValueID unicode_bidi_value =
+ ToCSSIdentifierValue(unicode_bidi)->GetValueID();
+ if (EditingStyleUtilities::IsEmbedOrIsolate(unicode_bidi_value)) {
+ const CSSValue* direction =
+ mutable_style_->GetPropertyCSSValue(CSSPropertyDirection);
+ if (!direction || !direction->IsIdentifierValue())
+ return false;
+
+ writing_direction =
+ ToCSSIdentifierValue(direction)->GetValueID() == CSSValueLtr
+ ? WritingDirection::kLeftToRight
+ : WritingDirection::kRightToLeft;
+
+ return true;
+ }
+
+ if (unicode_bidi_value == CSSValueNormal) {
+ writing_direction = WritingDirection::kNatural;
+ return true;
+ }
+
+ return false;
+}
+
+void EditingStyle::OverrideWithStyle(const CSSPropertyValueSet* style) {
+ if (!style || style->IsEmpty())
+ return;
+ if (!mutable_style_)
+ mutable_style_ = MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ mutable_style_->MergeAndOverrideOnConflict(style);
+ ExtractFontSizeDelta();
+}
+
+void EditingStyle::Clear() {
+ mutable_style_.Clear();
+ is_monospace_font_ = false;
+ font_size_delta_ = no_font_delta_;
+}
+
+EditingStyle* EditingStyle::Copy() const {
+ EditingStyle* copy = EditingStyle::Create();
+ if (mutable_style_)
+ copy->mutable_style_ = mutable_style_->MutableCopy();
+ copy->is_monospace_font_ = is_monospace_font_;
+ copy->font_size_delta_ = font_size_delta_;
+ return copy;
+}
+
+// This is the list of CSS properties that apply specially to block-level
+// elements.
+static const CSSPropertyID kStaticBlockProperties[] = {
+ CSSPropertyBreakAfter,
+ CSSPropertyBreakBefore,
+ CSSPropertyBreakInside,
+ CSSPropertyOrphans,
+ CSSPropertyOverflow, // This can be also be applied to replaced elements
+ CSSPropertyColumnCount,
+ CSSPropertyColumnGap,
+ CSSPropertyColumnRuleColor,
+ CSSPropertyColumnRuleStyle,
+ CSSPropertyColumnRuleWidth,
+ CSSPropertyWebkitColumnBreakBefore,
+ CSSPropertyWebkitColumnBreakAfter,
+ CSSPropertyWebkitColumnBreakInside,
+ CSSPropertyColumnWidth,
+ CSSPropertyPageBreakAfter,
+ CSSPropertyPageBreakBefore,
+ CSSPropertyPageBreakInside,
+ CSSPropertyTextAlign,
+ CSSPropertyTextAlignLast,
+ CSSPropertyTextIndent,
+ CSSPropertyTextJustify,
+ CSSPropertyWidows};
+
+static Vector<const CSSProperty*>& BlockPropertiesVector() {
+ DEFINE_STATIC_LOCAL(Vector<const CSSProperty*>, properties, ());
+ if (properties.IsEmpty())
+ CSSProperty::FilterEnabledCSSPropertiesIntoVector(
+ kStaticBlockProperties, WTF_ARRAY_LENGTH(kStaticBlockProperties),
+ properties);
+ return properties;
+}
+
+EditingStyle* EditingStyle::ExtractAndRemoveBlockProperties() {
+ EditingStyle* block_properties = EditingStyle::Create();
+ if (!mutable_style_)
+ return block_properties;
+
+ block_properties->mutable_style_ =
+ mutable_style_->CopyPropertiesInSet(BlockPropertiesVector());
+ RemoveBlockProperties();
+
+ return block_properties;
+}
+
+EditingStyle* EditingStyle::ExtractAndRemoveTextDirection(
+ SecureContextMode secure_context_mode) {
+ EditingStyle* text_direction = EditingStyle::Create();
+ text_direction->mutable_style_ =
+ MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ text_direction->mutable_style_->SetProperty(
+ CSSPropertyUnicodeBidi, CSSValueIsolate,
+ mutable_style_->PropertyIsImportant(CSSPropertyUnicodeBidi));
+
+ text_direction->mutable_style_->SetProperty(
+ CSSPropertyDirection,
+ mutable_style_->GetPropertyValue(CSSPropertyDirection),
+ mutable_style_->PropertyIsImportant(CSSPropertyDirection),
+ secure_context_mode);
+
+ mutable_style_->RemoveProperty(CSSPropertyUnicodeBidi);
+ mutable_style_->RemoveProperty(CSSPropertyDirection);
+
+ return text_direction;
+}
+
+void EditingStyle::RemoveBlockProperties() {
+ if (!mutable_style_)
+ return;
+
+ mutable_style_->RemovePropertiesInSet(BlockPropertiesVector().data(),
+ BlockPropertiesVector().size());
+}
+
+void EditingStyle::RemoveStyleAddedByElement(Element* element) {
+ if (!element || !element->parentNode())
+ return;
+ MutableCSSPropertyValueSet* parent_style = CopyEditingProperties(
+ CSSComputedStyleDeclaration::Create(element->parentNode()),
+ kAllEditingProperties);
+ MutableCSSPropertyValueSet* node_style = CopyEditingProperties(
+ CSSComputedStyleDeclaration::Create(element), kAllEditingProperties);
+ node_style->RemoveEquivalentProperties(parent_style);
+ mutable_style_->RemoveEquivalentProperties(node_style);
+}
+
+void EditingStyle::RemoveStyleConflictingWithStyleOfElement(Element* element) {
+ if (!element || !element->parentNode() || !mutable_style_)
+ return;
+
+ MutableCSSPropertyValueSet* parent_style = CopyEditingProperties(
+ CSSComputedStyleDeclaration::Create(element->parentNode()),
+ kAllEditingProperties);
+ MutableCSSPropertyValueSet* node_style = CopyEditingProperties(
+ CSSComputedStyleDeclaration::Create(element), kAllEditingProperties);
+ node_style->RemoveEquivalentProperties(parent_style);
+
+ unsigned property_count = node_style->PropertyCount();
+ for (unsigned i = 0; i < property_count; ++i)
+ mutable_style_->RemoveProperty(node_style->PropertyAt(i).Id());
+}
+
+void EditingStyle::CollapseTextDecorationProperties(
+ SecureContextMode secure_context_mode) {
+ if (!mutable_style_)
+ return;
+
+ const CSSValue* text_decorations_in_effect =
+ mutable_style_->GetPropertyCSSValue(
+ CSSPropertyWebkitTextDecorationsInEffect);
+ if (!text_decorations_in_effect)
+ return;
+
+ if (text_decorations_in_effect->IsValueList()) {
+ mutable_style_->SetProperty(
+ CSSPropertyTextDecorationLine, text_decorations_in_effect->CssText(),
+ mutable_style_->PropertyIsImportant(CSSPropertyTextDecorationLine),
+ secure_context_mode);
+ } else {
+ mutable_style_->RemoveProperty(CSSPropertyTextDecorationLine);
+ }
+ mutable_style_->RemoveProperty(CSSPropertyWebkitTextDecorationsInEffect);
+}
+
+EditingTriState EditingStyle::TriStateOfStyle(
+ EditingStyle* style,
+ SecureContextMode secure_context_mode) const {
+ if (!style || !style->mutable_style_)
+ return EditingTriState::kFalse;
+ return TriStateOfStyle(style->mutable_style_->EnsureCSSStyleDeclaration(),
+ kDoNotIgnoreTextOnlyProperties, secure_context_mode);
+}
+
+EditingTriState EditingStyle::TriStateOfStyle(
+ CSSStyleDeclaration* style_to_compare,
+ ShouldIgnoreTextOnlyProperties should_ignore_text_only_properties,
+ SecureContextMode secure_context_mode) const {
+ MutableCSSPropertyValueSet* difference = GetPropertiesNotIn(
+ mutable_style_.Get(), style_to_compare, secure_context_mode);
+
+ // CSS properties that create a visual difference only when applied to text.
+ static const CSSProperty* kTextOnlyProperties[] = {
+ // FIXME: CSSPropertyTextDecoration needs to be removed when CSS3 Text
+ // Decoration feature is no longer experimental.
+ &GetCSSPropertyTextDecoration(),
+ &GetCSSPropertyTextDecorationLine(),
+ &GetCSSPropertyWebkitTextDecorationsInEffect(),
+ &GetCSSPropertyFontStyle(),
+ &GetCSSPropertyFontWeight(),
+ &GetCSSPropertyColor(),
+ };
+ if (should_ignore_text_only_properties == kIgnoreTextOnlyProperties)
+ difference->RemovePropertiesInSet(kTextOnlyProperties,
+ WTF_ARRAY_LENGTH(kTextOnlyProperties));
+
+ if (difference->IsEmpty())
+ return EditingTriState::kTrue;
+ if (difference->PropertyCount() == mutable_style_->PropertyCount())
+ return EditingTriState::kFalse;
+
+ return EditingTriState::kMixed;
+}
+
+EditingTriState EditingStyle::TriStateOfStyle(
+ const VisibleSelection& selection,
+ SecureContextMode secure_context_mode) const {
+ if (selection.IsNone())
+ return EditingTriState::kFalse;
+
+ if (selection.IsCaret()) {
+ return TriStateOfStyle(
+ EditingStyleUtilities::CreateStyleAtSelectionStart(selection),
+ secure_context_mode);
+ }
+
+ EditingTriState state = EditingTriState::kFalse;
+ bool node_is_start = true;
+ for (Node& node : NodeTraversal::StartsAt(*selection.Start().AnchorNode())) {
+ if (node.GetLayoutObject() && HasEditableStyle(node)) {
+ CSSComputedStyleDeclaration* node_style =
+ CSSComputedStyleDeclaration::Create(&node);
+ if (node_style) {
+ // If the selected element has <sub> or <sup> ancestor element, apply
+ // the corresponding style(vertical-align) to it so that
+ // document.queryCommandState() works with the style. See bug
+ // http://crbug.com/582225.
+ if (is_vertical_align_ &&
+ GetIdentifierValue(node_style, CSSPropertyVerticalAlign) ==
+ CSSValueBaseline) {
+ const CSSIdentifierValue* vertical_align = ToCSSIdentifierValue(
+ mutable_style_->GetPropertyCSSValue(CSSPropertyVerticalAlign));
+ if (EditingStyleUtilities::HasAncestorVerticalAlignStyle(
+ node, vertical_align->GetValueID()))
+ node.MutableComputedStyle()->SetVerticalAlign(
+ vertical_align->ConvertTo<EVerticalAlign>());
+ }
+
+ // Pass EditingStyle::DoNotIgnoreTextOnlyProperties without checking if
+ // node.isTextNode() because the node can be an element node. See bug
+ // http://crbug.com/584939.
+ EditingTriState node_state = TriStateOfStyle(
+ node_style, EditingStyle::kDoNotIgnoreTextOnlyProperties,
+ secure_context_mode);
+ if (node_is_start) {
+ state = node_state;
+ node_is_start = false;
+ } else if (state != node_state && node.IsTextNode()) {
+ state = EditingTriState::kMixed;
+ break;
+ }
+ }
+ }
+ if (&node == selection.End().AnchorNode())
+ break;
+ }
+
+ return state;
+}
+
+bool EditingStyle::ConflictsWithInlineStyleOfElement(
+ HTMLElement* element,
+ EditingStyle* extracted_style,
+ Vector<CSSPropertyID>* conflicting_properties) const {
+ DCHECK(element);
+ DCHECK(!conflicting_properties || conflicting_properties->IsEmpty());
+
+ const CSSPropertyValueSet* inline_style = element->InlineStyle();
+ if (!mutable_style_ || !inline_style)
+ return false;
+
+ unsigned property_count = mutable_style_->PropertyCount();
+ for (unsigned i = 0; i < property_count; ++i) {
+ CSSPropertyID property_id = mutable_style_->PropertyAt(i).Id();
+
+ // We don't override whitespace property of a tab span because that would
+ // collapse the tab into a space.
+ if (property_id == CSSPropertyWhiteSpace && IsTabHTMLSpanElement(element))
+ continue;
+
+ if (property_id == CSSPropertyWebkitTextDecorationsInEffect &&
+ inline_style->GetPropertyCSSValue(CSSPropertyTextDecorationLine)) {
+ if (!conflicting_properties)
+ return true;
+ conflicting_properties->push_back(CSSPropertyTextDecoration);
+ // Because text-decoration expands to text-decoration-line,
+ // we also state it as conflicting.
+ conflicting_properties->push_back(CSSPropertyTextDecorationLine);
+ if (extracted_style) {
+ extracted_style->SetProperty(
+ CSSPropertyTextDecorationLine,
+ inline_style->GetPropertyValue(CSSPropertyTextDecorationLine),
+ inline_style->PropertyIsImportant(CSSPropertyTextDecorationLine),
+ element->GetDocument().GetSecureContextMode());
+ }
+ continue;
+ }
+
+ if (!inline_style->GetPropertyCSSValue(property_id))
+ continue;
+
+ if (property_id == CSSPropertyUnicodeBidi &&
+ inline_style->GetPropertyCSSValue(CSSPropertyDirection)) {
+ if (!conflicting_properties)
+ return true;
+ conflicting_properties->push_back(CSSPropertyDirection);
+ if (extracted_style) {
+ extracted_style->SetProperty(
+ property_id, inline_style->GetPropertyValue(property_id),
+ inline_style->PropertyIsImportant(property_id),
+ element->GetDocument().GetSecureContextMode());
+ }
+ }
+
+ if (!conflicting_properties)
+ return true;
+
+ conflicting_properties->push_back(property_id);
+
+ if (extracted_style) {
+ extracted_style->SetProperty(
+ property_id, inline_style->GetPropertyValue(property_id),
+ inline_style->PropertyIsImportant(property_id),
+ element->GetDocument().GetSecureContextMode());
+ }
+ }
+
+ return conflicting_properties && !conflicting_properties->IsEmpty();
+}
+
+static const HeapVector<Member<HTMLElementEquivalent>>&
+HtmlElementEquivalents() {
+ DEFINE_STATIC_LOCAL(HeapVector<Member<HTMLElementEquivalent>>,
+ html_element_equivalents,
+ (new HeapVector<Member<HTMLElementEquivalent>>));
+ if (!html_element_equivalents.size()) {
+ html_element_equivalents.push_back(HTMLElementEquivalent::Create(
+ CSSPropertyFontWeight, CSSValueBold, HTMLNames::bTag));
+ html_element_equivalents.push_back(HTMLElementEquivalent::Create(
+ CSSPropertyFontWeight, CSSValueBold, HTMLNames::strongTag));
+ html_element_equivalents.push_back(HTMLElementEquivalent::Create(
+ CSSPropertyVerticalAlign, CSSValueSub, HTMLNames::subTag));
+ html_element_equivalents.push_back(HTMLElementEquivalent::Create(
+ CSSPropertyVerticalAlign, CSSValueSuper, HTMLNames::supTag));
+ html_element_equivalents.push_back(HTMLElementEquivalent::Create(
+ CSSPropertyFontStyle, CSSValueItalic, HTMLNames::iTag));
+ html_element_equivalents.push_back(HTMLElementEquivalent::Create(
+ CSSPropertyFontStyle, CSSValueItalic, HTMLNames::emTag));
+
+ html_element_equivalents.push_back(HTMLTextDecorationEquivalent::Create(
+ CSSValueUnderline, HTMLNames::uTag));
+ html_element_equivalents.push_back(HTMLTextDecorationEquivalent::Create(
+ CSSValueLineThrough, HTMLNames::sTag));
+ html_element_equivalents.push_back(HTMLTextDecorationEquivalent::Create(
+ CSSValueLineThrough, HTMLNames::strikeTag));
+ }
+
+ return html_element_equivalents;
+}
+
+bool EditingStyle::ConflictsWithImplicitStyleOfElement(
+ HTMLElement* element,
+ EditingStyle* extracted_style,
+ ShouldExtractMatchingStyle should_extract_matching_style) const {
+ if (!mutable_style_)
+ return false;
+
+ const HeapVector<Member<HTMLElementEquivalent>>& html_element_equivalents =
+ HtmlElementEquivalents();
+ for (size_t i = 0; i < html_element_equivalents.size(); ++i) {
+ const HTMLElementEquivalent* equivalent = html_element_equivalents[i].Get();
+ if (equivalent->Matches(element) &&
+ equivalent->PropertyExistsInStyle(mutable_style_.Get()) &&
+ (should_extract_matching_style == kExtractMatchingStyle ||
+ !equivalent->ValueIsPresentInStyle(element, mutable_style_.Get()))) {
+ if (extracted_style)
+ equivalent->AddToStyle(element, extracted_style);
+ return true;
+ }
+ }
+ return false;
+}
+
+static const HeapVector<Member<HTMLAttributeEquivalent>>&
+HtmlAttributeEquivalents() {
+ DEFINE_STATIC_LOCAL(HeapVector<Member<HTMLAttributeEquivalent>>,
+ html_attribute_equivalents,
+ (new HeapVector<Member<HTMLAttributeEquivalent>>));
+ if (!html_attribute_equivalents.size()) {
+ // elementIsStyledSpanOrHTMLEquivalent depends on the fact each
+ // HTMLAttriuteEquivalent matches exactly one attribute of exactly one
+ // element except dirAttr.
+ html_attribute_equivalents.push_back(HTMLAttributeEquivalent::Create(
+ CSSPropertyColor, HTMLNames::fontTag, HTMLNames::colorAttr));
+ html_attribute_equivalents.push_back(HTMLAttributeEquivalent::Create(
+ CSSPropertyFontFamily, HTMLNames::fontTag, HTMLNames::faceAttr));
+ html_attribute_equivalents.push_back(HTMLFontSizeEquivalent::Create());
+
+ html_attribute_equivalents.push_back(HTMLAttributeEquivalent::Create(
+ CSSPropertyDirection, HTMLNames::dirAttr));
+ html_attribute_equivalents.push_back(HTMLAttributeEquivalent::Create(
+ CSSPropertyUnicodeBidi, HTMLNames::dirAttr));
+ }
+
+ return html_attribute_equivalents;
+}
+
+bool EditingStyle::ConflictsWithImplicitStyleOfAttributes(
+ HTMLElement* element) const {
+ DCHECK(element);
+ if (!mutable_style_)
+ return false;
+
+ const HeapVector<Member<HTMLAttributeEquivalent>>&
+ html_attribute_equivalents = HtmlAttributeEquivalents();
+ for (const auto& equivalent : html_attribute_equivalents) {
+ if (equivalent->Matches(element) &&
+ equivalent->PropertyExistsInStyle(mutable_style_.Get()) &&
+ !equivalent->ValueIsPresentInStyle(element, mutable_style_.Get()))
+ return true;
+ }
+
+ return false;
+}
+
+bool EditingStyle::ExtractConflictingImplicitStyleOfAttributes(
+ HTMLElement* element,
+ ShouldPreserveWritingDirection should_preserve_writing_direction,
+ EditingStyle* extracted_style,
+ Vector<QualifiedName>& conflicting_attributes,
+ ShouldExtractMatchingStyle should_extract_matching_style) const {
+ DCHECK(element);
+ // HTMLAttributeEquivalent::addToStyle doesn't support unicode-bidi and
+ // direction properties
+ if (extracted_style)
+ DCHECK_EQ(should_preserve_writing_direction, kPreserveWritingDirection);
+ if (!mutable_style_)
+ return false;
+
+ const HeapVector<Member<HTMLAttributeEquivalent>>&
+ html_attribute_equivalents = HtmlAttributeEquivalents();
+ bool removed = false;
+ for (const auto& attribute : html_attribute_equivalents) {
+ const HTMLAttributeEquivalent* equivalent = attribute.Get();
+
+ // unicode-bidi and direction are pushed down separately so don't push down
+ // with other styles.
+ if (should_preserve_writing_direction == kPreserveWritingDirection &&
+ equivalent->AttributeName() == HTMLNames::dirAttr)
+ continue;
+
+ if (!equivalent->Matches(element) ||
+ !equivalent->PropertyExistsInStyle(mutable_style_.Get()) ||
+ (should_extract_matching_style == kDoNotExtractMatchingStyle &&
+ equivalent->ValueIsPresentInStyle(element, mutable_style_.Get())))
+ continue;
+
+ if (extracted_style)
+ equivalent->AddToStyle(element, extracted_style);
+ conflicting_attributes.push_back(equivalent->AttributeName());
+ removed = true;
+ }
+
+ return removed;
+}
+
+bool EditingStyle::StyleIsPresentInComputedStyleOfNode(Node* node) const {
+ return !mutable_style_ ||
+ GetPropertiesNotIn(mutable_style_.Get(),
+ CSSComputedStyleDeclaration::Create(node),
+ node->GetDocument().GetSecureContextMode())
+ ->IsEmpty();
+}
+
+bool EditingStyle::ElementIsStyledSpanOrHTMLEquivalent(
+ const HTMLElement* element) {
+ DCHECK(element);
+ bool element_is_span_or_element_equivalent = false;
+ if (IsHTMLSpanElement(*element)) {
+ element_is_span_or_element_equivalent = true;
+ } else {
+ const HeapVector<Member<HTMLElementEquivalent>>& html_element_equivalents =
+ HtmlElementEquivalents();
+ size_t i;
+ for (i = 0; i < html_element_equivalents.size(); ++i) {
+ if (html_element_equivalents[i]->Matches(element)) {
+ element_is_span_or_element_equivalent = true;
+ break;
+ }
+ }
+ }
+
+ AttributeCollection attributes = element->Attributes();
+ if (attributes.IsEmpty()) {
+ // span, b, etc... without any attributes
+ return element_is_span_or_element_equivalent;
+ }
+
+ unsigned matched_attributes = 0;
+ const HeapVector<Member<HTMLAttributeEquivalent>>&
+ html_attribute_equivalents = HtmlAttributeEquivalents();
+ for (const auto& equivalent : html_attribute_equivalents) {
+ if (equivalent->Matches(element) &&
+ equivalent->AttributeName() != HTMLNames::dirAttr)
+ matched_attributes++;
+ }
+
+ if (!element_is_span_or_element_equivalent && !matched_attributes) {
+ // element is not a span, a html element equivalent, or font element.
+ return false;
+ }
+
+ if (element->hasAttribute(HTMLNames::styleAttr)) {
+ if (const CSSPropertyValueSet* style = element->InlineStyle()) {
+ unsigned property_count = style->PropertyCount();
+ for (unsigned i = 0; i < property_count; ++i) {
+ if (!IsEditingProperty(style->PropertyAt(i).Id()))
+ return false;
+ }
+ }
+ matched_attributes++;
+ }
+
+ // font with color attribute, span with style attribute, etc...
+ DCHECK_LE(matched_attributes, attributes.size());
+ return matched_attributes >= attributes.size();
+}
+
+void EditingStyle::PrepareToApplyAt(
+ const Position& position,
+ ShouldPreserveWritingDirection should_preserve_writing_direction) {
+ if (!mutable_style_)
+ return;
+
+ // ReplaceSelectionCommand::handleStyleSpans() requires that this function
+ // only removes the editing style. If this function was modified in the future
+ // to delete all redundant properties, then add a boolean value to indicate
+ // which one of editingStyleAtPosition or computedStyle is called.
+ EditingStyle* editing_style_at_position =
+ EditingStyle::Create(position, kEditingPropertiesInEffect);
+ CSSPropertyValueSet* style_at_position =
+ editing_style_at_position->mutable_style_.Get();
+
+ const CSSValue* unicode_bidi = nullptr;
+ const CSSValue* direction = nullptr;
+ if (should_preserve_writing_direction == kPreserveWritingDirection) {
+ unicode_bidi = mutable_style_->GetPropertyCSSValue(CSSPropertyUnicodeBidi);
+ direction = mutable_style_->GetPropertyCSSValue(CSSPropertyDirection);
+ }
+
+ mutable_style_->RemoveEquivalentProperties(style_at_position);
+
+ if (TextAlignResolvingStartAndEnd(mutable_style_.Get()) ==
+ TextAlignResolvingStartAndEnd(style_at_position))
+ mutable_style_->RemoveProperty(CSSPropertyTextAlign);
+
+ if (GetFontColor(mutable_style_.Get()) == GetFontColor(style_at_position))
+ mutable_style_->RemoveProperty(CSSPropertyColor);
+
+ if (EditingStyleUtilities::HasTransparentBackgroundColor(
+ mutable_style_.Get()) ||
+ CssValueToColor(
+ mutable_style_->GetPropertyCSSValue(CSSPropertyBackgroundColor)) ==
+ BackgroundColorInEffect(position.ComputeContainerNode()))
+ mutable_style_->RemoveProperty(CSSPropertyBackgroundColor);
+
+ if (unicode_bidi && unicode_bidi->IsIdentifierValue()) {
+ mutable_style_->SetProperty(
+ CSSPropertyUnicodeBidi,
+ ToCSSIdentifierValue(unicode_bidi)->GetValueID());
+ if (direction && direction->IsIdentifierValue()) {
+ mutable_style_->SetProperty(
+ CSSPropertyDirection, ToCSSIdentifierValue(direction)->GetValueID());
+ }
+ }
+}
+
+void EditingStyle::MergeTypingStyle(Document* document) {
+ DCHECK(document);
+
+ EditingStyle* typing_style = document->GetFrame()->GetEditor().TypingStyle();
+ if (!typing_style || typing_style == this)
+ return;
+
+ MergeStyle(typing_style->Style(), kOverrideValues);
+}
+
+void EditingStyle::MergeInlineStyleOfElement(
+ HTMLElement* element,
+ CSSPropertyOverrideMode mode,
+ PropertiesToInclude properties_to_include) {
+ DCHECK(element);
+ if (!element->InlineStyle())
+ return;
+
+ switch (properties_to_include) {
+ case kAllProperties:
+ MergeStyle(element->InlineStyle(), mode);
+ return;
+ case kOnlyEditingInheritableProperties:
+ MergeStyle(CopyEditingProperties(element->InlineStyle(),
+ kOnlyInheritableEditingProperties),
+ mode);
+ return;
+ case kEditingPropertiesInEffect:
+ MergeStyle(
+ CopyEditingProperties(element->InlineStyle(), kAllEditingProperties),
+ mode);
+ return;
+ }
+}
+
+static inline bool ElementMatchesAndPropertyIsNotInInlineStyleDecl(
+ const HTMLElementEquivalent* equivalent,
+ const Element* element,
+ EditingStyle::CSSPropertyOverrideMode mode,
+ CSSPropertyValueSet* style) {
+ return equivalent->Matches(element) &&
+ (!element->InlineStyle() ||
+ !equivalent->PropertyExistsInStyle(element->InlineStyle())) &&
+ (mode == EditingStyle::kOverrideValues ||
+ !equivalent->PropertyExistsInStyle(style));
+}
+
+static MutableCSSPropertyValueSet* ExtractEditingProperties(
+ const CSSPropertyValueSet* style,
+ EditingStyle::PropertiesToInclude properties_to_include) {
+ if (!style)
+ return nullptr;
+
+ switch (properties_to_include) {
+ case EditingStyle::kAllProperties:
+ case EditingStyle::kEditingPropertiesInEffect:
+ return CopyEditingProperties(style, kAllEditingProperties);
+ case EditingStyle::kOnlyEditingInheritableProperties:
+ return CopyEditingProperties(style, kOnlyInheritableEditingProperties);
+ }
+
+ NOTREACHED();
+ return nullptr;
+}
+
+void EditingStyle::MergeInlineAndImplicitStyleOfElement(
+ Element* element,
+ CSSPropertyOverrideMode mode,
+ PropertiesToInclude properties_to_include) {
+ EditingStyle* style_from_rules = EditingStyle::Create();
+ style_from_rules->MergeStyleFromRulesForSerialization(element);
+
+ if (element->InlineStyle())
+ style_from_rules->mutable_style_->MergeAndOverrideOnConflict(
+ element->InlineStyle());
+
+ style_from_rules->mutable_style_ = ExtractEditingProperties(
+ style_from_rules->mutable_style_.Get(), properties_to_include);
+ MergeStyle(style_from_rules->mutable_style_.Get(), mode);
+
+ const HeapVector<Member<HTMLElementEquivalent>>& element_equivalents =
+ HtmlElementEquivalents();
+ for (const auto& equivalent : element_equivalents) {
+ if (ElementMatchesAndPropertyIsNotInInlineStyleDecl(
+ equivalent.Get(), element, mode, mutable_style_.Get()))
+ equivalent->AddToStyle(element, this);
+ }
+
+ const HeapVector<Member<HTMLAttributeEquivalent>>& attribute_equivalents =
+ HtmlAttributeEquivalents();
+ for (const auto& attribute : attribute_equivalents) {
+ if (attribute->AttributeName() == HTMLNames::dirAttr)
+ continue; // We don't want to include directionality
+ if (ElementMatchesAndPropertyIsNotInInlineStyleDecl(
+ attribute.Get(), element, mode, mutable_style_.Get()))
+ attribute->AddToStyle(element, this);
+ }
+}
+
+static const CSSValueList& MergeTextDecorationValues(
+ const CSSValueList& merged_value,
+ const CSSValueList& value_to_merge) {
+ DEFINE_STATIC_LOCAL(CSSIdentifierValue, underline,
+ (CSSIdentifierValue::Create(CSSValueUnderline)));
+ DEFINE_STATIC_LOCAL(CSSIdentifierValue, line_through,
+ (CSSIdentifierValue::Create(CSSValueLineThrough)));
+ CSSValueList& result = *merged_value.Copy();
+ if (value_to_merge.HasValue(underline) && !merged_value.HasValue(underline))
+ result.Append(underline);
+
+ if (value_to_merge.HasValue(line_through) &&
+ !merged_value.HasValue(line_through))
+ result.Append(line_through);
+
+ return result;
+}
+
+void EditingStyle::MergeStyle(const CSSPropertyValueSet* style,
+ CSSPropertyOverrideMode mode) {
+ if (!style)
+ return;
+
+ if (!mutable_style_) {
+ mutable_style_ = style->MutableCopy();
+ return;
+ }
+
+ unsigned property_count = style->PropertyCount();
+ for (unsigned i = 0; i < property_count; ++i) {
+ CSSPropertyValueSet::PropertyReference property = style->PropertyAt(i);
+ const CSSValue* value = mutable_style_->GetPropertyCSSValue(property.Id());
+
+ // text decorations never override values
+ if ((property.Id() == CSSPropertyTextDecorationLine ||
+ property.Id() == CSSPropertyWebkitTextDecorationsInEffect) &&
+ property.Value().IsValueList() && value) {
+ if (value->IsValueList()) {
+ const CSSValueList& result = MergeTextDecorationValues(
+ *ToCSSValueList(value), ToCSSValueList(property.Value()));
+ mutable_style_->SetProperty(property.Id(), result,
+ property.IsImportant());
+ continue;
+ }
+ // text-decoration: none is equivalent to not having the property
+ value = nullptr;
+ }
+
+ if (mode == kOverrideValues || (mode == kDoNotOverrideValues && !value)) {
+ mutable_style_->SetProperty(
+ CSSPropertyValue(property.PropertyMetadata(), property.Value()));
+ }
+ }
+}
+
+static MutableCSSPropertyValueSet* StyleFromMatchedRulesForElement(
+ Element* element,
+ unsigned rules_to_include) {
+ MutableCSSPropertyValueSet* style =
+ MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ StyleRuleList* matched_rules =
+ element->GetDocument().EnsureStyleResolver().StyleRulesForElement(
+ element, rules_to_include);
+ if (matched_rules) {
+ for (unsigned i = 0; i < matched_rules->size(); ++i)
+ style->MergeAndOverrideOnConflict(&matched_rules->at(i)->Properties());
+ }
+ return style;
+}
+
+void EditingStyle::MergeStyleFromRules(Element* element) {
+ MutableCSSPropertyValueSet* style_from_matched_rules =
+ StyleFromMatchedRulesForElement(
+ element,
+ StyleResolver::kAuthorCSSRules | StyleResolver::kCrossOriginCSSRules);
+ // Styles from the inline style declaration, held in the variable "style",
+ // take precedence over those from matched rules.
+ if (mutable_style_)
+ style_from_matched_rules->MergeAndOverrideOnConflict(mutable_style_.Get());
+
+ Clear();
+ mutable_style_ = style_from_matched_rules;
+}
+
+void EditingStyle::MergeStyleFromRulesForSerialization(Element* element) {
+ MergeStyleFromRules(element);
+
+ // The property value, if it's a percentage, may not reflect the actual
+ // computed value.
+ // For example: style="height: 1%; overflow: visible;" in quirksmode
+ // FIXME: There are others like this, see <rdar://problem/5195123> Slashdot
+ // copy/paste fidelity problem
+ CSSComputedStyleDeclaration* computed_style_for_element =
+ CSSComputedStyleDeclaration::Create(element);
+ MutableCSSPropertyValueSet* from_computed_style =
+ MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ {
+ unsigned property_count = mutable_style_->PropertyCount();
+ for (unsigned i = 0; i < property_count; ++i) {
+ CSSPropertyValueSet::PropertyReference property =
+ mutable_style_->PropertyAt(i);
+ const CSSProperty& css_property = property.Property();
+ const CSSValue& value = property.Value();
+ if (!value.IsPrimitiveValue())
+ continue;
+ if (ToCSSPrimitiveValue(value).IsPercentage()) {
+ if (const CSSValue* computed_property_value =
+ computed_style_for_element->GetPropertyCSSValue(css_property)) {
+ from_computed_style->AddRespectingCascade(
+ CSSPropertyValue(css_property, *computed_property_value));
+ }
+ }
+ }
+ }
+ mutable_style_->MergeAndOverrideOnConflict(from_computed_style);
+}
+
+static void RemovePropertiesInStyle(
+ MutableCSSPropertyValueSet* style_to_remove_properties_from,
+ CSSPropertyValueSet* style) {
+ unsigned property_count = style->PropertyCount();
+ Vector<const CSSProperty*> properties_to_remove(property_count);
+ for (unsigned i = 0; i < property_count; ++i)
+ properties_to_remove[i] = &style->PropertyAt(i).Property();
+
+ style_to_remove_properties_from->RemovePropertiesInSet(
+ properties_to_remove.data(), properties_to_remove.size());
+}
+
+void EditingStyle::RemoveStyleFromRulesAndContext(Element* element,
+ ContainerNode* context) {
+ DCHECK(element);
+ if (!mutable_style_)
+ return;
+
+ // StyleResolver requires clean style.
+ DCHECK_GE(element->GetDocument().Lifecycle().GetState(),
+ DocumentLifecycle::kStyleClean);
+ DCHECK(element->GetDocument().IsActive());
+
+ SecureContextMode secure_context_mode =
+ element->GetDocument().GetSecureContextMode();
+
+ // 1. Remove style from matched rules because style remain without repeating
+ // it in inline style declaration
+ MutableCSSPropertyValueSet* style_from_matched_rules =
+ StyleFromMatchedRulesForElement(element,
+ StyleResolver::kAllButEmptyCSSRules);
+ if (style_from_matched_rules && !style_from_matched_rules->IsEmpty()) {
+ mutable_style_ = GetPropertiesNotIn(
+ mutable_style_.Get(),
+ style_from_matched_rules->EnsureCSSStyleDeclaration(),
+ secure_context_mode);
+ }
+
+ // 2. Remove style present in context and not overriden by matched rules.
+ EditingStyle* computed_style =
+ EditingStyle::Create(context, kEditingPropertiesInEffect);
+ if (computed_style->mutable_style_) {
+ if (!computed_style->mutable_style_->GetPropertyCSSValue(
+ CSSPropertyBackgroundColor))
+ computed_style->mutable_style_->SetProperty(CSSPropertyBackgroundColor,
+ CSSValueTransparent);
+
+ RemovePropertiesInStyle(computed_style->mutable_style_.Get(),
+ style_from_matched_rules);
+ mutable_style_ = GetPropertiesNotIn(
+ mutable_style_.Get(),
+ computed_style->mutable_style_->EnsureCSSStyleDeclaration(),
+ secure_context_mode);
+ }
+
+ // 3. If this element is a span and has display: inline or float: none, remove
+ // them unless they are overriden by rules. These rules are added by
+ // serialization code to wrap text nodes.
+ if (IsStyleSpanOrSpanWithOnlyStyleAttribute(element)) {
+ if (!style_from_matched_rules->GetPropertyCSSValue(CSSPropertyDisplay) &&
+ GetIdentifierValue(mutable_style_.Get(), CSSPropertyDisplay) ==
+ CSSValueInline)
+ mutable_style_->RemoveProperty(CSSPropertyDisplay);
+ if (!style_from_matched_rules->GetPropertyCSSValue(CSSPropertyFloat) &&
+ GetIdentifierValue(mutable_style_.Get(), CSSPropertyFloat) ==
+ CSSValueNone)
+ mutable_style_->RemoveProperty(CSSPropertyFloat);
+ }
+}
+
+void EditingStyle::RemovePropertiesInElementDefaultStyle(Element* element) {
+ if (!mutable_style_ || mutable_style_->IsEmpty())
+ return;
+
+ CSSPropertyValueSet* default_style = StyleFromMatchedRulesForElement(
+ element, StyleResolver::kUAAndUserCSSRules);
+
+ RemovePropertiesInStyle(mutable_style_.Get(), default_style);
+}
+
+void EditingStyle::ForceInline() {
+ if (!mutable_style_)
+ mutable_style_ = MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ const bool kPropertyIsImportant = true;
+ mutable_style_->SetProperty(CSSPropertyDisplay, CSSValueInline,
+ kPropertyIsImportant);
+}
+
+int EditingStyle::LegacyFontSize(Document* document) const {
+ const CSSValue* css_value =
+ mutable_style_->GetPropertyCSSValue(CSSPropertyFontSize);
+ if (!css_value ||
+ !(css_value->IsPrimitiveValue() || css_value->IsIdentifierValue()))
+ return 0;
+ return LegacyFontSizeFromCSSValue(document, css_value, is_monospace_font_,
+ kAlwaysUseLegacyFontSize);
+}
+
+void EditingStyle::Trace(blink::Visitor* visitor) {
+ visitor->Trace(mutable_style_);
+}
+
+static void ReconcileTextDecorationProperties(
+ MutableCSSPropertyValueSet* style,
+ SecureContextMode secure_context_mode) {
+ const CSSValue* text_decorations_in_effect =
+ style->GetPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect);
+ const CSSValue* text_decoration =
+ style->GetPropertyCSSValue(CSSPropertyTextDecorationLine);
+ // "LayoutTests/editing/execCommand/insert-list-and-strikethrough.html" makes
+ // both |textDecorationsInEffect| and |textDecoration| non-null.
+ if (text_decorations_in_effect) {
+ style->SetProperty(CSSPropertyTextDecorationLine,
+ text_decorations_in_effect->CssText(),
+ /* important */ false, secure_context_mode);
+ style->RemoveProperty(CSSPropertyWebkitTextDecorationsInEffect);
+ text_decoration = text_decorations_in_effect;
+ }
+
+ // If text-decoration is set to "none", remove the property because we don't
+ // want to add redundant "text-decoration: none".
+ if (text_decoration && !text_decoration->IsValueList())
+ style->RemoveProperty(CSSPropertyTextDecorationLine);
+}
+
+StyleChange::StyleChange(EditingStyle* style, const Position& position)
+ : apply_bold_(false),
+ apply_italic_(false),
+ apply_underline_(false),
+ apply_line_through_(false),
+ apply_subscript_(false),
+ apply_superscript_(false) {
+ Document* document = position.GetDocument();
+ if (!style || !style->Style() || !document || !document->GetFrame() ||
+ !AssociatedElementOf(position))
+ return;
+
+ CSSComputedStyleDeclaration* computed_style = EnsureComputedStyle(position);
+ // FIXME: take care of background-color in effect
+ MutableCSSPropertyValueSet* mutable_style = GetPropertiesNotIn(
+ style->Style(), computed_style, document->GetSecureContextMode());
+ DCHECK(mutable_style);
+
+ ReconcileTextDecorationProperties(mutable_style,
+ document->GetSecureContextMode());
+ if (!document->GetFrame()->GetEditor().ShouldStyleWithCSS())
+ ExtractTextStyles(document, mutable_style,
+ computed_style->IsMonospaceFont());
+
+ // Changing the whitespace style in a tab span would collapse the tab into a
+ // space.
+ if (IsTabHTMLSpanElementTextNode(position.AnchorNode()) ||
+ IsTabHTMLSpanElement((position.AnchorNode())))
+ mutable_style->RemoveProperty(CSSPropertyWhiteSpace);
+
+ // If unicode-bidi is present in mutableStyle and direction is not, then add
+ // direction to mutableStyle.
+ // FIXME: Shouldn't this be done in getPropertiesNotIn?
+ if (mutable_style->GetPropertyCSSValue(CSSPropertyUnicodeBidi) &&
+ !style->Style()->GetPropertyCSSValue(CSSPropertyDirection)) {
+ mutable_style->SetProperty(
+ CSSPropertyDirection,
+ style->Style()->GetPropertyValue(CSSPropertyDirection),
+ /* important */ false, document->GetSecureContextMode());
+ }
+
+ // Save the result for later
+ css_style_ = mutable_style->AsText().StripWhiteSpace();
+}
+
+static void SetTextDecorationProperty(MutableCSSPropertyValueSet* style,
+ const CSSValueList* new_text_decoration,
+ CSSPropertyID property_id,
+ SecureContextMode secure_context_mode) {
+ if (new_text_decoration->length()) {
+ style->SetProperty(property_id, new_text_decoration->CssText(),
+ style->PropertyIsImportant(property_id),
+ secure_context_mode);
+ } else {
+ // text-decoration: none is redundant since it does not remove any text
+ // decorations.
+ style->RemoveProperty(property_id);
+ }
+}
+
+static bool GetPrimitiveValueNumber(CSSPropertyValueSet* style,
+ CSSPropertyID property_id,
+ float& number) {
+ if (!style)
+ return false;
+ const CSSValue* value = style->GetPropertyCSSValue(property_id);
+ if (!value || !value->IsPrimitiveValue())
+ return false;
+ number = ToCSSPrimitiveValue(value)->GetFloatValue();
+ return true;
+}
+
+void StyleChange::ExtractTextStyles(Document* document,
+ MutableCSSPropertyValueSet* style,
+ bool is_monospace_font) {
+ DCHECK(style);
+
+ float weight = 0;
+ bool is_number =
+ GetPrimitiveValueNumber(style, CSSPropertyFontWeight, weight);
+ if (GetIdentifierValue(style, CSSPropertyFontWeight) == CSSValueBold ||
+ (is_number && weight >= BoldThreshold())) {
+ style->RemoveProperty(CSSPropertyFontWeight);
+ apply_bold_ = true;
+ }
+
+ int font_style = GetIdentifierValue(style, CSSPropertyFontStyle);
+ if (font_style == CSSValueItalic || font_style == CSSValueOblique) {
+ style->RemoveProperty(CSSPropertyFontStyle);
+ apply_italic_ = true;
+ }
+
+ // Assuming reconcileTextDecorationProperties has been called, there should
+ // not be -webkit-text-decorations-in-effect
+ // Furthermore, text-decoration: none has been trimmed so that text-decoration
+ // property is always a CSSValueList.
+ const CSSValue* text_decoration =
+ style->GetPropertyCSSValue(CSSPropertyTextDecorationLine);
+ if (text_decoration && text_decoration->IsValueList()) {
+ DEFINE_STATIC_LOCAL(CSSIdentifierValue, underline,
+ (CSSIdentifierValue::Create(CSSValueUnderline)));
+ DEFINE_STATIC_LOCAL(CSSIdentifierValue, line_through,
+ (CSSIdentifierValue::Create(CSSValueLineThrough)));
+ CSSValueList* new_text_decoration = ToCSSValueList(text_decoration)->Copy();
+ if (new_text_decoration->RemoveAll(underline))
+ apply_underline_ = true;
+ if (new_text_decoration->RemoveAll(line_through))
+ apply_line_through_ = true;
+
+ // If trimTextDecorations, delete underline and line-through
+ SetTextDecorationProperty(style, new_text_decoration,
+ CSSPropertyTextDecorationLine,
+ document->GetSecureContextMode());
+ }
+
+ int vertical_align = GetIdentifierValue(style, CSSPropertyVerticalAlign);
+ switch (vertical_align) {
+ case CSSValueSub:
+ style->RemoveProperty(CSSPropertyVerticalAlign);
+ apply_subscript_ = true;
+ break;
+ case CSSValueSuper:
+ style->RemoveProperty(CSSPropertyVerticalAlign);
+ apply_superscript_ = true;
+ break;
+ }
+
+ if (style->GetPropertyCSSValue(CSSPropertyColor)) {
+ apply_font_color_ = GetFontColor(style).Serialized();
+ style->RemoveProperty(CSSPropertyColor);
+ }
+
+ apply_font_face_ = style->GetPropertyValue(CSSPropertyFontFamily);
+ // Remove double quotes for Outlook 2007 compatibility. See
+ // https://bugs.webkit.org/show_bug.cgi?id=79448
+ apply_font_face_.Replace('"', "");
+ style->RemoveProperty(CSSPropertyFontFamily);
+
+ if (const CSSValue* font_size =
+ style->GetPropertyCSSValue(CSSPropertyFontSize)) {
+ if (!font_size->IsPrimitiveValue() && !font_size->IsIdentifierValue()) {
+ // Can't make sense of the number. Put no font size.
+ style->RemoveProperty(CSSPropertyFontSize);
+ } else if (int legacy_font_size = LegacyFontSizeFromCSSValue(
+ document, font_size, is_monospace_font,
+ kUseLegacyFontSizeOnlyIfPixelValuesMatch)) {
+ apply_font_size_ = String::Number(legacy_font_size);
+ style->RemoveProperty(CSSPropertyFontSize);
+ }
+ }
+}
+
+static void DiffTextDecorations(MutableCSSPropertyValueSet* style,
+ CSSPropertyID property_id,
+ const CSSValue* ref_text_decoration,
+ SecureContextMode secure_context_mode) {
+ const CSSValue* text_decoration = style->GetPropertyCSSValue(property_id);
+ if (!text_decoration || !text_decoration->IsValueList() ||
+ !ref_text_decoration || !ref_text_decoration->IsValueList())
+ return;
+
+ CSSValueList* new_text_decoration = ToCSSValueList(text_decoration)->Copy();
+ const CSSValueList* values_in_ref_text_decoration =
+ ToCSSValueList(ref_text_decoration);
+
+ for (size_t i = 0; i < values_in_ref_text_decoration->length(); i++)
+ new_text_decoration->RemoveAll(values_in_ref_text_decoration->Item(i));
+
+ SetTextDecorationProperty(style, new_text_decoration, property_id,
+ secure_context_mode);
+}
+
+static bool FontWeightIsBold(const CSSValue* font_weight) {
+ if (font_weight->IsIdentifierValue()) {
+ // Because b tag can only bold text, there are only two states in plain
+ // html: bold and not bold. Collapse all other values to either one of these
+ // two states for editing purposes.
+
+ switch (ToCSSIdentifierValue(font_weight)->GetValueID()) {
+ case CSSValueNormal:
+ return false;
+ case CSSValueBold:
+ return true;
+ default:
+ break;
+ }
+ }
+
+ CHECK(font_weight->IsPrimitiveValue());
+ CHECK(ToCSSPrimitiveValue(font_weight)->IsNumber());
+ return ToCSSPrimitiveValue(font_weight)->GetFloatValue() >= BoldThreshold();
+}
+
+static bool FontWeightNeedsResolving(const CSSValue* font_weight) {
+ if (font_weight->IsPrimitiveValue())
+ return false;
+ if (!font_weight->IsIdentifierValue())
+ return true;
+ const CSSValueID value = ToCSSIdentifierValue(font_weight)->GetValueID();
+ return value == CSSValueLighter || value == CSSValueBolder;
+}
+
+MutableCSSPropertyValueSet* GetPropertiesNotIn(
+ CSSPropertyValueSet* style_with_redundant_properties,
+ CSSStyleDeclaration* base_style,
+ SecureContextMode secure_context_mode) {
+ DCHECK(style_with_redundant_properties);
+ DCHECK(base_style);
+ MutableCSSPropertyValueSet* result =
+ style_with_redundant_properties->MutableCopy();
+
+ result->RemoveEquivalentProperties(base_style);
+
+ const CSSValue* base_text_decorations_in_effect =
+ base_style->GetPropertyCSSValueInternal(
+ CSSPropertyWebkitTextDecorationsInEffect);
+ DiffTextDecorations(result, CSSPropertyTextDecorationLine,
+ base_text_decorations_in_effect, secure_context_mode);
+ DiffTextDecorations(result, CSSPropertyWebkitTextDecorationsInEffect,
+ base_text_decorations_in_effect, secure_context_mode);
+
+ if (const CSSValue* base_font_weight =
+ base_style->GetPropertyCSSValueInternal(CSSPropertyFontWeight)) {
+ if (const CSSValue* font_weight =
+ result->GetPropertyCSSValue(CSSPropertyFontWeight)) {
+ if (!FontWeightNeedsResolving(font_weight) &&
+ !FontWeightNeedsResolving(base_font_weight) &&
+ (FontWeightIsBold(font_weight) == FontWeightIsBold(base_font_weight)))
+ result->RemoveProperty(CSSPropertyFontWeight);
+ }
+ }
+
+ if (base_style->GetPropertyCSSValueInternal(CSSPropertyColor) &&
+ GetFontColor(result) == GetFontColor(base_style))
+ result->RemoveProperty(CSSPropertyColor);
+
+ if (base_style->GetPropertyCSSValueInternal(CSSPropertyTextAlign) &&
+ TextAlignResolvingStartAndEnd(result) ==
+ TextAlignResolvingStartAndEnd(base_style))
+ result->RemoveProperty(CSSPropertyTextAlign);
+
+ if (base_style->GetPropertyCSSValueInternal(CSSPropertyBackgroundColor) &&
+ GetBackgroundColor(result) == GetBackgroundColor(base_style))
+ result->RemoveProperty(CSSPropertyBackgroundColor);
+
+ return result;
+}
+
+CSSValueID GetIdentifierValue(CSSPropertyValueSet* style,
+ CSSPropertyID property_id) {
+ if (!style)
+ return CSSValueInvalid;
+ const CSSValue* value = style->GetPropertyCSSValue(property_id);
+ if (!value || !value->IsIdentifierValue())
+ return CSSValueInvalid;
+ return ToCSSIdentifierValue(value)->GetValueID();
+}
+
+CSSValueID GetIdentifierValue(CSSStyleDeclaration* style,
+ CSSPropertyID property_id) {
+ if (!style)
+ return CSSValueInvalid;
+ const CSSValue* value = style->GetPropertyCSSValueInternal(property_id);
+ if (!value || !value->IsIdentifierValue())
+ return CSSValueInvalid;
+ return ToCSSIdentifierValue(value)->GetValueID();
+}
+
+int LegacyFontSizeFromCSSValue(Document* document,
+ const CSSValue* value,
+ bool is_monospace_font,
+ LegacyFontSizeMode mode) {
+ if (value->IsPrimitiveValue()) {
+ const CSSPrimitiveValue& primitive_value = ToCSSPrimitiveValue(*value);
+ CSSPrimitiveValue::LengthUnitType length_type;
+ if (CSSPrimitiveValue::UnitTypeToLengthUnitType(
+ primitive_value.TypeWithCalcResolved(), length_type) &&
+ length_type == CSSPrimitiveValue::kUnitTypePixels) {
+ double conversion =
+ CSSPrimitiveValue::ConversionToCanonicalUnitsScaleFactor(
+ primitive_value.TypeWithCalcResolved());
+ int pixel_font_size =
+ clampTo<int>(primitive_value.GetDoubleValue() * conversion);
+ int legacy_font_size = FontSizeFunctions::LegacyFontSize(
+ document, pixel_font_size, is_monospace_font);
+ // Use legacy font size only if pixel value matches exactly to that of
+ // legacy font size.
+ if (mode == kAlwaysUseLegacyFontSize ||
+ FontSizeFunctions::FontSizeForKeyword(
+ document, legacy_font_size, is_monospace_font) == pixel_font_size)
+ return legacy_font_size;
+
+ return 0;
+ }
+ }
+
+ if (value->IsIdentifierValue()) {
+ const CSSIdentifierValue& identifier_value = ToCSSIdentifierValue(*value);
+ if (CSSValueXSmall <= identifier_value.GetValueID() &&
+ identifier_value.GetValueID() <= CSSValueWebkitXxxLarge)
+ return identifier_value.GetValueID() - CSSValueXSmall + 1;
+ }
+
+ return 0;
+}
+
+EditingTriState EditingStyle::SelectionHasStyle(const LocalFrame& frame,
+ CSSPropertyID property_id,
+ const String& value) {
+ const SecureContextMode secure_context_mode =
+ frame.GetDocument()->GetSecureContextMode();
+
+ return Create(property_id, value, secure_context_mode)
+ ->TriStateOfStyle(
+ frame.Selection().ComputeVisibleSelectionInDOMTreeDeprecated(),
+ secure_context_mode);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/editing_style.h b/chromium/third_party/blink/renderer/core/editing/editing_style.h
new file mode 100644
index 00000000000..f777be5b3a6
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editing_style.h
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ * Copyright (C) 2013 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_STYLE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_STYLE_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/css_property_names.h"
+#include "third_party/blink/renderer/core/css_value_keywords.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+#include "third_party/blink/renderer/platform/wtf/vector.h"
+
+namespace blink {
+
+class CSSStyleDeclaration;
+class CSSComputedStyleDeclaration;
+class ContainerNode;
+class Document;
+class Element;
+class HTMLElement;
+class LocalFrame;
+class MutableCSSPropertyValueSet;
+class Node;
+class QualifiedName;
+class ComputedStyle;
+class CSSPropertyValueSet;
+enum class EditingTriState;
+enum class SecureContextMode;
+enum class WritingDirection;
+
+class CORE_EXPORT EditingStyle final : public GarbageCollected<EditingStyle> {
+ public:
+ enum PropertiesToInclude {
+ kAllProperties,
+ kOnlyEditingInheritableProperties,
+ kEditingPropertiesInEffect
+ };
+ enum ShouldPreserveWritingDirection {
+ kPreserveWritingDirection,
+ kDoNotPreserveWritingDirection
+ };
+ enum ShouldExtractMatchingStyle {
+ kExtractMatchingStyle,
+ kDoNotExtractMatchingStyle
+ };
+ static float no_font_delta_;
+
+ static EditingStyle* Create() { return new EditingStyle(); }
+
+ static EditingStyle* Create(ContainerNode* node,
+ PropertiesToInclude properties_to_include =
+ kOnlyEditingInheritableProperties) {
+ return new EditingStyle(node, properties_to_include);
+ }
+
+ static EditingStyle* Create(const Position& position,
+ PropertiesToInclude properties_to_include =
+ kOnlyEditingInheritableProperties) {
+ return new EditingStyle(position, properties_to_include);
+ }
+
+ static EditingStyle* Create(const CSSPropertyValueSet* style) {
+ return new EditingStyle(style);
+ }
+
+ static EditingStyle* Create(CSSPropertyID property_id,
+ const String& value,
+ SecureContextMode secure_context_mode) {
+ return new EditingStyle(property_id, value, secure_context_mode);
+ }
+
+ MutableCSSPropertyValueSet* Style() { return mutable_style_.Get(); }
+ bool GetTextDirection(WritingDirection&) const;
+ bool IsEmpty() const;
+ void OverrideWithStyle(const CSSPropertyValueSet*);
+ void Clear();
+ EditingStyle* Copy() const;
+ EditingStyle* ExtractAndRemoveBlockProperties();
+ EditingStyle* ExtractAndRemoveTextDirection(SecureContextMode);
+ void RemoveBlockProperties();
+ void RemoveStyleAddedByElement(Element*);
+ void RemoveStyleConflictingWithStyleOfElement(Element*);
+ void CollapseTextDecorationProperties(SecureContextMode);
+ enum ShouldIgnoreTextOnlyProperties {
+ kIgnoreTextOnlyProperties,
+ kDoNotIgnoreTextOnlyProperties
+ };
+ EditingTriState TriStateOfStyle(EditingStyle*, SecureContextMode) const;
+ EditingTriState TriStateOfStyle(const VisibleSelection&,
+ SecureContextMode) const;
+ bool ConflictsWithInlineStyleOfElement(HTMLElement* element) const {
+ return ConflictsWithInlineStyleOfElement(element, nullptr, nullptr);
+ }
+ bool ConflictsWithInlineStyleOfElement(
+ HTMLElement* element,
+ EditingStyle* extracted_style,
+ Vector<CSSPropertyID>& conflicting_properties) const {
+ return ConflictsWithInlineStyleOfElement(element, extracted_style,
+ &conflicting_properties);
+ }
+ bool ConflictsWithImplicitStyleOfElement(
+ HTMLElement*,
+ EditingStyle* extracted_style = nullptr,
+ ShouldExtractMatchingStyle = kDoNotExtractMatchingStyle) const;
+ bool ConflictsWithImplicitStyleOfAttributes(HTMLElement*) const;
+ bool ExtractConflictingImplicitStyleOfAttributes(
+ HTMLElement*,
+ ShouldPreserveWritingDirection,
+ EditingStyle* extracted_style,
+ Vector<QualifiedName>& conflicting_attributes,
+ ShouldExtractMatchingStyle) const;
+ bool StyleIsPresentInComputedStyleOfNode(Node*) const;
+
+ static bool ElementIsStyledSpanOrHTMLEquivalent(const HTMLElement*);
+
+ void PrepareToApplyAt(
+ const Position&,
+ ShouldPreserveWritingDirection = kDoNotPreserveWritingDirection);
+ void MergeTypingStyle(Document*);
+ enum CSSPropertyOverrideMode { kOverrideValues, kDoNotOverrideValues };
+ void MergeInlineStyleOfElement(HTMLElement*,
+ CSSPropertyOverrideMode,
+ PropertiesToInclude = kAllProperties);
+ void MergeInlineAndImplicitStyleOfElement(Element*,
+ CSSPropertyOverrideMode,
+ PropertiesToInclude);
+ void MergeStyleFromRules(Element*);
+ void MergeStyleFromRulesForSerialization(Element*);
+ void RemoveStyleFromRulesAndContext(Element*, ContainerNode* context);
+ void RemovePropertiesInElementDefaultStyle(Element*);
+ void ForceInline();
+ int LegacyFontSize(Document*) const;
+
+ float FontSizeDelta() const { return font_size_delta_; }
+ bool HasFontSizeDelta() const { return font_size_delta_ != no_font_delta_; }
+
+ void SetProperty(CSSPropertyID,
+ const String& value,
+ bool important,
+ SecureContextMode);
+
+ void Trace(blink::Visitor*);
+ static EditingTriState SelectionHasStyle(const LocalFrame&,
+ CSSPropertyID,
+ const String& value);
+
+ private:
+ EditingStyle() = default;
+ EditingStyle(ContainerNode*, PropertiesToInclude);
+ EditingStyle(const Position&, PropertiesToInclude);
+ explicit EditingStyle(const CSSPropertyValueSet*);
+ EditingStyle(CSSPropertyID, const String& value, SecureContextMode);
+ void Init(Node*, PropertiesToInclude);
+ void RemoveInheritedColorsIfNeeded(const ComputedStyle*);
+ void ReplaceFontSizeByKeywordIfPossible(const ComputedStyle*,
+ SecureContextMode,
+ CSSComputedStyleDeclaration*);
+ void ExtractFontSizeDelta();
+ EditingTriState TriStateOfStyle(CSSStyleDeclaration* style_to_compare,
+ ShouldIgnoreTextOnlyProperties,
+ SecureContextMode) const;
+ bool ConflictsWithInlineStyleOfElement(
+ HTMLElement*,
+ EditingStyle* extracted_style,
+ Vector<CSSPropertyID>* conflicting_properties) const;
+ void MergeStyle(const CSSPropertyValueSet*, CSSPropertyOverrideMode);
+
+ Member<MutableCSSPropertyValueSet> mutable_style_;
+ bool is_monospace_font_ = false;
+ float font_size_delta_ = no_font_delta_;
+ bool is_vertical_align_ = false;
+
+ friend class HTMLElementEquivalent;
+ friend class HTMLAttributeEquivalent;
+};
+
+class StyleChange {
+ DISALLOW_NEW();
+
+ public:
+ StyleChange()
+ : apply_bold_(false),
+ apply_italic_(false),
+ apply_underline_(false),
+ apply_line_through_(false),
+ apply_subscript_(false),
+ apply_superscript_(false) {}
+
+ StyleChange(EditingStyle*, const Position&);
+
+ String CssStyle() const { return css_style_; }
+ bool ApplyBold() const { return apply_bold_; }
+ bool ApplyItalic() const { return apply_italic_; }
+ bool ApplyUnderline() const { return apply_underline_; }
+ bool ApplyLineThrough() const { return apply_line_through_; }
+ bool ApplySubscript() const { return apply_subscript_; }
+ bool ApplySuperscript() const { return apply_superscript_; }
+ bool ApplyFontColor() const { return apply_font_color_.length() > 0; }
+ bool ApplyFontFace() const { return apply_font_face_.length() > 0; }
+ bool ApplyFontSize() const { return apply_font_size_.length() > 0; }
+
+ String FontColor() { return apply_font_color_; }
+ String FontFace() { return apply_font_face_; }
+ String FontSize() { return apply_font_size_; }
+
+ bool operator==(const StyleChange& other) {
+ return css_style_ == other.css_style_ && apply_bold_ == other.apply_bold_ &&
+ apply_italic_ == other.apply_italic_ &&
+ apply_underline_ == other.apply_underline_ &&
+ apply_line_through_ == other.apply_line_through_ &&
+ apply_subscript_ == other.apply_subscript_ &&
+ apply_superscript_ == other.apply_superscript_ &&
+ apply_font_color_ == other.apply_font_color_ &&
+ apply_font_face_ == other.apply_font_face_ &&
+ apply_font_size_ == other.apply_font_size_;
+ }
+ bool operator!=(const StyleChange& other) { return !(*this == other); }
+
+ private:
+ void ExtractTextStyles(Document*,
+ MutableCSSPropertyValueSet*,
+ bool is_monospace_font);
+
+ String css_style_;
+ bool apply_bold_;
+ bool apply_italic_;
+ bool apply_underline_;
+ bool apply_line_through_;
+ bool apply_subscript_;
+ bool apply_superscript_;
+ String apply_font_color_;
+ String apply_font_face_;
+ String apply_font_size_;
+};
+
+// FIXME: Remove these functions or make them non-global to discourage using
+// CSSStyleDeclaration directly.
+CSSValueID GetIdentifierValue(CSSStyleDeclaration*, CSSPropertyID);
+CSSValueID GetIdentifierValue(CSSPropertyValueSet*, CSSPropertyID);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_STYLE_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/editing_style_test.cc b/chromium/third_party/blink/renderer/core/editing/editing_style_test.cc
new file mode 100644
index 00000000000..ad191d2f546
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editing_style_test.cc
@@ -0,0 +1,38 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/editing_style.h"
+
+#include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/html/html_body_element.h"
+#include "third_party/blink/renderer/core/html/html_div_element.h"
+#include "third_party/blink/renderer/core/html/html_head_element.h"
+#include "third_party/blink/renderer/core/html/html_html_element.h"
+
+namespace blink {
+
+class EditingStyleTest : public EditingTestBase {};
+
+TEST_F(EditingStyleTest, mergeInlineStyleOfElement) {
+ SetBodyContent(
+ "<span id=s1 style='--A:var(---B)'>1</span>"
+ "<span id=s2 style='float:var(--C)'>2</span>");
+ UpdateAllLifecyclePhases();
+
+ EditingStyle* editing_style =
+ EditingStyle::Create(ToHTMLElement(GetDocument().getElementById("s2")));
+ editing_style->MergeInlineStyleOfElement(
+ ToHTMLElement(GetDocument().getElementById("s1")),
+ EditingStyle::kOverrideValues);
+
+ EXPECT_FALSE(editing_style->Style()->HasProperty(CSSPropertyFloat))
+ << "Don't merge a property with unresolved value";
+ EXPECT_EQ("var(---B)",
+ editing_style->Style()->GetPropertyValue(AtomicString("--A")))
+ << "Keep unresolved value on merging style";
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/editing_style_utilities.cc b/chromium/third_party/blink/renderer/core/editing/editing_style_utilities.cc
new file mode 100644
index 00000000000..d5608a3e2ae
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editing_style_utilities.cc
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2007, 2008, 2009 Apple Computer, Inc.
+ * Copyright (C) 2010, 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/editing_style_utilities.h"
+
+#include "third_party/blink/renderer/core/css/css_color_value.h"
+#include "third_party/blink/renderer/core/css/css_computed_style_declaration.h"
+#include "third_party/blink/renderer/core/css/css_identifier_value.h"
+#include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/css/parser/css_parser.h"
+#include "third_party/blink/renderer/core/css_property_names.h"
+#include "third_party/blink/renderer/core/editing/editing_style.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+
+namespace blink {
+
+using namespace cssvalue;
+
+namespace {
+
+Position AdjustedSelectionStartForStyleComputation(const Position& position) {
+ // This function is used by range style computations to avoid bugs like:
+ // <rdar://problem/4017641> REGRESSION (Mail): you can only bold/unbold a
+ // selection starting from end of line once
+ // It is important to skip certain irrelevant content at the start of the
+ // selection, so we do not wind up with a spurious "mixed" style.
+
+ VisiblePosition visible_position = CreateVisiblePosition(position);
+ if (visible_position.IsNull())
+ return Position();
+
+ // if the selection starts just before a paragraph break, skip over it
+ if (IsEndOfParagraph(visible_position)) {
+ return MostForwardCaretPosition(
+ NextPositionOf(visible_position).DeepEquivalent());
+ }
+
+ // otherwise, make sure to be at the start of the first selected node,
+ // instead of possibly at the end of the last node before the selection
+ return MostForwardCaretPosition(visible_position.DeepEquivalent());
+}
+
+} // anonymous namespace
+
+bool EditingStyleUtilities::HasAncestorVerticalAlignStyle(Node& node,
+ CSSValueID value) {
+ for (Node& runner : NodeTraversal::InclusiveAncestorsOf(node)) {
+ CSSComputedStyleDeclaration* ancestor_style =
+ CSSComputedStyleDeclaration::Create(&runner);
+ if (GetIdentifierValue(ancestor_style, CSSPropertyVerticalAlign) == value)
+ return true;
+ }
+ return false;
+}
+
+EditingStyle*
+EditingStyleUtilities::CreateWrappingStyleForAnnotatedSerialization(
+ ContainerNode* context) {
+ // TODO(editing-dev): Change this function to take |const ContainerNode&|.
+ // Tracking bug for this is crbug.com/766448.
+ DCHECK(context);
+ EditingStyle* wrapping_style =
+ EditingStyle::Create(context, EditingStyle::kEditingPropertiesInEffect);
+
+ // Styles that Mail blockquotes contribute should only be placed on the Mail
+ // blockquote, to help us differentiate those styles from ones that the user
+ // has applied. This helps us get the color of content pasted into
+ // blockquotes right.
+ wrapping_style->RemoveStyleAddedByElement(ToHTMLElement(EnclosingNodeOfType(
+ FirstPositionInOrBeforeNode(*context), IsMailHTMLBlockquoteElement,
+ kCanCrossEditingBoundary)));
+
+ // Call collapseTextDecorationProperties first or otherwise it'll copy the
+ // value over from in-effect to text-decorations.
+ wrapping_style->CollapseTextDecorationProperties(
+ context->GetDocument().GetSecureContextMode());
+
+ return wrapping_style;
+}
+
+EditingStyle* EditingStyleUtilities::CreateWrappingStyleForSerialization(
+ ContainerNode* context) {
+ DCHECK(context);
+ EditingStyle* wrapping_style = EditingStyle::Create();
+
+ // When not annotating for interchange, we only preserve inline style
+ // declarations.
+ for (Node& node : NodeTraversal::InclusiveAncestorsOf(*context)) {
+ if (node.IsDocumentNode())
+ break;
+ if (node.IsStyledElement() && !IsMailHTMLBlockquoteElement(&node)) {
+ wrapping_style->MergeInlineAndImplicitStyleOfElement(
+ ToElement(&node), EditingStyle::kDoNotOverrideValues,
+ EditingStyle::kEditingPropertiesInEffect);
+ }
+ }
+
+ return wrapping_style;
+}
+
+EditingStyle* EditingStyleUtilities::CreateStyleAtSelectionStart(
+ const VisibleSelection& selection,
+ bool should_use_background_color_in_effect,
+ MutableCSSPropertyValueSet* style_to_check) {
+ if (selection.IsNone())
+ return nullptr;
+
+ Document& document = *selection.Start().GetDocument();
+
+ DCHECK(!document.NeedsLayoutTreeUpdate());
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ document.Lifecycle());
+
+ // TODO(editing-dev): We should make |position| to |const Position&| by
+ // integrating this expression and if-statement below.
+ Position position =
+ selection.IsCaret()
+ ? CreateVisiblePosition(selection.Start()).DeepEquivalent()
+ : AdjustedSelectionStartForStyleComputation(selection.Start());
+
+ // If the pos is at the end of a text node, then this node is not fully
+ // selected. Move it to the next deep equivalent position to avoid removing
+ // the style from this node.
+ // e.g. if pos was at Position("hello", 5) in <b>hello<div>world</div></b>, we
+ // want Position("world", 0) instead.
+ // We only do this for range because caret at Position("hello", 5) in
+ // <b>hello</b>world should give you font-weight: bold.
+ Node* position_node = position.ComputeContainerNode();
+ if (selection.IsRange() && position_node && position_node->IsTextNode() &&
+ position.ComputeOffsetInContainerNode() ==
+ static_cast<int>(ToText(position_node)->length()))
+ position = NextVisuallyDistinctCandidate(position);
+
+ Element* element = AssociatedElementOf(position);
+ if (!element)
+ return nullptr;
+
+ EditingStyle* style =
+ EditingStyle::Create(element, EditingStyle::kAllProperties);
+ style->MergeTypingStyle(&element->GetDocument());
+
+ // If |element| has <sub> or <sup> ancestor element, apply the corresponding
+ // style(vertical-align) to it so that document.queryCommandState() works with
+ // the style. See bug http://crbug.com/582225.
+ CSSValueID value_id =
+ GetIdentifierValue(style_to_check, CSSPropertyVerticalAlign);
+ if (value_id == CSSValueSub || value_id == CSSValueSuper) {
+ CSSComputedStyleDeclaration* element_style =
+ CSSComputedStyleDeclaration::Create(element);
+ // Find the ancestor that has CSSValueSub or CSSValueSuper as the value of
+ // CSS vertical-align property.
+ if (GetIdentifierValue(element_style, CSSPropertyVerticalAlign) ==
+ CSSValueBaseline &&
+ HasAncestorVerticalAlignStyle(*element, value_id))
+ style->Style()->SetProperty(CSSPropertyVerticalAlign, value_id);
+ }
+
+ // If background color is transparent, traverse parent nodes until we hit a
+ // different value or document root Also, if the selection is a range, ignore
+ // the background color at the start of selection, and find the background
+ // color of the common ancestor.
+ if (should_use_background_color_in_effect &&
+ (selection.IsRange() || HasTransparentBackgroundColor(style->Style()))) {
+ const EphemeralRange range(selection.ToNormalizedEphemeralRange());
+ if (const CSSValue* value =
+ BackgroundColorValueInEffect(range.CommonAncestorContainer())) {
+ style->SetProperty(CSSPropertyBackgroundColor, value->CssText(),
+ /* important */ false,
+ document.GetSecureContextMode());
+ }
+ }
+
+ return style;
+}
+
+bool EditingStyleUtilities::IsTransparentColorValue(const CSSValue* css_value) {
+ if (!css_value)
+ return true;
+ if (css_value->IsColorValue())
+ return !ToCSSColorValue(css_value)->Value().Alpha();
+ if (!css_value->IsIdentifierValue())
+ return false;
+ return ToCSSIdentifierValue(css_value)->GetValueID() == CSSValueTransparent;
+}
+
+bool EditingStyleUtilities::HasTransparentBackgroundColor(
+ CSSStyleDeclaration* style) {
+ const CSSValue* css_value =
+ style->GetPropertyCSSValueInternal(CSSPropertyBackgroundColor);
+ return IsTransparentColorValue(css_value);
+}
+
+bool EditingStyleUtilities::HasTransparentBackgroundColor(
+ CSSPropertyValueSet* style) {
+ const CSSValue* css_value =
+ style->GetPropertyCSSValue(CSSPropertyBackgroundColor);
+ return IsTransparentColorValue(css_value);
+}
+
+const CSSValue* EditingStyleUtilities::BackgroundColorValueInEffect(
+ Node* node) {
+ for (Node* ancestor = node; ancestor; ancestor = ancestor->parentNode()) {
+ CSSComputedStyleDeclaration* ancestor_style =
+ CSSComputedStyleDeclaration::Create(ancestor);
+ if (!HasTransparentBackgroundColor(ancestor_style)) {
+ return ancestor_style->GetPropertyCSSValue(
+ GetCSSPropertyBackgroundColor());
+ }
+ }
+ return nullptr;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/editing_style_utilities.h b/chromium/third_party/blink/renderer/core/editing/editing_style_utilities.h
new file mode 100644
index 00000000000..6899b9d869a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editing_style_utilities.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ * Copyright (C) 2013 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_STYLE_UTILITIES_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_STYLE_UTILITIES_H_
+
+#include "third_party/blink/renderer/core/css/css_value.h"
+#include "third_party/blink/renderer/core/css_value_keywords.h"
+#include "third_party/blink/renderer/core/dom/container_node.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+
+namespace blink {
+
+class CSSStyleDeclaration;
+class EditingStyle;
+class MutableCSSPropertyValueSet;
+class Node;
+class CSSPropertyValueSet;
+
+class EditingStyleUtilities {
+ STATIC_ONLY(EditingStyleUtilities);
+
+ public:
+ static EditingStyle* CreateWrappingStyleForAnnotatedSerialization(
+ ContainerNode* context);
+ static EditingStyle* CreateWrappingStyleForSerialization(
+ ContainerNode* context);
+ static EditingStyle* CreateStyleAtSelectionStart(
+ const VisibleSelection&,
+ bool should_use_background_color_in_effect = false,
+ MutableCSSPropertyValueSet* style_to_check = nullptr);
+ static bool IsEmbedOrIsolate(CSSValueID unicode_bidi) {
+ return unicode_bidi == CSSValueIsolate ||
+ unicode_bidi == CSSValueWebkitIsolate ||
+ unicode_bidi == CSSValueEmbed;
+ }
+
+ static bool IsTransparentColorValue(const CSSValue*);
+ static bool HasTransparentBackgroundColor(CSSStyleDeclaration*);
+ static bool HasTransparentBackgroundColor(CSSPropertyValueSet*);
+ static const CSSValue* BackgroundColorValueInEffect(Node*);
+ static bool HasAncestorVerticalAlignStyle(Node&, CSSValueID);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_STYLE_UTILITIES_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/editing_tri_state.h b/chromium/third_party/blink/renderer/core/editing/editing_tri_state.h
new file mode 100644
index 00000000000..84ba3044966
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editing_tri_state.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2013 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_TRI_STATE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_TRI_STATE_H_
+
+namespace blink {
+
+enum class EditingTriState { kFalse, kTrue, kMixed };
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/editing_utilities.cc b/chromium/third_party/blink/renderer/core/editing/editing_utilities.cc
new file mode 100644
index 00000000000..3d370ad8e91
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editing_utilities.cc
@@ -0,0 +1,1764 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+
+#include "third_party/blink/renderer/core/clipboard/data_object.h"
+#include "third_party/blink/renderer/core/clipboard/pasteboard.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element_traversal.h"
+#include "third_party/blink/renderer/core/dom/node_computed_style.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/dom/shadow_root.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/editing_strategy.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/local_caret_rect.h"
+#include "third_party/blink/renderer/core/editing/plain_text_range.h"
+#include "third_party/blink/renderer/core/editing/position_iterator.h"
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/serializers/html_interchange.h"
+#include "third_party/blink/renderer/core/editing/state_machines/backspace_state_machine.h"
+#include "third_party/blink/renderer/core/editing/state_machines/backward_grapheme_boundary_state_machine.h"
+#include "third_party/blink/renderer/core/editing/state_machines/forward_grapheme_boundary_state_machine.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/use_counter.h"
+#include "third_party/blink/renderer/core/html/canvas/html_canvas_element.h"
+#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
+#include "third_party/blink/renderer/core/html/html_br_element.h"
+#include "third_party/blink/renderer/core/html/html_div_element.h"
+#include "third_party/blink/renderer/core/html/html_image_element.h"
+#include "third_party/blink/renderer/core/html/html_li_element.h"
+#include "third_party/blink/renderer/core/html/html_paragraph_element.h"
+#include "third_party/blink/renderer/core/html/html_span_element.h"
+#include "third_party/blink/renderer/core/html/html_table_cell_element.h"
+#include "third_party/blink/renderer/core/html/html_ulist_element.h"
+#include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h"
+#include "third_party/blink/renderer/core/html_element_factory.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/input_type_names.h"
+#include "third_party/blink/renderer/core/layout/layout_image.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/core/layout/layout_table_cell.h"
+#include "third_party/blink/renderer/core/svg/svg_image_element.h"
+#include "third_party/blink/renderer/platform/clipboard/clipboard_mime_types.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+namespace {
+
+std::ostream& operator<<(std::ostream& os, PositionMoveType type) {
+ static const char* const kTexts[] = {"CodeUnit", "BackwardDeletion",
+ "GraphemeCluster"};
+ const auto& it = std::begin(kTexts) + static_cast<size_t>(type);
+ DCHECK_GE(it, std::begin(kTexts)) << "Unknown PositionMoveType value";
+ DCHECK_LT(it, std::end(kTexts)) << "Unknown PositionMoveType value";
+ return os << *it;
+}
+
+InputEvent::EventCancelable InputTypeIsCancelable(
+ InputEvent::InputType input_type) {
+ using InputType = InputEvent::InputType;
+ switch (input_type) {
+ case InputType::kInsertText:
+ case InputType::kInsertLineBreak:
+ case InputType::kInsertParagraph:
+ case InputType::kInsertCompositionText:
+ case InputType::kInsertReplacementText:
+ case InputType::kDeleteWordBackward:
+ case InputType::kDeleteWordForward:
+ case InputType::kDeleteSoftLineBackward:
+ case InputType::kDeleteSoftLineForward:
+ case InputType::kDeleteHardLineBackward:
+ case InputType::kDeleteHardLineForward:
+ case InputType::kDeleteContentBackward:
+ case InputType::kDeleteContentForward:
+ return InputEvent::EventCancelable::kNotCancelable;
+ default:
+ return InputEvent::EventCancelable::kIsCancelable;
+ }
+}
+
+UChar WhitespaceRebalancingCharToAppend(const String& string,
+ bool start_is_start_of_paragraph,
+ bool should_emit_nbsp_before_end,
+ size_t index,
+ UChar previous) {
+ DCHECK_LT(index, string.length());
+
+ if (!IsWhitespace(string[index]))
+ return string[index];
+
+ if (!index && start_is_start_of_paragraph)
+ return kNoBreakSpaceCharacter;
+ if (index + 1 == string.length() && should_emit_nbsp_before_end)
+ return kNoBreakSpaceCharacter;
+
+ // Generally, alternate between space and no-break space.
+ if (previous == ' ')
+ return kNoBreakSpaceCharacter;
+ if (previous == kNoBreakSpaceCharacter)
+ return ' ';
+
+ // Run of two or more spaces starts with a no-break space (crbug.com/453042).
+ if (index + 1 < string.length() && IsWhitespace(string[index + 1]))
+ return kNoBreakSpaceCharacter;
+
+ return ' ';
+}
+
+} // namespace
+
+bool NeedsLayoutTreeUpdate(const Node& node) {
+ const Document& document = node.GetDocument();
+ if (document.NeedsLayoutTreeUpdate())
+ return true;
+ // TODO(yosin): We should make |document::needsLayoutTreeUpdate()| to
+ // check |LayoutView::needsLayout()|.
+ return document.View() && document.View()->NeedsLayout();
+}
+
+template <typename PositionType>
+static bool NeedsLayoutTreeUpdateAlgorithm(const PositionType& position) {
+ const Node* node = position.AnchorNode();
+ if (!node)
+ return false;
+ return NeedsLayoutTreeUpdate(*node);
+}
+
+bool NeedsLayoutTreeUpdate(const Position& position) {
+ return NeedsLayoutTreeUpdateAlgorithm<Position>(position);
+}
+
+bool NeedsLayoutTreeUpdate(const PositionInFlatTree& position) {
+ return NeedsLayoutTreeUpdateAlgorithm<PositionInFlatTree>(position);
+}
+
+// Atomic means that the node has no children, or has children which are ignored
+// for the purposes of editing.
+bool IsAtomicNode(const Node* node) {
+ return node && (!node->hasChildren() || EditingIgnoresContent(*node));
+}
+
+template <typename Traversal>
+static int ComparePositions(const Node* container_a,
+ int offset_a,
+ const Node* container_b,
+ int offset_b,
+ bool* disconnected) {
+ DCHECK(container_a);
+ DCHECK(container_b);
+
+ if (disconnected)
+ *disconnected = false;
+
+ if (!container_a)
+ return -1;
+ if (!container_b)
+ return 1;
+
+ // see DOM2 traversal & range section 2.5
+
+ // case 1: both points have the same container
+ if (container_a == container_b) {
+ if (offset_a == offset_b)
+ return 0; // A is equal to B
+ if (offset_a < offset_b)
+ return -1; // A is before B
+ return 1; // A is after B
+ }
+
+ // case 2: node C (container B or an ancestor) is a child node of A
+ const Node* c = container_b;
+ while (c && Traversal::Parent(*c) != container_a)
+ c = Traversal::Parent(*c);
+ if (c) {
+ int offset_c = 0;
+ Node* n = Traversal::FirstChild(*container_a);
+ while (n != c && offset_c < offset_a) {
+ offset_c++;
+ n = Traversal::NextSibling(*n);
+ }
+
+ if (offset_a <= offset_c)
+ return -1; // A is before B
+ return 1; // A is after B
+ }
+
+ // case 3: node C (container A or an ancestor) is a child node of B
+ c = container_a;
+ while (c && Traversal::Parent(*c) != container_b)
+ c = Traversal::Parent(*c);
+ if (c) {
+ int offset_c = 0;
+ Node* n = Traversal::FirstChild(*container_b);
+ while (n != c && offset_c < offset_b) {
+ offset_c++;
+ n = Traversal::NextSibling(*n);
+ }
+
+ if (offset_c < offset_b)
+ return -1; // A is before B
+ return 1; // A is after B
+ }
+
+ // case 4: containers A & B are siblings, or children of siblings
+ // ### we need to do a traversal here instead
+ Node* common_ancestor = Traversal::CommonAncestor(*container_a, *container_b);
+ if (!common_ancestor) {
+ if (disconnected)
+ *disconnected = true;
+ return 0;
+ }
+ const Node* child_a = container_a;
+ while (child_a && Traversal::Parent(*child_a) != common_ancestor)
+ child_a = Traversal::Parent(*child_a);
+ if (!child_a)
+ child_a = common_ancestor;
+ const Node* child_b = container_b;
+ while (child_b && Traversal::Parent(*child_b) != common_ancestor)
+ child_b = Traversal::Parent(*child_b);
+ if (!child_b)
+ child_b = common_ancestor;
+
+ if (child_a == child_b)
+ return 0; // A is equal to B
+
+ Node* n = Traversal::FirstChild(*common_ancestor);
+ while (n) {
+ if (n == child_a)
+ return -1; // A is before B
+ if (n == child_b)
+ return 1; // A is after B
+ n = Traversal::NextSibling(*n);
+ }
+
+ // Should never reach this point.
+ NOTREACHED();
+ return 0;
+}
+
+int ComparePositionsInDOMTree(const Node* container_a,
+ int offset_a,
+ const Node* container_b,
+ int offset_b,
+ bool* disconnected) {
+ return ComparePositions<NodeTraversal>(container_a, offset_a, container_b,
+ offset_b, disconnected);
+}
+
+int ComparePositionsInFlatTree(const Node* container_a,
+ int offset_a,
+ const Node* container_b,
+ int offset_b,
+ bool* disconnected) {
+ return ComparePositions<FlatTreeTraversal>(container_a, offset_a, container_b,
+ offset_b, disconnected);
+}
+
+// Compare two positions, taking into account the possibility that one or both
+// could be inside a shadow tree. Only works for non-null values.
+int ComparePositions(const Position& a, const Position& b) {
+ DCHECK(a.IsNotNull());
+ DCHECK(b.IsNotNull());
+ const TreeScope* common_scope = Position::CommonAncestorTreeScope(a, b);
+
+ DCHECK(common_scope);
+ if (!common_scope)
+ return 0;
+
+ Node* node_a = common_scope->AncestorInThisScope(a.ComputeContainerNode());
+ DCHECK(node_a);
+ bool has_descendent_a = node_a != a.ComputeContainerNode();
+ int offset_a = has_descendent_a ? 0 : a.ComputeOffsetInContainerNode();
+
+ Node* node_b = common_scope->AncestorInThisScope(b.ComputeContainerNode());
+ DCHECK(node_b);
+ bool has_descendent_b = node_b != b.ComputeContainerNode();
+ int offset_b = has_descendent_b ? 0 : b.ComputeOffsetInContainerNode();
+
+ int bias = 0;
+ if (node_a == node_b) {
+ if (has_descendent_a)
+ bias = -1;
+ else if (has_descendent_b)
+ bias = 1;
+ }
+
+ int result = ComparePositionsInDOMTree(node_a, offset_a, node_b, offset_b);
+ return result ? result : bias;
+}
+
+int ComparePositions(const PositionWithAffinity& a,
+ const PositionWithAffinity& b) {
+ return ComparePositions(a.GetPosition(), b.GetPosition());
+}
+
+int ComparePositions(const VisiblePosition& a, const VisiblePosition& b) {
+ return ComparePositions(a.DeepEquivalent(), b.DeepEquivalent());
+}
+
+bool IsNodeFullyContained(const EphemeralRange& range, const Node& node) {
+ if (range.IsNull())
+ return false;
+
+ if (!NodeTraversal::CommonAncestor(*range.StartPosition().AnchorNode(), node))
+ return false;
+
+ return range.StartPosition() <= Position::BeforeNode(node) &&
+ Position::AfterNode(node) <= range.EndPosition();
+}
+
+// TODO(editing-dev): We should implement real version which refers
+// "user-select" CSS property.
+// TODO(editing-dev): We should make |SelectionAdjuster| to use this funciton
+// instead of |isSelectionBondary()|.
+bool IsUserSelectContain(const Node& node) {
+ return IsHTMLTextAreaElement(node) || IsHTMLInputElement(node) ||
+ IsHTMLSelectElement(node);
+}
+
+enum EditableLevel { kEditable, kRichlyEditable };
+static bool HasEditableLevel(const Node& node, EditableLevel editable_level) {
+ DCHECK(node.GetDocument().IsActive());
+ // TODO(editing-dev): We should have this check:
+ // DCHECK_GE(node.document().lifecycle().state(),
+ // DocumentLifecycle::StyleClean);
+ if (node.IsPseudoElement())
+ return false;
+
+ // Ideally we'd call DCHECK(!needsStyleRecalc()) here, but
+ // ContainerNode::setFocus() calls setNeedsStyleRecalc(), so the assertion
+ // would fire in the middle of Document::setFocusedNode().
+
+ for (const Node& ancestor : NodeTraversal::InclusiveAncestorsOf(node)) {
+ if (!(ancestor.IsHTMLElement() || ancestor.IsDocumentNode()))
+ continue;
+ const ComputedStyle* style = ancestor.GetComputedStyle();
+ if (!style)
+ continue;
+ switch (style->UserModify()) {
+ case EUserModify::kReadOnly:
+ return false;
+ case EUserModify::kReadWrite:
+ return true;
+ case EUserModify::kReadWritePlaintextOnly:
+ return editable_level != kRichlyEditable;
+ }
+ }
+
+ return false;
+}
+
+bool HasEditableStyle(const Node& node) {
+ // TODO(editing-dev): We shouldn't check editable style in inactive documents.
+ // We should hoist this check in the call stack, replace it by a DCHECK of
+ // active document and ultimately cleanup the code paths with inactive
+ // documents. See crbug.com/667681
+ if (!node.GetDocument().IsActive())
+ return false;
+
+ return HasEditableLevel(node, kEditable);
+}
+
+bool HasRichlyEditableStyle(const Node& node) {
+ // TODO(editing-dev): We shouldn't check editable style in inactive documents.
+ // We should hoist this check in the call stack, replace it by a DCHECK of
+ // active document and ultimately cleanup the code paths with inactive
+ // documents. See crbug.com/667681
+ if (!node.GetDocument().IsActive())
+ return false;
+
+ return HasEditableLevel(node, kRichlyEditable);
+}
+
+bool IsRootEditableElement(const Node& node) {
+ return HasEditableStyle(node) && node.IsElementNode() &&
+ (!node.parentNode() || !HasEditableStyle(*node.parentNode()) ||
+ !node.parentNode()->IsElementNode() ||
+ &node == node.GetDocument().body());
+}
+
+Element* RootEditableElement(const Node& node) {
+ const Node* result = nullptr;
+ for (const Node* n = &node; n && HasEditableStyle(*n); n = n->parentNode()) {
+ if (n->IsElementNode())
+ result = n;
+ if (node.GetDocument().body() == n)
+ break;
+ }
+ return ToElement(const_cast<Node*>(result));
+}
+
+ContainerNode* HighestEditableRoot(
+ const Position& position,
+ Element* (*root_editable_element_of)(const Position&),
+ bool (*has_editable_style)(const Node&)) {
+ if (position.IsNull())
+ return nullptr;
+
+ ContainerNode* highest_root = root_editable_element_of(position);
+ if (!highest_root)
+ return nullptr;
+
+ if (IsHTMLBodyElement(*highest_root))
+ return highest_root;
+
+ ContainerNode* node = highest_root->parentNode();
+ while (node) {
+ if (has_editable_style(*node))
+ highest_root = node;
+ if (IsHTMLBodyElement(*node))
+ break;
+ node = node->parentNode();
+ }
+
+ return highest_root;
+}
+
+ContainerNode* HighestEditableRoot(const PositionInFlatTree& position) {
+ return HighestEditableRoot(ToPositionInDOMTree(position));
+}
+
+bool IsEditablePosition(const Position& position) {
+ const Node* node = position.ParentAnchoredEquivalent().AnchorNode();
+ if (!node)
+ return false;
+ DCHECK(node->GetDocument().IsActive());
+ if (node->GetDocument().Lifecycle().GetState() >=
+ DocumentLifecycle::kInStyleRecalc) {
+ // TODO(yosin): Update the condition and DCHECK here given that
+ // https://codereview.chromium.org/2665823002/ avoided this function from
+ // being called during InStyleRecalc.
+ } else {
+ DCHECK(!NeedsLayoutTreeUpdate(position)) << position;
+ }
+
+ if (IsDisplayInsideTable(node))
+ node = node->parentNode();
+
+ if (node->IsDocumentNode())
+ return false;
+ return HasEditableStyle(*node);
+}
+
+bool IsEditablePosition(const PositionInFlatTree& p) {
+ return IsEditablePosition(ToPositionInDOMTree(p));
+}
+
+bool IsRichlyEditablePosition(const Position& p) {
+ const Node* node = p.AnchorNode();
+ if (!node)
+ return false;
+
+ if (IsDisplayInsideTable(node))
+ node = node->parentNode();
+
+ return HasRichlyEditableStyle(*node);
+}
+
+Element* RootEditableElementOf(const Position& p) {
+ Node* node = p.ComputeContainerNode();
+ if (!node)
+ return nullptr;
+
+ if (IsDisplayInsideTable(node))
+ node = node->parentNode();
+
+ return RootEditableElement(*node);
+}
+
+Element* RootEditableElementOf(const PositionInFlatTree& p) {
+ return RootEditableElementOf(ToPositionInDOMTree(p));
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> NextCandidateAlgorithm(
+ const PositionTemplate<Strategy>& position) {
+ TRACE_EVENT0("input", "EditingUtility::nextCandidateAlgorithm");
+ PositionIteratorAlgorithm<Strategy> p(position);
+
+ p.Increment();
+ while (!p.AtEnd()) {
+ PositionTemplate<Strategy> candidate = p.ComputePosition();
+ if (IsVisuallyEquivalentCandidate(candidate))
+ return candidate;
+
+ p.Increment();
+ }
+
+ return PositionTemplate<Strategy>();
+}
+
+Position NextCandidate(const Position& position) {
+ return NextCandidateAlgorithm<EditingStrategy>(position);
+}
+
+PositionInFlatTree NextCandidate(const PositionInFlatTree& position) {
+ return NextCandidateAlgorithm<EditingInFlatTreeStrategy>(position);
+}
+
+// |nextVisuallyDistinctCandidate| is similar to |nextCandidate| except
+// for returning position which |downstream()| not equal to initial position's
+// |downstream()|.
+template <typename Strategy>
+static PositionTemplate<Strategy> NextVisuallyDistinctCandidateAlgorithm(
+ const PositionTemplate<Strategy>& position) {
+ TRACE_EVENT0("input",
+ "EditingUtility::nextVisuallyDistinctCandidateAlgorithm");
+ if (position.IsNull())
+ return PositionTemplate<Strategy>();
+
+ PositionIteratorAlgorithm<Strategy> p(position);
+ const PositionTemplate<Strategy> downstream_start =
+ MostForwardCaretPosition(position);
+
+ p.Increment();
+ while (!p.AtEnd()) {
+ PositionTemplate<Strategy> candidate = p.ComputePosition();
+ if (IsVisuallyEquivalentCandidate(candidate) &&
+ MostForwardCaretPosition(candidate) != downstream_start)
+ return candidate;
+
+ p.Increment();
+ }
+
+ return PositionTemplate<Strategy>();
+}
+
+Position NextVisuallyDistinctCandidate(const Position& position) {
+ return NextVisuallyDistinctCandidateAlgorithm<EditingStrategy>(position);
+}
+
+PositionInFlatTree NextVisuallyDistinctCandidate(
+ const PositionInFlatTree& position) {
+ return NextVisuallyDistinctCandidateAlgorithm<EditingInFlatTreeStrategy>(
+ position);
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> PreviousCandidateAlgorithm(
+ const PositionTemplate<Strategy>& position) {
+ TRACE_EVENT0("input", "EditingUtility::previousCandidateAlgorithm");
+ PositionIteratorAlgorithm<Strategy> p(position);
+
+ p.Decrement();
+ while (!p.AtStart()) {
+ PositionTemplate<Strategy> candidate = p.ComputePosition();
+ if (IsVisuallyEquivalentCandidate(candidate))
+ return candidate;
+
+ p.Decrement();
+ }
+
+ return PositionTemplate<Strategy>();
+}
+
+Position PreviousCandidate(const Position& position) {
+ return PreviousCandidateAlgorithm<EditingStrategy>(position);
+}
+
+PositionInFlatTree PreviousCandidate(const PositionInFlatTree& position) {
+ return PreviousCandidateAlgorithm<EditingInFlatTreeStrategy>(position);
+}
+
+// |previousVisuallyDistinctCandidate| is similar to |previousCandidate| except
+// for returning position which |downstream()| not equal to initial position's
+// |downstream()|.
+template <typename Strategy>
+PositionTemplate<Strategy> PreviousVisuallyDistinctCandidateAlgorithm(
+ const PositionTemplate<Strategy>& position) {
+ TRACE_EVENT0("input",
+ "EditingUtility::previousVisuallyDistinctCandidateAlgorithm");
+ if (position.IsNull())
+ return PositionTemplate<Strategy>();
+
+ PositionIteratorAlgorithm<Strategy> p(position);
+ PositionTemplate<Strategy> downstream_start =
+ MostForwardCaretPosition(position);
+
+ p.Decrement();
+ while (!p.AtStart()) {
+ PositionTemplate<Strategy> candidate = p.ComputePosition();
+ if (IsVisuallyEquivalentCandidate(candidate) &&
+ MostForwardCaretPosition(candidate) != downstream_start)
+ return candidate;
+
+ p.Decrement();
+ }
+
+ return PositionTemplate<Strategy>();
+}
+
+Position PreviousVisuallyDistinctCandidate(const Position& position) {
+ return PreviousVisuallyDistinctCandidateAlgorithm<EditingStrategy>(position);
+}
+
+PositionInFlatTree PreviousVisuallyDistinctCandidate(
+ const PositionInFlatTree& position) {
+ return PreviousVisuallyDistinctCandidateAlgorithm<EditingInFlatTreeStrategy>(
+ position);
+}
+
+VisiblePosition FirstEditableVisiblePositionAfterPositionInRoot(
+ const Position& position,
+ ContainerNode& highest_root) {
+ DCHECK(!NeedsLayoutTreeUpdate(position));
+ return CreateVisiblePosition(
+ FirstEditablePositionAfterPositionInRoot(position, highest_root));
+}
+
+VisiblePositionInFlatTree FirstEditableVisiblePositionAfterPositionInRoot(
+ const PositionInFlatTree& position,
+ ContainerNode& highest_root) {
+ DCHECK(!NeedsLayoutTreeUpdate(position));
+ return CreateVisiblePosition(
+ FirstEditablePositionAfterPositionInRoot(position, highest_root));
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> FirstEditablePositionAfterPositionInRootAlgorithm(
+ const PositionTemplate<Strategy>& position,
+ const Node& highest_root) {
+ DCHECK(!NeedsLayoutTreeUpdate(highest_root))
+ << position << ' ' << highest_root;
+ // position falls before highestRoot.
+ if (position.CompareTo(PositionTemplate<Strategy>::FirstPositionInNode(
+ highest_root)) == -1 &&
+ HasEditableStyle(highest_root))
+ return PositionTemplate<Strategy>::FirstPositionInNode(highest_root);
+
+ PositionTemplate<Strategy> editable_position = position;
+
+ if (position.AnchorNode()->GetTreeScope() != highest_root.GetTreeScope()) {
+ Node* shadow_ancestor = highest_root.GetTreeScope().AncestorInThisScope(
+ editable_position.AnchorNode());
+ if (!shadow_ancestor)
+ return PositionTemplate<Strategy>();
+
+ editable_position = PositionTemplate<Strategy>::AfterNode(*shadow_ancestor);
+ }
+
+ Node* non_editable_node = nullptr;
+ while (editable_position.AnchorNode() &&
+ !IsEditablePosition(editable_position) &&
+ editable_position.AnchorNode()->IsDescendantOf(&highest_root)) {
+ non_editable_node = editable_position.AnchorNode();
+ editable_position = IsAtomicNode(editable_position.AnchorNode())
+ ? PositionTemplate<Strategy>::InParentAfterNode(
+ *editable_position.AnchorNode())
+ : NextVisuallyDistinctCandidate(editable_position);
+ }
+
+ if (editable_position.AnchorNode() &&
+ editable_position.AnchorNode() != &highest_root &&
+ !editable_position.AnchorNode()->IsDescendantOf(&highest_root))
+ return PositionTemplate<Strategy>();
+
+ // If |editablePosition| has the non-editable child skipped, get the next
+ // sibling position. If not, we can't get the next paragraph in
+ // InsertListCommand::doApply's while loop. See http://crbug.com/571420
+ if (non_editable_node &&
+ non_editable_node->IsDescendantOf(editable_position.AnchorNode()))
+ editable_position = NextVisuallyDistinctCandidate(editable_position);
+ return editable_position;
+}
+
+Position FirstEditablePositionAfterPositionInRoot(const Position& position,
+ const Node& highest_root) {
+ return FirstEditablePositionAfterPositionInRootAlgorithm<EditingStrategy>(
+ position, highest_root);
+}
+
+PositionInFlatTree FirstEditablePositionAfterPositionInRoot(
+ const PositionInFlatTree& position,
+ const Node& highest_root) {
+ return FirstEditablePositionAfterPositionInRootAlgorithm<
+ EditingInFlatTreeStrategy>(position, highest_root);
+}
+
+VisiblePosition LastEditableVisiblePositionBeforePositionInRoot(
+ const Position& position,
+ ContainerNode& highest_root) {
+ DCHECK(!NeedsLayoutTreeUpdate(position));
+ return CreateVisiblePosition(
+ LastEditablePositionBeforePositionInRoot(position, highest_root));
+}
+
+VisiblePositionInFlatTree LastEditableVisiblePositionBeforePositionInRoot(
+ const PositionInFlatTree& position,
+ ContainerNode& highest_root) {
+ DCHECK(!NeedsLayoutTreeUpdate(position));
+ return CreateVisiblePosition(
+ LastEditablePositionBeforePositionInRoot(position, highest_root));
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> LastEditablePositionBeforePositionInRootAlgorithm(
+ const PositionTemplate<Strategy>& position,
+ const Node& highest_root) {
+ DCHECK(!NeedsLayoutTreeUpdate(highest_root))
+ << position << ' ' << highest_root;
+ // When position falls after highestRoot, the result is easy to compute.
+ if (position.CompareTo(
+ PositionTemplate<Strategy>::LastPositionInNode(highest_root)) == 1)
+ return PositionTemplate<Strategy>::LastPositionInNode(highest_root);
+
+ PositionTemplate<Strategy> editable_position = position;
+
+ if (position.AnchorNode()->GetTreeScope() != highest_root.GetTreeScope()) {
+ Node* shadow_ancestor = highest_root.GetTreeScope().AncestorInThisScope(
+ editable_position.AnchorNode());
+ if (!shadow_ancestor)
+ return PositionTemplate<Strategy>();
+
+ editable_position = PositionTemplate<Strategy>::FirstPositionInOrBeforeNode(
+ *shadow_ancestor);
+ }
+
+ while (editable_position.AnchorNode() &&
+ !IsEditablePosition(editable_position) &&
+ editable_position.AnchorNode()->IsDescendantOf(&highest_root))
+ editable_position =
+ IsAtomicNode(editable_position.AnchorNode())
+ ? PositionTemplate<Strategy>::InParentBeforeNode(
+ *editable_position.AnchorNode())
+ : PreviousVisuallyDistinctCandidate(editable_position);
+
+ if (editable_position.AnchorNode() &&
+ editable_position.AnchorNode() != &highest_root &&
+ !editable_position.AnchorNode()->IsDescendantOf(&highest_root))
+ return PositionTemplate<Strategy>();
+ return editable_position;
+}
+
+Position LastEditablePositionBeforePositionInRoot(const Position& position,
+ const Node& highest_root) {
+ return LastEditablePositionBeforePositionInRootAlgorithm<EditingStrategy>(
+ position, highest_root);
+}
+
+PositionInFlatTree LastEditablePositionBeforePositionInRoot(
+ const PositionInFlatTree& position,
+ const Node& highest_root) {
+ return LastEditablePositionBeforePositionInRootAlgorithm<
+ EditingInFlatTreeStrategy>(position, highest_root);
+}
+
+template <typename StateMachine>
+int FindNextBoundaryOffset(const String& str, int current) {
+ StateMachine machine;
+ TextSegmentationMachineState state = TextSegmentationMachineState::kInvalid;
+
+ for (int i = current - 1; i >= 0; --i) {
+ state = machine.FeedPrecedingCodeUnit(str[i]);
+ if (state != TextSegmentationMachineState::kNeedMoreCodeUnit)
+ break;
+ }
+ if (current == 0 || state == TextSegmentationMachineState::kNeedMoreCodeUnit)
+ state = machine.TellEndOfPrecedingText();
+ if (state == TextSegmentationMachineState::kFinished)
+ return current + machine.FinalizeAndGetBoundaryOffset();
+ const int length = str.length();
+ DCHECK_EQ(TextSegmentationMachineState::kNeedFollowingCodeUnit, state);
+ for (int i = current; i < length; ++i) {
+ state = machine.FeedFollowingCodeUnit(str[i]);
+ if (state != TextSegmentationMachineState::kNeedMoreCodeUnit)
+ break;
+ }
+ return current + machine.FinalizeAndGetBoundaryOffset();
+}
+
+int PreviousGraphemeBoundaryOf(const Node& node, int current) {
+ // TODO(yosin): Need to support grapheme crossing |Node| boundary.
+ DCHECK_GE(current, 0);
+ if (current <= 1 || !node.IsTextNode())
+ return current - 1;
+ const String& text = ToText(node).data();
+ // TODO(yosin): Replace with DCHECK for out-of-range request.
+ if (static_cast<unsigned>(current) > text.length())
+ return current - 1;
+ return FindNextBoundaryOffset<BackwardGraphemeBoundaryStateMachine>(text,
+ current);
+}
+
+static int PreviousBackwardDeletionOffsetOf(const Node& node, int current) {
+ DCHECK_GE(current, 0);
+ if (current <= 1)
+ return 0;
+ if (!node.IsTextNode())
+ return current - 1;
+
+ const String& text = ToText(node).data();
+ DCHECK_LT(static_cast<unsigned>(current - 1), text.length());
+ return FindNextBoundaryOffset<BackspaceStateMachine>(text, current);
+}
+
+int NextGraphemeBoundaryOf(const Node& node, int current) {
+ // TODO(yosin): Need to support grapheme crossing |Node| boundary.
+ if (!node.IsTextNode())
+ return current + 1;
+ const String& text = ToText(node).data();
+ const int length = text.length();
+ DCHECK_LE(current, length);
+ if (current >= length - 1)
+ return current + 1;
+ return FindNextBoundaryOffset<ForwardGraphemeBoundaryStateMachine>(text,
+ current);
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> PreviousPositionOfAlgorithm(
+ const PositionTemplate<Strategy>& position,
+ PositionMoveType move_type) {
+ Node* const node = position.AnchorNode();
+ if (!node)
+ return position;
+
+ const int offset = position.ComputeEditingOffset();
+
+ if (offset > 0) {
+ if (EditingIgnoresContent(*node))
+ return PositionTemplate<Strategy>::BeforeNode(*node);
+ if (Node* child = Strategy::ChildAt(*node, offset - 1)) {
+ return PositionTemplate<Strategy>::LastPositionInOrAfterNode(*child);
+ }
+
+ // There are two reasons child might be 0:
+ // 1) The node is node like a text node that is not an element, and
+ // therefore has no children. Going backward one character at a
+ // time is correct.
+ // 2) The old offset was a bogus offset like (<br>, 1), and there is
+ // no child. Going from 1 to 0 is correct.
+ switch (move_type) {
+ case PositionMoveType::kCodeUnit:
+ return PositionTemplate<Strategy>(node, offset - 1);
+ case PositionMoveType::kBackwardDeletion:
+ return PositionTemplate<Strategy>(
+ node, PreviousBackwardDeletionOffsetOf(*node, offset));
+ case PositionMoveType::kGraphemeCluster:
+ return PositionTemplate<Strategy>(
+ node, PreviousGraphemeBoundaryOf(*node, offset));
+ default:
+ NOTREACHED() << "Unhandled moveType: " << move_type;
+ }
+ }
+
+ if (ContainerNode* parent = Strategy::Parent(*node)) {
+ if (EditingIgnoresContent(*parent))
+ return PositionTemplate<Strategy>::BeforeNode(*parent);
+ // TODO(yosin) We should use |Strategy::index(Node&)| instead of
+ // |Node::nodeIndex()|.
+ return PositionTemplate<Strategy>(parent, node->NodeIndex());
+ }
+ return position;
+}
+
+Position PreviousPositionOf(const Position& position,
+ PositionMoveType move_type) {
+ return PreviousPositionOfAlgorithm<EditingStrategy>(position, move_type);
+}
+
+PositionInFlatTree PreviousPositionOf(const PositionInFlatTree& position,
+ PositionMoveType move_type) {
+ return PreviousPositionOfAlgorithm<EditingInFlatTreeStrategy>(position,
+ move_type);
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> NextPositionOfAlgorithm(
+ const PositionTemplate<Strategy>& position,
+ PositionMoveType move_type) {
+ // TODO(yosin): We should have printer for PositionMoveType.
+ DCHECK(move_type != PositionMoveType::kBackwardDeletion);
+
+ Node* node = position.AnchorNode();
+ if (!node)
+ return position;
+
+ const int offset = position.ComputeEditingOffset();
+
+ if (Node* child = Strategy::ChildAt(*node, offset)) {
+ return PositionTemplate<Strategy>::FirstPositionInOrBeforeNode(*child);
+ }
+
+ // TODO(yosin) We should use |Strategy::lastOffsetForEditing()| instead of
+ // DOM tree version.
+ if (!Strategy::HasChildren(*node) &&
+ offset < EditingStrategy::LastOffsetForEditing(node)) {
+ // There are two reasons child might be 0:
+ // 1) The node is node like a text node that is not an element, and
+ // therefore has no children. Going forward one character at a time
+ // is correct.
+ // 2) The new offset is a bogus offset like (<br>, 1), and there is no
+ // child. Going from 0 to 1 is correct.
+ switch (move_type) {
+ case PositionMoveType::kCodeUnit:
+ return PositionTemplate<Strategy>::EditingPositionOf(node, offset + 1);
+ case PositionMoveType::kBackwardDeletion:
+ NOTREACHED() << "BackwardDeletion is only available for prevPositionOf "
+ << "functions.";
+ return PositionTemplate<Strategy>::EditingPositionOf(node, offset + 1);
+ case PositionMoveType::kGraphemeCluster:
+ return PositionTemplate<Strategy>::EditingPositionOf(
+ node, NextGraphemeBoundaryOf(*node, offset));
+ default:
+ NOTREACHED() << "Unhandled moveType: " << move_type;
+ }
+ }
+
+ if (ContainerNode* parent = Strategy::Parent(*node))
+ return PositionTemplate<Strategy>::EditingPositionOf(
+ parent, Strategy::Index(*node) + 1);
+ return position;
+}
+
+Position NextPositionOf(const Position& position, PositionMoveType move_type) {
+ return NextPositionOfAlgorithm<EditingStrategy>(position, move_type);
+}
+
+PositionInFlatTree NextPositionOf(const PositionInFlatTree& position,
+ PositionMoveType move_type) {
+ return NextPositionOfAlgorithm<EditingInFlatTreeStrategy>(position,
+ move_type);
+}
+
+bool IsEnclosingBlock(const Node* node) {
+ return node && node->GetLayoutObject() &&
+ !node->GetLayoutObject()->IsInline() &&
+ !node->GetLayoutObject()->IsRubyText();
+}
+
+// TODO(yosin) Deploy this in all of the places where |enclosingBlockFlow()| and
+// |enclosingBlockFlowOrTableElement()| are used.
+// TODO(yosin) Callers of |Node| version of |enclosingBlock()| should use
+// |Position| version The enclosing block of [table, x] for example, should be
+// the block that contains the table and not the table, and this function should
+// be the only one responsible for knowing about these kinds of special cases.
+Element* EnclosingBlock(const Node* node, EditingBoundaryCrossingRule rule) {
+ if (!node)
+ return nullptr;
+ return EnclosingBlock(FirstPositionInOrBeforeNode(*node), rule);
+}
+
+template <typename Strategy>
+Element* EnclosingBlockAlgorithm(const PositionTemplate<Strategy>& position,
+ EditingBoundaryCrossingRule rule) {
+ Node* enclosing_node = EnclosingNodeOfType(position, IsEnclosingBlock, rule);
+ return enclosing_node && enclosing_node->IsElementNode()
+ ? ToElement(enclosing_node)
+ : nullptr;
+}
+
+Element* EnclosingBlock(const Position& position,
+ EditingBoundaryCrossingRule rule) {
+ return EnclosingBlockAlgorithm<EditingStrategy>(position, rule);
+}
+
+Element* EnclosingBlock(const PositionInFlatTree& position,
+ EditingBoundaryCrossingRule rule) {
+ return EnclosingBlockAlgorithm<EditingInFlatTreeStrategy>(position, rule);
+}
+
+Element* EnclosingBlockFlowElement(const Node& node) {
+ if (IsBlockFlowElement(node))
+ return const_cast<Element*>(&ToElement(node));
+
+ for (Node& runner : NodeTraversal::AncestorsOf(node)) {
+ if (IsBlockFlowElement(runner) || IsHTMLBodyElement(runner))
+ return ToElement(&runner);
+ }
+ return nullptr;
+}
+
+EUserSelect UsedValueOfUserSelect(const Node& node) {
+ if (node.IsHTMLElement() && ToHTMLElement(node).IsTextControl())
+ return EUserSelect::kText;
+ if (!node.GetLayoutObject())
+ return EUserSelect::kNone;
+
+ const ComputedStyle* style = node.GetLayoutObject()->Style();
+ if (style->UserModify() != EUserModify::kReadOnly)
+ return EUserSelect::kText;
+
+ return style->UserSelect();
+}
+
+template <typename Strategy>
+TextDirection DirectionOfEnclosingBlockOfAlgorithm(
+ const PositionTemplate<Strategy>& position) {
+ DCHECK(position.IsNotNull());
+ Element* enclosing_block_element =
+ EnclosingBlock(PositionTemplate<Strategy>::FirstPositionInOrBeforeNode(
+ *position.ComputeContainerNode()),
+ kCannotCrossEditingBoundary);
+ if (!enclosing_block_element)
+ return TextDirection::kLtr;
+ LayoutObject* layout_object = enclosing_block_element->GetLayoutObject();
+ return layout_object ? layout_object->Style()->Direction()
+ : TextDirection::kLtr;
+}
+
+TextDirection DirectionOfEnclosingBlockOf(const Position& position) {
+ return DirectionOfEnclosingBlockOfAlgorithm<EditingStrategy>(position);
+}
+
+TextDirection DirectionOfEnclosingBlockOf(const PositionInFlatTree& position) {
+ return DirectionOfEnclosingBlockOfAlgorithm<EditingInFlatTreeStrategy>(
+ position);
+}
+
+TextDirection PrimaryDirectionOf(const Node& node) {
+ TextDirection primary_direction = TextDirection::kLtr;
+ for (const LayoutObject* r = node.GetLayoutObject(); r; r = r->Parent()) {
+ if (r->IsLayoutBlockFlow()) {
+ primary_direction = r->Style()->Direction();
+ break;
+ }
+ }
+
+ return primary_direction;
+}
+
+String StringWithRebalancedWhitespace(const String& string,
+ bool start_is_start_of_paragraph,
+ bool should_emit_nbs_pbefore_end) {
+ unsigned length = string.length();
+
+ StringBuilder rebalanced_string;
+ rebalanced_string.ReserveCapacity(length);
+
+ UChar char_to_append = 0;
+ for (size_t index = 0; index < length; index++) {
+ char_to_append = WhitespaceRebalancingCharToAppend(
+ string, start_is_start_of_paragraph, should_emit_nbs_pbefore_end, index,
+ char_to_append);
+ rebalanced_string.Append(char_to_append);
+ }
+
+ DCHECK_EQ(rebalanced_string.length(), length);
+
+ return rebalanced_string.ToString();
+}
+
+String RepeatString(const String& string, unsigned count) {
+ StringBuilder builder;
+ builder.ReserveCapacity(string.length() * count);
+ for (unsigned counter = 0; counter < count; ++counter)
+ builder.Append(string);
+ return builder.ToString();
+}
+
+template <typename Strategy>
+static Element* TableElementJustBeforeAlgorithm(
+ const VisiblePositionTemplate<Strategy>& visible_position) {
+ const PositionTemplate<Strategy> upstream(
+ MostBackwardCaretPosition(visible_position.DeepEquivalent()));
+ if (IsDisplayInsideTable(upstream.AnchorNode()) &&
+ upstream.AtLastEditingPositionForNode())
+ return ToElement(upstream.AnchorNode());
+
+ return nullptr;
+}
+
+Element* TableElementJustBefore(const VisiblePosition& visible_position) {
+ return TableElementJustBeforeAlgorithm<EditingStrategy>(visible_position);
+}
+
+Element* TableElementJustBefore(
+ const VisiblePositionInFlatTree& visible_position) {
+ return TableElementJustBeforeAlgorithm<EditingInFlatTreeStrategy>(
+ visible_position);
+}
+
+Element* TableElementJustAfter(const VisiblePosition& visible_position) {
+ Position downstream(
+ MostForwardCaretPosition(visible_position.DeepEquivalent()));
+ if (IsDisplayInsideTable(downstream.AnchorNode()) &&
+ downstream.AtFirstEditingPositionForNode())
+ return ToElement(downstream.AnchorNode());
+
+ return nullptr;
+}
+
+// Returns the visible position at the beginning of a node
+VisiblePosition VisiblePositionBeforeNode(const Node& node) {
+ DCHECK(!NeedsLayoutTreeUpdate(node));
+ if (node.hasChildren())
+ return CreateVisiblePosition(FirstPositionInOrBeforeNode(node));
+ DCHECK(node.parentNode()) << node;
+ DCHECK(!node.parentNode()->IsShadowRoot()) << node.parentNode();
+ return VisiblePosition::InParentBeforeNode(node);
+}
+
+// Returns the visible position at the ending of a node
+VisiblePosition VisiblePositionAfterNode(const Node& node) {
+ DCHECK(!NeedsLayoutTreeUpdate(node));
+ if (node.hasChildren())
+ return CreateVisiblePosition(LastPositionInOrAfterNode(node));
+ DCHECK(node.parentNode()) << node.parentNode();
+ DCHECK(!node.parentNode()->IsShadowRoot()) << node.parentNode();
+ return VisiblePosition::InParentAfterNode(node);
+}
+
+bool IsHTMLListElement(const Node* n) {
+ return (n && (IsHTMLUListElement(*n) || IsHTMLOListElement(*n) ||
+ IsHTMLDListElement(*n)));
+}
+
+bool IsListItem(const Node* n) {
+ return n && n->GetLayoutObject() && n->GetLayoutObject()->IsListItem();
+}
+
+bool IsPresentationalHTMLElement(const Node* node) {
+ if (!node->IsHTMLElement())
+ return false;
+
+ const HTMLElement& element = ToHTMLElement(*node);
+ return element.HasTagName(uTag) || element.HasTagName(sTag) ||
+ element.HasTagName(strikeTag) || element.HasTagName(iTag) ||
+ element.HasTagName(emTag) || element.HasTagName(bTag) ||
+ element.HasTagName(strongTag);
+}
+
+Element* AssociatedElementOf(const Position& position) {
+ Node* node = position.AnchorNode();
+ if (!node || node->IsElementNode())
+ return ToElement(node);
+ ContainerNode* parent = NodeTraversal::Parent(*node);
+ return parent && parent->IsElementNode() ? ToElement(parent) : nullptr;
+}
+
+Element* EnclosingElementWithTag(const Position& p,
+ const QualifiedName& tag_name) {
+ if (p.IsNull())
+ return nullptr;
+
+ ContainerNode* root = HighestEditableRoot(p);
+ Element* ancestor = p.AnchorNode()->IsElementNode()
+ ? ToElement(p.AnchorNode())
+ : p.AnchorNode()->parentElement();
+ for (; ancestor; ancestor = ancestor->parentElement()) {
+ if (root && !HasEditableStyle(*ancestor))
+ continue;
+ if (ancestor->HasTagName(tag_name))
+ return ancestor;
+ if (ancestor == root)
+ return nullptr;
+ }
+
+ return nullptr;
+}
+
+template <typename Strategy>
+static Node* EnclosingNodeOfTypeAlgorithm(const PositionTemplate<Strategy>& p,
+ bool (*node_is_of_type)(const Node*),
+ EditingBoundaryCrossingRule rule) {
+ // TODO(yosin) support CanSkipCrossEditingBoundary
+ DCHECK(rule == kCanCrossEditingBoundary ||
+ rule == kCannotCrossEditingBoundary)
+ << rule;
+ if (p.IsNull())
+ return nullptr;
+
+ ContainerNode* const root =
+ rule == kCannotCrossEditingBoundary ? HighestEditableRoot(p) : nullptr;
+ for (Node* n = p.AnchorNode(); n; n = Strategy::Parent(*n)) {
+ // Don't return a non-editable node if the input position was editable,
+ // since the callers from editing will no doubt want to perform editing
+ // inside the returned node.
+ if (root && !HasEditableStyle(*n))
+ continue;
+ if (node_is_of_type(n))
+ return n;
+ if (n == root)
+ return nullptr;
+ }
+
+ return nullptr;
+}
+
+Node* EnclosingNodeOfType(const Position& p,
+ bool (*node_is_of_type)(const Node*),
+ EditingBoundaryCrossingRule rule) {
+ return EnclosingNodeOfTypeAlgorithm<EditingStrategy>(p, node_is_of_type,
+ rule);
+}
+
+Node* EnclosingNodeOfType(const PositionInFlatTree& p,
+ bool (*node_is_of_type)(const Node*),
+ EditingBoundaryCrossingRule rule) {
+ return EnclosingNodeOfTypeAlgorithm<EditingInFlatTreeStrategy>(
+ p, node_is_of_type, rule);
+}
+
+Node* HighestEnclosingNodeOfType(const Position& p,
+ bool (*node_is_of_type)(const Node*),
+ EditingBoundaryCrossingRule rule,
+ Node* stay_within) {
+ Node* highest = nullptr;
+ ContainerNode* root =
+ rule == kCannotCrossEditingBoundary ? HighestEditableRoot(p) : nullptr;
+ for (Node* n = p.ComputeContainerNode(); n && n != stay_within;
+ n = n->parentNode()) {
+ if (root && !HasEditableStyle(*n))
+ continue;
+ if (node_is_of_type(n))
+ highest = n;
+ if (n == root)
+ break;
+ }
+
+ return highest;
+}
+
+Element* EnclosingAnchorElement(const Position& p) {
+ if (p.IsNull())
+ return nullptr;
+
+ for (Element* ancestor =
+ ElementTraversal::FirstAncestorOrSelf(*p.AnchorNode());
+ ancestor; ancestor = ElementTraversal::FirstAncestor(*ancestor)) {
+ if (ancestor->IsLink())
+ return ancestor;
+ }
+ return nullptr;
+}
+
+bool IsDisplayInsideTable(const Node* node) {
+ return node && node->GetLayoutObject() && IsHTMLTableElement(node);
+}
+
+bool IsTableCell(const Node* node) {
+ DCHECK(node);
+ LayoutObject* r = node->GetLayoutObject();
+ return r ? r->IsTableCell() : IsHTMLTableCellElement(*node);
+}
+
+HTMLElement* CreateDefaultParagraphElement(Document& document) {
+ switch (document.GetFrame()->GetEditor().DefaultParagraphSeparator()) {
+ case EditorParagraphSeparator::kIsDiv:
+ return HTMLDivElement::Create(document);
+ case EditorParagraphSeparator::kIsP:
+ return HTMLParagraphElement::Create(document);
+ }
+
+ NOTREACHED();
+ return nullptr;
+}
+
+bool IsTabHTMLSpanElement(const Node* node) {
+ if (!IsHTMLSpanElement(node))
+ return false;
+ const Node* const first_child = NodeTraversal::FirstChild(*node);
+ if (!first_child || !first_child->IsTextNode())
+ return false;
+ if (!ToText(first_child)->data().Contains('\t'))
+ return false;
+ // TODO(editing-dev): Hoist the call of UpdateStyleAndLayoutTree to callers.
+ // See crbug.com/590369 for details.
+ node->GetDocument().UpdateStyleAndLayoutTree();
+ const ComputedStyle* style = node->GetComputedStyle();
+ return style && style->WhiteSpace() == EWhiteSpace::kPre;
+}
+
+bool IsTabHTMLSpanElementTextNode(const Node* node) {
+ return node && node->IsTextNode() && node->parentNode() &&
+ IsTabHTMLSpanElement(node->parentNode());
+}
+
+HTMLSpanElement* TabSpanElement(const Node* node) {
+ return IsTabHTMLSpanElementTextNode(node)
+ ? ToHTMLSpanElement(node->parentNode())
+ : nullptr;
+}
+
+static HTMLSpanElement* CreateTabSpanElement(Document& document,
+ Text* tab_text_node) {
+ // Make the span to hold the tab.
+ HTMLSpanElement* span_element = HTMLSpanElement::Create(document);
+ span_element->setAttribute(styleAttr, "white-space:pre");
+
+ // Add tab text to that span.
+ if (!tab_text_node)
+ tab_text_node = document.CreateEditingTextNode("\t");
+
+ span_element->AppendChild(tab_text_node);
+
+ return span_element;
+}
+
+HTMLSpanElement* CreateTabSpanElement(Document& document,
+ const String& tab_text) {
+ return CreateTabSpanElement(document, document.createTextNode(tab_text));
+}
+
+HTMLSpanElement* CreateTabSpanElement(Document& document) {
+ return CreateTabSpanElement(document, nullptr);
+}
+
+PositionWithAffinity PositionRespectingEditingBoundary(
+ const Position& position,
+ const LayoutPoint& local_point,
+ Node* target_node) {
+ if (!target_node->GetLayoutObject())
+ return PositionWithAffinity();
+
+ LayoutPoint selection_end_point = local_point;
+ Element* editable_element = RootEditableElementOf(position);
+
+ if (editable_element && !editable_element->contains(target_node)) {
+ if (!editable_element->GetLayoutObject())
+ return PositionWithAffinity();
+
+ FloatPoint absolute_point = target_node->GetLayoutObject()->LocalToAbsolute(
+ FloatPoint(selection_end_point));
+ selection_end_point = LayoutPoint(
+ editable_element->GetLayoutObject()->AbsoluteToLocal(absolute_point));
+ target_node = editable_element;
+ }
+
+ return target_node->GetLayoutObject()->PositionForPoint(selection_end_point);
+}
+
+Position ComputePositionForNodeRemoval(const Position& position,
+ const Node& node) {
+ if (position.IsNull())
+ return position;
+ switch (position.AnchorType()) {
+ case PositionAnchorType::kBeforeChildren:
+ if (!node.IsShadowIncludingInclusiveAncestorOf(
+ position.ComputeContainerNode())) {
+ return position;
+ }
+ return Position::InParentBeforeNode(node);
+ case PositionAnchorType::kAfterChildren:
+ if (!node.IsShadowIncludingInclusiveAncestorOf(
+ position.ComputeContainerNode())) {
+ return position;
+ }
+ return Position::InParentAfterNode(node);
+ case PositionAnchorType::kOffsetInAnchor:
+ if (position.ComputeContainerNode() == node.parentNode() &&
+ static_cast<unsigned>(position.OffsetInContainerNode()) >
+ node.NodeIndex()) {
+ return Position(position.ComputeContainerNode(),
+ position.OffsetInContainerNode() - 1);
+ }
+ if (!node.IsShadowIncludingInclusiveAncestorOf(
+ position.ComputeContainerNode())) {
+ return position;
+ }
+ return Position::InParentBeforeNode(node);
+ case PositionAnchorType::kAfterAnchor:
+ if (!node.IsShadowIncludingInclusiveAncestorOf(position.AnchorNode()))
+ return position;
+ return Position::InParentAfterNode(node);
+ case PositionAnchorType::kBeforeAnchor:
+ if (!node.IsShadowIncludingInclusiveAncestorOf(position.AnchorNode()))
+ return position;
+ return Position::InParentBeforeNode(node);
+ }
+ NOTREACHED() << "We should handle all PositionAnchorType";
+ return position;
+}
+
+bool IsMailHTMLBlockquoteElement(const Node* node) {
+ if (!node || !node->IsHTMLElement())
+ return false;
+
+ const HTMLElement& element = ToHTMLElement(*node);
+ return element.HasTagName(blockquoteTag) &&
+ element.getAttribute("type") == "cite";
+}
+
+bool ElementCannotHaveEndTag(const Node& node) {
+ if (!node.IsHTMLElement())
+ return false;
+
+ return !ToHTMLElement(node).ShouldSerializeEndTag();
+}
+
+// FIXME: indexForVisiblePosition and visiblePositionForIndex use TextIterators
+// to convert between VisiblePositions and indices. But TextIterator iteration
+// using TextIteratorEmitsCharactersBetweenAllVisiblePositions does not exactly
+// match VisiblePosition iteration, so using them to preserve a selection during
+// an editing opertion is unreliable. TextIterator's
+// TextIteratorEmitsCharactersBetweenAllVisiblePositions mode needs to be fixed,
+// or these functions need to be changed to iterate using actual
+// VisiblePositions.
+// FIXME: Deploy these functions everywhere that TextIterators are used to
+// convert between VisiblePositions and indices.
+int IndexForVisiblePosition(const VisiblePosition& visible_position,
+ ContainerNode*& scope) {
+ if (visible_position.IsNull())
+ return 0;
+
+ Position p(visible_position.DeepEquivalent());
+ Document& document = *p.GetDocument();
+ DCHECK(!document.NeedsLayoutTreeUpdate());
+
+ ShadowRoot* shadow_root = p.AnchorNode()->ContainingShadowRoot();
+
+ if (shadow_root)
+ scope = shadow_root;
+ else
+ scope = document.documentElement();
+
+ EphemeralRange range(Position::FirstPositionInNode(*scope),
+ p.ParentAnchoredEquivalent());
+
+ const TextIteratorBehavior& behavior =
+ TextIteratorBehavior::Builder(
+ TextIteratorBehavior::AllVisiblePositionsRangeLengthBehavior())
+ .SetSuppressesExtraNewlineEmission(true)
+ .Build();
+ return TextIterator::RangeLength(range.StartPosition(), range.EndPosition(),
+ behavior);
+}
+
+EphemeralRange MakeRange(const VisiblePosition& start,
+ const VisiblePosition& end) {
+ if (start.IsNull() || end.IsNull())
+ return EphemeralRange();
+
+ Position s = start.DeepEquivalent().ParentAnchoredEquivalent();
+ Position e = end.DeepEquivalent().ParentAnchoredEquivalent();
+ if (s.IsNull() || e.IsNull())
+ return EphemeralRange();
+
+ return EphemeralRange(s, e);
+}
+
+template <typename Strategy>
+static EphemeralRangeTemplate<Strategy> NormalizeRangeAlgorithm(
+ const EphemeralRangeTemplate<Strategy>& range) {
+ DCHECK(range.IsNotNull());
+ DCHECK(!range.GetDocument().NeedsLayoutTreeUpdate());
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ range.GetDocument().Lifecycle());
+
+ // TODO(yosin) We should not call |parentAnchoredEquivalent()|, it is
+ // redundant.
+ const PositionTemplate<Strategy> normalized_start =
+ MostForwardCaretPosition(range.StartPosition())
+ .ParentAnchoredEquivalent();
+ const PositionTemplate<Strategy> normalized_end =
+ MostBackwardCaretPosition(range.EndPosition()).ParentAnchoredEquivalent();
+ // The order of the positions of |start| and |end| can be swapped after
+ // upstream/downstream. e.g. editing/pasteboard/copy-display-none.html
+ if (normalized_start.CompareTo(normalized_end) > 0)
+ return EphemeralRangeTemplate<Strategy>(normalized_end, normalized_start);
+ return EphemeralRangeTemplate<Strategy>(normalized_start, normalized_end);
+}
+
+EphemeralRange NormalizeRange(const EphemeralRange& range) {
+ return NormalizeRangeAlgorithm<EditingStrategy>(range);
+}
+
+EphemeralRangeInFlatTree NormalizeRange(const EphemeralRangeInFlatTree& range) {
+ return NormalizeRangeAlgorithm<EditingInFlatTreeStrategy>(range);
+}
+
+VisiblePosition VisiblePositionForIndex(int index, ContainerNode* scope) {
+ if (!scope)
+ return VisiblePosition();
+ DCHECK(!scope->GetDocument().NeedsLayoutTreeUpdate());
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ scope->GetDocument().Lifecycle());
+
+ EphemeralRange range =
+ PlainTextRange(index).CreateRangeForSelectionIndexing(*scope);
+ // Check for an invalid index. Certain editing operations invalidate indices
+ // because of problems with
+ // TextIteratorEmitsCharactersBetweenAllVisiblePositions.
+ if (range.IsNull())
+ return VisiblePosition();
+ return CreateVisiblePosition(range.StartPosition());
+}
+
+bool IsRenderedAsNonInlineTableImageOrHR(const Node* node) {
+ if (!node)
+ return false;
+ LayoutObject* layout_object = node->GetLayoutObject();
+ return layout_object &&
+ ((layout_object->IsTable() && !layout_object->IsInline()) ||
+ (layout_object->IsImage() && !layout_object->IsInline()) ||
+ layout_object->IsHR());
+}
+
+bool IsNonTableCellHTMLBlockElement(const Node* node) {
+ if (!node->IsHTMLElement())
+ return false;
+
+ const HTMLElement& element = ToHTMLElement(*node);
+ return element.HasTagName(listingTag) || element.HasTagName(olTag) ||
+ element.HasTagName(preTag) || element.HasTagName(tableTag) ||
+ element.HasTagName(ulTag) || element.HasTagName(xmpTag) ||
+ element.HasTagName(h1Tag) || element.HasTagName(h2Tag) ||
+ element.HasTagName(h3Tag) || element.HasTagName(h4Tag) ||
+ element.HasTagName(h5Tag);
+}
+
+bool IsBlockFlowElement(const Node& node) {
+ LayoutObject* layout_object = node.GetLayoutObject();
+ return node.IsElementNode() && layout_object &&
+ layout_object->IsLayoutBlockFlow();
+}
+
+bool IsInPasswordField(const Position& position) {
+ TextControlElement* text_control = EnclosingTextControl(position);
+ return IsHTMLInputElement(text_control) &&
+ ToHTMLInputElement(text_control)->type() == InputTypeNames::password;
+}
+
+// If current position is at grapheme boundary, return 0; otherwise, return the
+// distance to its nearest left grapheme boundary.
+size_t ComputeDistanceToLeftGraphemeBoundary(const Position& position) {
+ const Position& adjusted_position = PreviousPositionOf(
+ NextPositionOf(position, PositionMoveType::kGraphemeCluster),
+ PositionMoveType::kGraphemeCluster);
+ DCHECK_EQ(position.AnchorNode(), adjusted_position.AnchorNode());
+ DCHECK_GE(position.ComputeOffsetInContainerNode(),
+ adjusted_position.ComputeOffsetInContainerNode());
+ return static_cast<size_t>(position.ComputeOffsetInContainerNode() -
+ adjusted_position.ComputeOffsetInContainerNode());
+}
+
+// If current position is at grapheme boundary, return 0; otherwise, return the
+// distance to its nearest right grapheme boundary.
+size_t ComputeDistanceToRightGraphemeBoundary(const Position& position) {
+ const Position& adjusted_position = NextPositionOf(
+ PreviousPositionOf(position, PositionMoveType::kGraphemeCluster),
+ PositionMoveType::kGraphemeCluster);
+ DCHECK_EQ(position.AnchorNode(), adjusted_position.AnchorNode());
+ DCHECK_GE(adjusted_position.ComputeOffsetInContainerNode(),
+ position.ComputeOffsetInContainerNode());
+ return static_cast<size_t>(adjusted_position.ComputeOffsetInContainerNode() -
+ position.ComputeOffsetInContainerNode());
+}
+
+FloatQuad LocalToAbsoluteQuadOf(const LocalCaretRect& caret_rect) {
+ return caret_rect.layout_object->LocalToAbsoluteQuad(
+ FloatRect(caret_rect.rect));
+}
+
+const StaticRangeVector* TargetRangesForInputEvent(const Node& node) {
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ node.GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (!HasRichlyEditableStyle(node))
+ return nullptr;
+ const EphemeralRange& range =
+ FirstEphemeralRangeOf(node.GetDocument()
+ .GetFrame()
+ ->Selection()
+ .ComputeVisibleSelectionInDOMTree());
+ if (range.IsNull())
+ return nullptr;
+ return new StaticRangeVector(1, StaticRange::Create(range));
+}
+
+DispatchEventResult DispatchBeforeInputInsertText(
+ Node* target,
+ const String& data,
+ InputEvent::InputType input_type,
+ const StaticRangeVector* ranges) {
+ if (!target)
+ return DispatchEventResult::kNotCanceled;
+ // TODO(chongz): Pass appropriate |ranges| after it's defined on spec.
+ // http://w3c.github.io/editing/input-events.html#dom-inputevent-inputtype
+ InputEvent* before_input_event = InputEvent::CreateBeforeInput(
+ input_type, data, InputTypeIsCancelable(input_type),
+ InputEvent::EventIsComposing::kNotComposing,
+ ranges ? ranges : TargetRangesForInputEvent(*target));
+ return target->DispatchEvent(before_input_event);
+}
+
+DispatchEventResult DispatchBeforeInputEditorCommand(
+ Node* target,
+ InputEvent::InputType input_type,
+ const StaticRangeVector* ranges) {
+ if (!target)
+ return DispatchEventResult::kNotCanceled;
+ InputEvent* before_input_event = InputEvent::CreateBeforeInput(
+ input_type, g_null_atom, InputTypeIsCancelable(input_type),
+ InputEvent::EventIsComposing::kNotComposing, ranges);
+ return target->DispatchEvent(before_input_event);
+}
+
+DispatchEventResult DispatchBeforeInputDataTransfer(
+ Node* target,
+ InputEvent::InputType input_type,
+ DataTransfer* data_transfer) {
+ if (!target)
+ return DispatchEventResult::kNotCanceled;
+
+ DCHECK(input_type == InputEvent::InputType::kInsertFromPaste ||
+ input_type == InputEvent::InputType::kInsertReplacementText ||
+ input_type == InputEvent::InputType::kInsertFromDrop ||
+ input_type == InputEvent::InputType::kDeleteByCut)
+ << "Unsupported inputType: " << (int)input_type;
+
+ InputEvent* before_input_event;
+
+ if (HasRichlyEditableStyle(*(target->ToNode())) || !data_transfer) {
+ before_input_event = InputEvent::CreateBeforeInput(
+ input_type, data_transfer, InputTypeIsCancelable(input_type),
+ InputEvent::EventIsComposing::kNotComposing,
+ TargetRangesForInputEvent(*target));
+ } else {
+ const String& data = data_transfer->getData(kMimeTypeTextPlain);
+ // TODO(chongz): Pass appropriate |ranges| after it's defined on spec.
+ // http://w3c.github.io/editing/input-events.html#dom-inputevent-inputtype
+ before_input_event = InputEvent::CreateBeforeInput(
+ input_type, data, InputTypeIsCancelable(input_type),
+ InputEvent::EventIsComposing::kNotComposing,
+ TargetRangesForInputEvent(*target));
+ }
+ return target->DispatchEvent(before_input_event);
+}
+
+// |IsEmptyNonEditableNodeInEditable()| is introduced for fixing
+// http://crbug.com/428986.
+static bool IsEmptyNonEditableNodeInEditable(const Node& node) {
+ // Editability is defined the DOM tree rather than the flat tree. For example:
+ // DOM:
+ // <host>
+ // <span>unedittable</span>
+ // <shadowroot><div ce><content /></div></shadowroot>
+ // </host>
+ //
+ // Flat Tree:
+ // <host><div ce><span1>unedittable</span></div></host>
+ // e.g. editing/shadow/breaking-editing-boundaries.html
+ return !NodeTraversal::HasChildren(node) && !HasEditableStyle(node) &&
+ node.parentNode() && HasEditableStyle(*node.parentNode());
+}
+
+// TODO(yosin): We should not use |IsEmptyNonEditableNodeInEditable()| in
+// |EditingIgnoresContent()| since |IsEmptyNonEditableNodeInEditable()|
+// requires clean layout tree.
+bool EditingIgnoresContent(const Node& node) {
+ return !node.CanContainRangeEndPoint() ||
+ IsEmptyNonEditableNodeInEditable(node);
+}
+
+ContainerNode* RootEditableElementOrTreeScopeRootNodeOf(
+ const Position& position) {
+ Element* const selection_root = RootEditableElementOf(position);
+ if (selection_root)
+ return selection_root;
+
+ Node* const node = position.ComputeContainerNode();
+ return node ? &node->GetTreeScope().RootNode() : nullptr;
+}
+
+static scoped_refptr<Image> ImageFromNode(const Node& node) {
+ DCHECK(!node.GetDocument().NeedsLayoutTreeUpdate());
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ node.GetDocument().Lifecycle());
+
+ const LayoutObject* const layout_object = node.GetLayoutObject();
+ if (!layout_object)
+ return nullptr;
+
+ if (layout_object->IsCanvas()) {
+ return ToHTMLCanvasElement(const_cast<Node&>(node))
+ .CopiedImage(kFrontBuffer, kPreferNoAcceleration);
+ }
+
+ if (!layout_object->IsImage())
+ return nullptr;
+
+ const LayoutImage& layout_image = ToLayoutImage(*layout_object);
+ const ImageResourceContent* const cached_image = layout_image.CachedImage();
+ if (!cached_image || cached_image->ErrorOccurred())
+ return nullptr;
+ return cached_image->GetImage();
+}
+
+AtomicString GetUrlStringFromNode(const Node& node) {
+ // TODO(editing-dev): This should probably be reconciled with
+ // HitTestResult::absoluteImageURL.
+ if (IsHTMLImageElement(node) || IsHTMLInputElement(node))
+ return ToHTMLElement(node).getAttribute(srcAttr);
+ if (IsSVGImageElement(node))
+ return ToSVGElement(node).ImageSourceURL();
+ if (IsHTMLEmbedElement(node) || IsHTMLObjectElement(node) ||
+ IsHTMLCanvasElement(node))
+ return ToHTMLElement(node).ImageSourceURL();
+ return AtomicString();
+}
+
+void WriteImageNodeToPasteboard(Pasteboard* pasteboard,
+ const Node& node,
+ const String& title) {
+ const scoped_refptr<Image> image = ImageFromNode(node);
+ if (!image.get())
+ return;
+ const KURL url_string = node.GetDocument().CompleteURL(
+ StripLeadingAndTrailingHTMLSpaces(GetUrlStringFromNode(node)));
+ pasteboard->WriteImage(image.get(), url_string, title);
+}
+
+Element* FindEventTargetFrom(LocalFrame& frame,
+ const VisibleSelection& selection) {
+ Element* const target = AssociatedElementOf(selection.Start());
+ if (!target)
+ return frame.GetDocument()->body();
+ if (target->IsInUserAgentShadowRoot())
+ return target->OwnerShadowHost();
+ return target;
+}
+
+HTMLImageElement* ImageElementFromImageDocument(const Document* document) {
+ if (!document)
+ return nullptr;
+ if (!document->IsImageDocument())
+ return nullptr;
+
+ const HTMLElement* const body = document->body();
+ if (!body)
+ return nullptr;
+
+ return ToHTMLImageElementOrNull(body->firstChild());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/editing_utilities.h b/chromium/third_party/blink/renderer/core/editing/editing_utilities.h
new file mode 100644
index 00000000000..cd9c07b7882
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editing_utilities.h
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2004, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_UTILITIES_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITING_UTILITIES_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/editing_boundary.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/events/input_event.h"
+#include "third_party/blink/renderer/platform/geometry/float_quad.h"
+#include "third_party/blink/renderer/platform/text/text_direction.h"
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+#include "third_party/blink/renderer/platform/wtf/text/character_names.h"
+
+namespace blink {
+
+enum class PositionMoveType {
+ // Move by a single code unit. |PositionMoveType::CodeUnit| is used for
+ // implementing other |PositionMoveType|. You should not use this.
+ kCodeUnit,
+ // Move to the next Unicode code point. At most two code unit when we are
+ // at surrogate pair. Please consider using |GraphemeCluster|.
+ kBackwardDeletion,
+ // Move by a grapheme cluster for user-perceived character in Unicode
+ // Standard Annex #29, Unicode text segmentation[1].
+ // [1] http://www.unicode.org/reports/tr29/
+ kGraphemeCluster,
+};
+
+class Document;
+class Element;
+class HTMLElement;
+class HTMLSpanElement;
+struct LocalCaretRect;
+class Node;
+class Pasteboard;
+
+// This file contains a set of helper functions used by the editing commands
+
+bool NeedsLayoutTreeUpdate(const Node&);
+CORE_EXPORT bool NeedsLayoutTreeUpdate(const Position&);
+CORE_EXPORT bool NeedsLayoutTreeUpdate(const PositionInFlatTree&);
+
+// -------------------------------------------------------------------------
+// Node
+// -------------------------------------------------------------------------
+
+// Returns true if |node| has "user-select:contain".
+bool IsUserSelectContain(const Node& /* node */);
+
+CORE_EXPORT bool HasEditableStyle(const Node&);
+CORE_EXPORT bool HasRichlyEditableStyle(const Node&);
+CORE_EXPORT bool IsRootEditableElement(const Node&);
+CORE_EXPORT Element* RootEditableElement(const Node&);
+Element* RootEditableElementOf(const Position&);
+Element* RootEditableElementOf(const PositionInFlatTree&);
+ContainerNode* RootEditableElementOrTreeScopeRootNodeOf(const Position&);
+// highestEditableRoot returns the highest editable node. If the
+// rootEditableElement of the speicified Position is <body>, this returns the
+// <body>. Otherwise, this searches ancestors for the highest editable node in
+// defiance of editing boundaries. This returns a Document if designMode="on"
+// and the specified Position is not in the <body>.
+CORE_EXPORT ContainerNode* HighestEditableRoot(
+ const Position&,
+ Element* (*)(const Position&) = RootEditableElementOf,
+ bool (*)(const Node&) = HasEditableStyle);
+ContainerNode* HighestEditableRoot(const PositionInFlatTree&);
+
+Node* HighestEnclosingNodeOfType(
+ const Position&,
+ bool (*node_is_of_type)(const Node*),
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary,
+ Node* stay_within = nullptr);
+
+Element* EnclosingBlock(
+ const Node*,
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary);
+CORE_EXPORT Element* EnclosingBlock(const Position&,
+ EditingBoundaryCrossingRule);
+CORE_EXPORT Element* EnclosingBlock(const PositionInFlatTree&,
+ EditingBoundaryCrossingRule);
+Element* EnclosingBlockFlowElement(
+ const Node&); // Deprecated, use enclosingBlock instead.
+Element* AssociatedElementOf(const Position&);
+Element* EnclosingAnchorElement(const Position&);
+// Returns the lowest ancestor with the specified QualifiedName. If the
+// specified Position is editable, this function returns an editable
+// Element. Otherwise, editability doesn't matter.
+Element* EnclosingElementWithTag(const Position&, const QualifiedName&);
+CORE_EXPORT Node* EnclosingNodeOfType(
+ const Position&,
+ bool (*node_is_of_type)(const Node*),
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary);
+CORE_EXPORT Node* EnclosingNodeOfType(
+ const PositionInFlatTree&,
+ bool (*node_is_of_type)(const Node*),
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary);
+
+HTMLSpanElement* TabSpanElement(const Node*);
+Element* TableElementJustAfter(const VisiblePosition&);
+CORE_EXPORT Element* TableElementJustBefore(const VisiblePosition&);
+CORE_EXPORT Element* TableElementJustBefore(const VisiblePositionInFlatTree&);
+
+template <typename Strategy>
+ContainerNode* ParentCrossingShadowBoundaries(const Node&);
+template <>
+inline ContainerNode* ParentCrossingShadowBoundaries<EditingStrategy>(
+ const Node& node) {
+ return NodeTraversal::ParentOrShadowHostNode(node);
+}
+template <>
+inline ContainerNode* ParentCrossingShadowBoundaries<EditingInFlatTreeStrategy>(
+ const Node& node) {
+ return FlatTreeTraversal::Parent(node);
+}
+
+void WriteImageNodeToPasteboard(Pasteboard*, const Node&, const String&);
+
+// boolean functions on Node
+
+// FIXME: editingIgnoresContent, canHaveChildrenForEditing, and isAtomicNode
+// should be renamed to reflect its usage.
+
+// Returns true for nodes that either have no content, or have content that is
+// ignored (skipped over) while editing. There are no VisiblePositions inside
+// these nodes.
+bool EditingIgnoresContent(const Node&);
+
+inline bool CanHaveChildrenForEditing(const Node* node) {
+ return !node->IsTextNode() && node->CanContainRangeEndPoint();
+}
+
+bool IsAtomicNode(const Node*);
+CORE_EXPORT bool IsEnclosingBlock(const Node*);
+CORE_EXPORT bool IsTabHTMLSpanElement(const Node*);
+bool IsTabHTMLSpanElementTextNode(const Node*);
+bool IsMailHTMLBlockquoteElement(const Node*);
+// Returns true if the specified node is visible <table>. We don't want to add
+// invalid nodes to <table> elements.
+bool IsDisplayInsideTable(const Node*);
+bool IsTableCell(const Node*);
+bool IsHTMLListElement(const Node*);
+bool IsListItem(const Node*);
+bool IsPresentationalHTMLElement(const Node*);
+bool IsRenderedAsNonInlineTableImageOrHR(const Node*);
+bool IsNonTableCellHTMLBlockElement(const Node*);
+bool IsBlockFlowElement(const Node&);
+EUserSelect UsedValueOfUserSelect(const Node&);
+bool IsInPasswordField(const Position&);
+CORE_EXPORT TextDirection DirectionOfEnclosingBlockOf(const Position&);
+CORE_EXPORT TextDirection
+DirectionOfEnclosingBlockOf(const PositionInFlatTree&);
+CORE_EXPORT TextDirection PrimaryDirectionOf(const Node&);
+
+// -------------------------------------------------------------------------
+// Position
+// -------------------------------------------------------------------------
+
+// Functions returning Position
+
+Position NextCandidate(const Position&);
+PositionInFlatTree NextCandidate(const PositionInFlatTree&);
+Position PreviousCandidate(const Position&);
+PositionInFlatTree PreviousCandidate(const PositionInFlatTree&);
+
+CORE_EXPORT Position NextVisuallyDistinctCandidate(const Position&);
+CORE_EXPORT PositionInFlatTree
+NextVisuallyDistinctCandidate(const PositionInFlatTree&);
+Position PreviousVisuallyDistinctCandidate(const Position&);
+PositionInFlatTree PreviousVisuallyDistinctCandidate(const PositionInFlatTree&);
+
+// This is a |const Node&| versions of two deprecated functions above.
+inline Position FirstPositionInOrBeforeNode(const Node& node) {
+ return Position::FirstPositionInOrBeforeNode(node);
+}
+
+inline Position LastPositionInOrAfterNode(const Node& node) {
+ return Position::LastPositionInOrAfterNode(node);
+}
+
+CORE_EXPORT Position FirstEditablePositionAfterPositionInRoot(const Position&,
+ const Node&);
+CORE_EXPORT Position LastEditablePositionBeforePositionInRoot(const Position&,
+ const Node&);
+CORE_EXPORT PositionInFlatTree
+FirstEditablePositionAfterPositionInRoot(const PositionInFlatTree&,
+ const Node&);
+CORE_EXPORT PositionInFlatTree
+LastEditablePositionBeforePositionInRoot(const PositionInFlatTree&,
+ const Node&);
+
+// Move up or down the DOM by one position.
+// Offsets are computed using layout text for nodes that have layoutObjects -
+// but note that even when using composed characters, the result may be inside
+// a single user-visible character if a ligature is formed.
+CORE_EXPORT Position PreviousPositionOf(const Position&, PositionMoveType);
+CORE_EXPORT Position NextPositionOf(const Position&, PositionMoveType);
+CORE_EXPORT PositionInFlatTree PreviousPositionOf(const PositionInFlatTree&,
+ PositionMoveType);
+CORE_EXPORT PositionInFlatTree NextPositionOf(const PositionInFlatTree&,
+ PositionMoveType);
+
+CORE_EXPORT int PreviousGraphemeBoundaryOf(const Node&, int current);
+CORE_EXPORT int NextGraphemeBoundaryOf(const Node&, int current);
+
+// comparision functions on Position
+
+// |disconnected| is optional output parameter having true if specified
+// positions don't have common ancestor.
+int ComparePositionsInDOMTree(const Node* container_a,
+ int offset_a,
+ const Node* container_b,
+ int offset_b,
+ bool* disconnected = nullptr);
+int ComparePositionsInFlatTree(const Node* container_a,
+ int offset_a,
+ const Node* container_b,
+ int offset_b,
+ bool* disconnected = nullptr);
+// TODO(yosin): We replace |comparePositions()| by |Position::opeator<()| to
+// utilize |DCHECK_XX()|.
+int ComparePositions(const Position&, const Position&);
+int ComparePositions(const PositionWithAffinity&, const PositionWithAffinity&);
+bool IsNodeFullyContained(const EphemeralRange&, const Node&);
+
+// boolean functions on Position
+
+// FIXME: Both isEditablePosition and isRichlyEditablePosition rely on
+// up-to-date style to give proper results. They shouldn't update style by
+// default, but should make it clear that that is the contract.
+CORE_EXPORT bool IsEditablePosition(const Position&);
+bool IsEditablePosition(const PositionInFlatTree&);
+bool IsRichlyEditablePosition(const Position&);
+
+PositionWithAffinity PositionRespectingEditingBoundary(
+ const Position&,
+ const LayoutPoint& local_point,
+ Node* target_node);
+Position ComputePositionForNodeRemoval(const Position&, const Node&);
+
+// -------------------------------------------------------------------------
+// VisiblePosition
+// -------------------------------------------------------------------------
+
+// Functions returning VisiblePosition
+
+// TODO(yosin) We should rename
+// |firstEditableVisiblePositionAfterPositionInRoot()| to a better name which
+// describes what this function returns, since it returns a position before
+// specified position due by canonicalization.
+CORE_EXPORT VisiblePosition
+FirstEditableVisiblePositionAfterPositionInRoot(const Position&,
+ ContainerNode&);
+CORE_EXPORT VisiblePositionInFlatTree
+FirstEditableVisiblePositionAfterPositionInRoot(const PositionInFlatTree&,
+ ContainerNode&);
+// TODO(yosin) We should rename
+// |lastEditableVisiblePositionBeforePositionInRoot()| to a better name which
+// describes what this function returns, since it returns a position after
+// specified position due by canonicalization.
+CORE_EXPORT VisiblePosition
+LastEditableVisiblePositionBeforePositionInRoot(const Position&,
+ ContainerNode&);
+CORE_EXPORT VisiblePositionInFlatTree
+LastEditableVisiblePositionBeforePositionInRoot(const PositionInFlatTree&,
+ ContainerNode&);
+CORE_EXPORT VisiblePosition VisiblePositionBeforeNode(const Node&);
+VisiblePosition VisiblePositionAfterNode(const Node&);
+
+int ComparePositions(const VisiblePosition&, const VisiblePosition&);
+
+CORE_EXPORT int IndexForVisiblePosition(const VisiblePosition&,
+ ContainerNode*& scope);
+EphemeralRange MakeRange(const VisiblePosition&, const VisiblePosition&);
+EphemeralRange NormalizeRange(const EphemeralRange&);
+EphemeralRangeInFlatTree NormalizeRange(const EphemeralRangeInFlatTree&);
+CORE_EXPORT VisiblePosition VisiblePositionForIndex(int index,
+ ContainerNode* scope);
+
+// -------------------------------------------------------------------------
+// HTMLElement
+// -------------------------------------------------------------------------
+
+// Functions returning HTMLElement
+
+HTMLElement* CreateDefaultParagraphElement(Document&);
+
+// -------------------------------------------------------------------------
+// Element
+// -------------------------------------------------------------------------
+
+// Functions returning Element
+
+HTMLSpanElement* CreateTabSpanElement(Document&);
+HTMLSpanElement* CreateTabSpanElement(Document&, const String& tab_text);
+
+Element* FindEventTargetFrom(LocalFrame&, const VisibleSelection&);
+
+// Note: ImageElementFromImageDocument() is both used in ExecuteCopy() and
+// Editor::CanCopy()
+HTMLImageElement* ImageElementFromImageDocument(const Document*);
+
+// Boolean functions on Element
+
+CORE_EXPORT bool ElementCannotHaveEndTag(const Node&);
+
+// -------------------------------------------------------------------------
+// VisibleSelection
+// -------------------------------------------------------------------------
+
+// Miscellaneous functions on Text
+inline bool IsWhitespace(UChar c) {
+ return c == kNoBreakSpaceCharacter || c == ' ' || c == '\n' || c == '\t';
+}
+
+// FIXME: Can't really answer this question correctly without knowing the
+// white-space mode.
+inline bool IsCollapsibleWhitespace(UChar c) {
+ return c == ' ' || c == '\n';
+}
+
+String StringWithRebalancedWhitespace(const String&,
+ bool start_is_start_of_paragraph,
+ bool should_emit_nbs_pbefore_end);
+
+CORE_EXPORT String RepeatString(const String&, unsigned);
+
+// -------------------------------------------------------------------------
+// Distance calculation functions
+// -------------------------------------------------------------------------
+
+// If current position is at grapheme boundary, return 0; otherwise, return the
+// distance to its nearest left grapheme boundary.
+size_t ComputeDistanceToLeftGraphemeBoundary(const Position&);
+
+// If current position is at grapheme boundary, return 0; otherwise, return the
+// distance to its nearest right grapheme boundary.
+size_t ComputeDistanceToRightGraphemeBoundary(const Position&);
+
+// -------------------------------------------------------------------------
+// LocalCaretRect conversions
+// -------------------------------------------------------------------------
+
+FloatQuad LocalToAbsoluteQuadOf(const LocalCaretRect&);
+
+// -------------------------------------------------------------------------
+// Events
+// -------------------------------------------------------------------------
+
+// Functions dispatch InputEvent
+const StaticRangeVector* TargetRangesForInputEvent(const Node&);
+DispatchEventResult DispatchBeforeInputInsertText(
+ Node*,
+ const String& data,
+ InputEvent::InputType = InputEvent::InputType::kInsertText,
+ const StaticRangeVector* = nullptr);
+DispatchEventResult DispatchBeforeInputEditorCommand(Node*,
+ InputEvent::InputType,
+ const StaticRangeVector*);
+DispatchEventResult DispatchBeforeInputDataTransfer(Node*,
+ InputEvent::InputType,
+ DataTransfer*);
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/editing_utilities_test.cc b/chromium/third_party/blink/renderer/core/editing/editing_utilities_test.cc
new file mode 100644
index 00000000000..017ce30913d
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editing_utilities_test.cc
@@ -0,0 +1,892 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+
+#include "third_party/blink/renderer/core/dom/static_node_list.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+
+namespace blink {
+
+class EditingUtilitiesTest : public EditingTestBase {};
+
+TEST_F(EditingUtilitiesTest, DirectionOfEnclosingBlockOf) {
+ const char* body_content =
+ "<p id='host'><b id='one'></b><b id='two'>22</b></p>";
+ const char* shadow_content =
+ "<content select=#two></content><p dir=rtl><content "
+ "select=#one></content><p>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+ Node* one = GetDocument().getElementById("one");
+
+ EXPECT_EQ(TextDirection::kLtr, DirectionOfEnclosingBlockOf(Position(one, 0)));
+ EXPECT_EQ(TextDirection::kRtl,
+ DirectionOfEnclosingBlockOf(PositionInFlatTree(one, 0)));
+}
+
+TEST_F(EditingUtilitiesTest, firstEditablePositionAfterPositionInRoot) {
+ const char* body_content =
+ "<p id='host' contenteditable><b id='one'>1</b><b id='two'>22</b></p>";
+ const char* shadow_content =
+ "<content select=#two></content><content select=#one></content><b "
+ "id='three'>333</b>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+ Element* host = GetDocument().getElementById("host");
+ Node* one = GetDocument().getElementById("one");
+ Node* two = GetDocument().getElementById("two");
+ Node* three = shadow_root->getElementById("three");
+
+ EXPECT_EQ(Position(one, 0),
+ FirstEditablePositionAfterPositionInRoot(Position(one, 0), *host));
+ EXPECT_EQ(
+ Position(one->firstChild(), 0),
+ FirstEditableVisiblePositionAfterPositionInRoot(Position(one, 0), *host)
+ .DeepEquivalent());
+
+ EXPECT_EQ(PositionInFlatTree(one, 0),
+ FirstEditablePositionAfterPositionInRoot(PositionInFlatTree(one, 0),
+ *host));
+ EXPECT_EQ(PositionInFlatTree(two->firstChild(), 2),
+ FirstEditableVisiblePositionAfterPositionInRoot(
+ PositionInFlatTree(one, 0), *host)
+ .DeepEquivalent());
+
+ EXPECT_EQ(
+ Position::FirstPositionInNode(*host),
+ FirstEditablePositionAfterPositionInRoot(Position(three, 0), *host));
+ EXPECT_EQ(
+ Position(one->firstChild(), 0),
+ FirstEditableVisiblePositionAfterPositionInRoot(Position(three, 0), *host)
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree::AfterNode(*host),
+ FirstEditablePositionAfterPositionInRoot(
+ PositionInFlatTree(three, 0), *host));
+ EXPECT_EQ(PositionInFlatTree::LastPositionInNode(*host),
+ FirstEditableVisiblePositionAfterPositionInRoot(
+ PositionInFlatTree(three, 0), *host)
+ .DeepEquivalent());
+}
+
+TEST_F(EditingUtilitiesTest, enclosingBlock) {
+ const char* body_content = "<p id='host'><b id='one'>11</b></p>";
+ const char* shadow_content =
+ "<content select=#two></content><div id='three'><content "
+ "select=#one></content></div>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+ Node* host = GetDocument().getElementById("host");
+ Node* one = GetDocument().getElementById("one");
+ Node* three = shadow_root->getElementById("three");
+
+ EXPECT_EQ(host,
+ EnclosingBlock(Position(one, 0), kCannotCrossEditingBoundary));
+ EXPECT_EQ(three, EnclosingBlock(PositionInFlatTree(one, 0),
+ kCannotCrossEditingBoundary));
+}
+
+TEST_F(EditingUtilitiesTest, enclosingNodeOfType) {
+ const char* body_content = "<p id='host'><b id='one'>11</b></p>";
+ const char* shadow_content =
+ "<content select=#two></content><div id='three'><content "
+ "select=#one></div></content>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+ Node* host = GetDocument().getElementById("host");
+ Node* one = GetDocument().getElementById("one");
+ Node* three = shadow_root->getElementById("three");
+
+ EXPECT_EQ(host, EnclosingNodeOfType(Position(one, 0), IsEnclosingBlock));
+ EXPECT_EQ(three,
+ EnclosingNodeOfType(PositionInFlatTree(one, 0), IsEnclosingBlock));
+}
+
+TEST_F(EditingUtilitiesTest, isEditablePositionWithTable) {
+ // We would like to have below DOM tree without HTML, HEAD and BODY element.
+ // <table id=table><caption>foo</caption></table>
+ // However, |setBodyContent()| automatically creates HTML, HEAD and BODY
+ // element. So, we build DOM tree manually.
+ // Note: This is unusual HTML taken from http://crbug.com/574230
+ Element* table = GetDocument().CreateRawElement(HTMLNames::tableTag);
+ table->SetInnerHTMLFromString("<caption>foo</caption>");
+ while (GetDocument().firstChild())
+ GetDocument().firstChild()->remove();
+ GetDocument().AppendChild(table);
+ GetDocument().setDesignMode("on");
+ UpdateAllLifecyclePhases();
+
+ EXPECT_FALSE(IsEditablePosition(Position(table, 0)));
+}
+
+TEST_F(EditingUtilitiesTest, RepeatString) {
+ EXPECT_EQ("", RepeatString("xyz", 0));
+ EXPECT_EQ("xyz", RepeatString("xyz", 1));
+ EXPECT_EQ("xyzxyz", RepeatString("xyz", 2));
+ EXPECT_EQ("xyzxyzxyz", RepeatString("xyz", 3));
+}
+
+TEST_F(EditingUtilitiesTest, tableElementJustBefore) {
+ const char* body_content =
+ "<div contenteditable id=host><table "
+ "id=table><tr><td>1</td></tr></table><b id=two>22</b></div>";
+ const char* shadow_content =
+ "<content select=#two></content><content select=#table></content>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+ Node* host = GetDocument().getElementById("host");
+ Node* table = GetDocument().getElementById("table");
+
+ EXPECT_EQ(table, TableElementJustBefore(VisiblePosition::AfterNode(*table)));
+ EXPECT_EQ(table, TableElementJustBefore(
+ VisiblePositionInFlatTree::AfterNode(*table)));
+
+ EXPECT_EQ(table, TableElementJustBefore(
+ VisiblePosition::LastPositionInNode(*table)));
+ EXPECT_EQ(table, TableElementJustBefore(CreateVisiblePosition(
+ PositionInFlatTree::LastPositionInNode(*table))));
+
+ EXPECT_EQ(nullptr,
+ TableElementJustBefore(CreateVisiblePosition(Position(host, 2))));
+ EXPECT_EQ(table, TableElementJustBefore(
+ CreateVisiblePosition(PositionInFlatTree(host, 2))));
+
+ EXPECT_EQ(nullptr, TableElementJustBefore(VisiblePosition::AfterNode(*host)));
+ EXPECT_EQ(nullptr, TableElementJustBefore(
+ VisiblePositionInFlatTree::AfterNode(*host)));
+
+ EXPECT_EQ(nullptr,
+ TableElementJustBefore(VisiblePosition::LastPositionInNode(*host)));
+ EXPECT_EQ(table, TableElementJustBefore(CreateVisiblePosition(
+ PositionInFlatTree::LastPositionInNode(*host))));
+}
+
+TEST_F(EditingUtilitiesTest, lastEditablePositionBeforePositionInRoot) {
+ const char* body_content =
+ "<p id='host' contenteditable><b id='one'>1</b><b id='two'>22</b></p>";
+ const char* shadow_content =
+ "<content select=#two></content><content select=#one></content><b "
+ "id='three'>333</b>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+ Element* host = GetDocument().getElementById("host");
+ Node* one = GetDocument().getElementById("one");
+ Node* two = GetDocument().getElementById("two");
+ Node* three = shadow_root->getElementById("three");
+
+ EXPECT_EQ(Position(one, 0),
+ LastEditablePositionBeforePositionInRoot(Position(one, 0), *host));
+ EXPECT_EQ(
+ Position(one->firstChild(), 0),
+ LastEditableVisiblePositionBeforePositionInRoot(Position(one, 0), *host)
+ .DeepEquivalent());
+
+ EXPECT_EQ(PositionInFlatTree(one, 0),
+ LastEditablePositionBeforePositionInRoot(PositionInFlatTree(one, 0),
+ *host));
+ EXPECT_EQ(PositionInFlatTree(two->firstChild(), 2),
+ LastEditableVisiblePositionBeforePositionInRoot(
+ PositionInFlatTree(one, 0), *host)
+ .DeepEquivalent());
+
+ EXPECT_EQ(
+ Position::FirstPositionInNode(*host),
+ LastEditablePositionBeforePositionInRoot(Position(three, 0), *host));
+ EXPECT_EQ(
+ Position(one->firstChild(), 0),
+ LastEditableVisiblePositionBeforePositionInRoot(Position(three, 0), *host)
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree::FirstPositionInNode(*host),
+ LastEditablePositionBeforePositionInRoot(
+ PositionInFlatTree(three, 0), *host));
+ EXPECT_EQ(PositionInFlatTree(two->firstChild(), 0),
+ LastEditableVisiblePositionBeforePositionInRoot(
+ PositionInFlatTree(three, 0), *host)
+ .DeepEquivalent());
+}
+
+TEST_F(EditingUtilitiesTest, NextNodeIndex) {
+ const char* body_content =
+ "<p id='host'>00<b id='one'>11</b><b id='two'>22</b>33</p>";
+ const char* shadow_content =
+ "<content select=#two></content><content select=#one></content>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+ Node* host = GetDocument().getElementById("host");
+ Node* two = GetDocument().getElementById("two");
+
+ EXPECT_EQ(
+ Position(host, 3),
+ NextPositionOf(Position(two, 1), PositionMoveType::kGraphemeCluster));
+ EXPECT_EQ(PositionInFlatTree(host, 1),
+ NextPositionOf(PositionInFlatTree(two, 1),
+ PositionMoveType::kGraphemeCluster));
+}
+
+TEST_F(EditingUtilitiesTest, NextVisuallyDistinctCandidate) {
+ const char* body_content =
+ "<p id='host'>00<b id='one'>11</b><b id='two'>22</b><b "
+ "id='three'>33</b></p>";
+ const char* shadow_content =
+ "<content select=#two></content><content select=#one></content><content "
+ "select=#three></content>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+ Node* one = GetDocument().getElementById("one");
+ Node* two = GetDocument().getElementById("two");
+ Node* three = GetDocument().getElementById("three");
+
+ EXPECT_EQ(Position(two->firstChild(), 1),
+ NextVisuallyDistinctCandidate(Position(one, 1)));
+ EXPECT_EQ(PositionInFlatTree(three->firstChild(), 1),
+ NextVisuallyDistinctCandidate(PositionInFlatTree(one, 1)));
+}
+
+TEST_F(EditingUtilitiesTest, uncheckedPreviousNextOffset_FirstLetter) {
+ SetBodyContent(
+ "<style>p::first-letter {color:red;}</style><p id='target'>abc</p>");
+ const Node& node = *GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(2, PreviousGraphemeBoundaryOf(node, 3));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(node, 2));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(node, 0));
+ EXPECT_EQ(2, NextGraphemeBoundaryOf(node, 1));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(node, 2));
+
+ UpdateAllLifecyclePhases();
+ EXPECT_NE(nullptr, node.GetLayoutObject());
+ EXPECT_EQ(2, PreviousGraphemeBoundaryOf(node, 3));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(node, 2));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(node, 0));
+ EXPECT_EQ(2, NextGraphemeBoundaryOf(node, 1));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(node, 2));
+}
+
+TEST_F(EditingUtilitiesTest, uncheckedPreviousNextOffset_textTransform) {
+ SetBodyContent(
+ "<style>p {text-transform:uppercase}</style><p id='target'>abc</p>");
+ const Node& node = *GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(2, PreviousGraphemeBoundaryOf(node, 3));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(node, 2));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(node, 0));
+ EXPECT_EQ(2, NextGraphemeBoundaryOf(node, 1));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(node, 2));
+
+ UpdateAllLifecyclePhases();
+ EXPECT_NE(nullptr, node.GetLayoutObject());
+ EXPECT_EQ(2, PreviousGraphemeBoundaryOf(node, 3));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(node, 2));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(node, 0));
+ EXPECT_EQ(2, NextGraphemeBoundaryOf(node, 1));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(node, 2));
+}
+
+// Following breaking rules come from http://unicode.org/reports/tr29/
+// Note that some of rules are in draft. Also see
+// http://www.unicode.org/reports/tr29/proposed.html
+TEST_F(EditingUtilitiesTest, uncheckedPreviousNextOffset) {
+ // GB1: Break at the start of text.
+ SetBodyContent("<p id='target'>a</p>");
+ Node* node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+
+ // GB2: Break at the end of text.
+ SetBodyContent("<p id='target'>a</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+
+ // GB3: Do not break between CR and LF.
+ SetBodyContent("<p id='target'>a&#x0D;&#x0A;b</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(3, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 3));
+
+ // GB4,GB5: Break before and after CR/LF/Control.
+ SetBodyContent("<p id='target'>a&#x0D;b</p>"); // CR
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(2, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(2, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 2));
+ SetBodyContent("<p id='target'>a&#x0A;b</p>"); // LF
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(2, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(2, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 2));
+ // U+00AD(SOFT HYPHEN) has Control property.
+ SetBodyContent("<p id='target'>a&#xAD;b</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(2, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(2, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 2));
+
+ // GB6: Don't break Hangul sequence.
+ const std::string l =
+ "&#x1100;"; // U+1100 (HANGUL CHOSEONG KIYEOK) has L property.
+ const std::string v =
+ "&#x1160;"; // U+1160 (HANGUL JUNGSEONG FILLER) has V property.
+ const std::string lv =
+ "&#xAC00;"; // U+AC00 (HANGUL SYLLABLE GA) has LV property.
+ const std::string lvt =
+ "&#xAC01;"; // U+AC01 (HANGUL SYLLABLE GAG) has LVT property.
+ const std::string t =
+ "&#x11A8;"; // U+11A8 (HANGUL JONGSEONG KIYEOK) has T property.
+ SetBodyContent("<p id='target'>a" + l + l + "b</p>"); // L x L
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(3, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 3));
+ SetBodyContent("<p id='target'>a" + l + v + "b</p>"); // L x V
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(3, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 3));
+ SetBodyContent("<p id='target'>a" + l + lv + "b</p>"); // L x LV
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(3, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 3));
+ SetBodyContent("<p id='target'>a" + l + lvt + "b</p>"); // L x LVT
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(3, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 3));
+
+ // GB7: Don't break Hangul sequence.
+ SetBodyContent("<p id='target'>a" + lv + v + "b</p>"); // LV x V
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(3, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 3));
+ SetBodyContent("<p id='target'>a" + lv + t + "b</p>"); // LV x T
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(3, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 3));
+ SetBodyContent("<p id='target'>a" + v + v + "b</p>"); // V x V
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(3, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 3));
+ SetBodyContent("<p id='target'>a" + v + t + "b</p>"); // V x T
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(3, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 3));
+
+ // GB8: Don't break Hangul sequence.
+ SetBodyContent("<p id='target'>a" + lvt + t + "b</p>"); // LVT x T
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(3, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 3));
+ SetBodyContent("<p id='target'>a" + t + t + "b</p>"); // T x T
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(3, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 3));
+
+ // Break other Hangul syllable combination. See test of GB999.
+
+ // GB8a: Don't break between regional indicator if there are even numbered
+ // regional indicator symbols before.
+ // U+1F1FA is REGIONAL INDICATOR SYMBOL LETTER U.
+ // U+1F1F8 is REGIONAL INDICATOR SYMBOL LETTER S.
+ const std::string flag = "&#x1F1FA;&#x1F1F8;"; // US flag.
+ // ^(RI RI)* RI x RI
+ SetBodyContent("<p id='target'>" + flag + flag + flag + flag + "a</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(16, PreviousGraphemeBoundaryOf(*node, 17));
+ EXPECT_EQ(12, PreviousGraphemeBoundaryOf(*node, 16));
+ EXPECT_EQ(8, PreviousGraphemeBoundaryOf(*node, 12));
+ EXPECT_EQ(4, PreviousGraphemeBoundaryOf(*node, 8));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(8, NextGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(12, NextGraphemeBoundaryOf(*node, 8));
+ EXPECT_EQ(16, NextGraphemeBoundaryOf(*node, 12));
+ EXPECT_EQ(17, NextGraphemeBoundaryOf(*node, 16));
+
+ // GB8c: Don't break between regional indicator if there are even numbered
+ // regional indicator symbols before.
+ // [^RI] (RI RI)* RI x RI
+ SetBodyContent("<p id='target'>a" + flag + flag + flag + flag + "b</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(17, PreviousGraphemeBoundaryOf(*node, 18));
+ EXPECT_EQ(13, PreviousGraphemeBoundaryOf(*node, 17));
+ EXPECT_EQ(9, PreviousGraphemeBoundaryOf(*node, 13));
+ EXPECT_EQ(5, PreviousGraphemeBoundaryOf(*node, 9));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 5));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(5, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(9, NextGraphemeBoundaryOf(*node, 5));
+ EXPECT_EQ(13, NextGraphemeBoundaryOf(*node, 9));
+ EXPECT_EQ(17, NextGraphemeBoundaryOf(*node, 13));
+ EXPECT_EQ(18, NextGraphemeBoundaryOf(*node, 17));
+
+ // GB8c: Break if there is an odd number of regional indicator symbols before.
+ SetBodyContent("<p id='target'>a" + flag + flag + flag + flag +
+ "&#x1F1F8;b</p>"); // RI ÷ RI
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(19, PreviousGraphemeBoundaryOf(*node, 20));
+ EXPECT_EQ(17, PreviousGraphemeBoundaryOf(*node, 19));
+ EXPECT_EQ(13, PreviousGraphemeBoundaryOf(*node, 17));
+ EXPECT_EQ(9, PreviousGraphemeBoundaryOf(*node, 13));
+ EXPECT_EQ(5, PreviousGraphemeBoundaryOf(*node, 9));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 5));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(5, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(9, NextGraphemeBoundaryOf(*node, 5));
+ EXPECT_EQ(13, NextGraphemeBoundaryOf(*node, 9));
+ EXPECT_EQ(17, NextGraphemeBoundaryOf(*node, 13));
+ EXPECT_EQ(19, NextGraphemeBoundaryOf(*node, 17));
+ EXPECT_EQ(20, NextGraphemeBoundaryOf(*node, 19));
+
+ // GB9: Do not break before extending characters or ZWJ.
+ // U+0300(COMBINING GRAVE ACCENT) has Extend property.
+ SetBodyContent("<p id='target'>a&#x0300;b</p>"); // x Extend
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(2, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(2, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 2));
+ // U+200D is ZERO WIDTH JOINER.
+ SetBodyContent("<p id='target'>a&#x200D;b</p>"); // x ZWJ
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(2, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(2, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 2));
+
+ // GB9a: Do not break before SpacingMarks.
+ // U+0903(DEVANAGARI SIGN VISARGA) has SpacingMark property.
+ SetBodyContent("<p id='target'>a&#x0903;b</p>"); // x SpacingMark
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(2, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(2, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 2));
+
+ // GB9b: Do not break after Prepend.
+ // TODO(nona): Introduce Prepend test case once ICU grabs Unicode 9.0.
+
+ // For https://bugs.webkit.org/show_bug.cgi?id=24342
+ // The break should happens after Thai character.
+ SetBodyContent("<p id='target'>a&#x0E40;b</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(2, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(2, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 2));
+
+ // Blink customization: Don't break before Japanese half-width katakana voiced
+ // marks.
+ SetBodyContent("<p id='target'>a&#xFF76;&#xFF9E;b</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(3, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 3));
+
+ // Additional rule for IndicSyllabicCategory=Virama: Do not break after that.
+ // See
+ // http://www.unicode.org/Public/9.0.0/ucd/IndicSyllabicCategory-9.0.0d2.txt
+ // U+0905 is DEVANAGARI LETTER A. This has Extend property.
+ // U+094D is DEVANAGARI SIGN VIRAMA. This has Virama property.
+ // U+0915 is DEVANAGARI LETTER KA.
+ SetBodyContent("<p id='target'>a&#x0905;&#x094D;&#x0915;b</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(4, PreviousGraphemeBoundaryOf(*node, 5));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(5, NextGraphemeBoundaryOf(*node, 4));
+ // U+0E01 is THAI CHARACTER KO KAI
+ // U+0E3A is THAI CHARACTER PHINTHU
+ // Should break after U+0E3A since U+0E3A has Virama property but not listed
+ // in IndicSyllabicCategory=Virama.
+ SetBodyContent("<p id='target'>a&#x0E01;&#x0E3A;&#x0E01;b</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(4, PreviousGraphemeBoundaryOf(*node, 5));
+ EXPECT_EQ(3, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(5, NextGraphemeBoundaryOf(*node, 4));
+
+ // GB10: Do not break within emoji modifier.
+ // U+1F385(FATHER CHRISTMAS) has E_Base property.
+ // U+1F3FB(EMOJI MODIFIER FITZPATRICK TYPE-1-2) has E_Modifier property.
+ SetBodyContent(
+ "<p id='target'>a&#x1F385;&#x1F3FB;b</p>"); // E_Base x E_Modifier
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(5, PreviousGraphemeBoundaryOf(*node, 6));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 5));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(5, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(6, NextGraphemeBoundaryOf(*node, 5));
+ // U+1F466(BOY) has EBG property.
+ SetBodyContent(
+ "<p id='target'>a&#x1F466;&#x1F3FB;b</p>"); // EBG x E_Modifier
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(5, PreviousGraphemeBoundaryOf(*node, 6));
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 5));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(5, NextGraphemeBoundaryOf(*node, 1));
+ EXPECT_EQ(6, NextGraphemeBoundaryOf(*node, 5));
+
+ // GB11: Do not break within ZWJ emoji sequence.
+ // U+2764(HEAVY BLACK HEART) has Glue_After_Zwj property.
+ SetBodyContent(
+ "<p id='target'>a&#x200D;&#x2764;b</p>"); // ZWJ x Glue_After_Zwj
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(3, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 3));
+ SetBodyContent("<p id='target'>a&#x200D;&#x1F466;b</p>"); // ZWJ x EBG
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(4, PreviousGraphemeBoundaryOf(*node, 5));
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(4, NextGraphemeBoundaryOf(*node, 0));
+ EXPECT_EQ(5, NextGraphemeBoundaryOf(*node, 4));
+
+ // Not only Glue_After_ZWJ or EBG but also other emoji shouldn't break
+ // before ZWJ.
+ // U+1F5FA(WORLD MAP) doesn't have either Glue_After_Zwj or EBG but has
+ // Emoji property.
+ SetBodyContent("<p id='target'>&#x200D;&#x1F5FA;</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(0, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(3, NextGraphemeBoundaryOf(*node, 0));
+
+ // GB999: Otherwise break everywhere.
+ // Breaks between Hangul syllable except for GB6, GB7, GB8.
+ SetBodyContent("<p id='target'>" + l + t + "</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ SetBodyContent("<p id='target'>" + v + l + "</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ SetBodyContent("<p id='target'>" + v + lv + "</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ SetBodyContent("<p id='target'>" + v + lvt + "</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ SetBodyContent("<p id='target'>" + lv + l + "</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ SetBodyContent("<p id='target'>" + lv + lv + "</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ SetBodyContent("<p id='target'>" + lv + lvt + "</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ SetBodyContent("<p id='target'>" + lvt + l + "</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ SetBodyContent("<p id='target'>" + lvt + v + "</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ SetBodyContent("<p id='target'>" + lvt + lv + "</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ SetBodyContent("<p id='target'>" + lvt + lvt + "</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ SetBodyContent("<p id='target'>" + t + l + "</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ SetBodyContent("<p id='target'>" + t + v + "</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ SetBodyContent("<p id='target'>" + t + lv + "</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ SetBodyContent("<p id='target'>" + t + lvt + "</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+
+ // For GB10, if base emoji character is not E_Base or EBG, break happens
+ // before E_Modifier.
+ SetBodyContent("<p id='target'>a&#x1F3FB;</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 3));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+ // U+1F5FA(WORLD MAP) doesn't have either E_Base or EBG property.
+ SetBodyContent("<p id='target'>&#x1F5FA;&#x1F3FB;</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(2, PreviousGraphemeBoundaryOf(*node, 4));
+ EXPECT_EQ(2, NextGraphemeBoundaryOf(*node, 0));
+
+ // For GB11, if trailing character is not Glue_After_Zwj or EBG, break happens
+ // after ZWJ.
+ // U+1F5FA(WORLD MAP) doesn't have either Glue_After_Zwj or EBG.
+ SetBodyContent("<p id='target'>&#x200D;a</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(1, PreviousGraphemeBoundaryOf(*node, 2));
+ EXPECT_EQ(1, NextGraphemeBoundaryOf(*node, 0));
+}
+
+TEST_F(EditingUtilitiesTest, previousPositionOf_Backspace) {
+ // BMP characters. Only one code point should be deleted.
+ SetBodyContent("<p id='target'>abc</p>");
+ Node* node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(Position(node, 2),
+ PreviousPositionOf(Position(node, 3),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 1),
+ PreviousPositionOf(Position(node, 2),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 0),
+ PreviousPositionOf(Position(node, 1),
+ PositionMoveType::kBackwardDeletion));
+}
+
+TEST_F(EditingUtilitiesTest, previousPositionOf_Backspace_FirstLetter) {
+ SetBodyContent(
+ "<style>p::first-letter {color:red;}</style><p id='target'>abc</p>");
+ Node* node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(Position(node, 2),
+ PreviousPositionOf(Position(node, 3),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 1),
+ PreviousPositionOf(Position(node, 2),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 0),
+ PreviousPositionOf(Position(node, 1),
+ PositionMoveType::kBackwardDeletion));
+
+ SetBodyContent(
+ "<style>p::first-letter {color:red;}</style><p id='target'>(a)bc</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(Position(node, 4),
+ PreviousPositionOf(Position(node, 5),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 3),
+ PreviousPositionOf(Position(node, 4),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 2),
+ PreviousPositionOf(Position(node, 3),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 1),
+ PreviousPositionOf(Position(node, 2),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 0),
+ PreviousPositionOf(Position(node, 1),
+ PositionMoveType::kBackwardDeletion));
+}
+
+TEST_F(EditingUtilitiesTest, previousPositionOf_Backspace_TextTransform) {
+ // Uppercase of &#x00DF; will be transformed to SS.
+ SetBodyContent(
+ "<style>p {text-transform:uppercase}</style><p "
+ "id='target'>&#x00DF;abc</p>");
+ Node* node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(Position(node, 3),
+ PreviousPositionOf(Position(node, 4),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 2),
+ PreviousPositionOf(Position(node, 3),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 1),
+ PreviousPositionOf(Position(node, 2),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 0),
+ PreviousPositionOf(Position(node, 1),
+ PositionMoveType::kBackwardDeletion));
+}
+
+TEST_F(EditingUtilitiesTest, IsTabHTMLSpanElementOnDisplayNone) {
+ SetBodyContent("<span style=\"display:none\">\t</span>");
+ const Node* const node = GetDocument().QuerySelector("span");
+ EXPECT_EQ(false, IsTabHTMLSpanElement(node));
+}
+
+TEST_F(EditingUtilitiesTest, previousPositionOf_Backspace_SurrogatePairs) {
+ // Supplementary plane characters. Only one code point should be deleted.
+ // &#x1F441; is EYE.
+ SetBodyContent("<p id='target'>&#x1F441;&#x1F441;&#x1F441;</p>");
+ Node* node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(Position(node, 4),
+ PreviousPositionOf(Position(node, 6),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 2),
+ PreviousPositionOf(Position(node, 4),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 0),
+ PreviousPositionOf(Position(node, 2),
+ PositionMoveType::kBackwardDeletion));
+
+ // BMP and Supplementary plane case.
+ SetBodyContent("<p id='target'>&#x1F441;a&#x1F441;a</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(Position(node, 5),
+ PreviousPositionOf(Position(node, 6),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 3),
+ PreviousPositionOf(Position(node, 5),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 2),
+ PreviousPositionOf(Position(node, 3),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 0),
+ PreviousPositionOf(Position(node, 2),
+ PositionMoveType::kBackwardDeletion));
+
+ // Edge case: broken surrogate pairs.
+ SetBodyContent(
+ "<p id='target'>&#xD83D;</p>"); // &#xD83D; is unpaired lead surrogate.
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(Position(node, 0),
+ PreviousPositionOf(Position(node, 1),
+ PositionMoveType::kBackwardDeletion));
+
+ // &#xD83D; is unpaired lead surrogate.
+ SetBodyContent("<p id='target'>&#x1F441;&#xD83D;&#x1F441;</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(Position(node, 3),
+ PreviousPositionOf(Position(node, 5),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 2),
+ PreviousPositionOf(Position(node, 3),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 0),
+ PreviousPositionOf(Position(node, 2),
+ PositionMoveType::kBackwardDeletion));
+
+ SetBodyContent(
+ "<p id='target'>a&#xD83D;a</p>"); // &#xD83D; is unpaired lead surrogate.
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(Position(node, 2),
+ PreviousPositionOf(Position(node, 3),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 1),
+ PreviousPositionOf(Position(node, 2),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 0),
+ PreviousPositionOf(Position(node, 1),
+ PositionMoveType::kBackwardDeletion));
+
+ SetBodyContent(
+ "<p id='target'>&#xDC41;</p>"); // &#xDC41; is unpaired trail surrogate.
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(Position(node, 0),
+ PreviousPositionOf(Position(node, 1),
+ PositionMoveType::kBackwardDeletion));
+
+ // &#xDC41; is unpaired trail surrogate.
+ SetBodyContent("<p id='target'>&#x1F441;&#xDC41;&#x1F441;</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(Position(node, 3),
+ PreviousPositionOf(Position(node, 5),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 2),
+ PreviousPositionOf(Position(node, 3),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 0),
+ PreviousPositionOf(Position(node, 2),
+ PositionMoveType::kBackwardDeletion));
+
+ // &#xDC41; is unpaired trail surrogate.
+ SetBodyContent("<p id='target'>a&#xDC41;a</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(Position(node, 2),
+ PreviousPositionOf(Position(node, 3),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 1),
+ PreviousPositionOf(Position(node, 2),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 0),
+ PreviousPositionOf(Position(node, 1),
+ PositionMoveType::kBackwardDeletion));
+
+ // Edge case: specify middle of surrogate pairs.
+ SetBodyContent("<p id='target'>&#x1F441;&#x1F441;&#x1F441</p>");
+ node = GetDocument().getElementById("target")->firstChild();
+ EXPECT_EQ(Position(node, 4),
+ PreviousPositionOf(Position(node, 5),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 2),
+ PreviousPositionOf(Position(node, 3),
+ PositionMoveType::kBackwardDeletion));
+ EXPECT_EQ(Position(node, 0),
+ PreviousPositionOf(Position(node, 1),
+ PositionMoveType::kBackwardDeletion));
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/editor.cc b/chromium/third_party/blink/renderer/core/editing/editor.cc
new file mode 100644
index 00000000000..2ecb924baf3
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editor.cc
@@ -0,0 +1,944 @@
+/*
+ * Copyright (C) 2006, 2007, 2008, 2011 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/editor.h"
+
+#include "third_party/blink/public/platform/web_scroll_into_view_params.h"
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/clipboard/data_object.h"
+#include "third_party/blink/renderer/core/clipboard/data_transfer.h"
+#include "third_party/blink/renderer/core/clipboard/data_transfer_access_policy.h"
+#include "third_party/blink/renderer/core/clipboard/pasteboard.h"
+#include "third_party/blink/renderer/core/css/css_computed_style_declaration.h"
+#include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/css_property_names.h"
+#include "third_party/blink/renderer/core/dom/ax_object_cache.h"
+#include "third_party/blink/renderer/core/dom/document_fragment.h"
+#include "third_party/blink/renderer/core/dom/element_traversal.h"
+#include "third_party/blink/renderer/core/dom/events/scoped_event_queue.h"
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/core/dom/parser_content_policy.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/commands/apply_style_command.h"
+#include "third_party/blink/renderer/core/editing/commands/delete_selection_command.h"
+#include "third_party/blink/renderer/core/editing/commands/indent_outdent_command.h"
+#include "third_party/blink/renderer/core/editing/commands/insert_list_command.h"
+#include "third_party/blink/renderer/core/editing/commands/replace_selection_command.h"
+#include "third_party/blink/renderer/core/editing/commands/simplify_markup_command.h"
+#include "third_party/blink/renderer/core/editing/commands/typing_command.h"
+#include "third_party/blink/renderer/core/editing/commands/undo_stack.h"
+#include "third_party/blink/renderer/core/editing/editing_style_utilities.h"
+#include "third_party/blink/renderer/core/editing/editing_tri_state.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/ime/input_method_controller.h"
+#include "third_party/blink/renderer/core/editing/iterators/search_buffer.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/serializers/serialization.h"
+#include "third_party/blink/renderer/core/editing/set_selection_options.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_checker.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/editing/writing_direction.h"
+#include "third_party/blink/renderer/core/event_names.h"
+#include "third_party/blink/renderer/core/events/keyboard_event.h"
+#include "third_party/blink/renderer/core/events/text_event.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/frame/use_counter.h"
+#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
+#include "third_party/blink/renderer/core/html/forms/html_text_area_element.h"
+#include "third_party/blink/renderer/core/html/html_image_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/input/event_handler.h"
+#include "third_party/blink/renderer/core/input_type_names.h"
+#include "third_party/blink/renderer/core/layout/hit_test_result.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/core/loader/empty_clients.h"
+#include "third_party/blink/renderer/core/loader/resource/image_resource_content.h"
+#include "third_party/blink/renderer/core/page/drag_data.h"
+#include "third_party/blink/renderer/core/page/focus_controller.h"
+#include "third_party/blink/renderer/core/page/page.h"
+#include "third_party/blink/renderer/platform/kill_ring.h"
+#include "third_party/blink/renderer/platform/scroll/scroll_alignment.h"
+#include "third_party/blink/renderer/platform/weborigin/kurl.h"
+#include "third_party/blink/renderer/platform/wtf/text/character_names.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+namespace {
+
+bool IsInPasswordFieldWithUnrevealedPassword(const Position& position) {
+ if (auto* input = ToHTMLInputElementOrNull(EnclosingTextControl(position))) {
+ return (input->type() == InputTypeNames::password) &&
+ !input->ShouldRevealPassword();
+ }
+ return false;
+}
+
+} // anonymous namespace
+
+// When an event handler has moved the selection outside of a text control
+// we should use the target control's selection for this editing operation.
+SelectionInDOMTree Editor::SelectionForCommand(Event* event) {
+ const SelectionInDOMTree selection =
+ GetFrameSelection().GetSelectionInDOMTree();
+ if (!event)
+ return selection;
+ // If the target is a text control, and the current selection is outside of
+ // its shadow tree, then use the saved selection for that text control.
+ if (!IsTextControl(*event->target()->ToNode()))
+ return selection;
+ auto* text_control_of_selection_start =
+ EnclosingTextControl(selection.Base());
+ auto* text_control_of_target = ToTextControl(event->target()->ToNode());
+ if (!selection.IsNone() &&
+ text_control_of_target == text_control_of_selection_start)
+ return selection;
+ const SelectionInDOMTree& select = text_control_of_target->Selection();
+ if (select.IsNone())
+ return selection;
+ return select;
+}
+
+// Function considers Mac editing behavior a fallback when Page or Settings is
+// not available.
+EditingBehavior Editor::Behavior() const {
+ if (!GetFrame().GetSettings())
+ return EditingBehavior(kEditingMacBehavior);
+
+ return EditingBehavior(GetFrame().GetSettings()->GetEditingBehaviorType());
+}
+
+static bool IsCaretAtStartOfWrappedLine(const FrameSelection& selection) {
+ if (!selection.ComputeVisibleSelectionInDOMTree().IsCaret())
+ return false;
+ if (selection.GetSelectionInDOMTree().Affinity() != TextAffinity::kDownstream)
+ return false;
+ const Position& position =
+ selection.ComputeVisibleSelectionInDOMTree().Start();
+ if (InSameLine(PositionWithAffinity(position, TextAffinity::kUpstream),
+ PositionWithAffinity(position, TextAffinity::kDownstream)))
+ return false;
+
+ // Only when the previous character is a space to avoid undesired side
+ // effects. There are cases where a new line is desired even if the previous
+ // character is not a space, but typing another space will do.
+ Position prev =
+ PreviousPositionOf(position, PositionMoveType::kGraphemeCluster);
+ const Node* prev_node = prev.ComputeContainerNode();
+ if (!prev_node || !prev_node->IsTextNode())
+ return false;
+ int prev_offset = prev.ComputeOffsetInContainerNode();
+ UChar prev_char = ToText(prev_node)->data()[prev_offset];
+ return prev_char == kSpaceCharacter;
+}
+
+bool Editor::HandleTextEvent(TextEvent* event) {
+ // Default event handling for Drag and Drop will be handled by DragController
+ // so we leave the event for it.
+ if (event->IsDrop())
+ return false;
+
+ // Default event handling for IncrementalInsertion will be handled by
+ // TypingCommand::insertText(), so we leave the event for it.
+ if (event->IsIncrementalInsertion())
+ return false;
+
+ // TODO(editing-dev): The use of UpdateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ frame_->GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (event->IsPaste()) {
+ if (event->PastingFragment()) {
+ ReplaceSelectionWithFragment(
+ event->PastingFragment(), false, event->ShouldSmartReplace(),
+ event->ShouldMatchStyle(), InputEvent::InputType::kInsertFromPaste);
+ } else {
+ ReplaceSelectionWithText(event->data(), false,
+ event->ShouldSmartReplace(),
+ InputEvent::InputType::kInsertFromPaste);
+ }
+ return true;
+ }
+
+ String data = event->data();
+ if (data == "\n") {
+ if (event->IsLineBreak())
+ return InsertLineBreak();
+ return InsertParagraphSeparator();
+ }
+
+ // Typing spaces at the beginning of wrapped line is confusing, because
+ // inserted spaces would appear in the previous line.
+ // Insert a line break automatically so that the spaces appear at the caret.
+ // TODO(kojii): rich editing has the same issue, but has more options and
+ // needs coordination with JS. Enable for plaintext only for now and collect
+ // feedback.
+ if (data == " " && !CanEditRichly() &&
+ IsCaretAtStartOfWrappedLine(GetFrameSelection())) {
+ InsertLineBreak();
+ }
+
+ return InsertTextWithoutSendingTextEvent(data, false, event);
+}
+
+bool Editor::CanEdit() const {
+ return GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .RootEditableElement();
+}
+
+bool Editor::CanEditRichly() const {
+ return IsRichlyEditablePosition(
+ GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .Base());
+}
+
+bool Editor::CanCut() const {
+ return CanCopy() && CanDelete();
+}
+
+bool Editor::CanCopy() const {
+ if (ImageElementFromImageDocument(GetFrame().GetDocument()))
+ return true;
+ FrameSelection& selection = GetFrameSelection();
+ if (!selection.IsAvailable())
+ return false;
+ return selection.ComputeVisibleSelectionInDOMTreeDeprecated().IsRange() &&
+ !IsInPasswordFieldWithUnrevealedPassword(
+ selection.ComputeVisibleSelectionInDOMTree().Start());
+}
+
+bool Editor::CanPaste() const {
+ return CanEdit();
+}
+
+bool Editor::CanDelete() const {
+ FrameSelection& selection = GetFrameSelection();
+ return selection.ComputeVisibleSelectionInDOMTreeDeprecated().IsRange() &&
+ selection.ComputeVisibleSelectionInDOMTree().RootEditableElement();
+}
+
+bool Editor::SmartInsertDeleteEnabled() const {
+ if (Settings* settings = GetFrame().GetSettings())
+ return settings->GetSmartInsertDeleteEnabled();
+ return false;
+}
+
+bool Editor::IsSelectTrailingWhitespaceEnabled() const {
+ if (Settings* settings = GetFrame().GetSettings())
+ return settings->GetSelectTrailingWhitespaceEnabled();
+ return false;
+}
+
+void Editor::DeleteSelectionWithSmartDelete(
+ DeleteMode delete_mode,
+ InputEvent::InputType input_type,
+ const Position& reference_move_position) {
+ if (GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .IsNone())
+ return;
+
+ DCHECK(GetFrame().GetDocument());
+ DeleteSelectionCommand::Create(
+ *GetFrame().GetDocument(),
+ DeleteSelectionOptions::Builder()
+ .SetSmartDelete(delete_mode == DeleteMode::kSmart)
+ .SetMergeBlocksAfterDelete(true)
+ .SetExpandForSpecialElements(true)
+ .SetSanitizeMarkup(true)
+ .Build(),
+ input_type, reference_move_position)
+ ->Apply();
+}
+
+void Editor::ReplaceSelectionWithFragment(DocumentFragment* fragment,
+ bool select_replacement,
+ bool smart_replace,
+ bool match_style,
+ InputEvent::InputType input_type) {
+ DCHECK(!GetFrame().GetDocument()->NeedsLayoutTreeUpdate());
+ const VisibleSelection& selection =
+ GetFrameSelection().ComputeVisibleSelectionInDOMTree();
+ if (selection.IsNone() || !selection.IsContentEditable() || !fragment)
+ return;
+
+ ReplaceSelectionCommand::CommandOptions options =
+ ReplaceSelectionCommand::kPreventNesting |
+ ReplaceSelectionCommand::kSanitizeFragment;
+ if (select_replacement)
+ options |= ReplaceSelectionCommand::kSelectReplacement;
+ if (smart_replace)
+ options |= ReplaceSelectionCommand::kSmartReplace;
+ if (match_style)
+ options |= ReplaceSelectionCommand::kMatchStyle;
+ DCHECK(GetFrame().GetDocument());
+ ReplaceSelectionCommand::Create(*GetFrame().GetDocument(), fragment, options,
+ input_type)
+ ->Apply();
+ RevealSelectionAfterEditingOperation();
+}
+
+void Editor::ReplaceSelectionWithText(const String& text,
+ bool select_replacement,
+ bool smart_replace,
+ InputEvent::InputType input_type) {
+ ReplaceSelectionWithFragment(CreateFragmentFromText(SelectedRange(), text),
+ select_replacement, smart_replace, true,
+ input_type);
+}
+
+void Editor::ReplaceSelectionAfterDragging(DocumentFragment* fragment,
+ InsertMode insert_mode,
+ DragSourceType drag_source_type) {
+ ReplaceSelectionCommand::CommandOptions options =
+ ReplaceSelectionCommand::kSelectReplacement |
+ ReplaceSelectionCommand::kPreventNesting;
+ if (insert_mode == InsertMode::kSmart)
+ options |= ReplaceSelectionCommand::kSmartReplace;
+ if (drag_source_type == DragSourceType::kPlainTextSource)
+ options |= ReplaceSelectionCommand::kMatchStyle;
+ DCHECK(GetFrame().GetDocument());
+ ReplaceSelectionCommand::Create(*GetFrame().GetDocument(), fragment, options,
+ InputEvent::InputType::kInsertFromDrop)
+ ->Apply();
+}
+
+bool Editor::DeleteSelectionAfterDraggingWithEvents(
+ Element* drag_source,
+ DeleteMode delete_mode,
+ const Position& reference_move_position) {
+ if (!drag_source || !drag_source->isConnected())
+ return true;
+
+ // Dispatch 'beforeinput'.
+ const bool should_delete =
+ DispatchBeforeInputEditorCommand(
+ drag_source, InputEvent::InputType::kDeleteByDrag,
+ TargetRangesForInputEvent(*drag_source)) ==
+ DispatchEventResult::kNotCanceled;
+
+ // 'beforeinput' event handler may destroy frame, return false to cancel
+ // remaining actions;
+ if (frame_->GetDocument()->GetFrame() != frame_)
+ return false;
+
+ if (should_delete && drag_source->isConnected()) {
+ DeleteSelectionWithSmartDelete(delete_mode,
+ InputEvent::InputType::kDeleteByDrag,
+ reference_move_position);
+ }
+
+ return true;
+}
+
+bool Editor::ReplaceSelectionAfterDraggingWithEvents(
+ Element* drop_target,
+ DragData* drag_data,
+ DocumentFragment* fragment,
+ Range* drop_caret_range,
+ InsertMode insert_mode,
+ DragSourceType drag_source_type) {
+ if (!drop_target || !drop_target->isConnected())
+ return true;
+
+ // Dispatch 'beforeinput'.
+ DataTransfer* data_transfer = DataTransfer::Create(
+ DataTransfer::kDragAndDrop, DataTransferAccessPolicy::kReadable,
+ drag_data->PlatformData());
+ data_transfer->SetSourceOperation(drag_data->DraggingSourceOperationMask());
+ const bool should_insert =
+ DispatchBeforeInputDataTransfer(
+ drop_target, InputEvent::InputType::kInsertFromDrop, data_transfer) ==
+ DispatchEventResult::kNotCanceled;
+
+ // 'beforeinput' event handler may destroy frame, return false to cancel
+ // remaining actions;
+ if (frame_->GetDocument()->GetFrame() != frame_)
+ return false;
+
+ if (should_insert && drop_target->isConnected())
+ ReplaceSelectionAfterDragging(fragment, insert_mode, drag_source_type);
+
+ return true;
+}
+
+EphemeralRange Editor::SelectedRange() {
+ return GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .ToNormalizedEphemeralRange();
+}
+
+void Editor::RespondToChangedContents(const Position& position) {
+ if (GetFrame().GetSettings() &&
+ GetFrame().GetSettings()->GetAccessibilityEnabled()) {
+ Node* node = position.AnchorNode();
+ if (AXObjectCache* cache =
+ GetFrame().GetDocument()->ExistingAXObjectCache())
+ cache->HandleEditableTextContentChanged(node);
+ }
+
+ GetSpellChecker().RespondToChangedContents();
+ frame_->Client()->DidChangeContents();
+}
+
+void Editor::RegisterCommandGroup(CompositeEditCommand* command_group_wrapper) {
+ DCHECK(command_group_wrapper->IsCommandGroupWrapper());
+ last_edit_command_ = command_group_wrapper;
+}
+
+void Editor::ApplyParagraphStyle(CSSPropertyValueSet* style,
+ InputEvent::InputType input_type) {
+ if (GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .IsNone() ||
+ !style)
+ return;
+ DCHECK(GetFrame().GetDocument());
+ ApplyStyleCommand::Create(*GetFrame().GetDocument(),
+ EditingStyle::Create(style), input_type,
+ ApplyStyleCommand::kForceBlockProperties)
+ ->Apply();
+}
+
+void Editor::ApplyParagraphStyleToSelection(CSSPropertyValueSet* style,
+ InputEvent::InputType input_type) {
+ if (!style || style->IsEmpty() || !CanEditRichly())
+ return;
+
+ ApplyParagraphStyle(style, input_type);
+}
+
+Editor* Editor::Create(LocalFrame& frame) {
+ return new Editor(frame);
+}
+
+Editor::Editor(LocalFrame& frame)
+ : frame_(&frame),
+ undo_stack_(UndoStack::Create()),
+ prevent_reveal_selection_(0),
+ should_start_new_kill_ring_sequence_(false),
+ // This is off by default, since most editors want this behavior (this
+ // matches IE but not FF).
+ should_style_with_css_(false),
+ kill_ring_(std::make_unique<KillRing>()),
+ are_marked_text_matches_highlighted_(false),
+ default_paragraph_separator_(EditorParagraphSeparator::kIsDiv),
+ overwrite_mode_enabled_(false) {}
+
+Editor::~Editor() = default;
+
+void Editor::Clear() {
+ should_style_with_css_ = false;
+ default_paragraph_separator_ = EditorParagraphSeparator::kIsDiv;
+ last_edit_command_ = nullptr;
+ undo_stack_->Clear();
+}
+
+bool Editor::InsertText(const String& text, KeyboardEvent* triggering_event) {
+ return GetFrame().GetEventHandler().HandleTextInputEvent(text,
+ triggering_event);
+}
+
+bool Editor::InsertTextWithoutSendingTextEvent(
+ const String& text,
+ bool select_inserted_text,
+ TextEvent* triggering_event,
+ InputEvent::InputType input_type) {
+ const VisibleSelection& selection =
+ CreateVisibleSelection(SelectionForCommand(triggering_event));
+ if (!selection.IsContentEditable())
+ return false;
+
+ EditingState editing_state;
+ // Insert the text
+ TypingCommand::InsertText(
+ *selection.Start().GetDocument(), text, selection.AsSelection(),
+ select_inserted_text ? TypingCommand::kSelectInsertedText : 0,
+ &editing_state,
+ triggering_event && triggering_event->IsComposition()
+ ? TypingCommand::kTextCompositionConfirm
+ : TypingCommand::kTextCompositionNone,
+ false, input_type);
+ if (editing_state.IsAborted())
+ return false;
+
+ // Reveal the current selection
+ if (LocalFrame* edited_frame = selection.Start().GetDocument()->GetFrame()) {
+ if (Page* page = edited_frame->GetPage()) {
+ LocalFrame* focused_or_main_frame =
+ ToLocalFrame(page->GetFocusController().FocusedOrMainFrame());
+ focused_or_main_frame->Selection().RevealSelection(
+ ScrollAlignment::kAlignCenterIfNeeded);
+ }
+ }
+
+ return true;
+}
+
+bool Editor::InsertLineBreak() {
+ if (!CanEdit())
+ return false;
+
+ VisiblePosition caret =
+ GetFrameSelection().ComputeVisibleSelectionInDOMTree().VisibleStart();
+ bool align_to_edge = IsEndOfEditableOrNonEditableContent(caret);
+ DCHECK(GetFrame().GetDocument());
+ if (!TypingCommand::InsertLineBreak(*GetFrame().GetDocument()))
+ return false;
+ RevealSelectionAfterEditingOperation(
+ align_to_edge ? ScrollAlignment::kAlignToEdgeIfNeeded
+ : ScrollAlignment::kAlignCenterIfNeeded);
+
+ return true;
+}
+
+bool Editor::InsertParagraphSeparator() {
+ if (!CanEdit())
+ return false;
+
+ if (!CanEditRichly())
+ return InsertLineBreak();
+
+ VisiblePosition caret =
+ GetFrameSelection().ComputeVisibleSelectionInDOMTree().VisibleStart();
+ bool align_to_edge = IsEndOfEditableOrNonEditableContent(caret);
+ DCHECK(GetFrame().GetDocument());
+ EditingState editing_state;
+ if (!TypingCommand::InsertParagraphSeparator(*GetFrame().GetDocument()))
+ return false;
+ RevealSelectionAfterEditingOperation(
+ align_to_edge ? ScrollAlignment::kAlignToEdgeIfNeeded
+ : ScrollAlignment::kAlignCenterIfNeeded);
+
+ return true;
+}
+
+static void CountEditingEvent(ExecutionContext* execution_context,
+ const Event* event,
+ WebFeature feature_on_input,
+ WebFeature feature_on_text_area,
+ WebFeature feature_on_content_editable,
+ WebFeature feature_on_non_node) {
+ EventTarget* event_target = event->target();
+ Node* node = event_target->ToNode();
+ if (!node) {
+ UseCounter::Count(execution_context, feature_on_non_node);
+ return;
+ }
+
+ if (IsHTMLInputElement(node)) {
+ UseCounter::Count(execution_context, feature_on_input);
+ return;
+ }
+
+ if (IsHTMLTextAreaElement(node)) {
+ UseCounter::Count(execution_context, feature_on_text_area);
+ return;
+ }
+
+ TextControlElement* control = EnclosingTextControl(node);
+ if (IsHTMLInputElement(control)) {
+ UseCounter::Count(execution_context, feature_on_input);
+ return;
+ }
+
+ if (IsHTMLTextAreaElement(control)) {
+ UseCounter::Count(execution_context, feature_on_text_area);
+ return;
+ }
+
+ UseCounter::Count(execution_context, feature_on_content_editable);
+}
+
+void Editor::CountEvent(ExecutionContext* execution_context,
+ const Event* event) {
+ if (!execution_context)
+ return;
+
+ if (event->type() == EventTypeNames::textInput) {
+ CountEditingEvent(execution_context, event,
+ WebFeature::kTextInputEventOnInput,
+ WebFeature::kTextInputEventOnTextArea,
+ WebFeature::kTextInputEventOnContentEditable,
+ WebFeature::kTextInputEventOnNotNode);
+ return;
+ }
+
+ if (event->type() == EventTypeNames::webkitBeforeTextInserted) {
+ CountEditingEvent(execution_context, event,
+ WebFeature::kWebkitBeforeTextInsertedOnInput,
+ WebFeature::kWebkitBeforeTextInsertedOnTextArea,
+ WebFeature::kWebkitBeforeTextInsertedOnContentEditable,
+ WebFeature::kWebkitBeforeTextInsertedOnNotNode);
+ return;
+ }
+
+ if (event->type() == EventTypeNames::webkitEditableContentChanged) {
+ CountEditingEvent(
+ execution_context, event,
+ WebFeature::kWebkitEditableContentChangedOnInput,
+ WebFeature::kWebkitEditableContentChangedOnTextArea,
+ WebFeature::kWebkitEditableContentChangedOnContentEditable,
+ WebFeature::kWebkitEditableContentChangedOnNotNode);
+ }
+}
+
+void Editor::CopyImage(const HitTestResult& result) {
+ WriteImageNodeToPasteboard(Pasteboard::GeneralPasteboard(),
+ *result.InnerNodeOrImageMapImage(),
+ result.AltDisplayString());
+}
+
+bool Editor::CanUndo() {
+ return undo_stack_->CanUndo();
+}
+
+void Editor::Undo() {
+ undo_stack_->Undo();
+}
+
+bool Editor::CanRedo() {
+ return undo_stack_->CanRedo();
+}
+
+void Editor::Redo() {
+ undo_stack_->Redo();
+}
+
+void Editor::SetBaseWritingDirection(WritingDirection direction) {
+ Element* focused_element = GetFrame().GetDocument()->FocusedElement();
+ if (IsTextControl(focused_element)) {
+ if (direction == WritingDirection::kNatural)
+ return;
+ focused_element->setAttribute(
+ dirAttr, direction == WritingDirection::kLeftToRight ? "ltr" : "rtl");
+ focused_element->DispatchInputEvent();
+ return;
+ }
+
+ MutableCSSPropertyValueSet* style =
+ MutableCSSPropertyValueSet::Create(kHTMLQuirksMode);
+ style->SetProperty(
+ CSSPropertyDirection,
+ direction == WritingDirection::kLeftToRight
+ ? "ltr"
+ : direction == WritingDirection::kRightToLeft ? "rtl" : "inherit",
+ /* important */ false, GetFrame().GetDocument()->GetSecureContextMode());
+ ApplyParagraphStyleToSelection(
+ style, InputEvent::InputType::kFormatSetBlockTextDirection);
+}
+
+void Editor::RevealSelectionAfterEditingOperation(
+ const ScrollAlignment& alignment) {
+ if (prevent_reveal_selection_)
+ return;
+ if (!GetFrameSelection().IsAvailable())
+ return;
+ GetFrameSelection().RevealSelection(alignment, kDoNotRevealExtent);
+}
+
+void Editor::AddToKillRing(const EphemeralRange& range) {
+ if (should_start_new_kill_ring_sequence_)
+ GetKillRing().StartNewSequence();
+
+ DCHECK(!GetFrame().GetDocument()->NeedsLayoutTreeUpdate());
+ String text = PlainText(range);
+ GetKillRing().Append(text);
+ should_start_new_kill_ring_sequence_ = false;
+}
+
+EphemeralRange Editor::RangeForPoint(const IntPoint& frame_point) const {
+ const PositionWithAffinity position_with_affinity =
+ GetFrame().PositionForPoint(frame_point);
+ if (position_with_affinity.IsNull())
+ return EphemeralRange();
+
+ const VisiblePosition position =
+ CreateVisiblePosition(position_with_affinity);
+ const VisiblePosition previous = PreviousPositionOf(position);
+ if (previous.IsNotNull()) {
+ const EphemeralRange previous_character_range =
+ MakeRange(previous, position);
+ const IntRect rect = FirstRectForRange(previous_character_range);
+ if (rect.Contains(frame_point))
+ return EphemeralRange(previous_character_range);
+ }
+
+ const VisiblePosition next = NextPositionOf(position);
+ const EphemeralRange next_character_range = MakeRange(position, next);
+ if (next_character_range.IsNotNull()) {
+ const IntRect rect = FirstRectForRange(next_character_range);
+ if (rect.Contains(frame_point))
+ return EphemeralRange(next_character_range);
+ }
+
+ return EphemeralRange();
+}
+
+void Editor::ComputeAndSetTypingStyle(CSSPropertyValueSet* style,
+ InputEvent::InputType input_type) {
+ if (!style || style->IsEmpty()) {
+ ClearTypingStyle();
+ return;
+ }
+
+ // Calculate the current typing style.
+ if (typing_style_)
+ typing_style_->OverrideWithStyle(style);
+ else
+ typing_style_ = EditingStyle::Create(style);
+
+ typing_style_->PrepareToApplyAt(
+ GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .VisibleStart()
+ .DeepEquivalent(),
+ EditingStyle::kPreserveWritingDirection);
+
+ // Handle block styles, substracting these from the typing style.
+ EditingStyle* block_style = typing_style_->ExtractAndRemoveBlockProperties();
+ if (!block_style->IsEmpty()) {
+ DCHECK(GetFrame().GetDocument());
+ ApplyStyleCommand::Create(*GetFrame().GetDocument(), block_style,
+ input_type)
+ ->Apply();
+ }
+}
+
+bool Editor::FindString(LocalFrame& frame,
+ const String& target,
+ FindOptions options) {
+ VisibleSelection selection =
+ frame.Selection().ComputeVisibleSelectionInDOMTreeDeprecated();
+
+ // TODO(yosin) We should make |findRangeOfString()| to return
+ // |EphemeralRange| rather than|Range| object.
+ Range* const result_range =
+ FindRangeOfString(*frame.GetDocument(), target,
+ EphemeralRange(selection.Start(), selection.End()),
+ static_cast<FindOptions>(options | kFindAPICall));
+
+ if (!result_range)
+ return false;
+
+ frame.Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(EphemeralRange(result_range))
+ .Build());
+ frame.Selection().RevealSelection();
+ return true;
+}
+
+// TODO(yosin) We should return |EphemeralRange| rather than |Range|. We use
+// |Range| object for checking whether start and end position crossing shadow
+// boundaries, however we can do it without |Range| object.
+template <typename Strategy>
+static Range* FindStringBetweenPositions(
+ const String& target,
+ const EphemeralRangeTemplate<Strategy>& reference_range,
+ FindOptions options) {
+ EphemeralRangeTemplate<Strategy> search_range(reference_range);
+
+ bool forward = !(options & kBackwards);
+
+ while (true) {
+ EphemeralRangeTemplate<Strategy> result_range =
+ FindPlainText(search_range, target, options);
+ if (result_range.IsCollapsed())
+ return nullptr;
+
+ Range* range_object =
+ Range::Create(result_range.GetDocument(),
+ ToPositionInDOMTree(result_range.StartPosition()),
+ ToPositionInDOMTree(result_range.EndPosition()));
+ if (!range_object->collapsed())
+ return range_object;
+
+ // Found text spans over multiple TreeScopes. Since it's impossible to
+ // return such section as a Range, we skip this match and seek for the
+ // next occurrence.
+ // TODO(yosin) Handle this case.
+ if (forward) {
+ search_range = EphemeralRangeTemplate<Strategy>(
+ NextPositionOf(result_range.StartPosition(),
+ PositionMoveType::kGraphemeCluster),
+ search_range.EndPosition());
+ } else {
+ search_range = EphemeralRangeTemplate<Strategy>(
+ search_range.StartPosition(),
+ PreviousPositionOf(result_range.EndPosition(),
+ PositionMoveType::kGraphemeCluster));
+ }
+ }
+
+ NOTREACHED();
+ return nullptr;
+}
+
+template <typename Strategy>
+static Range* FindRangeOfStringAlgorithm(
+ Document& document,
+ const String& target,
+ const EphemeralRangeTemplate<Strategy>& reference_range,
+ FindOptions options) {
+ if (target.IsEmpty())
+ return nullptr;
+
+ // Start from an edge of the reference range. Which edge is used depends on
+ // whether we're searching forward or backward, and whether startInSelection
+ // is set.
+ EphemeralRangeTemplate<Strategy> document_range =
+ EphemeralRangeTemplate<Strategy>::RangeOfContents(document);
+ EphemeralRangeTemplate<Strategy> search_range(document_range);
+
+ bool forward = !(options & kBackwards);
+ bool start_in_reference_range = false;
+ if (reference_range.IsNotNull()) {
+ start_in_reference_range = options & kStartInSelection;
+ if (forward && start_in_reference_range)
+ search_range = EphemeralRangeTemplate<Strategy>(
+ reference_range.StartPosition(), document_range.EndPosition());
+ else if (forward)
+ search_range = EphemeralRangeTemplate<Strategy>(
+ reference_range.EndPosition(), document_range.EndPosition());
+ else if (start_in_reference_range)
+ search_range = EphemeralRangeTemplate<Strategy>(
+ document_range.StartPosition(), reference_range.EndPosition());
+ else
+ search_range = EphemeralRangeTemplate<Strategy>(
+ document_range.StartPosition(), reference_range.StartPosition());
+ }
+
+ Range* result_range =
+ FindStringBetweenPositions(target, search_range, options);
+
+ // If we started in the reference range and the found range exactly matches
+ // the reference range, find again. Build a selection with the found range
+ // to remove collapsed whitespace. Compare ranges instead of selection
+ // objects to ignore the way that the current selection was made.
+ if (result_range && start_in_reference_range &&
+ NormalizeRange(EphemeralRangeTemplate<Strategy>(result_range)) ==
+ reference_range) {
+ if (forward)
+ search_range = EphemeralRangeTemplate<Strategy>(
+ FromPositionInDOMTree<Strategy>(result_range->EndPosition()),
+ search_range.EndPosition());
+ else
+ search_range = EphemeralRangeTemplate<Strategy>(
+ search_range.StartPosition(),
+ FromPositionInDOMTree<Strategy>(result_range->StartPosition()));
+ result_range = FindStringBetweenPositions(target, search_range, options);
+ }
+
+ if (!result_range && options & kWrapAround)
+ return FindStringBetweenPositions(target, document_range, options);
+
+ return result_range;
+}
+
+Range* Editor::FindRangeOfString(Document& document,
+ const String& target,
+ const EphemeralRange& reference,
+ FindOptions options) {
+ return FindRangeOfStringAlgorithm<EditingStrategy>(document, target,
+ reference, options);
+}
+
+Range* Editor::FindRangeOfString(Document& document,
+ const String& target,
+ const EphemeralRangeInFlatTree& reference,
+ FindOptions options) {
+ return FindRangeOfStringAlgorithm<EditingInFlatTreeStrategy>(
+ document, target, reference, options);
+}
+
+void Editor::SetMarkedTextMatchesAreHighlighted(bool flag) {
+ if (flag == are_marked_text_matches_highlighted_)
+ return;
+
+ are_marked_text_matches_highlighted_ = flag;
+ GetFrame().GetDocument()->Markers().RepaintMarkers(
+ DocumentMarker::kTextMatch);
+}
+
+void Editor::RespondToChangedSelection() {
+ GetSpellChecker().RespondToChangedSelection();
+ frame_->Client()->DidChangeSelection(
+ GetFrameSelection().GetSelectionInDOMTree().Type() != kRangeSelection);
+ SetStartNewKillRingSequence(true);
+}
+
+SpellChecker& Editor::GetSpellChecker() const {
+ return GetFrame().GetSpellChecker();
+}
+
+FrameSelection& Editor::GetFrameSelection() const {
+ return GetFrame().Selection();
+}
+
+void Editor::SetMark() {
+ mark_ = GetFrameSelection().ComputeVisibleSelectionInDOMTree();
+ mark_is_directional_ = GetFrameSelection().IsDirectional();
+}
+
+void Editor::ToggleOverwriteModeEnabled() {
+ overwrite_mode_enabled_ = !overwrite_mode_enabled_;
+ GetFrameSelection().SetShouldShowBlockCursor(overwrite_mode_enabled_);
+}
+
+void Editor::ReplaceSelection(const String& text) {
+ DCHECK(!GetFrame().GetDocument()->NeedsLayoutTreeUpdate());
+ bool select_replacement = Behavior().ShouldSelectReplacement();
+ bool smart_replace = true;
+ ReplaceSelectionWithText(text, select_replacement, smart_replace,
+ InputEvent::InputType::kInsertReplacementText);
+}
+
+void Editor::Trace(blink::Visitor* visitor) {
+ visitor->Trace(frame_);
+ visitor->Trace(last_edit_command_);
+ visitor->Trace(undo_stack_);
+ visitor->Trace(mark_);
+ visitor->Trace(typing_style_);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/editor.h b/chromium/third_party/blink/renderer/core/editing/editor.h
new file mode 100644
index 00000000000..7b44ff8eacc
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editor.h
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITOR_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITOR_H_
+
+#include <memory>
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/editing_behavior.h"
+#include "third_party/blink/renderer/core/editing/editing_style.h"
+#include "third_party/blink/renderer/core/editing/finder/find_options.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/events/input_event.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/scroll/scroll_alignment.h"
+
+namespace blink {
+
+class CompositeEditCommand;
+class DragData;
+class EditorCommand;
+class FrameSelection;
+class LocalFrame;
+class HitTestResult;
+class KillRing;
+class SpellChecker;
+class CSSPropertyValueSet;
+class TextEvent;
+class UndoStack;
+
+enum class DeleteDirection;
+enum class DeleteMode { kSimple, kSmart };
+enum class InsertMode { kSimple, kSmart };
+enum class DragSourceType { kHTMLSource, kPlainTextSource };
+enum class EditorParagraphSeparator { kIsDiv, kIsP };
+enum class EditorCommandSource { kMenuOrKeyBinding, kDOM };
+enum class WritingDirection;
+
+class CORE_EXPORT Editor final : public GarbageCollectedFinalized<Editor> {
+ public:
+ static Editor* Create(LocalFrame&);
+ ~Editor();
+
+ CompositeEditCommand* LastEditCommand() { return last_edit_command_.Get(); }
+ void SetLastEditCommand(CompositeEditCommand* last_edit_command) {
+ last_edit_command_ = last_edit_command;
+ }
+
+ void HandleKeyboardEvent(KeyboardEvent*);
+ bool HandleTextEvent(TextEvent*);
+
+ bool CanEdit() const;
+ bool CanEditRichly() const;
+
+ bool CanCut() const;
+ bool CanCopy() const;
+ bool CanPaste() const;
+ bool CanDelete() const;
+
+ static void CountEvent(ExecutionContext*, const Event*);
+ void CopyImage(const HitTestResult&);
+
+ void RespondToChangedContents(const Position&);
+
+ void RegisterCommandGroup(CompositeEditCommand* command_group_wrapper);
+
+ void DeleteSelectionWithSmartDelete(
+ DeleteMode,
+ InputEvent::InputType,
+ const Position& reference_move_position = Position());
+
+ void ApplyParagraphStyle(CSSPropertyValueSet*, InputEvent::InputType);
+ void ApplyParagraphStyleToSelection(CSSPropertyValueSet*,
+ InputEvent::InputType);
+
+ void SetShouldStyleWithCSS(bool flag) { should_style_with_css_ = flag; }
+ bool ShouldStyleWithCSS() const { return should_style_with_css_; }
+
+ EditorCommand CreateCommand(const String& command_name)
+ const; // Command source is CommandFromMenuOrKeyBinding.
+ EditorCommand CreateCommand(const String& command_name,
+ EditorCommandSource) const;
+
+ // |Editor::executeCommand| is implementation of |WebFrame::executeCommand|
+ // rather than |Document::execCommand|.
+ bool ExecuteCommand(const String&);
+ bool ExecuteCommand(const String& command_name, const String& value);
+ bool IsCommandEnabled(const String&) const;
+
+ bool InsertText(const String&, KeyboardEvent* triggering_event);
+ bool InsertTextWithoutSendingTextEvent(
+ const String&,
+ bool select_inserted_text,
+ TextEvent* triggering_event,
+ InputEvent::InputType = InputEvent::InputType::kInsertText);
+ bool InsertLineBreak();
+ bool InsertParagraphSeparator();
+
+ bool IsOverwriteModeEnabled() const { return overwrite_mode_enabled_; }
+ void ToggleOverwriteModeEnabled();
+
+ bool CanUndo();
+ void Undo();
+ bool CanRedo();
+ void Redo();
+
+ // Exposed for IdleSpellCheckCallback only.
+ // Supposed to be used as |const UndoStack&|.
+ UndoStack& GetUndoStack() const { return *undo_stack_; }
+
+ void SetBaseWritingDirection(WritingDirection);
+
+ // smartInsertDeleteEnabled and selectTrailingWhitespaceEnabled are
+ // mutually exclusive, meaning that enabling one will disable the other.
+ bool SmartInsertDeleteEnabled() const;
+ bool IsSelectTrailingWhitespaceEnabled() const;
+
+ bool PreventRevealSelection() const { return prevent_reveal_selection_; }
+ void IncreasePreventRevealSelection() { ++prevent_reveal_selection_; }
+ void DecreasePreventRevealSelection() { --prevent_reveal_selection_; }
+
+ void SetStartNewKillRingSequence(bool);
+
+ void Clear();
+
+ SelectionInDOMTree SelectionForCommand(Event*);
+
+ KillRing& GetKillRing() const { return *kill_ring_; }
+
+ EditingBehavior Behavior() const;
+
+ EphemeralRange SelectedRange();
+
+ void AddToKillRing(const EphemeralRange&);
+
+ static bool FindString(LocalFrame&, const String&, FindOptions);
+
+ static Range* FindRangeOfString(Document&,
+ const String& target,
+ const EphemeralRange& reference_range,
+ FindOptions);
+ static Range* FindRangeOfString(
+ Document&,
+ const String& target,
+ const EphemeralRangeInFlatTree& reference_range,
+ FindOptions);
+
+ const VisibleSelection& Mark() const; // Mark, to be used as emacs uses it.
+ bool MarkIsDirectional() const;
+ void SetMark();
+
+ void ComputeAndSetTypingStyle(CSSPropertyValueSet*, InputEvent::InputType);
+
+ EphemeralRange RangeForPoint(const IntPoint&) const;
+
+ void RespondToChangedSelection();
+
+ bool MarkedTextMatchesAreHighlighted() const;
+ void SetMarkedTextMatchesAreHighlighted(bool);
+
+ void ReplaceSelectionWithFragment(DocumentFragment*,
+ bool select_replacement,
+ bool smart_replace,
+ bool match_style,
+ InputEvent::InputType);
+ void ReplaceSelectionWithText(const String&,
+ bool select_replacement,
+ bool smart_replace,
+ InputEvent::InputType);
+
+ // Implementation of WebLocalFrameImpl::replaceSelection.
+ void ReplaceSelection(const String&);
+
+ void ReplaceSelectionAfterDragging(DocumentFragment*,
+ InsertMode,
+ DragSourceType);
+
+ // Return false if frame was destroyed by event handler, should stop executing
+ // remaining actions.
+ bool DeleteSelectionAfterDraggingWithEvents(
+ Element* drag_source,
+ DeleteMode,
+ const Position& reference_move_position);
+ bool ReplaceSelectionAfterDraggingWithEvents(Element* drop_target,
+ DragData*,
+ DocumentFragment*,
+ Range* drop_caret_range,
+ InsertMode,
+ DragSourceType);
+
+ EditorParagraphSeparator DefaultParagraphSeparator() const {
+ return default_paragraph_separator_;
+ }
+ void SetDefaultParagraphSeparator(EditorParagraphSeparator separator) {
+ default_paragraph_separator_ = separator;
+ }
+
+ EditingStyle* TypingStyle() const;
+ void SetTypingStyle(EditingStyle*);
+ void ClearTypingStyle();
+
+ void Trace(blink::Visitor*);
+
+ void RevealSelectionAfterEditingOperation(
+ const ScrollAlignment& = ScrollAlignment::kAlignCenterIfNeeded);
+
+ private:
+ Member<LocalFrame> frame_;
+ Member<CompositeEditCommand> last_edit_command_;
+ const Member<UndoStack> undo_stack_;
+ int prevent_reveal_selection_;
+ bool should_start_new_kill_ring_sequence_;
+ bool should_style_with_css_;
+ const std::unique_ptr<KillRing> kill_ring_;
+ VisibleSelection mark_;
+ bool are_marked_text_matches_highlighted_;
+ EditorParagraphSeparator default_paragraph_separator_;
+ bool overwrite_mode_enabled_;
+ Member<EditingStyle> typing_style_;
+ bool mark_is_directional_ = false;
+
+ explicit Editor(LocalFrame&);
+
+ LocalFrame& GetFrame() const {
+ DCHECK(frame_);
+ return *frame_;
+ }
+
+ SpellChecker& GetSpellChecker() const;
+ FrameSelection& GetFrameSelection() const;
+
+ bool HandleEditingKeyboardEvent(KeyboardEvent*);
+
+ DISALLOW_COPY_AND_ASSIGN(Editor);
+};
+
+inline void Editor::SetStartNewKillRingSequence(bool flag) {
+ should_start_new_kill_ring_sequence_ = flag;
+}
+
+inline const VisibleSelection& Editor::Mark() const {
+ return mark_;
+}
+
+inline bool Editor::MarkIsDirectional() const {
+ return mark_is_directional_;
+}
+
+inline bool Editor::MarkedTextMatchesAreHighlighted() const {
+ return are_marked_text_matches_highlighted_;
+}
+
+inline EditingStyle* Editor::TypingStyle() const {
+ return typing_style_.Get();
+}
+
+inline void Editor::ClearTypingStyle() {
+ typing_style_.Clear();
+}
+
+inline void Editor::SetTypingStyle(EditingStyle* style) {
+ typing_style_ = style;
+}
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EDITOR_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/editor_key_bindings.cc b/chromium/third_party/blink/renderer/core/editing/editor_key_bindings.cc
new file mode 100644
index 00000000000..fc3b5e56443
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editor_key_bindings.cc
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2006, 2007 Apple, Inc. All rights reserved.
+ * Copyright (C) 2012 Google, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/editor.h"
+
+#include "third_party/blink/public/platform/web_input_event.h"
+#include "third_party/blink/renderer/core/editing/commands/editor_command.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/events/keyboard_event.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/local_frame_client.h"
+
+namespace blink {
+
+bool Editor::HandleEditingKeyboardEvent(KeyboardEvent* evt) {
+ const WebKeyboardEvent* key_event = evt->KeyEvent();
+ // do not treat this as text input if it's a system key event
+ if (!key_event || key_event->is_system_key)
+ return false;
+
+ String command_name = Behavior().InterpretKeyEvent(*evt);
+ const EditorCommand command = this->CreateCommand(command_name);
+
+ if (key_event->GetType() == WebInputEvent::kRawKeyDown) {
+ // WebKit doesn't have enough information about mode to decide how
+ // commands that just insert text if executed via Editor should be treated,
+ // so we leave it upon WebCore to either handle them immediately
+ // (e.g. Tab that changes focus) or let a keypress event be generated
+ // (e.g. Tab that inserts a Tab character, or Enter).
+ if (command.IsTextInsertion() || command_name.IsEmpty())
+ return false;
+ return command.Execute(evt);
+ }
+
+ if (command.Execute(evt))
+ return true;
+
+ if (!Behavior().ShouldInsertCharacter(*evt) || !CanEdit())
+ return false;
+
+ const Element* const focused_element =
+ frame_->GetDocument()->FocusedElement();
+ if (!focused_element) {
+ // We may lose focused element by |command.execute(evt)|.
+ return false;
+ }
+ // We should not insert text at selection start if selection doesn't have
+ // focus.
+ if (!frame_->Selection().SelectionHasFocus())
+ return false;
+
+ // Return true to prevent default action. e.g. Space key scroll.
+ if (DispatchBeforeInputInsertText(evt->target()->ToNode(), key_event->text) !=
+ DispatchEventResult::kNotCanceled)
+ return true;
+
+ return InsertText(key_event->text, evt);
+}
+
+void Editor::HandleKeyboardEvent(KeyboardEvent* evt) {
+ // Give the embedder a chance to handle the keyboard event.
+ if (frame_->Client()->HandleCurrentKeyboardEvent() ||
+ HandleEditingKeyboardEvent(evt)) {
+ evt->SetDefaultHandled();
+ }
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/editor_test.cc b/chromium/third_party/blink/renderer/core/editing/editor_test.cc
new file mode 100644
index 00000000000..ea2815cf5c9
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/editor_test.cc
@@ -0,0 +1,60 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/editor.h"
+
+#include "third_party/blink/renderer/core/clipboard/pasteboard.h"
+#include "third_party/blink/renderer/core/editing/commands/editor_command.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
+
+namespace blink {
+
+class EditorTest : public EditingTestBase {
+};
+
+TEST_F(EditorTest, copyGeneratedPassword) {
+ // Checks that if the password field has the value generated by Chrome
+ // (HTMLInputElement::shouldRevealPassword will be true), copying the field
+ // should be available.
+ const char* body_content = "<input type='password' id='password'></input>";
+ SetBodyContent(body_content);
+
+ HTMLInputElement& element =
+ ToHTMLInputElement(*GetDocument().getElementById("password"));
+
+ const String kPasswordValue = "secret";
+ element.focus();
+ element.setValue(kPasswordValue);
+ element.SetSelectionRange(0, kPasswordValue.length());
+
+ Editor& editor = GetDocument().GetFrame()->GetEditor();
+ EXPECT_FALSE(editor.CanCopy());
+
+ element.SetShouldRevealPassword(true);
+ EXPECT_TRUE(editor.CanCopy());
+}
+
+TEST_F(EditorTest, DontCopyHiddenSelections) {
+ const char* body_content =
+ "<input type=checkbox id=checkbox>"
+ "<input id=hiding value=HEY></input>";
+ SetBodyContent(body_content);
+
+ HTMLInputElement& text_control =
+ ToHTMLInputElement(*GetDocument().getElementById("hiding"));
+ text_control.select();
+
+ HTMLInputElement& checkbox =
+ ToHTMLInputElement(*GetDocument().getElementById("checkbox"));
+ checkbox.focus();
+
+ Editor& editor = GetDocument().GetFrame()->GetEditor();
+ editor.CreateCommand("Copy").Execute();
+
+ const String copied = Pasteboard::GeneralPasteboard()->PlainText();
+ EXPECT_TRUE(copied.IsEmpty()) << copied << " was copied.";
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/ephemeral_range.cc b/chromium/third_party/blink/renderer/core/editing/ephemeral_range.cc
new file mode 100644
index 00000000000..dca55e7ebee
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/ephemeral_range.cc
@@ -0,0 +1,220 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+
+#include <ostream> // NOLINT
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+
+namespace blink {
+
+namespace {
+template <typename Strategy>
+Node* CommonAncestorContainerNode(const Node* container_a,
+ const Node* container_b) {
+ if (!container_a || !container_b)
+ return nullptr;
+ return Strategy::CommonAncestor(*container_a, *container_b);
+}
+}
+
+template <typename Strategy>
+EphemeralRangeTemplate<Strategy>::EphemeralRangeTemplate(
+ const PositionTemplate<Strategy>& start,
+ const PositionTemplate<Strategy>& end)
+ : start_position_(start),
+ end_position_(end)
+#if DCHECK_IS_ON()
+ ,
+ dom_tree_version_(start.IsNull() ? 0
+ : start.GetDocument()->DomTreeVersion())
+#endif
+{
+ if (start_position_.IsNull()) {
+ DCHECK(end_position_.IsNull());
+ return;
+ }
+ DCHECK(end_position_.IsNotNull());
+ DCHECK(start_position_.IsValidFor(*start_position_.GetDocument()));
+ DCHECK(end_position_.IsValidFor(*end_position_.GetDocument()));
+ DCHECK_EQ(start_position_.GetDocument(), end_position_.GetDocument());
+ DCHECK_LE(start_position_, end_position_);
+}
+
+template <typename Strategy>
+EphemeralRangeTemplate<Strategy>::EphemeralRangeTemplate(
+ const EphemeralRangeTemplate<Strategy>& other)
+ : EphemeralRangeTemplate(other.start_position_, other.end_position_) {
+ DCHECK(other.IsValid());
+}
+
+template <typename Strategy>
+EphemeralRangeTemplate<Strategy>::EphemeralRangeTemplate(
+ const PositionTemplate<Strategy>& position)
+ : EphemeralRangeTemplate(position, position) {}
+
+template <typename Strategy>
+EphemeralRangeTemplate<Strategy>::EphemeralRangeTemplate(const Range* range) {
+ if (!range)
+ return;
+ DCHECK(range->IsConnected());
+ start_position_ = FromPositionInDOMTree<Strategy>(range->StartPosition());
+ end_position_ = FromPositionInDOMTree<Strategy>(range->EndPosition());
+#if DCHECK_IS_ON()
+ dom_tree_version_ = range->OwnerDocument().DomTreeVersion();
+#endif
+}
+
+template <typename Strategy>
+EphemeralRangeTemplate<Strategy>::EphemeralRangeTemplate() = default;
+
+template <typename Strategy>
+EphemeralRangeTemplate<Strategy>::~EphemeralRangeTemplate() = default;
+
+template <typename Strategy>
+EphemeralRangeTemplate<Strategy>& EphemeralRangeTemplate<Strategy>::operator=(
+ const EphemeralRangeTemplate<Strategy>& other) {
+ DCHECK(other.IsValid());
+ start_position_ = other.start_position_;
+ end_position_ = other.end_position_;
+#if DCHECK_IS_ON()
+ dom_tree_version_ = other.dom_tree_version_;
+#endif
+ return *this;
+}
+
+template <typename Strategy>
+bool EphemeralRangeTemplate<Strategy>::operator==(
+ const EphemeralRangeTemplate<Strategy>& other) const {
+ return StartPosition() == other.StartPosition() &&
+ EndPosition() == other.EndPosition();
+}
+
+template <typename Strategy>
+bool EphemeralRangeTemplate<Strategy>::operator!=(
+ const EphemeralRangeTemplate<Strategy>& other) const {
+ return !operator==(other);
+}
+
+template <typename Strategy>
+Document& EphemeralRangeTemplate<Strategy>::GetDocument() const {
+ DCHECK(IsNotNull());
+ return *start_position_.GetDocument();
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> EphemeralRangeTemplate<Strategy>::StartPosition()
+ const {
+ DCHECK(IsValid());
+ return start_position_;
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> EphemeralRangeTemplate<Strategy>::EndPosition()
+ const {
+ DCHECK(IsValid());
+ return end_position_;
+}
+
+template <typename Strategy>
+Node* EphemeralRangeTemplate<Strategy>::CommonAncestorContainer() const {
+ return CommonAncestorContainerNode<Strategy>(
+ start_position_.ComputeContainerNode(),
+ end_position_.ComputeContainerNode());
+}
+
+template <typename Strategy>
+bool EphemeralRangeTemplate<Strategy>::IsCollapsed() const {
+ DCHECK(IsValid());
+ return start_position_ == end_position_;
+}
+
+template <typename Strategy>
+typename EphemeralRangeTemplate<Strategy>::RangeTraversal
+EphemeralRangeTemplate<Strategy>::Nodes() const {
+ return RangeTraversal(start_position_.NodeAsRangeFirstNode(),
+ end_position_.NodeAsRangePastLastNode());
+}
+
+template <typename Strategy>
+EphemeralRangeTemplate<Strategy>
+EphemeralRangeTemplate<Strategy>::RangeOfContents(const Node& node) {
+ return EphemeralRangeTemplate<Strategy>(
+ PositionTemplate<Strategy>::FirstPositionInNode(node),
+ PositionTemplate<Strategy>::LastPositionInNode(node));
+}
+
+#if DCHECK_IS_ON()
+template <typename Strategy>
+bool EphemeralRangeTemplate<Strategy>::IsValid() const {
+ return start_position_.IsNull() ||
+ dom_tree_version_ == start_position_.GetDocument()->DomTreeVersion();
+}
+#else
+template <typename Strategy>
+bool EphemeralRangeTemplate<Strategy>::IsValid() const {
+ return true;
+}
+#endif
+
+#ifndef NDEBUG
+
+template <typename Strategy>
+void EphemeralRangeTemplate<Strategy>::ShowTreeForThis() const {
+ if (IsNull()) {
+ LOG(INFO) << "<null range>" << std::endl;
+ return;
+ }
+ LOG(INFO) << std::endl
+ << StartPosition()
+ .AnchorNode()
+ ->ToMarkedTreeString(StartPosition().AnchorNode(), "S",
+ EndPosition().AnchorNode(), "E")
+ .Utf8()
+ .data()
+ << "start: "
+ << StartPosition().ToAnchorTypeAndOffsetString().Utf8().data()
+ << std::endl
+ << "end: "
+ << EndPosition().ToAnchorTypeAndOffsetString().Utf8().data();
+}
+
+#endif
+
+Range* CreateRange(const EphemeralRange& range) {
+ if (range.IsNull())
+ return nullptr;
+ return Range::Create(range.GetDocument(), range.StartPosition(),
+ range.EndPosition());
+}
+
+template <typename Strategy>
+static std::ostream& PrintEphemeralRange(
+ std::ostream& ostream,
+ const EphemeralRangeTemplate<Strategy> range) {
+ if (range.IsNull())
+ return ostream << "null";
+ if (range.IsCollapsed())
+ return ostream << range.StartPosition();
+ return ostream << '[' << range.StartPosition() << ", " << range.EndPosition()
+ << ']';
+}
+
+std::ostream& operator<<(std::ostream& ostream, const EphemeralRange& range) {
+ return PrintEphemeralRange(ostream, range);
+}
+
+std::ostream& operator<<(std::ostream& ostream,
+ const EphemeralRangeInFlatTree& range) {
+ return PrintEphemeralRange(ostream, range);
+}
+
+template class CORE_TEMPLATE_EXPORT EphemeralRangeTemplate<EditingStrategy>;
+template class CORE_TEMPLATE_EXPORT
+ EphemeralRangeTemplate<EditingInFlatTreeStrategy>;
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/ephemeral_range.h b/chromium/third_party/blink/renderer/core/editing/ephemeral_range.h
new file mode 100644
index 00000000000..c9bc293a7be
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/ephemeral_range.h
@@ -0,0 +1,158 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EPHEMERAL_RANGE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_EPHEMERAL_RANGE_H_
+
+#include "third_party/blink/renderer/core/editing/position.h"
+
+namespace blink {
+
+class Document;
+class Range;
+
+// We should restrict access to the unwanted version of |TraversalRange::end()|
+// function.
+template <class Iterator>
+class TraversalRangeNodes : private TraversalRange<Iterator> {
+ STACK_ALLOCATED();
+
+ public:
+ using StartNodeType = typename TraversalRange<Iterator>::StartNodeType;
+ TraversalRangeNodes(const StartNodeType* start,
+ const StartNodeType* past_end_node)
+ : TraversalRange<Iterator>(start), past_end_node_(past_end_node) {}
+
+ using TraversalRange<Iterator>::begin;
+
+ Iterator end() { return Iterator(past_end_node_); }
+
+ private:
+ const Member<const StartNodeType> past_end_node_;
+};
+
+// This class acts like |TraversalNextIterator| but in addition
+// it allows to set current position and checks |m_current| pointer before
+// dereferencing.
+template <class TraversalNext>
+class CheckedTraversalNextIterator
+ : public TraversalIteratorBase<TraversalNext> {
+ STACK_ALLOCATED();
+
+ using TraversalIteratorBase<TraversalNext>::current_;
+
+ public:
+ using StartNodeType = typename TraversalNext::TraversalNodeType;
+ explicit CheckedTraversalNextIterator(const StartNodeType* start)
+ : TraversalIteratorBase<TraversalNext>(
+ const_cast<StartNodeType*>(start)) {}
+
+ void operator++() {
+ DCHECK(current_);
+ current_ = TraversalNext::Next(*current_);
+ }
+};
+
+// Unlike |Range| objects, |EphemeralRangeTemplate| objects aren't relocated.
+// You should not use |EphemeralRangeTemplate| objects after DOM modification.
+//
+// EphemeralRangeTemplate is supposed to use returning or passing start and end
+// position.
+//
+// Example usage:
+// Range* range = produceRange();
+// consumeRange(range);
+// ... no DOM modification ...
+// consumeRange2(range);
+//
+// Above code should be:
+// EphemeralRangeTemplate range = produceRange();
+// consumeRange(range);
+// ... no DOM modification ...
+// consumeRange2(range);
+//
+// Because of |Range| objects consume heap memory and inserted into |Range|
+// object list in |Document| for relocation. These operations are redundant
+// if |Range| objects doesn't live after DOM mutation.
+//
+template <typename Strategy>
+class CORE_TEMPLATE_CLASS_EXPORT EphemeralRangeTemplate final {
+ STACK_ALLOCATED();
+
+ public:
+ using RangeTraversal =
+ TraversalRangeNodes<CheckedTraversalNextIterator<Strategy>>;
+
+ EphemeralRangeTemplate(const PositionTemplate<Strategy>& start,
+ const PositionTemplate<Strategy>& end);
+ EphemeralRangeTemplate(const EphemeralRangeTemplate& other);
+ // |position| should be |Position::isNull()| or in-document.
+ explicit EphemeralRangeTemplate(
+ const PositionTemplate<Strategy>& /* position */);
+ // When |range| is nullptr, |EphemeralRangeTemplate| is |isNull()|.
+ explicit EphemeralRangeTemplate(const Range* /* range */);
+ EphemeralRangeTemplate();
+ ~EphemeralRangeTemplate();
+
+ EphemeralRangeTemplate<Strategy>& operator=(
+ const EphemeralRangeTemplate<Strategy>& other);
+
+ bool operator==(const EphemeralRangeTemplate<Strategy>& other) const;
+ bool operator!=(const EphemeralRangeTemplate<Strategy>& other) const;
+
+ Document& GetDocument() const;
+ PositionTemplate<Strategy> StartPosition() const;
+ PositionTemplate<Strategy> EndPosition() const;
+
+ Node* CommonAncestorContainer() const;
+
+ // Returns true if |m_startPositoin| == |m_endPosition| or |isNull()|.
+ bool IsCollapsed() const;
+ bool IsNull() const {
+ DCHECK(IsValid());
+ return start_position_.IsNull();
+ }
+ bool IsNotNull() const { return !IsNull(); }
+
+ RangeTraversal Nodes() const;
+
+ // |node| should be in-document and valid for anchor node of
+ // |PositionTemplate<Strategy>|.
+ static EphemeralRangeTemplate<Strategy> RangeOfContents(
+ const Node& /* node */);
+
+#ifndef NDEBUG
+ void ShowTreeForThis() const;
+#endif
+
+ private:
+ bool IsValid() const;
+
+ PositionTemplate<Strategy> start_position_;
+ PositionTemplate<Strategy> end_position_;
+#if DCHECK_IS_ON()
+ uint64_t dom_tree_version_;
+#endif
+};
+
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ EphemeralRangeTemplate<EditingStrategy>;
+using EphemeralRange = EphemeralRangeTemplate<EditingStrategy>;
+
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ EphemeralRangeTemplate<EditingInFlatTreeStrategy>;
+using EphemeralRangeInFlatTree =
+ EphemeralRangeTemplate<EditingInFlatTreeStrategy>;
+
+// Returns a newly created |Range| object from |range| or |nullptr| if
+// |range.isNull()| returns true.
+CORE_EXPORT Range* CreateRange(const EphemeralRange& /* range */);
+
+CORE_EXPORT std::ostream& operator<<(std::ostream&, const EphemeralRange&);
+CORE_EXPORT std::ostream& operator<<(std::ostream&,
+ const EphemeralRangeInFlatTree&);
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/ephemeral_range_test.cc b/chromium/third_party/blink/renderer/core/editing/ephemeral_range_test.cc
new file mode 100644
index 00000000000..930aa4ec723
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/ephemeral_range_test.cc
@@ -0,0 +1,229 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+
+#include <sstream>
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class EphemeralRangeTest : public EditingTestBase {
+ protected:
+ template <typename Traversal = NodeTraversal>
+ std::string TraverseRange(Range*) const;
+
+ template <typename Strategy>
+ std::string TraverseRange(const EphemeralRangeTemplate<Strategy>&) const;
+
+ Range* GetBodyRange() const;
+};
+
+template <typename Traversal>
+std::string EphemeralRangeTest::TraverseRange(Range* range) const {
+ std::stringstream nodes_content;
+ for (Node* node = range->FirstNode(); node != range->PastLastNode();
+ node = Traversal::Next(*node)) {
+ nodes_content << "[" << *node << "]";
+ }
+
+ return nodes_content.str();
+}
+
+template <typename Strategy>
+std::string EphemeralRangeTest::TraverseRange(
+ const EphemeralRangeTemplate<Strategy>& range) const {
+ std::stringstream nodes_content;
+ for (const Node& node : range.Nodes())
+ nodes_content << "[" << node << "]";
+
+ return nodes_content.str();
+}
+
+Range* EphemeralRangeTest::GetBodyRange() const {
+ Range* range = Range::Create(GetDocument());
+ range->selectNode(GetDocument().body());
+ return range;
+}
+
+// Tests that |EphemeralRange::nodes()| will traverse the whole range exactly as
+// |for (Node* n = firstNode(); n != pastLastNode(); n = Traversal::next(*n))|
+// does.
+TEST_F(EphemeralRangeTest, rangeTraversalDOM) {
+ const char* body_content =
+ "<p id='host'>"
+ "<b id='zero'>0</b>"
+ "<b id='one'>1</b>"
+ "<b id='two'>22</b>"
+ "<span id='three'>333</span>"
+ "</p>";
+ SetBodyContent(body_content);
+
+ const std::string expected_nodes(
+ "[BODY][P id=\"host\"][B id=\"zero\"][#text \"0\"][B id=\"one\"][#text "
+ "\"1\"][B id=\"two\"][#text \"22\"][SPAN id=\"three\"][#text \"333\"]");
+
+ // Check two ways to traverse.
+ EXPECT_EQ(expected_nodes, TraverseRange<>(GetBodyRange()));
+ EXPECT_EQ(TraverseRange<>(GetBodyRange()),
+ TraverseRange(EphemeralRange(GetBodyRange())));
+
+ EXPECT_EQ(expected_nodes, TraverseRange<FlatTreeTraversal>(GetBodyRange()));
+ EXPECT_EQ(TraverseRange<FlatTreeTraversal>(GetBodyRange()),
+ TraverseRange(EphemeralRangeInFlatTree(GetBodyRange())));
+}
+
+// Tests that |inRange| helper will traverse the whole range with shadow DOM.
+TEST_F(EphemeralRangeTest, rangeShadowTraversal) {
+ const char* body_content =
+ "<b id='zero'>0</b>"
+ "<p id='host'>"
+ "<b id='one'>1</b>"
+ "<b id='two'>22</b>"
+ "<b id='three'>333</b>"
+ "</p>"
+ "<b id='four'>4444</b>";
+ const char* shadow_content =
+ "<p id='five'>55555</p>"
+ "<content select=#two></content>"
+ "<content select=#one></content>"
+ "<span id='six'>666666</span>"
+ "<p id='seven'>7777777</p>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ const std::string expected_nodes(
+ "[BODY][B id=\"zero\"][#text \"0\"][P id=\"host\"][P id=\"five\"][#text "
+ "\"55555\"][B id=\"two\"][#text \"22\"][B id=\"one\"][#text \"1\"][SPAN "
+ "id=\"six\"][#text \"666666\"][P id=\"seven\"][#text \"7777777\"][B "
+ "id=\"four\"][#text \"4444\"]");
+
+ EXPECT_EQ(expected_nodes, TraverseRange<FlatTreeTraversal>(GetBodyRange()));
+ EXPECT_EQ(TraverseRange<FlatTreeTraversal>(GetBodyRange()),
+ TraverseRange(EphemeralRangeInFlatTree(GetBodyRange())));
+ // Node 'three' should not appear in FlatTreeTraversal.
+ EXPECT_EQ(expected_nodes.find("three") == std::string::npos, true);
+}
+
+// Limit a range and check that it will be traversed correctly.
+TEST_F(EphemeralRangeTest, rangeTraversalLimitedDOM) {
+ const char* body_content =
+ "<p id='host'>"
+ "<b id='zero'>0</b>"
+ "<b id='one'>1</b>"
+ "<b id='two'>22</b>"
+ "<span id='three'>333</span>"
+ "</p>";
+ SetBodyContent(body_content);
+
+ Range* until_b = GetBodyRange();
+ until_b->setEnd(GetDocument().getElementById("one"), 0,
+ IGNORE_EXCEPTION_FOR_TESTING);
+ EXPECT_EQ("[BODY][P id=\"host\"][B id=\"zero\"][#text \"0\"][B id=\"one\"]",
+ TraverseRange<>(until_b));
+ EXPECT_EQ(TraverseRange<>(until_b), TraverseRange(EphemeralRange(until_b)));
+
+ Range* from_b_to_span = GetBodyRange();
+ from_b_to_span->setStart(GetDocument().getElementById("one"), 0,
+ IGNORE_EXCEPTION_FOR_TESTING);
+ from_b_to_span->setEnd(GetDocument().getElementById("three"), 0,
+ IGNORE_EXCEPTION_FOR_TESTING);
+ EXPECT_EQ("[#text \"1\"][B id=\"two\"][#text \"22\"][SPAN id=\"three\"]",
+ TraverseRange<>(from_b_to_span));
+ EXPECT_EQ(TraverseRange<>(from_b_to_span),
+ TraverseRange(EphemeralRange(from_b_to_span)));
+}
+
+TEST_F(EphemeralRangeTest, rangeTraversalLimitedFlatTree) {
+ const char* body_content =
+ "<b id='zero'>0</b>"
+ "<p id='host'>"
+ "<b id='one'>1</b>"
+ "<b id='two'>22</b>"
+ "</p>"
+ "<b id='three'>333</b>";
+ const char* shadow_content =
+ "<p id='four'>4444</p>"
+ "<content select=#two></content>"
+ "<content select=#one></content>"
+ "<span id='five'>55555</span>"
+ "<p id='six'>666666</p>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ const PositionInFlatTree start_position(GetDocument().getElementById("one"),
+ 0);
+ const PositionInFlatTree limit_position(shadow_root->getElementById("five"),
+ 0);
+ const PositionInFlatTree end_position(shadow_root->getElementById("six"), 0);
+ const EphemeralRangeInFlatTree from_b_to_span(start_position, limit_position);
+ EXPECT_EQ("[#text \"1\"][SPAN id=\"five\"]", TraverseRange(from_b_to_span));
+
+ const EphemeralRangeInFlatTree from_span_to_end(limit_position, end_position);
+ EXPECT_EQ("[#text \"55555\"][P id=\"six\"]", TraverseRange(from_span_to_end));
+}
+
+TEST_F(EphemeralRangeTest, traversalEmptyRanges) {
+ const char* body_content =
+ "<p id='host'>"
+ "<b id='one'>1</b>"
+ "</p>";
+ SetBodyContent(body_content);
+
+ // Expect no iterations in loop for an empty EphemeralRange.
+ EXPECT_EQ(std::string(), TraverseRange(EphemeralRange()));
+
+ auto iterable = EphemeralRange().Nodes();
+ // Tree iterators have only |operator !=| ATM.
+ EXPECT_FALSE(iterable.begin() != iterable.end());
+
+ const EphemeralRange single_position_range(GetBodyRange()->StartPosition());
+ EXPECT_FALSE(single_position_range.IsNull());
+ EXPECT_EQ(std::string(), TraverseRange(single_position_range));
+ EXPECT_EQ(single_position_range.StartPosition().NodeAsRangeFirstNode(),
+ single_position_range.EndPosition().NodeAsRangePastLastNode());
+}
+
+TEST_F(EphemeralRangeTest, commonAncesstorDOM) {
+ const char* body_content =
+ "<p id='host'>00"
+ "<b id='one'>11</b>"
+ "<b id='two'>22</b>"
+ "<b id='three'>33</b>"
+ "</p>";
+ SetBodyContent(body_content);
+
+ const Position start_position(GetDocument().getElementById("one"), 0);
+ const Position end_position(GetDocument().getElementById("two"), 0);
+ const EphemeralRange range(start_position, end_position);
+ EXPECT_EQ(GetDocument().getElementById("host"),
+ range.CommonAncestorContainer());
+}
+
+TEST_F(EphemeralRangeTest, commonAncesstorFlatTree) {
+ const char* body_content =
+ "<b id='zero'>0</b>"
+ "<p id='host'>"
+ "<b id='one'>1</b>"
+ "<b id='two'>22</b>"
+ "</p>"
+ "<b id='three'>333</b>";
+ const char* shadow_content =
+ "<p id='four'>4444</p>"
+ "<content select=#two></content>"
+ "<content select=#one></content>"
+ "<p id='five'>55555</p>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ const PositionInFlatTree start_position(GetDocument().getElementById("one"),
+ 0);
+ const PositionInFlatTree end_position(shadow_root->getElementById("five"), 0);
+ const EphemeralRangeInFlatTree range(start_position, end_position);
+ EXPECT_EQ(GetDocument().getElementById("host"),
+ range.CommonAncestorContainer());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/finder/find_in_page_coordinates.cc b/chromium/third_party/blink/renderer/core/editing/finder/find_in_page_coordinates.cc
new file mode 100644
index 00000000000..a3f06fe3c16
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/finder/find_in_page_coordinates.cc
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2012 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/finder/find_in_page_coordinates.h"
+
+#include "third_party/blink/renderer/core/dom/node.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/layout/layout_block.h"
+#include "third_party/blink/renderer/core/layout/layout_box.h"
+#include "third_party/blink/renderer/core/layout/layout_embedded_content.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/core/layout/layout_view.h"
+#include "third_party/blink/renderer/platform/geometry/float_point.h"
+#include "third_party/blink/renderer/platform/geometry/float_quad.h"
+#include "third_party/blink/renderer/platform/geometry/int_point.h"
+
+namespace blink {
+
+static const LayoutBlock* EnclosingScrollableAncestor(
+ const LayoutObject* layout_object) {
+ DCHECK(!layout_object->IsLayoutView());
+
+ // Trace up the containingBlocks until we reach either the layoutObject view
+ // or a scrollable object.
+ const LayoutBlock* container = layout_object->ContainingBlock();
+ while (!container->HasOverflowClip() && !container->IsLayoutView())
+ container = container->ContainingBlock();
+ return container;
+}
+
+static FloatRect ToNormalizedRect(const FloatRect& absolute_rect,
+ const LayoutObject* layout_object,
+ const LayoutBlock* container) {
+ DCHECK(layout_object);
+
+ DCHECK(container || layout_object->IsLayoutView());
+ if (!container)
+ return FloatRect();
+
+ // We want to normalize by the max layout overflow size instead of only the
+ // visible bounding box. Quads and their enclosing bounding boxes need to be
+ // used in order to keep results transform-friendly.
+ FloatPoint scrolled_origin;
+
+ // For overflow:scroll we need to get where the actual origin is independently
+ // of the scroll.
+ if (container->HasOverflowClip())
+ scrolled_origin = -IntPoint(container->ScrolledContentOffset());
+
+ FloatRect overflow_rect(scrolled_origin,
+ FloatSize(container->MaxLayoutOverflow()));
+ FloatRect container_rect =
+ container->LocalToAbsoluteQuad(FloatQuad(overflow_rect))
+ .EnclosingBoundingBox();
+
+ if (container_rect.IsEmpty())
+ return FloatRect();
+
+ // Make the coordinates relative to the container enclosing bounding box.
+ // Since we work with rects enclosing quad unions this is still
+ // transform-friendly.
+ FloatRect normalized_rect = absolute_rect;
+ normalized_rect.MoveBy(-container_rect.Location());
+
+ // Fixed positions do not make sense in this coordinate system, but need to
+ // leave consistent tickmarks. So, use their position when the view is not
+ // scrolled, like an absolute position.
+ if (layout_object->Style()->GetPosition() == EPosition::kFixed &&
+ container->IsLayoutView()) {
+ normalized_rect.Move(
+ -ToLayoutView(container)->GetFrameView()->GetScrollOffset());
+ }
+
+ normalized_rect.Scale(1 / container_rect.Width(),
+ 1 / container_rect.Height());
+ return normalized_rect;
+}
+
+FloatRect FindInPageRectFromAbsoluteRect(
+ const FloatRect& input_rect,
+ const LayoutObject* base_layout_object) {
+ if (!base_layout_object || input_rect.IsEmpty())
+ return FloatRect();
+
+ // Normalize the input rect to its container block.
+ const LayoutBlock* base_container =
+ EnclosingScrollableAncestor(base_layout_object);
+ FloatRect normalized_rect =
+ ToNormalizedRect(input_rect, base_layout_object, base_container);
+
+ // Go up across frames.
+ for (const LayoutBox* layout_object = base_container; layout_object;) {
+ // Go up the layout tree until we reach the root of the current frame (the
+ // LayoutView).
+ while (!layout_object->IsLayoutView()) {
+ const LayoutBlock* container = EnclosingScrollableAncestor(layout_object);
+
+ // Compose the normalized rects.
+ FloatRect normalized_box_rect = ToNormalizedRect(
+ layout_object->AbsoluteBoundingBoxRect(), layout_object, container);
+ normalized_rect.Scale(normalized_box_rect.Width(),
+ normalized_box_rect.Height());
+ normalized_rect.MoveBy(normalized_box_rect.Location());
+
+ layout_object = container;
+ }
+
+ DCHECK(layout_object->IsLayoutView());
+
+ // Jump to the layoutObject owning the frame, if any.
+ layout_object = layout_object->GetFrame()
+ ? layout_object->GetFrame()->OwnerLayoutObject()
+ : nullptr;
+ }
+
+ return normalized_rect;
+}
+
+FloatRect FindInPageRectFromRange(const EphemeralRange& range) {
+ if (range.IsNull() || !range.StartPosition().NodeAsRangeFirstNode())
+ return FloatRect();
+
+ const LayoutObject* const baseLayoutObject =
+ range.StartPosition().NodeAsRangeFirstNode()->GetLayoutObject();
+ if (!baseLayoutObject)
+ return FloatRect();
+
+ return FindInPageRectFromAbsoluteRect(
+ LayoutObject::AbsoluteBoundingBoxRectForRange(range), baseLayoutObject);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/finder/find_in_page_coordinates.h b/chromium/third_party/blink/renderer/core/editing/finder/find_in_page_coordinates.h
new file mode 100644
index 00000000000..365fe4caeb0
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/finder/find_in_page_coordinates.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2012 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FINDER_FIND_IN_PAGE_COORDINATES_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FINDER_FIND_IN_PAGE_COORDINATES_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/geometry/float_rect.h"
+
+namespace blink {
+class LayoutObject;
+
+// Find-in-page coordinate conversion methods.
+//
+// This coordinate system is designed to give consistent tickmarks in cases
+// where find matches are in scrollable areas but might not be visible (e.g.
+// child frames, scroll:overflow). In these cases, using absolute positions
+// might lead to tickmarks pointing outside the visible area of its container,
+// which is counter-intuitive for users.
+//
+// Find-in-page coordinates are represented as normalized fractions of the main
+// frame document with the property that they are built by composing the
+// relative position of each layoutObject to the maximum effective layout size
+// of its container all the way up the layout tree. The resulting coordinates
+// are scroll-independent, representing any contents scaled to the visible area
+// of their container. The provided methods support scroll:overflow and are
+// CSS position and transform-friendly.
+
+CORE_EXPORT FloatRect FindInPageRectFromAbsoluteRect(const FloatRect&,
+ const LayoutObject*);
+CORE_EXPORT FloatRect FindInPageRectFromRange(const EphemeralRange&);
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/finder/find_options.h b/chromium/third_party/blink/renderer/core/editing/finder/find_options.h
new file mode 100644
index 00000000000..06c986371da
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/finder/find_options.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2010 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FINDER_FIND_OPTIONS_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FINDER_FIND_OPTIONS_H_
+
+namespace blink {
+
+enum FindOptionFlag {
+ kCaseInsensitive = 1 << 0,
+ kAtWordStarts = 1 << 1,
+ // When combined with AtWordStarts, accepts a match in the middle of a word if
+ // the match begins with an uppercase letter followed by a lowercase or
+ // non-letter. Accepts several other intra-word matches.
+ kTreatMedialCapitalAsWordStart = 1 << 2,
+ kBackwards = 1 << 3,
+ kWrapAround = 1 << 4,
+ kStartInSelection = 1 << 5,
+ kWholeWord = 1 << 6, // WholeWord should imply AtWordStarts
+ // TODO(yosin) Once find UI works on flat tree and it doesn't use
+ // |rangeOfString()|, we should get rid of |FindAPICall| enum member.
+ kFindAPICall = 1 << 7, // Used for Window.find or execCommand('find')
+};
+
+typedef unsigned FindOptions;
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FINDER_FIND_OPTIONS_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/finder/text_finder.cc b/chromium/third_party/blink/renderer/core/editing/finder/text_finder.cc
new file mode 100644
index 00000000000..ac3287f5cf5
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/finder/text_finder.cc
@@ -0,0 +1,925 @@
+/*
+ * Copyright (C) 2009 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/finder/text_finder.h"
+
+#include "third_party/blink/public/platform/task_type.h"
+#include "third_party/blink/public/platform/web_float_rect.h"
+#include "third_party/blink/public/platform/web_scroll_into_view_params.h"
+#include "third_party/blink/public/platform/web_vector.h"
+#include "third_party/blink/public/web/web_find_options.h"
+#include "third_party/blink/public/web/web_frame_client.h"
+#include "third_party/blink/public/web/web_view_client.h"
+#include "third_party/blink/renderer/core/dom/ax_object_cache_base.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/dom/shadow_root.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/finder/find_in_page_coordinates.h"
+#include "third_party/blink/renderer/core/editing/finder/find_options.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/iterators/search_buffer.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/exported/web_view_impl.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/core/layout/text_autosizer.h"
+#include "third_party/blink/renderer/core/page/page.h"
+#include "third_party/blink/renderer/platform/timer.h"
+#include "third_party/blink/renderer/platform/wtf/time.h"
+
+namespace blink {
+
+TextFinder::FindMatch::FindMatch(Range* range, int ordinal)
+ : range_(range), ordinal_(ordinal) {}
+
+void TextFinder::FindMatch::Trace(blink::Visitor* visitor) {
+ visitor->Trace(range_);
+}
+
+class TextFinder::DeferredScopeStringMatches
+ : public GarbageCollectedFinalized<TextFinder::DeferredScopeStringMatches> {
+ public:
+ static DeferredScopeStringMatches* Create(TextFinder* text_finder,
+ int identifier,
+ const WebString& search_text,
+ const WebFindOptions& options) {
+ return new DeferredScopeStringMatches(text_finder, identifier, search_text,
+ options);
+ }
+
+ void Trace(blink::Visitor* visitor) { visitor->Trace(text_finder_); }
+
+ void Dispose() { timer_.Stop(); }
+
+ private:
+ DeferredScopeStringMatches(TextFinder* text_finder,
+ int identifier,
+ const WebString& search_text,
+ const WebFindOptions& options)
+ : timer_(text_finder->OwnerFrame().GetFrame()->GetTaskRunner(
+ TaskType::kUnspecedTimer),
+ this,
+ &DeferredScopeStringMatches::DoTimeout),
+ text_finder_(text_finder),
+ identifier_(identifier),
+ search_text_(search_text),
+ options_(options) {
+ timer_.StartOneShot(TimeDelta(), FROM_HERE);
+ }
+
+ void DoTimeout(TimerBase*) {
+ text_finder_->ResumeScopingStringMatches(identifier_, search_text_,
+ options_);
+ }
+
+ TaskRunnerTimer<DeferredScopeStringMatches> timer_;
+ Member<TextFinder> text_finder_;
+ const int identifier_;
+ const WebString search_text_;
+ const WebFindOptions options_;
+};
+
+static void ScrollToVisible(Range* match) {
+ const Node& first_node = *match->FirstNode();
+ Settings* settings = first_node.GetDocument().GetSettings();
+ bool smooth_find_enabled =
+ settings ? settings->GetSmoothScrollForFindEnabled() : false;
+ ScrollBehavior scroll_behavior =
+ smooth_find_enabled ? kScrollBehaviorSmooth : kScrollBehaviorAuto;
+ first_node.GetLayoutObject()->ScrollRectToVisible(
+ LayoutRect(match->BoundingBox()),
+ WebScrollIntoViewParams(ScrollAlignment::kAlignCenterIfNeeded,
+ ScrollAlignment::kAlignCenterIfNeeded,
+ kUserScroll, false, scroll_behavior, true));
+ first_node.GetDocument().SetSequentialFocusNavigationStartingPoint(
+ const_cast<Node*>(&first_node));
+}
+
+bool TextFinder::Find(int identifier,
+ const WebString& search_text,
+ const WebFindOptions& options,
+ bool wrap_within_frame,
+ bool* active_now) {
+ if (!options.find_next)
+ UnmarkAllTextMatches();
+ else
+ SetMarkerActive(active_match_.Get(), false);
+
+ if (active_match_ &&
+ &active_match_->OwnerDocument() != OwnerFrame().GetFrame()->GetDocument())
+ active_match_ = nullptr;
+
+ // If the user has selected something since the last Find operation we want
+ // to start from there. Otherwise, we start searching from where the last Find
+ // operation left off (either a Find or a FindNext operation).
+ // TODO(editing-dev): The use of VisibleSelection should be audited. See
+ // crbug.com/657237 for details.
+ VisibleSelection selection(
+ OwnerFrame().GetFrame()->Selection().ComputeVisibleSelectionInDOMTree());
+ bool active_selection = !selection.IsNone();
+ if (active_selection) {
+ active_match_ = CreateRange(FirstEphemeralRangeOf(selection));
+ OwnerFrame().GetFrame()->Selection().Clear();
+ }
+
+ DCHECK(OwnerFrame().GetFrame());
+ DCHECK(OwnerFrame().GetFrame()->View());
+ const FindOptions find_options =
+ (options.forward ? 0 : kBackwards) |
+ (options.match_case ? 0 : kCaseInsensitive) |
+ (wrap_within_frame ? kWrapAround : 0) |
+ (options.word_start ? kAtWordStarts : 0) |
+ (options.medial_capital_as_word_start ? kTreatMedialCapitalAsWordStart
+ : 0) |
+ (options.find_next ? 0 : kStartInSelection);
+ active_match_ = Editor::FindRangeOfString(
+ *OwnerFrame().GetFrame()->GetDocument(), search_text,
+ EphemeralRangeInFlatTree(active_match_.Get()), find_options);
+
+ if (!active_match_) {
+ // If we're finding next the next active match might not be in the current
+ // frame. In this case we don't want to clear the matches cache.
+ if (!options.find_next)
+ ClearFindMatchesCache();
+
+ OwnerFrame().GetFrameView()->InvalidatePaintForTickmarks();
+ return false;
+ }
+ ScrollToVisible(active_match_);
+
+ // If the user is browsing a page with autosizing, adjust the zoom to the
+ // column where the next hit has been found. Doing this when autosizing is
+ // not set will result in a zoom reset on small devices.
+ if (OwnerFrame()
+ .GetFrame()
+ ->GetDocument()
+ ->GetTextAutosizer()
+ ->PageNeedsAutosizing()) {
+ OwnerFrame().ViewImpl()->ZoomToFindInPageRect(
+ OwnerFrame().GetFrameView()->AbsoluteToRootFrame(
+ EnclosingIntRect(LayoutObject::AbsoluteBoundingBoxRectForRange(
+ EphemeralRange(active_match_.Get())))));
+ }
+
+ bool was_active_frame = current_active_match_frame_;
+ current_active_match_frame_ = true;
+
+ bool is_active = SetMarkerActive(active_match_.Get(), true);
+ if (active_now)
+ *active_now = is_active;
+
+ // Make sure no node is focused. See http://crbug.com/38700.
+ OwnerFrame().GetFrame()->GetDocument()->ClearFocusedElement();
+
+ // Set this frame as focused.
+ OwnerFrame().ViewImpl()->SetFocusedFrame(&OwnerFrame());
+
+ if (!options.find_next || active_selection || !is_active) {
+ // This is either an initial Find operation, a Find-next from a new
+ // start point due to a selection, or new matches were found during
+ // Find-next due to DOM alteration (that couldn't be set as active), so
+ // we set the flag to ask the scoping effort to find the active rect for
+ // us and report it back to the UI.
+ locating_active_rect_ = true;
+ } else {
+ if (!was_active_frame) {
+ if (options.forward)
+ active_match_index_ = 0;
+ else
+ active_match_index_ = last_match_count_ - 1;
+ } else {
+ if (options.forward)
+ ++active_match_index_;
+ else
+ --active_match_index_;
+
+ if (active_match_index_ + 1 > last_match_count_)
+ active_match_index_ = 0;
+ else if (active_match_index_ < 0)
+ active_match_index_ = last_match_count_ - 1;
+ }
+ WebRect selection_rect = OwnerFrame().GetFrameView()->AbsoluteToRootFrame(
+ active_match_->BoundingBox());
+ ReportFindInPageSelection(selection_rect, active_match_index_ + 1,
+ identifier);
+ }
+
+ // We found something, so the result of the previous scoping may be outdated.
+ last_find_request_completed_with_no_matches_ = false;
+
+ return true;
+}
+
+void TextFinder::ClearActiveFindMatch() {
+ current_active_match_frame_ = false;
+ SetMarkerActive(active_match_.Get(), false);
+ ResetActiveMatch();
+}
+
+LocalFrame* TextFinder::GetFrame() const {
+ return OwnerFrame().GetFrame();
+}
+
+void TextFinder::SetFindEndstateFocusAndSelection() {
+ if (!ActiveMatchFrame())
+ return;
+
+ Range* active_match = ActiveMatch();
+ if (!active_match)
+ return;
+
+ // If the user has set the selection since the match was found, we
+ // don't focus anything.
+ if (!GetFrame()->Selection().GetSelectionInDOMTree().IsNone())
+ return;
+
+ // Need to clean out style and layout state before querying
+ // Element::isFocusable().
+ GetFrame()->GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Try to find the first focusable node up the chain, which will, for
+ // example, focus links if we have found text within the link.
+ Node* node = active_match->FirstNode();
+ if (node && node->IsInShadowTree()) {
+ if (Node* host = node->OwnerShadowHost()) {
+ if (IsHTMLInputElement(*host) || IsHTMLTextAreaElement(*host))
+ node = host;
+ }
+ }
+ const EphemeralRange active_match_range(active_match);
+ if (node) {
+ for (Node& runner : NodeTraversal::InclusiveAncestorsOf(*node)) {
+ if (!runner.IsElementNode())
+ continue;
+ Element& element = ToElement(runner);
+ if (element.IsFocusable()) {
+ // Found a focusable parent node. Set the active match as the
+ // selection and focus to the focusable node.
+ GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(active_match_range)
+ .Build());
+ GetFrame()->GetDocument()->SetFocusedElement(
+ &element, FocusParams(SelectionBehaviorOnFocus::kNone,
+ kWebFocusTypeNone, nullptr));
+ return;
+ }
+ }
+ }
+
+ // Iterate over all the nodes in the range until we find a focusable node.
+ // This, for example, sets focus to the first link if you search for
+ // text and text that is within one or more links.
+ for (Node& runner : active_match_range.Nodes()) {
+ if (!runner.IsElementNode())
+ continue;
+ Element& element = ToElement(runner);
+ if (element.IsFocusable()) {
+ GetFrame()->GetDocument()->SetFocusedElement(
+ &element, FocusParams(SelectionBehaviorOnFocus::kNone,
+ kWebFocusTypeNone, nullptr));
+ return;
+ }
+ }
+
+ // No node related to the active match was focusable, so set the
+ // active match as the selection (so that when you end the Find session,
+ // you'll have the last thing you found highlighted) and make sure that
+ // we have nothing focused (otherwise you might have text selected but
+ // a link focused, which is weird).
+ GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(active_match_range)
+ .Build());
+ GetFrame()->GetDocument()->ClearFocusedElement();
+
+ // Finally clear the active match, for two reasons:
+ // We just finished the find 'session' and we don't want future (potentially
+ // unrelated) find 'sessions' operations to start at the same place.
+ // The WebLocalFrameImpl could get reused and the activeMatch could end up
+ // pointing to a document that is no longer valid. Keeping an invalid
+ // reference around is just asking for trouble.
+ ResetActiveMatch();
+}
+
+void TextFinder::StopFindingAndClearSelection() {
+ CancelPendingScopingEffort();
+
+ // Remove all markers for matches found and turn off the highlighting.
+ OwnerFrame().GetFrame()->GetDocument()->Markers().RemoveMarkersOfTypes(
+ DocumentMarker::kTextMatch);
+ OwnerFrame().GetFrame()->GetEditor().SetMarkedTextMatchesAreHighlighted(
+ false);
+ ClearFindMatchesCache();
+ ResetActiveMatch();
+
+ // Let the frame know that we don't want tickmarks anymore.
+ OwnerFrame().GetFrameView()->InvalidatePaintForTickmarks();
+}
+
+void TextFinder::ReportFindInPageResultToAccessibility(int identifier) {
+ if (!active_match_)
+ return;
+
+ AXObjectCacheBase* ax_object_cache = ToAXObjectCacheBase(
+ OwnerFrame().GetFrame()->GetDocument()->ExistingAXObjectCache());
+ if (!ax_object_cache)
+ return;
+
+ Node* start_node = active_match_->startContainer();
+ Node* end_node = active_match_->endContainer();
+ ax_object_cache->HandleTextMarkerDataAdded(start_node, end_node);
+
+ if (OwnerFrame().Client()) {
+ OwnerFrame().Client()->HandleAccessibilityFindInPageResult(
+ identifier, active_match_index_ + 1, blink::WebNode(start_node),
+ active_match_->startOffset(), blink::WebNode(end_node),
+ active_match_->endOffset());
+ }
+}
+
+void TextFinder::StartScopingStringMatches(int identifier,
+ const WebString& search_text,
+ const WebFindOptions& options) {
+ CancelPendingScopingEffort();
+
+ // This is a brand new search, so we need to reset everything.
+ // Scoping is just about to begin.
+ scoping_in_progress_ = true;
+
+ // Need to keep the current identifier locally in order to finish the
+ // request in case the frame is detached during the process.
+ find_request_identifier_ = identifier;
+
+ // Clear highlighting for this frame.
+ UnmarkAllTextMatches();
+
+ // Clear the tickmarks and results cache.
+ ClearFindMatchesCache();
+
+ // Clear the total match count and increment markers version.
+ ResetMatchCount();
+
+ // Clear the counters from last operation.
+ last_match_count_ = 0;
+ next_invalidate_after_ = 0;
+
+ // The view might be null on detached frames.
+ LocalFrame* frame = OwnerFrame().GetFrame();
+ if (frame && frame->GetPage())
+ frame_scoping_ = true;
+
+ // Now, defer scoping until later to allow find operation to finish quickly.
+ ScopeStringMatchesSoon(identifier, search_text, options);
+}
+
+void TextFinder::ScopeStringMatches(int identifier,
+ const WebString& search_text,
+ const WebFindOptions& options) {
+ if (!ShouldScopeMatches(search_text, options)) {
+ FinishCurrentScopingEffort(identifier);
+ return;
+ }
+
+ PositionInFlatTree search_start = PositionInFlatTree::FirstPositionInNode(
+ *OwnerFrame().GetFrame()->GetDocument());
+ PositionInFlatTree search_end = PositionInFlatTree::LastPositionInNode(
+ *OwnerFrame().GetFrame()->GetDocument());
+ DCHECK_EQ(search_start.GetDocument(), search_end.GetDocument());
+
+ if (resume_scoping_from_range_) {
+ // This is a continuation of a scoping operation that timed out and didn't
+ // complete last time around, so we should start from where we left off.
+ DCHECK(resume_scoping_from_range_->collapsed());
+ search_start = FromPositionInDOMTree<EditingInFlatTreeStrategy>(
+ resume_scoping_from_range_->EndPosition());
+ if (search_start.GetDocument() != search_end.GetDocument())
+ return;
+ }
+
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ search_start.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // This timeout controls how long we scope before releasing control. This
+ // value does not prevent us from running for longer than this, but it is
+ // periodically checked to see if we have exceeded our allocated time.
+ const double kMaxScopingDuration = 0.1; // seconds
+
+ int match_count = 0;
+ bool timed_out = false;
+ double start_time = CurrentTime();
+ PositionInFlatTree next_scoping_start;
+ do {
+ // Find next occurrence of the search string.
+ // FIXME: (http://crbug.com/6818) This WebKit operation may run for longer
+ // than the timeout value, and is not interruptible as it is currently
+ // written. We may need to rewrite it with interruptibility in mind, or
+ // find an alternative.
+ const EphemeralRangeInFlatTree result =
+ FindPlainText(EphemeralRangeInFlatTree(search_start, search_end),
+ search_text, options.match_case ? 0 : kCaseInsensitive);
+ if (result.IsCollapsed()) {
+ // Not found.
+ break;
+ }
+ Range* result_range = Range::Create(
+ result.GetDocument(), ToPositionInDOMTree(result.StartPosition()),
+ ToPositionInDOMTree(result.EndPosition()));
+ if (result_range->collapsed()) {
+ // resultRange will be collapsed if the matched text spans over multiple
+ // TreeScopes. FIXME: Show such matches to users.
+ search_start = result.EndPosition();
+ continue;
+ }
+
+ ++match_count;
+
+ // Catch a special case where Find found something but doesn't know what
+ // the bounding box for it is. In this case we set the first match we find
+ // as the active rect.
+ IntRect result_bounds = result_range->BoundingBox();
+ IntRect active_selection_rect;
+ if (locating_active_rect_) {
+ active_selection_rect =
+ active_match_.Get() ? active_match_->BoundingBox() : result_bounds;
+ }
+
+ // If the Find function found a match it will have stored where the
+ // match was found in m_activeSelectionRect on the current frame. If we
+ // find this rect during scoping it means we have found the active
+ // tickmark.
+ bool found_active_match = false;
+ if (locating_active_rect_ && (active_selection_rect == result_bounds)) {
+ // We have found the active tickmark frame.
+ current_active_match_frame_ = true;
+ found_active_match = true;
+ // We also know which tickmark is active now.
+ active_match_index_ = total_match_count_ + match_count - 1;
+ // To stop looking for the active tickmark, we set this flag.
+ locating_active_rect_ = false;
+
+ // Notify browser of new location for the selected rectangle.
+ ReportFindInPageSelection(
+ OwnerFrame().GetFrameView()->AbsoluteToRootFrame(result_bounds),
+ active_match_index_ + 1, identifier);
+ }
+
+ OwnerFrame().GetFrame()->GetDocument()->Markers().AddTextMatchMarker(
+ EphemeralRange(result_range),
+ found_active_match ? TextMatchMarker::MatchStatus::kActive
+ : TextMatchMarker::MatchStatus::kInactive);
+
+ find_matches_cache_.push_back(
+ FindMatch(result_range, last_match_count_ + match_count));
+
+ // Set the new start for the search range to be the end of the previous
+ // result range. There is no need to use a VisiblePosition here,
+ // since findPlainText will use a TextIterator to go over the visible
+ // text nodes.
+ search_start = result.EndPosition();
+
+ next_scoping_start = search_start;
+ timed_out = (CurrentTime() - start_time) >= kMaxScopingDuration;
+ } while (!timed_out);
+
+ if (next_scoping_start.IsNotNull()) {
+ resume_scoping_from_range_ =
+ Range::Create(*next_scoping_start.GetDocument(),
+ ToPositionInDOMTree(next_scoping_start),
+ ToPositionInDOMTree(next_scoping_start));
+ }
+
+ // Remember what we search for last time, so we can skip searching if more
+ // letters are added to the search string (and last outcome was 0).
+ last_search_string_ = search_text;
+
+ if (match_count > 0) {
+ OwnerFrame().GetFrame()->GetEditor().SetMarkedTextMatchesAreHighlighted(
+ true);
+
+ last_match_count_ += match_count;
+
+ // Let the frame know how many matches we found during this pass.
+ OwnerFrame().IncreaseMatchCount(match_count, identifier);
+ }
+
+ if (timed_out) {
+ // If we found anything during this pass, we should redraw. However, we
+ // don't want to spam too much if the page is extremely long, so if we
+ // reach a certain point we start throttling the redraw requests.
+ if (match_count > 0)
+ InvalidateIfNecessary();
+
+ // Scoping effort ran out of time, lets ask for another time-slice.
+ ScopeStringMatchesSoon(identifier, search_text, options);
+ return; // Done for now, resume work later.
+ }
+
+ FinishCurrentScopingEffort(identifier);
+}
+
+void TextFinder::FlushCurrentScopingEffort(int identifier) {
+ if (!OwnerFrame().GetFrame() || !OwnerFrame().GetFrame()->GetPage())
+ return;
+
+ frame_scoping_ = false;
+ OwnerFrame().IncreaseMatchCount(0, identifier);
+}
+
+void TextFinder::FinishCurrentScopingEffort(int identifier) {
+ if (!total_match_count_)
+ OwnerFrame().GetFrame()->Selection().Clear();
+
+ FlushCurrentScopingEffort(identifier);
+
+ scoping_in_progress_ = false;
+ last_find_request_completed_with_no_matches_ = !last_match_count_;
+
+ // This frame is done, so show any scrollbar tickmarks we haven't drawn yet.
+ OwnerFrame().GetFrameView()->InvalidatePaintForTickmarks();
+}
+
+void TextFinder::CancelPendingScopingEffort() {
+ if (deferred_scoping_work_) {
+ deferred_scoping_work_->Dispose();
+ deferred_scoping_work_.Clear();
+ }
+
+ active_match_index_ = -1;
+
+ // Last request didn't complete.
+ if (scoping_in_progress_)
+ last_find_request_completed_with_no_matches_ = false;
+
+ scoping_in_progress_ = false;
+
+ resume_scoping_from_range_ = nullptr;
+}
+
+void TextFinder::IncreaseMatchCount(int identifier, int count) {
+ if (count)
+ ++find_match_markers_version_;
+
+ total_match_count_ += count;
+
+ // Update the UI with the latest findings.
+ if (OwnerFrame().Client()) {
+ OwnerFrame().Client()->ReportFindInPageMatchCount(
+ identifier, total_match_count_, !frame_scoping_ || !total_match_count_);
+ }
+}
+
+void TextFinder::ReportFindInPageSelection(const WebRect& selection_rect,
+ int active_match_ordinal,
+ int identifier) {
+ // Update the UI with the latest selection rect.
+ if (OwnerFrame().Client()) {
+ OwnerFrame().Client()->ReportFindInPageSelection(
+ identifier, active_match_ordinal, selection_rect);
+ }
+ // Update accessibility too, so if the user commits to this query
+ // we can move accessibility focus to this result.
+ ReportFindInPageResultToAccessibility(identifier);
+}
+
+void TextFinder::ResetMatchCount() {
+ if (total_match_count_ > 0)
+ ++find_match_markers_version_;
+
+ total_match_count_ = 0;
+ frame_scoping_ = false;
+}
+
+void TextFinder::ClearFindMatchesCache() {
+ if (!find_matches_cache_.IsEmpty())
+ ++find_match_markers_version_;
+
+ find_matches_cache_.clear();
+ find_match_rects_are_valid_ = false;
+}
+
+void TextFinder::UpdateFindMatchRects() {
+ IntSize current_document_size = OwnerFrame().DocumentSize();
+ if (document_size_for_current_find_match_rects_ != current_document_size) {
+ document_size_for_current_find_match_rects_ = current_document_size;
+ find_match_rects_are_valid_ = false;
+ }
+
+ size_t dead_matches = 0;
+ for (FindMatch& match : find_matches_cache_) {
+ if (!match.range_->BoundaryPointsValid() ||
+ !match.range_->startContainer()->isConnected())
+ match.rect_ = FloatRect();
+ else if (!find_match_rects_are_valid_)
+ match.rect_ = FindInPageRectFromRange(EphemeralRange(match.range_.Get()));
+
+ if (match.rect_.IsEmpty())
+ ++dead_matches;
+ }
+
+ // Remove any invalid matches from the cache.
+ if (dead_matches) {
+ HeapVector<FindMatch> filtered_matches;
+ filtered_matches.ReserveCapacity(find_matches_cache_.size() - dead_matches);
+
+ for (const FindMatch& match : find_matches_cache_) {
+ if (!match.rect_.IsEmpty())
+ filtered_matches.push_back(match);
+ }
+
+ find_matches_cache_.swap(filtered_matches);
+ }
+
+ // Invalidate the rects in child frames. Will be updated later during
+ // traversal.
+ if (!find_match_rects_are_valid_) {
+ for (WebFrame* child = OwnerFrame().FirstChild(); child;
+ child = child->NextSibling()) {
+ ToWebLocalFrameImpl(child)
+ ->EnsureTextFinder()
+ .find_match_rects_are_valid_ = false;
+ }
+ }
+ find_match_rects_are_valid_ = true;
+}
+
+WebFloatRect TextFinder::ActiveFindMatchRect() {
+ if (!current_active_match_frame_ || !active_match_)
+ return WebFloatRect();
+
+ return WebFloatRect(FindInPageRectFromRange(EphemeralRange(ActiveMatch())));
+}
+
+void TextFinder::FindMatchRects(WebVector<WebFloatRect>& output_rects) {
+ UpdateFindMatchRects();
+
+ Vector<WebFloatRect> match_rects;
+ match_rects.ReserveCapacity(match_rects.size() + find_matches_cache_.size());
+ for (const FindMatch& match : find_matches_cache_) {
+ DCHECK(!match.rect_.IsEmpty());
+ match_rects.push_back(match.rect_);
+ }
+
+ output_rects = match_rects;
+}
+
+int TextFinder::SelectNearestFindMatch(const WebFloatPoint& point,
+ WebRect* selection_rect) {
+ int index = NearestFindMatch(point, nullptr);
+ if (index != -1)
+ return SelectFindMatch(static_cast<unsigned>(index), selection_rect);
+
+ return -1;
+}
+
+int TextFinder::NearestFindMatch(const FloatPoint& point,
+ float* distance_squared) {
+ UpdateFindMatchRects();
+
+ int nearest = -1;
+ float nearest_distance_squared = FLT_MAX;
+ for (size_t i = 0; i < find_matches_cache_.size(); ++i) {
+ DCHECK(!find_matches_cache_[i].rect_.IsEmpty());
+ FloatSize offset = point - find_matches_cache_[i].rect_.Center();
+ float width = offset.Width();
+ float height = offset.Height();
+ float current_distance_squared = width * width + height * height;
+ if (current_distance_squared < nearest_distance_squared) {
+ nearest = i;
+ nearest_distance_squared = current_distance_squared;
+ }
+ }
+
+ if (distance_squared)
+ *distance_squared = nearest_distance_squared;
+
+ return nearest;
+}
+
+int TextFinder::SelectFindMatch(unsigned index, WebRect* selection_rect) {
+ SECURITY_DCHECK(index < find_matches_cache_.size());
+
+ Range* range = find_matches_cache_[index].range_;
+ if (!range->BoundaryPointsValid() || !range->startContainer()->isConnected())
+ return -1;
+
+ // Check if the match is already selected.
+ if (!current_active_match_frame_ || !active_match_ ||
+ !AreRangesEqual(active_match_.Get(), range)) {
+ active_match_index_ = find_matches_cache_[index].ordinal_ - 1;
+
+ // Set this frame as the active frame (the one with the active highlight).
+ current_active_match_frame_ = true;
+ OwnerFrame().ViewImpl()->SetFocusedFrame(&OwnerFrame());
+
+ if (active_match_)
+ SetMarkerActive(active_match_.Get(), false);
+ active_match_ = range;
+ SetMarkerActive(active_match_.Get(), true);
+
+ // Clear any user selection, to make sure Find Next continues on from the
+ // match we just activated.
+ OwnerFrame().GetFrame()->Selection().Clear();
+
+ // Make sure no node is focused. See http://crbug.com/38700.
+ OwnerFrame().GetFrame()->GetDocument()->ClearFocusedElement();
+ }
+
+ IntRect active_match_rect;
+ IntRect active_match_bounding_box =
+ EnclosingIntRect(LayoutObject::AbsoluteBoundingBoxRectForRange(
+ EphemeralRange(active_match_.Get())));
+
+ if (!active_match_bounding_box.IsEmpty()) {
+ if (active_match_->FirstNode() &&
+ active_match_->FirstNode()->GetLayoutObject()) {
+ active_match_->FirstNode()->GetLayoutObject()->ScrollRectToVisible(
+ LayoutRect(active_match_bounding_box),
+ WebScrollIntoViewParams(ScrollAlignment::kAlignCenterIfNeeded,
+ ScrollAlignment::kAlignCenterIfNeeded,
+ kUserScroll));
+
+ if (RuntimeEnabledFeatures::RootLayerScrollingEnabled()) {
+ // If RLS is on, absolute coordinates are scroll-variant so the
+ // bounding box will change if the page is scrolled by
+ // ScrollRectToVisible above. Recompute the bounding box so we have the
+ // updated location for the zoom below.
+ // TODO(bokan): This should really use the return value from
+ // ScrollRectToVisible which returns the updated position of the
+ // scrolled rect. However, this was recently added and this is a fix
+ // that needs to be merged to a release branch.
+ // https://crbug.com/823365.
+ active_match_bounding_box =
+ EnclosingIntRect(LayoutObject::AbsoluteBoundingBoxRectForRange(
+ EphemeralRange(active_match_.Get())));
+ }
+ }
+
+ // Zoom to the active match.
+ active_match_rect = OwnerFrame().GetFrameView()->AbsoluteToRootFrame(
+ active_match_bounding_box);
+ OwnerFrame().ViewImpl()->ZoomToFindInPageRect(active_match_rect);
+ }
+
+ if (selection_rect)
+ *selection_rect = active_match_rect;
+
+ return active_match_index_ + 1;
+}
+
+TextFinder* TextFinder::Create(WebLocalFrameImpl& owner_frame) {
+ return new TextFinder(owner_frame);
+}
+
+TextFinder::TextFinder(WebLocalFrameImpl& owner_frame)
+ : owner_frame_(&owner_frame),
+ current_active_match_frame_(false),
+ active_match_index_(-1),
+ resume_scoping_from_range_(nullptr),
+ last_match_count_(-1),
+ total_match_count_(-1),
+ frame_scoping_(false),
+ find_request_identifier_(-1),
+ next_invalidate_after_(0),
+ find_match_markers_version_(0),
+ locating_active_rect_(false),
+ scoping_in_progress_(false),
+ last_find_request_completed_with_no_matches_(false),
+ find_match_rects_are_valid_(false) {}
+
+TextFinder::~TextFinder() = default;
+
+bool TextFinder::SetMarkerActive(Range* range, bool active) {
+ if (!range || range->collapsed())
+ return false;
+ return OwnerFrame()
+ .GetFrame()
+ ->GetDocument()
+ ->Markers()
+ .SetTextMatchMarkersActive(EphemeralRange(range), active);
+}
+
+void TextFinder::UnmarkAllTextMatches() {
+ LocalFrame* frame = OwnerFrame().GetFrame();
+ if (frame && frame->GetPage() &&
+ frame->GetEditor().MarkedTextMatchesAreHighlighted()) {
+ frame->GetDocument()->Markers().RemoveMarkersOfTypes(
+ DocumentMarker::kTextMatch);
+ }
+}
+
+bool TextFinder::ShouldScopeMatches(const String& search_text,
+ const WebFindOptions& options) {
+ // Don't scope if we can't find a frame or a view.
+ // The user may have closed the tab/application, so abort.
+ LocalFrame* frame = OwnerFrame().GetFrame();
+ if (!frame || !frame->View() || !frame->GetPage())
+ return false;
+
+ DCHECK(frame->GetDocument());
+ DCHECK(frame->View());
+
+ if (options.force)
+ return true;
+
+ if (!OwnerFrame().HasVisibleContent())
+ return false;
+
+ // If the frame completed the scoping operation and found 0 matches the last
+ // time it was searched, then we don't have to search it again if the user is
+ // just adding to the search string or sending the same search string again.
+ if (last_find_request_completed_with_no_matches_ &&
+ !last_search_string_.IsEmpty()) {
+ // Check to see if the search string prefixes match.
+ String previous_search_prefix =
+ search_text.Substring(0, last_search_string_.length());
+
+ if (previous_search_prefix == last_search_string_)
+ return false; // Don't search this frame, it will be fruitless.
+ }
+
+ return true;
+}
+
+void TextFinder::ScopeStringMatchesSoon(int identifier,
+ const WebString& search_text,
+ const WebFindOptions& options) {
+ DCHECK_EQ(deferred_scoping_work_, nullptr);
+ deferred_scoping_work_ = DeferredScopeStringMatches::Create(
+ this, identifier, search_text, options);
+}
+
+void TextFinder::ResumeScopingStringMatches(int identifier,
+ const WebString& search_text,
+ const WebFindOptions& options) {
+ deferred_scoping_work_.Clear();
+
+ ScopeStringMatches(identifier, search_text, options);
+}
+
+void TextFinder::InvalidateIfNecessary() {
+ if (last_match_count_ <= next_invalidate_after_)
+ return;
+
+ // FIXME: (http://crbug.com/6819) Optimize the drawing of the tickmarks and
+ // remove this. This calculation sets a milestone for when next to
+ // invalidate the scrollbar and the content area. We do this so that we
+ // don't spend too much time drawing the scrollbar over and over again.
+ // Basically, up until the first 500 matches there is no throttle.
+ // After the first 500 matches, we set set the milestone further and
+ // further out (750, 1125, 1688, 2K, 3K).
+ static const int kStartSlowingDownAfter = 500;
+ static const int kSlowdown = 750;
+
+ int i = last_match_count_ / kStartSlowingDownAfter;
+ next_invalidate_after_ += i * kSlowdown;
+ OwnerFrame().GetFrameView()->InvalidatePaintForTickmarks();
+}
+
+void TextFinder::FlushCurrentScoping() {
+ FlushCurrentScopingEffort(find_request_identifier_);
+}
+
+void TextFinder::Trace(blink::Visitor* visitor) {
+ visitor->Trace(owner_frame_);
+ visitor->Trace(active_match_);
+ visitor->Trace(resume_scoping_from_range_);
+ visitor->Trace(deferred_scoping_work_);
+ visitor->Trace(find_matches_cache_);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/finder/text_finder.h b/chromium/third_party/blink/renderer/core/editing/finder/text_finder.h
new file mode 100644
index 00000000000..1e5b0b387b1
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/finder/text_finder.h
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2009 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FINDER_TEXT_FINDER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FINDER_TEXT_FINDER_H_
+
+#include "base/macros.h"
+#include "third_party/blink/public/platform/web_float_point.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/platform/geometry/float_rect.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+#include "third_party/blink/renderer/platform/wtf/vector.h"
+
+namespace blink {
+
+class LocalFrame;
+class Range;
+class WebLocalFrameImpl;
+class WebString;
+struct WebFindOptions;
+struct WebFloatPoint;
+struct WebFloatRect;
+struct WebRect;
+
+template <typename T>
+class WebVector;
+
+class CORE_EXPORT TextFinder final
+ : public GarbageCollectedFinalized<TextFinder> {
+ public:
+ static TextFinder* Create(WebLocalFrameImpl& owner_frame);
+
+ bool Find(int identifier,
+ const WebString& search_text,
+ const WebFindOptions&,
+ bool wrap_within_frame,
+ bool* active_now = nullptr);
+ void ClearActiveFindMatch();
+ void SetFindEndstateFocusAndSelection();
+ void StopFindingAndClearSelection();
+ void IncreaseMatchCount(int identifier, int count);
+ int FindMatchMarkersVersion() const { return find_match_markers_version_; }
+ WebFloatRect ActiveFindMatchRect();
+ void FindMatchRects(WebVector<WebFloatRect>&);
+ int SelectNearestFindMatch(const WebFloatPoint&, WebRect* selection_rect);
+
+ // Starts brand new scoping request: resets the scoping state and
+ // asyncronously calls scopeStringMatches().
+ void StartScopingStringMatches(int identifier,
+ const WebString& search_text,
+ const WebFindOptions&);
+
+ // Cancels any outstanding requests for scoping string matches on the frame.
+ void CancelPendingScopingEffort();
+
+ // This function is called to reset the total number of matches found during
+ // the scoping effort.
+ void ResetMatchCount();
+
+ // Return the index in the find-in-page cache of the match closest to the
+ // provided point in find-in-page coordinates, or -1 in case of error.
+ // The squared distance to the closest match is returned in the
+ // |distanceSquared| parameter.
+ int NearestFindMatch(const FloatPoint&, float* distance_squared);
+
+ // Returns whether this frame has the active match.
+ bool ActiveMatchFrame() const { return current_active_match_frame_; }
+
+ // Returns the active match in the current frame. Could be a null range if
+ // the local frame has no active match.
+ Range* ActiveMatch() const { return active_match_.Get(); }
+
+ void FlushCurrentScoping();
+
+ void ResetActiveMatch() { active_match_ = nullptr; }
+
+ int TotalMatchCount() const { return total_match_count_; }
+ bool ScopingInProgress() const { return scoping_in_progress_; }
+ void IncreaseMarkerVersion() { ++find_match_markers_version_; }
+
+ ~TextFinder();
+
+ class FindMatch {
+ DISALLOW_NEW_EXCEPT_PLACEMENT_NEW();
+
+ public:
+ FindMatch(Range*, int ordinal);
+
+ void Trace(blink::Visitor*);
+
+ Member<Range> range_;
+
+ // 1-based index within this frame.
+ int ordinal_;
+
+ // In find-in-page coordinates.
+ // Lazily calculated by updateFindMatchRects.
+ FloatRect rect_;
+ };
+
+ void Trace(blink::Visitor*);
+
+ private:
+ class DeferredScopeStringMatches;
+ friend class DeferredScopeStringMatches;
+
+ explicit TextFinder(WebLocalFrameImpl& owner_frame);
+
+ // Notifies the delegate about a new selection rect.
+ void ReportFindInPageSelection(const WebRect& selection_rect,
+ int active_match_ordinal,
+ int identifier);
+
+ void ReportFindInPageResultToAccessibility(int identifier);
+
+ // Clear the find-in-page matches cache forcing rects to be fully
+ // calculated again next time updateFindMatchRects is called.
+ void ClearFindMatchesCache();
+
+ // Select a find-in-page match marker in the current frame using a cache
+ // match index returned by nearestFindMatch. Returns the ordinal of the new
+ // selected match or -1 in case of error. Also provides the bounding box of
+ // the marker in window coordinates if selectionRect is not null.
+ int SelectFindMatch(unsigned index, WebRect* selection_rect);
+
+ // Compute and cache the rects for FindMatches if required.
+ // Rects are automatically invalidated in case of content size changes,
+ // propagating the invalidation to child frames.
+ void UpdateFindMatchRects();
+
+ // Sets the markers within a range as active or inactive. Returns true if at
+ // least one such marker found.
+ bool SetMarkerActive(Range*, bool active);
+
+ // Removes all markers.
+ void UnmarkAllTextMatches();
+
+ // Determines whether the scoping effort is required for a particular frame.
+ // It is not necessary if the frame is invisible, for example, or if this
+ // is a repeat search that already returned nothing last time the same prefix
+ // was searched.
+ bool ShouldScopeMatches(const WTF::String& search_text,
+ const WebFindOptions&);
+
+ // Removes the current frame from the global scoping effort and triggers any
+ // updates if appropriate. This method does not mark the scoping operation
+ // as finished.
+ void FlushCurrentScopingEffort(int identifier);
+
+ // Finishes the current scoping effort and triggers any updates if
+ // appropriate.
+ void FinishCurrentScopingEffort(int identifier);
+
+ // Counts how many times a particular string occurs within the frame. It
+ // also retrieves the location of the string and updates a vector in the
+ // frame so that tick-marks and highlighting can be drawn. This function
+ // does its work asynchronously, by running for a certain time-slice and
+ // then scheduling itself (co-operative multitasking) to be invoked later
+ // (repeating the process until all matches have been found). This allows
+ // multiple frames to be searched at the same time and provides a way to
+ // cancel at any time (see cancelPendingScopingEffort). The parameter
+ // searchText specifies what to look for.
+ void ScopeStringMatches(int identifier,
+ const WebString& search_text,
+ const WebFindOptions&);
+
+ // Queue up a deferred call to scopeStringMatches.
+ void ScopeStringMatchesSoon(int identifier,
+ const WebString& search_text,
+ const WebFindOptions&);
+
+ // Called by a DeferredScopeStringMatches instance.
+ void ResumeScopingStringMatches(int identifier,
+ const WebString& search_text,
+ const WebFindOptions&);
+
+ // Determines whether to invalidate the content area and scrollbar.
+ void InvalidateIfNecessary();
+
+ LocalFrame* GetFrame() const;
+
+ WebLocalFrameImpl& OwnerFrame() const {
+ DCHECK(owner_frame_);
+ return *owner_frame_;
+ }
+
+ Member<WebLocalFrameImpl> owner_frame_;
+
+ // Indicates whether this frame currently has the active match.
+ bool current_active_match_frame_;
+
+ // The range of the active match for the current frame.
+ Member<Range> active_match_;
+
+ // The index of the active match for the current frame.
+ int active_match_index_;
+
+ // The scoping effort can time out and we need to keep track of where we
+ // ended our last search so we can continue from where we left of.
+ //
+ // This range is collapsed to the end position of the last successful
+ // search; the new search should start from this position.
+ Member<Range> resume_scoping_from_range_;
+
+ // Keeps track of the last string this frame searched for. This is used for
+ // short-circuiting searches in the following scenarios: When a frame has
+ // been searched and returned 0 results, we don't need to search that frame
+ // again if the user is just adding to the search (making it more specific).
+ WTF::String last_search_string_;
+
+ // Keeps track of how many matches this frame has found so far, so that we
+ // don't lose count between scoping efforts, and is also used (in conjunction
+ // with m_lastSearchString) to figure out if we need to search the frame
+ // again.
+ int last_match_count_;
+
+ // This variable keeps a cumulative total of matches found so far in this
+ // frame, and is only incremented by calling IncreaseMatchCount.
+ int total_match_count_;
+
+ // Keeps track of whether the frame is currently scoping (being searched for
+ // matches).
+ bool frame_scoping_;
+
+ // Identifier of the latest find-in-page request. Required to be stored in
+ // the frame in order to reply if required in case the frame is detached.
+ int find_request_identifier_;
+
+ // Keeps track of when the scoping effort should next invalidate the scrollbar
+ // and the frame area.
+ int next_invalidate_after_;
+
+ // Pending call to scopeStringMatches.
+ Member<DeferredScopeStringMatches> deferred_scoping_work_;
+
+ // Version number incremented whenever this frame's find-in-page match
+ // markers change.
+ int find_match_markers_version_;
+
+ // Local cache of the find match markers currently displayed for this frame.
+ HeapVector<FindMatch> find_matches_cache_;
+
+ // Contents size when find-in-page match rects were last computed for this
+ // frame's cache.
+ IntSize document_size_for_current_find_match_rects_;
+
+ // This flag is used by the scoping effort to determine if we need to figure
+ // out which rectangle is the active match. Once we find the active
+ // rectangle we clear this flag.
+ bool locating_active_rect_;
+
+ // Keeps track of whether there is an scoping effort ongoing in the frame.
+ bool scoping_in_progress_;
+
+ // Keeps track of whether the last find request completed its scoping effort
+ // without finding any matches in this frame.
+ bool last_find_request_completed_with_no_matches_;
+
+ // Determines if the rects in the find-in-page matches cache of this frame
+ // are invalid and should be recomputed.
+ bool find_match_rects_are_valid_;
+
+ DISALLOW_COPY_AND_ASSIGN(TextFinder);
+};
+
+} // namespace blink
+
+WTF_ALLOW_INIT_WITH_MEM_FUNCTIONS(blink::TextFinder::FindMatch);
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FINDER_TEXT_FINDER_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/finder/text_finder_test.cc b/chromium/third_party/blink/renderer/core/editing/finder/text_finder_test.cc
new file mode 100644
index 00000000000..0da1e87a463
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/finder/text_finder_test.cc
@@ -0,0 +1,638 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/finder/text_finder.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/platform/platform.h"
+#include "third_party/blink/public/platform/web_float_rect.h"
+#include "third_party/blink/public/web/web_document.h"
+#include "third_party/blink/public/web/web_find_options.h"
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/node_list.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/dom/shadow_root.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/finder/find_in_page_coordinates.h"
+#include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/visual_viewport.h"
+#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/layout/text_autosizer.h"
+#include "third_party/blink/renderer/core/page/page.h"
+#include "third_party/blink/renderer/platform/testing/testing_platform_support.h"
+#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
+
+using blink::test::RunPendingTasks;
+
+namespace blink {
+
+class TextFinderTest : public testing::Test {
+ protected:
+ TextFinderTest() {
+ web_view_helper_.Initialize();
+ WebLocalFrameImpl& frame_impl = *web_view_helper_.LocalMainFrame();
+ frame_impl.ViewImpl()->Resize(WebSize(640, 480));
+ frame_impl.ViewImpl()->UpdateAllLifecyclePhases();
+ document_ = static_cast<Document*>(frame_impl.GetDocument());
+ text_finder_ = &frame_impl.EnsureTextFinder();
+ }
+
+ Document& GetDocument() const;
+ TextFinder& GetTextFinder() const;
+
+ static WebFloatRect FindInPageRect(Node* start_container,
+ int start_offset,
+ Node* end_container,
+ int end_offset);
+
+ private:
+ FrameTestHelpers::WebViewHelper web_view_helper_;
+ Persistent<Document> document_;
+ Persistent<TextFinder> text_finder_;
+};
+
+Document& TextFinderTest::GetDocument() const {
+ return *document_;
+}
+
+TextFinder& TextFinderTest::GetTextFinder() const {
+ return *text_finder_;
+}
+
+WebFloatRect TextFinderTest::FindInPageRect(Node* start_container,
+ int start_offset,
+ Node* end_container,
+ int end_offset) {
+ const Position start_position(start_container, start_offset);
+ const Position end_position(end_container, end_offset);
+ const EphemeralRange range(start_position, end_position);
+ return WebFloatRect(FindInPageRectFromRange(range));
+}
+
+TEST_F(TextFinderTest, FindTextSimple) {
+ GetDocument().body()->SetInnerHTMLFromString("XXXXFindMeYYYYfindmeZZZZ");
+ GetDocument().UpdateStyleAndLayout();
+ Node* text_node = GetDocument().body()->firstChild();
+
+ int identifier = 0;
+ WebString search_text(String("FindMe"));
+ WebFindOptions find_options; // Default.
+ bool wrap_within_frame = true;
+
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ Range* active_match = GetTextFinder().ActiveMatch();
+ ASSERT_TRUE(active_match);
+ EXPECT_EQ(text_node, active_match->startContainer());
+ EXPECT_EQ(4u, active_match->startOffset());
+ EXPECT_EQ(text_node, active_match->endContainer());
+ EXPECT_EQ(10u, active_match->endOffset());
+
+ find_options.find_next = true;
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ active_match = GetTextFinder().ActiveMatch();
+ ASSERT_TRUE(active_match);
+ EXPECT_EQ(text_node, active_match->startContainer());
+ EXPECT_EQ(14u, active_match->startOffset());
+ EXPECT_EQ(text_node, active_match->endContainer());
+ EXPECT_EQ(20u, active_match->endOffset());
+
+ // Should wrap to the first match.
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ active_match = GetTextFinder().ActiveMatch();
+ ASSERT_TRUE(active_match);
+ EXPECT_EQ(text_node, active_match->startContainer());
+ EXPECT_EQ(4u, active_match->startOffset());
+ EXPECT_EQ(text_node, active_match->endContainer());
+ EXPECT_EQ(10u, active_match->endOffset());
+
+ // Search in the reverse order.
+ identifier = 1;
+ find_options = WebFindOptions();
+ find_options.forward = false;
+
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ active_match = GetTextFinder().ActiveMatch();
+ ASSERT_TRUE(active_match);
+ EXPECT_EQ(text_node, active_match->startContainer());
+ EXPECT_EQ(14u, active_match->startOffset());
+ EXPECT_EQ(text_node, active_match->endContainer());
+ EXPECT_EQ(20u, active_match->endOffset());
+
+ find_options.find_next = true;
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ active_match = GetTextFinder().ActiveMatch();
+ ASSERT_TRUE(active_match);
+ EXPECT_EQ(text_node, active_match->startContainer());
+ EXPECT_EQ(4u, active_match->startOffset());
+ EXPECT_EQ(text_node, active_match->endContainer());
+ EXPECT_EQ(10u, active_match->endOffset());
+
+ // Wrap to the first match (last occurence in the document).
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ active_match = GetTextFinder().ActiveMatch();
+ ASSERT_TRUE(active_match);
+ EXPECT_EQ(text_node, active_match->startContainer());
+ EXPECT_EQ(14u, active_match->startOffset());
+ EXPECT_EQ(text_node, active_match->endContainer());
+ EXPECT_EQ(20u, active_match->endOffset());
+}
+
+TEST_F(TextFinderTest, FindTextAutosizing) {
+ GetDocument().body()->SetInnerHTMLFromString("XXXXFindMeYYYYfindmeZZZZ");
+ GetDocument().UpdateStyleAndLayout();
+
+ int identifier = 0;
+ WebString search_text(String("FindMe"));
+ WebFindOptions find_options; // Default.
+ bool wrap_within_frame = true;
+
+ // Set viewport scale to 20 in order to simulate zoom-in
+ GetDocument().GetPage()->SetDefaultPageScaleLimits(1, 20);
+ GetDocument().GetPage()->SetPageScaleFactor(20);
+ VisualViewport& visual_viewport =
+ GetDocument().GetPage()->GetVisualViewport();
+
+ // Enforce autosizing
+ GetDocument().GetSettings()->SetTextAutosizingEnabled(true);
+ GetDocument().GetSettings()->SetTextAutosizingWindowSizeOverride(
+ IntSize(20, 20));
+ GetDocument().GetTextAutosizer()->UpdatePageInfo();
+ GetDocument().UpdateStyleAndLayout();
+
+ // In case of autosizing, scale _should_ change
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ ASSERT_TRUE(GetTextFinder().ActiveMatch());
+ ASSERT_EQ(1, visual_viewport.Scale()); // in this case to 1
+
+ // Disable autosizing and reset scale to 20
+ visual_viewport.SetScale(20);
+ GetDocument().GetSettings()->SetTextAutosizingEnabled(false);
+ GetDocument().GetTextAutosizer()->UpdatePageInfo();
+ GetDocument().UpdateStyleAndLayout();
+
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ ASSERT_TRUE(GetTextFinder().ActiveMatch());
+ ASSERT_EQ(20, visual_viewport.Scale());
+}
+
+TEST_F(TextFinderTest, FindTextNotFound) {
+ GetDocument().body()->SetInnerHTMLFromString("XXXXFindMeYYYYfindmeZZZZ");
+ GetDocument().UpdateStyleAndLayout();
+
+ int identifier = 0;
+ WebString search_text(String("Boo"));
+ WebFindOptions find_options; // Default.
+ bool wrap_within_frame = true;
+
+ EXPECT_FALSE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ EXPECT_FALSE(GetTextFinder().ActiveMatch());
+}
+
+TEST_F(TextFinderTest, FindTextInShadowDOM) {
+ GetDocument().body()->SetInnerHTMLFromString("<b>FOO</b><i>foo</i>");
+ ShadowRoot& shadow_root = GetDocument().body()->CreateShadowRootInternal();
+ shadow_root.SetInnerHTMLFromString(
+ "<content select=\"i\"></content><u>Foo</u><content></content>");
+ Node* text_in_b_element = GetDocument().body()->firstChild()->firstChild();
+ Node* text_in_i_element = GetDocument().body()->lastChild()->firstChild();
+ Node* text_in_u_element = shadow_root.childNodes()->item(1)->firstChild();
+ GetDocument().UpdateStyleAndLayout();
+
+ int identifier = 0;
+ WebString search_text(String("foo"));
+ WebFindOptions find_options; // Default.
+ bool wrap_within_frame = true;
+
+ // TextIterator currently returns the matches in the flat treeorder, so
+ // in this case the matches will be returned in the order of
+ // <i> -> <u> -> <b>.
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ Range* active_match = GetTextFinder().ActiveMatch();
+ ASSERT_TRUE(active_match);
+ EXPECT_EQ(text_in_i_element, active_match->startContainer());
+ EXPECT_EQ(0u, active_match->startOffset());
+ EXPECT_EQ(text_in_i_element, active_match->endContainer());
+ EXPECT_EQ(3u, active_match->endOffset());
+
+ find_options.find_next = true;
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ active_match = GetTextFinder().ActiveMatch();
+ ASSERT_TRUE(active_match);
+ EXPECT_EQ(text_in_u_element, active_match->startContainer());
+ EXPECT_EQ(0u, active_match->startOffset());
+ EXPECT_EQ(text_in_u_element, active_match->endContainer());
+ EXPECT_EQ(3u, active_match->endOffset());
+
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ active_match = GetTextFinder().ActiveMatch();
+ ASSERT_TRUE(active_match);
+ EXPECT_EQ(text_in_b_element, active_match->startContainer());
+ EXPECT_EQ(0u, active_match->startOffset());
+ EXPECT_EQ(text_in_b_element, active_match->endContainer());
+ EXPECT_EQ(3u, active_match->endOffset());
+
+ // Should wrap to the first match.
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ active_match = GetTextFinder().ActiveMatch();
+ ASSERT_TRUE(active_match);
+ EXPECT_EQ(text_in_i_element, active_match->startContainer());
+ EXPECT_EQ(0u, active_match->startOffset());
+ EXPECT_EQ(text_in_i_element, active_match->endContainer());
+ EXPECT_EQ(3u, active_match->endOffset());
+
+ // Fresh search in the reverse order.
+ identifier = 1;
+ find_options = WebFindOptions();
+ find_options.forward = false;
+
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ active_match = GetTextFinder().ActiveMatch();
+ ASSERT_TRUE(active_match);
+ EXPECT_EQ(text_in_b_element, active_match->startContainer());
+ EXPECT_EQ(0u, active_match->startOffset());
+ EXPECT_EQ(text_in_b_element, active_match->endContainer());
+ EXPECT_EQ(3u, active_match->endOffset());
+
+ find_options.find_next = true;
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ active_match = GetTextFinder().ActiveMatch();
+ ASSERT_TRUE(active_match);
+ EXPECT_EQ(text_in_u_element, active_match->startContainer());
+ EXPECT_EQ(0u, active_match->startOffset());
+ EXPECT_EQ(text_in_u_element, active_match->endContainer());
+ EXPECT_EQ(3u, active_match->endOffset());
+
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ active_match = GetTextFinder().ActiveMatch();
+ ASSERT_TRUE(active_match);
+ EXPECT_EQ(text_in_i_element, active_match->startContainer());
+ EXPECT_EQ(0u, active_match->startOffset());
+ EXPECT_EQ(text_in_i_element, active_match->endContainer());
+ EXPECT_EQ(3u, active_match->endOffset());
+
+ // And wrap.
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame));
+ active_match = GetTextFinder().ActiveMatch();
+ ASSERT_TRUE(active_match);
+ EXPECT_EQ(text_in_b_element, active_match->startContainer());
+ EXPECT_EQ(0u, active_match->startOffset());
+ EXPECT_EQ(text_in_b_element, active_match->endContainer());
+ EXPECT_EQ(3u, active_match->endOffset());
+}
+
+TEST_F(TextFinderTest, ScopeTextMatchesSimple) {
+ GetDocument().body()->SetInnerHTMLFromString("XXXXFindMeYYYYfindmeZZZZ");
+ GetDocument().UpdateStyleAndLayout();
+
+ Node* text_node = GetDocument().body()->firstChild();
+
+ int identifier = 0;
+ WebString search_text(String("FindMe"));
+ WebFindOptions find_options; // Default.
+
+ GetTextFinder().ResetMatchCount();
+ GetTextFinder().StartScopingStringMatches(identifier, search_text,
+ find_options);
+ while (GetTextFinder().ScopingInProgress())
+ RunPendingTasks();
+
+ EXPECT_EQ(2, GetTextFinder().TotalMatchCount());
+ WebVector<WebFloatRect> match_rects;
+ GetTextFinder().FindMatchRects(match_rects);
+ ASSERT_EQ(2u, match_rects.size());
+ EXPECT_EQ(FindInPageRect(text_node, 4, text_node, 10), match_rects[0]);
+ EXPECT_EQ(FindInPageRect(text_node, 14, text_node, 20), match_rects[1]);
+
+ // Modify the document size and ensure the cached match rects are recomputed
+ // to reflect the updated layout.
+ GetDocument().body()->setAttribute(HTMLNames::styleAttr, "margin: 2000px");
+ GetDocument().UpdateStyleAndLayout();
+
+ GetTextFinder().FindMatchRects(match_rects);
+ ASSERT_EQ(2u, match_rects.size());
+ EXPECT_EQ(FindInPageRect(text_node, 4, text_node, 10), match_rects[0]);
+ EXPECT_EQ(FindInPageRect(text_node, 14, text_node, 20), match_rects[1]);
+}
+
+TEST_F(TextFinderTest, ScopeTextMatchesRepeated) {
+ GetDocument().body()->SetInnerHTMLFromString("XXXXFindMeYYYYfindmeZZZZ");
+ GetDocument().UpdateStyleAndLayout();
+
+ Node* text_node = GetDocument().body()->firstChild();
+
+ int identifier = 0;
+ WebString search_text1(String("XFindMe"));
+ WebString search_text2(String("FindMe"));
+ WebFindOptions find_options; // Default.
+
+ GetTextFinder().ResetMatchCount();
+ GetTextFinder().StartScopingStringMatches(identifier, search_text1,
+ find_options);
+ GetTextFinder().StartScopingStringMatches(identifier, search_text2,
+ find_options);
+ while (GetTextFinder().ScopingInProgress())
+ RunPendingTasks();
+
+ // Only searchText2 should be highlighted.
+ EXPECT_EQ(2, GetTextFinder().TotalMatchCount());
+ WebVector<WebFloatRect> match_rects;
+ GetTextFinder().FindMatchRects(match_rects);
+ ASSERT_EQ(2u, match_rects.size());
+ EXPECT_EQ(FindInPageRect(text_node, 4, text_node, 10), match_rects[0]);
+ EXPECT_EQ(FindInPageRect(text_node, 14, text_node, 20), match_rects[1]);
+}
+
+TEST_F(TextFinderTest, ScopeTextMatchesWithShadowDOM) {
+ GetDocument().body()->SetInnerHTMLFromString("<b>FOO</b><i>foo</i>");
+ ShadowRoot& shadow_root = GetDocument().body()->CreateShadowRootInternal();
+ shadow_root.SetInnerHTMLFromString(
+ "<content select=\"i\"></content><u>Foo</u><content></content>");
+ Node* text_in_b_element = GetDocument().body()->firstChild()->firstChild();
+ Node* text_in_i_element = GetDocument().body()->lastChild()->firstChild();
+ Node* text_in_u_element = shadow_root.childNodes()->item(1)->firstChild();
+ GetDocument().UpdateStyleAndLayout();
+
+ int identifier = 0;
+ WebString search_text(String("fOO"));
+ WebFindOptions find_options; // Default.
+
+ GetTextFinder().ResetMatchCount();
+ GetTextFinder().StartScopingStringMatches(identifier, search_text,
+ find_options);
+ while (GetTextFinder().ScopingInProgress())
+ RunPendingTasks();
+
+ // TextIterator currently returns the matches in the flat tree order,
+ // so in this case the matches will be returned in the order of
+ // <i> -> <u> -> <b>.
+ EXPECT_EQ(3, GetTextFinder().TotalMatchCount());
+ WebVector<WebFloatRect> match_rects;
+ GetTextFinder().FindMatchRects(match_rects);
+ ASSERT_EQ(3u, match_rects.size());
+ EXPECT_EQ(FindInPageRect(text_in_i_element, 0, text_in_i_element, 3),
+ match_rects[0]);
+ EXPECT_EQ(FindInPageRect(text_in_u_element, 0, text_in_u_element, 3),
+ match_rects[1]);
+ EXPECT_EQ(FindInPageRect(text_in_b_element, 0, text_in_b_element, 3),
+ match_rects[2]);
+}
+
+TEST_F(TextFinderTest, ScopeRepeatPatternTextMatches) {
+ GetDocument().body()->SetInnerHTMLFromString("ab ab ab ab ab");
+ GetDocument().UpdateStyleAndLayout();
+
+ Node* text_node = GetDocument().body()->firstChild();
+
+ int identifier = 0;
+ WebString search_text(String("ab ab"));
+ WebFindOptions find_options; // Default.
+
+ GetTextFinder().ResetMatchCount();
+ GetTextFinder().StartScopingStringMatches(identifier, search_text,
+ find_options);
+ while (GetTextFinder().ScopingInProgress())
+ RunPendingTasks();
+
+ EXPECT_EQ(2, GetTextFinder().TotalMatchCount());
+ WebVector<WebFloatRect> match_rects;
+ GetTextFinder().FindMatchRects(match_rects);
+ ASSERT_EQ(2u, match_rects.size());
+ EXPECT_EQ(FindInPageRect(text_node, 0, text_node, 5), match_rects[0]);
+ EXPECT_EQ(FindInPageRect(text_node, 6, text_node, 11), match_rects[1]);
+}
+
+TEST_F(TextFinderTest, OverlappingMatches) {
+ GetDocument().body()->SetInnerHTMLFromString("aababaa");
+ GetDocument().UpdateStyleAndLayout();
+
+ Node* text_node = GetDocument().body()->firstChild();
+
+ int identifier = 0;
+ WebString search_text(String("aba"));
+ WebFindOptions find_options; // Default.
+
+ GetTextFinder().ResetMatchCount();
+ GetTextFinder().StartScopingStringMatches(identifier, search_text,
+ find_options);
+ while (GetTextFinder().ScopingInProgress())
+ RunPendingTasks();
+
+ // We shouldn't find overlapped matches.
+ EXPECT_EQ(1, GetTextFinder().TotalMatchCount());
+ WebVector<WebFloatRect> match_rects;
+ GetTextFinder().FindMatchRects(match_rects);
+ ASSERT_EQ(1u, match_rects.size());
+ EXPECT_EQ(FindInPageRect(text_node, 1, text_node, 4), match_rects[0]);
+}
+
+TEST_F(TextFinderTest, SequentialMatches) {
+ GetDocument().body()->SetInnerHTMLFromString("ababab");
+ GetDocument().UpdateStyleAndLayout();
+
+ Node* text_node = GetDocument().body()->firstChild();
+
+ int identifier = 0;
+ WebString search_text(String("ab"));
+ WebFindOptions find_options; // Default.
+
+ GetTextFinder().ResetMatchCount();
+ GetTextFinder().StartScopingStringMatches(identifier, search_text,
+ find_options);
+ while (GetTextFinder().ScopingInProgress())
+ RunPendingTasks();
+
+ EXPECT_EQ(3, GetTextFinder().TotalMatchCount());
+ WebVector<WebFloatRect> match_rects;
+ GetTextFinder().FindMatchRects(match_rects);
+ ASSERT_EQ(3u, match_rects.size());
+ EXPECT_EQ(FindInPageRect(text_node, 0, text_node, 2), match_rects[0]);
+ EXPECT_EQ(FindInPageRect(text_node, 2, text_node, 4), match_rects[1]);
+ EXPECT_EQ(FindInPageRect(text_node, 4, text_node, 6), match_rects[2]);
+}
+
+TEST_F(TextFinderTest, FindTextJavaScriptUpdatesDOM) {
+ GetDocument().body()->SetInnerHTMLFromString("<b>XXXXFindMeYYYY</b><i></i>");
+ GetDocument().UpdateStyleAndLayout();
+
+ int identifier = 0;
+ WebString search_text(String("FindMe"));
+ WebFindOptions find_options; // Default.
+ bool wrap_within_frame = true;
+ bool active_now;
+
+ GetTextFinder().ResetMatchCount();
+ GetTextFinder().StartScopingStringMatches(identifier, search_text,
+ find_options);
+ while (GetTextFinder().ScopingInProgress())
+ RunPendingTasks();
+
+ find_options.find_next = true;
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame, &active_now));
+ EXPECT_TRUE(active_now);
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame, &active_now));
+ EXPECT_TRUE(active_now);
+
+ // Add new text to DOM and try FindNext.
+ Element* i_element = ToElement(GetDocument().body()->lastChild());
+ ASSERT_TRUE(i_element);
+ i_element->SetInnerHTMLFromString("ZZFindMe");
+ GetDocument().UpdateStyleAndLayout();
+
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame, &active_now));
+ Range* active_match = GetTextFinder().ActiveMatch();
+ ASSERT_TRUE(active_match);
+ EXPECT_FALSE(active_now);
+ EXPECT_EQ(2u, active_match->startOffset());
+ EXPECT_EQ(8u, active_match->endOffset());
+
+ // Restart full search and check that added text is found.
+ find_options.find_next = false;
+ GetTextFinder().ResetMatchCount();
+ GetTextFinder().CancelPendingScopingEffort();
+ GetTextFinder().StartScopingStringMatches(identifier, search_text,
+ find_options);
+ while (GetTextFinder().ScopingInProgress())
+ RunPendingTasks();
+ EXPECT_EQ(2, GetTextFinder().TotalMatchCount());
+
+ WebVector<WebFloatRect> match_rects;
+ GetTextFinder().FindMatchRects(match_rects);
+ ASSERT_EQ(2u, match_rects.size());
+ Node* text_in_b_element = GetDocument().body()->firstChild()->firstChild();
+ Node* text_in_i_element = GetDocument().body()->lastChild()->firstChild();
+ EXPECT_EQ(FindInPageRect(text_in_b_element, 4, text_in_b_element, 10),
+ match_rects[0]);
+ EXPECT_EQ(FindInPageRect(text_in_i_element, 2, text_in_i_element, 8),
+ match_rects[1]);
+}
+
+TEST_F(TextFinderTest, FindTextJavaScriptUpdatesDOMAfterNoMatches) {
+ GetDocument().body()->SetInnerHTMLFromString("<b>XXXXYYYY</b><i></i>");
+ GetDocument().UpdateStyleAndLayout();
+
+ int identifier = 0;
+ WebString search_text(String("FindMe"));
+ WebFindOptions find_options; // Default.
+ bool wrap_within_frame = true;
+ bool active_now = false;
+
+ GetTextFinder().ResetMatchCount();
+ GetTextFinder().StartScopingStringMatches(identifier, search_text,
+ find_options);
+ while (GetTextFinder().ScopingInProgress())
+ RunPendingTasks();
+
+ find_options.find_next = true;
+ ASSERT_FALSE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame, &active_now));
+ EXPECT_FALSE(active_now);
+
+ // Add new text to DOM and try FindNext.
+ Element* i_element = ToElement(GetDocument().body()->lastChild());
+ ASSERT_TRUE(i_element);
+ i_element->SetInnerHTMLFromString("ZZFindMe");
+ GetDocument().UpdateStyleAndLayout();
+
+ ASSERT_TRUE(GetTextFinder().Find(identifier, search_text, find_options,
+ wrap_within_frame, &active_now));
+ Range* active_match = GetTextFinder().ActiveMatch();
+ ASSERT_TRUE(active_match);
+ EXPECT_FALSE(active_now);
+ EXPECT_EQ(2u, active_match->startOffset());
+ EXPECT_EQ(8u, active_match->endOffset());
+
+ // Restart full search and check that added text is found.
+ find_options.find_next = false;
+ GetTextFinder().ResetMatchCount();
+ GetTextFinder().CancelPendingScopingEffort();
+ GetTextFinder().StartScopingStringMatches(identifier, search_text,
+ find_options);
+ while (GetTextFinder().ScopingInProgress())
+ RunPendingTasks();
+ EXPECT_EQ(1, GetTextFinder().TotalMatchCount());
+
+ WebVector<WebFloatRect> match_rects;
+ GetTextFinder().FindMatchRects(match_rects);
+ ASSERT_EQ(1u, match_rects.size());
+ Node* text_in_i_element = GetDocument().body()->lastChild()->firstChild();
+ EXPECT_EQ(FindInPageRect(text_in_i_element, 2, text_in_i_element, 8),
+ match_rects[0]);
+}
+
+class TextFinderFakeTimerTest : public TextFinderTest {
+ protected:
+ void SetUp() override {
+ time_elapsed_ = 0.0;
+ original_time_function_ = SetTimeFunctionsForTesting(ReturnMockTime);
+ }
+
+ void TearDown() override {
+ SetTimeFunctionsForTesting(original_time_function_);
+ }
+
+ private:
+ static double ReturnMockTime() {
+ time_elapsed_ += 1.0;
+ return time_elapsed_;
+ }
+
+ TimeFunction original_time_function_;
+ static double time_elapsed_;
+};
+
+double TextFinderFakeTimerTest::time_elapsed_;
+
+TEST_F(TextFinderFakeTimerTest, ScopeWithTimeouts) {
+ // Make a long string.
+ String text(Vector<UChar>(100));
+ text.Fill('a');
+ String search_pattern("abc");
+ // Make 4 substrings "abc" in text.
+ text.insert(search_pattern, 1);
+ text.insert(search_pattern, 10);
+ text.insert(search_pattern, 50);
+ text.insert(search_pattern, 90);
+
+ GetDocument().body()->SetInnerHTMLFromString(text);
+ GetDocument().UpdateStyleAndLayout();
+
+ int identifier = 0;
+ WebFindOptions find_options; // Default.
+
+ GetTextFinder().ResetMatchCount();
+
+ // There will be only one iteration before timeout, because increment
+ // of the TimeProxyPlatform timer is greater than timeout threshold.
+ GetTextFinder().StartScopingStringMatches(identifier, search_pattern,
+ find_options);
+ while (GetTextFinder().ScopingInProgress())
+ RunPendingTasks();
+
+ EXPECT_EQ(4, GetTextFinder().TotalMatchCount());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/forward.h b/chromium/third_party/blink/renderer/core/editing/forward.h
new file mode 100644
index 00000000000..a9962c4a604
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/forward.h
@@ -0,0 +1,58 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This file contains forward declarations of template classes in editing/
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FORWARD_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FORWARD_H_
+
+namespace blink {
+
+enum class TextAffinity;
+
+class NodeTraversal;
+class FlatTreeTraversal;
+
+template <typename Traversal>
+class EditingAlgorithm;
+using EditingStrategy = EditingAlgorithm<NodeTraversal>;
+using EditingInFlatTreeStrategy = EditingAlgorithm<FlatTreeTraversal>;
+
+template <typename Strategy>
+class PositionTemplate;
+using Position = PositionTemplate<EditingStrategy>;
+using PositionInFlatTree = PositionTemplate<EditingInFlatTreeStrategy>;
+
+template <typename Strategy>
+class EphemeralRangeTemplate;
+using EphemeralRange = EphemeralRangeTemplate<EditingStrategy>;
+using EphemeralRangeInFlatTree =
+ EphemeralRangeTemplate<EditingInFlatTreeStrategy>;
+
+template <typename Strategy>
+class PositionWithAffinityTemplate;
+using PositionWithAffinity = PositionWithAffinityTemplate<EditingStrategy>;
+using PositionInFlatTreeWithAffinity =
+ PositionWithAffinityTemplate<EditingInFlatTreeStrategy>;
+
+template <typename Strategy>
+class SelectionTemplate;
+using SelectionInDOMTree = SelectionTemplate<EditingStrategy>;
+using SelectionInFlatTree = SelectionTemplate<EditingInFlatTreeStrategy>;
+
+template <typename Strategy>
+class VisiblePositionTemplate;
+using VisiblePosition = VisiblePositionTemplate<EditingStrategy>;
+using VisiblePositionInFlatTree =
+ VisiblePositionTemplate<EditingInFlatTreeStrategy>;
+
+template <typename Strategy>
+class VisibleSelectionTemplate;
+using VisibleSelection = VisibleSelectionTemplate<EditingStrategy>;
+using VisibleSelectionInFlatTree =
+ VisibleSelectionTemplate<EditingInFlatTreeStrategy>;
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FORWARD_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/frame_caret.cc b/chromium/third_party/blink/renderer/core/editing/frame_caret.cc
new file mode 100644
index 00000000000..f8913c79875
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/frame_caret.cc
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2004, 2008, 2009, 2010 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/frame_caret.h"
+
+#include "base/location.h"
+#include "third_party/blink/public/platform/task_type.h"
+#include "third_party/blink/renderer/core/editing/caret_display_item_client.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+#include "third_party/blink/renderer/core/editing/selection_editor.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+#include "third_party/blink/renderer/core/layout/layout_block.h"
+#include "third_party/blink/renderer/core/layout/layout_embedded_content.h"
+#include "third_party/blink/renderer/core/layout/layout_theme.h"
+#include "third_party/blink/renderer/core/page/page.h"
+
+namespace blink {
+
+FrameCaret::FrameCaret(LocalFrame& frame,
+ const SelectionEditor& selection_editor)
+ : selection_editor_(&selection_editor),
+ frame_(frame),
+ display_item_client_(new CaretDisplayItemClient()),
+ caret_visibility_(CaretVisibility::kHidden),
+ caret_blink_timer_(new TaskRunnerTimer<FrameCaret>(
+ frame.GetTaskRunner(TaskType::kInternalAnimation),
+ this,
+ &FrameCaret::CaretBlinkTimerFired)),
+ should_paint_caret_(true),
+ is_caret_blinking_suspended_(false),
+ should_show_block_cursor_(false) {}
+
+FrameCaret::~FrameCaret() = default;
+
+void FrameCaret::Trace(blink::Visitor* visitor) {
+ visitor->Trace(selection_editor_);
+ visitor->Trace(frame_);
+}
+
+const DisplayItemClient& FrameCaret::GetDisplayItemClient() const {
+ return *display_item_client_;
+}
+
+const PositionWithAffinity FrameCaret::CaretPosition() const {
+ const VisibleSelection& selection =
+ selection_editor_->ComputeVisibleSelectionInDOMTree();
+ if (!selection.IsCaret())
+ return PositionWithAffinity();
+ return PositionWithAffinity(selection.Start(), selection.Affinity());
+}
+
+bool FrameCaret::IsActive() const {
+ return CaretPosition().IsNotNull();
+}
+
+void FrameCaret::UpdateAppearance() {
+ DCHECK_GE(frame_->GetDocument()->Lifecycle().GetState(),
+ DocumentLifecycle::kLayoutClean);
+ // Paint a block cursor instead of a caret in overtype mode unless the caret
+ // is at the end of a line (in this case the FrameSelection will paint a
+ // blinking caret as usual).
+ const bool paint_block_cursor =
+ should_show_block_cursor_ && IsActive() &&
+ !IsLogicalEndOfLine(CreateVisiblePosition(CaretPosition()));
+
+ bool should_blink = !paint_block_cursor && ShouldBlinkCaret();
+ if (!should_blink) {
+ StopCaretBlinkTimer();
+ return;
+ }
+ // Start blinking with a black caret. Be sure not to restart if we're
+ // already blinking in the right location.
+ StartBlinkCaret();
+}
+
+void FrameCaret::StopCaretBlinkTimer() {
+ if (caret_blink_timer_->IsActive() || should_paint_caret_)
+ ScheduleVisualUpdateForPaintInvalidationIfNeeded();
+ should_paint_caret_ = false;
+ caret_blink_timer_->Stop();
+}
+
+void FrameCaret::StartBlinkCaret() {
+ // Start blinking with a black caret. Be sure not to restart if we're
+ // already blinking in the right location.
+ if (caret_blink_timer_->IsActive())
+ return;
+
+ TimeDelta blink_interval = LayoutTheme::GetTheme().CaretBlinkInterval();
+ if (!blink_interval.is_zero())
+ caret_blink_timer_->StartRepeating(blink_interval, FROM_HERE);
+
+ should_paint_caret_ = true;
+ ScheduleVisualUpdateForPaintInvalidationIfNeeded();
+}
+
+void FrameCaret::SetCaretVisibility(CaretVisibility visibility) {
+ if (caret_visibility_ == visibility)
+ return;
+
+ caret_visibility_ = visibility;
+
+ if (visibility == CaretVisibility::kHidden)
+ StopCaretBlinkTimer();
+ ScheduleVisualUpdateForPaintInvalidationIfNeeded();
+}
+
+void FrameCaret::ClearPreviousVisualRect(const LayoutBlock& block) {
+ display_item_client_->ClearPreviousVisualRect(block);
+}
+
+void FrameCaret::LayoutBlockWillBeDestroyed(const LayoutBlock& block) {
+ display_item_client_->LayoutBlockWillBeDestroyed(block);
+}
+
+void FrameCaret::UpdateStyleAndLayoutIfNeeded() {
+ DCHECK_GE(frame_->GetDocument()->Lifecycle().GetState(),
+ DocumentLifecycle::kLayoutClean);
+ UpdateAppearance();
+ bool should_paint_caret =
+ should_paint_caret_ && IsActive() &&
+ caret_visibility_ == CaretVisibility::kVisible &&
+ IsEditablePosition(
+ selection_editor_->ComputeVisibleSelectionInDOMTree().Start());
+
+ display_item_client_->UpdateStyleAndLayoutIfNeeded(
+ should_paint_caret ? CaretPosition() : PositionWithAffinity());
+}
+
+void FrameCaret::InvalidatePaint(const LayoutBlock& block,
+ const PaintInvalidatorContext& context) {
+ display_item_client_->InvalidatePaint(block, context);
+}
+
+static IntRect AbsoluteBoundsForLocalRect(Node* node, const LayoutRect& rect) {
+ LayoutBlock* caret_painter = CaretDisplayItemClient::CaretLayoutBlock(node);
+ if (!caret_painter)
+ return IntRect();
+
+ LayoutRect local_rect(rect);
+ return caret_painter->LocalToAbsoluteQuad(FloatRect(local_rect))
+ .EnclosingBoundingBox();
+}
+
+IntRect FrameCaret::AbsoluteCaretBounds() const {
+ DCHECK_NE(frame_->GetDocument()->Lifecycle().GetState(),
+ DocumentLifecycle::kInPrePaint);
+ DCHECK(!frame_->GetDocument()->NeedsLayoutTreeUpdate());
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ frame_->GetDocument()->Lifecycle());
+
+ Node* const caret_node = CaretPosition().AnchorNode();
+ if (!IsActive())
+ return AbsoluteBoundsForLocalRect(caret_node, LayoutRect());
+ return AbsoluteBoundsForLocalRect(
+ caret_node,
+ CaretDisplayItemClient::ComputeCaretRect(
+ CreateVisiblePosition(CaretPosition()).ToPositionWithAffinity()));
+}
+
+void FrameCaret::SetShouldShowBlockCursor(bool should_show_block_cursor) {
+ should_show_block_cursor_ = should_show_block_cursor;
+ ScheduleVisualUpdateForPaintInvalidationIfNeeded();
+}
+
+bool FrameCaret::ShouldPaintCaret(const LayoutBlock& block) const {
+ return display_item_client_->ShouldPaintCaret(block);
+}
+
+void FrameCaret::PaintCaret(GraphicsContext& context,
+ const LayoutPoint& paint_offset) const {
+ display_item_client_->PaintCaret(context, paint_offset, DisplayItem::kCaret);
+}
+
+bool FrameCaret::ShouldBlinkCaret() const {
+ if (caret_visibility_ != CaretVisibility::kVisible || !IsActive())
+ return false;
+
+ Element* root = RootEditableElementOf(CaretPosition().GetPosition());
+ if (!root)
+ return false;
+
+ Element* focused_element = root->GetDocument().FocusedElement();
+ if (!focused_element)
+ return false;
+
+ return frame_->Selection().SelectionHasFocus();
+}
+
+void FrameCaret::CaretBlinkTimerFired(TimerBase*) {
+ DCHECK_EQ(caret_visibility_, CaretVisibility::kVisible);
+ if (IsCaretBlinkingSuspended() && should_paint_caret_)
+ return;
+ should_paint_caret_ = !should_paint_caret_;
+ ScheduleVisualUpdateForPaintInvalidationIfNeeded();
+}
+
+void FrameCaret::ScheduleVisualUpdateForPaintInvalidationIfNeeded() {
+ if (LocalFrameView* frame_view = frame_->View())
+ frame_view->ScheduleVisualUpdateForPaintInvalidationIfNeeded();
+}
+
+void FrameCaret::RecreateCaretBlinkTimerForTesting(
+ scoped_refptr<base::SingleThreadTaskRunner> task_runner) {
+ caret_blink_timer_.reset(new TaskRunnerTimer<FrameCaret>(
+ std::move(task_runner), this, &FrameCaret::CaretBlinkTimerFired));
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/frame_caret.h b/chromium/third_party/blink/renderer/core/editing/frame_caret.h
new file mode 100644
index 00000000000..2ffeb785c72
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/frame_caret.h
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2004, 2008, 2009, 2010 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FRAME_CARET_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FRAME_CARET_H_
+
+#include <memory>
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/geometry/layout_rect.h"
+#include "third_party/blink/renderer/platform/graphics/paint_invalidation_reason.h"
+#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
+#include "third_party/blink/renderer/platform/heap/member.h"
+#include "third_party/blink/renderer/platform/timer.h"
+
+namespace blink {
+
+class CaretDisplayItemClient;
+class DisplayItemClient;
+class FrameCaret;
+class GraphicsContext;
+class LayoutBlock;
+class LocalFrame;
+class SelectionEditor;
+struct PaintInvalidatorContext;
+
+enum class CaretVisibility { kVisible, kHidden };
+
+class CORE_EXPORT FrameCaret final
+ : public GarbageCollectedFinalized<FrameCaret> {
+ public:
+ FrameCaret(LocalFrame&, const SelectionEditor&);
+ ~FrameCaret();
+
+ const DisplayItemClient& GetDisplayItemClient() const;
+ bool IsActive() const;
+
+ void ScheduleVisualUpdateForPaintInvalidationIfNeeded();
+
+ // Used to suspend caret blinking while the mouse is down.
+ void SetCaretBlinkingSuspended(bool suspended) {
+ is_caret_blinking_suspended_ = suspended;
+ }
+ bool IsCaretBlinkingSuspended() const { return is_caret_blinking_suspended_; }
+ void StopCaretBlinkTimer();
+ void StartBlinkCaret();
+ void SetCaretVisibility(CaretVisibility);
+ IntRect AbsoluteCaretBounds() const;
+
+ bool ShouldShowBlockCursor() const { return should_show_block_cursor_; }
+ void SetShouldShowBlockCursor(bool);
+
+ // Paint invalidation methods delegating to DisplayItemClient.
+ void ClearPreviousVisualRect(const LayoutBlock&);
+ void LayoutBlockWillBeDestroyed(const LayoutBlock&);
+ void UpdateStyleAndLayoutIfNeeded();
+ void InvalidatePaint(const LayoutBlock&, const PaintInvalidatorContext&);
+
+ bool ShouldPaintCaret(const LayoutBlock&) const;
+ void PaintCaret(GraphicsContext&, const LayoutPoint&) const;
+
+ // For unit tests.
+ const DisplayItemClient& CaretDisplayItemClientForTesting() const;
+ const LayoutBlock* CaretLayoutBlockForTesting() const;
+ bool ShouldPaintCaretForTesting() const { return should_paint_caret_; }
+ void RecreateCaretBlinkTimerForTesting(
+ scoped_refptr<base::SingleThreadTaskRunner>);
+
+ void Trace(blink::Visitor*);
+
+ private:
+ friend class FrameCaretTest;
+ friend class FrameSelectionTest;
+
+ const PositionWithAffinity CaretPosition() const;
+
+ bool ShouldBlinkCaret() const;
+ void CaretBlinkTimerFired(TimerBase*);
+ void UpdateAppearance();
+
+ const Member<const SelectionEditor> selection_editor_;
+ const Member<LocalFrame> frame_;
+ const std::unique_ptr<CaretDisplayItemClient> display_item_client_;
+ CaretVisibility caret_visibility_;
+ // TODO(https://crbug.com/668758): Consider using BeginFrame update for this.
+ std::unique_ptr<TaskRunnerTimer<FrameCaret>> caret_blink_timer_;
+ bool should_paint_caret_ : 1;
+ bool is_caret_blinking_suspended_ : 1;
+ bool should_show_block_cursor_ : 1;
+
+ DISALLOW_COPY_AND_ASSIGN(FrameCaret);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FRAME_CARET_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/frame_caret_test.cc b/chromium/third_party/blink/renderer/core/editing/frame_caret_test.cc
new file mode 100644
index 00000000000..26394e58b62
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/frame_caret_test.cc
@@ -0,0 +1,99 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/frame_caret.h"
+
+#include "third_party/blink/renderer/core/editing/commands/typing_command.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/layout/layout_theme.h"
+#include "third_party/blink/renderer/core/page/focus_controller.h"
+#include "third_party/blink/renderer/platform/layout_test_support.h"
+#include "third_party/blink/renderer/platform/scheduler/test/fake_task_runner.h"
+
+namespace blink {
+
+class FrameCaretTest : public EditingTestBase {
+ public:
+ FrameCaretTest()
+ : was_running_layout_test_(LayoutTestSupport::IsRunningLayoutTest()) {
+ // The caret blink timer doesn't work if isRunningLayoutTest() because
+ // LayoutTheme::caretBlinkInterval() returns 0.
+ LayoutTestSupport::SetIsRunningLayoutTest(false);
+ }
+ ~FrameCaretTest() override {
+ LayoutTestSupport::SetIsRunningLayoutTest(was_running_layout_test_);
+ }
+
+ static bool ShouldBlinkCaret(const FrameCaret& caret) {
+ return caret.ShouldBlinkCaret();
+ }
+
+ private:
+ const bool was_running_layout_test_;
+};
+
+TEST_F(FrameCaretTest, BlinkAfterTyping) {
+ FrameCaret& caret = Selection().FrameCaretForTesting();
+ scoped_refptr<scheduler::FakeTaskRunner> task_runner =
+ base::MakeRefCounted<scheduler::FakeTaskRunner>();
+ task_runner->SetTime(0);
+ caret.RecreateCaretBlinkTimerForTesting(task_runner.get());
+ const double kInterval = 10;
+ LayoutTheme::GetTheme().SetCaretBlinkInterval(
+ TimeDelta::FromSecondsD(kInterval));
+ GetDocument().GetPage()->GetFocusController().SetActive(true);
+ GetDocument().GetPage()->GetFocusController().SetFocused(true);
+ GetDocument().body()->SetInnerHTMLFromString("<textarea>");
+ Element* editor = ToElement(GetDocument().body()->firstChild());
+ editor->focus();
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ EXPECT_TRUE(caret.IsActive());
+ EXPECT_FALSE(caret.ShouldShowBlockCursor());
+ EXPECT_TRUE(caret.ShouldPaintCaretForTesting())
+ << "Initially a caret should be in visible cycle.";
+
+ task_runner->AdvanceTimeAndRun(kInterval);
+ EXPECT_FALSE(caret.ShouldPaintCaretForTesting())
+ << "The caret blinks normally.";
+
+ TypingCommand::InsertLineBreak(GetDocument());
+ GetDocument().View()->UpdateAllLifecyclePhases();
+ EXPECT_TRUE(caret.ShouldPaintCaretForTesting())
+ << "The caret should be in visible cycle just after a typing command.";
+
+ task_runner->AdvanceTimeAndRun(kInterval - 1);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+ EXPECT_TRUE(caret.ShouldPaintCaretForTesting())
+ << "The typing command reset the timer. The caret is still visible.";
+
+ task_runner->AdvanceTimeAndRun(1);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+ EXPECT_FALSE(caret.ShouldPaintCaretForTesting())
+ << "The caret should blink after the typing command.";
+}
+
+TEST_F(FrameCaretTest, ShouldNotBlinkWhenSelectionLooseFocus) {
+ FrameCaret& caret = Selection().FrameCaretForTesting();
+ GetDocument().GetPage()->GetFocusController().SetActive(true);
+ GetDocument().GetPage()->GetFocusController().SetFocused(true);
+ GetDocument().body()->SetInnerHTMLFromString(
+ "<div id='outer' tabindex='-1'>"
+ "<div id='input' contenteditable>foo</div>"
+ "</div>");
+ Element* input = GetDocument().QuerySelector("#input");
+ input->focus();
+ Element* outer = GetDocument().QuerySelector("#outer");
+ outer->focus();
+ GetDocument().View()->UpdateAllLifecyclePhases();
+ const SelectionInDOMTree& selection = Selection().GetSelectionInDOMTree();
+ EXPECT_EQ(selection.Base(),
+ Position(input, PositionAnchorType::kBeforeChildren));
+ EXPECT_FALSE(ShouldBlinkCaret(caret));
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/frame_selection.cc b/chromium/third_party/blink/renderer/core/editing/frame_selection.cc
new file mode 100644
index 00000000000..5fda2352bd6
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/frame_selection.cc
@@ -0,0 +1,1247 @@
+/*
+ * Copyright (C) 2004, 2008, 2009, 2010 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+
+#include <stdio.h>
+#include "third_party/blink/public/platform/web_scroll_into_view_params.h"
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/dom/ax_object_cache.h"
+#include "third_party/blink/renderer/core/dom/character_data.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/element_traversal.h"
+#include "third_party/blink/renderer/core/dom/events/event.h"
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/core/dom/node_with_index.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/caret_display_item_client.h"
+#include "third_party/blink/renderer/core/editing/commands/typing_command.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_caret.h"
+#include "third_party/blink/renderer/core/editing/granularity_strategy.h"
+#include "third_party/blink/renderer/core/editing/ime/input_method_controller.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/layout_selection.h"
+#include "third_party/blink/renderer/core/editing/local_caret_rect.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/selection_controller.h"
+#include "third_party/blink/renderer/core/editing/selection_editor.h"
+#include "third_party/blink/renderer/core/editing/selection_modifier.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/serializers/serialization.h"
+#include "third_party/blink/renderer/core/editing/text_affinity.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/frame/local_dom_window.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
+#include "third_party/blink/renderer/core/html/forms/html_select_element.h"
+#include "third_party/blink/renderer/core/html/html_body_element.h"
+#include "third_party/blink/renderer/core/html/html_frame_element_base.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/input/context_menu_allowed_scope.h"
+#include "third_party/blink/renderer/core/input/event_handler.h"
+#include "third_party/blink/renderer/core/layout/hit_test_request.h"
+#include "third_party/blink/renderer/core/layout/hit_test_result.h"
+#include "third_party/blink/renderer/core/layout/layout_embedded_content.h"
+#include "third_party/blink/renderer/core/layout/layout_view.h"
+#include "third_party/blink/renderer/core/loader/document_loader.h"
+#include "third_party/blink/renderer/core/page/focus_controller.h"
+#include "third_party/blink/renderer/core/page/frame_tree.h"
+#include "third_party/blink/renderer/core/page/page.h"
+#include "third_party/blink/renderer/core/page/spatial_navigation.h"
+#include "third_party/blink/renderer/core/paint/paint_layer.h"
+#include "third_party/blink/renderer/platform/geometry/float_quad.h"
+#include "third_party/blink/renderer/platform/graphics/graphics_context.h"
+#include "third_party/blink/renderer/platform/text/unicode_utilities.h"
+#include "third_party/blink/renderer/platform/wtf/text/cstring.h"
+
+#define EDIT_DEBUG 0
+
+namespace blink {
+
+using namespace HTMLNames;
+
+static inline bool ShouldAlwaysUseDirectionalSelection(LocalFrame* frame) {
+ return frame->GetEditor().Behavior().ShouldConsiderSelectionAsDirectional();
+}
+
+FrameSelection::FrameSelection(LocalFrame& frame)
+ : frame_(frame),
+ layout_selection_(LayoutSelection::Create(*this)),
+ selection_editor_(SelectionEditor::Create(frame)),
+ granularity_(TextGranularity::kCharacter),
+ x_pos_for_vertical_arrow_navigation_(NoXPosForVerticalArrowNavigation()),
+ focused_(frame.GetPage() &&
+ frame.GetPage()->GetFocusController().FocusedFrame() == frame),
+ is_directional_(ShouldAlwaysUseDirectionalSelection(frame_)),
+ frame_caret_(new FrameCaret(frame, *selection_editor_)) {}
+
+FrameSelection::~FrameSelection() = default;
+
+const DisplayItemClient& FrameSelection::CaretDisplayItemClientForTesting()
+ const {
+ return frame_caret_->GetDisplayItemClient();
+}
+
+Document& FrameSelection::GetDocument() const {
+ DCHECK(LifecycleContext());
+ return *LifecycleContext();
+}
+
+VisibleSelection FrameSelection::ComputeVisibleSelectionInDOMTree() const {
+ return selection_editor_->ComputeVisibleSelectionInDOMTree();
+}
+
+VisibleSelectionInFlatTree FrameSelection::ComputeVisibleSelectionInFlatTree()
+ const {
+ return selection_editor_->ComputeVisibleSelectionInFlatTree();
+}
+
+SelectionInDOMTree FrameSelection::GetSelectionInDOMTree() const {
+ return selection_editor_->GetSelectionInDOMTree();
+}
+
+Element* FrameSelection::RootEditableElementOrDocumentElement() const {
+ Element* selection_root =
+ ComputeVisibleSelectionInDOMTreeDeprecated().RootEditableElement();
+ return selection_root ? selection_root : GetDocument().documentElement();
+}
+
+size_t FrameSelection::CharacterIndexForPoint(const IntPoint& point) const {
+ const EphemeralRange range = GetFrame()->GetEditor().RangeForPoint(point);
+ if (range.IsNull())
+ return kNotFound;
+ Element* const editable = RootEditableElementOrDocumentElement();
+ DCHECK(editable);
+ return PlainTextRange::Create(*editable, range).Start();
+}
+
+VisibleSelection FrameSelection::ComputeVisibleSelectionInDOMTreeDeprecated()
+ const {
+ // TODO(editing-dev): Hoist updateStyleAndLayoutIgnorePendingStylesheets
+ // to caller. See http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ return ComputeVisibleSelectionInDOMTree();
+}
+
+VisibleSelectionInFlatTree FrameSelection::GetSelectionInFlatTree() const {
+ return ComputeVisibleSelectionInFlatTree();
+}
+
+void FrameSelection::MoveCaretSelection(const IntPoint& point) {
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+
+ Element* const editable =
+ ComputeVisibleSelectionInDOMTree().RootEditableElement();
+ if (!editable)
+ return;
+
+ const VisiblePosition position =
+ VisiblePositionForContentsPoint(point, GetFrame());
+ SelectionInDOMTree::Builder builder;
+ if (position.IsNotNull())
+ builder.Collapse(position.ToPositionWithAffinity());
+ SetSelection(builder.Build(), SetSelectionOptions::Builder()
+ .SetShouldCloseTyping(true)
+ .SetShouldClearTypingStyle(true)
+ .SetSetSelectionBy(SetSelectionBy::kUser)
+ .SetShouldShowHandle(true)
+ .SetIsDirectional(IsDirectional())
+ .Build());
+}
+
+void FrameSelection::SetSelection(const SelectionInDOMTree& selection,
+ const SetSelectionOptions& data) {
+ if (SetSelectionDeprecated(selection, data))
+ DidSetSelectionDeprecated(data);
+}
+
+void FrameSelection::SetSelectionAndEndTyping(
+ const SelectionInDOMTree& selection) {
+ SetSelection(selection, SetSelectionOptions::Builder()
+ .SetShouldCloseTyping(true)
+ .SetShouldClearTypingStyle(true)
+ .Build());
+}
+
+bool FrameSelection::SetSelectionDeprecated(
+ const SelectionInDOMTree& new_selection,
+ const SetSelectionOptions& passed_options) {
+ SetSelectionOptions::Builder options_builder(passed_options);
+ if (ShouldAlwaysUseDirectionalSelection(frame_)) {
+ options_builder.SetIsDirectional(true);
+ }
+ SetSelectionOptions options = options_builder.Build();
+
+ if (granularity_strategy_ && !options.DoNotClearStrategy())
+ granularity_strategy_->Clear();
+ granularity_ = options.Granularity();
+
+ // TODO(yosin): We should move to call |TypingCommand::closeTyping()| to
+ // |Editor| class.
+ if (options.ShouldCloseTyping())
+ TypingCommand::CloseTyping(frame_);
+
+ if (options.ShouldClearTypingStyle())
+ frame_->GetEditor().ClearTypingStyle();
+
+ const SelectionInDOMTree old_selection_in_dom_tree =
+ selection_editor_->GetSelectionInDOMTree();
+ const bool is_changed = old_selection_in_dom_tree != new_selection;
+ const bool should_show_handle = options.ShouldShowHandle();
+ if (!is_changed && is_handle_visible_ == should_show_handle &&
+ is_directional_ == options.IsDirectional())
+ return false;
+ if (is_changed)
+ selection_editor_->SetSelectionAndEndTyping(new_selection);
+ is_directional_ = options.IsDirectional();
+ should_shrink_next_tap_ = options.ShouldShrinkNextTap();
+ is_handle_visible_ = should_show_handle;
+ ScheduleVisualUpdateForPaintInvalidationIfNeeded();
+
+ const Document& current_document = GetDocument();
+ frame_->GetEditor().RespondToChangedSelection();
+ DCHECK_EQ(current_document, GetDocument());
+ return true;
+}
+
+void FrameSelection::DidSetSelectionDeprecated(
+ const SetSelectionOptions& options) {
+ const Document& current_document = GetDocument();
+ if (!GetSelectionInDOMTree().IsNone() && !options.DoNotSetFocus()) {
+ SetFocusedNodeIfNeeded();
+ // |setFocusedNodeIfNeeded()| dispatches sync events "FocusOut" and
+ // "FocusIn", |m_frame| may associate to another document.
+ if (!IsAvailable() || GetDocument() != current_document) {
+ // Once we get test case to reach here, we should change this
+ // if-statement to |DCHECK()|.
+ NOTREACHED();
+ return;
+ }
+ }
+
+ frame_caret_->StopCaretBlinkTimer();
+ UpdateAppearance();
+
+ // Always clear the x position used for vertical arrow navigation.
+ // It will be restored by the vertical arrow navigation code if necessary.
+ x_pos_for_vertical_arrow_navigation_ = NoXPosForVerticalArrowNavigation();
+
+ // TODO(yosin): Can we move this to at end of this function?
+ // This may dispatch a synchronous focus-related events.
+ if (!options.DoNotSetFocus()) {
+ SelectFrameElementInParentIfFullySelected();
+ if (!IsAvailable() || GetDocument() != current_document) {
+ // editing/selection/selectallchildren-crash.html and
+ // editing/selection/longpress-selection-in-iframe-removed-crash.html
+ // reach here.
+ return;
+ }
+ }
+ const SetSelectionBy set_selection_by = options.GetSetSelectionBy();
+ NotifyTextControlOfSelectionChange(set_selection_by);
+ if (set_selection_by == SetSelectionBy::kUser) {
+ const CursorAlignOnScroll align = options.GetCursorAlignOnScroll();
+ ScrollAlignment alignment;
+
+ if (frame_->GetEditor()
+ .Behavior()
+ .ShouldCenterAlignWhenSelectionIsRevealed())
+ alignment = (align == CursorAlignOnScroll::kAlways)
+ ? ScrollAlignment::kAlignCenterAlways
+ : ScrollAlignment::kAlignCenterIfNeeded;
+ else
+ alignment = (align == CursorAlignOnScroll::kAlways)
+ ? ScrollAlignment::kAlignTopAlways
+ : ScrollAlignment::kAlignToEdgeIfNeeded;
+
+ RevealSelection(alignment, kRevealExtent);
+ }
+
+ NotifyAccessibilityForSelectionChange();
+ NotifyCompositorForSelectionChange();
+ NotifyEventHandlerForSelectionChange();
+ frame_->DomWindow()->EnqueueDocumentEvent(
+ Event::Create(EventTypeNames::selectionchange));
+}
+
+void FrameSelection::NodeChildrenWillBeRemoved(ContainerNode& container) {
+ if (!container.InActiveDocument())
+ return;
+ // TODO(yosin): We should move to call |TypingCommand::closeTyping()| to
+ // |Editor| class.
+ if (!GetDocument().IsRunningExecCommand())
+ TypingCommand::CloseTyping(frame_);
+}
+
+void FrameSelection::NodeWillBeRemoved(Node& node) {
+ // There can't be a selection inside a fragment, so if a fragment's node is
+ // being removed, the selection in the document that created the fragment
+ // needs no adjustment.
+ if (!node.InActiveDocument())
+ return;
+ // TODO(yosin): We should move to call |TypingCommand::closeTyping()| to
+ // |Editor| class.
+ if (!GetDocument().IsRunningExecCommand())
+ TypingCommand::CloseTyping(frame_);
+}
+
+void FrameSelection::DidChangeFocus() {
+ // Hits in
+ // virtual/gpu/compositedscrolling/scrollbars/scrollbar-miss-mousemove-disabled.html
+ DisableCompositingQueryAsserts disabler;
+ UpdateAppearance();
+}
+
+static DispatchEventResult DispatchSelectStart(
+ const VisibleSelection& selection) {
+ Node* select_start_target = selection.Extent().ComputeContainerNode();
+ if (!select_start_target)
+ return DispatchEventResult::kNotCanceled;
+
+ return select_start_target->DispatchEvent(
+ Event::CreateCancelableBubble(EventTypeNames::selectstart));
+}
+
+// The return value of |FrameSelection::modify()| is different based on
+// value of |userTriggered| parameter.
+// When |userTriggered| is |userTriggered|, |modify()| returns false if
+// "selectstart" event is dispatched and canceled, otherwise returns true.
+// When |userTriggered| is |NotUserTrigged|, return value specifies whether
+// selection is modified or not.
+bool FrameSelection::Modify(SelectionModifyAlteration alter,
+ SelectionModifyDirection direction,
+ TextGranularity granularity,
+ SetSelectionBy set_selection_by) {
+ SelectionModifier selection_modifier(*GetFrame(), GetSelectionInDOMTree(),
+ x_pos_for_vertical_arrow_navigation_);
+ selection_modifier.SetSelectionIsDirectional(IsDirectional());
+ const bool modified =
+ selection_modifier.Modify(alter, direction, granularity);
+ if (set_selection_by == SetSelectionBy::kUser &&
+ selection_modifier.Selection().IsRange() &&
+ ComputeVisibleSelectionInDOMTree().IsCaret() &&
+ DispatchSelectStart(ComputeVisibleSelectionInDOMTree()) !=
+ DispatchEventResult::kNotCanceled) {
+ return false;
+ }
+ if (!modified) {
+ if (set_selection_by == SetSelectionBy::kSystem)
+ return false;
+ // If spatial navigation enabled, focus navigator will move focus to
+ // another element. See snav-input.html and snav-textarea.html
+ if (IsSpatialNavigationEnabled(frame_))
+ return false;
+ // Even if selection isn't changed, we prevent to default action, e.g.
+ // scroll window when caret is at end of content editable.
+ return true;
+ }
+
+ // For MacOS only selection is directionless at the beginning.
+ // Selection gets direction on extent.
+ const bool selection_is_directional =
+ alter == SelectionModifyAlteration::kExtend ||
+ ShouldAlwaysUseDirectionalSelection(frame_);
+
+ SetSelection(selection_modifier.Selection().AsSelection(),
+ SetSelectionOptions::Builder()
+ .SetShouldCloseTyping(true)
+ .SetShouldClearTypingStyle(true)
+ .SetSetSelectionBy(set_selection_by)
+ .SetIsDirectional(selection_is_directional)
+ .Build());
+
+ if (granularity == TextGranularity::kLine ||
+ granularity == TextGranularity::kParagraph)
+ x_pos_for_vertical_arrow_navigation_ =
+ selection_modifier.XPosForVerticalArrowNavigation();
+
+ if (set_selection_by == SetSelectionBy::kUser)
+ granularity_ = TextGranularity::kCharacter;
+
+ ScheduleVisualUpdateForPaintInvalidationIfNeeded();
+
+ return true;
+}
+
+void FrameSelection::Clear() {
+ granularity_ = TextGranularity::kCharacter;
+ if (granularity_strategy_)
+ granularity_strategy_->Clear();
+ SetSelectionAndEndTyping(SelectionInDOMTree());
+ is_handle_visible_ = false;
+ is_directional_ = ShouldAlwaysUseDirectionalSelection(frame_);
+}
+
+bool FrameSelection::SelectionHasFocus() const {
+ // TODO(editing-dev): Hoist UpdateStyleAndLayoutIgnorePendingStylesheets
+ // to caller. See http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (ComputeVisibleSelectionInFlatTree().IsNone())
+ return false;
+ const Node* current =
+ ComputeVisibleSelectionInFlatTree().Start().ComputeContainerNode();
+ if (!current)
+ return false;
+
+ // No focused element means document root has focus.
+ Element* const focused_element = GetDocument().FocusedElement()
+ ? GetDocument().FocusedElement()
+ : GetDocument().documentElement();
+ if (!focused_element)
+ return false;
+
+ if (focused_element->IsTextControl())
+ return focused_element->ContainsIncludingHostElements(*current);
+
+ // Selection has focus if it contains the focused element.
+ const PositionInFlatTree& focused_position =
+ PositionInFlatTree::FirstPositionInNode(*focused_element);
+ if (ComputeVisibleSelectionInFlatTree().Start() <= focused_position &&
+ ComputeVisibleSelectionInFlatTree().End() >= focused_position)
+ return true;
+
+ bool has_editable_style = HasEditableStyle(*current);
+ do {
+ // If the selection is within an editable sub tree and that sub tree
+ // doesn't have focus, the selection doesn't have focus either.
+ if (has_editable_style && !HasEditableStyle(*current))
+ return false;
+
+ // Selection has focus if its sub tree has focus.
+ if (current == focused_element)
+ return true;
+ current = current->ParentOrShadowHostNode();
+ } while (current);
+
+ return false;
+}
+
+bool FrameSelection::IsHidden() const {
+ if (SelectionHasFocus())
+ return false;
+
+ const Node* start =
+ ComputeVisibleSelectionInDOMTree().Start().ComputeContainerNode();
+ if (!start)
+ return true;
+
+ // The selection doesn't have focus, so hide everything but range selections.
+ if (!GetSelectionInDOMTree().IsRange())
+ return true;
+
+ // Here we know we have an unfocused range selection. Let's say that
+ // selection resides inside a text control. Since the selection doesn't have
+ // focus neither does the text control. Meaning, if the selection indeed
+ // resides inside a text control, it should be hidden.
+ return EnclosingTextControl(start);
+}
+
+void FrameSelection::DocumentAttached(Document* document) {
+ DCHECK(document);
+ selection_editor_->DocumentAttached(document);
+ SetContext(document);
+}
+
+void FrameSelection::ContextDestroyed(Document* document) {
+ granularity_ = TextGranularity::kCharacter;
+
+ layout_selection_->OnDocumentShutdown();
+
+ frame_->GetEditor().ClearTypingStyle();
+}
+
+void FrameSelection::ClearPreviousCaretVisualRect(const LayoutBlock& block) {
+ frame_caret_->ClearPreviousVisualRect(block);
+}
+
+void FrameSelection::LayoutBlockWillBeDestroyed(const LayoutBlock& block) {
+ frame_caret_->LayoutBlockWillBeDestroyed(block);
+}
+
+void FrameSelection::UpdateStyleAndLayoutIfNeeded() {
+ frame_caret_->UpdateStyleAndLayoutIfNeeded();
+}
+
+void FrameSelection::InvalidatePaint(const LayoutBlock& block,
+ const PaintInvalidatorContext& context) {
+ frame_caret_->InvalidatePaint(block, context);
+}
+
+bool FrameSelection::ShouldPaintCaret(const LayoutBlock& block) const {
+ DCHECK_GE(GetDocument().Lifecycle().GetState(),
+ DocumentLifecycle::kLayoutClean);
+ bool result = frame_caret_->ShouldPaintCaret(block);
+ DCHECK(!result ||
+ (ComputeVisibleSelectionInDOMTree().IsCaret() &&
+ IsEditablePosition(ComputeVisibleSelectionInDOMTree().Start())));
+ return result;
+}
+
+IntRect FrameSelection::AbsoluteCaretBounds() const {
+ DCHECK(ComputeVisibleSelectionInDOMTree().IsValidFor(*frame_->GetDocument()));
+ return frame_caret_->AbsoluteCaretBounds();
+}
+
+bool FrameSelection::ComputeAbsoluteBounds(IntRect& anchor,
+ IntRect& focus) const {
+ if (!IsAvailable() || GetSelectionInDOMTree().IsNone())
+ return false;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ frame_->GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+ if (ComputeVisibleSelectionInDOMTree().IsNone()) {
+ // plugins/mouse-capture-inside-shadow.html reaches here.
+ return false;
+ }
+
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ frame_->GetDocument()->Lifecycle());
+
+ if (ComputeVisibleSelectionInDOMTree().IsCaret()) {
+ anchor = focus = AbsoluteCaretBounds();
+ } else {
+ const EphemeralRange selected_range =
+ ComputeVisibleSelectionInDOMTree().ToNormalizedEphemeralRange();
+ if (selected_range.IsNull())
+ return false;
+ anchor = FirstRectForRange(EphemeralRange(selected_range.StartPosition()));
+ focus = FirstRectForRange(EphemeralRange(selected_range.EndPosition()));
+ }
+
+ if (!ComputeVisibleSelectionInDOMTree().IsBaseFirst())
+ std::swap(anchor, focus);
+ return true;
+}
+
+void FrameSelection::PaintCaret(GraphicsContext& context,
+ const LayoutPoint& paint_offset) {
+ frame_caret_->PaintCaret(context, paint_offset);
+}
+
+bool FrameSelection::Contains(const LayoutPoint& point) {
+ if (!GetDocument().GetLayoutView())
+ return false;
+
+ // Treat a collapsed selection like no selection.
+ const VisibleSelectionInFlatTree& visible_selection =
+ ComputeVisibleSelectionInFlatTree();
+ if (!visible_selection.IsRange())
+ return false;
+
+ HitTestRequest request(HitTestRequest::kReadOnly | HitTestRequest::kActive);
+ HitTestResult result(request, point);
+ GetDocument().GetLayoutView()->HitTest(result);
+ Node* inner_node = result.InnerNode();
+ if (!inner_node || !inner_node->GetLayoutObject())
+ return false;
+
+ const VisiblePositionInFlatTree& visible_pos =
+ CreateVisiblePosition(FromPositionInDOMTree<EditingInFlatTreeStrategy>(
+ inner_node->GetLayoutObject()->PositionForPoint(
+ result.LocalPoint())));
+ if (visible_pos.IsNull())
+ return false;
+
+ const VisiblePositionInFlatTree& visible_start =
+ visible_selection.VisibleStart();
+ const VisiblePositionInFlatTree& visible_end = visible_selection.VisibleEnd();
+ if (visible_start.IsNull() || visible_end.IsNull())
+ return false;
+
+ const PositionInFlatTree& start = visible_start.DeepEquivalent();
+ const PositionInFlatTree& end = visible_end.DeepEquivalent();
+ const PositionInFlatTree& pos = visible_pos.DeepEquivalent();
+ return start.CompareTo(pos) <= 0 && pos.CompareTo(end) <= 0;
+}
+
+// Workaround for the fact that it's hard to delete a frame.
+// Call this after doing user-triggered selections to make it easy to delete the
+// frame you entirely selected. Can't do this implicitly as part of every
+// setSelection call because in some contexts it might not be good for the focus
+// to move to another frame. So instead we call it from places where we are
+// selecting with the mouse or the keyboard after setting the selection.
+void FrameSelection::SelectFrameElementInParentIfFullySelected() {
+ // Find the parent frame; if there is none, then we have nothing to do.
+ Frame* parent = frame_->Tree().Parent();
+ if (!parent)
+ return;
+ Page* page = frame_->GetPage();
+ if (!page)
+ return;
+
+ // Check if the selection contains the entire frame contents; if not, then
+ // there is nothing to do.
+ if (GetSelectionInDOMTree().Type() != kRangeSelection) {
+ return;
+ }
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (!IsStartOfDocument(ComputeVisibleSelectionInDOMTree().VisibleStart()))
+ return;
+ if (!IsEndOfDocument(ComputeVisibleSelectionInDOMTree().VisibleEnd()))
+ return;
+
+ // FIXME: This is not yet implemented for cross-process frame relationships.
+ if (!parent->IsLocalFrame())
+ return;
+
+ // Get to the <iframe> or <frame> (or even <object>) element in the parent
+ // frame.
+ // FIXME: Doesn't work for OOPI.
+ HTMLFrameOwnerElement* owner_element = frame_->DeprecatedLocalOwner();
+ if (!owner_element)
+ return;
+ ContainerNode* owner_element_parent = owner_element->parentNode();
+ if (!owner_element_parent)
+ return;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ owner_element_parent->GetDocument()
+ .UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // This method's purpose is it to make it easier to select iframes (in order
+ // to delete them). Don't do anything if the iframe isn't deletable.
+ if (!blink::HasEditableStyle(*owner_element_parent))
+ return;
+
+ // Create compute positions before and after the element.
+ unsigned owner_element_node_index = owner_element->NodeIndex();
+ VisiblePosition before_owner_element = CreateVisiblePosition(
+ Position(owner_element_parent, owner_element_node_index));
+ VisiblePosition after_owner_element = CreateVisiblePosition(
+ Position(owner_element_parent, owner_element_node_index + 1),
+ TextAffinity::kUpstreamIfPossible);
+
+ SelectionInDOMTree::Builder builder;
+ builder
+ .SetBaseAndExtentDeprecated(before_owner_element.DeepEquivalent(),
+ after_owner_element.DeepEquivalent())
+ .SetAffinity(before_owner_element.Affinity());
+
+ // Focus on the parent frame, and then select from before this element to
+ // after.
+ VisibleSelection new_selection = CreateVisibleSelection(builder.Build());
+ // TODO(yosin): We should call |FocusController::setFocusedFrame()| before
+ // |createVisibleSelection()|.
+ page->GetFocusController().SetFocusedFrame(parent);
+ // setFocusedFrame can dispatch synchronous focus/blur events. The document
+ // tree might be modified.
+ if (!new_selection.IsNone() &&
+ new_selection.IsValidFor(*(ToLocalFrame(parent)->GetDocument()))) {
+ ToLocalFrame(parent)->Selection().SetSelectionAndEndTyping(
+ new_selection.AsSelection());
+ }
+}
+
+// Returns a shadow tree node for legacy shadow trees, a child of the
+// ShadowRoot node for new shadow trees, or 0 for non-shadow trees.
+static Node* NonBoundaryShadowTreeRootNode(const Position& position) {
+ return position.AnchorNode() && !position.AnchorNode()->IsShadowRoot()
+ ? position.AnchorNode()->NonBoundaryShadowTreeRootNode()
+ : nullptr;
+}
+
+void FrameSelection::SelectAll(SetSelectionBy set_selection_by) {
+ if (auto* select_element =
+ ToHTMLSelectElementOrNull(GetDocument().FocusedElement())) {
+ if (select_element->CanSelectAll()) {
+ select_element->SelectAll();
+ return;
+ }
+ }
+
+ Node* root = nullptr;
+ Node* select_start_target = nullptr;
+ if (set_selection_by == SetSelectionBy::kUser && IsHidden()) {
+ // Hidden selection appears as no selection to user, in which case user-
+ // triggered SelectAll should act as if there is no selection.
+ root = GetDocument().documentElement();
+ select_start_target = GetDocument().body();
+ } else if (ComputeVisibleSelectionInDOMTree().IsContentEditable()) {
+ root = HighestEditableRoot(ComputeVisibleSelectionInDOMTree().Start());
+ if (Node* shadow_root = NonBoundaryShadowTreeRootNode(
+ ComputeVisibleSelectionInDOMTree().Start()))
+ select_start_target = shadow_root->OwnerShadowHost();
+ else
+ select_start_target = root;
+ } else {
+ root = NonBoundaryShadowTreeRootNode(
+ ComputeVisibleSelectionInDOMTree().Start());
+ if (root) {
+ select_start_target = root->OwnerShadowHost();
+ } else {
+ root = GetDocument().documentElement();
+ select_start_target = GetDocument().body();
+ }
+ }
+ if (!root || EditingIgnoresContent(*root))
+ return;
+
+ if (select_start_target) {
+ const Document& expected_document = GetDocument();
+ if (select_start_target->DispatchEvent(Event::CreateCancelableBubble(
+ EventTypeNames::selectstart)) != DispatchEventResult::kNotCanceled)
+ return;
+ // The frame may be detached due to selectstart event.
+ if (!IsAvailable()) {
+ // Reached by editing/selection/selectstart_detach_frame.html
+ return;
+ }
+ // |root| may be detached due to selectstart event.
+ if (!root->isConnected() || expected_document != root->GetDocument())
+ return;
+ }
+
+ // TODO(editing-dev): Should we pass in set_selection_by?
+ SetSelection(SelectionInDOMTree::Builder().SelectAllChildren(*root).Build(),
+ SetSelectionOptions::Builder()
+ .SetShouldCloseTyping(true)
+ .SetShouldClearTypingStyle(true)
+ .SetShouldShowHandle(IsHandleVisible())
+ .Build());
+ SelectFrameElementInParentIfFullySelected();
+ // TODO(editing-dev): Should we pass in set_selection_by?
+ NotifyTextControlOfSelectionChange(SetSelectionBy::kUser);
+ if (IsHandleVisible()) {
+ ContextMenuAllowedScope scope;
+ frame_->GetEventHandler().ShowNonLocatedContextMenu(nullptr,
+ kMenuSourceTouch);
+ }
+}
+
+void FrameSelection::SelectAll() {
+ SelectAll(SetSelectionBy::kSystem);
+}
+
+// Implementation of |SVGTextControlElement::selectSubString()|
+void FrameSelection::SelectSubString(const Element& element,
+ int offset,
+ int length) {
+ // Find selection start
+ VisiblePosition start = VisiblePosition::FirstPositionInNode(element);
+ for (int i = 0; i < offset; ++i)
+ start = NextPositionOf(start);
+ if (start.IsNull())
+ return;
+
+ // Find selection end
+ VisiblePosition end(start);
+ for (int i = 0; i < length; ++i)
+ end = NextPositionOf(end);
+ if (end.IsNull())
+ return;
+
+ // TODO(editing-dev): We assume |start| and |end| are not null and we don't
+ // known when |start| and |end| are null. Once we get a such case, we check
+ // null for |start| and |end|.
+ SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(start.DeepEquivalent(), end.DeepEquivalent())
+ .SetAffinity(start.Affinity())
+ .Build());
+}
+
+void FrameSelection::NotifyAccessibilityForSelectionChange() {
+ if (GetSelectionInDOMTree().IsNone())
+ return;
+ AXObjectCache* cache = GetDocument().ExistingAXObjectCache();
+ if (!cache)
+ return;
+ const Position& start = GetSelectionInDOMTree().ComputeStartPosition();
+ cache->SelectionChanged(start.ComputeContainerNode());
+}
+
+void FrameSelection::NotifyCompositorForSelectionChange() {
+ if (!RuntimeEnabledFeatures::CompositedSelectionUpdateEnabled())
+ return;
+
+ ScheduleVisualUpdate();
+}
+
+void FrameSelection::NotifyEventHandlerForSelectionChange() {
+ frame_->GetEventHandler().GetSelectionController().NotifySelectionChanged();
+}
+
+void FrameSelection::FocusedOrActiveStateChanged() {
+ bool active_and_focused = FrameIsFocusedAndActive();
+
+ // Trigger style invalidation from the focused element. Even though
+ // the focused element hasn't changed, the evaluation of focus pseudo
+ // selectors are dependent on whether the frame is focused and active.
+ if (Element* element = GetDocument().FocusedElement())
+ element->FocusStateChanged();
+
+ GetDocument().UpdateStyleAndLayoutTree();
+
+ // Because LayoutObject::selectionBackgroundColor() and
+ // LayoutObject::selectionForegroundColor() check if the frame is active,
+ // we have to update places those colors were painted.
+ auto* view = GetDocument().GetLayoutView();
+ if (view)
+ layout_selection_->InvalidatePaintForSelection();
+
+ // Caret appears in the active frame.
+ if (active_and_focused)
+ SetSelectionFromNone();
+ frame_caret_->SetCaretVisibility(active_and_focused
+ ? CaretVisibility::kVisible
+ : CaretVisibility::kHidden);
+
+ // Update for caps lock state
+ frame_->GetEventHandler().CapsLockStateMayHaveChanged();
+}
+
+void FrameSelection::PageActivationChanged() {
+ FocusedOrActiveStateChanged();
+}
+
+void FrameSelection::SetFrameIsFocused(bool flag) {
+ if (focused_ == flag)
+ return;
+ focused_ = flag;
+
+ FocusedOrActiveStateChanged();
+}
+
+bool FrameSelection::FrameIsFocusedAndActive() const {
+ return focused_ && frame_->GetPage() &&
+ frame_->GetPage()->GetFocusController().IsActive();
+}
+
+bool FrameSelection::NeedsLayoutSelectionUpdate() const {
+ return layout_selection_->HasPendingSelection();
+}
+
+void FrameSelection::CommitAppearanceIfNeeded() {
+ return layout_selection_->Commit();
+}
+
+void FrameSelection::DidLayout() {
+ UpdateAppearance();
+}
+
+void FrameSelection::UpdateAppearance() {
+ DCHECK(frame_->ContentLayoutObject());
+ frame_caret_->ScheduleVisualUpdateForPaintInvalidationIfNeeded();
+ layout_selection_->SetHasPendingSelection();
+}
+
+void FrameSelection::NotifyTextControlOfSelectionChange(
+ SetSelectionBy set_selection_by) {
+ TextControlElement* text_control =
+ EnclosingTextControl(GetSelectionInDOMTree().Base());
+ if (!text_control)
+ return;
+ text_control->SelectionChanged(set_selection_by == SetSelectionBy::kUser);
+}
+
+// Helper function that tells whether a particular node is an element that has
+// an entire LocalFrame and LocalFrameView, a <frame>, <iframe>, or <object>.
+static bool IsFrameElement(const Node* n) {
+ if (!n)
+ return false;
+ LayoutObject* layout_object = n->GetLayoutObject();
+ if (!layout_object || !layout_object->IsLayoutEmbeddedContent())
+ return false;
+ return ToLayoutEmbeddedContent(layout_object)->ChildFrameView();
+}
+
+void FrameSelection::SetFocusedNodeIfNeeded() {
+ if (ComputeVisibleSelectionInDOMTreeDeprecated().IsNone() ||
+ !FrameIsFocused())
+ return;
+
+ if (Element* target =
+ ComputeVisibleSelectionInDOMTreeDeprecated().RootEditableElement()) {
+ // Walk up the DOM tree to search for a node to focus.
+ GetDocument().UpdateStyleAndLayoutTreeIgnorePendingStylesheets();
+ while (target) {
+ // We don't want to set focus on a subframe when selecting in a parent
+ // frame, so add the !isFrameElement check here. There's probably a better
+ // way to make this work in the long term, but this is the safest fix at
+ // this time.
+ if (target->IsMouseFocusable() && !IsFrameElement(target)) {
+ frame_->GetPage()->GetFocusController().SetFocusedElement(target,
+ frame_);
+ return;
+ }
+ target = target->ParentOrShadowHostElement();
+ }
+ GetDocument().ClearFocusedElement();
+ }
+}
+
+static String ExtractSelectedText(const FrameSelection& selection,
+ TextIteratorBehavior behavior) {
+ const VisibleSelectionInFlatTree& visible_selection =
+ selection.ComputeVisibleSelectionInFlatTree();
+ const EphemeralRangeInFlatTree& range =
+ visible_selection.ToNormalizedEphemeralRange();
+ // We remove '\0' characters because they are not visibly rendered to the
+ // user.
+ return PlainText(range, behavior).Replace(0, "");
+}
+
+String FrameSelection::SelectedHTMLForClipboard() const {
+ const VisibleSelectionInFlatTree& visible_selection =
+ ComputeVisibleSelectionInFlatTree();
+ const EphemeralRangeInFlatTree& range =
+ visible_selection.ToNormalizedEphemeralRange();
+ return CreateMarkup(
+ range.StartPosition(), range.EndPosition(), kAnnotateForInterchange,
+ ConvertBlocksToInlines::kNotConvert, kResolveNonLocalURLs);
+}
+
+String FrameSelection::SelectedText(
+ const TextIteratorBehavior& behavior) const {
+ return ExtractSelectedText(*this, behavior);
+}
+
+String FrameSelection::SelectedText() const {
+ return SelectedText(TextIteratorBehavior());
+}
+
+String FrameSelection::SelectedTextForClipboard() const {
+ return ExtractSelectedText(
+ *this, TextIteratorBehavior::Builder()
+ .SetEmitsImageAltText(
+ frame_->GetSettings() &&
+ frame_->GetSettings()->GetSelectionIncludesAltImageText())
+ .SetSkipsUnselectableContent(true)
+ .Build());
+}
+
+LayoutRect FrameSelection::AbsoluteUnclippedBounds() const {
+ LocalFrameView* view = frame_->View();
+ LayoutView* layout_view = frame_->ContentLayoutObject();
+
+ if (!view || !layout_view)
+ return LayoutRect();
+
+ view->UpdateLifecycleToLayoutClean();
+ return LayoutRect(layout_selection_->AbsoluteSelectionBounds());
+}
+
+IntRect FrameSelection::ComputeRectToScroll(
+ RevealExtentOption reveal_extent_option) {
+ const VisibleSelection& selection = ComputeVisibleSelectionInDOMTree();
+ if (selection.IsCaret())
+ return AbsoluteCaretBounds();
+ DCHECK(selection.IsRange());
+ if (reveal_extent_option == kRevealExtent)
+ return AbsoluteCaretBoundsOf(CreateVisiblePosition(selection.Extent()));
+ layout_selection_->SetHasPendingSelection();
+ return layout_selection_->AbsoluteSelectionBounds();
+}
+
+// TODO(editing-dev): This should be done in FlatTree world.
+void FrameSelection::RevealSelection(const ScrollAlignment& alignment,
+ RevealExtentOption reveal_extent_option) {
+ DCHECK(IsAvailable());
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ // Calculation of absolute caret bounds requires clean layout.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ const VisibleSelection& selection = ComputeVisibleSelectionInDOMTree();
+ if (selection.IsNone())
+ return;
+
+ // FIXME: This code only handles scrolling the startContainer's layer, but
+ // the selection rect could intersect more than just that.
+ if (DocumentLoader* document_loader = frame_->Loader().GetDocumentLoader())
+ document_loader->GetInitialScrollState().was_scrolled_by_user = true;
+ const Position& start = selection.Start();
+ DCHECK(start.AnchorNode());
+ DCHECK(start.AnchorNode()->GetLayoutObject());
+ // This function is needed to make sure that ComputeRectToScroll below has the
+ // sticky offset info available before the computation.
+ GetDocument().EnsurePaintLocationDataValidForNode(start.AnchorNode());
+ LayoutRect selection_rect =
+ LayoutRect(ComputeRectToScroll(reveal_extent_option));
+ if (selection_rect == LayoutRect() ||
+ !start.AnchorNode()->GetLayoutObject()->EnclosingBox())
+ return;
+
+ start.AnchorNode()->GetLayoutObject()->ScrollRectToVisible(
+ selection_rect, WebScrollIntoViewParams(alignment, alignment));
+ UpdateAppearance();
+}
+
+void FrameSelection::SetSelectionFromNone() {
+ // Put a caret inside the body if the entire frame is editable (either the
+ // entire WebView is editable or designMode is on for this document).
+
+ Document* document = frame_->GetDocument();
+ if (!ComputeVisibleSelectionInDOMTreeDeprecated().IsNone() ||
+ !(blink::HasEditableStyle(*document)))
+ return;
+
+ Element* document_element = document->documentElement();
+ if (!document_element)
+ return;
+ if (HTMLBodyElement* body =
+ Traversal<HTMLBodyElement>::FirstChild(*document_element)) {
+ SetSelectionAndEndTyping(SelectionInDOMTree::Builder()
+ .Collapse(FirstPositionInOrBeforeNode(*body))
+ .Build());
+ }
+}
+
+// TODO(yoichio): We should have LocalFrame having FrameCaret,
+// Editor and PendingSelection using FrameCaret directly
+// and get rid of this.
+bool FrameSelection::ShouldShowBlockCursor() const {
+ return frame_caret_->ShouldShowBlockCursor();
+}
+
+// TODO(yoichio): We should have LocalFrame having FrameCaret,
+// Editor and PendingSelection using FrameCaret directly
+// and get rid of this.
+// TODO(yoichio): We should use "caret-shape" in "CSS Basic User Interface
+// Module Level 4" https://drafts.csswg.org/css-ui-4/
+// To use "caret-shape", we need to expose inserting mode information to CSS;
+// https://github.com/w3c/csswg-drafts/issues/133
+void FrameSelection::SetShouldShowBlockCursor(bool should_show_block_cursor) {
+ frame_caret_->SetShouldShowBlockCursor(should_show_block_cursor);
+}
+
+#ifndef NDEBUG
+
+void FrameSelection::ShowTreeForThis() const {
+ ComputeVisibleSelectionInDOMTreeDeprecated().ShowTreeForThis();
+}
+
+#endif
+
+void FrameSelection::Trace(blink::Visitor* visitor) {
+ visitor->Trace(frame_);
+ visitor->Trace(layout_selection_);
+ visitor->Trace(selection_editor_);
+ visitor->Trace(frame_caret_);
+ SynchronousMutationObserver::Trace(visitor);
+}
+
+void FrameSelection::ScheduleVisualUpdate() const {
+ if (Page* page = frame_->GetPage())
+ page->Animator().ScheduleVisualUpdate(&frame_->LocalFrameRoot());
+}
+
+void FrameSelection::ScheduleVisualUpdateForPaintInvalidationIfNeeded() const {
+ if (LocalFrameView* frame_view = frame_->View())
+ frame_view->ScheduleVisualUpdateForPaintInvalidationIfNeeded();
+}
+
+bool FrameSelection::SelectWordAroundCaret() {
+ const VisibleSelection& selection = ComputeVisibleSelectionInDOMTree();
+ // TODO(editing-dev): The use of VisibleSelection needs to be audited. See
+ // http://crbug.com/657237 for more details.
+ if (!selection.IsCaret())
+ return false;
+ const VisiblePosition& position = selection.VisibleStart();
+ static const EWordSide kWordSideList[2] = {kNextWordIfOnBoundary,
+ kPreviousWordIfOnBoundary};
+ for (EWordSide word_side : kWordSideList) {
+ // TODO(yoichio): We should have Position version of |start/endOfWord|
+ // for avoiding unnecessary canonicalization.
+ VisiblePosition start = StartOfWord(position, word_side);
+ VisiblePosition end = EndOfWord(position, word_side);
+ String text =
+ PlainText(EphemeralRange(start.DeepEquivalent(), end.DeepEquivalent()));
+ if (!text.IsEmpty() && !IsSeparator(text.CharacterStartingAt(0))) {
+ SetSelection(SelectionInDOMTree::Builder()
+ .Collapse(start.ToPositionWithAffinity())
+ .Extend(end.DeepEquivalent())
+ .Build(),
+ SetSelectionOptions::Builder()
+ .SetShouldCloseTyping(true)
+ .SetShouldClearTypingStyle(true)
+ .SetGranularity(TextGranularity::kWord)
+ .Build());
+ return true;
+ }
+ }
+
+ return false;
+}
+
+GranularityStrategy* FrameSelection::GetGranularityStrategy() {
+ // We do lazy initalization for m_granularityStrategy, because if we
+ // initialize it right in the constructor - the correct settings may not be
+ // set yet.
+ SelectionStrategy strategy_type = SelectionStrategy::kCharacter;
+ Settings* settings = frame_ ? frame_->GetSettings() : nullptr;
+ if (settings &&
+ settings->GetSelectionStrategy() == SelectionStrategy::kDirection)
+ strategy_type = SelectionStrategy::kDirection;
+
+ if (granularity_strategy_ &&
+ granularity_strategy_->GetType() == strategy_type)
+ return granularity_strategy_.get();
+
+ if (strategy_type == SelectionStrategy::kDirection)
+ granularity_strategy_ = std::make_unique<DirectionGranularityStrategy>();
+ else
+ granularity_strategy_ = std::make_unique<CharacterGranularityStrategy>();
+ return granularity_strategy_.get();
+}
+
+void FrameSelection::MoveRangeSelectionExtent(const IntPoint& contents_point) {
+ if (ComputeVisibleSelectionInDOMTree().IsNone())
+ return;
+
+ SetSelection(
+ SelectionInDOMTree::Builder(
+ GetGranularityStrategy()->UpdateExtent(contents_point, frame_))
+ .Build(),
+ SetSelectionOptions::Builder()
+ .SetShouldCloseTyping(true)
+ .SetShouldClearTypingStyle(true)
+ .SetDoNotClearStrategy(true)
+ .SetSetSelectionBy(SetSelectionBy::kUser)
+ .SetShouldShowHandle(true)
+ .Build());
+}
+
+void FrameSelection::MoveRangeSelection(const IntPoint& base_point,
+ const IntPoint& extent_point,
+ TextGranularity granularity) {
+ const VisiblePosition& base_position =
+ VisiblePositionForContentsPoint(base_point, GetFrame());
+ const VisiblePosition& extent_position =
+ VisiblePositionForContentsPoint(extent_point, GetFrame());
+ MoveRangeSelectionInternal(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtentDeprecated(base_position.DeepEquivalent(),
+ extent_position.DeepEquivalent())
+ .SetAffinity(base_position.Affinity())
+ .Build(),
+ granularity);
+}
+
+void FrameSelection::MoveRangeSelectionInternal(
+ const SelectionInDOMTree& new_selection,
+ TextGranularity granularity) {
+ if (new_selection.IsNone())
+ return;
+
+ const VisibleSelection& visible_selection =
+ CreateVisibleSelectionWithGranularity(new_selection, granularity);
+ if (visible_selection.IsNone())
+ return;
+
+ SelectionInDOMTree::Builder builder;
+ if (visible_selection.IsBaseFirst()) {
+ builder.SetBaseAndExtent(visible_selection.Start(),
+ visible_selection.End());
+ } else {
+ builder.SetBaseAndExtent(visible_selection.End(),
+ visible_selection.Start());
+ }
+ builder.SetAffinity(visible_selection.Affinity());
+ SetSelection(builder.Build(), SetSelectionOptions::Builder()
+ .SetShouldCloseTyping(true)
+ .SetShouldClearTypingStyle(true)
+ .SetGranularity(granularity)
+ .SetShouldShowHandle(IsHandleVisible())
+ .Build());
+}
+
+void FrameSelection::SetCaretVisible(bool caret_is_visible) {
+ frame_caret_->SetCaretVisibility(caret_is_visible ? CaretVisibility::kVisible
+ : CaretVisibility::kHidden);
+}
+
+void FrameSelection::SetCaretBlinkingSuspended(bool suspended) {
+ frame_caret_->SetCaretBlinkingSuspended(suspended);
+}
+
+bool FrameSelection::IsCaretBlinkingSuspended() const {
+ return frame_caret_->IsCaretBlinkingSuspended();
+}
+
+void FrameSelection::CacheRangeOfDocument(Range* range) {
+ selection_editor_->CacheRangeOfDocument(range);
+}
+
+Range* FrameSelection::DocumentCachedRange() const {
+ return selection_editor_->DocumentCachedRange();
+}
+
+void FrameSelection::ClearDocumentCachedRange() {
+ selection_editor_->ClearDocumentCachedRange();
+}
+
+WTF::Optional<unsigned> FrameSelection::LayoutSelectionStart() const {
+ return layout_selection_->SelectionStart();
+}
+WTF::Optional<unsigned> FrameSelection::LayoutSelectionEnd() const {
+ return layout_selection_->SelectionEnd();
+}
+
+void FrameSelection::ClearLayoutSelection() {
+ layout_selection_->ClearSelection();
+}
+
+std::pair<unsigned, unsigned> FrameSelection::LayoutSelectionStartEndForNG(
+ const NGPhysicalTextFragment& text_fragment) const {
+ return layout_selection_->SelectionStartEndForNG(text_fragment);
+}
+
+bool FrameSelection::IsDirectional() const {
+ return is_directional_;
+}
+
+} // namespace blink
+
+#ifndef NDEBUG
+
+void showTree(const blink::FrameSelection& sel) {
+ sel.ShowTreeForThis();
+}
+
+void showTree(const blink::FrameSelection* sel) {
+ if (sel)
+ sel->ShowTreeForThis();
+ else
+ LOG(INFO) << "Cannot showTree for <null> FrameSelection.";
+}
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/frame_selection.h b/chromium/third_party/blink/renderer/core/editing/frame_selection.h
new file mode 100644
index 00000000000..2a2c208c860
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/frame_selection.h
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FRAME_SELECTION_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FRAME_SELECTION_H_
+
+#include <memory>
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/dom/synchronous_mutation_observer.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/set_selection_options.h"
+#include "third_party/blink/renderer/platform/geometry/int_rect.h"
+#include "third_party/blink/renderer/platform/geometry/layout_rect.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/scroll/scroll_alignment.h"
+#include "third_party/blink/renderer/platform/wtf/optional.h"
+
+namespace blink {
+
+class DisplayItemClient;
+class Element;
+class LayoutBlock;
+class LocalFrame;
+class FrameCaret;
+class GranularityStrategy;
+class GraphicsContext;
+class NGPhysicalTextFragment;
+class Range;
+class SelectionEditor;
+class LayoutSelection;
+enum class SelectionModifyAlteration;
+enum class SelectionModifyDirection;
+class TextIteratorBehavior;
+struct PaintInvalidatorContext;
+
+enum RevealExtentOption { kRevealExtent, kDoNotRevealExtent };
+
+enum class CaretVisibility;
+
+enum class HandleVisibility { kNotVisible, kVisible };
+
+class CORE_EXPORT FrameSelection final
+ : public GarbageCollectedFinalized<FrameSelection>,
+ public SynchronousMutationObserver {
+ USING_GARBAGE_COLLECTED_MIXIN(FrameSelection);
+
+ public:
+ static FrameSelection* Create(LocalFrame& frame) {
+ return new FrameSelection(frame);
+ }
+ ~FrameSelection();
+
+ bool IsAvailable() const { return LifecycleContext(); }
+ // You should not call |document()| when |!isAvailable()|.
+ Document& GetDocument() const;
+ LocalFrame* GetFrame() const { return frame_; }
+ Element* RootEditableElementOrDocumentElement() const;
+ size_t CharacterIndexForPoint(const IntPoint&) const;
+
+ // An implementation of |WebFrame::moveCaretSelection()|
+ void MoveCaretSelection(const IntPoint&);
+
+ VisibleSelection ComputeVisibleSelectionInDOMTree() const;
+ VisibleSelectionInFlatTree ComputeVisibleSelectionInFlatTree() const;
+
+ // TODO(editing-dev): We should replace
+ // |computeVisibleSelectionInDOMTreeDeprecated()| with update layout and
+ // |computeVisibleSelectionInDOMTree()| to increase places hoisting update
+ // layout.
+ VisibleSelection ComputeVisibleSelectionInDOMTreeDeprecated() const;
+
+ void SetSelection(const SelectionInDOMTree&, const SetSelectionOptions&);
+ void SetSelectionAndEndTyping(const SelectionInDOMTree&);
+ void SelectAll(SetSelectionBy);
+ void SelectAll();
+ void SelectSubString(const Element&, int offset, int count);
+ void Clear();
+ bool IsHidden() const;
+
+ // TODO(tkent): These two functions were added to fix crbug.com/695211 without
+ // changing focus behavior. Once we fix crbug.com/690272, we can remove these
+ // functions.
+ // setSelectionDeprecated() returns true if didSetSelectionDeprecated() should
+ // be called.
+ bool SetSelectionDeprecated(const SelectionInDOMTree&,
+ const SetSelectionOptions&);
+ void DidSetSelectionDeprecated(const SetSelectionOptions&);
+
+ // Call this after doing user-triggered selections to make it easy to delete
+ // the frame you entirely selected.
+ void SelectFrameElementInParentIfFullySelected();
+
+ bool Contains(const LayoutPoint&);
+
+ bool Modify(SelectionModifyAlteration,
+ SelectionModifyDirection,
+ TextGranularity,
+ SetSelectionBy);
+
+ // Moves the selection extent based on the selection granularity strategy.
+ // This function does not allow the selection to collapse. If the new
+ // extent is resolved to the same position as the current base, this
+ // function will do nothing.
+ void MoveRangeSelectionExtent(const IntPoint&);
+ void MoveRangeSelection(const IntPoint& base_point,
+ const IntPoint& extent_point,
+ TextGranularity);
+
+ TextGranularity Granularity() const { return granularity_; }
+
+ // Returns true if specified layout block should paint caret. This function is
+ // called during painting only.
+ bool ShouldPaintCaret(const LayoutBlock&) const;
+
+ // Bounds of (possibly transformed) caret in absolute coords
+ IntRect AbsoluteCaretBounds() const;
+
+ // Returns anchor and focus bounds in absolute coords.
+ // If the selection range is empty, returns the caret bounds.
+ // Note: this updates styles and layout, use cautiously.
+ bool ComputeAbsoluteBounds(IntRect& anchor, IntRect& focus) const;
+
+ void DidChangeFocus();
+
+ SelectionInDOMTree GetSelectionInDOMTree() const;
+ bool IsDirectional() const;
+
+ void DocumentAttached(Document*);
+
+ void DidLayout();
+ bool NeedsLayoutSelectionUpdate() const;
+ void CommitAppearanceIfNeeded();
+ void SetCaretVisible(bool caret_is_visible);
+ void ScheduleVisualUpdate() const;
+ void ScheduleVisualUpdateForPaintInvalidationIfNeeded() const;
+
+ // Paint invalidation methods delegating to FrameCaret.
+ void ClearPreviousCaretVisualRect(const LayoutBlock&);
+ void LayoutBlockWillBeDestroyed(const LayoutBlock&);
+ void UpdateStyleAndLayoutIfNeeded();
+ void InvalidatePaint(const LayoutBlock&, const PaintInvalidatorContext&);
+
+ void PaintCaret(GraphicsContext&, const LayoutPoint&);
+
+ // Used to suspend caret blinking while the mouse is down.
+ void SetCaretBlinkingSuspended(bool);
+ bool IsCaretBlinkingSuspended() const;
+
+ // Focus
+ bool SelectionHasFocus() const;
+ void SetFrameIsFocused(bool);
+ bool FrameIsFocused() const { return focused_; }
+ bool FrameIsFocusedAndActive() const;
+ void PageActivationChanged();
+
+ bool IsHandleVisible() const { return is_handle_visible_; }
+ bool ShouldShrinkNextTap() const { return should_shrink_next_tap_; }
+
+ // Returns true if a word is selected.
+ bool SelectWordAroundCaret();
+
+#ifndef NDEBUG
+ void ShowTreeForThis() const;
+#endif
+
+ void SetFocusedNodeIfNeeded();
+ void NotifyTextControlOfSelectionChange(SetSelectionBy);
+
+ String SelectedHTMLForClipboard() const;
+ String SelectedText(const TextIteratorBehavior&) const;
+ String SelectedText() const;
+ String SelectedTextForClipboard() const;
+
+ // This returns last layouted selection bounds of LayoutSelection rather than
+ // SelectionEditor keeps.
+ LayoutRect AbsoluteUnclippedBounds() const;
+
+ // TODO(tkent): This function has a bug that scrolling doesn't work well in
+ // a case of RangeSelection. crbug.com/443061
+ void RevealSelection(
+ const ScrollAlignment& = ScrollAlignment::kAlignCenterIfNeeded,
+ RevealExtentOption = kDoNotRevealExtent);
+ void SetSelectionFromNone();
+
+ void UpdateAppearance();
+ bool ShouldShowBlockCursor() const;
+ void SetShouldShowBlockCursor(bool);
+
+ void CacheRangeOfDocument(Range*);
+ Range* DocumentCachedRange() const;
+ void ClearDocumentCachedRange();
+
+ FrameCaret& FrameCaretForTesting() const { return *frame_caret_; }
+
+ WTF::Optional<unsigned> LayoutSelectionStart() const;
+ WTF::Optional<unsigned> LayoutSelectionEnd() const;
+ void ClearLayoutSelection();
+ std::pair<unsigned, unsigned> LayoutSelectionStartEndForNG(
+ const NGPhysicalTextFragment&) const;
+
+ void Trace(blink::Visitor*);
+
+ private:
+ friend class CaretDisplayItemClientTest;
+ friend class FrameSelectionTest;
+ friend class PaintControllerPaintTestBase;
+ friend class SelectionControllerTest;
+
+ explicit FrameSelection(LocalFrame&);
+
+ const DisplayItemClient& CaretDisplayItemClientForTesting() const;
+
+ // Note: We have |selectionInFlatTree()| for unit tests, we should
+ // use |visibleSelection<EditingInFlatTreeStrategy>()|.
+ VisibleSelectionInFlatTree GetSelectionInFlatTree() const;
+
+ void NotifyAccessibilityForSelectionChange();
+ void NotifyCompositorForSelectionChange();
+ void NotifyEventHandlerForSelectionChange();
+
+ void FocusedOrActiveStateChanged();
+
+ GranularityStrategy* GetGranularityStrategy();
+
+ IntRect ComputeRectToScroll(RevealExtentOption);
+
+ void MoveRangeSelectionInternal(const SelectionInDOMTree&, TextGranularity);
+
+ // Implementation of |SynchronousMutationObserver| member functions.
+ void ContextDestroyed(Document*) final;
+ void NodeChildrenWillBeRemoved(ContainerNode&) final;
+ void NodeWillBeRemoved(Node&) final;
+
+ Member<LocalFrame> frame_;
+ const Member<LayoutSelection> layout_selection_;
+ const Member<SelectionEditor> selection_editor_;
+
+ TextGranularity granularity_;
+ LayoutUnit x_pos_for_vertical_arrow_navigation_;
+
+ bool focused_ : 1;
+ bool is_handle_visible_ = false;
+ // TODO(editing-dev): We should change is_directional_ type to enum.
+ // as directional can have three values forward, backward or directionless.
+ bool is_directional_;
+ bool should_shrink_next_tap_ = false;
+
+ // Controls text granularity used to adjust the selection's extent in
+ // moveRangeSelectionExtent.
+ std::unique_ptr<GranularityStrategy> granularity_strategy_;
+
+ const Member<FrameCaret> frame_caret_;
+
+ DISALLOW_COPY_AND_ASSIGN(FrameSelection);
+};
+
+} // namespace blink
+
+#ifndef NDEBUG
+// Outside the WebCore namespace for ease of invocation from gdb.
+void showTree(const blink::FrameSelection&);
+void showTree(const blink::FrameSelection*);
+#endif
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_FRAME_SELECTION_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/frame_selection_test.cc b/chromium/third_party/blink/renderer/core/editing/frame_selection_test.cc
new file mode 100644
index 00000000000..99665b2102d
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/frame_selection_test.cc
@@ -0,0 +1,1086 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+
+#include <memory>
+#include "base/memory/scoped_refptr.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_caret.h"
+#include "third_party/blink/renderer/core/editing/selection_controller.h"
+#include "third_party/blink/renderer/core/editing/selection_modifier.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/html/html_body_element.h"
+#include "third_party/blink/renderer/core/input/event_handler.h"
+#include "third_party/blink/renderer/core/layout/layout_block.h"
+#include "third_party/blink/renderer/core/paint/paint_info.h"
+#include "third_party/blink/renderer/core/paint/paint_layer.h"
+#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
+#include "third_party/blink/renderer/platform/graphics/paint/drawing_recorder.h"
+#include "third_party/blink/renderer/platform/graphics/paint/paint_controller.h"
+#include "third_party/blink/renderer/platform/testing/fake_display_item_client.h"
+#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
+
+namespace blink {
+
+class FrameSelectionTest : public EditingTestBase {
+ public:
+ FrameSelectionTest()
+ : root_paint_property_client_("root"),
+ root_paint_chunk_id_(root_paint_property_client_,
+ DisplayItem::kUninitializedType) {}
+ FakeDisplayItemClient root_paint_property_client_;
+ PaintChunk::Id root_paint_chunk_id_;
+
+ protected:
+ VisibleSelection VisibleSelectionInDOMTree() const {
+ return Selection().ComputeVisibleSelectionInDOMTree();
+ }
+ VisibleSelectionInFlatTree GetVisibleSelectionInFlatTree() const {
+ return Selection().GetSelectionInFlatTree();
+ }
+
+ Text* AppendTextNode(const String& data);
+ int LayoutCount() const {
+ return GetDummyPageHolder().GetFrameView().LayoutCount();
+ }
+
+ PositionWithAffinity CaretPosition() const {
+ return Selection().frame_caret_->CaretPosition();
+ }
+
+ Page& GetPage() const { return GetDummyPageHolder().GetPage(); }
+
+ // Returns if a word is is selected.
+ bool SelectWordAroundPosition(const Position&);
+
+ void MoveRangeSelectionInternal(const Position& base,
+ const Position& extent,
+ TextGranularity granularity) {
+ Selection().MoveRangeSelectionInternal(
+ SelectionInDOMTree::Builder().SetBaseAndExtent(base, extent).Build(),
+ granularity);
+ }
+
+ private:
+ Persistent<Text> text_node_;
+};
+
+Text* FrameSelectionTest::AppendTextNode(const String& data) {
+ Text* text = GetDocument().createTextNode(data);
+ GetDocument().body()->AppendChild(text);
+ return text;
+}
+
+bool FrameSelectionTest::SelectWordAroundPosition(const Position& position) {
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder().Collapse(position).Build());
+ return Selection().SelectWordAroundCaret();
+}
+
+TEST_F(FrameSelectionTest, FirstEphemeralRangeOf) {
+ SetBodyContent("<div id=sample>0123456789</div>abc");
+ Element* const sample = GetDocument().getElementById("sample");
+ Node* const text = sample->firstChild();
+ Selection().SetSelection(SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(EphemeralRange(
+ Position(text, 3), Position(text, 6)))
+ .Build(),
+ SetSelectionOptions());
+ sample->setAttribute(HTMLNames::styleAttr, "display:none");
+ // Move |VisibleSelection| before "abc".
+ UpdateAllLifecyclePhases();
+ const EphemeralRange& range =
+ FirstEphemeralRangeOf(Selection().ComputeVisibleSelectionInDOMTree());
+ EXPECT_EQ(Position(sample->nextSibling(), 0), range.StartPosition())
+ << "firstRange() should return current selection value";
+ EXPECT_EQ(Position(sample->nextSibling(), 0), range.EndPosition());
+}
+
+TEST_F(FrameSelectionTest, SetValidSelection) {
+ Text* text = AppendTextNode("Hello, World!");
+ GetDocument().View()->UpdateAllLifecyclePhases();
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 0), Position(text, 5))
+ .Build());
+ EXPECT_FALSE(Selection().ComputeVisibleSelectionInDOMTree().IsNone());
+}
+
+TEST_F(FrameSelectionTest, PaintCaretShouldNotLayout) {
+ Text* text = AppendTextNode("Hello, World!");
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ GetDocument().body()->setContentEditable("true", ASSERT_NO_EXCEPTION);
+ GetDocument().body()->focus();
+ EXPECT_TRUE(GetDocument().body()->IsFocused());
+
+ Selection().SetCaretVisible(true);
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder().Collapse(Position(text, 0)).Build());
+ GetDocument().View()->UpdateAllLifecyclePhases();
+ EXPECT_TRUE(Selection().ComputeVisibleSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(ToLayoutBlock(GetDocument().body()->GetLayoutObject())
+ ->ShouldPaintCursorCaret());
+
+ int start_count = LayoutCount();
+ {
+ // To force layout in next updateLayout calling, widen view.
+ LocalFrameView& frame_view = GetDummyPageHolder().GetFrameView();
+ IntRect frame_rect = frame_view.FrameRect();
+ frame_rect.SetWidth(frame_rect.Width() + 1);
+ frame_rect.SetHeight(frame_rect.Height() + 1);
+ GetDummyPageHolder().GetFrameView().SetFrameRect(frame_rect);
+ }
+ std::unique_ptr<PaintController> paint_controller = PaintController::Create();
+ {
+ GraphicsContext context(*paint_controller);
+
+ if (RuntimeEnabledFeatures::SlimmingPaintV175Enabled()) {
+ paint_controller->UpdateCurrentPaintChunkProperties(
+ root_paint_chunk_id_,
+ PaintChunkProperties(PropertyTreeState(
+ TransformPaintPropertyNode::Root(), ClipPaintPropertyNode::Root(),
+ EffectPaintPropertyNode::Root())));
+ }
+
+ Selection().PaintCaret(context, LayoutPoint());
+ }
+ paint_controller->CommitNewDisplayItems();
+ EXPECT_EQ(start_count, LayoutCount());
+}
+
+#define EXPECT_EQ_SELECTED_TEXT(text) \
+ EXPECT_EQ(text, WebString(Selection().SelectedText()).Utf8())
+
+TEST_F(FrameSelectionTest, SelectWordAroundCaret) {
+ // "Foo Bar Baz,"
+ Text* text = AppendTextNode("Foo Bar&nbsp;&nbsp;Baz,");
+ UpdateAllLifecyclePhases();
+
+ // "Fo|o Bar Baz,"
+ EXPECT_TRUE(SelectWordAroundPosition(Position(text, 2)));
+ EXPECT_EQ_SELECTED_TEXT("Foo");
+ // "Foo| Bar Baz,"
+ EXPECT_TRUE(SelectWordAroundPosition(Position(text, 3)));
+ EXPECT_EQ_SELECTED_TEXT("Foo");
+ // "Foo Bar | Baz,"
+ EXPECT_FALSE(SelectWordAroundPosition(Position(text, 13)));
+ // "Foo Bar Baz|,"
+ EXPECT_TRUE(SelectWordAroundPosition(Position(text, 22)));
+ EXPECT_EQ_SELECTED_TEXT("Baz");
+}
+
+// crbug.com/657996
+TEST_F(FrameSelectionTest, SelectWordAroundCaret2) {
+ SetBodyContent(
+ "<p style='width:70px; font-size:14px'>foo bar<em>+</em> baz</p>");
+ // "foo bar
+ // b|az"
+ Node* const baz = GetDocument().body()->firstChild()->lastChild();
+ EXPECT_TRUE(SelectWordAroundPosition(Position(baz, 2)));
+ EXPECT_EQ_SELECTED_TEXT("baz");
+}
+
+TEST_F(FrameSelectionTest, ModifyExtendWithFlatTree) {
+ SetBodyContent("<span id=host></span>one");
+ SetShadowContent("two<content></content>", "host");
+ Element* host = GetDocument().getElementById("host");
+ Node* const two = FlatTreeTraversal::FirstChild(*host);
+ // Select "two" for selection in DOM tree
+ // Select "twoone" for selection in Flat tree
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(ToPositionInDOMTree(PositionInFlatTree(host, 0)))
+ .Extend(
+ ToPositionInDOMTree(PositionInFlatTree(GetDocument().body(), 2)))
+ .Build());
+ Selection().Modify(SelectionModifyAlteration::kExtend,
+ SelectionModifyDirection::kForward, TextGranularity::kWord,
+ SetSelectionBy::kSystem);
+ EXPECT_EQ(Position(two, 0), VisibleSelectionInDOMTree().Start());
+ EXPECT_EQ(Position(two, 3), VisibleSelectionInDOMTree().End());
+ EXPECT_EQ(PositionInFlatTree(two, 0),
+ GetVisibleSelectionInFlatTree().Start());
+ EXPECT_EQ(PositionInFlatTree(two, 3), GetVisibleSelectionInFlatTree().End());
+}
+
+TEST_F(FrameSelectionTest, ModifyWithUserTriggered) {
+ SetBodyContent("<div id=sample>abc</div>");
+ Element* sample = GetDocument().getElementById("sample");
+ const Position end_of_text(sample->firstChild(), 3);
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder().Collapse(end_of_text).Build());
+
+ EXPECT_FALSE(Selection().Modify(
+ SelectionModifyAlteration::kMove, SelectionModifyDirection::kForward,
+ TextGranularity::kCharacter, SetSelectionBy::kSystem))
+ << "Selection.modify() returns false for non-user-triggered call when "
+ "selection isn't modified.";
+ EXPECT_EQ(end_of_text, Selection().ComputeVisibleSelectionInDOMTree().Start())
+ << "Selection isn't modified";
+
+ EXPECT_TRUE(Selection().Modify(
+ SelectionModifyAlteration::kMove, SelectionModifyDirection::kForward,
+ TextGranularity::kCharacter, SetSelectionBy::kUser))
+ << "Selection.modify() returns true for user-triggered call";
+ EXPECT_EQ(end_of_text, Selection().ComputeVisibleSelectionInDOMTree().Start())
+ << "Selection isn't modified";
+}
+
+TEST_F(FrameSelectionTest, MoveRangeSelectionTest) {
+ // "Foo Bar Baz,"
+ Text* text = AppendTextNode("Foo Bar Baz,");
+ UpdateAllLifecyclePhases();
+
+ // Itinitializes with "Foo B|a>r Baz," (| means start and > means end).
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 5), Position(text, 6))
+ .Build());
+ EXPECT_EQ_SELECTED_TEXT("a");
+
+ // "Foo B|ar B>az," with the Character granularity.
+ MoveRangeSelectionInternal(Position(text, 5), Position(text, 9),
+ TextGranularity::kCharacter);
+ EXPECT_EQ_SELECTED_TEXT("ar B");
+ // "Foo B|ar B>az," with the Word granularity.
+ MoveRangeSelectionInternal(Position(text, 5), Position(text, 9),
+ TextGranularity::kWord);
+ EXPECT_EQ_SELECTED_TEXT("Bar Baz");
+ // "Fo<o B|ar Baz," with the Character granularity.
+ MoveRangeSelectionInternal(Position(text, 5), Position(text, 2),
+ TextGranularity::kCharacter);
+ EXPECT_EQ_SELECTED_TEXT("o B");
+ // "Fo<o B|ar Baz," with the Word granularity.
+ MoveRangeSelectionInternal(Position(text, 5), Position(text, 2),
+ TextGranularity::kWord);
+ EXPECT_EQ_SELECTED_TEXT("Foo Bar");
+}
+
+TEST_F(FrameSelectionTest, MoveRangeSelectionNoLiveness) {
+ SetBodyContent("<span id=sample>xyz</span>");
+ Element* const sample = GetDocument().getElementById("sample");
+ // Select as: <span id=sample>^xyz|</span>
+ MoveRangeSelectionInternal(Position(sample->firstChild(), 1),
+ Position(sample->firstChild(), 1),
+ TextGranularity::kWord);
+ EXPECT_EQ("xyz", Selection().SelectedText());
+ sample->insertBefore(Text::Create(GetDocument(), "abc"),
+ sample->firstChild());
+ GetDocument().UpdateStyleAndLayout();
+ const VisibleSelection& selection =
+ Selection().ComputeVisibleSelectionInDOMTree();
+ // Inserting "abc" before "xyz" should not affect to selection.
+ EXPECT_EQ(Position(sample->lastChild(), 0), selection.Start());
+ EXPECT_EQ(Position(sample->lastChild(), 3), selection.End());
+ EXPECT_EQ("xyz", Selection().SelectedText());
+ EXPECT_EQ("abcxyz", sample->innerText());
+}
+
+// For http://crbug.com/695317
+TEST_F(FrameSelectionTest, SelectAllWithInputElement) {
+ SetBodyContent("<input>123");
+ Element* const input = GetDocument().QuerySelector("input");
+ Node* const last_child = GetDocument().body()->lastChild();
+ Selection().SelectAll();
+ const SelectionInDOMTree& result_in_dom_tree =
+ Selection().ComputeVisibleSelectionInDOMTree().AsSelection();
+ const SelectionInFlatTree& result_in_flat_tree =
+ Selection().ComputeVisibleSelectionInFlatTree().AsSelection();
+ EXPECT_EQ(SelectionInDOMTree::Builder(result_in_dom_tree)
+ .Collapse(Position::BeforeNode(*input))
+ .Extend(Position(last_child, 3))
+ .Build(),
+ result_in_dom_tree);
+ EXPECT_EQ(SelectionInFlatTree::Builder(result_in_flat_tree)
+ .Collapse(PositionInFlatTree::BeforeNode(*input))
+ .Extend(PositionInFlatTree(last_child, 3))
+ .Build(),
+ result_in_flat_tree);
+}
+
+TEST_F(FrameSelectionTest, SelectAllWithUnselectableRoot) {
+ Element* select = GetDocument().CreateRawElement(HTMLNames::selectTag);
+ GetDocument().ReplaceChild(select, GetDocument().documentElement());
+ GetDocument().UpdateStyleAndLayout();
+ Selection().SelectAll();
+ EXPECT_TRUE(Selection().ComputeVisibleSelectionInDOMTree().IsNone())
+ << "Nothing should be selected if the "
+ "content of the documentElement is not "
+ "selctable.";
+}
+
+TEST_F(FrameSelectionTest, SelectAllPreservesHandle) {
+ SetBodyContent("<div id=sample>abc</div>");
+ Element* sample = GetDocument().getElementById("sample");
+ const Position end_of_text(sample->firstChild(), 3);
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder().Collapse(end_of_text).Build());
+ EXPECT_FALSE(Selection().IsHandleVisible());
+ Selection().SelectAll();
+ EXPECT_FALSE(Selection().IsHandleVisible())
+ << "If handles weren't present before "
+ "selectAll. Then they shouldn't be present "
+ "after it.";
+
+ Selection().SetSelection(
+ SelectionInDOMTree::Builder().Collapse(end_of_text).Build(),
+ SetSelectionOptions::Builder()
+ .SetShouldCloseTyping(true)
+ .SetShouldClearTypingStyle(true)
+ .SetShouldShowHandle(true)
+ .Build());
+ EXPECT_TRUE(Selection().IsHandleVisible());
+ Selection().SelectAll();
+ EXPECT_TRUE(Selection().IsHandleVisible())
+ << "If handles were present before "
+ "selectAll. Then they should be present "
+ "after it.";
+}
+
+TEST_F(FrameSelectionTest, BoldCommandPreservesHandle) {
+ SetBodyContent("<div id=sample contenteditable>abc</div>");
+ Element* sample = GetDocument().getElementById("sample");
+ const Position end_of_text(sample->firstChild(), 3);
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder().Collapse(end_of_text).Build());
+ EXPECT_FALSE(Selection().IsHandleVisible());
+ Selection().SelectAll();
+ GetDocument().execCommand("bold", false, "", ASSERT_NO_EXCEPTION);
+ EXPECT_FALSE(Selection().IsHandleVisible())
+ << "If handles weren't present before "
+ "bold command. Then they shouldn't "
+ "be present after it.";
+
+ Selection().SetSelection(
+ SelectionInDOMTree::Builder().Collapse(end_of_text).Build(),
+ SetSelectionOptions::Builder()
+ .SetShouldCloseTyping(true)
+ .SetShouldClearTypingStyle(true)
+ .SetShouldShowHandle(true)
+ .Build());
+ EXPECT_TRUE(Selection().IsHandleVisible());
+ Selection().SelectAll();
+ GetDocument().execCommand("bold", false, "", ASSERT_NO_EXCEPTION);
+ EXPECT_TRUE(Selection().IsHandleVisible())
+ << "If handles were present before "
+ "bold command. Then they should "
+ "be present after it.";
+}
+
+TEST_F(FrameSelectionTest, SelectionOnRangeHidesHandles) {
+ Text* text = AppendTextNode("Hello, World!");
+ GetDocument().View()->UpdateAllLifecyclePhases();
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 0), Position(text, 5))
+ .Build());
+
+ Selection().SetSelection(SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(EphemeralRange(
+ Position(text, 0), Position(text, 12)))
+ .Build(),
+ SetSelectionOptions());
+
+ EXPECT_FALSE(Selection().IsHandleVisible())
+ << "After SetSelection on Range, handles shouldn't be present.";
+
+ Selection().SetSelection(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 0), Position(text, 5))
+ .Build(),
+ SetSelectionOptions::Builder()
+ .SetShouldCloseTyping(true)
+ .SetShouldClearTypingStyle(true)
+ .SetShouldShowHandle(true)
+ .Build());
+
+ Selection().SetSelection(SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(EphemeralRange(
+ Position(text, 0), Position(text, 12)))
+ .Build(),
+ SetSelectionOptions());
+
+ EXPECT_FALSE(Selection().IsHandleVisible())
+ << "After SetSelection on Range, handles shouldn't be present.";
+}
+
+// Regression test for crbug.com/702756
+// Test case excerpted from editing/undo/redo_correct_selection.html
+TEST_F(FrameSelectionTest, SelectInvalidPositionInFlatTreeDoesntCrash) {
+ SetBodyContent("foo<option><select></select></option>");
+ Element* body = GetDocument().body();
+ Element* select = GetDocument().QuerySelector("select");
+ Node* foo = body->firstChild();
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(body, 0))
+ // SELECT@AfterAnchor is invalid in flat tree.
+ .Extend(Position::AfterNode(*select))
+ .Build());
+ // Should not crash inside.
+ const VisibleSelectionInFlatTree& selection =
+ Selection().ComputeVisibleSelectionInFlatTree();
+
+ // This only records the current behavior. It might be changed in the future.
+ EXPECT_EQ(PositionInFlatTree(foo, 0), selection.Base());
+ EXPECT_EQ(PositionInFlatTree(foo, 0), selection.Extent());
+}
+
+TEST_F(FrameSelectionTest, CaretInShadowTree) {
+ SetBodyContent("<p id=host></p>bar");
+ ShadowRoot* shadow_root =
+ SetShadowContent("<div contenteditable id='ce'>foo</div>", "host");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const ce = shadow_root->getElementById("ce");
+ ce->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ ce->blur(); // Move focus to document body.
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden()); // Caret is now hidden.
+}
+
+TEST_F(FrameSelectionTest, CaretInTextControl) {
+ SetBodyContent("<input id='field'>"); // <input> hosts a shadow tree.
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const field = GetDocument().getElementById("field");
+ field->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ field->blur(); // Move focus to document body.
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden()); // Caret is now hidden.
+}
+
+TEST_F(FrameSelectionTest, RangeInShadowTree) {
+ SetBodyContent("<p id='host'></p>");
+ ShadowRoot* shadow_root = SetShadowContent("hey", "host");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Node* text_node = shadow_root->firstChild();
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text_node, 0), Position(text_node, 3))
+ .Build());
+ EXPECT_EQ_SELECTED_TEXT("hey");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ GetDocument().body()->focus(); // Move focus to document body.
+ EXPECT_EQ_SELECTED_TEXT("hey");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+}
+
+TEST_F(FrameSelectionTest, RangeInTextControl) {
+ SetBodyContent("<input id='field' value='hola'>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const field = GetDocument().getElementById("field");
+ field->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ Selection().SelectAll();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ field->blur();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+}
+
+// crbug.com/692898
+TEST_F(FrameSelectionTest, FocusingLinkHidesCaretInTextControl) {
+ SetBodyContent(
+ "<input id='field'>"
+ "<a href='www' id='alink'>link</a>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const field = GetDocument().getElementById("field");
+ field->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ Element* const alink = GetDocument().getElementById("alink");
+ alink->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+}
+
+// crbug.com/692898
+TEST_F(FrameSelectionTest, FocusingLinkHidesRangeInTextControl) {
+ SetBodyContent(
+ "<input id='field' value='hola'>"
+ "<a href='www' id='alink'>link</a>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const field = GetDocument().getElementById("field");
+ field->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ Selection().SelectAll();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ Element* const alink = GetDocument().getElementById("alink");
+ alink->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+}
+
+TEST_F(FrameSelectionTest, FocusingButtonHidesRangeInReadOnlyTextControl) {
+ SetBodyContent(
+ "<textarea readonly>Berlin</textarea>"
+ "<input type='submit' value='Submit'>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const textarea = GetDocument().QuerySelector("textarea");
+ textarea->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+
+ Selection().SelectAll();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ Element* const submit = GetDocument().QuerySelector("input");
+ submit->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+}
+
+TEST_F(FrameSelectionTest, FocusingButtonHidesRangeInDisabledTextControl) {
+ SetBodyContent(
+ "<textarea disabled>Berlin</textarea>"
+ "<input type='submit' value='Submit'>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const textarea = GetDocument().QuerySelector("textarea");
+ textarea->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+
+ // We use a double click to create the selection [Berlin].
+ // FrameSelection::SelectAll (= textarea.select() in JavaScript) would have
+ // been shorter, but currently that doesn't work on a *disabled* text control.
+ const IntRect elem_bounds = textarea->BoundsInViewport();
+ WebMouseEvent double_click(WebMouseEvent::kMouseDown, 0,
+ WebInputEvent::GetStaticTimeStampForTests());
+ double_click.SetPositionInWidget(elem_bounds.X(), elem_bounds.Y());
+ double_click.SetPositionInScreen(elem_bounds.X(), elem_bounds.Y());
+ double_click.button = WebMouseEvent::Button::kLeft;
+ double_click.click_count = 2;
+ double_click.SetFrameScale(1);
+
+ GetFrame().GetEventHandler().HandleMousePressEvent(double_click);
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ Element* const submit = GetDocument().QuerySelector("input");
+ submit->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+}
+
+// crbug.com/713051
+TEST_F(FrameSelectionTest, FocusingNonEditableParentHidesCaretInTextControl) {
+ SetBodyContent(
+ "<div tabindex='-1' id='parent'>"
+ " <input id='field'>"
+ "</div>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const field = GetDocument().getElementById("field");
+ field->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ // Here the selection belongs to <input>'s shadow tree and that tree has a
+ // non-editable parent that is focused.
+ Element* const parent = GetDocument().getElementById("parent");
+ parent->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden()); // Focus is outside <input>
+ // so caret should not be visible.
+
+ parent->blur(); // Move focus to document body.
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden()); // Caret is still hidden.
+}
+
+// crbug.com/713051
+TEST_F(FrameSelectionTest, FocusingNonEditableParentHidesRangeInTextControl) {
+ SetBodyContent(
+ "<div tabindex='-1' id='parent'>"
+ " <input id='field' value='hola'>"
+ "</div>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const field = GetDocument().getElementById("field");
+ field->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ Selection().SelectAll();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ // Here the selection belongs to <input>'s shadow tree and that tree has a
+ // non-editable parent that is focused.
+ Element* const parent = GetDocument().getElementById("parent");
+ parent->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden()); // Focus is outside <input>
+ // so range should not be visible.
+
+ parent->blur(); // Move focus to document body.
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden()); // Range is still hidden.
+}
+
+TEST_F(FrameSelectionTest, CaretInEditableDiv) {
+ SetBodyContent("<div contenteditable id='ce'>blabla</div>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const ce = GetDocument().getElementById("ce");
+ ce->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ ce->blur(); // Move focus to document body.
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden()); // Caret is now hidden.
+}
+
+TEST_F(FrameSelectionTest, RangeInEditableDiv) {
+ SetBodyContent("<div contenteditable id='ce'>blabla</div>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const ce = GetDocument().getElementById("ce");
+ ce->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ Selection().SelectAll();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ ce->blur(); // Move focus to document body.
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden()); // Range is still visible.
+}
+
+TEST_F(FrameSelectionTest, RangeInEditableDivInShadowTree) {
+ SetBodyContent("<p id='host'></p>");
+ ShadowRoot* shadow_root =
+ SetShadowContent("<div id='ce' contenteditable>foo</div>", "host");
+
+ Element* const ce = shadow_root->getElementById("ce");
+ ce->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ Selection().SelectAll();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ ce->blur();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden()); // Range is still visible.
+}
+
+TEST_F(FrameSelectionTest, FocusingLinkHidesCaretInContentEditable) {
+ SetBodyContent(
+ "<div contenteditable id='ce'>blabla</div>"
+ "<a href='www' id='alink'>link</a>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const ce = GetDocument().getElementById("ce");
+ ce->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ Element* const alink = GetDocument().getElementById("alink");
+ alink->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+}
+
+TEST_F(FrameSelectionTest, FocusingLinkKeepsRangeInContentEditable) {
+ SetBodyContent(
+ "<div contenteditable id='ce'>blabla</div>"
+ "<a href='www' id='alink'>link</a>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const ce = GetDocument().getElementById("ce");
+ ce->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ Selection().SelectAll();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ Element* const alink = GetDocument().getElementById("alink");
+ alink->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+}
+
+TEST_F(FrameSelectionTest, FocusingEditableParentKeepsEditableCaret) {
+ SetBodyContent(
+ "<div contenteditable tabindex='-1' id='parent'>"
+ "<div contenteditable id='ce'>blabla</div>"
+ "</div>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ // TODO(editing-dev): Blink should be able to focus the inner <div>.
+ // Element* const ce = GetDocument().getElementById("ce");
+ // ce->focus();
+ // EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ // EXPECT_FALSE(Selection().IsHidden());
+
+ Element* const parent = GetDocument().getElementById("parent");
+ parent->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden()); // Focus is within editing boundary,
+ // caret should be visible.
+
+ parent->blur(); // Move focus to document body.
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden()); // Focus is outside editing boundary
+ // so caret should be hidden.
+}
+
+TEST_F(FrameSelectionTest, FocusingEditableParentKeepsEditableRange) {
+ SetBodyContent(
+ "<div contenteditable tabindex='-1' id='parent'>"
+ "<div contenteditable id='ce'>blabla</div>"
+ "</div>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ // TODO(editing-dev): Blink should be able to focus the inner <div>.
+ // Element* const ce = GetDocument().getElementById("ce");
+ // ce->focus();
+ // EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ // EXPECT_FALSE(Selection().IsHidden());
+
+ // Selection().SelectAll();
+ // EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ // EXPECT_FALSE(Selection().IsHidden());
+
+ Element* const parent = GetDocument().getElementById("parent");
+ parent->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden()); // Focus is within editing boundary,
+ // range should be visible.
+
+ Selection().SelectAll();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ parent->blur(); // Move focus to document body.
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden()); // Focus is outside editing boundary
+ // but range should still be visible.
+}
+
+TEST_F(FrameSelectionTest, FocusingNonEditableParentHidesEditableCaret) {
+ SetBodyContent(
+ "<div tabindex='-1' id='parent'>"
+ "<div contenteditable id='ce'>blabla</div>"
+ "</div>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const ce = GetDocument().getElementById("ce");
+ ce->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ // Here the selection belongs to <div>'s shadow tree and that tree has a
+ // non-editable parent that is focused.
+ Element* const parent = GetDocument().getElementById("parent");
+ parent->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden()); // Focus is outside editing boundary
+ // so caret should be hidden.
+
+ parent->blur(); // Move focus to document body.
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden()); // Caret is still hidden.
+}
+
+TEST_F(FrameSelectionTest, FocusingNonEditableParentKeepsEditableRange) {
+ SetBodyContent(
+ "<div tabindex='-1' id='parent'>"
+ "<div contenteditable id='ce'>blabla</div>"
+ "</div>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const ce = GetDocument().getElementById("ce");
+ ce->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsCaret());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ Selection().SelectAll();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ // Here the selection belongs to <div>'s shadow tree and that tree has a
+ // non-editable parent that is focused.
+ Element* const parent = GetDocument().getElementById("parent");
+ parent->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden()); // Focus is outside editing boundary
+ // but range should still be visible.
+
+ parent->blur(); // Move focus to document body.
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden()); // Range is still visible.
+}
+
+// crbug.com/707143
+TEST_F(FrameSelectionTest, RangeContainsFocus) {
+ SetBodyContent(
+ "<div>"
+ " <div>"
+ " <span id='start'>start</span>"
+ " </div>"
+ " <a href='www' id='alink'>link</a>"
+ " <div>line 1</div>"
+ " <div>line 2</div>"
+ " <div>line 3</div>"
+ " <div>line 4</div>"
+ " <span id='end'>end</span>"
+ " <div></div>"
+ "</div>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const start = GetDocument().getElementById("start");
+ Element* const end = GetDocument().getElementById("end");
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(start, 0), Position(end, 1))
+ .Build());
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ Element* const alink = GetDocument().getElementById("alink");
+ alink->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden()); // Range still visible.
+}
+
+// crbug.com/707143
+TEST_F(FrameSelectionTest, RangeOutsideFocus) {
+ // Here the selection sits on a sub tree that hasn't the focused element.
+ // This test case is the reason why we separate FrameSelection::HasFocus() and
+ // FrameSelection::IsHidden(). Even when the selection's DOM nodes are
+ // completely disconnected from the focused node, we still want the selection
+ // to be visible (not hidden).
+ SetBodyContent(
+ "<a href='www' id='alink'>link</a>"
+ "<div>"
+ " <div>"
+ " <span id='start'>start</span>"
+ " </div>"
+ " <div>line 1</div>"
+ " <div>line 2</div>"
+ " <div>line 3</div>"
+ " <div>line 4</div>"
+ " <span id='end'>end</span>"
+ " <div></div>"
+ "</div>");
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_TRUE(Selection().IsHidden());
+
+ Element* const start = GetDocument().getElementById("start");
+ Element* const end = GetDocument().getElementById("end");
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(start, 0), Position(end, 1))
+ .Build());
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_TRUE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden());
+
+ Element* const alink = GetDocument().getElementById("alink");
+ alink->focus();
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsRange());
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+ EXPECT_FALSE(Selection().IsHidden()); // Range still visible.
+}
+
+// crbug.com/725457
+TEST_F(FrameSelectionTest, InconsistentVisibleSelectionNoCrash) {
+ SetBodyContent("foo<div id=host><span id=anchor>bar</span></div>baz");
+ SetShadowContent("shadow", "host");
+
+ Element* anchor = GetDocument().getElementById("anchor");
+
+ // |start| and |end| are valid Positions in DOM tree, but do not participate
+ // in flat tree. They should be canonicalized to null VisiblePositions, but
+ // are currently not. See crbug.com/729636 for details.
+ const Position& start = Position::BeforeNode(*anchor);
+ const Position& end = Position::AfterNode(*anchor);
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder().Collapse(start).Extend(end).Build());
+
+ // Shouldn't crash inside.
+ EXPECT_FALSE(Selection().SelectionHasFocus());
+}
+
+TEST_F(FrameSelectionTest, SelectionBounds) {
+ SetBodyContent(
+ "<style>"
+ " * { margin: 0; } "
+ " html, body { height: 2000px; }"
+ " div {"
+ " width: 20px;"
+ " height: 1000px;"
+ " font-size: 30px;"
+ " overflow: hidden;"
+ " margin-top: 2px;"
+ " }"
+ "</style>"
+ "<div>"
+ " a<br>b<br>c<br>d<br>e<br>f<br>g<br>h<br>i<br>j<br>k<br>l<br>m<br>n<br>"
+ " a<br>b<br>c<br>d<br>e<br>f<br>g<br>h<br>i<br>j<br>k<br>l<br>m<br>n<br>"
+ " a<br>b<br>c<br>d<br>e<br>f<br>g<br>h<br>i<br>j<br>k<br>l<br>m<br>n<br>"
+ "</div>");
+ Selection().SelectAll();
+
+ const int node_width = 20;
+ const int node_height = 1000;
+ const int node_margin_top = 2;
+ // The top of the node should be visible but the bottom should be outside
+ // by the viewport. The unclipped selection bounds should not be clipped.
+ EXPECT_EQ(LayoutRect(0, node_margin_top, node_width, node_height),
+ Selection().AbsoluteUnclippedBounds());
+
+ // Scroll 500px down so the top of the node is outside the viewport and the
+ // bottom is visible. The unclipped selection bounds should not be clipped.
+ const int scroll_offset = 500;
+ LocalFrameView* frame_view = GetDocument().View();
+ frame_view->LayoutViewportScrollableArea()->SetScrollOffset(
+ ScrollOffset(0, scroll_offset), kProgrammaticScroll);
+ EXPECT_EQ(
+ LayoutRect(0, node_margin_top, node_width, node_height),
+ frame_view->AbsoluteToDocument(Selection().AbsoluteUnclippedBounds()));
+
+ // Adjust the page scale factor which changes the selection bounds as seen
+ // through the viewport. The unclipped selection bounds should not be clipped.
+ const int page_scale_factor = 2;
+ GetPage().SetPageScaleFactor(page_scale_factor);
+ EXPECT_EQ(
+ LayoutRect(0, node_margin_top, node_width, node_height),
+ frame_view->AbsoluteToDocument(Selection().AbsoluteUnclippedBounds()));
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/granularity_strategy.cc b/chromium/third_party/blink/renderer/core/editing/granularity_strategy.cc
new file mode 100644
index 00000000000..c6e06db17c5
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/granularity_strategy.cc
@@ -0,0 +1,290 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/granularity_strategy.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/local_caret_rect.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+
+namespace blink {
+
+enum class BoundAdjust { kCurrentPosIfOnBound, kNextBoundIfOnBound };
+enum class SearchDirection { kSearchBackwards, kSearchForward };
+
+// We use the bottom-left corner of the selection rect to represent the
+// location of a VisiblePosition. This way locations corresponding to
+// VisiblePositions on the same line will all have the same y coordinate
+// unless the text is transformed.
+static IntPoint PositionLocation(const VisiblePosition& vp) {
+ return AbsoluteSelectionBoundsOf(vp).MinXMaxYCorner();
+}
+
+// Order is specified using the same contract as comparePositions.
+static bool ArePositionsInSpecifiedOrder(const VisiblePosition& vp1,
+ const VisiblePosition& vp2,
+ int specified_order) {
+ int position_order = ComparePositions(vp1, vp2);
+ if (specified_order == 0)
+ return position_order == 0;
+ return specified_order > 0 ? position_order > 0 : position_order < 0;
+}
+
+// Returns the next word boundary starting from |pos|. |direction| specifies
+// the direction in which to search for the next bound. nextIfOnBound
+// controls whether |pos| or the next boundary is returned when |pos| is
+// located exactly on word boundary.
+static VisiblePosition NextWordBound(const VisiblePosition& pos,
+ SearchDirection direction,
+ BoundAdjust word_bound_adjust) {
+ bool next_bound_if_on_bound =
+ word_bound_adjust == BoundAdjust::kNextBoundIfOnBound;
+ if (direction == SearchDirection::kSearchForward) {
+ EWordSide word_side = next_bound_if_on_bound ? kNextWordIfOnBoundary
+ : kPreviousWordIfOnBoundary;
+ return EndOfWord(pos, word_side);
+ }
+ EWordSide word_side = next_bound_if_on_bound ? kPreviousWordIfOnBoundary
+ : kNextWordIfOnBoundary;
+ return StartOfWord(pos, word_side);
+}
+
+GranularityStrategy::GranularityStrategy() = default;
+
+GranularityStrategy::~GranularityStrategy() = default;
+
+CharacterGranularityStrategy::CharacterGranularityStrategy() = default;
+
+CharacterGranularityStrategy::~CharacterGranularityStrategy() = default;
+
+SelectionStrategy CharacterGranularityStrategy::GetType() const {
+ return SelectionStrategy::kCharacter;
+}
+
+void CharacterGranularityStrategy::Clear() {}
+
+SelectionInDOMTree CharacterGranularityStrategy::UpdateExtent(
+ const IntPoint& extent_point,
+ LocalFrame* frame) {
+ const VisiblePosition& extent_position =
+ VisiblePositionForContentsPoint(extent_point, frame);
+ const VisibleSelection& selection =
+ frame->Selection().ComputeVisibleSelectionInDOMTree();
+ if (extent_position.IsNull() || selection.VisibleBase().DeepEquivalent() ==
+ extent_position.DeepEquivalent())
+ return selection.AsSelection();
+ return SelectionInDOMTree::Builder()
+ .Collapse(selection.Base())
+ .Extend(extent_position.DeepEquivalent())
+ .SetAffinity(selection.Affinity())
+ .Build();
+}
+
+DirectionGranularityStrategy::DirectionGranularityStrategy()
+ : state_(StrategyState::kCleared),
+ granularity_(TextGranularity::kCharacter),
+ offset_(0) {}
+
+DirectionGranularityStrategy::~DirectionGranularityStrategy() = default;
+
+SelectionStrategy DirectionGranularityStrategy::GetType() const {
+ return SelectionStrategy::kDirection;
+}
+
+void DirectionGranularityStrategy::Clear() {
+ state_ = StrategyState::kCleared;
+ granularity_ = TextGranularity::kCharacter;
+ offset_ = 0;
+ diff_extent_point_from_extent_position_ = IntSize();
+}
+
+SelectionInDOMTree DirectionGranularityStrategy::UpdateExtent(
+ const IntPoint& extent_point,
+ LocalFrame* frame) {
+ const VisibleSelection& selection =
+ frame->Selection().ComputeVisibleSelectionInDOMTree();
+
+ if (state_ == StrategyState::kCleared)
+ state_ = StrategyState::kExpanding;
+
+ const VisiblePosition& old_offset_extent_position = selection.VisibleExtent();
+ IntPoint old_extent_location = PositionLocation(old_offset_extent_position);
+
+ IntPoint old_offset_extent_point =
+ old_extent_location + diff_extent_point_from_extent_position_;
+ IntPoint old_extent_point = IntPoint(old_offset_extent_point.X() - offset_,
+ old_offset_extent_point.Y());
+
+ // Apply the offset.
+ IntPoint new_offset_extent_point = extent_point;
+ int dx = extent_point.X() - old_extent_point.X();
+ if (offset_ != 0) {
+ if (offset_ > 0 && dx > 0)
+ offset_ = std::max(0, offset_ - dx);
+ else if (offset_ < 0 && dx < 0)
+ offset_ = std::min(0, offset_ - dx);
+ new_offset_extent_point.Move(offset_, 0);
+ }
+
+ VisiblePosition new_offset_extent_position =
+ VisiblePositionForContentsPoint(new_offset_extent_point, frame);
+ if (new_offset_extent_position.IsNull())
+ return selection.AsSelection();
+ IntPoint new_offset_location = PositionLocation(new_offset_extent_position);
+
+ // Reset the offset in case of a vertical change in the location (could be
+ // due to a line change or due to an unusual layout, e.g. rotated text).
+ bool vertical_change = new_offset_location.Y() != old_extent_location.Y();
+ if (vertical_change) {
+ offset_ = 0;
+ granularity_ = TextGranularity::kCharacter;
+ new_offset_extent_point = extent_point;
+ new_offset_extent_position =
+ VisiblePositionForContentsPoint(extent_point, frame);
+ }
+
+ const VisiblePosition base = selection.VisibleBase();
+
+ // Do not allow empty selection.
+ if (new_offset_extent_position.DeepEquivalent() == base.DeepEquivalent())
+ return selection.AsSelection();
+
+ // The direction granularity strategy, particularly the "offset" feature
+ // doesn't work with non-horizontal text (e.g. when the text is rotated).
+ // So revert to the behavior equivalent to the character granularity
+ // strategy if we detect that the text's baseline coordinate changed
+ // without a line change.
+ if (vertical_change &&
+ InSameLine(new_offset_extent_position, old_offset_extent_position)) {
+ return SelectionInDOMTree::Builder()
+ .Collapse(selection.Base())
+ .Extend(new_offset_extent_position.DeepEquivalent())
+ .SetAffinity(selection.Affinity())
+ .Build();
+ }
+
+ int old_extent_base_order = selection.IsBaseFirst() ? 1 : -1;
+
+ int new_extent_base_order;
+ bool this_move_shrunk_selection;
+ if (new_offset_extent_position.DeepEquivalent() ==
+ old_offset_extent_position.DeepEquivalent()) {
+ if (granularity_ == TextGranularity::kCharacter)
+ return selection.AsSelection();
+
+ // If we are in Word granularity, we cannot exit here, since we may pass
+ // the middle of the word without changing the position (in which case
+ // the selection needs to expand).
+ this_move_shrunk_selection = false;
+ new_extent_base_order = old_extent_base_order;
+ } else {
+ bool selection_expanded = ArePositionsInSpecifiedOrder(
+ new_offset_extent_position, old_offset_extent_position,
+ old_extent_base_order);
+ bool extent_base_order_switched =
+ selection_expanded
+ ? false
+ : !ArePositionsInSpecifiedOrder(new_offset_extent_position, base,
+ old_extent_base_order);
+ new_extent_base_order = extent_base_order_switched ? -old_extent_base_order
+ : old_extent_base_order;
+
+ // Determine the word boundary, i.e. the boundary extending beyond which
+ // should change the granularity to WordGranularity.
+ VisiblePosition word_boundary;
+ if (extent_base_order_switched) {
+ // Special case.
+ // If the extent-base order was switched, then the selection is now
+ // expanding in a different direction than before. Therefore we
+ // calculate the word boundary in this new direction and based on
+ // the |base| position.
+ word_boundary = NextWordBound(base,
+ new_extent_base_order > 0
+ ? SearchDirection::kSearchForward
+ : SearchDirection::kSearchBackwards,
+ BoundAdjust::kNextBoundIfOnBound);
+ granularity_ = TextGranularity::kCharacter;
+ } else {
+ // Calculate the word boundary based on |oldExtentWithGranularity|.
+ // If selection was shrunk in the last update and the extent is now
+ // exactly on the word boundary - we need to take the next bound as
+ // the bound of the current word.
+ word_boundary = NextWordBound(old_offset_extent_position,
+ old_extent_base_order > 0
+ ? SearchDirection::kSearchForward
+ : SearchDirection::kSearchBackwards,
+ state_ == StrategyState::kShrinking
+ ? BoundAdjust::kNextBoundIfOnBound
+ : BoundAdjust::kCurrentPosIfOnBound);
+ }
+
+ bool expanded_beyond_word_boundary;
+ if (selection_expanded)
+ expanded_beyond_word_boundary = ArePositionsInSpecifiedOrder(
+ new_offset_extent_position, word_boundary, new_extent_base_order);
+ else if (extent_base_order_switched)
+ expanded_beyond_word_boundary = ArePositionsInSpecifiedOrder(
+ new_offset_extent_position, word_boundary, new_extent_base_order);
+ else
+ expanded_beyond_word_boundary = false;
+
+ // The selection is shrunk if the extent changes position to be closer to
+ // the base, and the extent/base order wasn't switched.
+ this_move_shrunk_selection =
+ !extent_base_order_switched && !selection_expanded;
+
+ if (expanded_beyond_word_boundary)
+ granularity_ = TextGranularity::kWord;
+ else if (this_move_shrunk_selection)
+ granularity_ = TextGranularity::kCharacter;
+ }
+
+ VisiblePosition new_selection_extent = new_offset_extent_position;
+ if (granularity_ == TextGranularity::kWord) {
+ // Determine the bounds of the word where the extent is located.
+ // Set the selection extent to one of the two bounds depending on
+ // whether the extent is passed the middle of the word.
+ VisiblePosition bound_before_extent = NextWordBound(
+ new_offset_extent_position, SearchDirection::kSearchBackwards,
+ BoundAdjust::kCurrentPosIfOnBound);
+ VisiblePosition bound_after_extent = NextWordBound(
+ new_offset_extent_position, SearchDirection::kSearchForward,
+ BoundAdjust::kCurrentPosIfOnBound);
+ int x_middle_between_bounds = (PositionLocation(bound_after_extent).X() +
+ PositionLocation(bound_before_extent).X()) /
+ 2;
+ bool offset_extent_before_middle =
+ new_offset_extent_point.X() < x_middle_between_bounds;
+ new_selection_extent =
+ offset_extent_before_middle ? bound_before_extent : bound_after_extent;
+ // Update the offset if selection expanded in word granularity.
+ if (new_selection_extent.DeepEquivalent() !=
+ old_offset_extent_position.DeepEquivalent() &&
+ ((new_extent_base_order > 0 && !offset_extent_before_middle) ||
+ (new_extent_base_order < 0 && offset_extent_before_middle))) {
+ offset_ = PositionLocation(new_selection_extent).X() - extent_point.X();
+ }
+ }
+
+ // Only update the state if the selection actually changed as a result of
+ // this move.
+ if (new_selection_extent.DeepEquivalent() !=
+ old_offset_extent_position.DeepEquivalent())
+ state_ = this_move_shrunk_selection ? StrategyState::kShrinking
+ : StrategyState::kExpanding;
+
+ diff_extent_point_from_extent_position_ =
+ extent_point + IntSize(offset_, 0) -
+ PositionLocation(new_selection_extent);
+ return SelectionInDOMTree::Builder(selection.AsSelection())
+ .Collapse(selection.Base())
+ .Extend(new_selection_extent.DeepEquivalent())
+ .Build();
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/granularity_strategy.h b/chromium/third_party/blink/renderer/core/editing/granularity_strategy.h
new file mode 100644
index 00000000000..58377bf5936
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/granularity_strategy.h
@@ -0,0 +1,129 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_GRANULARITY_STRATEGY_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_GRANULARITY_STRATEGY_H_
+
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/selection_strategy.h"
+#include "third_party/blink/renderer/platform/geometry/int_size.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+
+namespace blink {
+
+class IntPoint;
+class LocalFrame;
+
+enum class TextGranularity;
+
+class GranularityStrategy {
+ USING_FAST_MALLOC(GranularityStrategy);
+
+ public:
+ virtual ~GranularityStrategy();
+ virtual SelectionStrategy GetType() const = 0;
+ virtual void Clear() = 0;
+
+ // Calculates and returns the new selection based on the updated extent
+ // location in absolute coordinates.
+ virtual SelectionInDOMTree UpdateExtent(const IntPoint&, LocalFrame*) = 0;
+
+ protected:
+ GranularityStrategy();
+};
+
+// Always uses character granularity.
+class CharacterGranularityStrategy final : public GranularityStrategy {
+ public:
+ CharacterGranularityStrategy();
+ ~CharacterGranularityStrategy() final;
+
+ // GranularityStrategy:
+ SelectionStrategy GetType() const final;
+ void Clear() final;
+ SelectionInDOMTree UpdateExtent(const IntPoint&, LocalFrame*) final;
+};
+
+// "Expand by word, shrink by character" selection strategy.
+// Uses character granularity when selection is shrinking. If the selection is
+// expanding, granularity doesn't change until a word boundary is passed, after
+// which the granularity switches to "word".
+// In word granularity, the word is not selected until the extent point passes
+// the middle of the word.
+//
+// The "offset" feature:
+// The offset is the horizontal distance between the extent point (passed in
+// updateExtent) and the end of the selection. In character granularity the
+// offset is typically zero or near zero, however in word granularity it can be
+// significant. When the offset is positive and the extent point moved to the
+// left, the offset is preserved, i.e. the selection tracks the extent point
+// with the constant offset. When the offset is positive and the extent point
+// is moved to the right, the offset gets reduced. Selection will not grow
+// until the offset is reduced all the way to zero.
+//
+// This behavior is best illustrated by an example:
+//
+// ^ marks base, | marks extent point, > marks selection end:
+// Lorem ip^sum|> dolor sit amet, consectetur
+//
+// Move extent over the middle of "dolor". Granularity should change to word
+// granularity and the selection end should jump to the end of the word.
+// Lorem ip^sum dolo|r> sit amet, consectetur
+//
+// Move extent back one character. Granularity changes to "character". The
+// selection end should move back one character as well. Note an offset between
+// the extent and the selection end.
+// Lorem ip^sum dol|o>r sit amet, consectetur
+//
+// Move extent forward one character. The offset is reduced to 0. Selection end
+// doesn't change.
+// Lorem ip^sum dolo|>r sit amet, consectetur
+//
+// Move forward one character. End moves with extent in character granularity.
+// Lorem ip^sum dolor|> sit amet, consectetur
+class DirectionGranularityStrategy final : public GranularityStrategy {
+ public:
+ DirectionGranularityStrategy();
+ ~DirectionGranularityStrategy() final;
+
+ // GranularityStrategy:
+ SelectionStrategy GetType() const final;
+ void Clear() final;
+ SelectionInDOMTree UpdateExtent(const IntPoint&, LocalFrame*) final;
+
+ private:
+ enum class StrategyState {
+ // Starting state.
+ // Selection was cleared and there were no extent updates since then.
+ // One an update is performed, the strategy goes into the Expanding
+ // state unless the update shrinks the selection without changing
+ // relative base/extent order, in which case the strategy goes into the
+ // Shrinking state.
+ kCleared,
+ // Last time the selection was changed by updateExtent - it was expanded
+ // or the relative base/extent order was changed.
+ kExpanding,
+ // Last time the selection was changed by updateExtent - it was shrunk
+ // (without changing relative base/extent order).
+ kShrinking
+ };
+
+ StrategyState state_;
+
+ // Current selection granularity being used.
+ TextGranularity granularity_;
+
+ // Horizontal offset in pixels in absolute coordinates applied to the extent
+ // point.
+ int offset_;
+
+ // This defines location of the offset-adjusted extent point (from the
+ // latest updateExtent call) relative to the location of extent's
+ // VisiblePosition. It is used to detect sub-position extent movement.
+ IntSize diff_extent_point_from_extent_position_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_GRANULARITY_STRATEGY_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/granularity_strategy_test.cc b/chromium/third_party/blink/renderer/core/editing/granularity_strategy_test.cc
new file mode 100644
index 00000000000..d895767e2c3
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/granularity_strategy_test.cc
@@ -0,0 +1,771 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <memory>
+#include "base/memory/scoped_refptr.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/local_caret_rect.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/html/html_body_element.h"
+#include "third_party/blink/renderer/core/html/html_span_element.h"
+#include "third_party/blink/renderer/core/testing/page_test_base.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
+
+namespace blink {
+
+#define EXPECT_EQ_SELECTED_TEXT(text) \
+ EXPECT_EQ(text, WebString(Selection().SelectedText()).Utf8())
+
+IntPoint VisiblePositionToContentsPoint(const VisiblePosition& pos) {
+ IntPoint result = AbsoluteSelectionBoundsOf(pos).MinXMaxYCorner();
+ // Need to move the point at least by 1 - caret's minXMaxYCorner is not
+ // evaluated to the same line as the text by hit testing.
+ result.Move(0, -1);
+ return result;
+}
+
+using TextNodeVector = HeapVector<Member<Text>>;
+
+class GranularityStrategyTest : public PageTestBase {
+ protected:
+ void SetUp() override;
+
+ void SetSelectionAndEndTyping(const VisibleSelection&);
+ Text* AppendTextNode(const String& data);
+ int LayoutCount() const {
+ return GetDummyPageHolder().GetFrameView().LayoutCount();
+ }
+ void SetInnerHTML(const char*);
+ // Parses the text node, appending the info to m_letterPos and m_wordMiddles.
+ void ParseText(Text*);
+ void ParseText(const TextNodeVector&);
+
+ Text* SetupTranslateZ(String);
+ Text* SetupTransform(String);
+ Text* SetupRotate(String);
+ void SetupTextSpan(String str1,
+ String str2,
+ String str3,
+ size_t sel_begin,
+ size_t sel_end);
+ void SetupVerticalAlign(String str1,
+ String str2,
+ String str3,
+ size_t sel_begin,
+ size_t sel_end);
+ void SetupFontSize(String str1,
+ String str2,
+ String str3,
+ size_t sel_begin,
+ size_t sel_end);
+
+ void TestDirectionExpand();
+ void TestDirectionShrink();
+ void TestDirectionSwitchSide();
+
+ // Pixel coordinates of the positions for each letter within the text being
+ // tested.
+ Vector<IntPoint> letter_pos_;
+ // Pixel coordinates of the middles of the words in the text being tested.
+ // (y coordinate is based on y coordinates of m_letterPos)
+ Vector<IntPoint> word_middles_;
+};
+
+void GranularityStrategyTest::SetUp() {
+ PageTestBase::SetUp();
+ GetFrame().GetSettings()->SetDefaultFontSize(12);
+ GetFrame().GetSettings()->SetSelectionStrategy(SelectionStrategy::kDirection);
+}
+
+void GranularityStrategyTest::SetSelectionAndEndTyping(
+ const VisibleSelection& new_selection) {
+ Selection().SetSelectionAndEndTyping(new_selection.AsSelection());
+}
+
+Text* GranularityStrategyTest::AppendTextNode(const String& data) {
+ Text* text = GetDocument().createTextNode(data);
+ GetDocument().body()->AppendChild(text);
+ return text;
+}
+
+void GranularityStrategyTest::SetInnerHTML(const char* html_content) {
+ GetDocument().documentElement()->SetInnerHTMLFromString(
+ String::FromUTF8(html_content));
+ GetDocument().View()->UpdateAllLifecyclePhases();
+}
+
+void GranularityStrategyTest::ParseText(Text* text) {
+ TextNodeVector text_nodes;
+ text_nodes.push_back(text);
+ ParseText(text_nodes);
+}
+
+void GranularityStrategyTest::ParseText(const TextNodeVector& text_nodes) {
+ bool word_started = false;
+ int word_start_index = 0;
+ for (auto& text : text_nodes) {
+ int word_start_index_offset = letter_pos_.size();
+ String str = text->wholeText();
+ for (size_t i = 0; i < str.length(); i++) {
+ letter_pos_.push_back(VisiblePositionToContentsPoint(
+ CreateVisiblePosition(Position(text, i))));
+ char c = str[i];
+ if (IsASCIIAlphanumeric(c) && !word_started) {
+ word_start_index = i + word_start_index_offset;
+ word_started = true;
+ } else if (!IsASCIIAlphanumeric(c) && word_started) {
+ IntPoint word_middle((letter_pos_[word_start_index].X() +
+ letter_pos_[i + word_start_index_offset].X()) /
+ 2,
+ letter_pos_[word_start_index].Y());
+ word_middles_.push_back(word_middle);
+ word_started = false;
+ }
+ }
+ }
+ if (word_started) {
+ const auto& last_node = text_nodes.back();
+ int x_end = VisiblePositionToContentsPoint(
+ CreateVisiblePosition(
+ Position(last_node, last_node->wholeText().length())))
+ .X();
+ IntPoint word_middle((letter_pos_[word_start_index].X() + x_end) / 2,
+ letter_pos_[word_start_index].Y());
+ word_middles_.push_back(word_middle);
+ }
+}
+
+Text* GranularityStrategyTest::SetupTranslateZ(String str) {
+ SetInnerHTML(
+ "<html>"
+ "<head>"
+ "<style>"
+ "div {"
+ "transform: translateZ(0);"
+ "}"
+ "</style>"
+ "</head>"
+ "<body>"
+ "<div id='mytext'></div>"
+ "</body>"
+ "</html>");
+
+ Text* text = GetDocument().createTextNode(str);
+ Element* div = GetDocument().getElementById("mytext");
+ div->AppendChild(text);
+
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ ParseText(text);
+ return text;
+}
+
+Text* GranularityStrategyTest::SetupTransform(String str) {
+ SetInnerHTML(
+ "<html>"
+ "<head>"
+ "<style>"
+ "div {"
+ "transform: scale(1,-1) translate(0,-100px);"
+ "}"
+ "</style>"
+ "</head>"
+ "<body>"
+ "<div id='mytext'></div>"
+ "</body>"
+ "</html>");
+
+ Text* text = GetDocument().createTextNode(str);
+ Element* div = GetDocument().getElementById("mytext");
+ div->AppendChild(text);
+
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ ParseText(text);
+ return text;
+}
+
+Text* GranularityStrategyTest::SetupRotate(String str) {
+ SetInnerHTML(
+ "<html>"
+ "<head>"
+ "<style>"
+ "div {"
+ "transform: translate(0px,600px) rotate(90deg);"
+ "}"
+ "</style>"
+ "</head>"
+ "<body>"
+ "<div id='mytext'></div>"
+ "</body>"
+ "</html>");
+
+ Text* text = GetDocument().createTextNode(str);
+ Element* div = GetDocument().getElementById("mytext");
+ div->AppendChild(text);
+
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ ParseText(text);
+ return text;
+}
+
+void GranularityStrategyTest::SetupTextSpan(String str1,
+ String str2,
+ String str3,
+ size_t sel_begin,
+ size_t sel_end) {
+ Text* text1 = GetDocument().createTextNode(str1);
+ Text* text2 = GetDocument().createTextNode(str2);
+ Text* text3 = GetDocument().createTextNode(str3);
+ Element* span = HTMLSpanElement::Create(GetDocument());
+ Element* div = GetDocument().getElementById("mytext");
+ div->AppendChild(text1);
+ div->AppendChild(span);
+ span->AppendChild(text2);
+ div->AppendChild(text3);
+
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ Vector<IntPoint> letter_pos;
+ Vector<IntPoint> word_middle_pos;
+
+ TextNodeVector text_nodes;
+ text_nodes.push_back(text1);
+ text_nodes.push_back(text2);
+ text_nodes.push_back(text3);
+ ParseText(text_nodes);
+
+ Position p1;
+ Position p2;
+ if (sel_begin < str1.length())
+ p1 = Position(text1, sel_begin);
+ else if (sel_begin < str1.length() + str2.length())
+ p1 = Position(text2, sel_begin - str1.length());
+ else
+ p1 = Position(text3, sel_begin - str1.length() - str2.length());
+ if (sel_end < str1.length())
+ p2 = Position(text1, sel_end);
+ else if (sel_end < str1.length() + str2.length())
+ p2 = Position(text2, sel_end - str1.length());
+ else
+ p2 = Position(text3, sel_end - str1.length() - str2.length());
+
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder().SetBaseAndExtent(p1, p2).Build());
+}
+
+void GranularityStrategyTest::SetupVerticalAlign(String str1,
+ String str2,
+ String str3,
+ size_t sel_begin,
+ size_t sel_end) {
+ SetInnerHTML(
+ "<html>"
+ "<head>"
+ "<style>"
+ "span {"
+ "vertical-align:20px;"
+ "}"
+ "</style>"
+ "</head>"
+ "<body>"
+ "<div id='mytext'></div>"
+ "</body>"
+ "</html>");
+
+ SetupTextSpan(str1, str2, str3, sel_begin, sel_end);
+}
+
+void GranularityStrategyTest::SetupFontSize(String str1,
+ String str2,
+ String str3,
+ size_t sel_begin,
+ size_t sel_end) {
+ SetInnerHTML(
+ "<html>"
+ "<head>"
+ "<style>"
+ "span {"
+ "font-size: 200%;"
+ "}"
+ "</style>"
+ "</head>"
+ "<body>"
+ "<div id='mytext'></div>"
+ "</body>"
+ "</html>");
+
+ SetupTextSpan(str1, str2, str3, sel_begin, sel_end);
+}
+
+// Tests expanding selection on text "abcdef ghij kl mno^p|>qr stuvwi inm mnii,"
+// (^ means base, | means extent, < means start, and > means end). Text needs to
+// be laid out on a single line with no rotation.
+void GranularityStrategyTest::TestDirectionExpand() {
+ // Expand selection using character granularity until the end of the word
+ // is reached.
+ // "abcdef ghij kl mno^pq|>r stuvwi inm mnii,"
+ Selection().MoveRangeSelectionExtent(letter_pos_[20]);
+ EXPECT_EQ_SELECTED_TEXT("pq");
+ // Move to the same postion shouldn't change anything.
+ Selection().MoveRangeSelectionExtent(letter_pos_[20]);
+ EXPECT_EQ_SELECTED_TEXT("pq");
+ // "abcdef ghij kl mno^pqr|> stuvwi inm mnii,"
+ Selection().MoveRangeSelectionExtent(letter_pos_[21]);
+ EXPECT_EQ_SELECTED_TEXT("pqr");
+ // Selection should stay the same until the middle of the word is passed.
+ // "abcdef ghij kl mno^pqr |>stuvwi inm mnii," -
+ Selection().MoveRangeSelectionExtent(letter_pos_[22]);
+ EXPECT_EQ_SELECTED_TEXT("pqr ");
+ // "abcdef ghij kl mno^pqr >st|uvwi inm mnii,"
+ Selection().MoveRangeSelectionExtent(letter_pos_[24]);
+ EXPECT_EQ_SELECTED_TEXT("pqr ");
+ IntPoint p = word_middles_[4];
+ p.Move(-1, 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("pqr ");
+ p.Move(1, 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("pqr stuvwi");
+ // Selection should stay the same until the end of the word is reached.
+ // "abcdef ghij kl mno^pqr stuvw|i> inm mnii,"
+ Selection().MoveRangeSelectionExtent(letter_pos_[27]);
+ EXPECT_EQ_SELECTED_TEXT("pqr stuvwi");
+ // "abcdef ghij kl mno^pqr stuvwi|> inm mnii,"
+ Selection().MoveRangeSelectionExtent(letter_pos_[28]);
+ EXPECT_EQ_SELECTED_TEXT("pqr stuvwi");
+ // "abcdef ghij kl mno^pqr stuvwi |>inm mnii,"
+ Selection().MoveRangeSelectionExtent(letter_pos_[29]);
+ EXPECT_EQ_SELECTED_TEXT("pqr stuvwi ");
+ // Now expand slowly to the middle of word #5.
+ int y = letter_pos_[29].Y();
+ for (int x = letter_pos_[29].X() + 1; x < word_middles_[5].X(); x++) {
+ Selection().MoveRangeSelectionExtent(IntPoint(x, y));
+ Selection().MoveRangeSelectionExtent(IntPoint(x, y));
+ EXPECT_EQ_SELECTED_TEXT("pqr stuvwi ");
+ }
+ Selection().MoveRangeSelectionExtent(word_middles_[5]);
+ EXPECT_EQ_SELECTED_TEXT("pqr stuvwi inm");
+ // Jump over quickly to just before the middle of the word #6 and then
+ // move over it.
+ p = word_middles_[6];
+ p.Move(-1, 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("pqr stuvwi inm ");
+ p.Move(1, 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("pqr stuvwi inm mnii");
+}
+
+// Tests shrinking selection on text "abcdef ghij kl mno^pqr|> iiinmni, abc"
+// (^ means base, | means extent, < means start, and > means end).
+// Text needs to be laid out on a single line with no rotation.
+void GranularityStrategyTest::TestDirectionShrink() {
+ // Move to the middle of word #4 to it and then move back, confirming
+ // that the selection end is moving with the extent. The offset between the
+ // extent and the selection end will be equal to half the width of "iiinmni".
+ Selection().MoveRangeSelectionExtent(word_middles_[4]);
+ EXPECT_EQ_SELECTED_TEXT("pqr iiinmni");
+ IntPoint p = word_middles_[4];
+ p.Move(letter_pos_[28].X() - letter_pos_[29].X(), 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("pqr iiinmn");
+ p.Move(letter_pos_[27].X() - letter_pos_[28].X(), 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("pqr iiinm");
+ p.Move(letter_pos_[26].X() - letter_pos_[27].X(), 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("pqr iiin");
+ // Move right by the width of char 30 ('m'). Selection shouldn't change,
+ // but offset should be reduced.
+ p.Move(letter_pos_[27].X() - letter_pos_[26].X(), 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("pqr iiin");
+ // Move back a couple of character widths and confirm the selection still
+ // updates accordingly.
+ p.Move(letter_pos_[25].X() - letter_pos_[26].X(), 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("pqr iii");
+ p.Move(letter_pos_[24].X() - letter_pos_[25].X(), 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("pqr ii");
+ // "Catch up" with the handle - move the extent to where the handle is.
+ // "abcdef ghij kl mno^pqr ii|>inmni, abc"
+ Selection().MoveRangeSelectionExtent(letter_pos_[24]);
+ EXPECT_EQ_SELECTED_TEXT("pqr ii");
+ // Move ahead and confirm the selection expands accordingly
+ // "abcdef ghij kl mno^pqr iii|>nmni, abc"
+ Selection().MoveRangeSelectionExtent(letter_pos_[25]);
+ EXPECT_EQ_SELECTED_TEXT("pqr iii");
+
+ // Confirm we stay in character granularity if the user moves within a word.
+ // "abcdef ghij kl mno^pqr |>iiinmni, abc"
+ Selection().MoveRangeSelectionExtent(letter_pos_[22]);
+ EXPECT_EQ_SELECTED_TEXT("pqr ");
+ // It's possible to get a move when position doesn't change.
+ // It shouldn't affect anything.
+ p = letter_pos_[22];
+ p.Move(1, 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("pqr ");
+ // "abcdef ghij kl mno^pqr i|>iinmni, abc"
+ Selection().MoveRangeSelectionExtent(letter_pos_[23]);
+ EXPECT_EQ_SELECTED_TEXT("pqr i");
+}
+
+// Tests moving selection extent over to the other side of the base
+// on text "abcd efgh ijkl mno^pqr|> iiinmni, abc"
+// (^ means base, | means extent, < means start, and > means end).
+// Text needs to be laid out on a single line with no rotation.
+void GranularityStrategyTest::TestDirectionSwitchSide() {
+ // Move to the middle of word #4, selecting it - this will set the offset to
+ // be half the width of "iiinmni.
+ Selection().MoveRangeSelectionExtent(word_middles_[4]);
+ EXPECT_EQ_SELECTED_TEXT("pqr iiinmni");
+ // Move back leaving only one letter selected.
+ IntPoint p = word_middles_[4];
+ p.Move(letter_pos_[19].X() - letter_pos_[29].X(), 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("p");
+ // Confirm selection doesn't change if extent is positioned at base.
+ p.Move(letter_pos_[18].X() - letter_pos_[19].X(), 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("p");
+ // Move over to the other side of the base. Confirm the offset is preserved.
+ // (i.e. the selection start stays on the right of the extent)
+ // Confirm we stay in character granularity until the beginning of the word
+ // is passed.
+ p.Move(letter_pos_[17].X() - letter_pos_[18].X(), 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("o");
+ p.Move(letter_pos_[16].X() - letter_pos_[17].X(), 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("no");
+ p.Move(letter_pos_[14].X() - letter_pos_[16].X(), 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT(" mno");
+ // Move to just one pixel on the right before the middle of the word #2.
+ // We should switch to word granularity, so the selection shouldn't change.
+ p.Move(word_middles_[2].X() - letter_pos_[14].X() + 1, 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT(" mno");
+ // Move over the middle of the word. The word should get selected.
+ // This should reduce the offset, but it should still stay greated than 0,
+ // since the width of "iiinmni" is greater than the width of "ijkl".
+ p.Move(-2, 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("ijkl mno");
+ // Move to just one pixel on the right of the middle of word #1.
+ // The selection should now include the space between the words.
+ p.Move(word_middles_[1].X() - letter_pos_[10].X() + 1, 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT(" ijkl mno");
+ // Move over the middle of the word. The word should get selected.
+ p.Move(-2, 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("efgh ijkl mno");
+}
+
+// Test for the default CharacterGranularityStrategy
+TEST_F(GranularityStrategyTest, Character) {
+ GetDummyPageHolder().GetFrame().GetSettings()->SetSelectionStrategy(
+ SelectionStrategy::kCharacter);
+ GetDummyPageHolder().GetFrame().GetSettings()->SetDefaultFontSize(12);
+ // "Foo Bar Baz,"
+ Text* text = AppendTextNode("Foo Bar Baz,");
+ GetDocument().UpdateStyleAndLayout();
+
+ // "Foo B^a|>r Baz," (^ means base, | means extent, , < means start, and >
+ // means end).
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 5), Position(text, 6))
+ .Build());
+ EXPECT_EQ_SELECTED_TEXT("a");
+ // "Foo B^ar B|>az,"
+ Selection().MoveRangeSelectionExtent(
+ VisiblePositionToContentsPoint(CreateVisiblePosition(Position(text, 9))));
+ EXPECT_EQ_SELECTED_TEXT("ar B");
+ // "F<|oo B^ar Baz,"
+ Selection().MoveRangeSelectionExtent(
+ VisiblePositionToContentsPoint(CreateVisiblePosition(Position(text, 1))));
+ EXPECT_EQ_SELECTED_TEXT("oo B");
+}
+
+// DirectionGranularityStrategy strategy on rotated text should revert to the
+// same behavior as CharacterGranularityStrategy
+TEST_F(GranularityStrategyTest, DirectionRotate) {
+ Text* text = SetupRotate("Foo Bar Baz,");
+ // "Foo B^a|>r Baz," (^ means base, | means extent, , < means start, and >
+ // means end).
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 5), Position(text, 6))
+ .Build());
+ EXPECT_EQ_SELECTED_TEXT("a");
+ IntPoint p = letter_pos_[9];
+ // Need to move by one pixel, otherwise this point is not evaluated
+ // to the same line as the text by hit testing.
+ p.Move(1, 0);
+ // "Foo B^ar B|>az,"
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("ar B");
+ p = letter_pos_[1];
+ p.Move(1, 0);
+ // "F<|oo B^ar Baz,"
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("oo B");
+}
+
+TEST_F(GranularityStrategyTest, DirectionExpandTranslateZ) {
+ Text* text = SetupTranslateZ("abcdef ghij kl mnopqr stuvwi inm mnii,");
+ // "abcdef ghij kl mno^p|>qr stuvwi inm mnii," (^ means base, | means extent,
+ // < means start, and > means end).
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 18), Position(text, 19))
+ .Build());
+ EXPECT_EQ_SELECTED_TEXT("p");
+ TestDirectionExpand();
+}
+
+TEST_F(GranularityStrategyTest, DirectionExpandTransform) {
+ Text* text = SetupTransform("abcdef ghij kl mnopqr stuvwi inm mnii,");
+ // "abcdef ghij kl mno^p|>qr stuvwi inm mnii," (^ means base, | means extent,
+ // < means start, and > means end).
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 18), Position(text, 19))
+ .Build());
+ EXPECT_EQ_SELECTED_TEXT("p");
+ TestDirectionExpand();
+}
+
+TEST_F(GranularityStrategyTest, DirectionExpandVerticalAlign) {
+ // "abcdef ghij kl mno^p|>qr stuvwi inm mnii," (^ means base, | means extent,
+ // < means start, and > means end).
+ SetupVerticalAlign("abcdef ghij kl m", "nopq", "r stuvwi inm mnii,", 18, 19);
+ EXPECT_EQ_SELECTED_TEXT("p");
+ TestDirectionExpand();
+}
+
+TEST_F(GranularityStrategyTest, DirectionExpandFontSizes) {
+ SetupFontSize("abcdef ghij kl mnopqr st", "uv", "wi inm mnii,", 18, 19);
+ EXPECT_EQ_SELECTED_TEXT("p");
+ TestDirectionExpand();
+}
+
+TEST_F(GranularityStrategyTest, DirectionShrinkTranslateZ) {
+ Text* text = SetupTranslateZ("abcdef ghij kl mnopqr iiinmni, abc");
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 18), Position(text, 21))
+ .Build());
+ EXPECT_EQ_SELECTED_TEXT("pqr");
+ TestDirectionShrink();
+}
+
+TEST_F(GranularityStrategyTest, DirectionShrinkTransform) {
+ Text* text = SetupTransform("abcdef ghij kl mnopqr iiinmni, abc");
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 18), Position(text, 21))
+ .Build());
+ EXPECT_EQ_SELECTED_TEXT("pqr");
+ TestDirectionShrink();
+}
+
+TEST_F(GranularityStrategyTest, DirectionShrinkVerticalAlign) {
+ SetupVerticalAlign("abcdef ghij kl mnopqr ii", "inm", "ni, abc", 18, 21);
+ EXPECT_EQ_SELECTED_TEXT("pqr");
+ TestDirectionShrink();
+}
+
+TEST_F(GranularityStrategyTest, DirectionShrinkFontSizes) {
+ SetupFontSize("abcdef ghij kl mnopqr ii", "inm", "ni, abc", 18, 21);
+ EXPECT_EQ_SELECTED_TEXT("pqr");
+ TestDirectionShrink();
+}
+
+TEST_F(GranularityStrategyTest, DirectionSwitchSideTranslateZ) {
+ Text* text = SetupTranslateZ("abcd efgh ijkl mnopqr iiinmni, abc");
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 18), Position(text, 21))
+ .Build());
+ EXPECT_EQ_SELECTED_TEXT("pqr");
+ TestDirectionSwitchSide();
+}
+
+TEST_F(GranularityStrategyTest, DirectionSwitchSideTransform) {
+ Text* text = SetupTransform("abcd efgh ijkl mnopqr iiinmni, abc");
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 18), Position(text, 21))
+ .Build());
+ EXPECT_EQ_SELECTED_TEXT("pqr");
+ TestDirectionSwitchSide();
+}
+
+TEST_F(GranularityStrategyTest, DirectionSwitchSideVerticalAlign) {
+ SetupVerticalAlign("abcd efgh ijkl", " mnopqr", " iiinmni, abc", 18, 21);
+ EXPECT_EQ_SELECTED_TEXT("pqr");
+ TestDirectionSwitchSide();
+}
+
+TEST_F(GranularityStrategyTest, DirectionSwitchSideFontSizes) {
+ SetupFontSize("abcd efgh i", "jk", "l mnopqr iiinmni, abc", 18, 21);
+ EXPECT_EQ_SELECTED_TEXT("pqr");
+ TestDirectionSwitchSide();
+}
+
+// Tests moving extent over to the other side of the vase and immediately
+// passing the word boundary and going into word granularity.
+TEST_F(GranularityStrategyTest, DirectionSwitchSideWordGranularityThenShrink) {
+ GetDummyPageHolder().GetFrame().GetSettings()->SetDefaultFontSize(12);
+ String str = "ab cd efghijkl mnopqr iiin, abc";
+ Text* text = GetDocument().createTextNode(str);
+ GetDocument().body()->AppendChild(text);
+ GetDocument().UpdateStyleAndLayout();
+ GetDummyPageHolder().GetFrame().GetSettings()->SetSelectionStrategy(
+ SelectionStrategy::kDirection);
+
+ ParseText(text);
+
+ // "abcd efgh ijkl mno^pqr|> iiin, abc" (^ means base, | means extent, < means
+ // start, and > means end).
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 18), Position(text, 21))
+ .Build());
+ EXPECT_EQ_SELECTED_TEXT("pqr");
+ // Move to the middle of word #4 selecting it - this will set the offset to
+ // be half the width of "iiin".
+ Selection().MoveRangeSelectionExtent(word_middles_[4]);
+ EXPECT_EQ_SELECTED_TEXT("pqr iiin");
+ // Move to the middle of word #2 - extent will switch over to the other
+ // side of the base, and we should enter word granularity since we pass
+ // the word boundary. The offset should become negative since the width
+ // of "efghjkkl" is greater than that of "iiin".
+ int offset = letter_pos_[26].X() - word_middles_[4].X();
+ IntPoint p =
+ IntPoint(word_middles_[2].X() - offset - 1, word_middles_[2].Y());
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("efghijkl mno");
+ p.Move(letter_pos_[7].X() - letter_pos_[6].X(), 0);
+ Selection().MoveRangeSelectionExtent(p);
+ EXPECT_EQ_SELECTED_TEXT("fghijkl mno");
+}
+
+// Make sure we switch to word granularity right away when starting on a
+// word boundary and extending.
+TEST_F(GranularityStrategyTest, DirectionSwitchStartOnBoundary) {
+ GetDummyPageHolder().GetFrame().GetSettings()->SetDefaultFontSize(12);
+ String str = "ab cd efghijkl mnopqr iiin, abc";
+ Text* text = GetDocument().createTextNode(str);
+ GetDocument().body()->AppendChild(text);
+ GetDocument().UpdateStyleAndLayout();
+ GetDummyPageHolder().GetFrame().GetSettings()->SetSelectionStrategy(
+ SelectionStrategy::kDirection);
+
+ ParseText(text);
+
+ // "ab cd efghijkl ^mnopqr |>stuvwi inm," (^ means base and | means extent,
+ // > means end).
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 15), Position(text, 22))
+ .Build());
+ EXPECT_EQ_SELECTED_TEXT("mnopqr ");
+ Selection().MoveRangeSelectionExtent(word_middles_[4]);
+ EXPECT_EQ_SELECTED_TEXT("mnopqr iiin");
+}
+
+// For http://crbug.com/704529
+TEST_F(GranularityStrategyTest, UpdateExtentWithNullPositionForCharacter) {
+ GetDummyPageHolder().GetFrame().GetSettings()->SetSelectionStrategy(
+ SelectionStrategy::kCharacter);
+ GetDocument().body()->SetInnerHTMLFromString(
+ "<div id=host></div><div id=sample>ab</div>");
+ // Simulate VIDEO element which has a RANGE as slider of video time.
+ Element* const host = GetDocument().getElementById("host");
+ ShadowRoot& shadow_root =
+ host->AttachShadowRootInternal(ShadowRootType::kOpen);
+ shadow_root.SetInnerHTMLFromString("<input type=range>");
+ Element* const sample = GetDocument().getElementById("sample");
+ GetDocument().UpdateStyleAndLayout();
+ const SelectionInDOMTree& selection_in_dom_tree =
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(sample->firstChild(), 2))
+ .Build();
+ Selection().SetSelection(selection_in_dom_tree,
+ SetSelectionOptions::Builder()
+ .SetShouldCloseTyping(true)
+ .SetShouldClearTypingStyle(true)
+ .SetShouldShowHandle(true)
+ .SetIsDirectional(true)
+ .Build());
+
+ // Since, it is not obvious that |visiblePositionForContentsPoint()| returns
+ // null position, we verify here.
+ ASSERT_EQ(Position(),
+ VisiblePositionForContentsPoint(IntPoint(0, 0), &GetFrame())
+ .DeepEquivalent())
+ << "This test requires null position.";
+
+ // Point to RANGE inside shadow root to get null position from
+ // |visiblePositionForContentsPoint()|.
+ Selection().MoveRangeSelectionExtent(IntPoint(0, 0));
+ EXPECT_EQ(selection_in_dom_tree, Selection().GetSelectionInDOMTree());
+}
+
+// For http://crbug.com/704529
+TEST_F(GranularityStrategyTest, UpdateExtentWithNullPositionForDirectional) {
+ GetDocument().body()->SetInnerHTMLFromString(
+ "<div id=host></div><div id=sample>ab</div>");
+ // Simulate VIDEO element which has a RANGE as slider of video time.
+ Element* const host = GetDocument().getElementById("host");
+ ShadowRoot& shadow_root =
+ host->AttachShadowRootInternal(ShadowRootType::kOpen);
+ shadow_root.SetInnerHTMLFromString("<input type=range>");
+ Element* const sample = GetDocument().getElementById("sample");
+ GetDocument().UpdateStyleAndLayout();
+ const SelectionInDOMTree& selection_in_dom_tree =
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(sample->firstChild(), 2))
+ .Build();
+ Selection().SetSelection(selection_in_dom_tree,
+ SetSelectionOptions::Builder()
+ .SetShouldCloseTyping(true)
+ .SetShouldClearTypingStyle(true)
+ .SetShouldShowHandle(true)
+ .SetIsDirectional(true)
+ .Build());
+
+ // Since, it is not obvious that |visiblePositionForContentsPoint()| returns
+ // null position, we verify here.
+ ASSERT_EQ(Position(),
+ VisiblePositionForContentsPoint(IntPoint(0, 0), &GetFrame())
+ .DeepEquivalent())
+ << "This test requires null position.";
+
+ // Point to RANGE inside shadow root to get null position from
+ // |visiblePositionForContentsPoint()|.
+ Selection().MoveRangeSelectionExtent(IntPoint(0, 0));
+
+ EXPECT_EQ(selection_in_dom_tree, Selection().GetSelectionInDOMTree());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/ime/ime_text_span.cc b/chromium/third_party/blink/renderer/core/editing/ime/ime_text_span.cc
new file mode 100644
index 00000000000..12251e0d5fc
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/ime/ime_text_span.cc
@@ -0,0 +1,71 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/ime/ime_text_span.h"
+
+#include <algorithm>
+#include "third_party/blink/public/web/web_ime_text_span.h"
+
+namespace blink {
+
+ImeTextSpan::ImeTextSpan(Type type,
+ unsigned start_offset,
+ unsigned end_offset,
+ const Color& underline_color,
+ ui::mojom::ImeTextSpanThickness thickness,
+ const Color& background_color,
+ const Color& suggestion_highlight_color,
+ const Vector<String>& suggestions)
+ : type_(type),
+ underline_color_(underline_color),
+ thickness_(thickness),
+ background_color_(background_color),
+ suggestion_highlight_color_(suggestion_highlight_color),
+ suggestions_(suggestions) {
+ // Sanitize offsets by ensuring a valid range corresponding to the last
+ // possible position.
+ // TODO(wkorman): Consider replacing with DCHECK_LT(startOffset, endOffset).
+ start_offset_ =
+ std::min(start_offset, std::numeric_limits<unsigned>::max() - 1u);
+ end_offset_ = std::max(start_offset_ + 1u, end_offset);
+}
+
+namespace {
+
+Vector<String> ConvertStdVectorOfStdStringsToVectorOfStrings(
+ const std::vector<std::string>& input) {
+ Vector<String> output;
+ for (const std::string& val : input) {
+ output.push_back(String::FromUTF8(val.c_str()));
+ }
+ return output;
+}
+
+ImeTextSpan::Type ConvertWebTypeToType(WebImeTextSpan::Type type) {
+ switch (type) {
+ case WebImeTextSpan::Type::kComposition:
+ return ImeTextSpan::Type::kComposition;
+ case WebImeTextSpan::Type::kSuggestion:
+ return ImeTextSpan::Type::kSuggestion;
+ case WebImeTextSpan::Type::kMisspellingSuggestion:
+ return ImeTextSpan::Type::kMisspellingSuggestion;
+ }
+
+ NOTREACHED();
+ return ImeTextSpan::Type::kComposition;
+}
+
+} // namespace
+
+ImeTextSpan::ImeTextSpan(const WebImeTextSpan& ime_text_span)
+ : ImeTextSpan(ConvertWebTypeToType(ime_text_span.type),
+ ime_text_span.start_offset,
+ ime_text_span.end_offset,
+ Color(ime_text_span.underline_color),
+ ime_text_span.thickness,
+ Color(ime_text_span.background_color),
+ Color(ime_text_span.suggestion_highlight_color),
+ ConvertStdVectorOfStdStringsToVectorOfStrings(
+ ime_text_span.suggestions)) {}
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/ime/ime_text_span.h b/chromium/third_party/blink/renderer/core/editing/ime/ime_text_span.h
new file mode 100644
index 00000000000..ea3c5047627
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/ime/ime_text_span.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_IME_IME_TEXT_SPAN_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_IME_IME_TEXT_SPAN_H_
+
+#include "services/ui/public/interfaces/ime/ime.mojom-shared.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/platform/graphics/color.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+#include "third_party/blink/renderer/platform/wtf/vector.h"
+
+namespace blink {
+
+struct WebImeTextSpan;
+
+class CORE_EXPORT ImeTextSpan {
+ DISALLOW_NEW_EXCEPT_PLACEMENT_NEW();
+
+ public:
+ enum class Type { kComposition, kSuggestion, kMisspellingSuggestion };
+
+ ImeTextSpan(Type,
+ unsigned start_offset,
+ unsigned end_offset,
+ const Color& underline_color,
+ ui::mojom::ImeTextSpanThickness,
+ const Color& background_color,
+ const Color& suggestion_highlight_color = Color::kTransparent,
+ const Vector<String>& suggestions = Vector<String>());
+
+ ImeTextSpan(const WebImeTextSpan&);
+
+ Type GetType() const { return type_; }
+ unsigned StartOffset() const { return start_offset_; }
+ unsigned EndOffset() const { return end_offset_; }
+ const Color& UnderlineColor() const { return underline_color_; }
+ ui::mojom::ImeTextSpanThickness Thickness() const { return thickness_; }
+ const Color& BackgroundColor() const { return background_color_; }
+ const Color& SuggestionHighlightColor() const {
+ return suggestion_highlight_color_;
+ }
+ const Vector<String>& Suggestions() const { return suggestions_; }
+
+ private:
+ Type type_;
+ unsigned start_offset_;
+ unsigned end_offset_;
+ Color underline_color_;
+ ui::mojom::ImeTextSpanThickness thickness_;
+ Color background_color_;
+ Color suggestion_highlight_color_;
+ Vector<String> suggestions_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_IME_IME_TEXT_SPAN_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/ime/ime_text_span_test.cc b/chromium/third_party/blink/renderer/core/editing/ime/ime_text_span_test.cc
new file mode 100644
index 00000000000..6118240220a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/ime/ime_text_span_test.cc
@@ -0,0 +1,76 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/ime/ime_text_span.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace blink {
+namespace {
+
+ImeTextSpan CreateImeTextSpan(unsigned start_offset, unsigned end_offset) {
+ return ImeTextSpan(ImeTextSpan::Type::kComposition, start_offset, end_offset,
+ Color::kTransparent,
+ ui::mojom::ImeTextSpanThickness::kNone,
+ Color::kTransparent);
+}
+
+TEST(ImeTextSpanTest, OneChar) {
+ ImeTextSpan ime_text_span = CreateImeTextSpan(0, 1);
+ EXPECT_EQ(0u, ime_text_span.StartOffset());
+ EXPECT_EQ(1u, ime_text_span.EndOffset());
+}
+
+TEST(ImeTextSpanTest, MultiChar) {
+ ImeTextSpan ime_text_span = CreateImeTextSpan(0, 5);
+ EXPECT_EQ(0u, ime_text_span.StartOffset());
+ EXPECT_EQ(5u, ime_text_span.EndOffset());
+}
+
+TEST(ImeTextSpanTest, ZeroLength) {
+ ImeTextSpan ime_text_span = CreateImeTextSpan(0, 0);
+ EXPECT_EQ(0u, ime_text_span.StartOffset());
+ EXPECT_EQ(1u, ime_text_span.EndOffset());
+}
+
+TEST(ImeTextSpanTest, ZeroLengthNonZeroStart) {
+ ImeTextSpan ime_text_span = CreateImeTextSpan(3, 3);
+ EXPECT_EQ(3u, ime_text_span.StartOffset());
+ EXPECT_EQ(4u, ime_text_span.EndOffset());
+}
+
+TEST(ImeTextSpanTest, EndBeforeStart) {
+ ImeTextSpan ime_text_span = CreateImeTextSpan(1, 0);
+ EXPECT_EQ(1u, ime_text_span.StartOffset());
+ EXPECT_EQ(2u, ime_text_span.EndOffset());
+}
+
+TEST(ImeTextSpanTest, LastChar) {
+ ImeTextSpan ime_text_span =
+ CreateImeTextSpan(std::numeric_limits<unsigned>::max() - 1,
+ std::numeric_limits<unsigned>::max());
+ EXPECT_EQ(std::numeric_limits<unsigned>::max() - 1,
+ ime_text_span.StartOffset());
+ EXPECT_EQ(std::numeric_limits<unsigned>::max(), ime_text_span.EndOffset());
+}
+
+TEST(ImeTextSpanTest, LastCharEndBeforeStart) {
+ ImeTextSpan ime_text_span =
+ CreateImeTextSpan(std::numeric_limits<unsigned>::max(),
+ std::numeric_limits<unsigned>::max() - 1);
+ EXPECT_EQ(std::numeric_limits<unsigned>::max() - 1,
+ ime_text_span.StartOffset());
+ EXPECT_EQ(std::numeric_limits<unsigned>::max(), ime_text_span.EndOffset());
+}
+
+TEST(ImeTextSpanTest, LastCharEndBeforeStartZeroEnd) {
+ ImeTextSpan ime_text_span =
+ CreateImeTextSpan(std::numeric_limits<unsigned>::max(), 0);
+ EXPECT_EQ(std::numeric_limits<unsigned>::max() - 1,
+ ime_text_span.StartOffset());
+ EXPECT_EQ(std::numeric_limits<unsigned>::max(), ime_text_span.EndOffset());
+}
+
+} // namespace
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/ime/ime_text_span_vector_builder.cc b/chromium/third_party/blink/renderer/core/editing/ime/ime_text_span_vector_builder.cc
new file mode 100644
index 00000000000..e257c41e495
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/ime/ime_text_span_vector_builder.cc
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/ime/ime_text_span_vector_builder.h"
+
+namespace blink {
+
+Vector<ImeTextSpan> ImeTextSpanVectorBuilder::Build(
+ const WebVector<WebImeTextSpan>& ime_text_spans) {
+ Vector<ImeTextSpan> result;
+ size_t size = ime_text_spans.size();
+ result.ReserveCapacity(size);
+ for (size_t i = 0; i < size; ++i)
+ result.push_back(ime_text_spans[i]);
+ return result;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/ime/ime_text_span_vector_builder.h b/chromium/third_party/blink/renderer/core/editing/ime/ime_text_span_vector_builder.h
new file mode 100644
index 00000000000..10e5ad55d95
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/ime/ime_text_span_vector_builder.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_IME_IME_TEXT_SPAN_VECTOR_BUILDER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_IME_IME_TEXT_SPAN_VECTOR_BUILDER_H_
+
+#include "third_party/blink/public/platform/web_vector.h"
+#include "third_party/blink/public/web/web_ime_text_span.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/ime/ime_text_span.h"
+#include "third_party/blink/renderer/platform/wtf/vector.h"
+
+namespace blink {
+
+// This class is used for converting from WebVector<WebImeTextSpan>
+// to Vector<ImeTextSpan>.
+
+class ImeTextSpanVectorBuilder {
+ STATIC_ONLY(ImeTextSpanVectorBuilder);
+
+ public:
+ CORE_EXPORT static Vector<ImeTextSpan> Build(
+ const WebVector<WebImeTextSpan>&);
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/ime/input_method_controller.cc b/chromium/third_party/blink/renderer/core/editing/ime/input_method_controller.cc
new file mode 100644
index 00000000000..2ef7efdaf69
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/ime/input_method_controller.cc
@@ -0,0 +1,1489 @@
+/*
+ * Copyright (C) 2006, 2007, 2008, 2011 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/ime/input_method_controller.h"
+
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/events/scoped_event_queue.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/commands/typing_command.h"
+#include "third_party/blink/renderer/core/editing/commands/undo_stack.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker_properties.h"
+#include "third_party/blink/renderer/core/editing/reveal_selection_scope.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/set_selection_options.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_checker.h"
+#include "third_party/blink/renderer/core/editing/state_machines/backward_code_point_state_machine.h"
+#include "third_party/blink/renderer/core/editing/state_machines/forward_code_point_state_machine.h"
+#include "third_party/blink/renderer/core/events/composition_event.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
+#include "third_party/blink/renderer/core/html/forms/html_text_area_element.h"
+#include "third_party/blink/renderer/core/input/event_handler.h"
+#include "third_party/blink/renderer/core/input_mode_names.h"
+#include "third_party/blink/renderer/core/input_type_names.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/core/layout/layout_theme.h"
+#include "third_party/blink/renderer/core/page/focus_controller.h"
+#include "third_party/blink/renderer/core/page/page.h"
+
+namespace blink {
+
+namespace {
+
+void DispatchCompositionUpdateEvent(LocalFrame& frame, const String& text) {
+ Element* target = frame.GetDocument()->FocusedElement();
+ if (!target)
+ return;
+
+ CompositionEvent* event = CompositionEvent::Create(
+ EventTypeNames::compositionupdate, frame.DomWindow(), text);
+ target->DispatchEvent(event);
+}
+
+void DispatchCompositionEndEvent(LocalFrame& frame, const String& text) {
+ // Verify that the caller is using an EventQueueScope to suppress the input
+ // event from being fired until the proper time (e.g. after applying an IME
+ // selection update, if necesary).
+ DCHECK(ScopedEventQueue::Instance()->ShouldQueueEvents());
+
+ Element* target = frame.GetDocument()->FocusedElement();
+ if (!target)
+ return;
+
+ CompositionEvent* event = CompositionEvent::Create(
+ EventTypeNames::compositionend, frame.DomWindow(), text);
+ EventDispatcher::DispatchScopedEvent(*target, event);
+}
+
+bool NeedsIncrementalInsertion(const LocalFrame& frame,
+ const String& new_text) {
+ // No need to apply incremental insertion if it doesn't support formated text.
+ if (!frame.GetEditor().CanEditRichly())
+ return false;
+
+ // No need to apply incremental insertion if the old text (text to be
+ // replaced) or the new text (text to be inserted) is empty.
+ if (frame.SelectedText().IsEmpty() || new_text.IsEmpty())
+ return false;
+
+ return true;
+}
+
+void DispatchBeforeInputFromComposition(EventTarget* target,
+ InputEvent::InputType input_type,
+ const String& data) {
+ if (!target)
+ return;
+ // TODO(chongz): Pass appropriate |ranges| after it's defined on spec.
+ // http://w3c.github.io/editing/input-events.html#dom-inputevent-inputtype
+ InputEvent* before_input_event = InputEvent::CreateBeforeInput(
+ input_type, data, InputEvent::kNotCancelable,
+ InputEvent::EventIsComposing::kIsComposing, nullptr);
+ target->DispatchEvent(before_input_event);
+}
+
+// Used to insert/replace text during composition update and confirm
+// composition.
+// Procedure:
+// 1. Fire 'beforeinput' event for (TODO(chongz): deleted composed text) and
+// inserted text
+// 2. Fire 'compositionupdate' event
+// 3. Fire TextEvent and modify DOM
+// 4. Fire 'input' event; dispatched by Editor::AppliedEditing()
+void InsertTextDuringCompositionWithEvents(
+ LocalFrame& frame,
+ const String& text,
+ TypingCommand::Options options,
+ TypingCommand::TextCompositionType composition_type) {
+ // Verify that the caller is using an EventQueueScope to suppress the input
+ // event from being fired until the proper time (e.g. after applying an IME
+ // selection update, if necesary).
+ DCHECK(ScopedEventQueue::Instance()->ShouldQueueEvents());
+ DCHECK(composition_type ==
+ TypingCommand::TextCompositionType::kTextCompositionUpdate ||
+ composition_type ==
+ TypingCommand::TextCompositionType::kTextCompositionConfirm ||
+ composition_type ==
+ TypingCommand::TextCompositionType::kTextCompositionCancel)
+ << "compositionType should be TextCompositionUpdate or "
+ "TextCompositionConfirm or TextCompositionCancel, but got "
+ << static_cast<int>(composition_type);
+ if (!frame.GetDocument())
+ return;
+
+ Element* target = frame.GetDocument()->FocusedElement();
+ if (!target)
+ return;
+
+ DispatchBeforeInputFromComposition(
+ target, InputEvent::InputType::kInsertCompositionText, text);
+
+ // 'beforeinput' event handler may destroy document.
+ if (!frame.GetDocument())
+ return;
+
+ DispatchCompositionUpdateEvent(frame, text);
+ // 'compositionupdate' event handler may destroy document.
+ if (!frame.GetDocument())
+ return;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ const bool is_incremental_insertion = NeedsIncrementalInsertion(frame, text);
+
+ switch (composition_type) {
+ case TypingCommand::TextCompositionType::kTextCompositionUpdate:
+ case TypingCommand::TextCompositionType::kTextCompositionConfirm:
+ // Calling |TypingCommand::insertText()| with empty text will result in an
+ // incorrect ending selection. We need to delete selection first.
+ // https://crbug.com/693481
+ if (text.IsEmpty())
+ TypingCommand::DeleteSelection(*frame.GetDocument(), 0);
+ frame.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+ TypingCommand::InsertText(*frame.GetDocument(), text, options,
+ composition_type, is_incremental_insertion);
+ break;
+ case TypingCommand::TextCompositionType::kTextCompositionCancel:
+ // TODO(chongz): Use TypingCommand::insertText after TextEvent was
+ // removed. (Removed from spec since 2012)
+ // See TextEvent.idl.
+ frame.GetEventHandler().HandleTextInputEvent(text, nullptr,
+ kTextEventInputComposition);
+ break;
+ default:
+ NOTREACHED();
+ }
+}
+
+AtomicString GetInputModeAttribute(Element* element) {
+ if (!element)
+ return AtomicString();
+
+ bool query_attribute = false;
+ if (auto* input = ToHTMLInputElementOrNull(*element)) {
+ query_attribute = input->SupportsInputModeAttribute();
+ } else if (IsHTMLTextAreaElement(*element)) {
+ query_attribute = true;
+ } else {
+ element->GetDocument().UpdateStyleAndLayoutTree();
+ if (HasEditableStyle(*element))
+ query_attribute = true;
+ }
+
+ if (!query_attribute)
+ return AtomicString();
+
+ // TODO(dtapuska): We may wish to restrict this to a yet to be proposed
+ // <contenteditable> or <richtext> element Mozilla discussed at TPAC 2016.
+ return element->FastGetAttribute(HTMLNames::inputmodeAttr).LowerASCII();
+}
+
+constexpr int kInvalidDeletionLength = -1;
+constexpr bool IsInvalidDeletionLength(const int length) {
+ return length == kInvalidDeletionLength;
+}
+
+int CalculateBeforeDeletionLengthsInCodePoints(
+ const String& text,
+ const int before_length_in_code_points,
+ const int selection_start) {
+ DCHECK_GE(before_length_in_code_points, 0);
+ DCHECK_GE(selection_start, 0);
+ DCHECK_LE(selection_start, static_cast<int>(text.length()));
+
+ const UChar* u_text = text.Characters16();
+ BackwardCodePointStateMachine backward_machine;
+ int counter = before_length_in_code_points;
+ int deletion_start = selection_start;
+ while (counter > 0 && deletion_start > 0) {
+ const TextSegmentationMachineState state =
+ backward_machine.FeedPrecedingCodeUnit(u_text[deletion_start - 1]);
+ // According to Android's InputConnection spec, we should do nothing if
+ // |text| has invalid surrogate pair in the deletion range.
+ if (state == TextSegmentationMachineState::kInvalid)
+ return kInvalidDeletionLength;
+
+ if (backward_machine.AtCodePointBoundary())
+ --counter;
+ --deletion_start;
+ }
+ if (!backward_machine.AtCodePointBoundary())
+ return kInvalidDeletionLength;
+
+ const int offset = backward_machine.GetBoundaryOffset();
+ DCHECK_EQ(-offset, selection_start - deletion_start);
+ return -offset;
+}
+
+int CalculateAfterDeletionLengthsInCodePoints(
+ const String& text,
+ const int after_length_in_code_points,
+ const int selection_end) {
+ DCHECK_GE(after_length_in_code_points, 0);
+ DCHECK_GE(selection_end, 0);
+ const int length = text.length();
+ DCHECK_LE(selection_end, length);
+
+ const UChar* u_text = text.Characters16();
+ ForwardCodePointStateMachine forward_machine;
+ int counter = after_length_in_code_points;
+ int deletion_end = selection_end;
+ while (counter > 0 && deletion_end < length) {
+ const TextSegmentationMachineState state =
+ forward_machine.FeedFollowingCodeUnit(u_text[deletion_end]);
+ // According to Android's InputConnection spec, we should do nothing if
+ // |text| has invalid surrogate pair in the deletion range.
+ if (state == TextSegmentationMachineState::kInvalid)
+ return kInvalidDeletionLength;
+
+ if (forward_machine.AtCodePointBoundary())
+ --counter;
+ ++deletion_end;
+ }
+ if (!forward_machine.AtCodePointBoundary())
+ return kInvalidDeletionLength;
+
+ const int offset = forward_machine.GetBoundaryOffset();
+ DCHECK_EQ(offset, deletion_end - selection_end);
+ return offset;
+}
+
+Element* RootEditableElementOfSelection(const FrameSelection& frameSelection) {
+ const SelectionInDOMTree& selection = frameSelection.GetSelectionInDOMTree();
+ if (selection.IsNone())
+ return nullptr;
+ // To avoid update layout, we attempt to get root editable element from
+ // a position where script/user specified.
+ if (Element* editable = RootEditableElementOf(selection.Base()))
+ return editable;
+
+ // This is work around for applications assumes a position before editable
+ // element as editable[1]
+ // [1] http://crbug.com/712761
+
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ frameSelection.GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ const VisibleSelection& visibleSeleciton =
+ frameSelection.ComputeVisibleSelectionInDOMTree();
+ return RootEditableElementOf(visibleSeleciton.Start());
+}
+
+std::pair<ContainerNode*, PlainTextRange> PlainTextRangeForEphemeralRange(
+ const EphemeralRange& range) {
+ if (range.IsNull())
+ return {};
+ ContainerNode* const editable =
+ RootEditableElementOrTreeScopeRootNodeOf(range.StartPosition());
+ DCHECK(editable);
+ return std::make_pair(editable, PlainTextRange::Create(*editable, range));
+}
+
+int ComputeAutocapitalizeFlags(const Element* element) {
+ const HTMLElement* const html_element = ToHTMLElementOrNull(element);
+ if (!html_element)
+ return 0;
+
+ // We set the autocapitalization flag corresponding to the "used
+ // autocapitalization hint" for the focused element:
+ // https://html.spec.whatwg.org/multipage/interaction.html#used-autocapitalization-hint
+ if (auto* input = ToHTMLInputElementOrNull(*html_element)) {
+ const AtomicString& input_type = input->type();
+ if (input_type == InputTypeNames::email ||
+ input_type == InputTypeNames::url ||
+ input_type == InputTypeNames::password) {
+ // The autocapitalize IDL attribute value is ignored for these input
+ // types, so we set the None flag.
+ return kWebTextInputFlagAutocapitalizeNone;
+ }
+ }
+
+ int flags = 0;
+
+ DEFINE_STATIC_LOCAL(const AtomicString, none, ("none"));
+ DEFINE_STATIC_LOCAL(const AtomicString, characters, ("characters"));
+ DEFINE_STATIC_LOCAL(const AtomicString, words, ("words"));
+ DEFINE_STATIC_LOCAL(const AtomicString, sentences, ("sentences"));
+
+ const AtomicString& autocapitalize = html_element->autocapitalize();
+ if (autocapitalize == none) {
+ flags |= kWebTextInputFlagAutocapitalizeNone;
+ } else if (autocapitalize == characters) {
+ flags |= kWebTextInputFlagAutocapitalizeCharacters;
+ } else if (autocapitalize == words) {
+ flags |= kWebTextInputFlagAutocapitalizeWords;
+ } else if (autocapitalize == sentences || autocapitalize == "") {
+ // Note: we tell the IME to enable autocapitalization for both the default
+ // state ("") and the sentences states. We could potentially treat these
+ // differently if we had a platform that supported autocapitalization but
+ // didn't want to enable it unless explicitly requested by a web page, but
+ // this so far has not been necessary.
+ flags |= kWebTextInputFlagAutocapitalizeSentences;
+ } else {
+ NOTREACHED();
+ }
+
+ return flags;
+}
+
+} // anonymous namespace
+
+enum class InputMethodController::TypingContinuation { kContinue, kEnd };
+
+InputMethodController* InputMethodController::Create(LocalFrame& frame) {
+ return new InputMethodController(frame);
+}
+
+InputMethodController::InputMethodController(LocalFrame& frame)
+ : frame_(&frame), has_composition_(false) {}
+
+InputMethodController::~InputMethodController() = default;
+
+bool InputMethodController::IsAvailable() const {
+ return LifecycleContext();
+}
+
+Document& InputMethodController::GetDocument() const {
+ DCHECK(IsAvailable());
+ return *LifecycleContext();
+}
+
+bool InputMethodController::HasComposition() const {
+ return has_composition_ && !composition_range_->collapsed() &&
+ composition_range_->IsConnected();
+}
+
+inline Editor& InputMethodController::GetEditor() const {
+ return GetFrame().GetEditor();
+}
+
+void InputMethodController::Clear() {
+ has_composition_ = false;
+ if (composition_range_) {
+ composition_range_->setStart(&GetDocument(), 0);
+ composition_range_->collapse(true);
+ }
+ GetDocument().Markers().RemoveMarkersOfTypes(DocumentMarker::kComposition);
+}
+
+void InputMethodController::ContextDestroyed(Document*) {
+ Clear();
+ composition_range_ = nullptr;
+}
+
+void InputMethodController::DocumentAttached(Document* document) {
+ DCHECK(document);
+ SetContext(document);
+}
+
+void InputMethodController::SelectComposition() const {
+ const EphemeralRange range = CompositionEphemeralRange();
+ if (range.IsNull())
+ return;
+
+ // The composition can start inside a composed character sequence, so we have
+ // to override checks. See <http://bugs.webkit.org/show_bug.cgi?id=15781>
+
+ // The SetSelectionOptions() parameter is necessary because without it,
+ // FrameSelection::SetSelection() will actually call
+ // SetShouldClearTypingStyle(true), which will cause problems applying
+ // formatting during composition. See https://crbug.com/803278.
+ GetFrame().Selection().SetSelection(
+ SelectionInDOMTree::Builder().SetBaseAndExtent(range).Build(),
+ SetSelectionOptions());
+}
+
+bool IsTextTooLongAt(const Position& position) {
+ const Element* element = EnclosingTextControl(position);
+ if (!element)
+ return false;
+ if (auto* input = ToHTMLInputElementOrNull(element))
+ return input->TooLong();
+ if (auto* textarea = ToHTMLTextAreaElementOrNull(element))
+ return textarea->TooLong();
+ return false;
+}
+
+bool InputMethodController::FinishComposingText(
+ ConfirmCompositionBehavior confirm_behavior) {
+ if (!HasComposition())
+ return false;
+
+ // If text is longer than maxlength, give input/textarea's handler a chance to
+ // clamp the text by replacing the composition with the same value.
+ const bool is_too_long = IsTextTooLongAt(composition_range_->StartPosition());
+
+ // TODO(editing-dev): Use of UpdateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ const String& composing = ComposingText();
+
+ // Suppress input event (if we hit the is_too_long case) and compositionend
+ // event until after we restore the original selection (to avoid clobbering a
+ // selection update applied by an event handler).
+ EventQueueScope scope;
+
+ if (confirm_behavior == kKeepSelection) {
+ // Do not dismiss handles even if we are moving selection, because we will
+ // eventually move back to the old selection offsets.
+ const bool is_handle_visible = GetFrame().Selection().IsHandleVisible();
+
+ const PlainTextRange& old_offsets = GetSelectionOffsets();
+ RevealSelectionScope reveal_selection_scope(GetFrame());
+
+ if (is_too_long) {
+ ignore_result(ReplaceComposition(ComposingText()));
+ } else {
+ Clear();
+ DispatchCompositionEndEvent(GetFrame(), composing);
+ }
+
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ const EphemeralRange& old_selection_range =
+ EphemeralRangeForOffsets(old_offsets);
+ if (old_selection_range.IsNull())
+ return false;
+ const SelectionInDOMTree& selection =
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(old_selection_range)
+ .Build();
+ GetFrame().Selection().SetSelection(
+ selection, SetSelectionOptions::Builder()
+ .SetShouldCloseTyping(true)
+ .SetShouldShowHandle(is_handle_visible)
+ .Build());
+ return true;
+ }
+
+ PlainTextRange composition_range =
+ PlainTextRangeForEphemeralRange(CompositionEphemeralRange()).second;
+ if (composition_range.IsNull())
+ return false;
+
+ if (is_too_long) {
+ // Don't move caret or dispatch compositionend event if
+ // ReplaceComposition() fails.
+ if (!ReplaceComposition(ComposingText()))
+ return false;
+ } else {
+ Clear();
+ DispatchCompositionEndEvent(GetFrame(), composing);
+ }
+
+ // Note: MoveCaret() occurs *before* the input and compositionend events are
+ // dispatched, due to the use of ScopedEventQueue. This allows input and
+ // compositionend event handlers to change the current selection without
+ // it getting overwritten again.
+ return MoveCaret(composition_range.End());
+}
+
+bool InputMethodController::CommitText(
+ const String& text,
+ const Vector<ImeTextSpan>& ime_text_spans,
+ int relative_caret_position) {
+ if (HasComposition()) {
+ return ReplaceCompositionAndMoveCaret(text, relative_caret_position,
+ ime_text_spans);
+ }
+
+ return InsertTextAndMoveCaret(text, relative_caret_position, ime_text_spans);
+}
+
+bool InputMethodController::ReplaceComposition(const String& text) {
+ // Verify that the caller is using an EventQueueScope to suppress the input
+ // event from being fired until the proper time (e.g. after applying an IME
+ // selection update, if necesary).
+ DCHECK(ScopedEventQueue::Instance()->ShouldQueueEvents());
+
+ if (!HasComposition())
+ return false;
+
+ // Select the text that will be deleted or replaced.
+ SelectComposition();
+
+ if (GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .IsNone())
+ return false;
+
+ if (!IsAvailable())
+ return false;
+
+ Clear();
+
+ InsertTextDuringCompositionWithEvents(
+ GetFrame(), text, 0,
+ TypingCommand::TextCompositionType::kTextCompositionConfirm);
+
+ // textInput event handler might destroy document (input event is queued
+ // until later).
+ if (!IsAvailable())
+ return false;
+
+ // No DOM update after 'compositionend'.
+ DispatchCompositionEndEvent(GetFrame(), text);
+
+ return true;
+}
+
+// relativeCaretPosition is relative to the end of the text.
+static int ComputeAbsoluteCaretPosition(size_t text_start,
+ size_t text_length,
+ int relative_caret_position) {
+ return text_start + text_length + relative_caret_position;
+}
+
+void InputMethodController::AddImeTextSpans(
+ const Vector<ImeTextSpan>& ime_text_spans,
+ ContainerNode* base_element,
+ unsigned offset_in_plain_chars) {
+ for (const auto& ime_text_span : ime_text_spans) {
+ unsigned ime_text_span_start =
+ offset_in_plain_chars + ime_text_span.StartOffset();
+ unsigned ime_text_span_end =
+ offset_in_plain_chars + ime_text_span.EndOffset();
+
+ EphemeralRange ephemeral_line_range =
+ PlainTextRange(ime_text_span_start, ime_text_span_end)
+ .CreateRange(*base_element);
+ if (ephemeral_line_range.IsNull())
+ continue;
+
+ switch (ime_text_span.GetType()) {
+ case ImeTextSpan::Type::kComposition:
+ GetDocument().Markers().AddCompositionMarker(
+ ephemeral_line_range, ime_text_span.UnderlineColor(),
+ ime_text_span.Thickness(), ime_text_span.BackgroundColor());
+ break;
+ case ImeTextSpan::Type::kSuggestion:
+ case ImeTextSpan::Type::kMisspellingSuggestion:
+ const SuggestionMarker::SuggestionType suggestion_type =
+ ime_text_span.GetType() == ImeTextSpan::Type::kMisspellingSuggestion
+ ? SuggestionMarker::SuggestionType::kMisspelling
+ : SuggestionMarker::SuggestionType::kNotMisspelling;
+
+ // If spell-checking is disabled for an element, we ignore suggestion
+ // markers used to mark misspelled words, but allow other ones (e.g.,
+ // markers added by an IME to allow picking between multiple possible
+ // words, none of which is necessarily misspelled).
+ if (suggestion_type == SuggestionMarker::SuggestionType::kMisspelling &&
+ !SpellChecker::IsSpellCheckingEnabledAt(
+ ephemeral_line_range.StartPosition()))
+ continue;
+
+ GetDocument().Markers().AddSuggestionMarker(
+ ephemeral_line_range,
+ SuggestionMarkerProperties::Builder()
+ .SetType(suggestion_type)
+ .SetSuggestions(ime_text_span.Suggestions())
+ .SetHighlightColor(ime_text_span.SuggestionHighlightColor())
+ .SetUnderlineColor(ime_text_span.UnderlineColor())
+ .SetThickness(ime_text_span.Thickness())
+ .SetBackgroundColor(ime_text_span.BackgroundColor())
+ .Build());
+ break;
+ }
+ }
+}
+
+bool InputMethodController::ReplaceCompositionAndMoveCaret(
+ const String& text,
+ int relative_caret_position,
+ const Vector<ImeTextSpan>& ime_text_spans) {
+ Element* root_editable_element =
+ GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .RootEditableElement();
+ if (!root_editable_element)
+ return false;
+ DCHECK(HasComposition());
+ PlainTextRange composition_range =
+ PlainTextRange::Create(*root_editable_element, *composition_range_);
+ if (composition_range.IsNull())
+ return false;
+ int text_start = composition_range.Start();
+
+ // Suppress input and compositionend events until after we move the caret to
+ // the new position.
+ EventQueueScope scope;
+ if (!ReplaceComposition(text))
+ return false;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ AddImeTextSpans(ime_text_spans, root_editable_element, text_start);
+
+ int absolute_caret_position = ComputeAbsoluteCaretPosition(
+ text_start, text.length(), relative_caret_position);
+ return MoveCaret(absolute_caret_position);
+}
+
+bool InputMethodController::InsertText(const String& text) {
+ if (DispatchBeforeInputInsertText(GetDocument().FocusedElement(), text) !=
+ DispatchEventResult::kNotCanceled)
+ return false;
+ GetEditor().InsertText(text, nullptr);
+ return true;
+}
+
+bool InputMethodController::InsertTextAndMoveCaret(
+ const String& text,
+ int relative_caret_position,
+ const Vector<ImeTextSpan>& ime_text_spans) {
+ PlainTextRange selection_range = GetSelectionOffsets();
+ if (selection_range.IsNull())
+ return false;
+ int text_start = selection_range.Start();
+
+ // Suppress input event until after we move the caret to the new position.
+ EventQueueScope scope;
+
+ // Don't fire events for a no-op operation.
+ if (!text.IsEmpty() || selection_range.length() > 0) {
+ if (!InsertText(text))
+ return false;
+ }
+
+ Element* root_editable_element =
+ GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .RootEditableElement();
+ if (root_editable_element) {
+ AddImeTextSpans(ime_text_spans, root_editable_element, text_start);
+ }
+
+ int absolute_caret_position = ComputeAbsoluteCaretPosition(
+ text_start, text.length(), relative_caret_position);
+ return MoveCaret(absolute_caret_position);
+}
+
+void InputMethodController::CancelComposition() {
+ if (!HasComposition())
+ return;
+
+ RevealSelectionScope reveal_selection_scope(GetFrame());
+
+ if (GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .IsNone())
+ return;
+
+ Clear();
+
+ InsertTextDuringCompositionWithEvents(
+ GetFrame(), g_empty_string, 0,
+ TypingCommand::TextCompositionType::kTextCompositionCancel);
+ // Event handler might destroy document.
+ if (!IsAvailable())
+ return;
+
+ // An open typing command that disagrees about current selection would cause
+ // issues with typing later on.
+ TypingCommand::CloseTyping(frame_);
+
+ // No DOM update after 'compositionend'.
+ DispatchCompositionEndEvent(GetFrame(), g_empty_string);
+}
+
+bool InputMethodController::DispatchCompositionStartEvent(const String& text) {
+ Element* target = GetDocument().FocusedElement();
+ if (!target)
+ return IsAvailable();
+
+ CompositionEvent* event = CompositionEvent::Create(
+ EventTypeNames::compositionstart, GetFrame().DomWindow(), text);
+ target->DispatchEvent(event);
+
+ return IsAvailable();
+}
+
+void InputMethodController::SetComposition(
+ const String& text,
+ const Vector<ImeTextSpan>& ime_text_spans,
+ int selection_start,
+ int selection_end) {
+ RevealSelectionScope reveal_selection_scope(GetFrame());
+
+ // Updates styles before setting selection for composition to prevent
+ // inserting the previous composition text into text nodes oddly.
+ // See https://bugs.webkit.org/show_bug.cgi?id=46868
+ GetDocument().UpdateStyleAndLayoutTree();
+
+ SelectComposition();
+
+ if (GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .IsNone())
+ return;
+
+ Element* target = GetDocument().FocusedElement();
+ if (!target)
+ return;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ PlainTextRange selected_range = CreateSelectionRangeForSetComposition(
+ selection_start, selection_end, text.length());
+
+ // Dispatch an appropriate composition event to the focused node.
+ // We check the composition status and choose an appropriate composition event
+ // since this function is used for three purposes:
+ // 1. Starting a new composition.
+ // Send a compositionstart and a compositionupdate event when this function
+ // creates a new composition node, i.e. !hasComposition() &&
+ // !text.isEmpty().
+ // Sending a compositionupdate event at this time ensures that at least one
+ // compositionupdate event is dispatched.
+ // 2. Updating the existing composition node.
+ // Send a compositionupdate event when this function updates the existing
+ // composition node, i.e. hasComposition() && !text.isEmpty().
+ // 3. Canceling the ongoing composition.
+ // Send a compositionend event when function deletes the existing
+ // composition node, i.e. !hasComposition() && test.isEmpty().
+ if (text.IsEmpty()) {
+ // Suppress input and compositionend events until after we move the caret
+ // to the new position.
+ EventQueueScope scope;
+ if (HasComposition()) {
+ RevealSelectionScope reveal_selection_scope(GetFrame());
+ // Do not attempt to apply IME selection offsets if ReplaceComposition()
+ // fails (we compute the new range assuming the replacement will succeed).
+ if (!ReplaceComposition(g_empty_string))
+ return;
+ } else {
+ // It's weird to call |setComposition()| with empty text outside
+ // composition, however some IME (e.g. Japanese IBus-Anthy) did this, so
+ // we simply delete selection without sending extra events.
+ if (!DeleteSelection())
+ return;
+ }
+
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ SetEditableSelectionOffsets(selected_range);
+ return;
+ }
+
+ // We should send a 'compositionstart' event only when the given text is not
+ // empty because this function doesn't create a composition node when the text
+ // is empty.
+ if (!HasComposition() &&
+ !DispatchCompositionStartEvent(GetFrame().SelectedText())) {
+ return;
+ }
+
+ DCHECK(!text.IsEmpty());
+
+ Clear();
+
+ // Suppress input event until after we move the caret to the new position.
+ EventQueueScope scope;
+ InsertTextDuringCompositionWithEvents(GetFrame(), text,
+ TypingCommand::kSelectInsertedText,
+ TypingCommand::kTextCompositionUpdate);
+ // Event handlers might destroy document.
+ if (!IsAvailable())
+ return;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // The undo stack could become empty if a JavaScript event handler calls
+ // execCommand('undo') to pop elements off the stack. Or, the top element of
+ // the stack could end up not corresponding to the TypingCommand. Make sure we
+ // don't crash in these cases (it's unclear what the composition range should
+ // be set to in these cases, so we don't worry too much about that).
+ SelectionInDOMTree selection;
+ if (GetEditor().GetUndoStack().CanUndo()) {
+ const UndoStep* undo_step = *GetEditor().GetUndoStack().UndoSteps().begin();
+ const SelectionForUndoStep& undo_selection = undo_step->EndingSelection();
+ if (undo_selection.IsValidFor(GetDocument()))
+ selection = undo_selection.AsSelection();
+ }
+
+ // Find out what node has the composition now.
+ const Position base =
+ MostForwardCaretPosition(selection.Base(), kCanSkipOverEditingBoundary);
+ Node* base_node = base.AnchorNode();
+ if (!base_node || !base_node->IsTextNode())
+ return;
+
+ const Position extent = selection.Extent();
+ Node* extent_node = extent.AnchorNode();
+
+ unsigned extent_offset = extent.ComputeOffsetInContainerNode();
+ unsigned base_offset = base.ComputeOffsetInContainerNode();
+
+ has_composition_ = true;
+ if (!composition_range_)
+ composition_range_ = Range::Create(GetDocument());
+ composition_range_->setStart(base_node, base_offset);
+ composition_range_->setEnd(extent_node, extent_offset);
+
+ if (base_node->GetLayoutObject())
+ base_node->GetLayoutObject()->SetShouldDoFullPaintInvalidation();
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // We shouldn't close typing in the middle of setComposition.
+ SetEditableSelectionOffsets(selected_range, TypingContinuation::kContinue);
+
+ if (TypingCommand* const last_typing_command =
+ TypingCommand::LastTypingCommandIfStillOpenForTyping(&GetFrame())) {
+ // When we called InsertTextDuringCompositionWithEvents() with the
+ // kSelectInsertedText flag, it set what is now the composition range as the
+ // ending selection on the open TypingCommand. We now update it to the
+ // current selection to fix two problems:
+ //
+ // 1. Certain operations, e.g. pressing enter on a physical keyboard on
+ // Android, would otherwise incorrectly replace the composition range.
+ //
+ // 2. Using undo would cause text to be selected, even though we never
+ // actually showed the selection to the user.
+ TypingCommand::UpdateSelectionIfDifferentFromCurrentSelection(
+ last_typing_command, &GetFrame());
+ }
+
+ // Even though we would've returned already if SetComposition() were called
+ // with an empty string, the composition range could still be empty right now
+ // due to Unicode grapheme cluster position normalization (e.g. if
+ // SetComposition() were passed an extending character which doesn't allow a
+ // grapheme cluster break immediately before.
+ if (!HasComposition())
+ return;
+
+ if (ime_text_spans.IsEmpty()) {
+ GetDocument().Markers().AddCompositionMarker(
+ CompositionEphemeralRange(), Color::kTransparent,
+ ui::mojom::ImeTextSpanThickness::kThin,
+ LayoutTheme::GetTheme().PlatformDefaultCompositionBackgroundColor());
+ return;
+ }
+
+ const std::pair<ContainerNode*, PlainTextRange>&
+ root_element_and_plain_text_range =
+ PlainTextRangeForEphemeralRange(CompositionEphemeralRange());
+ AddImeTextSpans(ime_text_spans, root_element_and_plain_text_range.first,
+ root_element_and_plain_text_range.second.Start());
+}
+
+PlainTextRange InputMethodController::CreateSelectionRangeForSetComposition(
+ int selection_start,
+ int selection_end,
+ size_t text_length) const {
+ const int selection_offsets_start =
+ static_cast<int>(GetSelectionOffsets().Start());
+ const int start = selection_offsets_start + selection_start;
+ const int end = selection_offsets_start + selection_end;
+ return CreateRangeForSelection(start, end, text_length);
+}
+
+void InputMethodController::SetCompositionFromExistingText(
+ const Vector<ImeTextSpan>& ime_text_spans,
+ unsigned composition_start,
+ unsigned composition_end) {
+ Element* target = GetDocument().FocusedElement();
+ if (!target)
+ return;
+
+ if (!HasComposition() && !DispatchCompositionStartEvent(""))
+ return;
+
+ Element* editable = GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .RootEditableElement();
+ if (!editable)
+ return;
+
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+
+ const EphemeralRange range =
+ PlainTextRange(composition_start, composition_end).CreateRange(*editable);
+ if (range.IsNull())
+ return;
+
+ const Position start = range.StartPosition();
+ if (RootEditableElementOf(start) != editable)
+ return;
+
+ const Position end = range.EndPosition();
+ if (RootEditableElementOf(end) != editable)
+ return;
+
+ Clear();
+
+ AddImeTextSpans(ime_text_spans, editable, composition_start);
+
+ has_composition_ = true;
+ if (!composition_range_)
+ composition_range_ = Range::Create(GetDocument());
+ composition_range_->setStart(range.StartPosition());
+ composition_range_->setEnd(range.EndPosition());
+
+ DispatchCompositionUpdateEvent(GetFrame(), ComposingText());
+}
+
+EphemeralRange InputMethodController::CompositionEphemeralRange() const {
+ if (!HasComposition())
+ return EphemeralRange();
+ return EphemeralRange(composition_range_.Get());
+}
+
+String InputMethodController::ComposingText() const {
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ GetDocument().Lifecycle());
+ return PlainText(
+ CompositionEphemeralRange(),
+ TextIteratorBehavior::Builder().SetEmitsOriginalText(true).Build());
+}
+
+PlainTextRange InputMethodController::GetSelectionOffsets() const {
+ EphemeralRange range = FirstEphemeralRangeOf(
+ GetFrame().Selection().ComputeVisibleSelectionInDOMTreeDeprecated());
+ return PlainTextRangeForEphemeralRange(range).second;
+}
+
+EphemeralRange InputMethodController::EphemeralRangeForOffsets(
+ const PlainTextRange& offsets) const {
+ if (offsets.IsNull())
+ return EphemeralRange();
+ Element* root_editable_element =
+ GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .RootEditableElement();
+ if (!root_editable_element)
+ return EphemeralRange();
+
+ DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
+
+ return offsets.CreateRange(*root_editable_element);
+}
+
+bool InputMethodController::SetSelectionOffsets(
+ const PlainTextRange& selection_offsets) {
+ return SetSelectionOffsets(selection_offsets, TypingContinuation::kEnd);
+}
+
+bool InputMethodController::SetSelectionOffsets(
+ const PlainTextRange& selection_offsets,
+ TypingContinuation typing_continuation) {
+ const EphemeralRange range = EphemeralRangeForOffsets(selection_offsets);
+ if (range.IsNull())
+ return false;
+
+ GetFrame().Selection().SetSelection(
+ SelectionInDOMTree::Builder().SetBaseAndExtent(range).Build(),
+ SetSelectionOptions::Builder()
+ .SetShouldCloseTyping(typing_continuation == TypingContinuation::kEnd)
+ .Build());
+ return true;
+}
+
+bool InputMethodController::SetEditableSelectionOffsets(
+ const PlainTextRange& selection_offsets) {
+ return SetEditableSelectionOffsets(selection_offsets,
+ TypingContinuation::kEnd);
+}
+
+bool InputMethodController::SetEditableSelectionOffsets(
+ const PlainTextRange& selection_offsets,
+ TypingContinuation typing_continuation) {
+ if (!GetEditor().CanEdit())
+ return false;
+ return SetSelectionOffsets(selection_offsets, typing_continuation);
+}
+
+PlainTextRange InputMethodController::CreateRangeForSelection(
+ int start,
+ int end,
+ size_t text_length) const {
+ // In case of exceeding the left boundary.
+ start = std::max(start, 0);
+ end = std::max(end, start);
+
+ Element* root_editable_element =
+ GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .RootEditableElement();
+ if (!root_editable_element)
+ return PlainTextRange();
+ const EphemeralRange& range =
+ EphemeralRange::RangeOfContents(*root_editable_element);
+ if (range.IsNull())
+ return PlainTextRange();
+
+ const TextIteratorBehavior& behavior =
+ TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(true)
+ .SetEmitsCharactersBetweenAllVisiblePositions(true)
+ .Build();
+ TextIterator it(range.StartPosition(), range.EndPosition(), behavior);
+
+ int right_boundary = 0;
+ for (; !it.AtEnd(); it.Advance())
+ right_boundary += it.length();
+
+ if (HasComposition())
+ right_boundary -= composition_range_->GetText().length();
+
+ right_boundary += text_length;
+
+ // In case of exceeding the right boundary.
+ start = std::min(start, right_boundary);
+ end = std::min(end, right_boundary);
+
+ return PlainTextRange(start, end);
+}
+
+bool InputMethodController::DeleteSelection() {
+ if (!GetFrame().Selection().ComputeVisibleSelectionInDOMTree().IsRange())
+ return true;
+
+ Node* target = GetFrame().GetDocument()->FocusedElement();
+ if (target) {
+ DispatchBeforeInputEditorCommand(
+ target, InputEvent::InputType::kDeleteContentBackward,
+ TargetRangesForInputEvent(*target));
+
+ // Frame could have been destroyed by the beforeinput event.
+ if (!IsAvailable())
+ return false;
+ }
+
+ TypingCommand::DeleteSelection(GetDocument());
+
+ // Frame could have been destroyed by the input event.
+ return IsAvailable();
+}
+
+bool InputMethodController::MoveCaret(int new_caret_position) {
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ PlainTextRange selected_range =
+ CreateRangeForSelection(new_caret_position, new_caret_position, 0);
+ if (selected_range.IsNull())
+ return false;
+ return SetEditableSelectionOffsets(selected_range);
+}
+
+void InputMethodController::ExtendSelectionAndDelete(int before, int after) {
+ if (!GetEditor().CanEdit())
+ return;
+ PlainTextRange selection_offsets(GetSelectionOffsets());
+ if (selection_offsets.IsNull())
+ return;
+
+ // A common call of before=1 and after=0 will fail if the last character
+ // is multi-code-word UTF-16, including both multi-16bit code-points and
+ // Unicode combining character sequences of multiple single-16bit code-
+ // points (officially called "compositions"). Try more until success.
+ // http://crbug.com/355995
+ //
+ // FIXME: Note that this is not an ideal solution when this function is
+ // called to implement "backspace". In that case, there should be some call
+ // that will not delete a full multi-code-point composition but rather
+ // only the last code-point so that it's possible for a user to correct
+ // a composition without starting it from the beginning.
+ // http://crbug.com/37993
+ do {
+ if (!SetSelectionOffsets(PlainTextRange(
+ std::max(static_cast<int>(selection_offsets.Start()) - before, 0),
+ selection_offsets.End() + after)))
+ return;
+ if (before == 0)
+ break;
+ ++before;
+ } while (GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .Start() == GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .End() &&
+ before <= static_cast<int>(selection_offsets.Start()));
+ // TODO(chongz): Find a way to distinguish Forward and Backward.
+ ignore_result(DeleteSelection());
+}
+
+// TODO(yabinh): We should reduce the number of selectionchange events.
+void InputMethodController::DeleteSurroundingText(int before, int after) {
+ if (!GetEditor().CanEdit())
+ return;
+ const PlainTextRange selection_offsets(GetSelectionOffsets());
+ if (selection_offsets.IsNull())
+ return;
+ Element* const root_editable_element =
+ GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .RootEditableElement();
+ if (!root_editable_element)
+ return;
+ int selection_start = static_cast<int>(selection_offsets.Start());
+ int selection_end = static_cast<int>(selection_offsets.End());
+
+ // Select the text to be deleted before SelectionState::kStart.
+ if (before > 0 && selection_start > 0) {
+ // In case of exceeding the left boundary.
+ const int start = std::max(selection_start - before, 0);
+
+ const EphemeralRange& range =
+ PlainTextRange(0, start).CreateRange(*root_editable_element);
+ if (range.IsNull())
+ return;
+ const Position& position = range.EndPosition();
+
+ // Adjust the start of selection for multi-code text(a grapheme cluster
+ // contains more than one code point). TODO(yabinh): Adjustment should be
+ // based on code point instead of grapheme cluster.
+ const size_t diff = ComputeDistanceToLeftGraphemeBoundary(position);
+ const int adjusted_start = start - static_cast<int>(diff);
+ if (!SetSelectionOffsets(PlainTextRange(adjusted_start, selection_start)))
+ return;
+ if (!DeleteSelection())
+ return;
+
+ selection_end = selection_end - (selection_start - adjusted_start);
+ selection_start = adjusted_start;
+ }
+
+ // Select the text to be deleted after SelectionState::kEnd.
+ if (after > 0) {
+ // Adjust the deleted range in case of exceeding the right boundary.
+ const PlainTextRange range(0, selection_end + after);
+ if (range.IsNull())
+ return;
+ const EphemeralRange& valid_range =
+ range.CreateRange(*root_editable_element);
+ if (valid_range.IsNull())
+ return;
+ const int end =
+ PlainTextRange::Create(*root_editable_element, valid_range).End();
+ const Position& position = valid_range.EndPosition();
+
+ // Adjust the end of selection for multi-code text. TODO(yabinh): Adjustment
+ // should be based on code point instead of grapheme cluster.
+ const size_t diff = ComputeDistanceToRightGraphemeBoundary(position);
+ const int adjusted_end = end + static_cast<int>(diff);
+ if (!SetSelectionOffsets(PlainTextRange(selection_end, adjusted_end)))
+ return;
+ if (!DeleteSelection())
+ return;
+ }
+
+ SetSelectionOffsets(PlainTextRange(selection_start, selection_end));
+}
+
+void InputMethodController::DeleteSurroundingTextInCodePoints(int before,
+ int after) {
+ DCHECK_GE(before, 0);
+ DCHECK_GE(after, 0);
+ if (!GetEditor().CanEdit())
+ return;
+ const PlainTextRange selection_offsets(GetSelectionOffsets());
+ if (selection_offsets.IsNull())
+ return;
+ Element* const root_editable_element =
+ GetFrame().Selection().RootEditableElementOrDocumentElement();
+ if (!root_editable_element)
+ return;
+
+ const TextIteratorBehavior& behavior =
+ TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(true)
+ .Build();
+ const String& text = PlainText(
+ EphemeralRange::RangeOfContents(*root_editable_element), behavior);
+
+ // 8-bit characters are Latin-1 characters, so the deletion lengths are
+ // trivial.
+ if (text.Is8Bit())
+ return DeleteSurroundingText(before, after);
+
+ const int selection_start = static_cast<int>(selection_offsets.Start());
+ const int selection_end = static_cast<int>(selection_offsets.End());
+
+ const int before_length =
+ CalculateBeforeDeletionLengthsInCodePoints(text, before, selection_start);
+ if (IsInvalidDeletionLength(before_length))
+ return;
+ const int after_length =
+ CalculateAfterDeletionLengthsInCodePoints(text, after, selection_end);
+ if (IsInvalidDeletionLength(after_length))
+ return;
+
+ return DeleteSurroundingText(before_length, after_length);
+}
+
+WebTextInputInfo InputMethodController::TextInputInfo() const {
+ WebTextInputInfo info;
+ if (!IsAvailable())
+ return info;
+
+ if (!GetFrame().Selection().IsAvailable()) {
+ // plugins/mouse-capture-inside-shadow.html reaches here.
+ return info;
+ }
+ Element* element = RootEditableElementOfSelection(GetFrame().Selection());
+ if (!element)
+ return info;
+
+ info.input_mode = InputModeOfFocusedElement();
+ info.type = TextInputType();
+ info.flags = TextInputFlags();
+ if (info.type == kWebTextInputTypeNone)
+ return info;
+
+ if (!GetFrame().GetEditor().CanEdit())
+ return info;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ GetDocument().Lifecycle());
+
+ // Emits an object replacement character for each replaced element so that
+ // it is exposed to IME and thus could be deleted by IME on android.
+ info.value = PlainText(EphemeralRange::RangeOfContents(*element),
+ TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(true)
+ .SetEmitsSpaceForNbsp(true)
+ .Build());
+
+ if (info.value.IsEmpty())
+ return info;
+
+ EphemeralRange first_range = FirstEphemeralRangeOf(
+ GetFrame().Selection().ComputeVisibleSelectionInDOMTreeDeprecated());
+ PlainTextRange selection_plain_text_range =
+ PlainTextRangeForEphemeralRange(first_range).second;
+ if (selection_plain_text_range.IsNotNull()) {
+ info.selection_start = selection_plain_text_range.Start();
+ info.selection_end = selection_plain_text_range.End();
+ }
+
+ EphemeralRange range = CompositionEphemeralRange();
+ PlainTextRange composition_plain_text_range =
+ PlainTextRangeForEphemeralRange(range).second;
+ if (composition_plain_text_range.IsNotNull()) {
+ info.composition_start = composition_plain_text_range.Start();
+ info.composition_end = composition_plain_text_range.End();
+ }
+
+ return info;
+}
+
+int InputMethodController::TextInputFlags() const {
+ Element* element = GetDocument().FocusedElement();
+ if (!element)
+ return kWebTextInputFlagNone;
+
+ int flags = 0;
+
+ const AtomicString& autocomplete =
+ element->getAttribute(HTMLNames::autocompleteAttr);
+ if (autocomplete == "on")
+ flags |= kWebTextInputFlagAutocompleteOn;
+ else if (autocomplete == "off")
+ flags |= kWebTextInputFlagAutocompleteOff;
+
+ const AtomicString& autocorrect =
+ element->getAttribute(HTMLNames::autocorrectAttr);
+ if (autocorrect == "on")
+ flags |= kWebTextInputFlagAutocorrectOn;
+ else if (autocorrect == "off")
+ flags |= kWebTextInputFlagAutocorrectOff;
+
+ SpellcheckAttributeState spellcheck = element->GetSpellcheckAttributeState();
+ if (spellcheck == kSpellcheckAttributeTrue)
+ flags |= kWebTextInputFlagSpellcheckOn;
+ else if (spellcheck == kSpellcheckAttributeFalse)
+ flags |= kWebTextInputFlagSpellcheckOff;
+
+ flags |= ComputeAutocapitalizeFlags(element);
+
+ if (HTMLInputElement* input = ToHTMLInputElementOrNull(element)) {
+ if (input->HasBeenPasswordField())
+ flags |= kWebTextInputFlagHasBeenPasswordField;
+ }
+
+ return flags;
+}
+
+int InputMethodController::ComputeWebTextInputNextPreviousFlags() const {
+ if (!IsAvailable())
+ return kWebTextInputFlagNone;
+
+ Element* const element = GetDocument().FocusedElement();
+ if (!element)
+ return kWebTextInputFlagNone;
+
+ Page* page = GetDocument().GetPage();
+ if (!page)
+ return kWebTextInputFlagNone;
+
+ int flags = kWebTextInputFlagNone;
+ if (page->GetFocusController().NextFocusableElementInForm(
+ element, kWebFocusTypeForward))
+ flags |= kWebTextInputFlagHaveNextFocusableElement;
+
+ if (page->GetFocusController().NextFocusableElementInForm(
+ element, kWebFocusTypeBackward))
+ flags |= kWebTextInputFlagHavePreviousFocusableElement;
+
+ return flags;
+}
+
+WebTextInputMode InputMethodController::InputModeOfFocusedElement() const {
+ if (!RuntimeEnabledFeatures::InputModeAttributeEnabled())
+ return kWebTextInputModeDefault;
+
+ AtomicString mode = GetInputModeAttribute(GetDocument().FocusedElement());
+
+ if (mode.IsEmpty())
+ return kWebTextInputModeDefault;
+ if (mode == InputModeNames::none)
+ return kWebTextInputModeNone;
+ if (mode == InputModeNames::text)
+ return kWebTextInputModeText;
+ if (mode == InputModeNames::tel)
+ return kWebTextInputModeTel;
+ if (mode == InputModeNames::url)
+ return kWebTextInputModeUrl;
+ if (mode == InputModeNames::email)
+ return kWebTextInputModeEmail;
+ if (mode == InputModeNames::numeric)
+ return kWebTextInputModeNumeric;
+ if (mode == InputModeNames::decimal)
+ return kWebTextInputModeDecimal;
+ if (mode == InputModeNames::search)
+ return kWebTextInputModeSearch;
+ return kWebTextInputModeDefault;
+}
+
+WebTextInputType InputMethodController::TextInputType() const {
+ if (!GetFrame().Selection().IsAvailable()) {
+ // "mouse-capture-inside-shadow.html" reaches here.
+ return kWebTextInputTypeNone;
+ }
+
+ // It's important to preserve the equivalence of textInputInfo().type and
+ // textInputType(), so perform the same rootEditableElement() existence check
+ // here for consistency.
+ if (!RootEditableElementOfSelection(GetFrame().Selection()))
+ return kWebTextInputTypeNone;
+
+ if (!IsAvailable())
+ return kWebTextInputTypeNone;
+
+ Element* element = GetDocument().FocusedElement();
+ if (!element)
+ return kWebTextInputTypeNone;
+
+ if (auto* input = ToHTMLInputElementOrNull(*element)) {
+ const AtomicString& type = input->type();
+
+ if (input->IsDisabledOrReadOnly())
+ return kWebTextInputTypeNone;
+
+ if (type == InputTypeNames::password)
+ return kWebTextInputTypePassword;
+ if (type == InputTypeNames::search)
+ return kWebTextInputTypeSearch;
+ if (type == InputTypeNames::email)
+ return kWebTextInputTypeEmail;
+ if (type == InputTypeNames::number)
+ return kWebTextInputTypeNumber;
+ if (type == InputTypeNames::tel)
+ return kWebTextInputTypeTelephone;
+ if (type == InputTypeNames::url)
+ return kWebTextInputTypeURL;
+ if (type == InputTypeNames::text)
+ return kWebTextInputTypeText;
+
+ return kWebTextInputTypeNone;
+ }
+
+ if (auto* textarea = ToHTMLTextAreaElementOrNull(*element)) {
+ if (textarea->IsDisabledOrReadOnly())
+ return kWebTextInputTypeNone;
+ return kWebTextInputTypeTextArea;
+ }
+
+ if (element->IsHTMLElement()) {
+ if (ToHTMLElement(element)->IsDateTimeFieldElement())
+ return kWebTextInputTypeDateTimeField;
+ }
+
+ GetDocument().UpdateStyleAndLayoutTree();
+ if (HasEditableStyle(*element))
+ return kWebTextInputTypeContentEditable;
+
+ return kWebTextInputTypeNone;
+}
+
+void InputMethodController::WillChangeFocus() {
+ FinishComposingText(kKeepSelection);
+}
+
+void InputMethodController::Trace(blink::Visitor* visitor) {
+ visitor->Trace(frame_);
+ visitor->Trace(composition_range_);
+ DocumentShutdownObserver::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/ime/input_method_controller.h b/chromium/third_party/blink/renderer/core/editing/ime/input_method_controller.h
new file mode 100644
index 00000000000..83f542d464a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/ime/input_method_controller.h
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_IME_INPUT_METHOD_CONTROLLER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_IME_INPUT_METHOD_CONTROLLER_H_
+
+#include "base/gtest_prod_util.h"
+#include "base/macros.h"
+#include "third_party/blink/public/platform/web_text_input_info.h"
+#include "third_party/blink/public/platform/web_text_input_type.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/dom/document_shutdown_observer.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/ime/ime_text_span.h"
+#include "third_party/blink/renderer/core/editing/plain_text_range.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/wtf/vector.h"
+
+namespace blink {
+
+class Editor;
+class LocalFrame;
+class Range;
+enum class TypingContinuation;
+
+class CORE_EXPORT InputMethodController final
+ : public GarbageCollectedFinalized<InputMethodController>,
+ public DocumentShutdownObserver {
+ USING_GARBAGE_COLLECTED_MIXIN(InputMethodController);
+
+ public:
+ enum ConfirmCompositionBehavior {
+ kDoNotKeepSelection,
+ kKeepSelection,
+ };
+
+ static InputMethodController* Create(LocalFrame&);
+ virtual ~InputMethodController();
+ void Trace(blink::Visitor*);
+
+ // international text input composition
+ bool HasComposition() const;
+ void SetComposition(const String& text,
+ const Vector<ImeTextSpan>& ime_text_spans,
+ int selection_start,
+ int selection_end);
+ void SetCompositionFromExistingText(const Vector<ImeTextSpan>& ime_text_spans,
+ unsigned composition_start,
+ unsigned composition_end);
+
+ // Deletes ongoing composing text if any, inserts specified text, and
+ // changes the selection according to relativeCaretPosition, which is
+ // relative to the end of the inserting text.
+ bool CommitText(const String& text,
+ const Vector<ImeTextSpan>& ime_text_spans,
+ int relative_caret_position);
+
+ // Inserts ongoing composing text; changes the selection to the end of
+ // the inserting text if DoNotKeepSelection, or holds the selection if
+ // KeepSelection.
+ bool FinishComposingText(ConfirmCompositionBehavior);
+
+ // Deletes the existing composition text.
+ void CancelComposition();
+
+ EphemeralRange CompositionEphemeralRange() const;
+
+ void Clear();
+ void DocumentAttached(Document*);
+
+ PlainTextRange GetSelectionOffsets() const;
+ // Returns true if setting selection to specified offsets, otherwise false.
+ bool SetEditableSelectionOffsets(const PlainTextRange&);
+ void ExtendSelectionAndDelete(int before, int after);
+ PlainTextRange CreateRangeForSelection(int start,
+ int end,
+ size_t text_length) const;
+ void DeleteSurroundingText(int before, int after);
+ void DeleteSurroundingTextInCodePoints(int before, int after);
+ WebTextInputInfo TextInputInfo() const;
+ // For finding NEXT/PREVIOUS everytime during frame update is a costly
+ // operation, so making it specific whenever needed by splitting from
+ // TextInputFlags()
+ int ComputeWebTextInputNextPreviousFlags() const;
+ WebTextInputType TextInputType() const;
+
+ // Call this when we will change focus.
+ void WillChangeFocus();
+
+ private:
+ friend class InputMethodControllerTest;
+
+ Document& GetDocument() const;
+ bool IsAvailable() const;
+
+ Member<LocalFrame> frame_;
+ Member<Range> composition_range_;
+ bool has_composition_;
+
+ explicit InputMethodController(LocalFrame&);
+
+ Editor& GetEditor() const;
+ LocalFrame& GetFrame() const {
+ DCHECK(frame_);
+ return *frame_;
+ }
+
+ String ComposingText() const;
+ void SelectComposition() const;
+
+ EphemeralRange EphemeralRangeForOffsets(const PlainTextRange&) const;
+
+ // Returns true if selection offsets were successfully set.
+ bool SetSelectionOffsets(const PlainTextRange&);
+
+ void AddImeTextSpans(const Vector<ImeTextSpan>& ime_text_spans,
+ ContainerNode* base_element,
+ unsigned offset_in_plain_chars);
+
+ bool InsertText(const String&);
+ bool InsertTextAndMoveCaret(const String&,
+ int relative_caret_position,
+ const Vector<ImeTextSpan>& ime_text_spans);
+
+ // Inserts the given text string in the place of the existing composition.
+ // Returns true if did replace.
+ bool ReplaceComposition(const String& text) WARN_UNUSED_RESULT;
+ // Inserts the given text string in the place of the existing composition
+ // and moves caret. Returns true if did replace and moved caret successfully.
+ bool ReplaceCompositionAndMoveCaret(
+ const String&,
+ int relative_caret_position,
+ const Vector<ImeTextSpan>& ime_text_spans);
+
+ // Returns false if the frame was destroyed, true otherwise.
+ bool DeleteSelection() WARN_UNUSED_RESULT;
+
+ // Returns true if moved caret successfully.
+ bool MoveCaret(int new_caret_position);
+
+ // Returns false if the frame is destroyed, true otherwise.
+ bool DispatchCompositionStartEvent(const String& text) WARN_UNUSED_RESULT;
+
+ PlainTextRange CreateSelectionRangeForSetComposition(
+ int selection_start,
+ int selection_end,
+ size_t text_length) const;
+ int TextInputFlags() const;
+ WebTextInputMode InputModeOfFocusedElement() const;
+
+ // Implements |DocumentShutdownObserver|.
+ void ContextDestroyed(Document*) final;
+
+ enum class TypingContinuation;
+
+ // Returns true if setting selection to specified offsets, otherwise false.
+ bool SetEditableSelectionOffsets(const PlainTextRange&, TypingContinuation);
+
+ // Returns true if selection offsets were successfully set.
+ bool SetSelectionOffsets(const PlainTextRange&, TypingContinuation);
+
+ FRIEND_TEST_ALL_PREFIXES(InputMethodControllerTest,
+ InputModeOfFocusedElement);
+
+ DISALLOW_COPY_AND_ASSIGN(InputMethodController);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_IME_INPUT_METHOD_CONTROLLER_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/ime/input_method_controller_test.cc b/chromium/third_party/blink/renderer/core/editing/ime/input_method_controller_test.cc
new file mode 100644
index 00000000000..700b09a46ad
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/ime/input_method_controller_test.cc
@@ -0,0 +1,3234 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/ime/input_method_controller.h"
+
+#include <memory>
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/node_list.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/events/mouse_event.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
+#include "third_party/blink/renderer/core/html/forms/html_text_area_element.h"
+
+using ui::mojom::ImeTextSpanThickness;
+
+namespace blink {
+
+class InputMethodControllerTest : public EditingTestBase {
+ protected:
+ InputMethodController& Controller() {
+ return GetFrame().GetInputMethodController();
+ }
+
+ // TODO(editing-dev): We should use |CompositionEphemeralRange()| instead
+ // of having |GetCompositionRange()| and marking |InputMethodControllerTest|
+ // as friend class.
+ Range* GetCompositionRange() { return Controller().composition_range_; }
+
+ Element* InsertHTMLElement(const char* element_code, const char* element_id);
+ void CreateHTMLWithCompositionInputEventListeners();
+ void CreateHTMLWithCompositionEndEventListener(const SelectionType);
+};
+
+Element* InputMethodControllerTest::InsertHTMLElement(const char* element_code,
+ const char* element_id) {
+ GetDocument().write(element_code);
+ GetDocument().UpdateStyleAndLayout();
+ Element* element = GetElementById(element_id);
+ element->focus();
+ return element;
+}
+
+void InputMethodControllerTest::CreateHTMLWithCompositionInputEventListeners() {
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* editable =
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('beforeinput', "
+ " event => document.title = `beforeinput.data:${event.data};`);"
+ "document.getElementById('sample').addEventListener('input', "
+ " event => document.title += `input.data:${event.data};`);"
+ "document.getElementById('sample').addEventListener('compositionend', "
+ " event => document.title += `compositionend.data:${event.data};`);");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+ editable->focus();
+}
+
+void InputMethodControllerTest::CreateHTMLWithCompositionEndEventListener(
+ const SelectionType type) {
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* editable =
+ InsertHTMLElement("<div id='sample' contentEditable></div>", "sample");
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+
+ switch (type) {
+ case kNoSelection:
+ script->SetInnerHTMLFromString(
+ // If the caret position is set before firing 'compositonend' event
+ // (and it should), the final caret position will be reset to null.
+ "document.getElementById('sample').addEventListener('compositionend',"
+ " event => getSelection().removeAllRanges());");
+ break;
+ case kCaretSelection:
+ script->SetInnerHTMLFromString(
+ // If the caret position is set before firing 'compositonend' event
+ // (and it should), the final caret position will be reset to [3,3].
+ "document.getElementById('sample').addEventListener('compositionend',"
+ " event => {"
+ " const node = document.getElementById('sample').firstChild;"
+ " getSelection().collapse(node, 3);"
+ "});");
+ break;
+ case kRangeSelection:
+ script->SetInnerHTMLFromString(
+ // If the caret position is set before firing 'compositonend' event
+ // (and it should), the final caret position will be reset to [2,4].
+ "document.getElementById('sample').addEventListener('compositionend',"
+ " event => {"
+ " const node = document.getElementById('sample').firstChild;"
+ " const selection = getSelection();"
+ " selection.collapse(node, 2);"
+ " selection.extend(node, 4);"
+ "});");
+ break;
+ default:
+ NOTREACHED();
+ }
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+ editable->focus();
+}
+
+TEST_F(InputMethodControllerTest, BackspaceFromEndOfInput) {
+ HTMLInputElement* input =
+ ToHTMLInputElement(InsertHTMLElement("<input id='sample'>", "sample"));
+
+ input->setValue("fooX");
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(4, 4));
+ EXPECT_STREQ("fooX", input->value().Utf8().data());
+ Controller().ExtendSelectionAndDelete(0, 0);
+ EXPECT_STREQ("fooX", input->value().Utf8().data());
+
+ input->setValue("fooX");
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(4, 4));
+ EXPECT_STREQ("fooX", input->value().Utf8().data());
+ Controller().ExtendSelectionAndDelete(1, 0);
+ EXPECT_STREQ("foo", input->value().Utf8().data());
+
+ input->setValue(
+ String::FromUTF8("foo\xE2\x98\x85")); // U+2605 == "black star"
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(4, 4));
+ EXPECT_STREQ("foo\xE2\x98\x85", input->value().Utf8().data());
+ Controller().ExtendSelectionAndDelete(1, 0);
+ EXPECT_STREQ("foo", input->value().Utf8().data());
+
+ input->setValue(
+ String::FromUTF8("foo\xF0\x9F\x8F\x86")); // U+1F3C6 == "trophy"
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(4, 4));
+ EXPECT_STREQ("foo\xF0\x9F\x8F\x86", input->value().Utf8().data());
+ Controller().ExtendSelectionAndDelete(1, 0);
+ EXPECT_STREQ("foo", input->value().Utf8().data());
+
+ // composed U+0E01 "ka kai" + U+0E49 "mai tho"
+ input->setValue(String::FromUTF8("foo\xE0\xB8\x81\xE0\xB9\x89"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(4, 4));
+ EXPECT_STREQ("foo\xE0\xB8\x81\xE0\xB9\x89", input->value().Utf8().data());
+ Controller().ExtendSelectionAndDelete(1, 0);
+ EXPECT_STREQ("foo", input->value().Utf8().data());
+
+ input->setValue("fooX");
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(4, 4));
+ EXPECT_STREQ("fooX", input->value().Utf8().data());
+ Controller().ExtendSelectionAndDelete(0, 1);
+ EXPECT_STREQ("fooX", input->value().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, SetCompositionFromExistingText) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>hello world</div>", "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 5,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().SetCompositionFromExistingText(ime_text_spans, 0, 5);
+
+ Range* range = GetCompositionRange();
+ EXPECT_EQ(0u, range->startOffset());
+ EXPECT_EQ(5u, range->endOffset());
+
+ PlainTextRange plain_text_range(PlainTextRange::Create(*div, *range));
+ EXPECT_EQ(0u, plain_text_range.Start());
+ EXPECT_EQ(5u, plain_text_range.End());
+}
+
+TEST_F(InputMethodControllerTest, SetCompositionAfterEmoji) {
+ // "trophy" = U+1F3C6 = 0xF0 0x9F 0x8F 0x86 (UTF8).
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>&#x1f3c6</div>", "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 2,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(2, 2));
+ EXPECT_EQ(2, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+ EXPECT_EQ(2, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Extent()
+ .ComputeOffsetInContainerNode());
+
+ Controller().SetComposition(String("a"), ime_text_spans, 1, 1);
+ EXPECT_STREQ("\xF0\x9F\x8F\x86\x61", div->innerText().Utf8().data());
+
+ Controller().SetComposition(String("ab"), ime_text_spans, 2, 2);
+ EXPECT_STREQ("\xF0\x9F\x8F\x86\x61\x62", div->innerText().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, SetCompositionWithGraphemeCluster) {
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 6, 6,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ GetDocument().UpdateStyleAndLayout();
+
+ // UTF16 = 0x0939 0x0947 0x0932 0x0932. Note that 0x0932 0x0932 is a grapheme
+ // cluster.
+ Controller().SetComposition(
+ String::FromUTF8("\xE0\xA4\xB9\xE0\xA5\x87\xE0\xA4\xB2\xE0\xA4\xB2"),
+ ime_text_spans, 4, 4);
+ EXPECT_EQ(4u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(4u, Controller().GetSelectionOffsets().End());
+
+ // UTF16 = 0x0939 0x0947 0x0932 0x094D 0x0932 0x094B.
+ Controller().SetComposition(
+ String::FromUTF8("\xE0\xA4\xB9\xE0\xA5\x87\xE0\xA4\xB2\xE0\xA5\x8D\xE0"
+ "\xA4\xB2\xE0\xA5\x8B"),
+ ime_text_spans, 6, 6);
+ EXPECT_EQ(6u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(6u, Controller().GetSelectionOffsets().End());
+}
+
+TEST_F(InputMethodControllerTest,
+ SetCompositionWithGraphemeClusterAndMultipleNodes) {
+ Element* div =
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 12, 12,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ GetDocument().UpdateStyleAndLayout();
+
+ // UTF16 = 0x0939 0x0947 0x0932 0x094D 0x0932 0x094B. 0x0939 0x0947 0x0932 is
+ // a grapheme cluster, so is the remainding 0x0932 0x094B.
+ Controller().CommitText(
+ String::FromUTF8("\xE0\xA4\xB9\xE0\xA5\x87\xE0\xA4\xB2\xE0\xA5\x8D\xE0"
+ "\xA4\xB2\xE0\xA5\x8B"),
+ ime_text_spans, 1);
+ Controller().CommitText("\nab ", ime_text_spans, 1);
+ Controller().SetComposition(String("c"), ime_text_spans, 1, 1);
+ EXPECT_STREQ(
+ "\xE0\xA4\xB9\xE0\xA5\x87\xE0\xA4\xB2\xE0\xA5\x8D\xE0\xA4\xB2\xE0\xA5"
+ "\x8B\nab c",
+ div->innerText().Utf8().data());
+ EXPECT_EQ(11u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(11u, Controller().GetSelectionOffsets().End());
+
+ Controller().SetComposition(String("cd"), ime_text_spans, 2, 2);
+ EXPECT_STREQ(
+ "\xE0\xA4\xB9\xE0\xA5\x87\xE0\xA4\xB2\xE0\xA5\x8D\xE0\xA4\xB2\xE0\xA5"
+ "\x8B\nab cd",
+ div->innerText().Utf8().data());
+ EXPECT_EQ(12u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(12u, Controller().GetSelectionOffsets().End());
+}
+
+TEST_F(InputMethodControllerTest, SetCompositionKeepingStyle) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' "
+ "contenteditable>abc1<b>2</b>34567<b>8</b>9d<b>e</b>f</div>",
+ "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 3, 12,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().SetCompositionFromExistingText(ime_text_spans, 3, 12);
+
+ // Subtract a character.
+ Controller().SetComposition(String("12345789"), ime_text_spans, 8, 8);
+ EXPECT_STREQ("abc1<b>2</b>3457<b>8</b>9d<b>e</b>f",
+ div->InnerHTMLAsString().Utf8().data());
+ EXPECT_EQ(11u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(11u, Controller().GetSelectionOffsets().End());
+
+ // Append a character.
+ Controller().SetComposition(String("123456789"), ime_text_spans, 9, 9);
+ EXPECT_STREQ("abc1<b>2</b>34567<b>8</b>9d<b>e</b>f",
+ div->InnerHTMLAsString().Utf8().data());
+ EXPECT_EQ(12u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(12u, Controller().GetSelectionOffsets().End());
+
+ // Subtract and append characters.
+ Controller().SetComposition(String("123hello789"), ime_text_spans, 11, 11);
+ EXPECT_STREQ("abc1<b>2</b>3hello7<b>8</b>9d<b>e</b>f",
+ div->InnerHTMLAsString().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, SetCompositionWithEmojiKeepingStyle) {
+ // U+1F3E0 = 0xF0 0x9F 0x8F 0xA0 (UTF8). It's an emoji character.
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable><b>&#x1f3e0</b></div>", "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 2,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+
+ Controller().SetCompositionFromExistingText(ime_text_spans, 0, 2);
+
+ // 0xF0 0x9F 0x8F 0xAB is also an emoji character, with the same leading
+ // surrogate pair to the previous one.
+ Controller().SetComposition(String::FromUTF8("\xF0\x9F\x8F\xAB"),
+ ime_text_spans, 2, 2);
+ EXPECT_STREQ("<b>\xF0\x9F\x8F\xAB</b>",
+ div->InnerHTMLAsString().Utf8().data());
+
+ Controller().SetComposition(String::FromUTF8("\xF0\x9F\x8F\xA0"),
+ ime_text_spans, 2, 2);
+ EXPECT_STREQ("<b>\xF0\x9F\x8F\xA0</b>",
+ div->InnerHTMLAsString().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest,
+ SetCompositionWithTeluguSignVisargaKeepingStyle) {
+ // U+0C03 = 0xE0 0xB0 0x83 (UTF8), a telugu sign visarga with one code point.
+ // It's one grapheme cluster if separated. It can also form one grapheme
+ // cluster with another code point(e.g, itself).
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable><b>&#xc03</b></div>", "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 2,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().SetCompositionFromExistingText(ime_text_spans, 0, 1);
+
+ // 0xE0 0xB0 0x83 0xE0 0xB0 0x83, a telugu character with 2 code points in
+ // 1 grapheme cluster.
+ Controller().SetComposition(String::FromUTF8("\xE0\xB0\x83\xE0\xB0\x83"),
+ ime_text_spans, 2, 2);
+ EXPECT_STREQ("<b>\xE0\xB0\x83\xE0\xB0\x83</b>",
+ div->InnerHTMLAsString().Utf8().data());
+
+ Controller().SetComposition(String::FromUTF8("\xE0\xB0\x83"), ime_text_spans,
+ 1, 1);
+ EXPECT_STREQ("<b>\xE0\xB0\x83</b>", div->InnerHTMLAsString().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, FinishComposingTextKeepingStyle) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' "
+ "contenteditable>abc1<b>2</b>34567<b>8</b>9</div>",
+ "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 3, 12,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().SetCompositionFromExistingText(ime_text_spans, 3, 12);
+
+ Controller().SetComposition(String("123hello789"), ime_text_spans, 11, 11);
+ EXPECT_STREQ("abc1<b>2</b>3hello7<b>8</b>9",
+ div->InnerHTMLAsString().Utf8().data());
+
+ Controller().FinishComposingText(InputMethodController::kKeepSelection);
+ EXPECT_STREQ("abc1<b>2</b>3hello7<b>8</b>9",
+ div->InnerHTMLAsString().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, CommitTextKeepingStyle) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' "
+ "contenteditable>abc1<b>2</b>34567<b>8</b>9</div>",
+ "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 3, 12,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().SetCompositionFromExistingText(ime_text_spans, 3, 12);
+
+ Controller().CommitText(String("123789"), ime_text_spans, 0);
+ EXPECT_STREQ("abc1<b>2</b>37<b>8</b>9",
+ div->InnerHTMLAsString().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, InsertTextWithNewLine) {
+ Element* div =
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 11,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+
+ Controller().CommitText(String("hello\nworld"), ime_text_spans, 0);
+ EXPECT_STREQ("hello<div>world</div>", div->InnerHTMLAsString().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, InsertTextWithNewLineIncrementally) {
+ Element* div =
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ Controller().CommitText("a", ime_text_spans, 0);
+ Controller().SetComposition("bcd", ime_text_spans, 0, 2);
+ EXPECT_STREQ("abcd", div->InnerHTMLAsString().Utf8().data());
+
+ Controller().CommitText(String("bcd\nefgh\nijkl"), ime_text_spans, 0);
+ EXPECT_STREQ("abcd<div>efgh</div><div>ijkl</div>",
+ div->InnerHTMLAsString().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, SelectionOnConfirmExistingText) {
+ InsertHTMLElement("<div id='sample' contenteditable>hello world</div>",
+ "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 5,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().SetCompositionFromExistingText(ime_text_spans, 0, 5);
+
+ Controller().FinishComposingText(InputMethodController::kKeepSelection);
+ EXPECT_EQ(0, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+ EXPECT_EQ(0, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Extent()
+ .ComputeOffsetInContainerNode());
+}
+
+TEST_F(InputMethodControllerTest, DeleteBySettingEmptyComposition) {
+ HTMLInputElement* input =
+ ToHTMLInputElement(InsertHTMLElement("<input id='sample'>", "sample"));
+
+ input->setValue("foo ");
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(4, 4));
+ EXPECT_STREQ("foo ", input->value().Utf8().data());
+ Controller().ExtendSelectionAndDelete(0, 0);
+ EXPECT_STREQ("foo ", input->value().Utf8().data());
+
+ input->setValue("foo ");
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(4, 4));
+ EXPECT_STREQ("foo ", input->value().Utf8().data());
+ Controller().ExtendSelectionAndDelete(1, 0);
+ EXPECT_STREQ("foo", input->value().Utf8().data());
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 3,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().SetCompositionFromExistingText(ime_text_spans, 0, 3);
+
+ Controller().SetComposition(String(""), ime_text_spans, 0, 3);
+
+ EXPECT_STREQ("", input->value().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest,
+ SetCompositionFromExistingTextWithCollapsedWhiteSpace) {
+ // Creates a div with one leading new line char. The new line char is hidden
+ // from the user and IME, but is visible to InputMethodController.
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>\nhello world</div>", "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 5,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().SetCompositionFromExistingText(ime_text_spans, 0, 5);
+
+ Range* range = GetCompositionRange();
+ EXPECT_EQ(1u, range->startOffset());
+ EXPECT_EQ(6u, range->endOffset());
+
+ PlainTextRange plain_text_range(PlainTextRange::Create(*div, *range));
+ EXPECT_EQ(0u, plain_text_range.Start());
+ EXPECT_EQ(5u, plain_text_range.End());
+}
+
+TEST_F(InputMethodControllerTest,
+ SetCompositionFromExistingTextWithInvalidOffsets) {
+ InsertHTMLElement("<div id='sample' contenteditable>test</div>", "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 7, 8,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().SetCompositionFromExistingText(ime_text_spans, 7, 8);
+
+ EXPECT_FALSE(GetCompositionRange());
+}
+
+TEST_F(InputMethodControllerTest, ConfirmPasswordComposition) {
+ HTMLInputElement* input = ToHTMLInputElement(InsertHTMLElement(
+ "<input id='sample' type='password' size='24'>", "sample"));
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 5,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().SetComposition("foo", ime_text_spans, 0, 3);
+ Controller().FinishComposingText(InputMethodController::kKeepSelection);
+
+ EXPECT_STREQ("foo", input->value().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, DeleteSurroundingTextWithEmptyText) {
+ HTMLInputElement* input =
+ ToHTMLInputElement(InsertHTMLElement("<input id='sample'>", "sample"));
+
+ input->setValue("");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("", input->value().Utf8().data());
+ Controller().DeleteSurroundingText(0, 0);
+ EXPECT_STREQ("", input->value().Utf8().data());
+
+ input->setValue("");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("", input->value().Utf8().data());
+ Controller().DeleteSurroundingText(1, 0);
+ EXPECT_STREQ("", input->value().Utf8().data());
+
+ input->setValue("");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("", input->value().Utf8().data());
+ Controller().DeleteSurroundingText(0, 1);
+ EXPECT_STREQ("", input->value().Utf8().data());
+
+ input->setValue("");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("", input->value().Utf8().data());
+ Controller().DeleteSurroundingText(1, 1);
+ EXPECT_STREQ("", input->value().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, DeleteSurroundingTextWithRangeSelection) {
+ HTMLInputElement* input =
+ ToHTMLInputElement(InsertHTMLElement("<input id='sample'>", "sample"));
+
+ input->setValue("hello");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("hello", input->value().Utf8().data());
+ Controller().SetEditableSelectionOffsets(PlainTextRange(1, 4));
+ Controller().DeleteSurroundingText(0, 0);
+ EXPECT_STREQ("hello", input->value().Utf8().data());
+
+ input->setValue("hello");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("hello", input->value().Utf8().data());
+ Controller().SetEditableSelectionOffsets(PlainTextRange(1, 4));
+ Controller().DeleteSurroundingText(1, 1);
+ EXPECT_STREQ("ell", input->value().Utf8().data());
+
+ input->setValue("hello");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("hello", input->value().Utf8().data());
+ Controller().SetEditableSelectionOffsets(PlainTextRange(1, 4));
+ Controller().DeleteSurroundingText(100, 0);
+ EXPECT_STREQ("ello", input->value().Utf8().data());
+
+ input->setValue("hello");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("hello", input->value().Utf8().data());
+ Controller().SetEditableSelectionOffsets(PlainTextRange(1, 4));
+ Controller().DeleteSurroundingText(0, 100);
+ EXPECT_STREQ("hell", input->value().Utf8().data());
+
+ input->setValue("hello");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("hello", input->value().Utf8().data());
+ Controller().SetEditableSelectionOffsets(PlainTextRange(1, 4));
+ Controller().DeleteSurroundingText(100, 100);
+ EXPECT_STREQ("ell", input->value().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, DeleteSurroundingTextWithCursorSelection) {
+ HTMLInputElement* input =
+ ToHTMLInputElement(InsertHTMLElement("<input id='sample'>", "sample"));
+
+ input->setValue("hello");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("hello", input->value().Utf8().data());
+ Controller().SetEditableSelectionOffsets(PlainTextRange(2, 2));
+ Controller().DeleteSurroundingText(1, 0);
+ EXPECT_STREQ("hllo", input->value().Utf8().data());
+
+ input->setValue("hello");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("hello", input->value().Utf8().data());
+ Controller().SetEditableSelectionOffsets(PlainTextRange(2, 2));
+ Controller().DeleteSurroundingText(0, 1);
+ EXPECT_STREQ("helo", input->value().Utf8().data());
+
+ input->setValue("hello");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("hello", input->value().Utf8().data());
+ Controller().SetEditableSelectionOffsets(PlainTextRange(2, 2));
+ Controller().DeleteSurroundingText(0, 0);
+ EXPECT_STREQ("hello", input->value().Utf8().data());
+
+ input->setValue("hello");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("hello", input->value().Utf8().data());
+ Controller().SetEditableSelectionOffsets(PlainTextRange(2, 2));
+ Controller().DeleteSurroundingText(1, 1);
+ EXPECT_STREQ("hlo", input->value().Utf8().data());
+
+ input->setValue("hello");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("hello", input->value().Utf8().data());
+ Controller().SetEditableSelectionOffsets(PlainTextRange(2, 2));
+ Controller().DeleteSurroundingText(100, 0);
+ EXPECT_STREQ("llo", input->value().Utf8().data());
+
+ input->setValue("hello");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("hello", input->value().Utf8().data());
+ Controller().SetEditableSelectionOffsets(PlainTextRange(2, 2));
+ Controller().DeleteSurroundingText(0, 100);
+ EXPECT_STREQ("he", input->value().Utf8().data());
+
+ input->setValue("hello");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("hello", input->value().Utf8().data());
+ Controller().SetEditableSelectionOffsets(PlainTextRange(2, 2));
+ Controller().DeleteSurroundingText(100, 100);
+ EXPECT_STREQ("", input->value().Utf8().data());
+
+ input->setValue("h");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("h", input->value().Utf8().data());
+ Controller().SetEditableSelectionOffsets(PlainTextRange(1, 1));
+ Controller().DeleteSurroundingText(1, 0);
+ EXPECT_STREQ("", input->value().Utf8().data());
+
+ input->setValue("h");
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_STREQ("h", input->value().Utf8().data());
+ Controller().SetEditableSelectionOffsets(PlainTextRange(0, 0));
+ Controller().DeleteSurroundingText(0, 1);
+ EXPECT_STREQ("", input->value().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest,
+ DeleteSurroundingTextWithMultiCodeTextOnTheLeft) {
+ HTMLInputElement* input =
+ ToHTMLInputElement(InsertHTMLElement("<input id='sample'>", "sample"));
+
+ // U+2605 == "black star". It takes up 1 space.
+ input->setValue(String::FromUTF8("foo\xE2\x98\x85"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(4, 4));
+ EXPECT_STREQ("foo\xE2\x98\x85", input->value().Utf8().data());
+ Controller().DeleteSurroundingText(1, 0);
+ EXPECT_STREQ("foo", input->value().Utf8().data());
+
+ // U+1F3C6 == "trophy". It takes up 2 space.
+ input->setValue(String::FromUTF8("foo\xF0\x9F\x8F\x86"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(5, 5));
+ EXPECT_STREQ("foo\xF0\x9F\x8F\x86", input->value().Utf8().data());
+ Controller().DeleteSurroundingText(1, 0);
+ EXPECT_STREQ("foo", input->value().Utf8().data());
+
+ // composed U+0E01 "ka kai" + U+0E49 "mai tho". It takes up 2 space.
+ input->setValue(String::FromUTF8("foo\xE0\xB8\x81\xE0\xB9\x89"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(5, 5));
+ EXPECT_STREQ("foo\xE0\xB8\x81\xE0\xB9\x89", input->value().Utf8().data());
+ Controller().DeleteSurroundingText(1, 0);
+ EXPECT_STREQ("foo", input->value().Utf8().data());
+
+ // "trophy" + "trophy".
+ input->setValue(String::FromUTF8("foo\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(7, 7));
+ EXPECT_STREQ("foo\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86",
+ input->value().Utf8().data());
+ Controller().DeleteSurroundingText(2, 0);
+ EXPECT_STREQ("foo\xF0\x9F\x8F\x86", input->value().Utf8().data());
+
+ // "trophy" + "trophy".
+ input->setValue(String::FromUTF8("foo\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(7, 7));
+ EXPECT_STREQ("foo\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86",
+ input->value().Utf8().data());
+ Controller().DeleteSurroundingText(3, 0);
+ EXPECT_STREQ("foo", input->value().Utf8().data());
+
+ // "trophy" + "trophy".
+ input->setValue(String::FromUTF8("foo\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(7, 7));
+ EXPECT_STREQ("foo\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86",
+ input->value().Utf8().data());
+ Controller().DeleteSurroundingText(4, 0);
+ EXPECT_STREQ("foo", input->value().Utf8().data());
+
+ // "trophy" + "trophy".
+ input->setValue(String::FromUTF8("foo\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(7, 7));
+ EXPECT_STREQ("foo\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86",
+ input->value().Utf8().data());
+ Controller().DeleteSurroundingText(5, 0);
+ EXPECT_STREQ("fo", input->value().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest,
+ DeleteSurroundingTextWithMultiCodeTextOnTheRight) {
+ HTMLInputElement* input =
+ ToHTMLInputElement(InsertHTMLElement("<input id='sample'>", "sample"));
+
+ // U+2605 == "black star". It takes up 1 space.
+ input->setValue(String::FromUTF8("\xE2\x98\x85 foo"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(0, 0));
+ EXPECT_STREQ("\xE2\x98\x85 foo", input->value().Utf8().data());
+ Controller().DeleteSurroundingText(0, 1);
+ EXPECT_STREQ(" foo", input->value().Utf8().data());
+
+ // U+1F3C6 == "trophy". It takes up 2 space.
+ input->setValue(String::FromUTF8("\xF0\x9F\x8F\x86 foo"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(0, 0));
+ EXPECT_STREQ("\xF0\x9F\x8F\x86 foo", input->value().Utf8().data());
+ Controller().DeleteSurroundingText(0, 1);
+ EXPECT_STREQ(" foo", input->value().Utf8().data());
+
+ // composed U+0E01 "ka kai" + U+0E49 "mai tho". It takes up 2 space.
+ input->setValue(String::FromUTF8("\xE0\xB8\x81\xE0\xB9\x89 foo"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(0, 0));
+ EXPECT_STREQ("\xE0\xB8\x81\xE0\xB9\x89 foo", input->value().Utf8().data());
+ Controller().DeleteSurroundingText(0, 1);
+ EXPECT_STREQ(" foo", input->value().Utf8().data());
+
+ // "trophy" + "trophy".
+ input->setValue(String::FromUTF8("\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86 foo"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(0, 0));
+ EXPECT_STREQ("\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86 foo",
+ input->value().Utf8().data());
+ Controller().DeleteSurroundingText(0, 2);
+ EXPECT_STREQ("\xF0\x9F\x8F\x86 foo", input->value().Utf8().data());
+
+ // "trophy" + "trophy".
+ input->setValue(String::FromUTF8("\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86 foo"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(0, 0));
+ EXPECT_STREQ("\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86 foo",
+ input->value().Utf8().data());
+ Controller().DeleteSurroundingText(0, 3);
+ EXPECT_STREQ(" foo", input->value().Utf8().data());
+
+ // "trophy" + "trophy".
+ input->setValue(String::FromUTF8("\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86 foo"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(0, 0));
+ EXPECT_STREQ("\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86 foo",
+ input->value().Utf8().data());
+ Controller().DeleteSurroundingText(0, 4);
+ EXPECT_STREQ(" foo", input->value().Utf8().data());
+
+ // "trophy" + "trophy".
+ input->setValue(String::FromUTF8("\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86 foo"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(0, 0));
+ EXPECT_STREQ("\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86 foo",
+ input->value().Utf8().data());
+ Controller().DeleteSurroundingText(0, 5);
+ EXPECT_STREQ("foo", input->value().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest,
+ DeleteSurroundingTextWithMultiCodeTextOnBothSides) {
+ HTMLInputElement* input =
+ ToHTMLInputElement(InsertHTMLElement("<input id='sample'>", "sample"));
+
+ // "trophy" + "trophy".
+ input->setValue(String::FromUTF8("\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(2, 2));
+ EXPECT_STREQ("\xF0\x9F\x8F\x86\xF0\x9F\x8F\x86",
+ input->value().Utf8().data());
+ Controller().DeleteSurroundingText(1, 1);
+ EXPECT_STREQ("", input->value().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, DeleteSurroundingTextForMultipleNodes) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>aaa"
+ "<div id='sample2' contenteditable>bbb"
+ "<div id='sample3' contenteditable>ccc"
+ "<div id='sample4' contenteditable>ddd"
+ "<div id='sample5' contenteditable>eee"
+ "</div></div></div></div></div>",
+ "sample");
+
+ Controller().SetEditableSelectionOffsets(PlainTextRange(8, 8));
+ EXPECT_STREQ("aaa\nbbb\nccc\nddd\neee", div->innerText().Utf8().data());
+ EXPECT_EQ(8u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(8u, Controller().GetSelectionOffsets().End());
+
+ Controller().DeleteSurroundingText(1, 0);
+ EXPECT_STREQ("aaa\nbbbccc\nddd\neee", div->innerText().Utf8().data());
+ EXPECT_EQ(7u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(7u, Controller().GetSelectionOffsets().End());
+
+ Controller().DeleteSurroundingText(0, 4);
+ EXPECT_STREQ("aaa\nbbbddd\neee", div->innerText().Utf8().data());
+ EXPECT_EQ(7u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(7u, Controller().GetSelectionOffsets().End());
+
+ Controller().DeleteSurroundingText(5, 5);
+ EXPECT_STREQ("aaee", div->innerText().Utf8().data());
+ EXPECT_EQ(2u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(2u, Controller().GetSelectionOffsets().End());
+}
+
+TEST_F(InputMethodControllerTest,
+ DeleteSurroundingTextInCodePointsWithMultiCodeTextOnTheLeft) {
+ HTMLInputElement* input =
+ ToHTMLInputElement(InsertHTMLElement("<input id='sample'>", "sample"));
+
+ // 'a' + "black star" + SPACE + "trophy" + SPACE + composed text (U+0E01
+ // "ka kai" + U+0E49 "mai tho").
+ // A "black star" is 1 grapheme cluster. It has 1 code point, and its length
+ // is 1 (abbreviated as [1,1,1]). A "trophy": [1,1,2]. The composed text:
+ // [1,2,2].
+ input->setValue(String::FromUTF8(
+ "a\xE2\x98\x85 \xF0\x9F\x8F\x86 \xE0\xB8\x81\xE0\xB9\x89"));
+ GetDocument().UpdateStyleAndLayout();
+ // The cursor is at the end of the text.
+ Controller().SetEditableSelectionOffsets(PlainTextRange(8, 8));
+
+ Controller().DeleteSurroundingTextInCodePoints(2, 0);
+ EXPECT_STREQ("a\xE2\x98\x85 \xF0\x9F\x8F\x86 ", input->value().Utf8().data());
+ Controller().DeleteSurroundingTextInCodePoints(4, 0);
+ EXPECT_STREQ("a", input->value().Utf8().data());
+
+ // 'a' + "black star" + SPACE + "trophy" + SPACE + composed text
+ input->setValue(String::FromUTF8(
+ "a\xE2\x98\x85 \xF0\x9F\x8F\x86 \xE0\xB8\x81\xE0\xB9\x89"));
+ GetDocument().UpdateStyleAndLayout();
+ // The cursor is at the end of the text.
+ Controller().SetEditableSelectionOffsets(PlainTextRange(8, 8));
+
+ // TODO(yabinh): We should only delete 1 code point instead of the entire
+ // grapheme cluster (2 code points). The root cause is that we adjust the
+ // selection by grapheme cluster in deleteSurroundingText().
+ Controller().DeleteSurroundingTextInCodePoints(1, 0);
+ EXPECT_STREQ("a\xE2\x98\x85 \xF0\x9F\x8F\x86 ", input->value().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest,
+ DeleteSurroundingTextInCodePointsWithMultiCodeTextOnTheRight) {
+ HTMLInputElement* input =
+ ToHTMLInputElement(InsertHTMLElement("<input id='sample'>", "sample"));
+
+ // 'a' + "black star" + SPACE + "trophy" + SPACE + composed text
+ input->setValue(String::FromUTF8(
+ "a\xE2\x98\x85 \xF0\x9F\x8F\x86 \xE0\xB8\x81\xE0\xB9\x89"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(0, 0));
+
+ Controller().DeleteSurroundingTextInCodePoints(0, 5);
+ EXPECT_STREQ("\xE0\xB8\x81\xE0\xB9\x89", input->value().Utf8().data());
+
+ Controller().DeleteSurroundingTextInCodePoints(0, 1);
+ // TODO(yabinh): Same here. We should only delete 1 code point.
+ EXPECT_STREQ("", input->value().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest,
+ DeleteSurroundingTextInCodePointsWithMultiCodeTextOnBothSides) {
+ HTMLInputElement* input =
+ ToHTMLInputElement(InsertHTMLElement("<input id='sample'>", "sample"));
+
+ // 'a' + "black star" + SPACE + "trophy" + SPACE + composed text
+ input->setValue(String::FromUTF8(
+ "a\xE2\x98\x85 \xF0\x9F\x8F\x86 \xE0\xB8\x81\xE0\xB9\x89"));
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(3, 3));
+ Controller().DeleteSurroundingTextInCodePoints(2, 2);
+ EXPECT_STREQ("a\xE0\xB8\x81\xE0\xB9\x89", input->value().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, DeleteSurroundingTextInCodePointsWithImage) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>aaa"
+ "<img src='empty.png'>bbb</div>",
+ "sample");
+
+ Controller().SetEditableSelectionOffsets(PlainTextRange(4, 4));
+ Controller().DeleteSurroundingTextInCodePoints(1, 1);
+ EXPECT_STREQ("aaabb", div->innerText().Utf8().data());
+ EXPECT_EQ(3u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(3u, Controller().GetSelectionOffsets().End());
+}
+
+TEST_F(InputMethodControllerTest,
+ DeleteSurroundingTextInCodePointsWithInvalidSurrogatePair) {
+ HTMLInputElement* input =
+ ToHTMLInputElement(InsertHTMLElement("<input id='sample'>", "sample"));
+
+ // 'a' + high surrogate of "trophy" + "black star" + low surrogate of "trophy"
+ // + SPACE
+ const UChar kUText[] = {'a', 0xD83C, 0x2605, 0xDFC6, ' ', '\0'};
+ const String& text = String(kUText);
+
+ input->setValue(text);
+ GetDocument().UpdateStyleAndLayout();
+ // The invalid high surrogate is encoded as '\xED\xA0\xBC', and invalid low
+ // surrogate is encoded as '\xED\xBF\x86'.
+ EXPECT_STREQ("a\xED\xA0\xBC\xE2\x98\x85\xED\xBF\x86 ",
+ input->value().Utf8().data());
+
+ Controller().SetEditableSelectionOffsets(PlainTextRange(5, 5));
+ // Delete a SPACE.
+ Controller().DeleteSurroundingTextInCodePoints(1, 0);
+ EXPECT_STREQ("a\xED\xA0\xBC\xE2\x98\x85\xED\xBF\x86",
+ input->value().Utf8().data());
+ // Do nothing since there is an invalid surrogate in the requested range.
+ Controller().DeleteSurroundingTextInCodePoints(2, 0);
+ EXPECT_STREQ("a\xED\xA0\xBC\xE2\x98\x85\xED\xBF\x86",
+ input->value().Utf8().data());
+
+ Controller().SetEditableSelectionOffsets(PlainTextRange(0, 0));
+ // Delete 'a'.
+ Controller().DeleteSurroundingTextInCodePoints(0, 1);
+ EXPECT_STREQ("\xED\xA0\xBC\xE2\x98\x85\xED\xBF\x86",
+ input->value().Utf8().data());
+ // Do nothing since there is an invalid surrogate in the requested range.
+ Controller().DeleteSurroundingTextInCodePoints(0, 2);
+ EXPECT_STREQ("\xED\xA0\xBC\xE2\x98\x85\xED\xBF\x86",
+ input->value().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, SetCompositionForInputWithNewCaretPositions) {
+ HTMLInputElement* input =
+ ToHTMLInputElement(InsertHTMLElement("<input id='sample'>", "sample"));
+
+ input->setValue("hello");
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(2, 2));
+ EXPECT_STREQ("hello", input->value().Utf8().data());
+ EXPECT_EQ(2u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(2u, Controller().GetSelectionOffsets().End());
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 2,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+
+ // The caret exceeds left boundary.
+ // "*heABllo", where * stands for caret.
+ Controller().SetComposition("AB", ime_text_spans, -100, -100);
+ EXPECT_STREQ("heABllo", input->value().Utf8().data());
+ EXPECT_EQ(0u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(0u, Controller().GetSelectionOffsets().End());
+
+ // The caret is on left boundary.
+ // "*heABllo".
+ Controller().SetComposition("AB", ime_text_spans, -2, -2);
+ EXPECT_STREQ("heABllo", input->value().Utf8().data());
+ EXPECT_EQ(0u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(0u, Controller().GetSelectionOffsets().End());
+
+ // The caret is before the composing text.
+ // "he*ABllo".
+ Controller().SetComposition("AB", ime_text_spans, 0, 0);
+ EXPECT_STREQ("heABllo", input->value().Utf8().data());
+ EXPECT_EQ(2u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(2u, Controller().GetSelectionOffsets().End());
+
+ // The caret is after the composing text.
+ // "heAB*llo".
+ Controller().SetComposition("AB", ime_text_spans, 2, 2);
+ EXPECT_STREQ("heABllo", input->value().Utf8().data());
+ EXPECT_EQ(4u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(4u, Controller().GetSelectionOffsets().End());
+
+ // The caret is on right boundary.
+ // "heABllo*".
+ Controller().SetComposition("AB", ime_text_spans, 5, 5);
+ EXPECT_STREQ("heABllo", input->value().Utf8().data());
+ EXPECT_EQ(7u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(7u, Controller().GetSelectionOffsets().End());
+
+ // The caret exceeds right boundary.
+ // "heABllo*".
+ Controller().SetComposition("AB", ime_text_spans, 100, 100);
+ EXPECT_STREQ("heABllo", input->value().Utf8().data());
+ EXPECT_EQ(7u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(7u, Controller().GetSelectionOffsets().End());
+}
+
+TEST_F(InputMethodControllerTest,
+ SetCompositionForContentEditableWithNewCaretPositions) {
+ // There are 7 nodes and 5+1+5+1+3+4+3 characters: "hello", '\n', "world",
+ // "\n", "012", "3456", "789".
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>"
+ "hello"
+ "<div id='sample2' contenteditable>world"
+ "<p>012<b>3456</b><i>789</i></p>"
+ "</div>"
+ "</div>",
+ "sample");
+
+ Controller().SetEditableSelectionOffsets(PlainTextRange(17, 17));
+ EXPECT_STREQ("hello\nworld\n0123456789", div->innerText().Utf8().data());
+ EXPECT_EQ(17u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(17u, Controller().GetSelectionOffsets().End());
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 2,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+
+ // The caret exceeds left boundary.
+ // "*hello\nworld\n01234AB56789", where * stands for caret.
+ Controller().SetComposition("AB", ime_text_spans, -100, -100);
+ EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().Utf8().data());
+ EXPECT_EQ(0u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(0u, Controller().GetSelectionOffsets().End());
+
+ // The caret is on left boundary.
+ // "*hello\nworld\n01234AB56789".
+ Controller().SetComposition("AB", ime_text_spans, -17, -17);
+ EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().Utf8().data());
+ EXPECT_EQ(0u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(0u, Controller().GetSelectionOffsets().End());
+
+ // The caret is in the 1st node.
+ // "he*llo\nworld\n01234AB56789".
+ Controller().SetComposition("AB", ime_text_spans, -15, -15);
+ EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().Utf8().data());
+ EXPECT_EQ(2u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(2u, Controller().GetSelectionOffsets().End());
+
+ // The caret is on right boundary of the 1st node.
+ // "hello*\nworld\n01234AB56789".
+ Controller().SetComposition("AB", ime_text_spans, -12, -12);
+ EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().Utf8().data());
+ EXPECT_EQ(5u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(5u, Controller().GetSelectionOffsets().End());
+
+ // The caret is on right boundary of the 2nd node.
+ // "hello\n*world\n01234AB56789".
+ Controller().SetComposition("AB", ime_text_spans, -11, -11);
+ EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().Utf8().data());
+ EXPECT_EQ(6u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(6u, Controller().GetSelectionOffsets().End());
+
+ // The caret is on right boundary of the 3rd node.
+ // "hello\nworld*\n01234AB56789".
+ Controller().SetComposition("AB", ime_text_spans, -6, -6);
+ EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().Utf8().data());
+ EXPECT_EQ(11u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(11u, Controller().GetSelectionOffsets().End());
+
+ // The caret is on right boundary of the 4th node.
+ // "hello\nworld\n*01234AB56789".
+ Controller().SetComposition("AB", ime_text_spans, -5, -5);
+ EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().Utf8().data());
+ EXPECT_EQ(12u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(12u, Controller().GetSelectionOffsets().End());
+
+ // The caret is before the composing text.
+ // "hello\nworld\n01234*AB56789".
+ Controller().SetComposition("AB", ime_text_spans, 0, 0);
+ EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().Utf8().data());
+ EXPECT_EQ(17u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(17u, Controller().GetSelectionOffsets().End());
+
+ // The caret is after the composing text.
+ // "hello\nworld\n01234AB*56789".
+ Controller().SetComposition("AB", ime_text_spans, 2, 2);
+ EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().Utf8().data());
+ EXPECT_EQ(19u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(19u, Controller().GetSelectionOffsets().End());
+
+ // The caret is on right boundary.
+ // "hello\nworld\n01234AB56789*".
+ Controller().SetComposition("AB", ime_text_spans, 7, 7);
+ EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().Utf8().data());
+ EXPECT_EQ(24u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(24u, Controller().GetSelectionOffsets().End());
+
+ // The caret exceeds right boundary.
+ // "hello\nworld\n01234AB56789*".
+ Controller().SetComposition("AB", ime_text_spans, 100, 100);
+ EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().Utf8().data());
+ EXPECT_EQ(24u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(24u, Controller().GetSelectionOffsets().End());
+}
+
+TEST_F(InputMethodControllerTest, SetCompositionWithEmptyText) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>hello</div>", "sample");
+
+ Controller().SetEditableSelectionOffsets(PlainTextRange(2, 2));
+ EXPECT_STREQ("hello", div->innerText().Utf8().data());
+ EXPECT_EQ(2u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(2u, Controller().GetSelectionOffsets().End());
+
+ Vector<ImeTextSpan> ime_text_spans0;
+ ime_text_spans0.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 0,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Vector<ImeTextSpan> ime_text_spans2;
+ ime_text_spans2.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 2,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+
+ Controller().SetComposition("AB", ime_text_spans2, 2, 2);
+ // With previous composition.
+ Controller().SetComposition("", ime_text_spans0, 2, 2);
+ EXPECT_STREQ("hello", div->innerText().Utf8().data());
+ EXPECT_EQ(4u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(4u, Controller().GetSelectionOffsets().End());
+
+ // Without previous composition.
+ Controller().SetComposition("", ime_text_spans0, -1, -1);
+ EXPECT_STREQ("hello", div->innerText().Utf8().data());
+ EXPECT_EQ(3u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(3u, Controller().GetSelectionOffsets().End());
+}
+
+TEST_F(InputMethodControllerTest, InsertLineBreakWhileComposingText) {
+ Element* div =
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 5,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().SetComposition("hello", ime_text_spans, 5, 5);
+ EXPECT_STREQ("hello", div->innerText().Utf8().data());
+ EXPECT_EQ(5u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(5u, Controller().GetSelectionOffsets().End());
+
+ GetFrame().GetEditor().InsertLineBreak();
+ EXPECT_STREQ("hello\n\n", div->innerText().Utf8().data());
+ EXPECT_EQ(6u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(6u, Controller().GetSelectionOffsets().End());
+}
+
+TEST_F(InputMethodControllerTest, InsertLineBreakAfterConfirmingText) {
+ Element* div =
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 2,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().CommitText("hello", ime_text_spans, 0);
+ EXPECT_STREQ("hello", div->innerText().Utf8().data());
+
+ Controller().SetEditableSelectionOffsets(PlainTextRange(2, 2));
+ EXPECT_EQ(2u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(2u, Controller().GetSelectionOffsets().End());
+
+ GetFrame().GetEditor().InsertLineBreak();
+ EXPECT_STREQ("he\nllo", div->innerText().Utf8().data());
+ EXPECT_EQ(3u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(3u, Controller().GetSelectionOffsets().End());
+}
+
+TEST_F(InputMethodControllerTest, CompositionInputEventIsComposing) {
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* editable =
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('beforeinput', "
+ " event => document.title = "
+ " `beforeinput.isComposing:${event.isComposing};`);"
+ "document.getElementById('sample').addEventListener('input', "
+ " event => document.title += "
+ " `input.isComposing:${event.isComposing};`);");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ // Simulate composition in the |contentEditable|.
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 5,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ editable->focus();
+
+ GetDocument().setTitle(g_empty_string);
+ Controller().SetComposition("foo", ime_text_spans, 0, 3);
+ EXPECT_STREQ("beforeinput.isComposing:true;input.isComposing:true;",
+ GetDocument().title().Utf8().data());
+
+ GetDocument().setTitle(g_empty_string);
+ Controller().CommitText("bar", ime_text_spans, 0);
+ // Last pair of InputEvent should also be inside composition scope.
+ EXPECT_STREQ("beforeinput.isComposing:true;input.isComposing:true;",
+ GetDocument().title().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, CompositionInputEventForReplace) {
+ CreateHTMLWithCompositionInputEventListeners();
+
+ // Simulate composition in the |contentEditable|.
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 5,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+
+ GetDocument().setTitle(g_empty_string);
+ Controller().SetComposition("hell", ime_text_spans, 4, 4);
+ EXPECT_STREQ("beforeinput.data:hell;input.data:hell;",
+ GetDocument().title().Utf8().data());
+
+ // Replace the existing composition.
+ GetDocument().setTitle(g_empty_string);
+ Controller().SetComposition("hello", ime_text_spans, 0, 0);
+ EXPECT_STREQ("beforeinput.data:hello;input.data:hello;",
+ GetDocument().title().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, CompositionInputEventForConfirm) {
+ CreateHTMLWithCompositionInputEventListeners();
+
+ // Simulate composition in the |contentEditable|.
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 5,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+
+ GetDocument().setTitle(g_empty_string);
+ Controller().SetComposition("hello", ime_text_spans, 5, 5);
+ EXPECT_STREQ("beforeinput.data:hello;input.data:hello;",
+ GetDocument().title().Utf8().data());
+
+ // Confirm the ongoing composition.
+ GetDocument().setTitle(g_empty_string);
+ Controller().FinishComposingText(InputMethodController::kKeepSelection);
+ EXPECT_STREQ("compositionend.data:hello;",
+ GetDocument().title().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, CompositionInputEventForDelete) {
+ CreateHTMLWithCompositionInputEventListeners();
+
+ // Simulate composition in the |contentEditable|.
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 5,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+
+ GetDocument().setTitle(g_empty_string);
+ Controller().SetComposition("hello", ime_text_spans, 5, 5);
+ EXPECT_STREQ("beforeinput.data:hello;input.data:hello;",
+ GetDocument().title().Utf8().data());
+
+ // Delete the existing composition.
+ GetDocument().setTitle(g_empty_string);
+ Controller().SetComposition("", ime_text_spans, 0, 0);
+ EXPECT_STREQ("beforeinput.data:;input.data:null;compositionend.data:;",
+ GetDocument().title().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, CompositionInputEventForInsert) {
+ CreateHTMLWithCompositionInputEventListeners();
+
+ // Simulate composition in the |contentEditable|.
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 5,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+
+ // Insert new text without previous composition.
+ GetDocument().setTitle(g_empty_string);
+ GetDocument().UpdateStyleAndLayout();
+ Controller().CommitText("hello", ime_text_spans, 0);
+ EXPECT_STREQ("beforeinput.data:hello;input.data:hello;",
+ GetDocument().title().Utf8().data());
+
+ GetDocument().setTitle(g_empty_string);
+ Controller().SetComposition("n", ime_text_spans, 1, 1);
+ EXPECT_STREQ("beforeinput.data:n;input.data:n;",
+ GetDocument().title().Utf8().data());
+
+ // Insert new text with previous composition.
+ GetDocument().setTitle(g_empty_string);
+ GetDocument().UpdateStyleAndLayout();
+ Controller().CommitText("hello", ime_text_spans, 1);
+ EXPECT_STREQ(
+ "beforeinput.data:hello;input.data:hello;compositionend.data:hello;",
+ GetDocument().title().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, CompositionInputEventForInsertEmptyText) {
+ CreateHTMLWithCompositionInputEventListeners();
+
+ // Simulate composition in the |contentEditable|.
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 5,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+
+ // Insert empty text without previous composition.
+ GetDocument().setTitle(g_empty_string);
+ GetDocument().UpdateStyleAndLayout();
+ Controller().CommitText("", ime_text_spans, 0);
+ EXPECT_STREQ("", GetDocument().title().Utf8().data());
+
+ GetDocument().setTitle(g_empty_string);
+ Controller().SetComposition("n", ime_text_spans, 1, 1);
+ EXPECT_STREQ("beforeinput.data:n;input.data:n;",
+ GetDocument().title().Utf8().data());
+
+ // Insert empty text with previous composition.
+ GetDocument().setTitle(g_empty_string);
+ GetDocument().UpdateStyleAndLayout();
+ Controller().CommitText("", ime_text_spans, 1);
+ EXPECT_STREQ("beforeinput.data:;input.data:null;compositionend.data:;",
+ GetDocument().title().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, CompositionEndEventWithNoSelection) {
+ CreateHTMLWithCompositionEndEventListener(kNoSelection);
+
+ // Simulate composition in the |contentEditable|.
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 5,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+
+ Controller().SetComposition("hello", ime_text_spans, 1, 1);
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_EQ(1u, Controller().GetSelectionOffsets().Start());
+ EXPECT_EQ(1u, Controller().GetSelectionOffsets().End());
+
+ // Confirm the ongoing composition. Note that it moves the caret to the end of
+ // text [5,5] before firing 'compositonend' event.
+ Controller().FinishComposingText(InputMethodController::kDoNotKeepSelection);
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_TRUE(Controller().GetSelectionOffsets().IsNull());
+}
+
+TEST_F(InputMethodControllerTest, FinishCompositionRemovedRange) {
+ Element* input_a =
+ InsertHTMLElement("<input id='a' /><br><input type='tel' id='b' />", "a");
+
+ EXPECT_EQ(kWebTextInputTypeText, Controller().TextInputType());
+
+ // The test requires non-empty composition.
+ Controller().SetComposition("hello", Vector<ImeTextSpan>(), 5, 5);
+ EXPECT_EQ(kWebTextInputTypeText, Controller().TextInputType());
+
+ // Remove element 'a'.
+ input_a->SetOuterHTMLFromString("", ASSERT_NO_EXCEPTION);
+ EXPECT_EQ(kWebTextInputTypeNone, Controller().TextInputType());
+
+ GetDocument().getElementById("b")->focus();
+ EXPECT_EQ(kWebTextInputTypeTelephone, Controller().TextInputType());
+
+ Controller().FinishComposingText(InputMethodController::kKeepSelection);
+ EXPECT_EQ(kWebTextInputTypeTelephone, Controller().TextInputType());
+}
+
+TEST_F(InputMethodControllerTest, ReflectsSpaceWithoutNbspMangling) {
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ Controller().CommitText(String(" "), ime_text_spans, 0);
+
+ // In a contenteditable, multiple spaces or a space at the edge needs to be
+ // nbsp to affect layout properly, but it confuses some IMEs (particularly
+ // Vietnamese, see crbug.com/663880) to have their spaces reflected back to
+ // them as nbsp.
+ EXPECT_EQ(' ', Controller().TextInputInfo().value.Ascii()[0]);
+ EXPECT_EQ(' ', Controller().TextInputInfo().value.Ascii()[1]);
+}
+
+TEST_F(InputMethodControllerTest, SetCompositionPlainTextWithIme_Text_Span) {
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 1,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+
+ Controller().SetComposition(" ", ime_text_spans, 1, 1);
+
+ ASSERT_EQ(1u, GetDocument().Markers().Markers().size());
+
+ EXPECT_EQ(0u, GetDocument().Markers().Markers()[0]->StartOffset());
+ EXPECT_EQ(1u, GetDocument().Markers().Markers()[0]->EndOffset());
+}
+
+TEST_F(InputMethodControllerTest, CommitPlainTextWithIme_Text_SpanInsert) {
+ InsertHTMLElement("<div id='sample' contenteditable>Initial text.</div>",
+ "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+
+ Controller().SetEditableSelectionOffsets(PlainTextRange(8, 8));
+
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 1, 11,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+
+ Controller().CommitText(String("ime_text_spand"), ime_text_spans, 0);
+
+ ASSERT_EQ(1u, GetDocument().Markers().Markers().size());
+
+ EXPECT_EQ(9u, GetDocument().Markers().Markers()[0]->StartOffset());
+ EXPECT_EQ(19u, GetDocument().Markers().Markers()[0]->EndOffset());
+}
+
+TEST_F(InputMethodControllerTest, CommitPlainTextWithIme_Text_SpanReplace) {
+ InsertHTMLElement("<div id='sample' contenteditable>Initial text.</div>",
+ "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+
+ Controller().SetCompositionFromExistingText(ime_text_spans, 8, 12);
+
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 1, 11,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+
+ Controller().CommitText(String("string"), ime_text_spans, 0);
+
+ ASSERT_EQ(1u, GetDocument().Markers().Markers().size());
+
+ EXPECT_EQ(9u, GetDocument().Markers().Markers()[0]->StartOffset());
+ EXPECT_EQ(15u, GetDocument().Markers().Markers()[0]->EndOffset());
+}
+
+TEST_F(InputMethodControllerTest, ImeTextSpanAppearsCorrectlyAfterNewline) {
+ Element* div =
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ Controller().SetComposition(String("hello"), ime_text_spans, 6, 6);
+ Controller().FinishComposingText(InputMethodController::kKeepSelection);
+ GetFrame().GetEditor().InsertLineBreak();
+
+ Controller().SetCompositionFromExistingText(ime_text_spans, 8, 8);
+
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 5,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().SetComposition(String("world"), ime_text_spans, 0, 0);
+ ASSERT_EQ(1u, GetDocument().Markers().Markers().size());
+
+ // Verify composition marker shows up on the second line, not the first
+ const Position& first_line_position =
+ PlainTextRange(2).CreateRange(*div).StartPosition();
+ const Position& second_line_position =
+ PlainTextRange(8).CreateRange(*div).StartPosition();
+ ASSERT_EQ(0u, GetDocument()
+ .Markers()
+ .MarkersFor(first_line_position.ComputeContainerNode())
+ .size());
+ ASSERT_EQ(1u, GetDocument()
+ .Markers()
+ .MarkersFor(second_line_position.ComputeContainerNode())
+ .size());
+
+ // Verify marker has correct start/end offsets (measured from the beginning
+ // of the node, which is the beginning of the line)
+ EXPECT_EQ(0u, GetDocument().Markers().Markers()[0]->StartOffset());
+ EXPECT_EQ(5u, GetDocument().Markers().Markers()[0]->EndOffset());
+}
+
+TEST_F(InputMethodControllerTest, SelectionWhenFocusChangeFinishesComposition) {
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* editable =
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+ editable->focus();
+
+ // Simulate composition in the |contentEditable|.
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 5,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().SetComposition("foo", ime_text_spans, 3, 3);
+
+ EXPECT_TRUE(Controller().HasComposition());
+ EXPECT_EQ(0u, GetCompositionRange()->startOffset());
+ EXPECT_EQ(3u, GetCompositionRange()->endOffset());
+ EXPECT_EQ(3, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+
+ // Insert 'test'.
+ NonThrowableExceptionState exception_state;
+ GetDocument().execCommand("insertText", false, "test", exception_state);
+
+ EXPECT_TRUE(Controller().HasComposition());
+ EXPECT_EQ(7, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+
+ // Focus change finishes composition.
+ editable->blur();
+ editable->focus();
+
+ // Make sure that caret is still at the end of the inserted text.
+ EXPECT_FALSE(Controller().HasComposition());
+ EXPECT_EQ(7, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+}
+
+TEST_F(InputMethodControllerTest, SetEmptyCompositionShouldNotMoveCaret) {
+ HTMLTextAreaElement* textarea =
+ ToHTMLTextAreaElement(InsertHTMLElement("<textarea id='txt'>", "txt"));
+
+ textarea->setValue("abc\n");
+ GetDocument().UpdateStyleAndLayout();
+ Controller().SetEditableSelectionOffsets(PlainTextRange(4, 4));
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 3,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().SetComposition(String("def"), ime_text_spans, 0, 3);
+ Controller().SetComposition(String(""), ime_text_spans, 0, 3);
+ Controller().CommitText(String("def"), ime_text_spans, 0);
+
+ EXPECT_STREQ("abc\ndef", textarea->value().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, WhitespaceFixup) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>Initial text blah</div>", "sample");
+
+ // Delete "Initial"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 0, 7);
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+
+ // The space at the beginning of the string should have been converted to an
+ // nbsp
+ EXPECT_STREQ("&nbsp;text blah", div->InnerHTMLAsString().Utf8().data());
+
+ // Delete "blah"
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 6, 10);
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+
+ // The space at the end of the string should have been converted to an nbsp
+ EXPECT_STREQ("&nbsp;text&nbsp;", div->InnerHTMLAsString().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, CommitEmptyTextDeletesSelection) {
+ HTMLInputElement* input =
+ ToHTMLInputElement(InsertHTMLElement("<input id='sample'>", "sample"));
+
+ input->setValue("Abc Def Ghi");
+ GetDocument().UpdateStyleAndLayout();
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetEditableSelectionOffsets(PlainTextRange(4, 8));
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+ EXPECT_STREQ("Abc Ghi", input->value().Utf8().data());
+
+ Controller().SetEditableSelectionOffsets(PlainTextRange(4, 7));
+ Controller().CommitText(String("1"), empty_ime_text_spans, 0);
+ EXPECT_STREQ("Abc 1", input->value().Utf8().data());
+}
+
+static String GetMarkedText(
+ DocumentMarkerController& document_marker_controller,
+ Node* node,
+ int marker_index) {
+ DocumentMarker* marker = document_marker_controller.Markers()[marker_index];
+ return node->textContent().Substring(
+ marker->StartOffset(), marker->EndOffset() - marker->StartOffset());
+}
+
+TEST_F(InputMethodControllerTest,
+ Marker_WhitespaceFixupAroundContentIndependentMarkerNotContainingSpace) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>Initial text blah</div>", "sample");
+
+ // Add marker under "text" (use TextMatch since Composition markers don't
+ // persist across editing operations)
+ EphemeralRange marker_range = PlainTextRange(8, 12).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+ // Delete "Initial"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 0, 7);
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+
+ // Delete "blah"
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 6, 10);
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+
+ // Check that the marker is still attached to "text" and doesn't include
+ // either space around it
+ EXPECT_EQ(1u, GetDocument().Markers().MarkersFor(div->firstChild()).size());
+ EXPECT_STREQ("text",
+ GetMarkedText(GetDocument().Markers(), div->firstChild(), 0)
+ .Utf8()
+ .data());
+}
+
+TEST_F(InputMethodControllerTest,
+ Marker_WhitespaceFixupAroundContentIndependentMarkerBeginningWithSpace) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>Initial text blah</div>", "sample");
+
+ // Add marker under " text" (use TextMatch since Composition markers don't
+ // persist across editing operations)
+ EphemeralRange marker_range = PlainTextRange(7, 12).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+ // Delete "Initial"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 0, 7);
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+
+ // Delete "blah"
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 6, 10);
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+
+ // Check that the marker is still attached to " text" and includes the space
+ // before "text" but not the space after
+ EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
+ ASSERT_STREQ("\xC2\xA0text",
+ GetMarkedText(GetDocument().Markers(), div->firstChild(), 0)
+ .Utf8()
+ .data());
+}
+
+TEST_F(InputMethodControllerTest,
+ Marker_WhitespaceFixupAroundContentIndependentMarkerEndingWithSpace) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>Initial text blah</div>", "sample");
+
+ // Add marker under "text " (use TextMatch since Composition markers don't
+ // persist across editing operations)
+ EphemeralRange marker_range = PlainTextRange(8, 13).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+ // Delete "Initial"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 0, 7);
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+
+ // Delete "blah"
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 6, 10);
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+
+ // Check that the marker is still attached to "text " and includes the space
+ // after "text" but not the space before
+ EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
+ ASSERT_STREQ("text\xC2\xA0",
+ GetMarkedText(GetDocument().Markers(), div->firstChild(), 0)
+ .Utf8()
+ .data());
+}
+
+TEST_F(
+ InputMethodControllerTest,
+ Marker_WhitespaceFixupAroundContentIndependentMarkerBeginningAndEndingWithSpaces) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>Initial text blah</div>", "sample");
+
+ // Add marker under " text " (use TextMatch since Composition markers don't
+ // persist across editing operations)
+ EphemeralRange marker_range = PlainTextRange(7, 13).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ // Delete "Initial"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 0, 7);
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+
+ // Delete "blah"
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 6, 10);
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+
+ // Check that the marker is still attached to " text " and includes both the
+ // space before "text" and the space after
+ EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
+ ASSERT_STREQ("\xC2\xA0text\xC2\xA0",
+ GetMarkedText(GetDocument().Markers(), div->firstChild(), 0)
+ .Utf8()
+ .data());
+}
+
+TEST_F(InputMethodControllerTest, ContentDependentMarker_ReplaceStartOfMarker) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>Initial text</div>", "sample");
+
+ // Add marker under "Initial text"
+ EphemeralRange marker_range = PlainTextRange(0, 12).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ // Replace "Initial" with "Original"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 0, 7);
+ Controller().CommitText(String("Original"), empty_ime_text_spans, 0);
+
+ // Verify marker was removed
+ EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
+}
+
+TEST_F(InputMethodControllerTest,
+ ContentIndependentMarker_ReplaceStartOfMarker) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>Initial text</div>", "sample");
+
+ // Add marker under "Initial text"
+ EphemeralRange marker_range = PlainTextRange(0, 12).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ // Replace "Initial" with "Original"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 0, 7);
+ Controller().CommitText(String("Original"), empty_ime_text_spans, 0);
+
+ // Verify marker is under "Original text"
+ EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
+ ASSERT_STREQ("Original text",
+ GetMarkedText(GetDocument().Markers(), div->firstChild(), 0)
+ .Utf8()
+ .data());
+}
+
+TEST_F(InputMethodControllerTest,
+ ContentDependentMarker_ReplaceTextContainsStartOfMarker) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>This is some initial text</div>",
+ "sample");
+
+ // Add marker under "initial text"
+ EphemeralRange marker_range = PlainTextRange(13, 25).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ // Replace "some initial" with "boring"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 8, 20);
+ Controller().CommitText(String("boring"), empty_ime_text_spans, 0);
+
+ // Verify marker was removed
+ EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
+}
+
+TEST_F(InputMethodControllerTest,
+ ContentIndependentMarker_ReplaceTextContainsStartOfMarker) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>This is some initial text</div>",
+ "sample");
+
+ // Add marker under "initial text"
+ EphemeralRange marker_range = PlainTextRange(13, 25).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ // Replace "some initial" with "boring"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 8, 20);
+ Controller().CommitText(String("boring"), empty_ime_text_spans, 0);
+
+ // Verify marker is under " text"
+ EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
+ EXPECT_STREQ(" text",
+ GetMarkedText(GetDocument().Markers(), div->firstChild(), 0)
+ .Utf8()
+ .data());
+}
+
+TEST_F(InputMethodControllerTest, ContentDependentMarker_ReplaceEndOfMarker) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>Initial text</div>", "sample");
+
+ // Add marker under "Initial text"
+ EphemeralRange marker_range = PlainTextRange(0, 12).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ // Replace "text" with "string"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 8, 12);
+ Controller().CommitText(String("string"), empty_ime_text_spans, 0);
+
+ // Verify marker was removed
+ EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
+}
+
+TEST_F(InputMethodControllerTest, ContentIndependentMarker_ReplaceEndOfMarker) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>Initial text</div>", "sample");
+
+ // Add marker under "Initial text"
+ EphemeralRange marker_range = PlainTextRange(0, 12).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ // Replace "text" with "string"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 8, 12);
+ Controller().CommitText(String("string"), empty_ime_text_spans, 0);
+
+ // Verify marker is under "Initial string"
+ EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
+ ASSERT_STREQ("Initial string",
+ GetMarkedText(GetDocument().Markers(), div->firstChild(), 0)
+ .Utf8()
+ .data());
+}
+
+TEST_F(InputMethodControllerTest,
+ ContentDependentMarker_ReplaceTextContainsEndOfMarker) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>This is some initial text</div>",
+ "sample");
+
+ // Add marker under "some initial"
+ EphemeralRange marker_range = PlainTextRange(8, 20).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ // Replace "initial text" with "content"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 13, 25);
+ Controller().CommitText(String("content"), empty_ime_text_spans, 0);
+
+ EXPECT_STREQ("This is some content", div->InnerHTMLAsString().Utf8().data());
+
+ // Verify marker was removed
+ EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
+}
+
+TEST_F(InputMethodControllerTest,
+ ContentIndependentMarker_ReplaceTextContainsEndOfMarker) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>This is some initial text</div>",
+ "sample");
+
+ // Add marker under "some initial"
+ EphemeralRange marker_range = PlainTextRange(8, 20).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ // Replace "initial text" with "content"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 13, 25);
+ Controller().CommitText(String("content"), empty_ime_text_spans, 0);
+
+ EXPECT_STREQ("This is some content", div->InnerHTMLAsString().Utf8().data());
+
+ // Verify marker is under "some "
+ EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
+ ASSERT_STREQ("some ",
+ GetMarkedText(GetDocument().Markers(), div->firstChild(), 0)
+ .Utf8()
+ .data());
+}
+
+TEST_F(InputMethodControllerTest, ContentDependentMarker_ReplaceEntireMarker) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>Initial text</div>", "sample");
+
+ // Add marker under "text"
+ EphemeralRange marker_range = PlainTextRange(8, 12).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ // Replace "text" with "string"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 8, 12);
+ Controller().CommitText(String("string"), empty_ime_text_spans, 0);
+
+ // Verify marker was removed
+ EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
+}
+
+TEST_F(InputMethodControllerTest,
+ ContentIndependentMarker_ReplaceEntireMarker) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>Initial text</div>", "sample");
+
+ // Add marker under "text"
+ EphemeralRange marker_range = PlainTextRange(8, 12).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ // Replace "text" with "string"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 8, 12);
+ Controller().CommitText(String("string"), empty_ime_text_spans, 0);
+
+ // Verify marker is under "string"
+ EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
+ ASSERT_STREQ("string",
+ GetMarkedText(GetDocument().Markers(), div->firstChild(), 0)
+ .Utf8()
+ .data());
+}
+
+TEST_F(InputMethodControllerTest,
+ ContentDependentMarker_ReplaceTextWithMarkerAtBeginning) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>Initial text</div>", "sample");
+
+ // Add marker under "Initial"
+ EphemeralRange marker_range = PlainTextRange(0, 7).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
+
+ // Replace "Initial text" with "New string"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 0, 12);
+ Controller().CommitText(String("New string"), empty_ime_text_spans, 0);
+
+ // Verify marker was removed
+ EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
+}
+
+TEST_F(InputMethodControllerTest,
+ ContentIndependentMarker_ReplaceTextWithMarkerAtBeginning) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>Initial text</div>", "sample");
+
+ // Add marker under "Initial"
+ EphemeralRange marker_range = PlainTextRange(0, 7).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
+
+ // Replace "Initial text" with "New string"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 0, 12);
+ Controller().CommitText(String("New string"), empty_ime_text_spans, 0);
+
+ // Verify marker was removed
+ EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
+}
+
+TEST_F(InputMethodControllerTest,
+ ContentDependentMarker_ReplaceTextWithMarkerAtEnd) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>Initial text</div>", "sample");
+
+ // Add marker under "text"
+ EphemeralRange marker_range = PlainTextRange(8, 12).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
+
+ // Replace "Initial text" with "New string"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 0, 12);
+ Controller().CommitText(String("New string"), empty_ime_text_spans, 0);
+
+ // Verify marker was removed
+ EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
+}
+
+TEST_F(InputMethodControllerTest,
+ ContentIndependentMarker_ReplaceTextWithMarkerAtEnd) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>Initial text</div>", "sample");
+
+ // Add marker under "text"
+ EphemeralRange marker_range = PlainTextRange(8, 12).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
+
+ // Replace "Initial text" with "New string"
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 0, 12);
+ Controller().CommitText(String("New string"), empty_ime_text_spans, 0);
+
+ // Verify marker was removed
+ EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
+}
+
+TEST_F(InputMethodControllerTest, ContentDependentMarker_Deletions) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>1111122222333334444455555</div>",
+ "sample");
+
+ EphemeralRange marker_range = PlainTextRange(0, 5).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ marker_range = PlainTextRange(5, 10).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ marker_range = PlainTextRange(10, 15).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ marker_range = PlainTextRange(15, 20).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ marker_range = PlainTextRange(20, 25).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ EXPECT_EQ(5u, GetDocument().Markers().Markers().size());
+
+ // Delete third marker and portions of second and fourth
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 8, 17);
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+
+ // Verify markers were updated correctly
+ EXPECT_EQ(2u, GetDocument().Markers().Markers().size());
+
+ EXPECT_EQ(0u, GetDocument().Markers().Markers()[0]->StartOffset());
+ EXPECT_EQ(5u, GetDocument().Markers().Markers()[0]->EndOffset());
+
+ EXPECT_EQ(11u, GetDocument().Markers().Markers()[1]->StartOffset());
+ EXPECT_EQ(16u, GetDocument().Markers().Markers()[1]->EndOffset());
+}
+
+TEST_F(InputMethodControllerTest, ContentIndependentMarker_Deletions) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>1111122222333334444455555</div>",
+ "sample");
+
+ EphemeralRange marker_range = PlainTextRange(0, 5).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ marker_range = PlainTextRange(5, 10).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ marker_range = PlainTextRange(10, 15).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ marker_range = PlainTextRange(15, 20).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ marker_range = PlainTextRange(20, 25).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ EXPECT_EQ(5u, GetDocument().Markers().Markers().size());
+
+ // Delete third marker and portions of second and fourth
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 8, 17);
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+
+ // Verify markers were updated correctly
+ EXPECT_EQ(4u, GetDocument().Markers().Markers().size());
+
+ EXPECT_EQ(0u, GetDocument().Markers().Markers()[0]->StartOffset());
+ EXPECT_EQ(5u, GetDocument().Markers().Markers()[0]->EndOffset());
+
+ EXPECT_EQ(5u, GetDocument().Markers().Markers()[1]->StartOffset());
+ EXPECT_EQ(8u, GetDocument().Markers().Markers()[1]->EndOffset());
+
+ EXPECT_EQ(8u, GetDocument().Markers().Markers()[2]->StartOffset());
+ EXPECT_EQ(11u, GetDocument().Markers().Markers()[2]->EndOffset());
+
+ EXPECT_EQ(11u, GetDocument().Markers().Markers()[3]->StartOffset());
+ EXPECT_EQ(16u, GetDocument().Markers().Markers()[3]->EndOffset());
+}
+
+TEST_F(InputMethodControllerTest,
+ ContentDependentMarker_DeleteExactlyOnMarker) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>1111122222333334444455555</div>",
+ "sample");
+
+ EphemeralRange marker_range = PlainTextRange(5, 10).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
+
+ // Delete exactly on the marker
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 5, 10);
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+ EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
+}
+
+TEST_F(InputMethodControllerTest,
+ ContentIndependentMarker_DeleteExactlyOnMarker) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>1111122222333334444455555</div>",
+ "sample");
+
+ EphemeralRange marker_range = PlainTextRange(5, 10).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
+
+ // Delete exactly on the marker
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 5, 10);
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+ EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
+}
+
+TEST_F(InputMethodControllerTest, ContentDependentMarker_DeleteMiddleOfMarker) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>1111122222333334444455555</div>",
+ "sample");
+
+ EphemeralRange marker_range = PlainTextRange(5, 10).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ // Delete middle of marker
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 6, 9);
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+
+ // Verify marker was removed
+ EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
+}
+
+TEST_F(InputMethodControllerTest,
+ ContentIndependentMarker_DeleteMiddleOfMarker) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>1111122222333334444455555</div>",
+ "sample");
+
+ EphemeralRange marker_range = PlainTextRange(5, 10).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ // Delete middle of marker
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetCompositionFromExistingText(empty_ime_text_spans, 6, 9);
+ Controller().CommitText(String(""), empty_ime_text_spans, 0);
+
+ EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
+
+ EXPECT_EQ(5u, GetDocument().Markers().Markers()[0]->StartOffset());
+ EXPECT_EQ(7u, GetDocument().Markers().Markers()[0]->EndOffset());
+}
+
+TEST_F(InputMethodControllerTest,
+ ContentDependentMarker_InsertInMarkerInterior) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>1111122222333334444455555</div>",
+ "sample");
+
+ EphemeralRange marker_range = PlainTextRange(0, 5).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ marker_range = PlainTextRange(5, 10).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ marker_range = PlainTextRange(10, 15).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ EXPECT_EQ(3u, GetDocument().Markers().Markers().size());
+
+ // insert in middle of second marker
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetComposition("", empty_ime_text_spans, 7, 7);
+ Controller().CommitText(String("66666"), empty_ime_text_spans, -7);
+
+ EXPECT_EQ(2u, GetDocument().Markers().Markers().size());
+
+ EXPECT_EQ(0u, GetDocument().Markers().Markers()[0]->StartOffset());
+ EXPECT_EQ(5u, GetDocument().Markers().Markers()[0]->EndOffset());
+
+ EXPECT_EQ(15u, GetDocument().Markers().Markers()[1]->StartOffset());
+ EXPECT_EQ(20u, GetDocument().Markers().Markers()[1]->EndOffset());
+}
+
+TEST_F(InputMethodControllerTest,
+ ContentIndependentMarker_InsertInMarkerInterior) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>1111122222333334444455555</div>",
+ "sample");
+
+ EphemeralRange marker_range = PlainTextRange(0, 5).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ marker_range = PlainTextRange(5, 10).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ marker_range = PlainTextRange(10, 15).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ EXPECT_EQ(3u, GetDocument().Markers().Markers().size());
+
+ // insert in middle of second marker
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetComposition("", empty_ime_text_spans, 7, 7);
+ Controller().CommitText(String("66666"), empty_ime_text_spans, -7);
+
+ EXPECT_EQ(3u, GetDocument().Markers().Markers().size());
+
+ EXPECT_EQ(0u, GetDocument().Markers().Markers()[0]->StartOffset());
+ EXPECT_EQ(5u, GetDocument().Markers().Markers()[0]->EndOffset());
+
+ EXPECT_EQ(5u, GetDocument().Markers().Markers()[1]->StartOffset());
+ EXPECT_EQ(15u, GetDocument().Markers().Markers()[1]->EndOffset());
+
+ EXPECT_EQ(15u, GetDocument().Markers().Markers()[2]->StartOffset());
+ EXPECT_EQ(20u, GetDocument().Markers().Markers()[2]->EndOffset());
+}
+
+TEST_F(InputMethodControllerTest, ContentDependentMarker_InsertBetweenMarkers) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>1111122222333334444455555</div>",
+ "sample");
+
+ EphemeralRange marker_range = PlainTextRange(0, 5).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ marker_range = PlainTextRange(5, 15).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ marker_range = PlainTextRange(15, 20).CreateRange(*div);
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ EXPECT_EQ(3u, GetDocument().Markers().Markers().size());
+
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetComposition("", empty_ime_text_spans, 5, 5);
+ Controller().CommitText(String("77777"), empty_ime_text_spans, 0);
+
+ EXPECT_EQ(3u, GetDocument().Markers().Markers().size());
+
+ EXPECT_EQ(0u, GetDocument().Markers().Markers()[0]->StartOffset());
+ EXPECT_EQ(5u, GetDocument().Markers().Markers()[0]->EndOffset());
+
+ EXPECT_EQ(10u, GetDocument().Markers().Markers()[1]->StartOffset());
+ EXPECT_EQ(20u, GetDocument().Markers().Markers()[1]->EndOffset());
+
+ EXPECT_EQ(20u, GetDocument().Markers().Markers()[2]->StartOffset());
+ EXPECT_EQ(25u, GetDocument().Markers().Markers()[2]->EndOffset());
+}
+
+TEST_F(InputMethodControllerTest,
+ ContentIndependentMarker_InsertBetweenMarkers) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>1111122222333334444455555</div>",
+ "sample");
+
+ EphemeralRange marker_range = PlainTextRange(0, 5).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ marker_range = PlainTextRange(5, 15).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ marker_range = PlainTextRange(15, 20).CreateRange(*div);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, Color::kBlack, ImeTextSpanThickness::kThin, Color::kBlack);
+
+ EXPECT_EQ(3u, GetDocument().Markers().Markers().size());
+
+ Vector<ImeTextSpan> empty_ime_text_spans;
+ Controller().SetComposition("", empty_ime_text_spans, 5, 5);
+ Controller().CommitText(String("77777"), empty_ime_text_spans, 0);
+
+ EXPECT_EQ(3u, GetDocument().Markers().Markers().size());
+
+ EXPECT_EQ(0u, GetDocument().Markers().Markers()[0]->StartOffset());
+ EXPECT_EQ(5u, GetDocument().Markers().Markers()[0]->EndOffset());
+
+ EXPECT_EQ(10u, GetDocument().Markers().Markers()[1]->StartOffset());
+ EXPECT_EQ(20u, GetDocument().Markers().Markers()[1]->EndOffset());
+
+ EXPECT_EQ(20u, GetDocument().Markers().Markers()[2]->StartOffset());
+ EXPECT_EQ(25u, GetDocument().Markers().Markers()[2]->EndOffset());
+}
+
+TEST_F(InputMethodControllerTest,
+ CommitNotMisspellingSuggestionMarkerWithSpellCheckingDisabled) {
+ InsertHTMLElement(
+ "<div id='sample' contenteditable spellcheck='false'>text</div>",
+ "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ // Try to commit a non-misspelling suggestion marker.
+ ime_text_spans.push_back(
+ ImeTextSpan(ImeTextSpan::Type::kSuggestion, 0, 5, Color::kTransparent,
+ ImeTextSpanThickness::kNone, Color::kTransparent));
+ Controller().CommitText("hello", ime_text_spans, 1);
+
+ // The marker should have been added.
+ EXPECT_EQ(1u, GetDocument().Markers().Markers().size());
+}
+
+TEST_F(InputMethodControllerTest,
+ CommitMisspellingSuggestionMarkerWithSpellCheckingDisabled) {
+ InsertHTMLElement(
+ "<div id='sample' contenteditable spellcheck='false'>text</div>",
+ "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ // Try to commit a non-misspelling suggestion marker.
+ ime_text_spans.push_back(ImeTextSpan(
+ ImeTextSpan::Type::kMisspellingSuggestion, 0, 5, Color::kTransparent,
+ ImeTextSpanThickness::kNone, Color::kTransparent));
+ Controller().CommitText("hello", ime_text_spans, 1);
+
+ // The marker should not have been added since the div has spell checking
+ // disabled.
+ EXPECT_EQ(0u, GetDocument().Markers().Markers().size());
+}
+
+// For http://crbug.com/712761
+TEST_F(InputMethodControllerTest, TextInputTypeAtBeforeEditable) {
+ GetDocument().body()->setContentEditable("true", ASSERT_NO_EXCEPTION);
+ GetDocument().body()->focus();
+
+ // Set selection before BODY(editable).
+ GetFrame().Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(GetDocument().documentElement(), 0))
+ .Build());
+
+ EXPECT_EQ(kWebTextInputTypeContentEditable, Controller().TextInputType());
+}
+
+// http://crbug.com/721666
+TEST_F(InputMethodControllerTest, MaxLength) {
+ HTMLInputElement* input = ToHTMLInputElement(
+ InsertHTMLElement("<input id='a' maxlength='4'/>", "a"));
+
+ EXPECT_EQ(kWebTextInputTypeText, Controller().TextInputType());
+
+ Controller().SetComposition("abcde", Vector<ImeTextSpan>(), 4, 4);
+ EXPECT_STREQ("abcde", input->value().Utf8().data());
+
+ Controller().FinishComposingText(InputMethodController::kKeepSelection);
+ EXPECT_STREQ("abcd", input->value().Utf8().data());
+}
+
+TEST_F(InputMethodControllerTest, InputModeOfFocusedElement) {
+ InsertHTMLElement("<input id='a' inputmode='decimal'>", "a")->focus();
+ EXPECT_EQ(kWebTextInputModeDecimal, Controller().InputModeOfFocusedElement());
+
+ InsertHTMLElement("<input id='b' inputmode='foo'>", "b")->focus();
+ EXPECT_EQ(kWebTextInputModeDefault, Controller().InputModeOfFocusedElement());
+}
+
+TEST_F(InputMethodControllerTest, CompositionUnderlineSpansMultipleNodes) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable><b>t</b>est</div>", "sample");
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 4,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().SetCompositionFromExistingText(Vector<ImeTextSpan>(), 0, 4);
+ Controller().SetComposition("test", ime_text_spans, 0, 4);
+
+ Node* b = div->firstChild();
+ Node* text1 = b->firstChild();
+ Node* text2 = b->nextSibling();
+
+ const DocumentMarkerVector& text1_markers =
+ GetDocument().Markers().MarkersFor(text1, DocumentMarker::kComposition);
+ EXPECT_EQ(1u, text1_markers.size());
+ EXPECT_EQ(0u, text1_markers[0]->StartOffset());
+ EXPECT_EQ(1u, text1_markers[0]->EndOffset());
+
+ const DocumentMarkerVector& text2_markers =
+ GetDocument().Markers().MarkersFor(text2, DocumentMarker::kComposition);
+ EXPECT_EQ(1u, text2_markers.size());
+ EXPECT_EQ(0u, text2_markers[0]->StartOffset());
+ EXPECT_EQ(3u, text2_markers[0]->EndOffset());
+}
+
+// The following tests are for http://crbug.com/766680.
+
+TEST_F(InputMethodControllerTest, SetCompositionDeletesMarkupBeforeText) {
+ Element* div = InsertHTMLElement(
+ "<div id='div' contenteditable='true'><img />test</div>", "div");
+ // Select the contents of the div element.
+ GetFrame().Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(EphemeralRange::RangeOfContents(*div))
+ .Build());
+
+ Controller().SetComposition("t", Vector<ImeTextSpan>(), 0, 1);
+
+ EXPECT_EQ(1u, div->CountChildren());
+ Text* text = ToText(div->firstChild());
+ EXPECT_EQ("t", text->data());
+}
+
+TEST_F(InputMethodControllerTest, SetCompositionDeletesMarkupAfterText) {
+ Element* div = InsertHTMLElement(
+ "<div id='div' contenteditable='true'>test<img /></div>", "div");
+ // Select the contents of the div element.
+ GetFrame().Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(EphemeralRange::RangeOfContents(*div))
+ .Build());
+
+ Controller().SetComposition("t", Vector<ImeTextSpan>(), 0, 1);
+
+ EXPECT_EQ(1u, div->CountChildren());
+ Text* text = ToText(div->firstChild());
+ EXPECT_EQ("t", text->data());
+}
+
+TEST_F(InputMethodControllerTest,
+ SetCompositionDeletesMarkupBeforeAndAfterText) {
+ Element* div = InsertHTMLElement(
+ "<div id='div' contenteditable='true'><img />test<img /></div>", "div");
+ // Select the contents of the div element.
+ GetFrame().Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(EphemeralRange::RangeOfContents(*div))
+ .Build());
+
+ Controller().SetComposition("t", Vector<ImeTextSpan>(), 0, 1);
+
+ EXPECT_EQ(1u, div->CountChildren());
+ Text* text = ToText(div->firstChild());
+ EXPECT_EQ("t", text->data());
+}
+
+TEST_F(InputMethodControllerTest,
+ SetCompositionWithPartialGraphemeWithCompositionUnderlineDoesntCrash) {
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+
+ Vector<ImeTextSpan> ime_text_spans;
+ ime_text_spans.push_back(ImeTextSpan(ImeTextSpan::Type::kComposition, 0, 1,
+ Color(255, 0, 0),
+ ImeTextSpanThickness::kThin, 0));
+ Controller().CommitText(" ", ime_text_spans, 0);
+ // Add character U+094D: 'DEVANAGARI SIGN VIRAMA'
+ Controller().SetComposition(String::FromUTF8("\xE0\xA5\x8D"), ime_text_spans,
+ 1, 1);
+}
+
+TEST_F(
+ InputMethodControllerTest,
+ SetCompositionWithPartialGraphemeWithoutCompositionUnderlineDoesntCrash) {
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+
+ Controller().CommitText(" ", Vector<ImeTextSpan>(), 0);
+ // Add character U+094D: 'DEVANAGARI SIGN VIRAMA'
+ Controller().SetComposition(String::FromUTF8("\xE0\xA5\x8D"),
+ Vector<ImeTextSpan>(), 1, 1);
+}
+
+TEST_F(InputMethodControllerTest, SetCompositionContainingNewline) {
+ Element* div =
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+ Controller().SetComposition("Hello", Vector<ImeTextSpan>(), 5, 5);
+ Controller().SetComposition("Hello\n", Vector<ImeTextSpan>(), 6, 6);
+
+ EXPECT_EQ("Hello\n\n", PlainText(EphemeralRange(
+ Position(div, PositionAnchorType::kBeforeAnchor),
+ Position(div, PositionAnchorType::kAfterAnchor))));
+}
+
+TEST_F(InputMethodControllerTest, SetCompositionTamilVirama) {
+ Element* div =
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+
+ // Commit TAMIL LETTER CA (U+0B9A) followed by TAMIL SIGN VIRAMA (U+U0BCD)
+ Controller().CommitText(String::FromUTF8("\xE0\xAE\x9A\xE0\xAF\x8D"),
+ Vector<ImeTextSpan>(), 0);
+
+ // Open composition with TAMIL LETTER CA (U+0B9A) followed by
+ // TAMIL SIGN VIRAMA (U+U0BCD)
+ Controller().SetComposition(String::FromUTF8("\xE0\xAE\x9A\xE0\xAF\x8D"),
+ Vector<ImeTextSpan>(), 2, 2);
+ // Remove the TAMIL SIGN VIRAMA from the end of the composition
+ Controller().SetComposition(String::FromUTF8("\xE0\xAE\x9A"),
+ Vector<ImeTextSpan>(), 1, 1);
+
+ EXPECT_EQ(1u, div->CountChildren());
+ Text* text = ToText(div->firstChild());
+ EXPECT_STREQ("\xE0\xAE\x9A\xE0\xAF\x8D\xE0\xAE\x9A",
+ text->data().Utf8().data());
+
+ Range* range = GetCompositionRange();
+ EXPECT_EQ(2u, range->startOffset());
+ EXPECT_EQ(3u, range->endOffset());
+}
+
+TEST_F(InputMethodControllerTest,
+ CommitTextWithOpenCompositionAndInputEventHandlerChangingText) {
+ InsertHTMLElement("<div id='sample' contenteditable>hello</div>", "sample");
+
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('input', "
+ " event => {"
+ " const node = event.currentTarget;"
+ " node.textContent = 'HELLO WORLD';"
+ " const selection = getSelection();"
+ " selection.collapse(node.firstChild, 11);"
+ " selection.extend(node.firstChild, 11);"
+ "});");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ // Open composition on "hello".
+ Controller().SetCompositionFromExistingText(Vector<ImeTextSpan>(), 0, 5);
+
+ // Commit text, leaving the cursor at the end of the newly-inserted text.
+ // JavaScript will make the text longer by changing it to "HELLO WORLD",
+ // while trying to move the selection to the end of the string (where we
+ // should leave it).
+ Controller().CommitText("HELLO", Vector<ImeTextSpan>(), 0);
+
+ EXPECT_EQ(11, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+}
+
+TEST_F(InputMethodControllerTest,
+ CommitTextWithoutCompositionAndInputEventHandlerChangingSelection) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>hello world</div>", "sample");
+
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('input', "
+ " event => {"
+ " const node = event.currentTarget;"
+ " const selection = getSelection();"
+ " selection.collapse(node.firstChild, 0);"
+ " selection.extend(node.firstChild, 0);"
+ "});");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ // Select "hello".
+ GetFrame().Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(EphemeralRange(Position(div->firstChild(), 0),
+ Position(div->firstChild(), 5)))
+ .Build());
+
+ // Commit text, leaving the cursor at the end of the newly-inserted text.
+ // JavaScript will move the cursor back to the beginning of the
+ // "HELLO world", where it should be left.
+ Controller().CommitText("HELLO", Vector<ImeTextSpan>(), 0);
+
+ EXPECT_EQ(0, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+}
+
+TEST_F(
+ InputMethodControllerTest,
+ SetCompositionToEmptyStringWithOpenCompositionAndInputEventHandlerChangingText) {
+ InsertHTMLElement("<div id='sample' contenteditable>hello world</div>",
+ "sample");
+
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('input', "
+ " event => {"
+ " const node = event.currentTarget;"
+ " node.textContent = 'HI ';"
+ " const selection = getSelection();"
+ " selection.collapse(node.firstChild, 2);"
+ " selection.extend(node.firstChild, 2);"
+ "});");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ // Open composition on "world".
+ Controller().SetCompositionFromExistingText(Vector<ImeTextSpan>(), 6, 11);
+
+ // Delete the composition range, leaving the cursor in place. JavaScript will
+ // change the text and move the cursor after "HI", where it should be left.
+ Controller().SetComposition("", Vector<ImeTextSpan>(), 0, 0);
+
+ EXPECT_EQ(2, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+}
+
+TEST_F(InputMethodControllerTest,
+ SetCompositionWithOpenCompositionAndInputEventHandlerChangingText) {
+ InsertHTMLElement("<div id='sample' contenteditable>hello world</div>",
+ "sample");
+
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('input', "
+ " event => {"
+ " const node = event.currentTarget;"
+ " node.textContent = 'HI WORLD';"
+ " const selection = getSelection();"
+ " selection.collapse(node.firstChild, 2);"
+ " selection.extend(node.firstChild, 2);"
+ "});");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ // Open composition on "world".
+ Controller().SetCompositionFromExistingText(Vector<ImeTextSpan>(), 6, 11);
+
+ // Change the composition text, leaving the cursor at the end of the
+ // composition. JavaScript will change the text and move the cursor after
+ // "HI", where it should be left.
+ Controller().SetComposition("WORLD", Vector<ImeTextSpan>(), 5, 5);
+
+ EXPECT_EQ(2, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+}
+
+TEST_F(InputMethodControllerTest,
+ SetCompositionWithOpenCompositionAndInputEventHandlerChangingSelection) {
+ InsertHTMLElement("<div id='sample' contenteditable>hello world</div>",
+ "sample");
+
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('input', "
+ " event => {"
+ " const node = event.currentTarget;"
+ " const selection = getSelection();"
+ " selection.collapse(node.firstChild, 5);"
+ " selection.extend(node.firstChild, 5);"
+ "});");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ // Open composition on "world".
+ Controller().SetCompositionFromExistingText(Vector<ImeTextSpan>(), 6, 11);
+
+ // Change the composition text, leaving the cursor at the end of the
+ // composition. JavaScript should move the cursor after "HELLO", where it
+ // should be left.
+ Controller().SetComposition("WORLD", Vector<ImeTextSpan>(), 5, 5);
+
+ // The IME cursor update should have been ignored.
+ EXPECT_EQ(5, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+}
+
+TEST_F(InputMethodControllerTest,
+ SetCompositionToEmptyStringAndInputEventHandlerChangingSelection) {
+ InsertHTMLElement("<div id='sample' contenteditable>hello world</div>",
+ "sample");
+
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('input', "
+ " event => {"
+ " const node = event.currentTarget;"
+ " const selection = getSelection();"
+ " selection.collapse(node.firstChild, 5);"
+ " selection.extend(node.firstChild, 5);"
+ "});");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ // Open composition on "world".
+ Controller().SetCompositionFromExistingText(Vector<ImeTextSpan>(), 6, 11);
+
+ // Change the composition text to the empty string (so we end up with
+ // "hello ") and move the cursor to before "hello". JavaScript will change
+ // the text and move the cursor after "hello", where it should be left.
+ Controller().SetComposition("", Vector<ImeTextSpan>(), -6, -6);
+
+ EXPECT_EQ(5, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+}
+
+TEST_F(InputMethodControllerTest,
+ SetCompositionDeleteSelectionAndInputEventHandlerChangingSelection) {
+ Element* div = InsertHTMLElement(
+ "<div id='sample' contenteditable>hello world</div>", "sample");
+
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('input', "
+ " event => {"
+ " const node = event.currentTarget;"
+ " const selection = getSelection();"
+ " selection.collapse(node.firstChild, 5);"
+ " selection.extend(node.firstChild, 5);"
+ "});");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ // Select "world".
+ GetFrame().Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(EphemeralRange(Position(div->firstChild(), 6),
+ Position(div->firstChild(), 11)))
+ .Build());
+
+ // Call SetComposition() passing the empty string to delete the selection
+ // (so we end up with "hello ") and move the cursor to before "hello".
+ // JavaScript will change the text and move the cursor after "hello", where
+ // it should be left.
+ Controller().SetComposition("", Vector<ImeTextSpan>(), -6, -6);
+
+ EXPECT_EQ(5, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+}
+
+TEST_F(InputMethodControllerTest,
+ CommitTextWithOpenCompositionAndCompositionEndEventHandlerChangingText) {
+ InsertHTMLElement("<div id='sample' contenteditable>hello</div>", "sample");
+
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('compositionend', "
+ " event => {"
+ " const node = event.currentTarget;"
+ " node.textContent = 'HELLO WORLD';"
+ " const selection = getSelection();"
+ " selection.collapse(node.firstChild, 11);"
+ " selection.extend(node.firstChild, 11);"
+ "});");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ // Open composition on "hello".
+ Controller().SetCompositionFromExistingText(Vector<ImeTextSpan>(), 0, 5);
+
+ // Commit text, leaving the cursor at the end of the newly-inserted text.
+ // JavaScript will make the text longer by changing it to "HELLO WORLD",
+ // while trying to move the selection to the end of the string (where we
+ // should leave it).
+ Controller().CommitText("HELLO", Vector<ImeTextSpan>(), 0);
+
+ EXPECT_EQ(11, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+}
+
+TEST_F(
+ InputMethodControllerTest,
+ SetCompositionToEmptyStringWithOpenCompositionAndCompositionEndEventHandlerChangingText) {
+ InsertHTMLElement("<div id='sample' contenteditable>hello world</div>",
+ "sample");
+
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('compositionend', "
+ " event => {"
+ " const node = event.currentTarget;"
+ " node.textContent = 'HI ';"
+ " const selection = getSelection();"
+ " selection.collapse(node.firstChild, 2);"
+ " selection.extend(node.firstChild, 2);"
+ "});");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ // Open composition on "world".
+ Controller().SetCompositionFromExistingText(Vector<ImeTextSpan>(), 6, 11);
+
+ // Delete the composition range, leaving the cursor in place. JavaScript will
+ // change the text and move the cursor after "HI", where it should be left.
+ Controller().SetComposition("", Vector<ImeTextSpan>(), 0, 0);
+
+ EXPECT_EQ(2, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+}
+
+TEST_F(
+ InputMethodControllerTest,
+ SetCompositionToEmptyStringAndCompositionEndEventHandlerChangingSelection) {
+ InsertHTMLElement("<div id='sample' contenteditable>hello world</div>",
+ "sample");
+
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('compositionend', "
+ " event => {"
+ " const node = event.currentTarget;"
+ " const selection = getSelection();"
+ " selection.collapse(node.firstChild, 5);"
+ " selection.extend(node.firstChild, 5);"
+ "});");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ // Open composition on "world".
+ Controller().SetCompositionFromExistingText(Vector<ImeTextSpan>(), 6, 11);
+
+ // Change the composition text to the empty string (so we end up with
+ // "hello ") and move the cursor to before "hello". JavaScript will change
+ // the text and move the cursor after "hello", where it should be left.
+ Controller().SetComposition("", Vector<ImeTextSpan>(), -6, -6);
+
+ EXPECT_EQ(5, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+}
+
+TEST_F(InputMethodControllerTest,
+ FinishComposingTextDoNotKeepSelectionAndCompositionEndEventHandler) {
+ InsertHTMLElement("<div id='sample' contenteditable>hello world</div>",
+ "sample");
+
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('compositionend', "
+ " event => {"
+ " const node = event.currentTarget;"
+ " const selection = getSelection();"
+ " selection.collapse(node.firstChild, 5);"
+ " selection.extend(node.firstChild, 5);"
+ "});");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ // Open composition on "world".
+ Controller().SetCompositionFromExistingText(Vector<ImeTextSpan>(), 6, 11);
+
+ // JavaScript will change the text and move the cursor after "hello", where
+ // it should be left.
+ Controller().FinishComposingText(InputMethodController::kKeepSelection);
+
+ EXPECT_EQ(5, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+}
+
+TEST_F(InputMethodControllerTest,
+ FinishComposingTextKeepSelectionAndCompositionEndEventHandler) {
+ InsertHTMLElement("<div id='sample' contenteditable>hello world</div>",
+ "sample");
+
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('compositionend', "
+ " event => {"
+ " const node = event.currentTarget;"
+ " const selection = getSelection();"
+ " selection.collapse(node.firstChild, 5);"
+ " selection.extend(node.firstChild, 5);"
+ "});");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ // Open composition on "world".
+ Controller().SetCompositionFromExistingText(Vector<ImeTextSpan>(), 6, 11);
+
+ // JavaScript will change the text and move the cursor after "hello", where
+ // it should be left.
+ Controller().FinishComposingText(InputMethodController::kDoNotKeepSelection);
+
+ EXPECT_EQ(5, GetFrame()
+ .Selection()
+ .GetSelectionInDOMTree()
+ .Base()
+ .ComputeOffsetInContainerNode());
+}
+
+TEST_F(InputMethodControllerTest,
+ FinishComposingTextTooLongKeepSelectionAndInputEventHandler) {
+ HTMLInputElement* input = ToHTMLInputElement(
+ InsertHTMLElement("<input id='sample' maxlength='2'>", "sample"));
+
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('input', "
+ " event => {"
+ " const node = event.currentTarget;"
+ " node.setSelectionRange(1, 1);"
+ "});");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ input->focus();
+
+ // Open a composition that's too long for the <input> element..
+ Controller().SetComposition("hello", Vector<ImeTextSpan>(), 0, 0);
+
+ // Close out the composition, triggering the input event handler.
+ Controller().FinishComposingText(InputMethodController::kKeepSelection);
+ EXPECT_STREQ("he", input->value().Utf8().data());
+
+ // Verify that the input handler was able to properly move the selection.
+ EXPECT_EQ(1u, input->selectionStart());
+ EXPECT_EQ(1u, input->selectionEnd());
+}
+
+TEST_F(InputMethodControllerTest,
+ FinishComposingTextTooLongDoNotKeepSelectionAndInputEventHandler) {
+ HTMLInputElement* input = ToHTMLInputElement(
+ InsertHTMLElement("<input id='sample' maxlength='2'>", "sample"));
+
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('input', "
+ " event => {"
+ " const node = event.currentTarget;"
+ " node.setSelectionRange(1, 1);"
+ "});");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ input->focus();
+
+ // Open a composition that's too long for the <input> element..
+ Controller().SetComposition("hello", Vector<ImeTextSpan>(), 0, 0);
+
+ // Close out the composition, triggering the input event handler.
+ Controller().FinishComposingText(InputMethodController::kDoNotKeepSelection);
+ EXPECT_STREQ("he", input->value().Utf8().data());
+
+ // Verify that the input handler was able to properly move the selection.
+ EXPECT_EQ(1u, input->selectionStart());
+ EXPECT_EQ(1u, input->selectionEnd());
+}
+
+TEST_F(InputMethodControllerTest,
+ FinishComposingTextTooLongKeepSelectionAndCompositionEndEventHandler) {
+ HTMLInputElement* input = ToHTMLInputElement(
+ InsertHTMLElement("<input id='sample' maxlength='2'>", "sample"));
+
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('compositionend', "
+ " event => {"
+ " const node = event.currentTarget;"
+ " node.setSelectionRange(1, 1);"
+ "});");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ input->focus();
+
+ // Open a composition that's too long for the <input> element..
+ Controller().SetComposition("hello", Vector<ImeTextSpan>(), 0, 0);
+
+ // Close out the composition, triggering the compositionend event handler.
+ Controller().FinishComposingText(InputMethodController::kKeepSelection);
+ EXPECT_STREQ("he", input->value().Utf8().data());
+
+ // Verify that the compositionend handler was able to properly move the
+ // selection.
+ EXPECT_EQ(1u, input->selectionStart());
+ EXPECT_EQ(1u, input->selectionEnd());
+}
+
+TEST_F(
+ InputMethodControllerTest,
+ FinishComposingTextTooLongDoNotKeepSelectionAndCompositionEndEventHandler) {
+ HTMLInputElement* input = ToHTMLInputElement(
+ InsertHTMLElement("<input id='sample' maxlength='2'>", "sample"));
+
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.getElementById('sample').addEventListener('compositionend', "
+ " event => {"
+ " const node = event.currentTarget;"
+ " node.setSelectionRange(1, 1);"
+ "});");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ input->focus();
+
+ // Open a composition that's too long for the <input> element..
+ Controller().SetComposition("hello", Vector<ImeTextSpan>(), 0, 0);
+
+ // Close out the composition, triggering the compositionend event handler.
+ Controller().FinishComposingText(InputMethodController::kDoNotKeepSelection);
+ EXPECT_STREQ("he", input->value().Utf8().data());
+
+ // Verify that the compositionend handler was able to properly move the
+ // selection.
+ EXPECT_EQ(1u, input->selectionStart());
+ EXPECT_EQ(1u, input->selectionEnd());
+}
+
+TEST_F(InputMethodControllerTest, AutocapitalizeTextInputFlags) {
+ // This test assumes that the behavior tested in
+ // LayoutTests/fast/forms/autocapitalize.html works properly and tests the
+ // following:
+ // - The autocapitalize IDL states map properly to WebTextInputFlags for
+ // <input> elements, <textarea> elements, and editable regions
+ // - We ignore the value of the IDL attribute for password/email/URL inputs
+ // and always send None for this case.
+ Vector<std::pair<String, int>> element_and_expected_flags_pairs = {
+ {"<input type='text'>", kWebTextInputFlagAutocapitalizeSentences},
+ {"<input type='text' autocapitalize='none'>",
+ kWebTextInputFlagAutocapitalizeNone},
+ {"<input type='text' autocapitalize='characters'>",
+ kWebTextInputFlagAutocapitalizeCharacters},
+ {"<input type='text' autocapitalize='sentences'>",
+ kWebTextInputFlagAutocapitalizeSentences},
+ {"<input type='text' autocapitalize='words'>",
+ kWebTextInputFlagAutocapitalizeWords},
+
+ {"<input type='search'>", kWebTextInputFlagAutocapitalizeSentences},
+ {"<input type='search' autocapitalize='none'>",
+ kWebTextInputFlagAutocapitalizeNone},
+ {"<input type='search' autocapitalize='characters'>",
+ kWebTextInputFlagAutocapitalizeCharacters},
+ {"<input type='search' autocapitalize='sentences'>",
+ kWebTextInputFlagAutocapitalizeSentences},
+ {"<input type='search' autocapitalize='words'>",
+ kWebTextInputFlagAutocapitalizeWords},
+
+ {"<input type='email'>", kWebTextInputFlagAutocapitalizeNone},
+ {"<input type='email' autocapitalize='none'>",
+ kWebTextInputFlagAutocapitalizeNone},
+ {"<input type='email' autocapitalize='characters'>",
+ kWebTextInputFlagAutocapitalizeNone},
+ {"<input type='email' autocapitalize='sentences'>",
+ kWebTextInputFlagAutocapitalizeNone},
+ {"<input type='email' autocapitalize='words'>",
+ kWebTextInputFlagAutocapitalizeNone},
+
+ {"<input type='url'>", kWebTextInputFlagAutocapitalizeNone},
+ {"<input type='url' autocapitalize='none'>",
+ kWebTextInputFlagAutocapitalizeNone},
+ {"<input type='url' autocapitalize='characters'>",
+ kWebTextInputFlagAutocapitalizeNone},
+ {"<input type='url' autocapitalize='sentences'>",
+ kWebTextInputFlagAutocapitalizeNone},
+ {"<input type='url' autocapitalize='words'>",
+ kWebTextInputFlagAutocapitalizeNone},
+
+ {"<input type='password'>", kWebTextInputFlagAutocapitalizeNone},
+ {"<input type='password' autocapitalize='none'>",
+ kWebTextInputFlagAutocapitalizeNone},
+ {"<input type='password' autocapitalize='characters'>",
+ kWebTextInputFlagAutocapitalizeNone},
+ {"<input type='password' autocapitalize='sentences'>",
+ kWebTextInputFlagAutocapitalizeNone},
+ {"<input type='password' autocapitalize='words'>",
+ kWebTextInputFlagAutocapitalizeNone},
+
+ {"<textarea></textarea>", kWebTextInputFlagAutocapitalizeSentences},
+ {"<textarea autocapitalize='none'></textarea>",
+ kWebTextInputFlagAutocapitalizeNone},
+ {"<textarea autocapitalize='characters'></textarea>",
+ kWebTextInputFlagAutocapitalizeCharacters},
+ {"<textarea autocapitalize='sentences'></textarea>",
+ kWebTextInputFlagAutocapitalizeSentences},
+ {"<textarea autocapitalize='words'></textarea>",
+ kWebTextInputFlagAutocapitalizeWords},
+
+ {"<div contenteditable></div>", kWebTextInputFlagAutocapitalizeSentences},
+ {"<div contenteditable autocapitalize='none'></div>",
+ kWebTextInputFlagAutocapitalizeNone},
+ {"<div contenteditable autocapitalize='characters'></div>",
+ kWebTextInputFlagAutocapitalizeCharacters},
+ {"<div contenteditable autocapitalize='sentences'></div>",
+ kWebTextInputFlagAutocapitalizeSentences},
+ {"<div contenteditable autocapitalize='words'></div>",
+ kWebTextInputFlagAutocapitalizeWords},
+ };
+
+ const int autocapitalize_mask = kWebTextInputFlagAutocapitalizeNone |
+ kWebTextInputFlagAutocapitalizeCharacters |
+ kWebTextInputFlagAutocapitalizeWords |
+ kWebTextInputFlagAutocapitalizeSentences;
+
+ for (const std::pair<String, int>& element_and_expected_flags_pair :
+ element_and_expected_flags_pairs) {
+ const String& element = element_and_expected_flags_pair.first;
+ const int expected_flags = element_and_expected_flags_pair.second;
+
+ GetDocument().write(element);
+ GetDocument().UpdateStyleAndLayout();
+ ToElement(GetDocument().body()->lastChild())->focus();
+
+ EXPECT_EQ(expected_flags,
+ Controller().TextInputInfo().flags & autocapitalize_mask);
+ }
+}
+
+TEST_F(InputMethodControllerTest, ExecCommandDuringComposition) {
+ Element* div =
+ InsertHTMLElement("<div id='sample' contenteditable></div>", "sample");
+
+ // Open a composition.
+ Controller().SetComposition(String::FromUTF8("hello"), Vector<ImeTextSpan>(),
+ 5, 5);
+ // Turn on bold formatting.
+ GetDocument().execCommand("bold", false, "", ASSERT_NO_EXCEPTION);
+
+ // Extend the composition with some more text.
+ Controller().SetComposition(String::FromUTF8("helloworld"),
+ Vector<ImeTextSpan>(), 10, 10);
+
+ // "world" should be bold.
+ EXPECT_EQ("hello<b>world</b>", div->InnerHTMLAsString());
+}
+
+TEST_F(InputMethodControllerTest, SetCompositionAfterNonEditableElement) {
+ GetFrame().Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<div id='sample' contenteditable='true'>"
+ "<span contenteditable='false'>a</span>|b</div>"));
+ Element* const div = GetDocument().getElementById("sample");
+ div->focus();
+
+ // Open a composition and insert some text.
+ Controller().SetComposition(String::FromUTF8("c"), Vector<ImeTextSpan>(), 1,
+ 1);
+
+ // Add some more text to the composition.
+ Controller().SetComposition(String::FromUTF8("cd"), Vector<ImeTextSpan>(), 2,
+ 2);
+
+ EXPECT_EQ(
+ "<div contenteditable=\"true\" id=\"sample\">"
+ "<span contenteditable=\"false\">a</span>^cd|b</div>",
+ GetSelectionTextFromBody(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Controller().CompositionEphemeralRange())
+ .Build()));
+}
+
+TEST_F(InputMethodControllerTest, SetCompositionInTableCell) {
+ GetFrame().Selection().SetSelection(
+ SetSelectionTextToBody(
+ "<table id='sample' contenteditable><tr><td>a</td><td "
+ "id='td2'>|</td></tr></table>"),
+ SetSelectionOptions());
+ Element* const table = GetDocument().getElementById("sample");
+ table->focus();
+
+ Controller().SetComposition(String::FromUTF8("c"), Vector<ImeTextSpan>(), 1,
+ 1);
+
+ Element* const td2 = GetDocument().getElementById("td2");
+ const Node* const text_node = td2->firstChild();
+
+ Range* range = GetCompositionRange();
+ EXPECT_EQ(text_node, range->startContainer());
+ EXPECT_EQ(0u, range->startOffset());
+ EXPECT_EQ(text_node, range->endContainer());
+ EXPECT_EQ(1u, range->endOffset());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/inline_box_position.cc b/chromium/third_party/blink/renderer/core/editing/inline_box_position.cc
new file mode 100644
index 00000000000..98307dac0cf
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/inline_box_position.cc
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/inline_box_position.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/inline_box_traversal.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/layout/api/line_layout_block_flow.h"
+#include "third_party/blink/renderer/core/layout/layout_block.h"
+#include "third_party/blink/renderer/core/layout/layout_text.h"
+#include "third_party/blink/renderer/core/layout/line/inline_text_box.h"
+#include "third_party/blink/renderer/core/layout/line/root_inline_box.h"
+
+namespace blink {
+
+namespace {
+
+bool IsNonTextLeafChild(LayoutObject* object) {
+ if (object->SlowFirstChild())
+ return false;
+ if (object->IsText())
+ return false;
+ return true;
+}
+
+InlineTextBox* SearchAheadForBetterMatch(const LayoutText* layout_object) {
+ LayoutBlock* container = layout_object->ContainingBlock();
+ for (LayoutObject* next = layout_object->NextInPreOrder(container); next;
+ next = next->NextInPreOrder(container)) {
+ if (next->IsLayoutBlock())
+ return nullptr;
+ if (next->IsBR())
+ return nullptr;
+ if (IsNonTextLeafChild(next))
+ return nullptr;
+ if (next->IsText()) {
+ InlineTextBox* match = nullptr;
+ int min_offset = INT_MAX;
+ for (InlineTextBox* box : ToLayoutText(next)->TextBoxes()) {
+ int caret_min_offset = box->CaretMinOffset();
+ if (caret_min_offset < min_offset) {
+ match = box;
+ min_offset = caret_min_offset;
+ }
+ }
+ if (match)
+ return match;
+ }
+ }
+ return nullptr;
+}
+
+// Returns true if |inlineBox| starts different direction of embedded text ru.
+// See [1] for details.
+// [1] UNICODE BIDIRECTIONAL ALGORITHM, http://unicode.org/reports/tr9/
+bool IsStartOfDifferentDirection(const InlineBox* inline_box) {
+ InlineBox* prev_box = inline_box->PrevLeafChild();
+ if (!prev_box)
+ return true;
+ if (prev_box->Direction() == inline_box->Direction())
+ return true;
+ DCHECK_NE(prev_box->BidiLevel(), inline_box->BidiLevel());
+ return prev_box->BidiLevel() > inline_box->BidiLevel();
+}
+
+// TODO(editing-dev): This function is almost identical to
+// |IsStartOfDifferentDirection|. Try to unify them.
+bool IsEndOfDifferentDirection(const InlineBox* inline_box) {
+ const InlineBox* const next_box = inline_box->NextLeafChild();
+ if (!next_box)
+ return true;
+ return next_box->BidiLevel() >= inline_box->BidiLevel();
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> DownstreamIgnoringEditingBoundaries(
+ PositionTemplate<Strategy> position) {
+ PositionTemplate<Strategy> last_position;
+ while (!position.IsEquivalent(last_position)) {
+ last_position = position;
+ position = MostForwardCaretPosition(position, kCanCrossEditingBoundary);
+ }
+ return position;
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> UpstreamIgnoringEditingBoundaries(
+ PositionTemplate<Strategy> position) {
+ PositionTemplate<Strategy> last_position;
+ while (!position.IsEquivalent(last_position)) {
+ last_position = position;
+ position = MostBackwardCaretPosition(position, kCanCrossEditingBoundary);
+ }
+ return position;
+}
+
+InlineBoxPosition AdjustInlineBoxPositionForPrimaryDirection(
+ InlineBox* inline_box,
+ int caret_offset) {
+ if (caret_offset == inline_box->CaretRightmostOffset()) {
+ if (IsEndOfDifferentDirection(inline_box))
+ return InlineBoxPosition(inline_box, caret_offset);
+
+ const unsigned level = inline_box->NextLeafChild()->BidiLevel();
+ InlineBox* const prev_box =
+ InlineBoxTraversal::FindLeftBidiRun(*inline_box, level);
+
+ // For example, abc FED 123 ^ CBA
+ if (prev_box && prev_box->BidiLevel() == level)
+ return InlineBoxPosition(inline_box, caret_offset);
+
+ // For example, abc 123 ^ CBA
+ InlineBox* const result_box =
+ InlineBoxTraversal::FindRightBoundaryOfEntireBidiRun(*inline_box,
+ level);
+ return InlineBoxPosition(result_box, result_box->CaretRightmostOffset());
+ }
+
+ if (IsStartOfDifferentDirection(inline_box))
+ return InlineBoxPosition(inline_box, caret_offset);
+
+ const unsigned level = inline_box->PrevLeafChild()->BidiLevel();
+ InlineBox* const next_box =
+ InlineBoxTraversal::FindRightBidiRun(*inline_box, level);
+
+ if (next_box && next_box->BidiLevel() == level)
+ return InlineBoxPosition(inline_box, caret_offset);
+
+ InlineBox* const result_box =
+ InlineBoxTraversal::FindLeftBoundaryOfEntireBidiRun(*inline_box, level);
+ return InlineBoxPosition(result_box, result_box->CaretLeftmostOffset());
+}
+
+InlineBoxPosition AdjustInlineBoxPositionForTextDirectionInternal(
+ InlineBox* inline_box,
+ int caret_offset,
+ UnicodeBidi unicode_bidi) {
+ DCHECK(caret_offset == inline_box->CaretLeftmostOffset() ||
+ caret_offset == inline_box->CaretRightmostOffset());
+
+ const TextDirection primary_direction =
+ inline_box->Root().Block().Style()->Direction();
+ if (inline_box->Direction() == primary_direction)
+ return AdjustInlineBoxPositionForPrimaryDirection(inline_box, caret_offset);
+
+ if (unicode_bidi == UnicodeBidi::kPlaintext)
+ return InlineBoxPosition(inline_box, caret_offset);
+
+ const unsigned char level = inline_box->BidiLevel();
+ if (caret_offset == inline_box->CaretLeftmostOffset()) {
+ InlineBox* const prev_box = inline_box->PrevLeafChildIgnoringLineBreak();
+ if (!prev_box || prev_box->BidiLevel() < level) {
+ // Left edge of a secondary run. Set to the right edge of the entire
+ // run.
+ InlineBox* const result_box =
+ InlineBoxTraversal::FindRightBoundaryOfEntireBidiRunIgnoringLineBreak(
+ *inline_box, level);
+ return InlineBoxPosition(result_box, result_box->CaretRightmostOffset());
+ }
+
+ if (prev_box->BidiLevel() <= level)
+ return InlineBoxPosition(inline_box, inline_box->CaretLeftmostOffset());
+ // Right edge of a "tertiary" run. Set to the left edge of that run.
+ InlineBox* const result_box =
+ InlineBoxTraversal::FindLeftBoundaryOfBidiRunIgnoringLineBreak(
+ *inline_box, level);
+ return InlineBoxPosition(result_box, result_box->CaretLeftmostOffset());
+ }
+
+ InlineBox* const next_box = inline_box->NextLeafChildIgnoringLineBreak();
+ if (!next_box || next_box->BidiLevel() < level) {
+ // Right edge of a secondary run. Set to the left edge of the entire
+ // run.
+ InlineBox* const result_box =
+ InlineBoxTraversal::FindLeftBoundaryOfEntireBidiRunIgnoringLineBreak(
+ *inline_box, level);
+ return InlineBoxPosition(result_box, result_box->CaretLeftmostOffset());
+ }
+
+ if (next_box->BidiLevel() <= level)
+ return InlineBoxPosition(inline_box, inline_box->CaretRightmostOffset());
+
+ // Left edge of a "tertiary" run. Set to the right edge of that run.
+ InlineBox* const result_box =
+ InlineBoxTraversal::FindRightBoundaryOfBidiRunIgnoringLineBreak(
+ *inline_box, level);
+ return InlineBoxPosition(result_box, result_box->CaretRightmostOffset());
+}
+
+// Returns true if |caret_offset| is at edge of |box| based on |affinity|.
+// |caret_offset| must be either |box.CaretMinOffset()| or
+// |box.CaretMaxOffset()|.
+bool IsCaretAtEdgeOfInlineTextBox(int caret_offset,
+ const InlineTextBox& box,
+ TextAffinity affinity) {
+ if (caret_offset == box.CaretMinOffset())
+ return affinity == TextAffinity::kDownstream;
+ DCHECK_EQ(caret_offset, box.CaretMaxOffset());
+ if (affinity == TextAffinity::kUpstream)
+ return true;
+ return box.NextLeafChild() && box.NextLeafChild()->IsLineBreak();
+}
+
+InlineBoxPosition ComputeInlineBoxPositionForTextNode(
+ const LayoutText* text_layout_object,
+ int caret_offset,
+ TextAffinity affinity) {
+ // TODO(editing-dev): Add the following DCHECK when ready.
+ // DCHECK(CanUseInlineBox(*text_layout_object));
+
+ InlineBox* inline_box = nullptr;
+ InlineTextBox* candidate = nullptr;
+
+ for (InlineTextBox* box : text_layout_object->TextBoxes()) {
+ int caret_min_offset = box->CaretMinOffset();
+ int caret_max_offset = box->CaretMaxOffset();
+
+ if (caret_offset < caret_min_offset || caret_offset > caret_max_offset ||
+ (caret_offset == caret_max_offset && box->IsLineBreak()))
+ continue;
+
+ if (caret_offset > caret_min_offset && caret_offset < caret_max_offset)
+ return InlineBoxPosition(box, caret_offset);
+
+ if (IsCaretAtEdgeOfInlineTextBox(caret_offset, *box, affinity)) {
+ inline_box = box;
+ break;
+ }
+
+ candidate = box;
+ }
+ if (candidate && candidate == text_layout_object->LastTextBox() &&
+ affinity == TextAffinity::kDownstream) {
+ inline_box = SearchAheadForBetterMatch(text_layout_object);
+ if (inline_box)
+ caret_offset = inline_box->CaretMinOffset();
+ }
+ if (!inline_box)
+ inline_box = candidate;
+
+ if (!inline_box)
+ return InlineBoxPosition();
+ return AdjustInlineBoxPositionForTextDirection(
+ inline_box, caret_offset, text_layout_object->Style()->GetUnicodeBidi());
+}
+
+InlineBoxPosition ComputeInlineBoxPositionForAtomicInline(
+ const LayoutObject* layout_object,
+ int caret_offset) {
+ // TODO(editing-dev): Add the following DCHECK when ready.
+ // DCHECK(CanUseInlineBox(*layout_object);
+ DCHECK(layout_object->IsBox());
+ InlineBox* const inline_box = ToLayoutBox(layout_object)->InlineBoxWrapper();
+ if (!inline_box)
+ return InlineBoxPosition();
+ if ((caret_offset > inline_box->CaretMinOffset() &&
+ caret_offset < inline_box->CaretMaxOffset()))
+ return InlineBoxPosition(inline_box, caret_offset);
+ return AdjustInlineBoxPositionForTextDirection(
+ inline_box, caret_offset, layout_object->Style()->GetUnicodeBidi());
+}
+
+template <typename Strategy>
+PositionWithAffinityTemplate<Strategy> ComputeInlineAdjustedPositionAlgorithm(
+ const PositionWithAffinityTemplate<Strategy>&);
+
+template <typename Strategy>
+PositionWithAffinityTemplate<Strategy> AdjustBlockFlowPositionToInline(
+ const PositionTemplate<Strategy>& position) {
+ // Try a visually equivalent position with possibly opposite editability. This
+ // helps in case |position| is in an editable block but surrounded by
+ // non-editable positions. It acts to negate the logic at the beginning of
+ // |LayoutObject::CreatePositionWithAffinity()|.
+ const PositionTemplate<Strategy>& downstream_equivalent =
+ DownstreamIgnoringEditingBoundaries(position);
+ if (downstream_equivalent != position) {
+ return ComputeInlineAdjustedPositionAlgorithm(
+ PositionWithAffinityTemplate<Strategy>(downstream_equivalent,
+ TextAffinity::kUpstream));
+ }
+ const PositionTemplate<Strategy>& upstream_equivalent =
+ UpstreamIgnoringEditingBoundaries(position);
+ if (upstream_equivalent == position ||
+ DownstreamIgnoringEditingBoundaries(upstream_equivalent) == position)
+ return PositionWithAffinityTemplate<Strategy>();
+
+ return ComputeInlineAdjustedPositionAlgorithm(
+ PositionWithAffinityTemplate<Strategy>(upstream_equivalent,
+ TextAffinity::kUpstream));
+}
+
+template <typename Strategy>
+PositionWithAffinityTemplate<Strategy> ComputeInlineAdjustedPositionAlgorithm(
+ const PositionWithAffinityTemplate<Strategy>& position) {
+ // TODO(yoichio): We don't assume |position| is canonicalized no longer and
+ // there are few cases failing to compute. Fix it: crbug.com/812535.
+ DCHECK(!position.AnchorNode()->IsShadowRoot()) << position;
+ DCHECK(position.GetPosition().AnchorNode()->GetLayoutObject()) << position;
+ const LayoutObject& layout_object =
+ *position.GetPosition().AnchorNode()->GetLayoutObject();
+
+ if (layout_object.IsText())
+ return position;
+
+ if (layout_object.IsAtomicInlineLevel()) {
+ // TODO(crbug.com/567964): Change the following branch to DCHECK once fixed.
+ if (!layout_object.IsInline())
+ return PositionWithAffinityTemplate<Strategy>();
+ return position;
+ }
+
+ if (!layout_object.IsLayoutBlockFlow() ||
+ !CanHaveChildrenForEditing(position.AnchorNode()) ||
+ !HasRenderedNonAnonymousDescendantsWithHeight(&layout_object))
+ return PositionWithAffinityTemplate<Strategy>();
+ return AdjustBlockFlowPositionToInline(position.GetPosition());
+}
+
+template <typename Strategy>
+InlineBoxPosition ComputeInlineBoxPositionForInlineAdjustedPositionAlgorithm(
+ const PositionWithAffinityTemplate<Strategy>& adjusted) {
+ const PositionTemplate<Strategy>& position = adjusted.GetPosition();
+ DCHECK(!position.AnchorNode()->IsShadowRoot()) << adjusted;
+ DCHECK(position.AnchorNode()->GetLayoutObject()) << adjusted;
+ const LayoutObject& layout_object = *position.AnchorNode()->GetLayoutObject();
+ const int caret_offset = position.ComputeEditingOffset();
+
+ if (layout_object.IsText()) {
+ return ComputeInlineBoxPositionForTextNode(
+ &ToLayoutText(layout_object), caret_offset, adjusted.Affinity());
+ }
+
+ DCHECK(layout_object.IsAtomicInlineLevel());
+ DCHECK(layout_object.IsInline());
+ return ComputeInlineBoxPositionForAtomicInline(&layout_object, caret_offset);
+}
+
+template <typename Strategy>
+InlineBoxPosition ComputeInlineBoxPositionTemplate(
+ const PositionWithAffinityTemplate<Strategy>& position) {
+ const PositionWithAffinityTemplate<Strategy> adjusted =
+ ComputeInlineAdjustedPosition(position);
+ if (adjusted.IsNull())
+ return InlineBoxPosition();
+ return ComputeInlineBoxPositionForInlineAdjustedPosition(adjusted);
+}
+
+} // namespace
+
+// TODO(xiaochengh): Migrate current callers of ComputeInlineBoxPosition to
+// ComputeInlineAdjustedPosition() instead.
+
+InlineBoxPosition ComputeInlineBoxPosition(
+ const PositionWithAffinity& position) {
+ return ComputeInlineBoxPositionTemplate<EditingStrategy>(position);
+}
+
+InlineBoxPosition ComputeInlineBoxPosition(
+ const PositionInFlatTreeWithAffinity& position) {
+ return ComputeInlineBoxPositionTemplate<EditingInFlatTreeStrategy>(position);
+}
+
+InlineBoxPosition ComputeInlineBoxPosition(const VisiblePosition& position) {
+ DCHECK(position.IsValid()) << position;
+ return ComputeInlineBoxPosition(position.ToPositionWithAffinity());
+}
+
+PositionWithAffinity ComputeInlineAdjustedPosition(
+ const PositionWithAffinity& position) {
+ return ComputeInlineAdjustedPositionAlgorithm(position);
+}
+
+PositionInFlatTreeWithAffinity ComputeInlineAdjustedPosition(
+ const PositionInFlatTreeWithAffinity& position) {
+ return ComputeInlineAdjustedPositionAlgorithm(position);
+}
+
+PositionWithAffinity ComputeInlineAdjustedPosition(
+ const VisiblePosition& position) {
+ DCHECK(position.IsValid()) << position;
+ return ComputeInlineAdjustedPositionAlgorithm(
+ position.ToPositionWithAffinity());
+}
+
+InlineBoxPosition ComputeInlineBoxPositionForInlineAdjustedPosition(
+ const PositionWithAffinity& position) {
+ return ComputeInlineBoxPositionForInlineAdjustedPositionAlgorithm(position);
+}
+
+InlineBoxPosition ComputeInlineBoxPositionForInlineAdjustedPosition(
+ const PositionInFlatTreeWithAffinity& position) {
+ return ComputeInlineBoxPositionForInlineAdjustedPositionAlgorithm(position);
+}
+
+InlineBoxPosition AdjustInlineBoxPositionForTextDirection(
+ InlineBox* inline_box,
+ int caret_offset,
+ UnicodeBidi unicode_bidi) {
+ return AdjustInlineBoxPositionForTextDirectionInternal(
+ inline_box, caret_offset, unicode_bidi);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/inline_box_position.h b/chromium/third_party/blink/renderer/core/editing/inline_box_position.h
new file mode 100644
index 00000000000..d976b94cb87
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/inline_box_position.h
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2004 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_INLINE_BOX_POSITION_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_INLINE_BOX_POSITION_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+
+namespace blink {
+
+class InlineBox;
+enum class UnicodeBidi : unsigned;
+
+struct InlineBoxPosition {
+ STACK_ALLOCATED();
+
+ const InlineBox* const inline_box;
+ const int offset_in_box;
+
+ InlineBoxPosition() : inline_box(nullptr), offset_in_box(0) {}
+
+ InlineBoxPosition(const InlineBox* inline_box, int offset_in_box)
+ : inline_box(inline_box), offset_in_box(offset_in_box) {
+ DCHECK(inline_box);
+ DCHECK_GE(offset_in_box, 0);
+ }
+
+ bool operator==(const InlineBoxPosition& other) const {
+ return inline_box == other.inline_box &&
+ offset_in_box == other.offset_in_box;
+ }
+
+ bool operator!=(const InlineBoxPosition& other) const {
+ return !operator==(other);
+ }
+};
+
+// TODO(yoichio): ComputeInlineBoxPosition returns null if position is at the
+// end of line and We fixed LocalCaretRectOfPosition for such position with
+// NeedsLineEndAdjustment and NextLinePositionOf.
+// We should include the fix into ComputeInlineBoxPosition however
+// SelectionModifierCharacter and SelectionModifierWord
+// depend on the null-line-end behavior of CIBP.
+// Move the fix into the CIBP while fixing the modifier functions.
+CORE_EXPORT InlineBoxPosition
+ComputeInlineBoxPosition(const PositionWithAffinity&);
+CORE_EXPORT InlineBoxPosition
+ComputeInlineBoxPosition(const PositionInFlatTreeWithAffinity&);
+CORE_EXPORT InlineBoxPosition ComputeInlineBoxPosition(const VisiblePosition&);
+
+PositionWithAffinity ComputeInlineAdjustedPosition(const PositionWithAffinity&);
+PositionInFlatTreeWithAffinity ComputeInlineAdjustedPosition(
+ const PositionInFlatTreeWithAffinity&);
+PositionWithAffinity ComputeInlineAdjustedPosition(const VisiblePosition&);
+
+InlineBoxPosition ComputeInlineBoxPositionForInlineAdjustedPosition(
+ const PositionWithAffinity&);
+InlineBoxPosition ComputeInlineBoxPositionForInlineAdjustedPosition(
+ const PositionInFlatTreeWithAffinity&);
+
+InlineBoxPosition AdjustInlineBoxPositionForTextDirection(InlineBox*,
+ int,
+ UnicodeBidi);
+
+// The print for |InlineBoxPosition| is available only for testing
+// in "webkit_unit_tests", and implemented in
+// "core/editing/InlineBoxPositionTest.cpp".
+std::ostream& operator<<(std::ostream&, const InlineBoxPosition&);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_INLINE_BOX_POSITION_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/inline_box_position_test.cc b/chromium/third_party/blink/renderer/core/editing/inline_box_position_test.cc
new file mode 100644
index 00000000000..5c089698041
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/inline_box_position_test.cc
@@ -0,0 +1,52 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/inline_box_position.h"
+
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/layout/line/inline_text_box.h"
+
+namespace blink {
+
+std::ostream& operator<<(std::ostream& ostream,
+ const InlineBoxPosition& inline_box_position) {
+ if (!inline_box_position.inline_box)
+ return ostream << "null";
+ return ostream
+ << inline_box_position.inline_box->GetLineLayoutItem().GetNode() << "@"
+ << inline_box_position.offset_in_box;
+}
+
+class InlineBoxPositionTest : public EditingTestBase {};
+
+TEST_F(InlineBoxPositionTest, ComputeInlineBoxPositionBidiIsolate) {
+ // "|" is bidi-level 0, and "foo" and "bar" are bidi-level 2
+ SetBodyContent(
+ "|<span id=sample style='unicode-bidi: isolate;'>foo<br>bar</span>|");
+
+ Element* sample = GetDocument().getElementById("sample");
+ Node* text = sample->firstChild();
+
+ const InlineBoxPosition& actual = ComputeInlineBoxPosition(
+ PositionWithAffinity(Position(text, 0), TextAffinity::kDownstream));
+ EXPECT_EQ(ToLayoutText(text->GetLayoutObject())->FirstTextBox(),
+ actual.inline_box);
+}
+
+// http://crbug.com/716093
+TEST_F(InlineBoxPositionTest, ComputeInlineBoxPositionMixedEditable) {
+ SetBodyContent(
+ "<div contenteditable id=sample>abc<input contenteditable=false></div>");
+ Element* const sample = GetDocument().getElementById("sample");
+
+ const InlineBoxPosition& actual =
+ ComputeInlineBoxPosition(PositionWithAffinity(
+ Position::LastPositionInNode(*sample), TextAffinity::kDownstream));
+ // Should not be in infinite-loop
+ EXPECT_EQ(nullptr, actual.inline_box);
+ EXPECT_EQ(0, actual.offset_in_box);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/inline_box_traversal.cc b/chromium/third_party/blink/renderer/core/editing/inline_box_traversal.cc
new file mode 100644
index 00000000000..9be08d5f34f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/inline_box_traversal.cc
@@ -0,0 +1,137 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/inline_box_traversal.h"
+
+#include "third_party/blink/renderer/core/layout/line/inline_box.h"
+
+namespace blink {
+
+namespace {
+
+// "Left" Traversal strategy
+struct TraverseLeft {
+ STATIC_ONLY(TraverseLeft);
+
+ static InlineBox* Forward(const InlineBox& inline_box) {
+ return inline_box.PrevLeafChild();
+ }
+};
+
+// "Left" Traversal strategy ignoring line break
+struct TraverseLeftIgnoringLineBreak {
+ STATIC_ONLY(TraverseLeftIgnoringLineBreak);
+
+ static InlineBox* Forward(const InlineBox& inline_box) {
+ return inline_box.PrevLeafChildIgnoringLineBreak();
+ }
+};
+
+// "Right" Traversal strategy
+struct TraverseRight {
+ STATIC_ONLY(TraverseRight);
+
+ static InlineBox* Forward(const InlineBox& inline_box) {
+ return inline_box.NextLeafChild();
+ }
+};
+
+// "Right" Traversal strategy ignoring line break
+struct TraverseRightIgnoringLineBreak {
+ STATIC_ONLY(TraverseRightIgnoringLineBreak);
+
+ static InlineBox* Forward(const InlineBox& inline_box) {
+ return inline_box.NextLeafChildIgnoringLineBreak();
+ }
+};
+
+template <typename TraversalStrategy>
+InlineBox* FindBidiRun(const InlineBox& start, unsigned bidi_level) {
+ for (InlineBox* runner = TraversalStrategy::Forward(start); runner;
+ runner = TraversalStrategy::Forward(*runner)) {
+ if (runner->BidiLevel() <= bidi_level)
+ return runner;
+ }
+ return nullptr;
+}
+
+template <typename TraversalStrategy>
+InlineBox* FindBoudnaryOfBidiRun(const InlineBox& start, unsigned bidi_level) {
+ InlineBox* result = const_cast<InlineBox*>(&start);
+ for (InlineBox* runner = TraversalStrategy::Forward(start); runner;
+ runner = TraversalStrategy::Forward(*runner)) {
+ if (runner->BidiLevel() <= bidi_level)
+ return result;
+ result = runner;
+ }
+ return result;
+}
+
+template <typename TraversalStrategy>
+InlineBox* FindBoudnaryOfEntireBidiRun(const InlineBox& start,
+ unsigned bidi_level) {
+ InlineBox* result = const_cast<InlineBox*>(&start);
+ for (InlineBox* runner = TraversalStrategy::Forward(start); runner;
+ runner = TraversalStrategy::Forward(*runner)) {
+ if (runner->BidiLevel() < bidi_level)
+ return result;
+ result = runner;
+ }
+ return result;
+}
+
+} // namespace
+
+InlineBox* InlineBoxTraversal::FindLeftBidiRun(const InlineBox& box,
+ unsigned bidi_level) {
+ return FindBidiRun<TraverseLeft>(box, bidi_level);
+}
+
+InlineBox* InlineBoxTraversal::FindRightBidiRun(const InlineBox& box,
+ unsigned bidi_level) {
+ return FindBidiRun<TraverseRight>(box, bidi_level);
+}
+
+InlineBox* InlineBoxTraversal::FindLeftBoundaryOfBidiRunIgnoringLineBreak(
+ const InlineBox& inline_box,
+ unsigned bidi_level) {
+ return FindBoudnaryOfBidiRun<TraverseLeftIgnoringLineBreak>(inline_box,
+ bidi_level);
+}
+
+InlineBox* InlineBoxTraversal::FindLeftBoundaryOfEntireBidiRun(
+ const InlineBox& inline_box,
+ unsigned bidi_level) {
+ return FindBoudnaryOfEntireBidiRun<TraverseLeft>(inline_box, bidi_level);
+}
+
+InlineBox* InlineBoxTraversal::FindLeftBoundaryOfEntireBidiRunIgnoringLineBreak(
+ const InlineBox& inline_box,
+ unsigned bidi_level) {
+ return FindBoudnaryOfEntireBidiRun<TraverseLeftIgnoringLineBreak>(inline_box,
+ bidi_level);
+}
+
+InlineBox* InlineBoxTraversal::FindRightBoundaryOfBidiRunIgnoringLineBreak(
+ const InlineBox& inline_box,
+ unsigned bidi_level) {
+ return FindBoudnaryOfBidiRun<TraverseRightIgnoringLineBreak>(inline_box,
+ bidi_level);
+}
+
+InlineBox* InlineBoxTraversal::FindRightBoundaryOfEntireBidiRun(
+ const InlineBox& inline_box,
+ unsigned bidi_level) {
+ return FindBoudnaryOfEntireBidiRun<TraverseRight>(inline_box, bidi_level);
+}
+
+InlineBox*
+InlineBoxTraversal::FindRightBoundaryOfEntireBidiRunIgnoringLineBreak(
+ const InlineBox& inline_box,
+ unsigned bidi_level) {
+ return FindBoudnaryOfEntireBidiRun<TraverseRightIgnoringLineBreak>(
+ inline_box, bidi_level);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/inline_box_traversal.h b/chromium/third_party/blink/renderer/core/editing/inline_box_traversal.h
new file mode 100644
index 00000000000..e05df0f77cd
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/inline_box_traversal.h
@@ -0,0 +1,50 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_INLINE_BOX_TRAVERSAL_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_INLINE_BOX_TRAVERSAL_H_
+
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+
+namespace blink {
+
+class InlineBox;
+
+// This class provides common traveral functions on list of |InlineBox|.
+class InlineBoxTraversal final {
+ STATIC_ONLY(InlineBoxTraversal);
+
+ public:
+ // TODO(yosin): We should take |bidi_level| from |InlineBox::BidiLevel()|,
+ // once all call sites satisfy it.
+
+ // Returns |InlineBox| which is less than or equal to |bidi_level| of
+ // left/right of specified |InlineBox|.
+ static InlineBox* FindLeftBidiRun(const InlineBox&, unsigned bidi_level);
+ static InlineBox* FindRightBidiRun(const InlineBox&, unsigned bidi_level);
+
+ // Find left boundary variations
+ static InlineBox* FindLeftBoundaryOfBidiRunIgnoringLineBreak(
+ const InlineBox&,
+ unsigned bidi_level);
+ static InlineBox* FindLeftBoundaryOfEntireBidiRun(const InlineBox&,
+ unsigned bidi_level);
+ static InlineBox* FindLeftBoundaryOfEntireBidiRunIgnoringLineBreak(
+ const InlineBox&,
+ unsigned bidi_level);
+
+ // Find right boundary variations
+ static InlineBox* FindRightBoundaryOfBidiRunIgnoringLineBreak(
+ const InlineBox&,
+ unsigned bidi_level);
+ static InlineBox* FindRightBoundaryOfEntireBidiRun(const InlineBox&,
+ unsigned bidi_level);
+ static InlineBox* FindRightBoundaryOfEntireBidiRunIgnoringLineBreak(
+ const InlineBox&,
+ unsigned bidi_level);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_INLINE_BOX_TRAVERSAL_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/input_mode_names.json5 b/chromium/third_party/blink/renderer/core/editing/input_mode_names.json5
new file mode 100644
index 00000000000..e210b74d5c8
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/input_mode_names.json5
@@ -0,0 +1,17 @@
+{
+ metadata: {
+ namespace: "InputMode",
+ export: "CORE_EXPORT",
+ },
+
+ data: [
+ "none",
+ "text",
+ "tel",
+ "url",
+ "email",
+ "numeric",
+ "decimal",
+ "search",
+ ],
+}
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/backwards_character_iterator.cc b/chromium/third_party/blink/renderer/core/editing/iterators/backwards_character_iterator.cc
new file mode 100644
index 00000000000..097d51ce2ea
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/backwards_character_iterator.cc
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012 Apple Inc. All
+ * rights reserved.
+ * Copyright (C) 2005 Alexey Proskuryakov.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/iterators/backwards_character_iterator.h"
+
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+
+namespace blink {
+
+template <typename Strategy>
+BackwardsCharacterIteratorAlgorithm<Strategy>::
+ BackwardsCharacterIteratorAlgorithm(
+ const EphemeralRangeTemplate<Strategy>& range,
+ const TextIteratorBehavior& behavior)
+ : offset_(0),
+ run_offset_(0),
+ at_break_(true),
+ text_iterator_(range, behavior) {
+ while (!AtEnd() && !text_iterator_.length())
+ text_iterator_.Advance();
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy>
+BackwardsCharacterIteratorAlgorithm<Strategy>::EndPosition() const {
+ if (!text_iterator_.AtEnd()) {
+ if (text_iterator_.length() > 1) {
+ const Node* n = text_iterator_.StartContainer();
+ return PositionTemplate<Strategy>::EditingPositionOf(
+ n, text_iterator_.EndOffset() - run_offset_);
+ }
+ DCHECK(!run_offset_);
+ }
+ return text_iterator_.EndPosition();
+}
+
+template <typename Strategy>
+void BackwardsCharacterIteratorAlgorithm<Strategy>::Advance(int count) {
+ if (count <= 0) {
+ DCHECK(!count);
+ return;
+ }
+
+ at_break_ = false;
+
+ int remaining = text_iterator_.length() - run_offset_;
+ if (count < remaining) {
+ run_offset_ += count;
+ offset_ += count;
+ return;
+ }
+
+ count -= remaining;
+ offset_ += remaining;
+
+ for (text_iterator_.Advance(); !AtEnd(); text_iterator_.Advance()) {
+ int run_length = text_iterator_.length();
+ if (!run_length) {
+ at_break_ = true;
+ } else {
+ if (count < run_length) {
+ run_offset_ = count;
+ offset_ += count;
+ return;
+ }
+
+ count -= run_length;
+ offset_ += run_length;
+ }
+ }
+
+ at_break_ = true;
+ run_offset_ = 0;
+}
+
+template class CORE_TEMPLATE_EXPORT
+ BackwardsCharacterIteratorAlgorithm<EditingStrategy>;
+template class CORE_TEMPLATE_EXPORT
+ BackwardsCharacterIteratorAlgorithm<EditingInFlatTreeStrategy>;
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/backwards_character_iterator.h b/chromium/third_party/blink/renderer/core/editing/iterators/backwards_character_iterator.h
new file mode 100644
index 00000000000..673e1206a54
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/backwards_character_iterator.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2004, 2006, 2009 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_BACKWARDS_CHARACTER_ITERATOR_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_BACKWARDS_CHARACTER_ITERATOR_H_
+
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator.h"
+#include "third_party/blink/renderer/platform/heap/heap.h"
+
+namespace blink {
+
+template <typename Strategy>
+class BackwardsCharacterIteratorAlgorithm {
+ STACK_ALLOCATED();
+
+ public:
+ BackwardsCharacterIteratorAlgorithm(
+ const EphemeralRangeTemplate<Strategy>&,
+ const TextIteratorBehavior& = TextIteratorBehavior());
+
+ void Advance(int);
+
+ bool AtEnd() const { return text_iterator_.AtEnd(); }
+
+ PositionTemplate<Strategy> EndPosition() const;
+
+ private:
+ int offset_;
+ int run_offset_;
+ bool at_break_;
+
+ SimplifiedBackwardsTextIteratorAlgorithm<Strategy> text_iterator_;
+};
+
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ BackwardsCharacterIteratorAlgorithm<EditingStrategy>;
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ BackwardsCharacterIteratorAlgorithm<EditingInFlatTreeStrategy>;
+
+using BackwardsCharacterIterator =
+ BackwardsCharacterIteratorAlgorithm<EditingStrategy>;
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_BACKWARDS_CHARACTER_ITERATOR_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/backwards_text_buffer.cc b/chromium/third_party/blink/renderer/core/editing/iterators/backwards_text_buffer.cc
new file mode 100644
index 00000000000..f3f9739b23f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/backwards_text_buffer.cc
@@ -0,0 +1,25 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/iterators/backwards_text_buffer.h"
+
+namespace blink {
+
+const UChar* BackwardsTextBuffer::Data() const {
+ return BufferEnd() - Size();
+}
+
+UChar* BackwardsTextBuffer::CalcDestination(size_t length) {
+ DCHECK_LE(Size() + length, Capacity());
+ return BufferEnd() - Size() - length;
+}
+
+void BackwardsTextBuffer::ShiftData(size_t old_capacity) {
+ DCHECK_LE(old_capacity, Capacity());
+ DCHECK_LE(Size(), old_capacity);
+ std::copy_backward(BufferBegin() + old_capacity - Size(),
+ BufferBegin() + old_capacity, BufferEnd());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/backwards_text_buffer.h b/chromium/third_party/blink/renderer/core/editing/iterators/backwards_text_buffer.h
new file mode 100644
index 00000000000..7341ae27188
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/backwards_text_buffer.h
@@ -0,0 +1,29 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_BACKWARDS_TEXT_BUFFER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_BACKWARDS_TEXT_BUFFER_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_buffer_base.h"
+
+namespace blink {
+
+class CORE_EXPORT BackwardsTextBuffer final : public TextBufferBase {
+ STACK_ALLOCATED();
+
+ public:
+ BackwardsTextBuffer() = default;
+ const UChar* Data() const override;
+
+ private:
+ UChar* CalcDestination(size_t length) override;
+ void ShiftData(size_t old_capacity) override;
+
+ DISALLOW_COPY_AND_ASSIGN(BackwardsTextBuffer);
+};
+
+} // namespace blink
+
+#endif // TextBuffer_h
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/backwards_text_buffer_test.cc b/chromium/third_party/blink/renderer/core/editing/iterators/backwards_text_buffer_test.cc
new file mode 100644
index 00000000000..7111929aa7f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/backwards_text_buffer_test.cc
@@ -0,0 +1,64 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/iterators/backwards_text_buffer.h"
+
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class BackwardsTextBufferTest : public EditingTestBase {};
+
+TEST_F(BackwardsTextBufferTest, pushCharacters) {
+ BackwardsTextBuffer buffer;
+
+ // Basic tests.
+ buffer.PushCharacters('a', 1);
+ buffer.PushCharacters(1u, 0);
+ buffer.PushCharacters('#', 2);
+ buffer.PushCharacters('\0', 1);
+ EXPECT_EQ('\0', buffer[0]);
+ EXPECT_EQ('#', buffer[1]);
+ EXPECT_EQ('#', buffer[2]);
+ EXPECT_EQ('a', buffer[3]);
+
+ // Tests with buffer reallocation.
+ buffer.PushCharacters('A', 4096);
+ EXPECT_EQ('A', buffer[0]);
+ EXPECT_EQ('A', buffer[4095]);
+ EXPECT_EQ('\0', buffer[4096]);
+ EXPECT_EQ('#', buffer[4097]);
+ EXPECT_EQ('#', buffer[4098]);
+ EXPECT_EQ('a', buffer[4099]);
+}
+
+TEST_F(BackwardsTextBufferTest, pushRange) {
+ BackwardsTextBuffer buffer;
+
+ // Basic tests.
+ buffer.PushRange("ababc", 1);
+ buffer.PushRange((UChar*)nullptr, 0);
+ buffer.PushRange("#@", 2);
+ UChar ch = 'x';
+ buffer.PushRange(&ch, 1);
+ EXPECT_EQ('x', buffer[0]);
+ EXPECT_EQ('#', buffer[1]);
+ EXPECT_EQ('@', buffer[2]);
+ EXPECT_EQ('a', buffer[3]);
+
+ // Tests with buffer reallocation.
+ Vector<UChar> chunk(4096);
+ for (unsigned i = 0; i < chunk.size(); ++i)
+ chunk[i] = i % 256;
+ buffer.PushRange(chunk.data(), chunk.size());
+ EXPECT_EQ(0, buffer[0]);
+ EXPECT_EQ(1111 % 256, buffer[1111]);
+ EXPECT_EQ(255, buffer[4095]);
+ EXPECT_EQ('x', buffer[4096]);
+ EXPECT_EQ('#', buffer[4097]);
+ EXPECT_EQ('@', buffer[4098]);
+ EXPECT_EQ('a', buffer[4099]);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/bit_stack.cc b/chromium/third_party/blink/renderer/core/editing/iterators/bit_stack.cc
new file mode 100644
index 00000000000..4a3860acfe4
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/bit_stack.cc
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012 Apple Inc. All
+ * rights reserved.
+ * Copyright (C) 2005 Alexey Proskuryakov.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/iterators/bit_stack.h"
+
+namespace blink {
+static const unsigned kBitsInWord = sizeof(unsigned) * 8;
+static const unsigned kBitInWordMask = kBitsInWord - 1;
+
+BitStack::BitStack() : size_(0) {}
+
+BitStack::~BitStack() = default;
+
+void BitStack::Push(bool bit) {
+ unsigned index = size_ / kBitsInWord;
+ unsigned shift = size_ & kBitInWordMask;
+ if (!shift && index == words_.size()) {
+ words_.Grow(index + 1);
+ words_[index] = 0;
+ }
+ unsigned& word = words_[index];
+ unsigned mask = 1U << shift;
+ if (bit)
+ word |= mask;
+ else
+ word &= ~mask;
+ ++size_;
+}
+
+void BitStack::Pop() {
+ if (size_)
+ --size_;
+}
+
+bool BitStack::Top() const {
+ if (!size_)
+ return false;
+ unsigned shift = (size_ - 1) & kBitInWordMask;
+ unsigned index = (size_ - 1) / kBitsInWord;
+ return words_[index] & (1U << shift);
+}
+
+unsigned BitStack::size() const {
+ return size_;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/bit_stack.h b/chromium/third_party/blink/renderer/core/editing/iterators/bit_stack.h
new file mode 100644
index 00000000000..9909e04cd38
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/bit_stack.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2004, 2006, 2009 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_BIT_STACK_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_BIT_STACK_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/vector.h"
+
+namespace blink {
+
+class CORE_EXPORT BitStack {
+ STACK_ALLOCATED();
+
+ public:
+ BitStack();
+ ~BitStack();
+
+ void Push(bool);
+ void Pop();
+
+ bool Top() const;
+ unsigned size() const;
+
+ private:
+ unsigned size_;
+ Vector<unsigned, 1> words_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_BIT_STACK_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/character_iterator.cc b/chromium/third_party/blink/renderer/core/editing/iterators/character_iterator.cc
new file mode 100644
index 00000000000..0e80978fb48
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/character_iterator.cc
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012 Apple Inc. All
+ * rights reserved.
+ * Copyright (C) 2005 Alexey Proskuryakov.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/iterators/character_iterator.h"
+
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+
+namespace blink {
+
+template <typename Strategy>
+CharacterIteratorAlgorithm<Strategy>::CharacterIteratorAlgorithm(
+ const PositionTemplate<Strategy>& start,
+ const PositionTemplate<Strategy>& end,
+ const TextIteratorBehavior& behavior)
+ : offset_(0),
+ run_offset_(0),
+ at_break_(true),
+ text_iterator_(start, end, behavior) {
+ Initialize();
+}
+
+template <typename Strategy>
+CharacterIteratorAlgorithm<Strategy>::CharacterIteratorAlgorithm(
+ const EphemeralRangeTemplate<Strategy>& range,
+ const TextIteratorBehavior& behavior)
+ : CharacterIteratorAlgorithm(range.StartPosition(),
+ range.EndPosition(),
+ behavior) {}
+
+template <typename Strategy>
+void CharacterIteratorAlgorithm<Strategy>::Initialize() {
+ while (!AtEnd() && !text_iterator_.length())
+ text_iterator_.Advance();
+}
+
+template <typename Strategy>
+Document* CharacterIteratorAlgorithm<Strategy>::OwnerDocument() const {
+ return text_iterator_.OwnerDocument();
+}
+
+template <typename Strategy>
+const Node* CharacterIteratorAlgorithm<Strategy>::CurrentContainer() const {
+ return text_iterator_.CurrentContainer();
+}
+
+template <typename Strategy>
+int CharacterIteratorAlgorithm<Strategy>::StartOffset() const {
+ if (!text_iterator_.AtEnd()) {
+ if (text_iterator_.length() > 1)
+ return text_iterator_.StartOffsetInCurrentContainer() + run_offset_;
+ DCHECK(!run_offset_);
+ }
+ return text_iterator_.StartOffsetInCurrentContainer();
+}
+
+template <typename Strategy>
+int CharacterIteratorAlgorithm<Strategy>::EndOffset() const {
+ if (!text_iterator_.AtEnd()) {
+ if (text_iterator_.length() > 1)
+ return text_iterator_.StartOffsetInCurrentContainer() + run_offset_ + 1;
+ DCHECK(!run_offset_);
+ }
+ return text_iterator_.EndOffsetInCurrentContainer();
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy>
+CharacterIteratorAlgorithm<Strategy>::GetPositionBefore() const {
+ const Node& node = *text_iterator_.CurrentContainer();
+ if (text_iterator_.AtEnd()) {
+ DCHECK_EQ(run_offset_, 0);
+ return PositionTemplate<Strategy>(
+ node, text_iterator_.StartOffsetInCurrentContainer());
+ }
+ DCHECK_GE(text_iterator_.length(), 1);
+ if (node.IsTextNode()) {
+ const int offset = text_iterator_.StartOffsetInCurrentContainer();
+ return PositionTemplate<Strategy>(node, offset + run_offset_);
+ }
+ return PositionTemplate<Strategy>::BeforeNode(node);
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy>
+CharacterIteratorAlgorithm<Strategy>::GetPositionAfter() const {
+ const Node& node = *text_iterator_.CurrentContainer();
+ if (text_iterator_.AtEnd()) {
+ DCHECK_EQ(run_offset_, 0);
+ return PositionTemplate<Strategy>(
+ node, text_iterator_.EndOffsetInCurrentContainer());
+ }
+ DCHECK_GE(text_iterator_.length(), 1);
+ if (node.IsTextNode()) {
+ const int offset = text_iterator_.StartOffsetInCurrentContainer();
+ return PositionTemplate<Strategy>(node, offset + run_offset_ + 1);
+ }
+ return PositionTemplate<Strategy>::AfterNode(node);
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> CharacterIteratorAlgorithm<Strategy>::StartPosition()
+ const {
+ if (!text_iterator_.AtEnd()) {
+ if (text_iterator_.length() > 1) {
+ const Node* n = text_iterator_.CurrentContainer();
+ int offset = text_iterator_.StartOffsetInCurrentContainer() + run_offset_;
+ return PositionTemplate<Strategy>::EditingPositionOf(n, offset);
+ }
+ DCHECK(!run_offset_);
+ }
+ return text_iterator_.StartPositionInCurrentContainer();
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> CharacterIteratorAlgorithm<Strategy>::EndPosition()
+ const {
+ if (!text_iterator_.AtEnd()) {
+ if (text_iterator_.length() > 1) {
+ const Node* n = text_iterator_.CurrentContainer();
+ int offset = text_iterator_.StartOffsetInCurrentContainer() + run_offset_;
+ return PositionTemplate<Strategy>::EditingPositionOf(n, offset + 1);
+ }
+ DCHECK(!run_offset_);
+ }
+ return text_iterator_.EndPositionInCurrentContainer();
+}
+
+template <typename Strategy>
+void CharacterIteratorAlgorithm<Strategy>::Advance(int count) {
+ if (count <= 0) {
+ DCHECK(!count);
+ return;
+ }
+
+ at_break_ = false;
+
+ // easy if there is enough left in the current m_textIterator run
+ int remaining = text_iterator_.length() - run_offset_;
+ if (count < remaining) {
+ run_offset_ += count;
+ offset_ += count;
+ return;
+ }
+
+ // exhaust the current m_textIterator run
+ count -= remaining;
+ offset_ += remaining;
+
+ // move to a subsequent m_textIterator run
+ for (text_iterator_.Advance(); !AtEnd(); text_iterator_.Advance()) {
+ int run_length = text_iterator_.length();
+ if (!run_length) {
+ at_break_ = text_iterator_.BreaksAtReplacedElement();
+ } else {
+ // see whether this is m_textIterator to use
+ if (count < run_length) {
+ run_offset_ = count;
+ offset_ += count;
+ return;
+ }
+
+ // exhaust this m_textIterator run
+ count -= run_length;
+ offset_ += run_length;
+ }
+ }
+
+ // ran to the end of the m_textIterator... no more runs left
+ at_break_ = true;
+ run_offset_ = 0;
+}
+
+template <typename Strategy>
+void CharacterIteratorAlgorithm<Strategy>::CopyTextTo(
+ ForwardsTextBuffer* output) {
+ text_iterator_.CopyTextTo(output, run_offset_);
+}
+
+template <typename Strategy>
+EphemeralRangeTemplate<Strategy>
+CharacterIteratorAlgorithm<Strategy>::CalculateCharacterSubrange(int offset,
+ int length) {
+ Advance(offset);
+ const PositionTemplate<Strategy> start_pos = StartPosition();
+
+ if (!length)
+ return EphemeralRangeTemplate<Strategy>(start_pos, start_pos);
+ if (length > 1)
+ Advance(length - 1);
+ return EphemeralRangeTemplate<Strategy>(start_pos, EndPosition());
+}
+
+EphemeralRange CalculateCharacterSubrange(const EphemeralRange& range,
+ int character_offset,
+ int character_count) {
+ CharacterIterator entire_range_iterator(
+ range, TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(true)
+ .Build());
+ return entire_range_iterator.CalculateCharacterSubrange(character_offset,
+ character_count);
+}
+
+template class CORE_TEMPLATE_EXPORT CharacterIteratorAlgorithm<EditingStrategy>;
+template class CORE_TEMPLATE_EXPORT
+ CharacterIteratorAlgorithm<EditingInFlatTreeStrategy>;
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/character_iterator.h b/chromium/third_party/blink/renderer/core/editing/iterators/character_iterator.h
new file mode 100644
index 00000000000..180e55deb26
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/character_iterator.h
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2004, 2006, 2009 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_CHARACTER_ITERATOR_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_CHARACTER_ITERATOR_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator_behavior.h"
+#include "third_party/blink/renderer/platform/heap/heap.h"
+
+namespace blink {
+
+// Builds on the text iterator, adding a character position so we can walk one
+// character at a time, or faster, as needed. Useful for searching.
+template <typename Strategy>
+class CORE_EXPORT CharacterIteratorAlgorithm {
+ STACK_ALLOCATED();
+
+ public:
+ CharacterIteratorAlgorithm(
+ const PositionTemplate<Strategy>& start,
+ const PositionTemplate<Strategy>& end,
+ const TextIteratorBehavior& = TextIteratorBehavior());
+ explicit CharacterIteratorAlgorithm(
+ const EphemeralRangeTemplate<Strategy>&,
+ const TextIteratorBehavior& = TextIteratorBehavior());
+
+ void Advance(int num_characters);
+
+ bool AtBreak() const { return at_break_; }
+ bool AtEnd() const { return text_iterator_.AtEnd(); }
+
+ int length() const { return text_iterator_.length() - run_offset_; }
+ UChar CharacterAt(unsigned index) const {
+ return text_iterator_.CharacterAt(run_offset_ + index);
+ }
+
+ void CopyTextTo(ForwardsTextBuffer* output);
+
+ int CharacterOffset() const { return offset_; }
+
+ Document* OwnerDocument() const;
+ const Node* CurrentContainer() const;
+ int StartOffset() const;
+ int EndOffset() const;
+
+ PositionTemplate<Strategy> GetPositionBefore() const;
+ PositionTemplate<Strategy> GetPositionAfter() const;
+
+ // TDOO(editing-dev): We should rename |StartPosition()| to
+ // |GetPositionBeforeDeprecated()| and use |GetPositionBefore()| to
+ // avoid using |EditingPositionOf()|.
+ // Note: Following two tests are failed when using |GetPositionBefore()|
+ // instead of |StartPosition()|:
+ // 1. extend-by-sentence-002.html
+ // 2. move_forward_sentence_empty_line_break.html
+ PositionTemplate<Strategy> StartPosition() const;
+ // TDOO(editing-dev): We should rename |EndPosition()| to
+ // |GetPositionAfterDeprecated()| and use |GetPositionAfter()| to
+ // avoid using |EditingPositionOf()|.
+ PositionTemplate<Strategy> EndPosition() const;
+
+ EphemeralRangeTemplate<Strategy> CalculateCharacterSubrange(int offset,
+ int length);
+
+ private:
+ void Initialize();
+
+ int offset_;
+ int run_offset_;
+ bool at_break_;
+
+ TextIteratorAlgorithm<Strategy> text_iterator_;
+};
+
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ CharacterIteratorAlgorithm<EditingStrategy>;
+using CharacterIterator = CharacterIteratorAlgorithm<EditingStrategy>;
+
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ CharacterIteratorAlgorithm<EditingInFlatTreeStrategy>;
+using CharacterIteratorInFlatTree =
+ CharacterIteratorAlgorithm<EditingInFlatTreeStrategy>;
+
+CORE_EXPORT EphemeralRange CalculateCharacterSubrange(const EphemeralRange&,
+ int character_offset,
+ int character_count);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_CHARACTER_ITERATOR_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/character_iterator_test.cc b/chromium/third_party/blink/renderer/core/editing/iterators/character_iterator_test.cc
new file mode 100644
index 00000000000..3146a0a7d8d
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/character_iterator_test.cc
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2013, Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/iterators/character_iterator.h"
+
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+
+namespace blink {
+
+class CharacterIteratorTest : public EditingTestBase {};
+
+TEST_F(CharacterIteratorTest, SubrangeWithReplacedElements) {
+ static const char* body_content =
+ "<div id='div' contenteditable='true'>1<img src='foo.png'>345</div>";
+ SetBodyContent(body_content);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ Node* div_node = GetDocument().getElementById("div");
+ Range* entire_range = Range::Create(GetDocument(), div_node, 0, div_node, 3);
+
+ EphemeralRange result =
+ CalculateCharacterSubrange(EphemeralRange(entire_range), 2, 3);
+ Node* text_node = div_node->lastChild();
+ EXPECT_EQ(Position(text_node, 0), result.StartPosition());
+ EXPECT_EQ(Position(text_node, 3), result.EndPosition());
+}
+
+TEST_F(CharacterIteratorTest, CollapsedSubrange) {
+ static const char* body_content =
+ "<div id='div' contenteditable='true'>hello</div>";
+ SetBodyContent(body_content);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ Node* text_node = GetDocument().getElementById("div")->lastChild();
+ Range* entire_range =
+ Range::Create(GetDocument(), text_node, 1, text_node, 4);
+ EXPECT_EQ(1u, entire_range->startOffset());
+ EXPECT_EQ(4u, entire_range->endOffset());
+
+ const EphemeralRange& result =
+ CalculateCharacterSubrange(EphemeralRange(entire_range), 2, 0);
+ EXPECT_EQ(Position(text_node, 3), result.StartPosition());
+ EXPECT_EQ(Position(text_node, 3), result.EndPosition());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/forwards_text_buffer.cc b/chromium/third_party/blink/renderer/core/editing/iterators/forwards_text_buffer.cc
new file mode 100644
index 00000000000..06b4c52c2fa
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/forwards_text_buffer.cc
@@ -0,0 +1,18 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/iterators/forwards_text_buffer.h"
+
+namespace blink {
+
+const UChar* ForwardsTextBuffer::Data() const {
+ return BufferBegin();
+}
+
+UChar* ForwardsTextBuffer::CalcDestination(size_t length) {
+ DCHECK_LE(Size() + length, Capacity());
+ return BufferBegin() + Size();
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/forwards_text_buffer.h b/chromium/third_party/blink/renderer/core/editing/iterators/forwards_text_buffer.h
new file mode 100644
index 00000000000..29a6ef69061
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/forwards_text_buffer.h
@@ -0,0 +1,28 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_FORWARDS_TEXT_BUFFER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_FORWARDS_TEXT_BUFFER_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_buffer_base.h"
+
+namespace blink {
+
+class CORE_EXPORT ForwardsTextBuffer final : public TextBufferBase {
+ STACK_ALLOCATED();
+
+ public:
+ ForwardsTextBuffer() = default;
+ const UChar* Data() const override;
+
+ private:
+ UChar* CalcDestination(size_t length) override;
+
+ DISALLOW_COPY_AND_ASSIGN(ForwardsTextBuffer);
+};
+
+} // namespace blink
+
+#endif // TextBuffer_h
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/forwards_text_buffer_test.cc b/chromium/third_party/blink/renderer/core/editing/iterators/forwards_text_buffer_test.cc
new file mode 100644
index 00000000000..0700242e47b
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/forwards_text_buffer_test.cc
@@ -0,0 +1,64 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/iterators/forwards_text_buffer.h"
+
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class ForwardsTextBufferTest : public EditingTestBase {};
+
+TEST_F(ForwardsTextBufferTest, pushCharacters) {
+ ForwardsTextBuffer buffer;
+
+ // Basic tests.
+ buffer.PushCharacters('a', 1);
+ buffer.PushCharacters(1u, 0);
+ buffer.PushCharacters('#', 2);
+ buffer.PushCharacters('\0', 1);
+ EXPECT_EQ('a', buffer[0]);
+ EXPECT_EQ('#', buffer[1]);
+ EXPECT_EQ('#', buffer[2]);
+ EXPECT_EQ('\0', buffer[3]);
+
+ // Tests with buffer reallocation.
+ buffer.PushCharacters('A', 4096);
+ EXPECT_EQ('a', buffer[0]);
+ EXPECT_EQ('#', buffer[1]);
+ EXPECT_EQ('#', buffer[2]);
+ EXPECT_EQ('\0', buffer[3]);
+ EXPECT_EQ('A', buffer[4]);
+ EXPECT_EQ('A', buffer[4 + 4095]);
+}
+
+TEST_F(ForwardsTextBufferTest, pushRange) {
+ ForwardsTextBuffer buffer;
+
+ // Basic tests.
+ buffer.PushRange("ababc", 1);
+ buffer.PushRange((UChar*)nullptr, 0);
+ buffer.PushRange("#@", 2);
+ UChar ch = 'x';
+ buffer.PushRange(&ch, 1);
+ EXPECT_EQ('a', buffer[0]);
+ EXPECT_EQ('#', buffer[1]);
+ EXPECT_EQ('@', buffer[2]);
+ EXPECT_EQ('x', buffer[3]);
+
+ // Tests with buffer reallocation.
+ Vector<UChar> chunk(4096);
+ for (unsigned i = 0; i < chunk.size(); ++i)
+ chunk[i] = i % 256;
+ buffer.PushRange(chunk.data(), chunk.size());
+ EXPECT_EQ('a', buffer[0]);
+ EXPECT_EQ('#', buffer[1]);
+ EXPECT_EQ('@', buffer[2]);
+ EXPECT_EQ('x', buffer[3]);
+ EXPECT_EQ(0, buffer[4]);
+ EXPECT_EQ(1111 % 256, buffer[4 + 1111]);
+ EXPECT_EQ(255, buffer[4 + 4095]);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/fully_clipped_state_stack.cc b/chromium/third_party/blink/renderer/core/editing/iterators/fully_clipped_state_stack.cc
new file mode 100644
index 00000000000..0e9fe13edbf
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/fully_clipped_state_stack.cc
@@ -0,0 +1,99 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/iterators/fully_clipped_state_stack.h"
+
+#include "third_party/blink/renderer/core/dom/container_node.h"
+#include "third_party/blink/renderer/core/dom/node.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/layout/layout_box.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+
+namespace blink {
+
+namespace {
+
+inline bool FullyClipsContents(const Node* node) {
+ LayoutObject* layout_object = node->GetLayoutObject();
+ if (!layout_object || !layout_object->IsBox() ||
+ !layout_object->HasOverflowClip() || layout_object->IsLayoutView())
+ return false;
+ return ToLayoutBox(layout_object)->Size().IsEmpty();
+}
+
+inline bool IgnoresContainerClip(const Node* node) {
+ LayoutObject* layout_object = node->GetLayoutObject();
+ if (!layout_object || layout_object->IsText())
+ return false;
+ return layout_object->Style()->HasOutOfFlowPosition();
+}
+
+template <typename Strategy>
+unsigned DepthCrossingShadowBoundaries(const Node& node) {
+ unsigned depth = 0;
+ for (ContainerNode* parent = ParentCrossingShadowBoundaries<Strategy>(node);
+ parent; parent = ParentCrossingShadowBoundaries<Strategy>(*parent))
+ ++depth;
+ return depth;
+}
+
+} // namespace
+
+template <typename Strategy>
+FullyClippedStateStackAlgorithm<Strategy>::FullyClippedStateStackAlgorithm() =
+ default;
+
+template <typename Strategy>
+FullyClippedStateStackAlgorithm<Strategy>::~FullyClippedStateStackAlgorithm() =
+ default;
+
+template <typename Strategy>
+void FullyClippedStateStackAlgorithm<Strategy>::PushFullyClippedState(
+ const Node* node) {
+ DCHECK_EQ(size(), DepthCrossingShadowBoundaries<Strategy>(*node));
+
+ // FIXME: m_fullyClippedStack was added in response to
+ // <https://bugs.webkit.org/show_bug.cgi?id=26364> ("Search can find text
+ // that's hidden by overflow:hidden"), but the logic here will not work
+ // correctly if a shadow tree redistributes nodes. m_fullyClippedStack relies
+ // on the assumption that DOM node hierarchy matches the layout tree, which is
+ // not necessarily true if there happens to be shadow DOM distribution or
+ // other mechanics that shuffle around the layout objects regardless of node
+ // tree hierarchy (like CSS flexbox).
+ //
+ // A more appropriate way to handle this situation is to detect
+ // overflow:hidden blocks by using only layout primitives, not with DOM
+ // primitives.
+
+ // Push true if this node full clips its contents, or if a parent already has
+ // fully
+ // clipped and this is not a node that ignores its container's clip.
+ Push(FullyClipsContents(node) || (Top() && !IgnoresContainerClip(node)));
+}
+
+template <typename Strategy>
+void FullyClippedStateStackAlgorithm<Strategy>::SetUpFullyClippedStack(
+ const Node* node) {
+ // Put the nodes in a vector so we can iterate in reverse order.
+ HeapVector<Member<ContainerNode>, 100> ancestry;
+ for (ContainerNode* parent = ParentCrossingShadowBoundaries<Strategy>(*node);
+ parent; parent = ParentCrossingShadowBoundaries<Strategy>(*parent))
+ ancestry.push_back(parent);
+
+ // Call pushFullyClippedState on each node starting with the earliest
+ // ancestor.
+ size_t ancestry_size = ancestry.size();
+ for (size_t i = 0; i < ancestry_size; ++i)
+ PushFullyClippedState(ancestry[ancestry_size - i - 1]);
+ PushFullyClippedState(node);
+
+ DCHECK_EQ(size(), 1 + DepthCrossingShadowBoundaries<Strategy>(*node));
+}
+
+template class CORE_TEMPLATE_EXPORT
+ FullyClippedStateStackAlgorithm<EditingStrategy>;
+template class CORE_TEMPLATE_EXPORT
+ FullyClippedStateStackAlgorithm<EditingInFlatTreeStrategy>;
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/fully_clipped_state_stack.h b/chromium/third_party/blink/renderer/core/editing/iterators/fully_clipped_state_stack.h
new file mode 100644
index 00000000000..3c8fa02e5b1
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/fully_clipped_state_stack.h
@@ -0,0 +1,36 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_FULLY_CLIPPED_STATE_STACK_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_FULLY_CLIPPED_STATE_STACK_H_
+
+#include "third_party/blink/renderer/core/editing/editing_strategy.h"
+#include "third_party/blink/renderer/core/editing/iterators/bit_stack.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+
+namespace blink {
+
+template <typename Strategy>
+class CORE_TEMPLATE_CLASS_EXPORT FullyClippedStateStackAlgorithm final
+ : public BitStack {
+ STACK_ALLOCATED();
+
+ public:
+ FullyClippedStateStackAlgorithm();
+ ~FullyClippedStateStackAlgorithm();
+
+ void PushFullyClippedState(const Node*);
+ void SetUpFullyClippedStack(const Node*);
+};
+
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ FullyClippedStateStackAlgorithm<EditingStrategy>;
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ FullyClippedStateStackAlgorithm<EditingInFlatTreeStrategy>;
+
+using FullyClippedStateStack = FullyClippedStateStackAlgorithm<EditingStrategy>;
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_FULLY_CLIPPED_STATE_STACK_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/search_buffer.cc b/chromium/third_party/blink/renderer/core/editing/iterators/search_buffer.cc
new file mode 100644
index 00000000000..94451b5c765
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/search_buffer.cc
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012 Apple Inc. All
+ * rights reserved.
+ * Copyright (C) 2005 Alexey Proskuryakov.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/iterators/search_buffer.h"
+
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/iterators/character_iterator.h"
+#include "third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_searcher_icu.h"
+#include "third_party/blink/renderer/platform/text/character.h"
+#include "third_party/blink/renderer/platform/text/text_boundaries.h"
+#include "third_party/blink/renderer/platform/text/unicode_utilities.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_view.h"
+
+namespace blink {
+
+namespace {
+
+const size_t kMinimumSearchBufferSize = 8192;
+
+UChar32 GetCodePointAt(const UChar* str, size_t index, size_t length) {
+ UChar32 c;
+ U16_GET(str, 0, index, length, c);
+ return c;
+}
+
+} // namespace
+
+inline SearchBuffer::SearchBuffer(const String& target, FindOptions options)
+ : options_(options),
+ prefix_length_(0),
+ number_of_characters_just_appended_(0),
+ at_break_(true),
+ needs_more_context_(options & kAtWordStarts),
+ target_requires_kana_workaround_(ContainsKanaLetters(target)) {
+ DCHECK(!target.IsEmpty()) << target;
+ target.AppendTo(target_);
+
+ // FIXME: We'd like to tailor the searcher to fold quote marks for us instead
+ // of doing it in a separate replacement pass here, but ICU doesn't offer a
+ // way to add tailoring on top of the locale-specific tailoring as of this
+ // writing.
+ FoldQuoteMarksAndSoftHyphens(target_.data(), target_.size());
+
+ size_t target_length = target_.size();
+ buffer_.ReserveInitialCapacity(
+ std::max(target_length * 8, kMinimumSearchBufferSize));
+ overlap_ = buffer_.capacity() / 4;
+
+ if ((options_ & kAtWordStarts) && target_length) {
+ const UChar32 target_first_character =
+ GetCodePointAt(target_.data(), 0, target_length);
+ // Characters in the separator category never really occur at the beginning
+ // of a word, so if the target begins with such a character, we just ignore
+ // the AtWordStart option.
+ if (IsSeparator(target_first_character)) {
+ options_ &= ~kAtWordStarts;
+ needs_more_context_ = false;
+ }
+ }
+
+ text_searcher_ = std::make_unique<TextSearcherICU>();
+ text_searcher_->SetPattern(StringView(target_.data(), target_.size()),
+ !(options_ & kCaseInsensitive));
+
+ // The kana workaround requires a normalized copy of the target string.
+ if (target_requires_kana_workaround_)
+ NormalizeCharactersIntoNFCForm(target_.data(), target_.size(),
+ normalized_target_);
+}
+
+inline SearchBuffer::~SearchBuffer() = default;
+
+template <typename CharType>
+inline void SearchBuffer::Append(const CharType* characters, size_t length) {
+ DCHECK(length);
+
+ if (at_break_) {
+ buffer_.Shrink(0);
+ prefix_length_ = 0;
+ at_break_ = false;
+ } else if (buffer_.size() == buffer_.capacity()) {
+ memcpy(buffer_.data(), buffer_.data() + buffer_.size() - overlap_,
+ overlap_ * sizeof(UChar));
+ prefix_length_ -= std::min(prefix_length_, buffer_.size() - overlap_);
+ buffer_.Shrink(overlap_);
+ }
+
+ size_t old_length = buffer_.size();
+ size_t usable_length = std::min(buffer_.capacity() - old_length, length);
+ DCHECK(usable_length);
+ buffer_.resize(old_length + usable_length);
+ UChar* destination = buffer_.data() + old_length;
+ StringImpl::CopyChars(destination, characters, usable_length);
+ FoldQuoteMarksAndSoftHyphens(destination, usable_length);
+ number_of_characters_just_appended_ = usable_length;
+}
+
+inline bool SearchBuffer::NeedsMoreContext() const {
+ return needs_more_context_;
+}
+
+inline void SearchBuffer::PrependContext(const UChar* characters,
+ size_t length) {
+ DCHECK(needs_more_context_);
+ DCHECK_EQ(prefix_length_, buffer_.size());
+
+ if (!length)
+ return;
+
+ at_break_ = false;
+
+ size_t word_boundary_context_start = length;
+ if (word_boundary_context_start) {
+ U16_BACK_1(characters, 0, word_boundary_context_start);
+ word_boundary_context_start =
+ StartOfLastWordBoundaryContext(characters, word_boundary_context_start);
+ }
+
+ size_t usable_length = std::min(buffer_.capacity() - prefix_length_,
+ length - word_boundary_context_start);
+ buffer_.push_front(characters + length - usable_length, usable_length);
+ prefix_length_ += usable_length;
+
+ if (word_boundary_context_start || prefix_length_ == buffer_.capacity())
+ needs_more_context_ = false;
+}
+
+inline bool SearchBuffer::AtBreak() const {
+ return at_break_;
+}
+
+inline void SearchBuffer::ReachedBreak() {
+ at_break_ = true;
+}
+
+inline bool SearchBuffer::IsBadMatch(const UChar* match,
+ size_t match_length) const {
+ // This function implements the kana workaround. If usearch treats
+ // it as a match, but we do not want to, then it's a "bad match".
+ if (!target_requires_kana_workaround_)
+ return false;
+
+ // Normalize into a match buffer. We reuse a single buffer rather than
+ // creating a new one each time.
+ NormalizeCharactersIntoNFCForm(match, match_length, normalized_match_);
+
+ return !CheckOnlyKanaLettersInStrings(
+ normalized_target_.begin(), normalized_target_.size(),
+ normalized_match_.begin(), normalized_match_.size());
+}
+
+inline bool SearchBuffer::IsWordStartMatch(size_t start, size_t length) const {
+ DCHECK(options_ & kAtWordStarts);
+
+ if (!start)
+ return true;
+
+ int size = buffer_.size();
+ int offset = start;
+ UChar32 first_character = GetCodePointAt(buffer_.data(), offset, size);
+
+ if (options_ & kTreatMedialCapitalAsWordStart) {
+ UChar32 previous_character;
+ U16_PREV(buffer_.data(), 0, offset, previous_character);
+
+ if (IsSeparator(first_character)) {
+ // The start of a separator run is a word start (".org" in "webkit.org").
+ if (!IsSeparator(previous_character))
+ return true;
+ } else if (IsASCIIUpper(first_character)) {
+ // The start of an uppercase run is a word start ("Kit" in "WebKit").
+ if (!IsASCIIUpper(previous_character))
+ return true;
+ // The last character of an uppercase run followed by a non-separator,
+ // non-digit is a word start ("Request" in "XMLHTTPRequest").
+ offset = start;
+ U16_FWD_1(buffer_.data(), offset, size);
+ UChar32 next_character = 0;
+ if (offset < size)
+ next_character = GetCodePointAt(buffer_.data(), offset, size);
+ if (!IsASCIIUpper(next_character) && !IsASCIIDigit(next_character) &&
+ !IsSeparator(next_character))
+ return true;
+ } else if (IsASCIIDigit(first_character)) {
+ // The start of a digit run is a word start ("2" in "WebKit2").
+ if (!IsASCIIDigit(previous_character))
+ return true;
+ } else if (IsSeparator(previous_character) ||
+ IsASCIIDigit(previous_character)) {
+ // The start of a non-separator, non-uppercase, non-digit run is a word
+ // start, except after an uppercase. ("org" in "webkit.org", but not "ore"
+ // in "WebCore").
+ return true;
+ }
+ }
+
+ // Chinese and Japanese lack word boundary marks, and there is no clear
+ // agreement on what constitutes a word, so treat the position before any CJK
+ // character as a word start.
+ if (Character::IsCJKIdeographOrSymbol(first_character))
+ return true;
+
+ size_t word_break_search_start = start + length;
+ while (word_break_search_start > start) {
+ word_break_search_start = FindNextWordBackward(
+ buffer_.data(), buffer_.size(), word_break_search_start);
+ }
+ if (word_break_search_start != start)
+ return false;
+ if (options_ & kWholeWord)
+ return static_cast<int>(start + length) ==
+ FindWordEndBoundary(buffer_.data(), buffer_.size(),
+ word_break_search_start);
+ return true;
+}
+
+inline size_t SearchBuffer::Search(size_t& start) {
+ size_t size = buffer_.size();
+ if (at_break_) {
+ if (!size)
+ return 0;
+ } else {
+ if (size != buffer_.capacity())
+ return 0;
+ }
+
+ text_searcher_->SetText(buffer_.data(), size);
+ text_searcher_->SetOffset(prefix_length_);
+
+ MatchResultICU match;
+
+nextMatch:
+ if (!text_searcher_->NextMatchResult(match))
+ return 0;
+
+ // Matches that start in the overlap area are only tentative.
+ // The same match may appear later, matching more characters,
+ // possibly including a combining character that's not yet in the buffer.
+ if (!at_break_ && match.start >= size - overlap_) {
+ size_t overlap = overlap_;
+ if (options_ & kAtWordStarts) {
+ // Ensure that there is sufficient context before matchStart the next time
+ // around for determining if it is at a word boundary.
+ int word_boundary_context_start = match.start;
+ U16_BACK_1(buffer_.data(), 0, word_boundary_context_start);
+ word_boundary_context_start = StartOfLastWordBoundaryContext(
+ buffer_.data(), word_boundary_context_start);
+ overlap = std::min(size - 1,
+ std::max(overlap, size - word_boundary_context_start));
+ }
+ memcpy(buffer_.data(), buffer_.data() + size - overlap,
+ overlap * sizeof(UChar));
+ prefix_length_ -= std::min(prefix_length_, size - overlap);
+ buffer_.Shrink(overlap);
+ return 0;
+ }
+
+ SECURITY_DCHECK(match.start + match.length <= size);
+
+ // If this match is "bad", move on to the next match.
+ if (IsBadMatch(buffer_.data() + match.start, match.length) ||
+ ((options_ & kAtWordStarts) &&
+ !IsWordStartMatch(match.start, match.length))) {
+ goto nextMatch;
+ }
+
+ size_t new_size = size - (match.start + 1);
+ memmove(buffer_.data(), buffer_.data() + match.start + 1,
+ new_size * sizeof(UChar));
+ prefix_length_ -= std::min<size_t>(prefix_length_, match.start + 1);
+ buffer_.Shrink(new_size);
+
+ start = size - match.start;
+ return match.length;
+}
+
+// Check if there's any unpaird surrogate code point.
+// Non-character code points are not checked.
+static bool IsValidUTF16(const String& s) {
+ if (s.Is8Bit())
+ return true;
+ const UChar* ustr = s.Characters16();
+ size_t length = s.length();
+ size_t position = 0;
+ while (position < length) {
+ UChar32 character;
+ U16_NEXT(ustr, position, length, character);
+ if (U_IS_SURROGATE(character))
+ return false;
+ }
+ return true;
+}
+
+template <typename Strategy>
+static size_t FindPlainTextInternal(CharacterIteratorAlgorithm<Strategy>& it,
+ const String& target,
+ FindOptions options,
+ size_t& match_start) {
+ match_start = 0;
+ size_t match_length = 0;
+
+ if (!IsValidUTF16(target))
+ return 0;
+
+ SearchBuffer buffer(target, options);
+
+ if (buffer.NeedsMoreContext()) {
+ for (SimplifiedBackwardsTextIteratorAlgorithm<Strategy> backwards_iterator(
+ EphemeralRangeTemplate<Strategy>(
+ PositionTemplate<Strategy>::FirstPositionInNode(
+ *it.OwnerDocument()),
+ PositionTemplate<Strategy>(it.CurrentContainer(),
+ it.StartOffset())));
+ !backwards_iterator.AtEnd(); backwards_iterator.Advance()) {
+ BackwardsTextBuffer characters;
+ backwards_iterator.CopyTextTo(&characters);
+ buffer.PrependContext(characters.Data(), characters.Size());
+ if (!buffer.NeedsMoreContext())
+ break;
+ }
+ }
+
+ while (!it.AtEnd()) {
+ ForwardsTextBuffer characters;
+ it.CopyTextTo(&characters);
+ buffer.Append(characters.Data(), characters.Size());
+ it.Advance(buffer.NumberOfCharactersJustAppended());
+ tryAgain:
+ size_t match_start_offset;
+ if (size_t new_match_length = buffer.Search(match_start_offset)) {
+ // Note that we found a match, and where we found it.
+ size_t last_character_in_buffer_offset = it.CharacterOffset();
+ DCHECK_GE(last_character_in_buffer_offset, match_start_offset);
+ match_start = last_character_in_buffer_offset - match_start_offset;
+ match_length = new_match_length;
+ // If searching forward, stop on the first match.
+ // If searching backward, don't stop, so we end up with the last match.
+ if (!(options & kBackwards))
+ break;
+ goto tryAgain;
+ }
+ if (it.AtBreak() && !buffer.AtBreak()) {
+ buffer.ReachedBreak();
+ goto tryAgain;
+ }
+ }
+
+ return match_length;
+}
+
+template <typename Strategy>
+static EphemeralRangeTemplate<Strategy> FindPlainTextAlgorithm(
+ const EphemeralRangeTemplate<Strategy>& input_range,
+ const String& target,
+ FindOptions options) {
+ // CharacterIterator requires layoutObjects to be up to date.
+ if (!input_range.StartPosition().IsConnected())
+ return EphemeralRangeTemplate<Strategy>();
+ DCHECK_EQ(input_range.StartPosition().GetDocument(),
+ input_range.EndPosition().GetDocument());
+
+ const TextIteratorBehavior& iterator_flags_for_find_plain_text =
+ TextIteratorBehavior::Builder()
+ .SetEntersTextControls(true)
+ .SetEntersOpenShadowRoots(true)
+ .SetDoesNotBreakAtReplacedElement(true)
+ .SetCollapseTrailingSpace(true)
+ .Build();
+
+ // FIXME: Reduce the code duplication with above (but how?).
+ size_t match_start;
+ size_t match_length;
+ {
+ const TextIteratorBehavior& behavior =
+ TextIteratorBehavior::Builder(iterator_flags_for_find_plain_text)
+ .SetForWindowFind(options & kFindAPICall)
+ .Build();
+ CharacterIteratorAlgorithm<Strategy> find_iterator(input_range, behavior);
+ match_length =
+ FindPlainTextInternal(find_iterator, target, options, match_start);
+ if (!match_length)
+ return EphemeralRangeTemplate<Strategy>(options & kBackwards
+ ? input_range.StartPosition()
+ : input_range.EndPosition());
+ }
+
+ CharacterIteratorAlgorithm<Strategy> compute_range_iterator(
+ input_range, iterator_flags_for_find_plain_text);
+ return compute_range_iterator.CalculateCharacterSubrange(match_start,
+ match_length);
+}
+
+EphemeralRange FindPlainText(const EphemeralRange& input_range,
+ const String& target,
+ FindOptions options) {
+ return FindPlainTextAlgorithm<EditingStrategy>(input_range, target, options);
+}
+
+EphemeralRangeInFlatTree FindPlainText(
+ const EphemeralRangeInFlatTree& input_range,
+ const String& target,
+ FindOptions options) {
+ return FindPlainTextAlgorithm<EditingInFlatTreeStrategy>(input_range, target,
+ options);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/search_buffer.h b/chromium/third_party/blink/renderer/core/editing/iterators/search_buffer.h
new file mode 100644
index 00000000000..dbe606fc24f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/search_buffer.h
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2004, 2006, 2009 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_SEARCH_BUFFER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_SEARCH_BUFFER_H_
+
+#include <memory>
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/finder/find_options.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+#include "third_party/blink/renderer/platform/wtf/vector.h"
+
+namespace blink {
+
+class TextSearcherICU;
+
+// Buffer that knows how to compare with a search target.
+// Keeps enough of the previous text to be able to search in the future, but no
+// more. Non-breaking spaces are always equal to normal spaces. Case folding is
+// also done if the CaseInsensitive option is specified. Matches are further
+// filtered if the AtWordStarts option is specified, although some matches
+// inside a word are permitted if TreatMedialCapitalAsWordStart is specified as
+// well.
+class SearchBuffer {
+ STACK_ALLOCATED();
+
+ public:
+ SearchBuffer(const String& target, FindOptions);
+ ~SearchBuffer();
+
+ // Returns number of characters appended; guaranteed to be in the range
+ // [1, length].
+ template <typename CharType>
+ void Append(const CharType*, size_t length);
+ size_t NumberOfCharactersJustAppended() const {
+ return number_of_characters_just_appended_;
+ }
+
+ bool NeedsMoreContext() const;
+ void PrependContext(const UChar*, size_t length);
+ void ReachedBreak();
+
+ // Result is the size in characters of what was found.
+ // And <startOffset> is the number of characters back to the start of what
+ // was found.
+ size_t Search(size_t& start_offset);
+ bool AtBreak() const;
+
+ private:
+ bool IsBadMatch(const UChar*, size_t length) const;
+ bool IsWordStartMatch(size_t start, size_t length) const;
+
+ Vector<UChar> target_;
+ FindOptions options_;
+
+ Vector<UChar> buffer_;
+ size_t overlap_;
+ size_t prefix_length_;
+ size_t number_of_characters_just_appended_;
+ bool at_break_;
+ bool needs_more_context_;
+
+ bool target_requires_kana_workaround_;
+ Vector<UChar> normalized_target_;
+ mutable Vector<UChar> normalized_match_;
+
+ std::unique_ptr<TextSearcherICU> text_searcher_;
+
+ DISALLOW_COPY_AND_ASSIGN(SearchBuffer);
+};
+
+CORE_EXPORT EphemeralRange FindPlainText(const EphemeralRange& input_range,
+ const String&,
+ FindOptions);
+CORE_EXPORT EphemeralRangeInFlatTree
+FindPlainText(const EphemeralRangeInFlatTree& input_range,
+ const String&,
+ FindOptions);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_SEARCH_BUFFER_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/search_buffer_test.cc b/chromium/third_party/blink/renderer/core/editing/iterators/search_buffer_test.cc
new file mode 100644
index 00000000000..e0a776b48d2
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/search_buffer_test.cc
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2013, Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/iterators/search_buffer.h"
+
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+
+namespace blink {
+
+class SearchBufferTest : public EditingTestBase {
+ protected:
+ Range* GetBodyRange() const;
+};
+
+Range* SearchBufferTest::GetBodyRange() const {
+ Range* range(Range::Create(GetDocument()));
+ range->selectNode(GetDocument().body());
+ return range;
+}
+
+TEST_F(SearchBufferTest, FindPlainTextInvalidTarget) {
+ static const char* body_content = "<div>foo bar test</div>";
+ SetBodyContent(body_content);
+ Range* range = GetBodyRange();
+
+ Range* expected_range = range->cloneRange();
+ expected_range->collapse(false);
+
+ // A lone lead surrogate (0xDA0A) example taken from fuzz-58.
+ static const UChar kInvalid1[] = {0x1461u, 0x2130u, 0x129bu, 0xd711u, 0xd6feu,
+ 0xccadu, 0x7064u, 0xd6a0u, 0x4e3bu, 0x03abu,
+ 0x17dcu, 0xb8b7u, 0xbf55u, 0xfca0u, 0x07fau,
+ 0x0427u, 0xda0au, 0};
+
+ // A lone trailing surrogate (U+DC01).
+ static const UChar kInvalid2[] = {0x1461u, 0x2130u, 0x129bu, 0xdc01u,
+ 0xd6feu, 0xccadu, 0};
+ // A trailing surrogate followed by a lead surrogate (U+DC03 U+D901).
+ static const UChar kInvalid3[] = {0xd800u, 0xdc00u, 0x0061u, 0xdc03u,
+ 0xd901u, 0xccadu, 0};
+
+ static const UChar* invalid_u_strings[] = {kInvalid1, kInvalid2, kInvalid3};
+
+ for (size_t i = 0; i < WTF_ARRAY_LENGTH(invalid_u_strings); ++i) {
+ String invalid_target(invalid_u_strings[i]);
+ EphemeralRange found_range =
+ FindPlainText(EphemeralRange(range), invalid_target, 0);
+ Range* actual_range = Range::Create(
+ GetDocument(), found_range.StartPosition(), found_range.EndPosition());
+ EXPECT_TRUE(AreRangesEqual(expected_range, actual_range));
+ }
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator.cc b/chromium/third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator.cc
new file mode 100644
index 00000000000..8981975a14e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator.cc
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012 Apple Inc. All
+ * rights reserved.
+ * Copyright (C) 2005 Alexey Proskuryakov.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator.h"
+
+#include "third_party/blink/renderer/core/dom/first_letter_pseudo_element.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/html/forms/html_form_control_element.h"
+#include "third_party/blink/renderer/core/layout/layout_text_fragment.h"
+
+namespace blink {
+
+static int CollapsedSpaceLength(LayoutText* layout_text, int text_end) {
+ const String& text = layout_text->GetText();
+ int length = text.length();
+ for (int i = text_end; i < length; ++i) {
+ if (!layout_text->Style()->IsCollapsibleWhiteSpace(text[i]))
+ return i - text_end;
+ }
+
+ return length - text_end;
+}
+
+static int MaxOffsetIncludingCollapsedSpaces(const Node* node) {
+ int offset = CaretMaxOffset(node);
+
+ if (node->GetLayoutObject() && node->GetLayoutObject()->IsText()) {
+ offset +=
+ CollapsedSpaceLength(ToLayoutText(node->GetLayoutObject()), offset) +
+ ToLayoutText(node->GetLayoutObject())->TextStartOffset();
+ }
+
+ return offset;
+}
+
+template <typename Strategy>
+SimplifiedBackwardsTextIteratorAlgorithm<Strategy>::
+ SimplifiedBackwardsTextIteratorAlgorithm(
+ const EphemeralRangeTemplate<Strategy>& range,
+ const TextIteratorBehavior& behavior)
+ : behavior_(behavior),
+ text_state_(behavior),
+ node_(nullptr),
+ offset_(0),
+ handled_node_(false),
+ handled_children_(false),
+ start_node_(nullptr),
+ start_offset_(0),
+ end_node_(nullptr),
+ end_offset_(0),
+ have_passed_start_node_(false),
+ should_handle_first_letter_(false),
+ should_stop_(false) {
+ const Node* start_node = range.StartPosition().AnchorNode();
+ if (!start_node)
+ return;
+ const Node* end_node = range.EndPosition().AnchorNode();
+ int start_offset = range.StartPosition().ComputeEditingOffset();
+ int end_offset = range.EndPosition().ComputeEditingOffset();
+
+ Init(start_node, end_node, start_offset, end_offset);
+}
+
+template <typename Strategy>
+void SimplifiedBackwardsTextIteratorAlgorithm<Strategy>::Init(
+ const Node* start_node,
+ const Node* end_node,
+ int start_offset,
+ int end_offset) {
+ if (!start_node->IsCharacterDataNode() && start_offset >= 0) {
+ // |Strategy::childAt()| will return 0 if the offset is out of range. We
+ // rely on this behavior instead of calling |countChildren()| to avoid
+ // traversing the children twice.
+ if (Node* child_at_offset = Strategy::ChildAt(*start_node, start_offset)) {
+ start_node = child_at_offset;
+ start_offset = 0;
+ }
+ }
+ if (!end_node->IsCharacterDataNode() && end_offset > 0) {
+ // |Strategy::childAt()| will return 0 if the offset is out of range. We
+ // rely on this behavior instead of calling |countChildren()| to avoid
+ // traversing the children twice.
+ if (Node* child_at_offset = Strategy::ChildAt(*end_node, end_offset - 1)) {
+ end_node = child_at_offset;
+ end_offset = Position::LastOffsetInNode(*end_node);
+ }
+ }
+
+ node_ = end_node;
+ fully_clipped_stack_.SetUpFullyClippedStack(node_);
+ offset_ = end_offset;
+ handled_node_ = false;
+ handled_children_ = !end_offset;
+
+ start_node_ = start_node;
+ start_offset_ = start_offset;
+ end_node_ = end_node;
+ end_offset_ = end_offset;
+
+ have_passed_start_node_ = false;
+
+ Advance();
+}
+
+template <typename Strategy>
+void SimplifiedBackwardsTextIteratorAlgorithm<Strategy>::Advance() {
+ if (should_stop_)
+ return;
+
+ if (behavior_.StopsOnFormControls() &&
+ HTMLFormControlElement::EnclosingFormControlElement(node_)) {
+ should_stop_ = true;
+ return;
+ }
+
+ text_state_.ResetRunInformation();
+
+ while (node_ && !have_passed_start_node_) {
+ // Don't handle node if we start iterating at [node, 0].
+ if (!handled_node_ && !(node_ == end_node_ && !end_offset_)) {
+ LayoutObject* layout_object = node_->GetLayoutObject();
+ if (layout_object && layout_object->IsText() &&
+ node_->getNodeType() == Node::kTextNode) {
+ // FIXME: What about kCdataSectionNode?
+ if (layout_object->Style()->Visibility() == EVisibility::kVisible &&
+ offset_ > 0)
+ handled_node_ = HandleTextNode();
+ } else if (layout_object && (layout_object->IsLayoutEmbeddedContent() ||
+ TextIterator::SupportsAltText(*node_))) {
+ if (layout_object->Style()->Visibility() == EVisibility::kVisible &&
+ offset_ > 0)
+ handled_node_ = HandleReplacedElement();
+ } else {
+ handled_node_ = HandleNonTextNode();
+ }
+ if (text_state_.PositionNode())
+ return;
+ }
+
+ if (!handled_children_ && Strategy::HasChildren(*node_)) {
+ node_ = Strategy::LastChild(*node_);
+ fully_clipped_stack_.PushFullyClippedState(node_);
+ } else {
+ // Exit empty containers as we pass over them or containers
+ // where [container, 0] is where we started iterating.
+ if (!handled_node_ && CanHaveChildrenForEditing(node_) &&
+ Strategy::Parent(*node_) &&
+ (!Strategy::LastChild(*node_) ||
+ (node_ == end_node_ && !end_offset_))) {
+ ExitNode();
+ if (text_state_.PositionNode()) {
+ handled_node_ = true;
+ handled_children_ = true;
+ return;
+ }
+ }
+
+ // Exit all other containers.
+ while (!Strategy::PreviousSibling(*node_)) {
+ if (!AdvanceRespectingRange(
+ ParentCrossingShadowBoundaries<Strategy>(*node_)))
+ break;
+ fully_clipped_stack_.Pop();
+ ExitNode();
+ if (text_state_.PositionNode()) {
+ handled_node_ = true;
+ handled_children_ = true;
+ return;
+ }
+ }
+
+ fully_clipped_stack_.Pop();
+ if (AdvanceRespectingRange(Strategy::PreviousSibling(*node_)))
+ fully_clipped_stack_.PushFullyClippedState(node_);
+ else
+ node_ = nullptr;
+ }
+
+ // For the purpose of word boundary detection,
+ // we should iterate all visible text and trailing (collapsed) whitespaces.
+ offset_ = node_ ? MaxOffsetIncludingCollapsedSpaces(node_) : 0;
+ handled_node_ = false;
+ handled_children_ = false;
+
+ if (text_state_.PositionNode())
+ return;
+ }
+}
+
+template <typename Strategy>
+bool SimplifiedBackwardsTextIteratorAlgorithm<Strategy>::HandleTextNode() {
+ int start_offset;
+ int offset_in_node;
+ LayoutText* layout_object = HandleFirstLetter(start_offset, offset_in_node);
+ if (!layout_object)
+ return true;
+
+ String text = layout_object->GetText();
+ if (!layout_object->HasTextBoxes() && text.length() > 0)
+ return true;
+
+ const int position_end_offset = offset_;
+ offset_ = start_offset;
+ const int position_start_offset = start_offset;
+ DCHECK_LE(0, position_start_offset - offset_in_node);
+ DCHECK_LE(position_start_offset - offset_in_node,
+ static_cast<int>(text.length()));
+ DCHECK_LE(1, position_end_offset - offset_in_node);
+ DCHECK_LE(position_end_offset - offset_in_node,
+ static_cast<int>(text.length()));
+ DCHECK_LE(position_start_offset, position_end_offset);
+
+ const int text_length = position_end_offset - position_start_offset;
+ const int text_offset = position_start_offset - offset_in_node;
+ CHECK_LE(static_cast<unsigned>(text_offset + text_length), text.length());
+ text_state_.EmitText(node_, position_start_offset, position_end_offset, text,
+ text_offset, text_offset + text_length);
+ return !should_handle_first_letter_;
+}
+
+template <typename Strategy>
+LayoutText* SimplifiedBackwardsTextIteratorAlgorithm<
+ Strategy>::HandleFirstLetter(int& start_offset, int& offset_in_node) {
+ LayoutText* layout_object = ToLayoutText(node_->GetLayoutObject());
+ start_offset = (node_ == start_node_) ? start_offset_ : 0;
+
+ if (!layout_object->IsTextFragment()) {
+ offset_in_node = 0;
+ return layout_object;
+ }
+
+ LayoutTextFragment* fragment = ToLayoutTextFragment(layout_object);
+ int offset_after_first_letter = fragment->Start();
+ if (start_offset >= offset_after_first_letter) {
+ // We'll stop in remaining part.
+ DCHECK(!should_handle_first_letter_);
+ offset_in_node = offset_after_first_letter;
+ return layout_object;
+ }
+
+ if (!should_handle_first_letter_ && offset_after_first_letter < offset_) {
+ // Enter into remaining part
+ should_handle_first_letter_ = true;
+ offset_in_node = offset_after_first_letter;
+ start_offset = offset_after_first_letter;
+ return layout_object;
+ }
+
+ // Enter into first-letter part
+ should_handle_first_letter_ = false;
+ offset_in_node = 0;
+
+ DCHECK(fragment->IsRemainingTextLayoutObject());
+ DCHECK(fragment->GetFirstLetterPseudoElement());
+
+ LayoutObject* pseudo_element_layout_object =
+ fragment->GetFirstLetterPseudoElement()->GetLayoutObject();
+ DCHECK(pseudo_element_layout_object);
+ DCHECK(pseudo_element_layout_object->SlowFirstChild());
+ LayoutText* first_letter_layout_object =
+ ToLayoutText(pseudo_element_layout_object->SlowFirstChild());
+
+ const int end_offset =
+ end_node_ == node_ && end_offset_ < offset_after_first_letter
+ ? end_offset_
+ : first_letter_layout_object->CaretMaxOffset();
+ offset_ =
+ end_offset + CollapsedSpaceLength(first_letter_layout_object, end_offset);
+
+ return first_letter_layout_object;
+}
+
+template <typename Strategy>
+bool SimplifiedBackwardsTextIteratorAlgorithm<
+ Strategy>::HandleReplacedElement() {
+ unsigned index = Strategy::Index(*node_);
+ // We want replaced elements to behave like punctuation for boundary
+ // finding, and to simply take up space for the selection preservation
+ // code in moveParagraphs, so we use a comma. Unconditionally emit
+ // here because this iterator is only used for boundary finding.
+ EmitCharacter(',', Strategy::Parent(*node_), index, index + 1);
+ return true;
+}
+
+template <typename Strategy>
+bool SimplifiedBackwardsTextIteratorAlgorithm<Strategy>::HandleNonTextNode() {
+ // We can use a linefeed in place of a tab because this simple iterator is
+ // only used to find boundaries, not actual content. A linefeed breaks words,
+ // sentences, and paragraphs.
+ if (TextIterator::ShouldEmitNewlineForNode(*node_, false) ||
+ TextIterator::ShouldEmitNewlineAfterNode(*node_) ||
+ TextIterator::ShouldEmitTabBeforeNode(*node_)) {
+ unsigned index = Strategy::Index(*node_);
+ // The start of this emitted range is wrong. Ensuring correctness would
+ // require VisiblePositions and so would be slow. previousBoundary expects
+ // this.
+ EmitCharacter('\n', Strategy::Parent(*node_), index + 1, index + 1);
+ }
+ return true;
+}
+
+template <typename Strategy>
+void SimplifiedBackwardsTextIteratorAlgorithm<Strategy>::ExitNode() {
+ if (TextIterator::ShouldEmitNewlineForNode(*node_, false) ||
+ TextIterator::ShouldEmitNewlineBeforeNode(*node_) ||
+ TextIterator::ShouldEmitTabBeforeNode(*node_)) {
+ // The start of this emitted range is wrong. Ensuring correctness would
+ // require VisiblePositions and so would be slow. previousBoundary expects
+ // this.
+ EmitCharacter('\n', node_, 0, 0);
+ }
+}
+
+template <typename Strategy>
+void SimplifiedBackwardsTextIteratorAlgorithm<Strategy>::EmitCharacter(
+ UChar c,
+ const Node* node,
+ int start_offset,
+ int end_offset) {
+ text_state_.SpliceBuffer(c, node, node, start_offset, end_offset);
+}
+
+template <typename Strategy>
+bool SimplifiedBackwardsTextIteratorAlgorithm<Strategy>::AdvanceRespectingRange(
+ const Node* next) {
+ if (!next)
+ return false;
+ have_passed_start_node_ |= node_ == start_node_;
+ if (have_passed_start_node_)
+ return false;
+ node_ = next;
+ return true;
+}
+
+template <typename Strategy>
+const Node* SimplifiedBackwardsTextIteratorAlgorithm<Strategy>::StartContainer()
+ const {
+ if (text_state_.PositionNode())
+ return text_state_.PositionNode();
+ return start_node_;
+}
+
+template <typename Strategy>
+int SimplifiedBackwardsTextIteratorAlgorithm<Strategy>::EndOffset() const {
+ if (text_state_.PositionNode())
+ return text_state_.PositionEndOffset();
+ return start_offset_;
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy>
+SimplifiedBackwardsTextIteratorAlgorithm<Strategy>::StartPosition() const {
+ if (text_state_.PositionNode()) {
+ return PositionTemplate<Strategy>::EditingPositionOf(
+ text_state_.PositionNode(), text_state_.PositionStartOffset());
+ }
+ return PositionTemplate<Strategy>::EditingPositionOf(start_node_.Get(),
+ start_offset_);
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy>
+SimplifiedBackwardsTextIteratorAlgorithm<Strategy>::EndPosition() const {
+ if (text_state_.PositionNode()) {
+ return PositionTemplate<Strategy>::EditingPositionOf(
+ text_state_.PositionNode(), text_state_.PositionEndOffset());
+ }
+ return PositionTemplate<Strategy>::EditingPositionOf(start_node_.Get(),
+ start_offset_);
+}
+
+template <typename Strategy>
+UChar SimplifiedBackwardsTextIteratorAlgorithm<Strategy>::CharacterAt(
+ unsigned index) const {
+ if (index >= text_state_.length())
+ return 0;
+ return text_state_.CharacterAt(text_state_.length() - index - 1);
+}
+
+template <typename Strategy>
+bool SimplifiedBackwardsTextIteratorAlgorithm<Strategy>::IsBetweenSurrogatePair(
+ int position) const {
+ DCHECK_GE(position, 0);
+ return position > 0 && position < length() &&
+ U16_IS_TRAIL(CharacterAt(position - 1)) &&
+ U16_IS_LEAD(CharacterAt(position));
+}
+
+template <typename Strategy>
+int SimplifiedBackwardsTextIteratorAlgorithm<Strategy>::CopyTextTo(
+ BackwardsTextBuffer* output,
+ int position,
+ int min_length) const {
+ int end = std::min(length(), position + min_length);
+ if (IsBetweenSurrogatePair(end))
+ ++end;
+ int copied_length = end - position;
+ text_state_.PrependTextTo(output, position, copied_length);
+ return copied_length;
+}
+
+template <typename Strategy>
+int SimplifiedBackwardsTextIteratorAlgorithm<Strategy>::CopyTextTo(
+ BackwardsTextBuffer* output,
+ int position) const {
+ return CopyTextTo(output, position, text_state_.length() - position);
+}
+
+template class CORE_TEMPLATE_EXPORT
+ SimplifiedBackwardsTextIteratorAlgorithm<EditingStrategy>;
+template class CORE_TEMPLATE_EXPORT
+ SimplifiedBackwardsTextIteratorAlgorithm<EditingInFlatTreeStrategy>;
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator.h b/chromium/third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator.h
new file mode 100644
index 00000000000..5eb6225f334
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator.h
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2004, 2006, 2009 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_SIMPLIFIED_BACKWARDS_TEXT_ITERATOR_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_SIMPLIFIED_BACKWARDS_TEXT_ITERATOR_H_
+
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/iterators/backwards_text_buffer.h"
+#include "third_party/blink/renderer/core/editing/iterators/fully_clipped_state_stack.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator_behavior.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator_text_state.h"
+#include "third_party/blink/renderer/platform/heap/heap.h"
+
+namespace blink {
+
+class LayoutText;
+
+// Iterates through the DOM range, returning all the text, and 0-length
+// boundaries at points where replaced elements break up the text flow. The text
+// comes back in chunks so as to optimize for performance of the iteration.
+template <typename Strategy>
+class CORE_TEMPLATE_CLASS_EXPORT SimplifiedBackwardsTextIteratorAlgorithm {
+ STACK_ALLOCATED();
+
+ public:
+ // Note: Usage of |behavior| should be limited, e.g.
+ // - EmitsObjectReplacementCharacter
+ // - StopsOnFormControls
+ // Please ask editing-dev if you want to use other behaviors.
+ SimplifiedBackwardsTextIteratorAlgorithm(
+ const EphemeralRangeTemplate<Strategy>&,
+ const TextIteratorBehavior& = TextIteratorBehavior());
+
+ bool AtEnd() const { return !text_state_.PositionNode() || should_stop_; }
+ void Advance();
+
+ int length() const { return text_state_.length(); }
+
+ // Note: |characterAt()| returns characters in the reversed order, since
+ // the iterator is backwards. For example, if the current text is "abc",
+ // then |characterAt(0)| returns 'c'.
+ UChar CharacterAt(unsigned index) const;
+
+ const Node* GetNode() const { return node_; }
+
+ // Calculate the minimum |actualLength >= minLength| such that code units
+ // with offset range [position, position + actualLength) are whole code
+ // points. Prepend these code points to |output| and return |actualLength|.
+ int CopyTextTo(BackwardsTextBuffer* output,
+ int position,
+ int min_length) const;
+ int CopyTextTo(BackwardsTextBuffer* output, int position = 0) const;
+
+ const Node* StartContainer() const;
+ int EndOffset() const;
+ PositionTemplate<Strategy> StartPosition() const;
+ PositionTemplate<Strategy> EndPosition() const;
+
+ private:
+ void Init(const Node* start_node,
+ const Node* end_node,
+ int start_offset,
+ int end_offset);
+ void ExitNode();
+ bool HandleTextNode();
+ LayoutText* HandleFirstLetter(int& start_offset, int& offset_in_node);
+ bool HandleReplacedElement();
+ bool HandleNonTextNode();
+ void EmitCharacter(UChar, const Node*, int start_offset, int end_offset);
+ bool AdvanceRespectingRange(const Node*);
+
+ bool IsBetweenSurrogatePair(int position) const;
+
+ TextIteratorBehavior behavior_;
+
+ // Contains state of emitted text.
+ TextIteratorTextState text_state_;
+
+ // Current position, not necessarily of the text being returned, but position
+ // as we walk through the DOM tree.
+ Member<const Node> node_;
+ int offset_;
+ bool handled_node_;
+ bool handled_children_;
+ FullyClippedStateStackAlgorithm<Strategy> fully_clipped_stack_;
+
+ // End of the range.
+ Member<const Node> start_node_;
+ int start_offset_;
+ // Start of the range.
+ Member<const Node> end_node_;
+ int end_offset_;
+
+ // Whether m_node has advanced beyond the iteration range (i.e. m_startNode).
+ bool have_passed_start_node_;
+
+ // Should handle first-letter layoutObject in the next call to handleTextNode.
+ bool should_handle_first_letter_;
+
+ // Used when m_stopsOnFormControls is set to determine if the iterator should
+ // keep advancing.
+ bool should_stop_;
+};
+
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ SimplifiedBackwardsTextIteratorAlgorithm<EditingStrategy>;
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ SimplifiedBackwardsTextIteratorAlgorithm<EditingInFlatTreeStrategy>;
+
+using SimplifiedBackwardsTextIterator =
+ SimplifiedBackwardsTextIteratorAlgorithm<EditingStrategy>;
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_SIMPLIFIED_BACKWARDS_TEXT_ITERATOR_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator_test.cc b/chromium/third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator_test.cc
new file mode 100644
index 00000000000..37a2316a4b3
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator_test.cc
@@ -0,0 +1,337 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator.h"
+
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+
+namespace blink {
+namespace simplified_backwards_text_iterator_test {
+
+TextIteratorBehavior EmitsSmallXForTextSecurityBehavior() {
+ return TextIteratorBehavior::Builder()
+ .SetEmitsSmallXForTextSecurity(true)
+ .Build();
+}
+
+class SimplifiedBackwardsTextIteratorTest : public EditingTestBase {
+ protected:
+ std::string ExtractStringInRange(
+ const std::string selection_text,
+ const TextIteratorBehavior& behavior = TextIteratorBehavior()) {
+ const SelectionInDOMTree selection = SetSelectionTextToBody(selection_text);
+ StringBuilder builder;
+ bool is_first = true;
+ for (SimplifiedBackwardsTextIterator iterator(selection.ComputeRange(),
+ behavior);
+ !iterator.AtEnd(); iterator.Advance()) {
+ BackwardsTextBuffer buffer;
+ iterator.CopyTextTo(&buffer);
+ if (!is_first)
+ builder.Append(", ", 2);
+ is_first = false;
+ builder.Append(buffer.Data(), buffer.Size());
+ }
+ CString utf8 = builder.ToString().Utf8();
+ return std::string(utf8.data(), utf8.length());
+ }
+};
+
+template <typename Strategy>
+static String ExtractString(const Element& element) {
+ const EphemeralRangeTemplate<Strategy> range =
+ EphemeralRangeTemplate<Strategy>::RangeOfContents(element);
+ BackwardsTextBuffer buffer;
+ for (SimplifiedBackwardsTextIteratorAlgorithm<Strategy> it(range);
+ !it.AtEnd(); it.Advance()) {
+ it.CopyTextTo(&buffer);
+ }
+ return String(buffer.Data(), buffer.Size());
+}
+
+TEST_F(SimplifiedBackwardsTextIteratorTest, CopyTextToWithFirstLetterPart) {
+ InsertStyleElement("p::first-letter {font-size: 200%}");
+ // TODO(editing-dev): |SimplifiedBackwardsTextIterator| should not account
+ // collapsed whitespace (http://crbug.com/760428)
+
+ // Simulate PreviousBoundary()
+ EXPECT_EQ(" , \n", ExtractStringInRange("^<p> |[(3)]678</p>"));
+ EXPECT_EQ(" [, \n", ExtractStringInRange("^<p> [|(3)]678</p>"));
+ EXPECT_EQ(" [(, \n", ExtractStringInRange("^<p> [(|3)]678</p>"));
+ EXPECT_EQ(" [(3, \n", ExtractStringInRange("^<p> [(3|)]678</p>"));
+ EXPECT_EQ(" [(3), \n", ExtractStringInRange("^<p> [(3)|]678</p>"));
+ EXPECT_EQ(" [(3)], \n", ExtractStringInRange("^<p> [(3)]|678</p>"));
+
+ EXPECT_EQ("6, [(3)], \n, ab", ExtractStringInRange("^ab<p> [(3)]6|78</p>"))
+ << "From remaining part to outside";
+
+ EXPECT_EQ("(3)", ExtractStringInRange("<p> [^(3)|]678</p>"))
+ << "Iterate in first-letter part";
+
+ EXPECT_EQ("67, (3)]", ExtractStringInRange("<p> [^(3)]67|8</p>"))
+ << "From remaining part to first-letter part";
+
+ EXPECT_EQ("789", ExtractStringInRange("<p> [(3)]6^789|a</p>"))
+ << "Iterate in remaining part";
+
+ EXPECT_EQ("9, \n, 78", ExtractStringInRange("<p> [(3)]6^78</p>9|a"))
+ << "Enter into remaining part and stop in remaining part";
+
+ EXPECT_EQ("9, \n, 678, (3)]", ExtractStringInRange("<p> [^(3)]678</p>9|a"))
+ << "Enter into remaining part and stop in first-letter part";
+}
+
+TEST_F(SimplifiedBackwardsTextIteratorTest, Basic) {
+ SetBodyContent("<p> [(3)]678</p>");
+ const Element* const sample = GetDocument().QuerySelector("p");
+ SimplifiedBackwardsTextIterator iterator(EphemeralRange(
+ Position(sample->firstChild(), 0), Position(sample->firstChild(), 9)));
+ // TODO(editing-dev): |SimplifiedBackwardsTextIterator| should not account
+ // collapsed whitespace (http://crbug.com/760428)
+ EXPECT_EQ(9, iterator.length())
+ << "We should have 8 as ignoring collapsed whitespace.";
+ EXPECT_EQ(Position(sample->firstChild(), 0), iterator.StartPosition());
+ EXPECT_EQ(Position(sample->firstChild(), 9), iterator.EndPosition());
+ EXPECT_EQ(sample->firstChild(), iterator.StartContainer());
+ EXPECT_EQ(9, iterator.EndOffset());
+ EXPECT_EQ(sample->firstChild(), iterator.GetNode());
+ EXPECT_EQ('8', iterator.CharacterAt(0));
+ EXPECT_EQ('7', iterator.CharacterAt(1));
+ EXPECT_EQ('6', iterator.CharacterAt(2));
+ EXPECT_EQ(']', iterator.CharacterAt(3));
+ EXPECT_EQ(')', iterator.CharacterAt(4));
+ EXPECT_EQ('3', iterator.CharacterAt(5));
+ EXPECT_EQ('(', iterator.CharacterAt(6));
+ EXPECT_EQ('[', iterator.CharacterAt(7));
+ EXPECT_EQ(' ', iterator.CharacterAt(8));
+
+ EXPECT_FALSE(iterator.AtEnd());
+ iterator.Advance();
+ EXPECT_TRUE(iterator.AtEnd());
+}
+
+TEST_F(SimplifiedBackwardsTextIteratorTest, FirstLetter) {
+ SetBodyContent(
+ "<style>p::first-letter {font-size: 200%}</style>"
+ "<p> [(3)]678</p>");
+ const Element* const sample = GetDocument().QuerySelector("p");
+ SimplifiedBackwardsTextIterator iterator(EphemeralRange(
+ Position(sample->firstChild(), 0), Position(sample->firstChild(), 9)));
+ EXPECT_EQ(3, iterator.length());
+ EXPECT_EQ(Position(sample->firstChild(), 6), iterator.StartPosition());
+ EXPECT_EQ(Position(sample->firstChild(), 9), iterator.EndPosition());
+ EXPECT_EQ(sample->firstChild(), iterator.StartContainer());
+ EXPECT_EQ(9, iterator.EndOffset());
+ EXPECT_EQ(sample->firstChild(), iterator.GetNode());
+ EXPECT_EQ('8', iterator.CharacterAt(0));
+ EXPECT_EQ('7', iterator.CharacterAt(1));
+ EXPECT_EQ('6', iterator.CharacterAt(2));
+
+ iterator.Advance();
+ // TODO(editing-dev): |SimplifiedBackwardsTextIterator| should not account
+ // collapsed whitespace (http://crbug.com/760428)
+ EXPECT_EQ(6, iterator.length())
+ << "We should have 5 as ignoring collapsed whitespace.";
+ EXPECT_EQ(Position(sample->firstChild(), 0), iterator.StartPosition());
+ EXPECT_EQ(Position(sample->firstChild(), 6), iterator.EndPosition());
+ EXPECT_EQ(sample->firstChild(), iterator.StartContainer());
+ EXPECT_EQ(6, iterator.EndOffset());
+ EXPECT_EQ(sample->firstChild(), iterator.GetNode());
+ EXPECT_EQ(']', iterator.CharacterAt(0));
+ EXPECT_EQ(')', iterator.CharacterAt(1));
+ EXPECT_EQ('3', iterator.CharacterAt(2));
+ EXPECT_EQ('(', iterator.CharacterAt(3));
+ EXPECT_EQ('[', iterator.CharacterAt(4));
+ EXPECT_EQ(' ', iterator.CharacterAt(5));
+
+ EXPECT_FALSE(iterator.AtEnd());
+ iterator.Advance();
+ EXPECT_TRUE(iterator.AtEnd());
+}
+
+TEST_F(SimplifiedBackwardsTextIteratorTest, SubrangeWithReplacedElements) {
+ static const char* body_content =
+ "<a id=host><b id=one>one</b> not appeared <b id=two>two</b></a>";
+ const char* shadow_content =
+ "three <content select=#two></content> <content select=#one></content> "
+ "zero";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ Element* host = GetDocument().getElementById("host");
+
+ // We should not apply DOM tree version to containing shadow tree in
+ // general. To record current behavior, we have this test. even if it
+ // isn't intuitive.
+ EXPECT_EQ("onetwo", ExtractString<EditingStrategy>(*host));
+ EXPECT_EQ("three two one zero",
+ ExtractString<EditingInFlatTreeStrategy>(*host));
+}
+
+TEST_F(SimplifiedBackwardsTextIteratorTest, characterAt) {
+ const char* body_content =
+ "<a id=host><b id=one>one</b> not appeared <b id=two>two</b></a>";
+ const char* shadow_content =
+ "three <content select=#two></content> <content select=#one></content> "
+ "zero";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ Element* host = GetDocument().getElementById("host");
+
+ EphemeralRangeTemplate<EditingStrategy> range1(
+ EphemeralRangeTemplate<EditingStrategy>::RangeOfContents(*host));
+ SimplifiedBackwardsTextIteratorAlgorithm<EditingStrategy> back_iter1(range1);
+ const char* message1 =
+ "|backIter1| should emit 'one' and 'two' in reverse order.";
+ EXPECT_EQ('o', back_iter1.CharacterAt(0)) << message1;
+ EXPECT_EQ('w', back_iter1.CharacterAt(1)) << message1;
+ EXPECT_EQ('t', back_iter1.CharacterAt(2)) << message1;
+ back_iter1.Advance();
+ EXPECT_EQ('e', back_iter1.CharacterAt(0)) << message1;
+ EXPECT_EQ('n', back_iter1.CharacterAt(1)) << message1;
+ EXPECT_EQ('o', back_iter1.CharacterAt(2)) << message1;
+
+ EphemeralRangeTemplate<EditingInFlatTreeStrategy> range2(
+ EphemeralRangeTemplate<EditingInFlatTreeStrategy>::RangeOfContents(
+ *host));
+ SimplifiedBackwardsTextIteratorAlgorithm<EditingInFlatTreeStrategy>
+ back_iter2(range2);
+ const char* message2 =
+ "|backIter2| should emit 'three ', 'two', ' ', 'one' and ' zero' in "
+ "reverse order.";
+ EXPECT_EQ('o', back_iter2.CharacterAt(0)) << message2;
+ EXPECT_EQ('r', back_iter2.CharacterAt(1)) << message2;
+ EXPECT_EQ('e', back_iter2.CharacterAt(2)) << message2;
+ EXPECT_EQ('z', back_iter2.CharacterAt(3)) << message2;
+ EXPECT_EQ(' ', back_iter2.CharacterAt(4)) << message2;
+ back_iter2.Advance();
+ EXPECT_EQ('e', back_iter2.CharacterAt(0)) << message2;
+ EXPECT_EQ('n', back_iter2.CharacterAt(1)) << message2;
+ EXPECT_EQ('o', back_iter2.CharacterAt(2)) << message2;
+ back_iter2.Advance();
+ EXPECT_EQ(' ', back_iter2.CharacterAt(0)) << message2;
+ back_iter2.Advance();
+ EXPECT_EQ('o', back_iter2.CharacterAt(0)) << message2;
+ EXPECT_EQ('w', back_iter2.CharacterAt(1)) << message2;
+ EXPECT_EQ('t', back_iter2.CharacterAt(2)) << message2;
+ back_iter2.Advance();
+ EXPECT_EQ(' ', back_iter2.CharacterAt(0)) << message2;
+ EXPECT_EQ('e', back_iter2.CharacterAt(1)) << message2;
+ EXPECT_EQ('e', back_iter2.CharacterAt(2)) << message2;
+ EXPECT_EQ('r', back_iter2.CharacterAt(3)) << message2;
+ EXPECT_EQ('h', back_iter2.CharacterAt(4)) << message2;
+ EXPECT_EQ('t', back_iter2.CharacterAt(5)) << message2;
+}
+
+TEST_F(SimplifiedBackwardsTextIteratorTest, copyTextTo) {
+ const char* body_content =
+ "<a id=host><b id=one>one</b> not appeared <b id=two>two</b></a>";
+ const char* shadow_content =
+ "three <content select=#two></content> <content select=#one></content> "
+ "zero";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ Element* host = GetDocument().getElementById("host");
+ const char* message =
+ "|backIter%d| should have emitted '%s' in reverse order.";
+
+ EphemeralRangeTemplate<EditingStrategy> range1(
+ EphemeralRangeTemplate<EditingStrategy>::RangeOfContents(*host));
+ SimplifiedBackwardsTextIteratorAlgorithm<EditingStrategy> back_iter1(range1);
+ BackwardsTextBuffer output1;
+ back_iter1.CopyTextTo(&output1, 0, 2);
+ EXPECT_EQ("wo", String(output1.Data(), output1.Size()))
+ << String::Format(message, 1, "wo").Utf8().data();
+ back_iter1.CopyTextTo(&output1, 2, 1);
+ EXPECT_EQ("two", String(output1.Data(), output1.Size()))
+ << String::Format(message, 1, "two").Utf8().data();
+ back_iter1.Advance();
+ back_iter1.CopyTextTo(&output1, 0, 1);
+ EXPECT_EQ("etwo", String(output1.Data(), output1.Size()))
+ << String::Format(message, 1, "etwo").Utf8().data();
+ back_iter1.CopyTextTo(&output1, 1, 2);
+ EXPECT_EQ("onetwo", String(output1.Data(), output1.Size()))
+ << String::Format(message, 1, "onetwo").Utf8().data();
+
+ EphemeralRangeTemplate<EditingInFlatTreeStrategy> range2(
+ EphemeralRangeTemplate<EditingInFlatTreeStrategy>::RangeOfContents(
+ *host));
+ SimplifiedBackwardsTextIteratorAlgorithm<EditingInFlatTreeStrategy>
+ back_iter2(range2);
+ BackwardsTextBuffer output2;
+ back_iter2.CopyTextTo(&output2, 0, 2);
+ EXPECT_EQ("ro", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, "ro").Utf8().data();
+ back_iter2.CopyTextTo(&output2, 2, 3);
+ EXPECT_EQ(" zero", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, " zero").Utf8().data();
+ back_iter2.Advance();
+ back_iter2.CopyTextTo(&output2, 0, 1);
+ EXPECT_EQ("e zero", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, "e zero").Utf8().data();
+ back_iter2.CopyTextTo(&output2, 1, 2);
+ EXPECT_EQ("one zero", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, "one zero").Utf8().data();
+ back_iter2.Advance();
+ back_iter2.CopyTextTo(&output2, 0, 1);
+ EXPECT_EQ(" one zero", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, " one zero").Utf8().data();
+ back_iter2.Advance();
+ back_iter2.CopyTextTo(&output2, 0, 2);
+ EXPECT_EQ("wo one zero", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, "wo one zero").Utf8().data();
+ back_iter2.CopyTextTo(&output2, 2, 1);
+ EXPECT_EQ("two one zero", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, "two one zero").Utf8().data();
+ back_iter2.Advance();
+ back_iter2.CopyTextTo(&output2, 0, 3);
+ EXPECT_EQ("ee two one zero", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, "ee two one zero").Utf8().data();
+ back_iter2.CopyTextTo(&output2, 3, 3);
+ EXPECT_EQ("three two one zero", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, "three two one zero").Utf8().data();
+}
+
+TEST_F(SimplifiedBackwardsTextIteratorTest, CopyWholeCodePoints) {
+ const char* body_content = "&#x13000;&#x13001;&#x13002; &#x13140;&#x13141;.";
+ SetBodyContent(body_content);
+
+ const UChar kExpected[] = {0xD80C, 0xDC00, 0xD80C, 0xDC01, 0xD80C, 0xDC02,
+ ' ', 0xD80C, 0xDD40, 0xD80C, 0xDD41, '.'};
+
+ EphemeralRange range(EphemeralRange::RangeOfContents(GetDocument()));
+ SimplifiedBackwardsTextIterator iter(range);
+ BackwardsTextBuffer buffer;
+ EXPECT_EQ(1, iter.CopyTextTo(&buffer, 0, 1))
+ << "Should emit 1 UChar for '.'.";
+ EXPECT_EQ(2, iter.CopyTextTo(&buffer, 1, 1))
+ << "Should emit 2 UChars for 'U+13141'.";
+ EXPECT_EQ(2, iter.CopyTextTo(&buffer, 3, 2))
+ << "Should emit 2 UChars for 'U+13140'.";
+ EXPECT_EQ(5, iter.CopyTextTo(&buffer, 5, 4))
+ << "Should emit 5 UChars for 'U+13001U+13002 '.";
+ EXPECT_EQ(2, iter.CopyTextTo(&buffer, 10, 2))
+ << "Should emit 2 UChars for 'U+13000'.";
+ for (int i = 0; i < 12; i++)
+ EXPECT_EQ(kExpected[i], buffer[i]);
+}
+
+TEST_F(SimplifiedBackwardsTextIteratorTest, TextSecurity) {
+ InsertStyleElement("s {-webkit-text-security:disc;}");
+ EXPECT_EQ("baz, xxx, abc",
+ ExtractStringInRange("^abc<s>foo</s>baz|",
+ EmitsSmallXForTextSecurityBehavior()));
+ // E2 80 A2 is U+2022 BULLET
+ EXPECT_EQ("baz, \xE2\x80\xA2\xE2\x80\xA2\xE2\x80\xA2, abc",
+ ExtractStringInRange("^abc<s>foo</s>baz|", TextIteratorBehavior()));
+}
+
+} // namespace simplified_backwards_text_iterator_test
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/text_buffer_base.cc b/chromium/third_party/blink/renderer/core/editing/iterators/text_buffer_base.cc
new file mode 100644
index 00000000000..0308c865faf
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/text_buffer_base.cc
@@ -0,0 +1,37 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/iterators/text_buffer_base.h"
+
+namespace blink {
+
+TextBufferBase::TextBufferBase() {
+ buffer_.ReserveCapacity(1024);
+ buffer_.resize(Capacity());
+}
+
+void TextBufferBase::ShiftData(size_t) {}
+
+void TextBufferBase::PushCharacters(UChar ch, size_t length) {
+ if (length == 0)
+ return;
+ std::fill_n(EnsureDestination(length), length, ch);
+}
+
+UChar* TextBufferBase::EnsureDestination(size_t length) {
+ if (size_ + length > Capacity())
+ Grow(size_ + length);
+ UChar* ans = CalcDestination(length);
+ size_ += length;
+ return ans;
+}
+
+void TextBufferBase::Grow(size_t demand) {
+ size_t old_capacity = Capacity();
+ buffer_.resize(demand);
+ buffer_.resize(Capacity());
+ ShiftData(old_capacity);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/text_buffer_base.h b/chromium/third_party/blink/renderer/core/editing/iterators/text_buffer_base.h
new file mode 100644
index 00000000000..568ce9ffd30
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/text_buffer_base.h
@@ -0,0 +1,65 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_BUFFER_BASE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_BUFFER_BASE_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+#include "third_party/blink/renderer/platform/wtf/vector.h"
+
+namespace blink {
+
+class CORE_EXPORT TextBufferBase {
+ STACK_ALLOCATED();
+
+ public:
+ void Clear() { size_ = 0; }
+ size_t Size() const { return size_; }
+ bool IsEmpty() const { return size_ == 0; }
+ size_t Capacity() const { return buffer_.capacity(); }
+ const UChar& operator[](size_t index) const {
+ DCHECK_LT(index, size_);
+ return Data()[index];
+ }
+ virtual const UChar* Data() const = 0;
+
+ void PushCharacters(UChar, size_t length);
+
+ template <typename T>
+ void PushRange(const T* other, size_t length) {
+ if (length == 0)
+ return;
+ std::copy(other, other + length, EnsureDestination(length));
+ }
+
+ void Shrink(size_t delta) {
+ DCHECK_LE(delta, size_);
+ size_ -= delta;
+ }
+
+ protected:
+ TextBufferBase();
+ UChar* EnsureDestination(size_t length);
+ void Grow(size_t demand);
+
+ virtual UChar* CalcDestination(size_t length) = 0;
+ virtual void ShiftData(size_t old_capacity);
+
+ const UChar* BufferBegin() const { return buffer_.begin(); }
+ const UChar* BufferEnd() const { return buffer_.end(); }
+ UChar* BufferBegin() { return buffer_.begin(); }
+ UChar* BufferEnd() { return buffer_.end(); }
+
+ private:
+ size_t size_ = 0;
+ Vector<UChar, 1024> buffer_;
+
+ DISALLOW_COPY_AND_ASSIGN(TextBufferBase);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_BUFFER_BASE_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator.cc b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator.cc
new file mode 100644
index 00000000000..e6e63a967f9
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator.cc
@@ -0,0 +1,1003 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012 Apple Inc. All
+ * rights reserved.
+ * Copyright (C) 2005 Alexey Proskuryakov.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+
+#include <unicode/utf16.h>
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/shadow_root.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/use_counter.h"
+#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/html/html_image_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/input_type_names.h"
+#include "third_party/blink/renderer/core/layout/layout_table_cell.h"
+#include "third_party/blink/renderer/core/layout/layout_table_row.h"
+#include "third_party/blink/renderer/platform/fonts/font.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+namespace {
+
+template <typename Strategy>
+TextIteratorBehavior AdjustBehaviorFlags(const TextIteratorBehavior&);
+
+template <>
+TextIteratorBehavior AdjustBehaviorFlags<EditingStrategy>(
+ const TextIteratorBehavior& behavior) {
+ if (!behavior.ForSelectionToString())
+ return behavior;
+ return TextIteratorBehavior::Builder(behavior)
+ .SetExcludeAutofilledValue(true)
+ .Build();
+}
+
+template <>
+TextIteratorBehavior AdjustBehaviorFlags<EditingInFlatTreeStrategy>(
+ const TextIteratorBehavior& behavior) {
+ return TextIteratorBehavior::Builder(behavior)
+ .SetExcludeAutofilledValue(behavior.ForSelectionToString() ||
+ behavior.ExcludeAutofilledValue())
+ .SetEntersOpenShadowRoots(false)
+ .SetEntersTextControls(false)
+ .Build();
+}
+
+static inline bool HasDisplayContents(const Node& node) {
+ return node.IsElementNode() && ToElement(node).HasDisplayContentsStyle();
+}
+
+// Checks if |advance()| skips the descendants of |node|, which is the case if
+// |node| is neither a shadow root nor the owner of a layout object.
+static bool NotSkipping(const Node& node) {
+ return node.GetLayoutObject() || HasDisplayContents(node) ||
+ (node.IsShadowRoot() && node.OwnerShadowHost()->GetLayoutObject());
+}
+
+template <typename Strategy>
+const Node* StartNode(const Node* start_container, unsigned start_offset) {
+ if (start_container->IsCharacterDataNode())
+ return start_container;
+ if (Node* child = Strategy::ChildAt(*start_container, start_offset))
+ return child;
+ if (!start_offset)
+ return start_container;
+ return Strategy::NextSkippingChildren(*start_container);
+}
+
+template <typename Strategy>
+const Node* EndNode(const Node& end_container, unsigned end_offset) {
+ if (!end_container.IsCharacterDataNode() && end_offset)
+ return Strategy::ChildAt(end_container, end_offset - 1);
+ return nullptr;
+}
+
+// This function is like Range::PastLastNode, except for the fact that it can
+// climb up out of shadow trees and ignores all nodes that will be skipped in
+// |advance()|.
+template <typename Strategy>
+const Node* PastLastNode(const Node& range_end_container,
+ unsigned range_end_offset) {
+ if (!range_end_container.IsCharacterDataNode() &&
+ NotSkipping(range_end_container)) {
+ for (Node* next = Strategy::ChildAt(range_end_container, range_end_offset);
+ next; next = Strategy::NextSibling(*next)) {
+ if (NotSkipping(*next))
+ return next;
+ }
+ }
+ for (const Node* node = &range_end_container; node;) {
+ const Node* parent = ParentCrossingShadowBoundaries<Strategy>(*node);
+ if (parent && NotSkipping(*parent)) {
+ if (Node* next = Strategy::NextSibling(*node))
+ return next;
+ }
+ node = parent;
+ }
+ return nullptr;
+}
+
+// Figure out the initial value of m_shadowDepth: the depth of startContainer's
+// tree scope from the common ancestor tree scope.
+template <typename Strategy>
+unsigned ShadowDepthOf(const Node& start_container, const Node& end_container);
+
+template <>
+unsigned ShadowDepthOf<EditingStrategy>(const Node& start_container,
+ const Node& end_container) {
+ const TreeScope* common_ancestor_tree_scope =
+ start_container.GetTreeScope().CommonAncestorTreeScope(
+ end_container.GetTreeScope());
+ DCHECK(common_ancestor_tree_scope);
+ unsigned shadow_depth = 0;
+ for (const TreeScope* tree_scope = &start_container.GetTreeScope();
+ tree_scope != common_ancestor_tree_scope;
+ tree_scope = tree_scope->ParentTreeScope())
+ ++shadow_depth;
+ return shadow_depth;
+}
+
+template <>
+unsigned ShadowDepthOf<EditingInFlatTreeStrategy>(const Node& start_container,
+ const Node& end_container) {
+ return 0;
+}
+
+bool IsRenderedAsTable(const Node* node) {
+ if (!node || !node->IsElementNode())
+ return false;
+ LayoutObject* layout_object = node->GetLayoutObject();
+ return layout_object && layout_object->IsTable();
+}
+
+} // namespace
+
+template <typename Strategy>
+TextIteratorAlgorithm<Strategy>::TextIteratorAlgorithm(
+ const EphemeralRangeTemplate<Strategy>& range,
+ const TextIteratorBehavior& behavior)
+ : TextIteratorAlgorithm(range.StartPosition(),
+ range.EndPosition(),
+ behavior) {}
+
+template <typename Strategy>
+TextIteratorAlgorithm<Strategy>::TextIteratorAlgorithm(
+ const PositionTemplate<Strategy>& start,
+ const PositionTemplate<Strategy>& end,
+ const TextIteratorBehavior& behavior)
+ : start_container_(start.ComputeContainerNode()),
+ start_offset_(start.ComputeOffsetInContainerNode()),
+ end_container_(end.ComputeContainerNode()),
+ end_offset_(end.ComputeOffsetInContainerNode()),
+ end_node_(EndNode<Strategy>(*end_container_, end_offset_)),
+ past_end_node_(PastLastNode<Strategy>(*end_container_, end_offset_)),
+ node_(StartNode<Strategy>(start_container_, start_offset_)),
+ iteration_progress_(kHandledNone),
+ shadow_depth_(
+ ShadowDepthOf<Strategy>(*start_container_, *end_container_)),
+ behavior_(AdjustBehaviorFlags<Strategy>(behavior)),
+ text_state_(behavior_),
+ text_node_handler_(behavior_, &text_state_) {
+ DCHECK(start_container_);
+ DCHECK(end_container_);
+
+ // TODO(dglazkov): TextIterator should not be created for documents that don't
+ // have a frame, but it currently still happens in some cases. See
+ // http://crbug.com/591877 for details.
+ DCHECK(!start.GetDocument()->View() ||
+ !start.GetDocument()->View()->NeedsLayout());
+ DCHECK(!start.GetDocument()->NeedsLayoutTreeUpdate());
+ // To avoid renderer hang, we use |CHECK_LE()| to catch the bad callers
+ // in release build.
+ CHECK_LE(start, end);
+
+ if (!node_)
+ return;
+
+ fully_clipped_stack_.SetUpFullyClippedStack(node_);
+
+ // Identify the first run.
+ Advance();
+}
+
+template <typename Strategy>
+TextIteratorAlgorithm<Strategy>::~TextIteratorAlgorithm() {
+ if (!handle_shadow_root_)
+ return;
+ Document* document = OwnerDocument();
+ if (!document)
+ return;
+ if (behavior_.ForInnerText())
+ UseCounter::Count(document, WebFeature::kInnerTextWithShadowTree);
+ if (behavior_.ForSelectionToString())
+ UseCounter::Count(document, WebFeature::kSelectionToStringWithShadowTree);
+ if (behavior_.ForWindowFind())
+ UseCounter::Count(document, WebFeature::kWindowFindWithShadowTree);
+}
+
+template <typename Strategy>
+bool TextIteratorAlgorithm<Strategy>::IsInsideAtomicInlineElement() const {
+ if (AtEnd() || length() != 1 || !node_)
+ return false;
+
+ LayoutObject* layout_object = node_->GetLayoutObject();
+ return layout_object && layout_object->IsAtomicInlineLevel();
+}
+
+template <typename Strategy>
+bool TextIteratorAlgorithm<Strategy>::HandleRememberedProgress() {
+ // Handle remembered node that needed a newline after the text node's newline
+ if (needs_another_newline_) {
+ // Emit the extra newline, and position it *inside* m_node, after m_node's
+ // contents, in case it's a block, in the same way that we position the
+ // first newline. The range for the emitted newline should start where the
+ // line break begins.
+ // FIXME: It would be cleaner if we emitted two newlines during the last
+ // iteration, instead of using m_needsAnotherNewline.
+ Node* last_child = Strategy::LastChild(*node_);
+ const Node* base_node = last_child ? last_child : node_.Get();
+ SpliceBuffer('\n', Strategy::Parent(*base_node), base_node, 1, 1);
+ needs_another_newline_ = false;
+ return true;
+ }
+
+ if (needs_handle_replaced_element_) {
+ HandleReplacedElement();
+ if (text_state_.PositionNode())
+ return true;
+ }
+
+ // Try to emit more text runs if we are handling a text node.
+ return text_node_handler_.HandleRemainingTextRuns();
+}
+
+template <typename Strategy>
+void TextIteratorAlgorithm<Strategy>::Advance() {
+ if (should_stop_)
+ return;
+
+ if (node_)
+ DCHECK(!node_->GetDocument().NeedsLayoutTreeUpdate()) << node_;
+
+ text_state_.ResetRunInformation();
+
+ if (HandleRememberedProgress())
+ return;
+
+ while (node_ && (node_ != past_end_node_ || shadow_depth_)) {
+ if (!should_stop_ && StopsOnFormControls() &&
+ HTMLFormControlElement::EnclosingFormControlElement(node_))
+ should_stop_ = true;
+
+ // if the range ends at offset 0 of an element, represent the
+ // position, but not the content, of that element e.g. if the
+ // node is a blockflow element, emit a newline that
+ // precedes the element
+ if (node_ == end_container_ && !end_offset_) {
+ RepresentNodeOffsetZero();
+ node_ = nullptr;
+ return;
+ }
+
+ LayoutObject* layout_object = node_->GetLayoutObject();
+ if (!layout_object) {
+ if (node_->IsShadowRoot() || HasDisplayContents(*node_)) {
+ // Shadow roots or display: contents elements don't have LayoutObjects,
+ // but we want to visit children anyway.
+ iteration_progress_ = iteration_progress_ < kHandledNode
+ ? kHandledNode
+ : iteration_progress_;
+ handle_shadow_root_ = node_->IsShadowRoot();
+ } else {
+ iteration_progress_ = kHandledChildren;
+ }
+ } else {
+ // Enter author shadow roots, from youngest, if any and if necessary.
+ if (iteration_progress_ < kHandledOpenShadowRoots) {
+ if (EntersOpenShadowRoots() && node_->IsElementNode() &&
+ ToElement(node_)->OpenShadowRoot()) {
+ ShadowRoot* youngest_shadow_root = ToElement(node_)->OpenShadowRoot();
+ DCHECK(youngest_shadow_root->GetType() == ShadowRootType::V0 ||
+ youngest_shadow_root->GetType() == ShadowRootType::kOpen);
+ node_ = youngest_shadow_root;
+ iteration_progress_ = kHandledNone;
+ ++shadow_depth_;
+ fully_clipped_stack_.PushFullyClippedState(node_);
+ continue;
+ }
+
+ iteration_progress_ = kHandledOpenShadowRoots;
+ }
+
+ // Enter user-agent shadow root, if necessary.
+ if (iteration_progress_ < kHandledUserAgentShadowRoot) {
+ if (EntersTextControls() && layout_object->IsTextControl()) {
+ ShadowRoot* user_agent_shadow_root =
+ ToElement(node_)->UserAgentShadowRoot();
+ DCHECK(user_agent_shadow_root->IsUserAgent());
+ node_ = user_agent_shadow_root;
+ iteration_progress_ = kHandledNone;
+ ++shadow_depth_;
+ fully_clipped_stack_.PushFullyClippedState(node_);
+ continue;
+ }
+ iteration_progress_ = kHandledUserAgentShadowRoot;
+ }
+
+ // Handle the current node according to its type.
+ if (iteration_progress_ < kHandledNode) {
+ if (!SkipsUnselectableContent() || layout_object->IsSelectable()) {
+ if (layout_object->IsText() &&
+ node_->getNodeType() ==
+ Node::kTextNode) { // FIXME: What about kCdataSectionNode?
+ if (!fully_clipped_stack_.Top() || IgnoresStyleVisibility())
+ HandleTextNode();
+ } else if (layout_object &&
+ (layout_object->IsImage() ||
+ layout_object->IsLayoutEmbeddedContent() ||
+ (node_ && node_->IsHTMLElement() &&
+ (IsHTMLFormControlElement(ToHTMLElement(*node_)) ||
+ IsHTMLLegendElement(ToHTMLElement(*node_)) ||
+ IsHTMLImageElement(ToHTMLElement(*node_)) ||
+ IsHTMLMeterElement(ToHTMLElement(*node_)) ||
+ IsHTMLProgressElement(ToHTMLElement(*node_)))))) {
+ HandleReplacedElement();
+ } else {
+ HandleNonTextNode();
+ }
+ }
+ iteration_progress_ = kHandledNode;
+ if (text_state_.PositionNode())
+ return;
+ }
+ }
+
+ // Find a new current node to handle in depth-first manner,
+ // calling exitNode() as we come back thru a parent node.
+ //
+ // 1. Iterate over child nodes, if we haven't done yet.
+ // To support |TextIteratorEmitsImageAltText|, we don't traversal child
+ // nodes, in flat tree.
+ Node* next =
+ iteration_progress_ < kHandledChildren && !IsHTMLImageElement(*node_)
+ ? Strategy::FirstChild(*node_)
+ : nullptr;
+ if (!next) {
+ // 2. If we've already iterated children or they are not available, go to
+ // the next sibling node.
+ next = Strategy::NextSibling(*node_);
+ if (!next) {
+ // 3. If we are at the last child, go up the node tree until we find a
+ // next sibling.
+ ContainerNode* parent_node = Strategy::Parent(*node_);
+ while (!next && parent_node) {
+ if (node_ == end_node_ ||
+ Strategy::IsDescendantOf(*end_container_, *parent_node))
+ return;
+ bool have_layout_object = node_->GetLayoutObject();
+ node_ = parent_node;
+ fully_clipped_stack_.Pop();
+ parent_node = Strategy::Parent(*node_);
+ if (have_layout_object)
+ ExitNode();
+ if (text_state_.PositionNode()) {
+ iteration_progress_ = kHandledChildren;
+ return;
+ }
+ next = Strategy::NextSibling(*node_);
+ }
+
+ if (!next && !parent_node && shadow_depth_) {
+ // 4. Reached the top of a shadow root. If it's created by author,
+ // then try to visit the next
+ // sibling shadow root, if any.
+ if (!node_->IsShadowRoot()) {
+ NOTREACHED();
+ should_stop_ = true;
+ return;
+ }
+ const ShadowRoot* shadow_root = ToShadowRoot(node_);
+ if (shadow_root->GetType() == ShadowRootType::V0 ||
+ shadow_root->GetType() == ShadowRootType::kOpen) {
+ // We are the shadow root; exit from here and go back to
+ // where we were.
+ node_ = &shadow_root->host();
+ iteration_progress_ = kHandledOpenShadowRoots;
+ --shadow_depth_;
+ fully_clipped_stack_.Pop();
+ } else {
+ // If we are in a closed or user-agent shadow root, then go back to
+ // the host.
+ // TODO(kochi): Make sure we treat closed shadow as user agent
+ // shadow here.
+ DCHECK(shadow_root->GetType() == ShadowRootType::kClosed ||
+ shadow_root->IsUserAgent());
+ node_ = &shadow_root->host();
+ iteration_progress_ = kHandledUserAgentShadowRoot;
+ --shadow_depth_;
+ fully_clipped_stack_.Pop();
+ }
+ continue;
+ }
+ }
+ fully_clipped_stack_.Pop();
+ }
+
+ // set the new current node
+ node_ = next;
+ if (node_)
+ fully_clipped_stack_.PushFullyClippedState(node_);
+ iteration_progress_ = kHandledNone;
+
+ // how would this ever be?
+ if (text_state_.PositionNode())
+ return;
+ }
+}
+
+template <typename Strategy>
+void TextIteratorAlgorithm<Strategy>::HandleTextNode() {
+ if (ExcludesAutofilledValue()) {
+ TextControlElement* control = EnclosingTextControl(node_);
+ // For security reason, we don't expose suggested value if it is
+ // auto-filled.
+ if (control && control->IsAutofilled())
+ return;
+ }
+
+ DCHECK_NE(last_text_node_, node_)
+ << "We should never call HandleTextNode on the same node twice";
+ const Text* text = ToText(node_);
+ last_text_node_ = text;
+
+ // TODO(editing-dev): Introduce a |DOMOffsetRange| class so that we can pass
+ // an offset range with unbounded endpoint(s) in an easy but still clear way.
+ if (node_ != start_container_) {
+ if (node_ != end_container_)
+ text_node_handler_.HandleTextNodeWhole(text);
+ else
+ text_node_handler_.HandleTextNodeEndAt(text, end_offset_);
+ return;
+ }
+ if (node_ != end_container_) {
+ text_node_handler_.HandleTextNodeStartFrom(text, start_offset_);
+ return;
+ }
+ text_node_handler_.HandleTextNodeInRange(text, start_offset_, end_offset_);
+}
+
+template <typename Strategy>
+bool TextIteratorAlgorithm<Strategy>::SupportsAltText(const Node& node) {
+ if (!node.IsHTMLElement())
+ return false;
+ const HTMLElement& element = ToHTMLElement(node);
+
+ // FIXME: Add isSVGImageElement.
+ if (IsHTMLImageElement(element))
+ return true;
+ if (IsHTMLInputElement(element) &&
+ ToHTMLInputElement(node).type() == InputTypeNames::image)
+ return true;
+ return false;
+}
+
+template <typename Strategy>
+void TextIteratorAlgorithm<Strategy>::HandleReplacedElement() {
+ needs_handle_replaced_element_ = false;
+
+ if (fully_clipped_stack_.Top())
+ return;
+
+ LayoutObject* layout_object = node_->GetLayoutObject();
+ if (layout_object->Style()->Visibility() != EVisibility::kVisible &&
+ !IgnoresStyleVisibility()) {
+ return;
+ }
+
+ if (EmitsObjectReplacementCharacter()) {
+ SpliceBuffer(kObjectReplacementCharacter, Strategy::Parent(*node_), node_,
+ 0, 1);
+ return;
+ }
+
+ DCHECK_EQ(last_text_node_, text_node_handler_.GetNode());
+ if (last_text_node_) {
+ if (text_node_handler_.FixLeadingWhiteSpaceForReplacedElement(
+ Strategy::Parent(*last_text_node_))) {
+ needs_handle_replaced_element_ = true;
+ return;
+ }
+ }
+
+ if (EntersTextControls() && layout_object->IsTextControl()) {
+ // The shadow tree should be already visited.
+ return;
+ }
+
+ if (EmitsCharactersBetweenAllVisiblePositions()) {
+ // We want replaced elements to behave like punctuation for boundary
+ // finding, and to simply take up space for the selection preservation
+ // code in moveParagraphs, so we use a comma.
+ SpliceBuffer(',', Strategy::Parent(*node_), node_, 0, 1);
+ return;
+ }
+
+ text_state_.UpdateForReplacedElement(node_);
+
+ if (EmitsImageAltText() && TextIterator::SupportsAltText(*node_)) {
+ text_state_.EmitAltText(node_);
+ if (text_state_.length())
+ return;
+ }
+}
+
+template <typename Strategy>
+bool TextIteratorAlgorithm<Strategy>::ShouldEmitTabBeforeNode(
+ const Node& node) {
+ LayoutObject* r = node.GetLayoutObject();
+
+ // Table cells are delimited by tabs.
+ if (!r || !IsTableCell(&node))
+ return false;
+
+ // Want a tab before every cell other than the first one
+ LayoutTableCell* rc = ToLayoutTableCell(r);
+ LayoutTable* t = rc->Table();
+ return t && (t->CellPreceding(*rc) || t->CellAbove(*rc));
+}
+
+template <typename Strategy>
+bool TextIteratorAlgorithm<Strategy>::ShouldEmitNewlineForNode(
+ const Node& node,
+ bool emits_original_text) {
+ LayoutObject* layout_object = node.GetLayoutObject();
+
+ if (layout_object ? !layout_object->IsBR() : !IsHTMLBRElement(node))
+ return false;
+ return emits_original_text || !(node.IsInShadowTree() &&
+ IsHTMLInputElement(*node.OwnerShadowHost()));
+}
+
+static bool ShouldEmitNewlinesBeforeAndAfterNode(const Node& node) {
+ // Block flow (versus inline flow) is represented by having
+ // a newline both before and after the element.
+ LayoutObject* r = node.GetLayoutObject();
+ if (!r) {
+ return (node.HasTagName(blockquoteTag) || node.HasTagName(ddTag) ||
+ node.HasTagName(divTag) || node.HasTagName(dlTag) ||
+ node.HasTagName(dtTag) || node.HasTagName(h1Tag) ||
+ node.HasTagName(h2Tag) || node.HasTagName(h3Tag) ||
+ node.HasTagName(h4Tag) || node.HasTagName(h5Tag) ||
+ node.HasTagName(h6Tag) || node.HasTagName(hrTag) ||
+ node.HasTagName(liTag) || node.HasTagName(listingTag) ||
+ node.HasTagName(olTag) || node.HasTagName(pTag) ||
+ node.HasTagName(preTag) || node.HasTagName(trTag) ||
+ node.HasTagName(ulTag));
+ }
+
+ // Need to make an exception for option and optgroup, because we want to
+ // keep the legacy behavior before we added layoutObjects to them.
+ if (IsHTMLOptionElement(node) || IsHTMLOptGroupElement(node))
+ return false;
+
+ // Need to make an exception for table cells, because they are blocks, but we
+ // want them tab-delimited rather than having newlines before and after.
+ if (IsTableCell(&node))
+ return false;
+
+ // Need to make an exception for table row elements, because they are neither
+ // "inline" or "LayoutBlock", but we want newlines for them.
+ if (r->IsTableRow()) {
+ LayoutTable* t = ToLayoutTableRow(r)->Table();
+ if (t && !t->IsInline())
+ return true;
+ }
+
+ return !r->IsInline() && r->IsLayoutBlock() &&
+ !r->IsFloatingOrOutOfFlowPositioned() && !r->IsBody() &&
+ !r->IsRubyText();
+}
+
+template <typename Strategy>
+bool TextIteratorAlgorithm<Strategy>::ShouldEmitNewlineAfterNode(
+ const Node& node) {
+ // FIXME: It should be better but slower to create a VisiblePosition here.
+ if (!ShouldEmitNewlinesBeforeAndAfterNode(node))
+ return false;
+ // Check if this is the very last layoutObject in the document.
+ // If so, then we should not emit a newline.
+ const Node* next = &node;
+ do {
+ next = Strategy::NextSkippingChildren(*next);
+ if (next && next->GetLayoutObject())
+ return true;
+ } while (next);
+ return false;
+}
+
+template <typename Strategy>
+bool TextIteratorAlgorithm<Strategy>::ShouldEmitNewlineBeforeNode(
+ const Node& node) {
+ return ShouldEmitNewlinesBeforeAndAfterNode(node);
+}
+
+static bool ShouldEmitExtraNewlineForNode(const Node* node) {
+ // https://html.spec.whatwg.org/multipage/dom.html#the-innertext-idl-attribute
+ // Append two required linebreaks after a P element.
+ LayoutObject* r = node->GetLayoutObject();
+ if (!r || !r->IsBox())
+ return false;
+
+ return node->HasTagName(pTag);
+}
+
+// Whether or not we should emit a character as we enter m_node (if it's a
+// container) or as we hit it (if it's atomic).
+template <typename Strategy>
+bool TextIteratorAlgorithm<Strategy>::ShouldRepresentNodeOffsetZero() {
+ if (EmitsCharactersBetweenAllVisiblePositions() && IsRenderedAsTable(node_))
+ return true;
+
+ // Leave element positioned flush with start of a paragraph
+ // (e.g. do not insert tab before a table cell at the start of a paragraph)
+ if (text_state_.LastCharacter() == '\n')
+ return false;
+
+ // Otherwise, show the position if we have emitted any characters
+ if (text_state_.HasEmitted())
+ return true;
+
+ // We've not emitted anything yet. Generally, there is no need for any
+ // positioning then. The only exception is when the element is visually not in
+ // the same line as the start of the range (e.g. the range starts at the end
+ // of the previous paragraph).
+ // NOTE: Creating VisiblePositions and comparing them is relatively expensive,
+ // so we make quicker checks to possibly avoid that. Another check that we
+ // could make is is whether the inline vs block flow changed since the
+ // previous visible element. I think we're already in a special enough case
+ // that that won't be needed, tho.
+
+ // No character needed if this is the first node in the range.
+ if (node_ == start_container_)
+ return false;
+
+ // If we are outside the start container's subtree, assume we need to emit.
+ // FIXME: m_startContainer could be an inline block
+ if (!Strategy::IsDescendantOf(*node_, *start_container_))
+ return true;
+
+ // If we started as m_startContainer offset 0 and the current node is a
+ // descendant of the start container, we already had enough context to
+ // correctly decide whether to emit after a preceding block. We chose not to
+ // emit (m_hasEmitted is false), so don't second guess that now.
+ // NOTE: Is this really correct when m_node is not a leftmost descendant?
+ // Probably immaterial since we likely would have already emitted something by
+ // now.
+ if (!start_offset_)
+ return false;
+
+ // If this node is unrendered or invisible the VisiblePosition checks below
+ // won't have much meaning.
+ // Additionally, if the range we are iterating over contains huge sections of
+ // unrendered content, we would create VisiblePositions on every call to this
+ // function without this check.
+ if (!node_->GetLayoutObject() ||
+ node_->GetLayoutObject()->Style()->Visibility() !=
+ EVisibility::kVisible ||
+ (node_->GetLayoutObject()->IsLayoutBlockFlow() &&
+ !ToLayoutBlock(node_->GetLayoutObject())->Size().Height() &&
+ !IsHTMLBodyElement(*node_)))
+ return false;
+
+ // The startPos.isNotNull() check is needed because the start could be before
+ // the body, and in that case we'll get null. We don't want to put in newlines
+ // at the start in that case.
+ // The currPos.isNotNull() check is needed because positions in non-HTML
+ // content (like SVG) do not have visible positions, and we don't want to emit
+ // for them either.
+ VisiblePosition start_pos =
+ CreateVisiblePosition(Position(start_container_, start_offset_));
+ VisiblePosition curr_pos = VisiblePosition::BeforeNode(*node_);
+ return start_pos.IsNotNull() && curr_pos.IsNotNull() &&
+ !InSameLine(start_pos, curr_pos);
+}
+
+template <typename Strategy>
+bool TextIteratorAlgorithm<Strategy>::ShouldEmitSpaceBeforeAndAfterNode(
+ const Node& node) {
+ return IsRenderedAsTable(&node) &&
+ (node.GetLayoutObject()->IsInline() ||
+ EmitsCharactersBetweenAllVisiblePositions());
+}
+
+template <typename Strategy>
+void TextIteratorAlgorithm<Strategy>::RepresentNodeOffsetZero() {
+ // Emit a character to show the positioning of m_node.
+
+ // When we haven't been emitting any characters,
+ // shouldRepresentNodeOffsetZero() can create VisiblePositions, which is
+ // expensive. So, we perform the inexpensive checks on m_node to see if it
+ // necessitates emitting a character first and will early return before
+ // encountering shouldRepresentNodeOffsetZero()s worse case behavior.
+ if (ShouldEmitTabBeforeNode(*node_)) {
+ if (ShouldRepresentNodeOffsetZero())
+ SpliceBuffer('\t', Strategy::Parent(*node_), node_, 0, 0);
+ } else if (ShouldEmitNewlineBeforeNode(*node_)) {
+ if (ShouldRepresentNodeOffsetZero())
+ SpliceBuffer('\n', Strategy::Parent(*node_), node_, 0, 0);
+ } else if (ShouldEmitSpaceBeforeAndAfterNode(*node_)) {
+ if (ShouldRepresentNodeOffsetZero())
+ SpliceBuffer(kSpaceCharacter, Strategy::Parent(*node_), node_, 0, 0);
+ }
+}
+
+template <typename Strategy>
+void TextIteratorAlgorithm<Strategy>::HandleNonTextNode() {
+ if (ShouldEmitNewlineForNode(*node_, EmitsOriginalText()))
+ SpliceBuffer('\n', Strategy::Parent(*node_), node_, 0, 1);
+ else if (EmitsCharactersBetweenAllVisiblePositions() &&
+ node_->GetLayoutObject() && node_->GetLayoutObject()->IsHR())
+ SpliceBuffer(kSpaceCharacter, Strategy::Parent(*node_), node_, 0, 1);
+ else
+ RepresentNodeOffsetZero();
+}
+
+template <typename Strategy>
+void TextIteratorAlgorithm<Strategy>::ExitNode() {
+ // prevent emitting a newline when exiting a collapsed block at beginning of
+ // the range
+ // FIXME: !m_hasEmitted does not necessarily mean there was a collapsed
+ // block... it could have been an hr (e.g.). Also, a collapsed block could
+ // have height (e.g. a table) and therefore look like a blank line.
+ if (!text_state_.HasEmitted())
+ return;
+
+ // Emit with a position *inside* m_node, after m_node's contents, in
+ // case it is a block, because the run should start where the
+ // emitted character is positioned visually.
+ Node* last_child = Strategy::LastChild(*node_);
+ const Node* base_node = last_child ? last_child : node_.Get();
+ // FIXME: This shouldn't require the m_lastTextNode to be true, but we can't
+ // change that without making the logic in _web_attributedStringFromRange
+ // match. We'll get that for free when we switch to use TextIterator in
+ // _web_attributedStringFromRange. See <rdar://problem/5428427> for an example
+ // of how this mismatch will cause problems.
+ if (last_text_node_ && ShouldEmitNewlineAfterNode(*node_)) {
+ // use extra newline to represent margin bottom, as needed
+ const bool add_newline = !behavior_.SuppressesExtraNewlineEmission() &&
+ ShouldEmitExtraNewlineForNode(node_);
+
+ // FIXME: We need to emit a '\n' as we leave an empty block(s) that
+ // contain a VisiblePosition when doing selection preservation.
+ if (text_state_.LastCharacter() != '\n') {
+ // insert a newline with a position following this block's contents.
+ SpliceBuffer(kNewlineCharacter, Strategy::Parent(*base_node), base_node,
+ 1, 1);
+ // remember whether to later add a newline for the current node
+ DCHECK(!needs_another_newline_);
+ needs_another_newline_ = add_newline;
+ } else if (add_newline) {
+ // insert a newline with a position following this block's contents.
+ SpliceBuffer(kNewlineCharacter, Strategy::Parent(*base_node), base_node,
+ 1, 1);
+ }
+ }
+
+ // If nothing was emitted, see if we need to emit a space.
+ if (!text_state_.PositionNode() && ShouldEmitSpaceBeforeAndAfterNode(*node_))
+ SpliceBuffer(kSpaceCharacter, Strategy::Parent(*base_node), base_node, 1,
+ 1);
+}
+
+template <typename Strategy>
+void TextIteratorAlgorithm<Strategy>::SpliceBuffer(UChar c,
+ const Node* text_node,
+ const Node* offset_base_node,
+ unsigned text_start_offset,
+ unsigned text_end_offset) {
+ text_state_.SpliceBuffer(c, text_node, offset_base_node, text_start_offset,
+ text_end_offset);
+ text_node_handler_.ResetCollapsedWhiteSpaceFixup();
+}
+
+template <typename Strategy>
+EphemeralRangeTemplate<Strategy> TextIteratorAlgorithm<Strategy>::Range()
+ const {
+ // use the current run information, if we have it
+ if (text_state_.PositionNode()) {
+ return EphemeralRangeTemplate<Strategy>(StartPositionInCurrentContainer(),
+ EndPositionInCurrentContainer());
+ }
+
+ // otherwise, return the end of the overall range we were given
+ if (end_container_)
+ return EphemeralRangeTemplate<Strategy>(
+ PositionTemplate<Strategy>(end_container_, end_offset_));
+
+ return EphemeralRangeTemplate<Strategy>();
+}
+
+template <typename Strategy>
+Document* TextIteratorAlgorithm<Strategy>::OwnerDocument() const {
+ if (text_state_.PositionNode())
+ return &text_state_.PositionNode()->GetDocument();
+ if (end_container_)
+ return &end_container_->GetDocument();
+ return nullptr;
+}
+
+template <typename Strategy>
+const Node* TextIteratorAlgorithm<Strategy>::GetNode() const {
+ if (text_state_.PositionNode() || end_container_) {
+ const Node* node = CurrentContainer();
+ if (node->IsCharacterDataNode())
+ return node;
+ return Strategy::ChildAt(*node, StartOffsetInCurrentContainer());
+ }
+ return nullptr;
+}
+
+template <typename Strategy>
+int TextIteratorAlgorithm<Strategy>::StartOffsetInCurrentContainer() const {
+ if (text_state_.PositionNode()) {
+ text_state_.FlushPositionOffsets();
+ return text_state_.PositionStartOffset();
+ }
+ DCHECK(end_container_);
+ return end_offset_;
+}
+
+template <typename Strategy>
+int TextIteratorAlgorithm<Strategy>::EndOffsetInCurrentContainer() const {
+ if (text_state_.PositionNode()) {
+ text_state_.FlushPositionOffsets();
+ return text_state_.PositionEndOffset();
+ }
+ DCHECK(end_container_);
+ return end_offset_;
+}
+
+template <typename Strategy>
+const Node* TextIteratorAlgorithm<Strategy>::CurrentContainer() const {
+ if (text_state_.PositionNode()) {
+ return text_state_.PositionNode();
+ }
+ DCHECK(end_container_);
+ return end_container_;
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy>
+TextIteratorAlgorithm<Strategy>::StartPositionInCurrentContainer() const {
+ return PositionTemplate<Strategy>::EditingPositionOf(
+ CurrentContainer(), StartOffsetInCurrentContainer());
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy>
+TextIteratorAlgorithm<Strategy>::EndPositionInCurrentContainer() const {
+ return PositionTemplate<Strategy>::EditingPositionOf(
+ CurrentContainer(), EndOffsetInCurrentContainer());
+}
+
+template <typename Strategy>
+int TextIteratorAlgorithm<Strategy>::RangeLength(
+ const PositionTemplate<Strategy>& start,
+ const PositionTemplate<Strategy>& end,
+ const TextIteratorBehavior& behavior) {
+ DCHECK(start.GetDocument());
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ start.GetDocument()->Lifecycle());
+
+ int length = 0;
+ for (TextIteratorAlgorithm<Strategy> it(start, end, behavior); !it.AtEnd();
+ it.Advance())
+ length += it.length();
+
+ return length;
+}
+
+template <typename Strategy>
+int TextIteratorAlgorithm<Strategy>::RangeLength(
+ const EphemeralRangeTemplate<Strategy>& range,
+ const TextIteratorBehavior& behavior) {
+ return RangeLength(range.StartPosition(), range.EndPosition(), behavior);
+}
+
+template <typename Strategy>
+bool TextIteratorAlgorithm<Strategy>::IsBetweenSurrogatePair(
+ unsigned position) const {
+ return position > 0 && position < static_cast<unsigned>(length()) &&
+ U16_IS_LEAD(CharacterAt(position - 1)) &&
+ U16_IS_TRAIL(CharacterAt(position));
+}
+
+template <typename Strategy>
+int TextIteratorAlgorithm<Strategy>::CopyTextTo(ForwardsTextBuffer* output,
+ int position,
+ int min_length) const {
+ unsigned end = std::min(length(), position + min_length);
+ if (IsBetweenSurrogatePair(end))
+ ++end;
+ unsigned copied_length = end - position;
+ CopyCodeUnitsTo(output, position, copied_length);
+ return copied_length;
+}
+
+template <typename Strategy>
+int TextIteratorAlgorithm<Strategy>::CopyTextTo(ForwardsTextBuffer* output,
+ int position) const {
+ return CopyTextTo(output, position, length() - position);
+}
+
+template <typename Strategy>
+void TextIteratorAlgorithm<Strategy>::CopyCodeUnitsTo(
+ ForwardsTextBuffer* output,
+ unsigned position,
+ unsigned copy_length) const {
+ text_state_.AppendTextTo(output, position, copy_length);
+}
+
+// --------
+
+template <typename Strategy>
+static String CreatePlainText(const EphemeralRangeTemplate<Strategy>& range,
+ const TextIteratorBehavior& behavior) {
+ if (range.IsNull())
+ return g_empty_string;
+
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ range.StartPosition().GetDocument()->Lifecycle());
+
+ TextIteratorAlgorithm<Strategy> it(range.StartPosition(), range.EndPosition(),
+ behavior);
+
+ if (it.AtEnd())
+ return g_empty_string;
+
+ // The initial buffer size can be critical for performance:
+ // https://bugs.webkit.org/show_bug.cgi?id=81192
+ static const unsigned kInitialCapacity = 1 << 15;
+
+ StringBuilder builder;
+ builder.ReserveCapacity(kInitialCapacity);
+
+ for (; !it.AtEnd(); it.Advance())
+ it.GetText().AppendTextToStringBuilder(builder);
+
+ if (builder.IsEmpty())
+ return g_empty_string;
+
+ return builder.ToString();
+}
+
+String PlainText(const EphemeralRange& range,
+ const TextIteratorBehavior& behavior) {
+ return CreatePlainText<EditingStrategy>(range, behavior);
+}
+
+String PlainText(const EphemeralRangeInFlatTree& range,
+ const TextIteratorBehavior& behavior) {
+ return CreatePlainText<EditingInFlatTreeStrategy>(range, behavior);
+}
+
+template class CORE_TEMPLATE_EXPORT TextIteratorAlgorithm<EditingStrategy>;
+template class CORE_TEMPLATE_EXPORT
+ TextIteratorAlgorithm<EditingInFlatTreeStrategy>;
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator.h b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator.h
new file mode 100644
index 00000000000..fb296dc57ff
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator.h
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2004, 2006, 2009 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_ITERATOR_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_ITERATOR_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/editing/finder/find_options.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/iterators/fully_clipped_state_stack.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator_behavior.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator_text_node_handler.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator_text_state.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+CORE_EXPORT String
+PlainText(const EphemeralRange&,
+ const TextIteratorBehavior& = TextIteratorBehavior());
+
+String PlainText(const EphemeralRangeInFlatTree&,
+ const TextIteratorBehavior& = TextIteratorBehavior());
+
+// Iterates through the DOM range, returning all the text, and 0-length
+// boundaries at points where replaced elements break up the text flow. The
+// text comes back in chunks so as to optimize for performance of the iteration.
+
+template <typename Strategy>
+class CORE_TEMPLATE_CLASS_EXPORT TextIteratorAlgorithm {
+ STACK_ALLOCATED();
+
+ public:
+ // [start, end] indicates the document range that the iteration should take
+ // place within (both ends inclusive).
+ TextIteratorAlgorithm(const PositionTemplate<Strategy>& start,
+ const PositionTemplate<Strategy>& end,
+ const TextIteratorBehavior& = TextIteratorBehavior());
+
+ // Same behavior as previous constructor but takes an EphemeralRange instead
+ // of two Positions
+ TextIteratorAlgorithm(const EphemeralRangeTemplate<Strategy>&,
+ const TextIteratorBehavior& = TextIteratorBehavior());
+
+ ~TextIteratorAlgorithm();
+
+ bool AtEnd() const { return !text_state_.PositionNode() || should_stop_; }
+ void Advance();
+ bool IsInsideAtomicInlineElement() const;
+
+ EphemeralRangeTemplate<Strategy> Range() const;
+ const Node* GetNode() const;
+
+ Document* OwnerDocument() const;
+ const Node* CurrentContainer() const;
+ int StartOffsetInCurrentContainer() const;
+ int EndOffsetInCurrentContainer() const;
+ PositionTemplate<Strategy> StartPositionInCurrentContainer() const;
+ PositionTemplate<Strategy> EndPositionInCurrentContainer() const;
+
+ const TextIteratorTextState& GetText() const { return text_state_; }
+ int length() const { return text_state_.length(); }
+ UChar CharacterAt(unsigned index) const {
+ return text_state_.CharacterAt(index);
+ }
+
+ bool BreaksAtReplacedElement() {
+ return !behavior_.DoesNotBreakAtReplacedElement();
+ }
+
+ // Calculate the minimum |actualLength >= minLength| such that code units
+ // with offset range [position, position + actualLength) are whole code
+ // points. Append these code points to |output| and return |actualLength|.
+ int CopyTextTo(ForwardsTextBuffer* output,
+ int position,
+ int min_length) const;
+ int CopyTextTo(ForwardsTextBuffer* output, int position = 0) const;
+
+ // Computes the length of the given range using a text iterator according to
+ // the specified iteration behavior. The default iteration behavior is to
+ // always emit object replacement characters for replaced elements.
+ // TODO(editing-dev): We should remove start/end version of |RangeLength()|.
+ static int RangeLength(
+ const PositionTemplate<Strategy>& start,
+ const PositionTemplate<Strategy>& end,
+ const TextIteratorBehavior& =
+ TextIteratorBehavior::DefaultRangeLengthBehavior());
+
+ static int RangeLength(
+ const EphemeralRangeTemplate<Strategy>&,
+ const TextIteratorBehavior& =
+ TextIteratorBehavior::DefaultRangeLengthBehavior());
+
+ static bool ShouldEmitTabBeforeNode(const Node&);
+ static bool ShouldEmitNewlineBeforeNode(const Node&);
+ static bool ShouldEmitNewlineAfterNode(const Node&);
+ static bool ShouldEmitNewlineForNode(const Node&, bool emits_original_text);
+
+ static bool SupportsAltText(const Node&);
+
+ private:
+ enum IterationProgress {
+ kHandledNone,
+ kHandledOpenShadowRoots,
+ kHandledUserAgentShadowRoot,
+ kHandledNode,
+ kHandledChildren
+ };
+
+ void ExitNode();
+ bool ShouldRepresentNodeOffsetZero();
+ bool ShouldEmitSpaceBeforeAndAfterNode(const Node&);
+ void RepresentNodeOffsetZero();
+
+ // Returns true if text is emitted from the remembered progress (if any).
+ bool HandleRememberedProgress();
+
+ void HandleTextNode();
+ void HandleReplacedElement();
+ void HandleNonTextNode();
+ void SpliceBuffer(UChar,
+ const Node* text_node,
+ const Node* offset_base_node,
+ unsigned text_start_offset,
+ unsigned text_end_offset);
+
+ // Used by selection preservation code. There should be one character emitted
+ // between every VisiblePosition in the Range used to create the TextIterator.
+ // FIXME <rdar://problem/6028818>: This functionality should eventually be
+ // phased out when we rewrite moveParagraphs to not clone/destroy moved
+ // content.
+ bool EmitsCharactersBetweenAllVisiblePositions() const {
+ return behavior_.EmitsCharactersBetweenAllVisiblePositions();
+ }
+
+ bool EntersTextControls() const { return behavior_.EntersTextControls(); }
+
+ // Used in pasting inside password field.
+ bool EmitsOriginalText() const { return behavior_.EmitsOriginalText(); }
+
+ // Used when the visibility of the style should not affect text gathering.
+ bool IgnoresStyleVisibility() const {
+ return behavior_.IgnoresStyleVisibility();
+ }
+
+ // Used when the iteration should stop if form controls are reached.
+ bool StopsOnFormControls() const { return behavior_.StopsOnFormControls(); }
+
+ bool EmitsImageAltText() const { return behavior_.EmitsImageAltText(); }
+
+ bool EntersOpenShadowRoots() const {
+ return behavior_.EntersOpenShadowRoots();
+ }
+
+ bool EmitsObjectReplacementCharacter() const {
+ return behavior_.EmitsObjectReplacementCharacter();
+ }
+
+ bool ExcludesAutofilledValue() const {
+ return behavior_.ExcludeAutofilledValue();
+ }
+
+ bool DoesNotBreakAtReplacedElement() const {
+ return behavior_.DoesNotBreakAtReplacedElement();
+ }
+
+ // Clipboard should respect user-select style attribute
+ bool SkipsUnselectableContent() const {
+ return behavior_.SkipsUnselectableContent();
+ }
+
+ bool ForInnerText() const { return behavior_.ForInnerText(); }
+
+ bool IsBetweenSurrogatePair(unsigned position) const;
+
+ // Append code units with offset range [position, position + copyLength)
+ // to the output buffer.
+ void CopyCodeUnitsTo(ForwardsTextBuffer* output,
+ unsigned position,
+ unsigned copy_length) const;
+
+ // The range.
+ const Member<const Node> start_container_;
+ const unsigned start_offset_;
+ const Member<const Node> end_container_;
+ const unsigned end_offset_;
+ // |m_endNode| stores |Strategy::childAt(*m_endContainer, m_endOffset - 1)|,
+ // if it exists, or |nullptr| otherwise.
+ const Member<const Node> end_node_;
+ const Member<const Node> past_end_node_;
+
+ // Current position, not necessarily of the text being returned, but position
+ // as we walk through the DOM tree.
+ Member<const Node> node_;
+ IterationProgress iteration_progress_;
+ FullyClippedStateStackAlgorithm<Strategy> fully_clipped_stack_;
+ unsigned shadow_depth_;
+
+ // Used when there is still some pending text from the current node; when
+ // these are false, we go back to normal iterating.
+ bool needs_another_newline_ = false;
+ bool needs_handle_replaced_element_ = false;
+
+ Member<const Text> last_text_node_;
+
+ const TextIteratorBehavior behavior_;
+
+ // Used when stopsOnFormControls() is true to determine if the iterator should
+ // keep advancing.
+ bool should_stop_ = false;
+ // Used for use counter |InnerTextWithShadowTree| and
+ // |SelectionToStringWithShadowTree|, we should not use other purpose.
+ bool handle_shadow_root_ = false;
+
+ // Contains state of emitted text.
+ TextIteratorTextState text_state_;
+
+ // Helper for extracting text content from text nodes.
+ TextIteratorTextNodeHandler text_node_handler_;
+};
+
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ TextIteratorAlgorithm<EditingStrategy>;
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ TextIteratorAlgorithm<EditingInFlatTreeStrategy>;
+
+using TextIterator = TextIteratorAlgorithm<EditingStrategy>;
+using TextIteratorInFlatTree = TextIteratorAlgorithm<EditingInFlatTreeStrategy>;
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_ITERATOR_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_behavior.cc b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_behavior.cc
new file mode 100644
index 00000000000..d8291acea5a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_behavior.cc
@@ -0,0 +1,191 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator_behavior.h"
+
+namespace blink {
+
+TextIteratorBehavior::Builder::Builder(const TextIteratorBehavior& behavior)
+ : behavior_(behavior) {}
+
+TextIteratorBehavior::Builder::Builder() = default;
+TextIteratorBehavior::Builder::~Builder() = default;
+
+TextIteratorBehavior TextIteratorBehavior::Builder::Build() {
+ return behavior_;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetCollapseTrailingSpace(bool value) {
+ behavior_.values_.bits.collapse_trailing_space = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetDoesNotBreakAtReplacedElement(bool value) {
+ behavior_.values_.bits.does_not_break_at_replaced_element = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetEmitsCharactersBetweenAllVisiblePositions(
+ bool value) {
+ behavior_.values_.bits.emits_characters_between_all_visible_positions = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetEmitsImageAltText(bool value) {
+ behavior_.values_.bits.emits_image_alt_text = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetEmitsSpaceForNbsp(bool value) {
+ behavior_.values_.bits.emits_space_for_nbsp = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetEmitsObjectReplacementCharacter(bool value) {
+ behavior_.values_.bits.emits_object_replacement_character = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetEmitsOriginalText(bool value) {
+ behavior_.values_.bits.emits_original_text = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetEmitsSmallXForTextSecurity(bool value) {
+ behavior_.values_.bits.emits_small_x_for_text_security = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetEntersOpenShadowRoots(bool value) {
+ behavior_.values_.bits.enters_open_shadow_roots = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetEntersTextControls(bool value) {
+ behavior_.values_.bits.enters_text_controls = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetExcludeAutofilledValue(bool value) {
+ behavior_.values_.bits.exclude_autofilled_value = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder& TextIteratorBehavior::Builder::SetForInnerText(
+ bool value) {
+ behavior_.values_.bits.for_inner_text = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetForSelectionToString(bool value) {
+ behavior_.values_.bits.for_selection_to_string = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder& TextIteratorBehavior::Builder::SetForWindowFind(
+ bool value) {
+ behavior_.values_.bits.for_window_find = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetIgnoresStyleVisibility(bool value) {
+ behavior_.values_.bits.ignores_style_visibility = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetStopsOnFormControls(bool value) {
+ behavior_.values_.bits.stops_on_form_controls = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetDoesNotEmitSpaceBeyondRangeEnd(bool value) {
+ behavior_.values_.bits.does_not_emit_space_beyond_range_end = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetSkipsUnselectableContent(bool value) {
+ behavior_.values_.bits.skips_unselectable_content = value;
+ return *this;
+}
+
+TextIteratorBehavior::Builder&
+TextIteratorBehavior::Builder::SetSuppressesExtraNewlineEmission(bool value) {
+ behavior_.values_.bits.suppresses_newline_emission = value;
+ return *this;
+}
+// -
+TextIteratorBehavior::TextIteratorBehavior(const TextIteratorBehavior& other) =
+ default;
+
+TextIteratorBehavior::TextIteratorBehavior() {
+ values_.all = 0;
+}
+
+TextIteratorBehavior::~TextIteratorBehavior() = default;
+
+bool TextIteratorBehavior::operator==(const TextIteratorBehavior& other) const {
+ return values_.all == other.values_.all;
+}
+
+bool TextIteratorBehavior::operator!=(const TextIteratorBehavior& other) const {
+ return !operator==(other);
+}
+
+// static
+TextIteratorBehavior
+TextIteratorBehavior::EmitsObjectReplacementCharacterBehavior() {
+ return TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(true)
+ .Build();
+}
+
+// static
+TextIteratorBehavior TextIteratorBehavior::IgnoresStyleVisibilityBehavior() {
+ return TextIteratorBehavior::Builder()
+ .SetIgnoresStyleVisibility(true)
+ .Build();
+}
+
+// static
+TextIteratorBehavior TextIteratorBehavior::DefaultRangeLengthBehavior() {
+ return TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(true)
+ .Build();
+}
+
+// static
+TextIteratorBehavior
+TextIteratorBehavior::AllVisiblePositionsRangeLengthBehavior() {
+ return TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(true)
+ .SetEmitsCharactersBetweenAllVisiblePositions(true)
+ .Build();
+}
+
+// static
+TextIteratorBehavior
+TextIteratorBehavior::NoTrailingSpaceRangeLengthBehavior() {
+ return TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(true)
+ .SetDoesNotEmitSpaceBeyondRangeEnd(true)
+ .Build();
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_behavior.h b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_behavior.h
new file mode 100644
index 00000000000..14bafd61ba2
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_behavior.h
@@ -0,0 +1,140 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_ITERATOR_BEHAVIOR_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_ITERATOR_BEHAVIOR_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+
+namespace blink {
+
+class CORE_EXPORT TextIteratorBehavior final {
+ public:
+ class CORE_EXPORT Builder;
+
+ TextIteratorBehavior(const TextIteratorBehavior& other);
+ TextIteratorBehavior();
+ ~TextIteratorBehavior();
+
+ bool operator==(const TextIteratorBehavior& other) const;
+ bool operator!=(const TextIteratorBehavior& other) const;
+
+ bool CollapseTrailingSpace() const {
+ return values_.bits.collapse_trailing_space;
+ }
+ bool DoesNotBreakAtReplacedElement() const {
+ return values_.bits.does_not_break_at_replaced_element;
+ }
+ bool EmitsCharactersBetweenAllVisiblePositions() const {
+ return values_.bits.emits_characters_between_all_visible_positions;
+ }
+ bool EmitsImageAltText() const { return values_.bits.emits_image_alt_text; }
+ bool EmitsSpaceForNbsp() const { return values_.bits.emits_space_for_nbsp; }
+ bool EmitsObjectReplacementCharacter() const {
+ return values_.bits.emits_object_replacement_character;
+ }
+ bool EmitsOriginalText() const { return values_.bits.emits_original_text; }
+ bool EntersOpenShadowRoots() const {
+ return values_.bits.enters_open_shadow_roots;
+ }
+ bool EmitsSmallXForTextSecurity() const {
+ return values_.bits.emits_small_x_for_text_security;
+ }
+ bool EntersTextControls() const { return values_.bits.enters_text_controls; }
+ bool ExcludeAutofilledValue() const {
+ return values_.bits.exclude_autofilled_value;
+ }
+ bool ForInnerText() const { return values_.bits.for_inner_text; }
+ bool ForSelectionToString() const {
+ return values_.bits.for_selection_to_string;
+ }
+ bool ForWindowFind() const { return values_.bits.for_window_find; }
+ bool IgnoresStyleVisibility() const {
+ return values_.bits.ignores_style_visibility;
+ }
+ bool StopsOnFormControls() const {
+ return values_.bits.stops_on_form_controls;
+ }
+ bool DoesNotEmitSpaceBeyondRangeEnd() const {
+ return values_.bits.does_not_emit_space_beyond_range_end;
+ }
+
+ bool SkipsUnselectableContent() const {
+ return values_.bits.skips_unselectable_content;
+ }
+
+ bool SuppressesExtraNewlineEmission() const {
+ return values_.bits.suppresses_newline_emission;
+ }
+ static TextIteratorBehavior EmitsObjectReplacementCharacterBehavior();
+ static TextIteratorBehavior IgnoresStyleVisibilityBehavior();
+ static TextIteratorBehavior DefaultRangeLengthBehavior();
+ static TextIteratorBehavior AllVisiblePositionsRangeLengthBehavior();
+ static TextIteratorBehavior NoTrailingSpaceRangeLengthBehavior();
+
+ private:
+ union {
+ unsigned all;
+ struct {
+ bool collapse_trailing_space : 1;
+ bool does_not_break_at_replaced_element : 1;
+ bool emits_characters_between_all_visible_positions : 1;
+ bool emits_image_alt_text : 1;
+ bool emits_space_for_nbsp : 1;
+ bool emits_object_replacement_character : 1;
+ bool emits_original_text : 1;
+ bool emits_small_x_for_text_security : 1;
+ bool enters_open_shadow_roots : 1;
+ bool enters_text_controls : 1;
+ bool exclude_autofilled_value : 1;
+ bool for_inner_text : 1;
+ bool for_selection_to_string : 1;
+ bool for_window_find : 1;
+ bool ignores_style_visibility : 1;
+ bool stops_on_form_controls : 1;
+ bool does_not_emit_space_beyond_range_end : 1;
+ bool skips_unselectable_content : 1;
+ bool suppresses_newline_emission : 1;
+ } bits;
+ } values_;
+};
+
+class CORE_EXPORT TextIteratorBehavior::Builder final {
+ public:
+ explicit Builder(const TextIteratorBehavior&);
+ Builder();
+ ~Builder();
+
+ TextIteratorBehavior Build();
+
+ Builder& SetCollapseTrailingSpace(bool);
+ Builder& SetDoesNotBreakAtReplacedElement(bool);
+ Builder& SetEmitsCharactersBetweenAllVisiblePositions(bool);
+ Builder& SetEmitsImageAltText(bool);
+ Builder& SetEmitsSpaceForNbsp(bool);
+ Builder& SetEmitsObjectReplacementCharacter(bool);
+ Builder& SetEmitsOriginalText(bool);
+ Builder& SetEmitsSmallXForTextSecurity(bool);
+ Builder& SetEntersOpenShadowRoots(bool);
+ Builder& SetEntersTextControls(bool);
+ Builder& SetExcludeAutofilledValue(bool);
+ Builder& SetForInnerText(bool);
+ Builder& SetForSelectionToString(bool);
+ Builder& SetForWindowFind(bool);
+ Builder& SetIgnoresStyleVisibility(bool);
+ Builder& SetStopsOnFormControls(bool);
+ Builder& SetDoesNotEmitSpaceBeyondRangeEnd(bool);
+ Builder& SetSkipsUnselectableContent(bool);
+ Builder& SetSuppressesExtraNewlineEmission(bool);
+
+ private:
+ TextIteratorBehavior behavior_;
+
+ DISALLOW_COPY_AND_ASSIGN(Builder);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_ITERATOR_BEHAVIOR_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_behavior_test.cc b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_behavior_test.cc
new file mode 100644
index 00000000000..256829cd1a1
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_behavior_test.cc
@@ -0,0 +1,107 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator_behavior.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace blink {
+
+TEST(TextIteratorBehaviorTest, Basic) {
+ EXPECT_TRUE(TextIteratorBehavior() == TextIteratorBehavior());
+ EXPECT_FALSE(TextIteratorBehavior() != TextIteratorBehavior());
+ EXPECT_NE(
+ TextIteratorBehavior(),
+ TextIteratorBehavior::Builder().SetCollapseTrailingSpace(true).Build());
+ EXPECT_NE(
+ TextIteratorBehavior::Builder().SetCollapseTrailingSpace(true).Build(),
+ TextIteratorBehavior());
+ EXPECT_NE(
+ TextIteratorBehavior::Builder().SetCollapseTrailingSpace(true).Build(),
+ TextIteratorBehavior::Builder().SetEmitsImageAltText(true).Build());
+ EXPECT_NE(
+ TextIteratorBehavior::Builder().SetEmitsImageAltText(true).Build(),
+ TextIteratorBehavior::Builder().SetCollapseTrailingSpace(true).Build());
+ EXPECT_EQ(TextIteratorBehavior::Builder()
+ .SetCollapseTrailingSpace(true)
+ .SetEmitsImageAltText(true)
+ .Build(),
+ TextIteratorBehavior::Builder()
+ .SetEmitsImageAltText(true)
+ .SetCollapseTrailingSpace(true)
+ .Build());
+}
+
+TEST(TextIteratorBehaviorTest, Values) {
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetCollapseTrailingSpace(true)
+ .Build()
+ .CollapseTrailingSpace());
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetDoesNotBreakAtReplacedElement(true)
+ .Build()
+ .DoesNotBreakAtReplacedElement());
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetEmitsCharactersBetweenAllVisiblePositions(true)
+ .Build()
+ .EmitsCharactersBetweenAllVisiblePositions());
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetEmitsImageAltText(true)
+ .Build()
+ .EmitsImageAltText());
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetEmitsSpaceForNbsp(true)
+ .Build()
+ .EmitsSpaceForNbsp());
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(true)
+ .Build()
+ .EmitsObjectReplacementCharacter());
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetEmitsOriginalText(true)
+ .Build()
+ .EmitsOriginalText());
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetEntersOpenShadowRoots(true)
+ .Build()
+ .EntersOpenShadowRoots());
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetEntersTextControls(true)
+ .Build()
+ .EntersTextControls());
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetExcludeAutofilledValue(true)
+ .Build()
+ .ExcludeAutofilledValue());
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetForInnerText(true)
+ .Build()
+ .ForInnerText());
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetForSelectionToString(true)
+ .Build()
+ .ForSelectionToString());
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetForWindowFind(true)
+ .Build()
+ .ForWindowFind());
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetIgnoresStyleVisibility(true)
+ .Build()
+ .IgnoresStyleVisibility());
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetStopsOnFormControls(true)
+ .Build()
+ .StopsOnFormControls());
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetDoesNotEmitSpaceBeyondRangeEnd(true)
+ .Build()
+ .DoesNotEmitSpaceBeyondRangeEnd());
+ EXPECT_TRUE(TextIteratorBehavior::Builder()
+ .SetSuppressesExtraNewlineEmission(true)
+ .Build()
+ .SuppressesExtraNewlineEmission());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_test.cc b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_test.cc
new file mode 100644
index 00000000000..9765d6643e6
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_test.cc
@@ -0,0 +1,1091 @@
+/*
+ * Copyright (c) 2013, Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
+
+namespace blink {
+namespace text_iterator_test {
+
+TextIteratorBehavior CollapseTrailingSpaceBehavior() {
+ return TextIteratorBehavior::Builder().SetCollapseTrailingSpace(true).Build();
+}
+
+TextIteratorBehavior EmitsImageAltTextBehavior() {
+ return TextIteratorBehavior::Builder().SetEmitsImageAltText(true).Build();
+}
+
+TextIteratorBehavior EntersTextControlsBehavior() {
+ return TextIteratorBehavior::Builder().SetEntersTextControls(true).Build();
+}
+
+TextIteratorBehavior EntersOpenShadowRootsBehavior() {
+ return TextIteratorBehavior::Builder().SetEntersOpenShadowRoots(true).Build();
+}
+
+TextIteratorBehavior EmitsObjectReplacementCharacterBehavior() {
+ return TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(true)
+ .Build();
+}
+
+TextIteratorBehavior EmitsSmallXForTextSecurityBehavior() {
+ return TextIteratorBehavior::Builder()
+ .SetEmitsSmallXForTextSecurity(true)
+ .Build();
+}
+
+struct DOMTree : NodeTraversal {
+ using PositionType = Position;
+ using TextIteratorType = TextIterator;
+};
+
+struct FlatTree : FlatTreeTraversal {
+ using PositionType = PositionInFlatTree;
+ using TextIteratorType = TextIteratorInFlatTree;
+};
+
+class TextIteratorTest : public EditingTestBase {
+ protected:
+ template <typename Tree>
+ std::string Iterate(const TextIteratorBehavior& = TextIteratorBehavior());
+
+ template <typename Tree>
+ std::string IteratePartial(
+ const typename Tree::PositionType& start,
+ const typename Tree::PositionType& end,
+ const TextIteratorBehavior& = TextIteratorBehavior());
+
+ Range* GetBodyRange() const;
+
+ private:
+ template <typename Tree>
+ std::string IterateWithIterator(typename Tree::TextIteratorType&);
+};
+
+template <typename Tree>
+std::string TextIteratorTest::Iterate(
+ const TextIteratorBehavior& iterator_behavior) {
+ Element* body = GetDocument().body();
+ using PositionType = typename Tree::PositionType;
+ auto start = PositionType(body, 0);
+ auto end = PositionType(body, Tree::CountChildren(*body));
+ typename Tree::TextIteratorType iterator(start, end, iterator_behavior);
+ return IterateWithIterator<Tree>(iterator);
+}
+
+template <typename Tree>
+std::string TextIteratorTest::IteratePartial(
+ const typename Tree::PositionType& start,
+ const typename Tree::PositionType& end,
+ const TextIteratorBehavior& iterator_behavior) {
+ typename Tree::TextIteratorType iterator(start, end, iterator_behavior);
+ return IterateWithIterator<Tree>(iterator);
+}
+
+template <typename Tree>
+std::string TextIteratorTest::IterateWithIterator(
+ typename Tree::TextIteratorType& iterator) {
+ String text_chunks;
+ for (; !iterator.AtEnd(); iterator.Advance()) {
+ text_chunks.append('[');
+ text_chunks.append(
+ iterator.GetText().Substring(0, iterator.GetText().length()));
+ text_chunks.append(']');
+ }
+ return std::string(text_chunks.Utf8().data());
+}
+
+Range* TextIteratorTest::GetBodyRange() const {
+ Range* range = Range::Create(GetDocument());
+ range->selectNode(GetDocument().body());
+ return range;
+}
+
+class ParameterizedTextIteratorTest : public testing::WithParamInterface<bool>,
+ private ScopedLayoutNGForTest,
+ public TextIteratorTest {
+ public:
+ ParameterizedTextIteratorTest() : ScopedLayoutNGForTest(GetParam()) {}
+
+ protected:
+ bool LayoutNGEnabled() const { return GetParam(); }
+
+ int TestRangeLength(const std::string& selection_text) {
+ return TextIterator::RangeLength(
+ SetSelectionTextToBody(selection_text).ComputeRange());
+ }
+};
+
+INSTANTIATE_TEST_CASE_P(All, ParameterizedTextIteratorTest, testing::Bool());
+
+TEST_F(TextIteratorTest, BitStackOverflow) {
+ const unsigned kBitsInWord = sizeof(unsigned) * 8;
+ BitStack bs;
+
+ for (unsigned i = 0; i < kBitsInWord + 1u; i++)
+ bs.Push(true);
+
+ bs.Pop();
+
+ EXPECT_TRUE(bs.Top());
+}
+
+TEST_F(TextIteratorTest, BasicIteration) {
+ static const char* input = "<p>Hello, \ntext</p><p>iterator.</p>";
+ SetBodyContent(input);
+ EXPECT_EQ("[Hello, ][text][\n][\n][iterator.]", Iterate<DOMTree>());
+ EXPECT_EQ("[Hello, ][text][\n][\n][iterator.]", Iterate<FlatTree>());
+}
+
+TEST_F(TextIteratorTest, EmitsSmallXForTextSecurity) {
+ InsertStyleElement("s {-webkit-text-security:disc;}");
+ SetBodyContent("abc<s>foo</s>baz");
+ // E2 80 A2 is U+2022 BULLET
+ EXPECT_EQ("[abc][xxx][baz]",
+ Iterate<DOMTree>(EmitsSmallXForTextSecurityBehavior()));
+ EXPECT_EQ("[abc][\xE2\x80\xA2\xE2\x80\xA2\xE2\x80\xA2][baz]",
+ Iterate<DOMTree>(TextIteratorBehavior()));
+ EXPECT_EQ("[abc][xxx][baz]",
+ Iterate<FlatTree>(EmitsSmallXForTextSecurityBehavior()));
+ EXPECT_EQ("[abc][\xE2\x80\xA2\xE2\x80\xA2\xE2\x80\xA2][baz]",
+ Iterate<FlatTree>(TextIteratorBehavior()));
+}
+
+TEST_F(TextIteratorTest, IgnoreAltTextInTextControls) {
+ static const char* input = "<p>Hello <input type='text' value='value'>!</p>";
+ SetBodyContent(input);
+ EXPECT_EQ("[Hello ][][!]", Iterate<DOMTree>(EmitsImageAltTextBehavior()));
+ EXPECT_EQ("[Hello ][][\n][value][\n][!]",
+ Iterate<FlatTree>(EmitsImageAltTextBehavior()));
+}
+
+TEST_F(TextIteratorTest, DisplayAltTextInImageControls) {
+ static const char* input = "<p>Hello <input type='image' alt='alt'>!</p>";
+ SetBodyContent(input);
+ EXPECT_EQ("[Hello ][alt][!]", Iterate<DOMTree>(EmitsImageAltTextBehavior()));
+ EXPECT_EQ("[Hello ][alt][!]", Iterate<FlatTree>(EmitsImageAltTextBehavior()));
+}
+
+TEST_F(TextIteratorTest, NotEnteringTextControls) {
+ static const char* input = "<p>Hello <input type='text' value='input'>!</p>";
+ SetBodyContent(input);
+ EXPECT_EQ("[Hello ][][!]", Iterate<DOMTree>());
+ EXPECT_EQ("[Hello ][][\n][input][\n][!]", Iterate<FlatTree>());
+}
+
+TEST_F(TextIteratorTest, EnteringTextControlsWithOption) {
+ static const char* input = "<p>Hello <input type='text' value='input'>!</p>";
+ SetBodyContent(input);
+ EXPECT_EQ("[Hello ][\n][input][!]",
+ Iterate<DOMTree>(EntersTextControlsBehavior()));
+ EXPECT_EQ("[Hello ][][\n][input][\n][!]",
+ Iterate<FlatTree>(EntersTextControlsBehavior()));
+}
+
+TEST_F(TextIteratorTest, EnteringTextControlsWithOptionComplex) {
+ static const char* input =
+ "<input type='text' value='Beginning of range'><div><div><input "
+ "type='text' value='Under DOM nodes'></div></div><input type='text' "
+ "value='End of range'>";
+ SetBodyContent(input);
+ EXPECT_EQ("[\n][Beginning of range][\n][Under DOM nodes][\n][End of range]",
+ Iterate<DOMTree>(EntersTextControlsBehavior()));
+ EXPECT_EQ(
+ "[][\n][Beginning of range][\n][][\n][Under DOM nodes][\n][][\n][End of "
+ "range]",
+ Iterate<FlatTree>(EntersTextControlsBehavior()));
+}
+
+TEST_F(TextIteratorTest, NotEnteringShadowTree) {
+ static const char* body_content =
+ "<div>Hello, <span id='host'>text</span> iterator.</div>";
+ static const char* shadow_content = "<span>shadow</span>";
+ SetBodyContent(body_content);
+ CreateShadowRootForElementWithIDAndSetInnerHTML(GetDocument(), "host",
+ shadow_content);
+ // TextIterator doesn't emit "text" since its layoutObject is not created. The
+ // shadow tree is ignored.
+ EXPECT_EQ("[Hello, ][ iterator.]", Iterate<DOMTree>());
+ EXPECT_EQ("[Hello, ][shadow][ iterator.]", Iterate<FlatTree>());
+}
+
+TEST_F(TextIteratorTest, NotEnteringShadowTreeWithNestedShadowTrees) {
+ static const char* body_content =
+ "<div>Hello, <span id='host-in-document'>text</span> iterator.</div>";
+ static const char* shadow_content1 =
+ "<span>first <span id='host-in-shadow'>shadow</span></span>";
+ static const char* shadow_content2 = "<span>second shadow</span>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root1 = CreateShadowRootForElementWithIDAndSetInnerHTML(
+ GetDocument(), "host-in-document", shadow_content1);
+ CreateShadowRootForElementWithIDAndSetInnerHTML(
+ *shadow_root1, "host-in-shadow", shadow_content2);
+ EXPECT_EQ("[Hello, ][ iterator.]", Iterate<DOMTree>());
+ EXPECT_EQ("[Hello, ][first ][second shadow][ iterator.]",
+ Iterate<FlatTree>());
+}
+
+TEST_F(TextIteratorTest, NotEnteringShadowTreeWithContentInsertionPoint) {
+ static const char* body_content =
+ "<div>Hello, <span id='host'>text</span> iterator.</div>";
+ static const char* shadow_content =
+ "<span>shadow <content>content</content></span>";
+ SetBodyContent(body_content);
+ CreateShadowRootForElementWithIDAndSetInnerHTML(GetDocument(), "host",
+ shadow_content);
+ // In this case a layoutObject for "text" is created, so it shows up here.
+ EXPECT_EQ("[Hello, ][text][ iterator.]", Iterate<DOMTree>());
+ EXPECT_EQ("[Hello, ][shadow ][text][ iterator.]", Iterate<FlatTree>());
+}
+
+TEST_F(TextIteratorTest, EnteringShadowTreeWithOption) {
+ static const char* body_content =
+ "<div>Hello, <span id='host'>text</span> iterator.</div>";
+ static const char* shadow_content = "<span>shadow</span>";
+ SetBodyContent(body_content);
+ CreateShadowRootForElementWithIDAndSetInnerHTML(GetDocument(), "host",
+ shadow_content);
+ // TextIterator emits "shadow" since entersOpenShadowRootsBehavior() is
+ // specified.
+ EXPECT_EQ("[Hello, ][shadow][ iterator.]",
+ Iterate<DOMTree>(EntersOpenShadowRootsBehavior()));
+ EXPECT_EQ("[Hello, ][shadow][ iterator.]",
+ Iterate<FlatTree>(EntersOpenShadowRootsBehavior()));
+}
+
+TEST_F(TextIteratorTest, EnteringShadowTreeWithNestedShadowTreesWithOption) {
+ static const char* body_content =
+ "<div>Hello, <span id='host-in-document'>text</span> iterator.</div>";
+ static const char* shadow_content1 =
+ "<span>first <span id='host-in-shadow'>shadow</span></span>";
+ static const char* shadow_content2 = "<span>second shadow</span>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root1 = CreateShadowRootForElementWithIDAndSetInnerHTML(
+ GetDocument(), "host-in-document", shadow_content1);
+ CreateShadowRootForElementWithIDAndSetInnerHTML(
+ *shadow_root1, "host-in-shadow", shadow_content2);
+ EXPECT_EQ("[Hello, ][first ][second shadow][ iterator.]",
+ Iterate<DOMTree>(EntersOpenShadowRootsBehavior()));
+ EXPECT_EQ("[Hello, ][first ][second shadow][ iterator.]",
+ Iterate<FlatTree>(EntersOpenShadowRootsBehavior()));
+}
+
+TEST_F(TextIteratorTest,
+ EnteringShadowTreeWithContentInsertionPointWithOption) {
+ static const char* body_content =
+ "<div>Hello, <span id='host'>text</span> iterator.</div>";
+ static const char* shadow_content =
+ "<span><content>content</content> shadow</span>";
+ // In this case a layoutObject for "text" is created, and emitted AFTER any
+ // nodes in the shadow tree. This order does not match the order of the
+ // rendered texts, but at this moment it's the expected behavior.
+ // FIXME: Fix this. We probably need pure-renderer-based implementation of
+ // TextIterator to achieve this.
+ SetBodyContent(body_content);
+ CreateShadowRootForElementWithIDAndSetInnerHTML(GetDocument(), "host",
+ shadow_content);
+ EXPECT_EQ("[Hello, ][ shadow][text][ iterator.]",
+ Iterate<DOMTree>(EntersOpenShadowRootsBehavior()));
+ EXPECT_EQ("[Hello, ][text][ shadow][ iterator.]",
+ Iterate<FlatTree>(EntersOpenShadowRootsBehavior()));
+}
+
+TEST_F(TextIteratorTest, StartingAtNodeInShadowRoot) {
+ static const char* body_content =
+ "<div id='outer'>Hello, <span id='host'>text</span> iterator.</div>";
+ static const char* shadow_content =
+ "<span><content>content</content> shadow</span>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = CreateShadowRootForElementWithIDAndSetInnerHTML(
+ GetDocument(), "host", shadow_content);
+ Node* outer_div = GetDocument().getElementById("outer");
+ Node* span_in_shadow = shadow_root->firstChild();
+ Position start(span_in_shadow, PositionAnchorType::kBeforeChildren);
+ Position end(outer_div, PositionAnchorType::kAfterChildren);
+ EXPECT_EQ(
+ "[ shadow][text][ iterator.]",
+ IteratePartial<DOMTree>(start, end, EntersOpenShadowRootsBehavior()));
+
+ PositionInFlatTree start_in_flat_tree(span_in_shadow,
+ PositionAnchorType::kBeforeChildren);
+ PositionInFlatTree end_in_flat_tree(outer_div,
+ PositionAnchorType::kAfterChildren);
+ EXPECT_EQ("[text][ shadow][ iterator.]",
+ IteratePartial<FlatTree>(start_in_flat_tree, end_in_flat_tree,
+ EntersOpenShadowRootsBehavior()));
+}
+
+TEST_F(TextIteratorTest, FinishingAtNodeInShadowRoot) {
+ static const char* body_content =
+ "<div id='outer'>Hello, <span id='host'>text</span> iterator.</div>";
+ static const char* shadow_content =
+ "<span><content>content</content> shadow</span>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = CreateShadowRootForElementWithIDAndSetInnerHTML(
+ GetDocument(), "host", shadow_content);
+ Node* outer_div = GetDocument().getElementById("outer");
+ Node* span_in_shadow = shadow_root->firstChild();
+ Position start(outer_div, PositionAnchorType::kBeforeChildren);
+ Position end(span_in_shadow, PositionAnchorType::kAfterChildren);
+ EXPECT_EQ(
+ "[Hello, ][ shadow]",
+ IteratePartial<DOMTree>(start, end, EntersOpenShadowRootsBehavior()));
+
+ PositionInFlatTree start_in_flat_tree(outer_div,
+ PositionAnchorType::kBeforeChildren);
+ PositionInFlatTree end_in_flat_tree(span_in_shadow,
+ PositionAnchorType::kAfterChildren);
+ EXPECT_EQ("[Hello, ][text][ shadow]",
+ IteratePartial<FlatTree>(start_in_flat_tree, end_in_flat_tree,
+ EntersOpenShadowRootsBehavior()));
+}
+
+TEST_F(TextIteratorTest, FullyClipsContents) {
+ static const char* body_content =
+ "<div style='overflow: hidden; width: 200px; height: 0;'>"
+ "I'm invisible"
+ "</div>";
+ SetBodyContent(body_content);
+ EXPECT_EQ("", Iterate<DOMTree>());
+ EXPECT_EQ("", Iterate<FlatTree>());
+}
+
+TEST_F(TextIteratorTest, IgnoresContainerClip) {
+ static const char* body_content =
+ "<div style='overflow: hidden; width: 200px; height: 0;'>"
+ "<div>I'm not visible</div>"
+ "<div style='position: absolute; width: 200px; height: 200px; top: 0; "
+ "right: 0;'>"
+ "but I am!"
+ "</div>"
+ "</div>";
+ SetBodyContent(body_content);
+ EXPECT_EQ("[but I am!]", Iterate<DOMTree>());
+ EXPECT_EQ("[but I am!]", Iterate<FlatTree>());
+}
+
+TEST_F(TextIteratorTest, FullyClippedContentsDistributed) {
+ static const char* body_content =
+ "<div id='host'>"
+ "<div>Am I visible?</div>"
+ "</div>";
+ static const char* shadow_content =
+ "<div style='overflow: hidden; width: 200px; height: 0;'>"
+ "<content></content>"
+ "</div>";
+ SetBodyContent(body_content);
+ CreateShadowRootForElementWithIDAndSetInnerHTML(GetDocument(), "host",
+ shadow_content);
+ // FIXME: The text below is actually invisible but TextIterator currently
+ // thinks it's visible.
+ EXPECT_EQ("[\n][Am I visible?]",
+ Iterate<DOMTree>(EntersOpenShadowRootsBehavior()));
+ EXPECT_EQ("", Iterate<FlatTree>(EntersOpenShadowRootsBehavior()));
+}
+
+TEST_F(TextIteratorTest, IgnoresContainersClipDistributed) {
+ static const char* body_content =
+ "<div id='host' style='overflow: hidden; width: 200px; height: 0;'>"
+ "<div>Nobody can find me!</div>"
+ "</div>";
+ static const char* shadow_content =
+ "<div style='position: absolute; width: 200px; height: 200px; top: 0; "
+ "right: 0;'>"
+ "<content></content>"
+ "</div>";
+ SetBodyContent(body_content);
+ CreateShadowRootForElementWithIDAndSetInnerHTML(GetDocument(), "host",
+ shadow_content);
+ // FIXME: The text below is actually visible but TextIterator currently thinks
+ // it's invisible.
+ // [\n][Nobody can find me!]
+ EXPECT_EQ("", Iterate<DOMTree>(EntersOpenShadowRootsBehavior()));
+ EXPECT_EQ("[Nobody can find me!]",
+ Iterate<FlatTree>(EntersOpenShadowRootsBehavior()));
+}
+
+TEST_F(TextIteratorTest, EmitsReplacementCharForInput) {
+ static const char* body_content =
+ "<div contenteditable='true'>"
+ "Before"
+ "<img src='foo.png'>"
+ "After"
+ "</div>";
+ SetBodyContent(body_content);
+ EXPECT_EQ("[Before][\xEF\xBF\xBC][After]",
+ Iterate<DOMTree>(EmitsObjectReplacementCharacterBehavior()));
+ EXPECT_EQ("[Before][\xEF\xBF\xBC][After]",
+ Iterate<FlatTree>(EmitsObjectReplacementCharacterBehavior()));
+}
+
+TEST_F(TextIteratorTest, RangeLengthWithReplacedElements) {
+ static const char* body_content =
+ "<div id='div' contenteditable='true'>1<img src='foo.png'>3</div>";
+ SetBodyContent(body_content);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ Node* div_node = GetDocument().getElementById("div");
+ const EphemeralRange range(Position(div_node, 0), Position(div_node, 3));
+
+ EXPECT_EQ(3, TextIterator::RangeLength(range));
+}
+
+TEST_F(TextIteratorTest, RangeLengthInMultilineSpan) {
+ static const char* body_content =
+ "<table style='width:5em'>"
+ "<tbody>"
+ "<tr>"
+ "<td>"
+ "<span id='span1'>one two three four five</span>"
+ "</td>"
+ "</tr>"
+ "</tbody>"
+ "</table>";
+
+ SetBodyContent(body_content);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ Node* span_node = GetDocument().getElementById("span1");
+ Node* text_node = span_node->firstChild();
+
+ // Select the word "two", this is the last word on the line.
+
+ const EphemeralRange range(Position(text_node, 4), Position(text_node, 7));
+
+ EXPECT_EQ(4, TextIterator::RangeLength(range));
+ EXPECT_EQ(3, TextIterator::RangeLength(
+ range,
+ TextIteratorBehavior::NoTrailingSpaceRangeLengthBehavior()));
+}
+
+TEST_P(ParameterizedTextIteratorTest, RangeLengthBasic) {
+ EXPECT_EQ(0, TestRangeLength("<p>^| (1) abc def</p>"));
+ EXPECT_EQ(0, TestRangeLength("<p>^ |(1) abc def</p>"));
+ EXPECT_EQ(1, TestRangeLength("<p>^ (|1) abc def</p>"));
+ EXPECT_EQ(2, TestRangeLength("<p>^ (1|) abc def</p>"));
+ EXPECT_EQ(3, TestRangeLength("<p>^ (1)| abc def</p>"));
+ EXPECT_EQ(4, TestRangeLength("<p>^ (1) |abc def</p>"));
+ EXPECT_EQ(5, TestRangeLength("<p>^ (1) a|bc def</p>"));
+ EXPECT_EQ(6, TestRangeLength("<p>^ (1) ab|c def</p>"));
+ EXPECT_EQ(7, TestRangeLength("<p>^ (1) abc| def</p>"));
+ EXPECT_EQ(8, TestRangeLength("<p>^ (1) abc |def</p>"));
+ EXPECT_EQ(9, TestRangeLength("<p>^ (1) abc d|ef</p>"));
+ EXPECT_EQ(10, TestRangeLength("<p>^ (1) abc de|f</p>"));
+ EXPECT_EQ(11, TestRangeLength("<p>^ (1) abc def|</p>"));
+}
+
+TEST_P(ParameterizedTextIteratorTest, RangeLengthWithFirstLetter) {
+ InsertStyleElement("p::first-letter {font-size:200%;}");
+ // Expectation should be as same as |RangeLengthBasic|
+ EXPECT_EQ(0, TestRangeLength("<p>^| (1) abc def</p>"));
+ EXPECT_EQ(0, TestRangeLength("<p>^ |(1) abc def</p>"));
+ EXPECT_EQ(1, TestRangeLength("<p>^ (|1) abc def</p>"));
+ EXPECT_EQ(2, TestRangeLength("<p>^ (1|) abc def</p>"));
+ EXPECT_EQ(3, TestRangeLength("<p>^ (1)| abc def</p>"));
+ EXPECT_EQ(4, TestRangeLength("<p>^ (1) |abc def</p>"));
+ EXPECT_EQ(5, TestRangeLength("<p>^ (1) a|bc def</p>"));
+ EXPECT_EQ(6, TestRangeLength("<p>^ (1) ab|c def</p>"));
+ EXPECT_EQ(7, TestRangeLength("<p>^ (1) abc| def</p>"));
+ EXPECT_EQ(8, TestRangeLength("<p>^ (1) abc |def</p>"));
+ EXPECT_EQ(9, TestRangeLength("<p>^ (1) abc d|ef</p>"));
+ EXPECT_EQ(10, TestRangeLength("<p>^ (1) abc de|f</p>"));
+ EXPECT_EQ(11, TestRangeLength("<p>^ (1) abc def|</p>"));
+}
+
+TEST_F(TextIteratorTest, WhitespaceCollapseForReplacedElements) {
+ static const char* body_content =
+ "<span>Some text </span> <input type='button' value='Button "
+ "text'/><span>Some more text</span>";
+ SetBodyContent(body_content);
+ EXPECT_EQ("[Some text ][][Some more text]",
+ Iterate<DOMTree>(CollapseTrailingSpaceBehavior()));
+ EXPECT_EQ("[Some text ][][Button text][Some more text]",
+ Iterate<FlatTree>(CollapseTrailingSpaceBehavior()));
+}
+
+TEST_F(TextIteratorTest, copyTextTo) {
+ const char* body_content =
+ "<a id=host><b id=one>one</b> not appeared <b id=two>two</b></a>";
+ const char* shadow_content =
+ "three <content select=#two></content> <content select=#one></content> "
+ "zero";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ Element* host = GetDocument().getElementById("host");
+ const char* message = "|iter%d| should have emitted '%s'.";
+
+ EphemeralRangeTemplate<EditingStrategy> range1(
+ EphemeralRangeTemplate<EditingStrategy>::RangeOfContents(*host));
+ TextIteratorAlgorithm<EditingStrategy> iter1(range1.StartPosition(),
+ range1.EndPosition());
+ ForwardsTextBuffer output1;
+ iter1.CopyTextTo(&output1, 0, 2);
+ EXPECT_EQ("on", String(output1.Data(), output1.Size()))
+ << String::Format(message, 1, "on").Utf8().data();
+ iter1.CopyTextTo(&output1, 2, 1);
+ EXPECT_EQ("one", String(output1.Data(), output1.Size()))
+ << String::Format(message, 1, "one").Utf8().data();
+ iter1.Advance();
+ iter1.CopyTextTo(&output1, 0, 1);
+ EXPECT_EQ("onet", String(output1.Data(), output1.Size()))
+ << String::Format(message, 1, "onet").Utf8().data();
+ iter1.CopyTextTo(&output1, 1, 2);
+ EXPECT_EQ("onetwo", String(output1.Data(), output1.Size()))
+ << String::Format(message, 1, "onetwo").Utf8().data();
+
+ EphemeralRangeTemplate<EditingInFlatTreeStrategy> range2(
+ EphemeralRangeTemplate<EditingInFlatTreeStrategy>::RangeOfContents(
+ *host));
+ TextIteratorAlgorithm<EditingInFlatTreeStrategy> iter2(range2.StartPosition(),
+ range2.EndPosition());
+ ForwardsTextBuffer output2;
+ iter2.CopyTextTo(&output2, 0, 3);
+ EXPECT_EQ("thr", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, "thr").Utf8().data();
+ iter2.CopyTextTo(&output2, 3, 3);
+ EXPECT_EQ("three ", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, "three ").Utf8().data();
+ iter2.Advance();
+ iter2.CopyTextTo(&output2, 0, 2);
+ EXPECT_EQ("three tw", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, "three tw").Utf8().data();
+ iter2.CopyTextTo(&output2, 2, 1);
+ EXPECT_EQ("three two", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, "three two").Utf8().data();
+ iter2.Advance();
+ iter2.CopyTextTo(&output2, 0, 1);
+ EXPECT_EQ("three two ", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, "three two ").Utf8().data();
+ iter2.Advance();
+ iter2.CopyTextTo(&output2, 0, 1);
+ EXPECT_EQ("three two o", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, "three two o").Utf8().data();
+ iter2.CopyTextTo(&output2, 1, 2);
+ EXPECT_EQ("three two one", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, "three two one").Utf8().data();
+ iter2.Advance();
+ iter2.CopyTextTo(&output2, 0, 2);
+ EXPECT_EQ("three two one z", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, "three two one z").Utf8().data();
+ iter2.CopyTextTo(&output2, 2, 3);
+ EXPECT_EQ("three two one zero", String(output2.Data(), output2.Size()))
+ << String::Format(message, 2, "three two one zero").Utf8().data();
+}
+
+TEST_F(TextIteratorTest, characterAt) {
+ const char* body_content =
+ "<a id=host><b id=one>one</b> not appeared <b id=two>two</b></a>";
+ const char* shadow_content =
+ "three <content select=#two></content> <content select=#one></content> "
+ "zero";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ Element* host = GetDocument().getElementById("host");
+
+ EphemeralRangeTemplate<EditingStrategy> range1(
+ EphemeralRangeTemplate<EditingStrategy>::RangeOfContents(*host));
+ TextIteratorAlgorithm<EditingStrategy> iter1(range1.StartPosition(),
+ range1.EndPosition());
+ const char* message1 = "|iter1| should emit 'one' and 'two'.";
+ EXPECT_EQ('o', iter1.CharacterAt(0)) << message1;
+ EXPECT_EQ('n', iter1.CharacterAt(1)) << message1;
+ EXPECT_EQ('e', iter1.CharacterAt(2)) << message1;
+ iter1.Advance();
+ EXPECT_EQ('t', iter1.CharacterAt(0)) << message1;
+ EXPECT_EQ('w', iter1.CharacterAt(1)) << message1;
+ EXPECT_EQ('o', iter1.CharacterAt(2)) << message1;
+
+ EphemeralRangeTemplate<EditingInFlatTreeStrategy> range2(
+ EphemeralRangeTemplate<EditingInFlatTreeStrategy>::RangeOfContents(
+ *host));
+ TextIteratorAlgorithm<EditingInFlatTreeStrategy> iter2(range2.StartPosition(),
+ range2.EndPosition());
+ const char* message2 =
+ "|iter2| should emit 'three ', 'two', ' ', 'one' and ' zero'.";
+ EXPECT_EQ('t', iter2.CharacterAt(0)) << message2;
+ EXPECT_EQ('h', iter2.CharacterAt(1)) << message2;
+ EXPECT_EQ('r', iter2.CharacterAt(2)) << message2;
+ EXPECT_EQ('e', iter2.CharacterAt(3)) << message2;
+ EXPECT_EQ('e', iter2.CharacterAt(4)) << message2;
+ EXPECT_EQ(' ', iter2.CharacterAt(5)) << message2;
+ iter2.Advance();
+ EXPECT_EQ('t', iter2.CharacterAt(0)) << message2;
+ EXPECT_EQ('w', iter2.CharacterAt(1)) << message2;
+ EXPECT_EQ('o', iter2.CharacterAt(2)) << message2;
+ iter2.Advance();
+ EXPECT_EQ(' ', iter2.CharacterAt(0)) << message2;
+ iter2.Advance();
+ EXPECT_EQ('o', iter2.CharacterAt(0)) << message2;
+ EXPECT_EQ('n', iter2.CharacterAt(1)) << message2;
+ EXPECT_EQ('e', iter2.CharacterAt(2)) << message2;
+ iter2.Advance();
+ EXPECT_EQ(' ', iter2.CharacterAt(0)) << message2;
+ EXPECT_EQ('z', iter2.CharacterAt(1)) << message2;
+ EXPECT_EQ('e', iter2.CharacterAt(2)) << message2;
+ EXPECT_EQ('r', iter2.CharacterAt(3)) << message2;
+ EXPECT_EQ('o', iter2.CharacterAt(4)) << message2;
+}
+
+TEST_F(TextIteratorTest, CopyWholeCodePoints) {
+ const char* body_content = "&#x13000;&#x13001;&#x13002; &#x13140;&#x13141;.";
+ SetBodyContent(body_content);
+
+ const UChar kExpected[] = {0xD80C, 0xDC00, 0xD80C, 0xDC01, 0xD80C, 0xDC02,
+ ' ', 0xD80C, 0xDD40, 0xD80C, 0xDD41, '.'};
+
+ EphemeralRange range(EphemeralRange::RangeOfContents(GetDocument()));
+ TextIterator iter(range.StartPosition(), range.EndPosition());
+ ForwardsTextBuffer buffer;
+ EXPECT_EQ(2, iter.CopyTextTo(&buffer, 0, 1))
+ << "Should emit 2 UChars for 'U+13000'.";
+ EXPECT_EQ(4, iter.CopyTextTo(&buffer, 2, 3))
+ << "Should emit 4 UChars for 'U+13001U+13002'.";
+ EXPECT_EQ(3, iter.CopyTextTo(&buffer, 6, 2))
+ << "Should emit 3 UChars for ' U+13140'.";
+ EXPECT_EQ(2, iter.CopyTextTo(&buffer, 9, 2))
+ << "Should emit 2 UChars for 'U+13141'.";
+ EXPECT_EQ(1, iter.CopyTextTo(&buffer, 11, 1))
+ << "Should emit 1 UChar for '.'.";
+ for (int i = 0; i < 12; i++)
+ EXPECT_EQ(kExpected[i], buffer[i]);
+}
+
+// Regression test for crbug.com/630921
+TEST_F(TextIteratorTest, EndingConditionWithDisplayNone) {
+ SetBodyContent(
+ "<div style='display: none'><span>hello</span>world</div>Lorem ipsum "
+ "dolor sit amet.");
+ Position start(&GetDocument(), 0);
+ Position end(GetDocument().QuerySelector("span"), 0);
+ TextIterator iter(start, end);
+ EXPECT_TRUE(iter.AtEnd());
+}
+
+// Trickier regression test for crbug.com/630921
+TEST_F(TextIteratorTest, EndingConditionWithDisplayNoneInShadowTree) {
+ const char* body_content =
+ "<div style='display: none'><span id=host><a></a></span>world</div>Lorem "
+ "ipsum dolor sit amet.";
+ const char* shadow_content = "<i><b id=end>he</b></i>llo";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ ShadowRoot* shadow_root =
+ GetDocument().getElementById("host")->OpenShadowRoot();
+ Node* b_in_shadow_tree = shadow_root->getElementById("end");
+
+ Position start(&GetDocument(), 0);
+ Position end(b_in_shadow_tree, 0);
+ TextIterator iter(start, end);
+ EXPECT_TRUE(iter.AtEnd());
+}
+
+TEST_F(TextIteratorTest, PreserveLeadingSpace) {
+ SetBodyContent("<div style='width: 2em;'><b><i>foo</i></b> bar</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Position start(div->firstChild()->firstChild()->firstChild(), 0);
+ Position end(div->lastChild(), 4);
+ EXPECT_EQ("foo bar",
+ PlainText(EphemeralRange(start, end), EmitsImageAltTextBehavior()));
+}
+
+// We used to have a bug where the leading space was duplicated if we didn't
+// emit alt text, this tests for that bug
+TEST_F(TextIteratorTest, PreserveLeadingSpaceWithoutEmittingAltText) {
+ SetBodyContent("<div style='width: 2em;'><b><i>foo</i></b> bar</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Position start(div->firstChild()->firstChild()->firstChild(), 0);
+ Position end(div->lastChild(), 4);
+ EXPECT_EQ("foo bar", PlainText(EphemeralRange(start, end)));
+}
+
+TEST_F(TextIteratorTest, PreserveOnlyLeadingSpace) {
+ SetBodyContent(
+ "<div style='width: 2em;'><b><i id='foo'>foo </i></b> bar</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Position start(GetDocument().getElementById("foo")->firstChild(), 0);
+ Position end(div->lastChild(), 4);
+ EXPECT_EQ("foo bar",
+ PlainText(EphemeralRange(start, end), EmitsImageAltTextBehavior()));
+}
+
+TEST_F(TextIteratorTest, StartAtFirstLetter) {
+ SetBodyContent("<style>div:first-letter {color:red;}</style><div>Axyz</div>");
+
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+ Position start(text, 0);
+ Position end(text, 4);
+ TextIterator iter(start, end);
+ ForwardsTextBuffer buffer;
+
+ EXPECT_FALSE(iter.AtEnd());
+ EXPECT_EQ(1, iter.length());
+ EXPECT_EQ(1, iter.CopyTextTo(&buffer, 0)) << "Should emit 'A'.";
+ EXPECT_EQ(text, iter.CurrentContainer());
+ EXPECT_EQ(Position(text, 0), iter.StartPositionInCurrentContainer());
+ EXPECT_EQ(Position(text, 1), iter.EndPositionInCurrentContainer());
+
+ iter.Advance();
+ EXPECT_FALSE(iter.AtEnd());
+ EXPECT_EQ(3, iter.length());
+ EXPECT_EQ(3, iter.CopyTextTo(&buffer, 0)) << "Should emit 'xyz'.";
+ EXPECT_EQ(text, iter.CurrentContainer());
+ EXPECT_EQ(Position(text, 1), iter.StartPositionInCurrentContainer());
+ EXPECT_EQ(Position(text, 4), iter.EndPositionInCurrentContainer());
+
+ iter.Advance();
+ EXPECT_TRUE(iter.AtEnd());
+
+ EXPECT_EQ("Axyz", String(buffer.Data()));
+}
+
+TEST_F(TextIteratorTest, StartInMultiCharFirstLetterWithCollapsedSpace) {
+ SetBodyContent(
+ "<style>div:first-letter {color:red;}</style><div> (A) xyz</div>");
+
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+ Position start(text, 3);
+ Position end(text, 10);
+ TextIterator iter(start, end);
+ ForwardsTextBuffer buffer;
+
+ EXPECT_FALSE(iter.AtEnd());
+ EXPECT_EQ(2, iter.length());
+ EXPECT_EQ(2, iter.CopyTextTo(&buffer, 0)) << "Should emit 'A)'.";
+ EXPECT_EQ(text, iter.CurrentContainer());
+ EXPECT_EQ(Position(text, 3), iter.StartPositionInCurrentContainer());
+ EXPECT_EQ(Position(text, 5), iter.EndPositionInCurrentContainer());
+
+ iter.Advance();
+ EXPECT_FALSE(iter.AtEnd());
+ EXPECT_EQ(1, iter.length());
+ EXPECT_EQ(1, iter.CopyTextTo(&buffer, 0)) << "Should emit ' '.";
+ EXPECT_EQ(text, iter.CurrentContainer());
+ EXPECT_EQ(Position(text, 5), iter.StartPositionInCurrentContainer());
+ EXPECT_EQ(Position(text, 6), iter.EndPositionInCurrentContainer());
+
+ iter.Advance();
+ EXPECT_FALSE(iter.AtEnd());
+ EXPECT_EQ(3, iter.length());
+ EXPECT_EQ(3, iter.CopyTextTo(&buffer, 0)) << "Should emit 'xyz'.";
+ EXPECT_EQ(text, iter.CurrentContainer());
+ EXPECT_EQ(Position(text, 7), iter.StartPositionInCurrentContainer());
+ EXPECT_EQ(Position(text, 10), iter.EndPositionInCurrentContainer());
+
+ iter.Advance();
+ EXPECT_TRUE(iter.AtEnd());
+
+ EXPECT_EQ("A) xyz", String(buffer.Data()));
+}
+
+TEST_F(TextIteratorTest, StartAndEndInMultiCharFirstLetterWithCollapsedSpace) {
+ SetBodyContent(
+ "<style>div:first-letter {color:red;}</style><div> (A) xyz</div>");
+
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+ Position start(text, 3);
+ Position end(text, 4);
+ TextIterator iter(start, end);
+ ForwardsTextBuffer buffer;
+
+ EXPECT_FALSE(iter.AtEnd());
+ EXPECT_EQ(1, iter.length());
+ EXPECT_EQ(1, iter.CopyTextTo(&buffer, 0)) << "Should emit 'A'.";
+ EXPECT_EQ(text, iter.CurrentContainer());
+ EXPECT_EQ(Position(text, 3), iter.StartPositionInCurrentContainer());
+ EXPECT_EQ(Position(text, 4), iter.EndPositionInCurrentContainer());
+
+ iter.Advance();
+ EXPECT_TRUE(iter.AtEnd());
+
+ EXPECT_EQ("A", String(buffer.Data()));
+}
+
+TEST_F(TextIteratorTest, StartAtRemainingText) {
+ SetBodyContent("<style>div:first-letter {color:red;}</style><div>Axyz</div>");
+
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+ Position start(text, 1);
+ Position end(text, 4);
+ TextIterator iter(start, end);
+ ForwardsTextBuffer buffer;
+
+ EXPECT_FALSE(iter.AtEnd());
+ EXPECT_EQ(3, iter.length());
+ EXPECT_EQ(3, iter.CopyTextTo(&buffer, 0)) << "Should emit 'xyz'.";
+ EXPECT_EQ(text, iter.CurrentContainer());
+ EXPECT_EQ(Position(text, 1), iter.StartPositionInCurrentContainer());
+ EXPECT_EQ(Position(text, 4), iter.EndPositionInCurrentContainer());
+
+ iter.Advance();
+ EXPECT_TRUE(iter.AtEnd());
+
+ EXPECT_EQ("xyz", String(buffer.Data()));
+}
+
+TEST_F(TextIteratorTest, StartAtFirstLetterInPre) {
+ SetBodyContent("<style>pre:first-letter {color:red;}</style><pre>Axyz</pre>");
+
+ Element* pre = GetDocument().QuerySelector("pre");
+ Node* text = pre->firstChild();
+ Position start(text, 0);
+ Position end(text, 4);
+ TextIterator iter(start, end);
+ ForwardsTextBuffer buffer;
+
+ EXPECT_FALSE(iter.AtEnd());
+ EXPECT_EQ(1, iter.length());
+ EXPECT_EQ(1, iter.CopyTextTo(&buffer, 0)) << "Should emit 'A'.";
+ EXPECT_EQ(text, iter.CurrentContainer());
+ EXPECT_EQ(Position(text, 0), iter.StartPositionInCurrentContainer());
+ EXPECT_EQ(Position(text, 1), iter.EndPositionInCurrentContainer());
+
+ iter.Advance();
+ EXPECT_FALSE(iter.AtEnd());
+ EXPECT_EQ(3, iter.length());
+ EXPECT_EQ(3, iter.CopyTextTo(&buffer, 0)) << "Should emit 'xyz'.";
+ EXPECT_EQ(text, iter.CurrentContainer());
+ EXPECT_EQ(Position(text, 1), iter.StartPositionInCurrentContainer());
+ EXPECT_EQ(Position(text, 4), iter.EndPositionInCurrentContainer());
+
+ iter.Advance();
+ EXPECT_TRUE(iter.AtEnd());
+
+ EXPECT_EQ("Axyz", String(buffer.Data()));
+}
+
+TEST_F(TextIteratorTest, StartInMultiCharFirstLetterInPre) {
+ SetBodyContent(
+ "<style>pre:first-letter {color:red;}</style><pre>(A)xyz</pre>");
+
+ Element* pre = GetDocument().QuerySelector("pre");
+ Node* text = pre->firstChild();
+ Position start(text, 1);
+ Position end(text, 6);
+ TextIterator iter(start, end);
+ ForwardsTextBuffer buffer;
+
+ EXPECT_FALSE(iter.AtEnd());
+ EXPECT_EQ(2, iter.length());
+ EXPECT_EQ(2, iter.CopyTextTo(&buffer, 0)) << "Should emit 'A)'.";
+ EXPECT_EQ(text, iter.CurrentContainer());
+ EXPECT_EQ(Position(text, 1), iter.StartPositionInCurrentContainer());
+ EXPECT_EQ(Position(text, 3), iter.EndPositionInCurrentContainer());
+
+ iter.Advance();
+ EXPECT_FALSE(iter.AtEnd());
+ EXPECT_EQ(3, iter.length());
+ EXPECT_EQ(3, iter.CopyTextTo(&buffer, 0)) << "Should emit 'xyz'.";
+ EXPECT_EQ(text, iter.CurrentContainer());
+ EXPECT_EQ(Position(text, 3), iter.StartPositionInCurrentContainer());
+ EXPECT_EQ(Position(text, 6), iter.EndPositionInCurrentContainer());
+
+ iter.Advance();
+ EXPECT_TRUE(iter.AtEnd());
+
+ EXPECT_EQ("A)xyz", String(buffer.Data()));
+}
+
+TEST_F(TextIteratorTest, StartAndEndInMultiCharFirstLetterInPre) {
+ SetBodyContent(
+ "<style>pre:first-letter {color:red;}</style><pre>(A)xyz</pre>");
+
+ Element* pre = GetDocument().QuerySelector("pre");
+ Node* text = pre->firstChild();
+ Position start(text, 1);
+ Position end(text, 2);
+ TextIterator iter(start, end);
+ ForwardsTextBuffer buffer;
+
+ EXPECT_FALSE(iter.AtEnd());
+ EXPECT_EQ(1, iter.length());
+ EXPECT_EQ(1, iter.CopyTextTo(&buffer, 0)) << "Should emit 'A'.";
+ EXPECT_EQ(text, iter.CurrentContainer());
+ EXPECT_EQ(Position(text, 1), iter.StartPositionInCurrentContainer());
+ EXPECT_EQ(Position(text, 2), iter.EndPositionInCurrentContainer());
+
+ iter.Advance();
+ EXPECT_TRUE(iter.AtEnd());
+
+ EXPECT_EQ("A", String(buffer.Data()));
+}
+
+TEST_F(TextIteratorTest, StartAtRemainingTextInPre) {
+ SetBodyContent("<style>pre:first-letter {color:red;}</style><pre>Axyz</pre>");
+
+ Element* pre = GetDocument().QuerySelector("pre");
+ Node* text = pre->firstChild();
+ Position start(text, 1);
+ Position end(text, 4);
+ TextIterator iter(start, end);
+ ForwardsTextBuffer buffer;
+
+ EXPECT_FALSE(iter.AtEnd());
+ EXPECT_EQ(3, iter.length());
+ EXPECT_EQ(3, iter.CopyTextTo(&buffer, 0)) << "Should emit 'xyz'.";
+ EXPECT_EQ(text, iter.CurrentContainer());
+ EXPECT_EQ(Position(text, 1), iter.StartPositionInCurrentContainer());
+ EXPECT_EQ(Position(text, 4), iter.EndPositionInCurrentContainer());
+
+ iter.Advance();
+ EXPECT_TRUE(iter.AtEnd());
+
+ EXPECT_EQ("xyz", String(buffer.Data()));
+}
+
+TEST_F(TextIteratorTest, VisitsDisplayContentsChildren) {
+ SetBodyContent(
+ "<p>Hello, \ntext</p><p style='display: contents'>iterator.</p>");
+
+ EXPECT_EQ("[Hello, ][text][iterator.]", Iterate<DOMTree>());
+ EXPECT_EQ("[Hello, ][text][iterator.]", Iterate<FlatTree>());
+}
+
+TEST_F(TextIteratorTest, BasicIterationEmptyContent) {
+ SetBodyContent("");
+ EXPECT_EQ("", Iterate<DOMTree>());
+}
+
+TEST_F(TextIteratorTest, BasicIterationSingleCharacter) {
+ SetBodyContent("a");
+ EXPECT_EQ("[a]", Iterate<DOMTree>());
+}
+
+TEST_F(TextIteratorTest, BasicIterationSingleDiv) {
+ SetBodyContent("<div>a</div>");
+ EXPECT_EQ("[a]", Iterate<DOMTree>());
+}
+
+TEST_F(TextIteratorTest, BasicIterationMultipleDivs) {
+ SetBodyContent("<div>a</div><div>b</div>");
+ EXPECT_EQ("[a][\n][b]", Iterate<DOMTree>());
+}
+
+TEST_F(TextIteratorTest, BasicIterationMultipleDivsWithStyle) {
+ SetBodyContent(
+ "<div style='line-height: 18px; min-height: 436px; '>"
+ "debugging this note"
+ "</div>");
+ EXPECT_EQ("[debugging this note]", Iterate<DOMTree>());
+}
+
+TEST_F(TextIteratorTest, BasicIterationMultipleDivsWithChildren) {
+ SetBodyContent("<div>Hello<div><br><span></span></div></div>");
+ EXPECT_EQ("[Hello][\n][\n]", Iterate<DOMTree>());
+}
+
+TEST_F(TextIteratorTest, BasicIterationOnChildrenWithStyle) {
+ SetBodyContent(
+ "<div style='left:22px'>"
+ "</div>"
+ "\t\t\n"
+ "<div style='left:26px'>"
+ "</div>"
+ "\t\t\n\n"
+ "<div>"
+ "\t\t\t\n"
+ "<div>"
+ "\t\t\t\t\n"
+ "<div>"
+ "\t\t\t\t\t\n"
+ "<div contenteditable style='line-height: 20px; min-height: 580px; '>"
+ "hey"
+ "</div>"
+ "\t\t\t\t\n"
+ "</div>"
+ "\t\t\t\n"
+ "</div>"
+ "\t\t\n"
+ "</div>"
+ "\n\t\n");
+ EXPECT_EQ("[hey]", Iterate<DOMTree>());
+}
+
+TEST_F(TextIteratorTest, BasicIterationInput) {
+ SetBodyContent("<input id='a' value='b'>");
+ auto* input_element = ToTextControl(GetDocument().getElementById("a"));
+ const ShadowRoot* shadow_root = input_element->UserAgentShadowRoot();
+ const Position start = Position::FirstPositionInNode(*shadow_root);
+ const Position end = Position::LastPositionInNode(*shadow_root);
+ EXPECT_EQ("[b]", IteratePartial<DOMTree>(start, end));
+}
+
+TEST_F(TextIteratorTest, BasicIterationInputiWithBr) {
+ SetBodyContent("<input id='a' value='b'>");
+ auto* input_element = ToTextControl(GetDocument().getElementById("a"));
+ Element* inner_editor = input_element->InnerEditorElement();
+ Element* br = GetDocument().CreateRawElement(HTMLNames::brTag);
+ inner_editor->AppendChild(br);
+ const ShadowRoot* shadow_root = input_element->UserAgentShadowRoot();
+ const Position start = Position::FirstPositionInNode(*shadow_root);
+ const Position end = Position::LastPositionInNode(*shadow_root);
+ GetDocument().UpdateStyleAndLayout();
+ EXPECT_EQ("[b]", IteratePartial<DOMTree>(start, end));
+}
+
+TEST_P(ParameterizedTextIteratorTest, FloatLeft) {
+ SetBodyContent("abc<span style='float:left'>DEF</span>ghi");
+ EXPECT_EQ("[abc][DEF][ghi]", Iterate<DOMTree>())
+ << "float doesn't affect text iteration";
+}
+
+TEST_P(ParameterizedTextIteratorTest, FloatRight) {
+ SetBodyContent("abc<span style='float:right'>DEF</span>ghi");
+ EXPECT_EQ("[abc][DEF][ghi]", Iterate<DOMTree>())
+ << "float doesn't affect text iteration";
+}
+
+TEST_P(ParameterizedTextIteratorTest, InlineBlock) {
+ SetBodyContent("abc<span style='display:inline-block'>DEF<br>GHI</span>jkl");
+ EXPECT_EQ("[abc][DEF][\n][GHI][jkl]", Iterate<DOMTree>())
+ << "inline-block doesn't insert newline around itself.";
+}
+
+TEST_P(ParameterizedTextIteratorTest, NoZWSForSpaceAfterNoWrapSpace) {
+ SetBodyContent("<span style='white-space: nowrap'>foo </span> bar");
+ EXPECT_EQ("[foo ][bar]", Iterate<DOMTree>());
+}
+
+} // namespace text_iterator_test
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_text_node_handler.cc b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_text_node_handler.cc
new file mode 100644
index 00000000000..db3d12a7bcf
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_text_node_handler.cc
@@ -0,0 +1,580 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator_text_node_handler.h"
+
+#include <algorithm>
+#include "third_party/blink/renderer/core/dom/first_letter_pseudo_element.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator_text_state.h"
+#include "third_party/blink/renderer/core/layout/layout_text_fragment.h"
+#include "third_party/blink/renderer/core/layout/line/inline_text_box.h"
+#include "third_party/blink/renderer/core/layout/line/root_inline_box.h"
+#include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h"
+
+namespace blink {
+
+namespace {
+
+// A magic value for infinity, used to indicate that text emission should
+// proceed till the end of the text node. Can be removed when we can handle text
+// length differences due to text-transform correctly.
+const unsigned kMaxOffset = std::numeric_limits<unsigned>::max();
+
+bool ShouldSkipInvisibleTextAt(const Text& text,
+ unsigned offset,
+ bool ignores_visibility) {
+ // TODO(xiaochengh): Get style from NGInlineItem or NGPhysicalTextFragment.
+ const LayoutObject* layout_object = AssociatedLayoutObjectOf(text, offset);
+ if (!layout_object)
+ return true;
+ if (layout_object->Style()->Display() == EDisplay::kNone)
+ return true;
+ if (ignores_visibility)
+ return false;
+ return layout_object->Style()->Visibility() != EVisibility::kVisible;
+}
+
+struct StringAndOffsetRange {
+ String string;
+ unsigned start;
+ unsigned end;
+};
+
+StringAndOffsetRange ComputeTextAndOffsetsForEmission(
+ const NGOffsetMapping& mapping,
+ const NGOffsetMappingUnit& unit,
+ unsigned run_start,
+ unsigned run_end,
+ const TextIteratorBehavior& behavior) {
+ // TODO(xiaochengh): Handle EmitsOriginalText.
+ unsigned text_content_start = unit.ConvertDOMOffsetToTextContent(run_start);
+ unsigned text_content_end = unit.ConvertDOMOffsetToTextContent(run_end);
+ unsigned length = text_content_end - text_content_start;
+ if (behavior.EmitsSpaceForNbsp()) {
+ String string = mapping.GetText().Substring(text_content_start, length);
+ return {string, 0, 0};
+ }
+ return {mapping.GetText(), text_content_start, text_content_end};
+}
+
+} // namespace
+
+TextIteratorTextNodeHandler::TextIteratorTextNodeHandler(
+ const TextIteratorBehavior& behavior,
+ TextIteratorTextState* text_state)
+ : behavior_(behavior), text_state_(*text_state) {}
+
+bool TextIteratorTextNodeHandler::HandleRemainingTextRuns() {
+ if (uses_layout_ng_) {
+ HandleTextNodeWithLayoutNG();
+ return text_state_.PositionNode();
+ }
+
+ if (ShouldProceedToRemainingText())
+ ProceedToRemainingText();
+ // Handle remembered text box
+ if (text_box_) {
+ HandleTextBox();
+ return text_state_.PositionNode();
+ }
+ // Handle remembered pre-formatted text node.
+ if (!needs_handle_pre_formatted_text_node_)
+ return false;
+ HandlePreFormattedTextNode();
+ return text_state_.PositionNode();
+}
+
+void TextIteratorTextNodeHandler::HandleTextNodeWithLayoutNG() {
+ DCHECK_LE(offset_, end_offset_);
+ DCHECK_LE(end_offset_, text_node_->data().length());
+
+ if (ShouldSkipInvisibleTextAt(*text_node_, offset_,
+ IgnoresStyleVisibility())) {
+ offset_ = end_offset_;
+ return;
+ }
+
+ while (offset_ < end_offset_ && !text_state_.PositionNode()) {
+ const EphemeralRange range_to_emit(Position(text_node_, offset_),
+ Position(text_node_, end_offset_));
+
+ // We may go through multiple mappings, which happens when there is
+ // ::first-letter and blockifying style.
+ auto* mapping = NGOffsetMapping::GetFor(range_to_emit.StartPosition());
+ if (!mapping) {
+ offset_ = end_offset_;
+ return;
+ }
+
+ const unsigned initial_offset = offset_;
+ for (const NGOffsetMappingUnit& unit :
+ mapping->GetMappingUnitsForDOMRange(range_to_emit)) {
+ const unsigned run_start = std::max(offset_, unit.DOMStart());
+ const unsigned run_end = std::min(end_offset_, unit.DOMEnd());
+ if (run_start >= run_end ||
+ unit.ConvertDOMOffsetToTextContent(run_start) ==
+ unit.ConvertDOMOffsetToTextContent(run_end)) {
+ offset_ = run_end;
+ continue;
+ }
+
+ auto string_and_offsets = ComputeTextAndOffsetsForEmission(
+ *mapping, unit, run_start, run_end, behavior_);
+ const String& string = string_and_offsets.string;
+ const unsigned text_content_start = string_and_offsets.start;
+ const unsigned text_content_end = string_and_offsets.end;
+ text_state_.EmitText(text_node_, run_start, run_end, string,
+ text_content_start, text_content_end);
+ offset_ = run_end;
+ return;
+ }
+
+ // Bail if |offset_| isn't advanced; Otherwise we enter a dead loop.
+ // However, this shouldn't happen and should be fixed once reached.
+ if (offset_ == initial_offset) {
+ NOTREACHED();
+ offset_ = end_offset_;
+ return;
+ }
+ }
+}
+
+bool TextIteratorTextNodeHandler::ShouldHandleFirstLetter(
+ const LayoutText& layout_text) const {
+ if (handled_first_letter_)
+ return false;
+ if (!layout_text.IsTextFragment())
+ return false;
+ const LayoutTextFragment& text_fragment = ToLayoutTextFragment(layout_text);
+ return offset_ < text_fragment.TextStartOffset();
+}
+
+static bool HasVisibleTextNode(const LayoutText* layout_object) {
+ if (layout_object->Style()->Visibility() == EVisibility::kVisible)
+ return true;
+
+ if (!layout_object->IsTextFragment())
+ return false;
+
+ const LayoutTextFragment* fragment = ToLayoutTextFragment(layout_object);
+ if (!fragment->IsRemainingTextLayoutObject())
+ return false;
+
+ DCHECK(fragment->GetFirstLetterPseudoElement());
+ LayoutObject* pseudo_element_layout_object =
+ fragment->GetFirstLetterPseudoElement()->GetLayoutObject();
+ return pseudo_element_layout_object &&
+ pseudo_element_layout_object->Style()->Visibility() ==
+ EVisibility::kVisible;
+}
+
+void TextIteratorTextNodeHandler::HandlePreFormattedTextNode() {
+ // TODO(xiaochengh): Get rid of repeated computation of these fields.
+ LayoutText* const layout_object = text_node_->GetLayoutObject();
+ const String str = layout_object->GetText();
+
+ needs_handle_pre_formatted_text_node_ = false;
+
+ if (last_text_node_ended_with_collapsed_space_ &&
+ HasVisibleTextNode(layout_object)) {
+ if (!behavior_.CollapseTrailingSpace() ||
+ (offset_ > 0 && str[offset_ - 1] == ' ')) {
+ SpliceBuffer(kSpaceCharacter, text_node_, nullptr, offset_, offset_);
+ needs_handle_pre_formatted_text_node_ = true;
+ return;
+ }
+ }
+ if (ShouldHandleFirstLetter(*layout_object)) {
+ HandleTextNodeFirstLetter(ToLayoutTextFragment(layout_object));
+ if (first_letter_text_) {
+ const String first_letter = first_letter_text_->GetText();
+ const unsigned run_start = offset_;
+ const bool stops_in_first_letter = end_offset_ <= first_letter.length();
+ const unsigned run_end =
+ stops_in_first_letter ? end_offset_ : first_letter.length();
+ EmitText(text_node_, first_letter_text_, run_start, run_end);
+ first_letter_text_ = nullptr;
+ text_box_ = nullptr;
+ offset_ = run_end;
+ if (!stops_in_first_letter)
+ needs_handle_pre_formatted_text_node_ = true;
+ return;
+ }
+ // We are here only if the DOM and/or layout trees are broken.
+ // For robustness, we should stop processing this node.
+ NOTREACHED();
+ return;
+ }
+ if (layout_object->Style()->Visibility() != EVisibility::kVisible &&
+ !IgnoresStyleVisibility())
+ return;
+ DCHECK_GE(offset_, layout_object->TextStartOffset());
+ const unsigned run_start = offset_ - layout_object->TextStartOffset();
+ const unsigned str_length = str.length();
+ const unsigned end = end_offset_ - layout_object->TextStartOffset();
+ const unsigned run_end = std::min(str_length, end);
+
+ if (run_start >= run_end)
+ return;
+
+ EmitText(text_node_, text_node_->GetLayoutObject(), run_start, run_end);
+}
+
+void TextIteratorTextNodeHandler::HandleTextNodeInRange(const Text* node,
+ unsigned start_offset,
+ unsigned end_offset) {
+ DCHECK(node);
+
+ // TODO(editing-dev): Stop passing in |start_offset == end_offset|.
+ DCHECK_LE(start_offset, end_offset);
+
+ text_node_ = node;
+ offset_ = start_offset;
+ end_offset_ = end_offset;
+ handled_first_letter_ = false;
+ first_letter_text_ = nullptr;
+ uses_layout_ng_ = false;
+
+ if (NGOffsetMapping::GetFor(Position(node, offset_))) {
+ // Restore end offset from magic value.
+ if (end_offset_ == kMaxOffset)
+ end_offset_ = node->data().length();
+ uses_layout_ng_ = true;
+ HandleTextNodeWithLayoutNG();
+ return;
+ }
+
+ LayoutText* layout_object = text_node_->GetLayoutObject();
+ String str = layout_object->GetText();
+
+ // Restore end offset from magic value.
+ if (end_offset_ == kMaxOffset)
+ end_offset_ = layout_object->TextStartOffset() + str.length();
+
+ // handle pre-formatted text
+ if (!layout_object->Style()->CollapseWhiteSpace()) {
+ HandlePreFormattedTextNode();
+ return;
+ }
+
+ if (layout_object->FirstTextBox())
+ text_box_ = layout_object->FirstTextBox();
+
+ const bool should_handle_first_letter =
+ ShouldHandleFirstLetter(*layout_object);
+ if (should_handle_first_letter)
+ HandleTextNodeFirstLetter(ToLayoutTextFragment(layout_object));
+
+ if (!layout_object->FirstTextBox() && str.length() > 0 &&
+ !should_handle_first_letter) {
+ if (layout_object->Style()->Visibility() == EVisibility::kVisible ||
+ IgnoresStyleVisibility()) {
+ last_text_node_ended_with_collapsed_space_ =
+ true; // entire block is collapsed space
+ }
+ return;
+ }
+
+ if (first_letter_text_)
+ layout_object = first_letter_text_;
+
+ // Used when text boxes are out of order (Hebrew/Arabic w/ embeded LTR text)
+ if (layout_object->ContainsReversedText()) {
+ sorted_text_boxes_.clear();
+ for (InlineTextBox* text_box : layout_object->TextBoxes()) {
+ sorted_text_boxes_.push_back(text_box);
+ }
+ std::sort(sorted_text_boxes_.begin(), sorted_text_boxes_.end(),
+ InlineTextBox::CompareByStart);
+ sorted_text_boxes_position_ = 0;
+ text_box_ = sorted_text_boxes_.IsEmpty() ? 0 : sorted_text_boxes_[0];
+ }
+
+ HandleTextBox();
+}
+
+void TextIteratorTextNodeHandler::HandleTextNodeStartFrom(
+ const Text* node,
+ unsigned start_offset) {
+ HandleTextNodeInRange(node, start_offset, kMaxOffset);
+}
+
+void TextIteratorTextNodeHandler::HandleTextNodeEndAt(const Text* node,
+ unsigned end_offset) {
+ HandleTextNodeInRange(node, 0, end_offset);
+}
+
+void TextIteratorTextNodeHandler::HandleTextNodeWhole(const Text* node) {
+ HandleTextNodeStartFrom(node, 0);
+}
+
+// Restore the collapsed space for copy & paste. See http://crbug.com/318925
+size_t TextIteratorTextNodeHandler::RestoreCollapsedTrailingSpace(
+ InlineTextBox* next_text_box,
+ size_t subrun_end) {
+ if (next_text_box || !text_box_->Root().NextRootBox() ||
+ text_box_->Root().LastChild() != text_box_)
+ return subrun_end;
+
+ const LayoutText* layout_object =
+ first_letter_text_ ? first_letter_text_ : text_node_->GetLayoutObject();
+ const String& text = layout_object->GetText();
+ if (text.EndsWith(' ') == 0 || subrun_end != text.length() - 1 ||
+ text[subrun_end - 1] == ' ')
+ return subrun_end;
+
+ // If there is the leading space in the next line, we don't need to restore
+ // the trailing space.
+ // Example: <div style="width: 2em;"><b><i>foo </i></b> bar</div>
+ InlineBox* first_box_of_next_line =
+ text_box_->Root().NextRootBox()->FirstChild();
+ if (!first_box_of_next_line)
+ return subrun_end + 1;
+ Node* first_node_of_next_line =
+ first_box_of_next_line->GetLineLayoutItem().GetNode();
+ if (!first_node_of_next_line ||
+ first_node_of_next_line->nodeValue()[0] != ' ')
+ return subrun_end + 1;
+
+ return subrun_end;
+}
+
+void TextIteratorTextNodeHandler::HandleTextBox() {
+ LayoutText* layout_object =
+ first_letter_text_ ? first_letter_text_ : text_node_->GetLayoutObject();
+ const unsigned text_start_offset = layout_object->TextStartOffset();
+
+ if (layout_object->Style()->Visibility() != EVisibility::kVisible &&
+ !IgnoresStyleVisibility()) {
+ text_box_ = nullptr;
+ } else {
+ String str = layout_object->GetText();
+ // Start and end offsets in |str|, i.e., str[start..end - 1] should be
+ // emitted (after handling whitespace collapsing).
+ const unsigned start = offset_ - layout_object->TextStartOffset();
+ const unsigned end = end_offset_ - text_start_offset;
+ while (text_box_) {
+ const unsigned text_box_start = text_box_->Start();
+ const unsigned run_start = std::max(text_box_start, start);
+
+ // Check for collapsed space at the start of this run.
+ InlineTextBox* first_text_box =
+ layout_object->ContainsReversedText()
+ ? (sorted_text_boxes_.IsEmpty() ? 0 : sorted_text_boxes_[0])
+ : layout_object->FirstTextBox();
+ const bool need_space = last_text_node_ended_with_collapsed_space_ ||
+ (text_box_ == first_text_box &&
+ text_box_start == run_start && run_start > 0);
+ if (need_space &&
+ !layout_object->Style()->IsCollapsibleWhiteSpace(
+ text_state_.LastCharacter()) &&
+ text_state_.LastCharacter()) {
+ if (run_start > 0 && str[run_start - 1] == ' ') {
+ unsigned space_run_start = run_start - 1;
+ while (space_run_start > 0 && str[space_run_start - 1] == ' ')
+ --space_run_start;
+ EmitText(text_node_, layout_object, space_run_start,
+ space_run_start + 1);
+ } else {
+ SpliceBuffer(kSpaceCharacter, text_node_, nullptr, run_start,
+ run_start);
+ }
+ return;
+ }
+ const unsigned text_box_end = text_box_start + text_box_->Len();
+ const unsigned run_end = std::min(text_box_end, end);
+
+ // Determine what the next text box will be, but don't advance yet
+ InlineTextBox* next_text_box = nullptr;
+ if (layout_object->ContainsReversedText()) {
+ if (sorted_text_boxes_position_ + 1 < sorted_text_boxes_.size())
+ next_text_box = sorted_text_boxes_[sorted_text_boxes_position_ + 1];
+ } else {
+ next_text_box = text_box_->NextForSameLayoutObject();
+ }
+
+ // FIXME: Based on the outcome of crbug.com/446502 it's possible we can
+ // remove this block. The reason we new it now is because BIDI and
+ // FirstLetter seem to have different ideas of where things can split.
+ // FirstLetter takes the punctuation + first letter, and BIDI will
+ // split out the punctuation and possibly reorder it.
+ if (next_text_box &&
+ !(next_text_box->GetLineLayoutItem().IsEqual(layout_object))) {
+ text_box_ = nullptr;
+ return;
+ }
+ DCHECK(!next_text_box ||
+ next_text_box->GetLineLayoutItem().IsEqual(layout_object));
+
+ if (run_start < run_end) {
+ // Handle either a single newline character (which becomes a space),
+ // or a run of characters that does not include a newline.
+ // This effectively translates newlines to spaces without copying the
+ // text.
+ if (str[run_start] == '\n') {
+ // We need to preserve new lines in case of PreLine.
+ // See bug crbug.com/317365.
+ if (layout_object->Style()->WhiteSpace() == EWhiteSpace::kPreLine) {
+ SpliceBuffer('\n', text_node_, nullptr, run_start, run_start);
+ } else {
+ SpliceBuffer(kSpaceCharacter, text_node_, nullptr, run_start,
+ run_start + 1);
+ }
+ offset_ = text_start_offset + run_start + 1;
+ } else {
+ size_t subrun_end = str.find('\n', run_start);
+ if (subrun_end == kNotFound || subrun_end > run_end) {
+ subrun_end = run_end;
+ subrun_end =
+ RestoreCollapsedTrailingSpace(next_text_box, subrun_end);
+ }
+
+ offset_ = text_start_offset + subrun_end;
+ EmitText(text_node_, layout_object, run_start, subrun_end);
+ }
+
+ // If we are doing a subrun that doesn't go to the end of the text box,
+ // come back again to finish handling this text box; don't advance to
+ // the next one.
+ if (text_state_.PositionEndOffset() < text_box_end + text_start_offset)
+ return;
+
+ if (behavior_.DoesNotEmitSpaceBeyondRangeEnd()) {
+ // If the subrun went to the text box end and this end is also the end
+ // of the range, do not advance to the next text box and do not
+ // generate a space, just stop.
+ if (text_box_end == end) {
+ text_box_ = nullptr;
+ return;
+ }
+ }
+
+ // Advance and return
+ const unsigned next_run_start =
+ next_text_box ? next_text_box->Start() : str.length();
+ if (next_run_start > run_end) {
+ last_text_node_ended_with_collapsed_space_ =
+ true; // collapsed space between runs or at the end
+ }
+
+ text_box_ = next_text_box;
+ if (layout_object->ContainsReversedText())
+ ++sorted_text_boxes_position_;
+ return;
+ }
+ // Advance and continue
+ if (run_start == run_end && run_end == end) {
+ // "<p>^ |(1) foo</p>" with ::first-letter reaches here.
+ // Where "^" is start of range and "|" is end of range.
+ offset_ = end_offset_;
+ return;
+ }
+ text_box_ = next_text_box;
+ if (layout_object->ContainsReversedText())
+ ++sorted_text_boxes_position_;
+ }
+ }
+
+ if (ShouldProceedToRemainingText()) {
+ ProceedToRemainingText();
+ HandleTextBox();
+ }
+}
+
+bool TextIteratorTextNodeHandler::ShouldProceedToRemainingText() const {
+ if (text_box_ || !remaining_text_box_)
+ return false;
+ return offset_ < end_offset_;
+}
+
+void TextIteratorTextNodeHandler::ProceedToRemainingText() {
+ text_box_ = remaining_text_box_;
+ remaining_text_box_ = nullptr;
+ first_letter_text_ = nullptr;
+ offset_ = text_node_->GetLayoutObject()->TextStartOffset();
+}
+
+void TextIteratorTextNodeHandler::HandleTextNodeFirstLetter(
+ LayoutTextFragment* layout_object) {
+ handled_first_letter_ = true;
+
+ if (!layout_object->IsRemainingTextLayoutObject())
+ return;
+
+ FirstLetterPseudoElement* first_letter_element =
+ layout_object->GetFirstLetterPseudoElement();
+ if (!first_letter_element)
+ return;
+
+ LayoutObject* pseudo_layout_object = first_letter_element->GetLayoutObject();
+ if (pseudo_layout_object->Style()->Visibility() != EVisibility::kVisible &&
+ !IgnoresStyleVisibility())
+ return;
+
+ LayoutObject* first_letter = pseudo_layout_object->SlowFirstChild();
+
+ sorted_text_boxes_.clear();
+ remaining_text_box_ = text_box_;
+ CHECK(first_letter && first_letter->IsText());
+ first_letter_text_ = ToLayoutText(first_letter);
+ text_box_ = first_letter_text_->FirstTextBox();
+}
+
+bool TextIteratorTextNodeHandler::FixLeadingWhiteSpaceForReplacedElement(
+ const Node* parent) {
+ // This is a hacky way for white space fixup in legacy layout. With LayoutNG,
+ // we can get rid of this function.
+ if (uses_layout_ng_)
+ return false;
+
+ if (behavior_.CollapseTrailingSpace()) {
+ if (text_node_) {
+ String str = text_node_->GetLayoutObject()->GetText();
+ if (last_text_node_ended_with_collapsed_space_ && offset_ > 0 &&
+ str[offset_ - 1] == ' ') {
+ SpliceBuffer(kSpaceCharacter, parent, text_node_, 1, 1);
+ return true;
+ }
+ }
+ } else if (last_text_node_ended_with_collapsed_space_) {
+ SpliceBuffer(kSpaceCharacter, parent, text_node_, 1, 1);
+ return true;
+ }
+
+ return false;
+}
+
+void TextIteratorTextNodeHandler::ResetCollapsedWhiteSpaceFixup() {
+ // This is a hacky way for white space fixup in legacy layout. With LayoutNG,
+ // we can get rid of this function.
+ last_text_node_ended_with_collapsed_space_ = false;
+}
+
+void TextIteratorTextNodeHandler::SpliceBuffer(UChar c,
+ const Node* text_node,
+ const Node* offset_base_node,
+ unsigned text_start_offset,
+ unsigned text_end_offset) {
+ text_state_.SpliceBuffer(c, text_node, offset_base_node, text_start_offset,
+ text_end_offset);
+ ResetCollapsedWhiteSpaceFixup();
+}
+
+void TextIteratorTextNodeHandler::EmitText(const Node* text_node,
+ const LayoutText* layout_object,
+ unsigned text_start_offset,
+ unsigned text_end_offset) {
+ String string = behavior_.EmitsOriginalText() ? layout_object->OriginalText()
+ : layout_object->GetText();
+ if (behavior_.EmitsSpaceForNbsp())
+ string.Replace(kNoBreakSpaceCharacter, kSpaceCharacter);
+ text_state_.EmitText(text_node,
+ text_start_offset + layout_object->TextStartOffset(),
+ text_end_offset + layout_object->TextStartOffset(),
+ string, text_start_offset, text_end_offset);
+ ResetCollapsedWhiteSpaceFixup();
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_text_node_handler.h b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_text_node_handler.h
new file mode 100644
index 00000000000..8a8c4fb7b9e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_text_node_handler.h
@@ -0,0 +1,114 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_ITERATOR_TEXT_NODE_HANDLER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_ITERATOR_TEXT_NODE_HANDLER_H_
+
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator_behavior.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/wtf/vector.h"
+
+namespace blink {
+
+class InlineTextBox;
+class LayoutText;
+class LayoutTextFragment;
+class TextIteratorTextState;
+
+// TextIteratorTextNodeHandler extracts plain text from a text node by calling
+// HandleTextNode() function. It should be used only by TextIterator.
+class TextIteratorTextNodeHandler {
+ STACK_ALLOCATED();
+
+ public:
+ TextIteratorTextNodeHandler(const TextIteratorBehavior&,
+ TextIteratorTextState*);
+
+ const Text* GetNode() const { return text_node_; }
+
+ // Returns true if more text is emitted without traversing to the next node.
+ bool HandleRemainingTextRuns();
+
+ // Returns true if a leading white space is emitted before a replaced element.
+ bool FixLeadingWhiteSpaceForReplacedElement(const Node*);
+
+ void ResetCollapsedWhiteSpaceFixup();
+
+ // Emit plain text from the given text node.
+ void HandleTextNodeWhole(const Text*);
+
+ // Variants that emit plain text within the given DOM offset range.
+ void HandleTextNodeStartFrom(const Text*, unsigned start_offset);
+ void HandleTextNodeEndAt(const Text*, unsigned end_offset);
+ void HandleTextNodeInRange(const Text*,
+ unsigned start_offset,
+ unsigned end_offset);
+
+ private:
+ void HandlePreFormattedTextNode();
+ void HandleTextBox();
+ void HandleTextNodeFirstLetter(LayoutTextFragment*);
+ bool ShouldHandleFirstLetter(const LayoutText&) const;
+ bool ShouldProceedToRemainingText() const;
+ void ProceedToRemainingText();
+ size_t RestoreCollapsedTrailingSpace(InlineTextBox* next_text_box,
+ size_t subrun_end);
+
+ void HandleTextNodeWithLayoutNG();
+
+ // Used when the visibility of the style should not affect text gathering.
+ bool IgnoresStyleVisibility() const {
+ return behavior_.IgnoresStyleVisibility();
+ }
+
+ void SpliceBuffer(UChar,
+ const Node* text_node,
+ const Node* offset_base_node,
+ unsigned text_start_offset,
+ unsigned text_end_offset);
+ void EmitText(const Node* text_node,
+ const LayoutText* layout_object,
+ unsigned text_start_offset,
+ unsigned text_end_offset);
+
+ // The current text node and offset range, from which text should be emitted.
+ Member<const Text> text_node_;
+ unsigned offset_ = 0;
+ unsigned end_offset_ = 0;
+
+ // Indicates if the text node is laid out with LayoutNG.
+ bool uses_layout_ng_ = false;
+
+ InlineTextBox* text_box_ = nullptr;
+
+ // Remember if we are in the middle of handling a pre-formatted text node.
+ bool needs_handle_pre_formatted_text_node_ = false;
+ // Used when deciding text fragment created by :first-letter should be looked
+ // into.
+ bool handled_first_letter_ = false;
+ // Used when iteration over :first-letter text to save pointer to
+ // remaining text box.
+ InlineTextBox* remaining_text_box_ = nullptr;
+ // Used to point to LayoutText object for :first-letter.
+ LayoutText* first_letter_text_ = nullptr;
+
+ // Used to do the whitespace collapsing logic.
+ bool last_text_node_ended_with_collapsed_space_ = false;
+
+ // Used when text boxes are out of order (Hebrew/Arabic w/ embeded LTR text)
+ Vector<InlineTextBox*> sorted_text_boxes_;
+ size_t sorted_text_boxes_position_ = 0;
+
+ const TextIteratorBehavior behavior_;
+
+ // Contains state of emitted text.
+ TextIteratorTextState& text_state_;
+
+ DISALLOW_COPY_AND_ASSIGN(TextIteratorTextNodeHandler);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_ITERATOR_TEXT_NODE_HANDLER_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_text_state.cc b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_text_state.cc
new file mode 100644
index 00000000000..a0fc9ec5d2c
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_text_state.cc
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012 Apple Inc. All
+ * rights reserved.
+ * Copyright (C) 2005 Alexey Proskuryakov.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator_text_state.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/iterators/backwards_text_buffer.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
+
+namespace blink {
+
+namespace {
+
+bool IsTextSecurityNode(const Node& node) {
+ return node.GetLayoutObject() &&
+ node.GetLayoutObject()->Style()->TextSecurity() !=
+ ETextSecurity::kNone;
+}
+
+} // anonymous namespace
+
+TextIteratorTextState::TextIteratorTextState(
+ const TextIteratorBehavior& behavior)
+ : behavior_(behavior) {}
+
+UChar TextIteratorTextState::CharacterAt(unsigned index) const {
+ SECURITY_DCHECK(index < length());
+ if (!(index < length()))
+ return 0;
+
+ if (single_character_buffer_) {
+ DCHECK_EQ(index, 0u);
+ DCHECK_EQ(length(), 1u);
+ return single_character_buffer_;
+ }
+
+ return text_[text_start_offset_ + index];
+}
+
+String TextIteratorTextState::Substring(unsigned position,
+ unsigned length) const {
+ SECURITY_DCHECK(position <= this->length());
+ SECURITY_DCHECK(position + length <= this->length());
+ if (!length)
+ return g_empty_string;
+ if (single_character_buffer_) {
+ DCHECK_EQ(position, 0u);
+ DCHECK_EQ(length, 1u);
+ return String(&single_character_buffer_, 1);
+ }
+ return text_.Substring(text_start_offset_ + position, length);
+}
+
+void TextIteratorTextState::AppendTextToStringBuilder(
+ StringBuilder& builder,
+ unsigned position,
+ unsigned max_length) const {
+ SECURITY_DCHECK(position <= this->length());
+ unsigned length_to_append = std::min(length() - position, max_length);
+ if (!length_to_append)
+ return;
+ if (single_character_buffer_) {
+ DCHECK_EQ(position, 0u);
+ builder.Append(single_character_buffer_);
+ } else {
+ builder.Append(text_, text_start_offset_ + position, length_to_append);
+ }
+}
+
+void TextIteratorTextState::UpdateForReplacedElement(const Node* base_node) {
+ has_emitted_ = true;
+ position_node_ = base_node->parentNode();
+ position_offset_base_node_ = base_node;
+ position_start_offset_ = 0;
+ position_end_offset_ = 1;
+ single_character_buffer_ = 0;
+
+ text_length_ = 0;
+ text_start_offset_ = 0;
+ last_character_ = 0;
+}
+
+void TextIteratorTextState::EmitAltText(const Node* node) {
+ text_ = ToHTMLElement(node)->AltText();
+ text_start_offset_ = 0;
+ text_length_ = text_.length();
+ last_character_ = text_length_ ? text_[text_length_ - 1] : 0;
+}
+
+void TextIteratorTextState::FlushPositionOffsets() const {
+ if (!position_offset_base_node_)
+ return;
+ unsigned index = position_offset_base_node_->NodeIndex();
+ position_start_offset_ += index;
+ position_end_offset_ += index;
+ position_offset_base_node_ = nullptr;
+}
+
+void TextIteratorTextState::SpliceBuffer(UChar c,
+ const Node* text_node,
+ const Node* offset_base_node,
+ unsigned text_start_offset,
+ unsigned text_end_offset) {
+ DCHECK(text_node);
+ has_emitted_ = true;
+
+ // Remember information with which to construct the TextIterator::range().
+ // NOTE: textNode is often not a text node, so the range will specify child
+ // nodes of positionNode
+ position_node_ = text_node;
+ position_offset_base_node_ = offset_base_node;
+ position_start_offset_ = text_start_offset;
+ position_end_offset_ = text_end_offset;
+
+ // remember information with which to construct the TextIterator::characters()
+ // and length()
+ single_character_buffer_ = c;
+ DCHECK(single_character_buffer_);
+ text_length_ = 1;
+ text_start_offset_ = 0;
+
+ // remember some iteration state
+ last_character_ = c;
+}
+
+void TextIteratorTextState::EmitText(const Node* text_node,
+ unsigned position_start_offset,
+ unsigned position_end_offset,
+ const String& string,
+ unsigned text_start_offset,
+ unsigned text_end_offset) {
+ DCHECK(text_node);
+ text_ =
+ behavior_.EmitsSmallXForTextSecurity() && IsTextSecurityNode(*text_node)
+ ? RepeatString("x", string.length())
+ : string,
+
+ DCHECK(!text_.IsEmpty());
+ DCHECK_LT(text_start_offset, text_.length());
+ DCHECK_LE(text_end_offset, text_.length());
+ DCHECK_LE(text_start_offset, text_end_offset);
+
+ position_node_ = text_node;
+ position_offset_base_node_ = nullptr;
+ position_start_offset_ = position_start_offset;
+ position_end_offset_ = position_end_offset;
+ single_character_buffer_ = 0;
+ text_start_offset_ = text_start_offset;
+ text_length_ = text_end_offset - text_start_offset;
+ last_character_ = text_[text_end_offset - 1];
+
+ has_emitted_ = true;
+}
+
+void TextIteratorTextState::AppendTextTo(ForwardsTextBuffer* output,
+ unsigned position,
+ unsigned length_to_append) const {
+ SECURITY_DCHECK(position + length_to_append <= length());
+ // Make sure there's no integer overflow.
+ SECURITY_DCHECK(position + length_to_append >= position);
+ if (!length_to_append)
+ return;
+ DCHECK(output);
+ if (single_character_buffer_) {
+ DCHECK_EQ(position, 0u);
+ DCHECK_EQ(length(), 1u);
+ output->PushCharacters(single_character_buffer_, 1);
+ return;
+ }
+ unsigned offset = text_start_offset_ + position;
+ // Any failure is a security bug (buffer overflow) and must be captured.
+ CHECK_LE(offset, text_.length());
+ CHECK_LE(offset + length_to_append, text_.length());
+ if (text_.Is8Bit())
+ output->PushRange(text_.Characters8() + offset, length_to_append);
+ else
+ output->PushRange(text_.Characters16() + offset, length_to_append);
+}
+
+void TextIteratorTextState::PrependTextTo(BackwardsTextBuffer* output,
+ unsigned position,
+ unsigned length_to_prepend) const {
+ SECURITY_DCHECK(position + length_to_prepend <= length());
+ // Make sure there's no integer overflow.
+ SECURITY_DCHECK(position + length_to_prepend >= position);
+ if (!length_to_prepend)
+ return;
+ DCHECK(output);
+ if (single_character_buffer_) {
+ DCHECK_EQ(position, 0u);
+ DCHECK_EQ(length(), 1u);
+ output->PushCharacters(single_character_buffer_, 1);
+ return;
+ }
+ const unsigned offset =
+ text_start_offset_ + length() - position - length_to_prepend;
+ // Any failure is a security bug (buffer overflow) and must be captured.
+ CHECK_LE(offset, text_.length());
+ CHECK_LE(offset + length_to_prepend, text_.length());
+ if (text_.Is8Bit())
+ output->PushRange(text_.Characters8() + offset, length_to_prepend);
+ else
+ output->PushRange(text_.Characters16() + offset, length_to_prepend);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_text_state.h b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_text_state.h
new file mode 100644
index 00000000000..c097f709d86
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/text_iterator_text_state.h
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2004, 2006, 2009 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_ITERATOR_TEXT_STATE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_ITERATOR_TEXT_STATE_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/iterators/forwards_text_buffer.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator_behavior.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+
+namespace blink {
+
+class BackwardsTextBuffer;
+
+class CORE_EXPORT TextIteratorTextState {
+ STACK_ALLOCATED();
+
+ public:
+ explicit TextIteratorTextState(const TextIteratorBehavior&);
+
+ // Return properties of the current text.
+ unsigned length() const { return text_length_; }
+ UChar CharacterAt(unsigned index) const;
+ String Substring(unsigned position, unsigned length) const;
+ void AppendTextToStringBuilder(StringBuilder&,
+ unsigned position = 0,
+ unsigned max_length = UINT_MAX) const;
+ void AppendTextTo(ForwardsTextBuffer* output,
+ unsigned position,
+ unsigned length_to_append) const;
+ void PrependTextTo(BackwardsTextBuffer* output,
+ unsigned position,
+ unsigned length_to_prepend) const;
+
+ void SpliceBuffer(UChar,
+ const Node* text_node,
+ const Node* offset_base_node,
+ unsigned text_start_offset,
+ unsigned text_end_offset);
+ void EmitText(const Node*,
+ unsigned position_start_offset,
+ unsigned position_end_offset,
+ const String&,
+ unsigned text_start_offset,
+ unsigned text_end_offset);
+ void EmitAltText(const Node*);
+ void UpdateForReplacedElement(const Node* base_node);
+
+ // Return position of the current text.
+ void FlushPositionOffsets() const;
+ unsigned PositionStartOffset() const { return position_start_offset_; }
+ unsigned PositionEndOffset() const { return position_end_offset_; }
+ const Node* PositionNode() const { return position_node_; }
+
+ bool HasEmitted() const { return has_emitted_; }
+ UChar LastCharacter() const { return last_character_; }
+ void ResetRunInformation() {
+ position_node_ = nullptr;
+ text_length_ = 0;
+ }
+
+ private:
+ const TextIteratorBehavior behavior_;
+ unsigned text_length_ = 0;
+
+ // Used for whitespace characters that aren't in the DOM, so we can point at
+ // them.
+ // If non-zero, overrides |text_|.
+ UChar single_character_buffer_ = 0;
+
+ // The current text when |single_character_buffer_| is zero, in which case it
+ // is |text_.Substring(text_start_offset_, text_length_)|.
+ String text_;
+ unsigned text_start_offset_ = 0;
+
+ // Position of the current text, in the form to be returned from the iterator.
+ Member<const Node> position_node_;
+ mutable Member<const Node> position_offset_base_node_;
+ mutable unsigned position_start_offset_ = 0;
+ mutable unsigned position_end_offset_ = 0;
+
+ // Used when deciding whether to emit a "positioning" (e.g. newline) before
+ // any other content
+ bool has_emitted_ = false;
+ UChar last_character_ = 0;
+
+ DISALLOW_COPY_AND_ASSIGN(TextIteratorTextState);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_ITERATOR_TEXT_STATE_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/text_searcher_icu.cc b/chromium/third_party/blink/renderer/core/editing/iterators/text_searcher_icu.cc
new file mode 100644
index 00000000000..0f5737c381a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/text_searcher_icu.cc
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012 Apple Inc. All
+ * rights reserved.
+ * Copyright (C) 2005 Alexey Proskuryakov.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/iterators/text_searcher_icu.h"
+
+#include <unicode/usearch.h>
+#include "base/macros.h"
+#include "third_party/blink/renderer/platform/text/text_break_iterator_internal_icu.h"
+#include "third_party/blink/renderer/platform/wtf/text/character_names.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+
+namespace blink {
+
+namespace {
+
+UStringSearch* CreateSearcher() {
+ // Provide a non-empty pattern and non-empty text so usearch_open will not
+ // fail, but it doesn't matter exactly what it is, since we don't perform any
+ // searches without setting both the pattern and the text.
+ UErrorCode status = U_ZERO_ERROR;
+ String search_collator_name =
+ CurrentSearchLocaleID() + String("@collation=search");
+ UStringSearch* searcher =
+ usearch_open(&kNewlineCharacter, 1, &kNewlineCharacter, 1,
+ search_collator_name.Utf8().data(), nullptr, &status);
+ DCHECK(status == U_ZERO_ERROR || status == U_USING_FALLBACK_WARNING ||
+ status == U_USING_DEFAULT_WARNING)
+ << status;
+ return searcher;
+}
+
+class ICULockableSearcher {
+ public:
+ static UStringSearch* AcquireSearcher() {
+ Instance().lock();
+ return Instance().searcher_;
+ }
+
+ static void ReleaseSearcher() { Instance().unlock(); }
+
+ private:
+ static ICULockableSearcher& Instance() {
+ static ICULockableSearcher searcher(CreateSearcher());
+ return searcher;
+ }
+
+ explicit ICULockableSearcher(UStringSearch* searcher) : searcher_(searcher) {}
+
+ void lock() {
+#if DCHECK_IS_ON()
+ DCHECK(!locked_);
+ locked_ = true;
+#endif
+ }
+
+ void unlock() {
+#if DCHECK_IS_ON()
+ DCHECK(locked_);
+ locked_ = false;
+#endif
+ }
+
+ UStringSearch* const searcher_ = nullptr;
+
+#if DCHECK_IS_ON()
+ bool locked_ = false;
+#endif
+
+ DISALLOW_COPY_AND_ASSIGN(ICULockableSearcher);
+};
+
+} // namespace
+
+// Grab the single global searcher.
+// If we ever have a reason to do more than once search buffer at once, we'll
+// have to move to multiple searchers.
+TextSearcherICU::TextSearcherICU()
+ : searcher_(ICULockableSearcher::AcquireSearcher()) {}
+
+TextSearcherICU::~TextSearcherICU() {
+ // Leave the static object pointing to valid strings (pattern=target,
+ // text=buffer). Otheriwse, usearch_reset() will results in 'use-after-free'
+ // error.
+ SetPattern(&kNewlineCharacter, 1);
+ SetText(&kNewlineCharacter, 1);
+ ICULockableSearcher::ReleaseSearcher();
+}
+
+void TextSearcherICU::SetPattern(const StringView& pattern,
+ bool case_sensitive) {
+ SetCaseSensitivity(case_sensitive);
+ SetPattern(pattern.Characters16(), pattern.length());
+}
+
+void TextSearcherICU::SetText(const UChar* text, size_t length) {
+ UErrorCode status = U_ZERO_ERROR;
+ usearch_setText(searcher_, text, length, &status);
+ DCHECK_EQ(status, U_ZERO_ERROR);
+ text_length_ = length;
+}
+
+void TextSearcherICU::SetOffset(size_t offset) {
+ UErrorCode status = U_ZERO_ERROR;
+ usearch_setOffset(searcher_, offset, &status);
+ DCHECK_EQ(status, U_ZERO_ERROR);
+}
+
+bool TextSearcherICU::NextMatchResult(MatchResultICU& result) {
+ UErrorCode status = U_ZERO_ERROR;
+ const int match_start = usearch_next(searcher_, &status);
+ DCHECK_EQ(status, U_ZERO_ERROR);
+
+ // TODO(iceman): It is possible to use |usearch_getText| function
+ // to retrieve text length and not store it explicitly.
+ if (!(match_start >= 0 && static_cast<size_t>(match_start) < text_length_)) {
+ DCHECK_EQ(match_start, USEARCH_DONE);
+ result.start = 0;
+ result.length = 0;
+ return false;
+ }
+
+ result.start = static_cast<size_t>(match_start);
+ result.length = usearch_getMatchedLength(searcher_);
+ return true;
+}
+
+void TextSearcherICU::SetPattern(const UChar* pattern, size_t length) {
+ UErrorCode status = U_ZERO_ERROR;
+ usearch_setPattern(searcher_, pattern, length, &status);
+ DCHECK_EQ(status, U_ZERO_ERROR);
+}
+
+void TextSearcherICU::SetCaseSensitivity(bool case_sensitive) {
+ const UCollationStrength strength =
+ case_sensitive ? UCOL_TERTIARY : UCOL_PRIMARY;
+
+ UCollator* const collator = usearch_getCollator(searcher_);
+ if (ucol_getStrength(collator) == strength)
+ return;
+
+ ucol_setStrength(collator, strength);
+ usearch_reset(searcher_);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/text_searcher_icu.h b/chromium/third_party/blink/renderer/core/editing/iterators/text_searcher_icu.h
new file mode 100644
index 00000000000..ab47ddc8849
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/text_searcher_icu.h
@@ -0,0 +1,44 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_SEARCHER_ICU_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_SEARCHER_ICU_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_view.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+
+struct UStringSearch;
+
+namespace blink {
+
+struct CORE_EXPORT MatchResultICU {
+ size_t start;
+ size_t length;
+};
+
+class CORE_EXPORT TextSearcherICU {
+ public:
+ TextSearcherICU();
+ ~TextSearcherICU();
+
+ void SetPattern(const StringView& pattern, bool sensitive);
+ void SetText(const UChar* text, size_t length);
+ void SetOffset(size_t);
+ bool NextMatchResult(MatchResultICU&);
+
+ private:
+ void SetPattern(const UChar* pattern, size_t length);
+ void SetCaseSensitivity(bool case_sensitive);
+
+ UStringSearch* searcher_ = nullptr;
+ size_t text_length_ = 0;
+
+ DISALLOW_COPY_AND_ASSIGN(TextSearcherICU);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_ITERATORS_TEXT_SEARCHER_ICU_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/iterators/text_searcher_icu_test.cc b/chromium/third_party/blink/renderer/core/editing/iterators/text_searcher_icu_test.cc
new file mode 100644
index 00000000000..8041b0f22aa
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/iterators/text_searcher_icu_test.cc
@@ -0,0 +1,101 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/iterators/text_searcher_icu.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_view.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+
+namespace blink {
+
+namespace {
+
+String MakeUTF16(const char* str) {
+ String utf16_string = String::FromUTF8(str);
+ utf16_string.Ensure16Bit();
+ return utf16_string;
+}
+
+} // namespace
+
+TEST(TextSearcherICUTest, FindSubstring) {
+ TextSearcherICU searcher;
+ const String& pattern = MakeUTF16("substring");
+ searcher.SetPattern(pattern, true);
+
+ const String& text = MakeUTF16("Long text with substring content.");
+ searcher.SetText(text.Characters16(), text.length());
+
+ MatchResultICU result;
+
+ EXPECT_TRUE(searcher.NextMatchResult(result));
+ EXPECT_NE(0u, result.start);
+ EXPECT_NE(0u, result.length);
+ ASSERT_LT(result.length, text.length());
+ EXPECT_EQ(pattern, text.Substring(result.start, result.length));
+
+ EXPECT_FALSE(searcher.NextMatchResult(result));
+ EXPECT_EQ(0u, result.start);
+ EXPECT_EQ(0u, result.length);
+}
+
+TEST(TextSearcherICUTest, FindIgnoreCaseSubstring) {
+ TextSearcherICU searcher;
+ const String& pattern = MakeUTF16("substring");
+ searcher.SetPattern(pattern, false);
+
+ const String& text = MakeUTF16("Long text with SubStrinG content.");
+ searcher.SetText(text.Characters16(), text.length());
+
+ MatchResultICU result;
+ EXPECT_TRUE(searcher.NextMatchResult(result));
+ EXPECT_NE(0u, result.start);
+ EXPECT_NE(0u, result.length);
+ ASSERT_LT(result.length, text.length());
+ EXPECT_EQ(pattern,
+ text.Substring(result.start, result.length).DeprecatedLower());
+
+ searcher.SetPattern(pattern, true);
+ searcher.SetOffset(0u);
+ EXPECT_FALSE(searcher.NextMatchResult(result));
+ EXPECT_EQ(0u, result.start);
+ EXPECT_EQ(0u, result.length);
+}
+
+TEST(TextSearcherICUTest, FindSubstringWithOffset) {
+ TextSearcherICU searcher;
+ const String& pattern = MakeUTF16("substring");
+ searcher.SetPattern(pattern, true);
+
+ const String& text =
+ MakeUTF16("Long text with substring content. Second substring");
+ searcher.SetText(text.Characters16(), text.length());
+
+ MatchResultICU first_result;
+
+ EXPECT_TRUE(searcher.NextMatchResult(first_result));
+ EXPECT_NE(0u, first_result.start);
+ EXPECT_NE(0u, first_result.length);
+
+ MatchResultICU second_result;
+ EXPECT_TRUE(searcher.NextMatchResult(second_result));
+ EXPECT_NE(0u, second_result.start);
+ EXPECT_NE(0u, second_result.length);
+
+ searcher.SetOffset(first_result.start + first_result.length);
+
+ MatchResultICU offset_result;
+ EXPECT_TRUE(searcher.NextMatchResult(offset_result));
+ EXPECT_EQ(offset_result.start, second_result.start);
+ EXPECT_EQ(offset_result.length, second_result.length);
+
+ searcher.SetOffset(first_result.start);
+
+ EXPECT_TRUE(searcher.NextMatchResult(offset_result));
+ EXPECT_EQ(offset_result.start, first_result.start);
+ EXPECT_EQ(offset_result.length, first_result.length);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/keyboard_test.cc b/chromium/third_party/blink/renderer/core/editing/keyboard_test.cc
new file mode 100644
index 00000000000..d74988d1558
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/keyboard_test.cc
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/editing_behavior.h"
+
+#include <memory>
+
+#include "build/build_config.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/platform/platform.h"
+#include "third_party/blink/public/platform/web_input_event.h"
+#include "third_party/blink/renderer/core/dom/events/event_target.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/events/keyboard_event.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/platform/keyboard_codes.h"
+
+namespace blink {
+
+class KeyboardTest : public testing::Test {
+ public:
+ // Pass a WebKeyboardEvent into the EditorClient and get back the string
+ // name of which editing event that key causes.
+ // E.g., sending in the enter key gives back "InsertNewline".
+ const char* InterpretKeyEvent(const WebKeyboardEvent& web_keyboard_event) {
+ KeyboardEvent* keyboard_event =
+ KeyboardEvent::Create(web_keyboard_event, nullptr);
+ std::unique_ptr<Settings> settings = Settings::Create();
+ EditingBehavior behavior(settings->GetEditingBehaviorType());
+ return behavior.InterpretKeyEvent(*keyboard_event);
+ }
+
+ WebKeyboardEvent CreateFakeKeyboardEvent(char key_code,
+ int modifiers,
+ WebInputEvent::Type type,
+ const String& key = g_empty_string) {
+ WebKeyboardEvent event(type, modifiers,
+ WebInputEvent::GetStaticTimeStampForTests());
+ event.text[0] = key_code;
+ event.windows_key_code = key_code;
+ event.dom_key = Platform::Current()->DomKeyEnumFromString(key);
+ return event;
+ }
+
+ // Like interpretKeyEvent, but with pressing down OSModifier+|keyCode|.
+ // OSModifier is the platform's standard modifier key: control on most
+ // platforms, but meta (command) on Mac.
+ const char* InterpretOSModifierKeyPress(char key_code) {
+#if defined(OS_MACOSX)
+ WebInputEvent::Modifiers os_modifier = WebInputEvent::kMetaKey;
+#else
+ WebInputEvent::Modifiers os_modifier = WebInputEvent::kControlKey;
+#endif
+ return InterpretKeyEvent(CreateFakeKeyboardEvent(
+ key_code, os_modifier, WebInputEvent::kRawKeyDown));
+ }
+
+ // Like interpretKeyEvent, but with pressing down ctrl+|keyCode|.
+ const char* InterpretCtrlKeyPress(char key_code) {
+ return InterpretKeyEvent(CreateFakeKeyboardEvent(
+ key_code, WebInputEvent::kControlKey, WebInputEvent::kRawKeyDown));
+ }
+
+ // Like interpretKeyEvent, but with typing a tab.
+ const char* InterpretTab(int modifiers) {
+ return InterpretKeyEvent(
+ CreateFakeKeyboardEvent('\t', modifiers, WebInputEvent::kChar));
+ }
+
+ // Like interpretKeyEvent, but with typing a newline.
+ const char* InterpretNewLine(int modifiers) {
+ return InterpretKeyEvent(
+ CreateFakeKeyboardEvent('\r', modifiers, WebInputEvent::kChar));
+ }
+
+ const char* InterpretDomKey(const char* key) {
+ return InterpretKeyEvent(CreateFakeKeyboardEvent(
+ 0, kNoModifiers, WebInputEvent::kRawKeyDown, key));
+ }
+
+ // A name for "no modifiers set".
+ static const int kNoModifiers = 0;
+};
+
+TEST_F(KeyboardTest, TestCtrlReturn) {
+ EXPECT_STREQ("InsertNewline", InterpretCtrlKeyPress(0xD));
+}
+
+TEST_F(KeyboardTest, TestOSModifierZ) {
+#if !defined(OS_MACOSX)
+ EXPECT_STREQ("Undo", InterpretOSModifierKeyPress('Z'));
+#endif
+}
+
+TEST_F(KeyboardTest, TestOSModifierY) {
+#if !defined(OS_MACOSX)
+ EXPECT_STREQ("Redo", InterpretOSModifierKeyPress('Y'));
+#endif
+}
+
+TEST_F(KeyboardTest, TestOSModifierA) {
+#if !defined(OS_MACOSX)
+ EXPECT_STREQ("SelectAll", InterpretOSModifierKeyPress('A'));
+#endif
+}
+
+TEST_F(KeyboardTest, TestOSModifierX) {
+#if !defined(OS_MACOSX)
+ EXPECT_STREQ("Cut", InterpretOSModifierKeyPress('X'));
+#endif
+}
+
+TEST_F(KeyboardTest, TestOSModifierC) {
+#if !defined(OS_MACOSX)
+ EXPECT_STREQ("Copy", InterpretOSModifierKeyPress('C'));
+#endif
+}
+
+TEST_F(KeyboardTest, TestOSModifierV) {
+#if !defined(OS_MACOSX)
+ EXPECT_STREQ("Paste", InterpretOSModifierKeyPress('V'));
+#endif
+}
+
+TEST_F(KeyboardTest, TestEscape) {
+ const char* result = InterpretKeyEvent(CreateFakeKeyboardEvent(
+ VKEY_ESCAPE, kNoModifiers, WebInputEvent::kRawKeyDown));
+ EXPECT_STREQ("Cancel", result);
+}
+
+TEST_F(KeyboardTest, TestInsertTab) {
+ EXPECT_STREQ("InsertTab", InterpretTab(kNoModifiers));
+}
+
+TEST_F(KeyboardTest, TestInsertBackTab) {
+ EXPECT_STREQ("InsertBacktab", InterpretTab(WebInputEvent::kShiftKey));
+}
+
+TEST_F(KeyboardTest, TestInsertNewline) {
+ EXPECT_STREQ("InsertNewline", InterpretNewLine(kNoModifiers));
+}
+
+TEST_F(KeyboardTest, TestInsertLineBreak) {
+ EXPECT_STREQ("InsertLineBreak", InterpretNewLine(WebInputEvent::kShiftKey));
+}
+
+TEST_F(KeyboardTest, TestDomKeyMap) {
+ struct TestCase {
+ const char* key;
+ const char* command;
+ } kDomKeyTestCases[] = {
+ {"Copy", "Copy"}, {"Cut", "Cut"}, {"Paste", "Paste"},
+ };
+
+ for (const auto& test_case : kDomKeyTestCases)
+ EXPECT_STREQ(test_case.command, InterpretDomKey(test_case.key));
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/layout_selection.cc b/chromium/third_party/blink/renderer/core/editing/layout_selection.cc
new file mode 100644
index 00000000000..56c1deb2b89
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/layout_selection.cc
@@ -0,0 +1,891 @@
+/*
+ * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "third_party/blink/renderer/core/editing/layout_selection.h"
+
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+#include "third_party/blink/renderer/core/layout/layout_text.h"
+#include "third_party/blink/renderer/core/layout/layout_text_fragment.h"
+#include "third_party/blink/renderer/core/layout/layout_view.h"
+#include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h"
+#include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.h"
+#include "third_party/blink/renderer/core/paint/paint_layer.h"
+
+namespace blink {
+
+SelectionPaintRange::SelectionPaintRange(LayoutObject* start_layout_object,
+ WTF::Optional<unsigned> start_offset,
+ LayoutObject* end_layout_object,
+ WTF::Optional<unsigned> end_offset)
+ : start_layout_object_(start_layout_object),
+ start_offset_(start_offset),
+ end_layout_object_(end_layout_object),
+ end_offset_(end_offset) {}
+
+bool SelectionPaintRange::operator==(const SelectionPaintRange& other) const {
+ return start_layout_object_ == other.start_layout_object_ &&
+ start_offset_ == other.start_offset_ &&
+ end_layout_object_ == other.end_layout_object_ &&
+ end_offset_ == other.end_offset_;
+}
+
+LayoutObject* SelectionPaintRange::StartLayoutObject() const {
+ DCHECK(!IsNull());
+ return start_layout_object_;
+}
+
+WTF::Optional<unsigned> SelectionPaintRange::StartOffset() const {
+ DCHECK(!IsNull());
+ return start_offset_;
+}
+
+LayoutObject* SelectionPaintRange::EndLayoutObject() const {
+ DCHECK(!IsNull());
+ return end_layout_object_;
+}
+
+WTF::Optional<unsigned> SelectionPaintRange::EndOffset() const {
+ DCHECK(!IsNull());
+ return end_offset_;
+}
+
+SelectionPaintRange::Iterator::Iterator(const SelectionPaintRange* range) {
+ if (!range || range->IsNull()) {
+ current_ = nullptr;
+ return;
+ }
+ current_ = range->StartLayoutObject();
+ stop_ = range->EndLayoutObject()->NextInPreOrder();
+}
+
+LayoutObject* SelectionPaintRange::Iterator::operator*() const {
+ DCHECK(current_);
+ return current_;
+}
+
+SelectionPaintRange::Iterator& SelectionPaintRange::Iterator::operator++() {
+ DCHECK(current_);
+ current_ = current_->NextInPreOrder();
+ if (current_ && current_ != stop_)
+ return *this;
+
+ current_ = nullptr;
+ return *this;
+}
+
+LayoutSelection::LayoutSelection(FrameSelection& frame_selection)
+ : frame_selection_(&frame_selection),
+ has_pending_selection_(false),
+ paint_range_(SelectionPaintRange()) {}
+
+enum class SelectionMode {
+ kNone,
+ kRange,
+ kBlockCursor,
+};
+
+static SelectionMode ComputeSelectionMode(
+ const FrameSelection& frame_selection) {
+ const SelectionInDOMTree& selection_in_dom =
+ frame_selection.GetSelectionInDOMTree();
+ if (selection_in_dom.IsRange())
+ return SelectionMode::kRange;
+ DCHECK(selection_in_dom.IsCaret());
+ if (!frame_selection.ShouldShowBlockCursor())
+ return SelectionMode::kNone;
+ if (IsLogicalEndOfLine(CreateVisiblePosition(selection_in_dom.Base())))
+ return SelectionMode::kNone;
+ return SelectionMode::kBlockCursor;
+}
+
+static EphemeralRangeInFlatTree CalcSelectionInFlatTree(
+ const FrameSelection& frame_selection) {
+ const SelectionInDOMTree& selection_in_dom =
+ frame_selection.GetSelectionInDOMTree();
+ switch (ComputeSelectionMode(frame_selection)) {
+ case SelectionMode::kNone:
+ return {};
+ case SelectionMode::kRange: {
+ const PositionInFlatTree& base =
+ ToPositionInFlatTree(selection_in_dom.Base());
+ const PositionInFlatTree& extent =
+ ToPositionInFlatTree(selection_in_dom.Extent());
+ if (base.IsNull() || extent.IsNull() || base == extent ||
+ !base.IsValidFor(frame_selection.GetDocument()) ||
+ !extent.IsValidFor(frame_selection.GetDocument()))
+ return {};
+ return base <= extent ? EphemeralRangeInFlatTree(base, extent)
+ : EphemeralRangeInFlatTree(extent, base);
+ }
+ case SelectionMode::kBlockCursor: {
+ const PositionInFlatTree& base =
+ CreateVisiblePosition(ToPositionInFlatTree(selection_in_dom.Base()))
+ .DeepEquivalent();
+ if (base.IsNull())
+ return {};
+ const PositionInFlatTree end_position =
+ NextPositionOf(base, PositionMoveType::kGraphemeCluster);
+ if (end_position.IsNull())
+ return {};
+ return base <= end_position
+ ? EphemeralRangeInFlatTree(base, end_position)
+ : EphemeralRangeInFlatTree(end_position, base);
+ }
+ }
+ NOTREACHED();
+ return {};
+}
+
+// LayoutObjects each has SelectionState of kStart, kEnd, kStartAndEnd, or
+// kInside.
+using SelectedLayoutObjects = HashSet<LayoutObject*>;
+// OldSelectedLayoutObjects is current selected LayoutObjects with
+// current SelectionState which is kStart, kEnd, kStartAndEnd or kInside.
+using OldSelectedLayoutObjects = HashMap<LayoutObject*, SelectionState>;
+
+#ifndef NDEBUG
+void PrintSelectedLayoutObjects(
+ const SelectedLayoutObjects& new_selected_objects) {
+ std::stringstream stream;
+ stream << std::endl;
+ for (LayoutObject* layout_object : new_selected_objects) {
+ PrintLayoutObjectForSelection(stream, layout_object);
+ stream << std::endl;
+ }
+ LOG(INFO) << stream.str();
+}
+
+void PrintOldSelectedLayoutObjects(
+ const OldSelectedLayoutObjects& old_selected_objects) {
+ std::stringstream stream;
+ stream << std::endl;
+ for (const auto& key_pair : old_selected_objects) {
+ LayoutObject* layout_object = key_pair.key;
+ SelectionState old_state = key_pair.value;
+ PrintLayoutObjectForSelection(stream, layout_object);
+ stream << " old: " << old_state << std::endl;
+ }
+ LOG(INFO) << stream.str();
+}
+
+void PrintSelectionPaintRange(const SelectionPaintRange& paint_range) {
+ std::stringstream stream;
+ stream << std::endl << "layout_objects:" << std::endl;
+ for (LayoutObject* layout_object : paint_range) {
+ PrintLayoutObjectForSelection(stream, layout_object);
+ stream << std::endl;
+ }
+ LOG(INFO) << stream.str();
+}
+
+void PrintSelectionStateInLayoutView(const FrameSelection& selection) {
+ std::stringstream stream;
+ stream << std::endl << "layout_objects:" << std::endl;
+ LayoutView* layout_view = selection.GetDocument().GetLayoutView();
+ for (LayoutObject* layout_object = layout_view; layout_object;
+ layout_object = layout_object->NextInPreOrder()) {
+ PrintLayoutObjectForSelection(stream, layout_object);
+ stream << std::endl;
+ }
+ LOG(INFO) << stream.str();
+}
+#endif
+
+// This class represents a selection range in layout tree and each LayoutObject
+// is SelectionState-marked.
+class NewPaintRangeAndSelectedLayoutObjects {
+ STACK_ALLOCATED();
+
+ public:
+ NewPaintRangeAndSelectedLayoutObjects() = default;
+ NewPaintRangeAndSelectedLayoutObjects(SelectionPaintRange paint_range,
+ SelectedLayoutObjects selected_objects)
+ : paint_range_(paint_range),
+ selected_objects_(std::move(selected_objects)) {}
+ NewPaintRangeAndSelectedLayoutObjects(
+ NewPaintRangeAndSelectedLayoutObjects&& other) {
+ paint_range_ = other.paint_range_;
+ selected_objects_ = std::move(other.selected_objects_);
+ }
+
+ SelectionPaintRange PaintRange() const { return paint_range_; }
+
+ const SelectedLayoutObjects& LayoutObjects() const {
+ return selected_objects_;
+ }
+
+ private:
+ SelectionPaintRange paint_range_;
+ SelectedLayoutObjects selected_objects_;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(NewPaintRangeAndSelectedLayoutObjects);
+};
+
+static void SetShouldInvalidateIfNeeded(LayoutObject* layout_object) {
+ if (layout_object->ShouldInvalidateSelection())
+ return;
+ layout_object->SetShouldInvalidateSelection();
+
+ // We should invalidate if ancestor of |layout_object| is LayoutSVGText
+ // because SVGRootInlineBoxPainter::Paint() paints selection for
+ // |layout_object| in/ LayoutSVGText and it is invoked when parent
+ // LayoutSVGText is invalidated.
+ // That is different from InlineTextBoxPainter::Paint() which paints
+ // LayoutText selection when LayoutText is invalidated.
+ if (!layout_object->IsSVG())
+ return;
+ for (LayoutObject* parent = layout_object->Parent(); parent;
+ parent = parent->Parent()) {
+ if (parent->IsSVGRoot())
+ return;
+ if (parent->IsSVGText()) {
+ if (!parent->ShouldInvalidateSelection())
+ parent->SetShouldInvalidateSelection();
+ return;
+ }
+ }
+}
+
+static void SetSelectionStateIfNeeded(LayoutObject* layout_object,
+ SelectionState state) {
+ DCHECK_NE(state, SelectionState::kContain) << layout_object;
+ DCHECK_NE(state, SelectionState::kNone) << layout_object;
+ if (layout_object->GetSelectionState() == state)
+ return;
+ layout_object->SetSelectionState(state);
+
+ // Set containing block SelectionState kContain for CSS ::selection style.
+ // See LayoutObject::InvalidatePaintForSelection().
+ for (LayoutObject* containing_block = layout_object->ContainingBlock();
+ containing_block;
+ containing_block = containing_block->ContainingBlock()) {
+ if (containing_block->GetSelectionState() == SelectionState::kContain)
+ return;
+ containing_block->LayoutObject::SetSelectionState(SelectionState::kContain);
+ }
+}
+
+// Set ShouldInvalidateSelection flag of LayoutObjects
+// comparing them in |new_range| and |old_range|.
+static void SetShouldInvalidateSelection(
+ const NewPaintRangeAndSelectedLayoutObjects& new_range,
+ const SelectionPaintRange& old_range,
+ const OldSelectedLayoutObjects& old_selected_objects) {
+ // We invalidate each LayoutObject in new SelectionPaintRange which
+ // has SelectionState of kStart, kEnd, kStartAndEnd, or kInside
+ // and is not in old SelectionPaintRange.
+ for (LayoutObject* layout_object : new_range.LayoutObjects()) {
+ if (old_selected_objects.Contains(layout_object))
+ continue;
+ const SelectionState new_state = layout_object->GetSelectionState();
+ DCHECK_NE(new_state, SelectionState::kContain) << layout_object;
+ DCHECK_NE(new_state, SelectionState::kNone) << layout_object;
+ SetShouldInvalidateIfNeeded(layout_object);
+ }
+ // For LayoutObject in old SelectionPaintRange, we invalidate LayoutObjects
+ // each of:
+ // 1. LayoutObject was painted and would not be painted.
+ // 2. LayoutObject was not painted and would be painted.
+ for (const auto& key_value : old_selected_objects) {
+ LayoutObject* const layout_object = key_value.key;
+ const SelectionState old_state = key_value.value;
+ const SelectionState new_state = layout_object->GetSelectionState();
+ if (new_state == old_state)
+ continue;
+ DCHECK(new_state != SelectionState::kNone ||
+ old_state != SelectionState::kNone)
+ << layout_object;
+ DCHECK_NE(new_state, SelectionState::kContain) << layout_object;
+ DCHECK_NE(old_state, SelectionState::kContain) << layout_object;
+ SetShouldInvalidateIfNeeded(layout_object);
+ }
+
+ // Invalidate Selection start/end is moving on a same node.
+ const SelectionPaintRange& new_paint_range = new_range.PaintRange();
+ if (new_paint_range.IsNull() || old_range.IsNull())
+ return;
+ if (new_paint_range.StartLayoutObject()->IsText() &&
+ new_paint_range.StartLayoutObject() == old_range.StartLayoutObject() &&
+ new_paint_range.StartOffset() != old_range.StartOffset())
+ SetShouldInvalidateIfNeeded(new_paint_range.StartLayoutObject());
+ if (new_paint_range.EndLayoutObject()->IsText() &&
+ new_paint_range.EndLayoutObject() == old_range.EndLayoutObject() &&
+ new_paint_range.EndOffset() != old_range.EndOffset())
+ SetShouldInvalidateIfNeeded(new_paint_range.EndLayoutObject());
+}
+
+WTF::Optional<unsigned> LayoutSelection::SelectionStart() const {
+ DCHECK(!HasPendingSelection());
+ if (paint_range_.IsNull())
+ return WTF::nullopt;
+ return paint_range_.StartOffset();
+}
+
+WTF::Optional<unsigned> LayoutSelection::SelectionEnd() const {
+ DCHECK(!HasPendingSelection());
+ if (paint_range_.IsNull())
+ return WTF::nullopt;
+ return paint_range_.EndOffset();
+}
+
+static OldSelectedLayoutObjects ResetOldSelectedLayoutObjects(
+ const SelectionPaintRange& old_range) {
+ OldSelectedLayoutObjects old_selected_objects;
+ HashSet<LayoutObject*> containing_block_set;
+ for (LayoutObject* layout_object : old_range) {
+ const SelectionState old_state = layout_object->GetSelectionState();
+ if (old_state == SelectionState::kNone)
+ continue;
+ if (old_state != SelectionState::kContain)
+ old_selected_objects.insert(layout_object, old_state);
+ layout_object->SetSelectionState(SelectionState::kNone);
+
+ // Reset containing block SelectionState for CSS ::selection style.
+ // See LayoutObject::InvalidatePaintForSelection().
+ for (LayoutObject* containing_block = layout_object->ContainingBlock();
+ containing_block;
+ containing_block = containing_block->ContainingBlock()) {
+ if (containing_block_set.Contains(containing_block))
+ break;
+ containing_block->SetSelectionState(SelectionState::kNone);
+ containing_block_set.insert(containing_block);
+ }
+ }
+ return old_selected_objects;
+}
+
+void LayoutSelection::ClearSelection() {
+ // For querying Layer::compositingState()
+ // This is correct, since destroying layout objects needs to cause eager paint
+ // invalidations.
+ DisableCompositingQueryAsserts disabler;
+
+ // Just return if the selection is already empty.
+ if (paint_range_.IsNull())
+ return;
+
+ const OldSelectedLayoutObjects& old_selected_objects =
+ ResetOldSelectedLayoutObjects(paint_range_);
+ for (LayoutObject* const layout_object : old_selected_objects.Keys())
+ SetShouldInvalidateIfNeeded(layout_object);
+
+ // Reset selection.
+ paint_range_ = SelectionPaintRange();
+}
+
+static WTF::Optional<unsigned> ComputeStartOffset(
+ const LayoutObject& layout_object,
+ const PositionInFlatTree& position) {
+ Node* const layout_node = layout_object.GetNode();
+ if (!layout_node || !layout_node->IsTextNode())
+ return WTF::nullopt;
+
+ if (layout_node == position.AnchorNode())
+ return position.OffsetInContainerNode();
+ return 0;
+}
+
+static WTF::Optional<unsigned> ComputeEndOffset(
+ const LayoutObject& layout_object,
+ const PositionInFlatTree& position) {
+ Node* const layout_node = layout_object.GetNode();
+ if (!layout_node || !layout_node->IsTextNode())
+ return WTF::nullopt;
+
+ if (layout_node == position.AnchorNode())
+ return position.OffsetInContainerNode();
+ return ToText(layout_node)->length();
+}
+
+static LayoutTextFragment* FirstLetterPartFor(LayoutObject* layout_object) {
+ if (!layout_object->IsText())
+ return nullptr;
+ if (!ToLayoutText(layout_object)->IsTextFragment())
+ return nullptr;
+ return ToLayoutTextFragment(const_cast<LayoutObject*>(
+ AssociatedLayoutObjectOf(*layout_object->GetNode(), 0)));
+}
+
+static void MarkSelected(SelectedLayoutObjects* selected_objects,
+ LayoutObject* layout_object,
+ SelectionState state) {
+ DCHECK(layout_object->CanBeSelectionLeaf());
+ SetSelectionStateIfNeeded(layout_object, state);
+ selected_objects->insert(layout_object);
+}
+
+static void MarkSelectedInside(SelectedLayoutObjects* selected_objects,
+ LayoutObject* layout_object) {
+ MarkSelected(selected_objects, layout_object, SelectionState::kInside);
+ LayoutTextFragment* const first_letter_part =
+ FirstLetterPartFor(layout_object);
+ if (!first_letter_part)
+ return;
+ MarkSelected(selected_objects, first_letter_part, SelectionState::kInside);
+}
+
+static NewPaintRangeAndSelectedLayoutObjects MarkStartAndEndInOneNode(
+ SelectedLayoutObjects selected_objects,
+ LayoutObject* layout_object,
+ WTF::Optional<unsigned> start_offset,
+ WTF::Optional<unsigned> end_offset) {
+ if (!layout_object->GetNode()->IsTextNode()) {
+ DCHECK(!start_offset.has_value());
+ DCHECK(!end_offset.has_value());
+ MarkSelected(&selected_objects, layout_object,
+ SelectionState::kStartAndEnd);
+ return {{layout_object, WTF::nullopt, layout_object, WTF::nullopt},
+ std::move(selected_objects)};
+ }
+
+ DCHECK(start_offset.has_value());
+ DCHECK(end_offset.has_value());
+ DCHECK_GE(end_offset.value(), start_offset.value());
+ if (start_offset.value() == end_offset.value())
+ return {};
+ LayoutTextFragment* const first_letter_part =
+ FirstLetterPartFor(layout_object);
+ if (!first_letter_part) {
+ MarkSelected(&selected_objects, layout_object,
+ SelectionState::kStartAndEnd);
+ return {{layout_object, start_offset, layout_object, end_offset},
+ std::move(selected_objects)};
+ }
+ const unsigned unsigned_start = start_offset.value();
+ const unsigned unsigned_end = end_offset.value();
+ LayoutTextFragment* const remaining_part =
+ ToLayoutTextFragment(layout_object);
+ if (unsigned_start >= remaining_part->Start()) {
+ // Case 1: The selection starts and ends in remaining part.
+ DCHECK_GT(unsigned_end, remaining_part->Start());
+ MarkSelected(&selected_objects, remaining_part,
+ SelectionState::kStartAndEnd);
+ return {{remaining_part, unsigned_start - remaining_part->Start(),
+ remaining_part, unsigned_end - remaining_part->Start()},
+ std::move(selected_objects)};
+ }
+ if (unsigned_end <= remaining_part->Start()) {
+ // Case 2: The selection starts and ends in first letter part.
+ MarkSelected(&selected_objects, first_letter_part,
+ SelectionState::kStartAndEnd);
+ return {{first_letter_part, start_offset, first_letter_part, end_offset},
+ std::move(selected_objects)};
+ }
+ // Case 3: The selection starts in first-letter part and ends in remaining
+ // part.
+ DCHECK_GT(unsigned_end, remaining_part->Start());
+ MarkSelected(&selected_objects, first_letter_part, SelectionState::kStart);
+ MarkSelected(&selected_objects, remaining_part, SelectionState::kEnd);
+ return {{first_letter_part, start_offset, remaining_part,
+ unsigned_end - remaining_part->Start()},
+ std::move(selected_objects)};
+}
+
+// LayoutObjectAndOffset represents start or end of SelectionPaintRange.
+struct LayoutObjectAndOffset {
+ STACK_ALLOCATED();
+ LayoutObject* layout_object;
+ WTF::Optional<unsigned> offset;
+
+ explicit LayoutObjectAndOffset(LayoutObject* passed_layout_object)
+ : layout_object(passed_layout_object), offset(WTF::nullopt) {
+ DCHECK(passed_layout_object);
+ DCHECK(!passed_layout_object->GetNode()->IsTextNode());
+ }
+ LayoutObjectAndOffset(LayoutText* layout_text, unsigned passed_offset)
+ : layout_object(layout_text), offset(passed_offset) {
+ DCHECK(layout_object);
+ }
+};
+
+LayoutObjectAndOffset MarkStart(SelectedLayoutObjects* selected_objects,
+ LayoutObject* start_layout_object,
+ WTF::Optional<unsigned> start_offset) {
+ if (!start_layout_object->GetNode()->IsTextNode()) {
+ DCHECK(!start_offset.has_value());
+ MarkSelected(selected_objects, start_layout_object, SelectionState::kStart);
+ return LayoutObjectAndOffset(start_layout_object);
+ }
+
+ DCHECK(start_offset.has_value());
+ const unsigned unsigned_offset = start_offset.value();
+ LayoutText* const start_layout_text = ToLayoutText(start_layout_object);
+ if (unsigned_offset >= start_layout_text->TextStartOffset()) {
+ // |start_offset| is within |start_layout_object| whether it has first
+ // letter part or not.
+ MarkSelected(selected_objects, start_layout_object, SelectionState::kStart);
+ return {start_layout_text,
+ unsigned_offset - start_layout_text->TextStartOffset()};
+ }
+
+ // |start_layout_object| has first letter part and |start_offset| is within
+ // the part.
+ LayoutTextFragment* const first_letter_part =
+ FirstLetterPartFor(start_layout_object);
+ DCHECK(first_letter_part);
+ MarkSelected(selected_objects, first_letter_part, SelectionState::kStart);
+ MarkSelected(selected_objects, start_layout_text, SelectionState::kInside);
+ return {first_letter_part, start_offset.value()};
+}
+
+LayoutObjectAndOffset MarkEnd(SelectedLayoutObjects* selected_objects,
+ LayoutObject* end_layout_object,
+ WTF::Optional<unsigned> end_offset) {
+ if (!end_layout_object->GetNode()->IsTextNode()) {
+ DCHECK(!end_offset.has_value());
+ MarkSelected(selected_objects, end_layout_object, SelectionState::kEnd);
+ return LayoutObjectAndOffset(end_layout_object);
+ }
+
+ DCHECK(end_offset.has_value());
+ const unsigned unsigned_offset = end_offset.value();
+ LayoutText* const end_layout_text = ToLayoutText(end_layout_object);
+ if (unsigned_offset >= end_layout_text->TextStartOffset()) {
+ // |end_offset| is within |end_layout_object| whether it has first
+ // letter part or not.
+ MarkSelected(selected_objects, end_layout_object, SelectionState::kEnd);
+ if (LayoutTextFragment* const first_letter_part =
+ FirstLetterPartFor(end_layout_object)) {
+ MarkSelected(selected_objects, first_letter_part,
+ SelectionState::kInside);
+ }
+ return {end_layout_text,
+ unsigned_offset - end_layout_text->TextStartOffset()};
+ }
+
+ // |end_layout_object| has first letter part and |end_offset| is within
+ // the part.
+ LayoutTextFragment* const first_letter_part =
+ FirstLetterPartFor(end_layout_object);
+ DCHECK(first_letter_part);
+ MarkSelected(selected_objects, first_letter_part, SelectionState::kEnd);
+ return {first_letter_part, end_offset.value()};
+}
+
+static NewPaintRangeAndSelectedLayoutObjects MarkStartAndEndInTwoNodes(
+ SelectedLayoutObjects selected_objects,
+ LayoutObject* start_layout_object,
+ WTF::Optional<unsigned> start_offset,
+ LayoutObject* end_layout_object,
+ WTF::Optional<unsigned> end_offset) {
+ const LayoutObjectAndOffset& start =
+ MarkStart(&selected_objects, start_layout_object, start_offset);
+ const LayoutObjectAndOffset& end =
+ MarkEnd(&selected_objects, end_layout_object, end_offset);
+ return {{start.layout_object, start.offset, end.layout_object, end.offset},
+ std::move(selected_objects)};
+}
+
+static WTF::Optional<unsigned> GetTextContentOffset(
+ LayoutObject* layout_object,
+ WTF::Optional<unsigned> node_offset) {
+ DCHECK(layout_object->EnclosingNGBlockFlow());
+ // |layout_object| is start or end of selection and offset is only valid
+ // if it is LayoutText.
+ if (!layout_object->IsText())
+ return WTF::nullopt;
+ // There are LayoutText that selection can't be inside it(BR, WBR,
+ // LayoutCounter).
+ if (!node_offset.has_value())
+ return WTF::nullopt;
+ const Position position_in_dom(*layout_object->GetNode(),
+ node_offset.value());
+ const NGOffsetMapping* const offset_mapping =
+ NGOffsetMapping::GetFor(position_in_dom);
+ DCHECK(offset_mapping);
+ const WTF::Optional<unsigned>& ng_offset =
+ offset_mapping->GetTextContentOffset(position_in_dom);
+ return ng_offset;
+}
+
+static NewPaintRangeAndSelectedLayoutObjects ComputeNewPaintRange(
+ const NewPaintRangeAndSelectedLayoutObjects& new_range,
+ LayoutObject* start_layout_object,
+ WTF::Optional<unsigned> start_node_offset,
+ LayoutObject* end_layout_object,
+ WTF::Optional<unsigned> end_node_offset) {
+ if (new_range.PaintRange().IsNull())
+ return {};
+ LayoutObject* const start = new_range.PaintRange().StartLayoutObject();
+ // If LayoutObject is not in NG, use legacy offset.
+ const WTF::Optional<unsigned> start_offset =
+ start->EnclosingNGBlockFlow()
+ ? GetTextContentOffset(start_layout_object, start_node_offset)
+ : new_range.PaintRange().StartOffset();
+
+ LayoutObject* const end = new_range.PaintRange().EndLayoutObject();
+ const WTF::Optional<unsigned> end_offset =
+ end->EnclosingNGBlockFlow()
+ ? GetTextContentOffset(end_layout_object, end_node_offset)
+ : new_range.PaintRange().EndOffset();
+
+ return {{start, start_offset, end, end_offset},
+ std::move(new_range.LayoutObjects())};
+}
+
+// ClampOffset modifies |offset| fixed in a range of |text_fragment| start/end
+// offsets.
+static unsigned ClampOffset(unsigned offset,
+ const NGPhysicalTextFragment& text_fragment) {
+ return std::min(std::max(offset, text_fragment.StartOffset()),
+ text_fragment.EndOffset());
+}
+
+std::pair<unsigned, unsigned> LayoutSelection::SelectionStartEndForNG(
+ const NGPhysicalTextFragment& text_fragment) const {
+ // FrameSelection holds selection offsets in layout block flow at
+ // LayoutSelection::Commit() if selection starts/ends within Text that
+ // each LayoutObject::SelectionState indicates.
+ // These offset can out of |text_fragment| because SelectionState is of each
+ // LayoutText and not |text_fragment|.
+ switch (text_fragment.GetLayoutObject()->GetSelectionState()) {
+ case SelectionState::kStart: {
+ DCHECK(SelectionStart().has_value());
+ unsigned start_in_block = SelectionStart().value_or(0);
+ return {ClampOffset(start_in_block, text_fragment),
+ text_fragment.EndOffset()};
+ }
+ case SelectionState::kEnd: {
+ DCHECK(SelectionEnd().has_value());
+ unsigned end_in_block =
+ SelectionEnd().value_or(text_fragment.EndOffset());
+ return {text_fragment.StartOffset(),
+ ClampOffset(end_in_block, text_fragment)};
+ }
+ case SelectionState::kStartAndEnd: {
+ DCHECK(SelectionStart().has_value());
+ DCHECK(SelectionEnd().has_value());
+ unsigned start_in_block = SelectionStart().value_or(0);
+ unsigned end_in_block =
+ SelectionEnd().value_or(text_fragment.EndOffset());
+ return {ClampOffset(start_in_block, text_fragment),
+ ClampOffset(end_in_block, text_fragment)};
+ }
+ case SelectionState::kInside:
+ return {text_fragment.StartOffset(), text_fragment.EndOffset()};
+ default:
+ // This block is not included in selection.
+ return {0, 0};
+ }
+}
+
+static NewPaintRangeAndSelectedLayoutObjects
+CalcSelectionRangeAndSetSelectionState(const FrameSelection& frame_selection) {
+ const SelectionInDOMTree& selection_in_dom =
+ frame_selection.GetSelectionInDOMTree();
+ if (selection_in_dom.IsNone())
+ return {};
+
+ const EphemeralRangeInFlatTree& selection =
+ CalcSelectionInFlatTree(frame_selection);
+ if (selection.IsCollapsed() || frame_selection.IsHidden())
+ return {};
+
+ // Find first/last visible LayoutObject while
+ // marking SelectionState and collecting invalidation candidate LayoutObjects.
+ LayoutObject* start_layout_object = nullptr;
+ LayoutObject* end_layout_object = nullptr;
+ SelectedLayoutObjects selected_objects;
+ for (const Node& node : selection.Nodes()) {
+ LayoutObject* const layout_object = node.GetLayoutObject();
+ if (!layout_object || !layout_object->CanBeSelectionLeaf())
+ continue;
+
+ if (!start_layout_object) {
+ DCHECK(!end_layout_object);
+ start_layout_object = end_layout_object = layout_object;
+ continue;
+ }
+
+ // In this loop, |end_layout_object| is pointing current last candidate
+ // LayoutObject and if it is not start and we find next, we mark the
+ // current one as kInside.
+ if (end_layout_object != start_layout_object)
+ MarkSelectedInside(&selected_objects, end_layout_object);
+ end_layout_object = layout_object;
+ }
+
+ // No valid LayOutObject found.
+ if (!start_layout_object) {
+ DCHECK(!end_layout_object);
+ return {};
+ }
+
+ // Compute offset. It has value iff start/end is text.
+ const WTF::Optional<unsigned> start_offset = ComputeStartOffset(
+ *start_layout_object, selection.StartPosition().ToOffsetInAnchor());
+ const WTF::Optional<unsigned> end_offset = ComputeEndOffset(
+ *end_layout_object, selection.EndPosition().ToOffsetInAnchor());
+
+ NewPaintRangeAndSelectedLayoutObjects new_range =
+ start_layout_object == end_layout_object
+ ? MarkStartAndEndInOneNode(std::move(selected_objects),
+ start_layout_object, start_offset,
+ end_offset)
+ : MarkStartAndEndInTwoNodes(std::move(selected_objects),
+ start_layout_object, start_offset,
+ end_layout_object, end_offset);
+
+ if (!RuntimeEnabledFeatures::LayoutNGEnabled())
+ return new_range;
+ return ComputeNewPaintRange(new_range, start_layout_object, start_offset,
+ end_layout_object, end_offset);
+}
+
+void LayoutSelection::SetHasPendingSelection() {
+ has_pending_selection_ = true;
+}
+
+void LayoutSelection::Commit() {
+ if (!HasPendingSelection())
+ return;
+ has_pending_selection_ = false;
+
+ DCHECK(!frame_selection_->GetDocument().NeedsLayoutTreeUpdate());
+ DCHECK_GE(frame_selection_->GetDocument().Lifecycle().GetState(),
+ DocumentLifecycle::kLayoutClean);
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ frame_selection_->GetDocument().Lifecycle());
+
+ const OldSelectedLayoutObjects& old_selected_objects =
+ ResetOldSelectedLayoutObjects(paint_range_);
+ const NewPaintRangeAndSelectedLayoutObjects& new_range =
+ CalcSelectionRangeAndSetSelectionState(*frame_selection_);
+ DCHECK(frame_selection_->GetDocument().GetLayoutView()->GetFrameView());
+ SetShouldInvalidateSelection(new_range, paint_range_, old_selected_objects);
+
+ paint_range_ = new_range.PaintRange();
+ if (paint_range_.IsNull())
+ return;
+ // TODO(yoichio): Remove this if state.
+ // This SelectionState reassignment is ad-hoc patch for
+ // prohibiting use-after-free(crbug.com/752715).
+ // LayoutText::setSelectionState(state) propergates |state| to ancestor
+ // LayoutObjects, which can accidentally change start/end LayoutObject state
+ // then LayoutObject::IsSelectionBorder() returns false although we should
+ // clear selection at LayoutObject::WillBeRemoved().
+ // We should make LayoutObject::setSelectionState() trivial and remove
+ // such propagation or at least do it in LayoutSelection.
+ if ((paint_range_.StartLayoutObject()->GetSelectionState() !=
+ SelectionState::kStart &&
+ paint_range_.StartLayoutObject()->GetSelectionState() !=
+ SelectionState::kStartAndEnd) ||
+ (paint_range_.EndLayoutObject()->GetSelectionState() !=
+ SelectionState::kEnd &&
+ paint_range_.EndLayoutObject()->GetSelectionState() !=
+ SelectionState::kStartAndEnd)) {
+ if (paint_range_.StartLayoutObject() == paint_range_.EndLayoutObject()) {
+ paint_range_.StartLayoutObject()->SetSelectionState(
+ SelectionState::kStartAndEnd);
+ } else {
+ paint_range_.StartLayoutObject()->SetSelectionState(
+ SelectionState::kStart);
+ paint_range_.EndLayoutObject()->SetSelectionState(SelectionState::kEnd);
+ }
+ }
+ // TODO(yoichio): If start == end, they should be kStartAndEnd.
+ // If not, start.SelectionState == kStart and vice versa.
+ DCHECK(paint_range_.StartLayoutObject()->GetSelectionState() ==
+ SelectionState::kStart ||
+ paint_range_.StartLayoutObject()->GetSelectionState() ==
+ SelectionState::kStartAndEnd);
+ DCHECK(paint_range_.EndLayoutObject()->GetSelectionState() ==
+ SelectionState::kEnd ||
+ paint_range_.EndLayoutObject()->GetSelectionState() ==
+ SelectionState::kStartAndEnd);
+}
+
+void LayoutSelection::OnDocumentShutdown() {
+ has_pending_selection_ = false;
+ paint_range_ = SelectionPaintRange();
+}
+
+static LayoutRect SelectionRectForLayoutObject(const LayoutObject* object) {
+ if (!object->IsRooted())
+ return LayoutRect();
+
+ if (!object->CanUpdateSelectionOnRootLineBoxes())
+ return LayoutRect();
+
+ return object->AbsoluteSelectionRect();
+}
+
+IntRect LayoutSelection::AbsoluteSelectionBounds() {
+ Commit();
+ if (paint_range_.IsNull())
+ return IntRect();
+
+ // Create a single bounding box rect that encloses the whole selection.
+ LayoutRect selected_rect;
+ for (LayoutObject* layout_object : paint_range_) {
+ const SelectionState state = layout_object->GetSelectionState();
+ if (state == SelectionState::kContain || state == SelectionState::kNone)
+ continue;
+ selected_rect.Unite(SelectionRectForLayoutObject(layout_object));
+ }
+
+ return PixelSnappedIntRect(selected_rect);
+}
+
+void LayoutSelection::InvalidatePaintForSelection() {
+ if (paint_range_.IsNull())
+ return;
+
+ for (LayoutObject* runner : paint_range_) {
+ if (runner->GetSelectionState() == SelectionState::kNone)
+ continue;
+
+ runner->SetShouldInvalidateSelection();
+ }
+}
+
+void LayoutSelection::Trace(blink::Visitor* visitor) {
+ visitor->Trace(frame_selection_);
+}
+
+void PrintLayoutObjectForSelection(std::ostream& ostream,
+ LayoutObject* layout_object) {
+ if (!layout_object) {
+ ostream << "<null>";
+ return;
+ }
+ ostream << (void*)layout_object << ' ' << layout_object->GetNode()
+ << ", state:" << layout_object->GetSelectionState()
+ << (layout_object->ShouldInvalidateSelection() ? ", ShouldInvalidate"
+ : ", NotInvalidate");
+}
+#ifndef NDEBUG
+void ShowLayoutObjectForSelection(LayoutObject* layout_object) {
+ std::stringstream stream;
+ PrintLayoutObjectForSelection(stream, layout_object);
+ LOG(INFO) << '\n' << stream.str();
+}
+#endif
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/layout_selection.h b/chromium/third_party/blink/renderer/core/editing/layout_selection.h
new file mode 100644
index 00000000000..dbd5a4e5902
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/layout_selection.h
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
+ * Copyright (C) 2006 Apple Computer, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_LAYOUT_SELECTION_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_LAYOUT_SELECTION_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/wtf/optional.h"
+
+namespace blink {
+
+class IntRect;
+class LayoutObject;
+class NGPhysicalTextFragment;
+class FrameSelection;
+
+// This class represents a selection range in layout tree for painting and
+// paint invalidation.
+// The current selection to be painted is represented as 2 pairs of
+// (LayoutObject, offset).
+// 2 LayoutObjects are only valid for |Text| node without 'transform' or
+// 'first-letter'.
+// TODO(editing-dev): Clarify the meaning of "offset".
+// editing/ passes them as offsets in the DOM tree but layout uses them as
+// offset in the layout tree. This doesn't work in the cases of
+// CSS first-letter or character transform. See crbug.com/17528.
+class SelectionPaintRange {
+ DISALLOW_NEW();
+
+ public:
+ class Iterator
+ : public std::iterator<std::input_iterator_tag, LayoutObject*> {
+ public:
+ explicit Iterator(const SelectionPaintRange*);
+ Iterator(const Iterator&) = default;
+ bool operator==(const Iterator& other) const {
+ return current_ == other.current_;
+ }
+ bool operator!=(const Iterator& other) const { return !operator==(other); }
+ Iterator& operator++();
+ LayoutObject* operator*() const;
+
+ private:
+ LayoutObject* current_;
+ const LayoutObject* stop_;
+ };
+ Iterator begin() const { return Iterator(this); };
+ Iterator end() const { return Iterator(nullptr); };
+
+ SelectionPaintRange() = default;
+ SelectionPaintRange(LayoutObject* start_layout_object,
+ WTF::Optional<unsigned> start_offset,
+ LayoutObject* end_layout_object,
+ WTF::Optional<unsigned> end_offset);
+
+ bool operator==(const SelectionPaintRange& other) const;
+
+ LayoutObject* StartLayoutObject() const;
+ WTF::Optional<unsigned> StartOffset() const;
+ LayoutObject* EndLayoutObject() const;
+ WTF::Optional<unsigned> EndOffset() const;
+
+ bool IsNull() const { return !start_layout_object_; }
+
+ private:
+ LayoutObject* start_layout_object_ = nullptr;
+ WTF::Optional<unsigned> start_offset_ = WTF::nullopt;
+ LayoutObject* end_layout_object_ = nullptr;
+ WTF::Optional<unsigned> end_offset_ = WTF::nullopt;
+};
+
+class LayoutSelection final : public GarbageCollected<LayoutSelection> {
+ public:
+ static LayoutSelection* Create(FrameSelection& frame_selection) {
+ return new LayoutSelection(frame_selection);
+ }
+
+ bool HasPendingSelection() const { return has_pending_selection_; }
+ void SetHasPendingSelection();
+ void Commit();
+
+ IntRect AbsoluteSelectionBounds();
+ void InvalidatePaintForSelection();
+
+ void ClearSelection();
+ WTF::Optional<unsigned> SelectionStart() const;
+ WTF::Optional<unsigned> SelectionEnd() const;
+ // This function returns selected part of |text_fragment|.
+ // Returned pair is a partial range of
+ // (text_fragment.StartOffset(), text_fragment.EndOffset()).
+ // If first equals second, it indicates "no selection in fragment".
+ std::pair<unsigned, unsigned> SelectionStartEndForNG(
+ const NGPhysicalTextFragment&) const;
+
+ void OnDocumentShutdown();
+
+ void Trace(blink::Visitor*);
+
+ private:
+ LayoutSelection(FrameSelection&);
+
+ Member<FrameSelection> frame_selection_;
+ bool has_pending_selection_ : 1;
+
+ SelectionPaintRange paint_range_;
+};
+
+void CORE_EXPORT PrintLayoutObjectForSelection(std::ostream&, LayoutObject*);
+#ifndef NDEBUG
+void ShowLayoutObjectForSelection(LayoutObject*);
+#endif
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/layout_selection_test.cc b/chromium/third_party/blink/renderer/core/editing/layout_selection_test.cc
new file mode 100644
index 00000000000..18cf7a357da
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/layout_selection_test.cc
@@ -0,0 +1,857 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/layout_selection.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
+#include "third_party/blink/renderer/core/dom/shadow_root_init.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/core/layout/layout_text.h"
+#include "third_party/blink/renderer/core/layout/layout_text_fragment.h"
+#include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.h"
+#include "third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.h"
+#include "third_party/blink/renderer/core/paint/ng/ng_paint_fragment.h"
+#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+#include "third_party/blink/renderer/platform/wtf/functional.h"
+
+namespace blink {
+
+class LayoutSelectionTest : public EditingTestBase {
+ public:
+ LayoutObject* Current() const { return current_; }
+ void Next() {
+ if (!current_) {
+ current_ = GetDocument().body()->GetLayoutObject();
+ return;
+ }
+ current_ = current_->NextInPreOrder();
+ }
+
+#ifndef NDEBUG
+ void PrintLayoutTreeForDebug() {
+ std::stringstream stream;
+ for (LayoutObject* runner = GetDocument().body()->GetLayoutObject(); runner;
+ runner = runner->NextInPreOrder()) {
+ PrintLayoutObjectForSelection(stream, runner);
+ stream << '\n';
+ }
+ LOG(INFO) << '\n' << stream.str();
+ }
+#endif
+
+ private:
+ LayoutObject* current_ = nullptr;
+};
+
+std::ostream& operator<<(std::ostream& ostream, LayoutObject* layout_object) {
+ PrintLayoutObjectForSelection(ostream, layout_object);
+ return ostream;
+}
+
+enum class InvalidateOption { ShouldInvalidate, NotInvalidate };
+
+static bool TestLayoutObjectState(LayoutObject* object,
+ SelectionState state,
+ InvalidateOption invalidate) {
+ if (!object)
+ return false;
+ if (object->GetSelectionState() != state)
+ return false;
+ if (object->ShouldInvalidateSelection() !=
+ (invalidate == InvalidateOption::ShouldInvalidate))
+ return false;
+ return true;
+}
+
+using IsTypeOf =
+ base::RepeatingCallback<bool(const LayoutObject& layout_object)>;
+using IsTypeOfSimple = bool(const LayoutObject& layout_object);
+#define USING_LAYOUTOBJECT_FUNC(member_func) \
+ static bool member_func(const LayoutObject& layout_object) { \
+ return layout_object.member_func(); \
+ }
+
+USING_LAYOUTOBJECT_FUNC(IsLayoutBlock);
+USING_LAYOUTOBJECT_FUNC(IsLayoutBlockFlow);
+USING_LAYOUTOBJECT_FUNC(IsLayoutNGBlockFlow);
+USING_LAYOUTOBJECT_FUNC(IsLayoutInline);
+USING_LAYOUTOBJECT_FUNC(IsBR);
+USING_LAYOUTOBJECT_FUNC(IsListItem);
+USING_LAYOUTOBJECT_FUNC(IsListMarker);
+USING_LAYOUTOBJECT_FUNC(IsLayoutImage);
+USING_LAYOUTOBJECT_FUNC(IsLayoutButton);
+USING_LAYOUTOBJECT_FUNC(IsSVGRoot);
+USING_LAYOUTOBJECT_FUNC(IsSVGText);
+USING_LAYOUTOBJECT_FUNC(IsLayoutEmbeddedContent);
+
+static IsTypeOf IsLayoutTextFragmentOf(const String& text) {
+ return WTF::BindRepeating(
+ [](const String& text, const LayoutObject& object) {
+ if (!object.IsText())
+ return false;
+ if (text != ToLayoutText(object).GetText())
+ return false;
+ return ToLayoutText(object).IsTextFragment();
+ },
+ text);
+}
+
+static bool IsSVGTSpan(const LayoutObject& layout_object) {
+ return layout_object.GetName() == String("LayoutSVGTSpan");
+}
+
+static bool IsLegacyBlockFlow(const LayoutObject& layout_object) {
+ return layout_object.IsLayoutBlockFlow() && !layout_object.IsLayoutNGMixin();
+}
+
+static bool TestLayoutObject(LayoutObject* object,
+ IsTypeOfSimple& predicate,
+ SelectionState state,
+ InvalidateOption invalidate) {
+ if (!TestLayoutObjectState(object, state, invalidate))
+ return false;
+
+ if (!predicate(*object))
+ return false;
+ return true;
+}
+static bool TestLayoutObject(LayoutObject* object,
+ const IsTypeOf& predicate,
+ SelectionState state,
+ InvalidateOption invalidate) {
+ if (!TestLayoutObjectState(object, state, invalidate))
+ return false;
+
+ if (!predicate.Run(*object))
+ return false;
+ return true;
+}
+static bool TestLayoutObject(LayoutObject* object,
+ const String& text,
+ SelectionState state,
+ InvalidateOption invalidate) {
+ return TestLayoutObject(
+ object,
+ WTF::BindRepeating(
+ [](const String& text, const LayoutObject& object) {
+ if (!object.IsText())
+ return false;
+ if (text != ToLayoutText(object).GetText())
+ return false;
+ return true;
+ },
+ text),
+ state, invalidate);
+}
+
+#define TEST_NEXT(predicate, state, invalidate) \
+ Next(); \
+ EXPECT_TRUE(TestLayoutObject(Current(), predicate, SelectionState::state, \
+ InvalidateOption::invalidate)) \
+ << Current();
+
+#define TEST_NO_NEXT_LAYOUT_OBJECT() \
+ Next(); \
+ EXPECT_EQ(Current(), nullptr)
+
+TEST_F(LayoutSelectionTest, TraverseLayoutObject) {
+ SetBodyContent("foo<br>bar");
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SelectAllChildren(*GetDocument().body())
+ .Build());
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStart, ShouldInvalidate);
+ TEST_NEXT(IsBR, kInside, ShouldInvalidate);
+ TEST_NEXT("bar", kEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+}
+
+TEST_F(LayoutSelectionTest, TraverseLayoutObjectTruncateVisibilityHidden) {
+ SetBodyContent(
+ "<span style='visibility:hidden;'>before</span>"
+ "foo"
+ "<span style='visibility:hidden;'>after</span>");
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SelectAllChildren(*GetDocument().body())
+ .Build());
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT("before", kNone, NotInvalidate);
+ TEST_NEXT("foo", kStartAndEnd, ShouldInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT("after", kNone, NotInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+}
+
+TEST_F(LayoutSelectionTest, TraverseLayoutObjectBRs) {
+ SetBodyContent("<br><br>foo<br><br>");
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SelectAllChildren(*GetDocument().body())
+ .Build());
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsBR, kStart, ShouldInvalidate);
+ TEST_NEXT(IsBR, kInside, ShouldInvalidate);
+ TEST_NEXT("foo", kInside, ShouldInvalidate);
+ TEST_NEXT(IsBR, kInside, ShouldInvalidate);
+ TEST_NEXT(IsBR, kEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_FALSE(Selection().LayoutSelectionStart().has_value());
+ EXPECT_FALSE(Selection().LayoutSelectionEnd().has_value());
+}
+
+TEST_F(LayoutSelectionTest, TraverseLayoutObjectListStyleImage) {
+ SetBodyContent(
+ "<style>ul {list-style-image:url(data:"
+ "image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=)}"
+ "</style>"
+ "<ul><li>foo<li>bar</ul>");
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SelectAllChildren(*GetDocument().body())
+ .Build());
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT(IsListItem, kContain, NotInvalidate);
+ TEST_NEXT(IsListMarker, kNone, NotInvalidate);
+ TEST_NEXT("foo", kStart, ShouldInvalidate);
+ TEST_NEXT(IsListItem, kContain, NotInvalidate);
+ TEST_NEXT(IsListMarker, kNone, NotInvalidate);
+ TEST_NEXT("bar", kEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+}
+
+TEST_F(LayoutSelectionTest, TraverseLayoutObjectCrossingShadowBoundary) {
+ Selection().SetSelectionAndEndTyping(SetSelectionTextToBody(
+ "^foo"
+ "<div>"
+ "<template data-mode=open>"
+ "Foo<slot name=s2></slot><slot name=s1></slot>"
+ "</template>"
+ // Set selection at SPAN@0 instead of "bar1"@0
+ "<span slot=s1><!--|-->bar1</span><span slot=s2>bar2</span>"
+ "</div>"));
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStart, ShouldInvalidate);
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("Foo", kInside, ShouldInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT("bar2", kEnd, ShouldInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT("bar1", kNone, NotInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+}
+
+// crbug.com/752715
+TEST_F(LayoutSelectionTest,
+ InvalidationShouldNotChangeRefferedLayoutObjectState) {
+ SetBodyContent(
+ "<div id='d1'>div1</div><div id='d2'>foo<span>bar</span>baz</div>");
+ Node* span = GetDocument().QuerySelector("span");
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(span->firstChild(), 0),
+ Position(span->firstChild(), 3))
+ .Build());
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutBlockFlow, kNone, NotInvalidate);
+ TEST_NEXT("div1", kNone, NotInvalidate);
+ TEST_NEXT(IsLayoutBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT("foo", kNone, NotInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT("bar", kStartAndEnd, ShouldInvalidate);
+ TEST_NEXT("baz", kNone, NotInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+
+ Node* d1 = GetDocument().QuerySelector("#d1");
+ Node* d2 = GetDocument().QuerySelector("#d2");
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(d1, 0), Position(d2, 0))
+ .Build());
+ // This commit should not crash.
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT("div1", kStartAndEnd, ShouldInvalidate);
+ TEST_NEXT(IsLayoutBlockFlow, kNone, NotInvalidate);
+ TEST_NEXT("foo", kNone, NotInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT("bar", kNone, ShouldInvalidate);
+ TEST_NEXT("baz", kNone, NotInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+}
+
+TEST_F(LayoutSelectionTest, TraverseLayoutObjectLineWrap) {
+ SetBodyContent("bar\n");
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SelectAllChildren(*GetDocument().body())
+ .Build());
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("bar\n", kStartAndEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_EQ(0u, Selection().LayoutSelectionStart());
+ EXPECT_EQ(4u, Selection().LayoutSelectionEnd());
+}
+
+TEST_F(LayoutSelectionTest, FirstLetter) {
+ SetBodyContent(
+ "<style>::first-letter { color: red; }</style>"
+ "<span>foo</span>");
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SelectAllChildren(*GetDocument().body())
+ .Build());
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT(IsLayoutTextFragmentOf("f"), kStart, ShouldInvalidate);
+ TEST_NEXT(IsLayoutTextFragmentOf("oo"), kEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+}
+
+TEST_F(LayoutSelectionTest, FirstLetterClearSeletion) {
+ InsertStyleElement("div::first-letter { color: red; }");
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("fo^o<div>bar</div>b|az"));
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStart, ShouldInvalidate);
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT(IsLayoutTextFragmentOf("b"), kInside, ShouldInvalidate);
+ TEST_NEXT(IsLayoutTextFragmentOf("ar"), kInside, ShouldInvalidate);
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("baz", kEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ Selection().ClearLayoutSelection();
+ TEST_NEXT(IsLayoutBlock, kNone, NotInvalidate);
+ TEST_NEXT(IsLayoutBlock, kNone, NotInvalidate);
+ TEST_NEXT("foo", kNone, ShouldInvalidate);
+ TEST_NEXT(IsLayoutBlock, kNone, NotInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT(IsLayoutTextFragmentOf("b"), kNone, ShouldInvalidate);
+ TEST_NEXT(IsLayoutTextFragmentOf("ar"), kNone, ShouldInvalidate);
+ TEST_NEXT(IsLayoutBlock, kNone, NotInvalidate);
+ TEST_NEXT("baz", kNone, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+}
+
+TEST_F(LayoutSelectionTest, FirstLetterUpdateSeletion) {
+ SetBodyContent(
+ "<style>div::first-letter { color: red; }</style>"
+ "foo<div>bar</div>baz");
+ Node* const foo = GetDocument().body()->firstChild()->nextSibling();
+ Node* const baz = GetDocument()
+ .body()
+ ->firstChild()
+ ->nextSibling()
+ ->nextSibling()
+ ->nextSibling();
+ // <div>fo^o</div><div>bar</div>b|az
+ Selection().SetSelectionAndEndTyping(SelectionInDOMTree::Builder()
+ .SetBaseAndExtent({foo, 2}, {baz, 1})
+ .Build());
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStart, ShouldInvalidate);
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT(IsLayoutTextFragmentOf("b"), kInside, ShouldInvalidate);
+ TEST_NEXT(IsLayoutTextFragmentOf("ar"), kInside, ShouldInvalidate);
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("baz", kEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ // <div>foo</div><div>bar</div>ba^z|
+ Selection().SetSelectionAndEndTyping(SelectionInDOMTree::Builder()
+ .SetBaseAndExtent({baz, 2}, {baz, 3})
+ .Build());
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutBlock, kNone, NotInvalidate);
+ TEST_NEXT("foo", kNone, ShouldInvalidate);
+ // TODO(yoichio): Invalidating next LayoutBlock is flaky but it doesn't
+ // matter in wild because we don't paint selection gap. I will update
+ // invalidating propagation so this flakiness should be fixed as:
+ // TEST_NEXT(IsLayoutBlock, kNone, NotInvalidate);
+ Next();
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT(IsLayoutTextFragmentOf("b"), kNone, ShouldInvalidate);
+ TEST_NEXT(IsLayoutTextFragmentOf("ar"), kNone, ShouldInvalidate);
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("baz", kStartAndEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+}
+
+TEST_F(LayoutSelectionTest, CommitAppearanceIfNeededNotCrash) {
+ Selection().SetSelectionAndEndTyping(SetSelectionTextToBody(
+ "<div>"
+ "<template data-mode=open>foo</template>"
+ "<span>|bar<span>" // <span> is not appeared in flat tree.
+ "</div>"
+ "<div>baz^</div>"));
+ Selection().CommitAppearanceIfNeeded();
+}
+
+TEST_F(LayoutSelectionTest, SelectImage) {
+ const SelectionInDOMTree& selection =
+ SetSelectionTextToBody("^<img style=\"width:100px; height:100px\"/>|");
+ Selection().SetSelectionAndEndTyping(selection);
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutImage, kStartAndEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_FALSE(Selection().LayoutSelectionStart().has_value());
+ EXPECT_FALSE(Selection().LayoutSelectionEnd().has_value());
+}
+
+TEST_F(LayoutSelectionTest, MoveOnSameNode_Start) {
+ const SelectionInDOMTree& selection =
+ SetSelectionTextToBody("f^oo<span>b|ar</span>");
+ Selection().SetSelectionAndEndTyping(selection);
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStart, ShouldInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT("bar", kEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_EQ(1u, Selection().LayoutSelectionStart());
+ EXPECT_EQ(1u, Selection().LayoutSelectionEnd());
+
+ // Paint virtually and clear ShouldInvalidate flag.
+ UpdateAllLifecyclePhases();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStart, NotInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT("bar", kEnd, NotInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+
+ // "fo^o<span>b|ar</span>"
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent({selection.Base().AnchorNode(), 2},
+ selection.Extent())
+ .Build());
+ Selection().CommitAppearanceIfNeeded();
+ // Only "foo" should be invalidated.
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStart, ShouldInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT("bar", kEnd, NotInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_EQ(2u, Selection().LayoutSelectionStart());
+ EXPECT_EQ(1u, Selection().LayoutSelectionEnd());
+}
+
+TEST_F(LayoutSelectionTest, MoveOnSameNode_End) {
+ const SelectionInDOMTree& selection =
+ SetSelectionTextToBody("f^oo<span>b|ar</span>");
+ Selection().SetSelectionAndEndTyping(selection);
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStart, ShouldInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT("bar", kEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_EQ(1u, Selection().LayoutSelectionStart());
+ EXPECT_EQ(1u, Selection().LayoutSelectionEnd());
+
+ // Paint virtually and clear ShouldInvalidate flag.
+ UpdateAllLifecyclePhases();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStart, NotInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT("bar", kEnd, NotInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+
+ // "fo^o<span>ba|r</span>"
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(selection.Base(),
+ {selection.Extent().AnchorNode(), 2})
+ .Build());
+ Selection().CommitAppearanceIfNeeded();
+ // Only "bar" should be invalidated.
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStart, NotInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT("bar", kEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_EQ(1u, Selection().LayoutSelectionStart());
+ EXPECT_EQ(2u, Selection().LayoutSelectionEnd());
+}
+
+TEST_F(LayoutSelectionTest, MoveOnSameNode_StartAndEnd) {
+ const SelectionInDOMTree& selection = SetSelectionTextToBody("f^oob|ar");
+ Selection().SetSelectionAndEndTyping(selection);
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foobar", kStartAndEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_EQ(1u, Selection().LayoutSelectionStart());
+ EXPECT_EQ(4u, Selection().LayoutSelectionEnd());
+
+ // Paint virtually and clear ShouldInvalidate flag.
+ UpdateAllLifecyclePhases();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foobar", kStartAndEnd, NotInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+
+ // "f^ooba|r"
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(selection.Base(),
+ {selection.Extent().AnchorNode(), 5})
+ .Build());
+ Selection().CommitAppearanceIfNeeded();
+ // "foobar" should be invalidated.
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foobar", kStartAndEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_EQ(1u, Selection().LayoutSelectionStart());
+ EXPECT_EQ(5u, Selection().LayoutSelectionEnd());
+}
+
+TEST_F(LayoutSelectionTest, MoveOnSameNode_StartAndEnd_Collapse) {
+ const SelectionInDOMTree& selection = SetSelectionTextToBody("f^oob|ar");
+ Selection().SetSelectionAndEndTyping(selection);
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foobar", kStartAndEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_EQ(1u, Selection().LayoutSelectionStart());
+ EXPECT_EQ(4u, Selection().LayoutSelectionEnd());
+
+ // Paint virtually and clear ShouldInvalidate flag.
+ UpdateAllLifecyclePhases();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foobar", kStartAndEnd, NotInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+
+ // "foo^|bar"
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse({selection.Base().AnchorNode(), 3})
+ .Build());
+ Selection().CommitAppearanceIfNeeded();
+ // "foobar" should be invalidated.
+ TEST_NEXT(IsLayoutBlock, kNone, NotInvalidate);
+ TEST_NEXT("foobar", kNone, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_FALSE(Selection().LayoutSelectionStart().has_value());
+ EXPECT_FALSE(Selection().LayoutSelectionEnd().has_value());
+}
+
+TEST_F(LayoutSelectionTest, ContentEditableButton) {
+ SetBodyContent("<input type=button value=foo contenteditable>");
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SelectAllChildren(*GetDocument().body())
+ .Build());
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutButton, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStartAndEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+}
+
+TEST_F(LayoutSelectionTest, ClearSelection) {
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("<div>f^o|o</div>"));
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStartAndEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_EQ(1u, Selection().LayoutSelectionStart());
+ EXPECT_EQ(2u, Selection().LayoutSelectionEnd());
+
+ UpdateAllLifecyclePhases();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStartAndEnd, NotInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+
+ Selection().ClearLayoutSelection();
+ TEST_NEXT(IsLayoutBlock, kNone, NotInvalidate);
+ TEST_NEXT(IsLayoutBlock, kNone, NotInvalidate);
+ TEST_NEXT("foo", kNone, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_FALSE(Selection().LayoutSelectionStart().has_value());
+ EXPECT_FALSE(Selection().LayoutSelectionEnd().has_value());
+}
+
+TEST_F(LayoutSelectionTest, SVG) {
+ const SelectionInDOMTree& selection =
+ SetSelectionTextToBody("<svg><text x=10 y=10>fo^o|bar</text></svg>");
+ Selection().SetSelectionAndEndTyping(selection);
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsSVGRoot, kNone, NotInvalidate);
+ // LayoutSVGText should be invalidate though it is kContain.
+ TEST_NEXT(IsSVGText, kContain, ShouldInvalidate);
+ TEST_NEXT("foobar", kStartAndEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_EQ(2u, Selection().LayoutSelectionStart());
+ EXPECT_EQ(3u, Selection().LayoutSelectionEnd());
+
+ UpdateAllLifecyclePhases();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsSVGRoot, kNone, NotInvalidate);
+ TEST_NEXT(IsSVGText, kContain, NotInvalidate);
+ TEST_NEXT("foobar", kStartAndEnd, NotInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(selection.Base(),
+ {selection.Extent().AnchorNode(), 4})
+ .Build());
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsSVGRoot, kNone, NotInvalidate);
+ TEST_NEXT(IsSVGText, kContain, ShouldInvalidate);
+ TEST_NEXT("foobar", kStartAndEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_EQ(2u, Selection().LayoutSelectionStart());
+ EXPECT_EQ(4u, Selection().LayoutSelectionEnd());
+}
+
+// crbug.com/781705
+TEST_F(LayoutSelectionTest, SVGAncestor) {
+ const SelectionInDOMTree& selection = SetSelectionTextToBody(
+ "<svg><text x=10 y=10><tspan>fo^o|bar</tspan></text></svg>");
+ Selection().SetSelectionAndEndTyping(selection);
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsSVGRoot, kNone, NotInvalidate);
+ // LayoutSVGText should be invalidated.
+ TEST_NEXT(IsSVGText, kContain, ShouldInvalidate);
+ TEST_NEXT(IsSVGTSpan, kNone, NotInvalidate);
+ TEST_NEXT("foobar", kStartAndEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_EQ(2u, Selection().LayoutSelectionStart());
+ EXPECT_EQ(3u, Selection().LayoutSelectionEnd());
+
+ UpdateAllLifecyclePhases();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsSVGRoot, kNone, NotInvalidate);
+ TEST_NEXT(IsSVGText, kContain, NotInvalidate);
+ TEST_NEXT(IsSVGTSpan, kNone, NotInvalidate);
+ TEST_NEXT("foobar", kStartAndEnd, NotInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(selection.Base(),
+ {selection.Extent().AnchorNode(), 4})
+ .Build());
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsSVGRoot, kNone, NotInvalidate);
+ TEST_NEXT(IsSVGText, kContain, ShouldInvalidate);
+ TEST_NEXT(IsSVGTSpan, kNone, NotInvalidate);
+ TEST_NEXT("foobar", kStartAndEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_EQ(2u, Selection().LayoutSelectionStart());
+ EXPECT_EQ(4u, Selection().LayoutSelectionEnd());
+}
+
+TEST_F(LayoutSelectionTest, Embed) {
+ Selection().SetSelectionAndEndTyping(
+ SetSelectionTextToBody("^<embed type=foobar></embed>|"));
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutBlock, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutEmbeddedContent, kStartAndEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+}
+
+class NGLayoutSelectionTest
+ : public LayoutSelectionTest,
+ private ScopedLayoutNGForTest,
+ private ScopedPaintUnderInvalidationCheckingForTest {
+ public:
+ NGLayoutSelectionTest()
+ : ScopedLayoutNGForTest(true),
+ ScopedPaintUnderInvalidationCheckingForTest(true) {}
+};
+
+static const NGPaintFragment* FindNGPaintFragmentInternal(
+ const NGPaintFragment* paint,
+ const LayoutObject* layout_object) {
+ if (paint->GetLayoutObject() == layout_object)
+ return paint;
+ for (const auto& child : paint->Children()) {
+ if (const NGPaintFragment* child_fragment =
+ FindNGPaintFragmentInternal(child.get(), layout_object))
+ return child_fragment;
+ }
+ return nullptr;
+}
+
+static const NGPhysicalTextFragment& GetNGPhysicalTextFragment(
+ const LayoutObject* layout_object) {
+ DCHECK(layout_object->IsText());
+ LayoutBlockFlow* block_flow = layout_object->EnclosingNGBlockFlow();
+ DCHECK(block_flow);
+ DCHECK(block_flow->IsLayoutNGMixin());
+ LayoutNGBlockFlow* layout_ng = ToLayoutNGBlockFlow(block_flow);
+ const NGPaintFragment* paint_fragment =
+ FindNGPaintFragmentInternal(layout_ng->PaintFragment(), layout_object);
+ const NGPhysicalFragment& physical_fragment =
+ paint_fragment->PhysicalFragment();
+ return ToNGPhysicalTextFragment(physical_fragment);
+}
+
+TEST_F(NGLayoutSelectionTest, SelectOnOneText) {
+#ifndef NDEBUG
+ // This line prohibits compiler optimization removing the debug function.
+ PrintLayoutTreeForDebug();
+#endif
+ const SelectionInDOMTree& selection =
+ SetSelectionTextToBody("foo<span>b^a|r</span>");
+ Selection().SetSelectionAndEndTyping(selection);
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutNGBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT("foo", kNone, NotInvalidate);
+ TEST_NEXT(IsLayoutInline, kNone, NotInvalidate);
+ TEST_NEXT("bar", kStartAndEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+
+ LayoutObject* const foo =
+ GetDocument().body()->firstChild()->GetLayoutObject();
+ EXPECT_EQ(std::make_pair(0u, 0u), Selection().LayoutSelectionStartEndForNG(
+ GetNGPhysicalTextFragment(foo)));
+ LayoutObject* const bar = GetDocument()
+ .body()
+ ->firstChild()
+ ->nextSibling()
+ ->firstChild()
+ ->GetLayoutObject();
+ EXPECT_EQ(std::make_pair(4u, 5u), Selection().LayoutSelectionStartEndForNG(
+ GetNGPhysicalTextFragment(bar)));
+}
+
+TEST_F(NGLayoutSelectionTest, FirstLetterInAnotherBlockFlow) {
+ const SelectionInDOMTree& selection = SetSelectionTextToBody(
+ "<style>:first-letter { float: right}</style>^fo|o");
+ Selection().SetSelectionAndEndTyping(selection);
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutNGBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutNGBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutTextFragmentOf("f"), kStart, ShouldInvalidate);
+ TEST_NEXT(IsLayoutTextFragmentOf("oo"), kEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ Node* const foo = GetDocument().body()->firstChild()->nextSibling();
+ const LayoutTextFragment* const foo_f =
+ ToLayoutTextFragment(AssociatedLayoutObjectOf(*foo, 0));
+ EXPECT_EQ(std::make_pair(0u, 1u), Selection().LayoutSelectionStartEndForNG(
+ GetNGPhysicalTextFragment(foo_f)));
+ const LayoutTextFragment* const foo_oo =
+ ToLayoutTextFragment(AssociatedLayoutObjectOf(*foo, 1));
+ EXPECT_EQ(std::make_pair(1u, 2u), Selection().LayoutSelectionStartEndForNG(
+ GetNGPhysicalTextFragment(foo_oo)));
+}
+
+TEST_F(NGLayoutSelectionTest, TwoNGBlockFlows) {
+ const SelectionInDOMTree& selection =
+ SetSelectionTextToBody("<div>f^oo</div><div>ba|r</div>");
+ Selection().SetSelectionAndEndTyping(selection);
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutNGBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutNGBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStart, ShouldInvalidate);
+ TEST_NEXT(IsLayoutNGBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT("bar", kEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ LayoutObject* const foo =
+ GetDocument().body()->firstChild()->firstChild()->GetLayoutObject();
+ EXPECT_EQ(std::make_pair(1u, 3u), Selection().LayoutSelectionStartEndForNG(
+ GetNGPhysicalTextFragment(foo)));
+ LayoutObject* const bar = GetDocument()
+ .body()
+ ->firstChild()
+ ->nextSibling()
+ ->firstChild()
+ ->GetLayoutObject();
+ EXPECT_EQ(std::make_pair(0u, 2u), Selection().LayoutSelectionStartEndForNG(
+ GetNGPhysicalTextFragment(bar)));
+}
+
+TEST_F(NGLayoutSelectionTest, MixedBlockFlowsAsSibling) {
+ const SelectionInDOMTree& selection = SetSelectionTextToBody(
+ "<div>f^oo</div>"
+ "<div contenteditable>ba|r</div>");
+ Selection().SetSelectionAndEndTyping(selection);
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutNGBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutNGBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStart, ShouldInvalidate);
+ TEST_NEXT(IsLegacyBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT("bar", kEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ LayoutObject* const foo =
+ GetDocument().body()->firstChild()->firstChild()->GetLayoutObject();
+ EXPECT_EQ(std::make_pair(1u, 3u), Selection().LayoutSelectionStartEndForNG(
+ GetNGPhysicalTextFragment(foo)));
+ EXPECT_EQ(2u, Selection().LayoutSelectionEnd().value());
+}
+
+TEST_F(NGLayoutSelectionTest, MixedBlockFlowsAnscestor) {
+ // Both "foo" and "bar" for DIV elements should be legacy LayoutBlock.
+ const SelectionInDOMTree& selection = SetSelectionTextToBody(
+ "<div contenteditable>f^oo"
+ "<div contenteditable=false>ba|r</div></div>");
+ Selection().SetSelectionAndEndTyping(selection);
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutNGBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT(IsLegacyBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutNGBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStart, ShouldInvalidate);
+ TEST_NEXT(IsLegacyBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT("bar", kEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ EXPECT_EQ(1u, Selection().LayoutSelectionStart().value());
+}
+
+TEST_F(NGLayoutSelectionTest, MixedBlockFlowsDecendant) {
+ const SelectionInDOMTree& selection = SetSelectionTextToBody(
+ "<div contenteditable=false>f^oo"
+ "<div contenteditable>ba|r</div></div>");
+ Selection().SetSelectionAndEndTyping(selection);
+ Selection().CommitAppearanceIfNeeded();
+ TEST_NEXT(IsLayoutNGBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutNGBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT(IsLayoutNGBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT("foo", kStart, ShouldInvalidate);
+ TEST_NEXT(IsLegacyBlockFlow, kContain, NotInvalidate);
+ TEST_NEXT("bar", kEnd, ShouldInvalidate);
+ TEST_NO_NEXT_LAYOUT_OBJECT();
+ LayoutObject* const foo =
+ GetDocument().body()->firstChild()->firstChild()->GetLayoutObject();
+ EXPECT_EQ(std::make_pair(1u, 3u), Selection().LayoutSelectionStartEndForNG(
+ GetNGPhysicalTextFragment(foo)));
+ EXPECT_EQ(2u, Selection().LayoutSelectionEnd().value());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/link_selection_test.cc b/chromium/third_party/blink/renderer/core/editing/link_selection_test.cc
new file mode 100644
index 00000000000..da3bb59ff5c
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/link_selection_test.cc
@@ -0,0 +1,358 @@
+// Copyright (c) 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/platform/web_coalesced_input_event.h"
+#include "third_party/blink/public/web/web_settings.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
+#include "third_party/blink/renderer/core/input/event_handler.h"
+#include "third_party/blink/renderer/core/page/chrome_client.h"
+#include "third_party/blink/renderer/core/page/context_menu_controller.h"
+#include "third_party/blink/renderer/core/page/focus_controller.h"
+#include "third_party/blink/renderer/core/page/page.h"
+#include "third_party/blink/renderer/platform/cursor.h"
+#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
+#include "third_party/blink/renderer/platform/testing/url_test_helpers.h"
+
+using testing::_;
+
+namespace blink {
+
+IntSize Scaled(IntSize p, float scale) {
+ p.Scale(scale, scale);
+ return p;
+}
+
+class LinkSelectionTestBase : public testing::Test {
+ protected:
+ enum DragFlag { kSendDownEvent = 1, kSendUpEvent = 1 << 1 };
+ using DragFlags = unsigned;
+
+ void EmulateMouseDrag(const IntPoint& down_point,
+ const IntPoint& up_point,
+ int modifiers,
+ DragFlags = kSendDownEvent | kSendUpEvent);
+
+ void EmulateMouseClick(const IntPoint& click_point,
+ WebMouseEvent::Button,
+ int modifiers,
+ int count = 1);
+ void EmulateMouseDown(const IntPoint& click_point,
+ WebMouseEvent::Button,
+ int modifiers,
+ int count = 1);
+
+ String GetSelectionText();
+
+ FrameTestHelpers::WebViewHelper helper_;
+ WebViewImpl* web_view_ = nullptr;
+ Persistent<WebLocalFrameImpl> main_frame_ = nullptr;
+};
+
+void LinkSelectionTestBase::EmulateMouseDrag(const IntPoint& down_point,
+ const IntPoint& up_point,
+ int modifiers,
+ DragFlags drag_flags) {
+ if (drag_flags & kSendDownEvent) {
+ const auto& down_event = FrameTestHelpers::CreateMouseEvent(
+ WebMouseEvent::kMouseDown, WebMouseEvent::Button::kLeft, down_point,
+ modifiers);
+ web_view_->HandleInputEvent(WebCoalescedInputEvent(down_event));
+ }
+
+ const int kMoveEventsNumber = 10;
+ const float kMoveIncrementFraction = 1. / kMoveEventsNumber;
+ const auto& up_down_vector = up_point - down_point;
+ for (int i = 0; i < kMoveEventsNumber; ++i) {
+ const auto& move_point =
+ down_point + Scaled(up_down_vector, i * kMoveIncrementFraction);
+ const auto& move_event = FrameTestHelpers::CreateMouseEvent(
+ WebMouseEvent::kMouseMove, WebMouseEvent::Button::kLeft, move_point,
+ modifiers);
+ web_view_->HandleInputEvent(WebCoalescedInputEvent(move_event));
+ }
+
+ if (drag_flags & kSendUpEvent) {
+ const auto& up_event = FrameTestHelpers::CreateMouseEvent(
+ WebMouseEvent::kMouseUp, WebMouseEvent::Button::kLeft, up_point,
+ modifiers);
+ web_view_->HandleInputEvent(WebCoalescedInputEvent(up_event));
+ }
+}
+
+void LinkSelectionTestBase::EmulateMouseClick(const IntPoint& click_point,
+ WebMouseEvent::Button button,
+ int modifiers,
+ int count) {
+ auto event = FrameTestHelpers::CreateMouseEvent(
+ WebMouseEvent::kMouseDown, button, click_point, modifiers);
+ event.click_count = count;
+ web_view_->HandleInputEvent(WebCoalescedInputEvent(event));
+ event.SetType(WebMouseEvent::kMouseUp);
+ web_view_->HandleInputEvent(WebCoalescedInputEvent(event));
+}
+
+void LinkSelectionTestBase::EmulateMouseDown(const IntPoint& click_point,
+ WebMouseEvent::Button button,
+ int modifiers,
+ int count) {
+ auto event = FrameTestHelpers::CreateMouseEvent(
+ WebMouseEvent::kMouseDown, button, click_point, modifiers);
+ event.click_count = count;
+ web_view_->HandleInputEvent(WebCoalescedInputEvent(event));
+}
+
+String LinkSelectionTestBase::GetSelectionText() {
+ return main_frame_->SelectionAsText();
+}
+
+class TestFrameClient : public FrameTestHelpers::TestWebFrameClient {
+ public:
+ WebNavigationPolicy DecidePolicyForNavigation(
+ const NavigationPolicyInfo& info) override {
+ last_policy_ = info.default_policy;
+ return kWebNavigationPolicyIgnore;
+ }
+
+ WebNavigationPolicy GetLastNavigationPolicy() const { return last_policy_; }
+
+ private:
+ WebNavigationPolicy last_policy_ = kWebNavigationPolicyCurrentTab;
+};
+
+class LinkSelectionTest : public LinkSelectionTestBase {
+ protected:
+ void SetUp() override {
+ constexpr char kHTMLString[] =
+ "<a id='link' href='foo.com' style='font-size:20pt'>Text to select "
+ "foobar</a>"
+ "<div id='page_text'>Lorem ipsum dolor sit amet</div>";
+
+ web_view_ = helper_.Initialize(&test_frame_client_);
+ main_frame_ = web_view_->MainFrameImpl();
+ FrameTestHelpers::LoadHTMLString(
+ main_frame_, kHTMLString, URLTestHelpers::ToKURL("http://foobar.com"));
+ web_view_->Resize(WebSize(800, 600));
+ web_view_->GetPage()->GetFocusController().SetActive(true);
+
+ auto* document = main_frame_->GetFrame()->GetDocument();
+ ASSERT_NE(nullptr, document);
+ auto* link_to_select = document->getElementById("link")->firstChild();
+ ASSERT_NE(nullptr, link_to_select);
+ // We get larger range that we actually want to select, because we need a
+ // slightly larger rect to include the last character to the selection.
+ const auto range_to_select =
+ Range::Create(*document, link_to_select, 5, link_to_select, 16);
+
+ const auto& selection_rect = range_to_select->BoundingBox();
+ const auto& selection_rect_center_y = selection_rect.Center().Y();
+ left_point_in_link_ = selection_rect.MinXMinYCorner();
+ left_point_in_link_.SetY(selection_rect_center_y);
+
+ right_point_in_link_ = selection_rect.MaxXMinYCorner();
+ right_point_in_link_.SetY(selection_rect_center_y);
+ right_point_in_link_.Move(-2, 0);
+ }
+
+ void TearDown() {
+ // Manually reset since |test_frame_client_| won't outlive |helper_|.
+ helper_.Reset();
+ }
+
+ TestFrameClient test_frame_client_;
+ IntPoint left_point_in_link_;
+ IntPoint right_point_in_link_;
+};
+
+TEST_F(LinkSelectionTest, MouseDragWithoutAltAllowNoLinkSelection) {
+ EmulateMouseDrag(left_point_in_link_, right_point_in_link_, 0);
+ EXPECT_TRUE(GetSelectionText().IsEmpty());
+}
+
+TEST_F(LinkSelectionTest, MouseDragWithAltAllowSelection) {
+ EmulateMouseDrag(left_point_in_link_, right_point_in_link_,
+ WebInputEvent::kAltKey);
+ EXPECT_EQ("to select", GetSelectionText());
+}
+
+TEST_F(LinkSelectionTest, HandCursorDuringLinkDrag) {
+ EmulateMouseDrag(right_point_in_link_, left_point_in_link_, 0,
+ kSendDownEvent);
+ main_frame_->GetFrame()
+ ->LocalFrameRoot()
+ .GetEventHandler()
+ .ScheduleCursorUpdate();
+ test::RunDelayedTasks(TimeDelta::FromMilliseconds(50));
+ const auto& cursor =
+ main_frame_->GetFrame()->GetChromeClient().LastSetCursorForTesting();
+ EXPECT_EQ(Cursor::kHand, cursor.GetType());
+}
+
+TEST_F(LinkSelectionTest, DragOnNothingShowsPointer) {
+ EmulateMouseDrag(IntPoint(100, 500), IntPoint(300, 500), 0, kSendDownEvent);
+ main_frame_->GetFrame()
+ ->LocalFrameRoot()
+ .GetEventHandler()
+ .ScheduleCursorUpdate();
+ test::RunDelayedTasks(TimeDelta::FromMilliseconds(50));
+ const auto& cursor =
+ main_frame_->GetFrame()->GetChromeClient().LastSetCursorForTesting();
+ EXPECT_EQ(Cursor::kPointer, cursor.GetType());
+}
+
+TEST_F(LinkSelectionTest, CaretCursorOverLinkDuringSelection) {
+ EmulateMouseDrag(right_point_in_link_, left_point_in_link_,
+ WebInputEvent::kAltKey, kSendDownEvent);
+ main_frame_->GetFrame()
+ ->LocalFrameRoot()
+ .GetEventHandler()
+ .ScheduleCursorUpdate();
+ test::RunDelayedTasks(TimeDelta::FromMilliseconds(50));
+ const auto& cursor =
+ main_frame_->GetFrame()->GetChromeClient().LastSetCursorForTesting();
+ EXPECT_EQ(Cursor::kIBeam, cursor.GetType());
+}
+
+TEST_F(LinkSelectionTest, HandCursorOverLinkAfterContextMenu) {
+ // Move mouse.
+ EmulateMouseDrag(right_point_in_link_, left_point_in_link_, 0, 0);
+
+ // Show context menu. We don't send mouseup event here since in browser it
+ // doesn't reach blink because of shown context menu.
+ EmulateMouseDown(left_point_in_link_, WebMouseEvent::Button::kRight, 0, 1);
+
+ LocalFrame* frame = main_frame_->GetFrame();
+ // Hide context menu.
+ frame->GetPage()->GetContextMenuController().ClearContextMenu();
+
+ frame->LocalFrameRoot().GetEventHandler().ScheduleCursorUpdate();
+ test::RunDelayedTasks(TimeDelta::FromMilliseconds(50));
+ const auto& cursor =
+ main_frame_->GetFrame()->GetChromeClient().LastSetCursorForTesting();
+ EXPECT_EQ(Cursor::kHand, cursor.GetType());
+}
+
+TEST_F(LinkSelectionTest, SingleClickWithAltStartsDownload) {
+ EmulateMouseClick(left_point_in_link_, WebMouseEvent::Button::kLeft,
+ WebInputEvent::kAltKey);
+ EXPECT_EQ(kWebNavigationPolicyDownload,
+ test_frame_client_.GetLastNavigationPolicy());
+}
+
+TEST_F(LinkSelectionTest, SingleClickWithAltStartsDownloadWhenTextSelected) {
+ auto* document = main_frame_->GetFrame()->GetDocument();
+ auto* text_to_select = document->getElementById("page_text")->firstChild();
+ ASSERT_NE(nullptr, text_to_select);
+
+ // Select some page text outside the link element.
+ const Range* range_to_select =
+ Range::Create(*document, text_to_select, 1, text_to_select, 20);
+ const auto& selection_rect = range_to_select->BoundingBox();
+ main_frame_->MoveRangeSelection(selection_rect.MinXMinYCorner(),
+ selection_rect.MaxXMaxYCorner());
+ EXPECT_FALSE(GetSelectionText().IsEmpty());
+
+ EmulateMouseClick(left_point_in_link_, WebMouseEvent::Button::kLeft,
+ WebInputEvent::kAltKey);
+ EXPECT_EQ(kWebNavigationPolicyDownload,
+ test_frame_client_.GetLastNavigationPolicy());
+}
+
+class LinkSelectionClickEventsTest : public LinkSelectionTestBase {
+ protected:
+ class MockEventListener final : public EventListener {
+ public:
+ static MockEventListener* Create() { return new MockEventListener(); }
+
+ bool operator==(const EventListener& other) const final {
+ return this == &other;
+ }
+
+ MOCK_METHOD2(handleEvent, void(ExecutionContext* executionContext, Event*));
+
+ private:
+ MockEventListener() : EventListener(kCPPEventListenerType) {}
+ };
+
+ void SetUp() override {
+ const char* const kHTMLString =
+ "<div id='empty_div' style='width: 100px; height: 100px;'></div>"
+ "<span id='text_div'>Sometexttoshow</span>";
+
+ web_view_ = helper_.Initialize();
+ main_frame_ = web_view_->MainFrameImpl();
+ FrameTestHelpers::LoadHTMLString(
+ main_frame_, kHTMLString, URLTestHelpers::ToKURL("http://foobar.com"));
+ web_view_->Resize(WebSize(800, 600));
+ web_view_->GetPage()->GetFocusController().SetActive(true);
+
+ auto* document = main_frame_->GetFrame()->GetDocument();
+ ASSERT_NE(nullptr, document);
+
+ auto* empty_div = document->getElementById("empty_div");
+ auto* text_div = document->getElementById("text_div");
+ ASSERT_NE(nullptr, empty_div);
+ ASSERT_NE(nullptr, text_div);
+ }
+
+ void CheckMouseClicks(Element& element, bool double_click_event) {
+ struct ScopedListenersCleaner {
+ ScopedListenersCleaner(Element* element) : element_(element) {}
+
+ ~ScopedListenersCleaner() { element_->RemoveAllEventListeners(); }
+
+ Persistent<Element> element_;
+ } const listeners_cleaner(&element);
+
+ MockEventListener* event_handler = MockEventListener::Create();
+ element.addEventListener(
+ double_click_event ? EventTypeNames::dblclick : EventTypeNames::click,
+ event_handler);
+
+ testing::InSequence s;
+ EXPECT_CALL(*event_handler, handleEvent(_, _)).Times(1);
+
+ const auto& elem_bounds = element.BoundsInViewport();
+ const int click_count = double_click_event ? 2 : 1;
+ EmulateMouseClick(elem_bounds.Center(), WebMouseEvent::Button::kLeft, 0,
+ click_count);
+
+ if (double_click_event) {
+ EXPECT_EQ(element.innerText().IsEmpty(), GetSelectionText().IsEmpty());
+ }
+ }
+};
+
+TEST_F(LinkSelectionClickEventsTest, SingleAndDoubleClickWillBeHandled) {
+ auto* document = main_frame_->GetFrame()->GetDocument();
+ auto* element = document->getElementById("empty_div");
+
+ {
+ SCOPED_TRACE("Empty div, single click");
+ CheckMouseClicks(*element, false);
+ }
+
+ {
+ SCOPED_TRACE("Empty div, double click");
+ CheckMouseClicks(*element, true);
+ }
+
+ element = document->getElementById("text_div");
+
+ {
+ SCOPED_TRACE("Text div, single click");
+ CheckMouseClicks(*element, false);
+ }
+
+ {
+ SCOPED_TRACE("Text div, double click");
+ CheckMouseClicks(*element, true);
+ }
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/local_caret_rect.cc b/chromium/third_party/blink/renderer/core/editing/local_caret_rect.cc
new file mode 100644
index 00000000000..d14410a80c5
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/local_caret_rect.cc
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/local_caret_rect.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/inline_box_position.h"
+#include "third_party/blink/renderer/core/editing/ng_flat_tree_shorthands.h"
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/layout/api/line_layout_api_shim.h"
+#include "third_party/blink/renderer/core/layout/line/inline_text_box.h"
+#include "third_party/blink/renderer/core/layout/line/root_inline_box.h"
+#include "third_party/blink/renderer/core/layout/ng/inline/ng_caret_rect.h"
+#include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h"
+
+namespace blink {
+
+namespace {
+
+// Returns true if |layout_object| and |offset| points after line end.
+template <typename Strategy>
+bool NeedsLineEndAdjustment(
+ const PositionWithAffinityTemplate<Strategy>& adjusted) {
+ const PositionTemplate<Strategy>& position = adjusted.GetPosition();
+ const LayoutObject& layout_object = *position.AnchorNode()->GetLayoutObject();
+ if (!layout_object.IsText())
+ return false;
+ const LayoutText& layout_text = ToLayoutText(layout_object);
+ if (layout_text.IsBR())
+ return position.IsAfterAnchor();
+ // For normal text nodes.
+ if (!layout_text.Style()->PreserveNewline())
+ return false;
+ if (!layout_text.TextLength() ||
+ layout_text.CharacterAt(layout_text.TextLength() - 1) != '\n')
+ return false;
+ if (position.IsAfterAnchor())
+ return true;
+ return position.IsOffsetInAnchor() &&
+ position.OffsetInContainerNode() ==
+ static_cast<int>(layout_text.TextLength());
+}
+
+// Returns the first InlineBoxPosition at next line of last InlineBoxPosition
+// in |layout_object| if it exists to avoid making InlineBoxPosition at end of
+// line.
+template <typename Strategy>
+InlineBoxPosition NextLinePositionOf(
+ const PositionWithAffinityTemplate<Strategy>& adjusted) {
+ const PositionTemplate<Strategy>& position = adjusted.GetPosition();
+ const LayoutText& layout_text =
+ ToLayoutTextOrDie(*position.AnchorNode()->GetLayoutObject());
+ InlineTextBox* const last = layout_text.LastTextBox();
+ if (!last)
+ return InlineBoxPosition();
+ const RootInlineBox& root = last->Root();
+ for (const RootInlineBox* runner = root.NextRootBox(); runner;
+ runner = runner->NextRootBox()) {
+ InlineBox* const inline_box = runner->FirstLeafChild();
+ if (!inline_box)
+ continue;
+
+ return AdjustInlineBoxPositionForTextDirection(
+ inline_box, inline_box->CaretMinOffset(),
+ layout_text.Style()->GetUnicodeBidi());
+ }
+ return InlineBoxPosition();
+}
+
+template <typename Strategy>
+LocalCaretRect LocalCaretRectOfPositionTemplate(
+ const PositionWithAffinityTemplate<Strategy>& position,
+ LayoutUnit* extra_width_to_end_of_line) {
+ if (position.IsNull())
+ return LocalCaretRect();
+ Node* const node = position.AnchorNode();
+ LayoutObject* const layout_object = node->GetLayoutObject();
+ if (!layout_object)
+ return LocalCaretRect();
+
+ const PositionWithAffinityTemplate<Strategy>& adjusted =
+ ComputeInlineAdjustedPosition(position);
+
+ if (adjusted.IsNotNull()) {
+ if (const LayoutBlockFlow* context =
+ NGInlineFormattingContextOf(adjusted.GetPosition()))
+ return ComputeNGLocalCaretRect(*context, adjusted);
+
+ // TODO(editing-dev): This DCHECK is for ensuring the correctness of
+ // breaking |ComputeInlineBoxPosition| into |ComputeInlineAdjustedPosition|
+ // and |ComputeInlineBoxPositionForInlineAdjustedPosition|. If there is any
+ // DCHECK hit, we should pass primary direction to the latter function.
+ // TODO(crbug.com/793098): Fix it so that we don't need to bother about
+ // primary direction.
+ DCHECK_EQ(PrimaryDirectionOf(*position.AnchorNode()),
+ PrimaryDirectionOf(*adjusted.AnchorNode()));
+ const InlineBoxPosition& box_position =
+ NeedsLineEndAdjustment(adjusted)
+ ? NextLinePositionOf(adjusted)
+ : ComputeInlineBoxPositionForInlineAdjustedPosition(adjusted);
+
+ if (box_position.inline_box) {
+ const LayoutObject* box_layout_object =
+ LineLayoutAPIShim::LayoutObjectFrom(
+ box_position.inline_box->GetLineLayoutItem());
+ return LocalCaretRect(
+ box_layout_object,
+ box_layout_object->LocalCaretRect(box_position.inline_box,
+ box_position.offset_in_box,
+ extra_width_to_end_of_line));
+ }
+ }
+
+ // DeleteSelectionCommandTest.deleteListFromTable goes here.
+ return LocalCaretRect(
+ layout_object, layout_object->LocalCaretRect(
+ nullptr, position.GetPosition().ComputeEditingOffset(),
+ extra_width_to_end_of_line));
+}
+
+// This function was added because the caret rect that is calculated by
+// using the line top value instead of the selection top.
+template <typename Strategy>
+LocalCaretRect LocalSelectionRectOfPositionTemplate(
+ const PositionWithAffinityTemplate<Strategy>& position) {
+ if (position.IsNull())
+ return LocalCaretRect();
+ Node* const node = position.AnchorNode();
+ if (!node->GetLayoutObject())
+ return LocalCaretRect();
+
+ const PositionWithAffinityTemplate<Strategy>& adjusted =
+ ComputeInlineAdjustedPosition(position);
+ if (adjusted.IsNull())
+ return LocalCaretRect();
+
+ if (const LayoutBlockFlow* context =
+ NGInlineFormattingContextOf(adjusted.GetPosition())) {
+ // TODO(editing-dev): Use selection height instead of caret height, or
+ // decide if we need to keep the distinction between caret height and
+ // selection height in NG.
+ return ComputeNGLocalCaretRect(*context, adjusted);
+ }
+
+ // TODO(editing-dev): This DCHECK is for ensuring the correctness of
+ // breaking |ComputeInlineBoxPosition| into |ComputeInlineAdjustedPosition|
+ // and |ComputeInlineBoxPositionForInlineAdjustedPosition|. If there is any
+ // DCHECK hit, we should pass primary direction to the latter function.
+ // TODO(crbug.com/793098): Fix it so that we don't need to bother about
+ // primary direction.
+ DCHECK_EQ(PrimaryDirectionOf(*position.AnchorNode()),
+ PrimaryDirectionOf(*adjusted.AnchorNode()));
+ const InlineBoxPosition& box_position =
+ ComputeInlineBoxPositionForInlineAdjustedPosition(adjusted);
+
+ if (!box_position.inline_box)
+ return LocalCaretRect();
+
+ LayoutObject* const layout_object = LineLayoutAPIShim::LayoutObjectFrom(
+ box_position.inline_box->GetLineLayoutItem());
+
+ const LayoutRect& rect = layout_object->LocalCaretRect(
+ box_position.inline_box, box_position.offset_in_box);
+
+ if (rect.IsEmpty())
+ return LocalCaretRect();
+
+ const InlineBox* const box = box_position.inline_box;
+ if (layout_object->Style()->IsHorizontalWritingMode()) {
+ return LocalCaretRect(
+ layout_object,
+ LayoutRect(LayoutPoint(rect.X(), box->Root().SelectionTop()),
+ LayoutSize(rect.Width(), box->Root().SelectionHeight())));
+ }
+
+ return LocalCaretRect(
+ layout_object,
+ LayoutRect(LayoutPoint(box->Root().SelectionTop(), rect.Y()),
+ LayoutSize(box->Root().SelectionHeight(), rect.Height())));
+}
+
+} // namespace
+
+LocalCaretRect LocalCaretRectOfPosition(
+ const PositionWithAffinity& position,
+ LayoutUnit* extra_width_to_end_of_line) {
+ return LocalCaretRectOfPositionTemplate<EditingStrategy>(
+ position, extra_width_to_end_of_line);
+}
+
+LocalCaretRect LocalCaretRectOfPosition(
+ const PositionInFlatTreeWithAffinity& position) {
+ return LocalCaretRectOfPositionTemplate<EditingInFlatTreeStrategy>(position,
+ nullptr);
+}
+
+LocalCaretRect LocalSelectionRectOfPosition(
+ const PositionWithAffinity& position) {
+ return LocalSelectionRectOfPositionTemplate<EditingStrategy>(position);
+}
+
+// ----
+
+template <typename Strategy>
+static IntRect AbsoluteCaretBoundsOfAlgorithm(
+ const VisiblePositionTemplate<Strategy>& visible_position) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ const LocalCaretRect& caret_rect =
+ LocalCaretRectOfPosition(visible_position.ToPositionWithAffinity());
+ if (caret_rect.IsEmpty())
+ return IntRect();
+ return LocalToAbsoluteQuadOf(caret_rect).EnclosingBoundingBox();
+}
+
+IntRect AbsoluteCaretBoundsOf(const VisiblePosition& visible_position) {
+ return AbsoluteCaretBoundsOfAlgorithm<EditingStrategy>(visible_position);
+}
+
+// TODO(editing-dev): This function does pretty much the same thing as
+// |AbsoluteCaretBoundsOf()|. Consider merging them.
+IntRect AbsoluteCaretRectOfPosition(const PositionWithAffinity& position,
+ LayoutUnit* extra_width_to_end_of_line) {
+ const LocalCaretRect local_caret_rect =
+ LocalCaretRectOfPosition(position, extra_width_to_end_of_line);
+ if (!local_caret_rect.layout_object)
+ return IntRect();
+
+ const IntRect local_rect = PixelSnappedIntRect(local_caret_rect.rect);
+ return local_rect == IntRect()
+ ? IntRect()
+ : local_caret_rect.layout_object
+ ->LocalToAbsoluteQuad(FloatRect(local_rect))
+ .EnclosingBoundingBox();
+}
+
+template <typename Strategy>
+static IntRect AbsoluteSelectionBoundsOfAlgorithm(
+ const VisiblePositionTemplate<Strategy>& visible_position) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ const LocalCaretRect& caret_rect =
+ LocalSelectionRectOfPosition(visible_position.ToPositionWithAffinity());
+ if (caret_rect.IsEmpty())
+ return IntRect();
+ return LocalToAbsoluteQuadOf(caret_rect).EnclosingBoundingBox();
+}
+
+IntRect AbsoluteSelectionBoundsOf(const VisiblePosition& visible_position) {
+ return AbsoluteSelectionBoundsOfAlgorithm<EditingStrategy>(visible_position);
+}
+
+IntRect AbsoluteCaretBoundsOf(
+ const VisiblePositionInFlatTree& visible_position) {
+ return AbsoluteCaretBoundsOfAlgorithm<EditingInFlatTreeStrategy>(
+ visible_position);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/local_caret_rect.h b/chromium/third_party/blink/renderer/core/editing/local_caret_rect.h
new file mode 100644
index 00000000000..9e6de0ba55f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/local_caret_rect.h
@@ -0,0 +1,57 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_LOCAL_CARET_RECT_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_LOCAL_CARET_RECT_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/geometry/layout_rect.h"
+
+namespace blink {
+
+class LayoutObject;
+
+// A transient struct representing a caret rect local to |layout_object|.
+struct LocalCaretRect {
+ STACK_ALLOCATED();
+
+ const LayoutObject* layout_object = nullptr;
+ LayoutRect rect;
+
+ LocalCaretRect() = default;
+ LocalCaretRect(const LayoutObject* layout_object, const LayoutRect& rect)
+ : layout_object(layout_object), rect(rect) {}
+
+ bool IsEmpty() const { return !layout_object || rect.IsEmpty(); }
+};
+
+// Rect is local to the returned layoutObject
+// TODO(xiaochengh): Get rid of the default parameter.
+CORE_EXPORT LocalCaretRect LocalCaretRectOfPosition(
+ const PositionWithAffinity&,
+ LayoutUnit* /* extra_width_to_end_of_line */ = nullptr);
+CORE_EXPORT LocalCaretRect
+LocalCaretRectOfPosition(const PositionInFlatTreeWithAffinity&);
+
+LocalCaretRect LocalSelectionRectOfPosition(const PositionWithAffinity&);
+
+// Bounds of (possibly transformed) caret in absolute coords
+CORE_EXPORT IntRect AbsoluteCaretBoundsOf(const VisiblePosition&);
+CORE_EXPORT IntRect AbsoluteCaretBoundsOf(const VisiblePositionInFlatTree&);
+
+IntRect AbsoluteCaretRectOfPosition(
+ const PositionWithAffinity&,
+ LayoutUnit* extra_width_to_end_of_line = nullptr);
+
+CORE_EXPORT IntRect AbsoluteSelectionBoundsOf(const VisiblePosition&);
+CORE_EXPORT IntRect AbsoluteSelectionBoundsOf(const VisiblePositionInFlatTree&);
+
+// Exposed to tests only. Implemented in LocalCaretRectTest.cpp.
+bool operator==(const LocalCaretRect&, const LocalCaretRect&);
+std::ostream& operator<<(std::ostream&, const LocalCaretRect&);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_LOCAL_CARET_RECT_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/local_caret_rect_bidi_test.cc b/chromium/third_party/blink/renderer/core/editing/local_caret_rect_bidi_test.cc
new file mode 100644
index 00000000000..98b6b046b3f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/local_caret_rect_bidi_test.cc
@@ -0,0 +1,1644 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/local_caret_rect.h"
+
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/text_affinity.h"
+
+namespace blink {
+
+class LocalCaretRectBidiTest : public EditingTestBase {};
+
+// This file contains script-generated tests for LocalCaretRectOfPosition()
+// that are related to Bidirectional text. The test cases are only for
+// behavior recording purposes, and do not necessarily reflect the
+// correct/desired behavior.
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockLtrBaseRunAfterRtlRunTouchingLineBoundary) {
+ // Sample: A B C|d e f
+ // Bidi: 1 1 1 0 0 0
+ // Visual: C B A|d e f
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr><bdo dir=rtl>ABC</bdo>|def</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(30, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLtrBaseRunAfterRtlRun) {
+ // Sample: g h i A B C|d e f
+ // Bidi: 0 0 0 1 1 1 0 0 0
+ // Visual: g h i C B A|d e f
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr>ghi<bdo dir=rtl>ABC</bdo>|def</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(60, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockLtrBaseRunAfterRtlRunTouchingLineBoundaryAtDeepPosition) {
+ // Sample: A B C|d e f
+ // Bidi: 1 1 1 0 0 0
+ // Visual: C B A|d e f
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr><bdo dir=rtl>ABC|</bdo>def</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(30, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLtrBaseRunAfterRtlRunAtDeepPosition) {
+ // Sample: g h i A B C|d e f
+ // Bidi: 0 0 0 1 1 1 0 0 0
+ // Visual: g h i C B A|d e f
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr>ghi<bdo dir=rtl>ABC|</bdo>def</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(60, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLtrBaseRunAfterTwoNestedRuns) {
+ // Sample: D E F a b c|g h i
+ // Bidi: 1 1 1 2 2 2 0 0 0
+ // Visual: a b c F E D|g h i
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr><bdo dir=rtl>DEF<bdo "
+ "dir=ltr>abc</bdo></bdo>|ghi</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(60, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockLtrBaseRunAfterTwoNestedRunsAtDeepPosition) {
+ // Sample: D E F a b c|g h i
+ // Bidi: 1 1 1 2 2 2 0 0 0
+ // Visual: a b c F E D|g h i
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr><bdo dir=rtl>DEF<bdo "
+ "dir=ltr>abc|</bdo></bdo>ghi</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(60, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLtrBaseRunAfterThreeNestedRuns) {
+ // Sample: G H I d e f A B C|j k l
+ // Bidi: 1 1 1 2 2 2 3 3 3 0 0 0
+ // Visual: d e f C B A I H G|j k l
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr><bdo dir=rtl>GHI<bdo dir=ltr>def<bdo "
+ "dir=rtl>ABC</bdo></bdo></bdo>|jkl</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(90, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockLtrBaseRunAfterThreeNestedRunsAtDeepPosition) {
+ // Sample: G H I d e f A B C|j k l
+ // Bidi: 1 1 1 2 2 2 3 3 3 0 0 0
+ // Visual: d e f C B A I H G|j k l
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr><bdo dir=rtl>GHI<bdo dir=ltr>def<bdo "
+ "dir=rtl>ABC|</bdo></bdo></bdo>jkl</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(90, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLtrBaseRunAfterFourNestedRuns) {
+ // Sample: J K L g h i D E F a b c|m n o
+ // Bidi: 1 1 1 2 2 2 3 3 3 4 4 4 0 0 0
+ // Visual: g h i a b c F E D L K J|m n o
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr><bdo dir=rtl>JKL<bdo dir=ltr>ghi<bdo "
+ "dir=rtl>DEF<bdo dir=ltr>abc</bdo></bdo></bdo></bdo>|mno</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(120, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockLtrBaseRunAfterFourNestedRunsAtDeepPosition) {
+ // Sample: J K L g h i D E F a b c|m n o
+ // Bidi: 1 1 1 2 2 2 3 3 3 4 4 4 0 0 0
+ // Visual: g h i a b c F E D L K J|m n o
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr><bdo dir=rtl>JKL<bdo dir=ltr>ghi<bdo "
+ "dir=rtl>DEF<bdo dir=ltr>abc|</bdo></bdo></bdo></bdo>mno</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(120, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockLtrBaseRunBeforeRtlRunTouchingLineBoundary) {
+ // Sample: d e f|A B C
+ // Bidi: 0 0 0 1 1 1
+ // Visual: d e f|C B A
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr>def|<bdo dir=rtl>ABC</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(30, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLtrBaseRunBeforeRtlRun) {
+ // Sample: d e f|A B C g h i
+ // Bidi: 0 0 0 1 1 1 0 0 0
+ // Visual: d e f|C B A g h i
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr>def|<bdo dir=rtl>ABC</bdo>ghi</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(30, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockLtrBaseRunBeforeRtlRunTouchingLineBoundaryAtDeepPosition) {
+ // Sample: d e f|A B C
+ // Bidi: 0 0 0 1 1 1
+ // Visual: d e f|C B A
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr>def<bdo dir=rtl>|ABC</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(30, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLtrBaseRunBeforeRtlRunAtDeepPosition) {
+ // Sample: d e f|A B C g h i
+ // Bidi: 0 0 0 1 1 1 0 0 0
+ // Visual: d e f|C B A g h i
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr>def<bdo dir=rtl>|ABC</bdo>ghi</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(30, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLtrBaseRunBeforeTwoNestedRuns) {
+ // Sample: g h i|a b c D E F
+ // Bidi: 0 0 0 2 2 2 1 1 1
+ // Visual: g h i|F E D a b c
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr>ghi|<bdo dir=rtl><bdo "
+ "dir=ltr>abc</bdo>DEF</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(30, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockLtrBaseRunBeforeTwoNestedRunsAtDeepPosition) {
+ // Sample: g h i|a b c D E F
+ // Bidi: 0 0 0 2 2 2 1 1 1
+ // Visual: g h i|F E D a b c
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr>ghi<bdo dir=rtl><bdo "
+ "dir=ltr>|abc</bdo>DEF</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(30, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLtrBaseRunBeforeThreeNestedRuns) {
+ // Sample: j k l|A B C d e f G H I
+ // Bidi: 0 0 0 3 3 3 2 2 2 1 1 1
+ // Visual: j k l I H G|C B A d e f
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr>jkl|<bdo dir=rtl><bdo dir=ltr><bdo "
+ "dir=rtl>ABC</bdo>def</bdo>GHI</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(60, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockLtrBaseRunBeforeThreeNestedRunsAtDeepPosition) {
+ // Sample: j k l|A B C d e f G H I
+ // Bidi: 0 0 0 3 3 3 2 2 2 1 1 1
+ // Visual: j k l I H G|C B A d e f
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr>jkl<bdo dir=rtl><bdo dir=ltr><bdo "
+ "dir=rtl>|ABC</bdo>def</bdo>GHI</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(60, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLtrBaseRunBeforeFourNestedRuns) {
+ // Sample: m n o|a b c D E F g h i J K L
+ // Bidi: 0 0 0 4 4 4 3 3 3 2 2 2 1 1 1
+ // Visual: m n o L K J|F E D a b c g h i
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr>mno|<bdo dir=rtl><bdo dir=ltr><bdo "
+ "dir=rtl><bdo dir=ltr>abc</bdo>DEF</bdo>ghi</bdo>JKL</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(60, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockLtrBaseRunBeforeFourNestedRunsAtDeepPosition) {
+ // Sample: m n o|a b c D E F g h i J K L
+ // Bidi: 0 0 0 4 4 4 3 3 3 2 2 2 1 1 1
+ // Visual: m n o L K J|F E D a b c g h i
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr>mno<bdo dir=rtl><bdo dir=ltr><bdo "
+ "dir=rtl><bdo dir=ltr>|abc</bdo>DEF</bdo>ghi</bdo>JKL</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(60, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockRtlBaseRunAfterLtrRunTouchingLineBoundary) {
+ // Sample: a b c|D E F
+ // Bidi: 2 2 2 1 1 1
+ // Visual: F E D a b c|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl><bdo dir=ltr>abc</bdo>|DEF</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(60, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockRtlBaseRunAfterLtrRun) {
+ // Sample: G H I a b c|D E F
+ // Bidi: 1 1 1 2 2 2 1 1 1
+ // Visual: F E D a b c|I H G
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl>GHI<bdo dir=ltr>abc</bdo>|DEF</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(60, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockRtlBaseRunAfterLtrRunTouchingLineBoundaryAtDeepPosition) {
+ // Sample: a b c|D E F
+ // Bidi: 2 2 2 1 1 1
+ // Visual: F E D a b c|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl><bdo dir=ltr>abc|</bdo>DEF</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(60, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockRtlBaseRunAfterLtrRunAtDeepPosition) {
+ // Sample: G H I a b c|D E F
+ // Bidi: 1 1 1 2 2 2 1 1 1
+ // Visual: F E D a b c|I H G
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl>GHI<bdo dir=ltr>abc|</bdo>DEF</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(60, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockRtlBaseRunAfterTwoNestedRuns) {
+ // Sample: d e f A B C|G H I
+ // Bidi: 2 2 2 3 3 3 1 1 1
+ // Visual: I H G d e f C B A|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl><bdo dir=ltr>def<bdo "
+ "dir=rtl>ABC</bdo></bdo>|GHI</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(90, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockRtlBaseRunAfterTwoNestedRunsAtDeepPosition) {
+ // Sample: d e f A B C|G H I
+ // Bidi: 2 2 2 3 3 3 1 1 1
+ // Visual: I H G d e f C B A|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl><bdo dir=ltr>def<bdo "
+ "dir=rtl>ABC|</bdo></bdo>GHI</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(90, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockRtlBaseRunAfterThreeNestedRuns) {
+ // Sample: g h i D E F a b c|J K L
+ // Bidi: 2 2 2 3 3 3 4 4 4 1 1 1
+ // Visual: L K J g h i a b c F E D|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl><bdo dir=ltr>ghi<bdo dir=rtl>DEF<bdo "
+ "dir=ltr>abc</bdo></bdo></bdo>|JKL</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(120, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockRtlBaseRunAfterThreeNestedRunsAtDeepPosition) {
+ // Sample: g h i D E F a b c|J K L
+ // Bidi: 2 2 2 3 3 3 4 4 4 1 1 1
+ // Visual: L K J g h i a b c F E D|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl><bdo dir=ltr>ghi<bdo dir=rtl>DEF<bdo "
+ "dir=ltr>abc|</bdo></bdo></bdo>JKL</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(120, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockRtlBaseRunAfterFourNestedRuns) {
+ // Sample: j k l G H I d e f A B C|M N O
+ // Bidi: 2 2 2 3 3 3 4 4 4 5 5 5 1 1 1
+ // Visual: O N M j k l d e f C B A I H G|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl><bdo dir=ltr>jkl<bdo dir=rtl>GHI<bdo "
+ "dir=ltr>def<bdo dir=rtl>ABC</bdo></bdo></bdo></bdo>|MNO</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(150, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockRtlBaseRunAfterFourNestedRunsAtDeepPosition) {
+ // Sample: j k l G H I d e f A B C|M N O
+ // Bidi: 2 2 2 3 3 3 4 4 4 5 5 5 1 1 1
+ // Visual: O N M j k l d e f C B A I H G|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl><bdo dir=ltr>jkl<bdo dir=rtl>GHI<bdo "
+ "dir=ltr>def<bdo dir=rtl>ABC|</bdo></bdo></bdo></bdo>MNO</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(150, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockRtlBaseRunBeforeLtrRunTouchingLineBoundary) {
+ // Sample: D E F|a b c
+ // Bidi: 1 1 1 2 2 2
+ // Visual:|a b c F E D
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl>DEF|<bdo dir=ltr>abc</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(0, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockRtlBaseRunBeforeLtrRun) {
+ // Sample: D E F|a b c G H I
+ // Bidi: 1 1 1 2 2 2 1 1 1
+ // Visual: I H G|a b c F E D
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl>DEF|<bdo dir=ltr>abc</bdo>GHI</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(30, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockRtlBaseRunBeforeLtrRunTouchingLineBoundaryAtDeepPosition) {
+ // Sample: D E F|a b c
+ // Bidi: 1 1 1 2 2 2
+ // Visual:|a b c F E D
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl>DEF<bdo dir=ltr>|abc</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(0, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockRtlBaseRunBeforeLtrRunAtDeepPosition) {
+ // Sample: D E F|a b c G H I
+ // Bidi: 1 1 1 2 2 2 1 1 1
+ // Visual: I H G|a b c F E D
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl>DEF<bdo dir=ltr>|abc</bdo>GHI</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(30, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockRtlBaseRunBeforeTwoNestedRuns) {
+ // Sample: G H I|A B C d e f
+ // Bidi: 1 1 1 3 3 3 2 2 2
+ // Visual:|C B A d e f I H G
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl>GHI|<bdo dir=ltr><bdo "
+ "dir=rtl>ABC</bdo>def</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(0, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockRtlBaseRunBeforeTwoNestedRunsAtDeepPosition) {
+ // Sample: G H I|A B C d e f
+ // Bidi: 1 1 1 3 3 3 2 2 2
+ // Visual:|C B A d e f I H G
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl>GHI<bdo dir=ltr><bdo "
+ "dir=rtl>|ABC</bdo>def</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(0, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockRtlBaseRunBeforeThreeNestedRuns) {
+ // Sample: J K L|a b c D E F g h i
+ // Bidi: 1 1 1 4 4 4 3 3 3 2 2 2
+ // Visual:|F E D a b c g h i L K J
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl>JKL|<bdo dir=ltr><bdo dir=rtl><bdo "
+ "dir=ltr>abc</bdo>DEF</bdo>ghi</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(0, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockRtlBaseRunBeforeThreeNestedRunsAtDeepPosition) {
+ // Sample: J K L|a b c D E F g h i
+ // Bidi: 1 1 1 4 4 4 3 3 3 2 2 2
+ // Visual:|F E D a b c g h i L K J
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl>JKL<bdo dir=ltr><bdo dir=rtl><bdo "
+ "dir=ltr>|abc</bdo>DEF</bdo>ghi</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(0, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockRtlBaseRunBeforeFourNestedRuns) {
+ // Sample: M N O|A B C d e f G H I j k l
+ // Bidi: 1 1 1 5 5 5 4 4 4 3 3 3 2 2 2
+ // Visual: I H G|C B A d e f j k l O N M
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl>MNO|<bdo dir=ltr><bdo dir=rtl><bdo "
+ "dir=ltr><bdo dir=rtl>ABC</bdo>def</bdo>GHI</bdo>jkl</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(30, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockRtlBaseRunBeforeFourNestedRunsAtDeepPosition) {
+ // Sample: M N O|A B C d e f G H I j k l
+ // Bidi: 1 1 1 5 5 5 4 4 4 3 3 3 2 2 2
+ // Visual: I H G|C B A d e f j k l O N M
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl>MNO<bdo dir=ltr><bdo dir=rtl><bdo "
+ "dir=ltr><bdo dir=rtl>|ABC</bdo>def</bdo>GHI</bdo>jkl</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(30, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockLtrBaseRunAfterRtlRunTouchingLineBoundary) {
+ // Sample: A B C|d e f
+ // Bidi: 3 3 3 2 2 2
+ // Visual:|C B A d e f
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr><bdo dir=rtl>ABC</bdo>|def</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(240, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLtrBaseRunAfterRtlRun) {
+ // Sample: g h i A B C|d e f
+ // Bidi: 2 2 2 3 3 3 2 2 2
+ // Visual: g h i|C B A d e f
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr>ghi<bdo dir=rtl>ABC</bdo>|def</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(240, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockLtrBaseRunAfterRtlRunTouchingLineBoundaryAtDeepPosition) {
+ // Sample: A B C|d e f
+ // Bidi: 3 3 3 2 2 2
+ // Visual:|C B A d e f
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr><bdo dir=rtl>ABC|</bdo>def</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(240, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLtrBaseRunAfterRtlRunAtDeepPosition) {
+ // Sample: g h i A B C|d e f
+ // Bidi: 2 2 2 3 3 3 2 2 2
+ // Visual: g h i|C B A d e f
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr>ghi<bdo dir=rtl>ABC|</bdo>def</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(240, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLtrBaseRunAfterTwoNestedRuns) {
+ // Sample: D E F a b c|g h i
+ // Bidi: 3 3 3 4 4 4 2 2 2
+ // Visual:|a b c F E D g h i
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr><bdo dir=rtl>DEF<bdo "
+ "dir=ltr>abc</bdo></bdo>|ghi</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(210, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockLtrBaseRunAfterTwoNestedRunsAtDeepPosition) {
+ // Sample: D E F a b c|g h i
+ // Bidi: 3 3 3 4 4 4 2 2 2
+ // Visual:|a b c F E D g h i
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr><bdo dir=rtl>DEF<bdo "
+ "dir=ltr>abc|</bdo></bdo>ghi</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(210, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLtrBaseRunAfterThreeNestedRuns) {
+ // Sample: G H I d e f A B C|j k l
+ // Bidi: 3 3 3 4 4 4 5 5 5 2 2 2
+ // Visual:|d e f C B A I H G j k l
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr><bdo dir=rtl>GHI<bdo dir=ltr>def<bdo "
+ "dir=rtl>ABC</bdo></bdo></bdo>|jkl</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(180, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockLtrBaseRunAfterThreeNestedRunsAtDeepPosition) {
+ // Sample: G H I d e f A B C|j k l
+ // Bidi: 3 3 3 4 4 4 5 5 5 2 2 2
+ // Visual:|d e f C B A I H G j k l
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr><bdo dir=rtl>GHI<bdo dir=ltr>def<bdo "
+ "dir=rtl>ABC|</bdo></bdo></bdo>jkl</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(180, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLtrBaseRunAfterFourNestedRuns) {
+ // Sample: J K L g h i D E F a b c|m n o
+ // Bidi: 3 3 3 4 4 4 5 5 5 6 6 6 2 2 2
+ // Visual:|g h i a b c F E D L K J m n o
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr><bdo dir=rtl>JKL<bdo dir=ltr>ghi<bdo "
+ "dir=rtl>DEF<bdo dir=ltr>abc</bdo></bdo></bdo></bdo>|mno</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(150, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockLtrBaseRunAfterFourNestedRunsAtDeepPosition) {
+ // Sample: J K L g h i D E F a b c|m n o
+ // Bidi: 3 3 3 4 4 4 5 5 5 6 6 6 2 2 2
+ // Visual:|g h i a b c F E D L K J m n o
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr><bdo dir=rtl>JKL<bdo dir=ltr>ghi<bdo "
+ "dir=rtl>DEF<bdo dir=ltr>abc|</bdo></bdo></bdo></bdo>mno</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(150, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockLtrBaseRunBeforeRtlRunTouchingLineBoundary) {
+ // Sample: d e f|A B C
+ // Bidi: 2 2 2 3 3 3
+ // Visual: d e f C B A|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr>def|<bdo dir=rtl>ABC</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(299, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLtrBaseRunBeforeRtlRun) {
+ // Sample: d e f|A B C g h i
+ // Bidi: 2 2 2 3 3 3 2 2 2
+ // Visual: d e f C B A|g h i
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr>def|<bdo dir=rtl>ABC</bdo>ghi</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(270, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockLtrBaseRunBeforeRtlRunTouchingLineBoundaryAtDeepPosition) {
+ // Sample: d e f|A B C
+ // Bidi: 2 2 2 3 3 3
+ // Visual: d e f C B A|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr>def<bdo dir=rtl>|ABC</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(299, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLtrBaseRunBeforeRtlRunAtDeepPosition) {
+ // Sample: d e f|A B C g h i
+ // Bidi: 2 2 2 3 3 3 2 2 2
+ // Visual: d e f C B A|g h i
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr>def<bdo dir=rtl>|ABC</bdo>ghi</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(270, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLtrBaseRunBeforeTwoNestedRuns) {
+ // Sample: g h i|a b c D E F
+ // Bidi: 2 2 2 4 4 4 3 3 3
+ // Visual: g h i F E D a b c|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr>ghi|<bdo dir=rtl><bdo "
+ "dir=ltr>abc</bdo>DEF</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(299, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockLtrBaseRunBeforeTwoNestedRunsAtDeepPosition) {
+ // Sample: g h i|a b c D E F
+ // Bidi: 2 2 2 4 4 4 3 3 3
+ // Visual: g h i F E D a b c|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr>ghi<bdo dir=rtl><bdo "
+ "dir=ltr>|abc</bdo>DEF</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(299, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLtrBaseRunBeforeThreeNestedRuns) {
+ // Sample: j k l|A B C d e f G H I
+ // Bidi: 2 2 2 5 5 5 4 4 4 3 3 3
+ // Visual: j k l I H G C B A d e f|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr>jkl|<bdo dir=rtl><bdo dir=ltr><bdo "
+ "dir=rtl>ABC</bdo>def</bdo>GHI</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(299, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockLtrBaseRunBeforeThreeNestedRunsAtDeepPosition) {
+ // Sample: j k l|A B C d e f G H I
+ // Bidi: 2 2 2 5 5 5 4 4 4 3 3 3
+ // Visual: j k l I H G C B A d e f|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr>jkl<bdo dir=rtl><bdo dir=ltr><bdo "
+ "dir=rtl>|ABC</bdo>def</bdo>GHI</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(299, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLtrBaseRunBeforeFourNestedRuns) {
+ // Sample: m n o|a b c D E F g h i J K L
+ // Bidi: 2 2 2 6 6 6 5 5 5 4 4 4 3 3 3
+ // Visual: m n o L K J F E D a b c|g h i
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr>mno|<bdo dir=rtl><bdo dir=ltr><bdo "
+ "dir=rtl><bdo dir=ltr>abc</bdo>DEF</bdo>ghi</bdo>JKL</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(270, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockLtrBaseRunBeforeFourNestedRunsAtDeepPosition) {
+ // Sample: m n o|a b c D E F g h i J K L
+ // Bidi: 2 2 2 6 6 6 5 5 5 4 4 4 3 3 3
+ // Visual: m n o L K J F E D a b c|g h i
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr>mno<bdo dir=rtl><bdo dir=ltr><bdo "
+ "dir=rtl><bdo dir=ltr>|abc</bdo>DEF</bdo>ghi</bdo>JKL</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(270, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockRtlBaseRunAfterLtrRunTouchingLineBoundary) {
+ // Sample: a b c|D E F
+ // Bidi: 2 2 2 1 1 1
+ // Visual: F E D|a b c
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl><bdo dir=ltr>abc</bdo>|DEF</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(270, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockRtlBaseRunAfterLtrRun) {
+ // Sample: G H I a b c|D E F
+ // Bidi: 1 1 1 2 2 2 1 1 1
+ // Visual: F E D|a b c I H G
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl>GHI<bdo dir=ltr>abc</bdo>|DEF</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(240, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockRtlBaseRunAfterLtrRunTouchingLineBoundaryAtDeepPosition) {
+ // Sample: a b c|D E F
+ // Bidi: 2 2 2 1 1 1
+ // Visual: F E D|a b c
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl><bdo dir=ltr>abc|</bdo>DEF</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(270, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockRtlBaseRunAfterLtrRunAtDeepPosition) {
+ // Sample: G H I a b c|D E F
+ // Bidi: 1 1 1 2 2 2 1 1 1
+ // Visual: F E D|a b c I H G
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl>GHI<bdo dir=ltr>abc|</bdo>DEF</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(240, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockRtlBaseRunAfterTwoNestedRuns) {
+ // Sample: d e f A B C|G H I
+ // Bidi: 2 2 2 3 3 3 1 1 1
+ // Visual: I H G|d e f C B A
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl><bdo dir=ltr>def<bdo "
+ "dir=rtl>ABC</bdo></bdo>|GHI</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(240, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockRtlBaseRunAfterTwoNestedRunsAtDeepPosition) {
+ // Sample: d e f A B C|G H I
+ // Bidi: 2 2 2 3 3 3 1 1 1
+ // Visual: I H G|d e f C B A
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl><bdo dir=ltr>def<bdo "
+ "dir=rtl>ABC|</bdo></bdo>GHI</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(240, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockRtlBaseRunAfterThreeNestedRuns) {
+ // Sample: g h i D E F a b c|J K L
+ // Bidi: 2 2 2 3 3 3 4 4 4 1 1 1
+ // Visual: L K J|g h i a b c F E D
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl><bdo dir=ltr>ghi<bdo dir=rtl>DEF<bdo "
+ "dir=ltr>abc</bdo></bdo></bdo>|JKL</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(210, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockRtlBaseRunAfterThreeNestedRunsAtDeepPosition) {
+ // Sample: g h i D E F a b c|J K L
+ // Bidi: 2 2 2 3 3 3 4 4 4 1 1 1
+ // Visual: L K J|g h i a b c F E D
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl><bdo dir=ltr>ghi<bdo dir=rtl>DEF<bdo "
+ "dir=ltr>abc|</bdo></bdo></bdo>JKL</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(210, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockRtlBaseRunAfterFourNestedRuns) {
+ // Sample: j k l G H I d e f A B C|M N O
+ // Bidi: 2 2 2 3 3 3 4 4 4 5 5 5 1 1 1
+ // Visual: O N M|j k l d e f C B A I H G
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl><bdo dir=ltr>jkl<bdo dir=rtl>GHI<bdo "
+ "dir=ltr>def<bdo dir=rtl>ABC</bdo></bdo></bdo></bdo>|MNO</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(180, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockRtlBaseRunAfterFourNestedRunsAtDeepPosition) {
+ // Sample: j k l G H I d e f A B C|M N O
+ // Bidi: 2 2 2 3 3 3 4 4 4 5 5 5 1 1 1
+ // Visual: O N M|j k l d e f C B A I H G
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl><bdo dir=ltr>jkl<bdo dir=rtl>GHI<bdo "
+ "dir=ltr>def<bdo dir=rtl>ABC|</bdo></bdo></bdo></bdo>MNO</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(180, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockRtlBaseRunBeforeLtrRunTouchingLineBoundary) {
+ // Sample: D E F|a b c
+ // Bidi: 1 1 1 2 2 2
+ // Visual: a b c|F E D
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl>DEF|<bdo dir=ltr>abc</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(270, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockRtlBaseRunBeforeLtrRun) {
+ // Sample: D E F|a b c G H I
+ // Bidi: 1 1 1 2 2 2 1 1 1
+ // Visual: I H G a b c|F E D
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl>DEF|<bdo dir=ltr>abc</bdo>GHI</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(270, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockRtlBaseRunBeforeLtrRunTouchingLineBoundaryAtDeepPosition) {
+ // Sample: D E F|a b c
+ // Bidi: 1 1 1 2 2 2
+ // Visual: a b c|F E D
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl>DEF<bdo dir=ltr>|abc</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(270, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockRtlBaseRunBeforeLtrRunAtDeepPosition) {
+ // Sample: D E F|a b c G H I
+ // Bidi: 1 1 1 2 2 2 1 1 1
+ // Visual: I H G a b c|F E D
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl>DEF<bdo dir=ltr>|abc</bdo>GHI</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(270, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockRtlBaseRunBeforeTwoNestedRuns) {
+ // Sample: G H I|A B C d e f
+ // Bidi: 1 1 1 3 3 3 2 2 2
+ // Visual: C B A d e f|I H G
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl>GHI|<bdo dir=ltr><bdo "
+ "dir=rtl>ABC</bdo>def</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(270, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockRtlBaseRunBeforeTwoNestedRunsAtDeepPosition) {
+ // Sample: G H I|A B C d e f
+ // Bidi: 1 1 1 3 3 3 2 2 2
+ // Visual: C B A d e f|I H G
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl>GHI<bdo dir=ltr><bdo "
+ "dir=rtl>|ABC</bdo>def</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(270, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockRtlBaseRunBeforeThreeNestedRuns) {
+ // Sample: J K L|a b c D E F g h i
+ // Bidi: 1 1 1 4 4 4 3 3 3 2 2 2
+ // Visual: F E D a b c|g h i L K J
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl>JKL|<bdo dir=ltr><bdo dir=rtl><bdo "
+ "dir=ltr>abc</bdo>DEF</bdo>ghi</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(240, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockRtlBaseRunBeforeThreeNestedRunsAtDeepPosition) {
+ // Sample: J K L|a b c D E F g h i
+ // Bidi: 1 1 1 4 4 4 3 3 3 2 2 2
+ // Visual: F E D a b c|g h i L K J
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl>JKL<bdo dir=ltr><bdo dir=rtl><bdo "
+ "dir=ltr>|abc</bdo>DEF</bdo>ghi</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(240, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockRtlBaseRunBeforeFourNestedRuns) {
+ // Sample: M N O|A B C d e f G H I j k l
+ // Bidi: 1 1 1 5 5 5 4 4 4 3 3 3 2 2 2
+ // Visual: I H G C B A d e f|j k l O N M
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl>MNO|<bdo dir=ltr><bdo dir=rtl><bdo "
+ "dir=ltr><bdo dir=rtl>ABC</bdo>def</bdo>GHI</bdo>jkl</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(240, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockRtlBaseRunBeforeFourNestedRunsAtDeepPosition) {
+ // Sample: M N O|A B C d e f G H I j k l
+ // Bidi: 1 1 1 5 5 5 4 4 4 3 3 3 2 2 2
+ // Visual: I H G C B A d e f|j k l O N M
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl>MNO<bdo dir=ltr><bdo dir=rtl><bdo "
+ "dir=ltr><bdo dir=rtl>|ABC</bdo>def</bdo>GHI</bdo>jkl</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(240, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLineBeginLtrBaseRunWithTwoNestedRuns) {
+ // Sample:|A B C d e f
+ // Bidi: 1 1 1 0 0 0
+ // Visual:|C B A d e f
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr><bdo dir=rtl>|ABC</bdo>def</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(0, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockLineBeginLtrBaseRunWithThreeNestedRuns) {
+ // Sample:|a b c D E F g h i
+ // Bidi: 2 2 2 1 1 1 0 0 0
+ // Visual:|F E D a b c g h i
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr><bdo dir=rtl><bdo "
+ "dir=ltr>|abc</bdo>DEF</bdo>ghi</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(0, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockLineBeginLtrBaseRunWithFourNestedRuns) {
+ // Sample:|A B C d e f G H I j k l
+ // Bidi: 3 3 3 2 2 2 1 1 1 0 0 0
+ // Visual: I H G|C B A d e f j k l
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr><bdo dir=rtl><bdo dir=ltr><bdo "
+ "dir=rtl>|ABC</bdo>def</bdo>GHI</bdo>jkl</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(30, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLineEndLtrBaseRunWithTwoNestedRuns) {
+ // Sample: d e f A B C|
+ // Bidi: 0 0 0 1 1 1
+ // Visual: d e f C B A|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr>def<bdo dir=rtl>ABC|</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(60, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLineEndLtrBaseRunWithThreeNestedRuns) {
+ // Sample: g h i D E F a b c|
+ // Bidi: 0 0 0 1 1 1 2 2 2
+ // Visual: g h i a b c F E D|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr>ghi<bdo dir=rtl>DEF<bdo "
+ "dir=ltr>abc|</bdo></bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(90, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLineEndLtrBaseRunWithFourNestedRuns) {
+ // Sample: j k l G H I d e f A B C|
+ // Bidi: 0 0 0 1 1 1 2 2 2 3 3 3
+ // Visual: j k l d e f C B A|I H G
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=ltr>jkl<bdo dir=rtl>GHI<bdo dir=ltr>def<bdo "
+ "dir=rtl>ABC|</bdo></bdo></bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(90, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLineBeginWithRtlRunOnly) {
+ // Sample:|A B C
+ // Bidi: 1 1 1
+ // Visual:|C B A
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position =
+ SetCaretTextToBody("<div dir=ltr><bdo dir=rtl>|ABC</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(0, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLineBeginRtlBaseRunWithTwoNestedRuns) {
+ // Sample:|a b c D E F
+ // Bidi: 2 2 2 1 1 1
+ // Visual:|F E D a b c
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl><bdo dir=ltr>|abc</bdo>DEF</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(0, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockLineBeginRtlBaseRunWithThreeNestedRuns) {
+ // Sample:|A B C d e f G H I
+ // Bidi: 3 3 3 2 2 2 1 1 1
+ // Visual: I H G|C B A d e f
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl><bdo dir=ltr><bdo "
+ "dir=rtl>|ABC</bdo>def</bdo>GHI</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(30, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InLtrBlockLineBeginRtlBaseRunWithFourNestedRuns) {
+ // Sample:|a b c D E F g h i J K L
+ // Bidi: 4 4 4 3 3 3 2 2 2 1 1 1
+ // Visual: L K J|F E D a b c g h i
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl><bdo dir=ltr><bdo dir=rtl><bdo "
+ "dir=ltr>|abc</bdo>DEF</bdo>ghi</bdo>JKL</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(30, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLineEndWithRtlRunOnly) {
+ // Sample: A B C|
+ // Bidi: 1 1 1
+ // Visual: C B A|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position =
+ SetCaretTextToBody("<div dir=ltr><bdo dir=rtl>ABC|</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(30, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLineEndRtlBaseRunWithTwoNestedRuns) {
+ // Sample: D E F a b c|
+ // Bidi: 1 1 1 2 2 2
+ // Visual: a b c F E D|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl>DEF<bdo dir=ltr>abc|</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(60, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLineEndRtlBaseRunWithThreeNestedRuns) {
+ // Sample: G H I d e f A B C|
+ // Bidi: 1 1 1 2 2 2 3 3 3
+ // Visual: d e f C B A|I H G
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl>GHI<bdo dir=ltr>def<bdo "
+ "dir=rtl>ABC|</bdo></bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(60, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InLtrBlockLineEndRtlBaseRunWithFourNestedRuns) {
+ // Sample: J K L g h i D E F a b c|
+ // Bidi: 1 1 1 2 2 2 3 3 3 4 4 4
+ // Visual: g h i a b c F E D|L K J
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=ltr><bdo dir=rtl>JKL<bdo dir=ltr>ghi<bdo dir=rtl>DEF<bdo "
+ "dir=ltr>abc|</bdo></bdo></bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(90, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLineBeginWithLtrRunOnly) {
+ // Sample:|a b c
+ // Bidi: 2 2 2
+ // Visual: a b c|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position =
+ SetCaretTextToBody("<div dir=rtl><bdo dir=ltr>|abc</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(299, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLineBeginLtrBaseRunWithTwoNestedRuns) {
+ // Sample:|A B C d e f
+ // Bidi: 3 3 3 2 2 2
+ // Visual: C B A d e f|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr><bdo dir=rtl>|ABC</bdo>def</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(299, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockLineBeginLtrBaseRunWithThreeNestedRuns) {
+ // Sample:|a b c D E F g h i
+ // Bidi: 4 4 4 3 3 3 2 2 2
+ // Visual: F E D a b c|g h i
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr><bdo dir=rtl><bdo "
+ "dir=ltr>|abc</bdo>DEF</bdo>ghi</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(270, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockLineBeginLtrBaseRunWithFourNestedRuns) {
+ // Sample:|A B C d e f G H I j k l
+ // Bidi: 5 5 5 4 4 4 3 3 3 2 2 2
+ // Visual: I H G C B A d e f|j k l
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr><bdo dir=rtl><bdo dir=ltr><bdo "
+ "dir=rtl>|ABC</bdo>def</bdo>GHI</bdo>jkl</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(270, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLineEndWithLtrRunOnly) {
+ // Sample: a b c|
+ // Bidi: 2 2 2
+ // Visual:|a b c
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position =
+ SetCaretTextToBody("<div dir=rtl><bdo dir=ltr>abc|</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(270, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLineEndLtrBaseRunWithTwoNestedRuns) {
+ // Sample: d e f A B C|
+ // Bidi: 2 2 2 3 3 3
+ // Visual:|d e f C B A
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr>def<bdo dir=rtl>ABC|</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(240, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLineEndLtrBaseRunWithThreeNestedRuns) {
+ // Sample: g h i D E F a b c|
+ // Bidi: 2 2 2 3 3 3 4 4 4
+ // Visual: g h i|a b c F E D
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr>ghi<bdo dir=rtl>DEF<bdo "
+ "dir=ltr>abc|</bdo></bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(240, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLineEndLtrBaseRunWithFourNestedRuns) {
+ // Sample: j k l G H I d e f A B C|
+ // Bidi: 2 2 2 3 3 3 4 4 4 5 5 5
+ // Visual: j k l|d e f C B A I H G
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=ltr>jkl<bdo dir=rtl>GHI<bdo dir=ltr>def<bdo "
+ "dir=rtl>ABC|</bdo></bdo></bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(210, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLineBeginRtlBaseRunWithTwoNestedRuns) {
+ // Sample:|a b c D E F
+ // Bidi: 2 2 2 1 1 1
+ // Visual: F E D a b c|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl><bdo dir=ltr>|abc</bdo>DEF</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(299, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockLineBeginRtlBaseRunWithThreeNestedRuns) {
+ // Sample:|A B C d e f G H I
+ // Bidi: 3 3 3 2 2 2 1 1 1
+ // Visual: I H G C B A d e f|
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl><bdo dir=ltr><bdo "
+ "dir=rtl>|ABC</bdo>def</bdo>GHI</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(299, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest,
+ InRtlBlockLineBeginRtlBaseRunWithFourNestedRuns) {
+ // Sample:|a b c D E F g h i J K L
+ // Bidi: 4 4 4 3 3 3 2 2 2 1 1 1
+ // Visual: L K J F E D a b c|g h i
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl><bdo dir=ltr><bdo dir=rtl><bdo "
+ "dir=ltr>|abc</bdo>DEF</bdo>ghi</bdo>JKL</bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(270, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLineEndRtlBaseRunWithTwoNestedRuns) {
+ // Sample: D E F a b c|
+ // Bidi: 1 1 1 2 2 2
+ // Visual:|a b c F E D
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl>DEF<bdo dir=ltr>abc|</bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(240, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLineEndRtlBaseRunWithThreeNestedRuns) {
+ // Sample: G H I d e f A B C|
+ // Bidi: 1 1 1 2 2 2 3 3 3
+ // Visual:|d e f C B A I H G
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl>GHI<bdo dir=ltr>def<bdo "
+ "dir=rtl>ABC|</bdo></bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(210, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+TEST_F(LocalCaretRectBidiTest, InRtlBlockLineEndRtlBaseRunWithFourNestedRuns) {
+ // Sample: J K L g h i D E F a b c|
+ // Bidi: 1 1 1 2 2 2 3 3 3 4 4 4
+ // Visual: g h i|a b c F E D L K J
+ LoadAhem();
+ InsertStyleElement("div {font: 10px/10px Ahem; width: 300px}");
+ const Position position = SetCaretTextToBody(
+ "<div dir=rtl><bdo dir=rtl>JKL<bdo dir=ltr>ghi<bdo dir=rtl>DEF<bdo "
+ "dir=ltr>abc|</bdo></bdo></bdo></bdo></div>");
+ const PositionWithAffinity position_with_affinity(position,
+ TextAffinity::kDownstream);
+ EXPECT_EQ(LayoutRect(210, 0, 1, 10),
+ LocalCaretRectOfPosition(position_with_affinity).rect);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/local_caret_rect_test.cc b/chromium/third_party/blink/renderer/core/editing/local_caret_rect_test.cc
new file mode 100644
index 00000000000..6ce9ee7c0dc
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/local_caret_rect_test.cc
@@ -0,0 +1,949 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/local_caret_rect.h"
+
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/text_affinity.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h"
+#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
+
+namespace blink {
+
+bool operator==(const LocalCaretRect& rect1, const LocalCaretRect& rect2) {
+ return rect1.layout_object == rect2.layout_object && rect1.rect == rect2.rect;
+}
+
+std::ostream& operator<<(std::ostream& out, const LocalCaretRect& caret_rect) {
+ return out << "layout_object = " << caret_rect.layout_object
+ << ", rect = " << caret_rect.rect;
+}
+
+class LocalCaretRectTest : public EditingTestBase {};
+
+// Helper class to run the same test code with and without LayoutNG
+class ParameterizedLocalCaretRectTest
+ : public testing::WithParamInterface<bool>,
+ private ScopedLayoutNGForTest,
+ public LocalCaretRectTest {
+ public:
+ ParameterizedLocalCaretRectTest() : ScopedLayoutNGForTest(GetParam()) {}
+
+ protected:
+ bool LayoutNGEnabled() const { return GetParam(); }
+};
+
+INSTANTIATE_TEST_CASE_P(All, ParameterizedLocalCaretRectTest, testing::Bool());
+
+TEST_P(ParameterizedLocalCaretRectTest, DOMAndFlatTrees) {
+ const char* body_content =
+ "<p id='host'><b id='one'>1</b></p><b id='two'>22</b>";
+ const char* shadow_content =
+ "<b id='two'>22</b><content select=#one></content><b id='three'>333</b>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ Element* one = GetDocument().getElementById("one");
+
+ const LocalCaretRect& caret_rect_from_dom_tree = LocalCaretRectOfPosition(
+ PositionWithAffinity(Position(one->firstChild(), 0)));
+
+ const LocalCaretRect& caret_rect_from_flat_tree = LocalCaretRectOfPosition(
+ PositionInFlatTreeWithAffinity(PositionInFlatTree(one->firstChild(), 0)));
+
+ EXPECT_FALSE(caret_rect_from_dom_tree.IsEmpty());
+ EXPECT_EQ(caret_rect_from_dom_tree, caret_rect_from_flat_tree);
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, SimpleText) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<div id=div style='font: 10px/10px Ahem; width: 30px'>XXX</div>");
+ const Node* foo = GetElementById("div")->firstChild();
+
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(0, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 0), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(10, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 1), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(20, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 2), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(29, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 3), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, MixedHeightText) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<div id=div style='font: 10px/10px Ahem; width: 30px'>Xpp</div>");
+ const Node* foo = GetElementById("div")->firstChild();
+
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(0, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 0), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(10, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 1), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(20, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 2), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(29, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 3), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, RtlText) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<bdo dir=rtl id=bdo style='display: block; "
+ "font: 10px/10px Ahem; width: 30px'>XXX</bdo>");
+ const Node* foo = GetElementById("bdo")->firstChild();
+
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(29, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 0), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(20, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 1), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(10, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 2), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(0, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 3), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, OverflowTextLtr) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<div id=root style='font: 10px/10px Ahem; width: 30px'>"
+ "XXXX"
+ "</div>");
+ const Node* text = GetElementById("root")->firstChild();
+ EXPECT_EQ(LocalCaretRect(text->GetLayoutObject(), LayoutRect(0, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text, 0), TextAffinity::kDownstream)));
+ // LocalCaretRect may be outside the containing block.
+ EXPECT_EQ(LocalCaretRect(text->GetLayoutObject(), LayoutRect(39, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text, 4), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, UnderflowTextLtr) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<div id=root style='font: 10px/10px Ahem; width: 30px'>"
+ "XX"
+ "</div>");
+ const Node* text = GetElementById("root")->firstChild();
+ EXPECT_EQ(LocalCaretRect(text->GetLayoutObject(), LayoutRect(0, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text, 0), TextAffinity::kDownstream)));
+ // LocalCaretRect may be outside the containing block.
+ EXPECT_EQ(LocalCaretRect(text->GetLayoutObject(), LayoutRect(20, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text, 2), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, OverflowTextRtl) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<bdo id=root style='display:block; font: 10px/10px Ahem; width: 30px' "
+ "dir=rtl>"
+ "XXXX"
+ "</bdo>");
+ const Node* text = GetElementById("root")->firstChild();
+ EXPECT_EQ(LocalCaretRect(text->GetLayoutObject(), LayoutRect(29, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text, 0), TextAffinity::kDownstream)));
+ // LocalCaretRect may be outside the containing block.
+ EXPECT_EQ(LocalCaretRect(text->GetLayoutObject(), LayoutRect(-10, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text, 4), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, UnderflowTextRtl) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<bdo id=root style='display:block; font: 10px/10px Ahem; width: 30px' "
+ "dir=rtl>"
+ "XX"
+ "</bdo>");
+ const Node* text = GetElementById("root")->firstChild();
+ EXPECT_EQ(LocalCaretRect(text->GetLayoutObject(), LayoutRect(29, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text, 0), TextAffinity::kDownstream)));
+ // LocalCaretRect may be outside the containing block.
+ EXPECT_EQ(LocalCaretRect(text->GetLayoutObject(), LayoutRect(10, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text, 2), TextAffinity::kDownstream)));
+}
+
+// TODO(xiaochengh): Fix NG LocalCaretText computation for vertical text.
+TEST_F(LocalCaretRectTest, VerticalRLText) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<div id=div style='writing-mode: vertical-rl; word-break: break-all; "
+ "font: 10px/10px Ahem; width: 30px; height: 30px'>XXXYYYZZZ</div>");
+ const Node* foo = GetElementById("div")->firstChild();
+
+ // TODO(xiaochengh): In vertical-rl writing mode, the |X| property of
+ // LocalCaretRect's LayoutRect seems to refer to the distance to the right
+ // edge of the inline formatting context instead. Figure out if this is
+ // correct.
+
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(0, 0, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 0), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(0, 10, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 1), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(0, 20, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 2), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(0, 29, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 3), TextAffinity::kUpstream)));
+
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(10, 0, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 3), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(10, 10, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 4), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(10, 20, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 5), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(10, 29, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 6), TextAffinity::kUpstream)));
+
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(20, 0, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 6), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(20, 10, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 7), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(20, 20, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 8), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(20, 29, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 9), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, VerticalLRText) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<div id=div style='writing-mode: vertical-lr; word-break: break-all; "
+ "font: 10px/10px Ahem; width: 30px; height: 30px'>XXXYYYZZZ</div>");
+ const Node* foo = GetElementById("div")->firstChild();
+
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(0, 0, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 0), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(0, 10, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 1), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(0, 20, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 2), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(0, 29, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 3), TextAffinity::kUpstream)));
+
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(10, 0, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 3), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(10, 10, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 4), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(10, 20, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 5), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(10, 29, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 6), TextAffinity::kUpstream)));
+
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(20, 0, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 6), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(20, 10, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 7), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(20, 20, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 8), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(20, 29, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 9), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, OverflowTextVerticalLtr) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<div id=root style='font: 10px/10px Ahem; height: 30px; writing-mode: "
+ "vertical-lr'>"
+ "XXXX"
+ "</div>");
+ const Node* text = GetElementById("root")->firstChild();
+ EXPECT_EQ(LocalCaretRect(text->GetLayoutObject(), LayoutRect(0, 0, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text, 0), TextAffinity::kDownstream)));
+ // LocalCaretRect may be outside the containing block.
+ EXPECT_EQ(LocalCaretRect(text->GetLayoutObject(), LayoutRect(0, 39, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text, 4), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, UnderflowTextVerticalLtr) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<div id=root style='font: 10px/10px Ahem; height: 30px; writing-mode: "
+ "vertical-lr'>"
+ "XX"
+ "</div>");
+ const Node* text = GetElementById("root")->firstChild();
+ EXPECT_EQ(LocalCaretRect(text->GetLayoutObject(), LayoutRect(0, 0, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text, 0), TextAffinity::kDownstream)));
+ // LocalCaretRect may be outside the containing block.
+ EXPECT_EQ(LocalCaretRect(text->GetLayoutObject(), LayoutRect(0, 20, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text, 2), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, OverflowTextVerticalRtl) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<bdo id=root style='display:block; font: 10px/10px Ahem; height: 30px; "
+ "writing-mode: vertical-lr' dir=rtl>"
+ "XXXX"
+ "</bdo>");
+ const Node* text = GetElementById("root")->firstChild();
+ EXPECT_EQ(LocalCaretRect(text->GetLayoutObject(), LayoutRect(0, 29, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text, 0), TextAffinity::kDownstream)));
+ // LocalCaretRect may be outside the containing block.
+ EXPECT_EQ(LocalCaretRect(text->GetLayoutObject(), LayoutRect(0, -10, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text, 4), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, UnderflowTextVerticalRtl) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<bdo id=root style='display:block; font: 10px/10px Ahem; height: 30px; "
+ "writing-mode: vertical-lr' dir=rtl>"
+ "XX"
+ "</bdo>");
+ const Node* text = GetElementById("root")->firstChild();
+ EXPECT_EQ(LocalCaretRect(text->GetLayoutObject(), LayoutRect(0, 29, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text, 0), TextAffinity::kDownstream)));
+ // LocalCaretRect may be outside the containing block.
+ EXPECT_EQ(LocalCaretRect(text->GetLayoutObject(), LayoutRect(0, 10, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text, 2), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, TwoLinesOfTextWithSoftWrap) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<div id=div style='font: 10px/10px Ahem; width: 30px; "
+ "word-break: break-all'>XXXXXX</div>");
+ const Node* foo = GetElementById("div")->firstChild();
+
+ // First line
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(0, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 0), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(10, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 1), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(20, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 2), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(29, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 3), TextAffinity::kUpstream)));
+
+ // Second line
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(0, 10, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 3), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(10, 10, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 4), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(20, 10, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 5), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(29, 10, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 6), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, SoftLineWrapBetweenMultipleTextNodes) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<div style='font: 10px/10px Ahem; width: 30px; word-break: break-all'>"
+ "<span>A</span>"
+ "<span>B</span>"
+ "<span id=span-c>C</span>"
+ "<span id=span-d>D</span>"
+ "<span>E</span>"
+ "<span>F</span>"
+ "</div>");
+ const Node* text_c = GetElementById("span-c")->firstChild();
+ const Node* text_d = GetElementById("span-d")->firstChild();
+
+ const Position after_c(text_c, 1);
+ EXPECT_EQ(LocalCaretRect(text_c->GetLayoutObject(), LayoutRect(29, 0, 1, 10)),
+ LocalCaretRectOfPosition(
+ PositionWithAffinity(after_c, TextAffinity::kUpstream)));
+ EXPECT_EQ(LocalCaretRect(text_d->GetLayoutObject(), LayoutRect(0, 10, 1, 10)),
+ LocalCaretRectOfPosition(
+ PositionWithAffinity(after_c, TextAffinity::kDownstream)));
+
+ const Position before_d(text_d, 0);
+ // TODO(xiaochengh): Should return the same result for legacy and LayoutNG.
+ EXPECT_EQ(
+ LayoutNGEnabled()
+ ? LocalCaretRect(text_c->GetLayoutObject(), LayoutRect(29, 0, 1, 10))
+ : LocalCaretRect(text_d->GetLayoutObject(), LayoutRect(0, 10, 1, 10)),
+ LocalCaretRectOfPosition(
+ PositionWithAffinity(before_d, TextAffinity::kUpstream)));
+ EXPECT_EQ(LocalCaretRect(text_d->GetLayoutObject(), LayoutRect(0, 10, 1, 10)),
+ LocalCaretRectOfPosition(
+ PositionWithAffinity(before_d, TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest,
+ SoftLineWrapBetweenMultipleTextNodesRtl) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<bdo dir=rtl style='font: 10px/10px Ahem; width: 30px; "
+ "word-break: break-all; display: block'>"
+ "<span>A</span>"
+ "<span>B</span>"
+ "<span id=span-c>C</span>"
+ "<span id=span-d>D</span>"
+ "<span>E</span>"
+ "<span>F</span>"
+ "</bdo>");
+ const Node* text_c = GetElementById("span-c")->firstChild();
+ const Node* text_d = GetElementById("span-d")->firstChild();
+
+ const Position after_c(text_c, 1);
+ EXPECT_EQ(LocalCaretRect(text_c->GetLayoutObject(), LayoutRect(0, 0, 1, 10)),
+ LocalCaretRectOfPosition(
+ PositionWithAffinity(after_c, TextAffinity::kUpstream)));
+ EXPECT_EQ(
+ LocalCaretRect(text_d->GetLayoutObject(), LayoutRect(29, 10, 1, 10)),
+ LocalCaretRectOfPosition(
+ PositionWithAffinity(after_c, TextAffinity::kDownstream)));
+
+ const Position before_d(text_d, 0);
+ // TODO(xiaochengh): Should return the same result for legacy and LayoutNG.
+ EXPECT_EQ(LayoutNGEnabled() ? LocalCaretRect(text_c->GetLayoutObject(),
+ LayoutRect(0, 0, 1, 10))
+ : LocalCaretRect(text_d->GetLayoutObject(),
+ LayoutRect(29, 10, 1, 10)),
+ LocalCaretRectOfPosition(
+ PositionWithAffinity(before_d, TextAffinity::kUpstream)));
+ EXPECT_EQ(
+ LocalCaretRect(text_d->GetLayoutObject(), LayoutRect(29, 10, 1, 10)),
+ LocalCaretRectOfPosition(
+ PositionWithAffinity(before_d, TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, CaretRectAtBR) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<div style='font: 10px/10px Ahem; width: 30px'><br>foo</div>");
+ const Element& br = *GetDocument().QuerySelector("br");
+
+ EXPECT_EQ(LocalCaretRect(br.GetLayoutObject(), LayoutRect(0, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::BeforeNode(br), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, CaretRectAtRtlBR) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<bdo dir=rtl style='display: block; font: 10px/10px Ahem; width: 30px'>"
+ "<br>foo</bdo>");
+ const Element& br = *GetDocument().QuerySelector("br");
+
+ EXPECT_EQ(LocalCaretRect(br.GetLayoutObject(), LayoutRect(29, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::BeforeNode(br), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, Images) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<div id=div style='font: 10px/10px Ahem; width: 30px'>"
+ "<img id=img1 width=10px height=10px>"
+ "<img id=img2 width=10px height=10px>"
+ "</div>");
+
+ const Element& img1 = *GetElementById("img1");
+
+ EXPECT_EQ(LocalCaretRect(img1.GetLayoutObject(), LayoutRect(0, 0, 1, 12)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::BeforeNode(img1), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(img1.GetLayoutObject(), LayoutRect(9, 0, 1, 12)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::AfterNode(img1), TextAffinity::kDownstream)));
+
+ const Element& img2 = *GetElementById("img2");
+
+ // Box-anchored LocalCaretRect is local to the box itself, instead of its
+ // containing block.
+ // TODO(xiaochengh): Should return the same result for legacy and LayoutNG.
+ EXPECT_EQ(
+ LayoutNGEnabled()
+ ? LocalCaretRect(img1.GetLayoutObject(), LayoutRect(9, 0, 1, 12))
+ : LocalCaretRect(img2.GetLayoutObject(), LayoutRect(0, 0, 1, 12)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::BeforeNode(img2), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(img2.GetLayoutObject(), LayoutRect(9, 0, 1, 12)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::AfterNode(img2), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, RtlImages) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<bdo dir=rtl style='font: 10px/10px Ahem; width: 30px; display: block'>"
+ "<img id=img1 width=10px height=10px>"
+ "<img id=img2 width=10px height=10px>"
+ "</bdo>");
+
+ const Element& img1 = *GetElementById("img1");
+ const Element& img2 = *GetElementById("img2");
+
+ // Box-anchored LocalCaretRect is local to the box itself, instead of its
+ // containing block.
+ EXPECT_EQ(LocalCaretRect(img1.GetLayoutObject(), LayoutRect(9, 0, 1, 12)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::BeforeNode(img1), TextAffinity::kDownstream)));
+ EXPECT_EQ(
+ LayoutNGEnabled()
+ ? LocalCaretRect(img2.GetLayoutObject(), LayoutRect(9, 0, 1, 12))
+ : LocalCaretRect(img1.GetLayoutObject(), LayoutRect(0, 0, 1, 12)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::AfterNode(img1), TextAffinity::kDownstream)));
+
+ EXPECT_EQ(LocalCaretRect(img2.GetLayoutObject(), LayoutRect(9, 0, 1, 12)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::BeforeNode(img2), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(img2.GetLayoutObject(), LayoutRect(0, 0, 1, 12)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::AfterNode(img2), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, VerticalImage) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ SetBodyContent(
+ "<div style='writing-mode: vertical-rl'>"
+ "<img id=img width=10px height=20px>"
+ "</div>");
+
+ const Element& img = *GetElementById("img");
+
+ // Box-anchored LocalCaretRect is local to the box itself, instead of its
+ // containing block.
+ EXPECT_EQ(LocalCaretRect(img.GetLayoutObject(), LayoutRect(0, 0, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::BeforeNode(img), TextAffinity::kDownstream)));
+
+ EXPECT_EQ(
+ LayoutNGEnabled()
+ ? LocalCaretRect(img.GetLayoutObject(), LayoutRect(0, 19, 10, 1))
+ // TODO(crbug.com/805064): The legacy behavior is wrong. Fix it.
+ : LocalCaretRect(img.GetLayoutObject(), LayoutRect(0, 9, 10, 1)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::AfterNode(img), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, TextAndImageMixedHeight) {
+ // This test only records the current behavior. Future changes are allowed.
+
+ LoadAhem();
+ SetBodyContent(
+ "<div id=div style='font: 10px/10px Ahem; width: 30px'>"
+ "X"
+ "<img id=img width=10px height=5px style='vertical-align: text-bottom'>"
+ "p</div>");
+
+ const Element& img = *GetElementById("img");
+ const Node* text1 = img.previousSibling();
+ const Node* text2 = img.nextSibling();
+
+ EXPECT_EQ(LocalCaretRect(text1->GetLayoutObject(), LayoutRect(0, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text1, 0), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(text1->GetLayoutObject(), LayoutRect(10, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text1, 1), TextAffinity::kDownstream)));
+
+ // TODO(xiaochengh): Should return the same result for legacy and LayoutNG.
+ EXPECT_EQ(
+ LayoutNGEnabled()
+ ? LocalCaretRect(text1->GetLayoutObject(), LayoutRect(10, 0, 1, 10))
+ : LocalCaretRect(img.GetLayoutObject(), LayoutRect(0, -5, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::BeforeNode(img), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(img.GetLayoutObject(), LayoutRect(9, -5, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::AfterNode(img), TextAffinity::kDownstream)));
+
+ // TODO(xiaochengh): Should return the same result for legacy and LayoutNG.
+ EXPECT_EQ(
+ LayoutNGEnabled()
+ ? LocalCaretRect(img.GetLayoutObject(), LayoutRect(9, -5, 1, 10))
+ : LocalCaretRect(text2->GetLayoutObject(), LayoutRect(20, 5, 1, 10)),
+ LocalCaretRectOfPosition(
+ PositionWithAffinity(Position(text2, 0), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(text2->GetLayoutObject(), LayoutRect(29, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(text2, 1), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, FloatFirstLetter) {
+ LoadAhem();
+ InsertStyleElement("#container::first-letter{float:right}");
+ SetBodyContent(
+ "<div id=container style='font: 10px/10px Ahem; width: 40px'>foo</div>");
+ const Node* foo = GetElementById("container")->firstChild();
+ const LayoutObject* first_letter = AssociatedLayoutObjectOf(*foo, 0);
+ const LayoutObject* remaining_text = AssociatedLayoutObjectOf(*foo, 1);
+
+ // TODO(editing-dev): Legacy LocalCaretRectOfPosition() is not aware of the
+ // first-letter LayoutObject. Fix it.
+
+ EXPECT_EQ(LocalCaretRect(LayoutNGEnabled() ? first_letter : remaining_text,
+ LayoutRect(0, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 0), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(remaining_text,
+ LayoutRect(LayoutNGEnabled() ? 0 : 10, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 1), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(remaining_text,
+ LayoutRect(LayoutNGEnabled() ? 10 : 20, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 2), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(remaining_text, LayoutNGEnabled()
+ ? LayoutRect(20, 0, 1, 10)
+ : LayoutRect()),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 3), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, AfterLineBreak) {
+ LoadAhem();
+ SetBodyContent("<div style='font: 10px/10px Ahem;'>foo<br><br></div>");
+ const Node* div = GetDocument().body()->firstChild();
+ const Node* foo = div->firstChild();
+ const Node* first_br = foo->nextSibling();
+ const Node* second_br = first_br->nextSibling();
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(30, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::AfterNode(*foo), TextAffinity::kDownstream)));
+ EXPECT_EQ(
+ LocalCaretRect(second_br->GetLayoutObject(), LayoutRect(0, 10, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::AfterNode(*first_br), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(second_br->GetLayoutObject(),
+ LayoutNGEnabled() ? LayoutRect(0, 10, 1, 10)
+ : LayoutRect(0, 0, 0, 0)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::AfterNode(*second_br), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, AfterLineBreakInPre) {
+ LoadAhem();
+ SetBodyContent("<pre style='font: 10px/10px Ahem;'>foo\n\n</pre>");
+ const Node* pre = GetDocument().body()->firstChild();
+ const Node* foo = pre->firstChild();
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(30, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 3), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(0, 10, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 4), TextAffinity::kDownstream)));
+ // TODO(yoichio): Legacy should return valid rect: crbug.com/812535.
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(),
+ LayoutNGEnabled() ? LayoutRect(0, 10, 1, 10)
+ : LayoutRect(0, 0, 0, 0)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 5), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, AfterLineBreakInPre2) {
+ LoadAhem();
+ // This test case simulates the rendering of the inner editor of
+ // <textarea>foo\n</textarea> without using text control element.
+ SetBodyContent("<pre style='font: 10px/10px Ahem;'>foo\n<br></pre>");
+ const Node* pre = GetDocument().body()->firstChild();
+ const Node* foo = pre->firstChild();
+ const Node* br = foo->nextSibling();
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(30, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 3), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(br->GetLayoutObject(), LayoutRect(0, 10, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 4), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(br->GetLayoutObject(), LayoutNGEnabled()
+ ? LayoutRect(0, 10, 1, 10)
+ : LayoutRect(0, 0, 0, 0)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::AfterNode(*br), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, AfterLineBreakTextArea) {
+ LoadAhem();
+ SetBodyContent("<textarea style='font: 10px/10px Ahem; '>foo\n\n</textarea>");
+ const auto* textarea = ToTextControl(GetDocument().body()->firstChild());
+ const Node* inner_text = textarea->InnerEditorElement()->firstChild();
+ EXPECT_EQ(
+ LocalCaretRect(inner_text->GetLayoutObject(), LayoutRect(30, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(inner_text, 3), TextAffinity::kDownstream)));
+ EXPECT_EQ(
+ LocalCaretRect(inner_text->GetLayoutObject(), LayoutRect(0, 10, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(inner_text, 4), TextAffinity::kDownstream)));
+ const Node* hidden_br = inner_text->nextSibling();
+ EXPECT_EQ(
+ LocalCaretRect(hidden_br->GetLayoutObject(), LayoutRect(0, 20, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(inner_text, 5), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, CollapsedSpace) {
+ LoadAhem();
+ SetBodyContent(
+ "<div style='font: 10px/10px Ahem;'>"
+ "<span>foo</span><span> </span></div>");
+ const Node* first_span = GetDocument().body()->firstChild()->firstChild();
+ const Node* foo = first_span->firstChild();
+ const Node* second_span = first_span->nextSibling();
+ const Node* white_spaces = second_span->firstChild();
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(30, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(foo, 3), TextAffinity::kDownstream)));
+ EXPECT_EQ(LocalCaretRect(foo->GetLayoutObject(), LayoutRect(30, 0, 1, 10)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position::AfterNode(*foo), TextAffinity::kDownstream)));
+ // TODO(yoichio): Following should return valid rect: crbug.com/812535.
+ EXPECT_EQ(
+ LocalCaretRect(first_span->GetLayoutObject(), LayoutRect(0, 0, 0, 0)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(first_span, PositionAnchorType::kAfterChildren),
+ TextAffinity::kDownstream)));
+ EXPECT_EQ(LayoutNGEnabled() ? LocalCaretRect(foo->GetLayoutObject(),
+ LayoutRect(30, 0, 1, 10))
+ : LocalCaretRect(white_spaces->GetLayoutObject(),
+ LayoutRect(0, 0, 0, 0)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(white_spaces, 0), TextAffinity::kDownstream)));
+ EXPECT_EQ(LayoutNGEnabled() ? LocalCaretRect(foo->GetLayoutObject(),
+ LayoutRect(30, 0, 1, 10))
+ : LocalCaretRect(white_spaces->GetLayoutObject(),
+ LayoutRect(0, 0, 0, 0)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(white_spaces, 1), TextAffinity::kDownstream)));
+ EXPECT_EQ(LayoutNGEnabled() ? LocalCaretRect(foo->GetLayoutObject(),
+ LayoutRect(30, 0, 1, 10))
+ : LocalCaretRect(white_spaces->GetLayoutObject(),
+ LayoutRect(0, 0, 0, 0)),
+ LocalCaretRectOfPosition(PositionWithAffinity(
+ Position(white_spaces, 2), TextAffinity::kDownstream)));
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, AbsoluteCaretBoundsOfWithShadowDOM) {
+ const char* body_content =
+ "<p id='host'><b id='one'>11</b><b id='two'>22</b></p>";
+ const char* shadow_content =
+ "<div><content select=#two></content><content "
+ "select=#one></content></div>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ Element* body = GetDocument().body();
+ Element* one = body->QuerySelector("#one");
+
+ IntRect bounds_in_dom_tree =
+ AbsoluteCaretBoundsOf(CreateVisiblePosition(Position(one, 0)));
+ IntRect bounds_in_flat_tree =
+ AbsoluteCaretBoundsOf(CreateVisiblePosition(PositionInFlatTree(one, 0)));
+
+ EXPECT_FALSE(bounds_in_dom_tree.IsEmpty());
+ EXPECT_EQ(bounds_in_dom_tree, bounds_in_flat_tree);
+}
+
+// Repro case of crbug.com/680428
+TEST_P(ParameterizedLocalCaretRectTest, AbsoluteSelectionBoundsOfWithImage) {
+ SetBodyContent("<div>foo<img></div>");
+
+ Node* node = GetDocument().QuerySelector("img");
+ IntRect rect =
+ AbsoluteSelectionBoundsOf(VisiblePosition::Create(PositionWithAffinity(
+ Position(node, PositionAnchorType::kAfterChildren))));
+ EXPECT_FALSE(rect.IsEmpty());
+}
+
+static std::pair<LayoutRect, LayoutRect> GetLayoutRects(const Position& caret) {
+ const PositionWithAffinity position(caret);
+ const LayoutRect& position_rect = LocalCaretRectOfPosition(position).rect;
+ const PositionWithAffinity visible_position(
+ CreateVisiblePosition(position).DeepEquivalent());
+ const LayoutRect& visible_position_rect =
+ LocalCaretRectOfPosition(visible_position).rect;
+ return {position_rect, visible_position_rect};
+}
+
+TEST_P(ParameterizedLocalCaretRectTest, AfterLineBreakInPreBlockLTRLineLTR) {
+ LoadAhem();
+ InsertStyleElement("pre{ font: 10px/10px Ahem; width: 300px }");
+ const Position& caret =
+ SetCaretTextToBody("<pre dir='ltr'>foo\n|<bdo dir='ltr'>abc</bdo></pre>");
+ LayoutRect position_rect, visible_position_rect;
+ std::tie(position_rect, visible_position_rect) = GetLayoutRects(caret);
+ // TODO(xiaochengh): Should return the same result for legacy and LayoutNG.
+ EXPECT_EQ(
+ LayoutNGEnabled() ? LayoutRect(30, 0, 1, 10) : LayoutRect(0, 10, 1, 10),
+ position_rect);
+ EXPECT_EQ(LayoutRect(0, 10, 1, 10), visible_position_rect);
+};
+
+TEST_P(ParameterizedLocalCaretRectTest, AfterLineBreakInPreBlockLTRLineRTL) {
+ LoadAhem();
+ InsertStyleElement("pre{ font: 10px/10px Ahem; width: 300px }");
+ const Position& caret =
+ SetCaretTextToBody("<pre dir='ltr'>foo\n|<bdo dir='rtl'>abc</bdo></pre>");
+ LayoutRect position_rect, visible_position_rect;
+ std::tie(position_rect, visible_position_rect) = GetLayoutRects(caret);
+ // TODO(xiaochengh): Should return the same result for legacy and LayoutNG.
+ EXPECT_EQ(
+ LayoutNGEnabled() ? LayoutRect(30, 0, 1, 10) : LayoutRect(0, 10, 1, 10),
+ position_rect);
+ EXPECT_EQ(
+ LayoutNGEnabled() ? LayoutRect(30, 10, 1, 10) : LayoutRect(0, 10, 1, 10),
+ visible_position_rect);
+};
+
+TEST_P(ParameterizedLocalCaretRectTest, AfterLineBreakInPreBlockRTLLineLTR) {
+ LoadAhem();
+ InsertStyleElement("pre{ font: 10px/10px Ahem; width: 300px }");
+ const Position& caret =
+ SetCaretTextToBody("<pre dir='rtl'>foo\n|<bdo dir='ltr'>abc</bdo></pre>");
+ LayoutRect position_rect, visible_position_rect;
+ std::tie(position_rect, visible_position_rect) = GetLayoutRects(caret);
+ // TODO(xiaochengh): Should return the same result for legacy and LayoutNG.
+ EXPECT_EQ(LayoutNGEnabled() ? LayoutRect(270, 0, 1, 10)
+ : LayoutRect(299, 10, 1, 10),
+ position_rect);
+ EXPECT_EQ(LayoutNGEnabled() ? LayoutRect(270, 10, 1, 10)
+ : LayoutRect(299, 10, 1, 10),
+ visible_position_rect);
+};
+
+TEST_P(ParameterizedLocalCaretRectTest, AfterLineBreakInPreBlockRTLLineRTL) {
+ LoadAhem();
+ InsertStyleElement("pre{ font: 10px/10px Ahem; width: 300px }");
+ const Position& caret =
+ SetCaretTextToBody("<pre dir='rtl'>foo\n|<bdo dir='rtl'>abc</bdo></pre>");
+ LayoutRect position_rect, visible_position_rect;
+ std::tie(position_rect, visible_position_rect) = GetLayoutRects(caret);
+ // TODO(xiaochengh): Should return the same result for legacy and LayoutNG.
+ EXPECT_EQ(LayoutNGEnabled() ? LayoutRect(270, 0, 1, 10)
+ : LayoutRect(299, 10, 1, 10),
+ position_rect);
+ EXPECT_EQ(LayoutRect(299, 10, 1, 10), visible_position_rect);
+};
+
+TEST_P(ParameterizedLocalCaretRectTest,
+ UnicodeBidiPlaintextWithDifferentBlockDirection) {
+ LoadAhem();
+ InsertStyleElement("div { font: 10px/10px Ahem; unicode-bidi: plaintext }");
+ const Position position = SetCaretTextToBody("<div dir='rtl'>|abc</div>");
+ const LayoutRect caret_rect =
+ LocalCaretRectOfPosition(PositionWithAffinity(position)).rect;
+ EXPECT_EQ(LayoutRect(0, 0, 1, 10), caret_rect);
+}
+
+// http://crbug.com/835779
+TEST_P(ParameterizedLocalCaretRectTest, NextLineWithoutLeafChild) {
+ LoadAhem();
+ InsertStyleElement("div { font: 10px/10px Ahem; width: 30px }");
+ SetBodyContent(
+ "<div>"
+ "<br>"
+ "<span style=\"border-left: 50px solid\"></span>"
+ "foo"
+ "</div>");
+
+ const Element& br = *GetDocument().QuerySelector("br");
+ EXPECT_EQ(
+ // TODO(xiaochengh): Should return the same result for legacy and
+ // LayoutNG.
+ LayoutNGEnabled() ? LayoutRect(50, 10, 1, 10) : LayoutRect(0, 20, 1, 10),
+ LocalCaretRectOfPosition(PositionWithAffinity(Position::AfterNode(br)))
+ .rect);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker.cc b/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker.cc
new file mode 100644
index 00000000000..926d2c85509
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker.cc
@@ -0,0 +1,25 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/active_suggestion_marker.h"
+
+namespace blink {
+
+ActiveSuggestionMarker::ActiveSuggestionMarker(
+ unsigned start_offset,
+ unsigned end_offset,
+ Color underline_color,
+ ui::mojom::ImeTextSpanThickness thickness,
+ Color background_color)
+ : StyleableMarker(start_offset,
+ end_offset,
+ underline_color,
+ thickness,
+ background_color) {}
+
+DocumentMarker::MarkerType ActiveSuggestionMarker::GetType() const {
+ return DocumentMarker::kActiveSuggestion;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker.h b/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker.h
new file mode 100644
index 00000000000..4c559c26792
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker.h
@@ -0,0 +1,38 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_ACTIVE_SUGGESTION_MARKER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_ACTIVE_SUGGESTION_MARKER_H_
+
+#include "third_party/blink/renderer/core/editing/markers/styleable_marker.h"
+
+namespace blink {
+
+// A subclass of StyleableMarker used to represent ActiveSuggestion markers,
+// which are used to mark the region of text an open spellcheck or suggestion
+// menu pertains to.
+class CORE_EXPORT ActiveSuggestionMarker final : public StyleableMarker {
+ public:
+ ActiveSuggestionMarker(unsigned start_offset,
+ unsigned end_offset,
+ Color underline_color,
+ ui::mojom::ImeTextSpanThickness,
+ Color background_color);
+
+ // DocumentMarker implementations
+ MarkerType GetType() const final;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(ActiveSuggestionMarker);
+};
+
+DEFINE_TYPE_CASTS(ActiveSuggestionMarker,
+ DocumentMarker,
+ marker,
+ marker->GetType() == DocumentMarker::kActiveSuggestion,
+ marker.GetType() == DocumentMarker::kActiveSuggestion);
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker_list_impl.cc b/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker_list_impl.cc
new file mode 100644
index 00000000000..d7226c3273a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker_list_impl.cc
@@ -0,0 +1,75 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/active_suggestion_marker_list_impl.h"
+
+#include "third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor.h"
+
+namespace blink {
+
+DocumentMarker::MarkerType ActiveSuggestionMarkerListImpl::MarkerType() const {
+ return DocumentMarker::kActiveSuggestion;
+}
+
+bool ActiveSuggestionMarkerListImpl::IsEmpty() const {
+ return markers_.IsEmpty();
+}
+
+void ActiveSuggestionMarkerListImpl::Add(DocumentMarker* marker) {
+ DCHECK_EQ(DocumentMarker::kActiveSuggestion, marker->GetType());
+ SortedDocumentMarkerListEditor::AddMarkerWithoutMergingOverlapping(&markers_,
+ marker);
+}
+
+void ActiveSuggestionMarkerListImpl::Clear() {
+ markers_.clear();
+}
+
+const HeapVector<Member<DocumentMarker>>&
+ActiveSuggestionMarkerListImpl::GetMarkers() const {
+ return markers_;
+}
+
+DocumentMarker* ActiveSuggestionMarkerListImpl::FirstMarkerIntersectingRange(
+ unsigned start_offset,
+ unsigned end_offset) const {
+ return SortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(
+ markers_, start_offset, end_offset);
+}
+
+HeapVector<Member<DocumentMarker>>
+ActiveSuggestionMarkerListImpl::MarkersIntersectingRange(
+ unsigned start_offset,
+ unsigned end_offset) const {
+ return SortedDocumentMarkerListEditor::MarkersIntersectingRange(
+ markers_, start_offset, end_offset);
+}
+
+bool ActiveSuggestionMarkerListImpl::MoveMarkers(
+ int length,
+ DocumentMarkerList* dst_markers_) {
+ return SortedDocumentMarkerListEditor::MoveMarkers(&markers_, length,
+ dst_markers_);
+}
+
+bool ActiveSuggestionMarkerListImpl::RemoveMarkers(unsigned start_offset,
+ int length) {
+ return SortedDocumentMarkerListEditor::RemoveMarkers(&markers_, start_offset,
+ length);
+}
+
+bool ActiveSuggestionMarkerListImpl::ShiftMarkers(const String&,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) {
+ return SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(
+ &markers_, offset, old_length, new_length);
+}
+
+void ActiveSuggestionMarkerListImpl::Trace(blink::Visitor* visitor) {
+ visitor->Trace(markers_);
+ DocumentMarkerList::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker_list_impl.h b/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker_list_impl.h
new file mode 100644
index 00000000000..b6aaaf3c947
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker_list_impl.h
@@ -0,0 +1,55 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_ACTIVE_SUGGESTION_MARKER_LIST_IMPL_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_ACTIVE_SUGGESTION_MARKER_LIST_IMPL_H_
+
+#include "third_party/blink/renderer/core/editing/markers/document_marker_list.h"
+
+namespace blink {
+
+// Implementation of DocumentMarkerList for ActiveSuggestion markers.
+// ActiveSuggestion markers are always inserted in order, aside from some
+// potential oddball cases (e.g. splitting the marker list into two nodes, then
+// undoing the split). This means we can keep the list in sorted order to do
+// some operations more efficiently, while still being able to do inserts in
+// O(1) time at the end of the list.
+class CORE_EXPORT ActiveSuggestionMarkerListImpl final
+ : public DocumentMarkerList {
+ public:
+ ActiveSuggestionMarkerListImpl() = default;
+
+ // DocumentMarkerList implementations
+ DocumentMarker::MarkerType MarkerType() const final;
+
+ bool IsEmpty() const final;
+
+ void Add(DocumentMarker*) final;
+ void Clear() final;
+
+ const HeapVector<Member<DocumentMarker>>& GetMarkers() const final;
+ DocumentMarker* FirstMarkerIntersectingRange(unsigned start_offset,
+ unsigned end_offset) const final;
+ HeapVector<Member<DocumentMarker>> MarkersIntersectingRange(
+ unsigned start_offset,
+ unsigned end_offset) const final;
+
+ bool MoveMarkers(int length, DocumentMarkerList* dst_list) final;
+ bool RemoveMarkers(unsigned start_offset, int length) final;
+ bool ShiftMarkers(const String& node_text,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) final;
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ HeapVector<Member<DocumentMarker>> markers_;
+
+ DISALLOW_COPY_AND_ASSIGN(ActiveSuggestionMarkerListImpl);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_ACTIVE_SUGGESTION_MARKER_LIST_IMPL_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker_list_impl_test.cc b/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker_list_impl_test.cc
new file mode 100644
index 00000000000..9f484b0d46e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker_list_impl_test.cc
@@ -0,0 +1,43 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/active_suggestion_marker_list_impl.h"
+
+#include "third_party/blink/renderer/core/editing/markers/active_suggestion_marker.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class ActiveSuggestionMarkerListImplTest : public EditingTestBase {
+ protected:
+ ActiveSuggestionMarkerListImplTest()
+ : marker_list_(new ActiveSuggestionMarkerListImpl()) {}
+
+ DocumentMarker* CreateMarker(unsigned start_offset, unsigned end_offset) {
+ return new ActiveSuggestionMarker(
+ start_offset, end_offset, Color::kTransparent,
+ ui::mojom::ImeTextSpanThickness::kThin, Color::kBlack);
+ }
+
+ Persistent<ActiveSuggestionMarkerListImpl> marker_list_;
+};
+
+// ActiveSuggestionMarkerListImpl shouldn't merge markers with touching
+// endpoints
+TEST_F(ActiveSuggestionMarkerListImplTest, Add) {
+ EXPECT_EQ(0u, marker_list_->GetMarkers().size());
+
+ marker_list_->Add(CreateMarker(0, 1));
+ marker_list_->Add(CreateMarker(1, 2));
+
+ EXPECT_EQ(2u, marker_list_->GetMarkers().size());
+
+ EXPECT_EQ(0u, marker_list_->GetMarkers()[0]->StartOffset());
+ EXPECT_EQ(1u, marker_list_->GetMarkers()[0]->EndOffset());
+
+ EXPECT_EQ(1u, marker_list_->GetMarkers()[1]->StartOffset());
+ EXPECT_EQ(2u, marker_list_->GetMarkers()[1]->EndOffset());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker_test.cc b/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker_test.cc
new file mode 100644
index 00000000000..946b9cac652
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/active_suggestion_marker_test.cc
@@ -0,0 +1,41 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/active_suggestion_marker.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+using ui::mojom::ImeTextSpanThickness;
+
+namespace blink {
+
+class ActiveSuggestionMarkerTest : public testing::Test {};
+
+TEST_F(ActiveSuggestionMarkerTest, MarkerType) {
+ DocumentMarker* marker = new ActiveSuggestionMarker(
+ 0, 1, Color::kTransparent, ImeTextSpanThickness::kNone,
+ Color::kTransparent);
+ EXPECT_EQ(DocumentMarker::kActiveSuggestion, marker->GetType());
+}
+
+TEST_F(ActiveSuggestionMarkerTest, IsStyleableMarker) {
+ DocumentMarker* marker = new ActiveSuggestionMarker(
+ 0, 1, Color::kTransparent, ImeTextSpanThickness::kNone,
+ Color::kTransparent);
+ EXPECT_TRUE(IsStyleableMarker(*marker));
+}
+
+TEST_F(ActiveSuggestionMarkerTest, ConstructorAndGetters) {
+ ActiveSuggestionMarker* marker = new ActiveSuggestionMarker(
+ 0, 1, Color::kDarkGray, ImeTextSpanThickness::kThin, Color::kGray);
+ EXPECT_EQ(Color::kDarkGray, marker->UnderlineColor());
+ EXPECT_FALSE(marker->HasThicknessThick());
+ EXPECT_EQ(Color::kGray, marker->BackgroundColor());
+
+ ActiveSuggestionMarker* thick_marker = new ActiveSuggestionMarker(
+ 0, 1, Color::kDarkGray, ImeTextSpanThickness::kThick, Color::kGray);
+ EXPECT_EQ(true, thick_marker->HasThicknessThick());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/composition_marker.cc b/chromium/third_party/blink/renderer/core/editing/markers/composition_marker.cc
new file mode 100644
index 00000000000..954fa2ae1e1
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/composition_marker.cc
@@ -0,0 +1,24 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/composition_marker.h"
+
+namespace blink {
+
+CompositionMarker::CompositionMarker(unsigned start_offset,
+ unsigned end_offset,
+ Color underline_color,
+ ui::mojom::ImeTextSpanThickness thickness,
+ Color background_color)
+ : StyleableMarker(start_offset,
+ end_offset,
+ underline_color,
+ thickness,
+ background_color) {}
+
+DocumentMarker::MarkerType CompositionMarker::GetType() const {
+ return DocumentMarker::kComposition;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/composition_marker.h b/chromium/third_party/blink/renderer/core/editing/markers/composition_marker.h
new file mode 100644
index 00000000000..ba5af65c07e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/composition_marker.h
@@ -0,0 +1,40 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_COMPOSITION_MARKER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_COMPOSITION_MARKER_H_
+
+#include "third_party/blink/renderer/core/editing/markers/styleable_marker.h"
+
+namespace blink {
+
+// A subclass of DocumentMarker used to store information specific to
+// composition markers. We store what color to display the underline (possibly
+// transparent), whether the underline should be thick or not, and what
+// background color should be used under the marked text (also possibly
+// transparent).
+class CORE_EXPORT CompositionMarker final : public StyleableMarker {
+ public:
+ CompositionMarker(unsigned start_offset,
+ unsigned end_offset,
+ Color underline_color,
+ ui::mojom::ImeTextSpanThickness,
+ Color background_color);
+
+ // DocumentMarker implementations
+ MarkerType GetType() const final;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(CompositionMarker);
+};
+
+DEFINE_TYPE_CASTS(CompositionMarker,
+ DocumentMarker,
+ marker,
+ marker->GetType() == DocumentMarker::kComposition,
+ marker.GetType() == DocumentMarker::kComposition);
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/composition_marker_list_impl.cc b/chromium/third_party/blink/renderer/core/editing/markers/composition_marker_list_impl.cc
new file mode 100644
index 00000000000..434f32efe50
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/composition_marker_list_impl.cc
@@ -0,0 +1,72 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/composition_marker_list_impl.h"
+
+#include "third_party/blink/renderer/core/editing/markers/unsorted_document_marker_list_editor.h"
+
+namespace blink {
+
+DocumentMarker::MarkerType CompositionMarkerListImpl::MarkerType() const {
+ return DocumentMarker::kComposition;
+}
+
+bool CompositionMarkerListImpl::IsEmpty() const {
+ return markers_.IsEmpty();
+}
+
+void CompositionMarkerListImpl::Add(DocumentMarker* marker) {
+ DCHECK_EQ(DocumentMarker::kComposition, marker->GetType());
+ markers_.push_back(marker);
+}
+
+void CompositionMarkerListImpl::Clear() {
+ markers_.clear();
+}
+
+const HeapVector<Member<DocumentMarker>>&
+CompositionMarkerListImpl::GetMarkers() const {
+ return markers_;
+}
+
+DocumentMarker* CompositionMarkerListImpl::FirstMarkerIntersectingRange(
+ unsigned start_offset,
+ unsigned end_offset) const {
+ return UnsortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(
+ markers_, start_offset, end_offset);
+}
+
+HeapVector<Member<DocumentMarker>>
+CompositionMarkerListImpl::MarkersIntersectingRange(unsigned start_offset,
+ unsigned end_offset) const {
+ return UnsortedDocumentMarkerListEditor::MarkersIntersectingRange(
+ markers_, start_offset, end_offset);
+}
+
+bool CompositionMarkerListImpl::MoveMarkers(int length,
+ DocumentMarkerList* dst_markers_) {
+ return UnsortedDocumentMarkerListEditor::MoveMarkers(&markers_, length,
+ dst_markers_);
+}
+
+bool CompositionMarkerListImpl::RemoveMarkers(unsigned start_offset,
+ int length) {
+ return UnsortedDocumentMarkerListEditor::RemoveMarkers(&markers_,
+ start_offset, length);
+}
+
+bool CompositionMarkerListImpl::ShiftMarkers(const String&,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) {
+ return UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(
+ &markers_, offset, old_length, new_length);
+}
+
+void CompositionMarkerListImpl::Trace(blink::Visitor* visitor) {
+ visitor->Trace(markers_);
+ DocumentMarkerList::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/composition_marker_list_impl.h b/chromium/third_party/blink/renderer/core/editing/markers/composition_marker_list_impl.h
new file mode 100644
index 00000000000..c668c548d58
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/composition_marker_list_impl.h
@@ -0,0 +1,52 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_COMPOSITION_MARKER_LIST_IMPL_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_COMPOSITION_MARKER_LIST_IMPL_H_
+
+#include "third_party/blink/renderer/core/editing/markers/document_marker_list.h"
+
+namespace blink {
+
+// Implementation of DocumentMarkerList for Composition markers. Composition
+// markers can overlap (e.g. an IME might pass two markers on the same region of
+// text, one to underline it and one to add a background color), so we store
+// them unsorted.
+class CORE_EXPORT CompositionMarkerListImpl final : public DocumentMarkerList {
+ public:
+ CompositionMarkerListImpl() = default;
+
+ // DocumentMarkerList implementations
+ DocumentMarker::MarkerType MarkerType() const final;
+
+ bool IsEmpty() const final;
+
+ void Add(DocumentMarker*) final;
+ void Clear() final;
+
+ const HeapVector<Member<DocumentMarker>>& GetMarkers() const final;
+ DocumentMarker* FirstMarkerIntersectingRange(unsigned start_offset,
+ unsigned end_offset) const final;
+ HeapVector<Member<DocumentMarker>> MarkersIntersectingRange(
+ unsigned start_offset,
+ unsigned end_offset) const final;
+
+ bool MoveMarkers(int length, DocumentMarkerList* dst_list) final;
+ bool RemoveMarkers(unsigned start_offset, int length) final;
+ bool ShiftMarkers(const String& node_text,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) final;
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ HeapVector<Member<DocumentMarker>> markers_;
+
+ DISALLOW_COPY_AND_ASSIGN(CompositionMarkerListImpl);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_COMPOSITION_MARKER_LIST_IMPL_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/composition_marker_list_impl_test.cc b/chromium/third_party/blink/renderer/core/editing/markers/composition_marker_list_impl_test.cc
new file mode 100644
index 00000000000..48ea3899453
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/composition_marker_list_impl_test.cc
@@ -0,0 +1,77 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/composition_marker_list_impl.h"
+
+#include "third_party/blink/renderer/core/editing/markers/composition_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/marker_test_utilities.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class CompositionMarkerListImplTest : public EditingTestBase {
+ protected:
+ CompositionMarkerListImplTest()
+ : marker_list_(new CompositionMarkerListImpl()) {}
+
+ DocumentMarker* CreateMarker(unsigned start_offset, unsigned end_offset) {
+ return new CompositionMarker(start_offset, end_offset, Color::kTransparent,
+ ui::mojom::ImeTextSpanThickness::kThin,
+ Color::kBlack);
+ }
+
+ Persistent<CompositionMarkerListImpl> marker_list_;
+};
+
+TEST_F(CompositionMarkerListImplTest, AddOverlapping) {
+ // Add some overlapping markers in an arbitrary order and verify that the
+ // list stores them properly
+ marker_list_->Add(CreateMarker(40, 50));
+ marker_list_->Add(CreateMarker(10, 40));
+ marker_list_->Add(CreateMarker(20, 50));
+ marker_list_->Add(CreateMarker(10, 30));
+ marker_list_->Add(CreateMarker(10, 50));
+ marker_list_->Add(CreateMarker(30, 50));
+ marker_list_->Add(CreateMarker(30, 40));
+ marker_list_->Add(CreateMarker(10, 20));
+ marker_list_->Add(CreateMarker(20, 40));
+ marker_list_->Add(CreateMarker(20, 30));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+ std::sort(markers.begin(), markers.end(), compare_markers);
+
+ EXPECT_EQ(10u, markers.size());
+
+ EXPECT_EQ(10u, markers[0]->StartOffset());
+ EXPECT_EQ(20u, markers[0]->EndOffset());
+
+ EXPECT_EQ(10u, markers[1]->StartOffset());
+ EXPECT_EQ(30u, markers[1]->EndOffset());
+
+ EXPECT_EQ(10u, markers[2]->StartOffset());
+ EXPECT_EQ(40u, markers[2]->EndOffset());
+
+ EXPECT_EQ(10u, markers[3]->StartOffset());
+ EXPECT_EQ(50u, markers[3]->EndOffset());
+
+ EXPECT_EQ(20u, markers[4]->StartOffset());
+ EXPECT_EQ(30u, markers[4]->EndOffset());
+
+ EXPECT_EQ(20u, markers[5]->StartOffset());
+ EXPECT_EQ(40u, markers[5]->EndOffset());
+
+ EXPECT_EQ(20u, markers[6]->StartOffset());
+ EXPECT_EQ(50u, markers[6]->EndOffset());
+
+ EXPECT_EQ(30u, markers[7]->StartOffset());
+ EXPECT_EQ(40u, markers[7]->EndOffset());
+
+ EXPECT_EQ(30u, markers[8]->StartOffset());
+ EXPECT_EQ(50u, markers[8]->EndOffset());
+
+ EXPECT_EQ(40u, markers[9]->StartOffset());
+ EXPECT_EQ(50u, markers[9]->EndOffset());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/composition_marker_test.cc b/chromium/third_party/blink/renderer/core/editing/markers/composition_marker_test.cc
new file mode 100644
index 00000000000..028e2658518
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/composition_marker_test.cc
@@ -0,0 +1,41 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/composition_marker.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+using ui::mojom::ImeTextSpanThickness;
+
+namespace blink {
+
+class CompositionMarkerTest : public testing::Test {};
+
+TEST_F(CompositionMarkerTest, MarkerType) {
+ DocumentMarker* marker =
+ new CompositionMarker(0, 1, Color::kTransparent,
+ ImeTextSpanThickness::kNone, Color::kTransparent);
+ EXPECT_EQ(DocumentMarker::kComposition, marker->GetType());
+}
+
+TEST_F(CompositionMarkerTest, IsStyleableMarker) {
+ DocumentMarker* marker =
+ new CompositionMarker(0, 1, Color::kTransparent,
+ ImeTextSpanThickness::kNone, Color::kTransparent);
+ EXPECT_TRUE(IsStyleableMarker(*marker));
+}
+
+TEST_F(CompositionMarkerTest, ConstructorAndGetters) {
+ CompositionMarker* marker = new CompositionMarker(
+ 0, 1, Color::kDarkGray, ImeTextSpanThickness::kThin, Color::kGray);
+ EXPECT_EQ(Color::kDarkGray, marker->UnderlineColor());
+ EXPECT_TRUE(marker->HasThicknessThin());
+ EXPECT_EQ(Color::kGray, marker->BackgroundColor());
+
+ CompositionMarker* thick_marker = new CompositionMarker(
+ 0, 1, Color::kDarkGray, ImeTextSpanThickness::kThick, Color::kGray);
+ EXPECT_TRUE(thick_marker->HasThicknessThick());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/document_marker.cc b/chromium/third_party/blink/renderer/core/editing/markers/document_marker.cc
new file mode 100644
index 00000000000..1fa1b804539
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/document_marker.cc
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/markers/document_marker.h"
+
+#include "third_party/blink/public/web/web_ax_enums.h"
+#include "third_party/blink/renderer/core/editing/markers/text_match_marker.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
+
+namespace blink {
+
+DocumentMarker::~DocumentMarker() = default;
+
+DocumentMarker::DocumentMarker(unsigned start_offset, unsigned end_offset)
+ : start_offset_(start_offset), end_offset_(end_offset) {
+ DCHECK_LT(start_offset_, end_offset_);
+}
+
+Optional<DocumentMarker::MarkerOffsets>
+DocumentMarker::ComputeOffsetsAfterShift(unsigned offset,
+ unsigned old_length,
+ unsigned new_length) const {
+ MarkerOffsets result;
+ result.start_offset = StartOffset();
+ result.end_offset = EndOffset();
+
+ // algorithm inspired by https://dom.spec.whatwg.org/#concept-cd-replace
+ // but with some changes
+
+ // Deviation from the concept-cd-replace algorithm: second condition in the
+ // next line (don't include text inserted immediately before a marker in the
+ // marked range, but do include the new text if it's replacing text in the
+ // marked range)
+ if (StartOffset() > offset || (StartOffset() == offset && old_length == 0)) {
+ if (StartOffset() <= offset + old_length) {
+ // Marker start was in the replaced text. Move to end of new text
+ // (Deviation from the concept-cd-replace algorithm: that algorithm
+ // would move to the beginning of the new text here)
+ result.start_offset = offset + new_length;
+ } else {
+ // Marker start was after the replaced text. Shift by length
+ // difference
+ result.start_offset = StartOffset() + new_length - old_length;
+ }
+ }
+
+ if (EndOffset() > offset) {
+ // Deviation from the concept-cd-replace algorithm: < instead of <= in
+ // the next line
+ if (EndOffset() < offset + old_length) {
+ // Marker end was in the replaced text. Move to beginning of new text
+ result.end_offset = offset;
+ } else {
+ // Marker end was after the replaced text. Shift by length difference
+ result.end_offset = EndOffset() + new_length - old_length;
+ }
+ }
+
+ if (result.start_offset >= result.end_offset)
+ return WTF::nullopt;
+
+ return result;
+}
+
+void DocumentMarker::ShiftOffsets(int delta) {
+ start_offset_ += delta;
+ end_offset_ += delta;
+}
+
+STATIC_ASSERT_ENUM(kWebAXMarkerTypeSpelling, DocumentMarker::kSpelling);
+STATIC_ASSERT_ENUM(kWebAXMarkerTypeGrammar, DocumentMarker::kGrammar);
+STATIC_ASSERT_ENUM(kWebAXMarkerTypeTextMatch, DocumentMarker::kTextMatch);
+STATIC_ASSERT_ENUM(kWebAXMarkerTypeActiveSuggestion,
+ DocumentMarker::kActiveSuggestion);
+STATIC_ASSERT_ENUM(kWebAXMarkerTypeSuggestion, DocumentMarker::kSuggestion);
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/document_marker.h b/chromium/third_party/blink/renderer/core/editing/markers/document_marker.h
new file mode 100644
index 00000000000..f1bcfbd8234
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/document_marker.h
@@ -0,0 +1,171 @@
+/*
+ * This file is part of the DOM implementation for WebCore.
+ *
+ * Copyright (C) 2006 Apple Computer, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_DOCUMENT_MARKER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_DOCUMENT_MARKER_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/platform/graphics/color.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/wtf/optional.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+#include "third_party/blink/renderer/platform/wtf/vector_traits.h"
+
+namespace blink {
+
+// A range of a node within a document that is "marked", such as the range of a
+// misspelled word. It optionally includes a description that could be displayed
+// in the user interface.
+class CORE_EXPORT DocumentMarker
+ : public GarbageCollectedFinalized<DocumentMarker> {
+ public:
+ enum MarkerTypeIndex {
+ kSpellingMarkerIndex = 0,
+ kGrammarMarkerIndex,
+ kTextMatchMarkerIndex,
+ kCompositionMarkerIndex,
+ kActiveSuggestionMarkerIndex,
+ kSuggestionMarkerIndex,
+ kMarkerTypeIndexesCount
+ };
+
+ enum MarkerType {
+ kSpelling = 1 << kSpellingMarkerIndex,
+ kGrammar = 1 << kGrammarMarkerIndex,
+ kTextMatch = 1 << kTextMatchMarkerIndex,
+ kComposition = 1 << kCompositionMarkerIndex,
+ kActiveSuggestion = 1 << kActiveSuggestionMarkerIndex,
+ kSuggestion = 1 << kSuggestionMarkerIndex,
+ };
+
+ class MarkerTypesIterator
+ : public std::iterator<std::forward_iterator_tag, MarkerType> {
+ public:
+ explicit MarkerTypesIterator(unsigned marker_types)
+ : remaining_types_(marker_types) {}
+ MarkerTypesIterator(const MarkerTypesIterator& other) = default;
+
+ bool operator==(const MarkerTypesIterator& other) {
+ return remaining_types_ == other.remaining_types_;
+ }
+ bool operator!=(const MarkerTypesIterator& other) {
+ return !operator==(other);
+ }
+
+ MarkerTypesIterator& operator++() {
+ DCHECK(remaining_types_);
+ // Turn off least significant 1-bit (from Hacker's Delight 2-1)
+ // Example:
+ // 7: 7 & 6 = 6
+ // 6: 6 & 5 = 4
+ // 4: 4 & 3 = 0
+ remaining_types_ &= (remaining_types_ - 1);
+ return *this;
+ }
+
+ MarkerType operator*() const {
+ DCHECK(remaining_types_);
+ // Isolate least significant 1-bit (from Hacker's Delight 2-1)
+ // Example:
+ // 7: 7 & -7 = 1
+ // 6: 6 & -6 = 2
+ // 4: 4 & -4 = 4
+ return static_cast<MarkerType>(remaining_types_ &
+ (~remaining_types_ + 1));
+ }
+
+ private:
+ unsigned remaining_types_;
+ };
+
+ class MarkerTypes {
+ public:
+ // The constructor is intentionally implicit to allow conversion from the
+ // bit-wise sum of above types
+ MarkerTypes(unsigned mask) : mask_(mask) {}
+
+ bool Contains(MarkerType type) const { return mask_ & type; }
+ bool Intersects(const MarkerTypes& types) const {
+ return (mask_ & types.mask_);
+ }
+ bool operator==(const MarkerTypes& other) const {
+ return mask_ == other.mask_;
+ }
+
+ void Add(const MarkerTypes& types) { mask_ |= types.mask_; }
+ void Remove(const MarkerTypes& types) { mask_ &= ~types.mask_; }
+
+ MarkerTypesIterator begin() const { return MarkerTypesIterator(mask_); }
+ MarkerTypesIterator end() const { return MarkerTypesIterator(0); }
+
+ private:
+ unsigned mask_;
+ };
+
+ class AllMarkers : public MarkerTypes {
+ public:
+ AllMarkers() : MarkerTypes((1 << kMarkerTypeIndexesCount) - 1) {}
+ };
+
+ class MisspellingMarkers : public MarkerTypes {
+ public:
+ MisspellingMarkers() : MarkerTypes(kSpelling | kGrammar) {}
+ };
+
+ virtual ~DocumentMarker();
+
+ virtual MarkerType GetType() const = 0;
+ unsigned StartOffset() const { return start_offset_; }
+ unsigned EndOffset() const { return end_offset_; }
+
+ struct MarkerOffsets {
+ unsigned start_offset;
+ unsigned end_offset;
+ };
+
+ Optional<MarkerOffsets> ComputeOffsetsAfterShift(unsigned offset,
+ unsigned old_length,
+ unsigned new_length) const;
+
+ // Offset modifications are done by DocumentMarkerController.
+ // Other classes should not call following setters.
+ void SetStartOffset(unsigned offset) { start_offset_ = offset; }
+ void SetEndOffset(unsigned offset) { end_offset_ = offset; }
+ void ShiftOffsets(int delta);
+
+ virtual void Trace(blink::Visitor* visitor) {}
+
+ protected:
+ DocumentMarker(unsigned start_offset, unsigned end_offset);
+
+ private:
+ unsigned start_offset_;
+ unsigned end_offset_;
+
+ DISALLOW_COPY_AND_ASSIGN(DocumentMarker);
+};
+
+using DocumentMarkerVector = HeapVector<Member<DocumentMarker>>;
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_DOCUMENT_MARKER_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/document_marker_controller.cc b/chromium/third_party/blink/renderer/core/editing/markers/document_marker_controller.cc
new file mode 100644
index 00000000000..1e851b0ba11
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/document_marker_controller.cc
@@ -0,0 +1,847 @@
+/*
+ * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
+ * (C) 1999 Antti Koivisto (koivisto@kde.org)
+ * (C) 2001 Dirk Mueller (mueller@kde.org)
+ * (C) 2006 Alexey Proskuryakov (ap@webkit.org)
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights
+ * reserved.
+ * Copyright (C) 2008, 2009 Torch Mobile Inc. All rights reserved.
+ * (http://www.torchmobile.com/)
+ * Copyright (C) Research In Motion Limited 2010. All rights reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ */
+
+#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+
+#include <algorithm>
+#include "third_party/blink/renderer/core/dom/ax_object_cache.h"
+#include "third_party/blink/renderer/core/dom/node.h"
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/markers/active_suggestion_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/active_suggestion_marker_list_impl.h"
+#include "third_party/blink/renderer/core/editing/markers/composition_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/composition_marker_list_impl.h"
+#include "third_party/blink/renderer/core/editing/markers/grammar_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/grammar_marker_list_impl.h"
+#include "third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor.h"
+#include "third_party/blink/renderer/core/editing/markers/spelling_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/spelling_marker_list_impl.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker_list_impl.h"
+#include "third_party/blink/renderer/core/editing/markers/text_match_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/text_match_marker_list_impl.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+
+#ifndef NDEBUG
+#include <stdio.h>
+#endif
+
+namespace blink {
+
+namespace {
+
+DocumentMarker::MarkerTypeIndex MarkerTypeToMarkerIndex(
+ DocumentMarker::MarkerType type) {
+ switch (type) {
+ case DocumentMarker::kSpelling:
+ return DocumentMarker::kSpellingMarkerIndex;
+ case DocumentMarker::kGrammar:
+ return DocumentMarker::kGrammarMarkerIndex;
+ case DocumentMarker::kTextMatch:
+ return DocumentMarker::kTextMatchMarkerIndex;
+ case DocumentMarker::kComposition:
+ return DocumentMarker::kCompositionMarkerIndex;
+ case DocumentMarker::kActiveSuggestion:
+ return DocumentMarker::kActiveSuggestionMarkerIndex;
+ case DocumentMarker::kSuggestion:
+ return DocumentMarker::kSuggestionMarkerIndex;
+ }
+
+ NOTREACHED();
+ return DocumentMarker::kSpellingMarkerIndex;
+}
+
+DocumentMarkerList* CreateListForType(DocumentMarker::MarkerType type) {
+ switch (type) {
+ case DocumentMarker::kActiveSuggestion:
+ return new ActiveSuggestionMarkerListImpl();
+ case DocumentMarker::kComposition:
+ return new CompositionMarkerListImpl();
+ case DocumentMarker::kSpelling:
+ return new SpellingMarkerListImpl();
+ case DocumentMarker::kGrammar:
+ return new GrammarMarkerListImpl();
+ case DocumentMarker::kSuggestion:
+ return new SuggestionMarkerListImpl();
+ case DocumentMarker::kTextMatch:
+ return new TextMatchMarkerListImpl();
+ }
+
+ NOTREACHED();
+ return nullptr;
+}
+
+void InvalidatePaintForNode(const Node& node) {
+ if (!node.GetLayoutObject())
+ return;
+
+ node.GetLayoutObject()->SetShouldDoFullPaintInvalidation(
+ PaintInvalidationReason::kDocumentMarker);
+
+ // Tell accessibility about the new marker.
+ AXObjectCache* ax_object_cache = node.GetDocument().ExistingAXObjectCache();
+ if (!ax_object_cache)
+ return;
+ // TODO(nektar): Do major refactoring of all AX classes to comply with const
+ // correctness.
+ Node* non_const_node = &const_cast<Node&>(node);
+ ax_object_cache->HandleTextMarkerDataAdded(non_const_node, non_const_node);
+}
+
+} // namespace
+
+Member<DocumentMarkerList>& DocumentMarkerController::ListForType(
+ MarkerLists* marker_lists,
+ DocumentMarker::MarkerType type) {
+ const size_t marker_list_index = MarkerTypeToMarkerIndex(type);
+ return (*marker_lists)[marker_list_index];
+}
+
+inline bool DocumentMarkerController::PossiblyHasMarkers(
+ DocumentMarker::MarkerTypes types) {
+ if (markers_.IsEmpty()) {
+ // It's possible for markers_ to become empty through garbage collection if
+ // all its Nodes are GC'ed since we only hold weak references, in which case
+ // possibly_existing_marker_types_ isn't reset to 0 as it is in the other
+ // codepaths that remove from markers_. Therefore, we check for this case
+ // here.
+
+ // Alternatively, we could handle this case at the time the Node is GC'ed,
+ // but that operation is more performance-sensitive than anywhere
+ // PossiblyHasMarkers() is used.
+ possibly_existing_marker_types_ = 0;
+ SetContext(nullptr);
+ return false;
+ }
+
+ return possibly_existing_marker_types_.Intersects(types);
+}
+
+DocumentMarkerController::DocumentMarkerController(Document& document)
+ : possibly_existing_marker_types_(0), document_(&document) {
+}
+
+void DocumentMarkerController::Clear() {
+ markers_.clear();
+ possibly_existing_marker_types_ = 0;
+ SetContext(nullptr);
+}
+
+void DocumentMarkerController::AddSpellingMarker(const EphemeralRange& range,
+ const String& description) {
+ AddMarkerInternal(range, [&description](int start_offset, int end_offset) {
+ return new SpellingMarker(start_offset, end_offset, description);
+ });
+}
+
+void DocumentMarkerController::AddGrammarMarker(const EphemeralRange& range,
+ const String& description) {
+ AddMarkerInternal(range, [&description](int start_offset, int end_offset) {
+ return new GrammarMarker(start_offset, end_offset, description);
+ });
+}
+
+void DocumentMarkerController::AddTextMatchMarker(
+ const EphemeralRange& range,
+ TextMatchMarker::MatchStatus match_status) {
+ DCHECK(!document_->NeedsLayoutTreeUpdate());
+ AddMarkerInternal(range, [match_status](int start_offset, int end_offset) {
+ return new TextMatchMarker(start_offset, end_offset, match_status);
+ });
+ // Don't invalidate tickmarks here. TextFinder invalidates tickmarks using a
+ // throttling algorithm. crbug.com/6819.
+}
+
+void DocumentMarkerController::AddCompositionMarker(
+ const EphemeralRange& range,
+ Color underline_color,
+ ui::mojom::ImeTextSpanThickness thickness,
+ Color background_color) {
+ DCHECK(!document_->NeedsLayoutTreeUpdate());
+ AddMarkerInternal(range, [underline_color, thickness, background_color](
+ int start_offset, int end_offset) {
+ return new CompositionMarker(start_offset, end_offset, underline_color,
+ thickness, background_color);
+ });
+}
+
+void DocumentMarkerController::AddActiveSuggestionMarker(
+ const EphemeralRange& range,
+ Color underline_color,
+ ui::mojom::ImeTextSpanThickness thickness,
+ Color background_color) {
+ DCHECK(!document_->NeedsLayoutTreeUpdate());
+ AddMarkerInternal(range, [underline_color, thickness, background_color](
+ int start_offset, int end_offset) {
+ return new ActiveSuggestionMarker(start_offset, end_offset, underline_color,
+ thickness, background_color);
+ });
+}
+
+void DocumentMarkerController::AddSuggestionMarker(
+ const EphemeralRange& range,
+ const SuggestionMarkerProperties& properties) {
+ DCHECK(!document_->NeedsLayoutTreeUpdate());
+ AddMarkerInternal(
+ range, [this, &properties](int start_offset, int end_offset) {
+ return new SuggestionMarker(start_offset, end_offset, properties);
+ });
+}
+
+void DocumentMarkerController::PrepareForDestruction() {
+ Clear();
+}
+
+void DocumentMarkerController::RemoveMarkers(
+ TextIterator& marked_text,
+ DocumentMarker::MarkerTypes marker_types) {
+ for (; !marked_text.AtEnd(); marked_text.Advance()) {
+ if (!PossiblyHasMarkers(marker_types))
+ return;
+ DCHECK(!markers_.IsEmpty());
+
+ int start_offset = marked_text.StartOffsetInCurrentContainer();
+ int end_offset = marked_text.EndOffsetInCurrentContainer();
+ RemoveMarkersInternal(marked_text.CurrentContainer(), start_offset,
+ end_offset - start_offset, marker_types);
+ }
+}
+
+void DocumentMarkerController::RemoveMarkersInRange(
+ const EphemeralRange& range,
+ DocumentMarker::MarkerTypes marker_types) {
+ DCHECK(!document_->NeedsLayoutTreeUpdate());
+
+ TextIterator marked_text(range.StartPosition(), range.EndPosition());
+ DocumentMarkerController::RemoveMarkers(marked_text, marker_types);
+}
+
+void DocumentMarkerController::AddMarkerInternal(
+ const EphemeralRange& range,
+ std::function<DocumentMarker*(int, int)> create_marker_from_offsets) {
+ for (TextIterator marked_text(range.StartPosition(), range.EndPosition());
+ !marked_text.AtEnd(); marked_text.Advance()) {
+ const int start_offset_in_current_container =
+ marked_text.StartOffsetInCurrentContainer();
+ const int end_offset_in_current_container =
+ marked_text.EndOffsetInCurrentContainer();
+
+ DCHECK_GE(end_offset_in_current_container,
+ start_offset_in_current_container);
+
+ // TODO(editing-dev): TextIterator sometimes emits ranges where the start
+ // and end offsets are the same. Investigate if TextIterator should be
+ // changed to not do this. See crbug.com/727929
+ if (end_offset_in_current_container == start_offset_in_current_container)
+ continue;
+
+ // Ignore text emitted by TextIterator for non-text nodes (e.g. implicit
+ // newlines)
+ const Node* const node = marked_text.CurrentContainer();
+ if (!node->IsTextNode())
+ continue;
+
+ DocumentMarker* const new_marker = create_marker_from_offsets(
+ start_offset_in_current_container, end_offset_in_current_container);
+ AddMarkerToNode(node, new_marker);
+ }
+}
+
+void DocumentMarkerController::AddMarkerToNode(const Node* node,
+ DocumentMarker* new_marker) {
+ possibly_existing_marker_types_.Add(new_marker->GetType());
+ SetContext(document_);
+
+ Member<MarkerLists>& markers =
+ markers_.insert(node, nullptr).stored_value->value;
+ if (!markers) {
+ markers = new MarkerLists;
+ markers->Grow(DocumentMarker::kMarkerTypeIndexesCount);
+ }
+
+ const DocumentMarker::MarkerType new_marker_type = new_marker->GetType();
+ if (!ListForType(markers, new_marker_type))
+ ListForType(markers, new_marker_type) = CreateListForType(new_marker_type);
+
+ DocumentMarkerList* const list = ListForType(markers, new_marker_type);
+ list->Add(new_marker);
+
+ InvalidatePaintForNode(*node);
+}
+
+// Moves markers from src_node to dst_node. Markers are moved if their start
+// offset is less than length. Markers that run past that point are truncated.
+void DocumentMarkerController::MoveMarkers(const Node* src_node,
+ int length,
+ const Node* dst_node) {
+ if (length <= 0)
+ return;
+
+ if (!PossiblyHasMarkers(DocumentMarker::AllMarkers()))
+ return;
+ DCHECK(!markers_.IsEmpty());
+
+ MarkerLists* src_markers = markers_.at(src_node);
+ if (!src_markers)
+ return;
+
+ if (!markers_.Contains(dst_node)) {
+ markers_.insert(dst_node,
+ new MarkerLists(DocumentMarker::kMarkerTypeIndexesCount));
+ }
+ MarkerLists* dst_markers = markers_.at(dst_node);
+
+ bool doc_dirty = false;
+ for (DocumentMarker::MarkerType type : DocumentMarker::AllMarkers()) {
+ DocumentMarkerList* const src_list = ListForType(src_markers, type);
+ if (!src_list)
+ continue;
+
+ if (!ListForType(dst_markers, type))
+ ListForType(dst_markers, type) = CreateListForType(type);
+
+ DocumentMarkerList* const dst_list = ListForType(dst_markers, type);
+ if (src_list->MoveMarkers(length, dst_list))
+ doc_dirty = true;
+ }
+
+ if (!doc_dirty)
+ return;
+
+ InvalidatePaintForNode(*dst_node);
+}
+
+void DocumentMarkerController::RemoveMarkersInternal(
+ const Node* node,
+ unsigned start_offset,
+ int length,
+ DocumentMarker::MarkerTypes marker_types) {
+ if (length <= 0)
+ return;
+
+ if (!PossiblyHasMarkers(marker_types))
+ return;
+ DCHECK(!(markers_.IsEmpty()));
+
+ MarkerLists* markers = markers_.at(node);
+ if (!markers)
+ return;
+
+ bool doc_dirty = false;
+ size_t empty_lists_count = 0;
+ for (DocumentMarker::MarkerType type : DocumentMarker::AllMarkers()) {
+ DocumentMarkerList* const list = ListForType(markers, type);
+ if (!list || list->IsEmpty()) {
+ if (list && list->IsEmpty())
+ ListForType(markers, type) = nullptr;
+ ++empty_lists_count;
+ continue;
+ }
+ if (!marker_types.Contains(type))
+ continue;
+
+ if (list->RemoveMarkers(start_offset, length))
+ doc_dirty = true;
+
+ if (list->IsEmpty()) {
+ ListForType(markers, type) = nullptr;
+ ++empty_lists_count;
+ }
+ }
+
+ if (empty_lists_count == DocumentMarker::kMarkerTypeIndexesCount) {
+ markers_.erase(node);
+ if (markers_.IsEmpty()) {
+ possibly_existing_marker_types_ = 0;
+ SetContext(nullptr);
+ }
+ }
+
+ if (!doc_dirty)
+ return;
+
+ InvalidatePaintForNode(*node);
+}
+
+DocumentMarker* DocumentMarkerController::FirstMarkerIntersectingOffsetRange(
+ const Text& node,
+ unsigned start_offset,
+ unsigned end_offset,
+ DocumentMarker::MarkerTypes types) {
+ if (!PossiblyHasMarkers(types))
+ return nullptr;
+
+ // Minor optimization: if we have an empty range at a node boundary, it
+ // doesn't fall in the interior of any marker.
+ if (start_offset == 0 && end_offset == 0)
+ return nullptr;
+ const unsigned node_length = node.length();
+ if (start_offset == node_length && end_offset == node_length)
+ return nullptr;
+
+ MarkerLists* const markers = markers_.at(&node);
+ if (!markers)
+ return nullptr;
+
+ for (DocumentMarker::MarkerType type : types) {
+ const DocumentMarkerList* const list = ListForType(markers, type);
+ if (!list)
+ continue;
+
+ DocumentMarker* found_marker =
+ list->FirstMarkerIntersectingRange(start_offset, end_offset);
+ if (found_marker)
+ return found_marker;
+ }
+
+ return nullptr;
+}
+
+HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>
+DocumentMarkerController::MarkersIntersectingRange(
+ const EphemeralRangeInFlatTree& range,
+ DocumentMarker::MarkerTypes types) {
+ HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>> node_marker_pairs;
+ if (!PossiblyHasMarkers(types))
+ return node_marker_pairs;
+
+ const Node* const range_start_container =
+ range.StartPosition().ComputeContainerNode();
+ const unsigned range_start_offset =
+ range.StartPosition().ComputeOffsetInContainerNode();
+ const Node* const range_end_container =
+ range.EndPosition().ComputeContainerNode();
+ const unsigned range_end_offset =
+ range.EndPosition().ComputeOffsetInContainerNode();
+
+ for (Node& node : range.Nodes()) {
+ MarkerLists* const markers = markers_.at(&node);
+ if (!markers)
+ continue;
+
+ for (DocumentMarker::MarkerType type : types) {
+ const DocumentMarkerList* const list = ListForType(markers, type);
+ if (!list)
+ continue;
+
+ const unsigned start_offset =
+ node == range_start_container ? range_start_offset : 0;
+ const unsigned max_character_offset = ToCharacterData(node).length();
+ const unsigned end_offset =
+ node == range_end_container ? range_end_offset : max_character_offset;
+
+ // Minor optimization: if we have an empty offset range at the boundary
+ // of a text node, it doesn't fall into the interior of any marker.
+ if (start_offset == 0 && end_offset == 0)
+ continue;
+ if (start_offset == max_character_offset && end_offset == 0)
+ continue;
+
+ const DocumentMarkerVector& markers_from_this_list =
+ list->MarkersIntersectingRange(start_offset, end_offset);
+ for (DocumentMarker* marker : markers_from_this_list)
+ node_marker_pairs.push_back(std::make_pair(&node, marker));
+ }
+ }
+
+ return node_marker_pairs;
+}
+
+DocumentMarkerVector DocumentMarkerController::MarkersFor(
+ const Node* node,
+ DocumentMarker::MarkerTypes marker_types) {
+ DocumentMarkerVector result;
+ if (!PossiblyHasMarkers(marker_types))
+ return result;
+
+ MarkerLists* markers = markers_.at(node);
+ if (!markers)
+ return result;
+
+ for (DocumentMarker::MarkerType type : marker_types) {
+ DocumentMarkerList* const list = ListForType(markers, type);
+ if (!list || list->IsEmpty())
+ continue;
+
+ result.AppendVector(list->GetMarkers());
+ }
+
+ std::sort(result.begin(), result.end(),
+ [](const Member<DocumentMarker>& marker1,
+ const Member<DocumentMarker>& marker2) {
+ return marker1->StartOffset() < marker2->StartOffset();
+ });
+ return result;
+}
+
+DocumentMarkerVector DocumentMarkerController::Markers() {
+ DocumentMarkerVector result;
+ for (MarkerMap::iterator i = markers_.begin(); i != markers_.end(); ++i) {
+ MarkerLists* markers = i->value.Get();
+ for (DocumentMarker::MarkerType type : DocumentMarker::AllMarkers()) {
+ DocumentMarkerList* const list = ListForType(markers, type);
+ if (!list)
+ continue;
+ result.AppendVector(list->GetMarkers());
+ }
+ }
+ std::sort(result.begin(), result.end(),
+ [](const Member<DocumentMarker>& marker1,
+ const Member<DocumentMarker>& marker2) {
+ return marker1->StartOffset() < marker2->StartOffset();
+ });
+ return result;
+}
+
+Vector<IntRect> DocumentMarkerController::LayoutRectsForTextMatchMarkers() {
+ DCHECK(!document_->View()->NeedsLayout());
+ DCHECK(!document_->NeedsLayoutTreeUpdate());
+
+ Vector<IntRect> result;
+
+ if (!PossiblyHasMarkers(DocumentMarker::kTextMatch))
+ return result;
+ DCHECK(!(markers_.IsEmpty()));
+
+ // outer loop: process each node
+ MarkerMap::iterator end = markers_.end();
+ for (MarkerMap::iterator node_iterator = markers_.begin();
+ node_iterator != end; ++node_iterator) {
+ // inner loop; process each marker in this node
+ const Node& node = *node_iterator->key;
+ if (!node.isConnected())
+ continue;
+ MarkerLists* markers = node_iterator->value.Get();
+ DocumentMarkerList* const list =
+ ListForType(markers, DocumentMarker::kTextMatch);
+ if (!list)
+ continue;
+ result.AppendVector(ToTextMatchMarkerListImpl(list)->LayoutRects(node));
+ }
+
+ return result;
+}
+
+static void InvalidatePaintForTickmarks(const Node& node) {
+ if (LocalFrameView* frame_view = node.GetDocument().View())
+ frame_view->InvalidatePaintForTickmarks();
+}
+
+void DocumentMarkerController::InvalidateRectsForTextMatchMarkersInNode(
+ const Node& node) {
+ MarkerLists* markers = markers_.at(&node);
+
+ const DocumentMarkerList* const marker_list =
+ ListForType(markers, DocumentMarker::kTextMatch);
+ if (!marker_list || marker_list->IsEmpty())
+ return;
+
+ const HeapVector<Member<DocumentMarker>>& markers_in_list =
+ marker_list->GetMarkers();
+ for (auto& marker : markers_in_list)
+ ToTextMatchMarker(marker)->Invalidate();
+
+ InvalidatePaintForTickmarks(node);
+}
+
+void DocumentMarkerController::InvalidateRectsForAllTextMatchMarkers() {
+ for (auto& node_markers : markers_) {
+ const Node& node = *node_markers.key;
+ InvalidateRectsForTextMatchMarkersInNode(node);
+ }
+}
+
+void DocumentMarkerController::Trace(blink::Visitor* visitor) {
+ visitor->Trace(markers_);
+ visitor->Trace(document_);
+ SynchronousMutationObserver::Trace(visitor);
+}
+
+void DocumentMarkerController::RemoveMarkersForNode(
+ const Node* node,
+ DocumentMarker::MarkerTypes marker_types) {
+ if (!PossiblyHasMarkers(marker_types))
+ return;
+ DCHECK(!markers_.IsEmpty());
+
+ MarkerMap::iterator iterator = markers_.find(node);
+ if (iterator != markers_.end())
+ RemoveMarkersFromList(iterator, marker_types);
+}
+
+void DocumentMarkerController::RemoveSpellingMarkersUnderWords(
+ const Vector<String>& words) {
+ for (auto& node_markers : markers_) {
+ const Node& node = *node_markers.key;
+ if (!node.IsTextNode())
+ continue;
+ MarkerLists* markers = node_markers.value;
+ for (DocumentMarker::MarkerType type :
+ DocumentMarker::MisspellingMarkers()) {
+ DocumentMarkerList* const list = ListForType(markers, type);
+ if (!list)
+ continue;
+ ToSpellCheckMarkerListImpl(list)->RemoveMarkersUnderWords(
+ ToText(node).data(), words);
+ }
+ }
+}
+
+void DocumentMarkerController::RemoveSuggestionMarkerByTag(const Node* node,
+ int32_t marker_tag) {
+ MarkerLists* markers = markers_.at(node);
+ SuggestionMarkerListImpl* const list = ToSuggestionMarkerListImpl(
+ ListForType(markers, DocumentMarker::kSuggestion));
+ if (!list->RemoveMarkerByTag(marker_tag))
+ return;
+ InvalidatePaintForNode(*node);
+}
+
+void DocumentMarkerController::RemoveMarkersOfTypes(
+ DocumentMarker::MarkerTypes marker_types) {
+ if (!PossiblyHasMarkers(marker_types))
+ return;
+ DCHECK(!markers_.IsEmpty());
+
+ HeapVector<Member<const Node>> nodes_with_markers;
+ CopyKeysToVector(markers_, nodes_with_markers);
+ unsigned size = nodes_with_markers.size();
+ for (unsigned i = 0; i < size; ++i) {
+ MarkerMap::iterator iterator = markers_.find(nodes_with_markers[i]);
+ if (iterator != markers_.end())
+ RemoveMarkersFromList(iterator, marker_types);
+ }
+
+ possibly_existing_marker_types_.Remove(marker_types);
+ if (PossiblyHasMarkers(DocumentMarker::AllMarkers()))
+ return;
+ SetContext(nullptr);
+}
+
+void DocumentMarkerController::RemoveMarkersFromList(
+ MarkerMap::iterator iterator,
+ DocumentMarker::MarkerTypes marker_types) {
+ bool needs_repainting = false;
+ bool node_can_be_removed;
+
+ size_t empty_lists_count = 0;
+ if (marker_types == DocumentMarker::AllMarkers()) {
+ needs_repainting = true;
+ node_can_be_removed = true;
+ } else {
+ MarkerLists* markers = iterator->value.Get();
+
+ for (DocumentMarker::MarkerType type : DocumentMarker::AllMarkers()) {
+ DocumentMarkerList* const list = ListForType(markers, type);
+ if (!list || list->IsEmpty()) {
+ if (list && list->IsEmpty())
+ ListForType(markers, type) = nullptr;
+ ++empty_lists_count;
+ continue;
+ }
+ if (marker_types.Contains(type)) {
+ list->Clear();
+ ListForType(markers, type) = nullptr;
+ ++empty_lists_count;
+ needs_repainting = true;
+ }
+ }
+
+ node_can_be_removed =
+ empty_lists_count == DocumentMarker::kMarkerTypeIndexesCount;
+ }
+
+ if (needs_repainting) {
+ const Node& node = *iterator->key;
+ InvalidatePaintForNode(node);
+ InvalidatePaintForTickmarks(node);
+ }
+
+ if (node_can_be_removed) {
+ markers_.erase(iterator);
+ if (markers_.IsEmpty()) {
+ possibly_existing_marker_types_ = 0;
+ SetContext(nullptr);
+ }
+ }
+}
+
+void DocumentMarkerController::RepaintMarkers(
+ DocumentMarker::MarkerTypes marker_types) {
+ if (!PossiblyHasMarkers(marker_types))
+ return;
+ DCHECK(!markers_.IsEmpty());
+
+ // outer loop: process each markered node in the document
+ MarkerMap::iterator end = markers_.end();
+ for (MarkerMap::iterator i = markers_.begin(); i != end; ++i) {
+ const Node* node = i->key;
+
+ // inner loop: process each marker in the current node
+ MarkerLists* markers = i->value.Get();
+ for (DocumentMarker::MarkerType type : DocumentMarker::AllMarkers()) {
+ DocumentMarkerList* const list = ListForType(markers, type);
+ if (!list || list->IsEmpty() || !marker_types.Contains(type))
+ continue;
+
+ InvalidatePaintForNode(*node);
+ }
+ }
+}
+
+bool DocumentMarkerController::SetTextMatchMarkersActive(
+ const EphemeralRange& range,
+ bool active) {
+ if (!PossiblyHasMarkers(DocumentMarker::kTextMatch))
+ return false;
+
+ DCHECK(!markers_.IsEmpty());
+
+ const Node* const start_container =
+ range.StartPosition().ComputeContainerNode();
+ DCHECK(start_container);
+ const Node* const end_container = range.EndPosition().ComputeContainerNode();
+ DCHECK(end_container);
+
+ const unsigned container_start_offset =
+ range.StartPosition().ComputeOffsetInContainerNode();
+ const unsigned container_end_offset =
+ range.EndPosition().ComputeOffsetInContainerNode();
+
+ bool marker_found = false;
+ for (Node& node : range.Nodes()) {
+ int start_offset = node == start_container ? container_start_offset : 0;
+ int end_offset = node == end_container ? container_end_offset : INT_MAX;
+ marker_found |=
+ SetTextMatchMarkersActive(&node, start_offset, end_offset, active);
+ }
+ return marker_found;
+}
+
+bool DocumentMarkerController::SetTextMatchMarkersActive(const Node* node,
+ unsigned start_offset,
+ unsigned end_offset,
+ bool active) {
+ MarkerLists* markers = markers_.at(node);
+ if (!markers)
+ return false;
+
+ DocumentMarkerList* const list =
+ ListForType(markers, DocumentMarker::kTextMatch);
+ if (!list)
+ return false;
+
+ bool doc_dirty = ToTextMatchMarkerListImpl(list)->SetTextMatchMarkersActive(
+ start_offset, end_offset, active);
+
+ if (!doc_dirty)
+ return false;
+ InvalidatePaintForNode(*node);
+ return true;
+}
+
+#ifndef NDEBUG
+void DocumentMarkerController::ShowMarkers() const {
+ StringBuilder builder;
+ MarkerMap::const_iterator end = markers_.end();
+ for (MarkerMap::const_iterator node_iterator = markers_.begin();
+ node_iterator != end; ++node_iterator) {
+ const Node* node = node_iterator->key;
+ builder.Append(String::Format("%p", node));
+ MarkerLists* markers = markers_.at(node);
+ for (DocumentMarker::MarkerType type : DocumentMarker::AllMarkers()) {
+ DocumentMarkerList* const list = ListForType(markers, type);
+ if (!list)
+ continue;
+
+ const HeapVector<Member<DocumentMarker>>& markers_in_list =
+ list->GetMarkers();
+ for (const DocumentMarker* marker : markers_in_list) {
+ builder.Append(" ");
+ builder.AppendNumber(marker->GetType());
+ builder.Append(":[");
+ builder.AppendNumber(marker->StartOffset());
+ builder.Append(":");
+ builder.AppendNumber(marker->EndOffset());
+ builder.Append("](");
+ builder.AppendNumber(type == DocumentMarker::kTextMatch
+ ? ToTextMatchMarker(marker)->IsActiveMatch()
+ : 0);
+ builder.Append(")");
+ }
+ }
+ builder.Append("\n");
+ }
+ LOG(INFO) << markers_.size() << " nodes have markers:\n"
+ << builder.ToString().Utf8().data();
+}
+#endif
+
+// SynchronousMutationObserver
+void DocumentMarkerController::DidUpdateCharacterData(CharacterData* node,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) {
+ if (!PossiblyHasMarkers(DocumentMarker::AllMarkers()))
+ return;
+ DCHECK(!markers_.IsEmpty());
+
+ MarkerLists* markers = markers_.at(node);
+ if (!markers)
+ return;
+
+ bool did_shift_marker = false;
+ for (DocumentMarkerList* const list : *markers) {
+ if (!list)
+ continue;
+
+ if (list->ShiftMarkers(node->data(), offset, old_length, new_length))
+ did_shift_marker = true;
+ }
+
+ if (!did_shift_marker)
+ return;
+ if (!node->GetLayoutObject())
+ return;
+ InvalidateRectsForTextMatchMarkersInNode(*node);
+ InvalidatePaintForNode(*node);
+}
+
+} // namespace blink
+
+#ifndef NDEBUG
+void showDocumentMarkers(const blink::DocumentMarkerController* controller) {
+ if (controller)
+ controller->ShowMarkers();
+}
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/document_marker_controller.h b/chromium/third_party/blink/renderer/core/editing/markers/document_marker_controller.h
new file mode 100644
index 00000000000..1ad98e2187e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/document_marker_controller.h
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
+ * (C) 1999 Antti Koivisto (koivisto@kde.org)
+ * (C) 2001 Dirk Mueller (mueller@kde.org)
+ * (C) 2006 Alexey Proskuryakov (ap@webkit.org)
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights
+ * reserved.
+ * Copyright (C) 2008, 2009 Torch Mobile Inc. All rights reserved.
+ * (http://www.torchmobile.com/)
+ * Copyright (C) Research In Motion Limited 2010. All rights reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_DOCUMENT_MARKER_CONTROLLER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_DOCUMENT_MARKER_CONTROLLER_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/dom/synchronous_mutation_observer.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/markers/composition_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/text_match_marker.h"
+#include "third_party/blink/renderer/platform/geometry/int_rect.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/wtf/hash_map.h"
+#include "third_party/blink/renderer/platform/wtf/vector.h"
+
+namespace blink {
+
+class DocumentMarkerList;
+class Node;
+class SuggestionMarkerProperties;
+
+class CORE_EXPORT DocumentMarkerController final
+ : public GarbageCollected<DocumentMarkerController>,
+ public SynchronousMutationObserver {
+ USING_GARBAGE_COLLECTED_MIXIN(DocumentMarkerController);
+
+ public:
+ explicit DocumentMarkerController(Document&);
+
+ void Clear();
+ void AddSpellingMarker(const EphemeralRange&,
+ const String& description = g_empty_string);
+ void AddGrammarMarker(const EphemeralRange&,
+ const String& description = g_empty_string);
+ void AddTextMatchMarker(const EphemeralRange&, TextMatchMarker::MatchStatus);
+ void AddCompositionMarker(const EphemeralRange&,
+ Color underline_color,
+ ui::mojom::ImeTextSpanThickness,
+ Color background_color);
+ void AddActiveSuggestionMarker(const EphemeralRange&,
+ Color underline_color,
+ ui::mojom::ImeTextSpanThickness,
+ Color background_color);
+ void AddSuggestionMarker(const EphemeralRange&,
+ const SuggestionMarkerProperties&);
+
+ void MoveMarkers(const Node* src_node, int length, const Node* dst_node);
+
+ void PrepareForDestruction();
+ void RemoveMarkersInRange(const EphemeralRange&, DocumentMarker::MarkerTypes);
+ void RemoveMarkersOfTypes(DocumentMarker::MarkerTypes);
+ void RemoveMarkersForNode(
+ const Node*,
+ DocumentMarker::MarkerTypes = DocumentMarker::AllMarkers());
+ void RemoveSpellingMarkersUnderWords(const Vector<String>& words);
+ void RemoveSuggestionMarkerByTag(const Node*, int32_t marker_tag);
+ void RepaintMarkers(
+ DocumentMarker::MarkerTypes = DocumentMarker::AllMarkers());
+ // Returns true if markers within a range are found.
+ bool SetTextMatchMarkersActive(const EphemeralRange&, bool);
+ // Returns true if markers within a range defined by a node, |startOffset| and
+ // |endOffset| are found.
+ bool SetTextMatchMarkersActive(const Node*,
+ unsigned start_offset,
+ unsigned end_offset,
+ bool);
+ bool HasMarkers(const Node* node) const { return markers_.Contains(node); }
+
+ // TODO(rlanday): can these methods for retrieving markers be consolidated
+ // without hurting efficiency?
+
+ // Looks for a marker in the specified node of the specified type whose
+ // interior has non-empty overlap with the range [start_offset, end_offset].
+ // If the range is collapsed, this looks for a marker containing the offset of
+ // the collapsed range in its interior.
+ // If such a marker exists, this method will return one of them (no guarantees
+ // are provided as to which one). Otherwise, this method will return null.
+ DocumentMarker* FirstMarkerIntersectingOffsetRange(
+ const Text&,
+ unsigned start_offset,
+ unsigned end_offset,
+ DocumentMarker::MarkerTypes);
+ // Return all markers of the specified types whose interiors have non-empty
+ // overlap with the specified range. Note that the range can be collapsed, in
+ // in which case markers containing the position in their interiors are
+ // returned.
+ HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>
+ MarkersIntersectingRange(const EphemeralRangeInFlatTree&,
+ DocumentMarker::MarkerTypes);
+ DocumentMarkerVector MarkersFor(
+ const Node*,
+ DocumentMarker::MarkerTypes = DocumentMarker::AllMarkers());
+ DocumentMarkerVector Markers();
+
+ Vector<IntRect> LayoutRectsForTextMatchMarkers();
+ void InvalidateRectsForAllTextMatchMarkers();
+ void InvalidateRectsForTextMatchMarkersInNode(const Node&);
+
+ void Trace(blink::Visitor*);
+
+#ifndef NDEBUG
+ void ShowMarkers() const;
+#endif
+
+ // SynchronousMutationObserver
+ // For performance, observer is only registered when
+ // |possibly_existing_marker_types_| is non-zero.
+ void DidUpdateCharacterData(CharacterData*,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) final;
+
+ private:
+ void AddMarkerInternal(
+ const EphemeralRange&,
+ std::function<DocumentMarker*(int, int)> create_marker_from_offsets);
+ void AddMarkerToNode(const Node*, DocumentMarker*);
+
+ using MarkerLists = HeapVector<Member<DocumentMarkerList>,
+ DocumentMarker::kMarkerTypeIndexesCount>;
+ using MarkerMap = HeapHashMap<WeakMember<const Node>, Member<MarkerLists>>;
+ static Member<DocumentMarkerList>& ListForType(MarkerLists*,
+ DocumentMarker::MarkerType);
+ bool PossiblyHasMarkers(DocumentMarker::MarkerTypes);
+ void RemoveMarkersFromList(MarkerMap::iterator, DocumentMarker::MarkerTypes);
+ void RemoveMarkers(TextIterator&, DocumentMarker::MarkerTypes);
+ void RemoveMarkersInternal(const Node*,
+ unsigned start_offset,
+ int length,
+ DocumentMarker::MarkerTypes);
+
+ MarkerMap markers_;
+ // Provide a quick way to determine whether a particular marker type is absent
+ // without going through the map.
+ DocumentMarker::MarkerTypes possibly_existing_marker_types_;
+ const Member<Document> document_;
+
+ DISALLOW_COPY_AND_ASSIGN(DocumentMarkerController);
+};
+
+} // namespace blink
+
+#ifndef NDEBUG
+void showDocumentMarkers(const blink::DocumentMarkerController*);
+#endif
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_DOCUMENT_MARKER_CONTROLLER_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/document_marker_controller_test.cc b/chromium/third_party/blink/renderer/core/editing/markers/document_marker_controller_test.cc
new file mode 100644
index 00000000000..f1bcea1c5a4
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/document_marker_controller_test.cc
@@ -0,0 +1,477 @@
+/*
+ * Copyright (c) 2013, Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+
+#include <memory>
+#include "base/memory/scoped_refptr.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker_properties.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
+
+namespace blink {
+
+class DocumentMarkerControllerTest : public EditingTestBase {
+ protected:
+ DocumentMarkerController& MarkerController() const {
+ return GetDocument().Markers();
+ }
+
+ Text* CreateTextNode(const char*);
+ void MarkNodeContents(Node*);
+ void MarkNodeContentsTextMatch(Node*);
+};
+
+Text* DocumentMarkerControllerTest::CreateTextNode(const char* text_contents) {
+ return GetDocument().createTextNode(String::FromUTF8(text_contents));
+}
+
+void DocumentMarkerControllerTest::MarkNodeContents(Node* node) {
+ // Force layoutObjects to be created; TextIterator, which is used in
+ // DocumentMarkerControllerTest::addMarker(), needs them.
+ GetDocument().UpdateStyleAndLayout();
+ auto range = EphemeralRange::RangeOfContents(*node);
+ MarkerController().AddSpellingMarker(range);
+}
+
+void DocumentMarkerControllerTest::MarkNodeContentsTextMatch(Node* node) {
+ // Force layoutObjects to be created; TextIterator, which is used in
+ // DocumentMarkerControllerTest::addMarker(), needs them.
+ GetDocument().UpdateStyleAndLayout();
+ auto range = EphemeralRange::RangeOfContents(*node);
+ MarkerController().AddTextMatchMarker(range,
+ TextMatchMarker::MatchStatus::kActive);
+}
+
+TEST_F(DocumentMarkerControllerTest, DidMoveToNewDocument) {
+ SetBodyContent("<b><i>foo</i></b>");
+ Element* parent = ToElement(GetDocument().body()->firstChild()->firstChild());
+ MarkNodeContents(parent);
+ EXPECT_EQ(1u, MarkerController().Markers().size());
+ Persistent<Document> another_document = Document::CreateForTest();
+ another_document->adoptNode(parent, ASSERT_NO_EXCEPTION);
+
+ // No more reference to marked node.
+ ThreadState::Current()->CollectAllGarbage();
+ EXPECT_EQ(0u, MarkerController().Markers().size());
+ EXPECT_EQ(0u, another_document->Markers().Markers().size());
+}
+
+TEST_F(DocumentMarkerControllerTest, NodeWillBeRemovedMarkedByNormalize) {
+ SetBodyContent("<b><i>foo</i></b>");
+ {
+ Element* parent =
+ ToElement(GetDocument().body()->firstChild()->firstChild());
+ parent->AppendChild(CreateTextNode("bar"));
+ MarkNodeContents(parent);
+ EXPECT_EQ(2u, MarkerController().Markers().size());
+ parent->normalize();
+ }
+ // No more reference to marked node.
+ ThreadState::Current()->CollectAllGarbage();
+ EXPECT_EQ(1u, MarkerController().Markers().size());
+}
+
+TEST_F(DocumentMarkerControllerTest, NodeWillBeRemovedMarkedByRemoveChildren) {
+ SetBodyContent("<b><i>foo</i></b>");
+ Element* parent = ToElement(GetDocument().body()->firstChild()->firstChild());
+ MarkNodeContents(parent);
+ EXPECT_EQ(1u, MarkerController().Markers().size());
+ parent->RemoveChildren();
+ // No more reference to marked node.
+ ThreadState::Current()->CollectAllGarbage();
+ EXPECT_EQ(0u, MarkerController().Markers().size());
+}
+
+TEST_F(DocumentMarkerControllerTest, NodeWillBeRemovedByRemoveMarked) {
+ SetBodyContent("<b><i>foo</i></b>");
+ {
+ Element* parent =
+ ToElement(GetDocument().body()->firstChild()->firstChild());
+ MarkNodeContents(parent);
+ EXPECT_EQ(1u, MarkerController().Markers().size());
+ parent->RemoveChild(parent->firstChild());
+ }
+ // No more reference to marked node.
+ ThreadState::Current()->CollectAllGarbage();
+ EXPECT_EQ(0u, MarkerController().Markers().size());
+}
+
+TEST_F(DocumentMarkerControllerTest, NodeWillBeRemovedMarkedByRemoveAncestor) {
+ SetBodyContent("<b><i>foo</i></b>");
+ {
+ Element* parent =
+ ToElement(GetDocument().body()->firstChild()->firstChild());
+ MarkNodeContents(parent);
+ EXPECT_EQ(1u, MarkerController().Markers().size());
+ parent->parentNode()->parentNode()->RemoveChild(parent->parentNode());
+ }
+ // No more reference to marked node.
+ ThreadState::Current()->CollectAllGarbage();
+ EXPECT_EQ(0u, MarkerController().Markers().size());
+}
+
+TEST_F(DocumentMarkerControllerTest, NodeWillBeRemovedMarkedByRemoveParent) {
+ SetBodyContent("<b><i>foo</i></b>");
+ {
+ Element* parent =
+ ToElement(GetDocument().body()->firstChild()->firstChild());
+ MarkNodeContents(parent);
+ EXPECT_EQ(1u, MarkerController().Markers().size());
+ parent->parentNode()->RemoveChild(parent);
+ }
+ // No more reference to marked node.
+ ThreadState::Current()->CollectAllGarbage();
+ EXPECT_EQ(0u, MarkerController().Markers().size());
+}
+
+TEST_F(DocumentMarkerControllerTest, NodeWillBeRemovedMarkedByReplaceChild) {
+ SetBodyContent("<b><i>foo</i></b>");
+ {
+ Element* parent =
+ ToElement(GetDocument().body()->firstChild()->firstChild());
+ MarkNodeContents(parent);
+ EXPECT_EQ(1u, MarkerController().Markers().size());
+ parent->ReplaceChild(CreateTextNode("bar"), parent->firstChild());
+ }
+ // No more reference to marked node.
+ ThreadState::Current()->CollectAllGarbage();
+ EXPECT_EQ(0u, MarkerController().Markers().size());
+}
+
+TEST_F(DocumentMarkerControllerTest, NodeWillBeRemovedBySetInnerHTML) {
+ SetBodyContent("<b><i>foo</i></b>");
+ {
+ Element* parent =
+ ToElement(GetDocument().body()->firstChild()->firstChild());
+ MarkNodeContents(parent);
+ EXPECT_EQ(1u, MarkerController().Markers().size());
+ SetBodyContent("");
+ }
+ // No more reference to marked node.
+ ThreadState::Current()->CollectAllGarbage();
+ EXPECT_EQ(0u, MarkerController().Markers().size());
+}
+
+TEST_F(DocumentMarkerControllerTest, UpdateRenderedRects) {
+ SetBodyContent("<div style='margin: 100px'>foo</div>");
+ Element* div = ToElement(GetDocument().body()->firstChild());
+ MarkNodeContentsTextMatch(div);
+ Vector<IntRect> rendered_rects =
+ MarkerController().LayoutRectsForTextMatchMarkers();
+ EXPECT_EQ(1u, rendered_rects.size());
+
+ div->setAttribute(HTMLNames::styleAttr, "margin: 200px");
+ GetDocument().UpdateStyleAndLayout();
+ Vector<IntRect> new_rendered_rects =
+ MarkerController().LayoutRectsForTextMatchMarkers();
+ EXPECT_EQ(1u, new_rendered_rects.size());
+ EXPECT_NE(rendered_rects[0], new_rendered_rects[0]);
+}
+
+TEST_F(DocumentMarkerControllerTest, CompositionMarkersNotMerged) {
+ SetBodyContent("<div style='margin: 100px'>foo</div>");
+ Node* text = GetDocument().body()->firstChild()->firstChild();
+ MarkerController().AddCompositionMarker(
+ EphemeralRange(Position(text, 0), Position(text, 1)), Color::kTransparent,
+ ui::mojom::ImeTextSpanThickness::kThin, Color::kBlack);
+ MarkerController().AddCompositionMarker(
+ EphemeralRange(Position(text, 1), Position(text, 3)), Color::kTransparent,
+ ui::mojom::ImeTextSpanThickness::kThick, Color::kBlack);
+
+ EXPECT_EQ(2u, MarkerController().Markers().size());
+}
+
+TEST_F(DocumentMarkerControllerTest, SetMarkerActiveTest) {
+ SetBodyContent("<b>foo</b>");
+ Element* b_element = ToElement(GetDocument().body()->firstChild());
+ EphemeralRange ephemeral_range = EphemeralRange::RangeOfContents(*b_element);
+ Position start_b_element =
+ ToPositionInDOMTree(ephemeral_range.StartPosition());
+ Position end_b_element = ToPositionInDOMTree(ephemeral_range.EndPosition());
+ const EphemeralRange range(start_b_element, end_b_element);
+ // Try to make active a marker that doesn't exist.
+ EXPECT_FALSE(MarkerController().SetTextMatchMarkersActive(range, true));
+
+ // Add a marker and try it once more.
+ MarkerController().AddTextMatchMarker(
+ range, TextMatchMarker::MatchStatus::kInactive);
+ EXPECT_EQ(1u, MarkerController().Markers().size());
+ EXPECT_TRUE(MarkerController().SetTextMatchMarkersActive(range, true));
+}
+
+TEST_F(DocumentMarkerControllerTest, RemoveStartOfMarker) {
+ SetBodyContent("<b>abc</b>");
+ Node* b_element = GetDocument().body()->firstChild();
+ Node* text = b_element->firstChild();
+
+ // Add marker under "abc"
+ EphemeralRange marker_range =
+ EphemeralRange(Position(text, 0), Position(text, 3));
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ // Remove markers that overlap "a"
+ marker_range = EphemeralRange(Position(text, 0), Position(text, 1));
+ GetDocument().Markers().RemoveMarkersInRange(marker_range,
+ DocumentMarker::AllMarkers());
+
+ EXPECT_EQ(0u, MarkerController().Markers().size());
+}
+
+TEST_F(DocumentMarkerControllerTest, RemoveMiddleOfMarker) {
+ SetBodyContent("<b>abc</b>");
+ Node* b_element = GetDocument().body()->firstChild();
+ Node* text = b_element->firstChild();
+
+ // Add marker under "abc"
+ EphemeralRange marker_range =
+ EphemeralRange(Position(text, 0), Position(text, 3));
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ // Remove markers that overlap "b"
+ marker_range = EphemeralRange(Position(text, 1), Position(text, 2));
+ GetDocument().Markers().RemoveMarkersInRange(marker_range,
+ DocumentMarker::AllMarkers());
+
+ EXPECT_EQ(0u, MarkerController().Markers().size());
+}
+
+TEST_F(DocumentMarkerControllerTest, RemoveEndOfMarker) {
+ SetBodyContent("<b>abc</b>");
+ Node* b_element = GetDocument().body()->firstChild();
+ Node* text = b_element->firstChild();
+
+ // Add marker under "abc"
+ EphemeralRange marker_range =
+ EphemeralRange(Position(text, 0), Position(text, 3));
+ GetDocument().Markers().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ // Remove markers that overlap "c"
+ marker_range = EphemeralRange(Position(text, 2), Position(text, 3));
+ GetDocument().Markers().RemoveMarkersInRange(marker_range,
+ DocumentMarker::AllMarkers());
+
+ EXPECT_EQ(0u, MarkerController().Markers().size());
+}
+
+TEST_F(DocumentMarkerControllerTest, RemoveSpellingMarkersUnderWords) {
+ SetBodyContent("<div contenteditable>foo</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ // Add a spelling marker and a text match marker to "foo".
+ const EphemeralRange marker_range(Position(text, 0), Position(text, 3));
+ MarkerController().AddSpellingMarker(marker_range);
+ MarkerController().AddTextMatchMarker(
+ marker_range, TextMatchMarker::MatchStatus::kInactive);
+
+ MarkerController().RemoveSpellingMarkersUnderWords({"foo"});
+
+ // RemoveSpellingMarkersUnderWords does not remove text match marker.
+ ASSERT_EQ(1u, MarkerController().Markers().size());
+ const DocumentMarker& marker = *MarkerController().Markers()[0];
+ EXPECT_EQ(0u, marker.StartOffset());
+ EXPECT_EQ(3u, marker.EndOffset());
+ EXPECT_EQ(DocumentMarker::kTextMatch, marker.GetType());
+}
+
+TEST_F(DocumentMarkerControllerTest, RemoveSuggestionMarkerByTag) {
+ SetBodyContent("<div contenteditable>foo</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ MarkerController().AddSuggestionMarker(
+ EphemeralRange(Position(text, 0), Position(text, 1)),
+ SuggestionMarkerProperties());
+
+ ASSERT_EQ(1u, MarkerController().Markers().size());
+ const SuggestionMarker& marker =
+ *ToSuggestionMarker(MarkerController().Markers()[0]);
+ MarkerController().RemoveSuggestionMarkerByTag(text, marker.Tag());
+ EXPECT_EQ(0u, MarkerController().Markers().size());
+}
+
+TEST_F(DocumentMarkerControllerTest, FirstMarkerIntersectingOffsetRange) {
+ SetBodyContent("<div contenteditable>123456789</div>");
+ GetDocument().UpdateStyleAndLayout();
+ Element* div = GetDocument().QuerySelector("div");
+ Text* text = ToText(div->firstChild());
+
+ // Add a spelling marker on "123"
+ MarkerController().AddSpellingMarker(
+ EphemeralRange(Position(text, 0), Position(text, 3)));
+
+ // Query for a spellcheck marker intersecting "3456"
+ const DocumentMarker* const result =
+ MarkerController().FirstMarkerIntersectingOffsetRange(
+ *text, 2, 6, DocumentMarker::MisspellingMarkers());
+
+ EXPECT_EQ(DocumentMarker::kSpelling, result->GetType());
+ EXPECT_EQ(0u, result->StartOffset());
+ EXPECT_EQ(3u, result->EndOffset());
+}
+
+TEST_F(DocumentMarkerControllerTest,
+ FirstMarkerIntersectingOffsetRange_collapsed) {
+ SetBodyContent("<div contenteditable>123456789</div>");
+ GetDocument().UpdateStyleAndLayout();
+ Element* div = GetDocument().QuerySelector("div");
+ Text* text = ToText(div->firstChild());
+
+ // Add a spelling marker on "123"
+ MarkerController().AddSpellingMarker(
+ EphemeralRange(Position(text, 0), Position(text, 3)));
+
+ // Query for a spellcheck marker containing the position between "1" and "2"
+ const DocumentMarker* const result =
+ MarkerController().FirstMarkerIntersectingOffsetRange(
+ *text, 1, 1, DocumentMarker::MisspellingMarkers());
+
+ EXPECT_EQ(DocumentMarker::kSpelling, result->GetType());
+ EXPECT_EQ(0u, result->StartOffset());
+ EXPECT_EQ(3u, result->EndOffset());
+}
+
+TEST_F(DocumentMarkerControllerTest, MarkersIntersectingRange) {
+ SetBodyContent("<div contenteditable>123456789</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ // Add a spelling marker on "123"
+ MarkerController().AddSpellingMarker(
+ EphemeralRange(Position(text, 0), Position(text, 3)));
+ // Add a text match marker on "456"
+ MarkerController().AddTextMatchMarker(
+ EphemeralRange(Position(text, 3), Position(text, 6)),
+ TextMatchMarker::MatchStatus::kInactive);
+ // Add a grammar marker on "789"
+ MarkerController().AddSpellingMarker(
+ EphemeralRange(Position(text, 6), Position(text, 9)));
+
+ // Query for spellcheck markers intersecting "3456". The text match marker
+ // should not be returned, nor should the spelling marker touching the range.
+ const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>& results =
+ MarkerController().MarkersIntersectingRange(
+ EphemeralRangeInFlatTree(PositionInFlatTree(text, 2),
+ PositionInFlatTree(text, 6)),
+ DocumentMarker::MisspellingMarkers());
+
+ EXPECT_EQ(1u, results.size());
+ EXPECT_EQ(DocumentMarker::kSpelling, results[0].second->GetType());
+ EXPECT_EQ(0u, results[0].second->StartOffset());
+ EXPECT_EQ(3u, results[0].second->EndOffset());
+}
+
+TEST_F(DocumentMarkerControllerTest, MarkersIntersectingCollapsedRange) {
+ SetBodyContent("<div contenteditable>123456789</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ // Add a spelling marker on "123"
+ MarkerController().AddSpellingMarker(
+ EphemeralRange(Position(text, 0), Position(text, 3)));
+
+ // Query for spellcheck markers containing the position between "1" and "2"
+ const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>& results =
+ MarkerController().MarkersIntersectingRange(
+ EphemeralRangeInFlatTree(PositionInFlatTree(text, 1),
+ PositionInFlatTree(text, 1)),
+ DocumentMarker::MisspellingMarkers());
+
+ EXPECT_EQ(1u, results.size());
+ EXPECT_EQ(DocumentMarker::kSpelling, results[0].second->GetType());
+ EXPECT_EQ(0u, results[0].second->StartOffset());
+ EXPECT_EQ(3u, results[0].second->EndOffset());
+}
+
+TEST_F(DocumentMarkerControllerTest, MarkersIntersectingRangeWithShadowDOM) {
+ // Set up some shadow elements in a way we know doesn't work properly when
+ // using EphemeralRange instead of EphemeralRangeInFlatTree:
+ // <div>not shadow</div>
+ // <div> (shadow DOM host)
+ // #shadow-root
+ // <div>shadow1</div>
+ // <div>shadow2</div>
+ // Caling MarkersIntersectingRange with an EphemeralRange starting in the
+ // "not shadow" text and ending in the "shadow1" text will crash.
+ SetBodyContent(
+ "<div id=\"not_shadow\">not shadow</div><div id=\"shadow_root\" />");
+ ShadowRoot* shadow_root = SetShadowContent(
+ "<div id=\"shadow1\">shadow1</div><div id=\"shadow2\">shadow2</div>",
+ "shadow_root");
+
+ Element* not_shadow_div = GetDocument().QuerySelector("#not_shadow");
+ Node* not_shadow_text = not_shadow_div->firstChild();
+
+ Element* shadow1 = shadow_root->QuerySelector("#shadow1");
+ Node* shadow1_text = shadow1->firstChild();
+
+ MarkerController().AddTextMatchMarker(
+ EphemeralRange(Position(not_shadow_text, 0),
+ Position(not_shadow_text, 10)),
+ TextMatchMarker::MatchStatus::kInactive);
+
+ const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>& results =
+ MarkerController().MarkersIntersectingRange(
+ EphemeralRangeInFlatTree(PositionInFlatTree(not_shadow_text, 9),
+ PositionInFlatTree(shadow1_text, 1)),
+ DocumentMarker::kTextMatch);
+ EXPECT_EQ(1u, results.size());
+}
+
+TEST_F(DocumentMarkerControllerTest, SuggestionMarkersHaveUniqueTags) {
+ SetBodyContent("<div contenteditable>foo</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ MarkerController().AddSuggestionMarker(
+ EphemeralRange(Position(text, 0), Position(text, 1)),
+ SuggestionMarkerProperties());
+ MarkerController().AddSuggestionMarker(
+ EphemeralRange(Position(text, 0), Position(text, 1)),
+ SuggestionMarkerProperties());
+
+ EXPECT_EQ(2u, MarkerController().Markers().size());
+ EXPECT_NE(ToSuggestionMarker(MarkerController().Markers()[0])->Tag(),
+ ToSuggestionMarker(MarkerController().Markers()[1])->Tag());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/document_marker_list.cc b/chromium/third_party/blink/renderer/core/editing/markers/document_marker_list.cc
new file mode 100644
index 00000000000..8608e8c08a7
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/document_marker_list.cc
@@ -0,0 +1,13 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/document_marker_list.h"
+
+namespace blink {
+
+DocumentMarkerList::DocumentMarkerList() = default;
+
+DocumentMarkerList::~DocumentMarkerList() = default;
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/document_marker_list.h b/chromium/third_party/blink/renderer/core/editing/markers/document_marker_list.h
new file mode 100644
index 00000000000..c067800d10a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/document_marker_list.h
@@ -0,0 +1,75 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_DOCUMENT_MARKER_LIST_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_DOCUMENT_MARKER_LIST_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+class DocumentMarker;
+
+// This is an interface implemented by classes that DocumentMarkerController
+// uses to store DocumentMarkers. Different implementations can be written
+// to handle different MarkerTypes (e.g. to provide more optimized handling of
+// MarkerTypes with different insertion/retrieval patterns, or to provide
+// different behavior for certain MarkerTypes).
+class CORE_EXPORT DocumentMarkerList
+ : public GarbageCollectedFinalized<DocumentMarkerList> {
+ public:
+ virtual ~DocumentMarkerList();
+
+ // Returns the single marker type supported by the list implementation.
+ virtual DocumentMarker::MarkerType MarkerType() const = 0;
+
+ virtual bool IsEmpty() const = 0;
+
+ virtual void Add(DocumentMarker*) = 0;
+ virtual void Clear() = 0;
+
+ // Returns all markers
+ virtual const HeapVector<Member<DocumentMarker>>& GetMarkers() const = 0;
+ // Returns the first marker whose interior overlaps with the range
+ // [start_offset, end_offset], or null if there is no such marker.
+ virtual DocumentMarker* FirstMarkerIntersectingRange(
+ unsigned start_offset,
+ unsigned end_offset) const = 0;
+ // Returns markers whose interiors have non-empty overlap with the range
+ // [start_offset, end_offset]. Note that the range can be collapsed, in which
+ // case markers containing the offset in their interiors are returned.
+ virtual HeapVector<Member<DocumentMarker>> MarkersIntersectingRange(
+ unsigned start_offset,
+ unsigned end_offset) const = 0;
+
+ // Returns true if at least one marker is copied, false otherwise
+ virtual bool MoveMarkers(int length, DocumentMarkerList* dst_list) = 0;
+
+ // Returns true if at least one marker is removed, false otherwise
+ virtual bool RemoveMarkers(unsigned start_offset, int length) = 0;
+
+ // Returns true if at least one marker is shifted or removed, false otherwise.
+ // Called in response to an edit replacing the range
+ // [offset, offset + old_length] by a string of length new_length.
+ // node_text is the full text of the affected node *after* the edit is
+ // applied.
+ virtual bool ShiftMarkers(const String& node_text,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) = 0;
+
+ virtual void Trace(blink::Visitor* visitor) {}
+
+ protected:
+ DocumentMarkerList();
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(DocumentMarkerList);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_DOCUMENT_MARKER_LIST_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/document_marker_test.cc b/chromium/third_party/blink/renderer/core/editing/markers/document_marker_test.cc
new file mode 100644
index 00000000000..996f4c40d84
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/document_marker_test.cc
@@ -0,0 +1,219 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/document_marker.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/editing/markers/text_match_marker.h"
+
+namespace blink {
+
+using MarkerOffsets = DocumentMarker::MarkerOffsets;
+
+class DocumentMarkerTest : public testing::Test {
+ protected:
+ DocumentMarker* CreateMarker(unsigned startOffset, unsigned endOffset) {
+ return new TextMatchMarker(startOffset, endOffset,
+ TextMatchMarker::MatchStatus::kInactive);
+ }
+};
+
+TEST_F(DocumentMarkerTest, MarkerTypeIteratorEmpty) {
+ DocumentMarker::MarkerTypes types(0);
+ EXPECT_TRUE(types.begin() == types.end());
+}
+
+TEST_F(DocumentMarkerTest, MarkerTypeIteratorOne) {
+ DocumentMarker::MarkerTypes types(DocumentMarker::kSpelling);
+ ASSERT_TRUE(types.begin() != types.end());
+ auto it = types.begin();
+ EXPECT_EQ(DocumentMarker::kSpelling, *it);
+ ++it;
+ EXPECT_TRUE(it == types.end());
+}
+
+TEST_F(DocumentMarkerTest, MarkerTypeIteratorConsecutive) {
+ DocumentMarker::MarkerTypes types(0b11); // Spelling | Grammar
+ ASSERT_TRUE(types.begin() != types.end());
+ auto it = types.begin();
+ EXPECT_EQ(DocumentMarker::kSpelling, *it);
+ ++it;
+ EXPECT_EQ(DocumentMarker::kGrammar, *it);
+ ++it;
+ EXPECT_TRUE(it == types.end());
+}
+
+TEST_F(DocumentMarkerTest, MarkerTypeIteratorDistributed) {
+ DocumentMarker::MarkerTypes types(0b101); // Spelling | TextMatch
+ ASSERT_TRUE(types.begin() != types.end());
+ auto it = types.begin();
+ EXPECT_EQ(DocumentMarker::kSpelling, *it);
+ ++it;
+ EXPECT_EQ(DocumentMarker::kTextMatch, *it);
+ ++it;
+ EXPECT_TRUE(it == types.end());
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_DeleteAfter) {
+ DocumentMarker* marker = CreateMarker(0, 5);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(13, 19, 0);
+ EXPECT_EQ(0u, result.value().start_offset);
+ EXPECT_EQ(5u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_DeleteEndAndAfter) {
+ DocumentMarker* marker = CreateMarker(10, 15);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(13, 19, 0);
+ EXPECT_EQ(10u, result.value().start_offset);
+ EXPECT_EQ(13u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_DeleteBeforeAndAfter) {
+ DocumentMarker* marker = CreateMarker(20, 25);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(13, 19, 0);
+ EXPECT_EQ(WTF::nullopt, result);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_DeleteBeforeAndBeginning) {
+ DocumentMarker* marker = CreateMarker(30, 35);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(13, 19, 0);
+ EXPECT_EQ(13u, result.value().start_offset);
+ EXPECT_EQ(16u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_DeleteBefore) {
+ DocumentMarker* marker = CreateMarker(40, 45);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(13, 19, 0);
+ EXPECT_EQ(21u, result.value().start_offset);
+ EXPECT_EQ(26u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_DeleteStartAndAfter) {
+ DocumentMarker* marker = CreateMarker(0, 5);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(0, 10, 0);
+ EXPECT_EQ(WTF::nullopt, result);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_DeleteBeforeAndEnd) {
+ DocumentMarker* marker = CreateMarker(5, 10);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(0, 10, 0);
+ EXPECT_EQ(WTF::nullopt, result);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_DeleteMarkerExactly) {
+ DocumentMarker* marker = CreateMarker(5, 10);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(5, 5, 0);
+ EXPECT_EQ(WTF::nullopt, result);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_DeleteMiddleOfMarker) {
+ DocumentMarker* marker = CreateMarker(5, 10);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(6, 3, 0);
+ EXPECT_EQ(5u, result.value().start_offset);
+ EXPECT_EQ(7u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_InsertAfter) {
+ DocumentMarker* marker = CreateMarker(0, 5);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(10, 0, 5);
+ EXPECT_EQ(0u, result.value().start_offset);
+ EXPECT_EQ(5u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_InsertImmediatelyAfter) {
+ DocumentMarker* marker = CreateMarker(0, 5);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(5, 0, 5);
+ EXPECT_EQ(0u, result.value().start_offset);
+ EXPECT_EQ(5u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_InsertInMiddle) {
+ DocumentMarker* marker = CreateMarker(0, 5);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(2, 0, 5);
+ EXPECT_EQ(0u, result.value().start_offset);
+ EXPECT_EQ(10u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_InsertImmediatelyBefore) {
+ DocumentMarker* marker = CreateMarker(0, 5);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(0, 0, 5);
+ EXPECT_EQ(5u, result.value().start_offset);
+ EXPECT_EQ(10u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_InsertBefore) {
+ DocumentMarker* marker = CreateMarker(5, 10);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(0, 0, 5);
+ EXPECT_EQ(10u, result.value().start_offset);
+ EXPECT_EQ(15u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_ReplaceAfter) {
+ DocumentMarker* marker = CreateMarker(0, 5);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(13, 19, 1);
+ EXPECT_EQ(0u, result.value().start_offset);
+ EXPECT_EQ(5u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_ReplaceEndAndAfter) {
+ DocumentMarker* marker = CreateMarker(10, 15);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(13, 19, 1);
+ EXPECT_EQ(10u, result.value().start_offset);
+ EXPECT_EQ(13u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_ReplaceBeforeAndAfter) {
+ DocumentMarker* marker = CreateMarker(20, 25);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(13, 19, 1);
+ EXPECT_EQ(WTF::nullopt, result);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_ReplaceBeforeAndBeginning) {
+ DocumentMarker* marker = CreateMarker(30, 35);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(13, 19, 1);
+ EXPECT_EQ(14u, result.value().start_offset);
+ EXPECT_EQ(17u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_ReplaceBefore) {
+ DocumentMarker* marker = CreateMarker(40, 45);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(13, 19, 1);
+ EXPECT_EQ(22u, result.value().start_offset);
+ EXPECT_EQ(27u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_ReplaceBeginning) {
+ DocumentMarker* marker = CreateMarker(0, 5);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(0, 2, 1);
+ EXPECT_EQ(0u, result.value().start_offset);
+ EXPECT_EQ(4u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_ReplaceEnd) {
+ DocumentMarker* marker = CreateMarker(0, 5);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(3, 2, 1);
+ EXPECT_EQ(0u, result.value().start_offset);
+ EXPECT_EQ(4u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_ReplaceExactly) {
+ DocumentMarker* marker = CreateMarker(0, 5);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(0, 5, 1);
+ EXPECT_EQ(0u, result.value().start_offset);
+ EXPECT_EQ(1u, result.value().end_offset);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_ReplaceBeginningAndAfter) {
+ DocumentMarker* marker = CreateMarker(0, 5);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(0, 6, 1);
+ EXPECT_EQ(WTF::nullopt, result);
+}
+
+TEST_F(DocumentMarkerTest, GetShiftedMarkerPosition_ReplaceBeforeAndEnd) {
+ DocumentMarker* marker = CreateMarker(5, 10);
+ Optional<MarkerOffsets> result = marker->ComputeOffsetsAfterShift(4, 6, 1);
+ EXPECT_EQ(WTF::nullopt, result);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker.cc b/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker.cc
new file mode 100644
index 00000000000..1131c8de855
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker.cc
@@ -0,0 +1,20 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/grammar_marker.h"
+
+namespace blink {
+
+GrammarMarker::GrammarMarker(unsigned start_offset,
+ unsigned end_offset,
+ const String& description)
+ : SpellCheckMarker(start_offset, end_offset, description) {
+ DCHECK_LT(start_offset, end_offset);
+}
+
+DocumentMarker::MarkerType GrammarMarker::GetType() const {
+ return DocumentMarker::kGrammar;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker.h b/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker.h
new file mode 100644
index 00000000000..54badaf4361
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker.h
@@ -0,0 +1,31 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_GRAMMAR_MARKER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_GRAMMAR_MARKER_H_
+
+#include "third_party/blink/renderer/core/editing/markers/spell_check_marker.h"
+
+namespace blink {
+
+// A subclass of DocumentMarker used to store information specific to grammar
+// markers. Spelling and grammar markers are identical except that they mark
+// either spelling or grammar errors, respectively, so nearly all functionality
+// is delegated to a common base class, SpellCheckMarker.
+class CORE_EXPORT GrammarMarker final : public SpellCheckMarker {
+ public:
+ GrammarMarker(unsigned start_offset,
+ unsigned end_offset,
+ const String& description);
+
+ private:
+ // DocumentMarker implementations
+ MarkerType GetType() const final;
+
+ DISALLOW_COPY_AND_ASSIGN(GrammarMarker);
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker_list_impl.cc b/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker_list_impl.cc
new file mode 100644
index 00000000000..fbe8d886bdd
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker_list_impl.cc
@@ -0,0 +1,13 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/grammar_marker_list_impl.h"
+
+namespace blink {
+
+DocumentMarker::MarkerType GrammarMarkerListImpl::MarkerType() const {
+ return DocumentMarker::kGrammar;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker_list_impl.h b/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker_list_impl.h
new file mode 100644
index 00000000000..26e7ef8e57d
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker_list_impl.h
@@ -0,0 +1,26 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_GRAMMAR_MARKER_LIST_IMPL_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_GRAMMAR_MARKER_LIST_IMPL_H_
+
+#include "third_party/blink/renderer/core/editing/markers/spell_check_marker_list_impl.h"
+
+namespace blink {
+
+// This is the DocumentMarkerList implementation used to store Grammar markers.
+class CORE_EXPORT GrammarMarkerListImpl final
+ : public SpellCheckMarkerListImpl {
+ public:
+ GrammarMarkerListImpl() = default;
+
+ DocumentMarker::MarkerType MarkerType() const final;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(GrammarMarkerListImpl);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_GRAMMAR_MARKER_LIST_IMPL_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker_list_impl_test.cc b/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker_list_impl_test.cc
new file mode 100644
index 00000000000..2cc83bb81f4
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker_list_impl_test.cc
@@ -0,0 +1,29 @@
+// Copyright 2017 The Chromium Authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/spell_check_marker_list_impl.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/editing/markers/grammar_marker_list_impl.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+// Functionality implemented in SpellCheckMarkerListImpl is tested in
+// SpellingMarkerListImplTest.cpp.
+
+class GrammarMarkerListImplTest : public testing::Test {
+ protected:
+ GrammarMarkerListImplTest() : marker_list_(new GrammarMarkerListImpl()) {}
+
+ Persistent<GrammarMarkerListImpl> marker_list_;
+};
+
+// Test cases for functionality implemented by GrammarMarkerListImpl.
+
+TEST_F(GrammarMarkerListImplTest, MarkerType) {
+ EXPECT_EQ(DocumentMarker::kGrammar, marker_list_->MarkerType());
+}
+
+} // namespace
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker_test.cc b/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker_test.cc
new file mode 100644
index 00000000000..c7c11974326
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/grammar_marker_test.cc
@@ -0,0 +1,30 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/grammar_marker.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace blink {
+
+const char* const kDescription = "Test description";
+
+class GrammarMarkerTest : public testing::Test {};
+
+TEST_F(GrammarMarkerTest, MarkerType) {
+ DocumentMarker* marker = new GrammarMarker(0, 1, kDescription);
+ EXPECT_EQ(DocumentMarker::kGrammar, marker->GetType());
+}
+
+TEST_F(GrammarMarkerTest, IsSpellCheckMarker) {
+ DocumentMarker* marker = new GrammarMarker(0, 1, kDescription);
+ EXPECT_TRUE(IsSpellCheckMarker(*marker));
+}
+
+TEST_F(GrammarMarkerTest, ConstructorAndGetters) {
+ GrammarMarker* marker = new GrammarMarker(0, 1, kDescription);
+ EXPECT_EQ(kDescription, marker->Description());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/marker_test_utilities.h b/chromium/third_party/blink/renderer/core/editing/markers/marker_test_utilities.h
new file mode 100644
index 00000000000..6fe2aff8601
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/marker_test_utilities.h
@@ -0,0 +1,21 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_MARKER_TEST_UTILITIES_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_MARKER_TEST_UTILITIES_H_
+
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+inline bool compare_markers(const Member<DocumentMarker>& marker1,
+ const Member<DocumentMarker>& marker2) {
+ if (marker1->StartOffset() != marker2->StartOffset())
+ return marker1->StartOffset() < marker2->StartOffset();
+
+ return marker1->EndOffset() < marker2->EndOffset();
+}
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_MARKER_TEST_UTILITIES_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor.cc b/chromium/third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor.cc
new file mode 100644
index 00000000000..1142f31b63c
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor.cc
@@ -0,0 +1,211 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor.h"
+
+#include "third_party/blink/renderer/core/editing/markers/spell_check_marker_list_impl.h"
+
+namespace blink {
+
+void SortedDocumentMarkerListEditor::AddMarkerWithoutMergingOverlapping(
+ MarkerList* list,
+ DocumentMarker* marker) {
+ if (list->IsEmpty() || list->back()->EndOffset() <= marker->StartOffset()) {
+ list->push_back(marker);
+ return;
+ }
+
+ const auto pos = std::lower_bound(
+ list->begin(), list->end(), marker,
+ [](const Member<DocumentMarker>& marker_in_list,
+ const DocumentMarker* marker_to_insert) {
+ return marker_in_list->StartOffset() < marker_to_insert->StartOffset();
+ });
+
+ // DCHECK that we're not trying to add a marker that overlaps an existing one
+ // (this method only works for lists which don't allow overlapping markers)
+ if (pos != list->end())
+ DCHECK_LE(marker->EndOffset(), (*pos)->StartOffset());
+
+ if (pos != list->begin())
+ DCHECK_GE(marker->StartOffset(), (*std::prev(pos))->EndOffset());
+
+ list->insert(pos - list->begin(), marker);
+}
+
+bool SortedDocumentMarkerListEditor::MoveMarkers(MarkerList* src_list,
+ int length,
+ DocumentMarkerList* dst_list) {
+ DCHECK_GT(length, 0);
+ bool didMoveMarker = false;
+ unsigned end_offset = length - 1;
+
+ MarkerList::iterator it;
+ for (it = src_list->begin(); it != src_list->end(); ++it) {
+ DocumentMarker& marker = **it;
+ if (marker.StartOffset() > end_offset)
+ break;
+
+ // Trim the marker to fit in dst_list's text node
+ if (marker.EndOffset() > end_offset)
+ marker.SetEndOffset(end_offset);
+
+ dst_list->Add(&marker);
+ didMoveMarker = true;
+ }
+
+ // Remove the range of markers that were moved to dstNode
+ src_list->EraseAt(0, it - src_list->begin());
+
+ return didMoveMarker;
+}
+
+bool SortedDocumentMarkerListEditor::RemoveMarkers(MarkerList* list,
+ unsigned start_offset,
+ int length) {
+ const unsigned end_offset = start_offset + length;
+ MarkerList::iterator start_pos = std::upper_bound(
+ list->begin(), list->end(), start_offset,
+ [](size_t start_offset, const Member<DocumentMarker>& marker) {
+ return start_offset < marker->EndOffset();
+ });
+
+ MarkerList::iterator end_pos = std::lower_bound(
+ list->begin(), list->end(), end_offset,
+ [](const Member<DocumentMarker>& marker, size_t end_offset) {
+ return marker->StartOffset() < end_offset;
+ });
+
+ list->EraseAt(start_pos - list->begin(), end_pos - start_pos);
+ return start_pos != end_pos;
+}
+
+bool SortedDocumentMarkerListEditor::ShiftMarkersContentDependent(
+ MarkerList* list,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) {
+ // Find first marker that ends after the start of the region being edited.
+ // Markers before this one can be left untouched. This saves us some time over
+ // scanning the entire list linearly if the edit region is near the end of the
+ // text node.
+ const MarkerList::iterator& shift_range_begin =
+ std::upper_bound(list->begin(), list->end(), offset,
+ [](size_t offset, const Member<DocumentMarker>& marker) {
+ return offset < marker->EndOffset();
+ });
+
+ MarkerList::iterator erase_range_end = shift_range_begin;
+
+ bool did_shift_marker = false;
+ for (MarkerList::iterator it = shift_range_begin; it != list->end(); ++it) {
+ DocumentMarker& marker = **it;
+
+ // marked text is (potentially) changed by edit, remove marker
+ if (marker.StartOffset() < offset + old_length) {
+ erase_range_end = std::next(it);
+ did_shift_marker = true;
+ continue;
+ }
+
+ // marked text is shifted but not changed
+ marker.ShiftOffsets(new_length - old_length);
+ did_shift_marker = true;
+ }
+
+ // Note: shift_range_begin could point at a marker being shifted instead of
+ // deleted, but if this is the case, we don't need to delete any markers, and
+ // EraseAt() will get 0 for the length param
+ list->EraseAt(shift_range_begin - list->begin(),
+ erase_range_end - shift_range_begin);
+ return did_shift_marker;
+}
+
+bool SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(
+ MarkerList* list,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) {
+ // Find first marker that ends after the start of the region being edited.
+ // Markers before this one can be left untouched. This saves us some time over
+ // scanning the entire list linearly if the edit region is near the end of the
+ // text node.
+ const MarkerList::iterator& shift_range_begin =
+ std::upper_bound(list->begin(), list->end(), offset,
+ [](size_t offset, const Member<DocumentMarker>& marker) {
+ return offset < marker->EndOffset();
+ });
+
+ MarkerList::iterator erase_range_begin = list->end();
+ MarkerList::iterator erase_range_end = list->end();
+
+ bool did_shift_marker = false;
+ for (MarkerList::iterator it = shift_range_begin; it != list->end(); ++it) {
+ DocumentMarker& marker = **it;
+ Optional<DocumentMarker::MarkerOffsets> result =
+ marker.ComputeOffsetsAfterShift(offset, old_length, new_length);
+ if (result == WTF::nullopt) {
+ if (erase_range_begin == list->end())
+ erase_range_begin = it;
+ erase_range_end = std::next(it);
+ did_shift_marker = true;
+ continue;
+ }
+
+ if (marker.StartOffset() != result.value().start_offset ||
+ marker.EndOffset() != result.value().end_offset) {
+ did_shift_marker = true;
+ marker.SetStartOffset(result.value().start_offset);
+ marker.SetEndOffset(result.value().end_offset);
+ }
+ }
+
+ list->EraseAt(erase_range_begin - list->begin(),
+ erase_range_end - erase_range_begin);
+ return did_shift_marker;
+}
+
+DocumentMarker* SortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(
+ const MarkerList& list,
+ unsigned start_offset,
+ unsigned end_offset) {
+ DCHECK_LE(start_offset, end_offset);
+
+ const auto& marker_it =
+ std::lower_bound(list.begin(), list.end(), start_offset,
+ [](const DocumentMarker* marker, unsigned start_offset) {
+ return marker->EndOffset() <= start_offset;
+ });
+ if (marker_it == list.end())
+ return nullptr;
+
+ DocumentMarker* marker = *marker_it;
+ if (marker->StartOffset() >= end_offset)
+ return nullptr;
+ return marker;
+}
+
+HeapVector<Member<DocumentMarker>>
+SortedDocumentMarkerListEditor::MarkersIntersectingRange(const MarkerList& list,
+ unsigned start_offset,
+ unsigned end_offset) {
+ DCHECK_LE(start_offset, end_offset);
+
+ const auto& start_it =
+ std::lower_bound(list.begin(), list.end(), start_offset,
+ [](const DocumentMarker* marker, unsigned start_offset) {
+ return marker->EndOffset() <= start_offset;
+ });
+ const auto& end_it =
+ std::upper_bound(list.begin(), list.end(), end_offset,
+ [](unsigned end_offset, const DocumentMarker* marker) {
+ return end_offset <= marker->StartOffset();
+ });
+
+ HeapVector<Member<DocumentMarker>> results;
+ std::copy(start_it, end_it, std::back_inserter(results));
+ return results;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor.h b/chromium/third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor.h
new file mode 100644
index 00000000000..7a430788a79
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor.h
@@ -0,0 +1,64 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SORTED_DOCUMENT_MARKER_LIST_EDITOR_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SORTED_DOCUMENT_MARKER_LIST_EDITOR_H_
+
+#include "third_party/blink/renderer/core/editing/markers/document_marker_list.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+class DocumentMarker;
+
+class CORE_EXPORT SortedDocumentMarkerListEditor final {
+ public:
+ using MarkerList = HeapVector<Member<DocumentMarker>>;
+
+ static void AddMarkerWithoutMergingOverlapping(MarkerList*, DocumentMarker*);
+
+ // Returns true if a marker was moved, false otherwise.
+ static bool MoveMarkers(MarkerList* src_list,
+ int length,
+ DocumentMarkerList* dst_list);
+
+ // Returns true if a marker was removed, false otherwise.
+ static bool RemoveMarkers(MarkerList*, unsigned start_offset, int length);
+
+ // The following two methods both update the position of a list's
+ // DocumentMarkers in response to editing operations. The difference is that
+ // if an editing operation actually changes the text spanned by a marker (as
+ // opposed to only changing text before or after the marker),
+ // ShiftMarkersContentDependent will remove the marker, and
+ // ShiftMarkersContentIndependent will attempt to keep tracking the marked
+ // region across edits.
+
+ // Returns true if a marker was shifted or removed, false otherwise.
+ static bool ShiftMarkersContentDependent(MarkerList*,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length);
+
+ // Returns true if a marker was shifted or removed, false otherwise.
+ static bool ShiftMarkersContentIndependent(MarkerList*,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length);
+
+ // Returns the first marker in the specified MarkerList whose interior
+ // overlaps overlap with the range [start_offset, end_offset], or null if
+ // there is no such marker.
+ static DocumentMarker* FirstMarkerIntersectingRange(const MarkerList&,
+ unsigned start_offset,
+ unsigned end_offset);
+
+ static HeapVector<Member<DocumentMarker>> MarkersIntersectingRange(
+ const MarkerList&,
+ unsigned start_offset,
+ unsigned end_offset);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SORTED_DOCUMENT_MARKER_LIST_EDITOR_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor_test.cc b/chromium/third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor_test.cc
new file mode 100644
index 00000000000..ac2da857338
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor_test.cc
@@ -0,0 +1,600 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/editing/markers/text_match_marker.h"
+
+namespace blink {
+
+class SortedDocumentMarkerListEditorTest : public testing::Test {
+ protected:
+ DocumentMarker* CreateMarker(unsigned startOffset, unsigned endOffset) {
+ return new TextMatchMarker(startOffset, endOffset,
+ TextMatchMarker::MatchStatus::kInactive);
+ }
+};
+
+TEST_F(SortedDocumentMarkerListEditorTest, RemoveMarkersEmptyList) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ SortedDocumentMarkerListEditor::RemoveMarkers(&markers, 0, 10);
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest, RemoveMarkersTouchingEndpoints) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+ markers.push_back(CreateMarker(10, 20));
+ markers.push_back(CreateMarker(20, 30));
+
+ SortedDocumentMarkerListEditor::RemoveMarkers(&markers, 10, 10);
+
+ EXPECT_EQ(2u, markers.size());
+
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(10u, markers[0]->EndOffset());
+
+ EXPECT_EQ(20u, markers[1]->StartOffset());
+ EXPECT_EQ(30u, markers[1]->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ RemoveMarkersOneCharacterIntoInterior) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+ markers.push_back(CreateMarker(10, 20));
+ markers.push_back(CreateMarker(20, 30));
+
+ SortedDocumentMarkerListEditor::RemoveMarkers(&markers, 9, 12);
+
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentDependentMarker_ReplaceStartOfMarker) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ SortedDocumentMarkerListEditor::ShiftMarkersContentDependent(&markers, 0, 5,
+ 5);
+
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_ReplaceStartOfMarker) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ // Replace with shorter text
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0, 5,
+ 4);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(9u, markers[0]->EndOffset());
+
+ // Replace with longer text
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0, 4,
+ 5);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(10u, markers[0]->EndOffset());
+
+ // Replace with text of same length
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0, 5,
+ 5);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(10u, markers[0]->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentDependentMarker_ReplaceContainsStartOfMarker) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(5, 15));
+
+ SortedDocumentMarkerListEditor::ShiftMarkersContentDependent(&markers, 0, 10,
+ 10);
+
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_ReplaceContainsStartOfMarker) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(5, 15));
+
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0,
+ 10, 10);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(10u, markers[0]->StartOffset());
+ EXPECT_EQ(15u, markers[0]->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentDependentMarker_ReplaceEndOfMarker) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ SortedDocumentMarkerListEditor::ShiftMarkersContentDependent(&markers, 5, 5,
+ 5);
+
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_ReplaceEndOfMarker) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ // Replace with shorter text
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 5, 5,
+ 4);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(9u, markers[0]->EndOffset());
+
+ // Replace with longer text
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 5, 4,
+ 5);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(10u, markers[0]->EndOffset());
+
+ // Replace with text of same length
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 5, 5,
+ 5);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(10u, markers[0]->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentDependentMarker_ReplaceContainsEndOfMarker) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ SortedDocumentMarkerListEditor::ShiftMarkersContentDependent(&markers, 5, 10,
+ 10);
+
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_ReplaceContainsEndOfMarker) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 5,
+ 10, 10);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(5u, markers[0]->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentDependentMarker_ReplaceEntireMarker) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ SortedDocumentMarkerListEditor::ShiftMarkersContentDependent(&markers, 0, 10,
+ 10);
+
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_ReplaceEntireMarker) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ // Replace with shorter text
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0,
+ 10, 9);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(9u, markers[0]->EndOffset());
+
+ // Replace with longer text
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0, 9,
+ 10);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(10u, markers[0]->EndOffset());
+
+ // Replace with text of same length
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0,
+ 10, 10);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(10u, markers[0]->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentDependentMarker_ReplaceTextWithMarkerAtBeginning) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ SortedDocumentMarkerListEditor::ShiftMarkersContentDependent(&markers, 0, 15,
+ 15);
+
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_ReplaceTextWithMarkerAtBeginning) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0,
+ 15, 15);
+
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentDependentMarker_ReplaceTextWithMarkerAtEnd) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(5, 15));
+
+ SortedDocumentMarkerListEditor::ShiftMarkersContentDependent(&markers, 0, 15,
+ 15);
+
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_ReplaceTextWithMarkerAtEnd) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(5, 15));
+
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0,
+ 15, 15);
+
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest, ContentDependentMarker_Deletions) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 5));
+ markers.push_back(CreateMarker(5, 10));
+ markers.push_back(CreateMarker(10, 15));
+ markers.push_back(CreateMarker(15, 20));
+ markers.push_back(CreateMarker(20, 25));
+
+ // Delete range containing the end of the second marker, the entire third
+ // marker, and the start of the fourth marker
+ SortedDocumentMarkerListEditor::ShiftMarkersContentDependent(&markers, 8, 9,
+ 0);
+
+ EXPECT_EQ(2u, markers.size());
+
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(5u, markers[0]->EndOffset());
+
+ EXPECT_EQ(11u, markers[1]->StartOffset());
+ EXPECT_EQ(16u, markers[1]->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest, ContentIndependentMarker_Deletions) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 5));
+ markers.push_back(CreateMarker(5, 10));
+ markers.push_back(CreateMarker(10, 15));
+ markers.push_back(CreateMarker(15, 20));
+ markers.push_back(CreateMarker(20, 25));
+
+ // Delete range containing the end of the second marker, the entire third
+ // marker, and the start of the fourth marker
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 8, 9,
+ 0);
+
+ EXPECT_EQ(4u, markers.size());
+
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(5u, markers[0]->EndOffset());
+
+ EXPECT_EQ(5u, markers[1]->StartOffset());
+ EXPECT_EQ(8u, markers[1]->EndOffset());
+
+ EXPECT_EQ(8u, markers[2]->StartOffset());
+ EXPECT_EQ(11u, markers[2]->EndOffset());
+
+ EXPECT_EQ(11u, markers[3]->StartOffset());
+ EXPECT_EQ(16u, markers[3]->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentDependentMarker_DeleteExactlyOnMarker) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ SortedDocumentMarkerListEditor::ShiftMarkersContentDependent(&markers, 0, 10,
+ 0);
+
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_DeleteExactlyOnMarker) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0,
+ 10, 0);
+
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentDependentMarker_InsertInMarkerInterior) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 5));
+ markers.push_back(CreateMarker(5, 10));
+ markers.push_back(CreateMarker(10, 15));
+
+ // insert in middle of second marker
+ SortedDocumentMarkerListEditor::ShiftMarkersContentDependent(&markers, 7, 0,
+ 5);
+
+ EXPECT_EQ(2u, markers.size());
+
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(5u, markers[0]->EndOffset());
+
+ EXPECT_EQ(15u, markers[1]->StartOffset());
+ EXPECT_EQ(20u, markers[1]->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_InsertInMarkerInterior) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 5));
+ markers.push_back(CreateMarker(5, 10));
+ markers.push_back(CreateMarker(10, 15));
+
+ // insert in middle of second marker
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 7, 0,
+ 5);
+
+ EXPECT_EQ(3u, markers.size());
+
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(5u, markers[0]->EndOffset());
+
+ EXPECT_EQ(5u, markers[1]->StartOffset());
+ EXPECT_EQ(15u, markers[1]->EndOffset());
+
+ EXPECT_EQ(15u, markers[2]->StartOffset());
+ EXPECT_EQ(20u, markers[2]->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentDependentMarker_InsertBetweenMarkers) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 5));
+ markers.push_back(CreateMarker(5, 10));
+ markers.push_back(CreateMarker(10, 15));
+
+ // insert before second marker
+ SortedDocumentMarkerListEditor::ShiftMarkersContentDependent(&markers, 5, 0,
+ 5);
+
+ EXPECT_EQ(3u, markers.size());
+
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(5u, markers[0]->EndOffset());
+
+ EXPECT_EQ(10u, markers[1]->StartOffset());
+ EXPECT_EQ(15u, markers[1]->EndOffset());
+
+ EXPECT_EQ(15u, markers[2]->StartOffset());
+ EXPECT_EQ(20u, markers[2]->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_InsertBetweenMarkers) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 5));
+ markers.push_back(CreateMarker(5, 10));
+ markers.push_back(CreateMarker(10, 15));
+
+ // insert before second marker
+ SortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 5, 0,
+ 5);
+
+ EXPECT_EQ(3u, markers.size());
+
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(5u, markers[0]->EndOffset());
+
+ EXPECT_EQ(10u, markers[1]->StartOffset());
+ EXPECT_EQ(15u, markers[1]->EndOffset());
+
+ EXPECT_EQ(15u, markers[2]->StartOffset());
+ EXPECT_EQ(20u, markers[2]->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest, FirstMarkerIntersectingRange_Empty) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 5));
+
+ DocumentMarker* marker =
+ SortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(markers, 10,
+ 15);
+ EXPECT_EQ(nullptr, marker);
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ FirstMarkerIntersectingRange_TouchingAfter) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 5));
+
+ DocumentMarker* marker =
+ SortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(markers, 5,
+ 10);
+ EXPECT_EQ(nullptr, marker);
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ FirstMarkerIntersectingRange_TouchingBefore) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(5, 10));
+
+ DocumentMarker* marker =
+ SortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(markers, 0,
+ 5);
+ EXPECT_EQ(nullptr, marker);
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ FirstMarkerIntersectingRange_IntersectingAfter) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(5, 10));
+
+ DocumentMarker* marker =
+ SortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(markers, 0,
+ 6);
+ EXPECT_NE(nullptr, marker);
+
+ EXPECT_EQ(5u, marker->StartOffset());
+ EXPECT_EQ(10u, marker->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ FirstMarkerIntersectingRange_IntersectingBefore) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(5, 10));
+
+ DocumentMarker* marker =
+ SortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(markers, 9,
+ 15);
+ EXPECT_NE(nullptr, marker);
+
+ EXPECT_EQ(5u, marker->StartOffset());
+ EXPECT_EQ(10u, marker->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ FirstMarkerIntersectingRange_MultipleMarkers) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 5));
+ markers.push_back(CreateMarker(5, 10));
+ markers.push_back(CreateMarker(10, 15));
+ markers.push_back(CreateMarker(15, 20));
+ markers.push_back(CreateMarker(20, 25));
+
+ DocumentMarker* marker =
+ SortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(markers, 7,
+ 17);
+ EXPECT_NE(nullptr, marker);
+
+ EXPECT_EQ(5u, marker->StartOffset());
+ EXPECT_EQ(10u, marker->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest, MarkersIntersectingRange_Empty) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 5));
+
+ SortedDocumentMarkerListEditor::MarkerList markers_intersecting_range =
+ SortedDocumentMarkerListEditor::MarkersIntersectingRange(markers, 10, 15);
+ EXPECT_EQ(0u, markers_intersecting_range.size());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ MarkersIntersectingRange_TouchingAfter) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 5));
+
+ SortedDocumentMarkerListEditor::MarkerList markers_intersecting_range =
+ SortedDocumentMarkerListEditor::MarkersIntersectingRange(markers, 5, 10);
+ EXPECT_EQ(0u, markers_intersecting_range.size());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ MarkersIntersectingRange_TouchingBefore) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(5, 10));
+
+ SortedDocumentMarkerListEditor::MarkerList markers_intersecting_range =
+ SortedDocumentMarkerListEditor::MarkersIntersectingRange(markers, 0, 5);
+ EXPECT_EQ(0u, markers_intersecting_range.size());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ MarkersIntersectingRange_IntersectingAfter) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(5, 10));
+
+ SortedDocumentMarkerListEditor::MarkerList markers_intersecting_range =
+ SortedDocumentMarkerListEditor::MarkersIntersectingRange(markers, 0, 6);
+ EXPECT_EQ(1u, markers_intersecting_range.size());
+
+ EXPECT_EQ(5u, markers_intersecting_range[0]->StartOffset());
+ EXPECT_EQ(10u, markers_intersecting_range[0]->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ MarkersIntersectingRange_IntersectingBefore) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(5, 10));
+
+ SortedDocumentMarkerListEditor::MarkerList markers_intersecting_range =
+ SortedDocumentMarkerListEditor::MarkersIntersectingRange(markers, 9, 15);
+ EXPECT_EQ(1u, markers_intersecting_range.size());
+
+ EXPECT_EQ(5u, markers_intersecting_range[0]->StartOffset());
+ EXPECT_EQ(10u, markers_intersecting_range[0]->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ MarkersIntersectingRange_CollapsedRange) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(5, 10));
+
+ SortedDocumentMarkerListEditor::MarkerList markers_intersecting_range =
+ SortedDocumentMarkerListEditor::MarkersIntersectingRange(markers, 7, 7);
+ EXPECT_EQ(1u, markers_intersecting_range.size());
+
+ EXPECT_EQ(5u, markers_intersecting_range[0]->StartOffset());
+ EXPECT_EQ(10u, markers_intersecting_range[0]->EndOffset());
+}
+
+TEST_F(SortedDocumentMarkerListEditorTest,
+ MarkersIntersectingRange_MultipleMarkers) {
+ SortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 5));
+ markers.push_back(CreateMarker(5, 10));
+ markers.push_back(CreateMarker(10, 15));
+ markers.push_back(CreateMarker(15, 20));
+ markers.push_back(CreateMarker(20, 25));
+
+ SortedDocumentMarkerListEditor::MarkerList markers_intersecting_range =
+ SortedDocumentMarkerListEditor::MarkersIntersectingRange(markers, 7, 17);
+ EXPECT_EQ(3u, markers_intersecting_range.size());
+
+ EXPECT_EQ(5u, markers_intersecting_range[0]->StartOffset());
+ EXPECT_EQ(10u, markers_intersecting_range[0]->EndOffset());
+
+ EXPECT_EQ(10u, markers_intersecting_range[1]->StartOffset());
+ EXPECT_EQ(15u, markers_intersecting_range[1]->EndOffset());
+
+ EXPECT_EQ(15u, markers_intersecting_range[2]->StartOffset());
+ EXPECT_EQ(20u, markers_intersecting_range[2]->EndOffset());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/spell_check_marker.cc b/chromium/third_party/blink/renderer/core/editing/markers/spell_check_marker.cc
new file mode 100644
index 00000000000..ed0c9813d75
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/spell_check_marker.cc
@@ -0,0 +1,21 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/spell_check_marker.h"
+
+namespace blink {
+
+SpellCheckMarker::SpellCheckMarker(unsigned start_offset,
+ unsigned end_offset,
+ const String& description)
+ : DocumentMarker(start_offset, end_offset), description_(description) {
+ DCHECK_LT(start_offset, end_offset);
+}
+
+bool IsSpellCheckMarker(const DocumentMarker& marker) {
+ DocumentMarker::MarkerType type = marker.GetType();
+ return type == DocumentMarker::kSpelling || type == DocumentMarker::kGrammar;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/spell_check_marker.h b/chromium/third_party/blink/renderer/core/editing/markers/spell_check_marker.h
new file mode 100644
index 00000000000..d89f33e6c18
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/spell_check_marker.h
@@ -0,0 +1,40 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SPELL_CHECK_MARKER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SPELL_CHECK_MARKER_H_
+
+#include "third_party/blink/renderer/core/editing/markers/document_marker.h"
+
+namespace blink {
+
+// A subclass of DocumentMarker used to implement functionality shared between
+// spelling and grammar markers. These two marker types both store a
+// description string that can contain suggested replacements for a misspelling
+// or grammar error.
+class CORE_EXPORT SpellCheckMarker : public DocumentMarker {
+ public:
+ SpellCheckMarker(unsigned start_offset,
+ unsigned end_offset,
+ const String& description);
+
+ const String& Description() const { return description_; }
+
+ private:
+ const String description_;
+
+ DISALLOW_COPY_AND_ASSIGN(SpellCheckMarker);
+};
+
+bool CORE_EXPORT IsSpellCheckMarker(const DocumentMarker&);
+
+DEFINE_TYPE_CASTS(SpellCheckMarker,
+ DocumentMarker,
+ marker,
+ IsSpellCheckMarker(*marker),
+ IsSpellCheckMarker(marker));
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/spell_check_marker_list_impl.cc b/chromium/third_party/blink/renderer/core/editing/markers/spell_check_marker_list_impl.cc
new file mode 100644
index 00000000000..b9462400476
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/spell_check_marker_list_impl.cc
@@ -0,0 +1,127 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/spell_check_marker_list_impl.h"
+
+#include "third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor.h"
+
+namespace blink {
+
+bool SpellCheckMarkerListImpl::IsEmpty() const {
+ return markers_.IsEmpty();
+}
+
+void SpellCheckMarkerListImpl::Add(DocumentMarker* marker) {
+ DCHECK_EQ(MarkerType(), marker->GetType());
+
+ if (markers_.IsEmpty() ||
+ markers_.back()->EndOffset() < marker->StartOffset()) {
+ markers_.push_back(marker);
+ return;
+ }
+
+ // Find first marker that ends after the one being inserted starts. If any
+ // markers overlap the one being inserted, this is the first one.
+ const auto& first_overlapping = std::lower_bound(
+ markers_.begin(), markers_.end(), marker,
+ [](const Member<DocumentMarker>& marker_in_list,
+ const DocumentMarker* marker_to_insert) {
+ return marker_in_list->EndOffset() < marker_to_insert->StartOffset();
+ });
+
+ // If this marker does not overlap the one being inserted, insert before it
+ // and we are done.
+ if (marker->EndOffset() < (*first_overlapping)->StartOffset()) {
+ markers_.insert(first_overlapping - markers_.begin(), marker);
+ return;
+ }
+
+ // Otherwise, find the last overlapping marker, replace the first marker with
+ // the newly-inserted marker (to get the new description), set its start and
+ // end offsets to include all the overlapped markers, and erase the rest of
+ // the old markers.
+
+ const auto& last_overlapping = std::upper_bound(
+ first_overlapping, markers_.end(), marker,
+ [](const DocumentMarker* marker_to_insert,
+ const Member<DocumentMarker>& marker_in_list) {
+ return marker_to_insert->EndOffset() < marker_in_list->StartOffset();
+ });
+
+ marker->SetStartOffset(
+ std::min(marker->StartOffset(), (*first_overlapping)->StartOffset()));
+ marker->SetEndOffset(
+ std::max(marker->EndOffset(), (*(last_overlapping - 1))->EndOffset()));
+
+ *first_overlapping = marker;
+ size_t num_to_erase = last_overlapping - (first_overlapping + 1);
+ markers_.EraseAt(first_overlapping + 1 - markers_.begin(), num_to_erase);
+}
+
+void SpellCheckMarkerListImpl::Clear() {
+ markers_.clear();
+}
+
+const HeapVector<Member<DocumentMarker>>& SpellCheckMarkerListImpl::GetMarkers()
+ const {
+ return markers_;
+}
+
+DocumentMarker* SpellCheckMarkerListImpl::FirstMarkerIntersectingRange(
+ unsigned start_offset,
+ unsigned end_offset) const {
+ return SortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(
+ markers_, start_offset, end_offset);
+}
+
+HeapVector<Member<DocumentMarker>>
+SpellCheckMarkerListImpl::MarkersIntersectingRange(unsigned start_offset,
+ unsigned end_offset) const {
+ return SortedDocumentMarkerListEditor::MarkersIntersectingRange(
+ markers_, start_offset, end_offset);
+}
+
+bool SpellCheckMarkerListImpl::MoveMarkers(int length,
+ DocumentMarkerList* dst_list) {
+ return SortedDocumentMarkerListEditor::MoveMarkers(&markers_, length,
+ dst_list);
+}
+
+bool SpellCheckMarkerListImpl::RemoveMarkers(unsigned start_offset,
+ int length) {
+ return SortedDocumentMarkerListEditor::RemoveMarkers(&markers_, start_offset,
+ length);
+}
+
+bool SpellCheckMarkerListImpl::ShiftMarkers(const String&,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) {
+ return SortedDocumentMarkerListEditor::ShiftMarkersContentDependent(
+ &markers_, offset, old_length, new_length);
+}
+
+void SpellCheckMarkerListImpl::Trace(blink::Visitor* visitor) {
+ visitor->Trace(markers_);
+ DocumentMarkerList::Trace(visitor);
+}
+
+bool SpellCheckMarkerListImpl::RemoveMarkersUnderWords(
+ const String& node_text,
+ const Vector<String>& words) {
+ bool removed_markers = false;
+ for (size_t j = markers_.size(); j > 0; --j) {
+ const DocumentMarker& marker = *markers_[j - 1];
+ const unsigned start = marker.StartOffset();
+ const unsigned length = marker.EndOffset() - marker.StartOffset();
+ const String& marker_text = node_text.Substring(start, length);
+ if (words.Contains(marker_text)) {
+ markers_.EraseAt(j - 1);
+ removed_markers = true;
+ }
+ }
+ return removed_markers;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/spell_check_marker_list_impl.h b/chromium/third_party/blink/renderer/core/editing/markers/spell_check_marker_list_impl.h
new file mode 100644
index 00000000000..d7fdd837a0f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/spell_check_marker_list_impl.h
@@ -0,0 +1,65 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SPELL_CHECK_MARKER_LIST_IMPL_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SPELL_CHECK_MARKER_LIST_IMPL_H_
+
+#include "third_party/blink/renderer/core/editing/markers/document_marker_list.h"
+
+namespace blink {
+
+// Nearly-complete implementation of DocumentMarkerList for Spelling or Grammar
+// markers (subclassed by SpellingMarkerListImpl and GrammarMarkerListImpl to
+// implement the MarkerType() method). Markers with touching endpoints are
+// merged on insert. Markers are kept sorted by start offset in order to be able
+// to do this efficiently.
+class CORE_EXPORT SpellCheckMarkerListImpl : public DocumentMarkerList {
+ public:
+ // DocumentMarkerList implementations
+ bool IsEmpty() const final;
+
+ void Add(DocumentMarker*) final;
+ void Clear() final;
+
+ const HeapVector<Member<DocumentMarker>>& GetMarkers() const final;
+ DocumentMarker* FirstMarkerIntersectingRange(unsigned start_offset,
+ unsigned end_offset) const final;
+ HeapVector<Member<DocumentMarker>> MarkersIntersectingRange(
+ unsigned start_offset,
+ unsigned end_offset) const final;
+
+ bool MoveMarkers(int length, DocumentMarkerList* dst_list) final;
+ bool RemoveMarkers(unsigned start_offset, int length) final;
+ bool ShiftMarkers(const String& node_text,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) final;
+
+ virtual void Trace(blink::Visitor*);
+
+ // SpellCheckMarkerListImpl-specific
+ // Returns true if a marker was removed, false otherwise.
+ bool RemoveMarkersUnderWords(const String& node_text,
+ const Vector<String>& words);
+
+ protected:
+ SpellCheckMarkerListImpl() = default;
+
+ private:
+ HeapVector<Member<DocumentMarker>> markers_;
+
+ DISALLOW_COPY_AND_ASSIGN(SpellCheckMarkerListImpl);
+};
+
+DEFINE_TYPE_CASTS(SpellCheckMarkerListImpl,
+ DocumentMarkerList,
+ list,
+ list->MarkerType() == DocumentMarker::kSpelling ||
+ list->MarkerType() == DocumentMarker::kGrammar,
+ list.MarkerType() == DocumentMarker::kSpelling ||
+ list.MarkerType() == DocumentMarker::kGrammar);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SPELL_CHECK_MARKER_LIST_IMPL_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker.cc b/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker.cc
new file mode 100644
index 00000000000..d7ff4f63261
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker.cc
@@ -0,0 +1,20 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/spelling_marker.h"
+
+namespace blink {
+
+SpellingMarker::SpellingMarker(unsigned start_offset,
+ unsigned end_offset,
+ const String& description)
+ : SpellCheckMarker(start_offset, end_offset, description) {
+ DCHECK_LT(start_offset, end_offset);
+}
+
+DocumentMarker::MarkerType SpellingMarker::GetType() const {
+ return DocumentMarker::kSpelling;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker.h b/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker.h
new file mode 100644
index 00000000000..0267b788f4b
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker.h
@@ -0,0 +1,31 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SPELLING_MARKER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SPELLING_MARKER_H_
+
+#include "third_party/blink/renderer/core/editing/markers/spell_check_marker.h"
+
+namespace blink {
+
+// A subclass of DocumentMarker used to store information specific to spelling
+// markers. Spelling and grammar markers are identical except that they mark
+// either spelling or grammar errors, respectively, so nearly all functionality
+// is delegated to a common base class, SpellCheckMarker.
+class CORE_EXPORT SpellingMarker final : public SpellCheckMarker {
+ public:
+ SpellingMarker(unsigned start_offset,
+ unsigned end_offset,
+ const String& description);
+
+ private:
+ // DocumentMarker implementations
+ MarkerType GetType() const final;
+
+ DISALLOW_COPY_AND_ASSIGN(SpellingMarker);
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker_list_impl.cc b/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker_list_impl.cc
new file mode 100644
index 00000000000..d7d4f030499
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker_list_impl.cc
@@ -0,0 +1,13 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/spelling_marker_list_impl.h"
+
+namespace blink {
+
+DocumentMarker::MarkerType SpellingMarkerListImpl::MarkerType() const {
+ return DocumentMarker::kSpelling;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker_list_impl.h b/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker_list_impl.h
new file mode 100644
index 00000000000..3462d86d79f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker_list_impl.h
@@ -0,0 +1,26 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SPELLING_MARKER_LIST_IMPL_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SPELLING_MARKER_LIST_IMPL_H_
+
+#include "third_party/blink/renderer/core/editing/markers/spell_check_marker_list_impl.h"
+
+namespace blink {
+
+// This is the DocumentMarkerList implementation used to store Spelling markers.
+class CORE_EXPORT SpellingMarkerListImpl final
+ : public SpellCheckMarkerListImpl {
+ public:
+ SpellingMarkerListImpl() = default;
+
+ DocumentMarker::MarkerType MarkerType() const final;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(SpellingMarkerListImpl);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SPELLING_MARKER_LIST_IMPL_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker_list_impl_test.cc b/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker_list_impl_test.cc
new file mode 100644
index 00000000000..dcfce27b16a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker_list_impl_test.cc
@@ -0,0 +1,160 @@
+// Copyright 2017 The Chromium Authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/spell_check_marker_list_impl.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/editing/markers/spelling_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/spelling_marker_list_impl.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+// This test class tests functionality implemented by SpellingMarkerListImpl and
+// also functionality implemented by its parent class SpellCheckMarkerListImpl.
+
+class SpellingMarkerListImplTest : public testing::Test {
+ protected:
+ SpellingMarkerListImplTest() : marker_list_(new SpellingMarkerListImpl()) {}
+
+ DocumentMarker* CreateMarker(unsigned start_offset, unsigned end_offset) {
+ return new SpellingMarker(start_offset, end_offset, g_empty_string);
+ }
+
+ Persistent<SpellingMarkerListImpl> marker_list_;
+};
+
+// Test cases for functionality implemented by SpellingMarkerListImpl.
+
+TEST_F(SpellingMarkerListImplTest, MarkerType) {
+ EXPECT_EQ(DocumentMarker::kSpelling, marker_list_->MarkerType());
+}
+
+// Test cases for functionality implemented by SpellCheckMarkerListImpl
+
+TEST_F(SpellingMarkerListImplTest, AddSorting) {
+ // Insert some markers in an arbitrary order and verify that the list stays
+ // sorted
+ marker_list_->Add(CreateMarker(80, 85));
+ marker_list_->Add(CreateMarker(40, 45));
+ marker_list_->Add(CreateMarker(10, 15));
+ marker_list_->Add(CreateMarker(0, 5));
+ marker_list_->Add(CreateMarker(70, 75));
+ marker_list_->Add(CreateMarker(90, 95));
+ marker_list_->Add(CreateMarker(60, 65));
+ marker_list_->Add(CreateMarker(50, 55));
+ marker_list_->Add(CreateMarker(30, 35));
+ marker_list_->Add(CreateMarker(20, 25));
+
+ EXPECT_EQ(10u, marker_list_->GetMarkers().size());
+
+ EXPECT_EQ(0u, marker_list_->GetMarkers()[0]->StartOffset());
+ EXPECT_EQ(5u, marker_list_->GetMarkers()[0]->EndOffset());
+
+ EXPECT_EQ(10u, marker_list_->GetMarkers()[1]->StartOffset());
+ EXPECT_EQ(15u, marker_list_->GetMarkers()[1]->EndOffset());
+
+ EXPECT_EQ(20u, marker_list_->GetMarkers()[2]->StartOffset());
+ EXPECT_EQ(25u, marker_list_->GetMarkers()[2]->EndOffset());
+
+ EXPECT_EQ(30u, marker_list_->GetMarkers()[3]->StartOffset());
+ EXPECT_EQ(35u, marker_list_->GetMarkers()[3]->EndOffset());
+
+ EXPECT_EQ(40u, marker_list_->GetMarkers()[4]->StartOffset());
+ EXPECT_EQ(45u, marker_list_->GetMarkers()[4]->EndOffset());
+
+ EXPECT_EQ(50u, marker_list_->GetMarkers()[5]->StartOffset());
+ EXPECT_EQ(55u, marker_list_->GetMarkers()[5]->EndOffset());
+
+ EXPECT_EQ(60u, marker_list_->GetMarkers()[6]->StartOffset());
+ EXPECT_EQ(65u, marker_list_->GetMarkers()[6]->EndOffset());
+
+ EXPECT_EQ(70u, marker_list_->GetMarkers()[7]->StartOffset());
+ EXPECT_EQ(75u, marker_list_->GetMarkers()[7]->EndOffset());
+
+ EXPECT_EQ(80u, marker_list_->GetMarkers()[8]->StartOffset());
+ EXPECT_EQ(85u, marker_list_->GetMarkers()[8]->EndOffset());
+
+ EXPECT_EQ(90u, marker_list_->GetMarkers()[9]->StartOffset());
+ EXPECT_EQ(95u, marker_list_->GetMarkers()[9]->EndOffset());
+}
+
+TEST_F(SpellingMarkerListImplTest, AddIntoEmptyList) {
+ marker_list_->Add(CreateMarker(5, 10));
+
+ EXPECT_EQ(1u, marker_list_->GetMarkers().size());
+
+ EXPECT_EQ(5u, marker_list_->GetMarkers()[0]->StartOffset());
+ EXPECT_EQ(10u, marker_list_->GetMarkers()[0]->EndOffset());
+}
+
+TEST_F(SpellingMarkerListImplTest, AddMarkerNonMerging) {
+ marker_list_->Add(CreateMarker(5, 10));
+ marker_list_->Add(CreateMarker(15, 20));
+
+ EXPECT_EQ(2u, marker_list_->GetMarkers().size());
+
+ EXPECT_EQ(5u, marker_list_->GetMarkers()[0]->StartOffset());
+ EXPECT_EQ(10u, marker_list_->GetMarkers()[0]->EndOffset());
+
+ EXPECT_EQ(15u, marker_list_->GetMarkers()[1]->StartOffset());
+ EXPECT_EQ(20u, marker_list_->GetMarkers()[1]->EndOffset());
+}
+
+TEST_F(SpellingMarkerListImplTest, AddMarkerMergingLater) {
+ marker_list_->Add(CreateMarker(5, 10));
+ marker_list_->Add(CreateMarker(0, 5));
+
+ EXPECT_EQ(1u, marker_list_->GetMarkers().size());
+
+ EXPECT_EQ(0u, marker_list_->GetMarkers()[0]->StartOffset());
+ EXPECT_EQ(10u, marker_list_->GetMarkers()[0]->EndOffset());
+}
+
+TEST_F(SpellingMarkerListImplTest, AddMarkerMergingEarlier) {
+ marker_list_->Add(CreateMarker(0, 5));
+ marker_list_->Add(CreateMarker(5, 10));
+
+ EXPECT_EQ(1u, marker_list_->GetMarkers().size());
+
+ EXPECT_EQ(0u, marker_list_->GetMarkers()[0]->StartOffset());
+ EXPECT_EQ(10u, marker_list_->GetMarkers()[0]->EndOffset());
+}
+
+TEST_F(SpellingMarkerListImplTest, AddMarkerMergingEarlierAndLater) {
+ marker_list_->Add(CreateMarker(0, 5));
+ marker_list_->Add(CreateMarker(10, 15));
+ marker_list_->Add(CreateMarker(5, 10));
+
+ EXPECT_EQ(1u, marker_list_->GetMarkers().size());
+
+ EXPECT_EQ(0u, marker_list_->GetMarkers()[0]->StartOffset());
+ EXPECT_EQ(15u, marker_list_->GetMarkers()[0]->EndOffset());
+}
+
+TEST_F(SpellingMarkerListImplTest, RemoveMarkersUnderWords) {
+ // wor
+ marker_list_->Add(CreateMarker(0, 3));
+
+ // word
+ marker_list_->Add(CreateMarker(4, 8));
+
+ // words
+ marker_list_->Add(CreateMarker(9, 14));
+
+ // word2
+ marker_list_->Add(CreateMarker(15, 20));
+
+ marker_list_->RemoveMarkersUnderWords("wor word words word2",
+ {"word", "word2"});
+ EXPECT_EQ(2u, marker_list_->GetMarkers().size());
+
+ EXPECT_EQ(0u, marker_list_->GetMarkers()[0]->StartOffset());
+ EXPECT_EQ(3u, marker_list_->GetMarkers()[0]->EndOffset());
+
+ EXPECT_EQ(9u, marker_list_->GetMarkers()[1]->StartOffset());
+ EXPECT_EQ(14u, marker_list_->GetMarkers()[1]->EndOffset());
+}
+
+} // namespace
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker_test.cc b/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker_test.cc
new file mode 100644
index 00000000000..f7e868ac199
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/spelling_marker_test.cc
@@ -0,0 +1,30 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/spelling_marker.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace blink {
+
+const char* const kTestDescription = "Test description";
+
+class SpellingMarkerTest : public testing::Test {};
+
+TEST_F(SpellingMarkerTest, MarkerType) {
+ DocumentMarker* marker = new SpellingMarker(0, 1, kTestDescription);
+ EXPECT_EQ(DocumentMarker::kSpelling, marker->GetType());
+}
+
+TEST_F(SpellingMarkerTest, IsSpellCheckMarker) {
+ DocumentMarker* marker = new SpellingMarker(0, 1, kTestDescription);
+ EXPECT_TRUE(IsSpellCheckMarker(*marker));
+}
+
+TEST_F(SpellingMarkerTest, ConstructorAndGetters) {
+ SpellingMarker* marker = new SpellingMarker(0, 1, kTestDescription);
+ EXPECT_EQ(kTestDescription, marker->Description());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/styleable_marker.cc b/chromium/third_party/blink/renderer/core/editing/markers/styleable_marker.cc
new file mode 100644
index 00000000000..51691b29762
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/styleable_marker.cc
@@ -0,0 +1,53 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/styleable_marker.h"
+
+using ui::mojom::ImeTextSpanThickness;
+
+namespace blink {
+
+StyleableMarker::StyleableMarker(unsigned start_offset,
+ unsigned end_offset,
+ Color underline_color,
+ ImeTextSpanThickness thickness,
+ Color background_color)
+ : DocumentMarker(start_offset, end_offset),
+ underline_color_(underline_color),
+ background_color_(background_color),
+ thickness_(thickness) {}
+
+Color StyleableMarker::UnderlineColor() const {
+ return underline_color_;
+}
+
+bool StyleableMarker::HasThicknessNone() const {
+ return thickness_ == ImeTextSpanThickness::kNone;
+}
+
+bool StyleableMarker::HasThicknessThin() const {
+ return thickness_ == ImeTextSpanThickness::kThin;
+}
+
+bool StyleableMarker::HasThicknessThick() const {
+ return thickness_ == ImeTextSpanThickness::kThick;
+}
+
+bool StyleableMarker::UseTextColor() const {
+ return thickness_ != ImeTextSpanThickness::kNone &&
+ underline_color_ == Color::kTransparent;
+}
+
+Color StyleableMarker::BackgroundColor() const {
+ return background_color_;
+}
+
+bool IsStyleableMarker(const DocumentMarker& marker) {
+ DocumentMarker::MarkerType type = marker.GetType();
+ return type == DocumentMarker::kComposition ||
+ type == DocumentMarker::kActiveSuggestion ||
+ type == DocumentMarker::kSuggestion;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/styleable_marker.h b/chromium/third_party/blink/renderer/core/editing/markers/styleable_marker.h
new file mode 100644
index 00000000000..d7d188106a1
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/styleable_marker.h
@@ -0,0 +1,49 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_STYLEABLE_MARKER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_STYLEABLE_MARKER_H_
+
+#include "services/ui/public/interfaces/ime/ime.mojom-shared.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker.h"
+
+namespace blink {
+
+// An abstract subclass of DocumentMarker to be subclassed by DocumentMarkers
+// that should be renderable with customizable formatting.
+class CORE_EXPORT StyleableMarker : public DocumentMarker {
+ public:
+ StyleableMarker(unsigned start_offset,
+ unsigned end_offset,
+ Color underline_color,
+ ui::mojom::ImeTextSpanThickness,
+ Color background_color);
+
+ // StyleableMarker-specific
+ Color UnderlineColor() const;
+ bool HasThicknessNone() const;
+ bool HasThicknessThin() const;
+ bool HasThicknessThick() const;
+ bool UseTextColor() const;
+ Color BackgroundColor() const;
+
+ private:
+ const Color underline_color_;
+ const Color background_color_;
+ const ui::mojom::ImeTextSpanThickness thickness_;
+
+ DISALLOW_COPY_AND_ASSIGN(StyleableMarker);
+};
+
+bool CORE_EXPORT IsStyleableMarker(const DocumentMarker&);
+
+DEFINE_TYPE_CASTS(StyleableMarker,
+ DocumentMarker,
+ marker,
+ IsStyleableMarker(*marker),
+ IsStyleableMarker(marker));
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker.cc b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker.cc
new file mode 100644
index 00000000000..2bc08704e06
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker.cc
@@ -0,0 +1,59 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker.h"
+
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker_properties.h"
+
+namespace blink {
+
+int32_t SuggestionMarker::current_tag_ = 0;
+
+SuggestionMarker::SuggestionMarker(unsigned start_offset,
+ unsigned end_offset,
+ const SuggestionMarkerProperties& properties)
+ : StyleableMarker(start_offset,
+ end_offset,
+ properties.UnderlineColor(),
+ properties.Thickness(),
+ properties.BackgroundColor()),
+ tag_(NextTag()),
+ suggestions_(properties.Suggestions()),
+ suggestion_type_(properties.Type()),
+ suggestion_highlight_color_(properties.HighlightColor()) {
+ DCHECK_GT(tag_, 0);
+}
+
+int32_t SuggestionMarker::Tag() const {
+ return tag_;
+}
+
+DocumentMarker::MarkerType SuggestionMarker::GetType() const {
+ return DocumentMarker::kSuggestion;
+}
+
+const Vector<String>& SuggestionMarker::Suggestions() const {
+ return suggestions_;
+}
+
+bool SuggestionMarker::IsMisspelling() const {
+ return suggestion_type_ == SuggestionType::kMisspelling;
+}
+
+Color SuggestionMarker::SuggestionHighlightColor() const {
+ return suggestion_highlight_color_;
+}
+
+void SuggestionMarker::SetSuggestion(uint32_t suggestion_index,
+ const String& new_suggestion) {
+ DCHECK_LT(suggestion_index, suggestions_.size());
+ suggestions_[suggestion_index] = new_suggestion;
+}
+
+// static
+int32_t SuggestionMarker::NextTag() {
+ return ++current_tag_;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker.h b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker.h
new file mode 100644
index 00000000000..3289450d94a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker.h
@@ -0,0 +1,63 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SUGGESTION_MARKER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SUGGESTION_MARKER_H_
+
+#include "third_party/blink/renderer/core/editing/markers/styleable_marker.h"
+
+namespace blink {
+
+class SuggestionMarkerProperties;
+
+// A subclass of StyleableMarker used to store information specific to
+// suggestion markers (used to represent Android SuggestionSpans). In addition
+// to the formatting information StyleableMarker holds, we also store a list of
+// suggested replacements for the marked region of text. In addition, each
+// SuggestionMarker is tagged with an integer so browser code can identify which
+// SuggestionMarker a suggestion replace operation pertains to.
+class CORE_EXPORT SuggestionMarker final : public StyleableMarker {
+ public:
+ enum class SuggestionType { kMisspelling, kNotMisspelling };
+
+ SuggestionMarker(unsigned start_offset,
+ unsigned end_offset,
+ const SuggestionMarkerProperties&);
+
+ // DocumentMarker implementations
+ MarkerType GetType() const final;
+
+ // SuggestionMarker-specific
+ int32_t Tag() const;
+ const Vector<String>& Suggestions() const;
+ bool IsMisspelling() const;
+ Color SuggestionHighlightColor() const;
+
+ // Replace the suggestion at suggestion_index with new_suggestion.
+ void SetSuggestion(unsigned suggestion_index, const String& new_suggestion);
+
+ private:
+ static int32_t NextTag();
+
+ static int32_t current_tag_;
+
+ // We use a signed int for the tag since it's passed to Java (as an opaque
+ // identifier), and Java does not support unsigned ints.
+ const int32_t tag_;
+ Vector<String> suggestions_;
+ const SuggestionType suggestion_type_;
+ const Color suggestion_highlight_color_;
+
+ DISALLOW_COPY_AND_ASSIGN(SuggestionMarker);
+};
+
+DEFINE_TYPE_CASTS(SuggestionMarker,
+ DocumentMarker,
+ marker,
+ marker->GetType() == DocumentMarker::kSuggestion,
+ marker.GetType() == DocumentMarker::kSuggestion);
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_list_impl.cc b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_list_impl.cc
new file mode 100644
index 00000000000..8395d3c5a51
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_list_impl.cc
@@ -0,0 +1,210 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker_list_impl.h"
+
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker_replacement_scope.h"
+#include "third_party/blink/renderer/core/editing/markers/unsorted_document_marker_list_editor.h"
+
+namespace blink {
+
+namespace {
+
+UChar32 GetCodePointAt(const String& text, size_t index) {
+ UChar32 c;
+ U16_GET(text, 0, index, text.length(), c);
+ return c;
+}
+
+Optional<DocumentMarker::MarkerOffsets>
+ComputeOffsetsAfterNonSuggestionEditingOperating(const DocumentMarker& marker,
+ const String& node_text,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) {
+ unsigned marker_start = marker.StartOffset();
+ unsigned marker_end = marker.EndOffset();
+
+ // Marked text was modified
+ if (offset < marker_end && offset + old_length > marker_start)
+ return {};
+
+ // Text inserted/replaced immediately after the marker, remove marker if first
+ // character is a (Unicode) letter or digit
+ if (offset == marker_end && new_length > 0) {
+ if (WTF::Unicode::IsAlphanumeric(GetCodePointAt(node_text, offset)))
+ return {};
+ return marker.ComputeOffsetsAfterShift(offset, old_length, new_length);
+ }
+
+ // Text inserted/replaced immediately before the marker, remove marker if
+ // first character is a (Unicode) letter or digit
+ if (offset == marker_start && new_length > 0) {
+ if (WTF::Unicode::IsAlphanumeric(
+ GetCodePointAt(node_text, offset + new_length - 1)))
+ return {};
+ return marker.ComputeOffsetsAfterShift(offset, old_length, new_length);
+ }
+
+ // Don't care if text was deleted immediately before or after the marker
+ return marker.ComputeOffsetsAfterShift(offset, old_length, new_length);
+}
+
+} // namespace
+
+DocumentMarker::MarkerType SuggestionMarkerListImpl::MarkerType() const {
+ return DocumentMarker::kSuggestion;
+}
+
+bool SuggestionMarkerListImpl::IsEmpty() const {
+ return markers_.IsEmpty();
+}
+
+void SuggestionMarkerListImpl::Add(DocumentMarker* marker) {
+ DCHECK_EQ(DocumentMarker::kSuggestion, marker->GetType());
+ markers_.push_back(marker);
+}
+
+void SuggestionMarkerListImpl::Clear() {
+ markers_.clear();
+}
+
+const HeapVector<Member<DocumentMarker>>& SuggestionMarkerListImpl::GetMarkers()
+ const {
+ return markers_;
+}
+
+DocumentMarker* SuggestionMarkerListImpl::FirstMarkerIntersectingRange(
+ unsigned start_offset,
+ unsigned end_offset) const {
+ return UnsortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(
+ markers_, start_offset, end_offset);
+}
+
+HeapVector<Member<DocumentMarker>>
+SuggestionMarkerListImpl::MarkersIntersectingRange(unsigned start_offset,
+ unsigned end_offset) const {
+ return UnsortedDocumentMarkerListEditor::MarkersIntersectingRange(
+ markers_, start_offset, end_offset);
+}
+
+bool SuggestionMarkerListImpl::MoveMarkers(int length,
+ DocumentMarkerList* dst_list) {
+ return UnsortedDocumentMarkerListEditor::MoveMarkers(&markers_, length,
+ dst_list);
+}
+
+bool SuggestionMarkerListImpl::RemoveMarkers(unsigned start_offset,
+ int length) {
+ return UnsortedDocumentMarkerListEditor::RemoveMarkers(&markers_,
+ start_offset, length);
+}
+
+bool SuggestionMarkerListImpl::ShiftMarkers(const String& node_text,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) {
+ if (SuggestionMarkerReplacementScope::CurrentlyInScope())
+ return ShiftMarkersForSuggestionReplacement(offset, old_length, new_length);
+
+ return ShiftMarkersForNonSuggestionEditingOperation(node_text, offset,
+ old_length, new_length);
+}
+
+bool SuggestionMarkerListImpl::ShiftMarkersForSuggestionReplacement(
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) {
+ // Since suggestion markers are stored unsorted, the quickest way to perform
+ // this operation is to build a new list with the markers not removed by the
+ // shift.
+ bool did_shift_marker = false;
+ unsigned end_offset = offset + old_length;
+ HeapVector<Member<DocumentMarker>> unremoved_markers;
+ for (const Member<DocumentMarker>& marker : markers_) {
+ // Markers that intersect the replacement range, but do not fully contain
+ // it, should be removed.
+ const bool marker_intersects_replacement_range =
+ marker->StartOffset() < end_offset && marker->EndOffset() > offset;
+ const bool marker_contains_replacement_range =
+ marker->StartOffset() <= offset && marker->EndOffset() >= end_offset;
+
+ if (marker_intersects_replacement_range &&
+ !marker_contains_replacement_range) {
+ did_shift_marker = true;
+ continue;
+ }
+
+ Optional<DocumentMarker::MarkerOffsets> result =
+ marker->ComputeOffsetsAfterShift(offset, old_length, new_length);
+ if (result == WTF::nullopt) {
+ did_shift_marker = true;
+ continue;
+ }
+
+ if (marker->StartOffset() != result.value().start_offset ||
+ marker->EndOffset() != result.value().end_offset) {
+ marker->SetStartOffset(result.value().start_offset);
+ marker->SetEndOffset(result.value().end_offset);
+ did_shift_marker = true;
+ }
+
+ unremoved_markers.push_back(marker);
+ }
+
+ markers_ = std::move(unremoved_markers);
+ return did_shift_marker;
+}
+
+bool SuggestionMarkerListImpl::ShiftMarkersForNonSuggestionEditingOperation(
+ const String& node_text,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) {
+ // Since suggestion markers are stored unsorted, the quickest way to perform
+ // this operation is to build a new list with the markers not removed by the
+ // shift.
+ bool did_shift_marker = false;
+ HeapVector<Member<DocumentMarker>> unremoved_markers;
+ for (const Member<DocumentMarker>& marker : markers_) {
+ Optional<DocumentMarker::MarkerOffsets> result =
+ ComputeOffsetsAfterNonSuggestionEditingOperating(
+ *marker, node_text, offset, old_length, new_length);
+ if (!result) {
+ did_shift_marker = true;
+ continue;
+ }
+
+ if (marker->StartOffset() != result.value().start_offset ||
+ marker->EndOffset() != result.value().end_offset) {
+ marker->SetStartOffset(result.value().start_offset);
+ marker->SetEndOffset(result.value().end_offset);
+ did_shift_marker = true;
+ }
+
+ unremoved_markers.push_back(marker);
+ }
+
+ markers_ = std::move(unremoved_markers);
+ return did_shift_marker;
+}
+
+void SuggestionMarkerListImpl::Trace(blink::Visitor* visitor) {
+ visitor->Trace(markers_);
+ DocumentMarkerList::Trace(visitor);
+}
+
+bool SuggestionMarkerListImpl::RemoveMarkerByTag(int32_t tag) {
+ for (auto it = markers_.begin(); it != markers_.end(); it++) {
+ if (ToSuggestionMarker(*it)->Tag() == tag) {
+ markers_.erase(it);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_list_impl.h b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_list_impl.h
new file mode 100644
index 00000000000..f4c1c748555
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_list_impl.h
@@ -0,0 +1,70 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SUGGESTION_MARKER_LIST_IMPL_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SUGGESTION_MARKER_LIST_IMPL_H_
+
+#include "third_party/blink/renderer/core/editing/markers/document_marker_list.h"
+
+namespace blink {
+
+// Implementation of DocumentMarkerList for Suggestion markers. Suggestion
+// markers are somewhat unusual compared to some other MarkerTypes in that they
+// can overlap. Since sorting by start offset doesn't help very much when the
+// markers can overlap, and other ways of doing this efficiently would add a lot
+// of complexity, suggestion markers are currently stored unsorted.
+class CORE_EXPORT SuggestionMarkerListImpl final : public DocumentMarkerList {
+ public:
+ SuggestionMarkerListImpl() = default;
+
+ // DocumentMarkerList implementations
+ DocumentMarker::MarkerType MarkerType() const final;
+
+ bool IsEmpty() const final;
+
+ void Add(DocumentMarker*) final;
+ void Clear() final;
+
+ const HeapVector<Member<DocumentMarker>>& GetMarkers() const final;
+ DocumentMarker* FirstMarkerIntersectingRange(unsigned start_offset,
+ unsigned end_offset) const final;
+ HeapVector<Member<DocumentMarker>> MarkersIntersectingRange(
+ unsigned start_offset,
+ unsigned end_offset) const final;
+
+ bool MoveMarkers(int length, DocumentMarkerList* dst_list) final;
+ bool RemoveMarkers(unsigned start_offset, int length) final;
+ bool ShiftMarkers(const String& node_text,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) final;
+
+ virtual void Trace(blink::Visitor*);
+
+ // SuggestionMarkerListImpl-specific
+ bool RemoveMarkerByTag(int32_t tag);
+
+ private:
+ bool ShiftMarkersForSuggestionReplacement(unsigned offset,
+ unsigned old_length,
+ unsigned new_length);
+ bool ShiftMarkersForNonSuggestionEditingOperation(const String& node_text,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length);
+
+ HeapVector<Member<DocumentMarker>> markers_;
+
+ DISALLOW_COPY_AND_ASSIGN(SuggestionMarkerListImpl);
+};
+
+DEFINE_TYPE_CASTS(SuggestionMarkerListImpl,
+ DocumentMarkerList,
+ list,
+ list->MarkerType() == DocumentMarker::kSuggestion,
+ list.MarkerType() == DocumentMarker::kSuggestion);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SUGGESTION_MARKER_LIST_IMPL_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_list_impl_test.cc b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_list_impl_test.cc
new file mode 100644
index 00000000000..275070efe3b
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_list_impl_test.cc
@@ -0,0 +1,346 @@
+// Copyright 2017 The Chromium Authors. All rights reserved. Use of
+// this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker_list_impl.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/editing/markers/marker_test_utilities.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker_properties.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker_replacement_scope.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+class SuggestionMarkerListImplTest : public testing::Test {
+ protected:
+ SuggestionMarkerListImplTest()
+ : marker_list_(new SuggestionMarkerListImpl()) {}
+
+ SuggestionMarker* CreateMarker(unsigned start_offset, unsigned end_offset) {
+ return new SuggestionMarker(start_offset, end_offset,
+ SuggestionMarkerProperties());
+ }
+
+ Persistent<SuggestionMarkerListImpl> marker_list_;
+};
+
+TEST_F(SuggestionMarkerListImplTest, MarkerType) {
+ EXPECT_EQ(DocumentMarker::kSuggestion, marker_list_->MarkerType());
+}
+
+TEST_F(SuggestionMarkerListImplTest, AddOverlapping) {
+ // Add some overlapping markers in an arbitrary order and verify that the
+ // list stores them properly
+ marker_list_->Add(CreateMarker(40, 50));
+ marker_list_->Add(CreateMarker(10, 40));
+ marker_list_->Add(CreateMarker(20, 50));
+ marker_list_->Add(CreateMarker(10, 30));
+ marker_list_->Add(CreateMarker(10, 50));
+ marker_list_->Add(CreateMarker(30, 50));
+ marker_list_->Add(CreateMarker(30, 40));
+ marker_list_->Add(CreateMarker(10, 20));
+ marker_list_->Add(CreateMarker(20, 40));
+ marker_list_->Add(CreateMarker(20, 30));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+ std::sort(markers.begin(), markers.end(), compare_markers);
+
+ EXPECT_EQ(10u, markers.size());
+
+ EXPECT_EQ(10u, markers[0]->StartOffset());
+ EXPECT_EQ(20u, markers[0]->EndOffset());
+
+ EXPECT_EQ(10u, markers[1]->StartOffset());
+ EXPECT_EQ(30u, markers[1]->EndOffset());
+
+ EXPECT_EQ(10u, markers[2]->StartOffset());
+ EXPECT_EQ(40u, markers[2]->EndOffset());
+
+ EXPECT_EQ(10u, markers[3]->StartOffset());
+ EXPECT_EQ(50u, markers[3]->EndOffset());
+
+ EXPECT_EQ(20u, markers[4]->StartOffset());
+ EXPECT_EQ(30u, markers[4]->EndOffset());
+
+ EXPECT_EQ(20u, markers[5]->StartOffset());
+ EXPECT_EQ(40u, markers[5]->EndOffset());
+
+ EXPECT_EQ(20u, markers[6]->StartOffset());
+ EXPECT_EQ(50u, markers[6]->EndOffset());
+
+ EXPECT_EQ(30u, markers[7]->StartOffset());
+ EXPECT_EQ(40u, markers[7]->EndOffset());
+
+ EXPECT_EQ(30u, markers[8]->StartOffset());
+ EXPECT_EQ(50u, markers[8]->EndOffset());
+
+ EXPECT_EQ(40u, markers[9]->StartOffset());
+ EXPECT_EQ(50u, markers[9]->EndOffset());
+}
+
+TEST_F(SuggestionMarkerListImplTest,
+ ShiftMarkersForSuggestionReplacement_ReturnsFalseWhenNoShift) {
+ marker_list_->Add(CreateMarker(0, 10));
+
+ {
+ SuggestionMarkerReplacementScope scope;
+ // Replace range 0 to 10 with a ten character string.
+ // Text is ignored for suggestion replacement, so we can just pass an empty
+ // string.
+ EXPECT_FALSE(marker_list_->ShiftMarkers("", 0, 10, 10));
+ }
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+ EXPECT_EQ(1u, markers.size());
+
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(10u, markers[0]->EndOffset());
+}
+
+TEST_F(SuggestionMarkerListImplTest,
+ ShiftMarkersForSuggestionReplacement_MarkersUpdateProperly) {
+ // Marker with suggestion to apply.
+ // Should be kept (and shifted).
+ marker_list_->Add(CreateMarker(10, 20));
+
+ // Marker touching start of replacement range.
+ // Should be kept.
+ marker_list_->Add(CreateMarker(0, 10));
+
+ // Marker partially overlapping start of replacement range.
+ // Should be removed,
+ marker_list_->Add(CreateMarker(0, 11));
+
+ // Marker touching end of replacement range.
+ // Should be kept (and shifted).
+ marker_list_->Add(CreateMarker(20, 30));
+
+ // Marker partially overlapping end of replacement range
+ // Should be removed.
+ marker_list_->Add(CreateMarker(19, 30));
+
+ // Marker contained inside replacement range
+ // Should be removed.
+ marker_list_->Add(CreateMarker(11, 19));
+
+ // Marker containing replacement range
+ // Should be kept (and shifted).
+ marker_list_->Add(CreateMarker(9, 21));
+
+ {
+ SuggestionMarkerReplacementScope scope;
+ // Replace range 10 to 20 with a five character string.
+ // Text is ignored for suggestion replacement, so we can just pass an empty
+ // string.
+ EXPECT_TRUE(marker_list_->ShiftMarkers("", 10, 10, 5));
+ }
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+ std::sort(markers.begin(), markers.end(), compare_markers);
+
+ EXPECT_EQ(4u, markers.size());
+
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(10u, markers[0]->EndOffset());
+
+ EXPECT_EQ(9u, markers[1]->StartOffset());
+ EXPECT_EQ(16u, markers[1]->EndOffset());
+
+ EXPECT_EQ(10u, markers[2]->StartOffset());
+ EXPECT_EQ(15u, markers[2]->EndOffset());
+
+ EXPECT_EQ(15u, markers[3]->StartOffset());
+ EXPECT_EQ(25u, markers[3]->EndOffset());
+}
+
+TEST_F(SuggestionMarkerListImplTest,
+ ShiftMarkersForNonSuggestionEditingOperation_DeleteFromMiddle) {
+ marker_list_->Add(CreateMarker(0, 5));
+
+ EXPECT_TRUE(marker_list_->ShiftMarkers("hello", 2, 1, 0));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SuggestionMarkerListImplTest,
+ ShiftMarkersForNonSuggestionEditingOperation_InsertIntoMiddle) {
+ marker_list_->Add(CreateMarker(0, 5));
+
+ EXPECT_TRUE(marker_list_->ShiftMarkers("hello", 2, 0, 1));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SuggestionMarkerListImplTest,
+ ShiftMarkersForNonSuggestionEditingOperation_PrependLetter) {
+ marker_list_->Add(CreateMarker(0, 5));
+
+ EXPECT_TRUE(marker_list_->ShiftMarkers("ahello", 0, 0, 1));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(
+ SuggestionMarkerListImplTest,
+ ShiftMarkersForNonSuggestionEditingOperation_PrependSurrogatePairLetter) {
+ marker_list_->Add(CreateMarker(0, 5));
+
+ // Prepending MATHEMATICAL SCRIPT CAPITAL C
+ EXPECT_TRUE(marker_list_->ShiftMarkers(u"\U0001d49ehello", 0, 0, 2));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SuggestionMarkerListImplTest,
+ ShiftMarkersForNonSuggestionEditingOperation_PrependDigit) {
+ marker_list_->Add(CreateMarker(0, 5));
+
+ EXPECT_TRUE(marker_list_->ShiftMarkers("0hello", 0, 0, 1));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SuggestionMarkerListImplTest,
+ ShiftMarkersForNonSuggestionEditingOperation_PrependSurrogatePairDigit) {
+ marker_list_->Add(CreateMarker(0, 5));
+
+ // Prepending MATHEMATICAL DOUBLE-STRUCK DIGIT ONE
+ EXPECT_TRUE(marker_list_->ShiftMarkers(u"\U0001d7d9hello", 0, 0, 2));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SuggestionMarkerListImplTest,
+ ShiftMarkersForNonSuggestionEditingOperation_PrependNonAlphanumeric) {
+ marker_list_->Add(CreateMarker(0, 5));
+
+ EXPECT_TRUE(marker_list_->ShiftMarkers(".hello", 0, 0, 1));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+
+ EXPECT_EQ(1u, markers.size());
+
+ EXPECT_EQ(1u, markers[0]->StartOffset());
+ EXPECT_EQ(6u, markers[0]->EndOffset());
+}
+
+TEST_F(
+ SuggestionMarkerListImplTest,
+ ShiftMarkersForNonSuggestionEditingOperation_PrependSurrogatePairNonAlphanumeric) {
+ marker_list_->Add(CreateMarker(0, 5));
+
+ // Prepending FACE WITH TEARS OF JOY
+ EXPECT_TRUE(marker_list_->ShiftMarkers(u"\U0001f602hello", 0, 0, 2));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+
+ EXPECT_EQ(1u, markers.size());
+
+ EXPECT_EQ(2u, markers[0]->StartOffset());
+ EXPECT_EQ(7u, markers[0]->EndOffset());
+}
+
+TEST_F(SuggestionMarkerListImplTest,
+ ShiftMarkersForNonSuggestionEditingOperation_AppendLetter) {
+ marker_list_->Add(CreateMarker(0, 5));
+
+ EXPECT_TRUE(marker_list_->ShiftMarkers("helloa", 5, 0, 1));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SuggestionMarkerListImplTest,
+ ShiftMarkersForNonSuggestionEditingOperation_AppendSurrogatePairLetter) {
+ marker_list_->Add(CreateMarker(0, 5));
+
+ // Appending MATHEMATICAL SCRIPT CAPITAL C
+ EXPECT_TRUE(marker_list_->ShiftMarkers(u"hello\U0001d49e", 5, 0, 2));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SuggestionMarkerListImplTest,
+ ShiftMarkersForNonSuggestionEditingOperation_AppendDigit) {
+ marker_list_->Add(CreateMarker(0, 5));
+
+ EXPECT_TRUE(marker_list_->ShiftMarkers("hello0", 5, 0, 1));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SuggestionMarkerListImplTest,
+ ShiftMarkersForNonSuggestionEditingOperation_AppendSurrogatePairDigit) {
+ marker_list_->Add(CreateMarker(0, 5));
+
+ // Appending MATHEMATICAL DOUBLE-STRUCK DIGIT ONE
+ EXPECT_TRUE(marker_list_->ShiftMarkers(u"hello\U0001d7d9", 5, 0, 2));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(SuggestionMarkerListImplTest,
+ ShiftMarkersForNonSuggestionEditingOperation_AppendNonAlphanumeric) {
+ marker_list_->Add(CreateMarker(0, 5));
+
+ EXPECT_FALSE(marker_list_->ShiftMarkers("hello.", 5, 0, 1));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+
+ EXPECT_EQ(1u, markers.size());
+
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(5u, markers[0]->EndOffset());
+}
+
+TEST_F(
+ SuggestionMarkerListImplTest,
+ ShiftMarkersForNonSuggestionEditingOperation_AppendSurrogatePairNonAlphanumeric) {
+ marker_list_->Add(CreateMarker(0, 5));
+
+ // Appending FACE WITH TEARS OF JOY
+ EXPECT_FALSE(marker_list_->ShiftMarkers(u"hello\U0001f602", 5, 0, 2));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+
+ EXPECT_EQ(1u, markers.size());
+
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(5u, markers[0]->EndOffset());
+}
+
+TEST_F(SuggestionMarkerListImplTest, RemoveMarkerByTag_NotFound) {
+ SuggestionMarker* const marker = CreateMarker(0, 10);
+ marker_list_->Add(marker);
+
+ EXPECT_FALSE(marker_list_->RemoveMarkerByTag(marker->Tag() + 1));
+}
+
+TEST_F(SuggestionMarkerListImplTest, RemoveMarkerByTag_Found) {
+ SuggestionMarker* const marker1 = CreateMarker(0, 10);
+ SuggestionMarker* const marker2 = CreateMarker(10, 20);
+
+ marker_list_->Add(marker1);
+ marker_list_->Add(marker2);
+
+ EXPECT_TRUE(marker_list_->RemoveMarkerByTag(marker1->Tag()));
+
+ DocumentMarkerVector markers = marker_list_->GetMarkers();
+ EXPECT_EQ(1u, markers.size());
+
+ EXPECT_EQ(10u, markers[0]->StartOffset());
+ EXPECT_EQ(20u, markers[0]->EndOffset());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_properties.cc b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_properties.cc
new file mode 100644
index 00000000000..435e6fdad54
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_properties.cc
@@ -0,0 +1,63 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker_properties.h"
+
+namespace blink {
+
+SuggestionMarkerProperties::SuggestionMarkerProperties(
+ const SuggestionMarkerProperties& other) = default;
+SuggestionMarkerProperties::SuggestionMarkerProperties() = default;
+SuggestionMarkerProperties::Builder::Builder() = default;
+
+SuggestionMarkerProperties::Builder::Builder(
+ const SuggestionMarkerProperties& data) {
+ data_ = data;
+}
+
+SuggestionMarkerProperties SuggestionMarkerProperties::Builder::Build() const {
+ return data_;
+}
+
+SuggestionMarkerProperties::Builder&
+SuggestionMarkerProperties::Builder::SetType(
+ SuggestionMarker::SuggestionType type) {
+ data_.type_ = type;
+ return *this;
+}
+
+SuggestionMarkerProperties::Builder&
+SuggestionMarkerProperties::Builder::SetSuggestions(
+ const Vector<String>& suggestions) {
+ data_.suggestions_ = suggestions;
+ return *this;
+}
+
+SuggestionMarkerProperties::Builder&
+SuggestionMarkerProperties::Builder::SetHighlightColor(Color highlight_color) {
+ data_.highlight_color_ = highlight_color;
+ return *this;
+}
+
+SuggestionMarkerProperties::Builder&
+SuggestionMarkerProperties::Builder::SetUnderlineColor(Color underline_color) {
+ data_.underline_color_ = underline_color;
+ return *this;
+}
+
+SuggestionMarkerProperties::Builder&
+SuggestionMarkerProperties::Builder::SetBackgroundColor(
+ Color background_color) {
+ data_.background_color_ = background_color;
+ return *this;
+}
+
+SuggestionMarkerProperties::Builder&
+SuggestionMarkerProperties::Builder::SetThickness(
+ ui::mojom::ImeTextSpanThickness thickness) {
+ data_.thickness_ = thickness;
+ return *this;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_properties.h b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_properties.h
new file mode 100644
index 00000000000..acad4f9e49d
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_properties.h
@@ -0,0 +1,69 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SUGGESTION_MARKER_PROPERTIES_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SUGGESTION_MARKER_PROPERTIES_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/markers/styleable_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker.h"
+
+using ui::mojom::ImeTextSpanThickness;
+
+namespace blink {
+
+// This class is used to pass parameters to
+// |DocumentMarkerController::AddSuggestionMarker()|.
+class CORE_EXPORT SuggestionMarkerProperties final {
+ STACK_ALLOCATED();
+
+ public:
+ class CORE_EXPORT Builder;
+
+ SuggestionMarkerProperties(const SuggestionMarkerProperties&);
+ SuggestionMarkerProperties();
+
+ SuggestionMarker::SuggestionType Type() const { return type_; }
+ Vector<String> Suggestions() const { return suggestions_; }
+ Color HighlightColor() const { return highlight_color_; }
+ Color UnderlineColor() const { return underline_color_; }
+ Color BackgroundColor() const { return background_color_; }
+ ImeTextSpanThickness Thickness() const { return thickness_; }
+
+ private:
+ SuggestionMarker::SuggestionType type_ =
+ SuggestionMarker::SuggestionType::kNotMisspelling;
+ Vector<String> suggestions_;
+ Color highlight_color_ = Color::kTransparent;
+ Color underline_color_ = Color::kTransparent;
+ Color background_color_ = Color::kTransparent;
+ ImeTextSpanThickness thickness_ = ImeTextSpanThickness::kThin;
+};
+
+// This class is used for building SuggestionMarkerProperties objects.
+class CORE_EXPORT SuggestionMarkerProperties::Builder final {
+ STACK_ALLOCATED();
+
+ public:
+ explicit Builder(const SuggestionMarkerProperties&);
+ Builder();
+
+ SuggestionMarkerProperties Build() const;
+
+ Builder& SetType(SuggestionMarker::SuggestionType);
+ Builder& SetSuggestions(const Vector<String>& suggestions);
+ Builder& SetHighlightColor(Color);
+ Builder& SetUnderlineColor(Color);
+ Builder& SetBackgroundColor(Color);
+ Builder& SetThickness(ImeTextSpanThickness);
+
+ private:
+ SuggestionMarkerProperties data_;
+
+ DISALLOW_COPY_AND_ASSIGN(Builder);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SUGGESTION_MARKER_PROPERTIES_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_replacement_scope.cc b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_replacement_scope.cc
new file mode 100644
index 00000000000..b74b9ed49d3
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_replacement_scope.cc
@@ -0,0 +1,25 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker_replacement_scope.h"
+
+namespace blink {
+
+bool SuggestionMarkerReplacementScope::currently_in_scope_ = false;
+
+SuggestionMarkerReplacementScope::SuggestionMarkerReplacementScope() {
+ DCHECK(!currently_in_scope_);
+ currently_in_scope_ = true;
+}
+
+SuggestionMarkerReplacementScope::~SuggestionMarkerReplacementScope() {
+ currently_in_scope_ = false;
+}
+
+// static
+bool SuggestionMarkerReplacementScope::CurrentlyInScope() {
+ return currently_in_scope_;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_replacement_scope.h b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_replacement_scope.h
new file mode 100644
index 00000000000..a6616c91e77
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_replacement_scope.h
@@ -0,0 +1,31 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SUGGESTION_MARKER_REPLACEMENT_SCOPE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_SUGGESTION_MARKER_REPLACEMENT_SCOPE_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+
+namespace blink {
+
+class CORE_EXPORT SuggestionMarkerReplacementScope {
+ STACK_ALLOCATED();
+
+ public:
+ SuggestionMarkerReplacementScope();
+ ~SuggestionMarkerReplacementScope();
+
+ static bool CurrentlyInScope();
+
+ private:
+ static bool currently_in_scope_;
+
+ DISALLOW_COPY_AND_ASSIGN(SuggestionMarkerReplacementScope);
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_test.cc b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_test.cc
new file mode 100644
index 00000000000..fcec1421a83
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/suggestion_marker_test.cc
@@ -0,0 +1,73 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker_properties.h"
+
+namespace blink {
+
+class SuggestionMarkerTest : public testing::Test {};
+
+TEST_F(SuggestionMarkerTest, MarkerType) {
+ DocumentMarker* marker =
+ new SuggestionMarker(0, 1, SuggestionMarkerProperties());
+ EXPECT_EQ(DocumentMarker::kSuggestion, marker->GetType());
+}
+
+TEST_F(SuggestionMarkerTest, IsStyleableMarker) {
+ DocumentMarker* marker =
+ new SuggestionMarker(0, 1, SuggestionMarkerProperties());
+ EXPECT_TRUE(IsStyleableMarker(*marker));
+}
+
+TEST_F(SuggestionMarkerTest, ConstructorAndGetters) {
+ Vector<String> suggestions = {"this", "that"};
+ SuggestionMarker* marker = new SuggestionMarker(
+ 0, 1,
+ SuggestionMarkerProperties::Builder()
+ .SetType(SuggestionMarker::SuggestionType::kNotMisspelling)
+ .SetSuggestions(suggestions)
+ .SetHighlightColor(Color::kTransparent)
+ .SetUnderlineColor(Color::kDarkGray)
+ .SetThickness(ui::mojom::ImeTextSpanThickness::kThin)
+ .SetBackgroundColor(Color::kGray)
+ .Build());
+ EXPECT_EQ(suggestions, marker->Suggestions());
+ EXPECT_FALSE(marker->IsMisspelling());
+ EXPECT_EQ(Color::kTransparent, marker->SuggestionHighlightColor());
+ EXPECT_EQ(Color::kDarkGray, marker->UnderlineColor());
+ EXPECT_TRUE(marker->HasThicknessThin());
+ EXPECT_EQ(Color::kGray, marker->BackgroundColor());
+
+ SuggestionMarker* marker2 = new SuggestionMarker(
+ 0, 1,
+ SuggestionMarkerProperties::Builder()
+ .SetType(SuggestionMarker::SuggestionType::kMisspelling)
+ .SetHighlightColor(Color::kBlack)
+ .SetThickness(ui::mojom::ImeTextSpanThickness::kThick)
+ .Build());
+ EXPECT_TRUE(marker2->HasThicknessThick());
+ EXPECT_TRUE(marker2->IsMisspelling());
+ EXPECT_EQ(marker2->SuggestionHighlightColor(), Color::kBlack);
+}
+
+TEST_F(SuggestionMarkerTest, SetSuggestion) {
+ Vector<String> suggestions = {"this", "that"};
+ SuggestionMarker* marker =
+ new SuggestionMarker(0, 1,
+ SuggestionMarkerProperties::Builder()
+ .SetSuggestions(suggestions)
+ .Build());
+
+ marker->SetSuggestion(1, "these");
+
+ EXPECT_EQ(2u, marker->Suggestions().size());
+
+ EXPECT_EQ("this", marker->Suggestions()[0]);
+ EXPECT_EQ("these", marker->Suggestions()[1]);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/text_match_marker.cc b/chromium/third_party/blink/renderer/core/editing/markers/text_match_marker.cc
new file mode 100644
index 00000000000..797cf3c2c68
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/text_match_marker.cc
@@ -0,0 +1,61 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/text_match_marker.h"
+
+namespace blink {
+
+TextMatchMarker::TextMatchMarker(unsigned start_offset,
+ unsigned end_offset,
+ MatchStatus status)
+ : DocumentMarker(start_offset, end_offset), match_status_(status) {}
+
+DocumentMarker::MarkerType TextMatchMarker::GetType() const {
+ return DocumentMarker::kTextMatch;
+}
+
+bool TextMatchMarker::IsActiveMatch() const {
+ return match_status_ == MatchStatus::kActive;
+}
+
+void TextMatchMarker::SetIsActiveMatch(bool active) {
+ match_status_ = active ? MatchStatus::kActive : MatchStatus::kInactive;
+}
+
+bool TextMatchMarker::IsRendered() const {
+ return layout_status_ == LayoutStatus::kValidNotNull;
+}
+
+bool TextMatchMarker::Contains(const LayoutPoint& point) const {
+ DCHECK_EQ(layout_status_, LayoutStatus::kValidNotNull);
+ return layout_rect_.Contains(point);
+}
+
+void TextMatchMarker::SetLayoutRect(const LayoutRect& rect) {
+ if (layout_status_ == LayoutStatus::kValidNotNull && rect == layout_rect_)
+ return;
+ layout_status_ = LayoutStatus::kValidNotNull;
+ layout_rect_ = rect;
+}
+
+const LayoutRect& TextMatchMarker::GetLayoutRect() const {
+ DCHECK_EQ(layout_status_, LayoutStatus::kValidNotNull);
+ return layout_rect_;
+}
+
+void TextMatchMarker::NullifyLayoutRect() {
+ layout_status_ = LayoutStatus::kValidNull;
+ // Now |rendered_rect_| can not be accessed until |SetRenderedRect| is
+ // called.
+}
+
+void TextMatchMarker::Invalidate() {
+ layout_status_ = LayoutStatus::kInvalid;
+}
+
+bool TextMatchMarker::IsValid() const {
+ return layout_status_ != LayoutStatus::kInvalid;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/text_match_marker.h b/chromium/third_party/blink/renderer/core/editing/markers/text_match_marker.h
new file mode 100644
index 00000000000..a2fb4c26047
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/text_match_marker.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_TEXT_MATCH_MARKER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_TEXT_MATCH_MARKER_H_
+
+#include "third_party/blink/renderer/core/editing/markers/document_marker.h"
+#include "third_party/blink/renderer/platform/geometry/layout_rect.h"
+
+namespace blink {
+
+// A subclass of DocumentMarker used to store information specific to TextMatch
+// markers. We store whether or not the match is active, a LayoutRect used for
+// rendering the marker, and whether or not the LayoutRect is currently
+// up-to-date.
+class CORE_EXPORT TextMatchMarker final : public DocumentMarker {
+ private:
+ enum class LayoutStatus { kInvalid, kValidNull, kValidNotNull };
+
+ public:
+ enum class MatchStatus { kInactive, kActive };
+
+ TextMatchMarker(unsigned start_offset, unsigned end_offset, MatchStatus);
+
+ // DocumentMarker implementations
+ MarkerType GetType() const final;
+
+ // TextMatchMarker-specific
+ bool IsActiveMatch() const;
+ void SetIsActiveMatch(bool active);
+
+ bool IsRendered() const;
+ bool Contains(const LayoutPoint&) const;
+ void SetLayoutRect(const LayoutRect&);
+ const LayoutRect& GetLayoutRect() const;
+ void NullifyLayoutRect();
+
+ void Invalidate();
+ bool IsValid() const;
+
+ private:
+ MatchStatus match_status_;
+ LayoutStatus layout_status_ = LayoutStatus::kInvalid;
+ LayoutRect layout_rect_;
+
+ DISALLOW_COPY_AND_ASSIGN(TextMatchMarker);
+};
+
+DEFINE_TYPE_CASTS(TextMatchMarker,
+ DocumentMarker,
+ marker,
+ marker->GetType() == DocumentMarker::kTextMatch,
+ marker.GetType() == DocumentMarker::kTextMatch);
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/text_match_marker_list_impl.cc b/chromium/third_party/blink/renderer/core/editing/markers/text_match_marker_list_impl.cc
new file mode 100644
index 00000000000..a9897592211
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/text_match_marker_list_impl.cc
@@ -0,0 +1,128 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/text_match_marker_list_impl.h"
+
+#include "third_party/blink/renderer/core/dom/node.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/markers/sorted_document_marker_list_editor.h"
+#include "third_party/blink/renderer/core/editing/markers/text_match_marker.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+
+namespace blink {
+
+DocumentMarker::MarkerType TextMatchMarkerListImpl::MarkerType() const {
+ return DocumentMarker::kTextMatch;
+}
+
+bool TextMatchMarkerListImpl::IsEmpty() const {
+ return markers_.IsEmpty();
+}
+
+void TextMatchMarkerListImpl::Add(DocumentMarker* marker) {
+ DCHECK_EQ(DocumentMarker::kTextMatch, marker->GetType());
+ SortedDocumentMarkerListEditor::AddMarkerWithoutMergingOverlapping(&markers_,
+ marker);
+}
+
+void TextMatchMarkerListImpl::Clear() {
+ markers_.clear();
+}
+
+const HeapVector<Member<DocumentMarker>>& TextMatchMarkerListImpl::GetMarkers()
+ const {
+ return markers_;
+}
+
+DocumentMarker* TextMatchMarkerListImpl::FirstMarkerIntersectingRange(
+ unsigned start_offset,
+ unsigned end_offset) const {
+ return SortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(
+ markers_, start_offset, end_offset);
+}
+
+HeapVector<Member<DocumentMarker>>
+TextMatchMarkerListImpl::MarkersIntersectingRange(unsigned start_offset,
+ unsigned end_offset) const {
+ return SortedDocumentMarkerListEditor::MarkersIntersectingRange(
+ markers_, start_offset, end_offset);
+}
+
+bool TextMatchMarkerListImpl::MoveMarkers(int length,
+ DocumentMarkerList* dst_list) {
+ return SortedDocumentMarkerListEditor::MoveMarkers(&markers_, length,
+ dst_list);
+}
+
+bool TextMatchMarkerListImpl::RemoveMarkers(unsigned start_offset, int length) {
+ return SortedDocumentMarkerListEditor::RemoveMarkers(&markers_, start_offset,
+ length);
+}
+
+bool TextMatchMarkerListImpl::ShiftMarkers(const String&,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) {
+ return SortedDocumentMarkerListEditor::ShiftMarkersContentDependent(
+ &markers_, offset, old_length, new_length);
+}
+
+void TextMatchMarkerListImpl::Trace(blink::Visitor* visitor) {
+ visitor->Trace(markers_);
+ DocumentMarkerList::Trace(visitor);
+}
+
+static void UpdateMarkerLayoutRect(const Node& node, TextMatchMarker& marker) {
+ const Position start_position(node, marker.StartOffset());
+ const Position end_position(node, marker.EndOffset());
+ EphemeralRange range(start_position, end_position);
+
+ DCHECK(node.GetDocument().GetFrame());
+ LocalFrameView* frame_view = node.GetDocument().GetFrame()->View();
+
+ DCHECK(frame_view);
+ marker.SetLayoutRect(
+ frame_view->AbsoluteToDocument(LayoutRect(ComputeTextRect(range))));
+}
+
+Vector<IntRect> TextMatchMarkerListImpl::LayoutRects(const Node& node) const {
+ Vector<IntRect> result;
+
+ for (DocumentMarker* marker : markers_) {
+ TextMatchMarker* const text_match_marker = ToTextMatchMarker(marker);
+ if (!text_match_marker->IsValid())
+ UpdateMarkerLayoutRect(node, *text_match_marker);
+ if (!text_match_marker->IsRendered())
+ continue;
+ result.push_back(PixelSnappedIntRect(text_match_marker->GetLayoutRect()));
+ }
+
+ return result;
+}
+
+bool TextMatchMarkerListImpl::SetTextMatchMarkersActive(unsigned start_offset,
+ unsigned end_offset,
+ bool active) {
+ bool doc_dirty = false;
+ const auto start = std::upper_bound(
+ markers_.begin(), markers_.end(), start_offset,
+ [](size_t start_offset, const Member<DocumentMarker>& marker) {
+ return start_offset < marker->EndOffset();
+ });
+ for (auto it = start; it != markers_.end(); ++it) {
+ DocumentMarker& marker = **it;
+ // Markers are returned in order, so stop if we are now past the specified
+ // range.
+ if (marker.StartOffset() >= end_offset)
+ break;
+ ToTextMatchMarker(marker).SetIsActiveMatch(active);
+ doc_dirty = true;
+ }
+ return doc_dirty;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/text_match_marker_list_impl.h b/chromium/third_party/blink/renderer/core/editing/markers/text_match_marker_list_impl.h
new file mode 100644
index 00000000000..102661c3a47
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/text_match_marker_list_impl.h
@@ -0,0 +1,66 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_TEXT_MATCH_MARKER_LIST_IMPL_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_TEXT_MATCH_MARKER_LIST_IMPL_H_
+
+#include "third_party/blink/renderer/core/editing/markers/document_marker_list.h"
+
+namespace blink {
+
+class IntRect;
+
+// Implementation of DocumentMarkerList for TextMatch markers.
+// Markers are kept sorted by start offset, under the assumption that
+// TextMatch markers are typically inserted in an order.
+class CORE_EXPORT TextMatchMarkerListImpl final : public DocumentMarkerList {
+ public:
+ TextMatchMarkerListImpl() = default;
+
+ // DocumentMarkerList implementations
+ DocumentMarker::MarkerType MarkerType() const final;
+
+ bool IsEmpty() const final;
+
+ void Add(DocumentMarker*) final;
+ void Clear() final;
+
+ const HeapVector<Member<DocumentMarker>>& GetMarkers() const final;
+ DocumentMarker* FirstMarkerIntersectingRange(unsigned start_offset,
+ unsigned end_offset) const final;
+ HeapVector<Member<DocumentMarker>> MarkersIntersectingRange(
+ unsigned start_offset,
+ unsigned end_offset) const final;
+
+ bool MoveMarkers(int length, DocumentMarkerList* dst_list) final;
+ bool RemoveMarkers(unsigned start_offset, int length) final;
+ bool ShiftMarkers(const String& node_text,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) final;
+ virtual void Trace(blink::Visitor*);
+
+ // TextMatchMarkerListImpl-specific
+ Vector<IntRect> LayoutRects(const Node&) const;
+ // Returns true if markers within a range defined by |startOffset| and
+ // |endOffset| are found.
+ bool SetTextMatchMarkersActive(unsigned start_offset,
+ unsigned end_offset,
+ bool);
+
+ private:
+ HeapVector<Member<DocumentMarker>> markers_;
+
+ DISALLOW_COPY_AND_ASSIGN(TextMatchMarkerListImpl);
+};
+
+DEFINE_TYPE_CASTS(TextMatchMarkerListImpl,
+ DocumentMarkerList,
+ list,
+ list->MarkerType() == DocumentMarker::kTextMatch,
+ list.MarkerType() == DocumentMarker::kTextMatch);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_TEXT_MATCH_MARKER_LIST_IMPL_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/text_match_marker_list_impl_test.cc b/chromium/third_party/blink/renderer/core/editing/markers/text_match_marker_list_impl_test.cc
new file mode 100644
index 00000000000..544e07eab33
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/text_match_marker_list_impl_test.cc
@@ -0,0 +1,43 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/text_match_marker_list_impl.h"
+
+#include "third_party/blink/renderer/core/editing/markers/text_match_marker.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class TextMatchMarkerListImplTest : public EditingTestBase {
+ protected:
+ TextMatchMarkerListImplTest() : marker_list_(new TextMatchMarkerListImpl()) {}
+
+ DocumentMarker* CreateMarker(unsigned start_offset, unsigned end_offset) {
+ return new TextMatchMarker(start_offset, end_offset,
+ TextMatchMarker::MatchStatus::kInactive);
+ }
+
+ Persistent<TextMatchMarkerListImpl> marker_list_;
+};
+
+TEST_F(TextMatchMarkerListImplTest, MarkerType) {
+ EXPECT_EQ(DocumentMarker::kTextMatch, marker_list_->MarkerType());
+}
+
+TEST_F(TextMatchMarkerListImplTest, Add) {
+ EXPECT_EQ(0u, marker_list_->GetMarkers().size());
+
+ marker_list_->Add(CreateMarker(0, 1));
+ marker_list_->Add(CreateMarker(1, 2));
+
+ EXPECT_EQ(2u, marker_list_->GetMarkers().size());
+
+ EXPECT_EQ(0u, marker_list_->GetMarkers()[0]->StartOffset());
+ EXPECT_EQ(1u, marker_list_->GetMarkers()[0]->EndOffset());
+
+ EXPECT_EQ(1u, marker_list_->GetMarkers()[1]->StartOffset());
+ EXPECT_EQ(2u, marker_list_->GetMarkers()[1]->EndOffset());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/unsorted_document_marker_list_editor.cc b/chromium/third_party/blink/renderer/core/editing/markers/unsorted_document_marker_list_editor.cc
new file mode 100644
index 00000000000..e369617dbcb
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/unsorted_document_marker_list_editor.cc
@@ -0,0 +1,124 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/unsorted_document_marker_list_editor.h"
+
+#include "third_party/blink/renderer/core/editing/markers/spell_check_marker_list_impl.h"
+
+namespace blink {
+
+bool UnsortedDocumentMarkerListEditor::MoveMarkers(
+ MarkerList* src_list,
+ int length,
+ DocumentMarkerList* dst_list) {
+ DCHECK_GT(length, 0);
+ bool did_move_marker = false;
+ const unsigned end_offset = length - 1;
+
+ HeapVector<Member<DocumentMarker>> unmoved_markers;
+ for (DocumentMarker* marker : *src_list) {
+ if (marker->StartOffset() > end_offset) {
+ unmoved_markers.push_back(marker);
+ continue;
+ }
+
+ // If we're splitting a text node in the middle of a suggestion marker,
+ // remove the marker
+ if (marker->EndOffset() > end_offset)
+ continue;
+
+ dst_list->Add(marker);
+ did_move_marker = true;
+ }
+
+ *src_list = std::move(unmoved_markers);
+ return did_move_marker;
+}
+
+bool UnsortedDocumentMarkerListEditor::RemoveMarkers(MarkerList* list,
+ unsigned start_offset,
+ int length) {
+ // For an unsorted marker list, the quickest way to perform this operation is
+ // to build a new list with the markers that aren't being removed.
+ const unsigned end_offset = start_offset + length;
+ HeapVector<Member<DocumentMarker>> unremoved_markers;
+ for (const Member<DocumentMarker>& marker : *list) {
+ if (marker->EndOffset() <= start_offset ||
+ marker->StartOffset() >= end_offset) {
+ unremoved_markers.push_back(marker);
+ continue;
+ }
+ }
+
+ const bool did_remove_marker = (unremoved_markers.size() != list->size());
+ *list = std::move(unremoved_markers);
+ return did_remove_marker;
+}
+
+bool UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(
+ MarkerList* list,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) {
+ // For an unsorted marker list, the quickest way to perform this operation is
+ // to build a new list with the markers not removed by the shift.
+ bool did_shift_marker = false;
+ HeapVector<Member<DocumentMarker>> unremoved_markers;
+ for (const Member<DocumentMarker>& marker : *list) {
+ Optional<DocumentMarker::MarkerOffsets> result =
+ marker->ComputeOffsetsAfterShift(offset, old_length, new_length);
+ if (!result) {
+ did_shift_marker = true;
+ continue;
+ }
+
+ if (marker->StartOffset() != result.value().start_offset ||
+ marker->EndOffset() != result.value().end_offset) {
+ marker->SetStartOffset(result.value().start_offset);
+ marker->SetEndOffset(result.value().end_offset);
+ did_shift_marker = true;
+ }
+
+ unremoved_markers.push_back(marker);
+ }
+
+ *list = std::move(unremoved_markers);
+ return did_shift_marker;
+}
+
+DocumentMarker* UnsortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(
+ const MarkerList& list,
+ unsigned start_offset,
+ unsigned end_offset) {
+ DCHECK_LE(start_offset, end_offset);
+
+ const auto it =
+ std::find_if(list.begin(), list.end(),
+ [start_offset, end_offset](const DocumentMarker* marker) {
+ return marker->StartOffset() < end_offset &&
+ marker->EndOffset() > start_offset;
+ });
+
+ if (it == list.end())
+ return nullptr;
+ return *it;
+}
+
+HeapVector<Member<DocumentMarker>>
+UnsortedDocumentMarkerListEditor::MarkersIntersectingRange(
+ const MarkerList& list,
+ unsigned start_offset,
+ unsigned end_offset) {
+ DCHECK_LE(start_offset, end_offset);
+
+ HeapVector<Member<DocumentMarker>> results;
+ std::copy_if(list.begin(), list.end(), std::back_inserter(results),
+ [start_offset, end_offset](const DocumentMarker* marker) {
+ return marker->StartOffset() < end_offset &&
+ marker->EndOffset() > start_offset;
+ });
+ return results;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/unsorted_document_marker_list_editor.h b/chromium/third_party/blink/renderer/core/editing/markers/unsorted_document_marker_list_editor.h
new file mode 100644
index 00000000000..0c3471ef694
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/unsorted_document_marker_list_editor.h
@@ -0,0 +1,58 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_UNSORTED_DOCUMENT_MARKER_LIST_EDITOR_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_UNSORTED_DOCUMENT_MARKER_LIST_EDITOR_H_
+
+#include "third_party/blink/renderer/core/editing/markers/document_marker_list.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+class DocumentMarker;
+
+// This class holds static utility methods to be used in DocumentMarkerList
+// implementations that store potentially overlapping markers in unsorted order.
+class CORE_EXPORT UnsortedDocumentMarkerListEditor final {
+ public:
+ using MarkerList = HeapVector<Member<DocumentMarker>>;
+
+ // Returns true if a marker was moved, false otherwise.
+ static bool MoveMarkers(MarkerList* src_list,
+ int length,
+ DocumentMarkerList* dst_list);
+
+ // Returns true if a marker was removed, false otherwise.
+ static bool RemoveMarkers(MarkerList*, unsigned start_offset, int length);
+
+ // Returns true if a marker was shifted or removed, false otherwise.
+ // If the text marked by a marker is changed by the edit, this method attempts
+ // to keep the marker tracking the marked region rather than removing the
+ // marker.
+ static bool ShiftMarkersContentIndependent(MarkerList*,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length);
+
+ // Returns the first marker in the specified MarkerList whose interior
+ // overlaps overlap with the range [start_offset, end_offset], or null if
+ // there is no such marker.
+ // Note: since the markers aren't stored in order in an unsorted marker list,
+ // the first marker found isn't necessarily going to be the first marker
+ // ordered by start or end offset.
+ static DocumentMarker* FirstMarkerIntersectingRange(const MarkerList&,
+ unsigned start_offset,
+ unsigned end_offset);
+
+ // Returns all markers in the specified MarkerList whose interior overlaps
+ // with the range [start_offset, end_offset].
+ static HeapVector<Member<DocumentMarker>> MarkersIntersectingRange(
+ const MarkerList&,
+ unsigned start_offset,
+ unsigned end_offset);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_MARKERS_UNSORTED_DOCUMENT_MARKER_LIST_EDITOR_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/markers/unsorted_document_marker_list_editor_test.cc b/chromium/third_party/blink/renderer/core/editing/markers/unsorted_document_marker_list_editor_test.cc
new file mode 100644
index 00000000000..d25d80d9ca5
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/markers/unsorted_document_marker_list_editor_test.cc
@@ -0,0 +1,472 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/markers/unsorted_document_marker_list_editor.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/editing/markers/marker_test_utilities.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker_list_impl.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker_properties.h"
+
+namespace blink {
+
+class UnsortedDocumentMarkerListEditorTest : public testing::Test {
+ protected:
+ DocumentMarker* CreateMarker(unsigned start_offset, unsigned end_offset) {
+ return new SuggestionMarker(start_offset, end_offset,
+ SuggestionMarkerProperties());
+ }
+
+ PersistentHeapVector<Member<DocumentMarker>> marker_list_;
+};
+
+TEST_F(UnsortedDocumentMarkerListEditorTest, MoveMarkers) {
+ marker_list_.push_back(CreateMarker(30, 40));
+ marker_list_.push_back(CreateMarker(0, 30));
+ marker_list_.push_back(CreateMarker(10, 40));
+ marker_list_.push_back(CreateMarker(0, 20));
+ marker_list_.push_back(CreateMarker(0, 40));
+ marker_list_.push_back(CreateMarker(20, 40));
+ marker_list_.push_back(CreateMarker(20, 30));
+ marker_list_.push_back(CreateMarker(0, 10));
+ marker_list_.push_back(CreateMarker(10, 30));
+ marker_list_.push_back(CreateMarker(10, 20));
+ marker_list_.push_back(CreateMarker(11, 21));
+
+ DocumentMarkerList* dst_list = new SuggestionMarkerListImpl();
+ // The markers with start and end offset < 11 should be moved to dst_list.
+ // Markers that start before 11 and end at 11 or later should be removed.
+ // Markers that start at 11 or later should not be moved.
+ UnsortedDocumentMarkerListEditor::MoveMarkers(&marker_list_, 11, dst_list);
+
+ std::sort(marker_list_.begin(), marker_list_.end(), compare_markers);
+
+ EXPECT_EQ(4u, marker_list_.size());
+
+ EXPECT_EQ(11u, marker_list_[0]->StartOffset());
+ EXPECT_EQ(21u, marker_list_[0]->EndOffset());
+
+ EXPECT_EQ(20u, marker_list_[1]->StartOffset());
+ EXPECT_EQ(30u, marker_list_[1]->EndOffset());
+
+ EXPECT_EQ(20u, marker_list_[2]->StartOffset());
+ EXPECT_EQ(40u, marker_list_[2]->EndOffset());
+
+ EXPECT_EQ(30u, marker_list_[3]->StartOffset());
+ EXPECT_EQ(40u, marker_list_[3]->EndOffset());
+
+ DocumentMarkerVector dst_list_markers = dst_list->GetMarkers();
+ std::sort(dst_list_markers.begin(), dst_list_markers.end(), compare_markers);
+
+ // Markers
+ EXPECT_EQ(1u, dst_list_markers.size());
+
+ EXPECT_EQ(0u, dst_list_markers[0]->StartOffset());
+ EXPECT_EQ(10u, dst_list_markers[0]->EndOffset());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest, RemoveMarkersEmptyList) {
+ EXPECT_FALSE(
+ UnsortedDocumentMarkerListEditor::RemoveMarkers(&marker_list_, 0, 10));
+ EXPECT_EQ(0u, marker_list_.size());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest, RemoveMarkersTouchingEndpoints) {
+ marker_list_.push_back(CreateMarker(30, 40));
+ marker_list_.push_back(CreateMarker(40, 50));
+ marker_list_.push_back(CreateMarker(10, 20));
+ marker_list_.push_back(CreateMarker(0, 10));
+ marker_list_.push_back(CreateMarker(20, 30));
+
+ EXPECT_TRUE(
+ UnsortedDocumentMarkerListEditor::RemoveMarkers(&marker_list_, 20, 10));
+
+ std::sort(marker_list_.begin(), marker_list_.end(), compare_markers);
+
+ EXPECT_EQ(4u, marker_list_.size());
+
+ EXPECT_EQ(0u, marker_list_[0]->StartOffset());
+ EXPECT_EQ(10u, marker_list_[0]->EndOffset());
+
+ EXPECT_EQ(10u, marker_list_[1]->StartOffset());
+ EXPECT_EQ(20u, marker_list_[1]->EndOffset());
+
+ EXPECT_EQ(30u, marker_list_[2]->StartOffset());
+ EXPECT_EQ(40u, marker_list_[2]->EndOffset());
+
+ EXPECT_EQ(40u, marker_list_[3]->StartOffset());
+ EXPECT_EQ(50u, marker_list_[3]->EndOffset());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ RemoveMarkersOneCharacterIntoInterior) {
+ marker_list_.push_back(CreateMarker(30, 40));
+ marker_list_.push_back(CreateMarker(40, 50));
+ marker_list_.push_back(CreateMarker(10, 20));
+ marker_list_.push_back(CreateMarker(0, 10));
+ marker_list_.push_back(CreateMarker(20, 30));
+
+ EXPECT_TRUE(
+ UnsortedDocumentMarkerListEditor::RemoveMarkers(&marker_list_, 19, 12));
+
+ std::sort(marker_list_.begin(), marker_list_.end(), compare_markers);
+
+ EXPECT_EQ(2u, marker_list_.size());
+
+ EXPECT_EQ(0u, marker_list_[0]->StartOffset());
+ EXPECT_EQ(10u, marker_list_[0]->EndOffset());
+
+ EXPECT_EQ(40u, marker_list_[1]->StartOffset());
+ EXPECT_EQ(50u, marker_list_[1]->EndOffset());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_ReplaceStartOfMarker) {
+ UnsortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ // Replace with shorter text
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0,
+ 5, 4);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(9u, markers[0]->EndOffset());
+
+ // Replace with longer text
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0,
+ 4, 5);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(10u, markers[0]->EndOffset());
+
+ // Replace with text of same length
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0,
+ 5, 5);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(10u, markers[0]->EndOffset());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_ReplaceContainsStartOfMarker) {
+ UnsortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(5, 15));
+
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0,
+ 10, 10);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(10u, markers[0]->StartOffset());
+ EXPECT_EQ(15u, markers[0]->EndOffset());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_ReplaceEndOfMarker) {
+ UnsortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ // Replace with shorter text
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 5,
+ 5, 4);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(9u, markers[0]->EndOffset());
+
+ // Replace with longer text
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 5,
+ 4, 5);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(10u, markers[0]->EndOffset());
+
+ // Replace with text of same length
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 5,
+ 5, 5);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(10u, markers[0]->EndOffset());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_ReplaceContainsEndOfMarker) {
+ UnsortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 5,
+ 10, 10);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(5u, markers[0]->EndOffset());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_ReplaceEntireMarker) {
+ UnsortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ // Replace with shorter text
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0,
+ 10, 9);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(9u, markers[0]->EndOffset());
+
+ // Replace with longer text
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0,
+ 9, 10);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(10u, markers[0]->EndOffset());
+
+ // Replace with text of same length
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0,
+ 10, 10);
+
+ EXPECT_EQ(1u, markers.size());
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(10u, markers[0]->EndOffset());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_ReplaceTextWithMarkerAtBeginning) {
+ UnsortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0,
+ 15, 15);
+
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_ReplaceTextWithMarkerAtEnd) {
+ UnsortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(5, 15));
+
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0,
+ 15, 15);
+
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_Deletions) {
+ UnsortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 5));
+ markers.push_back(CreateMarker(5, 10));
+ markers.push_back(CreateMarker(10, 15));
+ markers.push_back(CreateMarker(15, 20));
+ markers.push_back(CreateMarker(20, 25));
+
+ // Delete range containing the end of the second marker, the entire third
+ // marker, and the start of the fourth marker
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 8,
+ 9, 0);
+
+ EXPECT_EQ(4u, markers.size());
+
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(5u, markers[0]->EndOffset());
+
+ EXPECT_EQ(5u, markers[1]->StartOffset());
+ EXPECT_EQ(8u, markers[1]->EndOffset());
+
+ EXPECT_EQ(8u, markers[2]->StartOffset());
+ EXPECT_EQ(11u, markers[2]->EndOffset());
+
+ EXPECT_EQ(11u, markers[3]->StartOffset());
+ EXPECT_EQ(16u, markers[3]->EndOffset());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_DeleteExactlyOnMarker) {
+ UnsortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 0,
+ 10, 0);
+
+ EXPECT_EQ(0u, markers.size());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_InsertInMarkerInterior) {
+ UnsortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 5));
+ markers.push_back(CreateMarker(5, 10));
+ markers.push_back(CreateMarker(10, 15));
+
+ // insert in middle of second marker
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 7,
+ 0, 5);
+
+ EXPECT_EQ(3u, markers.size());
+
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(5u, markers[0]->EndOffset());
+
+ EXPECT_EQ(5u, markers[1]->StartOffset());
+ EXPECT_EQ(15u, markers[1]->EndOffset());
+
+ EXPECT_EQ(15u, markers[2]->StartOffset());
+ EXPECT_EQ(20u, markers[2]->EndOffset());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ ContentIndependentMarker_InsertBetweenMarkers) {
+ UnsortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 5));
+ markers.push_back(CreateMarker(5, 10));
+ markers.push_back(CreateMarker(10, 15));
+
+ // insert before second marker
+ UnsortedDocumentMarkerListEditor::ShiftMarkersContentIndependent(&markers, 5,
+ 0, 5);
+
+ EXPECT_EQ(3u, markers.size());
+
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(5u, markers[0]->EndOffset());
+
+ EXPECT_EQ(10u, markers[1]->StartOffset());
+ EXPECT_EQ(15u, markers[1]->EndOffset());
+
+ EXPECT_EQ(15u, markers[2]->StartOffset());
+ EXPECT_EQ(20u, markers[2]->EndOffset());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ FirstMarkerIntersectingRange_Empty) {
+ DocumentMarker* marker =
+ UnsortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(
+ marker_list_, 0, 10);
+ EXPECT_EQ(nullptr, marker);
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ FirstMarkerIntersectingRange_RangeContainingNoMarkers) {
+ UnsortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 5));
+ DocumentMarker* marker =
+ UnsortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(markers, 5,
+ 15);
+ EXPECT_EQ(nullptr, marker);
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ FirstMarkerIntersectingRange_TouchingStart) {
+ marker_list_.push_back(CreateMarker(1, 10));
+ marker_list_.push_back(CreateMarker(0, 10));
+
+ DocumentMarker* marker =
+ UnsortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(
+ marker_list_, 0, 1);
+
+ EXPECT_NE(nullptr, marker);
+ EXPECT_EQ(0u, marker->StartOffset());
+ EXPECT_EQ(10u, marker->EndOffset());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ FirstMarkerIntersectingRange_TouchingEnd) {
+ marker_list_.push_back(CreateMarker(0, 9));
+ marker_list_.push_back(CreateMarker(0, 10));
+
+ DocumentMarker* marker =
+ UnsortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(
+ marker_list_, 9, 10);
+
+ EXPECT_NE(nullptr, marker);
+ EXPECT_EQ(0u, marker->StartOffset());
+ EXPECT_EQ(10u, marker->EndOffset());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ FirstMarkerIntersectingRange_CollapsedRange) {
+ marker_list_.push_back(CreateMarker(5, 10));
+
+ DocumentMarker* marker =
+ UnsortedDocumentMarkerListEditor::FirstMarkerIntersectingRange(
+ marker_list_, 7, 7);
+
+ EXPECT_NE(nullptr, marker);
+ EXPECT_EQ(5u, marker->StartOffset());
+ EXPECT_EQ(10u, marker->EndOffset());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ MarkersIntersectingRange_RangeContainingNoMarkers) {
+ UnsortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(0, 10));
+
+ UnsortedDocumentMarkerListEditor::MarkerList markers_intersecting_range =
+ UnsortedDocumentMarkerListEditor::MarkersIntersectingRange(markers, 10,
+ 15);
+ EXPECT_EQ(0u, markers_intersecting_range.size());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ MarkersIntersectingRange_TouchingStart) {
+ marker_list_.push_back(CreateMarker(0, 9));
+ marker_list_.push_back(CreateMarker(1, 9));
+ marker_list_.push_back(CreateMarker(0, 10));
+ marker_list_.push_back(CreateMarker(1, 10));
+
+ UnsortedDocumentMarkerListEditor::MarkerList markers_intersecting_range =
+ UnsortedDocumentMarkerListEditor::MarkersIntersectingRange(marker_list_,
+ 0, 1);
+
+ EXPECT_EQ(2u, markers_intersecting_range.size());
+
+ EXPECT_EQ(0u, markers_intersecting_range[0]->StartOffset());
+ EXPECT_EQ(9u, markers_intersecting_range[0]->EndOffset());
+
+ EXPECT_EQ(0u, markers_intersecting_range[1]->StartOffset());
+ EXPECT_EQ(10u, markers_intersecting_range[1]->EndOffset());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ MarkersIntersectingRange_TouchingEnd) {
+ marker_list_.push_back(CreateMarker(0, 9));
+ marker_list_.push_back(CreateMarker(1, 9));
+ marker_list_.push_back(CreateMarker(0, 10));
+ marker_list_.push_back(CreateMarker(1, 10));
+
+ UnsortedDocumentMarkerListEditor::MarkerList markers_intersecting_range =
+ UnsortedDocumentMarkerListEditor::MarkersIntersectingRange(marker_list_,
+ 9, 10);
+
+ EXPECT_EQ(2u, markers_intersecting_range.size());
+
+ EXPECT_EQ(0u, markers_intersecting_range[0]->StartOffset());
+ EXPECT_EQ(10u, markers_intersecting_range[0]->EndOffset());
+
+ EXPECT_EQ(1u, markers_intersecting_range[1]->StartOffset());
+ EXPECT_EQ(10u, markers_intersecting_range[1]->EndOffset());
+}
+
+TEST_F(UnsortedDocumentMarkerListEditorTest,
+ MarkersIntersectingRange_CollapsedRange) {
+ UnsortedDocumentMarkerListEditor::MarkerList markers;
+ markers.push_back(CreateMarker(5, 10));
+
+ UnsortedDocumentMarkerListEditor::MarkerList markers_intersecting_range =
+ UnsortedDocumentMarkerListEditor::MarkersIntersectingRange(markers, 7, 7);
+ EXPECT_EQ(1u, markers_intersecting_range.size());
+
+ EXPECT_EQ(5u, markers_intersecting_range[0]->StartOffset());
+ EXPECT_EQ(10u, markers_intersecting_range[0]->EndOffset());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/ng_flat_tree_shorthands.cc b/chromium/third_party/blink/renderer/core/editing/ng_flat_tree_shorthands.cc
new file mode 100644
index 00000000000..6eacd06cdeb
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/ng_flat_tree_shorthands.cc
@@ -0,0 +1,28 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/ng_flat_tree_shorthands.h"
+
+#include "third_party/blink/renderer/core/editing/local_caret_rect.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+#include "third_party/blink/renderer/core/layout/ng/inline/ng_caret_rect.h"
+#include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h"
+
+namespace blink {
+
+const LayoutBlockFlow* NGInlineFormattingContextOf(
+ const PositionInFlatTree& position) {
+ return NGInlineFormattingContextOf(ToPositionInDOMTree(position));
+}
+
+LocalCaretRect ComputeNGLocalCaretRect(
+ const LayoutBlockFlow& context,
+ const PositionInFlatTreeWithAffinity& position) {
+ const PositionWithAffinity dom_position(
+ ToPositionInDOMTree(position.GetPosition()), position.Affinity());
+ return ComputeNGLocalCaretRect(context, dom_position);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/ng_flat_tree_shorthands.h b/chromium/third_party/blink/renderer/core/editing/ng_flat_tree_shorthands.h
new file mode 100644
index 00000000000..278d8c6b8b8
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/ng_flat_tree_shorthands.h
@@ -0,0 +1,26 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_NG_FLAT_TREE_SHORTHANDS_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_NG_FLAT_TREE_SHORTHANDS_H_
+
+#include "third_party/blink/renderer/core/editing/forward.h"
+
+namespace blink {
+
+class LayoutBlockFlow;
+struct LocalCaretRect;
+
+// This file contains shorthands that converts FlatTree-variants of editing
+// objects into DOM tree variants, and then pass them to LayoutNG utility
+// functions that accept DOM tree variants only.
+
+const LayoutBlockFlow* NGInlineFormattingContextOf(const PositionInFlatTree&);
+
+LocalCaretRect ComputeNGLocalCaretRect(const LayoutBlockFlow&,
+ const PositionInFlatTreeWithAffinity&);
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/plain_text_range.cc b/chromium/third_party/blink/renderer/core/editing/plain_text_range.cc
new file mode 100644
index 00000000000..11eb49abe86
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/plain_text_range.cc
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012 Apple Inc. All
+ * rights reserved.
+ * Copyright (C) 2005 Alexey Proskuryakov.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/plain_text_range.h"
+
+#include "third_party/blink/renderer/core/dom/container_node.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+
+namespace blink {
+
+PlainTextRange::PlainTextRange() : start_(kNotFound), end_(kNotFound) {}
+
+PlainTextRange::PlainTextRange(const PlainTextRange&) = default;
+
+PlainTextRange::PlainTextRange(int location)
+ : start_(location), end_(location) {
+ DCHECK_GE(location, 0);
+}
+
+PlainTextRange::PlainTextRange(int start, int end) : start_(start), end_(end) {
+ DCHECK_GE(start, 0);
+ DCHECK_GE(end, 0);
+ DCHECK_LE(start, end);
+}
+
+EphemeralRange PlainTextRange::CreateRange(const ContainerNode& scope) const {
+ const TextIteratorBehavior& behavior =
+ TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(true)
+ .Build();
+ return CreateRangeFor(scope, behavior);
+}
+
+EphemeralRange PlainTextRange::CreateRangeForSelection(
+ const ContainerNode& scope) const {
+ const TextIteratorBehavior& behavior =
+ TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(true)
+ .SetEmitsCharactersBetweenAllVisiblePositions(true)
+ .Build();
+ return CreateRangeFor(scope, behavior);
+}
+
+EphemeralRange PlainTextRange::CreateRangeForSelectionIndexing(
+ const ContainerNode& scope) const {
+ const TextIteratorBehavior& behavior =
+ TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(true)
+ .SetEmitsCharactersBetweenAllVisiblePositions(true)
+ .SetSuppressesExtraNewlineEmission(true)
+ .Build();
+ return CreateRangeFor(scope, behavior);
+}
+
+static Position CreatePositionInTextRun(size_t offset_in_run,
+ const Position& text_run_start_position,
+ const Position& text_run_end_position) {
+ if (text_run_start_position.ComputeContainerNode()->IsTextNode()) {
+ // TODO(editing-dev): DCHECK |offset_in_run| falls in int value range.
+ const int offset = offset_in_run;
+ return Position(text_run_start_position.ComputeContainerNode(),
+ offset + text_run_start_position.OffsetInContainerNode());
+ }
+
+ if (!offset_in_run)
+ return text_run_start_position;
+ return text_run_end_position;
+}
+
+EphemeralRange PlainTextRange::CreateRangeFor(
+ const ContainerNode& scope,
+ const TextIteratorBehavior& behavior) const {
+ DCHECK(IsNotNull());
+
+ size_t doc_text_position = 0;
+ bool start_range_found = false;
+
+ Position text_run_start_position;
+ Position text_run_end_position;
+
+ TextIterator it(EphemeralRange::RangeOfContents(scope), behavior);
+
+ // FIXME: the atEnd() check shouldn't be necessary, workaround for
+ // <http://bugs.webkit.org/show_bug.cgi?id=6289>.
+ if (!Start() && !length() && it.AtEnd())
+ return EphemeralRange(Position(it.CurrentContainer(), 0));
+
+ Position result_start = Position(&scope.GetDocument(), 0);
+ Position result_end = result_start;
+
+ for (; !it.AtEnd(); it.Advance()) {
+ const int len = it.length();
+
+ text_run_start_position =
+ it.StartPositionInCurrentContainer().ToOffsetInAnchor();
+ text_run_end_position =
+ it.EndPositionInCurrentContainer().ToOffsetInAnchor();
+
+ const bool found_start =
+ Start() >= doc_text_position && Start() <= doc_text_position + len;
+ const bool found_end =
+ End() >= doc_text_position && End() <= doc_text_position + len;
+
+ // Fix textRunRange->endPosition(), but only if foundStart || foundEnd,
+ // because it is only in those cases that textRunRange is used.
+ if (found_end) {
+ // FIXME: This is a workaround for the fact that the end of a run
+ // is often at the wrong position for emitted '\n's or if the
+ // layoutObject of the current node is a replaced element.
+ if (len == 1 && (it.CharacterAt(0) == '\n' || it.CharacterAt(0) == '\t' ||
+ it.IsInsideAtomicInlineElement())) {
+ it.Advance();
+ if (!it.AtEnd()) {
+ text_run_end_position = it.StartPositionInCurrentContainer();
+ } else {
+ Position run_end =
+ NextPositionOf(CreateVisiblePosition(text_run_start_position))
+ .DeepEquivalent();
+ if (run_end.IsNotNull())
+ text_run_end_position = run_end;
+ }
+ }
+ }
+
+ if (found_start) {
+ start_range_found = true;
+ result_start = CreatePositionInTextRun(Start() - doc_text_position,
+ text_run_start_position,
+ text_run_end_position);
+ }
+
+ if (found_end) {
+ result_end = CreatePositionInTextRun(End() - doc_text_position,
+ text_run_start_position,
+ text_run_end_position);
+
+ DCHECK(start_range_found);
+ return EphemeralRange(result_start, result_end);
+ }
+
+ doc_text_position += len;
+ }
+
+ // Start() is out of bounds
+ if (!start_range_found)
+ return EphemeralRange();
+
+ // End() is out of bounds
+ return EphemeralRange(result_start, text_run_end_position);
+}
+
+PlainTextRange PlainTextRange::Create(const ContainerNode& scope,
+ const EphemeralRange& range) {
+ if (range.IsNull())
+ return PlainTextRange();
+
+ // The critical assumption is that this only gets called with ranges that
+ // concentrate on a given area containing the selection root. This is done
+ // because of text fields and textareas. The DOM for those is not
+ // directly in the document DOM, so ensure that the range does not cross a
+ // boundary of one of those.
+ Node* start_container = range.StartPosition().ComputeContainerNode();
+ if (start_container != &scope && !start_container->IsDescendantOf(&scope))
+ return PlainTextRange();
+ Node* end_container = range.EndPosition().ComputeContainerNode();
+ if (end_container != scope && !end_container->IsDescendantOf(&scope))
+ return PlainTextRange();
+
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ scope.GetDocument().Lifecycle());
+
+ size_t start =
+ TextIterator::RangeLength(Position(scope, 0), range.StartPosition());
+ size_t end =
+ TextIterator::RangeLength(Position(scope, 0), range.EndPosition());
+
+ return PlainTextRange(start, end);
+}
+
+PlainTextRange PlainTextRange::Create(const ContainerNode& scope,
+ const Range& range) {
+ return Create(scope, EphemeralRange(&range));
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/plain_text_range.h b/chromium/third_party/blink/renderer/core/editing/plain_text_range.h
new file mode 100644
index 00000000000..b99e8c0d095
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/plain_text_range.h
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_PLAIN_TEXT_RANGE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_PLAIN_TEXT_RANGE_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/not_found.h"
+
+namespace blink {
+
+class ContainerNode;
+class Range;
+class TextIteratorBehavior;
+
+class CORE_EXPORT PlainTextRange {
+ STACK_ALLOCATED();
+
+ public:
+ PlainTextRange();
+ PlainTextRange(const PlainTextRange&);
+ explicit PlainTextRange(int location);
+ PlainTextRange(int start, int end);
+
+ size_t End() const {
+ DCHECK(IsNotNull());
+ return end_;
+ }
+ size_t Start() const {
+ DCHECK(IsNotNull());
+ return start_;
+ }
+ bool IsNull() const { return start_ == kNotFound; }
+ bool IsNotNull() const { return start_ != kNotFound; }
+ size_t length() const {
+ DCHECK(IsNotNull());
+ return end_ - start_;
+ }
+
+ EphemeralRange CreateRange(const ContainerNode& scope) const;
+ EphemeralRange CreateRangeForSelection(const ContainerNode& scope) const;
+ EphemeralRange CreateRangeForSelectionIndexing(
+ const ContainerNode& scope) const;
+
+ static PlainTextRange Create(const ContainerNode& scope,
+ const EphemeralRange&);
+ static PlainTextRange Create(const ContainerNode& scope, const Range&);
+
+ private:
+ PlainTextRange& operator=(const PlainTextRange&) = delete;
+
+ EphemeralRange CreateRangeFor(const ContainerNode& scope,
+ const TextIteratorBehavior&) const;
+
+ const size_t start_;
+ const size_t end_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_PLAIN_TEXT_RANGE_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/plain_text_range_test.cc b/chromium/third_party/blink/renderer/core/editing/plain_text_range_test.cc
new file mode 100644
index 00000000000..d57f351bf0c
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/plain_text_range_test.cc
@@ -0,0 +1,32 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/plain_text_range.h"
+
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class PlainTextRangeTest : public EditingTestBase {
+ protected:
+ Element* InsertHTMLElement(const char* element_code, const char* element_id);
+};
+
+TEST_F(PlainTextRangeTest, RangeContainingTableCellBoundary) {
+ SetBodyInnerHTML(
+ "<table id='sample' contenteditable><tr><td>a</td><td "
+ "id='td2'>b</td></tr></table>");
+ Element* table = GetElementById("sample");
+
+ PlainTextRange plain_text_range(2, 2);
+ const EphemeralRange& range = plain_text_range.CreateRange(*table);
+ EXPECT_EQ(
+ "<table contenteditable id=\"sample\"><tbody><tr><td>a</td><td "
+ "id=\"td2\">|b</td></tr></tbody></table>",
+ GetCaretTextFromBody(range.StartPosition()));
+ EXPECT_TRUE(range.IsCollapsed());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/position.cc b/chromium/third_party/blink/renderer/core/editing/position.cc
new file mode 100644
index 00000000000..f9e5071a963
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/position.cc
@@ -0,0 +1,783 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2009 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/position.h"
+
+#include <stdio.h>
+#include <ostream> // NOLINT
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/text_affinity.h"
+#include "third_party/blink/renderer/platform/wtf/text/cstring.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
+
+namespace blink {
+
+#if DCHECK_IS_ON()
+template <typename Strategy>
+static bool CanBeAnchorNode(Node*);
+
+template <>
+bool CanBeAnchorNode<EditingStrategy>(Node* node) {
+ return !node || !node->IsPseudoElement();
+}
+
+template <>
+bool CanBeAnchorNode<EditingInFlatTreeStrategy>(Node* node) {
+ return CanBeAnchorNode<EditingStrategy>(node) &&
+ node->CanParticipateInFlatTree();
+}
+#endif
+
+template <typename Strategy>
+void PositionTemplate<Strategy>::Trace(blink::Visitor* visitor) {
+ visitor->Trace(anchor_node_);
+}
+
+template <typename Strategy>
+const TreeScope* PositionTemplate<Strategy>::CommonAncestorTreeScope(
+ const PositionTemplate<Strategy>& a,
+ const PositionTemplate<Strategy>& b) {
+ if (!a.ComputeContainerNode() || !b.ComputeContainerNode())
+ return nullptr;
+ return a.ComputeContainerNode()->GetTreeScope().CommonAncestorTreeScope(
+ b.ComputeContainerNode()->GetTreeScope());
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> PositionTemplate<Strategy>::EditingPositionOf(
+ const Node* anchor_node,
+ int offset) {
+ if (!anchor_node || anchor_node->IsTextNode())
+ return PositionTemplate<Strategy>(anchor_node, offset);
+
+ if (!EditingIgnoresContent(*anchor_node)) {
+ return PositionTemplate<Strategy>::CreateWithoutValidationDeprecated(
+ *anchor_node, offset);
+ }
+
+ if (offset == 0)
+ return PositionTemplate<Strategy>(anchor_node,
+ PositionAnchorType::kBeforeAnchor);
+
+ // Note: |offset| can be >= 1, if |anchorNode| have child nodes, e.g.
+ // using Node.appendChild() to add a child node TEXTAREA.
+ DCHECK_GE(offset, 1);
+ return PositionTemplate<Strategy>(anchor_node,
+ PositionAnchorType::kAfterAnchor);
+}
+
+// TODO(editing-dev): Once we change type of |anchor_node_| to
+// |Member<const Node>|, we should get rid of |const_cast<Node*>()|.
+// See http://crbug.com/735327
+template <typename Strategy>
+PositionTemplate<Strategy>::PositionTemplate(const Node* anchor_node,
+ PositionAnchorType anchor_type)
+ : anchor_node_(const_cast<Node*>(anchor_node)),
+ offset_(0),
+ anchor_type_(anchor_type) {
+ if (!anchor_node_) {
+ anchor_type_ = PositionAnchorType::kOffsetInAnchor;
+ return;
+ }
+ if (anchor_node_->IsTextNode()) {
+ DCHECK(anchor_type_ == PositionAnchorType::kBeforeAnchor ||
+ anchor_type_ == PositionAnchorType::kAfterAnchor);
+ return;
+ }
+ if (anchor_node_->IsDocumentNode()) {
+ // Since |RangeBoundaryPoint| can't represent before/after Document, we
+ // should not use them.
+ DCHECK(IsBeforeChildren() || IsAfterChildren()) << anchor_type_;
+ return;
+ }
+#if DCHECK_IS_ON()
+ DCHECK(CanBeAnchorNode<Strategy>(anchor_node_.Get())) << anchor_node_;
+#endif
+ DCHECK_NE(anchor_type_, PositionAnchorType::kOffsetInAnchor);
+}
+
+// TODO(editing-dev): Once we change type of |anchor_node_| to
+// |Member<const Node>|, we should get rid of |const_cast<Node*>()|.
+// See http://crbug.com/735327
+template <typename Strategy>
+PositionTemplate<Strategy>::PositionTemplate(const Node* anchor_node,
+ int offset)
+ : anchor_node_(const_cast<Node*>(anchor_node)),
+ offset_(offset),
+ anchor_type_(PositionAnchorType::kOffsetInAnchor) {
+#if DCHECK_IS_ON()
+ DCHECK(CanBeAnchorNode<Strategy>(anchor_node_.Get())) << anchor_node_;
+ if (!anchor_node_) {
+ DCHECK_EQ(offset, 0);
+ return;
+ }
+ if (anchor_node_->IsCharacterDataNode()) {
+ DCHECK_GE(offset, 0);
+ DCHECK_LE(static_cast<unsigned>(offset),
+ ToCharacterData(anchor_node_)->length())
+ << anchor_node_;
+ return;
+ }
+ DCHECK_GE(offset, 0);
+ DCHECK_LE(static_cast<unsigned>(offset),
+ Strategy::CountChildren(*anchor_node))
+ << anchor_node_;
+#endif
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy>::PositionTemplate(const Node& anchor_node,
+ int offset)
+ : PositionTemplate(&anchor_node, offset) {}
+
+template <typename Strategy>
+PositionTemplate<Strategy>::PositionTemplate(const PositionTemplate& other)
+ : anchor_node_(other.anchor_node_),
+ offset_(other.offset_),
+ anchor_type_(other.anchor_type_) {}
+
+// static
+template <typename Strategy>
+PositionTemplate<Strategy> PositionTemplate<Strategy>::CreateWithoutValidation(
+ const Node& container,
+ int offset) {
+ PositionTemplate<Strategy> result(container, 0);
+ result.offset_ = offset;
+ return result;
+}
+
+// static
+template <typename Strategy>
+PositionTemplate<Strategy>
+PositionTemplate<Strategy>::CreateWithoutValidationDeprecated(
+ const Node& container,
+ int offset) {
+ return CreateWithoutValidation(container, offset);
+}
+
+// --
+
+template <typename Strategy>
+Node* PositionTemplate<Strategy>::ComputeContainerNode() const {
+ if (!anchor_node_)
+ return nullptr;
+
+ switch (AnchorType()) {
+ case PositionAnchorType::kBeforeChildren:
+ case PositionAnchorType::kAfterChildren:
+ case PositionAnchorType::kOffsetInAnchor:
+ return anchor_node_.Get();
+ case PositionAnchorType::kBeforeAnchor:
+ case PositionAnchorType::kAfterAnchor:
+ return Strategy::Parent(*anchor_node_);
+ }
+ NOTREACHED();
+ return nullptr;
+}
+
+template <typename Strategy>
+static int MinOffsetForNode(Node* anchor_node, int offset) {
+ if (anchor_node->IsCharacterDataNode())
+ return std::min(offset, static_cast<int>(ToCharacterData(anchor_node)->length()));
+
+ int new_offset = 0;
+ for (Node* node = Strategy::FirstChild(*anchor_node);
+ node && new_offset < offset; node = Strategy::NextSibling(*node))
+ new_offset++;
+
+ return new_offset;
+}
+
+template <typename Strategy>
+int PositionTemplate<Strategy>::ComputeOffsetInContainerNode() const {
+ if (!anchor_node_)
+ return 0;
+
+ switch (AnchorType()) {
+ case PositionAnchorType::kBeforeChildren:
+ return 0;
+ case PositionAnchorType::kAfterChildren:
+ return LastOffsetInNode(*anchor_node_);
+ case PositionAnchorType::kOffsetInAnchor:
+ return MinOffsetForNode<Strategy>(anchor_node_.Get(), offset_);
+ case PositionAnchorType::kBeforeAnchor:
+ return Strategy::Index(*anchor_node_);
+ case PositionAnchorType::kAfterAnchor:
+ return Strategy::Index(*anchor_node_) + 1;
+ }
+ NOTREACHED();
+ return 0;
+}
+
+// Neighbor-anchored positions are invalid DOM positions, so they need to be
+// fixed up before handing them off to the Range object.
+template <typename Strategy>
+PositionTemplate<Strategy>
+PositionTemplate<Strategy>::ParentAnchoredEquivalent() const {
+ if (!anchor_node_)
+ return PositionTemplate<Strategy>();
+
+ // FIXME: This should only be necessary for legacy positions, but is also
+ // needed for positions before and after Tables
+ if (offset_ == 0 && !IsAfterAnchorOrAfterChildren()) {
+ if (Strategy::Parent(*anchor_node_) &&
+ (EditingIgnoresContent(*anchor_node_) ||
+ IsDisplayInsideTable(anchor_node_.Get())))
+ return InParentBeforeNode(*anchor_node_);
+ return PositionTemplate<Strategy>(anchor_node_.Get(), 0);
+ }
+ if (!anchor_node_->IsCharacterDataNode() &&
+ (IsAfterAnchorOrAfterChildren() ||
+ static_cast<unsigned>(offset_) == anchor_node_->CountChildren()) &&
+ (EditingIgnoresContent(*anchor_node_) ||
+ IsDisplayInsideTable(anchor_node_.Get())) &&
+ ComputeContainerNode()) {
+ return InParentAfterNode(*anchor_node_);
+ }
+
+ return PositionTemplate<Strategy>(ComputeContainerNode(),
+ ComputeOffsetInContainerNode());
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> PositionTemplate<Strategy>::ToOffsetInAnchor()
+ const {
+ if (IsNull())
+ return PositionTemplate<Strategy>();
+
+ return PositionTemplate<Strategy>(ComputeContainerNode(),
+ ComputeOffsetInContainerNode());
+}
+
+template <typename Strategy>
+int PositionTemplate<Strategy>::ComputeEditingOffset() const {
+ if (IsAfterAnchorOrAfterChildren())
+ return Strategy::LastOffsetForEditing(anchor_node_.Get());
+ return offset_;
+}
+
+template <typename Strategy>
+Node* PositionTemplate<Strategy>::ComputeNodeBeforePosition() const {
+ if (!anchor_node_)
+ return nullptr;
+ switch (AnchorType()) {
+ case PositionAnchorType::kBeforeChildren:
+ return nullptr;
+ case PositionAnchorType::kAfterChildren:
+ return Strategy::LastChild(*anchor_node_);
+ case PositionAnchorType::kOffsetInAnchor:
+ return offset_ ? Strategy::ChildAt(*anchor_node_, offset_ - 1) : nullptr;
+ case PositionAnchorType::kBeforeAnchor:
+ return Strategy::PreviousSibling(*anchor_node_);
+ case PositionAnchorType::kAfterAnchor:
+ return anchor_node_.Get();
+ }
+ NOTREACHED();
+ return nullptr;
+}
+
+template <typename Strategy>
+Node* PositionTemplate<Strategy>::ComputeNodeAfterPosition() const {
+ if (!anchor_node_)
+ return nullptr;
+
+ switch (AnchorType()) {
+ case PositionAnchorType::kBeforeChildren:
+ return Strategy::FirstChild(*anchor_node_);
+ case PositionAnchorType::kAfterChildren:
+ return nullptr;
+ case PositionAnchorType::kOffsetInAnchor:
+ return Strategy::ChildAt(*anchor_node_, offset_);
+ case PositionAnchorType::kBeforeAnchor:
+ return anchor_node_.Get();
+ case PositionAnchorType::kAfterAnchor:
+ return Strategy::NextSibling(*anchor_node_);
+ }
+ NOTREACHED();
+ return nullptr;
+}
+
+// An implementation of |Range::firstNode()|.
+template <typename Strategy>
+Node* PositionTemplate<Strategy>::NodeAsRangeFirstNode() const {
+ if (!anchor_node_)
+ return nullptr;
+ if (!IsOffsetInAnchor())
+ return ToOffsetInAnchor().NodeAsRangeFirstNode();
+ if (anchor_node_->IsCharacterDataNode())
+ return anchor_node_.Get();
+ if (Node* child = Strategy::ChildAt(*anchor_node_, offset_))
+ return child;
+ if (!offset_)
+ return anchor_node_.Get();
+ return Strategy::NextSkippingChildren(*anchor_node_);
+}
+
+template <typename Strategy>
+Node* PositionTemplate<Strategy>::NodeAsRangeLastNode() const {
+ if (IsNull())
+ return nullptr;
+ if (Node* past_last_node = NodeAsRangePastLastNode())
+ return Strategy::Previous(*past_last_node);
+ return &Strategy::LastWithinOrSelf(*ComputeContainerNode());
+}
+
+// An implementation of |Range::pastLastNode()|.
+template <typename Strategy>
+Node* PositionTemplate<Strategy>::NodeAsRangePastLastNode() const {
+ if (!anchor_node_)
+ return nullptr;
+ if (!IsOffsetInAnchor())
+ return ToOffsetInAnchor().NodeAsRangePastLastNode();
+ if (anchor_node_->IsCharacterDataNode())
+ return Strategy::NextSkippingChildren(*anchor_node_);
+ if (Node* child = Strategy::ChildAt(*anchor_node_, offset_))
+ return child;
+ return Strategy::NextSkippingChildren(*anchor_node_);
+}
+
+template <typename Strategy>
+Node* PositionTemplate<Strategy>::CommonAncestorContainer(
+ const PositionTemplate<Strategy>& other) const {
+ return Strategy::CommonAncestor(*ComputeContainerNode(),
+ *other.ComputeContainerNode());
+}
+
+static bool IsPositionConnected(const Position& position) {
+ return position.AnchorNode() && position.AnchorNode()->isConnected();
+}
+
+static bool IsPositionConnected(const PositionInFlatTree& position) {
+ if (position.IsNull())
+ return false;
+ return FlatTreeTraversal::Contains(*position.GetDocument(),
+ *position.AnchorNode());
+}
+
+template <typename Strategy>
+bool PositionTemplate<Strategy>::IsConnected() const {
+ return IsPositionConnected(*this);
+}
+
+template <typename Strategy>
+bool PositionTemplate<Strategy>::IsValidFor(const Document& document) const {
+ if (IsNull())
+ return true;
+ if (GetDocument() != document)
+ return false;
+ if (!IsConnected())
+ return false;
+ return !IsOffsetInAnchor() ||
+ OffsetInContainerNode() <= LastOffsetInNode(*AnchorNode());
+}
+
+int ComparePositions(const PositionInFlatTree& position_a,
+ const PositionInFlatTree& position_b) {
+ DCHECK(position_a.IsNotNull());
+ DCHECK(position_b.IsNotNull());
+
+ position_a.AnchorNode()->UpdateDistribution();
+ Node* container_a = position_a.ComputeContainerNode();
+ position_b.AnchorNode()->UpdateDistribution();
+ Node* container_b = position_b.ComputeContainerNode();
+ int offset_a = position_a.ComputeOffsetInContainerNode();
+ int offset_b = position_b.ComputeOffsetInContainerNode();
+ return ComparePositionsInFlatTree(container_a, offset_a, container_b,
+ offset_b);
+}
+
+template <typename Strategy>
+int PositionTemplate<Strategy>::CompareTo(
+ const PositionTemplate<Strategy>& other) const {
+ return ComparePositions(*this, other);
+}
+
+template <typename Strategy>
+bool PositionTemplate<Strategy>::operator<(
+ const PositionTemplate<Strategy>& other) const {
+ return ComparePositions(*this, other) < 0;
+}
+
+template <typename Strategy>
+bool PositionTemplate<Strategy>::operator<=(
+ const PositionTemplate<Strategy>& other) const {
+ return ComparePositions(*this, other) <= 0;
+}
+
+template <typename Strategy>
+bool PositionTemplate<Strategy>::operator>(
+ const PositionTemplate<Strategy>& other) const {
+ return ComparePositions(*this, other) > 0;
+}
+
+template <typename Strategy>
+bool PositionTemplate<Strategy>::operator>=(
+ const PositionTemplate<Strategy>& other) const {
+ return ComparePositions(*this, other) >= 0;
+}
+
+template <typename Strategy>
+bool PositionTemplate<Strategy>::IsEquivalent(
+ const PositionTemplate<Strategy>& other) const {
+ if (IsNull())
+ return other.IsNull();
+ if (anchor_type_ == other.anchor_type_)
+ return *this == other;
+ return ToOffsetInAnchor() == other.ToOffsetInAnchor();
+}
+
+template <typename Strategy>
+bool PositionTemplate<Strategy>::AtFirstEditingPositionForNode() const {
+ if (IsNull())
+ return true;
+ // FIXME: Position before anchor shouldn't be considered as at the first
+ // editing position for node since that position resides outside of the node.
+ switch (anchor_type_) {
+ case PositionAnchorType::kOffsetInAnchor:
+ return offset_ == 0;
+ case PositionAnchorType::kBeforeChildren:
+ case PositionAnchorType::kBeforeAnchor:
+ return true;
+ case PositionAnchorType::kAfterChildren:
+ case PositionAnchorType::kAfterAnchor:
+ // TODO(yosin) We should use |Strategy::lastOffsetForEditing()| instead
+ // of DOM tree version.
+ return !EditingStrategy::LastOffsetForEditing(AnchorNode());
+ }
+ NOTREACHED();
+ return false;
+}
+
+template <typename Strategy>
+bool PositionTemplate<Strategy>::AtLastEditingPositionForNode() const {
+ if (IsNull())
+ return true;
+ // TODO(yosin): Position after anchor shouldn't be considered as at the
+ // first editing position for node since that position resides outside of
+ // the node.
+ // TODO(yosin) We should use |Strategy::lastOffsetForEditing()| instead of
+ // DOM tree version.
+ return IsAfterAnchorOrAfterChildren() ||
+ offset_ >= EditingStrategy::LastOffsetForEditing(AnchorNode());
+}
+
+template <typename Strategy>
+bool PositionTemplate<Strategy>::AtStartOfTree() const {
+ if (IsNull())
+ return true;
+ return !Strategy::Parent(*AnchorNode()) && offset_ == 0;
+}
+
+template <typename Strategy>
+bool PositionTemplate<Strategy>::AtEndOfTree() const {
+ if (IsNull())
+ return true;
+ // TODO(yosin) We should use |Strategy::lastOffsetForEditing()| instead of
+ // DOM tree version.
+ return !Strategy::Parent(*AnchorNode()) &&
+ offset_ >= EditingStrategy::LastOffsetForEditing(AnchorNode());
+}
+
+// static
+template <typename Strategy>
+PositionTemplate<Strategy> PositionTemplate<Strategy>::InParentBeforeNode(
+ const Node& node) {
+ // FIXME: This should DCHECK(node.parentNode()). At least one caller currently
+ // hits this DCHECK though, which indicates that the caller is trying to make
+ // a position relative to a disconnected node (which is likely an error)
+ // Specifically, editing/deleting/delete-ligature-001.html crashes with
+ // DCHECK(node->parentNode())
+ return PositionTemplate<Strategy>(Strategy::Parent(node),
+ Strategy::Index(node));
+}
+
+// static
+template <typename Strategy>
+PositionTemplate<Strategy> PositionTemplate<Strategy>::InParentAfterNode(
+ const Node& node) {
+ DCHECK(node.parentNode()) << node;
+ return PositionTemplate<Strategy>(Strategy::Parent(node),
+ Strategy::Index(node) + 1);
+}
+
+// static
+template <typename Strategy>
+PositionTemplate<Strategy> PositionTemplate<Strategy>::BeforeNode(
+ const Node& anchor_node) {
+ return PositionTemplate<Strategy>(&anchor_node,
+ PositionAnchorType::kBeforeAnchor);
+}
+
+// static
+template <typename Strategy>
+PositionTemplate<Strategy> PositionTemplate<Strategy>::AfterNode(
+ const Node& anchor_node) {
+ return PositionTemplate<Strategy>(&anchor_node,
+ PositionAnchorType::kAfterAnchor);
+}
+
+// static
+template <typename Strategy>
+int PositionTemplate<Strategy>::LastOffsetInNode(const Node& node) {
+ return node.IsCharacterDataNode()
+ ? static_cast<int>(ToCharacterData(node).length())
+ : static_cast<int>(Strategy::CountChildren(node));
+}
+
+// static
+template <typename Strategy>
+PositionTemplate<Strategy> PositionTemplate<Strategy>::FirstPositionInNode(
+ const Node& anchor_node) {
+ if (anchor_node.IsTextNode())
+ return PositionTemplate<Strategy>(anchor_node, 0);
+ return PositionTemplate<Strategy>(&anchor_node,
+ PositionAnchorType::kBeforeChildren);
+}
+
+// static
+template <typename Strategy>
+PositionTemplate<Strategy> PositionTemplate<Strategy>::LastPositionInNode(
+ const Node& anchor_node) {
+ if (anchor_node.IsTextNode()) {
+ return PositionTemplate<Strategy>(anchor_node,
+ LastOffsetInNode(anchor_node));
+ }
+ return PositionTemplate<Strategy>(&anchor_node,
+ PositionAnchorType::kAfterChildren);
+}
+
+// static
+template <typename Strategy>
+PositionTemplate<Strategy>
+PositionTemplate<Strategy>::FirstPositionInOrBeforeNode(const Node& node) {
+ return EditingIgnoresContent(node) ? BeforeNode(node)
+ : FirstPositionInNode(node);
+}
+
+// static
+template <typename Strategy>
+PositionTemplate<Strategy>
+PositionTemplate<Strategy>::LastPositionInOrAfterNode(const Node& node) {
+ return EditingIgnoresContent(node) ? AfterNode(node)
+ : LastPositionInNode(node);
+}
+
+PositionInFlatTree ToPositionInFlatTree(const Position& pos) {
+ if (pos.IsNull())
+ return PositionInFlatTree();
+
+ Node* const anchor = pos.AnchorNode();
+ if (pos.IsOffsetInAnchor()) {
+ if (anchor->IsCharacterDataNode())
+ return PositionInFlatTree(anchor, pos.ComputeOffsetInContainerNode());
+ DCHECK(!anchor->IsElementNode() || anchor->CanParticipateInFlatTree());
+ int offset = pos.ComputeOffsetInContainerNode();
+ Node* child = NodeTraversal::ChildAt(*anchor, offset);
+ if (!child) {
+ if (anchor->IsShadowRoot())
+ return PositionInFlatTree(anchor->OwnerShadowHost(),
+ PositionAnchorType::kAfterChildren);
+ return PositionInFlatTree(anchor, PositionAnchorType::kAfterChildren);
+ }
+ child->UpdateDistribution();
+ if (!child->CanParticipateInFlatTree()) {
+ if (anchor->IsShadowRoot())
+ return PositionInFlatTree(anchor->OwnerShadowHost(), offset);
+ return PositionInFlatTree(anchor, offset);
+ }
+ if (Node* parent = FlatTreeTraversal::Parent(*child))
+ return PositionInFlatTree(parent, FlatTreeTraversal::Index(*child));
+ // When |pos| isn't appeared in flat tree, we map |pos| to after
+ // children of shadow host.
+ // e.g. "foo",0 in <progress>foo</progress>
+ if (anchor->IsShadowRoot())
+ return PositionInFlatTree(anchor->OwnerShadowHost(),
+ PositionAnchorType::kAfterChildren);
+ return PositionInFlatTree(anchor, PositionAnchorType::kAfterChildren);
+ }
+
+ if (anchor->IsShadowRoot())
+ return PositionInFlatTree(anchor->OwnerShadowHost(), pos.AnchorType());
+ if (pos.IsBeforeAnchor() || pos.IsAfterAnchor()) {
+ if (anchor->CanParticipateInFlatTree() &&
+ !FlatTreeTraversal::Parent(*anchor)) {
+ // For Before/AfterAnchor, if |anchor| doesn't have parent in the flat
+ // tree, there is no valid corresponding PositionInFlatTree.
+ // Since this function is a primitive function, we do not adjust |pos|
+ // to somewhere else in flat tree.
+ // Reached by unit test
+ // FrameSelectionTest.SelectInvalidPositionInFlatTreeDoesntCrash.
+ return PositionInFlatTree();
+ }
+ }
+ // TODO(yosin): Once we have a test case for SLOT or active insertion point,
+ // this function should handle it.
+ return PositionInFlatTree(anchor, pos.AnchorType());
+}
+
+Position ToPositionInDOMTree(const Position& position) {
+ return position;
+}
+
+Position ToPositionInDOMTree(const PositionInFlatTree& position) {
+ if (position.IsNull())
+ return Position();
+
+ Node* anchor_node = position.AnchorNode();
+
+ switch (position.AnchorType()) {
+ case PositionAnchorType::kAfterChildren:
+ // FIXME: When anchorNode is <img>, assertion fails in the constructor.
+ return Position(anchor_node, PositionAnchorType::kAfterChildren);
+ case PositionAnchorType::kAfterAnchor:
+ return Position::AfterNode(*anchor_node);
+ case PositionAnchorType::kBeforeChildren:
+ return Position(anchor_node, PositionAnchorType::kBeforeChildren);
+ case PositionAnchorType::kBeforeAnchor:
+ return Position::BeforeNode(*anchor_node);
+ case PositionAnchorType::kOffsetInAnchor: {
+ int offset = position.OffsetInContainerNode();
+ if (anchor_node->IsCharacterDataNode())
+ return Position(anchor_node, offset);
+ Node* child = FlatTreeTraversal::ChildAt(*anchor_node, offset);
+ if (child)
+ return Position(child->parentNode(), child->NodeIndex());
+ if (!position.OffsetInContainerNode())
+ return Position(anchor_node, PositionAnchorType::kBeforeChildren);
+
+ // |child| is null when the position is at the end of the children.
+ // <div>foo|</div>
+ return Position(anchor_node, PositionAnchorType::kAfterChildren);
+ }
+ default:
+ NOTREACHED();
+ return Position();
+ }
+}
+
+template <typename Strategy>
+String PositionTemplate<Strategy>::ToAnchorTypeAndOffsetString() const {
+ switch (AnchorType()) {
+ case PositionAnchorType::kOffsetInAnchor: {
+ StringBuilder builder;
+ builder.Append("offsetInAnchor[");
+ builder.AppendNumber(offset_);
+ builder.Append("]");
+ return builder.ToString();
+ }
+ case PositionAnchorType::kBeforeChildren:
+ return "beforeChildren";
+ case PositionAnchorType::kAfterChildren:
+ return "afterChildren";
+ case PositionAnchorType::kBeforeAnchor:
+ return "beforeAnchor";
+ case PositionAnchorType::kAfterAnchor:
+ return "afterAnchor";
+ }
+ NOTREACHED();
+ return g_empty_string;
+}
+
+#ifndef NDEBUG
+
+template <typename Strategy>
+void PositionTemplate<Strategy>::ShowTreeForThis() const {
+ if (!AnchorNode()) {
+ LOG(INFO) << "\nposition is null";
+ return;
+ }
+ LOG(INFO) << "\n"
+ << AnchorNode()->ToTreeStringForThis().Utf8().data()
+ << ToAnchorTypeAndOffsetString().Utf8().data();
+}
+
+template <typename Strategy>
+void PositionTemplate<Strategy>::ShowTreeForThisInFlatTree() const {
+ if (!AnchorNode()) {
+ LOG(INFO) << "\nposition is null";
+ return;
+ }
+ LOG(INFO) << "\n"
+ << AnchorNode()->ToFlatTreeStringForThis().Utf8().data()
+ << ToAnchorTypeAndOffsetString().Utf8().data();
+}
+
+#endif
+
+template <typename PositionType>
+static std::ostream& PrintPosition(std::ostream& ostream,
+ const PositionType& position) {
+ if (position.IsNull())
+ return ostream << "null";
+ return ostream << position.AnchorNode() << "@"
+ << position.ToAnchorTypeAndOffsetString().Utf8().data();
+}
+
+std::ostream& operator<<(std::ostream& ostream,
+ PositionAnchorType anchor_type) {
+ switch (anchor_type) {
+ case PositionAnchorType::kAfterAnchor:
+ return ostream << "afterAnchor";
+ case PositionAnchorType::kAfterChildren:
+ return ostream << "afterChildren";
+ case PositionAnchorType::kBeforeAnchor:
+ return ostream << "beforeAnchor";
+ case PositionAnchorType::kBeforeChildren:
+ return ostream << "beforeChildren";
+ case PositionAnchorType::kOffsetInAnchor:
+ return ostream << "offsetInAnchor";
+ }
+ NOTREACHED();
+ return ostream << "anchorType=" << static_cast<int>(anchor_type);
+}
+
+std::ostream& operator<<(std::ostream& ostream, const Position& position) {
+ return PrintPosition(ostream, position);
+}
+
+std::ostream& operator<<(std::ostream& ostream,
+ const PositionInFlatTree& position) {
+ return PrintPosition(ostream, position);
+}
+
+template class CORE_TEMPLATE_EXPORT PositionTemplate<EditingStrategy>;
+template class CORE_TEMPLATE_EXPORT PositionTemplate<EditingInFlatTreeStrategy>;
+
+} // namespace blink
+
+#ifndef NDEBUG
+
+void showTree(const blink::Position& pos) {
+ pos.ShowTreeForThis();
+}
+
+void showTree(const blink::Position* pos) {
+ if (pos)
+ pos->ShowTreeForThis();
+ else
+ LOG(INFO) << "Cannot showTree for <null>";
+}
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/position.h b/chromium/third_party/blink/renderer/core/editing/position.h
new file mode 100644
index 00000000000..5da5181e77e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/position.h
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2004, 2006, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_POSITION_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_POSITION_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/editing_strategy.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+
+namespace blink {
+
+class Node;
+class TreeScope;
+
+enum class PositionAnchorType : unsigned {
+ kOffsetInAnchor,
+ kBeforeAnchor,
+ kAfterAnchor,
+ kBeforeChildren,
+ kAfterChildren,
+};
+
+// Instances of |PositionTemplate<Strategy>| are immutable.
+// TODO(editing-dev): Make constructor of |PositionTemplate| take |const Node*|.
+template <typename Strategy>
+class CORE_TEMPLATE_CLASS_EXPORT PositionTemplate {
+ DISALLOW_NEW();
+
+ public:
+ PositionTemplate()
+ : offset_(0), anchor_type_(PositionAnchorType::kOffsetInAnchor) {}
+
+ static const TreeScope* CommonAncestorTreeScope(
+ const PositionTemplate<Strategy>&,
+ const PositionTemplate<Strategy>& b);
+ static PositionTemplate<Strategy> EditingPositionOf(const Node* anchor_node,
+ int offset);
+
+ // For creating before/after positions:
+ PositionTemplate(const Node* anchor_node, PositionAnchorType);
+
+ // For creating offset positions:
+ PositionTemplate(const Node& anchor_node, int offset);
+ // TODO(editing-dev): We should not pass |nullptr| as |anchor_node| for
+ // |Position| constructor.
+ // TODO(editing-dev): This constructor should eventually go away. See bug
+ // http://wkb.ug/63040.
+ PositionTemplate(const Node* anchor_node, int offset);
+
+ PositionTemplate(const PositionTemplate&);
+
+ // Returns a newly created |Position| with |kOffsetInAnchor|. |offset| can be
+ // out of bound. Out of bound position is used for computing undo/redo
+ // selection for merging text typing.
+ static PositionTemplate<Strategy> CreateWithoutValidation(
+ const Node& container,
+ int offset);
+
+ // TODO(editing-dev): Once we get a reason to use out of bound position,
+ // we should change caller to use |CreateWithoutValidation()|.
+ static PositionTemplate<Strategy> CreateWithoutValidationDeprecated(
+ const Node& container,
+ int offset);
+
+ explicit operator bool() const { return IsNotNull(); }
+
+ PositionAnchorType AnchorType() const { return anchor_type_; }
+ bool IsAfterAnchor() const {
+ return anchor_type_ == PositionAnchorType::kAfterAnchor;
+ }
+ bool IsAfterChildren() const {
+ return anchor_type_ == PositionAnchorType::kAfterChildren;
+ }
+ bool IsBeforeAnchor() const {
+ return anchor_type_ == PositionAnchorType::kBeforeAnchor;
+ }
+ bool IsBeforeChildren() const {
+ return anchor_type_ == PositionAnchorType::kBeforeChildren;
+ }
+ bool IsOffsetInAnchor() const {
+ return anchor_type_ == PositionAnchorType::kOffsetInAnchor;
+ }
+
+ // These are always DOM compliant values. Editing positions like [img, 0]
+ // (aka [img, before]) will return img->parentNode() and img->nodeIndex() from
+ // these functions.
+
+ // null for a before/after position anchored to a node with no parent
+ Node* ComputeContainerNode() const;
+
+ // O(n) for before/after-anchored positions, O(1) for parent-anchored
+ // positions
+ int ComputeOffsetInContainerNode() const;
+
+ // Convenience method for DOM positions that also fixes up some positions for
+ // editing
+ PositionTemplate<Strategy> ParentAnchoredEquivalent() const;
+
+ // Returns |PositionIsAnchor| type |Position| which is compatible with
+ // |RangeBoundaryPoint| as safe to pass |Range| constructor. Return value
+ // of this function is different from |parentAnchoredEquivalent()| which
+ // returns editing specific position.
+ PositionTemplate<Strategy> ToOffsetInAnchor() const;
+
+ // Inline O(1) access for Positions which callers know to be parent-anchored
+ int OffsetInContainerNode() const {
+ DCHECK(IsOffsetInAnchor());
+ return offset_;
+ }
+
+ // Returns an offset for editing based on anchor type for using with
+ // |anchorNode()| function:
+ // - OffsetInAnchor m_offset
+ // - BeforeChildren 0
+ // - BeforeAnchor 0
+ // - AfterChildren last editing offset in anchor node
+ // - AfterAnchor last editing offset in anchor node
+ // Editing operations will change in anchor node rather than nodes around
+ // anchor node.
+ int ComputeEditingOffset() const;
+
+ // These are convenience methods which are smart about whether the position is
+ // neighbor anchored or parent anchored
+ Node* ComputeNodeBeforePosition() const;
+ Node* ComputeNodeAfterPosition() const;
+
+ // Returns node as |Range::firstNode()|. This position must be a
+ // |PositionAnchorType::OffsetInAhcor| to behave as |Range| boundary point.
+ Node* NodeAsRangeFirstNode() const;
+
+ // Similar to |nodeAsRangeLastNode()|, but returns a node in a range.
+ Node* NodeAsRangeLastNode() const;
+
+ // Returns a node as past last as same as |Range::pastLastNode()|. This
+ // function is supposed to used in HTML serialization and plain text
+ // iterator. This position must be a |PositionAnchorType::OffsetInAhcor| to
+ // behave as |Range| boundary point.
+ Node* NodeAsRangePastLastNode() const;
+
+ Node* CommonAncestorContainer(const PositionTemplate<Strategy>&) const;
+
+ Node* AnchorNode() const { return anchor_node_.Get(); }
+
+ Document* GetDocument() const {
+ return anchor_node_ ? &anchor_node_->GetDocument() : nullptr;
+ }
+
+ // For PositionInFlatTree, it requires an ancestor traversal to compute the
+ // value of IsConnected(), which can be expensive.
+ // TODO(crbug.com/761173): Rename to |ComputeIsConnected()| to indicate the
+ // cost.
+ bool IsConnected() const;
+
+ bool IsValidFor(const Document&) const;
+
+ bool IsNull() const { return !anchor_node_; }
+ bool IsNotNull() const { return anchor_node_; }
+ bool IsOrphan() const { return anchor_node_ && !IsConnected(); }
+
+ // Note: Comparison of positions require both parameters are non-null. You
+ // should check null-position before comparing them.
+ // TODO(yosin): We should use |Position::operator<()| instead of
+ // |Position::comapreTo()| to utilize |DHCECK_XX()|.
+ int CompareTo(const PositionTemplate<Strategy>&) const;
+ bool operator<(const PositionTemplate<Strategy>&) const;
+ bool operator<=(const PositionTemplate<Strategy>&) const;
+ bool operator>(const PositionTemplate<Strategy>&) const;
+ bool operator>=(const PositionTemplate<Strategy>&) const;
+
+ bool IsEquivalent(const PositionTemplate<Strategy>&) const;
+
+ // These can be either inside or just before/after the node, depending on
+ // if the node is ignored by editing or not.
+ // FIXME: These should go away. They only make sense for legacy positions.
+ bool AtFirstEditingPositionForNode() const;
+ bool AtLastEditingPositionForNode() const;
+
+ bool AtStartOfTree() const;
+ bool AtEndOfTree() const;
+
+ static PositionTemplate<Strategy> BeforeNode(const Node& anchor_node);
+ static PositionTemplate<Strategy> AfterNode(const Node& anchor_node);
+ static PositionTemplate<Strategy> InParentBeforeNode(const Node& anchor_node);
+ static PositionTemplate<Strategy> InParentAfterNode(const Node& anchor_node);
+ static int LastOffsetInNode(const Node& anchor_node);
+ static PositionTemplate<Strategy> FirstPositionInNode(
+ const Node& anchor_node);
+ static PositionTemplate<Strategy> LastPositionInNode(const Node& anchor_node);
+ static PositionTemplate<Strategy> FirstPositionInOrBeforeNode(
+ const Node& anchor_node);
+ static PositionTemplate<Strategy> LastPositionInOrAfterNode(
+ const Node& anchor_node);
+
+ String ToAnchorTypeAndOffsetString() const;
+#ifndef NDEBUG
+ void ShowTreeForThis() const;
+ void ShowTreeForThisInFlatTree() const;
+#endif
+
+ void Trace(blink::Visitor*);
+
+ private:
+ bool IsAfterAnchorOrAfterChildren() const {
+ return IsAfterAnchor() || IsAfterChildren();
+ }
+
+ // TODO(editing-dev): Since we should consider |Position| is constant in
+ // tree, we should use |Member<const Node>|. see http://crbug.com/735327
+ Member<Node> anchor_node_;
+ // m_offset can be the offset inside m_anchorNode, or if
+ // editingIgnoresContent(m_anchorNode) returns true, then other places in
+ // editing will treat m_offset == 0 as "before the anchor" and m_offset > 0 as
+ // "after the anchor node". See parentAnchoredEquivalent for more info.
+ int offset_;
+ PositionAnchorType anchor_type_;
+};
+
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ PositionTemplate<EditingStrategy>;
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ PositionTemplate<EditingInFlatTreeStrategy>;
+
+using Position = PositionTemplate<EditingStrategy>;
+using PositionInFlatTree = PositionTemplate<EditingInFlatTreeStrategy>;
+
+template <typename Strategy>
+bool operator==(const PositionTemplate<Strategy>& a,
+ const PositionTemplate<Strategy>& b) {
+ if (a.IsNull())
+ return b.IsNull();
+
+ if (a.AnchorNode() != b.AnchorNode() || a.AnchorType() != b.AnchorType())
+ return false;
+
+ if (!a.IsOffsetInAnchor()) {
+ // Note: |m_offset| only has meaning when
+ // |PositionAnchorType::OffsetInAnchor|.
+ return true;
+ }
+
+ // FIXME: In <div><img></div> [div, 0] != [img, 0] even though most of the
+ // editing code will treat them as identical.
+ return a.OffsetInContainerNode() == b.OffsetInContainerNode();
+}
+
+template <typename Strategy>
+bool operator!=(const PositionTemplate<Strategy>& a,
+ const PositionTemplate<Strategy>& b) {
+ return !(a == b);
+}
+
+CORE_EXPORT PositionInFlatTree ToPositionInFlatTree(const Position&);
+CORE_EXPORT Position ToPositionInDOMTree(const Position&);
+CORE_EXPORT Position ToPositionInDOMTree(const PositionInFlatTree&);
+
+template <typename Strategy>
+PositionTemplate<Strategy> FromPositionInDOMTree(const Position&);
+
+template <>
+inline Position FromPositionInDOMTree<EditingStrategy>(
+ const Position& position) {
+ return position;
+}
+
+template <>
+inline PositionInFlatTree FromPositionInDOMTree<EditingInFlatTreeStrategy>(
+ const Position& position) {
+ return ToPositionInFlatTree(position);
+}
+
+CORE_EXPORT std::ostream& operator<<(std::ostream&, PositionAnchorType);
+CORE_EXPORT std::ostream& operator<<(std::ostream&, const Position&);
+CORE_EXPORT std::ostream& operator<<(std::ostream&, const PositionInFlatTree&);
+
+} // namespace blink
+
+#ifndef NDEBUG
+// Outside the WebCore namespace for ease of invocation from gdb.
+void showTree(const blink::Position&);
+void showTree(const blink::Position*);
+#endif
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_POSITION_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/position_iterator.cc b/chromium/third_party/blink/renderer/core/editing/position_iterator.cc
new file mode 100644
index 00000000000..8cce67e8c72
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/position_iterator.cc
@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/position_iterator.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+
+namespace blink {
+
+namespace {
+
+// TODO(editing-dev): We should replace usages of |hasChildren()| in
+// |PositionIterator| to |shouldTraverseChildren()|.
+template <typename Strategy>
+bool ShouldTraverseChildren(const Node& node) {
+ return Strategy::HasChildren(node) && !IsUserSelectContain(node);
+}
+
+// TODO(editing-dev): We should replace usages of |parent()| in
+// |PositionIterator| to |selectableParentOf()|.
+template <typename Strategy>
+ContainerNode* SelectableParentOf(const Node& node) {
+ ContainerNode* const parent = Strategy::Parent(node);
+ return parent && !IsUserSelectContain(*parent) ? parent : nullptr;
+}
+
+} // namespace
+
+static const int kInvalidOffset = -1;
+
+template <typename Strategy>
+PositionIteratorAlgorithm<Strategy>::PositionIteratorAlgorithm(
+ Node* anchor_node,
+ int offset_in_anchor)
+ : anchor_node_(anchor_node),
+ node_after_position_in_anchor_(
+ Strategy::ChildAt(*anchor_node, offset_in_anchor)),
+ offset_in_anchor_(node_after_position_in_anchor_ ? 0 : offset_in_anchor),
+ depth_to_anchor_node_(0),
+ dom_tree_version_(anchor_node->GetDocument().DomTreeVersion()) {
+ for (Node* node = SelectableParentOf<Strategy>(*anchor_node); node;
+ node = SelectableParentOf<Strategy>(*node)) {
+ // Each m_offsetsInAnchorNode[offset] should be an index of node in
+ // parent, but delay to calculate the index until it is needed for
+ // performance.
+ offsets_in_anchor_node_.push_back(kInvalidOffset);
+ ++depth_to_anchor_node_;
+ }
+ if (node_after_position_in_anchor_)
+ offsets_in_anchor_node_.push_back(offset_in_anchor);
+}
+template <typename Strategy>
+PositionIteratorAlgorithm<Strategy>::PositionIteratorAlgorithm(
+ const PositionTemplate<Strategy>& pos)
+ : PositionIteratorAlgorithm(pos.AnchorNode(), pos.ComputeEditingOffset()) {}
+
+template <typename Strategy>
+PositionIteratorAlgorithm<Strategy>::PositionIteratorAlgorithm()
+ : anchor_node_(nullptr),
+ node_after_position_in_anchor_(nullptr),
+ offset_in_anchor_(0),
+ depth_to_anchor_node_(0),
+ dom_tree_version_(0) {}
+
+template <typename Strategy>
+PositionTemplate<Strategy>
+PositionIteratorAlgorithm<Strategy>::DeprecatedComputePosition() const {
+ // TODO(yoichio): Share code to check domTreeVersion with EphemeralRange.
+ DCHECK(IsValid());
+ if (node_after_position_in_anchor_) {
+ DCHECK_EQ(Strategy::Parent(*node_after_position_in_anchor_), anchor_node_);
+ DCHECK_NE(offsets_in_anchor_node_[depth_to_anchor_node_], kInvalidOffset);
+ // FIXME: This check is inadaquete because any ancestor could be ignored by
+ // editing
+ if (EditingIgnoresContent(
+ *Strategy::Parent(*node_after_position_in_anchor_)))
+ return PositionTemplate<Strategy>::BeforeNode(*anchor_node_);
+ return PositionTemplate<Strategy>(
+ anchor_node_, offsets_in_anchor_node_[depth_to_anchor_node_]);
+ }
+ if (Strategy::HasChildren(*anchor_node_)) {
+ return PositionTemplate<Strategy>::LastPositionInOrAfterNode(*anchor_node_);
+ }
+ return PositionTemplate<Strategy>::EditingPositionOf(anchor_node_,
+ offset_in_anchor_);
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy>
+PositionIteratorAlgorithm<Strategy>::ComputePosition() const {
+ DCHECK(IsValid());
+ // Assume that we have the following DOM tree:
+ // A
+ // |-B
+ // | |-E
+ // | +-F
+ // |
+ // |-C
+ // +-D
+ // |-G
+ // +-H
+ if (node_after_position_in_anchor_) {
+ // For example, position is before E, F.
+ DCHECK_EQ(Strategy::Parent(*node_after_position_in_anchor_), anchor_node_);
+ DCHECK_NE(offsets_in_anchor_node_[depth_to_anchor_node_], kInvalidOffset);
+ // TODO(yoichio): This should be equivalent to
+ // PositionTemplate<Strategy>(m_anchorNode,
+ // PositionAnchorType::BeforeAnchor);
+ return PositionTemplate<Strategy>(
+ anchor_node_, offsets_in_anchor_node_[depth_to_anchor_node_]);
+ }
+ if (ShouldTraverseChildren<Strategy>(*anchor_node_)) {
+ // For example, position is the end of B.
+ return PositionTemplate<Strategy>::LastPositionInOrAfterNode(*anchor_node_);
+ }
+ if (anchor_node_->IsTextNode())
+ return PositionTemplate<Strategy>(anchor_node_, offset_in_anchor_);
+ if (offset_in_anchor_)
+ // For example, position is after G.
+ return PositionTemplate<Strategy>(anchor_node_,
+ PositionAnchorType::kAfterAnchor);
+ // For example, position is before G.
+ return PositionTemplate<Strategy>(anchor_node_,
+ PositionAnchorType::kBeforeAnchor);
+}
+
+template <typename Strategy>
+void PositionIteratorAlgorithm<Strategy>::Increment() {
+ DCHECK(IsValid());
+ if (!anchor_node_)
+ return;
+
+ // Assume that we have the following DOM tree:
+ // A
+ // |-B
+ // | |-E
+ // | +-F
+ // |
+ // |-C
+ // +-D
+ // |-G
+ // +-H
+ // Let |anchor| as |m_anchorNode| and
+ // |child| as |m_nodeAfterPositionInAnchor|.
+ if (node_after_position_in_anchor_) {
+ // Case #1: Move to position before the first child of
+ // |m_nodeAfterPositionInAnchor|.
+ // This is a point just before |child|.
+ // Let |anchor| is A and |child| is B,
+ // then next |anchor| is B and |child| is E.
+ anchor_node_ = node_after_position_in_anchor_;
+ node_after_position_in_anchor_ =
+ ShouldTraverseChildren<Strategy>(*anchor_node_)
+ ? Strategy::FirstChild(*anchor_node_)
+ : nullptr;
+ offset_in_anchor_ = 0;
+ // Increment depth intializing with 0.
+ ++depth_to_anchor_node_;
+ if (depth_to_anchor_node_ == offsets_in_anchor_node_.size())
+ offsets_in_anchor_node_.push_back(0);
+ else
+ offsets_in_anchor_node_[depth_to_anchor_node_] = 0;
+ return;
+ }
+
+ if (anchor_node_->GetLayoutObject() &&
+ !ShouldTraverseChildren<Strategy>(*anchor_node_) &&
+ offset_in_anchor_ < Strategy::LastOffsetForEditing(anchor_node_)) {
+ // Case #2. This is the next of Case #1 or #2 itself.
+ // Position is (|anchor|, |m_offsetInAchor|).
+ // In this case |anchor| is a leaf(E,F,C,G or H) and
+ // |m_offsetInAnchor| is not on the end of |anchor|.
+ // Then just increment |m_offsetInAnchor|.
+ offset_in_anchor_ =
+ NextGraphemeBoundaryOf(*anchor_node_, offset_in_anchor_);
+ } else {
+ // Case #3. This is the next of Case #2 or #3.
+ // Position is the end of |anchor|.
+ // 3-a. If |anchor| has next sibling (let E),
+ // next |anchor| is B and |child| is F (next is Case #1.)
+ // 3-b. If |anchor| doesn't have next sibling (let F),
+ // next |anchor| is B and |child| is null. (next is Case #3.)
+ node_after_position_in_anchor_ = anchor_node_;
+ anchor_node_ =
+ SelectableParentOf<Strategy>(*node_after_position_in_anchor_);
+ if (!anchor_node_)
+ return;
+ DCHECK_GT(depth_to_anchor_node_, 0u);
+ --depth_to_anchor_node_;
+ // Increment offset of |child| or initialize if it have never been
+ // used.
+ if (offsets_in_anchor_node_[depth_to_anchor_node_] == kInvalidOffset)
+ offsets_in_anchor_node_[depth_to_anchor_node_] =
+ Strategy::Index(*node_after_position_in_anchor_) + 1;
+ else
+ ++offsets_in_anchor_node_[depth_to_anchor_node_];
+ node_after_position_in_anchor_ =
+ Strategy::NextSibling(*node_after_position_in_anchor_);
+ offset_in_anchor_ = 0;
+ }
+}
+
+template <typename Strategy>
+void PositionIteratorAlgorithm<Strategy>::Decrement() {
+ DCHECK(IsValid());
+ if (!anchor_node_)
+ return;
+
+ // Assume that we have the following DOM tree:
+ // A
+ // |-B
+ // | |-E
+ // | +-F
+ // |
+ // |-C
+ // +-D
+ // |-G
+ // +-H
+ // Let |anchor| as |m_anchorNode| and
+ // |child| as |m_nodeAfterPositionInAnchor|.
+ // decrement() is complex but logically reverse of increment(), of course:)
+ if (node_after_position_in_anchor_) {
+ anchor_node_ = Strategy::PreviousSibling(*node_after_position_in_anchor_);
+ if (anchor_node_) {
+ // Case #1-a. This is a revese of increment()::Case#3-a.
+ // |child| has a previous sibling.
+ // Let |anchor| is B and |child| is F,
+ // next |anchor| is E and |child| is null.
+ node_after_position_in_anchor_ = nullptr;
+ offset_in_anchor_ = ShouldTraverseChildren<Strategy>(*anchor_node_)
+ ? 0
+ : Strategy::LastOffsetForEditing(anchor_node_);
+ // Decrement offset of |child| or initialize if it have never been
+ // used.
+ if (offsets_in_anchor_node_[depth_to_anchor_node_] == kInvalidOffset)
+ offsets_in_anchor_node_[depth_to_anchor_node_] =
+ Strategy::Index(*node_after_position_in_anchor_);
+ else
+ --offsets_in_anchor_node_[depth_to_anchor_node_];
+ DCHECK_GE(offsets_in_anchor_node_[depth_to_anchor_node_], 0);
+ // Increment depth intializing with last offset.
+ ++depth_to_anchor_node_;
+ if (depth_to_anchor_node_ >= offsets_in_anchor_node_.size())
+ offsets_in_anchor_node_.push_back(offset_in_anchor_);
+ else
+ offsets_in_anchor_node_[depth_to_anchor_node_] = offset_in_anchor_;
+ return;
+ } else {
+ // Case #1-b. This is a revese of increment()::Case#1.
+ // |child| doesn't have a previous sibling.
+ // Let |anchor| is B and |child| is E,
+ // next |anchor| is A and |child| is B.
+ node_after_position_in_anchor_ =
+ Strategy::Parent(*node_after_position_in_anchor_);
+ anchor_node_ =
+ SelectableParentOf<Strategy>(*node_after_position_in_anchor_);
+ if (!anchor_node_)
+ return;
+ offset_in_anchor_ = 0;
+ // Decrement depth and intialize if needs.
+ DCHECK_GT(depth_to_anchor_node_, 0u);
+ --depth_to_anchor_node_;
+ if (offsets_in_anchor_node_[depth_to_anchor_node_] == kInvalidOffset)
+ offsets_in_anchor_node_[depth_to_anchor_node_] =
+ Strategy::Index(*node_after_position_in_anchor_);
+ }
+ return;
+ }
+
+ if (ShouldTraverseChildren<Strategy>(*anchor_node_)) {
+ // Case #2. This is a reverse of increment()::Case3-b.
+ // Let |anchor| is B, next |anchor| is F.
+ anchor_node_ = Strategy::LastChild(*anchor_node_);
+ offset_in_anchor_ = ShouldTraverseChildren<Strategy>(*anchor_node_)
+ ? 0
+ : Strategy::LastOffsetForEditing(anchor_node_);
+ // Decrement depth initializing with -1 because
+ // |m_nodeAfterPositionInAnchor| is null so still unneeded.
+ if (depth_to_anchor_node_ >= offsets_in_anchor_node_.size())
+ offsets_in_anchor_node_.push_back(kInvalidOffset);
+ else
+ offsets_in_anchor_node_[depth_to_anchor_node_] = kInvalidOffset;
+ ++depth_to_anchor_node_;
+ return;
+ }
+ if (offset_in_anchor_ && anchor_node_->GetLayoutObject()) {
+ // Case #3-a. This is a reverse of increment()::Case#2.
+ // In this case |anchor| is a leaf(E,F,C,G or H) and
+ // |m_offsetInAnchor| is not on the beginning of |anchor|.
+ // Then just decrement |m_offsetInAnchor|.
+ offset_in_anchor_ =
+ PreviousGraphemeBoundaryOf(*anchor_node_, offset_in_anchor_);
+ return;
+ }
+ // Case #3-b. This is a reverse of increment()::Case#1.
+ // In this case |anchor| is a leaf(E,F,C,G or H) and
+ // |m_offsetInAnchor| is on the beginning of |anchor|.
+ // Let |anchor| is E,
+ // next |anchor| is B and |child| is E.
+ node_after_position_in_anchor_ = anchor_node_;
+ anchor_node_ = SelectableParentOf<Strategy>(*anchor_node_);
+ if (!anchor_node_)
+ return;
+ DCHECK_GT(depth_to_anchor_node_, 0u);
+ --depth_to_anchor_node_;
+ if (offsets_in_anchor_node_[depth_to_anchor_node_] != kInvalidOffset)
+ return;
+ offsets_in_anchor_node_[depth_to_anchor_node_] =
+ Strategy::Index(*node_after_position_in_anchor_);
+}
+
+template <typename Strategy>
+bool PositionIteratorAlgorithm<Strategy>::AtStart() const {
+ DCHECK(IsValid());
+ if (!anchor_node_)
+ return true;
+ if (Strategy::Parent(*anchor_node_))
+ return false;
+ return (!Strategy::HasChildren(*anchor_node_) && !offset_in_anchor_) ||
+ (node_after_position_in_anchor_ &&
+ !Strategy::PreviousSibling(*node_after_position_in_anchor_));
+}
+
+template <typename Strategy>
+bool PositionIteratorAlgorithm<Strategy>::AtEnd() const {
+ DCHECK(IsValid());
+ if (!anchor_node_)
+ return true;
+ if (node_after_position_in_anchor_)
+ return false;
+ return !Strategy::Parent(*anchor_node_) &&
+ (Strategy::HasChildren(*anchor_node_) ||
+ offset_in_anchor_ >= Strategy::LastOffsetForEditing(anchor_node_));
+}
+
+template <typename Strategy>
+bool PositionIteratorAlgorithm<Strategy>::AtStartOfNode() const {
+ DCHECK(IsValid());
+ if (!anchor_node_)
+ return true;
+ if (!node_after_position_in_anchor_)
+ return !Strategy::HasChildren(*anchor_node_) && !offset_in_anchor_;
+ return !Strategy::PreviousSibling(*node_after_position_in_anchor_);
+}
+
+template <typename Strategy>
+bool PositionIteratorAlgorithm<Strategy>::AtEndOfNode() const {
+ DCHECK(IsValid());
+ if (!anchor_node_)
+ return true;
+ if (node_after_position_in_anchor_)
+ return false;
+ return Strategy::HasChildren(*anchor_node_) ||
+ offset_in_anchor_ >= Strategy::LastOffsetForEditing(anchor_node_);
+}
+
+template class CORE_TEMPLATE_EXPORT PositionIteratorAlgorithm<EditingStrategy>;
+template class CORE_TEMPLATE_EXPORT
+ PositionIteratorAlgorithm<EditingInFlatTreeStrategy>;
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/position_iterator.h b/chromium/third_party/blink/renderer/core/editing/position_iterator.h
new file mode 100644
index 00000000000..820e4ad8e7e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/position_iterator.h
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_POSITION_ITERATOR_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_POSITION_ITERATOR_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/dom/node.h"
+#include "third_party/blink/renderer/core/editing/editing_strategy.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+
+namespace blink {
+
+// A Position iterator with nearly constant-time
+// increment, decrement, and several predicates on the Position it is at.
+// Conversion from Position is O(n) in the depth.
+// Conversion to Position is O(1).
+// PositionIteratorAlgorithm must be used without DOM tree change.
+template <typename Strategy>
+class CORE_TEMPLATE_CLASS_EXPORT PositionIteratorAlgorithm {
+ STACK_ALLOCATED();
+
+ public:
+ explicit PositionIteratorAlgorithm(const PositionTemplate<Strategy>&);
+ PositionIteratorAlgorithm();
+
+ // Since |deprecatedComputePosition()| is slow, new code should use
+ // |computePosition()| instead.
+ PositionTemplate<Strategy> DeprecatedComputePosition() const;
+ PositionTemplate<Strategy> ComputePosition() const;
+
+ // increment() takes O(1) other than incrementing to a element that has
+ // new parent.
+ // In the later case, it takes time of O(<number of childlen>) but the case
+ // happens at most depth-of-the-tree times over whole tree traversal.
+ void Increment();
+ // decrement() takes O(1) other than decrement into new node that has
+ // childlen.
+ // In the later case, it takes time of O(<number of childlen>).
+ void Decrement();
+
+ Node* GetNode() const { return anchor_node_; }
+ int OffsetInLeafNode() const { return offset_in_anchor_; }
+
+ bool AtStart() const;
+ bool AtEnd() const;
+ bool AtStartOfNode() const;
+ bool AtEndOfNode() const;
+
+ private:
+ PositionIteratorAlgorithm(Node* anchor_node, int offset_in_anchoror_node);
+
+ bool IsValid() const {
+ return !anchor_node_ ||
+ dom_tree_version_ == anchor_node_->GetDocument().DomTreeVersion();
+ }
+
+ Member<Node> anchor_node_;
+ // If this is non-null, Strategy::parent(*m_nodeAfterPositionInAnchor) ==
+ // m_anchorNode;
+ Member<Node> node_after_position_in_anchor_;
+ int offset_in_anchor_;
+ size_t depth_to_anchor_node_;
+ // If |m_nodeAfterPositionInAnchor| is not null,
+ // m_offsetsInAnchorNode[m_depthToAnchorNode] ==
+ // Strategy::index(m_nodeAfterPositionInAnchor).
+ Vector<int> offsets_in_anchor_node_;
+ uint64_t dom_tree_version_;
+};
+
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ PositionIteratorAlgorithm<EditingStrategy>;
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ PositionIteratorAlgorithm<EditingInFlatTreeStrategy>;
+
+using PositionIterator = PositionIteratorAlgorithm<EditingStrategy>;
+using PositionIteratorInFlatTree =
+ PositionIteratorAlgorithm<EditingInFlatTreeStrategy>;
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_POSITION_ITERATOR_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/position_iterator_test.cc b/chromium/third_party/blink/renderer/core/editing/position_iterator_test.cc
new file mode 100644
index 00000000000..5fa5a32f9a0
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/position_iterator_test.cc
@@ -0,0 +1,247 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/position_iterator.h"
+
+#include "third_party/blink/renderer/core/dom/flat_tree_traversal.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class PositionIteratorTest : public EditingTestBase {};
+
+// For http://crbug.com/695317
+TEST_F(PositionIteratorTest, decrementWithInputElement) {
+ SetBodyContent("123<input value='abc'>");
+ Element* const input = GetDocument().QuerySelector("input");
+ Node* const text = input->previousSibling();
+
+ // Decrement until start of "123" from INPUT on DOM tree
+ PositionIterator dom_iterator(
+ Position::LastPositionInNode(*GetDocument().body()));
+ EXPECT_EQ(Position::LastPositionInNode(*GetDocument().body()),
+ dom_iterator.ComputePosition());
+ dom_iterator.Decrement();
+ EXPECT_EQ(Position::AfterNode(*input), dom_iterator.ComputePosition());
+ dom_iterator.Decrement();
+ EXPECT_EQ(Position::BeforeNode(*input), dom_iterator.ComputePosition());
+ dom_iterator.Decrement();
+ EXPECT_EQ(Position(GetDocument().body(), 1), dom_iterator.ComputePosition());
+ dom_iterator.Decrement();
+ EXPECT_EQ(Position(text, 3), dom_iterator.ComputePosition());
+
+ // Decrement until start of "123" from INPUT on flat tree
+ PositionIteratorInFlatTree flat_iterator(
+ PositionInFlatTree::LastPositionInNode(*GetDocument().body()));
+ EXPECT_EQ(PositionInFlatTree::LastPositionInNode(*GetDocument().body()),
+ flat_iterator.ComputePosition());
+ flat_iterator.Decrement();
+ EXPECT_EQ(PositionInFlatTree::AfterNode(*input),
+ flat_iterator.ComputePosition());
+ flat_iterator.Decrement();
+ EXPECT_EQ(PositionInFlatTree::BeforeNode(*input),
+ flat_iterator.ComputePosition());
+ flat_iterator.Decrement();
+ EXPECT_EQ(PositionInFlatTree(GetDocument().body(), 1),
+ flat_iterator.ComputePosition());
+ flat_iterator.Decrement();
+ EXPECT_EQ(PositionInFlatTree(text, 3), flat_iterator.ComputePosition());
+}
+
+TEST_F(PositionIteratorTest, decrementWithSelectElement) {
+ SetBodyContent("123<select><option>1</option><option>2</option></select>");
+ Element* const select = GetDocument().QuerySelector("select");
+ Node* text = select->previousSibling();
+
+ // Decrement until start of "123" from SELECT on DOM tree
+ PositionIterator dom_iterator(
+ Position::LastPositionInNode(*GetDocument().body()));
+ EXPECT_EQ(Position::LastPositionInNode(*GetDocument().body()),
+ dom_iterator.ComputePosition());
+ dom_iterator.Decrement();
+ EXPECT_EQ(Position::AfterNode(*select), dom_iterator.ComputePosition());
+ dom_iterator.Decrement();
+ EXPECT_EQ(Position::AfterNode(*select), dom_iterator.ComputePosition())
+ << "This is redundant result, we should not have. see "
+ "http://crbug.com/697283";
+ dom_iterator.Decrement();
+ EXPECT_EQ(Position::BeforeNode(*select), dom_iterator.ComputePosition());
+ dom_iterator.Decrement();
+ EXPECT_EQ(Position(GetDocument().body(), 1), dom_iterator.ComputePosition());
+ dom_iterator.Decrement();
+ EXPECT_EQ(Position(text, 3), dom_iterator.ComputePosition());
+
+ // Decrement until start of "123" from SELECT on flat tree
+ PositionIteratorInFlatTree flat_iterator(
+ PositionInFlatTree::LastPositionInNode(*GetDocument().body()));
+ EXPECT_EQ(PositionInFlatTree::LastPositionInNode(*GetDocument().body()),
+ flat_iterator.ComputePosition());
+ flat_iterator.Decrement();
+ EXPECT_EQ(PositionInFlatTree::AfterNode(*select),
+ flat_iterator.ComputePosition());
+ flat_iterator.Decrement();
+ EXPECT_EQ(PositionInFlatTree::BeforeNode(*select),
+ flat_iterator.ComputePosition());
+ flat_iterator.Decrement();
+ EXPECT_EQ(PositionInFlatTree(GetDocument().body(), 1),
+ flat_iterator.ComputePosition());
+ flat_iterator.Decrement();
+ EXPECT_EQ(PositionInFlatTree(text, 3), flat_iterator.ComputePosition());
+}
+
+// For http://crbug.com/695317
+TEST_F(PositionIteratorTest, decrementWithTextAreaElement) {
+ SetBodyContent("123<textarea>456</textarea>");
+ Element* const textarea = GetDocument().QuerySelector("textarea");
+ Node* const text = textarea->previousSibling();
+
+ // Decrement until end of "123" from after TEXTAREA on DOM tree
+ PositionIterator dom_iterator(
+ Position::LastPositionInNode(*GetDocument().body()));
+ EXPECT_EQ(Position::LastPositionInNode(*GetDocument().body()),
+ dom_iterator.ComputePosition());
+ dom_iterator.Decrement();
+ EXPECT_EQ(Position::AfterNode(*textarea), dom_iterator.ComputePosition());
+ dom_iterator.Decrement();
+ EXPECT_EQ(Position::BeforeNode(*textarea), dom_iterator.ComputePosition());
+ dom_iterator.Decrement();
+ EXPECT_EQ(Position(GetDocument().body(), 1), dom_iterator.ComputePosition());
+ dom_iterator.Decrement();
+ EXPECT_EQ(Position(text, 3), dom_iterator.ComputePosition());
+
+ // Decrement until end of "123" from after TEXTAREA on flat tree
+ PositionIteratorInFlatTree flat_iterator(
+ PositionInFlatTree::LastPositionInNode(*GetDocument().body()));
+ EXPECT_EQ(PositionInFlatTree::LastPositionInNode(*GetDocument().body()),
+ flat_iterator.ComputePosition());
+ flat_iterator.Decrement();
+ EXPECT_EQ(PositionInFlatTree::AfterNode(*textarea),
+ flat_iterator.ComputePosition());
+ flat_iterator.Decrement();
+ EXPECT_EQ(PositionInFlatTree::BeforeNode(*textarea),
+ flat_iterator.ComputePosition());
+ flat_iterator.Decrement();
+ EXPECT_EQ(PositionInFlatTree(GetDocument().body(), 1),
+ flat_iterator.ComputePosition());
+ flat_iterator.Decrement();
+ EXPECT_EQ(PositionInFlatTree(text, 3), flat_iterator.ComputePosition());
+}
+
+// For http://crbug.com/695317
+TEST_F(PositionIteratorTest, incrementWithInputElement) {
+ SetBodyContent("<input value='abc'>123");
+ Element* const input = GetDocument().QuerySelector("input");
+ Node* const text = input->nextSibling();
+
+ // Increment until start of "123" from INPUT on DOM tree
+ PositionIterator dom_iterator(
+ Position::FirstPositionInNode(*GetDocument().body()));
+ EXPECT_EQ(Position(GetDocument().body(), 0), dom_iterator.ComputePosition());
+ dom_iterator.Increment();
+ EXPECT_EQ(Position::BeforeNode(*input), dom_iterator.ComputePosition());
+ dom_iterator.Increment();
+ EXPECT_EQ(Position::AfterNode(*input), dom_iterator.ComputePosition());
+ dom_iterator.Increment();
+ EXPECT_EQ(Position(GetDocument().body(), 1), dom_iterator.ComputePosition());
+ dom_iterator.Increment();
+ EXPECT_EQ(Position(text, 0), dom_iterator.ComputePosition());
+
+ // Increment until start of "123" from INPUT on flat tree
+ PositionIteratorInFlatTree flat_iterator(
+ PositionInFlatTree::FirstPositionInNode(*GetDocument().body()));
+ EXPECT_EQ(PositionInFlatTree(GetDocument().body(), 0),
+ flat_iterator.ComputePosition());
+ flat_iterator.Increment();
+ EXPECT_EQ(PositionInFlatTree::BeforeNode(*input),
+ flat_iterator.ComputePosition());
+ flat_iterator.Increment();
+ EXPECT_EQ(PositionInFlatTree::AfterNode(*input),
+ flat_iterator.ComputePosition());
+ flat_iterator.Increment();
+ EXPECT_EQ(PositionInFlatTree(GetDocument().body(), 1),
+ flat_iterator.ComputePosition());
+ flat_iterator.Increment();
+ EXPECT_EQ(PositionInFlatTree(text, 0), flat_iterator.ComputePosition());
+}
+
+TEST_F(PositionIteratorTest, incrementWithSelectElement) {
+ SetBodyContent("<select><option>1</option><option>2</option></select>123");
+ Element* const select = GetDocument().QuerySelector("select");
+ Node* const text = select->nextSibling();
+
+ // Increment until start of "123" from SELECT on DOM tree
+ PositionIterator dom_iterator(
+ Position::FirstPositionInNode(*GetDocument().body()));
+ EXPECT_EQ(Position(GetDocument().body(), 0), dom_iterator.ComputePosition());
+ dom_iterator.Increment();
+ EXPECT_EQ(Position::BeforeNode(*select), dom_iterator.ComputePosition());
+ dom_iterator.Increment();
+ EXPECT_EQ(Position::AfterNode(*select), dom_iterator.ComputePosition());
+ dom_iterator.Increment();
+ EXPECT_EQ(Position::AfterNode(*select), dom_iterator.ComputePosition())
+ << "This is redundant result, we should not have. see "
+ "http://crbug.com/697283";
+ dom_iterator.Increment();
+ EXPECT_EQ(Position(GetDocument().body(), 1), dom_iterator.ComputePosition());
+ dom_iterator.Increment();
+ EXPECT_EQ(Position(text, 0), dom_iterator.ComputePosition());
+
+ // Increment until start of "123" from SELECT on flat tree
+ PositionIteratorInFlatTree flat_iterator(
+ PositionInFlatTree::FirstPositionInNode(*GetDocument().body()));
+ EXPECT_EQ(PositionInFlatTree(GetDocument().body(), 0),
+ flat_iterator.ComputePosition());
+ flat_iterator.Increment();
+ EXPECT_EQ(PositionInFlatTree::BeforeNode(*select),
+ flat_iterator.ComputePosition());
+ flat_iterator.Increment();
+ EXPECT_EQ(PositionInFlatTree::AfterNode(*select),
+ flat_iterator.ComputePosition());
+ flat_iterator.Increment();
+ EXPECT_EQ(PositionInFlatTree(GetDocument().body(), 1),
+ flat_iterator.ComputePosition());
+ flat_iterator.Increment();
+ EXPECT_EQ(PositionInFlatTree(text, 0), flat_iterator.ComputePosition());
+}
+
+// For http://crbug.com/695317
+TEST_F(PositionIteratorTest, incrementWithTextAreaElement) {
+ SetBodyContent("<textarea>123</textarea>456");
+ Element* const textarea = GetDocument().QuerySelector("textarea");
+ Node* const text = textarea->nextSibling();
+
+ // Increment until start of "123" from TEXTAREA on DOM tree
+ PositionIterator dom_iterator(
+ Position::FirstPositionInNode(*GetDocument().body()));
+ EXPECT_EQ(Position(GetDocument().body(), 0), dom_iterator.ComputePosition());
+ dom_iterator.Increment();
+ EXPECT_EQ(Position::BeforeNode(*textarea), dom_iterator.ComputePosition());
+ dom_iterator.Increment();
+ EXPECT_EQ(Position::AfterNode(*textarea), dom_iterator.ComputePosition());
+ dom_iterator.Increment();
+ EXPECT_EQ(Position(GetDocument().body(), 1), dom_iterator.ComputePosition());
+ dom_iterator.Increment();
+ EXPECT_EQ(Position(text, 0), dom_iterator.ComputePosition());
+
+ // Increment until start of "123" from TEXTAREA on flat tree
+ PositionIteratorInFlatTree flat_iterator(
+ PositionInFlatTree::FirstPositionInNode(*GetDocument().body()));
+ EXPECT_EQ(PositionInFlatTree(GetDocument().body(), 0),
+ flat_iterator.ComputePosition());
+ // TODO(yosin): We should not traverse inside TEXTAREA
+ flat_iterator.Increment();
+ EXPECT_EQ(PositionInFlatTree::BeforeNode(*textarea),
+ flat_iterator.ComputePosition());
+ flat_iterator.Increment();
+ EXPECT_EQ(PositionInFlatTree::AfterNode(*textarea),
+ flat_iterator.ComputePosition());
+ flat_iterator.Increment();
+ EXPECT_EQ(PositionInFlatTree(GetDocument().body(), 1),
+ flat_iterator.ComputePosition());
+ flat_iterator.Increment();
+ EXPECT_EQ(PositionInFlatTree(text, 0), flat_iterator.ComputePosition());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/position_test.cc b/chromium/third_party/blink/renderer/core/editing/position_test.cc
new file mode 100644
index 00000000000..f166b60116a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/position_test.cc
@@ -0,0 +1,259 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/position.h"
+
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class PositionTest : public EditingTestBase {};
+
+TEST_F(PositionTest, IsEquivalent) {
+ SetBodyContent("<a id=sample>0<b>1</b>2</a>");
+
+ Element* sample = GetDocument().getElementById("sample");
+
+ EXPECT_TRUE(Position(sample, 0).IsEquivalent(Position(sample, 0)));
+
+ EXPECT_TRUE(
+ Position(sample, 0).IsEquivalent(Position::FirstPositionInNode(*sample)));
+ EXPECT_TRUE(Position(sample, 0).IsEquivalent(
+ Position::BeforeNode(*sample->firstChild())));
+ EXPECT_TRUE(Position(sample, 1).IsEquivalent(
+ Position::AfterNode(*sample->firstChild())));
+ EXPECT_TRUE(Position(sample, 1).IsEquivalent(
+ Position::BeforeNode(*sample->firstChild()->nextSibling())));
+ EXPECT_TRUE(Position(sample, 2).IsEquivalent(
+ Position::BeforeNode(*sample->lastChild())));
+ EXPECT_TRUE(Position(sample, 3).IsEquivalent(
+ Position::AfterNode(*sample->lastChild())));
+ EXPECT_TRUE(
+ Position(sample, 3).IsEquivalent(Position::LastPositionInNode(*sample)));
+
+ EXPECT_FALSE(Position(sample, 0).IsEquivalent(Position(sample, 1)));
+ EXPECT_FALSE(
+ Position(sample, 0).IsEquivalent(Position::LastPositionInNode(*sample)));
+}
+
+TEST_F(PositionTest, NodeAsRangeLastNodeNull) {
+ EXPECT_EQ(nullptr, Position().NodeAsRangeLastNode());
+ EXPECT_EQ(nullptr, PositionInFlatTree().NodeAsRangeLastNode());
+}
+
+TEST_F(PositionTest, editingPositionOfWithEditingIgnoresContent) {
+ const char* body_content =
+ "<textarea id=textarea></textarea><a id=child1>1</a><b id=child2>2</b>";
+ SetBodyContent(body_content);
+ Node* textarea = GetDocument().getElementById("textarea");
+
+ EXPECT_EQ(Position::BeforeNode(*textarea),
+ Position::EditingPositionOf(textarea, 0));
+ EXPECT_EQ(Position::AfterNode(*textarea),
+ Position::EditingPositionOf(textarea, 1));
+ EXPECT_EQ(Position::AfterNode(*textarea),
+ Position::EditingPositionOf(textarea, 2));
+
+ // Change DOM tree to
+ // <textarea id=textarea><a id=child1>1</a><b id=child2>2</b></textarea>
+ Node* child1 = GetDocument().getElementById("child1");
+ Node* child2 = GetDocument().getElementById("child2");
+ textarea->appendChild(child1);
+ textarea->appendChild(child2);
+
+ EXPECT_EQ(Position::BeforeNode(*textarea),
+ Position::EditingPositionOf(textarea, 0));
+ EXPECT_EQ(Position::AfterNode(*textarea),
+ Position::EditingPositionOf(textarea, 1));
+ EXPECT_EQ(Position::AfterNode(*textarea),
+ Position::EditingPositionOf(textarea, 2));
+ EXPECT_EQ(Position::AfterNode(*textarea),
+ Position::EditingPositionOf(textarea, 3));
+}
+
+TEST_F(PositionTest, NodeAsRangeLastNode) {
+ const char* body_content =
+ "<p id='p1'>11</p><p id='p2'></p><p id='p3'>33</p>";
+ SetBodyContent(body_content);
+ Node* p1 = GetDocument().getElementById("p1");
+ Node* p2 = GetDocument().getElementById("p2");
+ Node* p3 = GetDocument().getElementById("p3");
+ Node* body = EditingStrategy::Parent(*p1);
+ Node* t1 = EditingStrategy::FirstChild(*p1);
+ Node* t3 = EditingStrategy::FirstChild(*p3);
+
+ EXPECT_EQ(body, Position::InParentBeforeNode(*p1).NodeAsRangeLastNode());
+ EXPECT_EQ(t1, Position::InParentBeforeNode(*p2).NodeAsRangeLastNode());
+ EXPECT_EQ(p2, Position::InParentBeforeNode(*p3).NodeAsRangeLastNode());
+ EXPECT_EQ(t1, Position::InParentAfterNode(*p1).NodeAsRangeLastNode());
+ EXPECT_EQ(p2, Position::InParentAfterNode(*p2).NodeAsRangeLastNode());
+ EXPECT_EQ(t3, Position::InParentAfterNode(*p3).NodeAsRangeLastNode());
+ EXPECT_EQ(t3, Position::AfterNode(*p3).NodeAsRangeLastNode());
+
+ EXPECT_EQ(body,
+ PositionInFlatTree::InParentBeforeNode(*p1).NodeAsRangeLastNode());
+ EXPECT_EQ(t1,
+ PositionInFlatTree::InParentBeforeNode(*p2).NodeAsRangeLastNode());
+ EXPECT_EQ(p2,
+ PositionInFlatTree::InParentBeforeNode(*p3).NodeAsRangeLastNode());
+ EXPECT_EQ(t1,
+ PositionInFlatTree::InParentAfterNode(*p1).NodeAsRangeLastNode());
+ EXPECT_EQ(p2,
+ PositionInFlatTree::InParentAfterNode(*p2).NodeAsRangeLastNode());
+ EXPECT_EQ(t3,
+ PositionInFlatTree::InParentAfterNode(*p3).NodeAsRangeLastNode());
+ EXPECT_EQ(t3, PositionInFlatTree::AfterNode(*p3).NodeAsRangeLastNode());
+}
+
+TEST_F(PositionTest, NodeAsRangeLastNodeShadow) {
+ const char* body_content =
+ "<p id='host'>00<b id='one'>11</b><b id='two'>22</b>33</p>";
+ const char* shadow_content =
+ "<a id='a'><content select=#two></content><content "
+ "select=#one></content></a>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* host = GetDocument().getElementById("host");
+ Node* n1 = GetDocument().getElementById("one");
+ Node* n2 = GetDocument().getElementById("two");
+ Node* t0 = EditingStrategy::FirstChild(*host);
+ Node* t1 = EditingStrategy::FirstChild(*n1);
+ Node* t2 = EditingStrategy::FirstChild(*n2);
+ Node* t3 = EditingStrategy::LastChild(*host);
+ Node* a = shadow_root->getElementById("a");
+
+ EXPECT_EQ(t0, Position::InParentBeforeNode(*n1).NodeAsRangeLastNode());
+ EXPECT_EQ(t1, Position::InParentBeforeNode(*n2).NodeAsRangeLastNode());
+ EXPECT_EQ(t1, Position::InParentAfterNode(*n1).NodeAsRangeLastNode());
+ EXPECT_EQ(t2, Position::InParentAfterNode(*n2).NodeAsRangeLastNode());
+ EXPECT_EQ(t3, Position::AfterNode(*host).NodeAsRangeLastNode());
+
+ EXPECT_EQ(t2,
+ PositionInFlatTree::InParentBeforeNode(*n1).NodeAsRangeLastNode());
+ EXPECT_EQ(a,
+ PositionInFlatTree::InParentBeforeNode(*n2).NodeAsRangeLastNode());
+ EXPECT_EQ(t1,
+ PositionInFlatTree::InParentAfterNode(*n1).NodeAsRangeLastNode());
+ EXPECT_EQ(t2,
+ PositionInFlatTree::InParentAfterNode(*n2).NodeAsRangeLastNode());
+ EXPECT_EQ(t1, PositionInFlatTree::AfterNode(*host).NodeAsRangeLastNode());
+}
+
+TEST_F(PositionTest, OperatorBool) {
+ SetBodyContent("foo");
+ EXPECT_FALSE(static_cast<bool>(Position()));
+ EXPECT_TRUE(static_cast<bool>(Position(GetDocument().body(), 0)));
+}
+
+TEST_F(PositionTest, ToPositionInFlatTreeWithActiveInsertionPoint) {
+ const char* body_content = "<p id='host'>00<b id='one'>11</b>22</p>";
+ const char* shadow_content =
+ "<a id='a'><content select=#one "
+ "id='content'></content><content></content></a>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+ Element* anchor = shadow_root->getElementById("a");
+
+ EXPECT_EQ(PositionInFlatTree(anchor, 0),
+ ToPositionInFlatTree(Position(anchor, 0)));
+ EXPECT_EQ(PositionInFlatTree(anchor, 1),
+ ToPositionInFlatTree(Position(anchor, 1)));
+ EXPECT_EQ(PositionInFlatTree(anchor, PositionAnchorType::kAfterChildren),
+ ToPositionInFlatTree(Position(anchor, 2)));
+}
+
+TEST_F(PositionTest, ToPositionInFlatTreeWithInactiveInsertionPoint) {
+ const char* body_content = "<p id='p'><content></content></p>";
+ SetBodyContent(body_content);
+ Element* anchor = GetDocument().getElementById("p");
+
+ EXPECT_EQ(PositionInFlatTree(anchor, 0),
+ ToPositionInFlatTree(Position(anchor, 0)));
+ EXPECT_EQ(PositionInFlatTree(anchor, PositionAnchorType::kAfterChildren),
+ ToPositionInFlatTree(Position(anchor, 1)));
+}
+
+// This test comes from "editing/style/block-style-progress-crash.html".
+TEST_F(PositionTest, ToPositionInFlatTreeWithNotDistributed) {
+ SetBodyContent("<progress id=sample>foo</progress>");
+ Element* sample = GetDocument().getElementById("sample");
+
+ EXPECT_EQ(PositionInFlatTree(sample, PositionAnchorType::kAfterChildren),
+ ToPositionInFlatTree(Position(sample, 0)));
+}
+
+TEST_F(PositionTest, ToPositionInFlatTreeWithShadowRoot) {
+ const char* body_content = "<p id='host'>00<b id='one'>11</b>22</p>";
+ const char* shadow_content = "<a><content select=#one></content></a>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+ Element* host = GetDocument().getElementById("host");
+
+ EXPECT_EQ(PositionInFlatTree(host, 0),
+ ToPositionInFlatTree(Position(shadow_root, 0)));
+ EXPECT_EQ(PositionInFlatTree(host, PositionAnchorType::kAfterChildren),
+ ToPositionInFlatTree(Position(shadow_root, 1)));
+ EXPECT_EQ(PositionInFlatTree(host, PositionAnchorType::kAfterChildren),
+ ToPositionInFlatTree(
+ Position(shadow_root, PositionAnchorType::kAfterChildren)));
+ EXPECT_EQ(PositionInFlatTree(host, PositionAnchorType::kBeforeChildren),
+ ToPositionInFlatTree(
+ Position(shadow_root, PositionAnchorType::kBeforeChildren)));
+ EXPECT_EQ(PositionInFlatTree(host, PositionAnchorType::kAfterAnchor),
+ ToPositionInFlatTree(
+ Position(shadow_root, PositionAnchorType::kAfterAnchor)));
+ EXPECT_EQ(PositionInFlatTree(host, PositionAnchorType::kBeforeAnchor),
+ ToPositionInFlatTree(
+ Position(shadow_root, PositionAnchorType::kBeforeAnchor)));
+}
+
+TEST_F(PositionTest,
+ ToPositionInFlatTreeWithShadowRootContainingSingleContent) {
+ const char* body_content = "<p id='host'>00<b id='one'>11</b>22</p>";
+ const char* shadow_content = "<content select=#one></content>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+ Element* host = GetDocument().getElementById("host");
+
+ EXPECT_EQ(PositionInFlatTree(host, 0),
+ ToPositionInFlatTree(Position(shadow_root, 0)));
+ EXPECT_EQ(PositionInFlatTree(host, PositionAnchorType::kAfterChildren),
+ ToPositionInFlatTree(Position(shadow_root, 1)));
+}
+
+TEST_F(PositionTest, ToPositionInFlatTreeWithEmptyShadowRoot) {
+ const char* body_content = "<p id='host'>00<b id='one'>11</b>22</p>";
+ const char* shadow_content = "";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+ Element* host = GetDocument().getElementById("host");
+
+ EXPECT_EQ(PositionInFlatTree(host, PositionAnchorType::kAfterChildren),
+ ToPositionInFlatTree(Position(shadow_root, 0)));
+}
+
+TEST_F(PositionTest, NullPositionNotConnected) {
+ EXPECT_FALSE(Position().IsConnected());
+ EXPECT_FALSE(PositionInFlatTree().IsConnected());
+}
+
+TEST_F(PositionTest, IsConnectedBasic) {
+ Position position = SetCaretTextToBody("<div>f|oo</div>");
+ EXPECT_TRUE(position.IsConnected());
+ EXPECT_TRUE(ToPositionInFlatTree(position).IsConnected());
+
+ position.AnchorNode()->remove();
+ EXPECT_FALSE(position.IsConnected());
+ EXPECT_FALSE(ToPositionInFlatTree(position).IsConnected());
+}
+
+TEST_F(PositionTest, IsConnectedInFlatTree) {
+ Position position = SetCaretTextToBody(
+ "<div>f|oo<template data-mode=open>bar</template></div>");
+ EXPECT_TRUE(position.IsConnected());
+ EXPECT_FALSE(ToPositionInFlatTree(position).IsConnected());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/position_with_affinity.cc b/chromium/third_party/blink/renderer/core/editing/position_with_affinity.cc
new file mode 100644
index 00000000000..d6e0da76088
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/position_with_affinity.cc
@@ -0,0 +1,54 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file./*
+
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+
+namespace blink {
+
+template <typename Strategy>
+PositionWithAffinityTemplate<Strategy>::PositionWithAffinityTemplate(
+ const PositionTemplate<Strategy>& position,
+ TextAffinity affinity)
+ : position_(position), affinity_(affinity) {}
+
+template <typename Strategy>
+PositionWithAffinityTemplate<Strategy>::PositionWithAffinityTemplate()
+ : affinity_(TextAffinity::kDownstream) {}
+
+template <typename Strategy>
+PositionWithAffinityTemplate<Strategy>::~PositionWithAffinityTemplate() =
+ default;
+
+template <typename Strategy>
+void PositionWithAffinityTemplate<Strategy>::Trace(blink::Visitor* visitor) {
+ visitor->Trace(position_);
+}
+
+template <typename Strategy>
+bool PositionWithAffinityTemplate<Strategy>::operator==(
+ const PositionWithAffinityTemplate& other) const {
+ if (IsNull())
+ return other.IsNull();
+ return affinity_ == other.affinity_ && position_ == other.position_;
+}
+
+template class CORE_TEMPLATE_EXPORT
+ PositionWithAffinityTemplate<EditingStrategy>;
+template class CORE_TEMPLATE_EXPORT
+ PositionWithAffinityTemplate<EditingInFlatTreeStrategy>;
+
+std::ostream& operator<<(std::ostream& ostream,
+ const PositionWithAffinity& position_with_affinity) {
+ return ostream << position_with_affinity.GetPosition() << '/'
+ << position_with_affinity.Affinity();
+}
+
+std::ostream& operator<<(
+ std::ostream& ostream,
+ const PositionInFlatTreeWithAffinity& position_with_affinity) {
+ return ostream << position_with_affinity.GetPosition() << '/'
+ << position_with_affinity.Affinity();
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/position_with_affinity.h b/chromium/third_party/blink/renderer/core/editing/position_with_affinity.h
new file mode 100644
index 00000000000..876dd9321a9
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/position_with_affinity.h
@@ -0,0 +1,94 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file./*
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_POSITION_WITH_AFFINITY_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_POSITION_WITH_AFFINITY_H_
+
+#include <iosfwd>
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/text_affinity.h"
+
+namespace blink {
+
+template <typename Strategy>
+class CORE_TEMPLATE_CLASS_EXPORT PositionWithAffinityTemplate {
+ DISALLOW_NEW();
+
+ public:
+ // TODO(yosin) We should have single parameter constructor not to use
+ // default parameter for avoiding include "TextAffinity.h"
+ explicit PositionWithAffinityTemplate(
+ const PositionTemplate<Strategy>&,
+ TextAffinity = TextAffinity::kDownstream);
+ PositionWithAffinityTemplate();
+ ~PositionWithAffinityTemplate();
+
+ explicit operator bool() const { return IsNotNull(); }
+
+ TextAffinity Affinity() const { return affinity_; }
+ const PositionTemplate<Strategy>& GetPosition() const { return position_; }
+
+ // Returns true if both |this| and |other| is null or both |m_position|
+ // and |m_affinity| equal.
+ bool operator==(const PositionWithAffinityTemplate& other) const;
+ bool operator!=(const PositionWithAffinityTemplate& other) const {
+ return !operator==(other);
+ }
+
+ bool IsValidFor(const Document& document) const {
+ return position_.IsValidFor(document);
+ }
+
+ bool IsNotNull() const { return position_.IsNotNull(); }
+ bool IsNull() const { return position_.IsNull(); }
+ bool IsOrphan() const { return position_.IsOrphan(); }
+ bool IsConnected() const { return position_.IsConnected(); }
+
+ Node* AnchorNode() const { return position_.AnchorNode(); }
+ Document* GetDocument() const { return position_.GetDocument(); }
+
+ void Trace(blink::Visitor*);
+
+ private:
+ PositionTemplate<Strategy> position_;
+ TextAffinity affinity_;
+};
+
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ PositionWithAffinityTemplate<EditingStrategy>;
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ PositionWithAffinityTemplate<EditingInFlatTreeStrategy>;
+
+using PositionWithAffinity = PositionWithAffinityTemplate<EditingStrategy>;
+using PositionInFlatTreeWithAffinity =
+ PositionWithAffinityTemplate<EditingInFlatTreeStrategy>;
+
+template <typename Strategy>
+PositionWithAffinityTemplate<Strategy> FromPositionInDOMTree(
+ const PositionWithAffinity&);
+
+template <>
+inline PositionWithAffinity FromPositionInDOMTree<EditingStrategy>(
+ const PositionWithAffinity& position_with_affinity) {
+ return position_with_affinity;
+}
+
+template <>
+inline PositionInFlatTreeWithAffinity
+FromPositionInDOMTree<EditingInFlatTreeStrategy>(
+ const PositionWithAffinity& position_with_affinity) {
+ return PositionInFlatTreeWithAffinity(
+ ToPositionInFlatTree(position_with_affinity.GetPosition()),
+ position_with_affinity.Affinity());
+}
+
+CORE_EXPORT std::ostream& operator<<(std::ostream&,
+ const PositionWithAffinity&);
+CORE_EXPORT std::ostream& operator<<(std::ostream&,
+ const PositionInFlatTreeWithAffinity&);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_POSITION_WITH_AFFINITY_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/position_with_affinity_test.cc b/chromium/third_party/blink/renderer/core/editing/position_with_affinity_test.cc
new file mode 100644
index 00000000000..6919f9216aa
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/position_with_affinity_test.cc
@@ -0,0 +1,20 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class PositionWithAffinityTest : public EditingTestBase {};
+
+TEST_F(PositionWithAffinityTest, OperatorBool) {
+ SetBodyContent("foo");
+ EXPECT_FALSE(static_cast<bool>(PositionWithAffinity()));
+ EXPECT_TRUE(static_cast<bool>(
+ PositionWithAffinity(Position(GetDocument().body(), 0))));
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/relocatable_position.cc b/chromium/third_party/blink/renderer/core/editing/relocatable_position.cc
new file mode 100644
index 00000000000..46b4d82907a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/relocatable_position.cc
@@ -0,0 +1,27 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/relocatable_position.h"
+
+namespace blink {
+
+RelocatablePosition::RelocatablePosition(const Position& position)
+ : range_(position.IsNotNull()
+ ? Range::Create(*position.GetDocument(), position, position)
+ : nullptr) {}
+
+RelocatablePosition::~RelocatablePosition() {
+ if (!range_)
+ return;
+ range_->Dispose();
+}
+
+Position RelocatablePosition::GetPosition() const {
+ if (!range_)
+ return Position();
+ DCHECK(range_->collapsed());
+ return range_->StartPosition();
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/relocatable_position.h b/chromium/third_party/blink/renderer/core/editing/relocatable_position.h
new file mode 100644
index 00000000000..796c64745db
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/relocatable_position.h
@@ -0,0 +1,35 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_RELOCATABLE_POSITION_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_RELOCATABLE_POSITION_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+
+namespace blink {
+
+// |RelocatablePosition| is a helper class for keeping track of a |Position| in
+// a document upon DOM tree changes even if the given |Position|'s original
+// anchor node is moved out of document. The class is implemented by using a
+// temporary |Range| object to keep track of the |Position|, and disposing the
+// |Range| when out of scope.
+class CORE_EXPORT RelocatablePosition final {
+ STACK_ALLOCATED();
+
+ public:
+ explicit RelocatablePosition(const Position&);
+ ~RelocatablePosition();
+
+ Position GetPosition() const;
+
+ private:
+ const Member<Range> range_;
+
+ DISALLOW_COPY_AND_ASSIGN(RelocatablePosition);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_RELOCATABLE_POSITION_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/relocatable_position_test.cc b/chromium/third_party/blink/renderer/core/editing/relocatable_position_test.cc
new file mode 100644
index 00000000000..f72c2016c6a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/relocatable_position_test.cc
@@ -0,0 +1,33 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/relocatable_position.h"
+
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+
+namespace blink {
+
+class RelocatablePositionTest : public EditingTestBase {};
+
+TEST_F(RelocatablePositionTest, position) {
+ SetBodyContent("<b>foo</b><textarea>bar</textarea>");
+ Node* boldface = GetDocument().QuerySelector("b");
+ Node* textarea = GetDocument().QuerySelector("textarea");
+
+ RelocatablePosition relocatable_position(
+ Position(textarea, PositionAnchorType::kBeforeAnchor));
+ textarea->remove();
+ GetDocument().UpdateStyleAndLayout();
+
+ // RelocatablePosition should track the given Position even if its original
+ // anchor node is moved away from the document.
+ Position expected_position(boldface, PositionAnchorType::kAfterAnchor);
+ Position tracked_position = relocatable_position.GetPosition();
+ EXPECT_TRUE(tracked_position.AnchorNode()->isConnected());
+ EXPECT_EQ(CreateVisiblePosition(expected_position).DeepEquivalent(),
+ CreateVisiblePosition(tracked_position).DeepEquivalent());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/rendered_position.cc b/chromium/third_party/blink/renderer/core/editing/rendered_position.cc
new file mode 100644
index 00000000000..1270b5e69e8
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/rendered_position.cc
@@ -0,0 +1,451 @@
+/*
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/rendered_position.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/inline_box_position.h"
+#include "third_party/blink/renderer/core/editing/inline_box_traversal.h"
+#include "third_party/blink/renderer/core/editing/local_caret_rect.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/text_affinity.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+#include "third_party/blink/renderer/core/layout/api/line_layout_api_shim.h"
+#include "third_party/blink/renderer/core/paint/compositing/composited_selection.h"
+#include "third_party/blink/renderer/core/paint/paint_layer.h"
+
+namespace blink {
+
+template <typename Strategy>
+static inline LayoutObject* LayoutObjectFromPosition(
+ const PositionTemplate<Strategy>& position) {
+ DCHECK(position.IsNotNull());
+ Node* layout_object_node = nullptr;
+ switch (position.AnchorType()) {
+ case PositionAnchorType::kOffsetInAnchor:
+ layout_object_node = position.ComputeNodeAfterPosition();
+ if (!layout_object_node || !layout_object_node->GetLayoutObject())
+ layout_object_node = position.AnchorNode()->lastChild();
+ break;
+
+ case PositionAnchorType::kBeforeAnchor:
+ case PositionAnchorType::kAfterAnchor:
+ break;
+
+ case PositionAnchorType::kBeforeChildren:
+ layout_object_node = Strategy::FirstChild(*position.AnchorNode());
+ break;
+ case PositionAnchorType::kAfterChildren:
+ layout_object_node = Strategy::LastChild(*position.AnchorNode());
+ break;
+ }
+ if (!layout_object_node || !layout_object_node->GetLayoutObject())
+ layout_object_node = position.AnchorNode();
+ return layout_object_node->GetLayoutObject();
+}
+
+RenderedPosition::RenderedPosition(const VisiblePosition& position)
+ : RenderedPosition(position.DeepEquivalent(), position.Affinity()) {}
+
+RenderedPosition::RenderedPosition(const VisiblePositionInFlatTree& position)
+ : RenderedPosition(position.DeepEquivalent(), position.Affinity()) {}
+
+// TODO(editing-dev): Stop duplicating code in the two constructors
+
+RenderedPosition::RenderedPosition(const Position& position,
+ TextAffinity affinity)
+ : layout_object_(nullptr), inline_box_(nullptr), offset_(0) {
+ if (position.IsNull())
+ return;
+ InlineBoxPosition box_position =
+ ComputeInlineBoxPosition(PositionWithAffinity(position, affinity));
+ inline_box_ = box_position.inline_box;
+ offset_ = box_position.offset_in_box;
+ if (inline_box_)
+ layout_object_ =
+ LineLayoutAPIShim::LayoutObjectFrom(inline_box_->GetLineLayoutItem());
+ else
+ layout_object_ = LayoutObjectFromPosition(position);
+}
+
+RenderedPosition::RenderedPosition(const PositionInFlatTree& position,
+ TextAffinity affinity)
+ : layout_object_(nullptr), inline_box_(nullptr), offset_(0) {
+ if (position.IsNull())
+ return;
+ InlineBoxPosition box_position = ComputeInlineBoxPosition(
+ PositionInFlatTreeWithAffinity(position, affinity));
+ inline_box_ = box_position.inline_box;
+ offset_ = box_position.offset_in_box;
+ if (inline_box_)
+ layout_object_ =
+ LineLayoutAPIShim::LayoutObjectFrom(inline_box_->GetLineLayoutItem());
+ else
+ layout_object_ = LayoutObjectFromPosition(position);
+}
+
+const InlineBox* RenderedPosition::PrevLeafChild() const {
+ if (!prev_leaf_child_.has_value())
+ prev_leaf_child_ = inline_box_->PrevLeafChildIgnoringLineBreak();
+ return prev_leaf_child_.value();
+}
+
+const InlineBox* RenderedPosition::NextLeafChild() const {
+ if (!next_leaf_child_.has_value())
+ next_leaf_child_ = inline_box_->NextLeafChildIgnoringLineBreak();
+ return next_leaf_child_.value();
+}
+
+bool RenderedPosition::IsEquivalent(const RenderedPosition& other) const {
+ return (layout_object_ == other.layout_object_ &&
+ inline_box_ == other.inline_box_ && offset_ == other.offset_) ||
+ (AtLeftmostOffsetInBox() && other.AtRightmostOffsetInBox() &&
+ PrevLeafChild() == other.inline_box_) ||
+ (AtRightmostOffsetInBox() && other.AtLeftmostOffsetInBox() &&
+ NextLeafChild() == other.inline_box_);
+}
+
+unsigned char RenderedPosition::BidiLevelOnLeft() const {
+ const InlineBox* box =
+ AtLeftmostOffsetInBox() ? PrevLeafChild() : inline_box_;
+ return box ? box->BidiLevel() : 0;
+}
+
+unsigned char RenderedPosition::BidiLevelOnRight() const {
+ const InlineBox* box =
+ AtRightmostOffsetInBox() ? NextLeafChild() : inline_box_;
+ return box ? box->BidiLevel() : 0;
+}
+
+RenderedPosition RenderedPosition::LeftBoundaryOfBidiRun(
+ unsigned char bidi_level_of_run) {
+ if (!inline_box_ || bidi_level_of_run > inline_box_->BidiLevel())
+ return RenderedPosition();
+
+ const InlineBox* const box =
+ InlineBoxTraversal::FindLeftBoundaryOfEntireBidiRunIgnoringLineBreak(
+ *inline_box_, bidi_level_of_run);
+ return RenderedPosition(
+ LineLayoutAPIShim::LayoutObjectFrom(box->GetLineLayoutItem()), box,
+ box->CaretLeftmostOffset());
+}
+
+RenderedPosition RenderedPosition::RightBoundaryOfBidiRun(
+ unsigned char bidi_level_of_run) {
+ if (!inline_box_ || bidi_level_of_run > inline_box_->BidiLevel())
+ return RenderedPosition();
+
+ const InlineBox* const box =
+ InlineBoxTraversal::FindRightBoundaryOfEntireBidiRunIgnoringLineBreak(
+ *inline_box_, bidi_level_of_run);
+ return RenderedPosition(
+ LineLayoutAPIShim::LayoutObjectFrom(box->GetLineLayoutItem()), box,
+ box->CaretRightmostOffset());
+}
+
+bool RenderedPosition::AtLeftBoundaryOfBidiRun(
+ ShouldMatchBidiLevel should_match_bidi_level,
+ unsigned char bidi_level_of_run) const {
+ if (!inline_box_)
+ return false;
+
+ if (AtLeftmostOffsetInBox()) {
+ if (should_match_bidi_level == kIgnoreBidiLevel)
+ return !PrevLeafChild() ||
+ PrevLeafChild()->BidiLevel() < inline_box_->BidiLevel();
+ return inline_box_->BidiLevel() >= bidi_level_of_run &&
+ (!PrevLeafChild() ||
+ PrevLeafChild()->BidiLevel() < bidi_level_of_run);
+ }
+
+ if (AtRightmostOffsetInBox()) {
+ if (should_match_bidi_level == kIgnoreBidiLevel)
+ return NextLeafChild() &&
+ inline_box_->BidiLevel() < NextLeafChild()->BidiLevel();
+ return NextLeafChild() && inline_box_->BidiLevel() < bidi_level_of_run &&
+ NextLeafChild()->BidiLevel() >= bidi_level_of_run;
+ }
+
+ return false;
+}
+
+bool RenderedPosition::AtRightBoundaryOfBidiRun(
+ ShouldMatchBidiLevel should_match_bidi_level,
+ unsigned char bidi_level_of_run) const {
+ if (!inline_box_)
+ return false;
+
+ if (AtRightmostOffsetInBox()) {
+ if (should_match_bidi_level == kIgnoreBidiLevel)
+ return !NextLeafChild() ||
+ NextLeafChild()->BidiLevel() < inline_box_->BidiLevel();
+ return inline_box_->BidiLevel() >= bidi_level_of_run &&
+ (!NextLeafChild() ||
+ NextLeafChild()->BidiLevel() < bidi_level_of_run);
+ }
+
+ if (AtLeftmostOffsetInBox()) {
+ if (should_match_bidi_level == kIgnoreBidiLevel)
+ return PrevLeafChild() &&
+ inline_box_->BidiLevel() < PrevLeafChild()->BidiLevel();
+ return PrevLeafChild() && inline_box_->BidiLevel() < bidi_level_of_run &&
+ PrevLeafChild()->BidiLevel() >= bidi_level_of_run;
+ }
+
+ return false;
+}
+
+Position RenderedPosition::PositionAtLeftBoundaryOfBiDiRun() const {
+ DCHECK(AtLeftBoundaryOfBidiRun());
+
+ if (AtLeftmostOffsetInBox())
+ return Position::EditingPositionOf(layout_object_->GetNode(), offset_);
+
+ return Position::EditingPositionOf(
+ NextLeafChild()->GetLineLayoutItem().GetNode(),
+ NextLeafChild()->CaretLeftmostOffset());
+}
+
+Position RenderedPosition::PositionAtRightBoundaryOfBiDiRun() const {
+ DCHECK(AtRightBoundaryOfBidiRun());
+
+ if (AtRightmostOffsetInBox())
+ return Position::EditingPositionOf(layout_object_->GetNode(), offset_);
+
+ return Position::EditingPositionOf(
+ PrevLeafChild()->GetLineLayoutItem().GetNode(),
+ PrevLeafChild()->CaretRightmostOffset());
+}
+
+// Note: If the layout object has a scrolling contents layer, the selection
+// will be relative to that.
+static GraphicsLayer* GetGraphicsLayerBacking(
+ const LayoutObject& layout_object) {
+ const LayoutBoxModelObject& paint_invalidation_container =
+ layout_object.ContainerForPaintInvalidation();
+ DCHECK(paint_invalidation_container.Layer());
+ if (paint_invalidation_container.Layer()->GetCompositingState() ==
+ kNotComposited)
+ return nullptr;
+ return paint_invalidation_container.Layer()->GraphicsLayerBacking(
+ &layout_object);
+}
+
+// Convert a local point into the coordinate system of backing coordinates.
+static FloatPoint LocalToInvalidationBackingPoint(
+ const LayoutPoint& local_point,
+ const LayoutObject& layout_object) {
+ const LayoutBoxModelObject& paint_invalidation_container =
+ layout_object.ContainerForPaintInvalidation();
+ DCHECK(paint_invalidation_container.Layer());
+
+ FloatPoint container_point = layout_object.LocalToAncestorPoint(
+ FloatPoint(local_point), &paint_invalidation_container,
+ kTraverseDocumentBoundaries);
+
+ // A layoutObject can have no invalidation backing if it is from a detached
+ // frame, or when forced compositing is disabled.
+ if (paint_invalidation_container.Layer()->GetCompositingState() ==
+ kNotComposited)
+ return container_point;
+
+ PaintLayer::MapPointInPaintInvalidationContainerToBacking(
+ paint_invalidation_container, container_point);
+
+ if (GraphicsLayer* graphics_layer = GetGraphicsLayerBacking(layout_object))
+ container_point.Move(-graphics_layer->OffsetFromLayoutObject());
+
+ // Ensure the coordinates are in the scrolling contents space, if the object
+ // is a scroller.
+ if (paint_invalidation_container.UsesCompositedScrolling()) {
+ container_point.Move(paint_invalidation_container.Layer()
+ ->GetScrollableArea()
+ ->GetScrollOffset());
+ }
+
+ return container_point;
+}
+
+std::pair<LayoutPoint, LayoutPoint> static GetLocalSelectionStartpoints(
+ const LocalCaretRect& local_caret_rect) {
+ const LayoutRect rect = local_caret_rect.rect;
+ if (local_caret_rect.layout_object->Style()->IsHorizontalWritingMode())
+ return {rect.MinXMinYCorner(), rect.MinXMaxYCorner()};
+
+ // When text is vertical, it looks better for the start handle baseline to
+ // be at the starting edge, to enclose the selection fully between the
+ // handles.
+ return {rect.MaxXMinYCorner(), rect.MinXMinYCorner()};
+}
+
+std::pair<LayoutPoint, LayoutPoint> static GetLocalSelectionEndpoints(
+ const LocalCaretRect& local_caret_rect) {
+ const LayoutRect rect = local_caret_rect.rect;
+ if (local_caret_rect.layout_object->Style()->IsHorizontalWritingMode())
+ return {rect.MinXMinYCorner(), rect.MinXMaxYCorner()};
+
+ return {rect.MinXMinYCorner(), rect.MaxXMinYCorner()};
+}
+
+static LayoutPoint GetSamplePointForVisibility(
+ const LayoutPoint& edge_top_in_layer,
+ const LayoutPoint& edge_bottom_in_layer) {
+ FloatSize diff(edge_top_in_layer - edge_bottom_in_layer);
+ // Adjust by ~1px to avoid integer snapping error. This logic is the same
+ // as that in ComputeViewportSelectionBound in cc.
+ diff.Scale(1 / diff.DiagonalLength());
+ LayoutPoint sample_point = edge_bottom_in_layer;
+ sample_point.Move(LayoutSize(diff));
+ return sample_point;
+}
+
+// Returns whether this position is not visible on the screen (because
+// clipped out).
+static bool IsVisible(const LayoutObject& rect_layout_object,
+ const LayoutPoint& edge_top_in_layer,
+ const LayoutPoint& edge_bottom_in_layer) {
+ Node* const node = rect_layout_object.GetNode();
+ if (!node)
+ return true;
+ TextControlElement* text_control = EnclosingTextControl(node);
+ if (!text_control)
+ return true;
+ if (!IsHTMLInputElement(text_control))
+ return true;
+
+ LayoutObject* layout_object = text_control->GetLayoutObject();
+ if (!layout_object || !layout_object->IsBox())
+ return true;
+
+ const LayoutPoint sample_point =
+ GetSamplePointForVisibility(edge_top_in_layer, edge_bottom_in_layer);
+
+ LayoutBox* const text_control_object = ToLayoutBox(layout_object);
+ const LayoutPoint position_in_input(rect_layout_object.LocalToAncestorPoint(
+ FloatPoint(sample_point), text_control_object,
+ kTraverseDocumentBoundaries));
+ return text_control_object->BorderBoxRect().Contains(position_in_input);
+}
+
+static CompositedSelectionBound ComputeSelectionBound(
+ const PositionWithAffinity& position,
+ const LayoutObject& layout_object,
+ const LayoutPoint& edge_top_in_layer,
+ const LayoutPoint& edge_bottom_in_layer) {
+ CompositedSelectionBound bound;
+ bound.is_text_direction_rtl =
+ layout_object.HasFlippedBlocksWritingMode() ||
+ PrimaryDirectionOf(*position.AnchorNode()) == TextDirection::kRtl;
+ bound.edge_top_in_layer =
+ LocalToInvalidationBackingPoint(edge_top_in_layer, layout_object);
+ bound.edge_bottom_in_layer =
+ LocalToInvalidationBackingPoint(edge_bottom_in_layer, layout_object);
+ bound.layer = GetGraphicsLayerBacking(layout_object);
+ bound.hidden =
+ !IsVisible(layout_object, edge_top_in_layer, edge_bottom_in_layer);
+ return bound;
+}
+
+static CompositedSelectionBound StartPositionInGraphicsLayerBacking(
+ const PositionWithAffinity& position) {
+ const LocalCaretRect& local_caret_rect = LocalCaretRectOfPosition(position);
+ const LayoutObject* const layout_object = local_caret_rect.layout_object;
+ if (!layout_object)
+ return CompositedSelectionBound();
+
+ LayoutPoint edge_top_in_layer, edge_bottom_in_layer;
+ std::tie(edge_top_in_layer, edge_bottom_in_layer) =
+ GetLocalSelectionStartpoints(local_caret_rect);
+ return ComputeSelectionBound(position, *layout_object, edge_top_in_layer,
+ edge_bottom_in_layer);
+}
+
+static CompositedSelectionBound EndPositionInGraphicsLayerBacking(
+ const PositionWithAffinity& position) {
+ const LocalCaretRect& local_caret_rect = LocalCaretRectOfPosition(position);
+ const LayoutObject* const layout_object = local_caret_rect.layout_object;
+ if (!layout_object)
+ return CompositedSelectionBound();
+
+ LayoutPoint edge_top_in_layer, edge_bottom_in_layer;
+ std::tie(edge_top_in_layer, edge_bottom_in_layer) =
+ GetLocalSelectionEndpoints(local_caret_rect);
+ return ComputeSelectionBound(position, *layout_object, edge_top_in_layer,
+ edge_bottom_in_layer);
+}
+
+bool LayoutObjectContainsPosition(LayoutObject* target,
+ const Position& position) {
+ for (LayoutObject* layout_object = LayoutObjectFromPosition(position);
+ layout_object && layout_object->GetNode();
+ layout_object = layout_object->Parent()) {
+ if (layout_object == target)
+ return true;
+ }
+ return false;
+}
+
+CompositedSelection RenderedPosition::ComputeCompositedSelection(
+ const FrameSelection& frame_selection) {
+ if (!frame_selection.IsHandleVisible() || frame_selection.IsHidden())
+ return {};
+
+ // TODO(yoichio): Compute SelectionInDOMTree w/o VS canonicalization.
+ // crbug.com/789870 for detail.
+ const SelectionInDOMTree& visible_selection =
+ frame_selection.ComputeVisibleSelectionInDOMTree().AsSelection();
+ // Non-editable caret selections lack any kind of UI affordance, and
+ // needn't be tracked by the client.
+ if (visible_selection.IsCaret() &&
+ !IsEditablePosition(visible_selection.ComputeStartPosition()))
+ return {};
+
+ CompositedSelection selection;
+ selection.start = StartPositionInGraphicsLayerBacking(PositionWithAffinity(
+ visible_selection.ComputeStartPosition(), visible_selection.Affinity()));
+ if (!selection.start.layer)
+ return {};
+
+ selection.end = EndPositionInGraphicsLayerBacking(PositionWithAffinity(
+ visible_selection.ComputeEndPosition(), visible_selection.Affinity()));
+ if (!selection.end.layer)
+ return {};
+
+ DCHECK(!visible_selection.IsNone());
+ selection.type =
+ visible_selection.IsRange() ? kRangeSelection : kCaretSelection;
+ return selection;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/rendered_position.h b/chromium/third_party/blink/renderer/core/editing/rendered_position.h
new file mode 100644
index 00000000000..665ae95c4f6
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/rendered_position.h
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_RENDERED_POSITION_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_RENDERED_POSITION_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/layout/line/inline_box.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/optional.h"
+
+namespace blink {
+
+class FrameSelection;
+class LayoutObject;
+struct CompositedSelection;
+
+class CORE_EXPORT RenderedPosition {
+ STACK_ALLOCATED();
+
+ public:
+ RenderedPosition();
+ explicit RenderedPosition(const VisiblePosition&);
+ explicit RenderedPosition(const VisiblePositionInFlatTree&);
+ RenderedPosition(const Position&, TextAffinity);
+ RenderedPosition(const PositionInFlatTree&, TextAffinity);
+ bool IsEquivalent(const RenderedPosition&) const;
+
+ bool IsNull() const { return !layout_object_; }
+ const RootInlineBox* RootBox() const {
+ return inline_box_ ? &inline_box_->Root() : nullptr;
+ }
+
+ unsigned char BidiLevelOnLeft() const;
+ unsigned char BidiLevelOnRight() const;
+ RenderedPosition LeftBoundaryOfBidiRun(unsigned char bidi_level_of_run);
+ RenderedPosition RightBoundaryOfBidiRun(unsigned char bidi_level_of_run);
+
+ enum ShouldMatchBidiLevel { kMatchBidiLevel, kIgnoreBidiLevel };
+ bool AtLeftBoundaryOfBidiRun() const {
+ return AtLeftBoundaryOfBidiRun(kIgnoreBidiLevel, 0);
+ }
+ bool AtRightBoundaryOfBidiRun() const {
+ return AtRightBoundaryOfBidiRun(kIgnoreBidiLevel, 0);
+ }
+ // The following two functions return true only if the current position is
+ // at the end of the bidi run of the specified bidi embedding level.
+ bool AtLeftBoundaryOfBidiRun(unsigned char bidi_level_of_run) const {
+ return AtLeftBoundaryOfBidiRun(kMatchBidiLevel, bidi_level_of_run);
+ }
+ bool AtRightBoundaryOfBidiRun(unsigned char bidi_level_of_run) const {
+ return AtRightBoundaryOfBidiRun(kMatchBidiLevel, bidi_level_of_run);
+ }
+
+ Position PositionAtLeftBoundaryOfBiDiRun() const;
+ Position PositionAtRightBoundaryOfBiDiRun() const;
+
+ // TODO(editing-dev): This function doesn't use RenderedPosition
+ // instance anymore. Consider moving.
+ static CompositedSelection ComputeCompositedSelection(const FrameSelection&);
+
+ private:
+ bool operator==(const RenderedPosition&) const { return false; }
+ explicit RenderedPosition(const LayoutObject*, const InlineBox*, int offset);
+
+ const InlineBox* PrevLeafChild() const;
+ const InlineBox* NextLeafChild() const;
+ bool AtLeftmostOffsetInBox() const {
+ return inline_box_ && offset_ == inline_box_->CaretLeftmostOffset();
+ }
+ bool AtRightmostOffsetInBox() const {
+ return inline_box_ && offset_ == inline_box_->CaretRightmostOffset();
+ }
+ bool AtLeftBoundaryOfBidiRun(ShouldMatchBidiLevel,
+ unsigned char bidi_level_of_run) const;
+ bool AtRightBoundaryOfBidiRun(ShouldMatchBidiLevel,
+ unsigned char bidi_level_of_run) const;
+
+ const LayoutObject* layout_object_;
+ const InlineBox* inline_box_;
+ int offset_;
+
+ mutable Optional<const InlineBox*> prev_leaf_child_;
+ mutable Optional<const InlineBox*> next_leaf_child_;
+};
+
+inline RenderedPosition::RenderedPosition()
+ : layout_object_(nullptr), inline_box_(nullptr), offset_(0) {}
+
+inline RenderedPosition::RenderedPosition(const LayoutObject* layout_object,
+ const InlineBox* box,
+ int offset)
+ : layout_object_(layout_object), inline_box_(box), offset_(offset) {}
+
+CORE_EXPORT bool LayoutObjectContainsPosition(LayoutObject*, const Position&);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_RENDERED_POSITION_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/rendered_position_test.cc b/chromium/third_party/blink/renderer/core/editing/rendered_position_test.cc
new file mode 100644
index 00000000000..152949d80a0
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/rendered_position_test.cc
@@ -0,0 +1,275 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/rendered_position.h"
+
+#include "build/build_config.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+#include "third_party/blink/renderer/core/layout/layout_box.h"
+#include "third_party/blink/renderer/core/paint/compositing/composited_selection.h"
+#include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
+#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
+#include "third_party/blink/renderer/platform/testing/use_mock_scrollbar_settings.h"
+
+namespace blink {
+
+class RenderedPositionTest : public testing::WithParamInterface<bool>,
+ private ScopedRootLayerScrollingForTest,
+ public EditingTestBase {
+ public:
+ RenderedPositionTest() : ScopedRootLayerScrollingForTest(GetParam()) {}
+ void SetUp() override {
+ EditingTestBase::SetUp();
+ GetPage().GetSettings().SetAcceleratedCompositingEnabled(true);
+ GetDocument().View()->SetParentVisible(true);
+ GetDocument().View()->SetSelfVisible(true);
+ LoadAhem();
+ }
+
+ void FocusAndSelectAll(Element* focus, const Node& select) {
+ DCHECK(focus);
+ focus->focus();
+ Selection().SetSelection(
+ SelectionInDOMTree::Builder().SelectAllChildren(select).Build(),
+ SetSelectionOptions::Builder().SetShouldShowHandle(true).Build());
+ UpdateAllLifecyclePhases();
+ }
+
+ void FocusAndSelectAll(TextControlElement* target) {
+ FocusAndSelectAll(target, *target->InnerEditorElement());
+ }
+
+ private:
+ UseMockScrollbarSettings mock_scrollbars_;
+};
+
+INSTANTIATE_TEST_CASE_P(All, RenderedPositionTest, testing::Bool());
+
+TEST_P(RenderedPositionTest, ComputeCompositedSelection) {
+ SetBodyContent(R"HTML(
+ <!DOCTYPE html>
+ input {
+ font: 10px/1 Ahem;
+ padding: 0;
+ border: 0;
+ }
+ <input id=target width=20 value='test test test test test tes tes test'
+ style='width: 100px; height: 20px;'>
+ )HTML");
+
+ FocusAndSelectAll(ToHTMLInputElement(GetDocument().getElementById("target")));
+
+ const CompositedSelection& composited_selection =
+ RenderedPosition::ComputeCompositedSelection(Selection());
+ EXPECT_FALSE(composited_selection.start.hidden);
+ EXPECT_TRUE(composited_selection.end.hidden);
+}
+
+TEST_P(RenderedPositionTest, PositionInScrollableRoot) {
+ SetBodyContent(R"HTML(
+ <!DOCTYPE html>
+ <style>
+ body {
+ margin: 0;
+ height: 2000px;
+ width: 2000px;
+ }
+ input {
+ font: 10px/1 Ahem;
+ padding: 0;
+ border: 0;
+ width: 100px;
+ height: 20px;
+ position: absolute;
+ top: 900px;
+ left: 1000px;
+ }
+ </style>
+ <input id=target width=20 value='test test test test test tes tes test'>
+ )HTML");
+
+ FocusAndSelectAll(ToHTMLInputElement(GetDocument().getElementById("target")));
+
+ ScrollableArea* root_scroller = GetDocument().View()->GetScrollableArea();
+ root_scroller->SetScrollOffset(ScrollOffset(800, 500), kProgrammaticScroll);
+ ASSERT_EQ(ScrollOffset(800, 500), root_scroller->GetScrollOffset());
+
+ UpdateAllLifecyclePhases();
+
+ const CompositedSelection& composited_selection =
+ RenderedPosition::ComputeCompositedSelection(Selection());
+
+ // Top-left corner should be around (1000, 905) - 10px centered in 20px
+ // height.
+ EXPECT_EQ(FloatPoint(1000, 905),
+ composited_selection.start.edge_top_in_layer);
+ EXPECT_EQ(FloatPoint(1000, 915),
+ composited_selection.start.edge_bottom_in_layer);
+ EXPECT_EQ(FloatPoint(1369, 905), composited_selection.end.edge_top_in_layer);
+ EXPECT_EQ(FloatPoint(1369, 915),
+ composited_selection.end.edge_bottom_in_layer);
+}
+
+TEST_P(RenderedPositionTest, PositionInScroller) {
+ SetBodyContent(R"HTML(
+ <!DOCTYPE html>
+ <style>
+ body {
+ margin: 0;
+ height: 2000px;
+ width: 2000px;
+ }
+ input {
+ font: 10px/1 Ahem;
+ padding: 0;
+ border: 0;
+ width: 100px;
+ height: 20px;
+ position: absolute;
+ top: 900px;
+ left: 1000px;
+ }
+
+ #scroller {
+ width: 300px;
+ height: 300px;
+ position: absolute;
+ left: 300px;
+ top: 400px;
+ overflow: scroll;
+ border: 200px;
+ will-change: transform;
+ }
+
+ #space {
+ width: 2000px;
+ height: 2000px;
+ }
+ </style>
+ <div id="scroller">
+ <div id="space"></div>
+ <input id=target width=20 value='test test test test test tes tes test'>
+ </div>
+ )HTML");
+
+ FocusAndSelectAll(ToHTMLInputElement(GetDocument().getElementById("target")));
+
+ Element* e = GetDocument().getElementById("scroller");
+ PaintLayerScrollableArea* scroller =
+ ToLayoutBox(e->GetLayoutObject())->GetScrollableArea();
+ scroller->SetScrollOffset(ScrollOffset(900, 800), kProgrammaticScroll);
+ ASSERT_EQ(ScrollOffset(900, 800), scroller->GetScrollOffset());
+
+ UpdateAllLifecyclePhases();
+
+ const CompositedSelection& composited_selection =
+ RenderedPosition::ComputeCompositedSelection(Selection());
+
+ // Top-left corner should be around (1000, 905) - 10px centered in 20px
+ // height.
+ EXPECT_EQ(FloatPoint(1000, 905),
+ composited_selection.start.edge_top_in_layer);
+ EXPECT_EQ(FloatPoint(1000, 915),
+ composited_selection.start.edge_bottom_in_layer);
+ EXPECT_EQ(FloatPoint(1369, 905), composited_selection.end.edge_top_in_layer);
+ EXPECT_EQ(FloatPoint(1369, 915),
+ composited_selection.end.edge_bottom_in_layer);
+}
+
+// crbug.com/807930
+TEST_P(RenderedPositionTest, ContentEditableLinebreak) {
+ SetBodyContent(
+ "<div style='font: 10px/10px Ahem;' contenteditable>"
+ "test<br><br></div>");
+ Element* target = GetDocument().QuerySelector("div");
+ FocusAndSelectAll(target, *target);
+ const CompositedSelection& composited_selection =
+ RenderedPosition::ComputeCompositedSelection(Selection());
+ EXPECT_EQ(composited_selection.start.edge_top_in_layer,
+ FloatPoint(8.0f, 8.0f));
+ EXPECT_EQ(composited_selection.start.edge_bottom_in_layer,
+ FloatPoint(8.0f, 18.0f));
+ EXPECT_EQ(composited_selection.end.edge_top_in_layer,
+ FloatPoint(8.0f, 18.0f));
+ EXPECT_EQ(composited_selection.end.edge_bottom_in_layer,
+ FloatPoint(8.0f, 28.0f));
+}
+
+// crbug.com/807930
+TEST_P(RenderedPositionTest, TextAreaLinebreak) {
+ SetBodyContent(
+ "<textarea style='font: 10px/10px Ahem;'>"
+ "test\n</textarea>");
+ FocusAndSelectAll(ToTextControl(GetDocument().QuerySelector("textarea")));
+ const CompositedSelection& composited_selection =
+ RenderedPosition::ComputeCompositedSelection(Selection());
+ EXPECT_EQ(composited_selection.start.edge_top_in_layer,
+ FloatPoint(11.0f, 11.0f));
+ EXPECT_EQ(composited_selection.start.edge_bottom_in_layer,
+ FloatPoint(11.0f, 21.0f));
+ EXPECT_EQ(composited_selection.end.edge_top_in_layer,
+ FloatPoint(11.0f, 21.0f));
+ EXPECT_EQ(composited_selection.end.edge_bottom_in_layer,
+ FloatPoint(11.0f, 31.0f));
+}
+
+// crbug.com/815099
+TEST_P(RenderedPositionTest, CaretBeforeSoftWrap) {
+ SetBodyContent(
+ "<div style='font: 10px/10px Ahem; width:20px;' "
+ "contenteditable>foo</div>");
+ Element* target = GetDocument().QuerySelector("div");
+ target->focus();
+ Node* text_foo = target->firstChild();
+ Selection().SetSelection(
+ SelectionInDOMTree::Builder()
+ .Collapse(
+ PositionWithAffinity({text_foo, 2}, TextAffinity::kUpstream))
+ .Build(),
+ SetSelectionOptions::Builder().SetShouldShowHandle(true).Build());
+ UpdateAllLifecyclePhases();
+ const CompositedSelection& composited_selection =
+ RenderedPosition::ComputeCompositedSelection(Selection());
+ EXPECT_EQ(composited_selection.start.edge_top_in_layer,
+ FloatPoint(27.0f, 8.0f));
+ EXPECT_EQ(composited_selection.start.edge_bottom_in_layer,
+ FloatPoint(27.0f, 18.0f));
+ EXPECT_EQ(composited_selection.end.edge_top_in_layer,
+ FloatPoint(27.0f, 8.0f));
+ EXPECT_EQ(composited_selection.end.edge_bottom_in_layer,
+ FloatPoint(27.0f, 18.0f));
+}
+
+TEST_P(RenderedPositionTest, CaretAfterSoftWrap) {
+ SetBodyContent(
+ "<div style='font: 10px/10px Ahem; width:20px;' "
+ "contenteditable>foo</div>");
+ Element* target = GetDocument().QuerySelector("div");
+ target->focus();
+ Node* text_foo = target->firstChild();
+ Selection().SetSelection(
+ SelectionInDOMTree::Builder()
+ .Collapse(
+ PositionWithAffinity({text_foo, 2}, TextAffinity::kDownstream))
+ .Build(),
+ SetSelectionOptions::Builder().SetShouldShowHandle(true).Build());
+ UpdateAllLifecyclePhases();
+ const CompositedSelection& composited_selection =
+ RenderedPosition::ComputeCompositedSelection(Selection());
+ EXPECT_EQ(composited_selection.start.edge_top_in_layer,
+ FloatPoint(8.0f, 18.0f));
+ EXPECT_EQ(composited_selection.start.edge_bottom_in_layer,
+ FloatPoint(8.0f, 28.0f));
+ EXPECT_EQ(composited_selection.end.edge_top_in_layer,
+ FloatPoint(8.0f, 18.0f));
+ EXPECT_EQ(composited_selection.end.edge_bottom_in_layer,
+ FloatPoint(8.0f, 28.0f));
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/reveal_selection_scope.cc b/chromium/third_party/blink/renderer/core/editing/reveal_selection_scope.cc
new file mode 100644
index 00000000000..afe5f8b63ee
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/reveal_selection_scope.cc
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2006, 2007, 2008, 2011 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/reveal_selection_scope.h"
+
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+
+namespace blink {
+
+RevealSelectionScope::RevealSelectionScope(LocalFrame& frame) : frame_(&frame) {
+ GetEditor().IncreasePreventRevealSelection();
+}
+
+RevealSelectionScope::~RevealSelectionScope() {
+ DCHECK(GetEditor().PreventRevealSelection());
+ GetEditor().DecreasePreventRevealSelection();
+ if (!GetEditor().PreventRevealSelection()) {
+ frame_->Selection().RevealSelection(ScrollAlignment::kAlignToEdgeIfNeeded,
+ kRevealExtent);
+ }
+}
+
+Editor& RevealSelectionScope::GetEditor() {
+ return frame_->GetEditor();
+}
+
+void RevealSelectionScope::Trace(blink::Visitor* visitor) {
+ visitor->Trace(frame_);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/reveal_selection_scope.h b/chromium/third_party/blink/renderer/core/editing/reveal_selection_scope.h
new file mode 100644
index 00000000000..0b45790e917
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/reveal_selection_scope.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_REVEAL_SELECTION_SCOPE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_REVEAL_SELECTION_SCOPE_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+class Editor;
+class LocalFrame;
+
+class RevealSelectionScope {
+ DISALLOW_NEW_EXCEPT_PLACEMENT_NEW();
+
+ public:
+ explicit RevealSelectionScope(LocalFrame&);
+ ~RevealSelectionScope();
+
+ void Trace(blink::Visitor*);
+
+ private:
+ Editor& GetEditor();
+
+ Member<LocalFrame> frame_;
+
+ DISALLOW_COPY_AND_ASSIGN(RevealSelectionScope);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_REVEAL_SELECTION_SCOPE_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/selection.idl b/chromium/third_party/blink/renderer/core/editing/selection.idl
new file mode 100644
index 00000000000..fc128c0574f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection.idl
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2007 Apple Inc. All rights reserved.
+ * Copyright (C) 2009 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// https://w3c.github.io/selection-api/#selection-interface
+[
+ ImplementedAs=DOMSelection
+] interface Selection {
+ [MeasureAs=SelectionAnchorNode] readonly attribute Node? anchorNode;
+ [MeasureAs=SelectionAnchorOffset] readonly attribute unsigned long anchorOffset;
+ [MeasureAs=SelectionFocusNode] readonly attribute Node? focusNode;
+ [MeasureAs=SelectionFocusOffset] readonly attribute unsigned long focusOffset;
+ [MeasureAs=SelectionIsCollapsed] readonly attribute boolean isCollapsed;
+ [MeasureAs=SelectionRangeCount] readonly attribute unsigned long rangeCount;
+ [MeasureAs=SelectionType] readonly attribute DOMString type;
+ [MeasureAs=SelectionGetRangeAt, RaisesException] Range getRangeAt(unsigned long index);
+ [MeasureAs=SelectionAddRange] void addRange(Range range);
+ void removeRange(Range range);
+ [MeasureAs=SelectionRemoveAllRanges] void removeAllRanges();
+ [MeasureAs=SelectionEmpty] void empty();
+ [MeasureAs=SelectionCollapse, RaisesException] void collapse(Node? node, optional unsigned long offset = 0);
+ [ImplementedAs=collapse, MeasureAs=SelectionSetPosition, RaisesException] void setPosition(Node? node, optional unsigned long offset = 0);
+ [MeasureAs=SelectionCollapseToStart, RaisesException] void collapseToStart();
+ [MeasureAs=SelectionCollapseToEnd, RaisesException] void collapseToEnd();
+ [MeasureAs=SelectionExtend, RaisesException] void extend(Node node, optional unsigned long offset = 0);
+ // TODO(foolip): The arguments should be anchorNode, anchorOffset,
+ // focusNode and focusOffset, and none of them are nullable in the spec.
+ [MeasureAs=SelectionSetBaseAndExtent, RaisesException] void setBaseAndExtent(Node? baseNode, unsigned long baseOffset,
+ Node? extentNode, unsigned long extentOffset);
+ [MeasureAs=SelectionSelectAllChildren, RaisesException] void selectAllChildren(Node node);
+ [MeasureAs=SelectionDeleteDromDocument, CEReactions, CustomElementCallbacks] void deleteFromDocument();
+ [MeasureAs=SelectionContainsNode] boolean containsNode(Node node, optional boolean allowPartialContainment = false);
+ [MeasureAs=SelectionDOMString] stringifier;
+
+ // Non-standard APIs
+
+ // https://github.com/w3c/selection-api/issues/34
+ [MeasureAs=SelectionBaseNode] readonly attribute Node? baseNode;
+ [MeasureAs=SelectionBaseOffset] readonly attribute unsigned long baseOffset;
+ [MeasureAs=SelectionExtentNode] readonly attribute Node? extentNode;
+ [MeasureAs=SelectionExtentOffset] readonly attribute unsigned long extentOffset;
+
+ // https://github.com/w3c/selection-api/issues/37
+ [MeasureAs=SelectionModify] void modify([Default=Undefined] optional DOMString alter,
+ [Default=Undefined] optional DOMString direction,
+ [Default=Undefined] optional DOMString granularity);
+};
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_adjuster.cc b/chromium/third_party/blink/renderer/core/editing/selection_adjuster.cc
new file mode 100644
index 00000000000..728e433002b
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_adjuster.cc
@@ -0,0 +1,769 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/selection_adjuster.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+
+namespace blink {
+
+namespace {
+
+template <typename Strategy>
+SelectionTemplate<Strategy> ComputeAdjustedSelection(
+ const SelectionTemplate<Strategy> selection,
+ const EphemeralRangeTemplate<Strategy>& range) {
+ if (selection.ComputeRange() == range) {
+ // To pass "editing/deleting/delete_after_block_image.html", we need to
+ // return original selection.
+ return selection;
+ }
+ if (range.StartPosition().CompareTo(range.EndPosition()) == 0) {
+ return typename SelectionTemplate<Strategy>::Builder()
+ .Collapse(selection.IsBaseFirst() ? range.StartPosition()
+ : range.EndPosition())
+ .Build();
+ }
+ if (selection.IsBaseFirst()) {
+ return typename SelectionTemplate<Strategy>::Builder()
+ .SetAsForwardSelection(range)
+ .Build();
+ }
+ return typename SelectionTemplate<Strategy>::Builder()
+ .SetAsBackwardSelection(range)
+ .Build();
+}
+
+bool IsEmptyTableCell(const Node* node) {
+ // Returns true IFF the passed in node is one of:
+ // .) a table cell with no children,
+ // .) a table cell with a single BR child, and which has no other child
+ // layoutObject, including :before and :after layoutObject
+ // .) the BR child of such a table cell
+
+ // Find rendered node
+ while (node && !node->GetLayoutObject())
+ node = node->parentNode();
+ if (!node)
+ return false;
+
+ // Make sure the rendered node is a table cell or <br>.
+ // If it's a <br>, then the parent node has to be a table cell.
+ const LayoutObject* layout_object = node->GetLayoutObject();
+ if (layout_object->IsBR()) {
+ layout_object = layout_object->Parent();
+ if (!layout_object)
+ return false;
+ }
+ if (!layout_object->IsTableCell())
+ return false;
+
+ // Check that the table cell contains no child layoutObjects except for
+ // perhaps a single <br>.
+ const LayoutObject* const child_layout_object =
+ layout_object->SlowFirstChild();
+ if (!child_layout_object)
+ return true;
+ if (!child_layout_object->IsBR())
+ return false;
+ return !child_layout_object->NextSibling();
+}
+
+} // anonymous namespace
+
+class GranularityAdjuster final {
+ STATIC_ONLY(GranularityAdjuster);
+
+ public:
+ template <typename Strategy>
+ static PositionTemplate<Strategy> ComputeStartRespectingGranularityAlgorithm(
+ const PositionWithAffinityTemplate<Strategy>& passed_start,
+ TextGranularity granularity) {
+ DCHECK(passed_start.IsNotNull());
+
+ switch (granularity) {
+ case TextGranularity::kCharacter:
+ // Don't do any expansion.
+ return passed_start.GetPosition();
+ case TextGranularity::kWord: {
+ // General case: Select the word the caret is positioned inside of.
+ // If the caret is on the word boundary, select the word according to
+ // |wordSide|.
+ // Edge case: If the caret is after the last word in a soft-wrapped line
+ // or the last word in the document, select that last word
+ // (kPreviousWordIfOnBoundary).
+ // Edge case: If the caret is after the last word in a paragraph, select
+ // from the the end of the last word to the line break (also
+ // kNextWordIfOnBoundary);
+ const VisiblePositionTemplate<Strategy> visible_start =
+ CreateVisiblePosition(passed_start);
+ return StartOfWord(visible_start, ChooseWordSide(visible_start))
+ .DeepEquivalent();
+ }
+ case TextGranularity::kSentence:
+ return StartOfSentence(CreateVisiblePosition(passed_start))
+ .DeepEquivalent();
+ case TextGranularity::kLine:
+ return StartOfLine(CreateVisiblePosition(passed_start))
+ .DeepEquivalent();
+ case TextGranularity::kLineBoundary:
+ return StartOfLine(CreateVisiblePosition(passed_start))
+ .DeepEquivalent();
+ case TextGranularity::kParagraph: {
+ const VisiblePositionTemplate<Strategy> pos =
+ CreateVisiblePosition(passed_start);
+ if (IsStartOfLine(pos) && IsEndOfEditableOrNonEditableContent(pos))
+ return StartOfParagraph(PreviousPositionOf(pos)).DeepEquivalent();
+ return StartOfParagraph(pos).DeepEquivalent();
+ }
+ case TextGranularity::kDocumentBoundary:
+ return StartOfDocument(CreateVisiblePosition(passed_start))
+ .DeepEquivalent();
+ case TextGranularity::kParagraphBoundary:
+ return StartOfParagraph(CreateVisiblePosition(passed_start))
+ .DeepEquivalent();
+ case TextGranularity::kSentenceBoundary:
+ return StartOfSentence(CreateVisiblePosition(passed_start))
+ .DeepEquivalent();
+ }
+
+ NOTREACHED();
+ return passed_start.GetPosition();
+ }
+
+ template <typename Strategy>
+ static PositionTemplate<Strategy> ComputeEndRespectingGranularityAlgorithm(
+ const PositionTemplate<Strategy>& start,
+ const PositionWithAffinityTemplate<Strategy>& passed_end,
+ TextGranularity granularity) {
+ DCHECK(passed_end.IsNotNull());
+
+ switch (granularity) {
+ case TextGranularity::kCharacter:
+ // Don't do any expansion.
+ return passed_end.GetPosition();
+ case TextGranularity::kWord: {
+ // General case: Select the word the caret is positioned inside of.
+ // If the caret is on the word boundary, select the word according to
+ // |wordSide|.
+ // Edge case: If the caret is after the last word in a soft-wrapped line
+ // or the last word in the document, select that last word
+ // (|kPreviousWordIfOnBoundary|).
+ // Edge case: If the caret is after the last word in a paragraph, select
+ // from the the end of the last word to the line break (also
+ // |kNextWordIfOnBoundary|);
+ const VisiblePositionTemplate<Strategy> original_end =
+ CreateVisiblePosition(passed_end);
+ const VisiblePositionTemplate<Strategy> word_end =
+ EndOfWord(original_end, ChooseWordSide(original_end));
+ if (!IsEndOfParagraph(original_end))
+ return word_end.DeepEquivalent();
+ if (IsEmptyTableCell(start.AnchorNode()))
+ return word_end.DeepEquivalent();
+
+ // Select the paragraph break (the space from the end of a paragraph
+ // to the start of the next one) to match TextEdit.
+ const VisiblePositionTemplate<Strategy> end = NextPositionOf(word_end);
+ Element* const table = TableElementJustBefore(end);
+ if (!table) {
+ if (end.IsNull())
+ return word_end.DeepEquivalent();
+ return end.DeepEquivalent();
+ }
+
+ if (!IsEnclosingBlock(table))
+ return word_end.DeepEquivalent();
+
+ // The paragraph break after the last paragraph in the last cell
+ // of a block table ends at the start of the paragraph after the
+ // table.
+ const VisiblePositionTemplate<Strategy> next =
+ NextPositionOf(end, kCannotCrossEditingBoundary);
+ if (next.IsNull())
+ return word_end.DeepEquivalent();
+ return next.DeepEquivalent();
+ }
+ case TextGranularity::kSentence:
+ return EndOfSentence(CreateVisiblePosition(passed_end))
+ .DeepEquivalent();
+ case TextGranularity::kLine: {
+ const VisiblePositionTemplate<Strategy> end =
+ EndOfLine(CreateVisiblePosition(passed_end));
+ if (!IsEndOfParagraph(end))
+ return end.DeepEquivalent();
+ // If the end of this line is at the end of a paragraph, include the
+ // space after the end of the line in the selection.
+ const VisiblePositionTemplate<Strategy> next = NextPositionOf(end);
+ if (next.IsNull())
+ return end.DeepEquivalent();
+ return next.DeepEquivalent();
+ }
+ case TextGranularity::kLineBoundary:
+ return EndOfLine(CreateVisiblePosition(passed_end)).DeepEquivalent();
+ case TextGranularity::kParagraph: {
+ const VisiblePositionTemplate<Strategy> visible_paragraph_end =
+ EndOfParagraph(CreateVisiblePosition(passed_end));
+
+ // Include the "paragraph break" (the space from the end of this
+ // paragraph to the start of the next one) in the selection.
+ const VisiblePositionTemplate<Strategy> end =
+ NextPositionOf(visible_paragraph_end);
+
+ Element* const table = TableElementJustBefore(end);
+ if (!table) {
+ if (end.IsNull())
+ return visible_paragraph_end.DeepEquivalent();
+ return end.DeepEquivalent();
+ }
+
+ if (!IsEnclosingBlock(table)) {
+ // There is no paragraph break after the last paragraph in the
+ // last cell of an inline table.
+ return visible_paragraph_end.DeepEquivalent();
+ }
+
+ // The paragraph break after the last paragraph in the last cell of
+ // a block table ends at the start of the paragraph after the table,
+ // not at the position just after the table.
+ const VisiblePositionTemplate<Strategy> next =
+ NextPositionOf(end, kCannotCrossEditingBoundary);
+ if (next.IsNull())
+ return visible_paragraph_end.DeepEquivalent();
+ return next.DeepEquivalent();
+ }
+ case TextGranularity::kDocumentBoundary:
+ return EndOfDocument(CreateVisiblePosition(passed_end))
+ .DeepEquivalent();
+ case TextGranularity::kParagraphBoundary:
+ return EndOfParagraph(CreateVisiblePosition(passed_end))
+ .DeepEquivalent();
+ case TextGranularity::kSentenceBoundary:
+ return EndOfSentence(CreateVisiblePosition(passed_end))
+ .DeepEquivalent();
+ }
+ NOTREACHED();
+ return passed_end.GetPosition();
+ }
+
+ template <typename Strategy>
+ static SelectionTemplate<Strategy> AdjustSelection(
+ const SelectionTemplate<Strategy>& canonicalized_selection,
+ TextGranularity granularity) {
+ const TextAffinity affinity = canonicalized_selection.Affinity();
+
+ const PositionTemplate<Strategy> start =
+ canonicalized_selection.ComputeStartPosition();
+ const PositionTemplate<Strategy> new_start =
+ ComputeStartRespectingGranularityAlgorithm(
+ PositionWithAffinityTemplate<Strategy>(start, affinity),
+ granularity);
+ const PositionTemplate<Strategy> expanded_start =
+ new_start.IsNotNull() ? new_start : start;
+
+ const PositionTemplate<Strategy> end =
+ canonicalized_selection.ComputeEndPosition();
+ const PositionTemplate<Strategy> new_end =
+ ComputeEndRespectingGranularityAlgorithm(
+ expanded_start,
+ PositionWithAffinityTemplate<Strategy>(end, affinity), granularity);
+ const PositionTemplate<Strategy> expanded_end =
+ new_end.IsNotNull() ? new_end : end;
+
+ const EphemeralRangeTemplate<Strategy> expanded_range(expanded_start,
+ expanded_end);
+ return ComputeAdjustedSelection(canonicalized_selection, expanded_range);
+ }
+
+ private:
+ template <typename Strategy>
+ static EWordSide ChooseWordSide(
+ const VisiblePositionTemplate<Strategy>& position) {
+ return IsEndOfEditableOrNonEditableContent(position) ||
+ (IsEndOfLine(position) && !IsStartOfLine(position) &&
+ !IsEndOfParagraph(position))
+ ? kPreviousWordIfOnBoundary
+ : kNextWordIfOnBoundary;
+ }
+};
+
+PositionInFlatTree ComputeStartRespectingGranularity(
+ const PositionInFlatTreeWithAffinity& start,
+ TextGranularity granularity) {
+ return GranularityAdjuster::ComputeStartRespectingGranularityAlgorithm(
+ start, granularity);
+}
+
+PositionInFlatTree ComputeEndRespectingGranularity(
+ const PositionInFlatTree& start,
+ const PositionInFlatTreeWithAffinity& end,
+ TextGranularity granularity) {
+ return GranularityAdjuster::ComputeEndRespectingGranularityAlgorithm(
+ start, end, granularity);
+}
+
+SelectionInDOMTree SelectionAdjuster::AdjustSelectionRespectingGranularity(
+ const SelectionInDOMTree& selection,
+ TextGranularity granularity) {
+ return GranularityAdjuster::AdjustSelection(selection, granularity);
+}
+
+SelectionInFlatTree SelectionAdjuster::AdjustSelectionRespectingGranularity(
+ const SelectionInFlatTree& selection,
+ TextGranularity granularity) {
+ return GranularityAdjuster::AdjustSelection(selection, granularity);
+}
+
+class ShadowBoundaryAdjuster final {
+ STATIC_ONLY(ShadowBoundaryAdjuster);
+
+ public:
+ template <typename Strategy>
+ static SelectionTemplate<Strategy> AdjustSelection(
+ const SelectionTemplate<Strategy>& selection) {
+ if (!selection.IsRange())
+ return selection;
+
+ const EphemeralRangeTemplate<Strategy> expanded_range =
+ selection.ComputeRange();
+
+ const EphemeralRangeTemplate<Strategy> shadow_adjusted_range =
+ selection.IsBaseFirst()
+ ? EphemeralRangeTemplate<Strategy>(
+ expanded_range.StartPosition(),
+ AdjustSelectionEndToAvoidCrossingShadowBoundaries(
+ expanded_range))
+ : EphemeralRangeTemplate<Strategy>(
+ AdjustSelectionStartToAvoidCrossingShadowBoundaries(
+ expanded_range),
+ expanded_range.EndPosition());
+ return ComputeAdjustedSelection(selection, shadow_adjusted_range);
+ }
+
+ private:
+ static Node* EnclosingShadowHost(Node* node) {
+ for (Node* runner = node; runner;
+ runner = FlatTreeTraversal::Parent(*runner)) {
+ if (IsShadowHost(runner))
+ return runner;
+ }
+ return nullptr;
+ }
+
+ static bool IsEnclosedBy(const PositionInFlatTree& position,
+ const Node& node) {
+ DCHECK(position.IsNotNull());
+ Node* anchor_node = position.AnchorNode();
+ if (anchor_node == node)
+ return !position.IsAfterAnchor() && !position.IsBeforeAnchor();
+
+ return FlatTreeTraversal::IsDescendantOf(*anchor_node, node);
+ }
+
+ static bool IsSelectionBoundary(const Node& node) {
+ return IsHTMLTextAreaElement(node) || IsHTMLInputElement(node) ||
+ IsHTMLSelectElement(node);
+ }
+
+ static Node* EnclosingShadowHostForStart(const PositionInFlatTree& position) {
+ Node* node = position.NodeAsRangeFirstNode();
+ if (!node)
+ return nullptr;
+ Node* shadow_host = EnclosingShadowHost(node);
+ if (!shadow_host)
+ return nullptr;
+ if (!IsEnclosedBy(position, *shadow_host))
+ return nullptr;
+ return IsSelectionBoundary(*shadow_host) ? shadow_host : nullptr;
+ }
+
+ static Node* EnclosingShadowHostForEnd(const PositionInFlatTree& position) {
+ Node* node = position.NodeAsRangeLastNode();
+ if (!node)
+ return nullptr;
+ Node* shadow_host = EnclosingShadowHost(node);
+ if (!shadow_host)
+ return nullptr;
+ if (!IsEnclosedBy(position, *shadow_host))
+ return nullptr;
+ return IsSelectionBoundary(*shadow_host) ? shadow_host : nullptr;
+ }
+
+ static PositionInFlatTree AdjustPositionInFlatTreeForStart(
+ const PositionInFlatTree& position,
+ Node* shadow_host) {
+ if (IsEnclosedBy(position, *shadow_host)) {
+ if (position.IsBeforeChildren())
+ return PositionInFlatTree::BeforeNode(*shadow_host);
+ return PositionInFlatTree::AfterNode(*shadow_host);
+ }
+
+ // We use |firstChild|'s after instead of beforeAllChildren for backward
+ // compatibility. The positions are same but the anchors would be different,
+ // and selection painting uses anchor nodes.
+ if (Node* first_child = FlatTreeTraversal::FirstChild(*shadow_host))
+ return PositionInFlatTree::BeforeNode(*first_child);
+ return PositionInFlatTree();
+ }
+
+ static Position AdjustPositionForEnd(const Position& current_position,
+ Node* start_container_node) {
+ TreeScope& tree_scope = start_container_node->GetTreeScope();
+
+ DCHECK(current_position.ComputeContainerNode()->GetTreeScope() !=
+ tree_scope);
+
+ if (Node* ancestor = tree_scope.AncestorInThisScope(
+ current_position.ComputeContainerNode())) {
+ if (ancestor->contains(start_container_node))
+ return Position::AfterNode(*ancestor);
+ return Position::BeforeNode(*ancestor);
+ }
+
+ if (Node* last_child = tree_scope.RootNode().lastChild())
+ return Position::AfterNode(*last_child);
+
+ return Position();
+ }
+
+ static PositionInFlatTree AdjustPositionInFlatTreeForEnd(
+ const PositionInFlatTree& position,
+ Node* shadow_host) {
+ if (IsEnclosedBy(position, *shadow_host)) {
+ if (position.IsAfterChildren())
+ return PositionInFlatTree::AfterNode(*shadow_host);
+ return PositionInFlatTree::BeforeNode(*shadow_host);
+ }
+
+ // We use |lastChild|'s after instead of afterAllChildren for backward
+ // compatibility. The positions are same but the anchors would be different,
+ // and selection painting uses anchor nodes.
+ if (Node* last_child = FlatTreeTraversal::LastChild(*shadow_host))
+ return PositionInFlatTree::AfterNode(*last_child);
+ return PositionInFlatTree();
+ }
+
+ static Position AdjustPositionForStart(const Position& current_position,
+ Node* end_container_node) {
+ TreeScope& tree_scope = end_container_node->GetTreeScope();
+
+ DCHECK(current_position.ComputeContainerNode()->GetTreeScope() !=
+ tree_scope);
+
+ if (Node* ancestor = tree_scope.AncestorInThisScope(
+ current_position.ComputeContainerNode())) {
+ if (ancestor->contains(end_container_node))
+ return Position::BeforeNode(*ancestor);
+ return Position::AfterNode(*ancestor);
+ }
+
+ if (Node* first_child = tree_scope.RootNode().firstChild())
+ return Position::BeforeNode(*first_child);
+
+ return Position();
+ }
+
+ // TODO(hajimehoshi): Checking treeScope is wrong when a node is
+ // distributed, but we leave it as it is for backward compatibility.
+ static bool IsCrossingShadowBoundaries(const EphemeralRange& range) {
+ DCHECK(range.IsNotNull());
+ return range.StartPosition().AnchorNode()->GetTreeScope() !=
+ range.EndPosition().AnchorNode()->GetTreeScope();
+ }
+
+ static Position AdjustSelectionStartToAvoidCrossingShadowBoundaries(
+ const EphemeralRange& range) {
+ DCHECK(range.IsNotNull());
+ if (!IsCrossingShadowBoundaries(range))
+ return range.StartPosition();
+ return AdjustPositionForStart(range.StartPosition(),
+ range.EndPosition().ComputeContainerNode());
+ }
+
+ static Position AdjustSelectionEndToAvoidCrossingShadowBoundaries(
+ const EphemeralRange& range) {
+ DCHECK(range.IsNotNull());
+ if (!IsCrossingShadowBoundaries(range))
+ return range.EndPosition();
+ return AdjustPositionForEnd(range.EndPosition(),
+ range.StartPosition().ComputeContainerNode());
+ }
+
+ static PositionInFlatTree AdjustSelectionStartToAvoidCrossingShadowBoundaries(
+ const EphemeralRangeInFlatTree& range) {
+ Node* const shadow_host_start =
+ EnclosingShadowHostForStart(range.StartPosition());
+ Node* const shadow_host_end =
+ EnclosingShadowHostForEnd(range.EndPosition());
+ if (shadow_host_start == shadow_host_end)
+ return range.StartPosition();
+ Node* const shadow_host =
+ shadow_host_end ? shadow_host_end : shadow_host_start;
+ return AdjustPositionInFlatTreeForStart(range.StartPosition(), shadow_host);
+ }
+
+ static PositionInFlatTree AdjustSelectionEndToAvoidCrossingShadowBoundaries(
+ const EphemeralRangeInFlatTree& range) {
+ Node* const shadow_host_start =
+ EnclosingShadowHostForStart(range.StartPosition());
+ Node* const shadow_host_end =
+ EnclosingShadowHostForEnd(range.EndPosition());
+ if (shadow_host_start == shadow_host_end)
+ return range.EndPosition();
+ Node* const shadow_host =
+ shadow_host_start ? shadow_host_start : shadow_host_end;
+ return AdjustPositionInFlatTreeForEnd(range.EndPosition(), shadow_host);
+ }
+};
+
+SelectionInDOMTree
+SelectionAdjuster::AdjustSelectionToAvoidCrossingShadowBoundaries(
+ const SelectionInDOMTree& selection) {
+ return ShadowBoundaryAdjuster::AdjustSelection(selection);
+}
+SelectionInFlatTree
+SelectionAdjuster::AdjustSelectionToAvoidCrossingShadowBoundaries(
+ const SelectionInFlatTree& selection) {
+ return ShadowBoundaryAdjuster::AdjustSelection(selection);
+}
+
+class EditingBoundaryAdjuster final {
+ STATIC_ONLY(EditingBoundaryAdjuster);
+
+ public:
+ template <typename Strategy>
+ static SelectionTemplate<Strategy> AdjustSelection(
+ const SelectionTemplate<Strategy>& shadow_adjusted_selection) {
+ // TODO(editing-dev): Refactor w/o EphemeralRange.
+ const EphemeralRangeTemplate<Strategy> shadow_adjusted_range =
+ shadow_adjusted_selection.ComputeRange();
+ const EphemeralRangeTemplate<Strategy> editing_adjusted_range =
+ AdjustSelectionToAvoidCrossingEditingBoundaries(
+ shadow_adjusted_range, shadow_adjusted_selection.Base());
+ return ComputeAdjustedSelection(shadow_adjusted_selection,
+ editing_adjusted_range);
+ }
+
+ private:
+ static Element* LowestEditableAncestor(Node* node) {
+ while (node) {
+ if (HasEditableStyle(*node))
+ return RootEditableElement(*node);
+ if (IsHTMLBodyElement(*node))
+ break;
+ node = node->parentNode();
+ }
+
+ return nullptr;
+ }
+
+ // Returns true if |position| is editable or its lowest editable root is not
+ // |base_editable_ancestor|.
+ template <typename Strategy>
+ static bool ShouldContinueSearchEditingBoundary(
+ const PositionTemplate<Strategy>& position,
+ Element* base_editable_ancestor) {
+ if (position.IsNull())
+ return false;
+ if (IsEditablePosition(position))
+ return true;
+ return LowestEditableAncestor(position.ComputeContainerNode()) !=
+ base_editable_ancestor;
+ }
+
+ template <typename Strategy>
+ static bool ShouldAdjustPositionToAvoidCrossingEditingBoundaries(
+ const PositionTemplate<Strategy>& position,
+ const ContainerNode* editable_root,
+ const Element* base_editable_ancestor) {
+ if (editable_root)
+ return true;
+ Element* const editable_ancestor =
+ LowestEditableAncestor(position.ComputeContainerNode());
+ return editable_ancestor != base_editable_ancestor;
+ }
+
+ // The selection ends in editable content or non-editable content inside a
+ // different editable ancestor, move backward until non-editable content
+ // inside the same lowest editable ancestor is reached.
+ template <typename Strategy>
+ static PositionTemplate<Strategy>
+ AdjustSelectionEndToAvoidCrossingEditingBoundaries(
+ const PositionTemplate<Strategy>& end,
+ ContainerNode* end_root,
+ Element* base_editable_ancestor) {
+ if (ShouldAdjustPositionToAvoidCrossingEditingBoundaries(
+ end, end_root, base_editable_ancestor)) {
+ PositionTemplate<Strategy> position =
+ PreviousVisuallyDistinctCandidate(end);
+ Element* shadow_ancestor =
+ end_root ? end_root->OwnerShadowHost() : nullptr;
+ if (position.IsNull() && shadow_ancestor)
+ position = PositionTemplate<Strategy>::AfterNode(*shadow_ancestor);
+ while (ShouldContinueSearchEditingBoundary(position,
+ base_editable_ancestor)) {
+ Element* root = RootEditableElementOf(position);
+ shadow_ancestor = root ? root->OwnerShadowHost() : nullptr;
+ position = IsAtomicNode(position.ComputeContainerNode())
+ ? PositionTemplate<Strategy>::InParentBeforeNode(
+ *position.ComputeContainerNode())
+ : PreviousVisuallyDistinctCandidate(position);
+ if (position.IsNull() && shadow_ancestor)
+ position = PositionTemplate<Strategy>::AfterNode(*shadow_ancestor);
+ }
+ return CreateVisiblePosition(position).DeepEquivalent();
+ }
+ return end;
+ }
+
+ // The selection starts in editable content or non-editable content inside a
+ // different editable ancestor, move forward until non-editable content inside
+ // the same lowest editable ancestor is reached.
+ template <typename Strategy>
+ static PositionTemplate<Strategy>
+ AdjustSelectionStartToAvoidCrossingEditingBoundaries(
+ const PositionTemplate<Strategy>& start,
+ ContainerNode* start_root,
+ Element* base_editable_ancestor) {
+ if (ShouldAdjustPositionToAvoidCrossingEditingBoundaries(
+ start, start_root, base_editable_ancestor)) {
+ PositionTemplate<Strategy> position =
+ NextVisuallyDistinctCandidate(start);
+ Element* shadow_ancestor =
+ start_root ? start_root->OwnerShadowHost() : nullptr;
+ if (position.IsNull() && shadow_ancestor)
+ position = PositionTemplate<Strategy>::BeforeNode(*shadow_ancestor);
+ while (ShouldContinueSearchEditingBoundary(position,
+ base_editable_ancestor)) {
+ Element* root = RootEditableElementOf(position);
+ shadow_ancestor = root ? root->OwnerShadowHost() : nullptr;
+ position = IsAtomicNode(position.ComputeContainerNode())
+ ? PositionTemplate<Strategy>::InParentAfterNode(
+ *position.ComputeContainerNode())
+ : NextVisuallyDistinctCandidate(position);
+ if (position.IsNull() && shadow_ancestor)
+ position = PositionTemplate<Strategy>::BeforeNode(*shadow_ancestor);
+ }
+ return CreateVisiblePosition(position).DeepEquivalent();
+ }
+ return start;
+ }
+
+ template <typename Strategy>
+ static EphemeralRangeTemplate<Strategy>
+ AdjustSelectionToAvoidCrossingEditingBoundaries(
+ const EphemeralRangeTemplate<Strategy>& range,
+ const PositionTemplate<Strategy>& base) {
+ DCHECK(base.IsNotNull());
+ DCHECK(range.IsNotNull());
+
+ ContainerNode* base_root = HighestEditableRoot(base);
+ ContainerNode* start_root = HighestEditableRoot(range.StartPosition());
+ ContainerNode* end_root = HighestEditableRoot(range.EndPosition());
+
+ Element* base_editable_ancestor =
+ LowestEditableAncestor(base.ComputeContainerNode());
+
+ // The base, start and end are all in the same region. No adjustment
+ // necessary.
+ if (base_root == start_root && base_root == end_root)
+ return range;
+
+ // The selection is based in editable content.
+ if (base_root) {
+ // If the start is outside the base's editable root, cap it at the start
+ // of that root. If the start is in non-editable content that is inside
+ // the base's editable root, put it at the first editable position after
+ // start inside the base's editable root.
+ PositionTemplate<Strategy> start = range.StartPosition();
+ if (start_root != base_root) {
+ const VisiblePositionTemplate<Strategy> first =
+ FirstEditableVisiblePositionAfterPositionInRoot(start, *base_root);
+ start = first.DeepEquivalent();
+ if (start.IsNull()) {
+ NOTREACHED();
+ return {};
+ }
+ }
+ // If the end is outside the base's editable root, cap it at the end of
+ // that root. If the end is in non-editable content that is inside the
+ // base's root, put it at the last editable position before the end inside
+ // the base's root.
+ PositionTemplate<Strategy> end = range.EndPosition();
+ if (end_root != base_root) {
+ const VisiblePositionTemplate<Strategy> last =
+ LastEditableVisiblePositionBeforePositionInRoot(end, *base_root);
+ end = last.DeepEquivalent();
+ if (end.IsNull())
+ end = start;
+ }
+ return {start, end};
+ }
+
+ // The selection is based in non-editable content.
+ // FIXME: Non-editable pieces inside editable content should be atomic, in
+ // the same way that editable pieces in non-editable content are atomic.
+ const PositionTemplate<Strategy>& end =
+ AdjustSelectionEndToAvoidCrossingEditingBoundaries(
+ range.EndPosition(), end_root, base_editable_ancestor);
+ if (end.IsNull()) {
+ // The selection crosses an Editing boundary. This is a
+ // programmer error in the editing code. Happy debugging!
+ NOTREACHED();
+ return {};
+ }
+
+ const PositionTemplate<Strategy>& start =
+ AdjustSelectionStartToAvoidCrossingEditingBoundaries(
+ range.StartPosition(), start_root, base_editable_ancestor);
+ if (start.IsNull()) {
+ // The selection crosses an Editing boundary. This is a
+ // programmer error in the editing code. Happy debugging!
+ NOTREACHED();
+ return {};
+ }
+ return {start, end};
+ }
+};
+
+SelectionInDOMTree
+SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries(
+ const SelectionInDOMTree& selection) {
+ return EditingBoundaryAdjuster::AdjustSelection(selection);
+}
+SelectionInFlatTree
+SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries(
+ const SelectionInFlatTree& selection) {
+ return EditingBoundaryAdjuster::AdjustSelection(selection);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_adjuster.h b/chromium/third_party/blink/renderer/core/editing/selection_adjuster.h
new file mode 100644
index 00000000000..f1261cf2c88
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_adjuster.h
@@ -0,0 +1,40 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_ADJUSTER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_ADJUSTER_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/text_granularity.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+
+namespace blink {
+
+// |SelectionAdjuster| adjusts positions in |VisibleSelection| directly without
+// calling |validate()|. Users of |SelectionAdjuster| should keep invariant of
+// |VisibleSelection|, e.g. all positions are canonicalized.
+class CORE_EXPORT SelectionAdjuster final {
+ STATIC_ONLY(SelectionAdjuster);
+
+ public:
+ static SelectionInDOMTree AdjustSelectionRespectingGranularity(
+ const SelectionInDOMTree&,
+ TextGranularity);
+ static SelectionInFlatTree AdjustSelectionRespectingGranularity(
+ const SelectionInFlatTree&,
+ TextGranularity);
+ static SelectionInDOMTree AdjustSelectionToAvoidCrossingShadowBoundaries(
+ const SelectionInDOMTree&);
+ static SelectionInFlatTree AdjustSelectionToAvoidCrossingShadowBoundaries(
+ const SelectionInFlatTree&);
+ static SelectionInDOMTree AdjustSelectionToAvoidCrossingEditingBoundaries(
+ const SelectionInDOMTree&);
+ static SelectionInFlatTree AdjustSelectionToAvoidCrossingEditingBoundaries(
+ const SelectionInFlatTree&);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_ADJUSTER_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_adjuster_test.cc b/chromium/third_party/blink/renderer/core/editing/selection_adjuster_test.cc
new file mode 100644
index 00000000000..6fe4e71d222
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_adjuster_test.cc
@@ -0,0 +1,40 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/selection_adjuster.h"
+
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+
+namespace blink {
+
+class SelectionAdjusterTest : public EditingTestBase {};
+
+TEST_F(SelectionAdjusterTest, AdjustShadowToCollpasedInDOMTree) {
+ const SelectionInDOMTree& selection = SetSelectionTextToBody(
+ "<span><template data-mode=\"open\">a|bc</template></span>^");
+ const SelectionInDOMTree& result =
+ SelectionAdjuster::AdjustSelectionToAvoidCrossingShadowBoundaries(
+ selection);
+ EXPECT_EQ("<span></span>|", GetSelectionTextFromBody(result));
+}
+
+TEST_F(SelectionAdjusterTest, AdjustShadowToCollpasedInFlatTree) {
+ SetBodyContent("<input value=abc>");
+ const auto& input = ToTextControl(*GetDocument().QuerySelector("input"));
+ const SelectionInFlatTree& selection =
+ SelectionInFlatTree::Builder()
+ .Collapse(PositionInFlatTree::AfterNode(input))
+ .Extend(
+ PositionInFlatTree(*input.InnerEditorElement()->firstChild(), 1))
+ .Build();
+ const SelectionInFlatTree& result =
+ SelectionAdjuster::AdjustSelectionToAvoidCrossingShadowBoundaries(
+ selection);
+ EXPECT_EQ("<input value=\"abc\"><div>abc</div></input>|",
+ GetSelectionTextInFlatTreeFromBody(result));
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_controller.cc b/chromium/third_party/blink/renderer/core/editing/selection_controller.cc
new file mode 100644
index 00000000000..176dd56acaa
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_controller.cc
@@ -0,0 +1,1362 @@
+/*
+ * Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Apple Inc. All rights
+ * reserved.
+ * Copyright (C) 2006 Alexey Proskuryakov (ap@webkit.org)
+ * Copyright (C) 2012 Digia Plc. and/or its subsidiary(-ies)
+ * Copyright (C) 2015 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/selection_controller.h"
+
+#include "third_party/blink/public/platform/web_menu_source_type.h"
+#include "third_party/blink/public/web/web_selection.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/events/event.h"
+#include "third_party/blink/renderer/core/editing/editing_boundary.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+#include "third_party/blink/renderer/core/editing/rendered_position.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/set_selection_options.h"
+#include "third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/input/event_handler.h"
+#include "third_party/blink/renderer/core/layout/layout_view.h"
+#include "third_party/blink/renderer/core/page/focus_controller.h"
+#include "third_party/blink/renderer/core/page/page.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+#include "third_party/blink/renderer/platform/wtf/auto_reset.h"
+
+namespace blink {
+SelectionController* SelectionController::Create(LocalFrame& frame) {
+ return new SelectionController(frame);
+}
+
+SelectionController::SelectionController(LocalFrame& frame)
+ : frame_(&frame),
+ mouse_down_may_start_select_(false),
+ mouse_down_was_single_click_in_selection_(false),
+ mouse_down_allows_multi_click_(false),
+ selection_state_(SelectionState::kHaveNotStartedSelection) {}
+
+void SelectionController::Trace(blink::Visitor* visitor) {
+ visitor->Trace(frame_);
+ visitor->Trace(original_base_in_flat_tree_);
+ DocumentShutdownObserver::Trace(visitor);
+}
+
+namespace {
+
+DispatchEventResult DispatchSelectStart(Node* node) {
+ if (!node || !node->GetLayoutObject())
+ return DispatchEventResult::kNotCanceled;
+
+ return node->DispatchEvent(
+ Event::CreateCancelableBubble(EventTypeNames::selectstart));
+}
+
+SelectionInFlatTree ExpandSelectionToRespectUserSelectAll(
+ Node* target_node,
+ const SelectionInFlatTree& selection) {
+ if (selection.IsNone())
+ return SelectionInFlatTree();
+ Node* const root_user_select_all =
+ EditingInFlatTreeStrategy::RootUserSelectAllForNode(target_node);
+ if (!root_user_select_all)
+ return selection;
+ return SelectionInFlatTree::Builder(selection)
+ .Collapse(MostBackwardCaretPosition(
+ PositionInFlatTree::BeforeNode(*root_user_select_all),
+ kCanCrossEditingBoundary))
+ .Extend(MostForwardCaretPosition(
+ PositionInFlatTree::AfterNode(*root_user_select_all),
+ kCanCrossEditingBoundary))
+ .Build();
+}
+
+static int TextDistance(const PositionInFlatTree& start,
+ const PositionInFlatTree& end) {
+ return TextIteratorInFlatTree::RangeLength(
+ start, end,
+ TextIteratorBehavior::AllVisiblePositionsRangeLengthBehavior());
+}
+
+bool CanMouseDownStartSelect(Node* node) {
+ if (!node || !node->GetLayoutObject())
+ return true;
+
+ if (!node->CanStartSelection())
+ return false;
+
+ return true;
+}
+
+VisiblePositionInFlatTree VisiblePositionOfHitTestResult(
+ const HitTestResult& hit_test_result) {
+ return CreateVisiblePosition(FromPositionInDOMTree<EditingInFlatTreeStrategy>(
+ hit_test_result.InnerNode()->GetLayoutObject()->PositionForPoint(
+ hit_test_result.LocalPoint())));
+}
+
+DocumentMarker* SpellCheckMarkerAtPosition(
+ DocumentMarkerController& document_marker_controller,
+ const Position& position) {
+ const Node* const node = position.ComputeContainerNode();
+ if (!node->IsTextNode())
+ return nullptr;
+
+ const unsigned offset = position.ComputeOffsetInContainerNode();
+ return document_marker_controller.FirstMarkerIntersectingOffsetRange(
+ *ToText(node), offset, offset, DocumentMarker::MisspellingMarkers());
+}
+
+} // namespace
+
+SelectionInFlatTree AdjustSelectionWithTrailingWhitespace(
+ const SelectionInFlatTree& selection) {
+ if (selection.IsNone())
+ return selection;
+ if (!selection.IsRange())
+ return selection;
+ const bool base_is_first =
+ selection.Base() == selection.ComputeStartPosition();
+ const PositionInFlatTree& end =
+ base_is_first ? selection.Extent() : selection.Base();
+ DCHECK_EQ(end, selection.ComputeEndPosition());
+ const PositionInFlatTree& new_end = SkipWhitespace(end);
+ if (end == new_end)
+ return selection;
+ if (base_is_first) {
+ return SelectionInFlatTree::Builder(selection)
+ .SetBaseAndExtent(selection.Base(), new_end)
+ .Build();
+ }
+ return SelectionInFlatTree::Builder(selection)
+ .SetBaseAndExtent(new_end, selection.Extent())
+ .Build();
+}
+
+SelectionController::~SelectionController() = default;
+
+Document& SelectionController::GetDocument() const {
+ DCHECK(frame_->GetDocument());
+ return *frame_->GetDocument();
+}
+
+void SelectionController::ContextDestroyed(Document*) {
+ original_base_in_flat_tree_ = PositionInFlatTreeWithAffinity();
+}
+
+static PositionInFlatTree AdjustPositionRespectUserSelectAll(
+ Node* inner_node,
+ const PositionInFlatTree& selection_start,
+ const PositionInFlatTree& selection_end,
+ const PositionInFlatTree& position) {
+ const VisibleSelectionInFlatTree& selection_in_user_select_all =
+ CreateVisibleSelection(ExpandSelectionToRespectUserSelectAll(
+ inner_node,
+ position.IsNull()
+ ? SelectionInFlatTree()
+ : SelectionInFlatTree::Builder().Collapse(position).Build()));
+ if (!selection_in_user_select_all.IsRange())
+ return position;
+ if (selection_in_user_select_all.Start().CompareTo(selection_start) < 0)
+ return selection_in_user_select_all.Start();
+ if (selection_end.CompareTo(selection_in_user_select_all.End()) < 0)
+ return selection_in_user_select_all.End();
+ return position;
+}
+
+static PositionInFlatTree ComputeStartFromEndForExtendForward(
+ const PositionInFlatTree& end,
+ TextGranularity granularity) {
+ if (granularity == TextGranularity::kCharacter)
+ return end;
+ // |ComputeStartRespectingGranularity()| returns next word/paragraph for
+ // end of word/paragraph position. To get start of word/paragraph at |end|,
+ // we pass previous position of |end|.
+ return ComputeStartRespectingGranularity(
+ PositionInFlatTreeWithAffinity(
+ PreviousPositionOf(CreateVisiblePosition(end),
+ kCannotCrossEditingBoundary)
+ .DeepEquivalent()),
+ granularity);
+}
+
+static SelectionInFlatTree ExtendSelectionAsDirectional(
+ const PositionInFlatTree& position,
+ const SelectionInFlatTree& selection,
+ TextGranularity granularity) {
+ DCHECK(!selection.IsNone());
+ DCHECK(position.IsNotNull());
+ const PositionInFlatTree& start = selection.ComputeStartPosition();
+ const PositionInFlatTree& end = selection.ComputeEndPosition();
+ const PositionInFlatTree& base = selection.IsBaseFirst() ? start : end;
+ if (position < base) {
+ // Extend backward yields backward selection
+ // - forward selection: *abc ^def ghi| => |abc def^ ghi
+ // - backward selection: *abc |def ghi^ => |abc def ghi^
+ const PositionInFlatTree& new_start = ComputeStartRespectingGranularity(
+ PositionInFlatTreeWithAffinity(position), granularity);
+ const PositionInFlatTree& new_end =
+ selection.IsBaseFirst()
+ ? ComputeEndRespectingGranularity(
+ new_start, PositionInFlatTreeWithAffinity(start), granularity)
+ : end;
+ return SelectionInFlatTree::Builder()
+ .SetBaseAndExtent(new_end, new_start)
+ .Build();
+ }
+
+ // Extend forward yields forward selection
+ // - forward selection: ^abc def| ghi* => ^abc def ghi|
+ // - backward selection: |abc def^ ghi* => abc ^def ghi|
+ const PositionInFlatTree& new_start =
+ selection.IsBaseFirst()
+ ? start
+ : ComputeStartFromEndForExtendForward(end, granularity);
+ const PositionInFlatTree& new_end = ComputeEndRespectingGranularity(
+ new_start, PositionInFlatTreeWithAffinity(position), granularity);
+ return SelectionInFlatTree::Builder()
+ .SetBaseAndExtent(new_start, new_end)
+ .Build();
+}
+
+static SelectionInFlatTree ExtendSelectionAsNonDirectional(
+ const PositionInFlatTree& position,
+ const SelectionInFlatTree& selection,
+ TextGranularity granularity) {
+ DCHECK(!selection.IsNone());
+ DCHECK(position.IsNotNull());
+ // Shift+Click deselects when selection was created right-to-left
+ const PositionInFlatTree& start = selection.ComputeStartPosition();
+ const PositionInFlatTree& end = selection.ComputeEndPosition();
+ if (position < start) {
+ return SelectionInFlatTree::Builder()
+ .SetBaseAndExtent(
+ end, ComputeStartRespectingGranularity(
+ PositionInFlatTreeWithAffinity(position), granularity))
+ .Build();
+ }
+ if (end < position) {
+ return SelectionInFlatTree::Builder()
+ .SetBaseAndExtent(
+ start,
+ ComputeEndRespectingGranularity(
+ start, PositionInFlatTreeWithAffinity(position), granularity))
+ .Build();
+ }
+ const int distance_to_start = TextDistance(start, position);
+ const int distance_to_end = TextDistance(position, end);
+ if (distance_to_start <= distance_to_end) {
+ return SelectionInFlatTree::Builder()
+ .SetBaseAndExtent(
+ end, ComputeStartRespectingGranularity(
+ PositionInFlatTreeWithAffinity(position), granularity))
+ .Build();
+ }
+ return SelectionInFlatTree::Builder()
+ .SetBaseAndExtent(
+ start,
+ ComputeEndRespectingGranularity(
+ start, PositionInFlatTreeWithAffinity(position), granularity))
+ .Build();
+}
+
+// Updating the selection is considered side-effect of the event and so it
+// doesn't impact the handled state.
+bool SelectionController::HandleSingleClick(
+ const MouseEventWithHitTestResults& event) {
+ TRACE_EVENT0("blink",
+ "SelectionController::handleMousePressEventSingleClick");
+
+ DCHECK(!frame_->GetDocument()->NeedsLayoutTreeUpdate());
+ Node* inner_node = event.InnerNode();
+ if (!(inner_node && inner_node->GetLayoutObject() &&
+ mouse_down_may_start_select_))
+ return false;
+
+ // Extend the selection if the Shift key is down, unless the click is in a
+ // link or image.
+ bool extend_selection = IsExtendingSelection(event);
+
+ const VisiblePositionInFlatTree& visible_hit_position =
+ VisiblePositionOfHitTestResult(event.GetHitTestResult());
+ const PositionInFlatTreeWithAffinity& position_to_use =
+ visible_hit_position.IsNull()
+ ? CreateVisiblePosition(
+ PositionInFlatTree::FirstPositionInOrBeforeNode(*inner_node))
+ .ToPositionWithAffinity()
+ : visible_hit_position.ToPositionWithAffinity();
+ const VisibleSelectionInFlatTree& selection =
+ this->Selection().ComputeVisibleSelectionInFlatTree();
+
+ // Don't restart the selection when the mouse is pressed on an
+ // existing selection so we can allow for text dragging.
+ if (LocalFrameView* view = frame_->View()) {
+ const LayoutPoint v_point = view->RootFrameToContents(
+ FlooredIntPoint(event.Event().PositionInRootFrame()));
+ if (!extend_selection && this->Selection().Contains(v_point)) {
+ mouse_down_was_single_click_in_selection_ = true;
+ if (!event.Event().FromTouch())
+ return false;
+
+ if (HandleTapInsideSelection(event, selection.AsSelection()))
+ return false;
+ }
+ }
+
+ if (extend_selection && !selection.IsNone()) {
+ // Note: "fast/events/shift-click-user-select-none.html" makes
+ // |pos.isNull()| true.
+ const PositionInFlatTree& adjusted_position =
+ AdjustPositionRespectUserSelectAll(inner_node, selection.Start(),
+ selection.End(),
+ position_to_use.GetPosition());
+ const TextGranularity granularity = Selection().Granularity();
+ if (adjusted_position.IsNull()) {
+ UpdateSelectionForMouseDownDispatchingSelectStart(
+ inner_node, selection.AsSelection(),
+ SetSelectionOptions::Builder().SetGranularity(granularity).Build());
+ return false;
+ }
+ UpdateSelectionForMouseDownDispatchingSelectStart(
+ inner_node,
+ frame_->GetEditor().Behavior().ShouldConsiderSelectionAsDirectional()
+ ? ExtendSelectionAsDirectional(adjusted_position,
+ selection.AsSelection(), granularity)
+ : ExtendSelectionAsNonDirectional(
+ adjusted_position, selection.AsSelection(), granularity),
+ SetSelectionOptions::Builder().SetGranularity(granularity).Build());
+ return false;
+ }
+
+ if (selection_state_ == SelectionState::kExtendedSelection) {
+ UpdateSelectionForMouseDownDispatchingSelectStart(
+ inner_node, selection.AsSelection(), SetSelectionOptions());
+ return false;
+ }
+
+ if (position_to_use.IsNull()) {
+ UpdateSelectionForMouseDownDispatchingSelectStart(
+ inner_node, SelectionInFlatTree(), SetSelectionOptions());
+ return false;
+ }
+
+ bool is_handle_visible = false;
+ const bool has_editable_style = HasEditableStyle(*inner_node);
+ if (has_editable_style) {
+ const bool is_text_box_empty =
+ !RootEditableElement(*inner_node)->HasChildren();
+ const bool not_left_click =
+ event.Event().button != WebPointerProperties::Button::kLeft;
+ if (!is_text_box_empty || not_left_click)
+ is_handle_visible = event.Event().FromTouch();
+ }
+
+ // This applies the JavaScript selectstart handler, which can change the DOM.
+ // SelectionControllerTest_SelectStartHandlerRemovesElement makes this return
+ // false.
+ if (!UpdateSelectionForMouseDownDispatchingSelectStart(
+ inner_node,
+ ExpandSelectionToRespectUserSelectAll(
+ inner_node,
+ SelectionInFlatTree::Builder().Collapse(position_to_use).Build()),
+ SetSelectionOptions::Builder()
+ .SetShouldShowHandle(is_handle_visible)
+ .Build())) {
+ // UpdateSelectionForMouseDownDispatchingSelectStart() returns false when
+ // the selectstart handler has prevented the default selection behavior from
+ // occurring.
+ return false;
+ }
+
+ // SelectionControllerTest_SetCaretAtHitTestResultWithDisconnectedPosition
+ // makes the IsValidFor() check fail.
+ if (has_editable_style && event.Event().FromTouch() &&
+ position_to_use.IsValidFor(*frame_->GetDocument())) {
+ frame_->GetTextSuggestionController().HandlePotentialSuggestionTap(
+ position_to_use.GetPosition());
+ }
+
+ return false;
+}
+
+// Returns true if the tap is processed.
+bool SelectionController::HandleTapInsideSelection(
+ const MouseEventWithHitTestResults& event,
+ const SelectionInFlatTree& selection) {
+ if (Selection().ShouldShrinkNextTap()) {
+ const bool did_select = SelectClosestWordFromHitTestResult(
+ event.GetHitTestResult(), AppendTrailingWhitespace::kDontAppend,
+ SelectInputEventType::kTouch);
+ if (did_select) {
+ frame_->GetEventHandler().ShowNonLocatedContextMenu(
+ nullptr, kMenuSourceAdjustSelectionReset);
+ }
+ return true;
+ }
+
+ if (Selection().IsHandleVisible())
+ return false;
+
+ const bool did_select = UpdateSelectionForMouseDownDispatchingSelectStart(
+ event.InnerNode(), selection,
+ SetSelectionOptions::Builder().SetShouldShowHandle(true).Build());
+ if (did_select) {
+ frame_->GetEventHandler().ShowNonLocatedContextMenu(nullptr,
+ kMenuSourceTouch);
+ }
+ return true;
+}
+
+// Returns true if selection starts from |SVGText| node and |target_node| is
+// not the containing block of |SVGText| node.
+// See https://bugs.webkit.org/show_bug.cgi?id=12334 for details.
+static bool ShouldRespectSVGTextBoundaries(
+ const Node& target_node,
+ const FrameSelection& frame_selection) {
+ const PositionInFlatTree& base =
+ frame_selection.ComputeVisibleSelectionInFlatTree().Base();
+ // TODO(editing-dev): We should use |ComputeContainerNode()|.
+ const Node* const base_node = base.AnchorNode();
+ if (!base_node)
+ return false;
+ LayoutObject* const base_layout_object = base_node->GetLayoutObject();
+ if (!base_layout_object || !base_layout_object->IsSVGText())
+ return false;
+ return target_node.GetLayoutObject()->ContainingBlock() !=
+ base_layout_object->ContainingBlock();
+}
+
+void SelectionController::UpdateSelectionForMouseDrag(
+ const HitTestResult& hit_test_result,
+ const LayoutPoint& drag_start_pos,
+ const IntPoint& last_known_mouse_position) {
+ if (!mouse_down_may_start_select_)
+ return;
+
+ Node* target = hit_test_result.InnerNode();
+ if (!target)
+ return;
+
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ frame_->GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ const PositionWithAffinity& raw_target_position =
+ Selection().SelectionHasFocus()
+ ? PositionRespectingEditingBoundary(
+ Selection().ComputeVisibleSelectionInDOMTree().Start(),
+ hit_test_result.LocalPoint(), target)
+ : PositionWithAffinity();
+ VisiblePositionInFlatTree target_position = CreateVisiblePosition(
+ FromPositionInDOMTree<EditingInFlatTreeStrategy>(raw_target_position));
+ // Don't modify the selection if we're not on a node.
+ if (target_position.IsNull())
+ return;
+
+ // Restart the selection if this is the first mouse move. This work is usually
+ // done in handleMousePressEvent, but not if the mouse press was on an
+ // existing selection.
+
+ // Special case to limit selection to the containing block for SVG text.
+ // TODO(editing_dev): Isn't there a better non-SVG-specific way to do this?
+ if (ShouldRespectSVGTextBoundaries(*target, Selection()))
+ return;
+
+ if (selection_state_ == SelectionState::kHaveNotStartedSelection &&
+ DispatchSelectStart(target) != DispatchEventResult::kNotCanceled)
+ return;
+
+ // |DispatchSelectStart()| can change |GetDocument()| or invalidate
+ // target_position by 'selectstart' event handler.
+ // TODO(editing-dev): We should also add a regression test when above
+ // behaviour happens. See crbug.com/775149.
+ if (!Selection().IsAvailable() || !target_position.IsValidFor(GetDocument()))
+ return;
+
+ const bool should_extend_selection =
+ selection_state_ == SelectionState::kExtendedSelection;
+ // Always extend selection here because it's caused by a mouse drag
+ selection_state_ = SelectionState::kExtendedSelection;
+
+ const VisibleSelectionInFlatTree& visible_selection =
+ Selection().ComputeVisibleSelectionInFlatTree();
+ if (visible_selection.IsNone()) {
+ // TODO(editing-dev): This is an urgent fix to crbug.com/745501. We should
+ // find the root cause and replace this by a proper fix.
+ return;
+ }
+
+ const PositionInFlatTree& adjusted_position =
+ AdjustPositionRespectUserSelectAll(target, visible_selection.Start(),
+ visible_selection.End(),
+ target_position.DeepEquivalent());
+ const SelectionInFlatTree& adjusted_selection =
+ should_extend_selection
+ ? ExtendSelectionAsDirectional(adjusted_position,
+ visible_selection.AsSelection(),
+ Selection().Granularity())
+ : SelectionInFlatTree::Builder().Collapse(adjusted_position).Build();
+
+ const bool selection_is_directional =
+ should_extend_selection ? Selection().IsDirectional() : false;
+ SetNonDirectionalSelectionIfNeeded(
+ adjusted_selection,
+ SetSelectionOptions::Builder()
+ .SetGranularity(Selection().Granularity())
+ .SetIsDirectional(selection_is_directional)
+ .Build(),
+ kAdjustEndpointsAtBidiBoundary);
+}
+
+bool SelectionController::UpdateSelectionForMouseDownDispatchingSelectStart(
+ Node* target_node,
+ const SelectionInFlatTree& selection,
+ const SetSelectionOptions& set_selection_options) {
+ if (target_node && target_node->GetLayoutObject() &&
+ !target_node->GetLayoutObject()->IsSelectable())
+ return false;
+
+ {
+ SelectionInFlatTree::InvalidSelectionResetter resetter(selection);
+ if (DispatchSelectStart(target_node) != DispatchEventResult::kNotCanceled)
+ return false;
+ }
+
+ // |dispatchSelectStart()| can change document hosted by |m_frame|.
+ if (!this->Selection().IsAvailable())
+ return false;
+
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+ const VisibleSelectionInFlatTree& visible_selection =
+ CreateVisibleSelection(selection);
+
+ if (visible_selection.IsRange()) {
+ selection_state_ = SelectionState::kExtendedSelection;
+ SetNonDirectionalSelectionIfNeeded(selection, set_selection_options,
+ kDoNotAdjustEndpoints);
+ return true;
+ }
+
+ selection_state_ = SelectionState::kPlacedCaret;
+ SetNonDirectionalSelectionIfNeeded(selection, set_selection_options,
+ kDoNotAdjustEndpoints);
+ return true;
+}
+
+static bool IsEmptyWordRange(const EphemeralRangeInFlatTree range) {
+ const String& str = PlainText(
+ range, TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(
+ HasEditableStyle(*range.StartPosition().AnchorNode()))
+ .Build());
+ return str.IsEmpty() || str.SimplifyWhiteSpace().ContainsOnlyWhitespace();
+}
+
+bool SelectionController::SelectClosestWordFromHitTestResult(
+ const HitTestResult& result,
+ AppendTrailingWhitespace append_trailing_whitespace,
+ SelectInputEventType select_input_event_type) {
+ Node* const inner_node = result.InnerNode();
+
+ if (!inner_node || !inner_node->GetLayoutObject() ||
+ !inner_node->GetLayoutObject()->IsSelectable())
+ return false;
+
+ // Special-case image local offset to always be zero, to avoid triggering
+ // LayoutReplaced::positionFromPoint's advancement of the position at the
+ // mid-point of the the image (which was intended for mouse-drag selection
+ // and isn't desirable for touch).
+ HitTestResult adjusted_hit_test_result = result;
+ if (select_input_event_type == SelectInputEventType::kTouch &&
+ result.GetImage())
+ adjusted_hit_test_result.SetNodeAndPosition(result.InnerNode(),
+ LayoutPoint(0, 0));
+
+ const VisiblePositionInFlatTree& pos =
+ VisiblePositionOfHitTestResult(adjusted_hit_test_result);
+ const VisibleSelectionInFlatTree& new_selection =
+ pos.IsNotNull() ? CreateVisibleSelectionWithGranularity(
+ SelectionInFlatTree::Builder()
+ .Collapse(pos.ToPositionWithAffinity())
+ .Build(),
+ TextGranularity::kWord)
+ : VisibleSelectionInFlatTree();
+
+ // TODO(editing-dev): Fix CreateVisibleSelectionWithGranularity() to not
+ // return invalid ranges. Until we do that, we need this check here to avoid a
+ // renderer crash when we call PlainText() below (see crbug.com/735774).
+ if (new_selection.IsNone() || new_selection.Start() > new_selection.End())
+ return false;
+
+ if (select_input_event_type == SelectInputEventType::kTouch) {
+ // If node doesn't have text except space, tab or line break, do not
+ // select that 'empty' area.
+ EphemeralRangeInFlatTree range(new_selection.Start(), new_selection.End());
+ if (IsEmptyWordRange(range))
+ return false;
+
+ Element* const editable = new_selection.RootEditableElement();
+ if (editable && pos.DeepEquivalent() ==
+ VisiblePositionInFlatTree::LastPositionInNode(*editable)
+ .DeepEquivalent())
+ return false;
+ }
+
+ const SelectionInFlatTree& adjusted_selection =
+ append_trailing_whitespace == AppendTrailingWhitespace::kShouldAppend
+ ? AdjustSelectionWithTrailingWhitespace(new_selection.AsSelection())
+ : new_selection.AsSelection();
+
+ return UpdateSelectionForMouseDownDispatchingSelectStart(
+ inner_node,
+ ExpandSelectionToRespectUserSelectAll(inner_node, adjusted_selection),
+ SetSelectionOptions::Builder()
+ .SetGranularity(TextGranularity::kWord)
+ .SetShouldShowHandle(select_input_event_type ==
+ SelectInputEventType::kTouch)
+ .Build());
+}
+
+void SelectionController::SelectClosestMisspellingFromHitTestResult(
+ const HitTestResult& result,
+ AppendTrailingWhitespace append_trailing_whitespace) {
+ Node* inner_node = result.InnerNode();
+
+ if (!inner_node || !inner_node->GetLayoutObject())
+ return;
+
+ const VisiblePositionInFlatTree& pos = VisiblePositionOfHitTestResult(result);
+ if (pos.IsNull()) {
+ UpdateSelectionForMouseDownDispatchingSelectStart(
+ inner_node, SelectionInFlatTree(),
+ SetSelectionOptions::Builder()
+ .SetGranularity(TextGranularity::kWord)
+ .Build());
+ return;
+ }
+
+ const PositionInFlatTree& marker_position =
+ pos.DeepEquivalent().ParentAnchoredEquivalent();
+ const DocumentMarker* const marker =
+ SpellCheckMarkerAtPosition(inner_node->GetDocument().Markers(),
+ ToPositionInDOMTree(marker_position));
+ if (!marker) {
+ UpdateSelectionForMouseDownDispatchingSelectStart(
+ inner_node, SelectionInFlatTree(),
+ SetSelectionOptions::Builder()
+ .SetGranularity(TextGranularity::kWord)
+ .Build());
+ return;
+ }
+
+ Node* const container_node = marker_position.ComputeContainerNode();
+ const PositionInFlatTree start(container_node, marker->StartOffset());
+ const PositionInFlatTree end(container_node, marker->EndOffset());
+ const VisibleSelectionInFlatTree& new_selection = CreateVisibleSelection(
+ SelectionInFlatTree::Builder().Collapse(start).Extend(end).Build());
+ const SelectionInFlatTree& adjusted_selection =
+ append_trailing_whitespace == AppendTrailingWhitespace::kShouldAppend
+ ? AdjustSelectionWithTrailingWhitespace(new_selection.AsSelection())
+ : new_selection.AsSelection();
+ UpdateSelectionForMouseDownDispatchingSelectStart(
+ inner_node,
+ ExpandSelectionToRespectUserSelectAll(inner_node, adjusted_selection),
+ SetSelectionOptions::Builder()
+ .SetGranularity(TextGranularity::kWord)
+ .Build());
+}
+
+bool SelectionController::SelectClosestWordFromMouseEvent(
+ const MouseEventWithHitTestResults& result) {
+ if (!mouse_down_may_start_select_)
+ return false;
+
+ AppendTrailingWhitespace append_trailing_whitespace =
+ (result.Event().click_count == 2 &&
+ frame_->GetEditor().IsSelectTrailingWhitespaceEnabled())
+ ? AppendTrailingWhitespace::kShouldAppend
+ : AppendTrailingWhitespace::kDontAppend;
+
+ DCHECK(!frame_->GetDocument()->NeedsLayoutTreeUpdate());
+
+ return SelectClosestWordFromHitTestResult(
+ result.GetHitTestResult(), append_trailing_whitespace,
+ result.Event().FromTouch() ? SelectInputEventType::kTouch
+ : SelectInputEventType::kMouse);
+}
+
+void SelectionController::SelectClosestMisspellingFromMouseEvent(
+ const MouseEventWithHitTestResults& result) {
+ if (!mouse_down_may_start_select_)
+ return;
+
+ SelectClosestMisspellingFromHitTestResult(
+ result.GetHitTestResult(),
+ (result.Event().click_count == 2 &&
+ frame_->GetEditor().IsSelectTrailingWhitespaceEnabled())
+ ? AppendTrailingWhitespace::kShouldAppend
+ : AppendTrailingWhitespace::kDontAppend);
+}
+
+void SelectionController::SelectClosestWordOrLinkFromMouseEvent(
+ const MouseEventWithHitTestResults& result) {
+ if (!result.GetHitTestResult().IsLiveLink()) {
+ SelectClosestWordFromMouseEvent(result);
+ return;
+ }
+
+ Node* const inner_node = result.InnerNode();
+
+ if (!inner_node || !inner_node->GetLayoutObject() ||
+ !mouse_down_may_start_select_)
+ return;
+
+ Element* url_element = result.GetHitTestResult().URLElement();
+ const VisiblePositionInFlatTree pos =
+ VisiblePositionOfHitTestResult(result.GetHitTestResult());
+ const SelectionInFlatTree& new_selection =
+ pos.IsNotNull() &&
+ pos.DeepEquivalent().AnchorNode()->IsDescendantOf(url_element)
+ ? SelectionInFlatTree::Builder()
+ .SelectAllChildren(*url_element)
+ .Build()
+ : SelectionInFlatTree();
+
+ UpdateSelectionForMouseDownDispatchingSelectStart(
+ inner_node,
+ ExpandSelectionToRespectUserSelectAll(inner_node, new_selection),
+ SetSelectionOptions::Builder()
+ .SetGranularity(TextGranularity::kWord)
+ .Build());
+}
+
+static SelectionInFlatTree AdjustEndpointsAtBidiBoundary(
+ const VisiblePositionInFlatTree& visible_base,
+ const VisiblePositionInFlatTree& visible_extent) {
+ DCHECK(visible_base.IsValid());
+ DCHECK(visible_extent.IsValid());
+
+ RenderedPosition base(visible_base);
+ RenderedPosition extent(visible_extent);
+
+ const SelectionInFlatTree& unchanged_selection =
+ SelectionInFlatTree::Builder()
+ .SetBaseAndExtent(visible_base.DeepEquivalent(),
+ visible_extent.DeepEquivalent())
+ .Build();
+
+ if (base.IsNull() || extent.IsNull() || base.IsEquivalent(extent))
+ return unchanged_selection;
+
+ if (base.AtLeftBoundaryOfBidiRun()) {
+ if (!extent.AtRightBoundaryOfBidiRun(base.BidiLevelOnRight()) &&
+ base.IsEquivalent(
+ extent.LeftBoundaryOfBidiRun(base.BidiLevelOnRight()))) {
+ return SelectionInFlatTree::Builder()
+ .SetBaseAndExtent(
+ CreateVisiblePosition(
+ ToPositionInFlatTree(base.PositionAtLeftBoundaryOfBiDiRun()))
+ .DeepEquivalent(),
+ visible_extent.DeepEquivalent())
+ .Build();
+ }
+ return unchanged_selection;
+ }
+
+ if (base.AtRightBoundaryOfBidiRun()) {
+ if (!extent.AtLeftBoundaryOfBidiRun(base.BidiLevelOnLeft()) &&
+ base.IsEquivalent(
+ extent.RightBoundaryOfBidiRun(base.BidiLevelOnLeft()))) {
+ return SelectionInFlatTree::Builder()
+ .SetBaseAndExtent(
+ CreateVisiblePosition(
+ ToPositionInFlatTree(base.PositionAtRightBoundaryOfBiDiRun()))
+ .DeepEquivalent(),
+ visible_extent.DeepEquivalent())
+ .Build();
+ }
+ return unchanged_selection;
+ }
+
+ if (extent.AtLeftBoundaryOfBidiRun() &&
+ extent.IsEquivalent(
+ base.LeftBoundaryOfBidiRun(extent.BidiLevelOnRight()))) {
+ return SelectionInFlatTree::Builder()
+ .SetBaseAndExtent(
+ visible_base.DeepEquivalent(),
+ CreateVisiblePosition(
+ ToPositionInFlatTree(extent.PositionAtLeftBoundaryOfBiDiRun()))
+ .DeepEquivalent())
+ .Build();
+ }
+
+ if (extent.AtRightBoundaryOfBidiRun() &&
+ extent.IsEquivalent(
+ base.RightBoundaryOfBidiRun(extent.BidiLevelOnLeft()))) {
+ return SelectionInFlatTree::Builder()
+ .SetBaseAndExtent(
+ visible_base.DeepEquivalent(),
+ CreateVisiblePosition(
+ ToPositionInFlatTree(extent.PositionAtRightBoundaryOfBiDiRun()))
+ .DeepEquivalent())
+ .Build();
+ }
+ return unchanged_selection;
+}
+
+// TODO(yosin): We should take |granularity| and |handleVisibility| from
+// |newSelection|.
+// We should rename this function to appropriate name because
+// set_selection_options has selection directional value in few cases.
+void SelectionController::SetNonDirectionalSelectionIfNeeded(
+ const SelectionInFlatTree& passed_selection,
+ const SetSelectionOptions& set_selection_options,
+ EndPointsAdjustmentMode endpoints_adjustment_mode) {
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ const VisibleSelectionInFlatTree& new_selection =
+ CreateVisibleSelection(passed_selection);
+ // TODO(editing-dev): We should use |PositionWithAffinity| to pass affinity
+ // to |CreateVisiblePosition()| for |original_base|.
+ const PositionInFlatTree& base_position =
+ original_base_in_flat_tree_.GetPosition();
+ const VisiblePositionInFlatTree& original_base =
+ base_position.IsConnected() ? CreateVisiblePosition(base_position)
+ : VisiblePositionInFlatTree();
+ const VisiblePositionInFlatTree& base =
+ original_base.IsNotNull() ? original_base
+ : CreateVisiblePosition(new_selection.Base());
+ const VisiblePositionInFlatTree& extent =
+ CreateVisiblePosition(new_selection.Extent());
+ const SelectionInFlatTree& adjusted_selection =
+ endpoints_adjustment_mode == kAdjustEndpointsAtBidiBoundary
+ ? AdjustEndpointsAtBidiBoundary(base, extent)
+ : SelectionInFlatTree::Builder()
+ .SetBaseAndExtent(base.DeepEquivalent(),
+ extent.DeepEquivalent())
+ .Build();
+
+ SelectionInFlatTree::Builder builder(new_selection.AsSelection());
+ if (adjusted_selection.Base() != base.DeepEquivalent() ||
+ adjusted_selection.Extent() != extent.DeepEquivalent()) {
+ original_base_in_flat_tree_ = base.ToPositionWithAffinity();
+ SetContext(&GetDocument());
+ builder.SetBaseAndExtent(adjusted_selection.Base(),
+ adjusted_selection.Extent());
+ } else if (original_base.IsNotNull()) {
+ if (CreateVisiblePosition(
+ Selection().ComputeVisibleSelectionInFlatTree().Base())
+ .DeepEquivalent() ==
+ CreateVisiblePosition(new_selection.Base()).DeepEquivalent()) {
+ builder.SetBaseAndExtent(original_base.DeepEquivalent(),
+ new_selection.Extent());
+ }
+ original_base_in_flat_tree_ = PositionInFlatTreeWithAffinity();
+ }
+
+ const bool selection_is_directional =
+ frame_->GetEditor().Behavior().ShouldConsiderSelectionAsDirectional() ||
+ set_selection_options.IsDirectional();
+ const SelectionInFlatTree& selection_in_flat_tree = builder.Build();
+
+ const bool selection_remains_the_same =
+ Selection().ComputeVisibleSelectionInFlatTree() ==
+ CreateVisibleSelection(selection_in_flat_tree) &&
+ Selection().IsHandleVisible() ==
+ set_selection_options.ShouldShowHandle() &&
+ selection_is_directional == Selection().IsDirectional();
+
+ // If selection has not changed we do not clear editing style.
+ if (selection_remains_the_same)
+ return;
+ Selection().SetSelection(
+ ConvertToSelectionInDOMTree(selection_in_flat_tree),
+ SetSelectionOptions::Builder(set_selection_options)
+ .SetShouldCloseTyping(true)
+ .SetShouldClearTypingStyle(true)
+ .SetIsDirectional(selection_is_directional)
+ .SetCursorAlignOnScroll(CursorAlignOnScroll::kIfNeeded)
+ .Build());
+}
+
+void SelectionController::SetCaretAtHitTestResult(
+ const HitTestResult& hit_test_result) {
+ Node* inner_node = hit_test_result.InnerNode();
+ DCHECK(inner_node);
+ const VisiblePositionInFlatTree& visible_hit_pos =
+ VisiblePositionOfHitTestResult(hit_test_result);
+ const VisiblePositionInFlatTree& visible_pos =
+ visible_hit_pos.IsNull()
+ ? CreateVisiblePosition(
+ PositionInFlatTree::FirstPositionInOrBeforeNode(*inner_node))
+ : visible_hit_pos;
+
+ if (visible_pos.IsNull()) {
+ UpdateSelectionForMouseDownDispatchingSelectStart(
+ inner_node, SelectionInFlatTree(),
+ SetSelectionOptions::Builder().SetShouldShowHandle(true).Build());
+ return;
+ }
+ UpdateSelectionForMouseDownDispatchingSelectStart(
+ inner_node,
+ ExpandSelectionToRespectUserSelectAll(
+ inner_node, SelectionInFlatTree::Builder()
+ .Collapse(visible_pos.ToPositionWithAffinity())
+ .Build()),
+ SetSelectionOptions::Builder().SetShouldShowHandle(true).Build());
+}
+
+bool SelectionController::HandleDoubleClick(
+ const MouseEventWithHitTestResults& event) {
+ TRACE_EVENT0("blink",
+ "SelectionController::handleMousePressEventDoubleClick");
+
+ if (!Selection().IsAvailable())
+ return false;
+
+ if (!mouse_down_allows_multi_click_)
+ return HandleSingleClick(event);
+
+ if (event.Event().button != WebPointerProperties::Button::kLeft)
+ return false;
+
+ if (Selection().ComputeVisibleSelectionInDOMTreeDeprecated().IsRange()) {
+ // A double-click when range is already selected
+ // should not change the selection. So, do not call
+ // selectClosestWordFromMouseEvent, but do set
+ // m_beganSelectingText to prevent handleMouseReleaseEvent
+ // from setting caret selection.
+ selection_state_ = SelectionState::kExtendedSelection;
+ return true;
+ }
+ if (!SelectClosestWordFromMouseEvent(event))
+ return true;
+ if (!Selection().IsHandleVisible())
+ return true;
+ frame_->GetEventHandler().ShowNonLocatedContextMenu(nullptr,
+ kMenuSourceTouch);
+ return true;
+}
+
+bool SelectionController::HandleTripleClick(
+ const MouseEventWithHitTestResults& event) {
+ TRACE_EVENT0("blink",
+ "SelectionController::handleMousePressEventTripleClick");
+
+ if (!Selection().IsAvailable()) {
+ // editing/shadow/doubleclick-on-meter-in-shadow-crash.html reach here.
+ return false;
+ }
+
+ if (!mouse_down_allows_multi_click_)
+ return HandleSingleClick(event);
+
+ if (event.Event().button != WebPointerProperties::Button::kLeft)
+ return false;
+
+ Node* const inner_node = event.InnerNode();
+ if (!(inner_node && inner_node->GetLayoutObject() &&
+ mouse_down_may_start_select_))
+ return false;
+
+ const VisiblePositionInFlatTree& pos =
+ VisiblePositionOfHitTestResult(event.GetHitTestResult());
+ const VisibleSelectionInFlatTree new_selection =
+ pos.IsNotNull() ? CreateVisibleSelectionWithGranularity(
+ SelectionInFlatTree::Builder()
+ .Collapse(pos.ToPositionWithAffinity())
+ .Build(),
+ TextGranularity::kParagraph)
+ : VisibleSelectionInFlatTree();
+
+ const bool is_handle_visible =
+ event.Event().FromTouch() && new_selection.IsRange();
+
+ const bool did_select = UpdateSelectionForMouseDownDispatchingSelectStart(
+ inner_node,
+ ExpandSelectionToRespectUserSelectAll(inner_node,
+ new_selection.AsSelection()),
+
+ SetSelectionOptions::Builder()
+ .SetGranularity(TextGranularity::kParagraph)
+ .SetShouldShowHandle(is_handle_visible)
+ .Build());
+ if (!did_select)
+ return false;
+
+ if (!Selection().IsHandleVisible())
+ return true;
+ frame_->GetEventHandler().ShowNonLocatedContextMenu(nullptr,
+ kMenuSourceTouch);
+ return true;
+}
+
+bool SelectionController::HandleMousePressEvent(
+ const MouseEventWithHitTestResults& event) {
+ TRACE_EVENT0("blink", "SelectionController::handleMousePressEvent");
+
+ // If we got the event back, that must mean it wasn't prevented,
+ // so it's allowed to start a drag or selection if it wasn't in a scrollbar.
+ mouse_down_may_start_select_ =
+ (CanMouseDownStartSelect(event.InnerNode()) || IsLinkSelection(event)) &&
+ !event.GetScrollbar();
+ mouse_down_was_single_click_in_selection_ = false;
+ if (!Selection().IsAvailable()) {
+ // "gesture-tap-frame-removed.html" reaches here.
+ mouse_down_allows_multi_click_ = !event.Event().FromTouch();
+ } else {
+ // Avoid double-tap touch gesture confusion by restricting multi-click side
+ // effects, e.g., word selection, to editable regions.
+ mouse_down_allows_multi_click_ =
+ !event.Event().FromTouch() ||
+ IsEditablePosition(
+ Selection().ComputeVisibleSelectionInDOMTreeDeprecated().Start());
+ }
+
+ if (event.Event().click_count >= 3)
+ return HandleTripleClick(event);
+ if (event.Event().click_count == 2)
+ return HandleDoubleClick(event);
+ return HandleSingleClick(event);
+}
+
+void SelectionController::HandleMouseDraggedEvent(
+ const MouseEventWithHitTestResults& event,
+ const IntPoint& mouse_down_pos,
+ const LayoutPoint& drag_start_pos,
+ const IntPoint& last_known_mouse_position) {
+ TRACE_EVENT0("blink", "SelectionController::handleMouseDraggedEvent");
+
+ if (!Selection().IsAvailable())
+ return;
+ if (selection_state_ != SelectionState::kExtendedSelection) {
+ HitTestRequest request(HitTestRequest::kReadOnly | HitTestRequest::kActive);
+ HitTestResult result(request, mouse_down_pos);
+ frame_->GetDocument()->GetLayoutView()->HitTest(result);
+
+ UpdateSelectionForMouseDrag(result, drag_start_pos,
+ last_known_mouse_position);
+ }
+ UpdateSelectionForMouseDrag(event.GetHitTestResult(), drag_start_pos,
+ last_known_mouse_position);
+}
+
+void SelectionController::UpdateSelectionForMouseDrag(
+ const LayoutPoint& drag_start_pos,
+ const IntPoint& last_known_mouse_position) {
+ LocalFrameView* view = frame_->View();
+ if (!view)
+ return;
+ LayoutView* layout_view = frame_->ContentLayoutObject();
+ if (!layout_view)
+ return;
+
+ HitTestRequest request(HitTestRequest::kReadOnly | HitTestRequest::kActive |
+ HitTestRequest::kMove);
+ HitTestResult result(request,
+ view->RootFrameToContents(last_known_mouse_position));
+ layout_view->HitTest(result);
+ UpdateSelectionForMouseDrag(result, drag_start_pos,
+ last_known_mouse_position);
+}
+
+bool SelectionController::HandleMouseReleaseEvent(
+ const MouseEventWithHitTestResults& event,
+ const LayoutPoint& drag_start_pos) {
+ TRACE_EVENT0("blink", "SelectionController::handleMouseReleaseEvent");
+
+ if (!Selection().IsAvailable())
+ return false;
+
+ bool handled = false;
+ mouse_down_may_start_select_ = false;
+ // Clear the selection if the mouse didn't move after the last mouse
+ // press and it's not a context menu click. We do this so when clicking
+ // on the selection, the selection goes away. However, if we are
+ // editing, place the caret.
+ if (mouse_down_was_single_click_in_selection_ &&
+ selection_state_ != SelectionState::kExtendedSelection &&
+ drag_start_pos == FlooredIntPoint(event.Event().PositionInRootFrame()) &&
+ Selection().ComputeVisibleSelectionInDOMTreeDeprecated().IsRange() &&
+ event.Event().button != WebPointerProperties::Button::kRight) {
+ // TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ frame_->GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ SelectionInFlatTree::Builder builder;
+ Node* node = event.InnerNode();
+ if (node && node->GetLayoutObject() && HasEditableStyle(*node)) {
+ const VisiblePositionInFlatTree pos =
+ VisiblePositionOfHitTestResult(event.GetHitTestResult());
+ if (pos.IsNotNull())
+ builder.Collapse(pos.ToPositionWithAffinity());
+ }
+
+ const SelectionInFlatTree new_selection = builder.Build();
+ if (Selection().ComputeVisibleSelectionInFlatTree() !=
+ CreateVisibleSelection(new_selection)) {
+ Selection().SetSelectionAndEndTyping(
+ ConvertToSelectionInDOMTree(new_selection));
+ }
+
+ handled = true;
+ }
+
+ Selection().NotifyTextControlOfSelectionChange(SetSelectionBy::kUser);
+
+ Selection().SelectFrameElementInParentIfFullySelected();
+
+ if (event.Event().button == WebPointerProperties::Button::kMiddle &&
+ !event.IsOverLink()) {
+ // Ignore handled, since we want to paste to where the caret was placed
+ // anyway.
+ handled = HandlePasteGlobalSelection(event.Event()) || handled;
+ }
+
+ return handled;
+}
+
+bool SelectionController::HandlePasteGlobalSelection(
+ const WebMouseEvent& mouse_event) {
+ // If the event was a middle click, attempt to copy global selection in after
+ // the newly set caret position.
+ //
+ // This code is called from either the mouse up or mouse down handling. There
+ // is some debate about when the global selection is pasted:
+ // xterm: pastes on up.
+ // GTK: pastes on down.
+ // Qt: pastes on up.
+ // Firefox: pastes on up.
+ // Chromium: pastes on up.
+ //
+ // There is something of a webcompat angle to this well, as highlighted by
+ // crbug.com/14608. Pages can clear text boxes 'onclick' and, if we paste on
+ // down then the text is pasted just before the onclick handler runs and
+ // clears the text box. So it's important this happens after the event
+ // handlers have been fired.
+ if (mouse_event.GetType() != WebInputEvent::kMouseUp)
+ return false;
+
+ if (!frame_->GetPage())
+ return false;
+ Frame* focus_frame =
+ frame_->GetPage()->GetFocusController().FocusedOrMainFrame();
+ // Do not paste here if the focus was moved somewhere else.
+ if (frame_ == focus_frame)
+ return frame_->GetEditor().ExecuteCommand("PasteGlobalSelection");
+
+ return false;
+}
+
+bool SelectionController::HandleGestureLongPress(
+ const HitTestResult& hit_test_result) {
+ TRACE_EVENT0("blink", "SelectionController::handleGestureLongPress");
+
+ if (!Selection().IsAvailable())
+ return false;
+ if (hit_test_result.IsLiveLink())
+ return false;
+
+ Node* inner_node = hit_test_result.InnerNode();
+ inner_node->GetDocument().UpdateStyleAndLayoutTree();
+ bool inner_node_is_selectable = HasEditableStyle(*inner_node) ||
+ inner_node->IsTextNode() ||
+ inner_node->CanStartSelection();
+ if (!inner_node_is_selectable)
+ return false;
+
+ if (SelectClosestWordFromHitTestResult(hit_test_result,
+ AppendTrailingWhitespace::kDontAppend,
+ SelectInputEventType::kTouch))
+ return Selection().IsAvailable();
+
+ if (!inner_node->isConnected() || !inner_node->GetLayoutObject())
+ return false;
+ SetCaretAtHitTestResult(hit_test_result);
+ return false;
+}
+
+void SelectionController::HandleGestureTwoFingerTap(
+ const GestureEventWithHitTestResults& targeted_event) {
+ TRACE_EVENT0("blink", "SelectionController::handleGestureTwoFingerTap");
+
+ SetCaretAtHitTestResult(targeted_event.GetHitTestResult());
+}
+
+void SelectionController::HandleGestureLongTap(
+ const GestureEventWithHitTestResults& targeted_event) {
+ TRACE_EVENT0("blink", "SelectionController::handleGestureLongTap");
+
+ SetCaretAtHitTestResult(targeted_event.GetHitTestResult());
+}
+
+static bool HitTestResultIsMisspelled(const HitTestResult& result) {
+ Node* inner_node = result.InnerNode();
+ if (!inner_node || !inner_node->GetLayoutObject())
+ return false;
+ VisiblePosition pos = CreateVisiblePosition(
+ inner_node->GetLayoutObject()->PositionForPoint(result.LocalPoint()));
+ if (pos.IsNull())
+ return false;
+ const Position& marker_position =
+ pos.DeepEquivalent().ParentAnchoredEquivalent();
+ return SpellCheckMarkerAtPosition(inner_node->GetDocument().Markers(),
+ marker_position);
+}
+
+void SelectionController::SendContextMenuEvent(
+ const MouseEventWithHitTestResults& mev,
+ const LayoutPoint& position) {
+ if (!Selection().IsAvailable())
+ return;
+ if (Selection().Contains(position) || mev.GetScrollbar() ||
+ // FIXME: In the editable case, word selection sometimes selects content
+ // that isn't underneath the mouse.
+ // If the selection is non-editable, we do word selection to make it
+ // easier to use the contextual menu items available for text selections.
+ // But only if we're above text.
+ !(Selection()
+ .ComputeVisibleSelectionInDOMTreeDeprecated()
+ .IsContentEditable() ||
+ (mev.InnerNode() && mev.InnerNode()->IsTextNode())))
+ return;
+
+ // Context menu events are always allowed to perform a selection.
+ AutoReset<bool> mouse_down_may_start_select_change(
+ &mouse_down_may_start_select_, true);
+
+ if (mev.Event().menu_source_type != kMenuSourceTouchHandle &&
+ HitTestResultIsMisspelled(mev.GetHitTestResult()))
+ return SelectClosestMisspellingFromMouseEvent(mev);
+
+ if (!frame_->GetEditor().Behavior().ShouldSelectOnContextualMenuClick())
+ return;
+
+ SelectClosestWordOrLinkFromMouseEvent(mev);
+}
+
+void SelectionController::PassMousePressEventToSubframe(
+ const MouseEventWithHitTestResults& mev) {
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ frame_->GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // If we're clicking into a frame that is selected, the frame will appear
+ // greyed out even though we're clicking on the selection. This looks
+ // really strange (having the whole frame be greyed out), so we deselect the
+ // selection.
+ IntPoint p = frame_->View()->RootFrameToContents(
+ FlooredIntPoint(mev.Event().PositionInRootFrame()));
+ if (!Selection().Contains(p))
+ return;
+
+ const VisiblePositionInFlatTree& visible_pos =
+ VisiblePositionOfHitTestResult(mev.GetHitTestResult());
+ if (visible_pos.IsNull()) {
+ Selection().SetSelectionAndEndTyping(SelectionInDOMTree());
+ return;
+ }
+ Selection().SetSelectionAndEndTyping(ConvertToSelectionInDOMTree(
+ SelectionInFlatTree::Builder()
+ .Collapse(visible_pos.ToPositionWithAffinity())
+ .Build()));
+}
+
+void SelectionController::InitializeSelectionState() {
+ selection_state_ = SelectionState::kHaveNotStartedSelection;
+}
+
+void SelectionController::SetMouseDownMayStartSelect(bool may_start_select) {
+ mouse_down_may_start_select_ = may_start_select;
+}
+
+bool SelectionController::MouseDownMayStartSelect() const {
+ return mouse_down_may_start_select_;
+}
+
+bool SelectionController::MouseDownWasSingleClickInSelection() const {
+ return mouse_down_was_single_click_in_selection_;
+}
+
+void SelectionController::NotifySelectionChanged() {
+ // To avoid regression on speedometer benchmark[1] test, we should not
+ // update layout tree in this code block.
+ // [1] http://browserbench.org/Speedometer/
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ frame_->GetDocument()->Lifecycle());
+
+ const SelectionInDOMTree& selection =
+ this->Selection().GetSelectionInDOMTree();
+ switch (selection.Type()) {
+ case kNoSelection:
+ selection_state_ = SelectionState::kHaveNotStartedSelection;
+ return;
+ case kCaretSelection:
+ selection_state_ = SelectionState::kPlacedCaret;
+ return;
+ case kRangeSelection:
+ selection_state_ = SelectionState::kExtendedSelection;
+ return;
+ }
+ NOTREACHED() << "We should handle all SelectionType" << selection;
+}
+
+FrameSelection& SelectionController::Selection() const {
+ return frame_->Selection();
+}
+
+bool IsLinkSelection(const MouseEventWithHitTestResults& event) {
+ return (event.Event().GetModifiers() & WebInputEvent::Modifiers::kAltKey) !=
+ 0 &&
+ event.IsOverLink();
+}
+
+bool IsExtendingSelection(const MouseEventWithHitTestResults& event) {
+ bool is_mouse_down_on_link_or_image =
+ event.IsOverLink() || event.GetHitTestResult().GetImage();
+ return (event.Event().GetModifiers() & WebInputEvent::Modifiers::kShiftKey) !=
+ 0 &&
+ !is_mouse_down_on_link_or_image;
+}
+
+STATIC_ASSERT_ENUM(WebSelection::kNoSelection, kNoSelection);
+STATIC_ASSERT_ENUM(WebSelection::kCaretSelection, kCaretSelection);
+STATIC_ASSERT_ENUM(WebSelection::kRangeSelection, kRangeSelection);
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_controller.h b/chromium/third_party/blink/renderer/core/editing/selection_controller.h
new file mode 100644
index 00000000000..cfdad110317
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_controller.h
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2006, 2007, 2009, 2010, 2011 Apple Inc. All rights reserved.
+ * Copyright (C) 2015 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_CONTROLLER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_CONTROLLER_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/dom/document_shutdown_observer.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+#include "third_party/blink/renderer/core/editing/text_granularity.h"
+#include "third_party/blink/renderer/core/page/event_with_hit_test_results.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+class HitTestResult;
+class LocalFrame;
+
+class CORE_EXPORT SelectionController final
+ : public GarbageCollectedFinalized<SelectionController>,
+ public DocumentShutdownObserver {
+ USING_GARBAGE_COLLECTED_MIXIN(SelectionController);
+
+ public:
+ static SelectionController* Create(LocalFrame&);
+ virtual ~SelectionController();
+ void Trace(blink::Visitor*);
+
+ bool HandleMousePressEvent(const MouseEventWithHitTestResults&);
+ void HandleMouseDraggedEvent(const MouseEventWithHitTestResults&,
+ const IntPoint&,
+ const LayoutPoint&,
+ const IntPoint&);
+ bool HandleMouseReleaseEvent(const MouseEventWithHitTestResults&,
+ const LayoutPoint&);
+ bool HandlePasteGlobalSelection(const WebMouseEvent&);
+ bool HandleGestureLongPress(const HitTestResult&);
+ void HandleGestureTwoFingerTap(const GestureEventWithHitTestResults&);
+ void HandleGestureLongTap(const GestureEventWithHitTestResults&);
+
+ void UpdateSelectionForMouseDrag(const LayoutPoint&, const IntPoint&);
+ void UpdateSelectionForMouseDrag(const HitTestResult&,
+ const LayoutPoint&,
+ const IntPoint&);
+ void SendContextMenuEvent(const MouseEventWithHitTestResults&,
+ const LayoutPoint&);
+ void PassMousePressEventToSubframe(const MouseEventWithHitTestResults&);
+
+ void InitializeSelectionState();
+ void SetMouseDownMayStartSelect(bool);
+ bool MouseDownMayStartSelect() const;
+ bool MouseDownWasSingleClickInSelection() const;
+ void NotifySelectionChanged();
+ bool HasExtendedSelection() const {
+ return selection_state_ == SelectionState::kExtendedSelection;
+ }
+
+ private:
+ friend class SelectionControllerTest;
+
+ explicit SelectionController(LocalFrame&);
+
+ enum class AppendTrailingWhitespace { kShouldAppend, kDontAppend };
+ enum class SelectInputEventType { kTouch, kMouse };
+ enum EndPointsAdjustmentMode {
+ kAdjustEndpointsAtBidiBoundary,
+ kDoNotAdjustEndpoints
+ };
+
+ Document& GetDocument() const;
+
+ // Returns |true| if a word was selected.
+ bool SelectClosestWordFromHitTestResult(const HitTestResult&,
+ AppendTrailingWhitespace,
+ SelectInputEventType);
+ void SelectClosestMisspellingFromHitTestResult(const HitTestResult&,
+ AppendTrailingWhitespace);
+ // Returns |true| if a word was selected.
+ bool SelectClosestWordFromMouseEvent(const MouseEventWithHitTestResults&);
+ void SelectClosestMisspellingFromMouseEvent(
+ const MouseEventWithHitTestResults&);
+ void SelectClosestWordOrLinkFromMouseEvent(
+ const MouseEventWithHitTestResults&);
+ void SetNonDirectionalSelectionIfNeeded(const SelectionInFlatTree&,
+ const SetSelectionOptions&,
+ EndPointsAdjustmentMode);
+ void SetCaretAtHitTestResult(const HitTestResult&);
+ bool UpdateSelectionForMouseDownDispatchingSelectStart(
+ Node*,
+ const SelectionInFlatTree&,
+ const SetSelectionOptions&);
+
+ FrameSelection& Selection() const;
+
+ // Implements |DocumentShutdownObserver|.
+ // TODO(yosin): We should relocate |m_originalBaseInFlatTree| when DOM tree
+ // changed.
+ void ContextDestroyed(Document*) final;
+
+ bool HandleSingleClick(const MouseEventWithHitTestResults&);
+ bool HandleDoubleClick(const MouseEventWithHitTestResults&);
+ bool HandleTripleClick(const MouseEventWithHitTestResults&);
+
+ bool HandleTapInsideSelection(const MouseEventWithHitTestResults&,
+ const SelectionInFlatTree&);
+
+ Member<LocalFrame> const frame_;
+ // Used to store base before the adjustment at bidi boundary
+ PositionInFlatTreeWithAffinity original_base_in_flat_tree_;
+ bool mouse_down_may_start_select_;
+ bool mouse_down_was_single_click_in_selection_;
+ bool mouse_down_allows_multi_click_;
+ enum class SelectionState {
+ kHaveNotStartedSelection,
+ kPlacedCaret,
+ kExtendedSelection
+ };
+ SelectionState selection_state_;
+
+ DISALLOW_COPY_AND_ASSIGN(SelectionController);
+};
+
+bool IsLinkSelection(const MouseEventWithHitTestResults&);
+bool IsExtendingSelection(const MouseEventWithHitTestResults&);
+CORE_EXPORT SelectionInFlatTree
+AdjustSelectionWithTrailingWhitespace(const SelectionInFlatTree&);
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_CONTROLLER_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_controller_test.cc b/chromium/third_party/blink/renderer/core/editing/selection_controller_test.cc
new file mode 100644
index 00000000000..f8d843f9030
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_controller_test.cc
@@ -0,0 +1,205 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/selection_controller.h"
+
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/input/event_handler.h"
+
+namespace blink {
+
+class SelectionControllerTest : public EditingTestBase {
+ protected:
+ SelectionControllerTest() = default;
+
+ VisibleSelection VisibleSelectionInDOMTree() const {
+ return Selection().ComputeVisibleSelectionInDOMTree();
+ }
+
+ VisibleSelectionInFlatTree GetVisibleSelectionInFlatTree() const {
+ return Selection().GetSelectionInFlatTree();
+ }
+
+ void SetCaretAtHitTestResult(const HitTestResult&);
+ void SetNonDirectionalSelectionIfNeeded(const SelectionInFlatTree&,
+ TextGranularity);
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(SelectionControllerTest);
+};
+
+void SelectionControllerTest::SetCaretAtHitTestResult(
+ const HitTestResult& hit_test_result) {
+ GetFrame().GetEventHandler().GetSelectionController().SetCaretAtHitTestResult(
+ hit_test_result);
+}
+
+void SelectionControllerTest::SetNonDirectionalSelectionIfNeeded(
+ const SelectionInFlatTree& new_selection,
+ TextGranularity granularity) {
+ GetFrame()
+ .GetEventHandler()
+ .GetSelectionController()
+ .SetNonDirectionalSelectionIfNeeded(
+ new_selection,
+ SetSelectionOptions::Builder().SetGranularity(granularity).Build(),
+ SelectionController::kDoNotAdjustEndpoints);
+}
+
+TEST_F(SelectionControllerTest, setNonDirectionalSelectionIfNeeded) {
+ const char* body_content = "<span id=top>top</span><span id=host></span>";
+ const char* shadow_content = "<span id=bottom>bottom</span>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* top = GetDocument().getElementById("top")->firstChild();
+ Node* bottom = shadow_root->getElementById("bottom")->firstChild();
+
+ // top to bottom
+ SetNonDirectionalSelectionIfNeeded(SelectionInFlatTree::Builder()
+ .Collapse(PositionInFlatTree(top, 1))
+ .Extend(PositionInFlatTree(bottom, 3))
+ .Build(),
+ TextGranularity::kCharacter);
+ EXPECT_EQ(VisibleSelectionInDOMTree().Start(),
+ VisibleSelectionInDOMTree().Base());
+ EXPECT_EQ(VisibleSelectionInDOMTree().End(),
+ VisibleSelectionInDOMTree().Extent());
+ EXPECT_EQ(Position(top, 1), VisibleSelectionInDOMTree().Start());
+ EXPECT_EQ(Position(top, 3), VisibleSelectionInDOMTree().End());
+
+ EXPECT_EQ(PositionInFlatTree(top, 1), GetVisibleSelectionInFlatTree().Base());
+ EXPECT_EQ(PositionInFlatTree(bottom, 3),
+ GetVisibleSelectionInFlatTree().Extent());
+ EXPECT_EQ(PositionInFlatTree(top, 1),
+ GetVisibleSelectionInFlatTree().Start());
+ EXPECT_EQ(PositionInFlatTree(bottom, 3),
+ GetVisibleSelectionInFlatTree().End());
+
+ // bottom to top
+ SetNonDirectionalSelectionIfNeeded(
+ SelectionInFlatTree::Builder()
+ .Collapse(PositionInFlatTree(bottom, 3))
+ .Extend(PositionInFlatTree(top, 1))
+ .Build(),
+ TextGranularity::kCharacter);
+ EXPECT_EQ(VisibleSelectionInDOMTree().End(),
+ VisibleSelectionInDOMTree().Base());
+ EXPECT_EQ(VisibleSelectionInDOMTree().Start(),
+ VisibleSelectionInDOMTree().Extent());
+ EXPECT_EQ(Position(bottom, 0), VisibleSelectionInDOMTree().Start());
+ EXPECT_EQ(Position(bottom, 3), VisibleSelectionInDOMTree().End());
+
+ EXPECT_EQ(PositionInFlatTree(bottom, 3),
+ GetVisibleSelectionInFlatTree().Base());
+ EXPECT_EQ(PositionInFlatTree(top, 1),
+ GetVisibleSelectionInFlatTree().Extent());
+ EXPECT_EQ(PositionInFlatTree(top, 1),
+ GetVisibleSelectionInFlatTree().Start());
+ EXPECT_EQ(PositionInFlatTree(bottom, 3),
+ GetVisibleSelectionInFlatTree().End());
+}
+
+TEST_F(SelectionControllerTest, setCaretAtHitTestResult) {
+ const char* body_content = "<div id='sample' contenteditable>sample</div>";
+ SetBodyContent(body_content);
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "var sample = document.getElementById('sample');"
+ "sample.addEventListener('onselectstart', "
+ " event => elem.parentNode.removeChild(elem));");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+ GetFrame().GetEventHandler().GetSelectionController().HandleGestureLongPress(
+ GetFrame().GetEventHandler().HitTestResultAtPoint(IntPoint(8, 8)));
+}
+
+// For http://crbug.com/704827
+TEST_F(SelectionControllerTest, setCaretAtHitTestResultWithNullPosition) {
+ SetBodyContent(
+ "<style>"
+ "#sample:before {content: '&nbsp;'}"
+ "#sample { user-select: none; }"
+ "</style>"
+ "<div id=sample></div>");
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ // Hit "&nbsp;" in before pseudo element of "sample".
+ SetCaretAtHitTestResult(
+ GetFrame().GetEventHandler().HitTestResultAtPoint(IntPoint(10, 10)));
+
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+}
+
+// For http://crbug.com/759971
+TEST_F(SelectionControllerTest,
+ SetCaretAtHitTestResultWithDisconnectedPosition) {
+ GetDocument().GetSettings()->SetScriptEnabled(true);
+ Element* script = GetDocument().CreateRawElement(HTMLNames::scriptTag);
+ script->SetInnerHTMLFromString(
+ "document.designMode = 'on';"
+ "const selection = window.getSelection();"
+ "const html = document.getElementsByTagName('html')[0];"
+ "selection.collapse(html);"
+ "const range = selection.getRangeAt(0);"
+
+ "function selectstart() {"
+ " const body = document.getElementsByTagName('body')[0];"
+ " range.surroundContents(body);"
+ " range.deleteContents();"
+ "}"
+ "document.addEventListener('selectstart', selectstart);");
+ GetDocument().body()->AppendChild(script);
+ GetDocument().View()->UpdateAllLifecyclePhases();
+
+ // Simulate a tap somewhere in the document
+ blink::WebMouseEvent mouse_event(
+ blink::WebInputEvent::kMouseDown,
+ blink::WebInputEvent::kIsCompatibilityEventForTouch,
+ blink::WebInputEvent::GetStaticTimeStampForTests());
+ // Frame scale defaults to 0, which would cause a divide-by-zero problem.
+ mouse_event.SetFrameScale(1);
+ GetFrame().GetEventHandler().GetSelectionController().HandleMousePressEvent(
+ MouseEventWithHitTestResults(
+ mouse_event,
+ GetFrame().GetEventHandler().HitTestResultAtPoint(IntPoint(0, 0))));
+
+ // The original bug was that this test would cause
+ // TextSuggestionController::HandlePotentialMisspelledWordTap() to crash. So
+ // the primary thing this test cases tests is that we can get here without
+ // crashing.
+
+ // Verify no selection was set.
+ EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
+}
+
+// For http://crbug.com/700368
+TEST_F(SelectionControllerTest, AdjustSelectionWithTrailingWhitespace) {
+ SetBodyContent(
+ "<input type=checkbox>"
+ "<div style='user-select:none'>abc</div>");
+ Element* const input = GetDocument().QuerySelector("input");
+
+ const VisibleSelectionInFlatTree& selection =
+ CreateVisibleSelectionWithGranularity(
+ SelectionInFlatTree::Builder()
+ .Collapse(PositionInFlatTree::BeforeNode(*input))
+ .Extend(PositionInFlatTree::AfterNode(*input))
+ .Build(),
+ TextGranularity::kWord);
+ const SelectionInFlatTree& result =
+ AdjustSelectionWithTrailingWhitespace(selection.AsSelection());
+
+ EXPECT_EQ(PositionInFlatTree::BeforeNode(*input),
+ result.ComputeStartPosition());
+ EXPECT_EQ(PositionInFlatTree::AfterNode(*input), result.ComputeEndPosition());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_editor.cc b/chromium/third_party/blink/renderer/core/editing/selection_editor.cc
new file mode 100644
index 00000000000..4a8c93b7117
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_editor.cc
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2004, 2008, 2009, 2010 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/selection_editor.h"
+
+#include "third_party/blink/renderer/core/dom/node_with_index.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/selection_adjuster.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+
+namespace blink {
+
+SelectionEditor::SelectionEditor(LocalFrame& frame) : frame_(frame) {
+ ClearVisibleSelection();
+}
+
+SelectionEditor::~SelectionEditor() = default;
+
+void SelectionEditor::AssertSelectionValid() const {
+#if DCHECK_IS_ON()
+ // Since We don't track dom tree version during attribute changes, we can't
+ // use it for validity of |m_selection|.
+ const_cast<SelectionEditor*>(this)->selection_.dom_tree_version_ =
+ GetDocument().DomTreeVersion();
+#endif
+ selection_.AssertValidFor(GetDocument());
+}
+
+void SelectionEditor::ClearVisibleSelection() {
+ selection_ = SelectionInDOMTree();
+ cached_visible_selection_in_dom_tree_ = VisibleSelection();
+ cached_visible_selection_in_flat_tree_ = VisibleSelectionInFlatTree();
+ cached_visible_selection_in_dom_tree_is_dirty_ = false;
+ cached_visible_selection_in_flat_tree_is_dirty_ = false;
+}
+
+void SelectionEditor::Dispose() {
+ ClearDocumentCachedRange();
+ ClearVisibleSelection();
+}
+
+Document& SelectionEditor::GetDocument() const {
+ DCHECK(LifecycleContext());
+ return *LifecycleContext();
+}
+
+VisibleSelection SelectionEditor::ComputeVisibleSelectionInDOMTree() const {
+ DCHECK_EQ(GetFrame()->GetDocument(), GetDocument());
+ DCHECK_EQ(GetFrame(), GetDocument().GetFrame());
+ UpdateCachedVisibleSelectionIfNeeded();
+ if (cached_visible_selection_in_dom_tree_.IsNone())
+ return cached_visible_selection_in_dom_tree_;
+ DCHECK_EQ(cached_visible_selection_in_dom_tree_.Base().GetDocument(),
+ GetDocument());
+ return cached_visible_selection_in_dom_tree_;
+}
+
+VisibleSelectionInFlatTree SelectionEditor::ComputeVisibleSelectionInFlatTree()
+ const {
+ DCHECK_EQ(GetFrame()->GetDocument(), GetDocument());
+ DCHECK_EQ(GetFrame(), GetDocument().GetFrame());
+ UpdateCachedVisibleSelectionInFlatTreeIfNeeded();
+ if (cached_visible_selection_in_flat_tree_.IsNone())
+ return cached_visible_selection_in_flat_tree_;
+ DCHECK_EQ(cached_visible_selection_in_flat_tree_.Base().GetDocument(),
+ GetDocument());
+ return cached_visible_selection_in_flat_tree_;
+}
+
+SelectionInDOMTree SelectionEditor::GetSelectionInDOMTree() const {
+ AssertSelectionValid();
+ return selection_;
+}
+
+void SelectionEditor::MarkCacheDirty() {
+ if (!cached_visible_selection_in_dom_tree_is_dirty_) {
+ cached_visible_selection_in_dom_tree_ = VisibleSelection();
+ cached_visible_selection_in_dom_tree_is_dirty_ = true;
+ }
+ if (!cached_visible_selection_in_flat_tree_is_dirty_) {
+ cached_visible_selection_in_flat_tree_ = VisibleSelectionInFlatTree();
+ cached_visible_selection_in_flat_tree_is_dirty_ = true;
+ }
+}
+
+void SelectionEditor::SetSelectionAndEndTyping(
+ const SelectionInDOMTree& new_selection) {
+ new_selection.AssertValidFor(GetDocument());
+ DCHECK_NE(selection_, new_selection);
+ ClearDocumentCachedRange();
+ MarkCacheDirty();
+ selection_ = new_selection;
+}
+
+void SelectionEditor::DidChangeChildren(const ContainerNode&) {
+ selection_.ResetDirectionCache();
+ MarkCacheDirty();
+ DidFinishDOMMutation();
+}
+
+void SelectionEditor::DidFinishTextChange(const Position& new_base,
+ const Position& new_extent) {
+ if (new_base == selection_.base_ && new_extent == selection_.extent_) {
+ DidFinishDOMMutation();
+ return;
+ }
+ selection_.base_ = new_base;
+ selection_.extent_ = new_extent;
+ selection_.ResetDirectionCache();
+ MarkCacheDirty();
+ DidFinishDOMMutation();
+}
+
+void SelectionEditor::DidFinishDOMMutation() {
+ AssertSelectionValid();
+}
+
+void SelectionEditor::DocumentAttached(Document* document) {
+ DCHECK(document);
+ DCHECK(!LifecycleContext()) << LifecycleContext();
+ style_version_for_dom_tree_ = static_cast<uint64_t>(-1);
+ style_version_for_flat_tree_ = static_cast<uint64_t>(-1);
+ ClearVisibleSelection();
+ SetContext(document);
+}
+
+void SelectionEditor::ContextDestroyed(Document*) {
+ Dispose();
+ style_version_for_dom_tree_ = static_cast<uint64_t>(-1);
+ style_version_for_flat_tree_ = static_cast<uint64_t>(-1);
+ selection_ = SelectionInDOMTree();
+ cached_visible_selection_in_dom_tree_ = VisibleSelection();
+ cached_visible_selection_in_flat_tree_ = VisibleSelectionInFlatTree();
+ cached_visible_selection_in_dom_tree_is_dirty_ = false;
+ cached_visible_selection_in_flat_tree_is_dirty_ = false;
+}
+
+static Position ComputePositionForChildrenRemoval(const Position& position,
+ ContainerNode& container) {
+ Node* node = position.ComputeContainerNode();
+ if (container.ContainsIncludingHostElements(*node))
+ return Position::FirstPositionInNode(container);
+ return position;
+}
+
+void SelectionEditor::NodeChildrenWillBeRemoved(ContainerNode& container) {
+ if (selection_.IsNone())
+ return;
+ const Position old_base = selection_.base_;
+ const Position old_extent = selection_.extent_;
+ const Position& new_base =
+ ComputePositionForChildrenRemoval(old_base, container);
+ const Position& new_extent =
+ ComputePositionForChildrenRemoval(old_extent, container);
+ if (new_base == old_base && new_extent == old_extent)
+ return;
+ selection_ = SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(new_base, new_extent)
+ .Build();
+ MarkCacheDirty();
+}
+
+void SelectionEditor::NodeWillBeRemoved(Node& node_to_be_removed) {
+ if (selection_.IsNone())
+ return;
+ const Position old_base = selection_.base_;
+ const Position old_extent = selection_.extent_;
+ const Position& new_base =
+ ComputePositionForNodeRemoval(old_base, node_to_be_removed);
+ const Position& new_extent =
+ ComputePositionForNodeRemoval(old_extent, node_to_be_removed);
+ if (new_base == old_base && new_extent == old_extent)
+ return;
+ selection_ = SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(new_base, new_extent)
+ .Build();
+ MarkCacheDirty();
+}
+
+static Position UpdatePositionAfterAdoptingTextReplacement(
+ const Position& position,
+ CharacterData* node,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) {
+ if (position.AnchorNode() != node)
+ return position;
+
+ if (position.IsBeforeAnchor()) {
+ return UpdatePositionAfterAdoptingTextReplacement(
+ Position(node, 0), node, offset, old_length, new_length);
+ }
+ if (position.IsAfterAnchor()) {
+ return UpdatePositionAfterAdoptingTextReplacement(
+ Position(node, old_length), node, offset, old_length, new_length);
+ }
+
+ // See:
+ // http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Mutation
+ DCHECK_GE(position.OffsetInContainerNode(), 0);
+ unsigned position_offset =
+ static_cast<unsigned>(position.OffsetInContainerNode());
+ // Replacing text can be viewed as a deletion followed by insertion.
+ if (position_offset >= offset && position_offset <= offset + old_length)
+ position_offset = offset;
+
+ // Adjust the offset if the position is after the end of the deleted contents
+ // (positionOffset > offset + oldLength) to avoid having a stale offset.
+ if (position_offset > offset + old_length)
+ position_offset = position_offset - old_length + new_length;
+
+ // Due to case folding
+ // (http://unicode.org/Public/UCD/latest/ucd/CaseFolding.txt), LayoutText
+ // length may be different from Text length. A correct implementation would
+ // translate the LayoutText offset to a Text offset; this is just a safety
+ // precaution to avoid offset values that run off the end of the Text.
+ if (position_offset > node->length())
+ position_offset = node->length();
+
+ return Position(node, position_offset);
+}
+
+void SelectionEditor::DidUpdateCharacterData(CharacterData* node,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) {
+ // The fragment check is a performance optimization. See
+ // http://trac.webkit.org/changeset/30062.
+ if (selection_.IsNone() || !node || !node->isConnected()) {
+ DidFinishDOMMutation();
+ return;
+ }
+ const Position& new_base = UpdatePositionAfterAdoptingTextReplacement(
+ selection_.base_, node, offset, old_length, new_length);
+ const Position& new_extent = UpdatePositionAfterAdoptingTextReplacement(
+ selection_.extent_, node, offset, old_length, new_length);
+ DidFinishTextChange(new_base, new_extent);
+}
+
+static Position UpdatePostionAfterAdoptingTextNodesMerged(
+ const Position& position,
+ const Text& merged_node,
+ const NodeWithIndex& node_to_be_removed_with_index,
+ unsigned old_length) {
+ Node* const anchor_node = position.AnchorNode();
+ const Node& node_to_be_removed = node_to_be_removed_with_index.GetNode();
+ switch (position.AnchorType()) {
+ case PositionAnchorType::kBeforeChildren:
+ case PositionAnchorType::kAfterChildren:
+ return position;
+ case PositionAnchorType::kBeforeAnchor:
+ if (anchor_node == node_to_be_removed)
+ return Position(merged_node, merged_node.length());
+ return position;
+ case PositionAnchorType::kAfterAnchor:
+ if (anchor_node == node_to_be_removed)
+ return Position(merged_node, merged_node.length());
+ if (anchor_node == merged_node)
+ return Position(merged_node, old_length);
+ return position;
+ case PositionAnchorType::kOffsetInAnchor: {
+ const int offset = position.OffsetInContainerNode();
+ if (anchor_node == node_to_be_removed)
+ return Position(merged_node, old_length + offset);
+ if (anchor_node == node_to_be_removed.parentNode() &&
+ offset == node_to_be_removed_with_index.Index()) {
+ return Position(merged_node, old_length);
+ }
+ return position;
+ }
+ }
+ NOTREACHED() << position;
+ return position;
+}
+
+void SelectionEditor::DidMergeTextNodes(
+ const Text& merged_node,
+ const NodeWithIndex& node_to_be_removed_with_index,
+ unsigned old_length) {
+ if (selection_.IsNone()) {
+ DidFinishDOMMutation();
+ return;
+ }
+ const Position& new_base = UpdatePostionAfterAdoptingTextNodesMerged(
+ selection_.base_, merged_node, node_to_be_removed_with_index, old_length);
+ const Position& new_extent = UpdatePostionAfterAdoptingTextNodesMerged(
+ selection_.extent_, merged_node, node_to_be_removed_with_index,
+ old_length);
+ DidFinishTextChange(new_base, new_extent);
+}
+
+static Position UpdatePostionAfterAdoptingTextNodeSplit(
+ const Position& position,
+ const Text& old_node) {
+ if (!position.AnchorNode() || position.AnchorNode() != &old_node ||
+ !position.IsOffsetInAnchor())
+ return position;
+ // See:
+ // http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Mutation
+ DCHECK_GE(position.OffsetInContainerNode(), 0);
+ unsigned position_offset =
+ static_cast<unsigned>(position.OffsetInContainerNode());
+ unsigned old_length = old_node.length();
+ if (position_offset <= old_length)
+ return position;
+ return Position(ToText(old_node.nextSibling()), position_offset - old_length);
+}
+
+void SelectionEditor::DidSplitTextNode(const Text& old_node) {
+ if (selection_.IsNone() || !old_node.isConnected()) {
+ DidFinishDOMMutation();
+ return;
+ }
+ const Position& new_base =
+ UpdatePostionAfterAdoptingTextNodeSplit(selection_.base_, old_node);
+ const Position& new_extent =
+ UpdatePostionAfterAdoptingTextNodeSplit(selection_.extent_, old_node);
+ DidFinishTextChange(new_base, new_extent);
+}
+
+bool SelectionEditor::ShouldAlwaysUseDirectionalSelection() const {
+ return GetFrame()
+ ->GetEditor()
+ .Behavior()
+ .ShouldConsiderSelectionAsDirectional();
+}
+
+bool SelectionEditor::NeedsUpdateVisibleSelection() const {
+ return cached_visible_selection_in_dom_tree_is_dirty_ ||
+ style_version_for_dom_tree_ != GetDocument().StyleVersion();
+}
+
+void SelectionEditor::UpdateCachedVisibleSelectionIfNeeded() const {
+ // Note: Since we |FrameCaret::updateApperance()| is called from
+ // |FrameView::performPostLayoutTasks()|, we check lifecycle against
+ // |AfterPerformLayout| instead of |LayoutClean|.
+ DCHECK_GE(GetDocument().Lifecycle().GetState(),
+ DocumentLifecycle::kAfterPerformLayout);
+ AssertSelectionValid();
+ if (!NeedsUpdateVisibleSelection())
+ return;
+ style_version_for_dom_tree_ = GetDocument().StyleVersion();
+ cached_visible_selection_in_dom_tree_is_dirty_ = false;
+ cached_visible_selection_in_dom_tree_ = CreateVisibleSelection(selection_);
+ if (!cached_visible_selection_in_dom_tree_.IsNone())
+ return;
+ style_version_for_flat_tree_ = GetDocument().StyleVersion();
+ cached_visible_selection_in_flat_tree_is_dirty_ = false;
+ cached_visible_selection_in_flat_tree_ = VisibleSelectionInFlatTree();
+}
+
+bool SelectionEditor::NeedsUpdateVisibleSelectionInFlatTree() const {
+ return cached_visible_selection_in_flat_tree_is_dirty_ ||
+ style_version_for_flat_tree_ != GetDocument().StyleVersion();
+}
+
+void SelectionEditor::UpdateCachedVisibleSelectionInFlatTreeIfNeeded() const {
+ // Note: Since we |FrameCaret::updateApperance()| is called from
+ // |FrameView::performPostLayoutTasks()|, we check lifecycle against
+ // |AfterPerformLayout| instead of |LayoutClean|.
+ DCHECK_GE(GetDocument().Lifecycle().GetState(),
+ DocumentLifecycle::kAfterPerformLayout);
+ AssertSelectionValid();
+ if (!NeedsUpdateVisibleSelectionInFlatTree())
+ return;
+ style_version_for_flat_tree_ = GetDocument().StyleVersion();
+ cached_visible_selection_in_flat_tree_is_dirty_ = false;
+ SelectionInFlatTree::Builder builder;
+ const PositionInFlatTree& base = ToPositionInFlatTree(selection_.Base());
+ const PositionInFlatTree& extent = ToPositionInFlatTree(selection_.Extent());
+ if (base.IsNotNull() && extent.IsNotNull())
+ builder.SetBaseAndExtent(base, extent);
+ else if (base.IsNotNull())
+ builder.Collapse(base);
+ else if (extent.IsNotNull())
+ builder.Collapse(extent);
+ builder.SetAffinity(selection_.Affinity());
+ cached_visible_selection_in_flat_tree_ =
+ CreateVisibleSelection(builder.Build());
+ if (!cached_visible_selection_in_flat_tree_.IsNone())
+ return;
+ style_version_for_dom_tree_ = GetDocument().StyleVersion();
+ cached_visible_selection_in_dom_tree_is_dirty_ = false;
+ cached_visible_selection_in_dom_tree_ = VisibleSelection();
+}
+
+void SelectionEditor::CacheRangeOfDocument(Range* range) {
+ cached_range_ = range;
+}
+
+Range* SelectionEditor::DocumentCachedRange() const {
+ return cached_range_;
+}
+
+void SelectionEditor::ClearDocumentCachedRange() {
+ cached_range_ = nullptr;
+}
+
+void SelectionEditor::Trace(blink::Visitor* visitor) {
+ visitor->Trace(frame_);
+ visitor->Trace(selection_);
+ visitor->Trace(cached_visible_selection_in_dom_tree_);
+ visitor->Trace(cached_visible_selection_in_flat_tree_);
+ visitor->Trace(cached_range_);
+ SynchronousMutationObserver::Trace(visitor);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_editor.h b/chromium/third_party/blink/renderer/core/editing/selection_editor.h
new file mode 100644
index 00000000000..2ae7c356963
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_editor.h
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_EDITOR_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_EDITOR_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/dom/events/event_dispatch_result.h"
+#include "third_party/blink/renderer/core/dom/synchronous_mutation_observer.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+
+namespace blink {
+
+// TODO(yosin): We will rename |SelectionEditor| to appropriate name since
+// it is no longer have a changing selection functionality, it was moved to
+// |SelectionModifier| class.
+class SelectionEditor final : public GarbageCollectedFinalized<SelectionEditor>,
+ public SynchronousMutationObserver {
+ USING_GARBAGE_COLLECTED_MIXIN(SelectionEditor);
+
+ public:
+ static SelectionEditor* Create(LocalFrame& frame) {
+ return new SelectionEditor(frame);
+ }
+ virtual ~SelectionEditor();
+ void Dispose();
+
+ SelectionInDOMTree GetSelectionInDOMTree() const;
+
+ VisibleSelection ComputeVisibleSelectionInDOMTree() const;
+ VisibleSelectionInFlatTree ComputeVisibleSelectionInFlatTree() const;
+ void SetSelectionAndEndTyping(const SelectionInDOMTree&);
+
+ void DocumentAttached(Document*);
+
+ // There functions are exposed for |FrameSelection|.
+ void CacheRangeOfDocument(Range*);
+ Range* DocumentCachedRange() const;
+ void ClearDocumentCachedRange();
+
+ void Trace(blink::Visitor*);
+
+ private:
+ explicit SelectionEditor(LocalFrame&);
+
+ Document& GetDocument() const;
+ LocalFrame* GetFrame() const { return frame_.Get(); }
+
+ void AssertSelectionValid() const;
+ void ClearVisibleSelection();
+ void MarkCacheDirty();
+ bool ShouldAlwaysUseDirectionalSelection() const;
+
+ // VisibleSelection cache related
+ bool NeedsUpdateVisibleSelection() const;
+ bool NeedsUpdateVisibleSelectionInFlatTree() const;
+ void UpdateCachedVisibleSelectionIfNeeded() const;
+ void UpdateCachedVisibleSelectionInFlatTreeIfNeeded() const;
+
+ void DidFinishTextChange(const Position& base, const Position& extent);
+ void DidFinishDOMMutation();
+
+ // Implementation of |SynchronousMutationObsderver| member functions.
+ void ContextDestroyed(Document*) final;
+ void DidChangeChildren(const ContainerNode&) final;
+ void DidMergeTextNodes(const Text& merged_node,
+ const NodeWithIndex& node_to_be_removed_with_index,
+ unsigned old_length) final;
+ void DidSplitTextNode(const Text&) final;
+ void DidUpdateCharacterData(CharacterData*,
+ unsigned offset,
+ unsigned old_length,
+ unsigned new_length) final;
+ void NodeChildrenWillBeRemoved(ContainerNode&) final;
+ void NodeWillBeRemoved(Node&) final;
+
+ Member<LocalFrame> frame_;
+
+ SelectionInDOMTree selection_;
+
+ // If document is root, document.getSelection().addRange(range) is cached on
+ // this.
+ Member<Range> cached_range_;
+
+ mutable VisibleSelection cached_visible_selection_in_dom_tree_;
+ mutable VisibleSelectionInFlatTree cached_visible_selection_in_flat_tree_;
+ mutable uint64_t style_version_for_dom_tree_ = static_cast<uint64_t>(-1);
+ mutable uint64_t style_version_for_flat_tree_ = static_cast<uint64_t>(-1);
+ mutable bool cached_visible_selection_in_dom_tree_is_dirty_ = false;
+ mutable bool cached_visible_selection_in_flat_tree_is_dirty_ = false;
+
+ DISALLOW_COPY_AND_ASSIGN(SelectionEditor);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_EDITOR_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_modifier.cc b/chromium/third_party/blink/renderer/core/editing/selection_modifier.cc
new file mode 100644
index 00000000000..1da9fadca4d
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_modifier.cc
@@ -0,0 +1,892 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/selection_modifier.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/inline_box_position.h"
+#include "third_party/blink/renderer/core/editing/local_caret_rect.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/layout/layout_block.h"
+#include "third_party/blink/renderer/core/layout/line/inline_text_box.h"
+#include "third_party/blink/renderer/core/page/spatial_navigation.h"
+
+namespace blink {
+
+namespace {
+
+VisiblePosition LeftBoundaryOfLine(const VisiblePosition& c,
+ TextDirection direction) {
+ DCHECK(c.IsValid()) << c;
+ return direction == TextDirection::kLtr ? LogicalStartOfLine(c)
+ : LogicalEndOfLine(c);
+}
+
+VisiblePosition RightBoundaryOfLine(const VisiblePosition& c,
+ TextDirection direction) {
+ DCHECK(c.IsValid()) << c;
+ return direction == TextDirection::kLtr ? LogicalEndOfLine(c)
+ : LogicalStartOfLine(c);
+}
+
+VisiblePosition PreviousParagraphPosition(
+ const VisiblePosition& passed_position,
+ LayoutUnit x_point) {
+ DCHECK(passed_position.IsValid()) << passed_position;
+ VisiblePosition position = passed_position;
+ do {
+ const VisiblePosition& new_position =
+ PreviousLinePosition(position, x_point);
+ if (new_position.IsNull() ||
+ new_position.DeepEquivalent() == position.DeepEquivalent())
+ break;
+ position = new_position;
+ } while (InSameParagraph(passed_position, position));
+ return position;
+}
+
+VisiblePosition NextParagraphPosition(const VisiblePosition& passed_position,
+ LayoutUnit x_point) {
+ DCHECK(passed_position.IsValid()) << passed_position;
+ VisiblePosition position = passed_position;
+ do {
+ const VisiblePosition& new_position = NextLinePosition(position, x_point);
+ if (new_position.IsNull() ||
+ new_position.DeepEquivalent() == position.DeepEquivalent())
+ break;
+ position = new_position;
+ } while (InSameParagraph(passed_position, position));
+ return position;
+}
+
+} // namespace
+
+LayoutUnit NoXPosForVerticalArrowNavigation() {
+ return LayoutUnit::Min();
+}
+
+bool SelectionModifier::ShouldAlwaysUseDirectionalSelection(
+ const LocalFrame& frame) {
+ return frame.GetEditor().Behavior().ShouldConsiderSelectionAsDirectional();
+}
+
+SelectionModifier::SelectionModifier(
+ const LocalFrame& frame,
+ const SelectionInDOMTree& selection,
+ LayoutUnit x_pos_for_vertical_arrow_navigation)
+ : frame_(&frame),
+ current_selection_(selection),
+ x_pos_for_vertical_arrow_navigation_(
+ x_pos_for_vertical_arrow_navigation) {}
+
+SelectionModifier::SelectionModifier(const LocalFrame& frame,
+ const SelectionInDOMTree& selection)
+ : SelectionModifier(frame, selection, NoXPosForVerticalArrowNavigation()) {}
+
+VisibleSelection SelectionModifier::Selection() const {
+ return CreateVisibleSelection(current_selection_);
+}
+
+static VisiblePosition ComputeVisibleExtent(
+ const VisibleSelection& visible_selection) {
+ return CreateVisiblePosition(visible_selection.Extent(),
+ visible_selection.Affinity());
+}
+
+TextDirection SelectionModifier::DirectionOfEnclosingBlock() const {
+ const Position& selection_extent = selection_.Extent();
+
+ // TODO(editing-dev): Check for Position::IsNotNull is an easy fix for few
+ // editing/ layout tests, that didn't expect that (e.g.
+ // editing/selection/extend-byline-withfloat.html).
+ // That should be fixed in a more appropriate manner.
+ // We should either have SelectionModifier aborted earlier for null selection,
+ // or do not allow null selection in SelectionModifier at all.
+ return selection_extent.IsNotNull()
+ ? DirectionOfEnclosingBlockOf(selection_extent)
+ : TextDirection::kLtr;
+}
+
+static TextDirection DirectionOf(const VisibleSelection& visible_selection) {
+ const InlineBox* start_box = nullptr;
+ const InlineBox* end_box = nullptr;
+ // Cache the VisiblePositions because visibleStart() and visibleEnd()
+ // can cause layout, which has the potential to invalidate lineboxes.
+ const VisiblePosition& start_position = visible_selection.VisibleStart();
+ const VisiblePosition& end_position = visible_selection.VisibleEnd();
+ if (start_position.IsNotNull())
+ start_box = ComputeInlineBoxPosition(start_position).inline_box;
+ if (end_position.IsNotNull())
+ end_box = ComputeInlineBoxPosition(end_position).inline_box;
+ if (start_box && end_box && start_box->Direction() == end_box->Direction())
+ return start_box->Direction();
+
+ return DirectionOfEnclosingBlockOf(visible_selection.Extent());
+}
+
+TextDirection SelectionModifier::DirectionOfSelection() const {
+ return DirectionOf(selection_);
+}
+
+static bool IsBaseStart(const VisibleSelection& visible_selection,
+ SelectionModifyDirection direction) {
+ switch (direction) {
+ case SelectionModifyDirection::kRight:
+ return DirectionOf(visible_selection) == TextDirection::kLtr;
+ case SelectionModifyDirection::kForward:
+ return true;
+ case SelectionModifyDirection::kLeft:
+ return DirectionOf(visible_selection) != TextDirection::kLtr;
+ case SelectionModifyDirection::kBackward:
+ return false;
+ }
+ NOTREACHED() << "We should handle " << static_cast<int>(direction);
+ return true;
+}
+
+// This function returns |VisibleSelection| from start and end position of
+// current_selection_'s |VisibleSelection| with |direction| and ordering of base
+// and extent to handle base/extent don't match to start/end, e.g. granularity
+// != character, and start/end adjustment in |visibleSelection::validate()| for
+// range selection.
+VisibleSelection SelectionModifier::PrepareToModifySelection(
+ SelectionModifyAlteration alter,
+ SelectionModifyDirection direction) const {
+ const VisibleSelection& visible_selection =
+ CreateVisibleSelection(current_selection_);
+ if (alter != SelectionModifyAlteration::kExtend)
+ return visible_selection;
+ if (visible_selection.IsNone())
+ return visible_selection;
+
+ const EphemeralRange& range = visible_selection.AsSelection().ComputeRange();
+ if (range.IsCollapsed())
+ return visible_selection;
+ SelectionInDOMTree::Builder builder;
+ // Make base and extent match start and end so we extend the user-visible
+ // selection. This only matters for cases where base and extend point to
+ // different positions than start and end (e.g. after a double-click to
+ // select a word).
+ const bool base_is_start = selection_is_directional_
+ ? visible_selection.IsBaseFirst()
+ : IsBaseStart(visible_selection, direction);
+ if (base_is_start)
+ builder.SetAsForwardSelection(range);
+ else
+ builder.SetAsBackwardSelection(range);
+ return CreateVisibleSelection(builder.Build());
+}
+
+VisiblePosition SelectionModifier::PositionForPlatform(
+ bool is_get_start) const {
+ Settings* settings = GetFrame().GetSettings();
+ if (settings && settings->GetEditingBehaviorType() == kEditingMacBehavior)
+ return is_get_start ? selection_.VisibleStart() : selection_.VisibleEnd();
+ // Linux and Windows always extend selections from the extent endpoint.
+ // FIXME: VisibleSelection should be fixed to ensure as an invariant that
+ // base/extent always point to the same nodes as start/end, but which points
+ // to which depends on the value of isBaseFirst. Then this can be changed
+ // to just return m_sel.extent().
+ return selection_.IsBaseFirst() ? selection_.VisibleEnd()
+ : selection_.VisibleStart();
+}
+
+VisiblePosition SelectionModifier::StartForPlatform() const {
+ return PositionForPlatform(true);
+}
+
+VisiblePosition SelectionModifier::EndForPlatform() const {
+ return PositionForPlatform(false);
+}
+
+VisiblePosition SelectionModifier::NextWordPositionForPlatform(
+ const VisiblePosition& original_position) {
+ VisiblePosition position_after_current_word =
+ NextWordPosition(original_position);
+
+ if (!GetFrame().GetEditor().Behavior().ShouldSkipSpaceWhenMovingRight())
+ return position_after_current_word;
+ return CreateVisiblePosition(
+ SkipWhitespace(position_after_current_word.DeepEquivalent()));
+}
+
+static VisiblePosition AdjustForwardPositionForUserSelectAll(
+ const VisiblePosition& position) {
+ Node* const root_user_select_all = EditingStrategy::RootUserSelectAllForNode(
+ position.DeepEquivalent().AnchorNode());
+ if (!root_user_select_all)
+ return position;
+ return CreateVisiblePosition(MostForwardCaretPosition(
+ Position::AfterNode(*root_user_select_all), kCanCrossEditingBoundary));
+}
+
+static VisiblePosition AdjustBackwardPositionForUserSelectAll(
+ const VisiblePosition& position) {
+ Node* const root_user_select_all = EditingStrategy::RootUserSelectAllForNode(
+ position.DeepEquivalent().AnchorNode());
+ if (!root_user_select_all)
+ return position;
+ return CreateVisiblePosition(MostBackwardCaretPosition(
+ Position::BeforeNode(*root_user_select_all), kCanCrossEditingBoundary));
+}
+
+VisiblePosition SelectionModifier::ModifyExtendingRightInternal(
+ TextGranularity granularity) {
+ // The difference between modifyExtendingRight and modifyExtendingForward is:
+ // modifyExtendingForward always extends forward logically.
+ // modifyExtendingRight behaves the same as modifyExtendingForward except for
+ // extending character or word, it extends forward logically if the enclosing
+ // block is LTR direction, but it extends backward logically if the enclosing
+ // block is RTL direction.
+ switch (granularity) {
+ case TextGranularity::kCharacter:
+ if (DirectionOfEnclosingBlock() == TextDirection::kLtr) {
+ return NextPositionOf(ComputeVisibleExtent(selection_),
+ kCanSkipOverEditingBoundary);
+ }
+ return PreviousPositionOf(ComputeVisibleExtent(selection_),
+ kCanSkipOverEditingBoundary);
+ case TextGranularity::kWord:
+ if (DirectionOfEnclosingBlock() == TextDirection::kLtr) {
+ return NextWordPositionForPlatform(ComputeVisibleExtent(selection_));
+ }
+ return PreviousWordPosition(ComputeVisibleExtent(selection_));
+ case TextGranularity::kLineBoundary:
+ if (DirectionOfEnclosingBlock() == TextDirection::kLtr)
+ return ModifyExtendingForwardInternal(granularity);
+ return ModifyExtendingBackwardInternal(granularity);
+ case TextGranularity::kSentence:
+ case TextGranularity::kLine:
+ case TextGranularity::kParagraph:
+ case TextGranularity::kSentenceBoundary:
+ case TextGranularity::kParagraphBoundary:
+ case TextGranularity::kDocumentBoundary:
+ // TODO(editing-dev): implement all of the above?
+ return ModifyExtendingForwardInternal(granularity);
+ }
+ NOTREACHED() << static_cast<int>(granularity);
+ return VisiblePosition();
+}
+
+VisiblePosition SelectionModifier::ModifyExtendingRight(
+ TextGranularity granularity) {
+ const VisiblePosition& pos = ModifyExtendingRightInternal(granularity);
+ if (DirectionOfEnclosingBlock() == TextDirection::kLtr)
+ return AdjustForwardPositionForUserSelectAll(pos);
+ return AdjustBackwardPositionForUserSelectAll(pos);
+}
+
+VisiblePosition SelectionModifier::ModifyExtendingForwardInternal(
+ TextGranularity granularity) {
+ switch (granularity) {
+ case TextGranularity::kCharacter:
+ return NextPositionOf(ComputeVisibleExtent(selection_),
+ kCanSkipOverEditingBoundary);
+ case TextGranularity::kWord:
+ return NextWordPositionForPlatform(ComputeVisibleExtent(selection_));
+ case TextGranularity::kSentence:
+ return NextSentencePosition(ComputeVisibleExtent(selection_));
+ case TextGranularity::kLine:
+ return NextLinePosition(
+ ComputeVisibleExtent(selection_),
+ LineDirectionPointForBlockDirectionNavigation(selection_.Extent()));
+ case TextGranularity::kParagraph:
+ return NextParagraphPosition(
+ ComputeVisibleExtent(selection_),
+ LineDirectionPointForBlockDirectionNavigation(selection_.Extent()));
+ case TextGranularity::kSentenceBoundary:
+ return EndOfSentence(EndForPlatform());
+ case TextGranularity::kLineBoundary:
+ return LogicalEndOfLine(EndForPlatform());
+ case TextGranularity::kParagraphBoundary:
+ return EndOfParagraph(EndForPlatform());
+ break;
+ case TextGranularity::kDocumentBoundary: {
+ const VisiblePosition& pos = EndForPlatform();
+ if (IsEditablePosition(pos.DeepEquivalent()))
+ return EndOfEditableContent(pos);
+ return EndOfDocument(pos);
+ }
+ }
+ NOTREACHED() << static_cast<int>(granularity);
+ return VisiblePosition();
+}
+
+VisiblePosition SelectionModifier::ModifyExtendingForward(
+ TextGranularity granularity) {
+ const VisiblePosition pos = ModifyExtendingForwardInternal(granularity);
+ if (DirectionOfEnclosingBlock() == TextDirection::kLtr)
+ return AdjustForwardPositionForUserSelectAll(pos);
+ return AdjustBackwardPositionForUserSelectAll(pos);
+}
+
+VisiblePosition SelectionModifier::ModifyMovingRight(
+ TextGranularity granularity) {
+ switch (granularity) {
+ case TextGranularity::kCharacter:
+ if (!selection_.IsRange()) {
+ return RightPositionOf(ComputeVisibleExtent(selection_));
+ }
+ if (DirectionOfSelection() == TextDirection::kLtr)
+ return CreateVisiblePosition(selection_.End(), selection_.Affinity());
+ return CreateVisiblePosition(selection_.Start(), selection_.Affinity());
+ case TextGranularity::kWord: {
+ const bool skips_space_when_moving_right =
+ GetFrame().GetEditor().Behavior().ShouldSkipSpaceWhenMovingRight();
+ return RightWordPosition(ComputeVisibleExtent(selection_),
+ skips_space_when_moving_right);
+ }
+ case TextGranularity::kSentence:
+ case TextGranularity::kLine:
+ case TextGranularity::kParagraph:
+ case TextGranularity::kSentenceBoundary:
+ case TextGranularity::kParagraphBoundary:
+ case TextGranularity::kDocumentBoundary:
+ // TODO(editing-dev): Implement all of the above.
+ return ModifyMovingForward(granularity);
+ case TextGranularity::kLineBoundary:
+ return RightBoundaryOfLine(StartForPlatform(),
+ DirectionOfEnclosingBlock());
+ }
+ NOTREACHED() << static_cast<int>(granularity);
+ return VisiblePosition();
+}
+
+VisiblePosition SelectionModifier::ModifyMovingForward(
+ TextGranularity granularity) {
+ // TODO(editing-dev): Stay in editable content for the less common
+ // granularities.
+ switch (granularity) {
+ case TextGranularity::kCharacter:
+ if (selection_.IsRange())
+ return CreateVisiblePosition(selection_.End(), selection_.Affinity());
+ return NextPositionOf(ComputeVisibleExtent(selection_),
+ kCanSkipOverEditingBoundary);
+ case TextGranularity::kWord:
+ return NextWordPositionForPlatform(ComputeVisibleExtent(selection_));
+ case TextGranularity::kSentence:
+ return NextSentencePosition(ComputeVisibleExtent(selection_));
+ case TextGranularity::kLine: {
+ // down-arrowing from a range selection that ends at the start of a line
+ // needs to leave the selection at that line start (no need to call
+ // nextLinePosition!)
+ const VisiblePosition& pos = EndForPlatform();
+ if (selection_.IsRange() && IsStartOfLine(pos))
+ return pos;
+ return NextLinePosition(
+ pos,
+ LineDirectionPointForBlockDirectionNavigation(selection_.Start()));
+ }
+ case TextGranularity::kParagraph:
+ return NextParagraphPosition(
+ EndForPlatform(),
+ LineDirectionPointForBlockDirectionNavigation(selection_.Start()));
+ case TextGranularity::kSentenceBoundary:
+ return EndOfSentence(EndForPlatform());
+ case TextGranularity::kLineBoundary:
+ return LogicalEndOfLine(EndForPlatform());
+ case TextGranularity::kParagraphBoundary:
+ return EndOfParagraph(EndForPlatform());
+ case TextGranularity::kDocumentBoundary: {
+ const VisiblePosition& pos = EndForPlatform();
+ if (IsEditablePosition(pos.DeepEquivalent()))
+ return EndOfEditableContent(pos);
+ return EndOfDocument(pos);
+ }
+ }
+ NOTREACHED() << static_cast<int>(granularity);
+ return VisiblePosition();
+}
+
+VisiblePosition SelectionModifier::ModifyExtendingLeftInternal(
+ TextGranularity granularity) {
+ // The difference between modifyExtendingLeft and modifyExtendingBackward is:
+ // modifyExtendingBackward always extends backward logically.
+ // modifyExtendingLeft behaves the same as modifyExtendingBackward except for
+ // extending character or word, it extends backward logically if the enclosing
+ // block is LTR direction, but it extends forward logically if the enclosing
+ // block is RTL direction.
+ switch (granularity) {
+ case TextGranularity::kCharacter:
+ if (DirectionOfEnclosingBlock() == TextDirection::kLtr) {
+ return PreviousPositionOf(ComputeVisibleExtent(selection_),
+ kCanSkipOverEditingBoundary);
+ }
+ return NextPositionOf(ComputeVisibleExtent(selection_),
+ kCanSkipOverEditingBoundary);
+ case TextGranularity::kWord:
+ if (DirectionOfEnclosingBlock() == TextDirection::kLtr) {
+ return PreviousWordPosition(ComputeVisibleExtent(selection_));
+ }
+ return NextWordPositionForPlatform(ComputeVisibleExtent(selection_));
+ case TextGranularity::kLineBoundary:
+ if (DirectionOfEnclosingBlock() == TextDirection::kLtr)
+ return ModifyExtendingBackwardInternal(granularity);
+ return ModifyExtendingForwardInternal(granularity);
+ case TextGranularity::kSentence:
+ case TextGranularity::kLine:
+ case TextGranularity::kParagraph:
+ case TextGranularity::kSentenceBoundary:
+ case TextGranularity::kParagraphBoundary:
+ case TextGranularity::kDocumentBoundary:
+ return ModifyExtendingBackwardInternal(granularity);
+ }
+ NOTREACHED() << static_cast<int>(granularity);
+ return VisiblePosition();
+}
+
+VisiblePosition SelectionModifier::ModifyExtendingLeft(
+ TextGranularity granularity) {
+ const VisiblePosition& pos = ModifyExtendingLeftInternal(granularity);
+ if (DirectionOfEnclosingBlock() == TextDirection::kLtr)
+ return AdjustBackwardPositionForUserSelectAll(pos);
+ return AdjustForwardPositionForUserSelectAll(pos);
+}
+
+VisiblePosition SelectionModifier::ModifyExtendingBackwardInternal(
+ TextGranularity granularity) {
+ // Extending a selection backward by word or character from just after a table
+ // selects the table. This "makes sense" from the user perspective, esp. when
+ // deleting. It was done here instead of in VisiblePosition because we want
+ // VPs to iterate over everything.
+ switch (granularity) {
+ case TextGranularity::kCharacter:
+ return PreviousPositionOf(ComputeVisibleExtent(selection_),
+ kCanSkipOverEditingBoundary);
+ case TextGranularity::kWord:
+ return PreviousWordPosition(ComputeVisibleExtent(selection_));
+ case TextGranularity::kSentence:
+ return PreviousSentencePosition(ComputeVisibleExtent(selection_));
+ case TextGranularity::kLine:
+ return PreviousLinePosition(
+ ComputeVisibleExtent(selection_),
+ LineDirectionPointForBlockDirectionNavigation(selection_.Extent()));
+ case TextGranularity::kParagraph:
+ return PreviousParagraphPosition(
+ ComputeVisibleExtent(selection_),
+ LineDirectionPointForBlockDirectionNavigation(selection_.Extent()));
+ case TextGranularity::kSentenceBoundary:
+ return StartOfSentence(StartForPlatform());
+ case TextGranularity::kLineBoundary:
+ return LogicalStartOfLine(StartForPlatform());
+ case TextGranularity::kParagraphBoundary:
+ return StartOfParagraph(StartForPlatform());
+ case TextGranularity::kDocumentBoundary: {
+ const VisiblePosition pos = StartForPlatform();
+ if (IsEditablePosition(pos.DeepEquivalent()))
+ return StartOfEditableContent(pos);
+ return StartOfDocument(pos);
+ }
+ }
+ NOTREACHED() << static_cast<int>(granularity);
+ return VisiblePosition();
+}
+
+VisiblePosition SelectionModifier::ModifyExtendingBackward(
+ TextGranularity granularity) {
+ const VisiblePosition pos = ModifyExtendingBackwardInternal(granularity);
+ if (DirectionOfEnclosingBlock() == TextDirection::kLtr)
+ return AdjustBackwardPositionForUserSelectAll(pos);
+ return AdjustForwardPositionForUserSelectAll(pos);
+}
+
+VisiblePosition SelectionModifier::ModifyMovingLeft(
+ TextGranularity granularity) {
+ switch (granularity) {
+ case TextGranularity::kCharacter:
+ if (!selection_.IsRange()) {
+ return LeftPositionOf(ComputeVisibleExtent(selection_));
+ }
+ if (DirectionOfSelection() == TextDirection::kLtr)
+ return CreateVisiblePosition(selection_.Start(), selection_.Affinity());
+ return CreateVisiblePosition(selection_.End(), selection_.Affinity());
+ case TextGranularity::kWord: {
+ const bool skips_space_when_moving_right =
+ GetFrame().GetEditor().Behavior().ShouldSkipSpaceWhenMovingRight();
+ return LeftWordPosition(ComputeVisibleExtent(selection_),
+ skips_space_when_moving_right);
+ }
+ case TextGranularity::kSentence:
+ case TextGranularity::kLine:
+ case TextGranularity::kParagraph:
+ case TextGranularity::kSentenceBoundary:
+ case TextGranularity::kParagraphBoundary:
+ case TextGranularity::kDocumentBoundary:
+ // FIXME: Implement all of the above.
+ return ModifyMovingBackward(granularity);
+ case TextGranularity::kLineBoundary:
+ return LeftBoundaryOfLine(StartForPlatform(),
+ DirectionOfEnclosingBlock());
+ }
+ NOTREACHED() << static_cast<int>(granularity);
+ return VisiblePosition();
+}
+
+VisiblePosition SelectionModifier::ModifyMovingBackward(
+ TextGranularity granularity) {
+ VisiblePosition pos;
+ switch (granularity) {
+ case TextGranularity::kCharacter:
+ if (selection_.IsRange()) {
+ pos = CreateVisiblePosition(selection_.Start(), selection_.Affinity());
+ } else {
+ pos = PreviousPositionOf(ComputeVisibleExtent(selection_),
+ kCanSkipOverEditingBoundary);
+ }
+ break;
+ case TextGranularity::kWord:
+ pos = PreviousWordPosition(ComputeVisibleExtent(selection_));
+ break;
+ case TextGranularity::kSentence:
+ pos = PreviousSentencePosition(ComputeVisibleExtent(selection_));
+ break;
+ case TextGranularity::kLine:
+ pos = PreviousLinePosition(
+ StartForPlatform(),
+ LineDirectionPointForBlockDirectionNavigation(selection_.Start()));
+ break;
+ case TextGranularity::kParagraph:
+ pos = PreviousParagraphPosition(
+ StartForPlatform(),
+ LineDirectionPointForBlockDirectionNavigation(selection_.Start()));
+ break;
+ case TextGranularity::kSentenceBoundary:
+ pos = StartOfSentence(StartForPlatform());
+ break;
+ case TextGranularity::kLineBoundary:
+ pos = LogicalStartOfLine(StartForPlatform());
+ break;
+ case TextGranularity::kParagraphBoundary:
+ pos = StartOfParagraph(StartForPlatform());
+ break;
+ case TextGranularity::kDocumentBoundary:
+ pos = StartForPlatform();
+ if (IsEditablePosition(pos.DeepEquivalent()))
+ pos = StartOfEditableContent(pos);
+ else
+ pos = StartOfDocument(pos);
+ break;
+ }
+ return pos;
+}
+
+static bool IsBoundary(TextGranularity granularity) {
+ return granularity == TextGranularity::kLineBoundary ||
+ granularity == TextGranularity::kParagraphBoundary ||
+ granularity == TextGranularity::kDocumentBoundary;
+}
+
+VisiblePosition SelectionModifier::ComputeModifyPosition(
+ SelectionModifyAlteration alter,
+ SelectionModifyDirection direction,
+ TextGranularity granularity) {
+ switch (direction) {
+ case SelectionModifyDirection::kRight:
+ if (alter == SelectionModifyAlteration::kMove)
+ return ModifyMovingRight(granularity);
+ return ModifyExtendingRight(granularity);
+ case SelectionModifyDirection::kForward:
+ if (alter == SelectionModifyAlteration::kExtend)
+ return ModifyExtendingForward(granularity);
+ return ModifyMovingForward(granularity);
+ case SelectionModifyDirection::kLeft:
+ if (alter == SelectionModifyAlteration::kMove)
+ return ModifyMovingLeft(granularity);
+ return ModifyExtendingLeft(granularity);
+ case SelectionModifyDirection::kBackward:
+ if (alter == SelectionModifyAlteration::kExtend)
+ return ModifyExtendingBackward(granularity);
+ return ModifyMovingBackward(granularity);
+ }
+ NOTREACHED() << static_cast<int>(direction);
+ return VisiblePosition();
+}
+
+bool SelectionModifier::Modify(SelectionModifyAlteration alter,
+ SelectionModifyDirection direction,
+ TextGranularity granularity) {
+ DCHECK(!GetFrame().GetDocument()->NeedsLayoutTreeUpdate());
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ GetFrame().GetDocument()->Lifecycle());
+
+ selection_ = PrepareToModifySelection(alter, direction);
+
+ bool was_range = selection_.IsRange();
+ VisiblePosition original_start_position = selection_.VisibleStart();
+ VisiblePosition position =
+ ComputeModifyPosition(alter, direction, granularity);
+ if (position.IsNull())
+ return false;
+
+ if (IsSpatialNavigationEnabled(&GetFrame())) {
+ if (!was_range && alter == SelectionModifyAlteration::kMove &&
+ position.DeepEquivalent() == original_start_position.DeepEquivalent())
+ return false;
+ }
+
+ // Some of the above operations set an xPosForVerticalArrowNavigation.
+ // Setting a selection will clear it, so save it to possibly restore later.
+ // Note: the START position type is arbitrary because it is unused, it would
+ // be the requested position type if there were no
+ // xPosForVerticalArrowNavigation set.
+ LayoutUnit x =
+ LineDirectionPointForBlockDirectionNavigation(selection_.Start());
+
+ switch (alter) {
+ case SelectionModifyAlteration::kMove:
+ current_selection_ =
+ SelectionInDOMTree::Builder()
+ .Collapse(position.ToPositionWithAffinity())
+ .Build();
+ break;
+ case SelectionModifyAlteration::kExtend:
+
+ if (!selection_.IsCaret() &&
+ (granularity == TextGranularity::kWord ||
+ granularity == TextGranularity::kParagraph ||
+ granularity == TextGranularity::kLine) &&
+ !GetFrame()
+ .GetEditor()
+ .Behavior()
+ .ShouldExtendSelectionByWordOrLineAcrossCaret()) {
+ // Don't let the selection go across the base position directly. Needed
+ // to match mac behavior when, for instance, word-selecting backwards
+ // starting with the caret in the middle of a word and then
+ // word-selecting forward, leaving the caret in the same place where it
+ // was, instead of directly selecting to the end of the word.
+ const VisibleSelection& new_selection = CreateVisibleSelection(
+ SelectionInDOMTree::Builder(selection_.AsSelection())
+ .Extend(position.DeepEquivalent())
+ .Build());
+ if (selection_.IsBaseFirst() != new_selection.IsBaseFirst())
+ position = selection_.VisibleBase();
+ }
+
+ // Standard Mac behavior when extending to a boundary is grow the
+ // selection rather than leaving the base in place and moving the
+ // extent. Matches NSTextView.
+ if (!GetFrame()
+ .GetEditor()
+ .Behavior()
+ .ShouldAlwaysGrowSelectionWhenExtendingToBoundary() ||
+ selection_.IsCaret() || !IsBoundary(granularity)) {
+ current_selection_ = SelectionInDOMTree::Builder()
+ .Collapse(selection_.Base())
+ .Extend(position.DeepEquivalent())
+ .Build();
+ } else {
+ TextDirection text_direction = DirectionOfEnclosingBlock();
+ if (direction == SelectionModifyDirection::kForward ||
+ (text_direction == TextDirection::kLtr &&
+ direction == SelectionModifyDirection::kRight) ||
+ (text_direction == TextDirection::kRtl &&
+ direction == SelectionModifyDirection::kLeft)) {
+ current_selection_ =
+ SelectionInDOMTree::Builder()
+ .Collapse(selection_.IsBaseFirst()
+ ? selection_.Base()
+ : position.DeepEquivalent())
+ .Extend(selection_.IsBaseFirst() ? position.DeepEquivalent()
+ : selection_.Extent())
+ .Build();
+ } else {
+ current_selection_ =
+ SelectionInDOMTree::Builder()
+ .Collapse(selection_.IsBaseFirst() ? position.DeepEquivalent()
+ : selection_.Base())
+ .Extend(selection_.IsBaseFirst() ? selection_.Extent()
+ : position.DeepEquivalent())
+ .Build();
+ }
+ }
+ break;
+ }
+
+ if (granularity == TextGranularity::kLine ||
+ granularity == TextGranularity::kParagraph)
+ x_pos_for_vertical_arrow_navigation_ = x;
+
+ return true;
+}
+
+// TODO(yosin): Maybe baseline would be better?
+static bool AbsoluteCaretY(const VisiblePosition& c, int& y) {
+ IntRect rect = AbsoluteCaretBoundsOf(c);
+ if (rect.IsEmpty())
+ return false;
+ y = rect.Y() + rect.Height() / 2;
+ return true;
+}
+
+bool SelectionModifier::ModifyWithPageGranularity(
+ SelectionModifyAlteration alter,
+ unsigned vertical_distance,
+ SelectionModifyVerticalDirection direction) {
+ if (!vertical_distance)
+ return false;
+
+ DCHECK(!GetFrame().GetDocument()->NeedsLayoutTreeUpdate());
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ GetFrame().GetDocument()->Lifecycle());
+
+ selection_ = PrepareToModifySelection(
+ alter, direction == SelectionModifyVerticalDirection::kUp
+ ? SelectionModifyDirection::kBackward
+ : SelectionModifyDirection::kForward);
+
+ VisiblePosition pos;
+ LayoutUnit x_pos;
+ switch (alter) {
+ case SelectionModifyAlteration::kMove:
+ pos = CreateVisiblePosition(
+ direction == SelectionModifyVerticalDirection::kUp
+ ? selection_.Start()
+ : selection_.End(),
+ selection_.Affinity());
+ x_pos = LineDirectionPointForBlockDirectionNavigation(
+ direction == SelectionModifyVerticalDirection::kUp
+ ? selection_.Start()
+ : selection_.End());
+ break;
+ case SelectionModifyAlteration::kExtend:
+ pos = ComputeVisibleExtent(selection_);
+ x_pos =
+ LineDirectionPointForBlockDirectionNavigation(selection_.Extent());
+ break;
+ }
+
+ int start_y;
+ if (!AbsoluteCaretY(pos, start_y))
+ return false;
+ if (direction == SelectionModifyVerticalDirection::kUp)
+ start_y = -start_y;
+ int last_y = start_y;
+
+ VisiblePosition result;
+ VisiblePosition next;
+ for (VisiblePosition p = pos;; p = next) {
+ if (direction == SelectionModifyVerticalDirection::kUp)
+ next = PreviousLinePosition(p, x_pos);
+ else
+ next = NextLinePosition(p, x_pos);
+
+ if (next.IsNull() || next.DeepEquivalent() == p.DeepEquivalent())
+ break;
+ int next_y;
+ if (!AbsoluteCaretY(next, next_y))
+ break;
+ if (direction == SelectionModifyVerticalDirection::kUp)
+ next_y = -next_y;
+ if (next_y - start_y > static_cast<int>(vertical_distance))
+ break;
+ if (next_y >= last_y) {
+ last_y = next_y;
+ result = next;
+ }
+ }
+
+ if (result.IsNull())
+ return false;
+
+ switch (alter) {
+ case SelectionModifyAlteration::kMove:
+ current_selection_ =
+ SelectionInDOMTree::Builder()
+ .Collapse(result.ToPositionWithAffinity())
+ .SetAffinity(direction == SelectionModifyVerticalDirection::kUp
+ ? TextAffinity::kUpstream
+ : TextAffinity::kDownstream)
+ .Build();
+ break;
+ case SelectionModifyAlteration::kExtend: {
+ current_selection_ = SelectionInDOMTree::Builder()
+ .Collapse(selection_.Base())
+ .Extend(result.DeepEquivalent())
+ .Build();
+ break;
+ }
+ }
+
+ return true;
+}
+
+// Abs x/y position of the caret ignoring transforms.
+// TODO(yosin) navigation with transforms should be smarter.
+static LayoutUnit LineDirectionPointForBlockDirectionNavigationOf(
+ const VisiblePosition& visible_position) {
+ if (visible_position.IsNull())
+ return LayoutUnit();
+
+ const LocalCaretRect& caret_rect =
+ LocalCaretRectOfPosition(visible_position.ToPositionWithAffinity());
+ if (caret_rect.IsEmpty())
+ return LayoutUnit();
+
+ // This ignores transforms on purpose, for now. Vertical navigation is done
+ // without consulting transforms, so that 'up' in transformed text is 'up'
+ // relative to the text, not absolute 'up'.
+ const FloatPoint& caret_point = caret_rect.layout_object->LocalToAbsolute(
+ FloatPoint(caret_rect.rect.Location()));
+ const LayoutObject* const containing_block =
+ caret_rect.layout_object->ContainingBlock();
+ // Just use ourselves to determine the writing mode if we have no containing
+ // block.
+ const LayoutObject* const layout_object =
+ containing_block ? containing_block : caret_rect.layout_object;
+ return LayoutUnit(layout_object->IsHorizontalWritingMode() ? caret_point.X()
+ : caret_point.Y());
+}
+
+LayoutUnit SelectionModifier::LineDirectionPointForBlockDirectionNavigation(
+ const Position& pos) {
+ LayoutUnit x;
+
+ if (selection_.IsNone())
+ return x;
+
+ if (x_pos_for_vertical_arrow_navigation_ ==
+ NoXPosForVerticalArrowNavigation()) {
+ VisiblePosition visible_position =
+ CreateVisiblePosition(pos, selection_.Affinity());
+ // VisiblePosition creation can fail here if a node containing the selection
+ // becomes visibility:hidden after the selection is created and before this
+ // function is called.
+ x = LineDirectionPointForBlockDirectionNavigationOf(visible_position);
+ x_pos_for_vertical_arrow_navigation_ = x;
+ } else {
+ x = x_pos_for_vertical_arrow_navigation_;
+ }
+
+ return x;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_modifier.h b/chromium/third_party/blink/renderer/core/editing/selection_modifier.h
new file mode 100644
index 00000000000..21b439942b9
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_modifier.h
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_MODIFIER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_MODIFIER_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/platform/layout_unit.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+
+namespace blink {
+
+class LocalFrame;
+
+enum class SelectionModifyAlteration { kMove, kExtend };
+enum class SelectionModifyVerticalDirection { kUp, kDown };
+enum class SelectionModifyDirection { kBackward, kForward, kLeft, kRight };
+
+class SelectionModifier {
+ STACK_ALLOCATED();
+
+ public:
+ // |frame| is used for providing settings.
+ SelectionModifier(const LocalFrame& /* frame */,
+ const SelectionInDOMTree&,
+ LayoutUnit);
+ SelectionModifier(const LocalFrame&, const SelectionInDOMTree&);
+
+ LayoutUnit XPosForVerticalArrowNavigation() const {
+ return x_pos_for_vertical_arrow_navigation_;
+ }
+
+ // TODO(editing-dev): We should rename |Selection()| to
+ // |ComputeVisibleSelectionDeprecated()| and introduce |GetSelection()|
+ // to return |current_selection_|.
+ VisibleSelection Selection() const;
+
+ bool Modify(SelectionModifyAlteration,
+ SelectionModifyDirection,
+ TextGranularity);
+ bool ModifyWithPageGranularity(SelectionModifyAlteration,
+ unsigned vertical_distance,
+ SelectionModifyVerticalDirection);
+ void SetSelectionIsDirectional(bool selection_is_directional) {
+ selection_is_directional_ = selection_is_directional;
+ }
+
+ private:
+ const LocalFrame& GetFrame() const { return *frame_; }
+
+ static bool ShouldAlwaysUseDirectionalSelection(const LocalFrame&);
+ VisibleSelection PrepareToModifySelection(SelectionModifyAlteration,
+ SelectionModifyDirection) const;
+ TextDirection DirectionOfEnclosingBlock() const;
+ TextDirection DirectionOfSelection() const;
+ VisiblePosition PositionForPlatform(bool is_get_start) const;
+ VisiblePosition StartForPlatform() const;
+ VisiblePosition EndForPlatform() const;
+ LayoutUnit LineDirectionPointForBlockDirectionNavigation(const Position&);
+ VisiblePosition ComputeModifyPosition(SelectionModifyAlteration,
+ SelectionModifyDirection,
+ TextGranularity);
+ VisiblePosition ModifyExtendingRight(TextGranularity);
+ VisiblePosition ModifyExtendingRightInternal(TextGranularity);
+ VisiblePosition ModifyExtendingForward(TextGranularity);
+ VisiblePosition ModifyExtendingForwardInternal(TextGranularity);
+ VisiblePosition ModifyMovingRight(TextGranularity);
+ VisiblePosition ModifyMovingForward(TextGranularity);
+ VisiblePosition ModifyExtendingLeft(TextGranularity);
+ VisiblePosition ModifyExtendingLeftInternal(TextGranularity);
+ VisiblePosition ModifyExtendingBackward(TextGranularity);
+ VisiblePosition ModifyExtendingBackwardInternal(TextGranularity);
+ VisiblePosition ModifyMovingLeft(TextGranularity);
+ VisiblePosition ModifyMovingBackward(TextGranularity);
+ VisiblePosition NextWordPositionForPlatform(const VisiblePosition&);
+
+ // TODO(editing-dev): We should handle |skips_spaces_when_moving_right| in
+ // another way, e.g. pass |EditingBehavior()|.
+ static VisiblePosition LeftWordPosition(const VisiblePosition&,
+ bool skips_space_when_moving_right);
+ static VisiblePosition RightWordPosition(const VisiblePosition&,
+ bool skips_space_when_moving_right);
+
+ Member<const LocalFrame> frame_;
+ // TODO(editing-dev): We should get rid of |selection_| once we change
+ // all member functions not to use |selection_|.
+ // |selection_| is used as implicit parameter or a cache instead of pass it.
+ VisibleSelection selection_;
+ // TODO(editing-dev): We should introduce |GetSelection()| to return
+ // |result_| to replace |Selection().AsSelection()|.
+ // |current_selection_| holds initial value and result of |Modify()|.
+ SelectionInDOMTree current_selection_;
+ LayoutUnit x_pos_for_vertical_arrow_navigation_;
+ bool selection_is_directional_ = false;
+
+ DISALLOW_COPY_AND_ASSIGN(SelectionModifier);
+};
+
+LayoutUnit NoXPosForVerticalArrowNavigation();
+
+// Following functions are exported for using in SelectionModifier and
+// testing only.
+
+// TODO(yosin) Since return value of |leftPositionOf()| with |VisiblePosition|
+// isn't defined well on flat tree, we should not use it for a position in
+// flat tree.
+CORE_EXPORT VisiblePosition LeftPositionOf(const VisiblePosition&);
+CORE_EXPORT VisiblePositionInFlatTree
+LeftPositionOf(const VisiblePositionInFlatTree&);
+// TODO(yosin) Since return value of |rightPositionOf()| with |VisiblePosition|
+// isn't defined well on flat tree, we should not use it for a position in
+// flat tree.
+CORE_EXPORT VisiblePosition RightPositionOf(const VisiblePosition&);
+CORE_EXPORT VisiblePositionInFlatTree
+RightPositionOf(const VisiblePositionInFlatTree&);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_MODIFIER_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_modifier_character.cc b/chromium/third_party/blink/renderer/core/editing/selection_modifier_character.cc
new file mode 100644
index 00000000000..71b16170594
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_modifier_character.cc
@@ -0,0 +1,441 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/selection_modifier.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/inline_box_position.h"
+#include "third_party/blink/renderer/core/editing/inline_box_traversal.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/layout/api/line_layout_api_shim.h"
+#include "third_party/blink/renderer/core/layout/api/line_layout_item.h"
+#include "third_party/blink/renderer/core/layout/line/inline_text_box.h"
+#include "third_party/blink/renderer/core/layout/line/root_inline_box.h"
+
+namespace blink {
+
+namespace {
+
+// The traversal strategy for |LeftPositionOf()|.
+template <typename Strategy>
+struct TraversalLeft {
+ STATIC_ONLY(TraversalLeft);
+
+ static InlineBox* BackwardLeafChildOf(const InlineBox& box) {
+ return box.NextLeafChild();
+ }
+
+ static int CaretEndOffsetOf(const InlineBox& box) {
+ return box.CaretRightmostOffset();
+ }
+
+ static int CaretMinOffsetOf(TextDirection direction, const InlineBox& box) {
+ if (direction == TextDirection::kLtr)
+ return box.CaretMinOffset();
+ return box.CaretMaxOffset();
+ }
+
+ static int CaretStartOffsetOf(const InlineBox& box) {
+ return box.CaretLeftmostOffset();
+ }
+
+ static InlineBox* FindBackwardBidiRun(const InlineBox& box,
+ unsigned bidi_level) {
+ return InlineBoxTraversal::FindRightBidiRun(box, bidi_level);
+ }
+
+ static InlineBox* FindBackwardBoundaryOfEntireBidiRun(const InlineBox& box,
+ unsigned bidi_level) {
+ return InlineBoxTraversal::FindRightBoundaryOfEntireBidiRun(box,
+ bidi_level);
+ }
+
+ static InlineBox* FindForwardBidiRun(const InlineBox& box,
+ unsigned bidi_level) {
+ return InlineBoxTraversal::FindLeftBidiRun(box, bidi_level);
+ }
+
+ static InlineBox* FindForwardBoundaryOfEntireBidiRun(const InlineBox& box,
+ unsigned bidi_level) {
+ return InlineBoxTraversal::FindLeftBoundaryOfEntireBidiRun(box, bidi_level);
+ }
+
+ static int ForwardGraphemeBoundaryOf(TextDirection direction,
+ const Node& node,
+ int offset) {
+ if (direction == TextDirection::kLtr)
+ return PreviousGraphemeBoundaryOf(node, offset);
+ return NextGraphemeBoundaryOf(node, offset);
+ }
+
+ static InlineBox* ForwardLeafChildOf(const InlineBox& box) {
+ return box.PrevLeafChild();
+ }
+
+ static InlineBox* ForwardLeafChildIgnoringLineBreakOf(const InlineBox& box) {
+ return box.PrevLeafChildIgnoringLineBreak();
+ }
+
+ static PositionTemplate<Strategy> ForwardVisuallyDistinctCandidateOf(
+ TextDirection direction,
+ const PositionTemplate<Strategy>& position) {
+ if (direction == TextDirection::kLtr)
+ return PreviousVisuallyDistinctCandidate(position);
+ return NextVisuallyDistinctCandidate(position);
+ }
+
+ static VisiblePositionTemplate<Strategy> HonorEditingBoundary(
+ TextDirection direction,
+ const VisiblePositionTemplate<Strategy>& visible_position,
+ const PositionTemplate<Strategy>& anchor) {
+ if (direction == TextDirection::kLtr) {
+ return AdjustBackwardPositionToAvoidCrossingEditingBoundaries(
+ visible_position, anchor);
+ }
+ return AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ visible_position, anchor);
+ }
+
+ static Node* LogicalStartBoxOf(TextDirection direction,
+ const InlineBox& box,
+ InlineBox*& result_box) {
+ if (direction == TextDirection::kLtr)
+ return box.Root().GetLogicalStartBoxWithNode(result_box);
+ return box.Root().GetLogicalEndBoxWithNode(result_box);
+ }
+
+ static bool IsOvershot(int offset, const InlineBox& box) {
+ if (box.IsLeftToRightDirection())
+ return offset < box.CaretMinOffset();
+ return offset > box.CaretMaxOffset();
+ }
+};
+
+// The traversal strategy for |RightPositionOf()|.
+template <typename Strategy>
+struct TraversalRight {
+ STATIC_ONLY(TraversalRight);
+
+ static InlineBox* BackwardLeafChildOf(const InlineBox& box) {
+ return box.PrevLeafChild();
+ }
+
+ static int CaretEndOffsetOf(const InlineBox& box) {
+ return box.CaretLeftmostOffset();
+ }
+
+ static int CaretMinOffsetOf(TextDirection direction, const InlineBox& box) {
+ if (direction == TextDirection::kLtr)
+ return box.CaretMaxOffset();
+ return box.CaretMinOffset();
+ }
+
+ static int CaretStartOffsetOf(const InlineBox& box) {
+ return box.CaretRightmostOffset();
+ }
+
+ static InlineBox* FindBackwardBidiRun(const InlineBox& box,
+ unsigned bidi_level) {
+ return InlineBoxTraversal::FindLeftBidiRun(box, bidi_level);
+ }
+
+ static InlineBox* FindBackwardBoundaryOfEntireBidiRun(const InlineBox& box,
+ unsigned bidi_level) {
+ return InlineBoxTraversal::FindLeftBoundaryOfEntireBidiRun(box, bidi_level);
+ }
+
+ static InlineBox* FindForwardBidiRun(const InlineBox& box,
+ unsigned bidi_level) {
+ return InlineBoxTraversal::FindRightBidiRun(box, bidi_level);
+ }
+
+ static InlineBox* FindForwardBoundaryOfEntireBidiRun(const InlineBox& box,
+ unsigned bidi_level) {
+ return InlineBoxTraversal::FindRightBoundaryOfEntireBidiRun(box,
+ bidi_level);
+ }
+
+ static int ForwardGraphemeBoundaryOf(TextDirection direction,
+ const Node& node,
+ int offset) {
+ if (direction == TextDirection::kLtr)
+ return NextGraphemeBoundaryOf(node, offset);
+ return PreviousGraphemeBoundaryOf(node, offset);
+ }
+
+ static InlineBox* ForwardLeafChildOf(const InlineBox& box) {
+ return box.NextLeafChild();
+ }
+
+ static InlineBox* ForwardLeafChildIgnoringLineBreakOf(const InlineBox& box) {
+ return box.NextLeafChildIgnoringLineBreak();
+ }
+
+ static PositionTemplate<Strategy> ForwardVisuallyDistinctCandidateOf(
+ TextDirection direction,
+ const PositionTemplate<Strategy>& position) {
+ if (direction == TextDirection::kLtr)
+ return NextVisuallyDistinctCandidate(position);
+ return PreviousVisuallyDistinctCandidate(position);
+ }
+
+ static VisiblePositionTemplate<Strategy> HonorEditingBoundary(
+ TextDirection direction,
+ const VisiblePositionTemplate<Strategy>& visible_position,
+ const PositionTemplate<Strategy>& anchor) {
+ if (direction == TextDirection::kLtr) {
+ return AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ visible_position, anchor);
+ }
+ return AdjustBackwardPositionToAvoidCrossingEditingBoundaries(
+ visible_position, anchor);
+ }
+
+ static Node* LogicalStartBoxOf(TextDirection direction,
+ const InlineBox& box,
+ InlineBox*& result_box) {
+ if (direction == TextDirection::kLtr)
+ return box.Root().GetLogicalEndBoxWithNode(result_box);
+ return box.Root().GetLogicalStartBoxWithNode(result_box);
+ }
+
+ static bool IsOvershot(int offset, const InlineBox& box) {
+ if (box.IsLeftToRightDirection())
+ return offset > box.CaretMaxOffset();
+ return offset < box.CaretMinOffset();
+ }
+};
+
+// TODO(yosin): We should rename local variables and comments in
+// |TraverseInternalAlgorithm()| to generic name based on |Traversal| instead of
+// assuming right-to-left traversal.
+template <typename Strategy, typename Traversal>
+static PositionTemplate<Strategy> TraverseInternalAlgorithm(
+ const VisiblePositionTemplate<Strategy>& visible_position) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ const PositionTemplate<Strategy> deep_position =
+ visible_position.DeepEquivalent();
+ PositionTemplate<Strategy> p = deep_position;
+
+ if (p.IsNull())
+ return PositionTemplate<Strategy>();
+
+ const PositionTemplate<Strategy> downstream_start =
+ MostForwardCaretPosition(p);
+ const TextDirection primary_direction = PrimaryDirectionOf(*p.AnchorNode());
+ const TextAffinity affinity = visible_position.Affinity();
+
+ while (true) {
+ InlineBoxPosition box_position = ComputeInlineBoxPosition(
+ PositionWithAffinityTemplate<Strategy>(p, affinity));
+ const InlineBox* box = box_position.inline_box;
+ int offset = box_position.offset_in_box;
+ if (!box) {
+ return Traversal::ForwardVisuallyDistinctCandidateOf(primary_direction,
+ deep_position);
+ }
+ LineLayoutItem line_layout_item = box->GetLineLayoutItem();
+
+ while (true) {
+ if ((line_layout_item.IsAtomicInlineLevel() || line_layout_item.IsBR()) &&
+ offset == Traversal::CaretEndOffsetOf(*box)) {
+ return Traversal::ForwardVisuallyDistinctCandidateOf(box->Direction(),
+ deep_position);
+ }
+ if (!line_layout_item.GetNode()) {
+ box = Traversal::ForwardLeafChildOf(*box);
+ if (!box) {
+ return Traversal::ForwardVisuallyDistinctCandidateOf(
+ primary_direction, deep_position);
+ }
+ line_layout_item = box->GetLineLayoutItem();
+ offset = Traversal::CaretEndOffsetOf(*box);
+ continue;
+ }
+
+ offset = Traversal::ForwardGraphemeBoundaryOf(
+ box->Direction(), *line_layout_item.GetNode(), offset);
+
+ const int caret_min_offset = box->CaretMinOffset();
+ const int caret_max_offset = box->CaretMaxOffset();
+
+ if (offset > caret_min_offset && offset < caret_max_offset)
+ break;
+
+ if (Traversal::IsOvershot(offset, *box)) {
+ // Overshot to the left.
+ InlineBox* const prev_box =
+ Traversal::ForwardLeafChildIgnoringLineBreakOf(*box);
+ if (!prev_box) {
+ const PositionTemplate<Strategy>& position_on_left =
+ Traversal::ForwardVisuallyDistinctCandidateOf(
+ primary_direction, visible_position.DeepEquivalent());
+ if (position_on_left.IsNull())
+ return PositionTemplate<Strategy>();
+
+ const InlineBox* box_on_left =
+ ComputeInlineBoxPosition(PositionWithAffinityTemplate<Strategy>(
+ position_on_left, affinity))
+ .inline_box;
+ if (box_on_left && box_on_left->Root() == box->Root())
+ return PositionTemplate<Strategy>();
+ return position_on_left;
+ }
+
+ // Reposition at the other logical position corresponding to our
+ // edge's visual position and go for another round.
+ box = prev_box;
+ line_layout_item = box->GetLineLayoutItem();
+ offset = Traversal::CaretEndOffsetOf(*prev_box);
+ continue;
+ }
+
+ DCHECK_EQ(offset, Traversal::CaretStartOffsetOf(*box));
+
+ unsigned char level = box->BidiLevel();
+ InlineBox* prev_box = Traversal::ForwardLeafChildOf(*box);
+
+ if (box->Direction() == primary_direction) {
+ if (!prev_box) {
+ InlineBox* logical_start = nullptr;
+ if (Traversal::LogicalStartBoxOf(primary_direction, *box,
+ logical_start)) {
+ box = logical_start;
+ line_layout_item = box->GetLineLayoutItem();
+ offset = Traversal::CaretMinOffsetOf(primary_direction, *box);
+ }
+ break;
+ }
+ if (prev_box->BidiLevel() >= level)
+ break;
+
+ level = prev_box->BidiLevel();
+
+ InlineBox* const next_box = Traversal::FindBackwardBidiRun(*box, level);
+ if (next_box && next_box->BidiLevel() == level)
+ break;
+
+ box = prev_box;
+ line_layout_item = box->GetLineLayoutItem();
+ offset = Traversal::CaretEndOffsetOf(*box);
+ if (box->Direction() == primary_direction)
+ break;
+ continue;
+ }
+
+ while (prev_box && !prev_box->GetLineLayoutItem().GetNode())
+ prev_box = Traversal::ForwardLeafChildOf(*prev_box);
+
+ if (prev_box) {
+ box = prev_box;
+ line_layout_item = box->GetLineLayoutItem();
+ offset = Traversal::CaretEndOffsetOf(*box);
+ if (box->BidiLevel() > level) {
+ prev_box = Traversal::FindForwardBidiRun(*prev_box, level);
+ if (!prev_box || prev_box->BidiLevel() < level)
+ continue;
+ }
+ break;
+ }
+ // Trailing edge of a secondary run. Set to the leading edge of
+ // the entire run.
+ while (true) {
+ box = Traversal::FindBackwardBoundaryOfEntireBidiRun(*box, level);
+ if (box->BidiLevel() == level)
+ break;
+ level = box->BidiLevel();
+ box = Traversal::FindForwardBoundaryOfEntireBidiRun(*box, level);
+ if (box->BidiLevel() == level)
+ break;
+ level = box->BidiLevel();
+ }
+ line_layout_item = box->GetLineLayoutItem();
+ offset = Traversal::CaretMinOffsetOf(primary_direction, *box);
+ break;
+ }
+
+ p = PositionTemplate<Strategy>::EditingPositionOf(
+ line_layout_item.GetNode(), offset);
+
+ if ((IsVisuallyEquivalentCandidate(p) &&
+ MostForwardCaretPosition(p) != downstream_start) ||
+ p.AtStartOfTree() || p.AtEndOfTree())
+ return p;
+
+ DCHECK_NE(p, deep_position);
+ }
+}
+
+template <typename Strategy, typename Traversal>
+VisiblePositionTemplate<Strategy> TraverseAlgorithm(
+ const VisiblePositionTemplate<Strategy>& visible_position) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ const PositionTemplate<Strategy> pos =
+ TraverseInternalAlgorithm<Strategy, Traversal>(visible_position);
+ // TODO(yosin) Why can't we move left from the last position in a tree?
+ if (pos.AtStartOfTree() || pos.AtEndOfTree())
+ return VisiblePositionTemplate<Strategy>();
+
+ const VisiblePositionTemplate<Strategy> result = CreateVisiblePosition(pos);
+ DCHECK_NE(result.DeepEquivalent(), visible_position.DeepEquivalent());
+
+ return Traversal::HonorEditingBoundary(
+ DirectionOfEnclosingBlockOf(result.DeepEquivalent()), result,
+ visible_position.DeepEquivalent());
+}
+
+} // namespace
+
+VisiblePosition LeftPositionOf(const VisiblePosition& visible_position) {
+ return TraverseAlgorithm<EditingStrategy, TraversalLeft<EditingStrategy>>(
+ visible_position);
+}
+
+VisiblePositionInFlatTree LeftPositionOf(
+ const VisiblePositionInFlatTree& visible_position) {
+ return TraverseAlgorithm<EditingInFlatTreeStrategy,
+ TraversalLeft<EditingInFlatTreeStrategy>>(
+ visible_position);
+}
+
+VisiblePosition RightPositionOf(const VisiblePosition& visible_position) {
+ return TraverseAlgorithm<EditingStrategy, TraversalRight<EditingStrategy>>(
+ visible_position);
+}
+
+VisiblePositionInFlatTree RightPositionOf(
+ const VisiblePositionInFlatTree& visible_position) {
+ return TraverseAlgorithm<EditingInFlatTreeStrategy,
+ TraversalRight<EditingInFlatTreeStrategy>>(
+ visible_position);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_modifier_test.cc b/chromium/third_party/blink/renderer/core/editing/selection_modifier_test.cc
new file mode 100644
index 00000000000..e7de4b49bbf
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_modifier_test.cc
@@ -0,0 +1,96 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/selection_modifier.h"
+
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+
+namespace blink {
+
+class SelectionModifierTest : public EditingTestBase {};
+
+TEST_F(SelectionModifierTest, leftPositionOf) {
+ const char* body_content =
+ "<b id=zero>0</b><p id=host><b id=one>1</b><b id=two>22</b></p><b "
+ "id=three>333</b>";
+ const char* shadow_content =
+ "<b id=four>4444</b><content select=#two></content><content "
+ "select=#one></content><b id=five>55555</b>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Element* one = GetDocument().getElementById("one");
+ Element* two = GetDocument().getElementById("two");
+ Element* three = GetDocument().getElementById("three");
+ Element* four = shadow_root->getElementById("four");
+ Element* five = shadow_root->getElementById("five");
+
+ EXPECT_EQ(
+ Position(two->firstChild(), 1),
+ LeftPositionOf(CreateVisiblePosition(Position(one, 0))).DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(two->firstChild(), 1),
+ LeftPositionOf(CreateVisiblePosition(PositionInFlatTree(one, 0)))
+ .DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(one->firstChild(), 0),
+ LeftPositionOf(CreateVisiblePosition(Position(two, 0))).DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(four->firstChild(), 3),
+ LeftPositionOf(CreateVisiblePosition(PositionInFlatTree(two, 0)))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(two->firstChild(), 2),
+ LeftPositionOf(CreateVisiblePosition(Position(three, 0)))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(five->firstChild(), 5),
+ LeftPositionOf(CreateVisiblePosition(PositionInFlatTree(three, 0)))
+ .DeepEquivalent());
+}
+
+TEST_F(SelectionModifierTest, rightPositionOf) {
+ const char* body_content =
+ "<b id=zero>0</b><p id=host><b id=one>1</b><b id=two>22</b></p><b "
+ "id=three>333</b>";
+ const char* shadow_content =
+ "<p id=four>4444</p><content select=#two></content><content "
+ "select=#one></content><p id=five>55555</p>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* three = GetDocument().getElementById("three")->firstChild();
+ Node* four = shadow_root->getElementById("four")->firstChild();
+ Node* five = shadow_root->getElementById("five")->firstChild();
+
+ EXPECT_EQ(Position(), RightPositionOf(CreateVisiblePosition(Position(one, 1)))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(five, 0),
+ RightPositionOf(CreateVisiblePosition(PositionInFlatTree(one, 1)))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(one, 1),
+ RightPositionOf(CreateVisiblePosition(Position(two, 2)))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(one, 1),
+ RightPositionOf(CreateVisiblePosition(PositionInFlatTree(two, 2)))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(five, 0),
+ RightPositionOf(CreateVisiblePosition(Position(four, 4)))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(two, 0),
+ RightPositionOf(CreateVisiblePosition(PositionInFlatTree(four, 4)))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(),
+ RightPositionOf(CreateVisiblePosition(Position(five, 5)))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(three, 0),
+ RightPositionOf(CreateVisiblePosition(PositionInFlatTree(five, 5)))
+ .DeepEquivalent());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_modifier_word.cc b/chromium/third_party/blink/renderer/core/editing/selection_modifier_word.cc
new file mode 100644
index 00000000000..93248cf4926
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_modifier_word.cc
@@ -0,0 +1,437 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+#include "third_party/blink/renderer/core/editing/selection_modifier.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/inline_box_position.h"
+#include "third_party/blink/renderer/core/editing/rendered_position.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/layout/line/inline_text_box.h"
+#include "third_party/blink/renderer/core/layout/line/root_inline_box.h"
+#include "third_party/blink/renderer/platform/text/text_break_iterator.h"
+
+namespace blink {
+
+namespace {
+
+// This class holds a list of |InlineBox| in logical order.
+// TDOO(editing-dev): We should utilize |CachedLogicallyOrderedLeafBoxes| class
+// in |CompositeEditCommand::DeleteInsignificantText()|.
+class CachedLogicallyOrderedLeafBoxes final {
+ public:
+ CachedLogicallyOrderedLeafBoxes() = default;
+
+ const InlineTextBox* PreviousTextBox(const RootInlineBox*,
+ const InlineTextBox*);
+ const InlineTextBox* NextTextBox(const RootInlineBox*, const InlineTextBox*);
+
+ size_t size() const { return leaf_boxes_.size(); }
+ const InlineBox* FirstBox() const { return leaf_boxes_[0]; }
+
+ private:
+ const Vector<InlineBox*>& CollectBoxes(const RootInlineBox*);
+ int BoxIndexInLeaves(const InlineTextBox*) const;
+
+ const RootInlineBox* root_inline_box_ = nullptr;
+ Vector<InlineBox*> leaf_boxes_;
+};
+
+const InlineTextBox* CachedLogicallyOrderedLeafBoxes::PreviousTextBox(
+ const RootInlineBox* root,
+ const InlineTextBox* box) {
+ if (!root)
+ return nullptr;
+
+ CollectBoxes(root);
+
+ // If box is null, root is box's previous RootInlineBox, and previousBox is
+ // the last logical box in root.
+ int box_index = leaf_boxes_.size() - 1;
+ if (box)
+ box_index = BoxIndexInLeaves(box) - 1;
+
+ for (int i = box_index; i >= 0; --i) {
+ if (leaf_boxes_[i]->IsInlineTextBox())
+ return ToInlineTextBox(leaf_boxes_[i]);
+ }
+
+ return nullptr;
+}
+
+const InlineTextBox* CachedLogicallyOrderedLeafBoxes::NextTextBox(
+ const RootInlineBox* root,
+ const InlineTextBox* box) {
+ if (!root)
+ return nullptr;
+
+ CollectBoxes(root);
+
+ // If box is null, root is box's next RootInlineBox, and nextBox is the first
+ // logical box in root. Otherwise, root is box's RootInlineBox, and nextBox is
+ // the next logical box in the same line.
+ size_t next_box_index = 0;
+ if (box)
+ next_box_index = BoxIndexInLeaves(box) + 1;
+
+ for (size_t i = next_box_index; i < leaf_boxes_.size(); ++i) {
+ if (leaf_boxes_[i]->IsInlineTextBox())
+ return ToInlineTextBox(leaf_boxes_[i]);
+ }
+
+ return nullptr;
+}
+
+const Vector<InlineBox*>& CachedLogicallyOrderedLeafBoxes::CollectBoxes(
+ const RootInlineBox* root) {
+ if (root_inline_box_ != root) {
+ root_inline_box_ = root;
+ leaf_boxes_.clear();
+ root->CollectLeafBoxesInLogicalOrder(leaf_boxes_);
+ }
+ return leaf_boxes_;
+}
+
+int CachedLogicallyOrderedLeafBoxes::BoxIndexInLeaves(
+ const InlineTextBox* box) const {
+ for (size_t i = 0; i < leaf_boxes_.size(); ++i) {
+ if (box == leaf_boxes_[i])
+ return i;
+ }
+ return 0;
+}
+
+const InlineTextBox* LogicallyPreviousBox(
+ const VisiblePosition& visible_position,
+ const InlineTextBox* text_box,
+ bool& previous_box_in_different_block,
+ CachedLogicallyOrderedLeafBoxes& leaf_boxes) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ const InlineBox* start_box = text_box;
+
+ const InlineTextBox* previous_box =
+ leaf_boxes.PreviousTextBox(&start_box->Root(), text_box);
+ if (previous_box)
+ return previous_box;
+
+ previous_box =
+ leaf_boxes.PreviousTextBox(start_box->Root().PrevRootBox(), nullptr);
+ if (previous_box)
+ return previous_box;
+
+ for (;;) {
+ Node* start_node = start_box->GetLineLayoutItem().NonPseudoNode();
+ if (!start_node)
+ break;
+
+ Position position = PreviousRootInlineBoxCandidatePosition(
+ start_node, visible_position, kContentIsEditable);
+ if (position.IsNull())
+ break;
+
+ RenderedPosition rendered_position(position, TextAffinity::kDownstream);
+ const RootInlineBox* previous_root = rendered_position.RootBox();
+ if (!previous_root)
+ break;
+
+ previous_box = leaf_boxes.PreviousTextBox(previous_root, nullptr);
+ if (previous_box) {
+ previous_box_in_different_block = true;
+ return previous_box;
+ }
+
+ if (!leaf_boxes.size())
+ break;
+ start_box = leaf_boxes.FirstBox();
+ }
+ return nullptr;
+}
+
+const InlineTextBox* LogicallyNextBox(
+ const VisiblePosition& visible_position,
+ const InlineTextBox* text_box,
+ bool& next_box_in_different_block,
+ CachedLogicallyOrderedLeafBoxes& leaf_boxes) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ const InlineBox* start_box = text_box;
+
+ const InlineTextBox* next_box =
+ leaf_boxes.NextTextBox(&start_box->Root(), text_box);
+ if (next_box)
+ return next_box;
+
+ next_box = leaf_boxes.NextTextBox(start_box->Root().NextRootBox(), nullptr);
+ if (next_box)
+ return next_box;
+
+ for (;;) {
+ Node* start_node = start_box->GetLineLayoutItem().NonPseudoNode();
+ if (!start_node)
+ break;
+
+ Position position = NextRootInlineBoxCandidatePosition(
+ start_node, visible_position, kContentIsEditable);
+ if (position.IsNull())
+ break;
+
+ RenderedPosition rendered_position(position, TextAffinity::kDownstream);
+ const RootInlineBox* next_root = rendered_position.RootBox();
+ if (!next_root)
+ break;
+
+ next_box = leaf_boxes.NextTextBox(next_root, nullptr);
+ if (next_box) {
+ next_box_in_different_block = true;
+ return next_box;
+ }
+
+ if (!leaf_boxes.size())
+ break;
+ start_box = leaf_boxes.FirstBox();
+ }
+ return nullptr;
+}
+
+TextBreakIterator* WordBreakIteratorForMinOffsetBoundary(
+ const VisiblePosition& visible_position,
+ const InlineTextBox* text_box,
+ int& previous_box_length,
+ bool& previous_box_in_different_block,
+ Vector<UChar, 1024>& string,
+ CachedLogicallyOrderedLeafBoxes& leaf_boxes) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ previous_box_in_different_block = false;
+
+ // TODO(editing-dev) Handle the case when we don't have an inline text box.
+ const InlineTextBox* previous_box = LogicallyPreviousBox(
+ visible_position, text_box, previous_box_in_different_block, leaf_boxes);
+
+ int len = 0;
+ string.clear();
+ if (previous_box) {
+ previous_box_length = previous_box->Len();
+ previous_box->GetLineLayoutItem().GetText().AppendTo(
+ string, previous_box->Start(), previous_box_length);
+ len += previous_box_length;
+ }
+ text_box->GetLineLayoutItem().GetText().AppendTo(string, text_box->Start(),
+ text_box->Len());
+ len += text_box->Len();
+
+ return WordBreakIterator(string.data(), len);
+}
+
+TextBreakIterator* WordBreakIteratorForMaxOffsetBoundary(
+ const VisiblePosition& visible_position,
+ const InlineTextBox* text_box,
+ bool& next_box_in_different_block,
+ Vector<UChar, 1024>& string,
+ CachedLogicallyOrderedLeafBoxes& leaf_boxes) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ next_box_in_different_block = false;
+
+ // TODO(editing-dev) Handle the case when we don't have an inline text box.
+ const InlineTextBox* next_box = LogicallyNextBox(
+ visible_position, text_box, next_box_in_different_block, leaf_boxes);
+
+ int len = 0;
+ string.clear();
+ text_box->GetLineLayoutItem().GetText().AppendTo(string, text_box->Start(),
+ text_box->Len());
+ len += text_box->Len();
+ if (next_box) {
+ next_box->GetLineLayoutItem().GetText().AppendTo(string, next_box->Start(),
+ next_box->Len());
+ len += next_box->Len();
+ }
+
+ return WordBreakIterator(string.data(), len);
+}
+
+bool IsLogicalStartOfWord(TextBreakIterator* iter,
+ int position,
+ bool hard_line_break) {
+ bool boundary = hard_line_break ? true : iter->isBoundary(position);
+ if (!boundary)
+ return false;
+
+ // isWordTextBreak returns true after moving across a word and false after
+ // moving across a punctuation/space.
+ // If |iter| is already at the end before |iter->following| is called,
+ // IsWordTextBreak behaves differently depending on the ICU version. We have
+ // to check if |iter| is at the end, first.
+ // See https://ssl.icu-project.org/trac/ticket/13447 .
+ if (iter->following(position) == TextBreakIterator::DONE)
+ return false;
+ return IsWordTextBreak(iter);
+}
+
+bool IslogicalEndOfWord(TextBreakIterator* iter,
+ int position,
+ bool hard_line_break) {
+ bool boundary = iter->isBoundary(position);
+ return (hard_line_break || boundary) && IsWordTextBreak(iter);
+}
+
+enum CursorMovementDirection { kMoveLeft, kMoveRight };
+
+VisiblePosition VisualWordPosition(const VisiblePosition& visible_position,
+ CursorMovementDirection direction,
+ bool skips_space_when_moving_right) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ if (visible_position.IsNull())
+ return VisiblePosition();
+
+ TextDirection block_direction =
+ DirectionOfEnclosingBlockOf(visible_position.DeepEquivalent());
+ const InlineBox* previously_visited_box = nullptr;
+ VisiblePosition current = visible_position;
+ TextBreakIterator* iter = nullptr;
+
+ CachedLogicallyOrderedLeafBoxes leaf_boxes;
+ Vector<UChar, 1024> string;
+
+ for (;;) {
+ VisiblePosition adjacent_character_position = direction == kMoveRight
+ ? RightPositionOf(current)
+ : LeftPositionOf(current);
+ if (adjacent_character_position.DeepEquivalent() ==
+ current.DeepEquivalent() ||
+ adjacent_character_position.IsNull())
+ return VisiblePosition();
+
+ InlineBoxPosition box_position = ComputeInlineBoxPosition(
+ PositionWithAffinity(adjacent_character_position.DeepEquivalent(),
+ TextAffinity::kUpstream));
+ const InlineBox* box = box_position.inline_box;
+ int offset_in_box = box_position.offset_in_box;
+
+ if (!box)
+ break;
+ if (!box->IsInlineTextBox()) {
+ current = adjacent_character_position;
+ continue;
+ }
+
+ const InlineTextBox* text_box = ToInlineTextBox(box);
+ int previous_box_length = 0;
+ bool previous_box_in_different_block = false;
+ bool next_box_in_different_block = false;
+ bool moving_into_new_box = previously_visited_box != box;
+
+ if (offset_in_box == box->CaretMinOffset()) {
+ iter = WordBreakIteratorForMinOffsetBoundary(
+ visible_position, text_box, previous_box_length,
+ previous_box_in_different_block, string, leaf_boxes);
+ } else if (offset_in_box == box->CaretMaxOffset()) {
+ iter = WordBreakIteratorForMaxOffsetBoundary(visible_position, text_box,
+ next_box_in_different_block,
+ string, leaf_boxes);
+ } else if (moving_into_new_box) {
+ iter = WordBreakIterator(text_box->GetLineLayoutItem().GetText(),
+ text_box->Start(), text_box->Len());
+ previously_visited_box = box;
+ }
+
+ if (!iter)
+ break;
+
+ iter->first();
+ int offset_in_iterator =
+ offset_in_box - text_box->Start() + previous_box_length;
+
+ bool is_word_break;
+ bool box_has_same_directionality_as_block =
+ box->Direction() == block_direction;
+ bool moving_backward =
+ (direction == kMoveLeft && box->Direction() == TextDirection::kLtr) ||
+ (direction == kMoveRight && box->Direction() == TextDirection::kRtl);
+ if ((skips_space_when_moving_right &&
+ box_has_same_directionality_as_block) ||
+ (!skips_space_when_moving_right && moving_backward)) {
+ bool logical_start_in_layout_object =
+ offset_in_box == static_cast<int>(text_box->Start()) &&
+ previous_box_in_different_block;
+ is_word_break = IsLogicalStartOfWord(iter, offset_in_iterator,
+ logical_start_in_layout_object);
+ } else {
+ bool logical_end_in_layout_object =
+ offset_in_box ==
+ static_cast<int>(text_box->Start() + text_box->Len()) &&
+ next_box_in_different_block;
+ is_word_break = IslogicalEndOfWord(iter, offset_in_iterator,
+ logical_end_in_layout_object);
+ }
+
+ if (is_word_break) {
+ return AdjustBackwardPositionToAvoidCrossingEditingBoundaries(
+ adjacent_character_position, visible_position.DeepEquivalent());
+ }
+
+ current = adjacent_character_position;
+ }
+ return VisiblePosition();
+}
+
+} // namespace
+
+VisiblePosition SelectionModifier::LeftWordPosition(
+ const VisiblePosition& visible_position,
+ bool skips_space_when_moving_right) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ const VisiblePosition& left_word_break = VisualWordPosition(
+ visible_position, kMoveLeft, skips_space_when_moving_right);
+ if (left_word_break.IsNotNull())
+ return left_word_break;
+ // TODO(editing-dev) How should we handle a non-editable position?
+ if (!IsEditablePosition(visible_position.DeepEquivalent()))
+ return left_word_break;
+ const TextDirection block_direction =
+ DirectionOfEnclosingBlockOf(visible_position.DeepEquivalent());
+ return block_direction == TextDirection::kLtr
+ ? StartOfEditableContent(visible_position)
+ : EndOfEditableContent(visible_position);
+}
+
+VisiblePosition SelectionModifier::RightWordPosition(
+ const VisiblePosition& visible_position,
+ bool skips_space_when_moving_right) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ const VisiblePosition& right_word_break = VisualWordPosition(
+ visible_position, kMoveRight, skips_space_when_moving_right);
+ if (right_word_break.IsNotNull())
+ return right_word_break;
+ // TODO(editing-dev) How should we handle a non-editable position?
+ if (!IsEditablePosition(visible_position.DeepEquivalent()))
+ return right_word_break;
+ const TextDirection block_direction =
+ blink::DirectionOfEnclosingBlockOf(visible_position.DeepEquivalent());
+ return block_direction == TextDirection::kLtr
+ ? EndOfEditableContent(visible_position)
+ : StartOfEditableContent(visible_position);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_strategy.h b/chromium/third_party/blink/renderer/core/editing/selection_strategy.h
new file mode 100644
index 00000000000..5d4371c025d
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_strategy.h
@@ -0,0 +1,20 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_STRATEGY_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_STRATEGY_H_
+
+namespace blink {
+
+enum class SelectionStrategy {
+ // Always using CharacterGranularity
+ kCharacter,
+ // Switches between WordGranularity and CharacterGranularity
+ // Depending on whether the selection or growing or shrinking
+ kDirection,
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_STRATEGY_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_template.cc b/chromium/third_party/blink/renderer/core/editing/selection_template.cc
new file mode 100644
index 00000000000..b0194fef85b
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_template.cc
@@ -0,0 +1,438 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+
+#include <ostream> // NOLINT
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+
+namespace blink {
+
+template <typename Strategy>
+SelectionTemplate<Strategy>::SelectionTemplate(const SelectionTemplate& other)
+ : base_(other.base_),
+ extent_(other.extent_),
+ affinity_(other.affinity_),
+ direction_(other.direction_)
+#if DCHECK_IS_ON()
+ ,
+ dom_tree_version_(other.dom_tree_version_)
+#endif
+{
+ DCHECK(other.AssertValid());
+}
+
+template <typename Strategy>
+SelectionTemplate<Strategy>::SelectionTemplate() = default;
+
+template <typename Strategy>
+bool SelectionTemplate<Strategy>::operator==(
+ const SelectionTemplate& other) const {
+ DCHECK(AssertValid());
+ DCHECK(other.AssertValid());
+ if (IsNone())
+ return other.IsNone();
+ if (other.IsNone())
+ return false;
+ DCHECK_EQ(base_.GetDocument(), other.GetDocument()) << *this << ' ' << other;
+ return base_ == other.base_ && extent_ == other.extent_ &&
+ affinity_ == other.affinity_;
+}
+
+template <typename Strategy>
+bool SelectionTemplate<Strategy>::operator!=(
+ const SelectionTemplate& other) const {
+ return !operator==(other);
+}
+
+template <typename Strategy>
+void SelectionTemplate<Strategy>::Trace(blink::Visitor* visitor) {
+ visitor->Trace(base_);
+ visitor->Trace(extent_);
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> SelectionTemplate<Strategy>::Base() const {
+ DCHECK(AssertValid());
+ DCHECK(!base_.IsOrphan()) << base_;
+ return base_;
+}
+
+template <typename Strategy>
+Document* SelectionTemplate<Strategy>::GetDocument() const {
+ DCHECK(AssertValid());
+ return base_.GetDocument();
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> SelectionTemplate<Strategy>::Extent() const {
+ DCHECK(AssertValid());
+ DCHECK(!extent_.IsOrphan()) << extent_;
+ return extent_;
+}
+
+template <typename Strategy>
+bool SelectionTemplate<Strategy>::IsCaret() const {
+ return base_.IsNotNull() && base_ == extent_;
+}
+
+template <typename Strategy>
+bool SelectionTemplate<Strategy>::IsRange() const {
+ return base_ != extent_;
+}
+
+template <typename Strategy>
+bool SelectionTemplate<Strategy>::IsValidFor(const Document& document) const {
+ if (IsNone())
+ return true;
+ return base_.IsValidFor(document) && extent_.IsValidFor(document);
+}
+
+template <typename Strategy>
+bool SelectionTemplate<Strategy>::AssertValidFor(
+ const Document& document) const {
+ if (!AssertValid())
+ return false;
+ if (base_.IsNull())
+ return true;
+ DCHECK_EQ(base_.GetDocument(), document) << *this;
+ return true;
+}
+
+#if DCHECK_IS_ON()
+template <typename Strategy>
+bool SelectionTemplate<Strategy>::AssertValid() const {
+ if (base_.IsNull())
+ return true;
+ DCHECK_EQ(base_.GetDocument()->DomTreeVersion(), dom_tree_version_) << *this;
+ DCHECK(!base_.IsOrphan()) << *this;
+ DCHECK(!extent_.IsOrphan()) << *this;
+ DCHECK_EQ(base_.GetDocument(), extent_.GetDocument());
+ return true;
+}
+#else
+template <typename Strategy>
+bool SelectionTemplate<Strategy>::AssertValid() const {
+ return true;
+}
+#endif
+
+#ifndef NDEBUG
+template <typename Strategy>
+void SelectionTemplate<Strategy>::ShowTreeForThis() const {
+ if (base_.IsNull()) {
+ LOG(INFO) << "\nbase is null";
+ return;
+ }
+
+ LOG(INFO) << "\n"
+ << base_.AnchorNode()
+ ->ToMarkedTreeString(base_.AnchorNode(), "B",
+ extent_.AnchorNode(), "E")
+ .Utf8()
+ .data()
+ << "base: " << base_.ToAnchorTypeAndOffsetString().Utf8().data()
+ << "\n"
+ << "extent: "
+ << extent_.ToAnchorTypeAndOffsetString().Utf8().data();
+}
+#endif
+
+template <typename Strategy>
+PositionTemplate<Strategy> SelectionTemplate<Strategy>::ComputeEndPosition()
+ const {
+ return IsBaseFirst() ? extent_ : base_;
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> SelectionTemplate<Strategy>::ComputeStartPosition()
+ const {
+ return IsBaseFirst() ? base_ : extent_;
+}
+
+template <typename Strategy>
+EphemeralRangeTemplate<Strategy> SelectionTemplate<Strategy>::ComputeRange()
+ const {
+ return EphemeralRangeTemplate<Strategy>(ComputeStartPosition(),
+ ComputeEndPosition());
+}
+
+template <typename Strategy>
+bool SelectionTemplate<Strategy>::IsBaseFirst() const {
+ DCHECK(AssertValid());
+ if (base_ == extent_) {
+ DCHECK_EQ(direction_, Direction::kForward);
+ return true;
+ }
+ if (direction_ == Direction::kForward) {
+ DCHECK_LE(base_, extent_);
+ return true;
+ }
+ if (direction_ == Direction::kBackward) {
+ DCHECK_GT(base_, extent_);
+ return false;
+ }
+ // Note: Since same position can be represented in different anchor type,
+ // e.g. Position(div, 0) and BeforeNode(first-child), we use |<=| to check
+ // forward selection.
+ DCHECK_EQ(direction_, Direction::kNotComputed);
+ direction_ = base_ <= extent_ ? Direction::kForward : Direction::kBackward;
+ return direction_ == Direction::kForward;
+}
+
+template <typename Strategy>
+void SelectionTemplate<Strategy>::ResetDirectionCache() const {
+ direction_ = base_ == extent_ ? Direction::kForward : Direction::kNotComputed;
+}
+
+template <typename Strategy>
+SelectionType SelectionTemplate<Strategy>::Type() const {
+ if (base_.IsNull())
+ return kNoSelection;
+ if (base_ == extent_)
+ return kCaretSelection;
+ return kRangeSelection;
+}
+
+template <typename Strategy>
+void SelectionTemplate<Strategy>::PrintTo(std::ostream* ostream,
+ const char* type) const {
+ if (IsNone()) {
+ *ostream << "()";
+ return;
+ }
+ *ostream << type << '(';
+#if DCHECK_IS_ON()
+ if (dom_tree_version_ != base_.GetDocument()->DomTreeVersion()) {
+ *ostream << "Dirty: " << dom_tree_version_;
+ *ostream << " != " << base_.GetDocument()->DomTreeVersion() << ' ';
+ }
+#endif
+ *ostream << "base: " << base_ << ", extent: " << extent_ << ')';
+}
+
+std::ostream& operator<<(std::ostream& ostream,
+ const SelectionInDOMTree& selection) {
+ selection.PrintTo(&ostream, "Selection");
+ return ostream;
+}
+
+std::ostream& operator<<(std::ostream& ostream,
+ const SelectionInFlatTree& selection) {
+ selection.PrintTo(&ostream, "SelectionInFlatTree");
+ return ostream;
+}
+
+// --
+
+template <typename Strategy>
+SelectionTemplate<Strategy>::Builder::Builder(
+ const SelectionTemplate<Strategy>& selection)
+ : selection_(selection) {}
+
+template <typename Strategy>
+SelectionTemplate<Strategy>::Builder::Builder() = default;
+
+template <typename Strategy>
+SelectionTemplate<Strategy> SelectionTemplate<Strategy>::Builder::Build()
+ const {
+ DCHECK(selection_.AssertValid());
+ if (selection_.direction_ == Direction::kBackward) {
+ DCHECK_LE(selection_.extent_, selection_.base_);
+ return selection_;
+ }
+ if (selection_.direction_ == Direction::kForward) {
+ if (selection_.IsNone())
+ return selection_;
+ DCHECK_LE(selection_.base_, selection_.extent_);
+ return selection_;
+ }
+ DCHECK_EQ(selection_.direction_, Direction::kNotComputed);
+ selection_.ResetDirectionCache();
+ return selection_;
+}
+
+template <typename Strategy>
+typename SelectionTemplate<Strategy>::Builder&
+SelectionTemplate<Strategy>::Builder::Collapse(
+ const PositionTemplate<Strategy>& position) {
+ DCHECK(position.IsConnected()) << position;
+ selection_.base_ = position;
+ selection_.extent_ = position;
+#if DCHECK_IS_ON()
+ selection_.dom_tree_version_ = position.GetDocument()->DomTreeVersion();
+#endif
+ return *this;
+}
+
+template <typename Strategy>
+typename SelectionTemplate<Strategy>::Builder&
+SelectionTemplate<Strategy>::Builder::Collapse(
+ const PositionWithAffinityTemplate<Strategy>& position_with_affinity) {
+ Collapse(position_with_affinity.GetPosition());
+ SetAffinity(position_with_affinity.Affinity());
+ return *this;
+}
+
+template <typename Strategy>
+typename SelectionTemplate<Strategy>::Builder&
+SelectionTemplate<Strategy>::Builder::Extend(
+ const PositionTemplate<Strategy>& position) {
+ DCHECK(position.IsConnected()) << position;
+ DCHECK_EQ(selection_.GetDocument(), position.GetDocument());
+ DCHECK(selection_.Base().IsConnected()) << selection_.Base();
+ DCHECK(selection_.AssertValid());
+ selection_.extent_ = position;
+ selection_.direction_ = Direction::kNotComputed;
+ return *this;
+}
+
+template <typename Strategy>
+typename SelectionTemplate<Strategy>::Builder&
+SelectionTemplate<Strategy>::Builder::SelectAllChildren(const Node& node) {
+ DCHECK(node.CanContainRangeEndPoint()) << node;
+ return SetBaseAndExtent(
+ EphemeralRangeTemplate<Strategy>::RangeOfContents(node));
+}
+
+template <typename Strategy>
+typename SelectionTemplate<Strategy>::Builder&
+SelectionTemplate<Strategy>::Builder::SetAffinity(TextAffinity affinity) {
+ selection_.affinity_ = affinity;
+ return *this;
+}
+
+template <typename Strategy>
+typename SelectionTemplate<Strategy>::Builder&
+SelectionTemplate<Strategy>::Builder::SetAsBackwardSelection(
+ const EphemeralRangeTemplate<Strategy>& range) {
+ DCHECK(range.IsNotNull());
+ DCHECK(!range.IsCollapsed());
+ DCHECK(selection_.IsNone()) << selection_;
+ selection_.base_ = range.EndPosition();
+ selection_.extent_ = range.StartPosition();
+ selection_.direction_ = Direction::kBackward;
+ DCHECK_GT(selection_.base_, selection_.extent_);
+#if DCHECK_IS_ON()
+ selection_.dom_tree_version_ = range.GetDocument().DomTreeVersion();
+#endif
+ return *this;
+}
+
+template <typename Strategy>
+typename SelectionTemplate<Strategy>::Builder&
+SelectionTemplate<Strategy>::Builder::SetAsForwardSelection(
+ const EphemeralRangeTemplate<Strategy>& range) {
+ DCHECK(range.IsNotNull());
+ DCHECK(selection_.IsNone()) << selection_;
+ selection_.base_ = range.StartPosition();
+ selection_.extent_ = range.EndPosition();
+ selection_.direction_ = Direction::kForward;
+ DCHECK_LE(selection_.base_, selection_.extent_);
+#if DCHECK_IS_ON()
+ selection_.dom_tree_version_ = range.GetDocument().DomTreeVersion();
+#endif
+ return *this;
+}
+
+template <typename Strategy>
+typename SelectionTemplate<Strategy>::Builder&
+SelectionTemplate<Strategy>::Builder::SetBaseAndExtent(
+ const EphemeralRangeTemplate<Strategy>& range) {
+ if (range.IsNull()) {
+ selection_.base_ = PositionTemplate<Strategy>();
+ selection_.extent_ = PositionTemplate<Strategy>();
+#if DCHECK_IS_ON()
+ selection_.dom_tree_version_ = 0;
+#endif
+ return *this;
+ }
+ return SetAsForwardSelection(range);
+}
+
+template <typename Strategy>
+typename SelectionTemplate<Strategy>::Builder&
+SelectionTemplate<Strategy>::Builder::SetBaseAndExtent(
+ const PositionTemplate<Strategy>& base,
+ const PositionTemplate<Strategy>& extent) {
+ if (base.IsNull()) {
+ DCHECK(extent.IsNull()) << extent;
+ return SetBaseAndExtent(EphemeralRangeTemplate<Strategy>());
+ }
+ DCHECK(extent.IsNotNull());
+ return Collapse(base).Extend(extent);
+}
+
+template <typename Strategy>
+typename SelectionTemplate<Strategy>::Builder&
+SelectionTemplate<Strategy>::Builder::SetBaseAndExtentDeprecated(
+ const PositionTemplate<Strategy>& base,
+ const PositionTemplate<Strategy>& extent) {
+ if (base.IsNotNull() && extent.IsNotNull()) {
+ return SetBaseAndExtent(base, extent);
+ }
+ if (base.IsNotNull())
+ return Collapse(base);
+ if (extent.IsNotNull())
+ return Collapse(extent);
+ return SetBaseAndExtent(EphemeralRangeTemplate<Strategy>());
+}
+
+// ---
+
+template <typename Strategy>
+SelectionTemplate<Strategy>::InvalidSelectionResetter::InvalidSelectionResetter(
+ const SelectionTemplate<Strategy>& selection)
+ : document_(selection.GetDocument()),
+ selection_(const_cast<SelectionTemplate&>(selection)) {
+ DCHECK(selection_.AssertValid());
+}
+
+template <typename Strategy>
+SelectionTemplate<
+ Strategy>::InvalidSelectionResetter::~InvalidSelectionResetter() {
+ if (selection_.IsNone())
+ return;
+ DCHECK(document_);
+ if (!selection_.IsValidFor(*document_)) {
+ selection_ = SelectionTemplate<Strategy>();
+ return;
+ }
+#if DCHECK_IS_ON()
+ selection_.dom_tree_version_ = document_->DomTreeVersion();
+#endif
+ selection_.ResetDirectionCache();
+}
+
+SelectionInDOMTree ConvertToSelectionInDOMTree(
+ const SelectionInFlatTree& selection_in_flat_tree) {
+ return SelectionInDOMTree::Builder()
+ .SetAffinity(selection_in_flat_tree.Affinity())
+ .SetBaseAndExtent(ToPositionInDOMTree(selection_in_flat_tree.Base()),
+ ToPositionInDOMTree(selection_in_flat_tree.Extent()))
+ .Build();
+}
+
+SelectionInFlatTree ConvertToSelectionInFlatTree(
+ const SelectionInDOMTree& selection) {
+ return SelectionInFlatTree::Builder()
+ .SetAffinity(selection.Affinity())
+ .SetBaseAndExtent(ToPositionInFlatTree(selection.Base()),
+ ToPositionInFlatTree(selection.Extent()))
+ .Build();
+}
+
+template <typename Strategy>
+void SelectionTemplate<Strategy>::InvalidSelectionResetter::Trace(
+ blink::Visitor* visitor) {
+ visitor->Trace(document_);
+}
+
+template class CORE_TEMPLATE_EXPORT SelectionTemplate<EditingStrategy>;
+template class CORE_TEMPLATE_EXPORT
+ SelectionTemplate<EditingInFlatTreeStrategy>;
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_template.h b/chromium/third_party/blink/renderer/core/editing/selection_template.h
new file mode 100644
index 00000000000..732728d4b2c
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_template.h
@@ -0,0 +1,171 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_TEMPLATE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_TEMPLATE_H_
+
+#include <iosfwd>
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/selection_type.h"
+#include "third_party/blink/renderer/core/editing/text_affinity.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+
+namespace blink {
+
+// SelectionTemplate is used for representing a selection in DOM tree or Flat
+// tree with template parameter |Strategy|. Instances of |SelectionTemplate|
+// are "virtually" immutable objects, we change |SelectionTemplate| by copying
+// in |SelectionEdtior| and |InvalidSelectionResetter|.
+//
+// To construct |SelectionTemplate| object, please use |Builder| class.
+template <typename Strategy>
+class CORE_EXPORT SelectionTemplate final {
+ DISALLOW_NEW();
+
+ public:
+ // |Builder| is a helper class for constructing |SelectionTemplate| object.
+ class CORE_EXPORT Builder final {
+ DISALLOW_NEW();
+
+ public:
+ explicit Builder(const SelectionTemplate&);
+ Builder();
+
+ SelectionTemplate Build() const;
+
+ // Move selection to |base|. |base| can't be null.
+ Builder& Collapse(const PositionTemplate<Strategy>& base);
+ Builder& Collapse(const PositionWithAffinityTemplate<Strategy>& base);
+
+ // Extend selection to |extent|. It is error if selection is none.
+ // |extent| can be in different tree scope of base, but should be in same
+ // document.
+ Builder& Extend(const PositionTemplate<Strategy>& extent);
+
+ // Select all children in |node|.
+ Builder& SelectAllChildren(const Node& /* node */);
+
+ // Note: Since collapsed selection is a forward selection, we can't use
+ // |SetAsBackwardSelection()| for collapsed range.
+ Builder& SetAsBackwardSelection(const EphemeralRangeTemplate<Strategy>&);
+ Builder& SetAsForwardSelection(const EphemeralRangeTemplate<Strategy>&);
+
+ Builder& SetBaseAndExtent(const EphemeralRangeTemplate<Strategy>&);
+
+ // |extent| can not be null if |base| isn't null.
+ Builder& SetBaseAndExtent(const PositionTemplate<Strategy>& base,
+ const PositionTemplate<Strategy>& extent);
+
+ // |extent| can be non-null while |base| is null, which is undesired.
+ // Note: This function should be used only in "core/editing/commands".
+ // TODO(yosin): Once all we get rid of all call sites, we remove this.
+ Builder& SetBaseAndExtentDeprecated(
+ const PositionTemplate<Strategy>& base,
+ const PositionTemplate<Strategy>& extent);
+
+ Builder& SetAffinity(TextAffinity);
+
+ private:
+ SelectionTemplate selection_;
+
+ DISALLOW_COPY_AND_ASSIGN(Builder);
+ };
+
+ // Resets selection at end of life time of the object when base and extent
+ // are disconnected or moved to another document.
+ class InvalidSelectionResetter final {
+ DISALLOW_NEW();
+
+ public:
+ explicit InvalidSelectionResetter(const SelectionTemplate&);
+ ~InvalidSelectionResetter();
+
+ void Trace(blink::Visitor*);
+
+ private:
+ const Member<const Document> document_;
+ SelectionTemplate& selection_;
+
+ DISALLOW_COPY_AND_ASSIGN(InvalidSelectionResetter);
+ };
+
+ SelectionTemplate(const SelectionTemplate& other);
+ SelectionTemplate();
+
+ SelectionTemplate& operator=(const SelectionTemplate&) = default;
+
+ bool operator==(const SelectionTemplate&) const;
+ bool operator!=(const SelectionTemplate&) const;
+
+ PositionTemplate<Strategy> Base() const;
+ PositionTemplate<Strategy> Extent() const;
+ TextAffinity Affinity() const { return affinity_; }
+ bool IsBaseFirst() const;
+ bool IsCaret() const;
+ bool IsNone() const { return base_.IsNull(); }
+ bool IsRange() const;
+
+ // Returns true if |this| selection holds valid values otherwise it causes
+ // assertion failure.
+ bool AssertValid() const;
+ bool AssertValidFor(const Document&) const;
+
+ PositionTemplate<Strategy> ComputeEndPosition() const;
+ PositionTemplate<Strategy> ComputeStartPosition() const;
+ EphemeralRangeTemplate<Strategy> ComputeRange() const;
+
+ // Returns |SelectionType| for |this| based on |base_| and |extent_|.
+ SelectionType Type() const;
+
+ void Trace(blink::Visitor*);
+
+ void PrintTo(std::ostream*, const char* type) const;
+#ifndef NDEBUG
+ void ShowTreeForThis() const;
+#endif
+
+ private:
+ friend class SelectionEditor;
+
+ enum class Direction {
+ kNotComputed,
+ kForward, // base <= extent
+ kBackward, // base > extent
+ };
+
+ Document* GetDocument() const;
+ bool IsValidFor(const Document&) const;
+ void ResetDirectionCache() const;
+
+ PositionTemplate<Strategy> base_;
+ PositionTemplate<Strategy> extent_;
+ TextAffinity affinity_ = TextAffinity::kDownstream;
+ mutable Direction direction_ = Direction::kForward;
+#if DCHECK_IS_ON()
+ uint64_t dom_tree_version_;
+#endif
+};
+
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ SelectionTemplate<EditingStrategy>;
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ SelectionTemplate<EditingInFlatTreeStrategy>;
+
+using SelectionInDOMTree = SelectionTemplate<EditingStrategy>;
+using SelectionInFlatTree = SelectionTemplate<EditingInFlatTreeStrategy>;
+
+CORE_EXPORT SelectionInDOMTree
+ConvertToSelectionInDOMTree(const SelectionInFlatTree&);
+CORE_EXPORT SelectionInFlatTree
+ConvertToSelectionInFlatTree(const SelectionInDOMTree&);
+
+CORE_EXPORT std::ostream& operator<<(std::ostream&, const SelectionInDOMTree&);
+CORE_EXPORT std::ostream& operator<<(std::ostream&, const SelectionInFlatTree&);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_TEMPLATE_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_template_test.cc b/chromium/third_party/blink/renderer/core/editing/selection_template_test.cc
new file mode 100644
index 00000000000..de00bfeb078
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_template_test.cc
@@ -0,0 +1,122 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class SelectionTest : public EditingTestBase {};
+
+TEST_F(SelectionTest, defaultConstructor) {
+ SelectionInDOMTree selection;
+
+ EXPECT_EQ(TextAffinity::kDownstream, selection.Affinity());
+ EXPECT_TRUE(selection.IsBaseFirst());
+ EXPECT_TRUE(selection.IsNone());
+ EXPECT_EQ(Position(), selection.Base());
+ EXPECT_EQ(Position(), selection.Extent());
+ EXPECT_EQ(EphemeralRange(), selection.ComputeRange());
+}
+
+TEST_F(SelectionTest, IsBaseFirst) {
+ SetBodyContent("<div id='sample'>abcdef</div>");
+
+ Element* sample = GetDocument().getElementById("sample");
+ Position base(Position(sample->firstChild(), 4));
+ Position extent(Position(sample->firstChild(), 2));
+ SelectionInDOMTree::Builder builder;
+ builder.Collapse(base);
+ builder.Extend(extent);
+ const SelectionInDOMTree& selection = builder.Build();
+
+ EXPECT_EQ(TextAffinity::kDownstream, selection.Affinity());
+ EXPECT_FALSE(selection.IsBaseFirst());
+ EXPECT_FALSE(selection.IsNone());
+ EXPECT_EQ(base, selection.Base());
+ EXPECT_EQ(extent, selection.Extent());
+}
+
+TEST_F(SelectionTest, caret) {
+ SetBodyContent("<div id='sample'>abcdef</div>");
+
+ Element* sample = GetDocument().getElementById("sample");
+ Position position(Position(sample->firstChild(), 2));
+ SelectionInDOMTree::Builder builder;
+ builder.Collapse(position);
+ const SelectionInDOMTree& selection = builder.Build();
+
+ EXPECT_EQ(TextAffinity::kDownstream, selection.Affinity());
+ EXPECT_TRUE(selection.IsBaseFirst());
+ EXPECT_FALSE(selection.IsNone());
+ EXPECT_EQ(position, selection.Base());
+ EXPECT_EQ(position, selection.Extent());
+}
+
+TEST_F(SelectionTest, range) {
+ SetBodyContent("<div id='sample'>abcdef</div>");
+
+ Element* sample = GetDocument().getElementById("sample");
+ Position base(Position(sample->firstChild(), 2));
+ Position extent(Position(sample->firstChild(), 4));
+ SelectionInDOMTree::Builder builder;
+ builder.Collapse(base);
+ builder.Extend(extent);
+ const SelectionInDOMTree& selection = builder.Build();
+
+ EXPECT_EQ(TextAffinity::kDownstream, selection.Affinity());
+ EXPECT_TRUE(selection.IsBaseFirst());
+ EXPECT_FALSE(selection.IsNone());
+ EXPECT_EQ(base, selection.Base());
+ EXPECT_EQ(extent, selection.Extent());
+}
+
+TEST_F(SelectionTest, SetAsBacwardAndForward) {
+ SetBodyContent("<div id='sample'>abcdef</div>");
+
+ Element* sample = GetDocument().getElementById("sample");
+ Position start(Position(sample->firstChild(), 2));
+ Position end(Position(sample->firstChild(), 4));
+ EphemeralRange range(start, end);
+ const SelectionInDOMTree& backward_selection =
+ SelectionInDOMTree::Builder().SetAsBackwardSelection(range).Build();
+ const SelectionInDOMTree& forward_selection =
+ SelectionInDOMTree::Builder().SetAsForwardSelection(range).Build();
+ const SelectionInDOMTree& collapsed_selection =
+ SelectionInDOMTree::Builder()
+ .SetAsForwardSelection(EphemeralRange(start))
+ .Build();
+
+ EXPECT_EQ(TextAffinity::kDownstream, backward_selection.Affinity());
+ EXPECT_FALSE(backward_selection.IsBaseFirst());
+ EXPECT_FALSE(backward_selection.IsNone());
+ EXPECT_EQ(end, backward_selection.Base());
+ EXPECT_EQ(start, backward_selection.Extent());
+ EXPECT_EQ(start, backward_selection.ComputeStartPosition());
+ EXPECT_EQ(end, backward_selection.ComputeEndPosition());
+ EXPECT_EQ(range, backward_selection.ComputeRange());
+
+ EXPECT_EQ(TextAffinity::kDownstream, forward_selection.Affinity());
+ EXPECT_TRUE(forward_selection.IsBaseFirst());
+ EXPECT_FALSE(forward_selection.IsNone());
+ EXPECT_EQ(start, forward_selection.Base());
+ EXPECT_EQ(end, forward_selection.Extent());
+ EXPECT_EQ(start, forward_selection.ComputeStartPosition());
+ EXPECT_EQ(end, forward_selection.ComputeEndPosition());
+ EXPECT_EQ(range, forward_selection.ComputeRange());
+
+ EXPECT_EQ(TextAffinity::kDownstream, collapsed_selection.Affinity());
+ EXPECT_TRUE(collapsed_selection.IsBaseFirst());
+ EXPECT_FALSE(collapsed_selection.IsNone());
+ EXPECT_EQ(start, collapsed_selection.Base());
+ EXPECT_EQ(start, collapsed_selection.Extent());
+ EXPECT_EQ(start, collapsed_selection.ComputeStartPosition());
+ EXPECT_EQ(start, collapsed_selection.ComputeEndPosition());
+ EXPECT_EQ(EphemeralRange(start, start), collapsed_selection.ComputeRange());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/selection_type.h b/chromium/third_party/blink/renderer/core/editing/selection_type.h
new file mode 100644
index 00000000000..c2d2f2ace05
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/selection_type.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2004 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_TYPE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_TYPE_H_
+
+namespace blink {
+
+enum SelectionType { kNoSelection, kCaretSelection, kRangeSelection };
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SELECTION_TYPE_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/serializers/html_interchange.cc b/chromium/third_party/blink/renderer/core/editing/serializers/html_interchange.cc
new file mode 100644
index 00000000000..8910a081d57
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/serializers/html_interchange.cc
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2004, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/serializers/html_interchange.h"
+
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/core/layout/layout_text.h"
+#include "third_party/blink/renderer/platform/wtf/text/character_names.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
+
+namespace blink {
+
+String ConvertHTMLTextToInterchangeFormat(const String& in, const Text& node) {
+ // Assume all the text comes from node.
+ if (node.GetLayoutObject() &&
+ node.GetLayoutObject()->Style()->PreserveNewline())
+ return in;
+
+ const char kConvertedSpaceString[] = "<span>\xA0</span>";
+ static_assert((static_cast<unsigned char>('\xA0') == kNoBreakSpaceCharacter),
+ "\\xA0 should be non-breaking space");
+
+ StringBuilder s;
+
+ unsigned i = 0;
+ unsigned consumed = 0;
+ while (i < in.length()) {
+ consumed = 1;
+ if (IsCollapsibleWhitespace(in[i])) {
+ // count number of adjoining spaces
+ unsigned j = i + 1;
+ while (j < in.length() && IsCollapsibleWhitespace(in[j]))
+ j++;
+ unsigned count = j - i;
+ consumed = count;
+ while (count) {
+ unsigned add = count % 3;
+ switch (add) {
+ case 0:
+ s.Append(kConvertedSpaceString);
+ s.Append(' ');
+ s.Append(kConvertedSpaceString);
+ add = 3;
+ break;
+ case 1:
+ if (i == 0 || i + 1 == in.length()) // at start or end of string
+ s.Append(kConvertedSpaceString);
+ else
+ s.Append(' ');
+ break;
+ case 2:
+ if (i == 0) {
+ // at start of string
+ s.Append(kConvertedSpaceString);
+ s.Append(' ');
+ } else if (i + 2 == in.length()) {
+ // at end of string
+ s.Append(kConvertedSpaceString);
+ s.Append(kConvertedSpaceString);
+ } else {
+ s.Append(kConvertedSpaceString);
+ s.Append(' ');
+ }
+ break;
+ }
+ count -= add;
+ }
+ } else {
+ s.Append(in[i]);
+ }
+ i += consumed;
+ }
+
+ return s.ToString();
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/serializers/html_interchange.h b/chromium/third_party/blink/renderer/core/editing/serializers/html_interchange.h
new file mode 100644
index 00000000000..f2065d0fa70
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/serializers/html_interchange.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2004, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_HTML_INTERCHANGE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_HTML_INTERCHANGE_H_
+
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+
+namespace blink {
+
+class Text;
+
+#define AppleInterchangeNewline "Apple-interchange-newline"
+
+enum EAnnotateForInterchange {
+ kDoNotAnnotateForInterchange,
+ kAnnotateForInterchange
+};
+
+String ConvertHTMLTextToInterchangeFormat(const String&, const Text&);
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/serializers/markup_accumulator.cc b/chromium/third_party/blink/renderer/core/editing/serializers/markup_accumulator.cc
new file mode 100644
index 00000000000..f9b7c87e6a1
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/serializers/markup_accumulator.cc
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2012 Apple Inc. All rights
+ * reserved.
+ * Copyright (C) 2009, 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/serializers/markup_accumulator.h"
+
+#include "third_party/blink/renderer/core/dom/attr.h"
+#include "third_party/blink/renderer/core/dom/cdata_section.h"
+#include "third_party/blink/renderer/core/dom/comment.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/document_fragment.h"
+#include "third_party/blink/renderer/core/dom/document_type.h"
+#include "third_party/blink/renderer/core/dom/processing_instruction.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/html/html_template_element.h"
+#include "third_party/blink/renderer/core/xlink_names.h"
+#include "third_party/blink/renderer/core/xml_names.h"
+#include "third_party/blink/renderer/core/xmlns_names.h"
+#include "third_party/blink/renderer/platform/weborigin/kurl.h"
+#include "third_party/blink/renderer/platform/wtf/text/character_names.h"
+
+namespace blink {
+
+MarkupAccumulator::MarkupAccumulator(EAbsoluteURLs resolve_urls_method,
+ SerializationType serialization_type)
+ : formatter_(resolve_urls_method, serialization_type) {}
+
+MarkupAccumulator::~MarkupAccumulator() = default;
+
+void MarkupAccumulator::AppendString(const String& string) {
+ markup_.Append(string);
+}
+
+void MarkupAccumulator::AppendStartTag(Node& node, Namespaces* namespaces) {
+ AppendStartMarkup(markup_, node, namespaces);
+}
+
+void MarkupAccumulator::AppendEndTag(const Element& element) {
+ AppendEndMarkup(markup_, element);
+}
+
+void MarkupAccumulator::AppendStartMarkup(StringBuilder& result,
+ Node& node,
+ Namespaces* namespaces) {
+ switch (node.getNodeType()) {
+ case Node::kTextNode:
+ AppendText(result, ToText(node));
+ break;
+ case Node::kElementNode:
+ AppendElement(result, ToElement(node), namespaces);
+ break;
+ case Node::kAttributeNode:
+ // Only XMLSerializer can pass an Attr. So, |documentIsHTML| flag is
+ // false.
+ formatter_.AppendAttributeValue(result, ToAttr(node).value(), false);
+ break;
+ default:
+ formatter_.AppendStartMarkup(result, node, namespaces);
+ break;
+ }
+}
+
+void MarkupAccumulator::AppendEndMarkup(StringBuilder& result,
+ const Element& element) {
+ formatter_.AppendEndMarkup(result, element);
+}
+
+void MarkupAccumulator::AppendCustomAttributes(StringBuilder&,
+ const Element&,
+ Namespaces*) {}
+
+void MarkupAccumulator::AppendText(StringBuilder& result, Text& text) {
+ formatter_.AppendText(result, text);
+}
+
+bool MarkupAccumulator::ShouldIgnoreAttribute(
+ const Element& element,
+ const Attribute& attribute) const {
+ return false;
+}
+
+bool MarkupAccumulator::ShouldIgnoreElement(const Element& element) const {
+ return false;
+}
+
+void MarkupAccumulator::AppendElement(StringBuilder& result,
+ const Element& element,
+ Namespaces* namespaces) {
+ // https://html.spec.whatwg.org/multipage/parsing.html#html-fragment-serialisation-algorithm
+ AppendOpenTag(result, element, namespaces);
+
+ AttributeCollection attributes = element.Attributes();
+ if (SerializeAsHTMLDocument(element)) {
+ // 3.2. Element: If current node's is value is not null, and the
+ // element does not have an is attribute in its attribute list, ...
+ const AtomicString& is_value = element.IsValue();
+ if (!is_value.IsNull() && !attributes.Find(HTMLNames::isAttr)) {
+ AppendAttribute(result, element, Attribute(HTMLNames::isAttr, is_value),
+ namespaces);
+ }
+ }
+ for (const auto& attribute : attributes) {
+ if (!ShouldIgnoreAttribute(element, attribute))
+ AppendAttribute(result, element, attribute, namespaces);
+ }
+
+ // Give an opportunity to subclasses to add their own attributes.
+ AppendCustomAttributes(result, element, namespaces);
+
+ AppendCloseTag(result, element);
+}
+
+void MarkupAccumulator::AppendOpenTag(StringBuilder& result,
+ const Element& element,
+ Namespaces* namespaces) {
+ formatter_.AppendOpenTag(result, element, namespaces);
+}
+
+void MarkupAccumulator::AppendCloseTag(StringBuilder& result,
+ const Element& element) {
+ formatter_.AppendCloseTag(result, element);
+}
+
+void MarkupAccumulator::AppendAttribute(StringBuilder& result,
+ const Element& element,
+ const Attribute& attribute,
+ Namespaces* namespaces) {
+ formatter_.AppendAttribute(result, element, attribute, namespaces);
+}
+
+EntityMask MarkupAccumulator::EntityMaskForText(const Text& text) const {
+ return formatter_.EntityMaskForText(text);
+}
+
+bool MarkupAccumulator::SerializeAsHTMLDocument(const Node& node) const {
+ return formatter_.SerializeAsHTMLDocument(node);
+}
+
+std::pair<Node*, Element*> MarkupAccumulator::GetAuxiliaryDOMTree(
+ const Element& element) const {
+ return std::pair<Node*, Element*>();
+}
+
+template <typename Strategy>
+static void SerializeNodesWithNamespaces(MarkupAccumulator& accumulator,
+ Node& target_node,
+ EChildrenOnly children_only,
+ const Namespaces* namespaces) {
+ if (target_node.IsElementNode() &&
+ accumulator.ShouldIgnoreElement(ToElement(target_node))) {
+ return;
+ }
+
+ Namespaces namespace_hash;
+ if (namespaces)
+ namespace_hash = *namespaces;
+
+ if (!children_only)
+ accumulator.AppendStartTag(target_node, &namespace_hash);
+
+ if (!(accumulator.SerializeAsHTMLDocument(target_node) &&
+ ElementCannotHaveEndTag(target_node))) {
+ Node* current = IsHTMLTemplateElement(target_node)
+ ? Strategy::FirstChild(
+ *ToHTMLTemplateElement(target_node).content())
+ : Strategy::FirstChild(target_node);
+ for (; current; current = Strategy::NextSibling(*current))
+ SerializeNodesWithNamespaces<Strategy>(accumulator, *current,
+ kIncludeNode, &namespace_hash);
+
+ // Traverses other DOM tree, i.e., shadow tree.
+ if (target_node.IsElementNode()) {
+ std::pair<Node*, Element*> auxiliary_pair =
+ accumulator.GetAuxiliaryDOMTree(ToElement(target_node));
+ Node* auxiliary_tree = auxiliary_pair.first;
+ Element* enclosing_element = auxiliary_pair.second;
+ if (auxiliary_tree) {
+ if (auxiliary_pair.second)
+ accumulator.AppendStartTag(*enclosing_element);
+ current = Strategy::FirstChild(*auxiliary_tree);
+ for (; current; current = Strategy::NextSibling(*current)) {
+ SerializeNodesWithNamespaces<Strategy>(accumulator, *current,
+ kIncludeNode, &namespace_hash);
+ }
+ if (enclosing_element)
+ accumulator.AppendEndTag(*enclosing_element);
+ }
+ }
+ }
+
+ if ((!children_only && target_node.IsElementNode()) &&
+ !(accumulator.SerializeAsHTMLDocument(target_node) &&
+ ElementCannotHaveEndTag(target_node)))
+ accumulator.AppendEndTag(ToElement(target_node));
+}
+
+template <typename Strategy>
+String SerializeNodes(MarkupAccumulator& accumulator,
+ Node& target_node,
+ EChildrenOnly children_only) {
+ Namespaces* namespaces = nullptr;
+ Namespaces namespace_hash;
+ if (!accumulator.SerializeAsHTMLDocument(target_node)) {
+ // Add pre-bound namespaces for XML fragments.
+ namespace_hash.Set(g_xml_atom, XMLNames::xmlNamespaceURI);
+ namespaces = &namespace_hash;
+ }
+
+ SerializeNodesWithNamespaces<Strategy>(accumulator, target_node,
+ children_only, namespaces);
+ return accumulator.ToString();
+}
+
+template String SerializeNodes<EditingStrategy>(MarkupAccumulator&,
+ Node&,
+ EChildrenOnly);
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/serializers/markup_accumulator.h b/chromium/third_party/blink/renderer/core/editing/serializers/markup_accumulator.h
new file mode 100644
index 00000000000..54ed11c0ed6
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/serializers/markup_accumulator.h
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_MARKUP_ACCUMULATOR_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_MARKUP_ACCUMULATOR_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/editing/editing_strategy.h"
+#include "third_party/blink/renderer/core/editing/serializers/markup_formatter.h"
+#include "third_party/blink/renderer/core/editing/serializers/serialization.h"
+#include "third_party/blink/renderer/platform/wtf/hash_map.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
+
+namespace blink {
+
+class Attribute;
+class Element;
+class Node;
+
+class MarkupAccumulator {
+ STACK_ALLOCATED();
+
+ public:
+ MarkupAccumulator(EAbsoluteURLs,
+ SerializationType = SerializationType::kAsOwnerDocument);
+ virtual ~MarkupAccumulator();
+
+ void AppendString(const String&);
+ virtual void AppendStartTag(Node&, Namespaces* = nullptr);
+ virtual void AppendEndTag(const Element&);
+ void AppendStartMarkup(StringBuilder&, Node&, Namespaces*);
+ void AppendEndMarkup(StringBuilder&, const Element&);
+
+ bool SerializeAsHTMLDocument(const Node&) const;
+ String ToString() { return markup_.ToString(); }
+
+ virtual void AppendCustomAttributes(StringBuilder&,
+ const Element&,
+ Namespaces*);
+
+ virtual void AppendText(StringBuilder&, Text&);
+ virtual bool ShouldIgnoreAttribute(const Element&, const Attribute&) const;
+ virtual bool ShouldIgnoreElement(const Element&) const;
+ virtual void AppendElement(StringBuilder&, const Element&, Namespaces*);
+ void AppendOpenTag(StringBuilder&, const Element&, Namespaces*);
+ void AppendCloseTag(StringBuilder&, const Element&);
+ virtual void AppendAttribute(StringBuilder&,
+ const Element&,
+ const Attribute&,
+ Namespaces*);
+
+ EntityMask EntityMaskForText(const Text&) const;
+
+ // Returns an auxiliary DOM tree, i.e. shadow tree, that needs also to be
+ // serialized. The root of auxiliary DOM tree is returned as an 1st element
+ // in the pair. It can be null if no auxiliary DOM tree exists. An additional
+ // element used to enclose the serialized content of auxiliary DOM tree
+ // can be returned as 2nd element in the pair. It can be null if this is not
+ // needed. For shadow tree, a <template> element is needed to wrap the shadow
+ // tree content.
+ virtual std::pair<Node*, Element*> GetAuxiliaryDOMTree(const Element&) const;
+
+ private:
+ MarkupFormatter formatter_;
+ StringBuilder markup_;
+
+ DISALLOW_COPY_AND_ASSIGN(MarkupAccumulator);
+};
+
+template <typename Strategy>
+String SerializeNodes(MarkupAccumulator&, Node&, EChildrenOnly);
+
+extern template String SerializeNodes<EditingStrategy>(MarkupAccumulator&,
+ Node&,
+ EChildrenOnly);
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/serializers/markup_formatter.cc b/chromium/third_party/blink/renderer/core/editing/serializers/markup_formatter.cc
new file mode 100644
index 00000000000..6bcfe715010
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/serializers/markup_formatter.cc
@@ -0,0 +1,510 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2012 Apple Inc. All rights
+ * reserved.
+ * Copyright (C) 2009, 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/serializers/markup_formatter.h"
+
+#include "third_party/blink/renderer/core/dom/cdata_section.h"
+#include "third_party/blink/renderer/core/dom/comment.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/document_fragment.h"
+#include "third_party/blink/renderer/core/dom/document_type.h"
+#include "third_party/blink/renderer/core/dom/processing_instruction.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/html/html_template_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/xlink_names.h"
+#include "third_party/blink/renderer/core/xml_names.h"
+#include "third_party/blink/renderer/core/xmlns_names.h"
+#include "third_party/blink/renderer/platform/weborigin/kurl.h"
+#include "third_party/blink/renderer/platform/wtf/text/character_names.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+struct EntityDescription {
+ UChar entity;
+ const CString& reference;
+ EntityMask mask;
+};
+
+template <typename CharType>
+static inline void AppendCharactersReplacingEntitiesInternal(
+ StringBuilder& result,
+ CharType* text,
+ unsigned length,
+ const EntityDescription entity_maps[],
+ unsigned entity_maps_count,
+ EntityMask entity_mask) {
+ unsigned position_after_last_entity = 0;
+ for (unsigned i = 0; i < length; ++i) {
+ for (unsigned entity_index = 0; entity_index < entity_maps_count;
+ ++entity_index) {
+ if (text[i] == entity_maps[entity_index].entity &&
+ entity_maps[entity_index].mask & entity_mask) {
+ result.Append(text + position_after_last_entity,
+ i - position_after_last_entity);
+ const CString& replacement = entity_maps[entity_index].reference;
+ result.Append(replacement.data(), replacement.length());
+ position_after_last_entity = i + 1;
+ break;
+ }
+ }
+ }
+ result.Append(text + position_after_last_entity,
+ length - position_after_last_entity);
+}
+
+void MarkupFormatter::AppendCharactersReplacingEntities(
+ StringBuilder& result,
+ const String& source,
+ unsigned offset,
+ unsigned length,
+ EntityMask entity_mask) {
+ DEFINE_STATIC_LOCAL(const CString, amp_reference, ("&amp;"));
+ DEFINE_STATIC_LOCAL(const CString, lt_reference, ("&lt;"));
+ DEFINE_STATIC_LOCAL(const CString, gt_reference, ("&gt;"));
+ DEFINE_STATIC_LOCAL(const CString, quot_reference, ("&quot;"));
+ DEFINE_STATIC_LOCAL(const CString, nbsp_reference, ("&nbsp;"));
+ DEFINE_STATIC_LOCAL(const CString, tab_reference, ("&#9;"));
+ DEFINE_STATIC_LOCAL(const CString, line_feed_reference, ("&#10;"));
+ DEFINE_STATIC_LOCAL(const CString, carriage_return_reference, ("&#13;"));
+
+ static const EntityDescription kEntityMaps[] = {
+ {'&', amp_reference, kEntityAmp},
+ {'<', lt_reference, kEntityLt},
+ {'>', gt_reference, kEntityGt},
+ {'"', quot_reference, kEntityQuot},
+ {kNoBreakSpaceCharacter, nbsp_reference, kEntityNbsp},
+ {'\t', tab_reference, kEntityTab},
+ {'\n', line_feed_reference, kEntityLineFeed},
+ {'\r', carriage_return_reference, kEntityCarriageReturn},
+ };
+
+ if (!(offset + length))
+ return;
+
+ DCHECK_LE(offset + length, source.length());
+ if (source.Is8Bit())
+ AppendCharactersReplacingEntitiesInternal(
+ result, source.Characters8() + offset, length, kEntityMaps,
+ WTF_ARRAY_LENGTH(kEntityMaps), entity_mask);
+ else
+ AppendCharactersReplacingEntitiesInternal(
+ result, source.Characters16() + offset, length, kEntityMaps,
+ WTF_ARRAY_LENGTH(kEntityMaps), entity_mask);
+}
+
+MarkupFormatter::MarkupFormatter(EAbsoluteURLs resolve_urls_method,
+ SerializationType serialization_type)
+ : resolve_urls_method_(resolve_urls_method),
+ serialization_type_(serialization_type) {}
+
+MarkupFormatter::~MarkupFormatter() = default;
+
+String MarkupFormatter::ResolveURLIfNeeded(const Element& element,
+ const String& url_string) const {
+ switch (resolve_urls_method_) {
+ case kResolveAllURLs:
+ return element.GetDocument().CompleteURL(url_string).GetString();
+
+ case kResolveNonLocalURLs:
+ if (!element.GetDocument().Url().IsLocalFile())
+ return element.GetDocument().CompleteURL(url_string).GetString();
+ break;
+
+ case kDoNotResolveURLs:
+ break;
+ }
+ return url_string;
+}
+
+void MarkupFormatter::AppendStartMarkup(StringBuilder& result,
+ const Node& node,
+ Namespaces* namespaces) {
+ switch (node.getNodeType()) {
+ case Node::kTextNode:
+ NOTREACHED();
+ break;
+ case Node::kCommentNode:
+ AppendComment(result, ToComment(node).data());
+ break;
+ case Node::kDocumentNode:
+ AppendXMLDeclaration(result, ToDocument(node));
+ break;
+ case Node::kDocumentFragmentNode:
+ break;
+ case Node::kDocumentTypeNode:
+ AppendDocumentType(result, ToDocumentType(node));
+ break;
+ case Node::kProcessingInstructionNode:
+ AppendProcessingInstruction(result,
+ ToProcessingInstruction(node).target(),
+ ToProcessingInstruction(node).data());
+ break;
+ case Node::kElementNode:
+ NOTREACHED();
+ break;
+ case Node::kCdataSectionNode:
+ AppendCDATASection(result, ToCDATASection(node).data());
+ break;
+ case Node::kAttributeNode:
+ NOTREACHED();
+ break;
+ }
+}
+
+void MarkupFormatter::AppendEndMarkup(StringBuilder& result,
+ const Element& element) {
+ if (ShouldSelfClose(element) ||
+ (!element.HasChildren() && ElementCannotHaveEndTag(element)))
+ return;
+
+ result.Append("</");
+ result.Append(element.TagQName().ToString());
+ result.Append('>');
+}
+
+void MarkupFormatter::AppendAttributeValue(StringBuilder& result,
+ const String& attribute,
+ bool document_is_html) {
+ AppendCharactersReplacingEntities(result, attribute, 0, attribute.length(),
+ document_is_html
+ ? kEntityMaskInHTMLAttributeValue
+ : kEntityMaskInAttributeValue);
+}
+
+void MarkupFormatter::AppendQuotedURLAttributeValue(
+ StringBuilder& result,
+ const Element& element,
+ const Attribute& attribute) {
+ DCHECK(element.IsURLAttribute(attribute)) << element;
+ String resolved_url_string = ResolveURLIfNeeded(element, attribute.Value());
+ UChar quote_char = '"';
+ if (ProtocolIsJavaScript(resolved_url_string)) {
+ // minimal escaping for javascript urls
+ if (resolved_url_string.Contains('&'))
+ resolved_url_string.Replace('&', "&amp;");
+
+ if (resolved_url_string.Contains('"')) {
+ if (resolved_url_string.Contains('\''))
+ resolved_url_string.Replace('"', "&quot;");
+ else
+ quote_char = '\'';
+ }
+ result.Append(quote_char);
+ result.Append(resolved_url_string);
+ result.Append(quote_char);
+ return;
+ }
+
+ // FIXME: This does not fully match other browsers. Firefox percent-escapes
+ // non-ASCII characters for innerHTML.
+ result.Append(quote_char);
+ AppendAttributeValue(result, resolved_url_string, false);
+ result.Append(quote_char);
+}
+
+void MarkupFormatter::AppendNamespace(StringBuilder& result,
+ const AtomicString& prefix,
+ const AtomicString& namespace_uri,
+ Namespaces& namespaces) {
+ const AtomicString& lookup_key = (!prefix) ? g_empty_atom : prefix;
+ AtomicString found_uri = namespaces.at(lookup_key);
+ if (!EqualIgnoringNullity(found_uri, namespace_uri)) {
+ namespaces.Set(lookup_key, namespace_uri);
+ result.Append(' ');
+ result.Append(g_xmlns_atom.GetString());
+ if (!prefix.IsEmpty()) {
+ result.Append(':');
+ result.Append(prefix);
+ }
+
+ result.Append("=\"");
+ AppendAttributeValue(result, namespace_uri, false);
+ result.Append('"');
+ }
+}
+
+void MarkupFormatter::AppendText(StringBuilder& result, Text& text) {
+ const String& str = text.data();
+ AppendCharactersReplacingEntities(result, str, 0, str.length(),
+ EntityMaskForText(text));
+}
+
+void MarkupFormatter::AppendComment(StringBuilder& result,
+ const String& comment) {
+ // FIXME: Comment content is not escaped, but XMLSerializer (and possibly
+ // other callers) should raise an exception if it includes "-->".
+ result.Append("<!--");
+ result.Append(comment);
+ result.Append("-->");
+}
+
+void MarkupFormatter::AppendXMLDeclaration(StringBuilder& result,
+ const Document& document) {
+ if (!document.HasXMLDeclaration())
+ return;
+
+ result.Append("<?xml version=\"");
+ result.Append(document.xmlVersion());
+ const String& encoding = document.xmlEncoding();
+ if (!encoding.IsEmpty()) {
+ result.Append("\" encoding=\"");
+ result.Append(encoding);
+ }
+ if (document.XmlStandaloneStatus() != Document::kStandaloneUnspecified) {
+ result.Append("\" standalone=\"");
+ if (document.xmlStandalone())
+ result.Append("yes");
+ else
+ result.Append("no");
+ }
+
+ result.Append("\"?>");
+}
+
+void MarkupFormatter::AppendDocumentType(StringBuilder& result,
+ const DocumentType& n) {
+ if (n.name().IsEmpty())
+ return;
+
+ result.Append("<!DOCTYPE ");
+ result.Append(n.name());
+ if (!n.publicId().IsEmpty()) {
+ result.Append(" PUBLIC \"");
+ result.Append(n.publicId());
+ result.Append('"');
+ if (!n.systemId().IsEmpty()) {
+ result.Append(" \"");
+ result.Append(n.systemId());
+ result.Append('"');
+ }
+ } else if (!n.systemId().IsEmpty()) {
+ result.Append(" SYSTEM \"");
+ result.Append(n.systemId());
+ result.Append('"');
+ }
+ result.Append('>');
+}
+
+void MarkupFormatter::AppendProcessingInstruction(StringBuilder& result,
+ const String& target,
+ const String& data) {
+ // FIXME: PI data is not escaped, but XMLSerializer (and possibly other
+ // callers) this should raise an exception if it includes "?>".
+ result.Append("<?");
+ result.Append(target);
+ result.Append(' ');
+ result.Append(data);
+ result.Append("?>");
+}
+
+void MarkupFormatter::AppendOpenTag(StringBuilder& result,
+ const Element& element,
+ Namespaces* namespaces) {
+ result.Append('<');
+ result.Append(element.TagQName().ToString());
+ if (!SerializeAsHTMLDocument(element) && namespaces &&
+ ShouldAddNamespaceElement(element, *namespaces))
+ AppendNamespace(result, element.prefix(), element.namespaceURI(),
+ *namespaces);
+}
+
+void MarkupFormatter::AppendCloseTag(StringBuilder& result,
+ const Element& element) {
+ if (ShouldSelfClose(element)) {
+ if (element.IsHTMLElement())
+ result.Append(' '); // XHTML 1.0 <-> HTML compatibility.
+ result.Append('/');
+ }
+ result.Append('>');
+}
+
+void MarkupFormatter::AppendAttribute(StringBuilder& result,
+ const Element& element,
+ const Attribute& attribute,
+ Namespaces* namespaces) {
+ bool document_is_html = SerializeAsHTMLDocument(element);
+
+ QualifiedName prefixed_name = attribute.GetName();
+ if (document_is_html) {
+ if (attribute.NamespaceURI() == XMLNSNames::xmlnsNamespaceURI) {
+ if (!attribute.Prefix() && attribute.LocalName() != g_xmlns_atom)
+ prefixed_name.SetPrefix(g_xmlns_atom);
+ } else if (attribute.NamespaceURI() == XMLNames::xmlNamespaceURI) {
+ prefixed_name.SetPrefix(g_xml_atom);
+ } else if (attribute.NamespaceURI() == XLinkNames::xlinkNamespaceURI) {
+ prefixed_name.SetPrefix(g_xlink_atom);
+ }
+ result.Append(' ');
+ result.Append(prefixed_name.ToString());
+ } else {
+ if (attribute.NamespaceURI() == XMLNSNames::xmlnsNamespaceURI) {
+ if (!attribute.Prefix() && attribute.LocalName() != g_xmlns_atom)
+ prefixed_name.SetPrefix(g_xmlns_atom);
+ // Account for the namespace attribute we're about to append.
+ if (namespaces) {
+ const AtomicString& lookup_key =
+ (!attribute.Prefix()) ? g_empty_atom : attribute.LocalName();
+ namespaces->Set(lookup_key, attribute.Value());
+ }
+ } else if (attribute.NamespaceURI() == XMLNames::xmlNamespaceURI) {
+ if (!attribute.Prefix())
+ prefixed_name.SetPrefix(g_xml_atom);
+ } else {
+ if (attribute.NamespaceURI() == XLinkNames::xlinkNamespaceURI) {
+ if (!attribute.Prefix())
+ prefixed_name.SetPrefix(g_xlink_atom);
+ }
+
+ if (namespaces && ShouldAddNamespaceAttribute(attribute, element)) {
+ if (!prefixed_name.Prefix()) {
+ // This behavior is in process of being standardized. See
+ // crbug.com/248044 and
+ // https://www.w3.org/Bugs/Public/show_bug.cgi?id=24208
+ String prefix_prefix("ns", 2);
+ for (unsigned i = attribute.NamespaceURI().Impl()->ExistingHash();;
+ ++i) {
+ AtomicString new_prefix(String(prefix_prefix + String::Number(i)));
+ AtomicString found_uri = namespaces->at(new_prefix);
+ if (found_uri == attribute.NamespaceURI() ||
+ found_uri == g_null_atom) {
+ // We already generated a prefix for this namespace.
+ prefixed_name.SetPrefix(new_prefix);
+ break;
+ }
+ }
+ }
+ DCHECK(prefixed_name.Prefix());
+ AppendNamespace(result, prefixed_name.Prefix(),
+ attribute.NamespaceURI(), *namespaces);
+ }
+ }
+ result.Append(' ');
+ result.Append(prefixed_name.ToString());
+ }
+
+ result.Append('=');
+
+ if (element.IsURLAttribute(attribute)) {
+ AppendQuotedURLAttributeValue(result, element, attribute);
+ } else {
+ result.Append('"');
+ AppendAttributeValue(result, attribute.Value(), document_is_html);
+ result.Append('"');
+ }
+}
+
+void MarkupFormatter::AppendCDATASection(StringBuilder& result,
+ const String& section) {
+ // FIXME: CDATA content is not escaped, but XMLSerializer (and possibly other
+ // callers) should raise an exception if it includes "]]>".
+ result.Append("<![CDATA[");
+ result.Append(section);
+ result.Append("]]>");
+}
+
+bool MarkupFormatter::ShouldAddNamespaceElement(const Element& element,
+ Namespaces& namespaces) const {
+ // Don't add namespace attribute if it is already defined for this elem.
+ const AtomicString& prefix = element.prefix();
+ if (prefix.IsEmpty()) {
+ if (element.hasAttribute(g_xmlns_atom)) {
+ namespaces.Set(g_empty_atom, element.namespaceURI());
+ return false;
+ }
+ return true;
+ }
+
+ return !element.hasAttribute(WTF::g_xmlns_with_colon + prefix);
+}
+
+bool MarkupFormatter::ShouldAddNamespaceAttribute(
+ const Attribute& attribute,
+ const Element& element) const {
+ // xmlns and xmlns:prefix attributes should be handled by another branch in
+ // appendAttribute.
+ DCHECK_NE(attribute.NamespaceURI(), XMLNSNames::xmlnsNamespaceURI);
+
+ // Attributes are in the null namespace by default.
+ if (!attribute.NamespaceURI())
+ return false;
+
+ // Attributes without a prefix will need one generated for them, and an xmlns
+ // attribute for that prefix.
+ if (!attribute.Prefix())
+ return true;
+
+ return !element.hasAttribute(WTF::g_xmlns_with_colon + attribute.Prefix());
+}
+
+EntityMask MarkupFormatter::EntityMaskForText(const Text& text) const {
+ if (!SerializeAsHTMLDocument(text))
+ return kEntityMaskInPCDATA;
+
+ // TODO(hajimehoshi): We need to switch EditingStrategy.
+ const QualifiedName* parent_name = nullptr;
+ if (text.parentElement())
+ parent_name = &(text.parentElement())->TagQName();
+
+ if (parent_name &&
+ (*parent_name == scriptTag || *parent_name == styleTag ||
+ *parent_name == xmpTag || *parent_name == iframeTag ||
+ *parent_name == plaintextTag || *parent_name == noembedTag ||
+ *parent_name == noframesTag ||
+ (*parent_name == noscriptTag && text.GetDocument().GetFrame() &&
+ text.GetDocument().CanExecuteScripts(kNotAboutToExecuteScript))))
+ return kEntityMaskInCDATA;
+ return kEntityMaskInHTMLPCDATA;
+}
+
+// Rules of self-closure
+// 1. No elements in HTML documents use the self-closing syntax.
+// 2. Elements w/ children never self-close because they use a separate end tag.
+// 3. HTML elements which do not listed in spec will close with a
+// separate end tag.
+// 4. Other elements self-close.
+bool MarkupFormatter::ShouldSelfClose(const Element& element) const {
+ if (SerializeAsHTMLDocument(element))
+ return false;
+ if (element.HasChildren())
+ return false;
+ if (element.IsHTMLElement() && !ElementCannotHaveEndTag(element))
+ return false;
+ return true;
+}
+
+bool MarkupFormatter::SerializeAsHTMLDocument(const Node& node) const {
+ if (serialization_type_ == SerializationType::kForcedXML)
+ return false;
+ return node.GetDocument().IsHTMLDocument();
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/serializers/markup_formatter.h b/chromium/third_party/blink/renderer/core/editing/serializers/markup_formatter.h
new file mode 100644
index 00000000000..31693ee08ea
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/serializers/markup_formatter.h
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_MARKUP_FORMATTER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_MARKUP_FORMATTER_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/editing/editing_strategy.h"
+#include "third_party/blink/renderer/core/editing/serializers/serialization.h"
+#include "third_party/blink/renderer/platform/wtf/hash_map.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
+
+namespace blink {
+
+class Attribute;
+class DocumentType;
+class Element;
+class Node;
+
+typedef HashMap<AtomicString, AtomicString> Namespaces;
+
+enum EntityMask {
+ kEntityAmp = 0x0001,
+ kEntityLt = 0x0002,
+ kEntityGt = 0x0004,
+ kEntityQuot = 0x0008,
+ kEntityNbsp = 0x0010,
+ kEntityTab = 0x0020,
+ kEntityLineFeed = 0x0040,
+ kEntityCarriageReturn = 0x0080,
+
+ // Non-breaking space needs to be escaped in innerHTML for compatibility
+ // reasons. See http://trac.webkit.org/changeset/32879. However, we cannot do
+ // this in an XML document because it does not have the entity reference
+ // defined (see bug 19215).
+ kEntityMaskInCDATA = 0,
+ kEntityMaskInPCDATA = kEntityAmp | kEntityLt | kEntityGt,
+ kEntityMaskInHTMLPCDATA = kEntityMaskInPCDATA | kEntityNbsp,
+ kEntityMaskInAttributeValue =
+ kEntityAmp | kEntityQuot | kEntityLt | kEntityGt | kEntityTab |
+ kEntityLineFeed |
+ kEntityCarriageReturn,
+ kEntityMaskInHTMLAttributeValue = kEntityAmp | kEntityQuot | kEntityNbsp,
+};
+
+enum class SerializationType { kAsOwnerDocument, kForcedXML };
+
+class MarkupFormatter final {
+ STACK_ALLOCATED();
+
+ public:
+ static void AppendAttributeValue(StringBuilder&, const String&, bool);
+ static void AppendCDATASection(StringBuilder&, const String&);
+ static void AppendCharactersReplacingEntities(StringBuilder&,
+ const String&,
+ unsigned,
+ unsigned,
+ EntityMask);
+ static void AppendComment(StringBuilder&, const String&);
+ static void AppendDocumentType(StringBuilder&, const DocumentType&);
+ static void AppendNamespace(StringBuilder&,
+ const AtomicString& prefix,
+ const AtomicString& namespace_uri,
+ Namespaces&);
+ static void AppendProcessingInstruction(StringBuilder&,
+ const String& target,
+ const String& data);
+ static void AppendXMLDeclaration(StringBuilder&, const Document&);
+
+ MarkupFormatter(EAbsoluteURLs,
+ SerializationType = SerializationType::kAsOwnerDocument);
+ ~MarkupFormatter();
+
+ void AppendStartMarkup(StringBuilder&, const Node&, Namespaces*);
+ void AppendEndMarkup(StringBuilder&, const Element&);
+
+ bool SerializeAsHTMLDocument(const Node&) const;
+
+ void AppendText(StringBuilder&, Text&);
+ void AppendOpenTag(StringBuilder&, const Element&, Namespaces*);
+ void AppendCloseTag(StringBuilder&, const Element&);
+ void AppendAttribute(StringBuilder&,
+ const Element&,
+ const Attribute&,
+ Namespaces*);
+
+ bool ShouldAddNamespaceElement(const Element&, Namespaces&) const;
+ bool ShouldAddNamespaceAttribute(const Attribute&, const Element&) const;
+ EntityMask EntityMaskForText(const Text&) const;
+ bool ShouldSelfClose(const Element&) const;
+
+ private:
+ String ResolveURLIfNeeded(const Element&, const String&) const;
+ void AppendQuotedURLAttributeValue(StringBuilder&,
+ const Element&,
+ const Attribute&);
+
+ const EAbsoluteURLs resolve_urls_method_;
+ SerializationType serialization_type_;
+
+ DISALLOW_COPY_AND_ASSIGN(MarkupFormatter);
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/serializers/serialization.cc b/chromium/third_party/blink/renderer/core/editing/serializers/serialization.cc
new file mode 100644
index 00000000000..c6d2d4d5251
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/serializers/serialization.cc
@@ -0,0 +1,782 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ * Copyright (C) 2008, 2009, 2010, 2011 Google Inc. All rights reserved.
+ * Copyright (C) 2011 Igalia S.L.
+ * Copyright (C) 2011 Motorola Mobility. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/serializers/serialization.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/css/css_identifier_value.h"
+#include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/css/css_value.h"
+#include "third_party/blink/renderer/core/css_value_keywords.h"
+#include "third_party/blink/renderer/core/dom/cdata_section.h"
+#include "third_party/blink/renderer/core/dom/child_list_mutation_scope.h"
+#include "third_party/blink/renderer/core/dom/comment.h"
+#include "third_party/blink/renderer/core/dom/context_features.h"
+#include "third_party/blink/renderer/core/dom/document_fragment.h"
+#include "third_party/blink/renderer/core/dom/element_traversal.h"
+#include "third_party/blink/renderer/core/dom/exception_code.h"
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/editing/editing_strategy.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/serializers/markup_accumulator.h"
+#include "third_party/blink/renderer/core/editing/serializers/styled_markup_serializer.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/html/html_anchor_element.h"
+#include "third_party/blink/renderer/core/html/html_body_element.h"
+#include "third_party/blink/renderer/core/html/html_br_element.h"
+#include "third_party/blink/renderer/core/html/html_div_element.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/html/html_quote_element.h"
+#include "third_party/blink/renderer/core/html/html_span_element.h"
+#include "third_party/blink/renderer/core/html/html_table_cell_element.h"
+#include "third_party/blink/renderer/core/html/html_table_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/platform/bindings/runtime_call_stats.h"
+#include "third_party/blink/renderer/platform/bindings/v8_per_isolate_data.h"
+#include "third_party/blink/renderer/platform/weborigin/kurl.h"
+#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+class AttributeChange {
+ DISALLOW_NEW_EXCEPT_PLACEMENT_NEW();
+
+ public:
+ AttributeChange() : name_(g_null_atom, g_null_atom, g_null_atom) {}
+
+ AttributeChange(Element* element,
+ const QualifiedName& name,
+ const String& value)
+ : element_(element), name_(name), value_(value) {}
+
+ void Apply() { element_->setAttribute(name_, AtomicString(value_)); }
+
+ void Trace(blink::Visitor* visitor) { visitor->Trace(element_); }
+
+ private:
+ Member<Element> element_;
+ QualifiedName name_;
+ String value_;
+};
+
+} // namespace blink
+
+WTF_ALLOW_INIT_WITH_MEM_FUNCTIONS(blink::AttributeChange);
+
+namespace blink {
+
+static void CompleteURLs(DocumentFragment& fragment, const String& base_url) {
+ HeapVector<AttributeChange> changes;
+
+ KURL parsed_base_url(base_url);
+
+ for (Element& element : ElementTraversal::DescendantsOf(fragment)) {
+ AttributeCollection attributes = element.Attributes();
+ // AttributeCollection::iterator end = attributes.end();
+ for (const auto& attribute : attributes) {
+ if (element.IsURLAttribute(attribute) && !attribute.Value().IsEmpty())
+ changes.push_back(AttributeChange(
+ &element, attribute.GetName(),
+ KURL(parsed_base_url, attribute.Value()).GetString()));
+ }
+ }
+
+ for (auto& change : changes)
+ change.Apply();
+}
+
+static bool IsHTMLBlockElement(const Node* node) {
+ DCHECK(node);
+ return IsHTMLTableCellElement(*node) || IsNonTableCellHTMLBlockElement(node);
+}
+
+static HTMLElement* AncestorToRetainStructureAndAppearanceForBlock(
+ Element* common_ancestor_block) {
+ if (!common_ancestor_block)
+ return nullptr;
+
+ if (common_ancestor_block->HasTagName(tbodyTag) ||
+ IsHTMLTableRowElement(*common_ancestor_block))
+ return Traversal<HTMLTableElement>::FirstAncestor(*common_ancestor_block);
+
+ if (IsNonTableCellHTMLBlockElement(common_ancestor_block))
+ return ToHTMLElement(common_ancestor_block);
+
+ return nullptr;
+}
+
+static inline HTMLElement* AncestorToRetainStructureAndAppearance(
+ Node* common_ancestor) {
+ return AncestorToRetainStructureAndAppearanceForBlock(
+ EnclosingBlock(common_ancestor));
+}
+
+static inline HTMLElement*
+AncestorToRetainStructureAndAppearanceWithNoLayoutObject(
+ const Node& common_ancestor) {
+ HTMLElement* common_ancestor_block = ToHTMLElement(EnclosingNodeOfType(
+ FirstPositionInOrBeforeNode(common_ancestor), IsHTMLBlockElement));
+ return AncestorToRetainStructureAndAppearanceForBlock(common_ancestor_block);
+}
+
+bool PropertyMissingOrEqualToNone(CSSPropertyValueSet* style,
+ CSSPropertyID property_id) {
+ if (!style)
+ return false;
+ const CSSValue* value = style->GetPropertyCSSValue(property_id);
+ if (!value)
+ return true;
+ if (!value->IsIdentifierValue())
+ return false;
+ return ToCSSIdentifierValue(value)->GetValueID() == CSSValueNone;
+}
+
+template <typename Strategy>
+static HTMLElement* HighestAncestorToWrapMarkup(
+ const PositionTemplate<Strategy>& start_position,
+ const PositionTemplate<Strategy>& end_position,
+ EAnnotateForInterchange should_annotate,
+ Node* constraining_ancestor) {
+ Node* first_node = start_position.NodeAsRangeFirstNode();
+ // For compatibility reason, we use container node of start and end
+ // positions rather than first node and last node in selection.
+ Node* common_ancestor =
+ Strategy::CommonAncestor(*start_position.ComputeContainerNode(),
+ *end_position.ComputeContainerNode());
+ DCHECK(common_ancestor);
+ HTMLElement* special_common_ancestor = nullptr;
+ if (should_annotate == kAnnotateForInterchange) {
+ // Include ancestors that aren't completely inside the range but are
+ // required to retain the structure and appearance of the copied markup.
+ special_common_ancestor =
+ AncestorToRetainStructureAndAppearance(common_ancestor);
+ if (first_node) {
+ const Position& first_node_position =
+ FirstPositionInOrBeforeNode(*first_node);
+ if (Node* parent_list_node =
+ EnclosingNodeOfType(first_node_position, IsListItem)) {
+ EphemeralRangeTemplate<Strategy> markup_range =
+ EphemeralRangeTemplate<Strategy>(start_position, end_position);
+ EphemeralRangeTemplate<Strategy> node_range =
+ NormalizeRange(EphemeralRangeTemplate<Strategy>::RangeOfContents(
+ *parent_list_node));
+ if (node_range == markup_range) {
+ ContainerNode* ancestor = parent_list_node->parentNode();
+ while (ancestor && !IsHTMLListElement(ancestor))
+ ancestor = ancestor->parentNode();
+ special_common_ancestor = ToHTMLElement(ancestor);
+ }
+ }
+
+ // Retain the Mail quote level by including all ancestor mail block
+ // quotes.
+ if (HTMLQuoteElement* highest_mail_blockquote =
+ ToHTMLQuoteElement(HighestEnclosingNodeOfType(
+ first_node_position, IsMailHTMLBlockquoteElement,
+ kCanCrossEditingBoundary))) {
+ special_common_ancestor = highest_mail_blockquote;
+ }
+ }
+ }
+
+ Node* check_ancestor =
+ special_common_ancestor ? special_common_ancestor : common_ancestor;
+ if (check_ancestor->GetLayoutObject()) {
+ HTMLElement* new_special_common_ancestor =
+ ToHTMLElement(HighestEnclosingNodeOfType(
+ Position::FirstPositionInNode(*check_ancestor),
+ &IsPresentationalHTMLElement, kCanCrossEditingBoundary,
+ constraining_ancestor));
+ if (new_special_common_ancestor)
+ special_common_ancestor = new_special_common_ancestor;
+ }
+
+ // If a single tab is selected, commonAncestor will be a text node inside a
+ // tab span. If two or more tabs are selected, commonAncestor will be the tab
+ // span. In either case, if there is a specialCommonAncestor already, it will
+ // necessarily be above any tab span that needs to be included.
+ if (!special_common_ancestor &&
+ IsTabHTMLSpanElementTextNode(common_ancestor)) {
+ special_common_ancestor =
+ ToHTMLSpanElement(Strategy::Parent(*common_ancestor));
+ }
+ if (!special_common_ancestor && IsTabHTMLSpanElement(common_ancestor))
+ special_common_ancestor = ToHTMLSpanElement(common_ancestor);
+
+ if (HTMLAnchorElement* enclosing_anchor =
+ ToHTMLAnchorElement(EnclosingElementWithTag(
+ Position::FirstPositionInNode(special_common_ancestor
+ ? *special_common_ancestor
+ : *common_ancestor),
+ aTag)))
+ special_common_ancestor = enclosing_anchor;
+
+ return special_common_ancestor;
+}
+
+template <typename Strategy>
+class CreateMarkupAlgorithm {
+ public:
+ static String CreateMarkup(
+ const PositionTemplate<Strategy>& start_position,
+ const PositionTemplate<Strategy>& end_position,
+ EAnnotateForInterchange should_annotate = kDoNotAnnotateForInterchange,
+ ConvertBlocksToInlines = ConvertBlocksToInlines::kNotConvert,
+ EAbsoluteURLs should_resolve_urls = kDoNotResolveURLs,
+ Node* constraining_ancestor = nullptr);
+};
+
+// FIXME: Shouldn't we omit style info when annotate ==
+// DoNotAnnotateForInterchange?
+// FIXME: At least, annotation and style info should probably not be included in
+// range.markupString()
+template <typename Strategy>
+String CreateMarkupAlgorithm<Strategy>::CreateMarkup(
+ const PositionTemplate<Strategy>& start_position,
+ const PositionTemplate<Strategy>& end_position,
+ EAnnotateForInterchange should_annotate,
+ ConvertBlocksToInlines convert_blocks_to_inlines,
+ EAbsoluteURLs should_resolve_urls,
+ Node* constraining_ancestor) {
+ if (start_position.IsNull() || end_position.IsNull())
+ return g_empty_string;
+
+ CHECK_LE(start_position.CompareTo(end_position), 0);
+
+ bool collapsed = start_position == end_position;
+ if (collapsed)
+ return g_empty_string;
+ Node* common_ancestor =
+ Strategy::CommonAncestor(*start_position.ComputeContainerNode(),
+ *end_position.ComputeContainerNode());
+ if (!common_ancestor)
+ return g_empty_string;
+
+ Document* document = start_position.GetDocument();
+
+ DCHECK(!document->NeedsLayoutTreeUpdate());
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ document->Lifecycle());
+
+ HTMLElement* special_common_ancestor = HighestAncestorToWrapMarkup<Strategy>(
+ start_position, end_position, should_annotate, constraining_ancestor);
+ StyledMarkupSerializer<Strategy> serializer(
+ should_resolve_urls, should_annotate, start_position, end_position,
+ special_common_ancestor, convert_blocks_to_inlines);
+ return serializer.CreateMarkup();
+}
+
+String CreateMarkup(const Position& start_position,
+ const Position& end_position,
+ EAnnotateForInterchange should_annotate,
+ ConvertBlocksToInlines convert_blocks_to_inlines,
+ EAbsoluteURLs should_resolve_urls,
+ Node* constraining_ancestor) {
+ return CreateMarkupAlgorithm<EditingStrategy>::CreateMarkup(
+ start_position, end_position, should_annotate, convert_blocks_to_inlines,
+ should_resolve_urls, constraining_ancestor);
+}
+
+String CreateMarkup(const PositionInFlatTree& start_position,
+ const PositionInFlatTree& end_position,
+ EAnnotateForInterchange should_annotate,
+ ConvertBlocksToInlines convert_blocks_to_inlines,
+ EAbsoluteURLs should_resolve_urls,
+ Node* constraining_ancestor) {
+ return CreateMarkupAlgorithm<EditingInFlatTreeStrategy>::CreateMarkup(
+ start_position, end_position, should_annotate, convert_blocks_to_inlines,
+ should_resolve_urls, constraining_ancestor);
+}
+
+DocumentFragment* CreateFragmentFromMarkup(
+ Document& document,
+ const String& markup,
+ const String& base_url,
+ ParserContentPolicy parser_content_policy) {
+ // We use a fake body element here to trick the HTML parser to using the
+ // InBody insertion mode.
+ HTMLBodyElement* fake_body = HTMLBodyElement::Create(document);
+ DocumentFragment* fragment = DocumentFragment::Create(document);
+
+ fragment->ParseHTML(markup, fake_body, parser_content_policy);
+
+ if (!base_url.IsEmpty() && base_url != BlankURL() &&
+ base_url != document.BaseURL())
+ CompleteURLs(*fragment, base_url);
+
+ return fragment;
+}
+
+static const char kFragmentMarkerTag[] = "webkit-fragment-marker";
+
+static bool FindNodesSurroundingContext(DocumentFragment* fragment,
+ Comment*& node_before_context,
+ Comment*& node_after_context) {
+ if (!fragment->firstChild())
+ return false;
+ for (Node& node : NodeTraversal::StartsAt(*fragment->firstChild())) {
+ if (node.getNodeType() == Node::kCommentNode &&
+ ToComment(node).data() == kFragmentMarkerTag) {
+ if (!node_before_context) {
+ node_before_context = &ToComment(node);
+ } else {
+ node_after_context = &ToComment(node);
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+static void TrimFragment(DocumentFragment* fragment,
+ Comment* node_before_context,
+ Comment* node_after_context) {
+ Node* next = nullptr;
+ for (Node* node = fragment->firstChild(); node; node = next) {
+ if (node_before_context->IsDescendantOf(node)) {
+ next = NodeTraversal::Next(*node);
+ continue;
+ }
+ next = NodeTraversal::NextSkippingChildren(*node);
+ DCHECK(!node->contains(node_after_context))
+ << node << " " << node_after_context;
+ node->parentNode()->RemoveChild(node, ASSERT_NO_EXCEPTION);
+ if (node_before_context == node)
+ break;
+ }
+
+ DCHECK(node_after_context->parentNode()) << node_after_context;
+ for (Node* node = node_after_context; node; node = next) {
+ next = NodeTraversal::NextSkippingChildren(*node);
+ node->parentNode()->RemoveChild(node, ASSERT_NO_EXCEPTION);
+ }
+}
+
+DocumentFragment* CreateFragmentFromMarkupWithContext(
+ Document& document,
+ const String& markup,
+ unsigned fragment_start,
+ unsigned fragment_end,
+ const String& base_url,
+ ParserContentPolicy parser_content_policy) {
+ // FIXME: Need to handle the case where the markup already contains these
+ // markers.
+
+ StringBuilder tagged_markup;
+ tagged_markup.Append(markup.Left(fragment_start));
+ MarkupFormatter::AppendComment(tagged_markup, kFragmentMarkerTag);
+ tagged_markup.Append(
+ markup.Substring(fragment_start, fragment_end - fragment_start));
+ MarkupFormatter::AppendComment(tagged_markup, kFragmentMarkerTag);
+ tagged_markup.Append(markup.Substring(fragment_end));
+
+ DocumentFragment* tagged_fragment = CreateFragmentFromMarkup(
+ document, tagged_markup.ToString(), base_url, parser_content_policy);
+
+ Comment* node_before_context = nullptr;
+ Comment* node_after_context = nullptr;
+ if (!FindNodesSurroundingContext(tagged_fragment, node_before_context,
+ node_after_context))
+ return nullptr;
+
+ Document* tagged_document = Document::Create(DocumentInit::Create());
+ tagged_document->SetContextFeatures(document.GetContextFeatures());
+
+ Element* root = Element::Create(QualifiedName::Null(), tagged_document);
+ root->AppendChild(tagged_fragment);
+ tagged_document->AppendChild(root);
+
+ const EphemeralRange range(
+ Position::AfterNode(*node_before_context).ParentAnchoredEquivalent(),
+ Position::BeforeNode(*node_after_context).ParentAnchoredEquivalent());
+
+ DCHECK(range.CommonAncestorContainer());
+ Node& common_ancestor = *range.CommonAncestorContainer();
+ HTMLElement* special_common_ancestor =
+ AncestorToRetainStructureAndAppearanceWithNoLayoutObject(common_ancestor);
+
+ // When there's a special common ancestor outside of the fragment, we must
+ // include it as well to preserve the structure and appearance of the
+ // fragment. For example, if the fragment contains TD, we need to include the
+ // enclosing TABLE tag as well.
+ DocumentFragment* fragment = DocumentFragment::Create(document);
+ if (special_common_ancestor)
+ fragment->AppendChild(special_common_ancestor);
+ else
+ fragment->ParserTakeAllChildrenFrom(ToContainerNode(common_ancestor));
+
+ TrimFragment(fragment, node_before_context, node_after_context);
+
+ return fragment;
+}
+
+String CreateMarkup(const Node* node,
+ EChildrenOnly children_only,
+ EAbsoluteURLs should_resolve_urls) {
+ if (!node)
+ return "";
+
+ MarkupAccumulator accumulator(should_resolve_urls);
+ return SerializeNodes<EditingStrategy>(accumulator, const_cast<Node&>(*node),
+ children_only);
+}
+
+static void FillContainerFromString(ContainerNode* paragraph,
+ const String& string) {
+ Document& document = paragraph->GetDocument();
+
+ if (string.IsEmpty()) {
+ paragraph->AppendChild(HTMLBRElement::Create(document));
+ return;
+ }
+
+ DCHECK_EQ(string.find('\n'), kNotFound) << string;
+
+ Vector<String> tab_list;
+ string.Split('\t', true, tab_list);
+ StringBuilder tab_text;
+ bool first = true;
+ size_t num_entries = tab_list.size();
+ for (size_t i = 0; i < num_entries; ++i) {
+ const String& s = tab_list[i];
+
+ // append the non-tab textual part
+ if (!s.IsEmpty()) {
+ if (!tab_text.IsEmpty()) {
+ paragraph->AppendChild(
+ CreateTabSpanElement(document, tab_text.ToString()));
+ tab_text.Clear();
+ }
+ Text* text_node = document.createTextNode(
+ StringWithRebalancedWhitespace(s, first, i + 1 == num_entries));
+ paragraph->AppendChild(text_node);
+ }
+
+ // there is a tab after every entry, except the last entry
+ // (if the last character is a tab, the list gets an extra empty entry)
+ if (i + 1 != num_entries)
+ tab_text.Append('\t');
+ else if (!tab_text.IsEmpty())
+ paragraph->AppendChild(
+ CreateTabSpanElement(document, tab_text.ToString()));
+
+ first = false;
+ }
+}
+
+bool IsPlainTextMarkup(Node* node) {
+ DCHECK(node);
+ if (!IsHTMLDivElement(*node))
+ return false;
+
+ HTMLDivElement& element = ToHTMLDivElement(*node);
+ if (!element.hasAttributes())
+ return false;
+
+ if (element.HasOneChild())
+ return element.firstChild()->IsTextNode() ||
+ element.firstChild()->hasChildren();
+
+ return element.HasChildCount(2) &&
+ IsTabHTMLSpanElementTextNode(element.firstChild()->firstChild()) &&
+ element.lastChild()->IsTextNode();
+}
+
+static bool ShouldPreserveNewline(const EphemeralRange& range) {
+ if (Node* node = range.StartPosition().NodeAsRangeFirstNode()) {
+ if (LayoutObject* layout_object = node->GetLayoutObject())
+ return layout_object->Style()->PreserveNewline();
+ }
+
+ if (Node* node = range.StartPosition().AnchorNode()) {
+ if (LayoutObject* layout_object = node->GetLayoutObject())
+ return layout_object->Style()->PreserveNewline();
+ }
+
+ return false;
+}
+
+DocumentFragment* CreateFragmentFromText(const EphemeralRange& context,
+ const String& text) {
+ if (context.IsNull())
+ return nullptr;
+
+ Document& document = context.GetDocument();
+ DocumentFragment* fragment = document.createDocumentFragment();
+
+ if (text.IsEmpty())
+ return fragment;
+
+ String string = text;
+ string.Replace("\r\n", "\n");
+ string.Replace('\r', '\n');
+
+ if (!IsRichlyEditablePosition(context.StartPosition()) ||
+ ShouldPreserveNewline(context)) {
+ fragment->AppendChild(document.createTextNode(string));
+ if (string.EndsWith('\n')) {
+ HTMLBRElement* element = HTMLBRElement::Create(document);
+ element->setAttribute(classAttr, AppleInterchangeNewline);
+ fragment->AppendChild(element);
+ }
+ return fragment;
+ }
+
+ // A string with no newlines gets added inline, rather than being put into a
+ // paragraph.
+ if (string.find('\n') == kNotFound) {
+ FillContainerFromString(fragment, string);
+ return fragment;
+ }
+
+ // Break string into paragraphs. Extra line breaks turn into empty paragraphs.
+ Element* block =
+ EnclosingBlock(context.StartPosition().NodeAsRangeFirstNode());
+ bool use_clones_of_enclosing_block =
+ block && !IsHTMLBodyElement(*block) && !IsHTMLHtmlElement(*block) &&
+ block != RootEditableElementOf(context.StartPosition());
+
+ Vector<String> list;
+ string.Split('\n', true, list); // true gets us empty strings in the list
+ size_t num_lines = list.size();
+ for (size_t i = 0; i < num_lines; ++i) {
+ const String& s = list[i];
+
+ Element* element = nullptr;
+ if (s.IsEmpty() && i + 1 == num_lines) {
+ // For last line, use the "magic BR" rather than a P.
+ element = HTMLBRElement::Create(document);
+ element->setAttribute(classAttr, AppleInterchangeNewline);
+ } else {
+ if (use_clones_of_enclosing_block)
+ element = block->CloneWithoutChildren();
+ else
+ element = CreateDefaultParagraphElement(document);
+ FillContainerFromString(element, s);
+ }
+ fragment->AppendChild(element);
+ }
+ return fragment;
+}
+
+DocumentFragment* CreateFragmentForInnerOuterHTML(
+ const String& markup,
+ Element* context_element,
+ ParserContentPolicy parser_content_policy,
+ const char* method,
+ ExceptionState& exception_state) {
+ DCHECK(context_element);
+ Document& document =
+ IsHTMLTemplateElement(*context_element)
+ ? context_element->GetDocument().EnsureTemplateDocument()
+ : context_element->GetDocument();
+ DocumentFragment* fragment = DocumentFragment::Create(document);
+
+ if (document.IsHTMLDocument()) {
+ fragment->ParseHTML(markup, context_element, parser_content_policy);
+ return fragment;
+ }
+
+ bool was_valid =
+ fragment->ParseXML(markup, context_element, parser_content_policy);
+ if (!was_valid) {
+ exception_state.ThrowDOMException(
+ kSyntaxError,
+ "The provided markup is invalid XML, and "
+ "therefore cannot be inserted into an XML "
+ "document.");
+ return nullptr;
+ }
+ return fragment;
+}
+
+DocumentFragment* CreateFragmentForTransformToFragment(
+ const String& source_string,
+ const String& source_mime_type,
+ Document& output_doc) {
+ DocumentFragment* fragment = output_doc.createDocumentFragment();
+
+ if (source_mime_type == "text/html") {
+ // As far as I can tell, there isn't a spec for how transformToFragment is
+ // supposed to work. Based on the documentation I can find, it looks like we
+ // want to start parsing the fragment in the InBody insertion mode.
+ // Unfortunately, that's an implementation detail of the parser. We achieve
+ // that effect here by passing in a fake body element as context for the
+ // fragment.
+ HTMLBodyElement* fake_body = HTMLBodyElement::Create(output_doc);
+ fragment->ParseHTML(source_string, fake_body);
+ } else if (source_mime_type == "text/plain") {
+ fragment->ParserAppendChild(Text::Create(output_doc, source_string));
+ } else {
+ bool successful_parse = fragment->ParseXML(source_string, nullptr);
+ if (!successful_parse)
+ return nullptr;
+ }
+
+ // FIXME: Do we need to mess with URLs here?
+
+ return fragment;
+}
+
+static inline void RemoveElementPreservingChildren(DocumentFragment* fragment,
+ HTMLElement* element) {
+ Node* next_child = nullptr;
+ for (Node* child = element->firstChild(); child; child = next_child) {
+ next_child = child->nextSibling();
+ element->RemoveChild(child);
+ fragment->InsertBefore(child, element);
+ }
+ fragment->RemoveChild(element);
+}
+
+DocumentFragment* CreateContextualFragment(
+ const String& markup,
+ Element* element,
+ ParserContentPolicy parser_content_policy,
+ ExceptionState& exception_state) {
+ DCHECK(element);
+
+ DocumentFragment* fragment = CreateFragmentForInnerOuterHTML(
+ markup, element, parser_content_policy, "createContextualFragment",
+ exception_state);
+ if (!fragment)
+ return nullptr;
+
+ // We need to pop <html> and <body> elements and remove <head> to
+ // accommodate folks passing complete HTML documents to make the
+ // child of an element.
+
+ Node* next_node = nullptr;
+ for (Node* node = fragment->firstChild(); node; node = next_node) {
+ next_node = node->nextSibling();
+ if (IsHTMLHtmlElement(*node) || IsHTMLHeadElement(*node) ||
+ IsHTMLBodyElement(*node)) {
+ HTMLElement* element = ToHTMLElement(node);
+ if (Node* first_child = element->firstChild())
+ next_node = first_child;
+ RemoveElementPreservingChildren(fragment, element);
+ }
+ }
+ return fragment;
+}
+
+void ReplaceChildrenWithFragment(ContainerNode* container,
+ DocumentFragment* fragment,
+ ExceptionState& exception_state) {
+ RUNTIME_CALL_TIMER_SCOPE(
+ V8PerIsolateData::MainThreadIsolate(),
+ RuntimeCallStats::CounterId::kReplaceChildrenWithFragment);
+ DCHECK(container);
+ ContainerNode* container_node(container);
+
+ ChildListMutationScope mutation(*container_node);
+
+ if (!fragment->firstChild()) {
+ container_node->RemoveChildren();
+ return;
+ }
+
+ // FIXME: No need to replace the child it is a text node and its contents are
+ // already == text.
+ if (container_node->HasOneChild()) {
+ container_node->ReplaceChild(fragment, container_node->firstChild(),
+ exception_state);
+ return;
+ }
+
+ container_node->RemoveChildren();
+ container_node->AppendChild(fragment, exception_state);
+}
+
+void ReplaceChildrenWithText(ContainerNode* container,
+ const String& text,
+ ExceptionState& exception_state) {
+ DCHECK(container);
+ ContainerNode* container_node(container);
+
+ ChildListMutationScope mutation(*container_node);
+
+ // FIXME: This is wrong if containerNode->firstChild() has more than one ref!
+ // Example:
+ // <div>foo</div>
+ // <script>
+ // var oldText = div.firstChild;
+ // console.log(oldText.data); // foo
+ // div.innerText = "bar";
+ // console.log(oldText.data); // bar!?!
+ // </script>
+ // I believe this is an intentional benchmark cheat from years ago.
+ // We should re-visit if we actually want this still.
+ if (container_node->HasOneTextChild()) {
+ ToText(container_node->firstChild())->setData(text);
+ return;
+ }
+
+ // NOTE: This method currently always creates a text node, even if that text
+ // node will be empty.
+ Text* text_node = Text::Create(container_node->GetDocument(), text);
+
+ // FIXME: No need to replace the child it is a text node and its contents are
+ // already == text.
+ if (container_node->HasOneChild()) {
+ container_node->ReplaceChild(text_node, container_node->firstChild(),
+ exception_state);
+ return;
+ }
+
+ container_node->RemoveChildren();
+ container_node->AppendChild(text_node, exception_state);
+}
+
+void MergeWithNextTextNode(Text* text_node, ExceptionState& exception_state) {
+ DCHECK(text_node);
+ Node* next = text_node->nextSibling();
+ if (!next || !next->IsTextNode())
+ return;
+
+ Text* text_next = ToText(next);
+ text_node->appendData(text_next->data());
+ if (text_next->parentNode()) // Might have been removed by mutation event.
+ text_next->remove(exception_state);
+}
+
+template class CORE_TEMPLATE_EXPORT CreateMarkupAlgorithm<EditingStrategy>;
+template class CORE_TEMPLATE_EXPORT
+ CreateMarkupAlgorithm<EditingInFlatTreeStrategy>;
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/serializers/serialization.h b/chromium/third_party/blink/renderer/core/editing/serializers/serialization.h
new file mode 100644
index 00000000000..0dc9a4f9255
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/serializers/serialization.h
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2004 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_SERIALIZATION_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_SERIALIZATION_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/css_property_names.h"
+#include "third_party/blink/renderer/core/dom/parser_content_policy.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/serializers/html_interchange.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+
+namespace blink {
+
+class ContainerNode;
+class Document;
+class DocumentFragment;
+class Element;
+class ExceptionState;
+class Node;
+class CSSPropertyValueSet;
+
+enum EChildrenOnly { kIncludeNode, kChildrenOnly };
+enum EAbsoluteURLs { kDoNotResolveURLs, kResolveAllURLs, kResolveNonLocalURLs };
+enum class ConvertBlocksToInlines { kNotConvert, kConvert };
+
+DocumentFragment* CreateFragmentFromText(const EphemeralRange& context,
+ const String& text);
+DocumentFragment* CreateFragmentFromMarkup(
+ Document&,
+ const String& markup,
+ const String& base_url,
+ ParserContentPolicy = kAllowScriptingContent);
+DocumentFragment* CreateFragmentFromMarkupWithContext(Document&,
+ const String& markup,
+ unsigned fragment_start,
+ unsigned fragment_end,
+ const String& base_url,
+ ParserContentPolicy);
+DocumentFragment* CreateFragmentForInnerOuterHTML(const String&,
+ Element*,
+ ParserContentPolicy,
+ const char* method,
+ ExceptionState&);
+DocumentFragment* CreateFragmentForTransformToFragment(
+ const String&,
+ const String& source_mime_type,
+ Document& output_doc);
+DocumentFragment* CreateContextualFragment(const String&,
+ Element*,
+ ParserContentPolicy,
+ ExceptionState&);
+
+bool IsPlainTextMarkup(Node*);
+
+// These methods are used by HTMLElement & ShadowRoot to replace the
+// children with respected fragment/text.
+void ReplaceChildrenWithFragment(ContainerNode*,
+ DocumentFragment*,
+ ExceptionState&);
+void ReplaceChildrenWithText(ContainerNode*, const String&, ExceptionState&);
+
+CORE_EXPORT String CreateMarkup(const Node*,
+ EChildrenOnly = kIncludeNode,
+ EAbsoluteURLs = kDoNotResolveURLs);
+
+CORE_EXPORT String
+CreateMarkup(const Position& start,
+ const Position& end,
+ EAnnotateForInterchange = kDoNotAnnotateForInterchange,
+ ConvertBlocksToInlines = ConvertBlocksToInlines::kNotConvert,
+ EAbsoluteURLs = kDoNotResolveURLs,
+ Node* constraining_ancestor = nullptr);
+CORE_EXPORT String
+CreateMarkup(const PositionInFlatTree& start,
+ const PositionInFlatTree& end,
+ EAnnotateForInterchange = kDoNotAnnotateForInterchange,
+ ConvertBlocksToInlines = ConvertBlocksToInlines::kNotConvert,
+ EAbsoluteURLs = kDoNotResolveURLs,
+ Node* constraining_ancestor = nullptr);
+
+void MergeWithNextTextNode(Text*, ExceptionState&);
+
+bool PropertyMissingOrEqualToNone(CSSPropertyValueSet*, CSSPropertyID);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_SERIALIZATION_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_accumulator.cc b/chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_accumulator.cc
new file mode 100644
index 00000000000..5cd195d3c4c
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_accumulator.cc
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ * Copyright (C) 2008, 2009, 2010, 2011 Google Inc. All rights reserved.
+ * Copyright (C) 2011 Igalia S.L.
+ * Copyright (C) 2011 Motorola Mobility. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/serializers/styled_markup_accumulator.h"
+
+#include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
+
+namespace blink {
+
+namespace {
+
+size_t TotalLength(const Vector<String>& strings) {
+ size_t length = 0;
+ for (const auto& string : strings)
+ length += string.length();
+ return length;
+}
+
+} // namespace
+
+using namespace HTMLNames;
+
+StyledMarkupAccumulator::StyledMarkupAccumulator(
+ EAbsoluteURLs should_resolve_urls,
+ const TextOffset& start,
+ const TextOffset& end,
+ Document* document,
+ EAnnotateForInterchange should_annotate,
+ ConvertBlocksToInlines convert_blocks_to_inlines)
+ : formatter_(should_resolve_urls),
+ start_(start),
+ end_(end),
+ document_(document),
+ should_annotate_(should_annotate),
+ convert_blocks_to_inlines_(convert_blocks_to_inlines) {}
+
+void StyledMarkupAccumulator::AppendEndTag(const Element& element) {
+ AppendEndMarkup(result_, element);
+}
+
+void StyledMarkupAccumulator::AppendStartMarkup(Node& node) {
+ formatter_.AppendStartMarkup(result_, node, nullptr);
+}
+
+void StyledMarkupAccumulator::AppendEndMarkup(StringBuilder& result,
+ const Element& element) {
+ formatter_.AppendEndMarkup(result, element);
+}
+
+void StyledMarkupAccumulator::AppendText(Text& text) {
+ const String& str = text.data();
+ unsigned length = str.length();
+ unsigned start = 0;
+ if (end_.IsNotNull()) {
+ if (text == end_.GetText())
+ length = end_.Offset();
+ }
+ if (start_.IsNotNull()) {
+ if (text == start_.GetText()) {
+ start = start_.Offset();
+ length -= start;
+ }
+ }
+ MarkupFormatter::AppendCharactersReplacingEntities(
+ result_, str, start, length, formatter_.EntityMaskForText(text));
+}
+
+void StyledMarkupAccumulator::AppendTextWithInlineStyle(
+ Text& text,
+ EditingStyle* inline_style) {
+ if (inline_style) {
+ // wrappingStyleForAnnotatedSerialization should have removed
+ // -webkit-text-decorations-in-effect.
+ DCHECK(!ShouldAnnotate() || PropertyMissingOrEqualToNone(
+ inline_style->Style(),
+ CSSPropertyWebkitTextDecorationsInEffect));
+ DCHECK(document_);
+
+ result_.Append("<span style=\"");
+ MarkupFormatter::AppendAttributeValue(
+ result_, inline_style->Style()->AsText(), document_->IsHTMLDocument());
+ result_.Append("\">");
+ }
+ if (!ShouldAnnotate()) {
+ AppendText(text);
+ } else {
+ const bool use_rendered_text = !EnclosingElementWithTag(
+ Position::FirstPositionInNode(text), selectTag);
+ String content =
+ use_rendered_text ? RenderedText(text) : StringValueForRange(text);
+ StringBuilder buffer;
+ MarkupFormatter::AppendCharactersReplacingEntities(
+ buffer, content, 0, content.length(), kEntityMaskInPCDATA);
+ result_.Append(ConvertHTMLTextToInterchangeFormat(buffer.ToString(), text));
+ }
+ if (inline_style)
+ result_.Append("</span>");
+}
+
+void StyledMarkupAccumulator::AppendElementWithInlineStyle(
+ const Element& element,
+ EditingStyle* style) {
+ AppendElementWithInlineStyle(result_, element, style);
+}
+
+void StyledMarkupAccumulator::AppendElementWithInlineStyle(
+ StringBuilder& out,
+ const Element& element,
+ EditingStyle* style) {
+ const bool document_is_html = element.GetDocument().IsHTMLDocument();
+ formatter_.AppendOpenTag(out, element, nullptr);
+ AttributeCollection attributes = element.Attributes();
+ for (const auto& attribute : attributes) {
+ // We'll handle the style attribute separately, below.
+ if (attribute.GetName() == styleAttr)
+ continue;
+ formatter_.AppendAttribute(out, element, attribute, nullptr);
+ }
+ if (style && !style->IsEmpty()) {
+ out.Append(" style=\"");
+ MarkupFormatter::AppendAttributeValue(out, style->Style()->AsText(),
+ document_is_html);
+ out.Append('\"');
+ }
+ formatter_.AppendCloseTag(out, element);
+}
+
+void StyledMarkupAccumulator::AppendElement(const Element& element) {
+ AppendElement(result_, element);
+}
+
+void StyledMarkupAccumulator::AppendElement(StringBuilder& out,
+ const Element& element) {
+ formatter_.AppendOpenTag(out, element, nullptr);
+ AttributeCollection attributes = element.Attributes();
+ for (const auto& attribute : attributes)
+ formatter_.AppendAttribute(out, element, attribute, nullptr);
+ formatter_.AppendCloseTag(out, element);
+}
+
+void StyledMarkupAccumulator::WrapWithStyleNode(CSSPropertyValueSet* style) {
+ // wrappingStyleForSerialization should have removed
+ // -webkit-text-decorations-in-effect.
+ DCHECK(PropertyMissingOrEqualToNone(
+ style, CSSPropertyWebkitTextDecorationsInEffect));
+ DCHECK(document_);
+
+ StringBuilder open_tag;
+ open_tag.Append("<div style=\"");
+ MarkupFormatter::AppendAttributeValue(open_tag, style->AsText(),
+ document_->IsHTMLDocument());
+ open_tag.Append("\">");
+ reversed_preceding_markup_.push_back(open_tag.ToString());
+
+ result_.Append("</div>");
+}
+
+String StyledMarkupAccumulator::TakeResults() {
+ StringBuilder result;
+ result.ReserveCapacity(TotalLength(reversed_preceding_markup_) +
+ result_.length());
+
+ for (size_t i = reversed_preceding_markup_.size(); i > 0; --i)
+ result.Append(reversed_preceding_markup_[i - 1]);
+
+ result.Append(result_);
+
+ // We remove '\0' characters because they are not visibly rendered to the
+ // user.
+ return result.ToString().Replace(0, "");
+}
+
+String StyledMarkupAccumulator::RenderedText(Text& text_node) {
+ int start_offset = 0;
+ int end_offset = text_node.length();
+ if (start_.GetText() == text_node)
+ start_offset = start_.Offset();
+ if (end_.GetText() == text_node)
+ end_offset = end_.Offset();
+ return PlainText(EphemeralRange(Position(&text_node, start_offset),
+ Position(&text_node, end_offset)));
+}
+
+String StyledMarkupAccumulator::StringValueForRange(const Text& node) {
+ if (start_.IsNull())
+ return node.data();
+
+ String str = node.data();
+ if (start_.GetText() == node)
+ str.Truncate(end_.Offset());
+ if (end_.GetText() == node)
+ str.Remove(0, start_.Offset());
+ return str;
+}
+
+bool StyledMarkupAccumulator::ShouldAnnotate() const {
+ return should_annotate_ == kAnnotateForInterchange;
+}
+
+void StyledMarkupAccumulator::PushMarkup(const String& str) {
+ reversed_preceding_markup_.push_back(str);
+}
+
+void StyledMarkupAccumulator::AppendInterchangeNewline() {
+ DEFINE_STATIC_LOCAL(const String, interchange_newline_string,
+ ("<br class=\"" AppleInterchangeNewline "\">"));
+ result_.Append(interchange_newline_string);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_accumulator.h b/chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_accumulator.h
new file mode 100644
index 00000000000..e211def8741
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_accumulator.h
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ * Copyright (C) 2008, 2009, 2010, 2011 Google Inc. All rights reserved.
+ * Copyright (C) 2011 Igalia S.L.
+ * Copyright (C) 2011 Motorola Mobility. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_STYLED_MARKUP_ACCUMULATOR_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_STYLED_MARKUP_ACCUMULATOR_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/editing/editing_style.h"
+#include "third_party/blink/renderer/core/editing/serializers/markup_formatter.h"
+#include "third_party/blink/renderer/core/editing/serializers/text_offset.h"
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+
+namespace blink {
+
+class Document;
+class CSSPropertyValueSet;
+class Text;
+
+class StyledMarkupAccumulator final {
+ STACK_ALLOCATED();
+
+ public:
+ StyledMarkupAccumulator(EAbsoluteURLs,
+ const TextOffset& start,
+ const TextOffset& end,
+ Document*,
+ EAnnotateForInterchange,
+ ConvertBlocksToInlines);
+
+ void AppendEndTag(const Element&);
+ void AppendInterchangeNewline();
+
+ void AppendText(Text&);
+ void AppendTextWithInlineStyle(Text&, EditingStyle*);
+
+ void WrapWithStyleNode(CSSPropertyValueSet*);
+ String TakeResults();
+
+ void PushMarkup(const String&);
+
+ void AppendElement(const Element&);
+ void AppendElement(StringBuilder&, const Element&);
+ void AppendElementWithInlineStyle(const Element&, EditingStyle*);
+ void AppendElementWithInlineStyle(StringBuilder&,
+ const Element&,
+ EditingStyle*);
+ void AppendStartMarkup(Node&);
+
+ bool ShouldAnnotate() const;
+ bool ShouldConvertBlocksToInlines() const {
+ return convert_blocks_to_inlines_ == ConvertBlocksToInlines::kConvert;
+ }
+
+ private:
+ String RenderedText(Text&);
+ String StringValueForRange(const Text&);
+
+ void AppendEndMarkup(StringBuilder&, const Element&);
+
+ MarkupFormatter formatter_;
+ const TextOffset start_;
+ const TextOffset end_;
+ const Member<Document> document_;
+ const EAnnotateForInterchange should_annotate_;
+ StringBuilder result_;
+ Vector<String> reversed_preceding_markup_;
+ const ConvertBlocksToInlines convert_blocks_to_inlines_;
+
+ DISALLOW_COPY_AND_ASSIGN(StyledMarkupAccumulator);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_STYLED_MARKUP_ACCUMULATOR_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_serializer.cc b/chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_serializer.cc
new file mode 100644
index 00000000000..09e14cae1ba
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_serializer.cc
@@ -0,0 +1,560 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ * Copyright (C) 2008, 2009, 2010, 2011 Google Inc. All rights reserved.
+ * Copyright (C) 2011 Igalia S.L.
+ * Copyright (C) 2011 Motorola Mobility. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/serializers/styled_markup_serializer.h"
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/css/css_property_value_set.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/editing_style.h"
+#include "third_party/blink/renderer/core/editing/editing_style_utilities.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/serializers/serialization.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/html/html_body_element.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
+
+namespace blink {
+
+namespace {
+
+template <typename Strategy>
+TextOffset ToTextOffset(const PositionTemplate<Strategy>& position) {
+ if (position.IsNull())
+ return TextOffset();
+
+ if (!position.ComputeContainerNode()->IsTextNode())
+ return TextOffset();
+
+ return TextOffset(ToText(position.ComputeContainerNode()),
+ position.OffsetInContainerNode());
+}
+
+template <typename EditingStrategy>
+static bool HandleSelectionBoundary(const Node&);
+
+template <>
+bool HandleSelectionBoundary<EditingStrategy>(const Node&) {
+ return false;
+}
+
+template <>
+bool HandleSelectionBoundary<EditingInFlatTreeStrategy>(const Node& node) {
+ ShadowRoot* root = node.GetShadowRoot();
+ return root && root->IsUserAgent();
+}
+
+} // namespace
+
+using namespace HTMLNames;
+
+template <typename Strategy>
+class StyledMarkupTraverser {
+ STACK_ALLOCATED();
+
+ public:
+ StyledMarkupTraverser();
+ StyledMarkupTraverser(StyledMarkupAccumulator*, Node*);
+
+ Node* Traverse(Node*, Node*);
+ void WrapWithNode(ContainerNode&, EditingStyle*);
+ EditingStyle* CreateInlineStyleIfNeeded(Node&);
+
+ private:
+ bool ShouldAnnotate() const;
+ bool ShouldConvertBlocksToInlines() const;
+ void AppendStartMarkup(Node&);
+ void AppendEndMarkup(Node&);
+ EditingStyle* CreateInlineStyle(Element&);
+ bool NeedsInlineStyle(const Element&);
+ bool ShouldApplyWrappingStyle(const Node&) const;
+
+ StyledMarkupAccumulator* accumulator_;
+ Member<Node> last_closed_;
+ Member<EditingStyle> wrapping_style_;
+ DISALLOW_COPY_AND_ASSIGN(StyledMarkupTraverser);
+};
+
+template <typename Strategy>
+bool StyledMarkupTraverser<Strategy>::ShouldAnnotate() const {
+ return accumulator_->ShouldAnnotate();
+}
+
+template <typename Strategy>
+bool StyledMarkupTraverser<Strategy>::ShouldConvertBlocksToInlines() const {
+ return accumulator_->ShouldConvertBlocksToInlines();
+}
+
+template <typename Strategy>
+StyledMarkupSerializer<Strategy>::StyledMarkupSerializer(
+ EAbsoluteURLs should_resolve_urls,
+ EAnnotateForInterchange should_annotate,
+ const PositionTemplate<Strategy>& start,
+ const PositionTemplate<Strategy>& end,
+ Node* highest_node_to_be_serialized,
+ ConvertBlocksToInlines convert_blocks_to_inlines)
+ : start_(start),
+ end_(end),
+ should_resolve_urls_(should_resolve_urls),
+ should_annotate_(should_annotate),
+ highest_node_to_be_serialized_(highest_node_to_be_serialized),
+ convert_blocks_to_inlines_(convert_blocks_to_inlines),
+ last_closed_(highest_node_to_be_serialized) {}
+
+template <typename Strategy>
+static bool NeedInterchangeNewlineAfter(
+ const VisiblePositionTemplate<Strategy>& v) {
+ const VisiblePositionTemplate<Strategy> next = NextPositionOf(v);
+ Node* upstream_node =
+ MostBackwardCaretPosition(next.DeepEquivalent()).AnchorNode();
+ Node* downstream_node =
+ MostForwardCaretPosition(v.DeepEquivalent()).AnchorNode();
+ // Add an interchange newline if a paragraph break is selected and a br won't
+ // already be added to the markup to represent it.
+ return IsEndOfParagraph(v) && IsStartOfParagraph(next) &&
+ !(IsHTMLBRElement(*upstream_node) && upstream_node == downstream_node);
+}
+
+template <typename Strategy>
+static bool NeedInterchangeNewlineAt(
+ const VisiblePositionTemplate<Strategy>& v) {
+ return NeedInterchangeNewlineAfter(PreviousPositionOf(v));
+}
+
+template <typename Strategy>
+static bool AreSameRanges(Node* node,
+ const PositionTemplate<Strategy>& start_position,
+ const PositionTemplate<Strategy>& end_position) {
+ DCHECK(node);
+ const EphemeralRange range =
+ CreateVisibleSelection(
+ SelectionInDOMTree::Builder().SelectAllChildren(*node).Build())
+ .ToNormalizedEphemeralRange();
+ return ToPositionInDOMTree(start_position) == range.StartPosition() &&
+ ToPositionInDOMTree(end_position) == range.EndPosition();
+}
+
+static EditingStyle* StyleFromMatchedRulesAndInlineDecl(
+ const HTMLElement* element) {
+ EditingStyle* style = EditingStyle::Create(element->InlineStyle());
+ // FIXME: Having to const_cast here is ugly, but it is quite a bit of work to
+ // untangle the non-const-ness of styleFromMatchedRulesForElement.
+ style->MergeStyleFromRules(const_cast<HTMLElement*>(element));
+ return style;
+}
+
+template <typename Strategy>
+String StyledMarkupSerializer<Strategy>::CreateMarkup() {
+ StyledMarkupAccumulator markup_accumulator(
+ should_resolve_urls_, ToTextOffset(start_.ParentAnchoredEquivalent()),
+ ToTextOffset(end_.ParentAnchoredEquivalent()), start_.GetDocument(),
+ should_annotate_, convert_blocks_to_inlines_);
+
+ Node* past_end = end_.NodeAsRangePastLastNode();
+
+ Node* first_node = start_.NodeAsRangeFirstNode();
+ const VisiblePositionTemplate<Strategy> visible_start =
+ CreateVisiblePosition(start_);
+ const VisiblePositionTemplate<Strategy> visible_end =
+ CreateVisiblePosition(end_);
+ if (ShouldAnnotate() && NeedInterchangeNewlineAfter(visible_start)) {
+ markup_accumulator.AppendInterchangeNewline();
+ if (visible_start.DeepEquivalent() ==
+ PreviousPositionOf(visible_end).DeepEquivalent())
+ return markup_accumulator.TakeResults();
+
+ first_node = NextPositionOf(visible_start).DeepEquivalent().AnchorNode();
+
+ if (past_end && PositionTemplate<Strategy>::BeforeNode(*first_node)
+ .CompareTo(PositionTemplate<Strategy>::BeforeNode(
+ *past_end)) >= 0) {
+ // This condition hits in editing/pasteboard/copy-display-none.html.
+ return markup_accumulator.TakeResults();
+ }
+ }
+
+ // If there is no the highest node in the selected nodes, |m_lastClosed| can
+ // be #text when its parent is a formatting tag. In this case, #text is
+ // wrapped by <span> tag, but this text should be wrapped by the formatting
+ // tag. See http://crbug.com/634482
+ bool should_append_parent_tag = false;
+ if (!last_closed_) {
+ last_closed_ =
+ StyledMarkupTraverser<Strategy>().Traverse(first_node, past_end);
+ if (last_closed_ && last_closed_->IsTextNode() &&
+ IsPresentationalHTMLElement(last_closed_->parentNode())) {
+ last_closed_ = last_closed_->parentElement();
+ should_append_parent_tag = true;
+ }
+ }
+
+ StyledMarkupTraverser<Strategy> traverser(&markup_accumulator, last_closed_);
+ Node* last_closed = traverser.Traverse(first_node, past_end);
+
+ if (highest_node_to_be_serialized_ && last_closed) {
+ // TODO(hajimehoshi): This is calculated at createMarkupInternal too.
+ Node* common_ancestor = Strategy::CommonAncestor(
+ *start_.ComputeContainerNode(), *end_.ComputeContainerNode());
+ DCHECK(common_ancestor);
+ HTMLBodyElement* body = ToHTMLBodyElement(EnclosingElementWithTag(
+ Position::FirstPositionInNode(*common_ancestor), bodyTag));
+ HTMLBodyElement* fully_selected_root = nullptr;
+ // FIXME: Do this for all fully selected blocks, not just the body.
+ if (body && AreSameRanges(body, start_, end_))
+ fully_selected_root = body;
+
+ // Also include all of the ancestors of lastClosed up to this special
+ // ancestor.
+ // FIXME: What is ancestor?
+ for (ContainerNode* ancestor = Strategy::Parent(*last_closed); ancestor;
+ ancestor = Strategy::Parent(*ancestor)) {
+ if (ancestor == fully_selected_root &&
+ !markup_accumulator.ShouldConvertBlocksToInlines()) {
+ EditingStyle* fully_selected_root_style =
+ StyleFromMatchedRulesAndInlineDecl(fully_selected_root);
+
+ // Bring the background attribute over, but not as an attribute because
+ // a background attribute on a div appears to have no effect.
+ if ((!fully_selected_root_style ||
+ !fully_selected_root_style->Style() ||
+ !fully_selected_root_style->Style()->GetPropertyCSSValue(
+ CSSPropertyBackgroundImage)) &&
+ fully_selected_root->hasAttribute(backgroundAttr)) {
+ fully_selected_root_style->Style()->SetProperty(
+ CSSPropertyBackgroundImage,
+ "url('" + fully_selected_root->getAttribute(backgroundAttr) +
+ "')",
+ /* important */ false,
+ fully_selected_root->GetDocument().GetSecureContextMode());
+ }
+
+ if (fully_selected_root_style->Style()) {
+ // Reset the CSS properties to avoid an assertion error in
+ // addStyleMarkup(). This assertion is caused at least when we select
+ // all text of a <body> element whose 'text-decoration' property is
+ // "inherit", and copy it.
+ if (!PropertyMissingOrEqualToNone(fully_selected_root_style->Style(),
+ CSSPropertyTextDecoration))
+ fully_selected_root_style->Style()->SetProperty(
+ CSSPropertyTextDecoration, CSSValueNone);
+ if (!PropertyMissingOrEqualToNone(
+ fully_selected_root_style->Style(),
+ CSSPropertyWebkitTextDecorationsInEffect))
+ fully_selected_root_style->Style()->SetProperty(
+ CSSPropertyWebkitTextDecorationsInEffect, CSSValueNone);
+ markup_accumulator.WrapWithStyleNode(
+ fully_selected_root_style->Style());
+ }
+ } else {
+ EditingStyle* style = traverser.CreateInlineStyleIfNeeded(*ancestor);
+ // Since this node and all the other ancestors are not in the selection
+ // we want styles that affect the exterior of the node not to be not
+ // included. If the node is not fully selected by the range, then we
+ // don't want to keep styles that affect its relationship to the nodes
+ // around it only the ones that affect it and the nodes within it.
+ if (style && style->Style())
+ style->Style()->RemoveProperty(CSSPropertyFloat);
+ traverser.WrapWithNode(*ancestor, style);
+ }
+
+ if (ancestor == highest_node_to_be_serialized_)
+ break;
+ }
+ } else if (should_append_parent_tag) {
+ EditingStyle* style = traverser.CreateInlineStyleIfNeeded(*last_closed_);
+ traverser.WrapWithNode(*ToContainerNode(last_closed_), style);
+ }
+
+ // FIXME: The interchange newline should be placed in the block that it's in,
+ // not after all of the content, unconditionally.
+ if (ShouldAnnotate() && NeedInterchangeNewlineAt(visible_end))
+ markup_accumulator.AppendInterchangeNewline();
+
+ return markup_accumulator.TakeResults();
+}
+
+template <typename Strategy>
+StyledMarkupTraverser<Strategy>::StyledMarkupTraverser()
+ : StyledMarkupTraverser(nullptr, nullptr) {}
+
+template <typename Strategy>
+StyledMarkupTraverser<Strategy>::StyledMarkupTraverser(
+ StyledMarkupAccumulator* accumulator,
+ Node* last_closed)
+ : accumulator_(accumulator),
+ last_closed_(last_closed),
+ wrapping_style_(nullptr) {
+ if (!accumulator_) {
+ DCHECK_EQ(last_closed_, static_cast<decltype(last_closed_)>(nullptr));
+ return;
+ }
+ if (!last_closed_)
+ return;
+ ContainerNode* parent = Strategy::Parent(*last_closed_);
+ if (!parent)
+ return;
+ if (ShouldAnnotate()) {
+ wrapping_style_ =
+ EditingStyleUtilities::CreateWrappingStyleForAnnotatedSerialization(
+ parent);
+ return;
+ }
+ wrapping_style_ =
+ EditingStyleUtilities::CreateWrappingStyleForSerialization(parent);
+}
+
+template <typename Strategy>
+Node* StyledMarkupTraverser<Strategy>::Traverse(Node* start_node,
+ Node* past_end) {
+ HeapVector<Member<ContainerNode>> ancestors_to_close;
+ Node* next;
+ Node* last_closed = nullptr;
+ for (Node* n = start_node; n && n != past_end; n = next) {
+ // If |n| is a selection boundary such as <input>, traverse the child
+ // nodes in the DOM tree instead of the flat tree.
+ if (HandleSelectionBoundary<Strategy>(*n)) {
+ last_closed = StyledMarkupTraverser<EditingStrategy>(accumulator_,
+ last_closed_.Get())
+ .Traverse(n, EditingStrategy::NextSkippingChildren(*n));
+ next = EditingInFlatTreeStrategy::NextSkippingChildren(*n);
+ } else {
+ next = Strategy::Next(*n);
+ if (IsEnclosingBlock(n) && CanHaveChildrenForEditing(n) &&
+ next == past_end) {
+ // Don't write out empty block containers that aren't fully selected.
+ continue;
+ }
+
+ if (!n->GetLayoutObject() &&
+ (!n->IsElementNode() || !ToElement(n)->HasDisplayContentsStyle()) &&
+ !EnclosingElementWithTag(FirstPositionInOrBeforeNode(*n),
+ selectTag)) {
+ next = Strategy::NextSkippingChildren(*n);
+ // Don't skip over pastEnd.
+ if (past_end && Strategy::IsDescendantOf(*past_end, *n))
+ next = past_end;
+ } else {
+ // Add the node to the markup if we're not skipping the descendants
+ AppendStartMarkup(*n);
+
+ // If node has no children, close the tag now.
+ if (Strategy::HasChildren(*n)) {
+ ancestors_to_close.push_back(ToContainerNode(n));
+ continue;
+ }
+ AppendEndMarkup(*n);
+ last_closed = n;
+ }
+ }
+
+ // If we didn't insert open tag and there's no more siblings or we're at the
+ // end of the traversal, take care of ancestors.
+ // FIXME: What happens if we just inserted open tag and reached the end?
+ if (Strategy::NextSibling(*n) && next != past_end)
+ continue;
+
+ // Close up the ancestors.
+ while (!ancestors_to_close.IsEmpty()) {
+ ContainerNode* ancestor = ancestors_to_close.back();
+ DCHECK(ancestor);
+ if (next && next != past_end &&
+ Strategy::IsDescendantOf(*next, *ancestor))
+ break;
+ // Not at the end of the range, close ancestors up to sibling of next
+ // node.
+ AppendEndMarkup(*ancestor);
+ last_closed = ancestor;
+ ancestors_to_close.pop_back();
+ }
+
+ // Surround the currently accumulated markup with markup for ancestors we
+ // never opened as we leave the subtree(s) rooted at those ancestors.
+ ContainerNode* next_parent = next ? Strategy::Parent(*next) : nullptr;
+ if (next == past_end || n == next_parent)
+ continue;
+
+ DCHECK(n);
+ Node* last_ancestor_closed_or_self =
+ (last_closed && Strategy::IsDescendantOf(*n, *last_closed))
+ ? last_closed
+ : n;
+ for (ContainerNode* parent =
+ Strategy::Parent(*last_ancestor_closed_or_self);
+ parent && parent != next_parent; parent = Strategy::Parent(*parent)) {
+ // All ancestors that aren't in the ancestorsToClose list should either be
+ // a) unrendered:
+ if (!parent->GetLayoutObject())
+ continue;
+ // or b) ancestors that we never encountered during a pre-order traversal
+ // starting at startNode:
+ DCHECK(start_node);
+ DCHECK(Strategy::IsDescendantOf(*start_node, *parent));
+ EditingStyle* style = CreateInlineStyleIfNeeded(*parent);
+ WrapWithNode(*parent, style);
+ last_closed = parent;
+ }
+ }
+
+ return last_closed;
+}
+
+template <typename Strategy>
+bool StyledMarkupTraverser<Strategy>::NeedsInlineStyle(const Element& element) {
+ if (!element.IsHTMLElement())
+ return false;
+ if (ShouldAnnotate())
+ return true;
+ return ShouldConvertBlocksToInlines() && IsEnclosingBlock(&element);
+}
+
+template <typename Strategy>
+void StyledMarkupTraverser<Strategy>::WrapWithNode(ContainerNode& node,
+ EditingStyle* style) {
+ if (!accumulator_)
+ return;
+ StringBuilder markup;
+ if (node.IsDocumentNode()) {
+ MarkupFormatter::AppendXMLDeclaration(markup, ToDocument(node));
+ accumulator_->PushMarkup(markup.ToString());
+ return;
+ }
+ if (!node.IsElementNode())
+ return;
+ Element& element = ToElement(node);
+ if (ShouldApplyWrappingStyle(element) || NeedsInlineStyle(element))
+ accumulator_->AppendElementWithInlineStyle(markup, element, style);
+ else
+ accumulator_->AppendElement(markup, element);
+ accumulator_->PushMarkup(markup.ToString());
+ accumulator_->AppendEndTag(ToElement(node));
+}
+
+template <typename Strategy>
+EditingStyle* StyledMarkupTraverser<Strategy>::CreateInlineStyleIfNeeded(
+ Node& node) {
+ if (!accumulator_)
+ return nullptr;
+ if (!node.IsElementNode())
+ return nullptr;
+ EditingStyle* inline_style = CreateInlineStyle(ToElement(node));
+ if (ShouldConvertBlocksToInlines() && IsEnclosingBlock(&node))
+ inline_style->ForceInline();
+ return inline_style;
+}
+
+template <typename Strategy>
+void StyledMarkupTraverser<Strategy>::AppendStartMarkup(Node& node) {
+ if (!accumulator_)
+ return;
+ switch (node.getNodeType()) {
+ case Node::kTextNode: {
+ Text& text = ToText(node);
+ if (text.parentElement() && IsHTMLTextAreaElement(text.parentElement())) {
+ accumulator_->AppendText(text);
+ break;
+ }
+ EditingStyle* inline_style = nullptr;
+ if (ShouldApplyWrappingStyle(text)) {
+ inline_style = wrapping_style_->Copy();
+ // FIXME: <rdar://problem/5371536> Style rules that match pasted content
+ // can change its appearance.
+ // Make sure spans are inline style in paste side e.g. span { display:
+ // block }.
+ inline_style->ForceInline();
+ // FIXME: Should this be included in forceInline?
+ inline_style->Style()->SetProperty(CSSPropertyFloat, CSSValueNone);
+ }
+ accumulator_->AppendTextWithInlineStyle(text, inline_style);
+ break;
+ }
+ case Node::kElementNode: {
+ Element& element = ToElement(node);
+ if ((element.IsHTMLElement() && ShouldAnnotate()) ||
+ ShouldApplyWrappingStyle(element)) {
+ EditingStyle* inline_style = CreateInlineStyle(element);
+ accumulator_->AppendElementWithInlineStyle(element, inline_style);
+ break;
+ }
+ accumulator_->AppendElement(element);
+ break;
+ }
+ default:
+ accumulator_->AppendStartMarkup(node);
+ break;
+ }
+}
+
+template <typename Strategy>
+void StyledMarkupTraverser<Strategy>::AppendEndMarkup(Node& node) {
+ if (!accumulator_ || !node.IsElementNode())
+ return;
+ accumulator_->AppendEndTag(ToElement(node));
+}
+
+template <typename Strategy>
+bool StyledMarkupTraverser<Strategy>::ShouldApplyWrappingStyle(
+ const Node& node) const {
+ return last_closed_ &&
+ Strategy::Parent(*last_closed_) == Strategy::Parent(node) &&
+ wrapping_style_ && wrapping_style_->Style();
+}
+
+template <typename Strategy>
+EditingStyle* StyledMarkupTraverser<Strategy>::CreateInlineStyle(
+ Element& element) {
+ EditingStyle* inline_style = nullptr;
+
+ if (ShouldApplyWrappingStyle(element)) {
+ inline_style = wrapping_style_->Copy();
+ inline_style->RemovePropertiesInElementDefaultStyle(&element);
+ inline_style->RemoveStyleConflictingWithStyleOfElement(&element);
+ } else {
+ inline_style = EditingStyle::Create();
+ }
+
+ if (element.IsStyledElement() && element.InlineStyle())
+ inline_style->OverrideWithStyle(element.InlineStyle());
+
+ if (element.IsHTMLElement() && ShouldAnnotate())
+ inline_style->MergeStyleFromRulesForSerialization(&ToHTMLElement(element));
+
+ return inline_style;
+}
+
+template class StyledMarkupSerializer<EditingStrategy>;
+template class StyledMarkupSerializer<EditingInFlatTreeStrategy>;
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_serializer.h b/chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_serializer.h
new file mode 100644
index 00000000000..7c5472768cf
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_serializer.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ * Copyright (C) 2008, 2009, 2010, 2011 Google Inc. All rights reserved.
+ * Copyright (C) 2011 Igalia S.L.
+ * Copyright (C) 2011 Motorola Mobility. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_STYLED_MARKUP_SERIALIZER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_STYLED_MARKUP_SERIALIZER_H_
+
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/core/editing/editing_strategy.h"
+#include "third_party/blink/renderer/core/editing/editing_style.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/serializers/styled_markup_accumulator.h"
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+
+namespace blink {
+
+template <typename Strategy>
+class StyledMarkupSerializer final {
+ STACK_ALLOCATED();
+
+ public:
+ StyledMarkupSerializer(EAbsoluteURLs,
+ EAnnotateForInterchange,
+ const PositionTemplate<Strategy>& start,
+ const PositionTemplate<Strategy>& end,
+ Node* highest_node_to_be_serialized,
+ ConvertBlocksToInlines);
+
+ String CreateMarkup();
+
+ private:
+ bool ShouldAnnotate() const {
+ return should_annotate_ == kAnnotateForInterchange;
+ }
+
+ const PositionTemplate<Strategy> start_;
+ const PositionTemplate<Strategy> end_;
+ const EAbsoluteURLs should_resolve_urls_;
+ const EAnnotateForInterchange should_annotate_;
+ const Member<Node> highest_node_to_be_serialized_;
+ const ConvertBlocksToInlines convert_blocks_to_inlines_;
+ Member<Node> last_closed_;
+ Member<EditingStyle> wrapping_style_;
+};
+
+extern template class StyledMarkupSerializer<EditingStrategy>;
+extern template class StyledMarkupSerializer<EditingInFlatTreeStrategy>;
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_STYLED_MARKUP_SERIALIZER_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_serializer_test.cc b/chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_serializer_test.cc
new file mode 100644
index 00000000000..25c3c356383
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/serializers/styled_markup_serializer_test.cc
@@ -0,0 +1,306 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/serializers/styled_markup_serializer.h"
+
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+// Returns the first mismatching index in |input1| and |input2|.
+static size_t Mismatch(const std::string& input1, const std::string& input2) {
+ size_t index = 0;
+ for (auto char1 : input1) {
+ if (index == input2.size() || char1 != input2[index])
+ return index;
+ ++index;
+ }
+ return input1.size();
+}
+
+// This is smoke test of |StyledMarkupSerializer|. Full testing will be done
+// in layout tests.
+class StyledMarkupSerializerTest : public EditingTestBase {
+ protected:
+ template <typename Strategy>
+ std::string Serialize(EAnnotateForInterchange = kDoNotAnnotateForInterchange);
+
+ template <typename Strategy>
+ std::string SerializePart(
+ const PositionTemplate<Strategy>& start,
+ const PositionTemplate<Strategy>& end,
+ EAnnotateForInterchange = kDoNotAnnotateForInterchange);
+};
+
+template <typename Strategy>
+std::string StyledMarkupSerializerTest::Serialize(
+ EAnnotateForInterchange should_annotate) {
+ PositionTemplate<Strategy> start = PositionTemplate<Strategy>(
+ GetDocument().body(), PositionAnchorType::kBeforeChildren);
+ PositionTemplate<Strategy> end = PositionTemplate<Strategy>(
+ GetDocument().body(), PositionAnchorType::kAfterChildren);
+ return CreateMarkup(start, end, should_annotate).Utf8().data();
+}
+
+template <typename Strategy>
+std::string StyledMarkupSerializerTest::SerializePart(
+ const PositionTemplate<Strategy>& start,
+ const PositionTemplate<Strategy>& end,
+ EAnnotateForInterchange should_annotate) {
+ return CreateMarkup(start, end, should_annotate).Utf8().data();
+}
+
+TEST_F(StyledMarkupSerializerTest, TextOnly) {
+ const char* body_content = "Hello world!";
+ SetBodyContent(body_content);
+ const char* expected_result =
+ "<span style=\"display: inline !important; float: none;\">Hello "
+ "world!</span>";
+ EXPECT_EQ(expected_result, Serialize<EditingStrategy>());
+ EXPECT_EQ(expected_result, Serialize<EditingInFlatTreeStrategy>());
+}
+
+TEST_F(StyledMarkupSerializerTest, BlockFormatting) {
+ const char* body_content = "<div>Hello world!</div>";
+ SetBodyContent(body_content);
+ EXPECT_EQ(body_content, Serialize<EditingStrategy>());
+ EXPECT_EQ(body_content, Serialize<EditingInFlatTreeStrategy>());
+}
+
+TEST_F(StyledMarkupSerializerTest, FormControlInput) {
+ const char* body_content = "<input value='foo'>";
+ SetBodyContent(body_content);
+ const char* expected_result = "<input value=\"foo\">";
+ EXPECT_EQ(expected_result, Serialize<EditingStrategy>());
+ EXPECT_EQ(expected_result, Serialize<EditingInFlatTreeStrategy>());
+}
+
+TEST_F(StyledMarkupSerializerTest, FormControlInputRange) {
+ const char* body_content = "<input type=range>";
+ SetBodyContent(body_content);
+ const char* expected_result = "<input type=\"range\">";
+ EXPECT_EQ(expected_result, Serialize<EditingStrategy>());
+ EXPECT_EQ(expected_result, Serialize<EditingInFlatTreeStrategy>());
+}
+
+TEST_F(StyledMarkupSerializerTest, FormControlSelect) {
+ const char* body_content =
+ "<select><option value=\"1\">one</option><option "
+ "value=\"2\">two</option></select>";
+ SetBodyContent(body_content);
+ EXPECT_EQ(body_content, Serialize<EditingStrategy>());
+ EXPECT_EQ(body_content, Serialize<EditingInFlatTreeStrategy>());
+}
+
+TEST_F(StyledMarkupSerializerTest, FormControlTextArea) {
+ const char* body_content = "<textarea>foo bar</textarea>";
+ SetBodyContent(body_content);
+ const char* expected_result = "<textarea></textarea>";
+ EXPECT_EQ(expected_result, Serialize<EditingStrategy>())
+ << "contents of TEXTAREA element should not be appeared.";
+ EXPECT_EQ(expected_result, Serialize<EditingInFlatTreeStrategy>());
+}
+
+TEST_F(StyledMarkupSerializerTest, HeadingFormatting) {
+ const char* body_content = "<h4>Hello world!</h4>";
+ SetBodyContent(body_content);
+ EXPECT_EQ(body_content, Serialize<EditingStrategy>());
+ EXPECT_EQ(body_content, Serialize<EditingInFlatTreeStrategy>());
+}
+
+TEST_F(StyledMarkupSerializerTest, InlineFormatting) {
+ const char* body_content = "<b>Hello world!</b>";
+ SetBodyContent(body_content);
+ EXPECT_EQ(body_content, Serialize<EditingStrategy>());
+ EXPECT_EQ(body_content, Serialize<EditingInFlatTreeStrategy>());
+}
+
+TEST_F(StyledMarkupSerializerTest, Mixed) {
+ const char* body_content = "<i>foo<b>bar</b>baz</i>";
+ SetBodyContent(body_content);
+ EXPECT_EQ(body_content, Serialize<EditingStrategy>());
+ EXPECT_EQ(body_content, Serialize<EditingInFlatTreeStrategy>());
+}
+
+TEST_F(StyledMarkupSerializerTest, ShadowTreeDistributeOrder) {
+ const char* body_content =
+ "<p id=\"host\">00<b id=\"one\">11</b><b id=\"two\">22</b>33</p>";
+ const char* shadow_content =
+ "<a><content select=#two></content><content select=#one></content></a>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+ EXPECT_EQ("<p id=\"host\"><b id=\"one\">11</b><b id=\"two\">22</b></p>",
+ Serialize<EditingStrategy>())
+ << "00 and 33 aren't appeared since they aren't distributed.";
+ EXPECT_EQ(
+ "<p id=\"host\"><a><b id=\"two\">22</b><b id=\"one\">11</b></a></p>",
+ Serialize<EditingInFlatTreeStrategy>())
+ << "00 and 33 aren't appeared since they aren't distributed.";
+}
+
+TEST_F(StyledMarkupSerializerTest, ShadowTreeInput) {
+ const char* body_content =
+ "<p id=\"host\">00<b id=\"one\">11</b><b id=\"two\"><input "
+ "value=\"22\"></b>33</p>";
+ const char* shadow_content =
+ "<a><content select=#two></content><content select=#one></content></a>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+ EXPECT_EQ(
+ "<p id=\"host\"><b id=\"one\">11</b><b id=\"two\"><input "
+ "value=\"22\"></b></p>",
+ Serialize<EditingStrategy>())
+ << "00 and 33 aren't appeared since they aren't distributed.";
+ EXPECT_EQ(
+ "<p id=\"host\"><a><b id=\"two\"><input value=\"22\"></b><b "
+ "id=\"one\">11</b></a></p>",
+ Serialize<EditingInFlatTreeStrategy>())
+ << "00 and 33 aren't appeared since they aren't distributed.";
+}
+
+TEST_F(StyledMarkupSerializerTest, ShadowTreeNested) {
+ const char* body_content =
+ "<p id=\"host\">00<b id=\"one\">11</b><b id=\"two\">22</b>33</p>";
+ const char* shadow_content1 =
+ "<a><content select=#two></content><b id=host2></b><content "
+ "select=#one></content></a>";
+ const char* shadow_content2 = "NESTED";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root1 = SetShadowContent(shadow_content1, "host");
+ CreateShadowRootForElementWithIDAndSetInnerHTML(*shadow_root1, "host2",
+ shadow_content2);
+
+ EXPECT_EQ("<p id=\"host\"><b id=\"one\">11</b><b id=\"two\">22</b></p>",
+ Serialize<EditingStrategy>())
+ << "00 and 33 aren't appeared since they aren't distributed.";
+ EXPECT_EQ(
+ "<p id=\"host\"><a><b id=\"two\">22</b><b id=\"host2\">NESTED</b><b "
+ "id=\"one\">11</b></a></p>",
+ Serialize<EditingInFlatTreeStrategy>())
+ << "00 and 33 aren't appeared since they aren't distributed.";
+}
+
+TEST_F(StyledMarkupSerializerTest, ShadowTreeInterchangedNewline) {
+ const char* body_content = "<a id=host><b id=one>1</b></a>";
+ const char* shadow_content = "<content select=#one></content><div><br></div>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ std::string result_from_dom_tree =
+ Serialize<EditingStrategy>(kAnnotateForInterchange);
+ std::string result_from_flat_tree =
+ Serialize<EditingInFlatTreeStrategy>(kAnnotateForInterchange);
+ size_t mismatched_index =
+ Mismatch(result_from_dom_tree, result_from_flat_tree);
+
+ // Note: We check difference between DOM tree result and flat tree
+ // result, because results contain "style" attribute and this test
+ // doesn't care about actual value of "style" attribute.
+ EXPECT_EQ("/a>", result_from_dom_tree.substr(mismatched_index));
+ EXPECT_EQ("div><br></div></a><br class=\"Apple-interchange-newline\">",
+ result_from_flat_tree.substr(mismatched_index));
+}
+
+TEST_F(StyledMarkupSerializerTest, StyleDisplayNone) {
+ const char* body_content = "<b>00<i style='display:none'>11</i>22</b>";
+ SetBodyContent(body_content);
+ const char* expected_result = "<b>0022</b>";
+ EXPECT_EQ(expected_result, Serialize<EditingStrategy>());
+ EXPECT_EQ(expected_result, Serialize<EditingInFlatTreeStrategy>());
+}
+
+TEST_F(StyledMarkupSerializerTest, StyleDisplayNoneAndNewLines) {
+ const char* body_content = "<div style='display:none'>11</div>\n\n";
+ SetBodyContent(body_content);
+ EXPECT_EQ("", Serialize<EditingStrategy>());
+ EXPECT_EQ("", Serialize<EditingInFlatTreeStrategy>());
+}
+
+TEST_F(StyledMarkupSerializerTest, ShadowTreeStyle) {
+ const char* body_content =
+ "<p id='host' style='color: red'><span style='font-weight: bold;'><span "
+ "id='one'>11</span></span></p>\n";
+ SetBodyContent(body_content);
+ Element* one = GetDocument().getElementById("one");
+ Text* text = ToText(one->firstChild());
+ Position start_dom(text, 0);
+ Position end_dom(text, 2);
+ const std::string& serialized_dom = SerializePart<EditingStrategy>(
+ start_dom, end_dom, kAnnotateForInterchange);
+
+ body_content =
+ "<p id='host' style='color: red'>00<span id='one'>11</span>22</p>\n";
+ const char* shadow_content =
+ "<span style='font-weight: bold'><content select=#one></content></span>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+ one = GetDocument().getElementById("one");
+ text = ToText(one->firstChild());
+ PositionInFlatTree start_ict(text, 0);
+ PositionInFlatTree end_ict(text, 2);
+ const std::string& serialized_ict = SerializePart<EditingInFlatTreeStrategy>(
+ start_ict, end_ict, kAnnotateForInterchange);
+
+ EXPECT_EQ(serialized_dom, serialized_ict);
+}
+
+TEST_F(StyledMarkupSerializerTest, AcrossShadow) {
+ const char* body_content =
+ "<p id='host1'>[<span id='one'>11</span>]</p><p id='host2'>[<span "
+ "id='two'>22</span>]</p>";
+ SetBodyContent(body_content);
+ Element* one = GetDocument().getElementById("one");
+ Element* two = GetDocument().getElementById("two");
+ Position start_dom(ToText(one->firstChild()), 0);
+ Position end_dom(ToText(two->firstChild()), 2);
+ const std::string& serialized_dom = SerializePart<EditingStrategy>(
+ start_dom, end_dom, kAnnotateForInterchange);
+
+ body_content =
+ "<p id='host1'><span id='one'>11</span></p><p id='host2'><span "
+ "id='two'>22</span></p>";
+ const char* shadow_content1 = "[<content select=#one></content>]";
+ const char* shadow_content2 = "[<content select=#two></content>]";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content1, "host1");
+ SetShadowContent(shadow_content2, "host2");
+ one = GetDocument().getElementById("one");
+ two = GetDocument().getElementById("two");
+ PositionInFlatTree start_ict(ToText(one->firstChild()), 0);
+ PositionInFlatTree end_ict(ToText(two->firstChild()), 2);
+ const std::string& serialized_ict = SerializePart<EditingInFlatTreeStrategy>(
+ start_ict, end_ict, kAnnotateForInterchange);
+
+ EXPECT_EQ(serialized_dom, serialized_ict);
+}
+
+TEST_F(StyledMarkupSerializerTest, AcrossInvisibleElements) {
+ const char* body_content =
+ "<span id='span1' style='display: none'>11</span><span id='span2' "
+ "style='display: none'>22</span>";
+ SetBodyContent(body_content);
+ Element* span1 = GetDocument().getElementById("span1");
+ Element* span2 = GetDocument().getElementById("span2");
+ Position start_dom = Position::FirstPositionInNode(*span1);
+ Position end_dom = Position::LastPositionInNode(*span2);
+ EXPECT_EQ("", SerializePart<EditingStrategy>(start_dom, end_dom));
+ PositionInFlatTree start_ict =
+ PositionInFlatTree::FirstPositionInNode(*span1);
+ PositionInFlatTree end_ict = PositionInFlatTree::LastPositionInNode(*span2);
+ EXPECT_EQ("", SerializePart<EditingInFlatTreeStrategy>(start_ict, end_ict));
+}
+
+TEST_F(StyledMarkupSerializerTest, DisplayContentsStyle) {
+ const char* body_content = "1<span style='display: contents'>2</span>3";
+ const char* expected_result =
+ "<span style=\"display: inline !important; float: none;\">1</span><span "
+ "style=\"display: contents;\">2</span><span style=\"display: inline "
+ "!important; float: none;\">3</span>";
+ SetBodyContent(body_content);
+ EXPECT_EQ(expected_result, Serialize<EditingStrategy>());
+ EXPECT_EQ(expected_result, Serialize<EditingInFlatTreeStrategy>());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/serializers/text_offset.cc b/chromium/third_party/blink/renderer/core/editing/serializers/text_offset.cc
new file mode 100644
index 00000000000..4d2dc102d92
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/serializers/text_offset.cc
@@ -0,0 +1,25 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/serializers/text_offset.h"
+
+#include "third_party/blink/renderer/core/dom/text.h"
+
+namespace blink {
+
+TextOffset::TextOffset() : offset_(0) {}
+
+TextOffset::TextOffset(Text* text, int offset) : text_(text), offset_(offset) {}
+
+TextOffset::TextOffset(const TextOffset& other) = default;
+
+bool TextOffset::IsNull() const {
+ return !text_;
+}
+
+bool TextOffset::IsNotNull() const {
+ return text_;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/serializers/text_offset.h b/chromium/third_party/blink/renderer/core/editing/serializers/text_offset.h
new file mode 100644
index 00000000000..f5367f7190a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/serializers/text_offset.h
@@ -0,0 +1,36 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_TEXT_OFFSET_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_TEXT_OFFSET_H_
+
+#include "third_party/blink/public/platform/platform.h"
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+
+namespace blink {
+
+class Text;
+
+class TextOffset {
+ STACK_ALLOCATED();
+
+ public:
+ TextOffset();
+ TextOffset(Text*, int);
+ TextOffset(const TextOffset&);
+
+ Text* GetText() const { return text_.Get(); }
+ int Offset() const { return offset_; }
+
+ bool IsNull() const;
+ bool IsNotNull() const;
+
+ private:
+ Member<Text> text_;
+ int offset_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SERIALIZERS_TEXT_OFFSET_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/set_selection_options.cc b/chromium/third_party/blink/renderer/core/editing/set_selection_options.cc
new file mode 100644
index 00000000000..fb288805ebd
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/set_selection_options.cc
@@ -0,0 +1,82 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/set_selection_options.h"
+
+namespace blink {
+
+SetSelectionOptions::SetSelectionOptions(const SetSelectionOptions& other) =
+ default;
+SetSelectionOptions::SetSelectionOptions() = default;
+SetSelectionOptions::Builder::Builder() = default;
+
+SetSelectionOptions::Builder::Builder(const SetSelectionOptions& data) {
+ data_ = data;
+}
+
+SetSelectionOptions SetSelectionOptions::Builder::Build() const {
+ return data_;
+}
+
+SetSelectionOptions::Builder&
+SetSelectionOptions::Builder::SetCursorAlignOnScroll(
+ CursorAlignOnScroll align) {
+ data_.cursor_align_on_scroll_ = align;
+ return *this;
+}
+
+SetSelectionOptions::Builder&
+SetSelectionOptions::Builder::SetDoNotClearStrategy(bool new_value) {
+ data_.do_not_clear_strategy_ = new_value;
+ return *this;
+}
+
+SetSelectionOptions::Builder& SetSelectionOptions::Builder::SetDoNotSetFocus(
+ bool new_value) {
+ data_.do_not_set_focus_ = new_value;
+ return *this;
+}
+
+SetSelectionOptions::Builder& SetSelectionOptions::Builder::SetGranularity(
+ TextGranularity new_value) {
+ data_.granularity_ = new_value;
+ return *this;
+}
+
+SetSelectionOptions::Builder& SetSelectionOptions::Builder::SetSetSelectionBy(
+ SetSelectionBy new_value) {
+ data_.set_selection_by_ = new_value;
+ return *this;
+}
+
+SetSelectionOptions::Builder&
+SetSelectionOptions::Builder::SetShouldClearTypingStyle(bool new_value) {
+ data_.should_clear_typing_style_ = new_value;
+ return *this;
+}
+
+SetSelectionOptions::Builder&
+SetSelectionOptions::Builder::SetShouldCloseTyping(bool new_value) {
+ data_.should_close_typing_ = new_value;
+ return *this;
+}
+
+SetSelectionOptions::Builder& SetSelectionOptions::Builder::SetShouldShowHandle(
+ bool new_value) {
+ data_.should_show_handle_ = new_value;
+ return *this;
+}
+
+SetSelectionOptions::Builder&
+SetSelectionOptions::Builder::SetShouldShrinkNextTap(bool new_value) {
+ data_.should_shrink_next_tap_ = new_value;
+ return *this;
+}
+
+SetSelectionOptions::Builder& SetSelectionOptions::Builder::SetIsDirectional(
+ bool new_value) {
+ data_.is_directional_ = new_value;
+ return *this;
+}
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/set_selection_options.h b/chromium/third_party/blink/renderer/core/editing/set_selection_options.h
new file mode 100644
index 00000000000..9cbb5824a57
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/set_selection_options.h
@@ -0,0 +1,83 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SET_SELECTION_OPTIONS_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SET_SELECTION_OPTIONS_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/text_granularity.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+
+namespace blink {
+
+enum class CursorAlignOnScroll { kIfNeeded, kAlways };
+enum class SetSelectionBy { kSystem = 0, kUser = 1 };
+
+// This class represents parameters of
+// |FrameSelection::SetSelectionAndEndTyping()|.
+class CORE_EXPORT SetSelectionOptions final {
+ STACK_ALLOCATED();
+
+ public:
+ class CORE_EXPORT Builder;
+
+ SetSelectionOptions(const SetSelectionOptions&);
+ SetSelectionOptions();
+
+ CursorAlignOnScroll GetCursorAlignOnScroll() const {
+ return cursor_align_on_scroll_;
+ }
+ bool DoNotClearStrategy() const { return do_not_clear_strategy_; }
+ bool DoNotSetFocus() const { return do_not_set_focus_; }
+ TextGranularity Granularity() const { return granularity_; }
+ SetSelectionBy GetSetSelectionBy() const { return set_selection_by_; }
+ bool ShouldClearTypingStyle() const { return should_clear_typing_style_; }
+ bool ShouldCloseTyping() const { return should_close_typing_; }
+ bool ShouldShowHandle() const { return should_show_handle_; }
+ bool ShouldShrinkNextTap() const { return should_shrink_next_tap_; }
+ bool IsDirectional() const { return is_directional_; }
+
+ private:
+ CursorAlignOnScroll cursor_align_on_scroll_ = CursorAlignOnScroll::kIfNeeded;
+ bool do_not_clear_strategy_ = false;
+ bool do_not_set_focus_ = false;
+ TextGranularity granularity_ = TextGranularity::kCharacter;
+ SetSelectionBy set_selection_by_ = SetSelectionBy::kSystem;
+ bool should_clear_typing_style_ = false;
+ bool should_close_typing_ = false;
+ bool should_show_handle_ = false;
+ bool should_shrink_next_tap_ = false;
+ bool is_directional_ = false;
+};
+
+// This class is used for building |SelectionData| object.
+class CORE_EXPORT SetSelectionOptions::Builder final {
+ STACK_ALLOCATED();
+
+ public:
+ explicit Builder(const SetSelectionOptions&);
+ Builder();
+
+ SetSelectionOptions Build() const;
+
+ Builder& SetCursorAlignOnScroll(CursorAlignOnScroll);
+ Builder& SetDoNotClearStrategy(bool);
+ Builder& SetDoNotSetFocus(bool);
+ Builder& SetGranularity(TextGranularity);
+ Builder& SetSetSelectionBy(SetSelectionBy);
+ Builder& SetShouldClearTypingStyle(bool);
+ Builder& SetShouldCloseTyping(bool);
+ Builder& SetShouldShowHandle(bool);
+ Builder& SetShouldShrinkNextTap(bool);
+ Builder& SetIsDirectional(bool);
+
+ private:
+ SetSelectionOptions data_;
+
+ DISALLOW_COPY_AND_ASSIGN(Builder);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SET_SELECTION_OPTIONS_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/set_selection_options_test.cc b/chromium/third_party/blink/renderer/core/editing/set_selection_options_test.cc
new file mode 100644
index 00000000000..00b13d505ef
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/set_selection_options_test.cc
@@ -0,0 +1,54 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/set_selection_options.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class SetSelectionOptionsTest : public EditingTestBase {};
+
+TEST_F(SetSelectionOptionsTest, DefaultValues) {
+ SetSelectionOptions options = SetSelectionOptions::Builder().Build();
+
+ EXPECT_EQ(CursorAlignOnScroll::kIfNeeded, options.GetCursorAlignOnScroll());
+ EXPECT_FALSE(options.DoNotClearStrategy());
+ EXPECT_FALSE(options.DoNotSetFocus());
+ EXPECT_EQ(TextGranularity::kCharacter, options.Granularity());
+ EXPECT_EQ(SetSelectionBy::kSystem, options.GetSetSelectionBy());
+ EXPECT_FALSE(options.ShouldClearTypingStyle());
+ EXPECT_FALSE(options.ShouldCloseTyping());
+ EXPECT_FALSE(options.ShouldShowHandle());
+ EXPECT_FALSE(options.ShouldShrinkNextTap());
+}
+
+TEST_F(SetSelectionOptionsTest, Setter) {
+ SetSelectionOptions::Builder builder;
+
+ builder.SetCursorAlignOnScroll(CursorAlignOnScroll::kAlways)
+ .SetDoNotClearStrategy(true)
+ .SetDoNotSetFocus(true)
+ .SetGranularity(TextGranularity::kDocumentBoundary)
+ .SetSetSelectionBy(SetSelectionBy::kUser)
+ .SetShouldClearTypingStyle(true)
+ .SetShouldCloseTyping(true)
+ .SetShouldShowHandle(true)
+ .SetShouldShrinkNextTap(true);
+
+ SetSelectionOptions options = builder.Build();
+
+ EXPECT_EQ(CursorAlignOnScroll::kAlways, options.GetCursorAlignOnScroll());
+ EXPECT_TRUE(options.DoNotClearStrategy());
+ EXPECT_TRUE(options.DoNotSetFocus());
+ EXPECT_EQ(TextGranularity::kDocumentBoundary, options.Granularity());
+ EXPECT_EQ(SetSelectionBy::kUser, options.GetSetSelectionBy());
+ EXPECT_TRUE(options.ShouldClearTypingStyle());
+ EXPECT_TRUE(options.ShouldCloseTyping());
+ EXPECT_TRUE(options.ShouldShowHandle());
+ EXPECT_TRUE(options.ShouldShrinkNextTap());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/cold_mode_spell_check_requester.cc b/chromium/third_party/blink/renderer/core/editing/spellcheck/cold_mode_spell_check_requester.cc
new file mode 100644
index 00000000000..26052ae11e2
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/cold_mode_spell_check_requester.cc
@@ -0,0 +1,182 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/spellcheck/cold_mode_spell_check_requester.h"
+
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/idle_deadline.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/iterators/character_iterator.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_check_requester.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_checker.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
+
+namespace blink {
+
+namespace {
+
+const int kColdModeChunkSize = 16384; // in UTF16 code units
+const int kInvalidLength = -1;
+const int kInvalidChunkIndex = -1;
+
+bool ShouldCheckNode(const Node& node) {
+ if (!node.IsElementNode())
+ return false;
+ // TODO(editing-dev): Make |Position| constructors take const parameters.
+ const Position& position = Position::FirstPositionInNode(node);
+ if (!IsEditablePosition(position))
+ return false;
+ return SpellChecker::IsSpellCheckingEnabledAt(position);
+}
+
+} // namespace
+
+// static
+ColdModeSpellCheckRequester* ColdModeSpellCheckRequester::Create(
+ LocalFrame& frame) {
+ return new ColdModeSpellCheckRequester(frame);
+}
+
+void ColdModeSpellCheckRequester::Trace(blink::Visitor* visitor) {
+ visitor->Trace(frame_);
+ visitor->Trace(next_node_);
+ visitor->Trace(current_root_editable_);
+ visitor->Trace(current_chunk_start_);
+}
+
+ColdModeSpellCheckRequester::ColdModeSpellCheckRequester(LocalFrame& frame)
+ : frame_(frame),
+ last_checked_dom_tree_version_(0),
+ needs_more_invocation_for_testing_(false) {}
+
+bool ColdModeSpellCheckRequester::FullDocumentChecked() const {
+ if (needs_more_invocation_for_testing_) {
+ needs_more_invocation_for_testing_ = false;
+ return false;
+ }
+ return !next_node_;
+}
+
+SpellCheckRequester& ColdModeSpellCheckRequester::GetSpellCheckRequester()
+ const {
+ return GetFrame().GetSpellChecker().GetSpellCheckRequester();
+}
+
+void ColdModeSpellCheckRequester::Invoke(IdleDeadline* deadline) {
+ TRACE_EVENT0("blink", "ColdModeSpellCheckRequester::invoke");
+
+ Node* body = GetFrame().GetDocument()->body();
+ if (!body) {
+ ResetCheckingProgress();
+ last_checked_dom_tree_version_ = GetFrame().GetDocument()->DomTreeVersion();
+ return;
+ }
+
+ // TODO(xiaochengh): Figure out if this has any performance impact.
+ GetFrame().GetDocument()->UpdateStyleAndLayout();
+
+ if (last_checked_dom_tree_version_ !=
+ GetFrame().GetDocument()->DomTreeVersion())
+ ResetCheckingProgress();
+
+ while (next_node_ && deadline->timeRemaining() > 0)
+ Step();
+ last_checked_dom_tree_version_ = GetFrame().GetDocument()->DomTreeVersion();
+}
+
+void ColdModeSpellCheckRequester::ResetCheckingProgress() {
+ next_node_ = GetFrame().GetDocument()->body();
+ current_root_editable_ = nullptr;
+ current_full_length_ = kInvalidLength;
+ current_chunk_index_ = kInvalidChunkIndex;
+ current_chunk_start_ = Position();
+}
+
+void ColdModeSpellCheckRequester::Step() {
+ if (!next_node_)
+ return;
+
+ if (!current_root_editable_) {
+ SearchForNextRootEditable();
+ return;
+ }
+
+ if (current_full_length_ == kInvalidLength) {
+ InitializeForCurrentRootEditable();
+ return;
+ }
+
+ DCHECK(current_chunk_index_ != kInvalidChunkIndex);
+ RequestCheckingForNextChunk();
+}
+
+void ColdModeSpellCheckRequester::SearchForNextRootEditable() {
+ // TODO(xiaochengh): Figure out if such small steps, which result in frequent
+ // calls of |timeRemaining()|, have any performance impact. We might not want
+ // to check remaining time so frequently in a page with millions of nodes.
+
+ if (ShouldCheckNode(*next_node_)) {
+ current_root_editable_ = ToElement(next_node_);
+ return;
+ }
+
+ next_node_ =
+ FlatTreeTraversal::Next(*next_node_, GetFrame().GetDocument()->body());
+}
+
+void ColdModeSpellCheckRequester::InitializeForCurrentRootEditable() {
+ const EphemeralRange& full_range =
+ EphemeralRange::RangeOfContents(*current_root_editable_);
+ current_full_length_ = TextIterator::RangeLength(full_range);
+
+ current_chunk_index_ = 0;
+ current_chunk_start_ = full_range.StartPosition();
+}
+
+void ColdModeSpellCheckRequester::RequestCheckingForNextChunk() {
+ // Check the full content if it is short.
+ if (current_full_length_ <= kColdModeChunkSize) {
+ GetSpellCheckRequester().RequestCheckingFor(
+ EphemeralRange::RangeOfContents(*current_root_editable_));
+ FinishCheckingCurrentRootEditable();
+ return;
+ }
+
+ const Position& chunk_end =
+ CalculateCharacterSubrange(
+ EphemeralRange(current_chunk_start_,
+ Position::LastPositionInNode(*current_root_editable_)),
+ 0, kColdModeChunkSize)
+ .EndPosition();
+ if (chunk_end <= current_chunk_start_) {
+ FinishCheckingCurrentRootEditable();
+ return;
+ }
+ const EphemeralRange chunk_range(current_chunk_start_, chunk_end);
+ const EphemeralRange& check_range = ExpandEndToSentenceBoundary(chunk_range);
+ GetSpellCheckRequester().RequestCheckingFor(check_range,
+ current_chunk_index_);
+
+ current_chunk_start_ = check_range.EndPosition();
+ ++current_chunk_index_;
+
+ if (current_chunk_index_ * kColdModeChunkSize >= current_full_length_)
+ FinishCheckingCurrentRootEditable();
+}
+
+void ColdModeSpellCheckRequester::FinishCheckingCurrentRootEditable() {
+ DCHECK_EQ(next_node_, current_root_editable_);
+ next_node_ = FlatTreeTraversal::NextSkippingChildren(
+ *next_node_, GetFrame().GetDocument()->body());
+
+ current_root_editable_ = nullptr;
+ current_full_length_ = kInvalidLength;
+ current_chunk_index_ = kInvalidChunkIndex;
+ current_chunk_start_ = Position();
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/cold_mode_spell_check_requester.h b/chromium/third_party/blink/renderer/core/editing/spellcheck/cold_mode_spell_check_requester.h
new file mode 100644
index 00000000000..d3da55cdadf
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/cold_mode_spell_check_requester.h
@@ -0,0 +1,66 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_COLD_MODE_SPELL_CHECK_REQUESTER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_COLD_MODE_SPELL_CHECK_REQUESTER_H_
+
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+class Element;
+class LocalFrame;
+class IdleDeadline;
+class SpellCheckRequester;
+
+// This class is only supposed to be used by IdleSpellCheckCallback in cold mode
+// invocation. Not to be confused with SpellCheckRequester.
+class ColdModeSpellCheckRequester
+ : public GarbageCollected<ColdModeSpellCheckRequester> {
+ public:
+ static ColdModeSpellCheckRequester* Create(LocalFrame&);
+
+ void SetNeedsMoreInvocationForTesting() {
+ needs_more_invocation_for_testing_ = true;
+ }
+
+ void Invoke(IdleDeadline*);
+ bool FullDocumentChecked() const;
+
+ void Trace(blink::Visitor*);
+
+ private:
+ explicit ColdModeSpellCheckRequester(LocalFrame&);
+
+ LocalFrame& GetFrame() const { return *frame_; }
+ SpellCheckRequester& GetSpellCheckRequester() const;
+
+ // Perform checking task incrementally based on the stored state.
+ void Step();
+
+ void SearchForNextRootEditable();
+ void InitializeForCurrentRootEditable();
+ bool HaveMoreChunksToCheck();
+ void RequestCheckingForNextChunk();
+ void FinishCheckingCurrentRootEditable();
+
+ void ResetCheckingProgress();
+ void ChunkAndRequestFullCheckingFor(const Element&);
+
+ const Member<LocalFrame> frame_;
+ Member<Node> next_node_;
+ Member<Element> current_root_editable_;
+ int current_full_length_;
+ int current_chunk_index_;
+ Position current_chunk_start_;
+ uint64_t last_checked_dom_tree_version_;
+ mutable bool needs_more_invocation_for_testing_;
+
+ DISALLOW_COPY_AND_ASSIGN(ColdModeSpellCheckRequester);
+};
+}
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/hot_mode_spell_check_requester.cc b/chromium/third_party/blink/renderer/core/editing/spellcheck/hot_mode_spell_check_requester.cc
new file mode 100644
index 00000000000..b9731680d1a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/hot_mode_spell_check_requester.cc
@@ -0,0 +1,128 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/spellcheck/hot_mode_spell_check_requester.h"
+
+#include "third_party/blink/renderer/core/editing/commands/composite_edit_command.h"
+#include "third_party/blink/renderer/core/editing/commands/typing_command.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/iterators/backwards_character_iterator.h"
+#include "third_party/blink/renderer/core/editing/iterators/character_iterator.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_check_requester.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_checker.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+
+namespace blink {
+
+namespace {
+
+const int kHotModeCheckAllThreshold = 128;
+const int kHotModeChunkSize = 1024;
+
+EphemeralRange AdjacentWordIfExists(const Position& pos) {
+ const VisiblePosition& visible_pos = CreateVisiblePosition(pos);
+ const VisiblePosition& word_start = PreviousWordPosition(visible_pos);
+ if (word_start.IsNull())
+ return EphemeralRange();
+ const VisiblePosition& word_end = EndOfWord(word_start);
+ if (word_end.IsNull())
+ return EphemeralRange();
+ if (ComparePositions(visible_pos, word_end) > 0)
+ return EphemeralRange();
+ return EphemeralRange(word_start.DeepEquivalent(), word_end.DeepEquivalent());
+}
+
+EphemeralRange CurrentWordIfTypingInPartialWord(const Element& editable) {
+ const LocalFrame& frame = *editable.GetDocument().GetFrame();
+ const SelectionInDOMTree& selection =
+ frame.Selection().GetSelectionInDOMTree();
+ if (!selection.IsCaret())
+ return EphemeralRange();
+ if (RootEditableElementOf(selection.Base()) != &editable)
+ return EphemeralRange();
+
+ CompositeEditCommand* last_command = frame.GetEditor().LastEditCommand();
+ if (!last_command || !last_command->IsTypingCommand())
+ return EphemeralRange();
+ if (!last_command->EndingSelection().IsValidFor(*frame.GetDocument()))
+ return EphemeralRange();
+ if (last_command->EndingSelection().AsSelection() != selection)
+ return EphemeralRange();
+ return AdjacentWordIfExists(selection.Base());
+}
+
+EphemeralRange CalculateHotModeCheckingRange(const Element& editable,
+ const Position& position) {
+ // Check everything in |editable| if its total length is short.
+ const EphemeralRange& full_range = EphemeralRange::RangeOfContents(editable);
+ const int full_length = TextIterator::RangeLength(full_range);
+ // TODO(xiaochengh): There is no need to check if |full_length <= 2|, since
+ // we don't consider two characters as misspelled. However, a lot of layout
+ // tests depend on "zz" as misspelled, which should be changed.
+ if (full_length <= kHotModeCheckAllThreshold)
+ return full_range;
+
+ // Otherwise, if |position| is in a short paragraph, check the paragraph.
+ const EphemeralRange& paragraph_range =
+ ExpandToParagraphBoundary(EphemeralRange(position));
+ const int paragraph_length = TextIterator::RangeLength(paragraph_range);
+ if (paragraph_length <= kHotModeChunkSize)
+ return paragraph_range;
+
+ // Otherwise, check a chunk of text centered at |position|.
+ TextIteratorBehavior behavior = TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(true)
+ .Build();
+ BackwardsCharacterIterator backward_iterator(
+ EphemeralRange(full_range.StartPosition(), position), behavior);
+ if (!backward_iterator.AtEnd())
+ backward_iterator.Advance(kHotModeChunkSize / 2);
+ const Position& chunk_start = backward_iterator.EndPosition();
+ CharacterIterator forward_iterator(position, full_range.EndPosition(),
+ behavior);
+ if (!forward_iterator.AtEnd())
+ forward_iterator.Advance(kHotModeChunkSize / 2);
+ const Position& chunk_end = forward_iterator.EndPosition();
+ return ExpandRangeToSentenceBoundary(EphemeralRange(chunk_start, chunk_end));
+}
+
+} // namespace
+
+HotModeSpellCheckRequester::HotModeSpellCheckRequester(
+ SpellCheckRequester& requester)
+ : requester_(requester) {}
+
+void HotModeSpellCheckRequester::CheckSpellingAt(const Position& position) {
+ const Element* root_editable = RootEditableElementOf(position);
+ if (!root_editable || !root_editable->isConnected())
+ return;
+
+ if (processed_root_editables_.Contains(root_editable))
+ return;
+ processed_root_editables_.push_back(root_editable);
+
+ if (!root_editable->IsSpellCheckingEnabled() &&
+ !SpellChecker::IsSpellCheckingEnabledAt(position)) {
+ return;
+ }
+
+ const EphemeralRange& current_word =
+ CurrentWordIfTypingInPartialWord(*root_editable);
+ if (current_word.IsNotNull()) {
+ root_editable->GetDocument().Markers().RemoveMarkersInRange(
+ current_word, DocumentMarker::MisspellingMarkers());
+ return;
+ }
+
+ const EphemeralRange& checking_range =
+ CalculateHotModeCheckingRange(*root_editable, position);
+ requester_->RequestCheckingFor(checking_range);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/hot_mode_spell_check_requester.h b/chromium/third_party/blink/renderer/core/editing/spellcheck/hot_mode_spell_check_requester.h
new file mode 100644
index 00000000000..9a6e8f62112
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/hot_mode_spell_check_requester.h
@@ -0,0 +1,34 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_HOT_MODE_SPELL_CHECK_REQUESTER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_HOT_MODE_SPELL_CHECK_REQUESTER_H_
+
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+class Element;
+class SpellCheckRequester;
+
+// This class is only supposed to be used by IdleSpellCheckCallback in hot mode
+// invocation. Not to be confused with SpellCheckRequester.
+class HotModeSpellCheckRequester {
+ STACK_ALLOCATED();
+
+ public:
+ explicit HotModeSpellCheckRequester(SpellCheckRequester&);
+ void CheckSpellingAt(const Position&);
+
+ private:
+ HeapVector<Member<const Element>> processed_root_editables_;
+ Member<SpellCheckRequester> requester_;
+
+ DISALLOW_COPY_AND_ASSIGN(HotModeSpellCheckRequester);
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_callback.cc b/chromium/third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_callback.cc
new file mode 100644
index 00000000000..1e11d214697
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_callback.cc
@@ -0,0 +1,240 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_callback.h"
+
+#include "third_party/blink/public/platform/task_type.h"
+#include "third_party/blink/renderer/core/dom/idle_request_options.h"
+#include "third_party/blink/renderer/core/editing/commands/undo_stack.h"
+#include "third_party/blink/renderer/core/editing/commands/undo_step.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/iterators/character_iterator.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/cold_mode_spell_check_requester.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/hot_mode_spell_check_requester.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_check_requester.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_checker.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
+#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
+#include "third_party/blink/renderer/platform/wtf/time.h"
+
+namespace blink {
+
+namespace {
+
+const int kColdModeTimerIntervalMS = 1000;
+const int kConsecutiveColdModeTimerIntervalMS = 200;
+const int kHotModeRequestTimeoutMS = 200;
+const int kInvalidHandle = -1;
+const int kDummyHandleForForcedInvocation = -2;
+const double kForcedInvocationDeadlineSeconds = 10;
+
+} // namespace
+
+IdleSpellCheckCallback::~IdleSpellCheckCallback() = default;
+
+void IdleSpellCheckCallback::Trace(blink::Visitor* visitor) {
+ visitor->Trace(frame_);
+ visitor->Trace(cold_mode_requester_);
+ DocumentShutdownObserver::Trace(visitor);
+ ScriptedIdleTaskController::IdleTask::Trace(visitor);
+}
+
+IdleSpellCheckCallback* IdleSpellCheckCallback::Create(LocalFrame& frame) {
+ return new IdleSpellCheckCallback(frame);
+}
+
+IdleSpellCheckCallback::IdleSpellCheckCallback(LocalFrame& frame)
+ : state_(State::kInactive),
+ idle_callback_handle_(kInvalidHandle),
+ frame_(frame),
+ last_processed_undo_step_sequence_(0),
+ cold_mode_requester_(ColdModeSpellCheckRequester::Create(frame)),
+ cold_mode_timer_(frame.GetTaskRunner(TaskType::kUnspecedTimer),
+ this,
+ &IdleSpellCheckCallback::ColdModeTimerFired) {}
+
+SpellCheckRequester& IdleSpellCheckCallback::GetSpellCheckRequester() const {
+ return GetFrame().GetSpellChecker().GetSpellCheckRequester();
+}
+
+bool IdleSpellCheckCallback::IsSpellCheckingEnabled() const {
+ return GetFrame().GetSpellChecker().IsSpellCheckingEnabled();
+}
+
+void IdleSpellCheckCallback::Deactivate() {
+ state_ = State::kInactive;
+ if (cold_mode_timer_.IsActive())
+ cold_mode_timer_.Stop();
+ if (idle_callback_handle_ != kInvalidHandle && IsAvailable())
+ GetDocument().CancelIdleCallback(idle_callback_handle_);
+ idle_callback_handle_ = kInvalidHandle;
+}
+
+void IdleSpellCheckCallback::SetNeedsInvocation() {
+ if (!IsSpellCheckingEnabled() || !IsAvailable()) {
+ Deactivate();
+ return;
+ }
+
+ if (state_ == State::kHotModeRequested)
+ return;
+
+ if (state_ == State::kColdModeTimerStarted) {
+ DCHECK(cold_mode_timer_.IsActive());
+ cold_mode_timer_.Stop();
+ }
+
+ if (state_ == State::kColdModeRequested) {
+ GetDocument().CancelIdleCallback(idle_callback_handle_);
+ idle_callback_handle_ = kInvalidHandle;
+ }
+
+ IdleRequestOptions options;
+ options.setTimeout(kHotModeRequestTimeoutMS);
+ idle_callback_handle_ = GetDocument().RequestIdleCallback(this, options);
+ state_ = State::kHotModeRequested;
+}
+
+void IdleSpellCheckCallback::SetNeedsColdModeInvocation() {
+ if (!RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled() ||
+ !IsSpellCheckingEnabled()) {
+ Deactivate();
+ return;
+ }
+
+ if (state_ != State::kInactive && state_ != State::kInHotModeInvocation &&
+ state_ != State::kInColdModeInvocation)
+ return;
+
+ DCHECK(!cold_mode_timer_.IsActive());
+ int interval_ms = state_ == State::kInColdModeInvocation
+ ? kConsecutiveColdModeTimerIntervalMS
+ : kColdModeTimerIntervalMS;
+ cold_mode_timer_.StartOneShot(interval_ms / 1000.0, FROM_HERE);
+ state_ = State::kColdModeTimerStarted;
+}
+
+void IdleSpellCheckCallback::ColdModeTimerFired(TimerBase*) {
+ DCHECK(RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled());
+ DCHECK_EQ(State::kColdModeTimerStarted, state_);
+
+ if (!IsSpellCheckingEnabled() || !IsAvailable()) {
+ Deactivate();
+ return;
+ }
+
+ idle_callback_handle_ =
+ GetDocument().RequestIdleCallback(this, IdleRequestOptions());
+ state_ = State::kColdModeRequested;
+}
+
+void IdleSpellCheckCallback::HotModeInvocation(IdleDeadline* deadline) {
+ TRACE_EVENT0("blink", "IdleSpellCheckCallback::hotModeInvocation");
+
+ // TODO(xiaochengh): Figure out if this has any performance impact.
+ GetDocument().UpdateStyleAndLayout();
+
+ HotModeSpellCheckRequester requester(GetSpellCheckRequester());
+
+ requester.CheckSpellingAt(
+ GetFrame().Selection().GetSelectionInDOMTree().Extent());
+
+ const uint64_t watermark = last_processed_undo_step_sequence_;
+ for (const UndoStep* step :
+ GetFrame().GetEditor().GetUndoStack().UndoSteps()) {
+ if (step->SequenceNumber() <= watermark)
+ break;
+ last_processed_undo_step_sequence_ =
+ std::max(step->SequenceNumber(), last_processed_undo_step_sequence_);
+ if (deadline->timeRemaining() == 0)
+ break;
+ // The ending selection stored in undo stack can be invalid, disconnected
+ // or have been moved to another document, so we should check its validity
+ // before using it.
+ if (!step->EndingSelection().IsValidFor(GetDocument()))
+ continue;
+ requester.CheckSpellingAt(step->EndingSelection().Extent());
+ }
+}
+
+void IdleSpellCheckCallback::invoke(IdleDeadline* deadline) {
+ DCHECK_NE(idle_callback_handle_, kInvalidHandle);
+ idle_callback_handle_ = kInvalidHandle;
+
+ if (!IsSpellCheckingEnabled() || !IsAvailable()) {
+ Deactivate();
+ return;
+ }
+
+ if (state_ == State::kHotModeRequested) {
+ state_ = State::kInHotModeInvocation;
+ HotModeInvocation(deadline);
+ SetNeedsColdModeInvocation();
+ } else if (state_ == State::kColdModeRequested) {
+ DCHECK(RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled());
+ state_ = State::kInColdModeInvocation;
+ cold_mode_requester_->Invoke(deadline);
+ if (cold_mode_requester_->FullDocumentChecked())
+ state_ = State::kInactive;
+ else
+ SetNeedsColdModeInvocation();
+ } else {
+ NOTREACHED();
+ }
+}
+
+void IdleSpellCheckCallback::DocumentAttached(Document* document) {
+ SetNeedsColdModeInvocation();
+ SetContext(document);
+}
+
+void IdleSpellCheckCallback::ContextDestroyed(Document*) {
+ Deactivate();
+}
+
+void IdleSpellCheckCallback::ForceInvocationForTesting() {
+ if (!IsSpellCheckingEnabled())
+ return;
+
+ IdleDeadline* deadline = IdleDeadline::Create(
+ kForcedInvocationDeadlineSeconds + CurrentTimeTicksInSeconds(),
+ IdleDeadline::CallbackType::kCalledWhenIdle);
+
+ switch (state_) {
+ case State::kColdModeTimerStarted:
+ cold_mode_timer_.Stop();
+ state_ = State::kColdModeRequested;
+ idle_callback_handle_ = kDummyHandleForForcedInvocation;
+ invoke(deadline);
+ break;
+ case State::kHotModeRequested:
+ case State::kColdModeRequested:
+ GetDocument().CancelIdleCallback(idle_callback_handle_);
+ invoke(deadline);
+ break;
+ case State::kInactive:
+ case State::kInHotModeInvocation:
+ case State::kInColdModeInvocation:
+ NOTREACHED();
+ }
+}
+
+void IdleSpellCheckCallback::SkipColdModeTimerForTesting() {
+ DCHECK(cold_mode_timer_.IsActive());
+ cold_mode_timer_.Stop();
+ ColdModeTimerFired(&cold_mode_timer_);
+}
+
+void IdleSpellCheckCallback::SetNeedsMoreColdModeInvocationForTesting() {
+ cold_mode_requester_->SetNeedsMoreInvocationForTesting();
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_callback.h b/chromium/third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_callback.h
new file mode 100644
index 00000000000..6a03e7a3500
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_callback.h
@@ -0,0 +1,109 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_IDLE_SPELL_CHECK_CALLBACK_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_IDLE_SPELL_CHECK_CALLBACK_H_
+
+#include "third_party/blink/renderer/core/dom/document_shutdown_observer.h"
+#include "third_party/blink/renderer/core/dom/scripted_idle_task_controller.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/timer.h"
+
+namespace blink {
+
+class ColdModeSpellCheckRequester;
+class LocalFrame;
+class SpellCheckRequester;
+
+#define FOR_EACH_IDLE_SPELL_CHECK_CALLBACK_STATE(V) \
+ V(Inactive) \
+ V(HotModeRequested) \
+ V(InHotModeInvocation) \
+ V(ColdModeTimerStarted) \
+ V(ColdModeRequested) \
+ V(InColdModeInvocation)
+
+// Main class for the implementation of idle time spell checker.
+class CORE_EXPORT IdleSpellCheckCallback final
+ : public ScriptedIdleTaskController::IdleTask,
+ public DocumentShutdownObserver {
+ DISALLOW_COPY_AND_ASSIGN(IdleSpellCheckCallback);
+ USING_GARBAGE_COLLECTED_MIXIN(IdleSpellCheckCallback);
+
+ public:
+ static IdleSpellCheckCallback* Create(LocalFrame&);
+ ~IdleSpellCheckCallback() override;
+
+ enum class State {
+#define V(state) k##state,
+ FOR_EACH_IDLE_SPELL_CHECK_CALLBACK_STATE(V)
+#undef V
+ };
+
+ State GetState() const { return state_; }
+
+ // Transit to HotModeRequested, if possible. Called by operations that need
+ // spell checker to follow up.
+ void SetNeedsInvocation();
+
+ // Cleans everything up and makes the callback inactive. Should be called when
+ // document is detached or spellchecking is globally disabled.
+ void Deactivate();
+
+ void DocumentAttached(Document*);
+
+ // Exposed for testing only.
+ SpellCheckRequester& GetSpellCheckRequester() const;
+ void ForceInvocationForTesting();
+ void SetNeedsMoreColdModeInvocationForTesting();
+ void SkipColdModeTimerForTesting();
+ int IdleCallbackHandle() const { return idle_callback_handle_; }
+
+ virtual void Trace(blink::Visitor*);
+
+ private:
+ explicit IdleSpellCheckCallback(LocalFrame&);
+ void invoke(IdleDeadline*) override;
+
+ LocalFrame& GetFrame() const { return *frame_; }
+
+ // Returns whether there is an active document to work on.
+ bool IsAvailable() const { return LifecycleContext(); }
+
+ // Return the document to work on. Callable only when IsAvailable() is true.
+ Document& GetDocument() const {
+ DCHECK(IsAvailable());
+ return *LifecycleContext();
+ }
+
+ // Returns whether spell checking is globally enabled.
+ bool IsSpellCheckingEnabled() const;
+
+ // Functions for hot mode.
+ void HotModeInvocation(IdleDeadline*);
+
+ // Transit to ColdModeTimerStarted, if possible. Sets up a timer, and requests
+ // cold mode invocation if no critical operation occurs before timer firing.
+ void SetNeedsColdModeInvocation();
+
+ // Functions for cold mode.
+ void ColdModeTimerFired(TimerBase*);
+ void ColdModeInvocation(IdleDeadline*);
+
+ // Implements |DocumentShutdownObserver|.
+ void ContextDestroyed(Document*) final;
+
+ State state_;
+ int idle_callback_handle_;
+ const Member<LocalFrame> frame_;
+ uint64_t last_processed_undo_step_sequence_;
+ const Member<ColdModeSpellCheckRequester> cold_mode_requester_;
+ TaskRunnerTimer<IdleSpellCheckCallback> cold_mode_timer_;
+
+ friend class IdleSpellCheckCallbackTest;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_IDLE_SPELL_CHECK_CALLBACK_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_callback_test.cc b/chromium/third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_callback_test.cc
new file mode 100644
index 00000000000..4f46e6432ea
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_callback_test.cc
@@ -0,0 +1,184 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_callback.h"
+
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_check_test_base.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_checker.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
+
+namespace blink {
+
+using State = IdleSpellCheckCallback::State;
+
+class IdleSpellCheckCallbackTest : public SpellCheckTestBase {
+ protected:
+ IdleSpellCheckCallback& IdleChecker() {
+ return GetSpellChecker().GetIdleSpellCheckCallback();
+ }
+
+ void SetUp() override {
+ SpellCheckTestBase::SetUp();
+
+ // The initial cold mode request is on on document startup. This doesn't
+ // work in unit test where SpellChecker is enabled after document startup.
+ // Post another request here to ensure the activation of cold mode checker.
+ IdleChecker().SetNeedsColdModeInvocation();
+ }
+
+ void TransitTo(State state) {
+ switch (state) {
+ case State::kInactive:
+ IdleChecker().Deactivate();
+ break;
+ case State::kHotModeRequested:
+ IdleChecker().SetNeedsInvocation();
+ break;
+ case State::kColdModeTimerStarted:
+ DCHECK(RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled());
+ break;
+ case State::kColdModeRequested:
+ DCHECK(RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled());
+ IdleChecker().SkipColdModeTimerForTesting();
+ break;
+ case State::kInHotModeInvocation:
+ case State::kInColdModeInvocation:
+ NOTREACHED();
+ }
+ }
+};
+
+// Test cases for lifecycle state transitions.
+
+TEST_F(IdleSpellCheckCallbackTest, InitializationWithColdMode) {
+ if (!RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled())
+ return;
+
+ EXPECT_EQ(State::kColdModeTimerStarted, IdleChecker().GetState());
+}
+
+TEST_F(IdleSpellCheckCallbackTest, InitializationWithoutColdMode) {
+ if (RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled())
+ return;
+
+ EXPECT_EQ(State::kInactive, IdleChecker().GetState());
+}
+
+TEST_F(IdleSpellCheckCallbackTest, RequestWhenInactive) {
+ TransitTo(State::kInactive);
+ IdleChecker().SetNeedsInvocation();
+ EXPECT_EQ(State::kHotModeRequested, IdleChecker().GetState());
+ EXPECT_NE(-1, IdleChecker().IdleCallbackHandle());
+}
+
+TEST_F(IdleSpellCheckCallbackTest, RequestWhenHotModeRequested) {
+ TransitTo(State::kHotModeRequested);
+ int handle = IdleChecker().IdleCallbackHandle();
+ IdleChecker().SetNeedsInvocation();
+ EXPECT_EQ(State::kHotModeRequested, IdleChecker().GetState());
+ EXPECT_EQ(handle, IdleChecker().IdleCallbackHandle());
+ EXPECT_NE(-1, IdleChecker().IdleCallbackHandle());
+}
+
+TEST_F(IdleSpellCheckCallbackTest, RequestWhenColdModeTimerStarted) {
+ if (!RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled())
+ return;
+
+ TransitTo(State::kColdModeTimerStarted);
+ IdleChecker().SetNeedsInvocation();
+ EXPECT_EQ(State::kHotModeRequested, IdleChecker().GetState());
+ EXPECT_NE(-1, IdleChecker().IdleCallbackHandle());
+}
+
+TEST_F(IdleSpellCheckCallbackTest, RequestWhenColdModeRequested) {
+ if (!RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled())
+ return;
+
+ TransitTo(State::kColdModeRequested);
+ int handle = IdleChecker().IdleCallbackHandle();
+ IdleChecker().SetNeedsInvocation();
+ EXPECT_EQ(State::kHotModeRequested, IdleChecker().GetState());
+ EXPECT_NE(handle, IdleChecker().IdleCallbackHandle());
+ EXPECT_NE(-1, IdleChecker().IdleCallbackHandle());
+}
+
+TEST_F(IdleSpellCheckCallbackTest, HotModeTransitToInactive) {
+ if (RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled())
+ return;
+
+ TransitTo(State::kHotModeRequested);
+ IdleChecker().ForceInvocationForTesting();
+ EXPECT_EQ(State::kInactive, IdleChecker().GetState());
+}
+
+TEST_F(IdleSpellCheckCallbackTest, HotModeTransitToColdMode) {
+ if (!RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled())
+ return;
+
+ TransitTo(State::kHotModeRequested);
+ IdleChecker().ForceInvocationForTesting();
+ EXPECT_EQ(State::kColdModeTimerStarted, IdleChecker().GetState());
+}
+
+TEST_F(IdleSpellCheckCallbackTest, ColdModeTimerStartedToRequested) {
+ if (!RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled())
+ return;
+
+ TransitTo(State::kColdModeTimerStarted);
+ IdleChecker().SkipColdModeTimerForTesting();
+ EXPECT_EQ(State::kColdModeRequested, IdleChecker().GetState());
+ EXPECT_NE(-1, IdleChecker().IdleCallbackHandle());
+}
+
+TEST_F(IdleSpellCheckCallbackTest, ColdModeStayAtColdMode) {
+ if (!RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled())
+ return;
+
+ TransitTo(State::kColdModeRequested);
+ IdleChecker().SetNeedsMoreColdModeInvocationForTesting();
+ IdleChecker().ForceInvocationForTesting();
+ EXPECT_EQ(State::kColdModeTimerStarted, IdleChecker().GetState());
+}
+
+TEST_F(IdleSpellCheckCallbackTest, ColdModeToInactive) {
+ if (!RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled())
+ return;
+
+ TransitTo(State::kColdModeRequested);
+ IdleChecker().ForceInvocationForTesting();
+ EXPECT_EQ(State::kInactive, IdleChecker().GetState());
+}
+
+TEST_F(IdleSpellCheckCallbackTest, DetachWhenInactive) {
+ TransitTo(State::kInactive);
+ GetDocument().Shutdown();
+ EXPECT_EQ(State::kInactive, IdleChecker().GetState());
+}
+
+TEST_F(IdleSpellCheckCallbackTest, DetachWhenHotModeRequested) {
+ TransitTo(State::kHotModeRequested);
+ GetDocument().Shutdown();
+ EXPECT_EQ(State::kInactive, IdleChecker().GetState());
+}
+
+TEST_F(IdleSpellCheckCallbackTest, DetachWhenColdModeTimerStarted) {
+ if (!RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled())
+ return;
+
+ TransitTo(State::kColdModeTimerStarted);
+ GetDocument().Shutdown();
+ EXPECT_EQ(State::kInactive, IdleChecker().GetState());
+}
+
+TEST_F(IdleSpellCheckCallbackTest, DetachWhenColdModeRequested) {
+ if (!RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled())
+ return;
+
+ TransitTo(State::kColdModeRequested);
+ GetDocument().Shutdown();
+ EXPECT_EQ(State::kInactive, IdleChecker().GetState());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_check_requester.cc b/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_check_requester.cc
new file mode 100644
index 00000000000..80a03fbc61a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_check_requester.cc
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_check_requester.h"
+
+#include "third_party/blink/public/platform/task_type.h"
+#include "third_party/blink/public/web/web_text_check_client.h"
+#include "third_party/blink/public/web/web_text_checking_completion.h"
+#include "third_party/blink/public/web/web_text_checking_result.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/node.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_checker.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+#include "third_party/blink/renderer/platform/histogram.h"
+
+namespace blink {
+
+namespace {
+
+static Vector<TextCheckingResult> ToCoreResults(
+ const WebVector<WebTextCheckingResult>& results) {
+ Vector<TextCheckingResult> core_results;
+ for (size_t i = 0; i < results.size(); ++i)
+ core_results.push_back(results[i]);
+ return core_results;
+}
+
+class WebTextCheckingCompletionImpl : public WebTextCheckingCompletion {
+ public:
+ explicit WebTextCheckingCompletionImpl(SpellCheckRequest* request)
+ : request_(request) {}
+
+ void DidFinishCheckingText(
+ const WebVector<WebTextCheckingResult>& results) override {
+ request_->DidSucceed(ToCoreResults(results));
+ delete this;
+ }
+
+ void DidCancelCheckingText() override {
+ request_->DidCancel();
+ // TODO(dgozman): use std::unique_ptr.
+ delete this;
+ }
+
+ private:
+ virtual ~WebTextCheckingCompletionImpl() = default;
+
+ Persistent<SpellCheckRequest> request_;
+};
+
+} // namespace
+
+SpellCheckRequest::SpellCheckRequest(Range* checking_range,
+ const String& text,
+ int request_number)
+ : requester_(nullptr),
+ checking_range_(checking_range),
+ root_editable_element_(
+ blink::RootEditableElement(*checking_range_->startContainer())),
+ text_(text),
+ request_number_(request_number) {
+ DCHECK(checking_range_);
+ DCHECK(checking_range_->IsConnected());
+ DCHECK(root_editable_element_);
+}
+
+SpellCheckRequest::~SpellCheckRequest() = default;
+
+void SpellCheckRequest::Trace(blink::Visitor* visitor) {
+ visitor->Trace(requester_);
+ visitor->Trace(checking_range_);
+ visitor->Trace(root_editable_element_);
+}
+
+void SpellCheckRequest::Dispose() {
+ if (checking_range_)
+ checking_range_->Dispose();
+}
+
+// static
+SpellCheckRequest* SpellCheckRequest::Create(
+ const EphemeralRange& checking_range,
+ int request_number) {
+ if (checking_range.IsNull())
+ return nullptr;
+ if (!blink::RootEditableElement(
+ *checking_range.StartPosition().ComputeContainerNode()))
+ return nullptr;
+
+ String text =
+ PlainText(checking_range, TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(true)
+ .Build());
+ if (text.IsEmpty())
+ return nullptr;
+
+ Range* checking_range_object = CreateRange(checking_range);
+
+ return new SpellCheckRequest(checking_range_object, text, request_number);
+}
+
+bool SpellCheckRequest::IsValid() const {
+ return checking_range_->IsConnected() &&
+ root_editable_element_->isConnected();
+}
+
+void SpellCheckRequest::DidSucceed(const Vector<TextCheckingResult>& results) {
+ if (!requester_)
+ return;
+ SpellCheckRequester* requester = requester_;
+ requester_ = nullptr;
+ requester->DidCheckSucceed(sequence_, results);
+}
+
+void SpellCheckRequest::DidCancel() {
+ if (!requester_)
+ return;
+ SpellCheckRequester* requester = requester_;
+ requester_ = nullptr;
+ requester->DidCheckCancel(sequence_);
+}
+
+void SpellCheckRequest::SetCheckerAndSequence(SpellCheckRequester* requester,
+ int sequence) {
+ DCHECK(!requester_);
+ DCHECK_EQ(sequence_, kUnrequestedTextCheckingSequence);
+ requester_ = requester;
+ sequence_ = sequence;
+}
+
+SpellCheckRequester::SpellCheckRequester(LocalFrame& frame)
+ : frame_(&frame),
+ last_request_sequence_(0),
+ last_processed_sequence_(0),
+ last_request_time_(0.0),
+ timer_to_process_queued_request_(
+ frame.GetTaskRunner(TaskType::kUnspecedTimer),
+ this,
+ &SpellCheckRequester::TimerFiredToProcessQueuedRequest) {}
+
+SpellCheckRequester::~SpellCheckRequester() = default;
+
+WebTextCheckClient* SpellCheckRequester::GetTextCheckerClient() const {
+ return GetFrame().GetSpellChecker().GetTextCheckerClient();
+}
+
+void SpellCheckRequester::TimerFiredToProcessQueuedRequest(TimerBase*) {
+ DCHECK(!request_queue_.IsEmpty());
+ if (request_queue_.IsEmpty())
+ return;
+
+ InvokeRequest(request_queue_.TakeFirst());
+}
+
+void SpellCheckRequester::RequestCheckingFor(const EphemeralRange& range) {
+ RequestCheckingFor(range, 0);
+}
+
+void SpellCheckRequester::RequestCheckingFor(const EphemeralRange& range,
+ int request_num) {
+ SpellCheckRequest* request = SpellCheckRequest::Create(range, request_num);
+ if (!request)
+ return;
+
+ DEFINE_STATIC_LOCAL(CustomCountHistogram,
+ spell_checker_request_interval_histogram,
+ ("WebCore.SpellChecker.RequestInterval", 0, 10000, 50));
+ const double current_request_time = CurrentTimeTicksInSeconds();
+ if (request_num == 0 && last_request_time_ > 0) {
+ const double interval_ms =
+ (current_request_time - last_request_time_) * 1000.0;
+ spell_checker_request_interval_histogram.Count(interval_ms);
+ }
+ last_request_time_ = current_request_time;
+
+ DCHECK_EQ(request->Sequence(),
+ SpellCheckRequest::kUnrequestedTextCheckingSequence);
+ int sequence = ++last_request_sequence_;
+ if (sequence == SpellCheckRequest::kUnrequestedTextCheckingSequence)
+ sequence = ++last_request_sequence_;
+
+ request->SetCheckerAndSequence(this, sequence);
+
+ if (timer_to_process_queued_request_.IsActive() || processing_request_) {
+ EnqueueRequest(request);
+ return;
+ }
+
+ InvokeRequest(request);
+}
+
+void SpellCheckRequester::CancelCheck() {
+ if (processing_request_)
+ processing_request_->DidCancel();
+}
+
+void SpellCheckRequester::PrepareForLeakDetection() {
+ timer_to_process_queued_request_.Stop();
+ // Empty the queue of pending requests to prevent it being a leak source.
+ // Pending spell checker requests are cancellable requests not representing
+ // leaks, just async work items waiting to be processed.
+ //
+ // Rather than somehow wait for this async queue to drain before running
+ // the leak detector, they're all cancelled to prevent flaky leaks being
+ // reported.
+ request_queue_.clear();
+ // WebTextCheckClient stores a set of WebTextCheckingCompletion objects,
+ // which may store references to already invoked requests. We should clear
+ // these references to prevent them from being a leak source.
+ if (WebTextCheckClient* text_checker_client = GetTextCheckerClient())
+ text_checker_client->CancelAllPendingRequests();
+}
+
+void SpellCheckRequester::InvokeRequest(SpellCheckRequest* request) {
+ DCHECK(!processing_request_);
+ processing_request_ = request;
+ if (WebTextCheckClient* text_checker_client = GetTextCheckerClient()) {
+ text_checker_client->RequestCheckingOfText(
+ processing_request_->GetText(),
+ new WebTextCheckingCompletionImpl(request));
+ }
+}
+
+void SpellCheckRequester::ClearProcessingRequest() {
+ if (!processing_request_)
+ return;
+
+ processing_request_->Dispose();
+ processing_request_.Clear();
+}
+
+void SpellCheckRequester::EnqueueRequest(SpellCheckRequest* request) {
+ DCHECK(request);
+ bool continuation = false;
+ if (!request_queue_.IsEmpty()) {
+ SpellCheckRequest* last_request = request_queue_.back();
+ // It's a continuation if the number of the last request got incremented in
+ // the new one and both apply to the same editable.
+ continuation =
+ request->RootEditableElement() == last_request->RootEditableElement() &&
+ request->RequestNumber() == last_request->RequestNumber() + 1;
+ }
+
+ // Spellcheck requests for chunks of text in the same element should not
+ // overwrite each other.
+ if (!continuation) {
+ RequestQueue::const_iterator same_element_request = std::find_if(
+ request_queue_.begin(), request_queue_.end(),
+ [request](const SpellCheckRequest* queued_request) -> bool {
+ return request->RootEditableElement() ==
+ queued_request->RootEditableElement();
+ });
+ if (same_element_request != request_queue_.end())
+ request_queue_.erase(same_element_request);
+ }
+
+ request_queue_.push_back(request);
+}
+
+bool SpellCheckRequester::EnsureValidRequestQueueFor(int sequence) {
+ DCHECK(processing_request_);
+ if (processing_request_->Sequence() == sequence)
+ return true;
+ NOTREACHED();
+ request_queue_.clear();
+ return false;
+}
+
+void SpellCheckRequester::DidCheck(int sequence) {
+ DCHECK_LT(last_processed_sequence_, sequence);
+ last_processed_sequence_ = sequence;
+
+ ClearProcessingRequest();
+ if (!request_queue_.IsEmpty())
+ timer_to_process_queued_request_.StartOneShot(TimeDelta(), FROM_HERE);
+}
+
+void SpellCheckRequester::DidCheckSucceed(
+ int sequence,
+ const Vector<TextCheckingResult>& results) {
+ if (!EnsureValidRequestQueueFor(sequence))
+ return;
+ GetFrame().GetSpellChecker().MarkAndReplaceFor(processing_request_, results);
+ DidCheck(sequence);
+}
+
+void SpellCheckRequester::DidCheckCancel(int sequence) {
+ if (!EnsureValidRequestQueueFor(sequence))
+ return;
+ DidCheck(sequence);
+}
+
+void SpellCheckRequester::Trace(blink::Visitor* visitor) {
+ visitor->Trace(frame_);
+ visitor->Trace(processing_request_);
+ visitor->Trace(request_queue_);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_check_requester.h b/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_check_requester.h
new file mode 100644
index 00000000000..ed80b0a1a5e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_check_requester.h
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_SPELL_CHECK_REQUESTER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_SPELL_CHECK_REQUESTER_H_
+
+#include "base/macros.h"
+#include "base/memory/scoped_refptr.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/text_checking.h"
+#include "third_party/blink/renderer/platform/timer.h"
+#include "third_party/blink/renderer/platform/wtf/deque.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+#include "third_party/blink/renderer/platform/wtf/vector.h"
+
+namespace blink {
+
+class LocalFrame;
+class SpellCheckRequester;
+class WebTextCheckClient;
+
+class CORE_EXPORT SpellCheckRequest
+ : public GarbageCollectedFinalized<SpellCheckRequest> {
+ public:
+ static const int kUnrequestedTextCheckingSequence = -1;
+
+ static SpellCheckRequest* Create(const EphemeralRange& checking_range,
+ int request_number);
+
+ ~SpellCheckRequest();
+ void Dispose();
+
+ Range* CheckingRange() const { return checking_range_; }
+ Element* RootEditableElement() const { return root_editable_element_; }
+
+ void SetCheckerAndSequence(SpellCheckRequester*, int sequence);
+ int Sequence() const { return sequence_; }
+ String GetText() const { return text_; }
+
+ bool IsValid() const;
+ void DidSucceed(const Vector<TextCheckingResult>&);
+ void DidCancel();
+
+ int RequestNumber() const { return request_number_; }
+
+ void Trace(blink::Visitor*);
+
+ private:
+ SpellCheckRequest(Range* checking_range, const String&, int request_number);
+
+ Member<SpellCheckRequester> requester_;
+ Member<Range> checking_range_;
+ Member<Element> root_editable_element_;
+ int sequence_ = kUnrequestedTextCheckingSequence;
+ String text_;
+ int request_number_;
+};
+
+class CORE_EXPORT SpellCheckRequester final
+ : public GarbageCollectedFinalized<SpellCheckRequester> {
+ public:
+ static SpellCheckRequester* Create(LocalFrame& frame) {
+ return new SpellCheckRequester(frame);
+ }
+
+ ~SpellCheckRequester();
+ void Trace(blink::Visitor*);
+
+ void RequestCheckingFor(const EphemeralRange&);
+ void RequestCheckingFor(const EphemeralRange&, int request_num);
+ void CancelCheck();
+
+ int LastRequestSequence() const { return last_request_sequence_; }
+
+ int LastProcessedSequence() const { return last_processed_sequence_; }
+
+ // Exposed for leak detector only, see comment for corresponding
+ // SpellChecker method.
+ void PrepareForLeakDetection();
+
+ private:
+ friend class SpellCheckRequest;
+
+ explicit SpellCheckRequester(LocalFrame&);
+
+ WebTextCheckClient* GetTextCheckerClient() const;
+ void TimerFiredToProcessQueuedRequest(TimerBase*);
+ void InvokeRequest(SpellCheckRequest*);
+ void EnqueueRequest(SpellCheckRequest*);
+ bool EnsureValidRequestQueueFor(int sequence);
+ void DidCheckSucceed(int sequence, const Vector<TextCheckingResult>&);
+ void DidCheckCancel(int sequence);
+ void DidCheck(int sequence);
+
+ void ClearProcessingRequest();
+
+ Member<LocalFrame> frame_;
+ LocalFrame& GetFrame() const {
+ DCHECK(frame_);
+ return *frame_;
+ }
+
+ int last_request_sequence_;
+ int last_processed_sequence_;
+ double last_request_time_;
+
+ TaskRunnerTimer<SpellCheckRequester> timer_to_process_queued_request_;
+
+ Member<SpellCheckRequest> processing_request_;
+
+ typedef HeapDeque<Member<SpellCheckRequest>> RequestQueue;
+ RequestQueue request_queue_;
+
+ DISALLOW_COPY_AND_ASSIGN(SpellCheckRequester);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_SPELL_CHECK_REQUESTER_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_check_test_base.cc b/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_check_test_base.cc
new file mode 100644
index 00000000000..5dadf935a6c
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_check_test_base.cc
@@ -0,0 +1,41 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_check_test_base.h"
+
+#include "third_party/blink/public/web/web_text_check_client.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/loader/empty_clients.h"
+
+namespace blink {
+
+namespace {
+
+class EnabledTextCheckerClient : public WebTextCheckClient {
+ public:
+ EnabledTextCheckerClient() = default;
+ ~EnabledTextCheckerClient() override = default;
+ bool IsSpellCheckingEnabled() const override { return true; }
+};
+
+EnabledTextCheckerClient* GetEnabledTextCheckerClient() {
+ DEFINE_STATIC_LOCAL(EnabledTextCheckerClient, client, ());
+ return &client;
+}
+
+} // namespace
+
+void SpellCheckTestBase::SetUp() {
+ EditingTestBase::SetUp();
+
+ EmptyLocalFrameClient* frame_client =
+ static_cast<EmptyLocalFrameClient*>(GetFrame().Client());
+ frame_client->SetTextCheckerClientForTesting(GetEnabledTextCheckerClient());
+}
+
+SpellChecker& SpellCheckTestBase::GetSpellChecker() const {
+ return GetFrame().GetSpellChecker();
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_check_test_base.h b/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_check_test_base.h
new file mode 100644
index 00000000000..9debb3f8826
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_check_test_base.h
@@ -0,0 +1,23 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_SPELL_CHECK_TEST_BASE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_SPELL_CHECK_TEST_BASE_H_
+
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class SpellChecker;
+
+class SpellCheckTestBase : public EditingTestBase {
+ protected:
+ void SetUp() override;
+
+ SpellChecker& GetSpellChecker() const;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_SPELL_CHECK_TEST_BASE_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_checker.cc b/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_checker.cc
new file mode 100644
index 00000000000..f5f87c90fd0
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_checker.cc
@@ -0,0 +1,792 @@
+/*
+ * Copyright (C) 2006, 2007, 2008, 2011 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_checker.h"
+
+#include "third_party/blink/public/platform/web_spell_check_panel_host_client.h"
+#include "third_party/blink/public/platform/web_string.h"
+#include "third_party/blink/public/web/web_text_check_client.h"
+#include "third_party/blink/public/web/web_text_decoration_type.h"
+#include "third_party/blink/renderer/core/clipboard/data_transfer_access_policy.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/element_traversal.h"
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/iterators/character_iterator.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+#include "third_party/blink/renderer/core/editing/markers/spell_check_marker.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/idle_spell_check_callback.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_check_requester.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/text_checking_paragraph.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/input_type_names.h"
+#include "third_party/blink/renderer/core/layout/layout_text_control.h"
+#include "third_party/blink/renderer/core/loader/empty_clients.h"
+#include "third_party/blink/renderer/core/page/page.h"
+#include "third_party/blink/renderer/platform/text/text_break_iterator.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+namespace {
+
+static bool IsWhiteSpaceOrPunctuation(UChar c) {
+ return IsSpaceOrNewline(c) || WTF::Unicode::IsPunct(c);
+}
+
+bool IsAmbiguousBoundaryCharacter(UChar character) {
+ // These are characters that can behave as word boundaries, but can appear
+ // within words. If they are just typed, i.e. if they are immediately followed
+ // by a caret, we want to delay text checking until the next character has
+ // been typed.
+ // FIXME: this is required until 6853027 is fixed and text checking can do
+ // this for us.
+ return character == '\'' || character == kRightSingleQuotationMarkCharacter ||
+ character == kHebrewPunctuationGershayimCharacter;
+}
+
+} // namespace
+
+SpellChecker* SpellChecker::Create(LocalFrame& frame) {
+ return new SpellChecker(frame);
+}
+
+static WebSpellCheckPanelHostClient& GetEmptySpellCheckPanelHostClient() {
+ DEFINE_STATIC_LOCAL(EmptySpellCheckPanelHostClient, client, ());
+ return client;
+}
+
+WebSpellCheckPanelHostClient& SpellChecker::SpellCheckPanelHostClient() const {
+ WebSpellCheckPanelHostClient* spell_check_panel_host_client =
+ GetFrame().Client()->SpellCheckPanelHostClient();
+ if (!spell_check_panel_host_client)
+ return GetEmptySpellCheckPanelHostClient();
+ return *spell_check_panel_host_client;
+}
+
+WebTextCheckClient* SpellChecker::GetTextCheckerClient() const {
+ // There is no frame client if the frame is detached.
+ if (!GetFrame().Client())
+ return nullptr;
+ return GetFrame().Client()->GetTextCheckerClient();
+}
+
+SpellChecker::SpellChecker(LocalFrame& frame)
+ : frame_(&frame),
+ spell_check_requester_(SpellCheckRequester::Create(frame)),
+ idle_spell_check_callback_(IdleSpellCheckCallback::Create(frame)) {}
+
+bool SpellChecker::IsSpellCheckingEnabled() const {
+ if (WebTextCheckClient* client = GetTextCheckerClient())
+ return client->IsSpellCheckingEnabled();
+ return false;
+}
+
+void SpellChecker::IgnoreSpelling() {
+ RemoveMarkers(GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTree()
+ .ToNormalizedEphemeralRange(),
+ DocumentMarker::kSpelling);
+}
+
+void SpellChecker::AdvanceToNextMisspelling(bool start_before_selection) {
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ GetFrame().GetDocument()->Lifecycle());
+
+ // The basic approach is to search in two phases - from the selection end to
+ // the end of the doc, and then we wrap and search from the doc start to
+ // (approximately) where we started.
+
+ // Start at the end of the selection, search to edge of document. Starting at
+ // the selection end makes repeated "check spelling" commands work.
+ VisibleSelection selection(
+ GetFrame().Selection().ComputeVisibleSelectionInDOMTree());
+ Position spelling_search_start, spelling_search_end;
+ Range::selectNodeContents(GetFrame().GetDocument(), spelling_search_start,
+ spelling_search_end);
+
+ bool started_with_selection = false;
+ if (selection.Start().AnchorNode()) {
+ started_with_selection = true;
+ if (start_before_selection) {
+ VisiblePosition start(selection.VisibleStart());
+ // We match AppKit's rule: Start 1 character before the selection.
+ VisiblePosition one_before_start = PreviousPositionOf(start);
+ spelling_search_start =
+ (one_before_start.IsNotNull() ? one_before_start : start)
+ .ToParentAnchoredPosition();
+ } else {
+ spelling_search_start = selection.VisibleEnd().ToParentAnchoredPosition();
+ }
+ }
+
+ Position position = spelling_search_start;
+ if (!IsEditablePosition(position)) {
+ // This shouldn't happen in very often because the Spelling menu items
+ // aren't enabled unless the selection is editable. This can happen in Mail
+ // for a mix of non-editable and editable content (like Stationary), when
+ // spell checking the whole document before sending the message. In that
+ // case the document might not be editable, but there are editable pockets
+ // that need to be spell checked.
+
+ if (!GetFrame().GetDocument()->documentElement())
+ return;
+ position = FirstEditableVisiblePositionAfterPositionInRoot(
+ position, *GetFrame().GetDocument()->documentElement())
+ .DeepEquivalent();
+ if (position.IsNull())
+ return;
+
+ spelling_search_start = position.ParentAnchoredEquivalent();
+ started_with_selection = false; // won't need to wrap
+ }
+
+ // topNode defines the whole range we want to operate on
+ ContainerNode* top_node = HighestEditableRoot(position);
+ // TODO(yosin): |lastOffsetForEditing()| is wrong here if
+ // |editingIgnoresContent(highestEditableRoot())| returns true, e.g. <table>
+ spelling_search_end = Position::EditingPositionOf(
+ top_node, EditingStrategy::LastOffsetForEditing(top_node));
+
+ // If spellingSearchRange starts in the middle of a word, advance to the
+ // next word so we start checking at a word boundary. Going back by one char
+ // and then forward by a word does the trick.
+ if (started_with_selection) {
+ VisiblePosition one_before_start =
+ PreviousPositionOf(CreateVisiblePosition(spelling_search_start));
+ if (one_before_start.IsNotNull() &&
+ RootEditableElementOf(one_before_start.DeepEquivalent()) ==
+ RootEditableElementOf(spelling_search_start))
+ spelling_search_start =
+ EndOfWord(one_before_start).ToParentAnchoredPosition();
+ // else we were already at the start of the editable node
+ }
+
+ if (spelling_search_start == spelling_search_end)
+ return; // nothing to search in
+
+ // We go to the end of our first range instead of the start of it, just to be
+ // sure we don't get foiled by any word boundary problems at the start. It
+ // means we might do a tiny bit more searching.
+ Node* search_end_node_after_wrap = spelling_search_end.ComputeContainerNode();
+ int search_end_offset_after_wrap =
+ spelling_search_end.OffsetInContainerNode();
+
+ std::pair<String, int> misspelled_item(String(), 0);
+ String& misspelled_word = misspelled_item.first;
+ int& misspelling_offset = misspelled_item.second;
+ misspelled_item =
+ FindFirstMisspelling(spelling_search_start, spelling_search_end);
+
+ // If we did not find a misspelled word, wrap and try again (but don't bother
+ // if we started at the beginning of the block rather than at a selection).
+ if (started_with_selection && !misspelled_word) {
+ spelling_search_start = Position::EditingPositionOf(top_node, 0);
+ // going until the end of the very first chunk we tested is far enough
+ spelling_search_end = Position::EditingPositionOf(
+ search_end_node_after_wrap, search_end_offset_after_wrap);
+ misspelled_item =
+ FindFirstMisspelling(spelling_search_start, spelling_search_end);
+ }
+
+ if (!misspelled_word.IsEmpty()) {
+ // We found a misspelling. Select the misspelling, update the spelling
+ // panel, and store a marker so we draw the red squiggle later.
+
+ const EphemeralRange misspelling_range = CalculateCharacterSubrange(
+ EphemeralRange(spelling_search_start, spelling_search_end),
+ misspelling_offset, misspelled_word.length());
+ GetFrame().Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(misspelling_range)
+ .Build());
+ GetFrame().Selection().RevealSelection();
+ SpellCheckPanelHostClient().UpdateSpellingUIWithMisspelledWord(
+ misspelled_word);
+ GetFrame().GetDocument()->Markers().AddSpellingMarker(misspelling_range);
+ }
+}
+
+void SpellChecker::ShowSpellingGuessPanel() {
+ if (SpellCheckPanelHostClient().IsShowingSpellingUI()) {
+ SpellCheckPanelHostClient().ShowSpellingUI(false);
+ return;
+ }
+
+ AdvanceToNextMisspelling(true);
+ SpellCheckPanelHostClient().ShowSpellingUI(true);
+}
+
+static void AddMarker(Document* document,
+ const EphemeralRange& checking_range,
+ DocumentMarker::MarkerType type,
+ int location,
+ int length,
+ const Vector<String>& descriptions) {
+ DCHECK(type == DocumentMarker::kSpelling || type == DocumentMarker::kGrammar)
+ << type;
+ DCHECK_GT(length, 0);
+ DCHECK_GE(location, 0);
+ const EphemeralRange& range_to_mark =
+ CalculateCharacterSubrange(checking_range, location, length);
+ if (!SpellChecker::IsSpellCheckingEnabledAt(range_to_mark.StartPosition()))
+ return;
+ if (!SpellChecker::IsSpellCheckingEnabledAt(range_to_mark.EndPosition()))
+ return;
+
+ String description;
+ for (size_t i = 0; i < descriptions.size(); ++i) {
+ if (i != 0)
+ description.append('\n');
+ description.append(descriptions[i]);
+ }
+
+ if (type == DocumentMarker::kSpelling) {
+ document->Markers().AddSpellingMarker(range_to_mark, description);
+ return;
+ }
+
+ DCHECK_EQ(type, DocumentMarker::kGrammar);
+ document->Markers().AddGrammarMarker(range_to_mark, description);
+}
+
+void SpellChecker::MarkAndReplaceFor(
+ SpellCheckRequest* request,
+ const Vector<TextCheckingResult>& results) {
+ TRACE_EVENT0("blink", "SpellChecker::markAndReplaceFor");
+ DCHECK(request);
+ if (!GetFrame().Selection().IsAvailable()) {
+ // "editing/spelling/spellcheck-async-remove-frame.html" reaches here.
+ return;
+ }
+ if (!request->IsValid())
+ return;
+ if (request->RootEditableElement()->GetDocument() !=
+ GetFrame().Selection().GetDocument()) {
+ // we ignore |request| made for another document.
+ // "editing/spelling/spellcheck-sequencenum.html" and others reach here.
+ return;
+ }
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ GetFrame().GetDocument()->Lifecycle());
+
+ EphemeralRange checking_range(request->CheckingRange());
+
+ // Abort marking if the content of the checking change has been modified.
+ String current_content =
+ PlainText(checking_range, TextIteratorBehavior::Builder()
+ .SetEmitsObjectReplacementCharacter(true)
+ .Build());
+ if (current_content != request->GetText()) {
+ // "editing/spelling/spellcheck-async-mutation.html" reaches here.
+ return;
+ }
+
+ // Clear the stale markers.
+ RemoveMarkers(checking_range, DocumentMarker::MisspellingMarkers());
+
+ if (!results.size())
+ return;
+
+ TextCheckingParagraph paragraph(checking_range, checking_range);
+
+ // TODO(crbug.com/230387): The following comment does not match the current
+ // behavior and should be rewritten.
+ // Expand the range to encompass entire paragraphs, since text checking needs
+ // that much context.
+ int ambiguous_boundary_offset = -1;
+
+ if (GetFrame().Selection().ComputeVisibleSelectionInDOMTree().IsCaret()) {
+ // TODO(crbug.com/230387): The following comment does not match the current
+ // behavior and should be rewritten.
+ // Attempt to save the caret position so we can restore it later if needed
+ const Position& caret_position =
+ GetFrame().Selection().ComputeVisibleSelectionInDOMTree().End();
+ const Position& paragraph_start = checking_range.StartPosition();
+ const int selection_offset =
+ paragraph_start < caret_position
+ ? TextIterator::RangeLength(paragraph_start, caret_position)
+ : 0;
+ if (selection_offset > 0 &&
+ static_cast<unsigned>(selection_offset) <=
+ paragraph.GetText().length() &&
+ IsAmbiguousBoundaryCharacter(
+ paragraph.TextCharAt(selection_offset - 1))) {
+ ambiguous_boundary_offset = selection_offset - 1;
+ }
+ }
+
+ const int spelling_range_end_offset = paragraph.CheckingEnd();
+ for (const TextCheckingResult& result : results) {
+ const int result_location = result.location + paragraph.CheckingStart();
+ const int result_length = result.length;
+ const bool result_ends_at_ambiguous_boundary =
+ ambiguous_boundary_offset >= 0 &&
+ result_location + result_length == ambiguous_boundary_offset;
+
+ // Only mark misspelling if:
+ // 1. Result falls within spellingRange.
+ // 2. The word in question doesn't end at an ambiguous boundary. For
+ // instance, we would not mark "wouldn'" as misspelled right after
+ // apostrophe is typed.
+ switch (result.decoration) {
+ case kTextDecorationTypeSpelling:
+ if (result_location < paragraph.CheckingStart() ||
+ result_location + result_length > spelling_range_end_offset ||
+ result_ends_at_ambiguous_boundary)
+ continue;
+ AddMarker(GetFrame().GetDocument(), paragraph.CheckingRange(),
+ DocumentMarker::kSpelling, result_location, result_length,
+ result.replacements);
+ continue;
+
+ case kTextDecorationTypeGrammar:
+ if (!paragraph.CheckingRangeCovers(result_location, result_length))
+ continue;
+ DCHECK_GT(result_length, 0);
+ DCHECK_GE(result_location, 0);
+ for (const GrammarDetail& detail : result.details) {
+ DCHECK_GT(detail.length, 0);
+ DCHECK_GE(detail.location, 0);
+ if (!paragraph.CheckingRangeCovers(result_location + detail.location,
+ detail.length))
+ continue;
+ AddMarker(GetFrame().GetDocument(), paragraph.CheckingRange(),
+ DocumentMarker::kGrammar, result_location + detail.location,
+ detail.length, result.replacements);
+ }
+ continue;
+ }
+ NOTREACHED();
+ }
+}
+
+void SpellChecker::DidEndEditingOnTextField(Element* e) {
+ TRACE_EVENT0("blink", "SpellChecker::didEndEditingOnTextField");
+
+ // Remove markers when deactivating a selection in an <input type="text"/>.
+ // Prevent new ones from appearing too.
+ HTMLElement* inner_editor = ToTextControl(e)->InnerEditorElement();
+ RemoveSpellingAndGrammarMarkers(*inner_editor);
+}
+
+void SpellChecker::RemoveSpellingAndGrammarMarkers(const HTMLElement& element,
+ ElementsType elements_type) {
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetFrame().GetDocument()->UpdateStyleAndLayoutTreeForNode(&element);
+
+ DocumentMarker::MarkerTypes marker_types(DocumentMarker::kSpelling);
+ marker_types.Add(DocumentMarker::kGrammar);
+ for (Node& node : NodeTraversal::InclusiveDescendantsOf(element)) {
+ if (elements_type == ElementsType::kAll || !HasEditableStyle(node)) {
+ GetFrame().GetDocument()->Markers().RemoveMarkersForNode(&node,
+ marker_types);
+ }
+ }
+}
+
+std::pair<Node*, SpellCheckMarker*>
+SpellChecker::GetSpellCheckMarkerUnderSelection() const {
+ const VisibleSelection& selection =
+ GetFrame().Selection().ComputeVisibleSelectionInDOMTree();
+ if (selection.IsNone())
+ return {};
+
+ // Caret and range selections always return valid normalized ranges.
+ const EphemeralRange& selection_range = FirstEphemeralRangeOf(selection);
+
+ Node* const selection_start_container =
+ selection_range.StartPosition().ComputeContainerNode();
+ Node* const selection_end_container =
+ selection_range.EndPosition().ComputeContainerNode();
+
+ // We don't currently support the case where a misspelling spans multiple
+ // nodes. See crbug.com/720065
+ if (selection_start_container != selection_end_container)
+ return {};
+
+ if (!selection_start_container->IsTextNode())
+ return {};
+
+ const unsigned selection_start_offset =
+ selection_range.StartPosition().ComputeOffsetInContainerNode();
+ const unsigned selection_end_offset =
+ selection_range.EndPosition().ComputeOffsetInContainerNode();
+
+ DocumentMarker* const marker =
+ GetFrame().GetDocument()->Markers().FirstMarkerIntersectingOffsetRange(
+ ToText(*selection_start_container), selection_start_offset,
+ selection_end_offset, DocumentMarker::MisspellingMarkers());
+ if (!marker)
+ return {};
+
+ return std::make_pair(selection_start_container, ToSpellCheckMarker(marker));
+}
+
+std::pair<String, String> SpellChecker::SelectMisspellingAsync() {
+ const std::pair<Node*, SpellCheckMarker*>& node_and_marker =
+ GetSpellCheckMarkerUnderSelection();
+ if (!node_and_marker.first)
+ return {};
+
+ Node* const marker_node = node_and_marker.first;
+ const SpellCheckMarker* const marker = node_and_marker.second;
+
+ const VisibleSelection& selection =
+ GetFrame().Selection().ComputeVisibleSelectionInDOMTree();
+ // Caret and range selections (one of which we must have since we found a
+ // marker) always return valid normalized ranges.
+ const EphemeralRange& selection_range =
+ selection.ToNormalizedEphemeralRange();
+
+ const EphemeralRange marker_range(
+ Position(marker_node, marker->StartOffset()),
+ Position(marker_node, marker->EndOffset()));
+ const String& marked_text = PlainText(marker_range);
+ if (marked_text.StripWhiteSpace(&IsWhiteSpaceOrPunctuation) !=
+ PlainText(selection_range).StripWhiteSpace(&IsWhiteSpaceOrPunctuation))
+ return {};
+
+ return std::make_pair(marked_text, marker->Description());
+}
+
+void SpellChecker::ReplaceMisspelledRange(const String& text) {
+ const std::pair<Node*, SpellCheckMarker*>& node_and_marker =
+ GetSpellCheckMarkerUnderSelection();
+ if (!node_and_marker.first)
+ return;
+
+ Node* const container_node = node_and_marker.first;
+ const SpellCheckMarker* const marker = node_and_marker.second;
+
+ GetFrame().Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(container_node, marker->StartOffset()))
+ .Extend(Position(container_node, marker->EndOffset()))
+ .Build());
+
+ Document& current_document = *GetFrame().GetDocument();
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ current_document.UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Dispatch 'beforeinput'.
+ Element* const target = FindEventTargetFrom(
+ GetFrame(), GetFrame().Selection().ComputeVisibleSelectionInDOMTree());
+
+ DataTransfer* const data_transfer = DataTransfer::Create(
+ DataTransfer::DataTransferType::kInsertReplacementText,
+ DataTransferAccessPolicy::kReadable, DataObject::CreateFromString(text));
+
+ const bool cancel = DispatchBeforeInputDataTransfer(
+ target, InputEvent::InputType::kInsertReplacementText,
+ data_transfer) != DispatchEventResult::kNotCanceled;
+
+ // 'beforeinput' event handler may destroy target frame.
+ if (current_document != GetFrame().GetDocument())
+ return;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (cancel)
+ return;
+ GetFrame().GetEditor().ReplaceSelectionWithText(
+ text, false, false, InputEvent::InputType::kInsertReplacementText);
+}
+
+void SpellChecker::RespondToChangedSelection() {
+ idle_spell_check_callback_->SetNeedsInvocation();
+}
+
+void SpellChecker::RespondToChangedContents() {
+ idle_spell_check_callback_->SetNeedsInvocation();
+}
+
+void SpellChecker::RemoveSpellingMarkers() {
+ GetFrame().GetDocument()->Markers().RemoveMarkersOfTypes(
+ DocumentMarker::MisspellingMarkers());
+}
+
+void SpellChecker::RemoveSpellingMarkersUnderWords(
+ const Vector<String>& words) {
+ DocumentMarkerController& marker_controller =
+ GetFrame().GetDocument()->Markers();
+ marker_controller.RemoveSpellingMarkersUnderWords(words);
+ marker_controller.RepaintMarkers();
+}
+
+static Node* FindFirstMarkable(Node* node) {
+ while (node) {
+ if (!node->GetLayoutObject())
+ return nullptr;
+ if (node->GetLayoutObject()->IsText())
+ return node;
+ if (node->GetLayoutObject()->IsTextControl())
+ node = ToLayoutTextControl(node->GetLayoutObject())
+ ->GetTextControlElement()
+ ->VisiblePositionForIndex(1)
+ .DeepEquivalent()
+ .AnchorNode();
+ else if (node->hasChildren())
+ node = node->firstChild();
+ else
+ node = node->nextSibling();
+ }
+
+ return nullptr;
+}
+
+bool SpellChecker::SelectionStartHasMarkerFor(
+ DocumentMarker::MarkerType marker_type,
+ int from,
+ int length) const {
+ Node* node = FindFirstMarkable(GetFrame()
+ .Selection()
+ .ComputeVisibleSelectionInDOMTree()
+ .Start()
+ .AnchorNode());
+ if (!node)
+ return false;
+
+ unsigned start_offset = static_cast<unsigned>(from);
+ unsigned end_offset = static_cast<unsigned>(from + length);
+ DocumentMarkerVector markers =
+ GetFrame().GetDocument()->Markers().MarkersFor(node);
+ for (size_t i = 0; i < markers.size(); ++i) {
+ DocumentMarker* marker = markers[i];
+ if (marker->StartOffset() <= start_offset &&
+ end_offset <= marker->EndOffset() && marker->GetType() == marker_type)
+ return true;
+ }
+
+ return false;
+}
+
+void SpellChecker::RemoveMarkers(const EphemeralRange& range,
+ DocumentMarker::MarkerTypes marker_types) {
+ DCHECK(!GetFrame().GetDocument()->NeedsLayoutTreeUpdate());
+
+ if (range.IsNull())
+ return;
+
+ GetFrame().GetDocument()->Markers().RemoveMarkersInRange(range, marker_types);
+}
+
+void SpellChecker::CancelCheck() {
+ spell_check_requester_->CancelCheck();
+}
+
+void SpellChecker::DocumentAttached(Document* document) {
+ idle_spell_check_callback_->DocumentAttached(document);
+}
+
+void SpellChecker::Trace(blink::Visitor* visitor) {
+ visitor->Trace(frame_);
+ visitor->Trace(spell_check_requester_);
+ visitor->Trace(idle_spell_check_callback_);
+}
+
+void SpellChecker::PrepareForLeakDetection() {
+ spell_check_requester_->PrepareForLeakDetection();
+ idle_spell_check_callback_->Deactivate();
+}
+
+Vector<TextCheckingResult> SpellChecker::FindMisspellings(const String& text) {
+ Vector<UChar> characters;
+ text.AppendTo(characters);
+ unsigned length = text.length();
+
+ TextBreakIterator* iterator = WordBreakIterator(characters.data(), length);
+ if (!iterator)
+ return Vector<TextCheckingResult>();
+
+ Vector<TextCheckingResult> results;
+ int word_start = iterator->current();
+ while (word_start >= 0) {
+ int word_end = iterator->next();
+ if (word_end < 0)
+ break;
+ int word_length = word_end - word_start;
+ int misspelling_location = -1;
+ int misspelling_length = 0;
+ if (WebTextCheckClient* text_checker_client = GetTextCheckerClient()) {
+ // SpellCheckWord will write (0, 0) into the output vars, which is what
+ // our caller expects if the word is spelled correctly.
+ text_checker_client->CheckSpelling(
+ String(characters.data() + word_start, word_length),
+ misspelling_location, misspelling_length, nullptr);
+ } else {
+ misspelling_location = 0;
+ }
+ if (misspelling_length > 0) {
+ DCHECK_GE(misspelling_location, 0);
+ DCHECK_LE(misspelling_location + misspelling_length, word_length);
+ TextCheckingResult misspelling;
+ misspelling.decoration = kTextDecorationTypeSpelling;
+ misspelling.location = word_start + misspelling_location;
+ misspelling.length = misspelling_length;
+ results.push_back(misspelling);
+ }
+ word_start = word_end;
+ }
+ return results;
+}
+
+std::pair<String, int> SpellChecker::FindFirstMisspelling(const Position& start,
+ const Position& end) {
+ String misspelled_word;
+
+ // Initialize out parameters; they will be updated if we find something to
+ // return.
+ String first_found_item;
+ int first_found_offset = 0;
+
+ // Expand the search range to encompass entire paragraphs, since text checking
+ // needs that much context. Determine the character offset from the start of
+ // the paragraph to the start of the original search range, since we will want
+ // to ignore results in this area.
+ EphemeralRange paragraph_range =
+ ExpandToParagraphBoundary(EphemeralRange(start, start));
+ Position paragraph_start = paragraph_range.StartPosition();
+ Position paragraph_end = paragraph_range.EndPosition();
+
+ const int total_range_length =
+ TextIterator::RangeLength(paragraph_start, end);
+ const int range_start_offset =
+ TextIterator::RangeLength(paragraph_start, start);
+ int total_length_processed = 0;
+
+ bool first_iteration = true;
+ bool last_iteration = false;
+ while (total_length_processed < total_range_length) {
+ // Iterate through the search range by paragraphs, checking each one for
+ // spelling.
+ int current_length =
+ TextIterator::RangeLength(paragraph_start, paragraph_end);
+ int current_start_offset = first_iteration ? range_start_offset : 0;
+ int current_end_offset = current_length;
+ if (InSameParagraph(CreateVisiblePosition(paragraph_start),
+ CreateVisiblePosition(end))) {
+ // Determine the character offset from the end of the original search
+ // range to the end of the paragraph, since we will want to ignore results
+ // in this area.
+ current_end_offset = TextIterator::RangeLength(paragraph_start, end);
+ last_iteration = true;
+ }
+ if (current_start_offset < current_end_offset) {
+ String paragraph_string = PlainText(paragraph_range);
+ if (paragraph_string.length() > 0) {
+ int spelling_location = 0;
+
+ Vector<TextCheckingResult> results = FindMisspellings(paragraph_string);
+
+ for (unsigned i = 0; i < results.size(); i++) {
+ const TextCheckingResult* result = &results[i];
+ if (result->location >= current_start_offset &&
+ result->location + result->length <= current_end_offset) {
+ DCHECK_GT(result->length, 0);
+ DCHECK_GE(result->location, 0);
+ spelling_location = result->location;
+ misspelled_word =
+ paragraph_string.Substring(result->location, result->length);
+ DCHECK(misspelled_word.length());
+ break;
+ }
+ }
+
+ if (!misspelled_word.IsEmpty()) {
+ int spelling_offset = spelling_location - current_start_offset;
+ if (!first_iteration)
+ spelling_offset +=
+ TextIterator::RangeLength(start, paragraph_start);
+ first_found_offset = spelling_offset;
+ first_found_item = misspelled_word;
+ break;
+ }
+ }
+ }
+ if (last_iteration ||
+ total_length_processed + current_length >= total_range_length)
+ break;
+ Position new_paragraph_start =
+ StartOfNextParagraph(CreateVisiblePosition(paragraph_end))
+ .DeepEquivalent();
+ if (new_paragraph_start.IsNull())
+ break;
+
+ paragraph_range = ExpandToParagraphBoundary(
+ EphemeralRange(new_paragraph_start, new_paragraph_start));
+ paragraph_start = paragraph_range.StartPosition();
+ paragraph_end = paragraph_range.EndPosition();
+ first_iteration = false;
+ total_length_processed += current_length;
+ }
+ return std::make_pair(first_found_item, first_found_offset);
+}
+
+// static
+bool SpellChecker::IsSpellCheckingEnabledAt(const Position& position) {
+ if (position.IsNull())
+ return false;
+ if (TextControlElement* text_control = EnclosingTextControl(position)) {
+ if (auto* input = ToHTMLInputElementOrNull(text_control)) {
+ // TODO(tkent): The following password type check should be done in
+ // HTMLElement::spellcheck(). crbug.com/371567
+ if (input->type() == InputTypeNames::password)
+ return false;
+ if (!input->IsFocusedElementInDocument())
+ return false;
+ }
+ }
+ HTMLElement* element =
+ Traversal<HTMLElement>::FirstAncestorOrSelf(*position.AnchorNode());
+ return element && element->IsSpellCheckingEnabled();
+}
+
+STATIC_ASSERT_ENUM(kWebTextDecorationTypeSpelling, kTextDecorationTypeSpelling);
+STATIC_ASSERT_ENUM(kWebTextDecorationTypeGrammar, kTextDecorationTypeGrammar);
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_checker.h b/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_checker.h
new file mode 100644
index 00000000000..ac10024ee57
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_checker.h
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_SPELL_CHECKER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_SPELL_CHECKER_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/text_checking.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+class Document;
+class Element;
+class IdleSpellCheckCallback;
+class LocalFrame;
+class HTMLElement;
+class SpellCheckMarker;
+class SpellCheckRequest;
+class SpellCheckRequester;
+struct TextCheckingResult;
+class WebSpellCheckPanelHostClient;
+class WebTextCheckClient;
+
+class CORE_EXPORT SpellChecker final : public GarbageCollected<SpellChecker> {
+ public:
+ static SpellChecker* Create(LocalFrame&);
+
+ void Trace(blink::Visitor*);
+
+ WebSpellCheckPanelHostClient& SpellCheckPanelHostClient() const;
+ WebTextCheckClient* GetTextCheckerClient() const;
+
+ static bool IsSpellCheckingEnabledAt(const Position&);
+ bool IsSpellCheckingEnabled() const;
+ void IgnoreSpelling();
+ void MarkAndReplaceFor(SpellCheckRequest*, const Vector<TextCheckingResult>&);
+ void AdvanceToNextMisspelling(bool start_before_selection);
+ void ShowSpellingGuessPanel();
+ void RespondToChangedContents();
+ void RespondToChangedSelection();
+ std::pair<Node*, SpellCheckMarker*> GetSpellCheckMarkerUnderSelection() const;
+ // The first String returned in the pair is the selected text.
+ // The second String is the marker's description.
+ std::pair<String, String> SelectMisspellingAsync();
+ void ReplaceMisspelledRange(const String&);
+ void RemoveSpellingMarkers();
+ void RemoveSpellingMarkersUnderWords(const Vector<String>& words);
+ enum class ElementsType { kAll, kOnlyNonEditable };
+ void RemoveSpellingAndGrammarMarkers(const HTMLElement&,
+ ElementsType = ElementsType::kAll);
+
+ void DidEndEditingOnTextField(Element*);
+ bool SelectionStartHasMarkerFor(DocumentMarker::MarkerType,
+ int from,
+ int length) const;
+ void CancelCheck();
+
+ // Exposed for testing and idle time spell checker
+ SpellCheckRequester& GetSpellCheckRequester() const {
+ return *spell_check_requester_;
+ }
+ IdleSpellCheckCallback& GetIdleSpellCheckCallback() const {
+ return *idle_spell_check_callback_;
+ }
+
+ // The leak detector will report leaks should queued requests be posted
+ // while it GCs repeatedly, as the requests keep their associated element
+ // alive.
+ //
+ // Hence allow the leak detector to effectively stop the spell checker to
+ // ensure leak reporting stability.
+ void PrepareForLeakDetection();
+
+ void DocumentAttached(Document*);
+
+ private:
+ explicit SpellChecker(LocalFrame&);
+
+ LocalFrame& GetFrame() const {
+ DCHECK(frame_);
+ return *frame_;
+ }
+
+ // Helper functions for advanceToNextMisspelling()
+ Vector<TextCheckingResult> FindMisspellings(const String&);
+ std::pair<String, int> FindFirstMisspelling(const Position&, const Position&);
+
+ void RemoveMarkers(const EphemeralRange&, DocumentMarker::MarkerTypes);
+
+ Member<LocalFrame> frame_;
+
+ const Member<SpellCheckRequester> spell_check_requester_;
+ const Member<IdleSpellCheckCallback> idle_spell_check_callback_;
+
+ DISALLOW_COPY_AND_ASSIGN(SpellChecker);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_SPELL_CHECKER_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_checker_test.cc b/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_checker_test.cc
new file mode 100644
index 00000000000..ae13fc64b71
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/spell_checker_test.cc
@@ -0,0 +1,376 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_checker.h"
+
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+#include "third_party/blink/renderer/core/editing/markers/spell_check_marker.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_check_requester.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_check_test_base.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
+
+namespace blink {
+
+class SpellCheckerTest : public SpellCheckTestBase {
+ protected:
+ int LayoutCount() const { return Page().GetFrameView().LayoutCount(); }
+ DummyPageHolder& Page() const { return GetDummyPageHolder(); }
+
+ void ForceLayout();
+};
+
+void SpellCheckerTest::ForceLayout() {
+ LocalFrameView& frame_view = Page().GetFrameView();
+ IntRect frame_rect = frame_view.FrameRect();
+ frame_rect.SetWidth(frame_rect.Width() + 1);
+ frame_rect.SetHeight(frame_rect.Height() + 1);
+ Page().GetFrameView().SetFrameRect(frame_rect);
+ GetDocument().UpdateStyleAndLayoutIgnorePendingStylesheets();
+}
+
+TEST_F(SpellCheckerTest, AdvanceToNextMisspellingWithEmptyInputNoCrash) {
+ SetBodyContent("<input placeholder='placeholder'>abc");
+ UpdateAllLifecyclePhases();
+ Element* input = GetDocument().QuerySelector("input");
+ input->focus();
+ // Do not crash in advanceToNextMisspelling.
+ GetSpellChecker().AdvanceToNextMisspelling(false);
+}
+
+// Regression test for crbug.com/701309
+TEST_F(SpellCheckerTest, AdvanceToNextMisspellingWithImageInTableNoCrash) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "<table><tr><td>"
+ "<img src=foo.jpg>"
+ "</td></tr></table>"
+ "zz zz zz"
+ "</div>");
+ GetDocument().QuerySelector("div")->focus();
+ UpdateAllLifecyclePhases();
+
+ // Do not crash in advanceToNextMisspelling.
+ GetSpellChecker().AdvanceToNextMisspelling(false);
+}
+
+// Regression test for crbug.com/728801
+TEST_F(SpellCheckerTest, AdvancedToNextMisspellingWrapSearchNoCrash) {
+ SetBodyContent("<div contenteditable> zz zz zz </div>");
+
+ Element* div = GetDocument().QuerySelector("div");
+ div->focus();
+ Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::LastPositionInNode(*div))
+ .Build());
+ UpdateAllLifecyclePhases();
+
+ GetSpellChecker().AdvanceToNextMisspelling(false);
+}
+
+TEST_F(SpellCheckerTest, SpellCheckDoesNotCauseUpdateLayout) {
+ SetBodyContent("<input>");
+ HTMLInputElement* input =
+ ToHTMLInputElement(GetDocument().QuerySelector("input"));
+ input->focus();
+ input->setValue("Hello, input field");
+ GetDocument().UpdateStyleAndLayout();
+
+ Position new_position(input->InnerEditorElement()->firstChild(), 3);
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder().Collapse(new_position).Build());
+ ASSERT_EQ(3u, input->selectionStart());
+
+ EXPECT_TRUE(GetSpellChecker().IsSpellCheckingEnabled());
+ ForceLayout();
+ int start_count = LayoutCount();
+ GetSpellChecker().RespondToChangedSelection();
+ EXPECT_EQ(start_count, LayoutCount());
+}
+
+TEST_F(SpellCheckerTest, MarkAndReplaceForHandlesMultipleReplacements) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "spllchck"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+ EphemeralRange range_to_check =
+ EphemeralRange(Position(text, 0), Position(text, 8));
+
+ SpellCheckRequest* request = SpellCheckRequest::Create(range_to_check, 0);
+
+ TextCheckingResult result;
+ result.decoration = TextDecorationType::kTextDecorationTypeSpelling;
+ result.location = 0;
+ result.length = 8;
+ result.replacements = Vector<String>({"spellcheck", "spillchuck"});
+
+ GetDocument().GetFrame()->GetSpellChecker().MarkAndReplaceFor(
+ request, Vector<TextCheckingResult>({result}));
+
+ ASSERT_EQ(1u, GetDocument().Markers().Markers().size());
+
+ // The Spelling marker's description should be a newline-separated list of the
+ // suggested replacements
+ EXPECT_EQ(
+ "spellcheck\nspillchuck",
+ ToSpellCheckMarker(GetDocument().Markers().Markers()[0])->Description());
+}
+
+TEST_F(SpellCheckerTest, GetSpellCheckMarkerUnderSelection_FirstCharSelected) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "spllchck"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ GetDocument().Markers().AddSpellingMarker(
+ EphemeralRange(Position(text, 0), Position(text, 8)));
+
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 0), Position(text, 1))
+ .Build());
+
+ std::pair<Node*, SpellCheckMarker*> result =
+ GetDocument()
+ .GetFrame()
+ ->GetSpellChecker()
+ .GetSpellCheckMarkerUnderSelection();
+ EXPECT_NE(nullptr, result.first);
+}
+
+TEST_F(SpellCheckerTest, GetSpellCheckMarkerUnderSelection_LastCharSelected) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "spllchck"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ GetDocument().Markers().AddSpellingMarker(
+ EphemeralRange(Position(text, 0), Position(text, 8)));
+
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 7), Position(text, 8))
+ .Build());
+
+ std::pair<Node*, SpellCheckMarker*> result =
+ GetDocument()
+ .GetFrame()
+ ->GetSpellChecker()
+ .GetSpellCheckMarkerUnderSelection();
+ EXPECT_NE(nullptr, result.first);
+}
+
+TEST_F(SpellCheckerTest,
+ GetSpellCheckMarkerUnderSelection_SingleCharWordSelected) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "s"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ GetDocument().Markers().AddSpellingMarker(
+ EphemeralRange(Position(text, 0), Position(text, 1)));
+
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 0), Position(text, 1))
+ .Build());
+
+ std::pair<Node*, SpellCheckMarker*> result =
+ GetDocument()
+ .GetFrame()
+ ->GetSpellChecker()
+ .GetSpellCheckMarkerUnderSelection();
+ EXPECT_NE(nullptr, result.first);
+}
+
+TEST_F(SpellCheckerTest,
+ GetSpellCheckMarkerUnderSelection_CaretLeftOfSingleCharWord) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "s"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ GetDocument().Markers().AddSpellingMarker(
+ EphemeralRange(Position(text, 0), Position(text, 1)));
+
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 0), Position(text, 0))
+ .Build());
+
+ std::pair<Node*, SpellCheckMarker*> result =
+ GetDocument()
+ .GetFrame()
+ ->GetSpellChecker()
+ .GetSpellCheckMarkerUnderSelection();
+ EXPECT_EQ(nullptr, result.first);
+}
+
+TEST_F(SpellCheckerTest,
+ GetSpellCheckMarkerUnderSelection_CaretRightOfSingleCharWord) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "s"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ GetDocument().Markers().AddSpellingMarker(
+ EphemeralRange(Position(text, 0), Position(text, 1)));
+
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 1), Position(text, 1))
+ .Build());
+
+ std::pair<Node*, SpellCheckMarker*> result =
+ GetDocument()
+ .GetFrame()
+ ->GetSpellChecker()
+ .GetSpellCheckMarkerUnderSelection();
+ EXPECT_EQ(nullptr, result.first);
+}
+
+TEST_F(SpellCheckerTest,
+ GetSpellCheckMarkerUnderSelection_CaretLeftOfMultiCharWord) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "spllchck"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ GetDocument().Markers().AddSpellingMarker(
+ EphemeralRange(Position(text, 0), Position(text, 8)));
+
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 0), Position(text, 0))
+ .Build());
+
+ std::pair<Node*, SpellCheckMarker*> result =
+ GetDocument()
+ .GetFrame()
+ ->GetSpellChecker()
+ .GetSpellCheckMarkerUnderSelection();
+ EXPECT_EQ(nullptr, result.first);
+}
+
+TEST_F(SpellCheckerTest,
+ GetSpellCheckMarkerUnderSelection_CaretRightOfMultiCharWord) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "spllchck"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ GetDocument().Markers().AddSpellingMarker(
+ EphemeralRange(Position(text, 0), Position(text, 8)));
+
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 8), Position(text, 8))
+ .Build());
+
+ std::pair<Node*, SpellCheckMarker*> result =
+ GetDocument()
+ .GetFrame()
+ ->GetSpellChecker()
+ .GetSpellCheckMarkerUnderSelection();
+ EXPECT_EQ(nullptr, result.first);
+}
+
+TEST_F(SpellCheckerTest, GetSpellCheckMarkerUnderSelection_CaretMiddleOfWord) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "spllchck"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ GetDocument().Markers().AddSpellingMarker(
+ EphemeralRange(Position(text, 0), Position(text, 8)));
+
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 4), Position(text, 4))
+ .Build());
+
+ std::pair<Node*, SpellCheckMarker*> result =
+ GetDocument()
+ .GetFrame()
+ ->GetSpellChecker()
+ .GetSpellCheckMarkerUnderSelection();
+ EXPECT_NE(nullptr, result.first);
+}
+
+TEST_F(SpellCheckerTest,
+ GetSpellCheckMarkerUnderSelection_CaretOneCharLeftOfMisspelling) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "a spllchck"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ GetDocument().Markers().AddSpellingMarker(
+ EphemeralRange(Position(text, 2), Position(text, 10)));
+
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 1), Position(text, 1))
+ .Build());
+
+ std::pair<Node*, SpellCheckMarker*> result =
+ GetDocument()
+ .GetFrame()
+ ->GetSpellChecker()
+ .GetSpellCheckMarkerUnderSelection();
+ EXPECT_EQ(nullptr, result.first);
+}
+
+TEST_F(SpellCheckerTest,
+ GetSpellCheckMarkerUnderSelection_CaretOneCharRightOfMisspelling) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "spllchck a"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ GetDocument().Markers().AddSpellingMarker(
+ EphemeralRange(Position(text, 0), Position(text, 8)));
+
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 9), Position(text, 9))
+ .Build());
+
+ std::pair<Node*, SpellCheckMarker*> result =
+ GetDocument()
+ .GetFrame()
+ ->GetSpellChecker()
+ .GetSpellCheckMarkerUnderSelection();
+ EXPECT_EQ(nullptr, result.first);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/text_checking.h b/chromium/third_party/blink/renderer/core/editing/spellcheck/text_checking.h
new file mode 100644
index 00000000000..0294153f8b7
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/text_checking.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_TEXT_CHECKING_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_TEXT_CHECKING_H_
+
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+#include "third_party/blink/renderer/platform/wtf/vector.h"
+
+namespace blink {
+
+enum TextDecorationType {
+ kTextDecorationTypeSpelling,
+ kTextDecorationTypeGrammar,
+};
+
+struct GrammarDetail {
+ DISALLOW_NEW_EXCEPT_PLACEMENT_NEW();
+ int location;
+ int length;
+ Vector<String> guesses;
+ String user_description;
+};
+
+struct TextCheckingResult {
+ DISALLOW_NEW_EXCEPT_PLACEMENT_NEW();
+ TextDecorationType decoration;
+ int location;
+ int length;
+ Vector<GrammarDetail> details;
+ Vector<String> replacements;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_TEXT_CHECKING_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/text_checking_paragraph.cc b/chromium/third_party/blink/renderer/core/editing/spellcheck/text_checking_paragraph.cc
new file mode 100644
index 00000000000..1fe34f6bf1e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/text_checking_paragraph.cc
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2006, 2007 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/spellcheck/text_checking_paragraph.h"
+
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/editing/iterators/character_iterator.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+
+namespace blink {
+
+TextCheckingParagraph::TextCheckingParagraph(
+ const EphemeralRange& checking_range)
+ : checking_range_(checking_range),
+ checking_start_(-1),
+ checking_end_(-1),
+ checking_length_(-1) {}
+
+TextCheckingParagraph::TextCheckingParagraph(
+ const EphemeralRange& checking_range,
+ const EphemeralRange& paragraph_range)
+ : checking_range_(checking_range),
+ paragraph_range_(paragraph_range),
+ checking_start_(-1),
+ checking_end_(-1),
+ checking_length_(-1) {}
+
+TextCheckingParagraph::TextCheckingParagraph(Range* checking_range,
+ Range* paragraph_range)
+ : checking_range_(checking_range),
+ paragraph_range_(paragraph_range),
+ checking_start_(-1),
+ checking_end_(-1),
+ checking_length_(-1) {}
+
+TextCheckingParagraph::~TextCheckingParagraph() = default;
+
+void TextCheckingParagraph::ExpandRangeToNextEnd() {
+ DCHECK(checking_range_.IsNotNull());
+ SetParagraphRange(
+ EphemeralRange(ParagraphRange().StartPosition(),
+ EndOfParagraph(StartOfNextParagraph(CreateVisiblePosition(
+ ParagraphRange().StartPosition())))
+ .DeepEquivalent()));
+ InvalidateParagraphRangeValues();
+}
+
+void TextCheckingParagraph::InvalidateParagraphRangeValues() {
+ checking_start_ = checking_end_ = -1;
+ offset_as_range_ = EphemeralRange();
+ text_ = String();
+}
+
+int TextCheckingParagraph::RangeLength() const {
+ DCHECK(checking_range_.IsNotNull());
+ return TextIterator::RangeLength(ParagraphRange());
+}
+
+EphemeralRange TextCheckingParagraph::ParagraphRange() const {
+ DCHECK(checking_range_.IsNotNull());
+ if (paragraph_range_.IsNull())
+ paragraph_range_ = ExpandToParagraphBoundary(CheckingRange());
+ return paragraph_range_;
+}
+
+void TextCheckingParagraph::SetParagraphRange(const EphemeralRange& range) {
+ paragraph_range_ = range;
+}
+
+EphemeralRange TextCheckingParagraph::Subrange(int character_offset,
+ int character_count) const {
+ DCHECK(checking_range_.IsNotNull());
+ return CalculateCharacterSubrange(ParagraphRange(), character_offset,
+ character_count);
+}
+
+bool TextCheckingParagraph::IsEmpty() const {
+ // Both predicates should have same result, but we check both just to be sure.
+ // We need to investigate to remove this redundancy.
+ return IsRangeEmpty() || IsTextEmpty();
+}
+
+EphemeralRange TextCheckingParagraph::OffsetAsRange() const {
+ DCHECK(checking_range_.IsNotNull());
+ if (offset_as_range_.IsNotNull())
+ return offset_as_range_;
+ const Position& paragraph_start = ParagraphRange().StartPosition();
+ const Position& checking_start = CheckingRange().StartPosition();
+ if (paragraph_start <= checking_start) {
+ offset_as_range_ = EphemeralRange(paragraph_start, checking_start);
+ return offset_as_range_;
+ }
+ // editing/pasteboard/paste-table-001.html and more reach here.
+ offset_as_range_ = EphemeralRange(checking_start, paragraph_start);
+ return offset_as_range_;
+}
+
+const String& TextCheckingParagraph::GetText() const {
+ DCHECK(checking_range_.IsNotNull());
+ if (text_.IsEmpty())
+ text_ = PlainText(ParagraphRange());
+ return text_;
+}
+
+int TextCheckingParagraph::CheckingStart() const {
+ DCHECK(checking_range_.IsNotNull());
+ if (checking_start_ == -1)
+ checking_start_ = TextIterator::RangeLength(OffsetAsRange());
+ return checking_start_;
+}
+
+int TextCheckingParagraph::CheckingEnd() const {
+ DCHECK(checking_range_.IsNotNull());
+ if (checking_end_ == -1) {
+ checking_end_ =
+ CheckingStart() + TextIterator::RangeLength(CheckingRange());
+ }
+ return checking_end_;
+}
+
+int TextCheckingParagraph::CheckingLength() const {
+ DCHECK(checking_range_.IsNotNull());
+ if (-1 == checking_length_)
+ checking_length_ = TextIterator::RangeLength(
+ CheckingRange().StartPosition(), CheckingRange().EndPosition());
+ return checking_length_;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/spellcheck/text_checking_paragraph.h b/chromium/third_party/blink/renderer/core/editing/spellcheck/text_checking_paragraph.h
new file mode 100644
index 00000000000..451a423169d
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/spellcheck/text_checking_paragraph.h
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_TEXT_CHECKING_PARAGRAPH_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_TEXT_CHECKING_PARAGRAPH_H_
+
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+
+namespace blink {
+
+class Range;
+
+class TextCheckingParagraph {
+ STACK_ALLOCATED();
+
+ public:
+ explicit TextCheckingParagraph(const EphemeralRange& checking_range);
+ TextCheckingParagraph(const EphemeralRange& checking_range,
+ const EphemeralRange& paragraph_range);
+ TextCheckingParagraph(Range* checking_range, Range* paragraph_range);
+ ~TextCheckingParagraph();
+
+ int RangeLength() const;
+ EphemeralRange Subrange(int character_offset, int character_count) const;
+ void ExpandRangeToNextEnd();
+
+ const String& GetText() const;
+ // Why not let clients call these functions on text() themselves?
+ String TextSubstring(unsigned pos, unsigned len = INT_MAX) const {
+ return GetText().Substring(pos, len);
+ }
+ UChar TextCharAt(int index) const {
+ return GetText()[static_cast<unsigned>(index)];
+ }
+
+ bool IsEmpty() const;
+
+ int CheckingStart() const;
+ int CheckingEnd() const;
+ int CheckingLength() const;
+
+ bool CheckingRangeCovers(int location, int length) const {
+ return location < CheckingEnd() && location + length > CheckingStart();
+ }
+ EphemeralRange ParagraphRange() const;
+ void SetParagraphRange(const EphemeralRange&);
+
+ EphemeralRange CheckingRange() const { return checking_range_; }
+
+ private:
+ void InvalidateParagraphRangeValues();
+ EphemeralRange OffsetAsRange() const;
+
+ bool IsTextEmpty() const { return GetText().IsEmpty(); }
+ bool IsRangeEmpty() const { return CheckingStart() >= CheckingEnd(); }
+
+ EphemeralRange checking_range_;
+ mutable EphemeralRange paragraph_range_;
+ mutable EphemeralRange offset_as_range_;
+ mutable String text_;
+ mutable int checking_start_;
+ mutable int checking_end_;
+ mutable int checking_length_;
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SPELLCHECK_TEXT_CHECKING_PARAGRAPH_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/backspace_state_machine.cc b/chromium/third_party/blink/renderer/core/editing/state_machines/backspace_state_machine.cc
new file mode 100644
index 00000000000..f40e2a96c02
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/backspace_state_machine.cc
@@ -0,0 +1,255 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/state_machines/backspace_state_machine.h"
+
+#include <ostream> // NOLINT
+#include "third_party/blink/renderer/platform/text/character.h"
+#include "third_party/blink/renderer/platform/wtf/text/character_names.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+
+namespace blink {
+
+#define FOR_EACH_BACKSPACE_STATE_MACHINE_STATE(V) \
+ /* Initial state */ \
+ V(kStart) \
+ /* The current offset is just before line feed. */ \
+ V(kBeforeLF) \
+ /* The current offset is just before keycap. */ \
+ V(kBeforeKeycap) \
+ /* The current offset is just before variation selector and keycap. */ \
+ V(kBeforeVSAndKeycap) \
+ /* The current offset is just before emoji modifier. */ \
+ V(kBeforeEmojiModifier) \
+ /* The current offset is just before variation selector and emoji*/ \
+ /* modifier. */ \
+ V(kBeforeVSAndEmojiModifier) \
+ /* The current offset is just before variation sequence. */ \
+ V(kBeforeVS) \
+ /* The current offset is just before ZWJ emoji. */ \
+ V(kBeforeZWJEmoji) \
+ /* The current offset is just before ZWJ. */ \
+ V(kBeforeZWJ) \
+ /* The current offset is just before variation selector and ZWJ. */ \
+ V(kBeforeVSAndZWJ) \
+ /* That there are odd numbered RIS from the beggining. */ \
+ V(kOddNumberedRIS) \
+ /* That there are even numbered RIS from the begging. */ \
+ V(kEvenNumberedRIS) \
+ /* This state machine has finished. */ \
+ V(kFinished)
+
+enum class BackspaceStateMachine::BackspaceState {
+#define V(name) name,
+ FOR_EACH_BACKSPACE_STATE_MACHINE_STATE(V)
+#undef V
+};
+
+std::ostream& operator<<(std::ostream& os,
+ BackspaceStateMachine::BackspaceState state) {
+ static const char* const kTexts[] = {
+#define V(name) #name,
+ FOR_EACH_BACKSPACE_STATE_MACHINE_STATE(V)
+#undef V
+ };
+ const auto& it = std::begin(kTexts) + static_cast<size_t>(state);
+ DCHECK_GE(it, std::begin(kTexts)) << "Unknown backspace value";
+ DCHECK_LT(it, std::end(kTexts)) << "Unknown backspace value";
+ return os << *it;
+}
+
+BackspaceStateMachine::BackspaceStateMachine()
+ : state_(BackspaceState::kStart) {}
+
+TextSegmentationMachineState BackspaceStateMachine::FeedPrecedingCodeUnit(
+ UChar code_unit) {
+ DCHECK_NE(BackspaceState::kFinished, state_);
+ uint32_t code_point = code_unit;
+ if (U16_IS_LEAD(code_unit)) {
+ if (trail_surrogate_ == 0) {
+ // Unpaired lead surrogate. Aborting with deleting broken surrogate.
+ ++code_units_to_be_deleted_;
+ return TextSegmentationMachineState::kFinished;
+ }
+ code_point = U16_GET_SUPPLEMENTARY(code_unit, trail_surrogate_);
+ trail_surrogate_ = 0;
+ } else if (U16_IS_TRAIL(code_unit)) {
+ if (trail_surrogate_ != 0) {
+ // Unpaired trail surrogate. Aborting with deleting broken
+ // surrogate.
+ return TextSegmentationMachineState::kFinished;
+ }
+ trail_surrogate_ = code_unit;
+ return TextSegmentationMachineState::kNeedMoreCodeUnit;
+ } else {
+ if (trail_surrogate_ != 0) {
+ // Unpaired trail surrogate. Aborting with deleting broken
+ // surrogate.
+ return TextSegmentationMachineState::kFinished;
+ }
+ }
+
+ switch (state_) {
+ case BackspaceState::kStart:
+ code_units_to_be_deleted_ = U16_LENGTH(code_point);
+ if (code_point == kNewlineCharacter)
+ return MoveToNextState(BackspaceState::kBeforeLF);
+ if (u_hasBinaryProperty(code_point, UCHAR_VARIATION_SELECTOR))
+ return MoveToNextState(BackspaceState::kBeforeVS);
+ if (Character::IsRegionalIndicator(code_point))
+ return MoveToNextState(BackspaceState::kOddNumberedRIS);
+ if (Character::IsModifier(code_point))
+ return MoveToNextState(BackspaceState::kBeforeEmojiModifier);
+ if (Character::IsEmoji(code_point))
+ return MoveToNextState(BackspaceState::kBeforeZWJEmoji);
+ if (code_point == kCombiningEnclosingKeycapCharacter)
+ return MoveToNextState(BackspaceState::kBeforeKeycap);
+ return Finish();
+ case BackspaceState::kBeforeLF:
+ if (code_point == kCarriageReturnCharacter)
+ ++code_units_to_be_deleted_;
+ return Finish();
+ case BackspaceState::kBeforeKeycap:
+ if (u_hasBinaryProperty(code_point, UCHAR_VARIATION_SELECTOR)) {
+ DCHECK_EQ(last_seen_vs_code_units_, 0);
+ last_seen_vs_code_units_ = U16_LENGTH(code_point);
+ return MoveToNextState(BackspaceState::kBeforeVSAndKeycap);
+ }
+ if (Character::IsEmojiKeycapBase(code_point))
+ code_units_to_be_deleted_ += U16_LENGTH(code_point);
+ return Finish();
+ case BackspaceState::kBeforeVSAndKeycap:
+ if (Character::IsEmojiKeycapBase(code_point)) {
+ DCHECK_GT(last_seen_vs_code_units_, 0);
+ DCHECK_LE(last_seen_vs_code_units_, 2);
+ code_units_to_be_deleted_ +=
+ last_seen_vs_code_units_ + U16_LENGTH(code_point);
+ }
+ return Finish();
+ case BackspaceState::kBeforeEmojiModifier:
+ if (u_hasBinaryProperty(code_point, UCHAR_VARIATION_SELECTOR)) {
+ DCHECK_EQ(last_seen_vs_code_units_, 0);
+ last_seen_vs_code_units_ = U16_LENGTH(code_point);
+ return MoveToNextState(BackspaceState::kBeforeVSAndEmojiModifier);
+ }
+ if (Character::IsEmojiModifierBase(code_point))
+ code_units_to_be_deleted_ += U16_LENGTH(code_point);
+ return Finish();
+ case BackspaceState::kBeforeVSAndEmojiModifier:
+ if (Character::IsEmojiModifierBase(code_point)) {
+ DCHECK_GT(last_seen_vs_code_units_, 0);
+ DCHECK_LE(last_seen_vs_code_units_, 2);
+ code_units_to_be_deleted_ +=
+ last_seen_vs_code_units_ + U16_LENGTH(code_point);
+ }
+ return Finish();
+ case BackspaceState::kBeforeVS:
+ if (Character::IsEmoji(code_point)) {
+ code_units_to_be_deleted_ += U16_LENGTH(code_point);
+ return MoveToNextState(BackspaceState::kBeforeZWJEmoji);
+ }
+ if (!u_hasBinaryProperty(code_point, UCHAR_VARIATION_SELECTOR) &&
+ u_getCombiningClass(code_point) == 0)
+ code_units_to_be_deleted_ += U16_LENGTH(code_point);
+ return Finish();
+ case BackspaceState::kBeforeZWJEmoji:
+ return code_point == kZeroWidthJoinerCharacter
+ ? MoveToNextState(BackspaceState::kBeforeZWJ)
+ : Finish();
+ case BackspaceState::kBeforeZWJ:
+ if (Character::IsEmoji(code_point)) {
+ code_units_to_be_deleted_ += U16_LENGTH(code_point) + 1; // +1 for ZWJ
+ return Character::IsModifier(code_point)
+ ? MoveToNextState(BackspaceState::kBeforeEmojiModifier)
+ : MoveToNextState(BackspaceState::kBeforeZWJEmoji);
+ }
+ if (u_hasBinaryProperty(code_point, UCHAR_VARIATION_SELECTOR)) {
+ DCHECK_EQ(last_seen_vs_code_units_, 0);
+ last_seen_vs_code_units_ = U16_LENGTH(code_point);
+ return MoveToNextState(BackspaceState::kBeforeVSAndZWJ);
+ }
+ return Finish();
+ case BackspaceState::kBeforeVSAndZWJ:
+ if (!Character::IsEmoji(code_point))
+ return Finish();
+
+ DCHECK_GT(last_seen_vs_code_units_, 0);
+ DCHECK_LE(last_seen_vs_code_units_, 2);
+ // +1 for ZWJ
+ code_units_to_be_deleted_ +=
+ U16_LENGTH(code_point) + 1 + last_seen_vs_code_units_;
+ last_seen_vs_code_units_ = 0;
+ return MoveToNextState(BackspaceState::kBeforeZWJEmoji);
+ case BackspaceState::kOddNumberedRIS:
+ if (!Character::IsRegionalIndicator(code_point))
+ return Finish();
+ code_units_to_be_deleted_ += 2; // Code units of RIS
+ return MoveToNextState(BackspaceState::kEvenNumberedRIS);
+ case BackspaceState::kEvenNumberedRIS:
+ if (!Character::IsRegionalIndicator(code_point))
+ return Finish();
+ code_units_to_be_deleted_ -= 2; // Code units of RIS
+ return MoveToNextState(BackspaceState::kOddNumberedRIS);
+ case BackspaceState::kFinished:
+ NOTREACHED() << "Do not call feedPrecedingCodeUnit() once it finishes.";
+ break;
+ default:
+ NOTREACHED() << "Unhandled state: " << state_;
+ }
+ NOTREACHED() << "Unhandled state: " << state_;
+ return TextSegmentationMachineState::kInvalid;
+}
+
+TextSegmentationMachineState BackspaceStateMachine::TellEndOfPrecedingText() {
+ if (trail_surrogate_ != 0) {
+ // Unpaired trail surrogate. Removing broken surrogate.
+ ++code_units_to_be_deleted_;
+ trail_surrogate_ = 0;
+ }
+ return TextSegmentationMachineState::kFinished;
+}
+
+TextSegmentationMachineState BackspaceStateMachine::FeedFollowingCodeUnit(
+ UChar code_unit) {
+ NOTREACHED();
+ return TextSegmentationMachineState::kInvalid;
+}
+
+int BackspaceStateMachine::FinalizeAndGetBoundaryOffset() {
+ if (trail_surrogate_ != 0) {
+ // Unpaired trail surrogate. Removing broken surrogate.
+ ++code_units_to_be_deleted_;
+ trail_surrogate_ = 0;
+ }
+ if (state_ != BackspaceState::kFinished) {
+ last_seen_vs_code_units_ = 0;
+ state_ = BackspaceState::kFinished;
+ }
+ return -code_units_to_be_deleted_;
+}
+
+void BackspaceStateMachine::Reset() {
+ code_units_to_be_deleted_ = 0;
+ trail_surrogate_ = 0;
+ state_ = BackspaceState::kStart;
+ last_seen_vs_code_units_ = 0;
+}
+
+TextSegmentationMachineState BackspaceStateMachine::MoveToNextState(
+ BackspaceState new_state) {
+ DCHECK_NE(BackspaceState::kFinished, new_state) << "Use finish() instead.";
+ DCHECK_NE(BackspaceState::kStart, new_state) << "Don't move to Start.";
+ // Below |DCHECK_NE()| prevent us to infinite loop in state machine.
+ DCHECK_NE(state_, new_state) << "State should be changed.";
+ state_ = new_state;
+ return TextSegmentationMachineState::kNeedMoreCodeUnit;
+}
+
+TextSegmentationMachineState BackspaceStateMachine::Finish() {
+ DCHECK_NE(BackspaceState::kFinished, state_);
+ state_ = BackspaceState::kFinished;
+ return TextSegmentationMachineState::kFinished;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/backspace_state_machine.h b/chromium/third_party/blink/renderer/core/editing/state_machines/backspace_state_machine.h
new file mode 100644
index 00000000000..5ae422a1032
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/backspace_state_machine.h
@@ -0,0 +1,72 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_BACKSPACE_STATE_MACHINE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_BACKSPACE_STATE_MACHINE_H_
+
+#include <iosfwd>
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+
+namespace blink {
+
+class CORE_EXPORT BackspaceStateMachine {
+ STACK_ALLOCATED();
+
+ public:
+ BackspaceStateMachine();
+
+ // Prepares by feeding preceding text.
+ // This method must not be called after feedFollowingCodeUnit().
+ TextSegmentationMachineState FeedPrecedingCodeUnit(UChar code_unit);
+
+ // Tells the end of preceding text to the state machine.
+ TextSegmentationMachineState TellEndOfPrecedingText();
+
+ // Find boundary offset by feeding following text.
+ // This method must be called after feedPrecedingCodeUnit() returns
+ // NeedsFollowingCodeUnit.
+ TextSegmentationMachineState FeedFollowingCodeUnit(UChar code_unit);
+
+ // Returns the next boundary offset. This method finalizes the state machine
+ // if it is not finished.
+ int FinalizeAndGetBoundaryOffset();
+
+ // Resets the internal state to the initial state.
+ void Reset();
+
+ private:
+ enum class BackspaceState;
+ friend std::ostream& operator<<(std::ostream&, BackspaceState);
+
+ // Updates the internal state to the |newState| then return
+ // InternalState::NeedMoreCodeUnit.
+ TextSegmentationMachineState MoveToNextState(BackspaceState new_state);
+
+ // Update the internal state to BackspaceState::Finished, then return
+ // MachineState::Finished.
+ TextSegmentationMachineState Finish();
+
+ // Used for composing supplementary code point with surrogate pairs.
+ UChar trail_surrogate_ = 0;
+
+ // The number of code units to be deleted.
+ int code_units_to_be_deleted_ = 0;
+
+ // The length of the previously seen variation selector.
+ int last_seen_vs_code_units_ = 0;
+
+ // The internal state.
+ BackspaceState state_;
+
+ DISALLOW_COPY_AND_ASSIGN(BackspaceStateMachine);
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/backspace_state_machine_test.cc b/chromium/third_party/blink/renderer/core/editing/state_machines/backspace_state_machine_test.cc
new file mode 100644
index 00000000000..0b724da1dea
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/backspace_state_machine_test.cc
@@ -0,0 +1,993 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/state_machines/backspace_state_machine.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+
+namespace blink {
+
+namespace backspace_state_machine_test {
+
+const TextSegmentationMachineState kNeedMoreCodeUnit =
+ TextSegmentationMachineState::kNeedMoreCodeUnit;
+const TextSegmentationMachineState kFinished =
+ TextSegmentationMachineState::kFinished;
+
+TEST(BackspaceStateMachineTest, DoNothingCase) {
+ BackspaceStateMachine machine;
+ EXPECT_EQ(0, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(0, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST(BackspaceStateMachineTest, SingleCharacter) {
+ BackspaceStateMachine machine;
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit('a'));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ machine.Reset();
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit('-'));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ machine.Reset();
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit('\t'));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ machine.Reset();
+ // U+3042 HIRAGANA LETTER A.
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(0x3042));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST(BackspaceStateMachineTest, SurrogatePair) {
+ BackspaceStateMachine machine;
+
+ // U+20BB7 is \uD83D\uDDFA in UTF-16.
+ const UChar kLeadSurrogate = 0xD842;
+ const UChar kTrailSurrogate = 0xDFB7;
+
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kTrailSurrogate));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kLeadSurrogate));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Edge cases
+ // Unpaired trailing surrogate. Delete only broken trail surrogate.
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kTrailSurrogate));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kTrailSurrogate));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit('a'));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kTrailSurrogate));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kTrailSurrogate));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // Unpaired leading surrogate. Delete only broken lead surrogate.
+ machine.Reset();
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kLeadSurrogate));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST(BackspaceStateMachineTest, CRLF) {
+ BackspaceStateMachine machine;
+
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit('\r'));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit('\n'));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit('\n'));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(' '));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // CR LF should be deleted at the same time.
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit('\n'));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit('\r'));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST(BackspaceStateMachineTest, KeyCap) {
+ BackspaceStateMachine machine;
+
+ const UChar kKeycap = 0x20E3;
+ const UChar kVs16 = 0xFE0F;
+ const UChar kNotKeycapBaseLead = 0xD83C;
+ const UChar kNotKeycapBaseTrail = 0xDCCF;
+
+ // keycapBase + keycap
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kKeycap));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit('0'));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // keycapBase + VS + keycap
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kKeycap));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kVs16));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit('0'));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+
+ // Followings are edge cases. Remove only keycap character.
+ // Not keycapBase + keycap
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kKeycap));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit('a'));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not keycapBase + VS + keycap
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kKeycap));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kVs16));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit('a'));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not keycapBase(surrogate pair) + keycap
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kKeycap));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kNotKeycapBaseTrail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kNotKeycapBaseLead));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not keycapBase(surrogate pair) + VS + keycap
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kKeycap));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kVs16));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kNotKeycapBaseTrail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kNotKeycapBaseLead));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + keycap
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kKeycap));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + VS + keycap
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kKeycap));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kVs16));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST(BackspaceStateMachineTest, EmojiModifier) {
+ BackspaceStateMachine machine;
+
+ const UChar kEmojiModifierLead = 0xD83C;
+ const UChar kEmojiModifierTrail = 0xDFFB;
+ const UChar kEmojiModifierBase = 0x261D;
+ const UChar kEmojiModifierBaseLead = 0xD83D;
+ const UChar kEmojiModifierBaseTrail = 0xDC66;
+ const UChar kNotEmojiModifierBaseLead = 0xD83C;
+ const UChar kNotEmojiModifierBaseTrail = 0xDCCF;
+ const UChar kVs16 = 0xFE0F;
+
+ // EMOJI_MODIFIER_BASE + EMOJI_MODIFIER
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierLead));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kEmojiModifierBase));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+
+ // EMOJI_MODIFIER_BASE(surrogate pairs) + EMOJI_MODIFIER
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierLead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierBaseTrail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kEmojiModifierBaseLead));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // EMOJI_MODIFIER_BASE + VS + EMOJI_MODIFIER
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kVs16));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kEmojiModifierBase));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // EMOJI_MODIFIER_BASE(surrogate pairs) + VS + EMOJI_MODIFIER
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kVs16));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierBaseTrail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kEmojiModifierBaseLead));
+ EXPECT_EQ(-5, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-5, machine.FinalizeAndGetBoundaryOffset());
+
+ // Followings are edge cases. Remove only emoji modifier.
+ // Not EMOJI_MODIFIER_BASE + EMOJI_MODIFIER
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierLead));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit('a'));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not EMOJI_MODIFIER_BASE(surrogate pairs) + EMOJI_MODIFIER
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierLead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kNotEmojiModifierBaseTrail));
+ EXPECT_EQ(kFinished,
+ machine.FeedPrecedingCodeUnit(kNotEmojiModifierBaseLead));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not EMOJI_MODIFIER_BASE + VS + EMOJI_MODIFIER
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kVs16));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit('a'));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not EMOJI_MODIFIER_BASE(surrogate pairs) + VS + EMOJI_MODIFIER
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kVs16));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kNotEmojiModifierBaseTrail));
+ EXPECT_EQ(kFinished,
+ machine.FeedPrecedingCodeUnit(kNotEmojiModifierBaseLead));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + EMOJI_MODIFIER
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierLead));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + VS + EMOJI_MODIFIER
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kVs16));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST(BackspaceStateMachineTest, RegionalIndicator) {
+ BackspaceStateMachine machine;
+
+ const UChar kRegionalIndicatorULead = 0xD83C;
+ const UChar kRegionalIndicatorUTrail = 0xDDFA;
+ const UChar kRegionalIndicatorSLead = 0xD83C;
+ const UChar kRegionalIndicatorSTrail = 0xDDF8;
+ const UChar kNotRegionalIndicatorLead = 0xD83C;
+ const UChar kNotRegionalIndicatorTrail = 0xDCCF;
+
+ // Not RI + RI + RI
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSLead));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit('a'));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not RI(surrogate pairs) + RI + RI
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSLead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kNotRegionalIndicatorTrail));
+ EXPECT_EQ(kFinished,
+ machine.FeedPrecedingCodeUnit(kNotRegionalIndicatorLead));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + RI + RI
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSLead));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not RI + RI + RI + RI + RI
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSLead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSLead));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit('a'));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not RI(surrogate pairs) + RI + RI + RI + RI
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSLead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSLead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kNotRegionalIndicatorTrail));
+ EXPECT_EQ(kFinished,
+ machine.FeedPrecedingCodeUnit(kNotRegionalIndicatorLead));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + RI + RI + RI + RI
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSLead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSLead));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // Followings are edge cases. Delete last regional indicator only.
+ // Not RI + RI
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit('a'));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not RI(surrogate pairs) + RI
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kNotRegionalIndicatorTrail));
+ EXPECT_EQ(kFinished,
+ machine.FeedPrecedingCodeUnit(kNotRegionalIndicatorLead));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + RI
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not RI + RI + RI + RI
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSLead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit('a'));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not RI(surrogate pairs) + RI + RI + RI
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSLead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kNotRegionalIndicatorTrail));
+ EXPECT_EQ(kFinished,
+ machine.FeedPrecedingCodeUnit(kNotRegionalIndicatorLead));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + RI + RI + RI
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorSLead));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorUTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kRegionalIndicatorULead));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST(BackspaceStateMachineTest, VariationSequencec) {
+ BackspaceStateMachine machine;
+
+ UChar vs01 = 0xFE00;
+ UChar vs01_base = 0xA85E;
+ UChar vs01_base_lead = 0xD802;
+ UChar vs01_base_trail = 0xDEC6;
+
+ UChar vs17_lead = 0xDB40;
+ UChar vs17_trail = 0xDD00;
+ UChar vs17_base = 0x3402;
+ UChar vs17_base_lead = 0xD841;
+ UChar vs17_base_trail = 0xDC8C;
+
+ UChar mongolian_vs = 0x180B;
+ UChar mongolian_vs_base = 0x1820;
+ // Variation selectors can't be a base of variation sequence.
+ UChar notvs_base = 0xFE00;
+ UChar notvs_base_lead = 0xDB40;
+ UChar notvs_base_trail = 0xDD01;
+
+ // VS_BASE + VS
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs01));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(vs01_base));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // VS_BASE + VS(surrogate pairs)
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs17_trail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs17_lead));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(vs17_base));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+
+ // VS_BASE(surrogate pairs) + VS
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs01));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs01_base_trail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(vs01_base_lead));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+
+ // VS_BASE(surrogate pairs) + VS(surrogate pairs)
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs17_trail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs17_lead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs17_base_trail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(vs17_base_lead));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // mongolianVsBase + mongolianVs
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(mongolian_vs));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(mongolian_vs_base));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Followings are edge case. Delete only variation selector.
+ // Not VS_BASE + VS
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs01));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(notvs_base));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not VS_BASE + VS(surrogate pairs)
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs17_trail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs17_lead));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(notvs_base));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not VS_BASE(surrogate pairs) + VS
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs01));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(notvs_base_trail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(notvs_base_lead));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not VS_BASE(surrogate pairs) + VS(surrogate pairs)
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs17_trail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs17_lead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(notvs_base_trail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(notvs_base_lead));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not VS_BASE + MONGOLIAN_VS
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(mongolian_vs));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(notvs_base));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // Not VS_BASE(surrogate pairs) + MONGOLIAN_VS
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(mongolian_vs));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(notvs_base_trail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(notvs_base_lead));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + VS
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs01));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + VS(surrogate pair)
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs17_trail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(vs17_lead));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + MONGOLIAN_VS
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(mongolian_vs));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST(BackspaceStateMachineTest, ZWJSequence) {
+ BackspaceStateMachine machine;
+
+ const UChar kZwj = 0x200D;
+ const UChar kEyeLead = 0xD83D;
+ const UChar kEyeTrail = 0xDC41;
+ const UChar kLeftSpeachBubbleLead = 0xD83D;
+ const UChar kLeftSpeachBubbleTrail = 0xDDE8;
+ const UChar kManLead = 0xD83D;
+ const UChar kManTrail = 0xDC68;
+ const UChar kBoyLead = 0xD83D;
+ const UChar kBoyTrail = 0xDC66;
+ const UChar kHeart = 0x2764;
+ const UChar kKissLead = 0xD83D;
+ const UChar kKillTrail = 0xDC8B;
+ const UChar kVs16 = 0xFE0F;
+ const UChar kOther = 'a';
+ const UChar kOtherLead = 0xD83C;
+ const UChar kOtherTrail = 0xDCCF;
+
+ // Followings are chosen from valid zwj sequcne.
+ // See http://www.unicode.org/Public/emoji/2.0//emoji-zwj-sequences.txt
+
+ // others + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI
+ // As an example, use EYE + ZWJ + LEFT_SPEACH_BUBBLE
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kLeftSpeachBubbleTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kLeftSpeachBubbleLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kEyeTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kEyeLead));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kOther));
+ EXPECT_EQ(-5, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-5, machine.FinalizeAndGetBoundaryOffset());
+
+ // others(surrogate pairs) + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI
+ // As an example, use EYE + ZWJ + LEFT_SPEACH_BUBBLE
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kLeftSpeachBubbleTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kLeftSpeachBubbleLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kEyeTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kEyeLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kOtherTrail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kOtherLead));
+ EXPECT_EQ(-5, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-5, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI
+ // As an example, use EYE + ZWJ + LEFT_SPEACH_BUBBLE
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kLeftSpeachBubbleTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kLeftSpeachBubbleLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kEyeTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kEyeLead));
+ EXPECT_EQ(-5, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-5, machine.FinalizeAndGetBoundaryOffset());
+
+ // others + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI
+ // As an example, use MAN + ZWJ + heart + ZWJ + MAN
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kHeart));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kOther));
+ EXPECT_EQ(-7, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-7, machine.FinalizeAndGetBoundaryOffset());
+
+ // others(surrogate pairs) + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI
+ // As an example, use MAN + ZWJ + heart + ZWJ + MAN
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kHeart));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kOtherTrail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kOtherLead));
+ EXPECT_EQ(-7, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-7, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI
+ // As an example, use MAN + ZWJ + heart + ZWJ + MAN
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kHeart));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(-7, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-7, machine.FinalizeAndGetBoundaryOffset());
+
+ // others + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI + VS + ZWJ + ZWJ_EMOJI
+ // As an example, use MAN + ZWJ + heart + vs16 + ZWJ + MAN
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kVs16));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kHeart));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kOther));
+ EXPECT_EQ(-8, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-8, machine.FinalizeAndGetBoundaryOffset());
+
+ // others(surrogate pairs) + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI + VS + ZWJ +
+ // ZWJ_EMOJI
+ // As an example, use MAN + ZWJ + heart + vs16 + ZWJ + MAN
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kVs16));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kHeart));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kOtherTrail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kOtherLead));
+ EXPECT_EQ(-8, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-8, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI + VS + ZWJ + ZWJ_EMOJI
+ // As an example, use MAN + ZWJ + heart + vs16 + ZWJ + MAN
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kVs16));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kHeart));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(-8, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-8, machine.FinalizeAndGetBoundaryOffset());
+
+ // others + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI
+ // As an example, use MAN + ZWJ + MAN + ZWJ + boy + ZWJ + BOY
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kBoyTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kBoyLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kBoyTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kBoyLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kOther));
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+
+ // others(surrogate pairs) + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI +
+ // ZWJ + ZWJ_EMOJI
+ // As an example, use MAN + ZWJ + MAN + ZWJ + boy + ZWJ + BOY
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kBoyTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kBoyLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kBoyTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kBoyLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kOtherTrail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kOtherLead));
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI
+ // As an example, use MAN + ZWJ + MAN + ZWJ + boy + ZWJ + BOY
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kBoyTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kBoyLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kBoyTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kBoyLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+
+ // others + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI + VS + ZWJ + ZWJ_EMOJI + ZWJ +
+ // ZWJ_EMOJI
+ // As an example, use MAN + ZWJ + heart + VS + ZWJ + KISS + ZWJ + MAN
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kKillTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kKissLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kVs16));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kHeart));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kOther));
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+
+ // others(surrogate pairs) + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI + VS + ZWJ +
+ // ZWJ_EMOJI + ZWJ + ZWJ_EMOJI
+ // As an example, use MAN + ZWJ + heart + VS + ZWJ + KISS + ZWJ + MAN
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kKillTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kKissLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kVs16));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kHeart));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kOtherTrail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kOtherLead));
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI + VS + ZWJ + ZWJ_EMOJI + ZWJ + ZWJ_EMOJI
+ // As an example, use MAN + ZWJ + heart + VS + ZWJ + KISS + ZWJ + MAN
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kKillTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kKissLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kVs16));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kHeart));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + EMOJI_MODIFIER_BASE + EMOJI_MODIFIER + ZWJ + ZWJ_EMOJI
+ // As an example, use WOMAN + MODIFIER + ZWJ + BRIEFCASE
+ const UChar kWomanLead = 0xD83D;
+ const UChar kWomanTrail = 0xDC69;
+ const UChar kEmojiModifierLead = 0xD83C;
+ const UChar kEmojiModifierTrail = 0xDFFB;
+ const UChar kBriefcaseLead = 0xD83D;
+ const UChar kBriefcaseTrail = 0xDCBC;
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kBriefcaseTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kBriefcaseLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kEmojiModifierLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kWomanTrail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kWomanLead));
+ EXPECT_EQ(-7, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-7, machine.FinalizeAndGetBoundaryOffset());
+
+ // Followings are not edge cases but good to check.
+ // If leading character is not zwj, delete only ZWJ_EMOJI.
+ // other + ZWJ_EMOJI
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kHeart));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kOther));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // other(surrogate pairs) + ZWJ_EMOJI
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kHeart));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kOtherTrail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kOtherLead));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + ZWJ_EMOJI
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kHeart));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // other + ZWJ_EMOJI(surrogate pairs)
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kOther));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // other(surrogate pairs) + ZWJ_EMOJI(surrogate pairs)
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kOtherTrail));
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kOtherLead));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Sot + ZWJ_EMOJI(surrogate pairs)
+ machine.Reset();
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManTrail));
+ EXPECT_EQ(kNeedMoreCodeUnit, machine.FeedPrecedingCodeUnit(kManLead));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Followings are edge case.
+ // It is hard to list all edge case patterns. Check only over deleting by ZWJ.
+ // any + ZWJ: should delete only last ZWJ.
+ machine.Reset();
+ EXPECT_EQ(kFinished, machine.FeedPrecedingCodeUnit(kZwj));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+}
+
+} // namespace backspace_state_machine_test
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/backward_code_point_state_machine.cc b/chromium/third_party/blink/renderer/core/editing/state_machines/backward_code_point_state_machine.cc
new file mode 100644
index 00000000000..99bca785c64
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/backward_code_point_state_machine.cc
@@ -0,0 +1,69 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/state_machines/backward_code_point_state_machine.h"
+
+namespace blink {
+
+enum class BackwardCodePointStateMachine::BackwardCodePointState {
+ kNotSurrogate,
+ kTrailSurrogate,
+ kInvalid,
+};
+
+BackwardCodePointStateMachine::BackwardCodePointStateMachine()
+ : state_(BackwardCodePointState::kNotSurrogate) {}
+
+TextSegmentationMachineState
+BackwardCodePointStateMachine::FeedPrecedingCodeUnit(UChar code_unit) {
+ switch (state_) {
+ case BackwardCodePointState::kNotSurrogate:
+ if (U16_IS_LEAD(code_unit)) {
+ code_units_to_be_deleted_ = 0;
+ state_ = BackwardCodePointState::kInvalid;
+ return TextSegmentationMachineState::kInvalid;
+ }
+ ++code_units_to_be_deleted_;
+ if (U16_IS_TRAIL(code_unit)) {
+ state_ = BackwardCodePointState::kTrailSurrogate;
+ return TextSegmentationMachineState::kNeedMoreCodeUnit;
+ }
+ return TextSegmentationMachineState::kFinished;
+ case BackwardCodePointState::kTrailSurrogate:
+ if (U16_IS_LEAD(code_unit)) {
+ ++code_units_to_be_deleted_;
+ state_ = BackwardCodePointState::kNotSurrogate;
+ return TextSegmentationMachineState::kFinished;
+ }
+ code_units_to_be_deleted_ = 0;
+ state_ = BackwardCodePointState::kInvalid;
+ return TextSegmentationMachineState::kInvalid;
+ case BackwardCodePointState::kInvalid:
+ code_units_to_be_deleted_ = 0;
+ return TextSegmentationMachineState::kInvalid;
+ }
+ NOTREACHED();
+ return TextSegmentationMachineState::kInvalid;
+}
+
+TextSegmentationMachineState
+BackwardCodePointStateMachine::FeedFollowingCodeUnit(UChar code_unit) {
+ NOTREACHED();
+ return TextSegmentationMachineState::kInvalid;
+}
+
+bool BackwardCodePointStateMachine::AtCodePointBoundary() {
+ return state_ == BackwardCodePointState::kNotSurrogate;
+}
+
+int BackwardCodePointStateMachine::GetBoundaryOffset() {
+ return -code_units_to_be_deleted_;
+}
+
+void BackwardCodePointStateMachine::Reset() {
+ code_units_to_be_deleted_ = 0;
+ state_ = BackwardCodePointState::kNotSurrogate;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/backward_code_point_state_machine.h b/chromium/third_party/blink/renderer/core/editing/state_machines/backward_code_point_state_machine.h
new file mode 100644
index 00000000000..8a6be8d9b7f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/backward_code_point_state_machine.h
@@ -0,0 +1,53 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_BACKWARD_CODE_POINT_STATE_MACHINE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_BACKWARD_CODE_POINT_STATE_MACHINE_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+
+namespace blink {
+
+class CORE_EXPORT BackwardCodePointStateMachine {
+ STACK_ALLOCATED();
+
+ public:
+ BackwardCodePointStateMachine();
+ ~BackwardCodePointStateMachine() = default;
+
+ // Prepares by feeding preceding text.
+ TextSegmentationMachineState FeedPrecedingCodeUnit(UChar code_unit);
+
+ // Finds boundary offset by feeding following text.
+ TextSegmentationMachineState FeedFollowingCodeUnit(UChar code_unit);
+
+ // Returns true if we are at code point boundary.
+ bool AtCodePointBoundary();
+
+ // Returns the next boundary offset.
+ int GetBoundaryOffset();
+
+ // Resets the internal state to the initial state.
+ void Reset();
+
+ private:
+ enum class BackwardCodePointState;
+
+ // The number of code units to be deleted.
+ // Nothing to delete if there is an invalid surrogate pair.
+ int code_units_to_be_deleted_ = 0;
+
+ // The internal state.
+ BackwardCodePointState state_;
+
+ DISALLOW_COPY_AND_ASSIGN(BackwardCodePointStateMachine);
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/backward_code_point_state_machine_test.cc b/chromium/third_party/blink/renderer/core/editing/state_machines/backward_code_point_state_machine_test.cc
new file mode 100644
index 00000000000..beb7ef1c91e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/backward_code_point_state_machine_test.cc
@@ -0,0 +1,79 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/state_machines/backward_code_point_state_machine.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace blink {
+
+namespace backward_code_point_state_machine_test {
+
+TEST(BackwardCodePointStateMachineTest, DoNothingCase) {
+ BackwardCodePointStateMachine machine;
+ EXPECT_EQ(0, machine.GetBoundaryOffset());
+}
+
+TEST(BackwardCodePointStateMachineTest, SingleCharacter) {
+ BackwardCodePointStateMachine machine;
+ EXPECT_EQ(TextSegmentationMachineState::kFinished,
+ machine.FeedPrecedingCodeUnit('a'));
+ EXPECT_EQ(-1, machine.GetBoundaryOffset());
+
+ machine.Reset();
+ EXPECT_EQ(TextSegmentationMachineState::kFinished,
+ machine.FeedPrecedingCodeUnit('-'));
+ EXPECT_EQ(-1, machine.GetBoundaryOffset());
+
+ machine.Reset();
+ EXPECT_EQ(TextSegmentationMachineState::kFinished,
+ machine.FeedPrecedingCodeUnit('\t'));
+ EXPECT_EQ(-1, machine.GetBoundaryOffset());
+
+ machine.Reset();
+ // U+3042 HIRAGANA LETTER A.
+ EXPECT_EQ(TextSegmentationMachineState::kFinished,
+ machine.FeedPrecedingCodeUnit(0x3042));
+ EXPECT_EQ(-1, machine.GetBoundaryOffset());
+}
+
+TEST(BackwardCodePointStateMachineTest, SurrogatePair) {
+ BackwardCodePointStateMachine machine;
+
+ // U+20BB7 is \uD83D\uDDFA in UTF-16.
+ const UChar kLeadSurrogate = 0xD842;
+ const UChar kTrailSurrogate = 0xDFB7;
+
+ EXPECT_EQ(TextSegmentationMachineState::kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kTrailSurrogate));
+ EXPECT_EQ(TextSegmentationMachineState::kFinished,
+ machine.FeedPrecedingCodeUnit(kLeadSurrogate));
+ EXPECT_EQ(-2, machine.GetBoundaryOffset());
+
+ // Edge cases
+ // Unpaired trailing surrogate. Nothing to delete.
+ machine.Reset();
+ EXPECT_EQ(TextSegmentationMachineState::kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kTrailSurrogate));
+ EXPECT_EQ(TextSegmentationMachineState::kInvalid,
+ machine.FeedPrecedingCodeUnit('a'));
+ EXPECT_EQ(0, machine.GetBoundaryOffset());
+
+ machine.Reset();
+ EXPECT_EQ(TextSegmentationMachineState::kNeedMoreCodeUnit,
+ machine.FeedPrecedingCodeUnit(kTrailSurrogate));
+ EXPECT_EQ(TextSegmentationMachineState::kInvalid,
+ machine.FeedPrecedingCodeUnit(kTrailSurrogate));
+ EXPECT_EQ(0, machine.GetBoundaryOffset());
+
+ // Unpaired leading surrogate. Nothing to delete.
+ machine.Reset();
+ EXPECT_EQ(TextSegmentationMachineState::kInvalid,
+ machine.FeedPrecedingCodeUnit(kLeadSurrogate));
+ EXPECT_EQ(0, machine.GetBoundaryOffset());
+}
+
+} // namespace backward_code_point_state_machine_test
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/backward_grapheme_boundary_state_machine.cc b/chromium/third_party/blink/renderer/core/editing/state_machines/backward_grapheme_boundary_state_machine.cc
new file mode 100644
index 00000000000..5f89fd97cfc
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/backward_grapheme_boundary_state_machine.cc
@@ -0,0 +1,242 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/state_machines/backward_grapheme_boundary_state_machine.h"
+
+#include <ostream> // NOLINT
+#include "third_party/blink/renderer/core/editing/state_machines/state_machine_util.h"
+#include "third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.h"
+#include "third_party/blink/renderer/platform/text/character.h"
+#include "third_party/blink/renderer/platform/wtf/text/character_names.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+
+namespace blink {
+
+namespace {
+const UChar32 kInvalidCodePoint = WTF::Unicode::kMaxCodepoint + 1;
+} // namespace
+
+#define FOR_EACH_BACKWARD_GRAPHEME_BOUNDARY_STATE(V) \
+ /* Initial state */ \
+ V(kStart) \
+ /* Wating lead surrogate during initial state. */ \
+ V(kStartWaitLeadSurrogate) \
+ /* Searching grapheme boundary. */ \
+ V(kSearch) \
+ /* Waiting lead surrogate during searching grapheme boundary. */ \
+ V(kSearchWaitLeadSurrogate) \
+ /* Counting preceding regional indicators. */ \
+ V(kCountRIS) \
+ /* Wating lead surrogate during counting preceding regional indicators. */ \
+ V(kCountRISWaitLeadSurrogate) \
+ /* The state machine has stopped. */ \
+ V(kFinished)
+
+enum class BackwardGraphemeBoundaryStateMachine::InternalState {
+#define V(name) name,
+ FOR_EACH_BACKWARD_GRAPHEME_BOUNDARY_STATE(V)
+#undef V
+};
+
+std::ostream& operator<<(
+ std::ostream& os,
+ BackwardGraphemeBoundaryStateMachine::InternalState state) {
+ static const char* const kTexts[] = {
+#define V(name) #name,
+ FOR_EACH_BACKWARD_GRAPHEME_BOUNDARY_STATE(V)
+#undef V
+ };
+ const auto& it = std::begin(kTexts) + static_cast<size_t>(state);
+ DCHECK_GE(it, std::begin(kTexts)) << "Unknown state value";
+ DCHECK_LT(it, std::end(kTexts)) << "Unknown state value";
+ return os << *it;
+}
+
+BackwardGraphemeBoundaryStateMachine::BackwardGraphemeBoundaryStateMachine()
+ : next_code_point_(kInvalidCodePoint),
+ internal_state_(InternalState::kStart) {}
+
+TextSegmentationMachineState
+BackwardGraphemeBoundaryStateMachine::FeedPrecedingCodeUnit(UChar code_unit) {
+ switch (internal_state_) {
+ case InternalState::kStart:
+ DCHECK_EQ(trail_surrogate_, 0);
+ DCHECK_EQ(next_code_point_, kInvalidCodePoint);
+ DCHECK_EQ(boundary_offset_, 0);
+ DCHECK_EQ(preceding_ris_count_, 0);
+ if (U16_IS_TRAIL(code_unit)) {
+ trail_surrogate_ = code_unit;
+ return MoveToNextState(InternalState::kStartWaitLeadSurrogate);
+ }
+ if (U16_IS_LEAD(code_unit)) {
+ // Lonely lead surrogate. Move to previous offset.
+ boundary_offset_ = -1;
+ return Finish();
+ }
+ next_code_point_ = code_unit;
+ boundary_offset_ -= 1;
+ return MoveToNextState(InternalState::kSearch);
+ case InternalState::kStartWaitLeadSurrogate:
+ DCHECK_NE(trail_surrogate_, 0);
+ DCHECK_EQ(next_code_point_, kInvalidCodePoint);
+ DCHECK_EQ(boundary_offset_, 0);
+ DCHECK_EQ(preceding_ris_count_, 0);
+ if (!U16_IS_LEAD(code_unit)) {
+ // Lonely trail surrogate. Move to previous offset.
+ boundary_offset_ = -1;
+ return Finish();
+ }
+ next_code_point_ = U16_GET_SUPPLEMENTARY(code_unit, trail_surrogate_);
+ boundary_offset_ = -2;
+ trail_surrogate_ = 0;
+ return MoveToNextState(InternalState::kSearch);
+ case InternalState::kSearch:
+ DCHECK_EQ(trail_surrogate_, 0);
+ DCHECK_NE(next_code_point_, kInvalidCodePoint);
+ DCHECK_LT(boundary_offset_, 0);
+ DCHECK_EQ(preceding_ris_count_, 0);
+ if (U16_IS_TRAIL(code_unit)) {
+ DCHECK_EQ(trail_surrogate_, 0);
+ trail_surrogate_ = code_unit;
+ return MoveToNextState(InternalState::kSearchWaitLeadSurrogate);
+ }
+ if (U16_IS_LEAD(code_unit))
+ return Finish(); // Lonely lead surrogate.
+ if (IsGraphemeBreak(code_unit, next_code_point_))
+ return Finish();
+ next_code_point_ = code_unit;
+ boundary_offset_ -= 1;
+ return StaySameState();
+ case InternalState::kSearchWaitLeadSurrogate:
+ DCHECK_NE(trail_surrogate_, 0);
+ DCHECK_NE(next_code_point_, kInvalidCodePoint);
+ DCHECK_LT(boundary_offset_, 0);
+ DCHECK_EQ(preceding_ris_count_, 0);
+ if (!U16_IS_LEAD(code_unit))
+ return Finish(); // Lonely trail surrogate.
+ {
+ const UChar32 code_point =
+ U16_GET_SUPPLEMENTARY(code_unit, trail_surrogate_);
+ trail_surrogate_ = 0;
+ if (Character::IsRegionalIndicator(next_code_point_) &&
+ Character::IsRegionalIndicator(code_point)) {
+ preceding_ris_count_ = 1;
+ return MoveToNextState(InternalState::kCountRIS);
+ }
+ if (IsGraphemeBreak(code_point, next_code_point_))
+ return Finish();
+ next_code_point_ = code_point;
+ boundary_offset_ -= 2;
+ return MoveToNextState(InternalState::kSearch);
+ }
+ case InternalState::kCountRIS:
+ DCHECK_EQ(trail_surrogate_, 0);
+ DCHECK(Character::IsRegionalIndicator(next_code_point_));
+ DCHECK_LT(boundary_offset_, 0);
+ DCHECK_GT(preceding_ris_count_, 0);
+ if (U16_IS_TRAIL(code_unit)) {
+ DCHECK_EQ(trail_surrogate_, 0);
+ trail_surrogate_ = code_unit;
+ return MoveToNextState(InternalState::kCountRISWaitLeadSurrogate);
+ }
+ if (preceding_ris_count_ % 2 != 0)
+ boundary_offset_ -= 2;
+ return Finish();
+ case InternalState::kCountRISWaitLeadSurrogate:
+ DCHECK_NE(trail_surrogate_, 0);
+ DCHECK(Character::IsRegionalIndicator(next_code_point_));
+ DCHECK_LT(boundary_offset_, 0);
+ DCHECK_GT(preceding_ris_count_, 0);
+ if (U16_IS_LEAD(code_unit)) {
+ DCHECK_NE(trail_surrogate_, 0);
+ const UChar32 code_point =
+ U16_GET_SUPPLEMENTARY(code_unit, trail_surrogate_);
+ trail_surrogate_ = 0;
+ if (Character::IsRegionalIndicator(code_point)) {
+ ++preceding_ris_count_;
+ return MoveToNextState(InternalState::kCountRIS);
+ }
+ }
+ if (preceding_ris_count_ % 2 != 0)
+ boundary_offset_ -= 2;
+ return Finish();
+ case InternalState::kFinished:
+ NOTREACHED() << "Do not call feedPrecedingCodeUnit() once it finishes.";
+ }
+ NOTREACHED() << "Unhandled state: " << internal_state_;
+ return Finish();
+}
+
+TextSegmentationMachineState
+BackwardGraphemeBoundaryStateMachine::TellEndOfPrecedingText() {
+ switch (internal_state_) {
+ case InternalState::kStart:
+ // Did nothing.
+ DCHECK_EQ(boundary_offset_, 0);
+ return Finish();
+ case InternalState::kStartWaitLeadSurrogate:
+ // Lonely trail surrogate. Move to before of it.
+ DCHECK_EQ(boundary_offset_, 0);
+ boundary_offset_ = -1;
+ return Finish();
+ case InternalState::kSearch: // fallthrough
+ case InternalState::kSearchWaitLeadSurrogate:
+ return Finish();
+ case InternalState::kCountRIS: // fallthrough
+ case InternalState::kCountRISWaitLeadSurrogate:
+ DCHECK_GT(preceding_ris_count_, 0);
+ if (preceding_ris_count_ % 2 != 0)
+ boundary_offset_ -= 2;
+ return Finish();
+ case InternalState::kFinished:
+ NOTREACHED() << "Do not call tellEndOfPrecedingText() once it finishes.";
+ }
+ NOTREACHED() << "Unhandled state: " << internal_state_;
+ return Finish();
+}
+
+TextSegmentationMachineState
+BackwardGraphemeBoundaryStateMachine::FeedFollowingCodeUnit(UChar code_unit) {
+ NOTREACHED();
+ return TextSegmentationMachineState::kInvalid;
+}
+
+int BackwardGraphemeBoundaryStateMachine::FinalizeAndGetBoundaryOffset() {
+ if (internal_state_ != InternalState::kFinished)
+ TellEndOfPrecedingText();
+ DCHECK_LE(boundary_offset_, 0);
+ return boundary_offset_;
+}
+
+TextSegmentationMachineState
+BackwardGraphemeBoundaryStateMachine::MoveToNextState(
+ InternalState next_state) {
+ DCHECK_NE(next_state, InternalState::kFinished) << "Use finish() instead";
+ DCHECK_NE(next_state, InternalState::kStart) << "Unable to move to Start";
+ DCHECK_NE(internal_state_, next_state) << "Use staySameState() instead.";
+ internal_state_ = next_state;
+ return TextSegmentationMachineState::kNeedMoreCodeUnit;
+}
+
+TextSegmentationMachineState
+BackwardGraphemeBoundaryStateMachine::StaySameState() {
+ DCHECK_EQ(internal_state_, InternalState::kSearch) << "Only Search can stay.";
+ return TextSegmentationMachineState::kNeedMoreCodeUnit;
+}
+
+TextSegmentationMachineState BackwardGraphemeBoundaryStateMachine::Finish() {
+ DCHECK_NE(internal_state_, InternalState::kFinished);
+ internal_state_ = InternalState::kFinished;
+ return TextSegmentationMachineState::kFinished;
+}
+
+void BackwardGraphemeBoundaryStateMachine::Reset() {
+ trail_surrogate_ = 0;
+ next_code_point_ = kInvalidCodePoint;
+ boundary_offset_ = 0;
+ preceding_ris_count_ = 0;
+ internal_state_ = InternalState::kStart;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/backward_grapheme_boundary_state_machine.h b/chromium/third_party/blink/renderer/core/editing/state_machines/backward_grapheme_boundary_state_machine.h
new file mode 100644
index 00000000000..ff523a0a947
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/backward_grapheme_boundary_state_machine.h
@@ -0,0 +1,75 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_BACKWARD_GRAPHEME_BOUNDARY_STATE_MACHINE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_BACKWARD_GRAPHEME_BOUNDARY_STATE_MACHINE_H_
+
+#include <iosfwd>
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+
+namespace blink {
+
+class CORE_EXPORT BackwardGraphemeBoundaryStateMachine {
+ STACK_ALLOCATED();
+
+ public:
+ BackwardGraphemeBoundaryStateMachine();
+
+ // Find boundary offset by feeding preceding text.
+ // This method must not be called after feedFollowingCodeUnit().
+ TextSegmentationMachineState FeedPrecedingCodeUnit(UChar code_unit);
+
+ // Tells the end of preceding text to the state machine.
+ TextSegmentationMachineState TellEndOfPrecedingText();
+
+ // Find boundary offset by feeding following text.
+ // This method must be called after feedPrecedingCodeUnit() returns
+ // NeedsFollowingCodeUnit.
+ TextSegmentationMachineState FeedFollowingCodeUnit(UChar code_unit);
+
+ // Returns the next boundary offset. This method finalizes the state machine
+ // if it is not finished.
+ int FinalizeAndGetBoundaryOffset();
+
+ // Resets the internal state to the initial state.
+ void Reset();
+
+ private:
+ enum class InternalState;
+ friend std::ostream& operator<<(std::ostream&, InternalState);
+
+ TextSegmentationMachineState MoveToNextState(InternalState next_state);
+
+ TextSegmentationMachineState StaySameState();
+
+ // Updates the internal state to InternalState::Finished then returns
+ // TextSegmentationMachineState::Finished.
+ TextSegmentationMachineState Finish();
+
+ // Used for composing supplementary code point with surrogate pairs.
+ UChar trail_surrogate_ = 0;
+
+ // The code point immediately after the m_BoundaryOffset.
+ UChar32 next_code_point_;
+
+ // The relative offset from the begging of this state machine.
+ int boundary_offset_ = 0;
+
+ // The number of regional indicator symbols preceding to the begging offset.
+ int preceding_ris_count_ = 0;
+
+ // The internal state.
+ InternalState internal_state_;
+
+ DISALLOW_COPY_AND_ASSIGN(BackwardGraphemeBoundaryStateMachine);
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/backward_grapheme_boundary_state_machine_test.cc b/chromium/third_party/blink/renderer/core/editing/state_machines/backward_grapheme_boundary_state_machine_test.cc
new file mode 100644
index 00000000000..a6e1c6b1e6d
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/backward_grapheme_boundary_state_machine_test.cc
@@ -0,0 +1,509 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/state_machines/backward_grapheme_boundary_state_machine.h"
+
+#include "third_party/blink/renderer/core/editing/state_machines/state_machine_test_util.h"
+#include "third_party/blink/renderer/platform/wtf/text/character_names.h"
+
+namespace blink {
+
+namespace backward_grapheme_boundary_state_machine_test {
+
+// Notations:
+// SOT indicates start of text.
+// [Lead] indicates broken lonely lead surrogate.
+// [Trail] indicates broken lonely trail surrogate.
+// [U] indicates regional indicator symbol U.
+// [S] indicates regional indicator symbol S.
+
+// kWatch kVS16, kEye kVS16 are valid standardized variants.
+const UChar32 kWatch = 0x231A;
+const UChar32 kEye = WTF::Unicode::kEyeCharacter;
+const UChar32 kVS16 = 0xFE0F;
+
+// kHanBMP KVS17, kHanSIP kVS17 are valie IVD sequences.
+const UChar32 kHanBMP = 0x845B;
+const UChar32 kHanSIP = 0x20000;
+const UChar32 kVS17 = 0xE0100;
+
+// Following lead/trail values are used for invalid surrogate pairs.
+const UChar kLead = 0xD83D;
+const UChar kTrail = 0xDC66;
+
+// U+1F1FA is REGIONAL INDICATOR SYMBOL LETTER U
+// U+1F1F8 is REGIONAL INDICATOR SYMBOL LETTER S
+const UChar32 kRisU = 0x1F1FA;
+const UChar32 kRisS = 0x1F1F8;
+
+class BackwardGraphemeBoundaryStatemachineTest
+ : public GraphemeStateMachineTestBase {
+ protected:
+ BackwardGraphemeBoundaryStatemachineTest() = default;
+ ~BackwardGraphemeBoundaryStatemachineTest() override = default;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(BackwardGraphemeBoundaryStatemachineTest);
+};
+
+TEST_F(BackwardGraphemeBoundaryStatemachineTest, DoNothingCase) {
+ BackwardGraphemeBoundaryStateMachine machine;
+
+ EXPECT_EQ(0, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(0, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(BackwardGraphemeBoundaryStatemachineTest, BrokenSurrogatePair) {
+ BackwardGraphemeBoundaryStateMachine machine;
+
+ // [Lead]
+ EXPECT_EQ("F", ProcessSequenceBackward(&machine, AsCodePoints(kLead)));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [Trail]
+ EXPECT_EQ("RF", ProcessSequenceBackward(&machine, AsCodePoints('a', kTrail)));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Trail] + [Trail]
+ EXPECT_EQ("RF",
+ ProcessSequenceBackward(&machine, AsCodePoints(kTrail, kTrail)));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [Trail]
+ EXPECT_EQ("RF", ProcessSequenceBackward(&machine, AsCodePoints(kTrail)));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(BackwardGraphemeBoundaryStatemachineTest, BreakImmediately_BMP) {
+ BackwardGraphemeBoundaryStateMachine machine;
+
+ // U+0000 + U+0000
+ EXPECT_EQ("RF", ProcessSequenceBackward(&machine, AsCodePoints(0, 0)));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + 'a'
+ EXPECT_EQ("RF", ProcessSequenceBackward(&machine, AsCodePoints('a', 'a')));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // U+1F441 + 'a'
+ EXPECT_EQ("RRF", ProcessSequenceBackward(&machine, AsCodePoints(kEye, 'a')));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + 'a'
+ EXPECT_EQ("RF", ProcessSequenceBackward(&machine, AsCodePoints('a')));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // Broken surrogates.
+ // [Lead] + 'a'
+ EXPECT_EQ("RF", ProcessSequenceBackward(&machine, AsCodePoints(kLead, 'a')));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [Trail] + 'a'
+ EXPECT_EQ("RRF",
+ ProcessSequenceBackward(&machine, AsCodePoints('a', kTrail, 'a')));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Trail] + [Trail] + 'a'
+ EXPECT_EQ("RRF", ProcessSequenceBackward(&machine,
+ AsCodePoints(kTrail, kTrail, 'a')));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [Trail] + 'a'
+ EXPECT_EQ("RRF",
+ ProcessSequenceBackward(&machine, AsCodePoints(kTrail, 'a')));
+ EXPECT_EQ(-1, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(BackwardGraphemeBoundaryStatemachineTest,
+ BreakImmediately_SupplementaryPlane) {
+ BackwardGraphemeBoundaryStateMachine machine;
+
+ // 'a' + U+1F441
+ EXPECT_EQ("RRF", ProcessSequenceBackward(&machine, AsCodePoints('a', kEye)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // U+1F441 + U+1F441
+ EXPECT_EQ("RRRF",
+ ProcessSequenceBackward(&machine, AsCodePoints(kEye, kEye)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + U+1F441
+ EXPECT_EQ("RRF", ProcessSequenceBackward(&machine, AsCodePoints(kEye)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // Broken surrogates.
+ // [Lead] + U+1F441
+ EXPECT_EQ("RRF",
+ ProcessSequenceBackward(&machine, AsCodePoints(kLead, kEye)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [Trail] + U+1F441
+ EXPECT_EQ("RRRF",
+ ProcessSequenceBackward(&machine, AsCodePoints('a', kTrail, kEye)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Trail] + [Trail] + U+1F441
+ EXPECT_EQ("RRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kTrail, kTrail, kEye)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [Trail] + U+1F441
+ EXPECT_EQ("RRRF",
+ ProcessSequenceBackward(&machine, AsCodePoints(kTrail, kEye)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(BackwardGraphemeBoundaryStatemachineTest,
+ NotBreakImmediatelyBefore_BMP_BMP) {
+ BackwardGraphemeBoundaryStateMachine machine;
+
+ // 'a' + U+231A + U+FE0F
+ EXPECT_EQ("RRF", ProcessSequenceBackward(&machine,
+ AsCodePoints('a', kWatch, kVS16)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // U+1F441 + U+231A + U+FE0F
+ EXPECT_EQ("RRRF", ProcessSequenceBackward(&machine,
+ AsCodePoints(kEye, kWatch, kVS16)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + U+231A + U+FE0F
+ EXPECT_EQ("RRF",
+ ProcessSequenceBackward(&machine, AsCodePoints(kWatch, kVS16)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Lead] + U+231A + U+FE0F
+ EXPECT_EQ("RRF", ProcessSequenceBackward(&machine,
+ AsCodePoints(kLead, kWatch, kVS16)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [Trail] + U+231A + U+FE0F
+ EXPECT_EQ("RRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints('a', kTrail, kWatch, kVS16)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Trail] + [Trail] + U+231A + U+FE0F
+ EXPECT_EQ("RRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kTrail, kTrail, kWatch, kVS16)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [Trail] + U+231A + U+FE0F
+ EXPECT_EQ("RRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kTrail, kWatch, kVS16)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(BackwardGraphemeBoundaryStatemachineTest,
+ NotBreakImmediatelyBefore_Supplementary_BMP) {
+ BackwardGraphemeBoundaryStateMachine machine;
+
+ // 'a' + U+1F441 + U+FE0F
+ EXPECT_EQ("RRRF",
+ ProcessSequenceBackward(&machine, AsCodePoints('a', kEye, kVS16)));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+
+ // U+1F441 + U+1F441 + U+FE0F
+ EXPECT_EQ("RRRRF",
+ ProcessSequenceBackward(&machine, AsCodePoints(kEye, kEye, kVS16)));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + U+1F441 + U+FE0F
+ EXPECT_EQ("RRRF",
+ ProcessSequenceBackward(&machine, AsCodePoints(kEye, kVS16)));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Lead] + U+1F441 + U+FE0F
+ EXPECT_EQ("RRRF", ProcessSequenceBackward(&machine,
+ AsCodePoints(kLead, kEye, kVS16)));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [Trail] + U+1F441 + U+FE0F
+ EXPECT_EQ("RRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints('a', kTrail, kEye, kVS16)));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Trail] + [Trail] + U+1F441 + U+FE0F
+ EXPECT_EQ("RRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kTrail, kTrail, kEye, kVS16)));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [Trail] + U+1F441 + U+FE0F
+ EXPECT_EQ("RRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kTrail, kEye, kVS16)));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(BackwardGraphemeBoundaryStatemachineTest,
+ NotBreakImmediatelyBefore_BMP_Supplementary) {
+ BackwardGraphemeBoundaryStateMachine machine;
+
+ // 'a' + U+845B + U+E0100
+ EXPECT_EQ("RRRF", ProcessSequenceBackward(&machine,
+ AsCodePoints('a', kHanBMP, kVS17)));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+
+ // U+1F441 + U+845B + U+E0100
+ EXPECT_EQ("RRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kEye, kHanBMP, kVS17)));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + U+845B + U+E0100
+ EXPECT_EQ("RRRF",
+ ProcessSequenceBackward(&machine, AsCodePoints(kHanBMP, kVS17)));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Lead] + U+845B + U+E0100
+ EXPECT_EQ("RRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kLead, kHanBMP, kVS17)));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [Trail] + U+845B + U+E0100
+ EXPECT_EQ("RRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints('a', kTrail, kHanBMP, kVS17)));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Trail] + [Trail] + U+845B + U+E0100
+ EXPECT_EQ("RRRRF",
+ ProcessSequenceBackward(
+ &machine, AsCodePoints(kTrail, kTrail, kHanBMP, kVS17)));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [Trail] + U+845B + U+E0100
+ EXPECT_EQ("RRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kTrail, kHanBMP, kVS17)));
+ EXPECT_EQ(-3, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(BackwardGraphemeBoundaryStatemachineTest,
+ NotBreakImmediatelyBefore_Supplementary_Supplementary) {
+ BackwardGraphemeBoundaryStateMachine machine;
+
+ // 'a' + U+20000 + U+E0100
+ EXPECT_EQ("RRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints('a', kHanSIP, kVS17)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // U+1F441 + U+20000 + U+E0100
+ EXPECT_EQ("RRRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kEye, kHanSIP, kVS17)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + U+20000 + U+E0100
+ EXPECT_EQ("RRRRF",
+ ProcessSequenceBackward(&machine, AsCodePoints(kHanSIP, kVS17)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Lead] + U+20000 + U+E0100
+ EXPECT_EQ("RRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kLead, kHanSIP, kVS17)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [Trail] + U+20000 + U+E0100
+ EXPECT_EQ("RRRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints('a', kTrail, kHanSIP, kVS17)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Trail] + [Trail] + U+20000 + U+E0100
+ EXPECT_EQ("RRRRRF",
+ ProcessSequenceBackward(
+ &machine, AsCodePoints(kTrail, kTrail, kHanSIP, kVS17)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [Trail] + U+20000 + U+E0100
+ EXPECT_EQ("RRRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kTrail, kHanSIP, kVS17)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(BackwardGraphemeBoundaryStatemachineTest, MuchLongerCase) {
+ const UChar32 kMan = WTF::Unicode::kManCharacter;
+ const UChar32 kZwj = WTF::Unicode::kZeroWidthJoinerCharacter;
+ const UChar32 kHeart = WTF::Unicode::kHeavyBlackHeartCharacter;
+ const UChar32 kKiss = WTF::Unicode::kKissMarkCharacter;
+
+ BackwardGraphemeBoundaryStateMachine machine;
+
+ // U+1F468 U+200D U+2764 U+FE0F U+200D U+1F48B U+200D U+1F468 is a valid ZWJ
+ // emoji sequence.
+ // 'a' + ZWJ Emoji Sequence
+ EXPECT_EQ("RRRRRRRRRRRF",
+ ProcessSequenceBackward(
+ &machine, AsCodePoints('a', kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan)));
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+
+ // U+1F441 + ZWJ Emoji Sequence
+ EXPECT_EQ("RRRRRRRRRRRRF",
+ ProcessSequenceBackward(
+ &machine, AsCodePoints(kEye, kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan)));
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + ZWJ Emoji Sequence
+ EXPECT_EQ(
+ "RRRRRRRRRRRF",
+ ProcessSequenceBackward(&machine, AsCodePoints(kMan, kZwj, kHeart, kVS16,
+ kZwj, kKiss, kZwj, kMan)));
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Lead] + ZWJ Emoji Sequence
+ EXPECT_EQ("RRRRRRRRRRRF",
+ ProcessSequenceBackward(
+ &machine, AsCodePoints(kLead, kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan)));
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [Trail] + ZWJ Emoji Sequence
+ EXPECT_EQ("RRRRRRRRRRRRF",
+ ProcessSequenceBackward(
+ &machine, AsCodePoints('a', kTrail, kMan, kZwj, kHeart, kVS16,
+ kZwj, kKiss, kZwj, kMan)));
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Trail] + [Trail] + ZWJ Emoji Sequence
+ EXPECT_EQ("RRRRRRRRRRRRF",
+ ProcessSequenceBackward(
+ &machine, AsCodePoints(kTrail, kTrail, kMan, kZwj, kHeart,
+ kVS16, kZwj, kKiss, kZwj, kMan)));
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [Trail] + ZWJ Emoji Sequence
+ EXPECT_EQ("RRRRRRRRRRRRF",
+ ProcessSequenceBackward(
+ &machine, AsCodePoints(kTrail, kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan)));
+ EXPECT_EQ(-11, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(BackwardGraphemeBoundaryStatemachineTest, Flags_singleFlag) {
+ BackwardGraphemeBoundaryStateMachine machine;
+
+ // 'a' + [U] + [S]
+ EXPECT_EQ("RRRRF",
+ ProcessSequenceBackward(&machine, AsCodePoints('a', kRisU, kRisS)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // U+1F441 + [U] + [S]
+ EXPECT_EQ("RRRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kEye, kRisU, kRisS)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [U] + [S]
+ EXPECT_EQ("RRRRF",
+ ProcessSequenceBackward(&machine, AsCodePoints(kRisU, kRisS)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Lead] + [U] + [S]
+ EXPECT_EQ("RRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kLead, kRisU, kRisS)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [Trail] + [U] + [S]
+ EXPECT_EQ("RRRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints('a', kTrail, kRisU, kRisS)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Trail] + [Trail] + [U] + [S]
+ EXPECT_EQ("RRRRRF",
+ ProcessSequenceBackward(
+ &machine, AsCodePoints(kTrail, kTrail, kRisU, kRisS)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [Trail] + [U] + [S]
+ EXPECT_EQ("RRRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kTrail, kRisU, kRisS)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(BackwardGraphemeBoundaryStatemachineTest, Flags_twoFlags) {
+ BackwardGraphemeBoundaryStateMachine machine;
+
+ // 'a' + [U] + [S] + [U] + [S]
+ EXPECT_EQ("RRRRRRRRF",
+ ProcessSequenceBackward(
+ &machine, AsCodePoints('a', kRisU, kRisS, kRisU, kRisS)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // U+1F441 + [U] + [S] + [U] + [S]
+ EXPECT_EQ("RRRRRRRRRF",
+ ProcessSequenceBackward(
+ &machine, AsCodePoints(kEye, kRisU, kRisS, kRisU, kRisS)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [U] + [S] + [U] + [S]
+ EXPECT_EQ("RRRRRRRRF",
+ ProcessSequenceBackward(&machine,
+ AsCodePoints(kRisU, kRisS, kRisU, kRisS)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Lead] + [U] + [S] + [U] + [S]
+ EXPECT_EQ("RRRRRRRRF",
+ ProcessSequenceBackward(
+ &machine, AsCodePoints(kLead, kRisU, kRisS, kRisU, kRisS)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [Trail] + [U] + [S] + [U] + [S]
+ EXPECT_EQ("RRRRRRRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints('a', kTrail, kRisU, kRisS,
+ kRisU, kRisS)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Trail] + [Trail] + [U] + [S] + [U] + [S]
+ EXPECT_EQ("RRRRRRRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kTrail, kTrail, kRisU,
+ kRisS, kRisU, kRisS)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [Trail] + [U] + [S] + [U] + [S]
+ EXPECT_EQ("RRRRRRRRRF",
+ ProcessSequenceBackward(
+ &machine, AsCodePoints(kTrail, kRisU, kRisS, kRisU, kRisS)));
+ EXPECT_EQ(-4, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(BackwardGraphemeBoundaryStatemachineTest, Flags_oddNumberedRIS) {
+ BackwardGraphemeBoundaryStateMachine machine;
+
+ // 'a' + [U] + [S] + [U]
+ EXPECT_EQ("RRRRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints('a', kRisU, kRisS, kRisU)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // U+1F441 + [U] + [S] + [U]
+ EXPECT_EQ("RRRRRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kEye, kRisU, kRisS, kRisU)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [U] + [S] + [U]
+ EXPECT_EQ("RRRRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kRisU, kRisS, kRisU)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Lead] + [U] + [S] + [U]
+ EXPECT_EQ("RRRRRRF", ProcessSequenceBackward(
+ &machine, AsCodePoints(kLead, kRisU, kRisS, kRisU)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [Trail] + [U] + [S] + [U]
+ EXPECT_EQ("RRRRRRRF",
+ ProcessSequenceBackward(
+ &machine, AsCodePoints('a', kTrail, kRisU, kRisS, kRisU)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Trail] + [Trail] + [U] + [S] + [U]
+ EXPECT_EQ("RRRRRRRF",
+ ProcessSequenceBackward(
+ &machine, AsCodePoints(kTrail, kTrail, kRisU, kRisS, kRisU)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [Trail] + [U] + [S] + [U]
+ EXPECT_EQ("RRRRRRRF",
+ ProcessSequenceBackward(&machine,
+ AsCodePoints(kTrail, kRisU, kRisS, kRisU)));
+ EXPECT_EQ(-2, machine.FinalizeAndGetBoundaryOffset());
+}
+
+} // namespace backward_grapheme_boundary_state_machine_test
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/forward_code_point_state_machine.cc b/chromium/third_party/blink/renderer/core/editing/state_machines/forward_code_point_state_machine.cc
new file mode 100644
index 00000000000..1375df8ab12
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/forward_code_point_state_machine.cc
@@ -0,0 +1,69 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/state_machines/forward_code_point_state_machine.h"
+
+namespace blink {
+
+enum class ForwardCodePointStateMachine::ForwardCodePointState {
+ kNotSurrogate,
+ kLeadSurrogate,
+ kInvalid,
+};
+
+ForwardCodePointStateMachine::ForwardCodePointStateMachine()
+ : state_(ForwardCodePointState::kNotSurrogate) {}
+
+TextSegmentationMachineState
+ForwardCodePointStateMachine::FeedFollowingCodeUnit(UChar code_unit) {
+ switch (state_) {
+ case ForwardCodePointState::kNotSurrogate:
+ if (U16_IS_TRAIL(code_unit)) {
+ code_units_to_be_deleted_ = 0;
+ state_ = ForwardCodePointState::kInvalid;
+ return TextSegmentationMachineState::kInvalid;
+ }
+ ++code_units_to_be_deleted_;
+ if (U16_IS_LEAD(code_unit)) {
+ state_ = ForwardCodePointState::kLeadSurrogate;
+ return TextSegmentationMachineState::kNeedMoreCodeUnit;
+ }
+ return TextSegmentationMachineState::kFinished;
+ case ForwardCodePointState::kLeadSurrogate:
+ if (U16_IS_TRAIL(code_unit)) {
+ ++code_units_to_be_deleted_;
+ state_ = ForwardCodePointState::kNotSurrogate;
+ return TextSegmentationMachineState::kFinished;
+ }
+ code_units_to_be_deleted_ = 0;
+ state_ = ForwardCodePointState::kInvalid;
+ return TextSegmentationMachineState::kInvalid;
+ case ForwardCodePointState::kInvalid:
+ code_units_to_be_deleted_ = 0;
+ return TextSegmentationMachineState::kInvalid;
+ }
+ NOTREACHED();
+ return TextSegmentationMachineState::kInvalid;
+}
+
+TextSegmentationMachineState
+ForwardCodePointStateMachine::FeedPrecedingCodeUnit(UChar code_unit) {
+ NOTREACHED();
+ return TextSegmentationMachineState::kInvalid;
+}
+
+bool ForwardCodePointStateMachine::AtCodePointBoundary() {
+ return state_ == ForwardCodePointState::kNotSurrogate;
+}
+
+int ForwardCodePointStateMachine::GetBoundaryOffset() {
+ return code_units_to_be_deleted_;
+}
+
+void ForwardCodePointStateMachine::Reset() {
+ code_units_to_be_deleted_ = 0;
+ state_ = ForwardCodePointState::kNotSurrogate;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/forward_code_point_state_machine.h b/chromium/third_party/blink/renderer/core/editing/state_machines/forward_code_point_state_machine.h
new file mode 100644
index 00000000000..dda9b50eb1e
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/forward_code_point_state_machine.h
@@ -0,0 +1,53 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_FORWARD_CODE_POINT_STATE_MACHINE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_FORWARD_CODE_POINT_STATE_MACHINE_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+
+namespace blink {
+
+class CORE_EXPORT ForwardCodePointStateMachine {
+ STACK_ALLOCATED();
+
+ public:
+ ForwardCodePointStateMachine();
+ ~ForwardCodePointStateMachine() = default;
+
+ // Prepares by feeding preceding text.
+ TextSegmentationMachineState FeedPrecedingCodeUnit(UChar code_unit);
+
+ // Finds boundary offset by feeding following text.
+ TextSegmentationMachineState FeedFollowingCodeUnit(UChar code_unit);
+
+ // Returns true if we are at code point boundary.
+ bool AtCodePointBoundary();
+
+ // Returns the next boundary offset.
+ int GetBoundaryOffset();
+
+ // Resets the internal state to the initial state.
+ void Reset();
+
+ private:
+ enum class ForwardCodePointState;
+
+ // The number of code units to be deleted.
+ // Nothing to delete if there is an invalid surrogate pair.
+ int code_units_to_be_deleted_ = 0;
+
+ // The internal state.
+ ForwardCodePointState state_;
+
+ DISALLOW_COPY_AND_ASSIGN(ForwardCodePointStateMachine);
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/forward_code_point_state_machine_test.cc b/chromium/third_party/blink/renderer/core/editing/state_machines/forward_code_point_state_machine_test.cc
new file mode 100644
index 00000000000..077db40f0cf
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/forward_code_point_state_machine_test.cc
@@ -0,0 +1,75 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/state_machines/forward_code_point_state_machine.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace blink {
+
+TEST(ForwardCodePointStateMachineTest, DoNothingCase) {
+ ForwardCodePointStateMachine machine;
+ EXPECT_EQ(0, machine.GetBoundaryOffset());
+}
+
+TEST(ForwardCodePointStateMachineTest, SingleCharacter) {
+ ForwardCodePointStateMachine machine;
+ EXPECT_EQ(TextSegmentationMachineState::kFinished,
+ machine.FeedFollowingCodeUnit('a'));
+ EXPECT_EQ(1, machine.GetBoundaryOffset());
+
+ machine.Reset();
+ EXPECT_EQ(TextSegmentationMachineState::kFinished,
+ machine.FeedFollowingCodeUnit('-'));
+ EXPECT_EQ(1, machine.GetBoundaryOffset());
+
+ machine.Reset();
+ EXPECT_EQ(TextSegmentationMachineState::kFinished,
+ machine.FeedFollowingCodeUnit('\t'));
+ EXPECT_EQ(1, machine.GetBoundaryOffset());
+
+ machine.Reset();
+ // U+3042 HIRAGANA LETTER A.
+ EXPECT_EQ(TextSegmentationMachineState::kFinished,
+ machine.FeedFollowingCodeUnit(0x3042));
+ EXPECT_EQ(1, machine.GetBoundaryOffset());
+}
+
+TEST(ForwardCodePointStateMachineTest, SurrogatePair) {
+ ForwardCodePointStateMachine machine;
+
+ // U+20BB7 is \uD83D\uDDFA in UTF-16.
+ const UChar kLeadSurrogate = 0xD842;
+ const UChar kTrailSurrogate = 0xDFB7;
+
+ EXPECT_EQ(TextSegmentationMachineState::kNeedMoreCodeUnit,
+ machine.FeedFollowingCodeUnit(kLeadSurrogate));
+ EXPECT_EQ(TextSegmentationMachineState::kFinished,
+ machine.FeedFollowingCodeUnit(kTrailSurrogate));
+ EXPECT_EQ(2, machine.GetBoundaryOffset());
+
+ // Edge cases
+ // Unpaired leading surrogate. Nothing to delete.
+ machine.Reset();
+ EXPECT_EQ(TextSegmentationMachineState::kNeedMoreCodeUnit,
+ machine.FeedFollowingCodeUnit(kLeadSurrogate));
+ EXPECT_EQ(TextSegmentationMachineState::kInvalid,
+ machine.FeedFollowingCodeUnit('a'));
+ EXPECT_EQ(0, machine.GetBoundaryOffset());
+
+ machine.Reset();
+ EXPECT_EQ(TextSegmentationMachineState::kNeedMoreCodeUnit,
+ machine.FeedFollowingCodeUnit(kLeadSurrogate));
+ EXPECT_EQ(TextSegmentationMachineState::kInvalid,
+ machine.FeedFollowingCodeUnit(kLeadSurrogate));
+ EXPECT_EQ(0, machine.GetBoundaryOffset());
+
+ // Unpaired trailing surrogate. Nothing to delete.
+ machine.Reset();
+ EXPECT_EQ(TextSegmentationMachineState::kInvalid,
+ machine.FeedFollowingCodeUnit(kTrailSurrogate));
+ EXPECT_EQ(0, machine.GetBoundaryOffset());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/forward_grapheme_boundary_state_machine.cc b/chromium/third_party/blink/renderer/core/editing/state_machines/forward_grapheme_boundary_state_machine.cc
new file mode 100644
index 00000000000..2519ede0812
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/forward_grapheme_boundary_state_machine.cc
@@ -0,0 +1,257 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/state_machines/forward_grapheme_boundary_state_machine.h"
+
+#include <ostream> // NOLINT
+#include "third_party/blink/renderer/core/editing/state_machines/state_machine_util.h"
+#include "third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.h"
+#include "third_party/blink/renderer/platform/text/character.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+
+namespace blink {
+namespace {
+const UChar32 kUnsetCodePoint = WTF::Unicode::kMaxCodepoint + 1;
+} // namespace
+
+#define FOR_EACH_FORWARD_GRAPHEME_BOUNDARY_STATE(V) \
+ /* Counting preceding regional indicators. This is initial state. */ \
+ V(kCountRIS) \
+ /* Waiting lead surrogate during counting regional indicators. */ \
+ V(kCountRISWaitLeadSurrogate) \
+ /* Waiting first following code unit. */ \
+ V(kStartForward) \
+ /* Waiting trail surrogate for the first following code point. */ \
+ V(kStartForwardWaitTrailSurrgate) \
+ /* Searching grapheme boundary. */ \
+ V(kSearch) \
+ /* Waiting trail surrogate during searching grapheme boundary. */ \
+ V(kSearchWaitTrailSurrogate) \
+ /* The state machine has stopped. */ \
+ V(kFinished)
+
+enum class ForwardGraphemeBoundaryStateMachine::InternalState {
+#define V(name) name,
+ FOR_EACH_FORWARD_GRAPHEME_BOUNDARY_STATE(V)
+#undef V
+};
+
+std::ostream& operator<<(
+ std::ostream& os,
+ ForwardGraphemeBoundaryStateMachine::InternalState state) {
+ static const char* const kTexts[] = {
+#define V(name) #name,
+ FOR_EACH_FORWARD_GRAPHEME_BOUNDARY_STATE(V)
+#undef V
+ };
+ const auto& it = std::begin(kTexts) + static_cast<size_t>(state);
+ DCHECK_GE(it, std::begin(kTexts)) << "Unknown state value";
+ DCHECK_LT(it, std::end(kTexts)) << "Unknown state value";
+ return os << *it;
+}
+
+ForwardGraphemeBoundaryStateMachine::ForwardGraphemeBoundaryStateMachine()
+ : prev_code_point_(kUnsetCodePoint),
+ internal_state_(InternalState::kCountRIS) {}
+
+TextSegmentationMachineState
+ForwardGraphemeBoundaryStateMachine::FeedPrecedingCodeUnit(UChar code_unit) {
+ DCHECK_EQ(prev_code_point_, kUnsetCodePoint);
+ DCHECK_EQ(boundary_offset_, 0);
+ switch (internal_state_) {
+ case InternalState::kCountRIS:
+ DCHECK_EQ(pending_code_unit_, 0);
+ if (U16_IS_TRAIL(code_unit)) {
+ pending_code_unit_ = code_unit;
+ return MoveToNextState(InternalState::kCountRISWaitLeadSurrogate);
+ }
+ return MoveToNextState(InternalState::kStartForward);
+ case InternalState::kCountRISWaitLeadSurrogate:
+ DCHECK_NE(pending_code_unit_, 0);
+ if (U16_IS_LEAD(code_unit)) {
+ const UChar32 code_point =
+ U16_GET_SUPPLEMENTARY(code_unit, pending_code_unit_);
+ pending_code_unit_ = 0;
+ if (Character::IsRegionalIndicator(code_point)) {
+ ++preceding_ris_count_;
+ return MoveToNextState(InternalState::kCountRIS);
+ }
+ }
+ pending_code_unit_ = 0;
+ return MoveToNextState(InternalState::kStartForward);
+ case InternalState::kStartForward: // Fallthrough
+ case InternalState::kStartForwardWaitTrailSurrgate: // Fallthrough
+ case InternalState::kSearch: // Fallthrough
+ case InternalState::kSearchWaitTrailSurrogate: // Fallthrough
+ NOTREACHED() << "Do not call feedPrecedingCodeUnit() once "
+ << TextSegmentationMachineState::kNeedFollowingCodeUnit
+ << " is returned. InternalState: " << internal_state_;
+ return Finish();
+ case InternalState::kFinished:
+ NOTREACHED() << "Do not call feedPrecedingCodeUnit() once it finishes.";
+ return Finish();
+ }
+ NOTREACHED() << "Unhandled state: " << internal_state_;
+ return Finish();
+}
+
+TextSegmentationMachineState
+ForwardGraphemeBoundaryStateMachine::FeedFollowingCodeUnit(UChar code_unit) {
+ switch (internal_state_) {
+ case InternalState::kCountRIS: // Fallthrough
+ case InternalState::kCountRISWaitLeadSurrogate:
+ NOTREACHED() << "Do not call feedFollowingCodeUnit() until "
+ << TextSegmentationMachineState::kNeedFollowingCodeUnit
+ << " is returned. InternalState: " << internal_state_;
+ return Finish();
+ case InternalState::kStartForward:
+ DCHECK_EQ(prev_code_point_, kUnsetCodePoint);
+ DCHECK_EQ(boundary_offset_, 0);
+ DCHECK_EQ(pending_code_unit_, 0);
+ if (U16_IS_TRAIL(code_unit)) {
+ // Lonely trail surrogate.
+ boundary_offset_ = 1;
+ return Finish();
+ }
+ if (U16_IS_LEAD(code_unit)) {
+ pending_code_unit_ = code_unit;
+ return MoveToNextState(InternalState::kStartForwardWaitTrailSurrgate);
+ }
+ prev_code_point_ = code_unit;
+ boundary_offset_ = 1;
+ return MoveToNextState(InternalState::kSearch);
+ case InternalState::kStartForwardWaitTrailSurrgate:
+ DCHECK_EQ(prev_code_point_, kUnsetCodePoint);
+ DCHECK_EQ(boundary_offset_, 0);
+ DCHECK_NE(pending_code_unit_, 0);
+ if (U16_IS_TRAIL(code_unit)) {
+ prev_code_point_ = U16_GET_SUPPLEMENTARY(pending_code_unit_, code_unit);
+ boundary_offset_ = 2;
+ pending_code_unit_ = 0;
+ return MoveToNextState(InternalState::kSearch);
+ }
+ // Lonely lead surrogate.
+ boundary_offset_ = 1;
+ return Finish();
+ case InternalState::kSearch:
+ DCHECK_NE(prev_code_point_, kUnsetCodePoint);
+ DCHECK_NE(boundary_offset_, 0);
+ DCHECK_EQ(pending_code_unit_, 0);
+ if (U16_IS_LEAD(code_unit)) {
+ pending_code_unit_ = code_unit;
+ return MoveToNextState(InternalState::kSearchWaitTrailSurrogate);
+ }
+ if (U16_IS_TRAIL(code_unit))
+ return Finish(); // Lonely trail surrogate.
+ if (IsGraphemeBreak(prev_code_point_, code_unit))
+ return Finish();
+ prev_code_point_ = code_unit;
+ boundary_offset_ += 1;
+ return StaySameState();
+ case InternalState::kSearchWaitTrailSurrogate:
+ DCHECK_NE(prev_code_point_, kUnsetCodePoint);
+ DCHECK_NE(boundary_offset_, 0);
+ DCHECK_NE(pending_code_unit_, 0);
+ if (!U16_IS_TRAIL(code_unit))
+ return Finish(); // Lonely lead surrogate.
+
+ {
+ const UChar32 code_point =
+ U16_GET_SUPPLEMENTARY(pending_code_unit_, code_unit);
+ pending_code_unit_ = 0;
+ if (Character::IsRegionalIndicator(prev_code_point_) &&
+ Character::IsRegionalIndicator(code_point)) {
+ if (preceding_ris_count_ % 2 == 0) {
+ // Odd numbered RI case, note that m_prevCodePoint is also
+ // RI.
+ boundary_offset_ += 2;
+ }
+ return Finish();
+ }
+ if (IsGraphemeBreak(prev_code_point_, code_point))
+ return Finish();
+ prev_code_point_ = code_point;
+ boundary_offset_ += 2;
+ return MoveToNextState(InternalState::kSearch);
+ }
+ case InternalState::kFinished:
+ NOTREACHED() << "Do not call feedFollowingCodeUnit() once it finishes.";
+ return Finish();
+ }
+ NOTREACHED() << "Unhandled staet: " << internal_state_;
+ return Finish();
+}
+
+TextSegmentationMachineState
+ForwardGraphemeBoundaryStateMachine::TellEndOfPrecedingText() {
+ DCHECK(internal_state_ == InternalState::kCountRIS ||
+ internal_state_ == InternalState::kCountRISWaitLeadSurrogate)
+ << "Do not call tellEndOfPrecedingText() once "
+ << TextSegmentationMachineState::kNeedFollowingCodeUnit
+ << " is returned. InternalState: " << internal_state_;
+
+ // Clear pending code unit since preceding buffer may end with lonely trail
+ // surrogate. We can just ignore it since preceding buffer is only used for
+ // counting preceding regional indicators.
+ pending_code_unit_ = 0;
+ return MoveToNextState(InternalState::kStartForward);
+}
+
+int ForwardGraphemeBoundaryStateMachine::FinalizeAndGetBoundaryOffset() {
+ if (internal_state_ != InternalState::kFinished)
+ FinishWithEndOfText();
+ DCHECK_GE(boundary_offset_, 0);
+ return boundary_offset_;
+}
+
+void ForwardGraphemeBoundaryStateMachine::Reset() {
+ pending_code_unit_ = 0;
+ boundary_offset_ = 0;
+ preceding_ris_count_ = 0;
+ prev_code_point_ = kUnsetCodePoint;
+ internal_state_ = InternalState::kCountRIS;
+}
+
+TextSegmentationMachineState ForwardGraphemeBoundaryStateMachine::Finish() {
+ DCHECK_NE(internal_state_, InternalState::kFinished);
+ internal_state_ = InternalState::kFinished;
+ return TextSegmentationMachineState::kFinished;
+}
+
+TextSegmentationMachineState
+ForwardGraphemeBoundaryStateMachine::MoveToNextState(InternalState next_state) {
+ DCHECK_NE(next_state, InternalState::kFinished) << "Use finish() instead";
+ DCHECK_NE(next_state, internal_state_) << "Use staySameSatate() instead";
+ internal_state_ = next_state;
+ if (next_state == InternalState::kStartForward)
+ return TextSegmentationMachineState::kNeedFollowingCodeUnit;
+ return TextSegmentationMachineState::kNeedMoreCodeUnit;
+}
+
+TextSegmentationMachineState
+ForwardGraphemeBoundaryStateMachine::StaySameState() {
+ DCHECK_EQ(internal_state_, InternalState::kSearch)
+ << "Only Search can stay the same state.";
+ return TextSegmentationMachineState::kNeedMoreCodeUnit;
+}
+
+void ForwardGraphemeBoundaryStateMachine::FinishWithEndOfText() {
+ switch (internal_state_) {
+ case InternalState::kCountRIS: // Fallthrough
+ case InternalState::kCountRISWaitLeadSurrogate: // Fallthrough
+ case InternalState::kStartForward: // Fallthrough
+ return; // Haven't search anything to forward. Just finish.
+ case InternalState::kStartForwardWaitTrailSurrgate:
+ // Lonely lead surrogate.
+ boundary_offset_ = 1;
+ return;
+ case InternalState::kSearch: // Fallthrough
+ case InternalState::kSearchWaitTrailSurrogate: // Fallthrough
+ return;
+ case InternalState::kFinished: // Fallthrough
+ NOTREACHED() << "Do not call finishWithEndOfText() once it finishes.";
+ }
+ NOTREACHED() << "Unhandled state: " << internal_state_;
+}
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/forward_grapheme_boundary_state_machine.h b/chromium/third_party/blink/renderer/core/editing/state_machines/forward_grapheme_boundary_state_machine.h
new file mode 100644
index 00000000000..8c0a54f52c5
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/forward_grapheme_boundary_state_machine.h
@@ -0,0 +1,78 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_FORWARD_GRAPHEME_BOUNDARY_STATE_MACHINE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_FORWARD_GRAPHEME_BOUNDARY_STATE_MACHINE_H_
+
+#include <iosfwd>
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+
+namespace blink {
+
+class CORE_EXPORT ForwardGraphemeBoundaryStateMachine {
+ STACK_ALLOCATED();
+
+ public:
+ ForwardGraphemeBoundaryStateMachine();
+
+ // Find boundary offset by feeding preceding text.
+ // This method must not be called after feedFollowingCodeUnit().
+ TextSegmentationMachineState FeedPrecedingCodeUnit(UChar code_unit);
+
+ // Tells the end of preceding text to the state machine.
+ TextSegmentationMachineState TellEndOfPrecedingText();
+
+ // Find boundary offset by feeding following text.
+ // This method must be called after feedPrecedingCodeUnit() returns
+ // NeedsFollowingCodeUnit.
+ TextSegmentationMachineState FeedFollowingCodeUnit(UChar code_unit);
+
+ // Returns the next boundary offset. This method finalizes the state machine
+ // if it is not finished.
+ int FinalizeAndGetBoundaryOffset();
+
+ // Resets the internal state to the initial state.
+ void Reset();
+
+ private:
+ enum class InternalState;
+ friend std::ostream& operator<<(std::ostream&, InternalState);
+
+ TextSegmentationMachineState MoveToNextState(InternalState);
+
+ TextSegmentationMachineState StaySameState();
+
+ // Updates the internal state to InternalState::Finished then
+ // returnsTextSegmentationMachineState::Finished.
+ TextSegmentationMachineState Finish();
+
+ // Handles end of text. This method always finishes the state machine.
+ void FinishWithEndOfText();
+
+ // Used for composing supplementary code point with surrogate pairs.
+ UChar pending_code_unit_ = 0;
+
+ // The code point immediately before the m_BoundaryOffset.
+ UChar32 prev_code_point_;
+
+ // The relative offset from the begging of this state machine.
+ int boundary_offset_ = 0;
+
+ // The number of regional indicator symbols preceding to the begging offset.
+ int preceding_ris_count_ = 0;
+
+ // The internal state.
+ InternalState internal_state_;
+
+ DISALLOW_COPY_AND_ASSIGN(ForwardGraphemeBoundaryStateMachine);
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/forward_grapheme_boundary_state_machine_test.cc b/chromium/third_party/blink/renderer/core/editing/state_machines/forward_grapheme_boundary_state_machine_test.cc
new file mode 100644
index 00000000000..4a4753be9a1
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/forward_grapheme_boundary_state_machine_test.cc
@@ -0,0 +1,718 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/state_machines/forward_grapheme_boundary_state_machine.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/editing/state_machines/state_machine_test_util.h"
+#include "third_party/blink/renderer/platform/wtf/text/character_names.h"
+
+namespace blink {
+
+namespace forward_grapheme_boundary_state_machine_test {
+
+// Notations:
+// | indicates inidicates initial offset position.
+// SOT indicates start of text.
+// EOT indicates end of text.
+// [Lead] indicates broken lonely lead surrogate.
+// [Trail] indicates broken lonely trail surrogate.
+// [U] indicates regional indicator symbol U.
+// [S] indicates regional indicator symbol S.
+
+// kWatch kVS16, kEye kVS16 are valid standardized variants.
+const UChar32 kWatch = 0x231A;
+const UChar32 kEye = WTF::Unicode::kEyeCharacter;
+const UChar32 kVS16 = 0xFE0F;
+
+// kHanBMP KVS17, kHanSIP kVS17 are valie IVD sequences.
+const UChar32 kHanBMP = 0x845B;
+const UChar32 kHanSIP = 0x20000;
+const UChar32 kVS17 = 0xE0100;
+
+// Following lead/trail values are used for invalid surrogate pairs.
+const UChar kLead = 0xD83D;
+const UChar kTrail = 0xDC66;
+
+// U+1F1FA is REGIONAL INDICATOR SYMBOL LETTER U
+const UChar32 kRisU = 0x1F1FA;
+// U+1F1F8 is REGIONAL INDICATOR SYMBOL LETTER S
+const UChar32 kRisS = 0x1F1F8;
+
+class ForwardGraphemeBoundaryStatemachineTest
+ : public GraphemeStateMachineTestBase {
+ protected:
+ ForwardGraphemeBoundaryStatemachineTest() = default;
+ ~ForwardGraphemeBoundaryStatemachineTest() override = default;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(ForwardGraphemeBoundaryStatemachineTest);
+};
+
+TEST_F(ForwardGraphemeBoundaryStatemachineTest, DoNothingCase) {
+ ForwardGraphemeBoundaryStateMachine machine;
+
+ EXPECT_EQ(0, machine.FinalizeAndGetBoundaryOffset());
+ EXPECT_EQ(0, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(ForwardGraphemeBoundaryStatemachineTest, PrecedingText) {
+ ForwardGraphemeBoundaryStateMachine machine;
+ // Preceding text should not affect the result except for flags.
+ // SOT + | + 'a' + 'a'
+ EXPECT_EQ("SRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // SOT + [U] + | + 'a' + 'a'
+ EXPECT_EQ("RRSRF", ProcessSequenceForward(&machine, AsCodePoints(kRisU),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // SOT + [U] + [S] + | + 'a' + 'a'
+ EXPECT_EQ("RRRRSRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kRisU, kRisS),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+
+ // U+0000 + | + 'a' + 'a'
+ EXPECT_EQ("SRF", ProcessSequenceForward(&machine, AsCodePoints(0),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // U+0000 + [U] + | + 'a' + 'a'
+ EXPECT_EQ("RRSRF", ProcessSequenceForward(&machine, AsCodePoints(0, kRisU),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // U+0000 + [U] + [S] + | + 'a' + 'a'
+ EXPECT_EQ("RRRRSRF",
+ ProcessSequenceForward(&machine, AsCodePoints(0, kRisU, kRisS),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + | + 'a' + 'a'
+ EXPECT_EQ("SRF", ProcessSequenceForward(&machine, AsCodePoints('a'),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // 'a' + [U] + | + 'a' + 'a'
+ EXPECT_EQ("RRSRF", ProcessSequenceForward(&machine, AsCodePoints('a', kRisU),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // 'a' + [U] + [S] + | + 'a' + 'a'
+ EXPECT_EQ("RRRRSRF",
+ ProcessSequenceForward(&machine, AsCodePoints('a', kRisU, kRisS),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+
+ // U+1F441 + | + 'a' + 'a'
+ EXPECT_EQ("RSRF", ProcessSequenceForward(&machine, AsCodePoints(kEye),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // U+1F441 + [U] + | + 'a' + 'a'
+ EXPECT_EQ("RRRSRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kEye, kRisU),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // U+1F441 + [U] + [S] + | + 'a' + 'a'
+ EXPECT_EQ("RRRRRSRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kEye, kRisU, kRisS),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+
+ // Broken surrogates in preceding text.
+
+ // [Lead] + | + 'a' + 'a'
+ EXPECT_EQ("SRF", ProcessSequenceForward(&machine, AsCodePoints(kLead),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // [Lead] + [U] + | + 'a' + 'a'
+ EXPECT_EQ("RRSRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kLead, kRisU),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // [Lead] + [U] + [S] + | + 'a' + 'a'
+ EXPECT_EQ("RRRRSRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kLead, kRisU, kRisS),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [Trail] + | + 'a' + 'a'
+ EXPECT_EQ("RSRF", ProcessSequenceForward(&machine, AsCodePoints('a', kTrail),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // 'a' + [Trail] + [U] + | + 'a' + 'a'
+ EXPECT_EQ("RRRSRF",
+ ProcessSequenceForward(&machine, AsCodePoints('a', kTrail, kRisU),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // 'a' + [Trail] + [U] + [S] + | + 'a' + 'a'
+ EXPECT_EQ("RRRRRSRF", ProcessSequenceForward(
+ &machine, AsCodePoints('a', kTrail, kRisU, kRisS),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Trail] + [Trail] + | + 'a' + 'a'
+ EXPECT_EQ("RSRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kTrail, kTrail),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // [Trail] + [Trail] + [U] + | + 'a' + 'a'
+ EXPECT_EQ("RRRSRF", ProcessSequenceForward(
+ &machine, AsCodePoints(kTrail, kTrail, kRisU),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // [Trail] + [Trail] + [U] + [S] + | + 'a' + 'a'
+ EXPECT_EQ("RRRRRSRF",
+ ProcessSequenceForward(&machine,
+ AsCodePoints(kTrail, kTrail, kRisU, kRisS),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [Trail] + | + 'a' + 'a'
+ EXPECT_EQ("RSRF", ProcessSequenceForward(&machine, AsCodePoints(kTrail),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // SOT + [Trail] + [U] + | + 'a' + 'a'
+ EXPECT_EQ("RRRSRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kTrail, kRisU),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // SOT + [Trail] + [U] + [S] + | + 'a' + 'a'
+ EXPECT_EQ("RRRRRSRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kTrail, kRisU, kRisS),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(ForwardGraphemeBoundaryStatemachineTest, BrokenSurrogatePair) {
+ ForwardGraphemeBoundaryStateMachine machine;
+ // SOT + | + [Trail]
+ EXPECT_EQ("SF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kTrail)));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // SOT + | + [Lead] + 'a'
+ EXPECT_EQ("SRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kLead, 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // SOT + | + [Lead] + [Lead]
+ EXPECT_EQ("SRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kLead, kLead)));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+ // SOT + | + [Lead] + EOT
+ EXPECT_EQ("SR", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kLead)));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(ForwardGraphemeBoundaryStatemachineTest, BreakImmediately_BMP) {
+ ForwardGraphemeBoundaryStateMachine machine;
+
+ // SOT + | + U+0000 + U+0000
+ EXPECT_EQ("SRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(0, 0)));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + 'a' + 'a'
+ EXPECT_EQ("SRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints('a', 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + 'a' + U+1F441
+ EXPECT_EQ("SRRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints('a', kEye)));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + 'a' + EOT
+ EXPECT_EQ("SR", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints('a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + 'a' + [Trail]
+ EXPECT_EQ("SRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints('a', kTrail)));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + 'a' + [Lead] + 'a'
+ EXPECT_EQ("SRRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints('a', kLead, 'a')));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + 'a' + [Lead] + [Lead]
+ EXPECT_EQ("SRRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints('a', kLead, kLead)));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + 'a' + [Lead] + EOT
+ EXPECT_EQ("SRR", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints('a', kLead)));
+ EXPECT_EQ(1, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(ForwardGraphemeBoundaryStatemachineTest,
+ BreakImmediately_Supplementary) {
+ ForwardGraphemeBoundaryStateMachine machine;
+
+ // SOT + | + U+1F441 + 'a'
+ EXPECT_EQ("SRRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kEye, 'a')));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+1F441 + U+1F441
+ EXPECT_EQ("SRRRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kEye, kEye)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+1F441 + EOT
+ EXPECT_EQ("SRR", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kEye)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+1F441 + [Trail]
+ EXPECT_EQ("SRRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kEye, kTrail)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+1F441 + [Lead] + 'a'
+ EXPECT_EQ("SRRRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kEye, kLead, 'a')));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+1F441 + [Lead] + [Lead]
+ EXPECT_EQ("SRRRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kEye, kLead, kLead)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+1F441 + [Lead] + EOT
+ EXPECT_EQ("SRRR", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kEye, kLead)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(ForwardGraphemeBoundaryStatemachineTest,
+ NotBreakImmediatelyAfter_BMP_BMP) {
+ ForwardGraphemeBoundaryStateMachine machine;
+
+ // SOT + | + U+231A + U+FE0F + 'a'
+ EXPECT_EQ("SRRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kWatch, kVS16, 'a')));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+231A + U+FE0F + U+1F441
+ EXPECT_EQ("SRRRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kWatch, kVS16, kEye)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+231A + U+FE0F + EOT
+ EXPECT_EQ("SRR", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kWatch, kVS16)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+231A + U+FE0F + [Trail]
+ EXPECT_EQ("SRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kWatch, kVS16, kTrail)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+231A + U+FE0F + [Lead] + 'a'
+ EXPECT_EQ("SRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kWatch, kVS16, kLead, 'a')));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+231A + U+FE0F + [Lead] + [Lead]
+ EXPECT_EQ("SRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kWatch, kVS16, kLead, kLead)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+231A + U+FE0F + [Lead] + EOT
+ EXPECT_EQ("SRRR", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kWatch, kVS16, kLead)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(ForwardGraphemeBoundaryStatemachineTest,
+ NotBreakImmediatelyAfter_Supplementary_BMP) {
+ ForwardGraphemeBoundaryStateMachine machine;
+
+ // SOT + | + U+1F441 + U+FE0F + 'a'
+ EXPECT_EQ("SRRRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kEye, kVS16, 'a')));
+ EXPECT_EQ(3, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+1F441 + U+FE0F + U+1F441
+ EXPECT_EQ("SRRRRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kEye, kVS16, kEye)));
+ EXPECT_EQ(3, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+1F441 + U+FE0F + EOT
+ EXPECT_EQ("SRRR", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kEye, kVS16)));
+ EXPECT_EQ(3, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+1F441 + U+FE0F + [Trail]
+ EXPECT_EQ("SRRRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kEye, kVS16, kTrail)));
+ EXPECT_EQ(3, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+1F441 + U+FE0F + [Lead] + 'a'
+ EXPECT_EQ("SRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kEye, kVS16, kLead, 'a')));
+ EXPECT_EQ(3, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+1F441 + U+FE0F + [Lead] + [Lead]
+ EXPECT_EQ("SRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kEye, kVS16, kLead, kLead)));
+ EXPECT_EQ(3, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+1F441 + U+FE0F + [Lead] + EOT
+ EXPECT_EQ("SRRRR", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kEye, kVS16, kLead)));
+ EXPECT_EQ(3, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(ForwardGraphemeBoundaryStatemachineTest,
+ NotBreakImmediatelyAfter_BMP_Supplementary) {
+ ForwardGraphemeBoundaryStateMachine machine;
+
+ // SOT + | + U+845B + U+E0100 + 'a'
+ EXPECT_EQ("SRRRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kHanBMP, kVS17, 'a')));
+ EXPECT_EQ(3, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+845B + U+E0100 + U+1F441
+ EXPECT_EQ("SRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kHanBMP, kVS17, kEye)));
+ EXPECT_EQ(3, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+845B + U+E0100 + EOT
+ EXPECT_EQ("SRRR", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kHanBMP, kVS17)));
+ EXPECT_EQ(3, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+845B + U+E0100 + [Trail]
+ EXPECT_EQ("SRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kHanBMP, kVS17, kTrail)));
+ EXPECT_EQ(3, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+845B + U+E0100 + [Lead] + 'a'
+ EXPECT_EQ("SRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kHanBMP, kVS17, kLead, 'a')));
+ EXPECT_EQ(3, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+845B + U+E0100 + [Lead] + [Lead]
+ EXPECT_EQ("SRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kHanBMP, kVS17, kLead, kLead)));
+ EXPECT_EQ(3, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+845B + U+E0100 + [Lead] + EOT
+ EXPECT_EQ("SRRRR",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kHanBMP, kVS17, kLead)));
+ EXPECT_EQ(3, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(ForwardGraphemeBoundaryStatemachineTest,
+ NotBreakImmediatelyAfter_Supplementary_Supplementary) {
+ ForwardGraphemeBoundaryStateMachine machine;
+
+ // SOT + | + U+20000 + U+E0100 + 'a'
+ EXPECT_EQ("SRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kHanSIP, kVS17, 'a')));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+20000 + U+E0100 + U+1F441
+ EXPECT_EQ("SRRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kHanSIP, kVS17, kEye)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+20000 + U+E0100 + EOT
+ EXPECT_EQ("SRRRR", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kHanSIP, kVS17)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+20000 + U+E0100 + [Trail]
+ EXPECT_EQ("SRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kHanSIP, kVS17, kTrail)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+20000 + U+E0100 + [Lead] + 'a'
+ EXPECT_EQ("SRRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kHanSIP, kVS17, kLead, 'a')));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+20000 + U+E0100 + [Lead] + [Lead]
+ EXPECT_EQ("SRRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kHanSIP, kVS17, kLead, kLead)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + U+20000 + U+E0100 + [Lead] + EOT
+ EXPECT_EQ("SRRRRR",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kHanSIP, kVS17, kLead)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(ForwardGraphemeBoundaryStatemachineTest, MuchLongerCase) {
+ ForwardGraphemeBoundaryStateMachine machine;
+
+ const UChar32 kMan = WTF::Unicode::kManCharacter;
+ const UChar32 kZwj = WTF::Unicode::kZeroWidthJoinerCharacter;
+ const UChar32 kHeart = WTF::Unicode::kHeavyBlackHeartCharacter;
+ const UChar32 kKiss = WTF::Unicode::kKissMarkCharacter;
+
+ // U+1F468 U+200D U+2764 U+FE0F U+200D U+1F48B U+200D U+1F468 is a valid ZWJ
+ // emoji sequence.
+ // SOT + | + ZWJ Emoji Sequence + 'a'
+ EXPECT_EQ("SRRRRRRRRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan, 'a')));
+ EXPECT_EQ(11, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + ZWJ Emoji Sequence + U+1F441
+ EXPECT_EQ("SRRRRRRRRRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan, kEye)));
+ EXPECT_EQ(11, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + ZWJ Emoji Sequence + EOT
+ EXPECT_EQ("SRRRRRRRRRRR",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan)));
+ EXPECT_EQ(11, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + ZWJ Emoji Sequence + [Trail]
+ EXPECT_EQ("SRRRRRRRRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan, kTrail)));
+ EXPECT_EQ(11, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + ZWJ Emoji Sequence + [Lead] + 'a'
+ EXPECT_EQ("SRRRRRRRRRRRRF", ProcessSequenceForward(
+ &machine, AsCodePoints(),
+ AsCodePoints(kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan, kLead, 'a')));
+ EXPECT_EQ(11, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + ZWJ Emoji Sequence + [Lead] + [Lead]
+ EXPECT_EQ(
+ "SRRRRRRRRRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan, kLead, kLead)));
+ EXPECT_EQ(11, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + | + ZWJ Emoji Sequence + [Lead] + EOT
+ EXPECT_EQ("SRRRRRRRRRRRR",
+ ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan, kLead)));
+ EXPECT_EQ(11, machine.FinalizeAndGetBoundaryOffset());
+
+ // Preceding text should not affect the result except for flags.
+ // 'a' + | + ZWJ Emoji Sequence + [Lead] + EOT
+ EXPECT_EQ("SRRRRRRRRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints('a'),
+ AsCodePoints(kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan, 'a')));
+ EXPECT_EQ(11, machine.FinalizeAndGetBoundaryOffset());
+
+ // U+1F441 + | + ZWJ Emoji Sequence + [Lead] + EOT
+ EXPECT_EQ("RSRRRRRRRRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kEye),
+ AsCodePoints(kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan, 'a')));
+ EXPECT_EQ(11, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Lead] + | + ZWJ Emoji Sequence + [Lead] + EOT
+ EXPECT_EQ("SRRRRRRRRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kLead),
+ AsCodePoints(kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan, 'a')));
+ EXPECT_EQ(11, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [Trail] + | + ZWJ Emoji Sequence + [Lead] + EOT
+ EXPECT_EQ("RSRRRRRRRRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints('a', kTrail),
+ AsCodePoints(kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan, 'a')));
+ EXPECT_EQ(11, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Trail] + [Trail] + | + ZWJ Emoji Sequence + [Lead] + EOT
+ EXPECT_EQ("RSRRRRRRRRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kTrail, kTrail),
+ AsCodePoints(kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan, 'a')));
+ EXPECT_EQ(11, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [Trail] + | + ZWJ Emoji Sequence + [Lead] + EOT
+ EXPECT_EQ("RSRRRRRRRRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kTrail),
+ AsCodePoints(kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan, 'a')));
+ EXPECT_EQ(11, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [U] + | + ZWJ Emoji Sequence + [Lead] + EOT
+ EXPECT_EQ("RRSRRRRRRRRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints('a', kRisU),
+ AsCodePoints(kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan, 'a')));
+ EXPECT_EQ(11, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [U] + [S] + | + ZWJ Emoji Sequence + [Lead] + EOT
+ EXPECT_EQ("RRRRSRRRRRRRRRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints('a', kRisU, kRisS),
+ AsCodePoints(kMan, kZwj, kHeart, kVS16, kZwj,
+ kKiss, kZwj, kMan, 'a')));
+ EXPECT_EQ(11, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(ForwardGraphemeBoundaryStatemachineTest, singleFlags) {
+ ForwardGraphemeBoundaryStateMachine machine;
+
+ // SOT + | + [U] + [S]
+ EXPECT_EQ("SRRRF", ProcessSequenceForward(&machine, AsCodePoints(),
+ AsCodePoints(kRisU, kRisS)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + | + [U] + [S]
+ EXPECT_EQ("SRRRF", ProcessSequenceForward(&machine, AsCodePoints('a'),
+ AsCodePoints(kRisU, kRisS)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // U+1F441 + | + [U] + [S]
+ EXPECT_EQ("RSRRRF", ProcessSequenceForward(&machine, AsCodePoints(kEye),
+ AsCodePoints(kRisU, kRisS)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Lead] + | + [U] + [S]
+ EXPECT_EQ("SRRRF", ProcessSequenceForward(&machine, AsCodePoints(kLead),
+ AsCodePoints(kRisU, kRisS)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [Trail] + | + [U] + [S]
+ EXPECT_EQ("RSRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints('a', kTrail),
+ AsCodePoints(kRisU, kRisS)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Trail] + [Trail] + | + [U] + [S]
+ EXPECT_EQ("RSRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kTrail, kTrail),
+ AsCodePoints(kRisU, kRisS)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [Trail] + | + [U] + [S]
+ EXPECT_EQ("RSRRRF", ProcessSequenceForward(&machine, AsCodePoints(kTrail),
+ AsCodePoints(kRisU, kRisS)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(ForwardGraphemeBoundaryStatemachineTest, twoFlags) {
+ ForwardGraphemeBoundaryStateMachine machine;
+
+ // SOT + [U] + [S] + | + [U] + [S]
+ EXPECT_EQ("RRRRSRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kRisU, kRisS),
+ AsCodePoints(kRisU, kRisS)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [U] + [S] + | + [U] + [S]
+ EXPECT_EQ("RRRRSRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints('a', kRisU, kRisS),
+ AsCodePoints(kRisU, kRisS)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // U+1F441 + [U] + [S] + | + [U] + [S]
+ EXPECT_EQ("RRRRRSRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kEye, kRisU, kRisS),
+ AsCodePoints(kRisU, kRisS)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Lead] + [U] + [S] + | + [U] + [S]
+ EXPECT_EQ("RRRRSRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kLead, kRisU, kRisS),
+ AsCodePoints(kRisU, kRisS)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [Trail] + [U] + [S] + | + [U] + [S]
+ EXPECT_EQ("RRRRRSRRRF", ProcessSequenceForward(
+ &machine, AsCodePoints('a', kTrail, kRisU, kRisS),
+ AsCodePoints(kRisU, kRisS)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Trail] + [Trail] + [U] + [S] + | + [U] + [S]
+ EXPECT_EQ("RRRRRSRRRF",
+ ProcessSequenceForward(&machine,
+ AsCodePoints(kTrail, kTrail, kRisU, kRisS),
+ AsCodePoints(kRisU, kRisS)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [Trail] + [U] + [S] + | + [U] + [S]
+ EXPECT_EQ("RRRRRSRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kTrail, kRisU, kRisS),
+ AsCodePoints(kRisU, kRisS)));
+ EXPECT_EQ(4, machine.FinalizeAndGetBoundaryOffset());
+}
+
+TEST_F(ForwardGraphemeBoundaryStatemachineTest, oddNumberedFlags) {
+ ForwardGraphemeBoundaryStateMachine machine;
+
+ // SOT + [U] + | + [S] + [S]
+ EXPECT_EQ("RRSRRRF", ProcessSequenceForward(&machine, AsCodePoints(kRisU),
+ AsCodePoints(kRisS, kRisU)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [U] + | + [S] + [S]
+ EXPECT_EQ("RRSRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints('a', kRisU),
+ AsCodePoints(kRisS, kRisU)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // U+1F441 + [U] + | + [S] + [S]
+ EXPECT_EQ("RRRSRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kEye, kRisU),
+ AsCodePoints(kRisS, kRisU)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Lead] + [U] + | + [S] + [S]
+ EXPECT_EQ("RRSRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kLead, kRisU),
+ AsCodePoints(kRisS, kRisU)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // 'a' + [Trail] + [U] + | + [S] + [S]
+ EXPECT_EQ("RRRSRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints('a', kTrail, kRisU),
+ AsCodePoints(kRisS, kRisU)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // [Trail] + [Trail] + [U] + | + [S] + [S]
+ EXPECT_EQ("RRRSRRRF", ProcessSequenceForward(
+ &machine, AsCodePoints(kTrail, kTrail, kRisU),
+ AsCodePoints(kRisS, kRisU)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+
+ // SOT + [Trail] + [U] + | + [S] + [S]
+ EXPECT_EQ("RRRSRRRF",
+ ProcessSequenceForward(&machine, AsCodePoints(kTrail, kRisU),
+ AsCodePoints(kRisS, kRisU)));
+ EXPECT_EQ(2, machine.FinalizeAndGetBoundaryOffset());
+}
+
+} // namespace forward_grapheme_boundary_state_machine_test
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_test_util.cc b/chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_test_util.cc
new file mode 100644
index 00000000000..79f914e1696
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_test_util.cc
@@ -0,0 +1,112 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/state_machines/state_machine_test_util.h"
+
+#include <algorithm>
+#include "third_party/blink/renderer/core/editing/state_machines/backward_grapheme_boundary_state_machine.h"
+#include "third_party/blink/renderer/core/editing/state_machines/forward_grapheme_boundary_state_machine.h"
+#include "third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
+
+namespace blink {
+
+namespace {
+char MachineStateToChar(TextSegmentationMachineState state) {
+ static const char kIndicators[] = {
+ 'I', // Invalid
+ 'R', // NeedMoreCodeUnit (Repeat)
+ 'S', // NeedFollowingCodeUnit (Switch)
+ 'F', // Finished
+ };
+ const auto& it = std::begin(kIndicators) + static_cast<size_t>(state);
+ DCHECK_GE(it, std::begin(kIndicators)) << "Unknown backspace value";
+ DCHECK_LT(it, std::end(kIndicators)) << "Unknown backspace value";
+ return *it;
+}
+
+Vector<UChar> CodePointsToCodeUnits(const Vector<UChar32>& code_points) {
+ Vector<UChar> out;
+ for (const auto& code_point : code_points) {
+ if (U16_LENGTH(code_point) == 2) {
+ out.push_back(U16_LEAD(code_point));
+ out.push_back(U16_TRAIL(code_point));
+ } else {
+ out.push_back(static_cast<UChar>(code_point));
+ }
+ }
+ return out;
+}
+
+template <typename StateMachine>
+String ProcessSequence(StateMachine* machine,
+ const Vector<UChar32>& preceding,
+ const Vector<UChar32>& following) {
+ machine->Reset();
+ StringBuilder out;
+ TextSegmentationMachineState state = TextSegmentationMachineState::kInvalid;
+ Vector<UChar> preceding_code_units = CodePointsToCodeUnits(preceding);
+ std::reverse(preceding_code_units.begin(), preceding_code_units.end());
+ for (const auto& code_unit : preceding_code_units) {
+ state = machine->FeedPrecedingCodeUnit(code_unit);
+ out.Append(MachineStateToChar(state));
+ switch (state) {
+ case TextSegmentationMachineState::kInvalid:
+ case TextSegmentationMachineState::kFinished:
+ return out.ToString();
+ case TextSegmentationMachineState::kNeedMoreCodeUnit:
+ continue;
+ case TextSegmentationMachineState::kNeedFollowingCodeUnit:
+ break;
+ }
+ }
+ if (preceding.IsEmpty() ||
+ state == TextSegmentationMachineState::kNeedMoreCodeUnit) {
+ state = machine->TellEndOfPrecedingText();
+ out.Append(MachineStateToChar(state));
+ }
+ if (state == TextSegmentationMachineState::kFinished)
+ return out.ToString();
+
+ Vector<UChar> following_code_units = CodePointsToCodeUnits(following);
+ for (const auto& code_unit : following_code_units) {
+ state = machine->FeedFollowingCodeUnit(code_unit);
+ out.Append(MachineStateToChar(state));
+ switch (state) {
+ case TextSegmentationMachineState::kInvalid:
+ case TextSegmentationMachineState::kFinished:
+ return out.ToString();
+ case TextSegmentationMachineState::kNeedMoreCodeUnit:
+ continue;
+ case TextSegmentationMachineState::kNeedFollowingCodeUnit:
+ break;
+ }
+ }
+ return out.ToString();
+}
+} // namespace
+
+String GraphemeStateMachineTestBase::ProcessSequenceBackward(
+ BackwardGraphemeBoundaryStateMachine* machine,
+ const Vector<UChar32>& preceding) {
+ const String& out = ProcessSequence(machine, preceding, Vector<UChar32>());
+ if (machine->FinalizeAndGetBoundaryOffset() !=
+ machine->FinalizeAndGetBoundaryOffset())
+ return "State machine changes final offset after finished.";
+ return out;
+}
+
+String GraphemeStateMachineTestBase::ProcessSequenceForward(
+ ForwardGraphemeBoundaryStateMachine* machine,
+ const Vector<UChar32>& preceding,
+ const Vector<UChar32>& following) {
+ const String& out = ProcessSequence(machine, preceding, following);
+ if (machine->FinalizeAndGetBoundaryOffset() !=
+ machine->FinalizeAndGetBoundaryOffset())
+ return "State machine changes final offset after finished.";
+ return out;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_test_util.h b/chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_test_util.h
new file mode 100644
index 00000000000..9c2aaa4add4
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_test_util.h
@@ -0,0 +1,59 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_STATE_MACHINE_TEST_UTIL_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_STATE_MACHINE_TEST_UTIL_H_
+
+#include "base/macros.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+#include "third_party/blink/renderer/platform/wtf/vector.h"
+
+namespace blink {
+
+class BackwardGraphemeBoundaryStateMachine;
+class ForwardGraphemeBoundaryStateMachine;
+
+class GraphemeStateMachineTestBase : public testing::Test {
+ protected:
+ GraphemeStateMachineTestBase() = default;
+ ~GraphemeStateMachineTestBase() override = default;
+
+ Vector<UChar32> AsCodePoints() { return Vector<UChar32>(); }
+
+ template <typename... Args>
+ Vector<UChar32> AsCodePoints(Args... args) {
+ UChar32 code_points[] = {args...};
+ Vector<UChar32> result(sizeof...(args));
+ for (size_t index = 0; index < sizeof...(args); ++index)
+ result[index] = code_points[index];
+ return result;
+ }
+
+ // Processes the |machine| with preceding/following code points.
+ // The result string represents the output sequence of the state machine.
+ // Each character represents returned state of each action.
+ // I : Invalid
+ // R : NeedMoreCodeUnit (Repeat)
+ // S : NeedFollowingCodeUnit (Switch)
+ // F : Finished
+ //
+ // For example, if a state machine returns following sequence:
+ // NeedMoreCodeUnit, NeedFollowingCodeUnit, Finished
+ // the returned string will be "RSF".
+ String ProcessSequenceBackward(BackwardGraphemeBoundaryStateMachine*,
+ const Vector<UChar32>& preceding);
+
+ String ProcessSequenceForward(ForwardGraphemeBoundaryStateMachine*,
+ const Vector<UChar32>& preceding,
+ const Vector<UChar32>& following);
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(GraphemeStateMachineTestBase);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_STATE_MACHINE_TEST_UTIL_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_util.cc b/chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_util.cc
new file mode 100644
index 00000000000..fc998b6ed1c
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_util.cc
@@ -0,0 +1,131 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/state_machines/state_machine_util.h"
+
+#include "third_party/blink/renderer/platform/text/character.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+#include "third_party/blink/renderer/platform/wtf/text/character_names.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+
+namespace blink {
+
+namespace {
+
+// Returns true if the code point has E_Basae_GAZ grapheme break property.
+// See
+// http://www.unicode.org/Public/9.0.0/ucd/auxiliary/GraphemeBreakProperty-9.0.0d18.txt
+bool IsEBaseGAZ(uint32_t code_point) {
+ return code_point == WTF::Unicode::kBoyCharacter ||
+ code_point == WTF::Unicode::kGirlCharacter ||
+ code_point == WTF::Unicode::kManCharacter ||
+ code_point == WTF::Unicode::kWomanCharacter;
+}
+
+// The list of code points which has Indic_Syllabic_Category=Virama property.
+// Must be sorted.
+// See http://www.unicode.org/Public/9.0.0/ucd/IndicSyllabicCategory-9.0.0d2.txt
+const uint32_t kIndicSyllabicCategoryViramaList[] = {
+ // Do not include 0+0BCD TAMIL SIGN VIRAMA as Tamil works differently from
+ // other Indic languages. See crbug.com/693687.
+ 0x094D, 0x09CD, 0x0A4D, 0x0ACD, 0x0B4D, 0x0C4D, 0x0CCD, 0x0D4D,
+ 0x0DCA, 0x1B44, 0xA8C4, 0xA9C0, 0x11046, 0x110B9, 0x111C0, 0x11235,
+ 0x1134D, 0x11442, 0x114C2, 0x115BF, 0x1163F, 0x116B6, 0x11C3F,
+};
+
+// Returns true if the code point has Indic_Syllabic_Category=Virama property.
+// See http://www.unicode.org/Public/9.0.0/ucd/IndicSyllabicCategory-9.0.0d2.txt
+bool IsIndicSyllabicCategoryVirama(uint32_t code_point) {
+ const int length = WTF_ARRAY_LENGTH(kIndicSyllabicCategoryViramaList);
+ return std::binary_search(kIndicSyllabicCategoryViramaList,
+ kIndicSyllabicCategoryViramaList + length,
+ code_point);
+}
+
+} // namespace
+
+bool IsGraphemeBreak(UChar32 prev_code_point, UChar32 next_code_point) {
+ // The following breaking rules come from Unicode Standard Annex #29 on
+ // Unicode Text Segmaentation. See http://www.unicode.org/reports/tr29/
+ // Note that some of rules are in proposal.
+ // Also see http://www.unicode.org/reports/tr29/proposed.html
+ int prev_prop =
+ u_getIntPropertyValue(prev_code_point, UCHAR_GRAPHEME_CLUSTER_BREAK);
+ int next_prop =
+ u_getIntPropertyValue(next_code_point, UCHAR_GRAPHEME_CLUSTER_BREAK);
+
+ // Rule1 GB1 sot ÷
+ // Rule2 GB2 ÷ eot
+ // Should be handled by caller.
+
+ // Rule GB3, CR x LF
+ if (prev_prop == U_GCB_CR && next_prop == U_GCB_LF)
+ return false;
+
+ // Rule GB4, (Control | CR | LF) ÷
+ if (prev_prop == U_GCB_CONTROL || prev_prop == U_GCB_CR ||
+ prev_prop == U_GCB_LF)
+ return true;
+
+ // Rule GB5, ÷ (Control | CR | LF)
+ if (next_prop == U_GCB_CONTROL || next_prop == U_GCB_CR ||
+ next_prop == U_GCB_LF)
+ return true;
+
+ // Rule GB6, L x (L | V | LV | LVT)
+ if (prev_prop == U_GCB_L && (next_prop == U_GCB_L || next_prop == U_GCB_V ||
+ next_prop == U_GCB_LV || next_prop == U_GCB_LVT))
+ return false;
+
+ // Rule GB7, (LV | V) x (V | T)
+ if ((prev_prop == U_GCB_LV || prev_prop == U_GCB_V) &&
+ (next_prop == U_GCB_V || next_prop == U_GCB_T))
+ return false;
+
+ // Rule GB8, (LVT | T) x T
+ if ((prev_prop == U_GCB_LVT || prev_prop == U_GCB_T) && next_prop == U_GCB_T)
+ return false;
+
+ // Rule GB8a
+ //
+ // sot (RI RI)* RI x RI
+ // [^RI] (RI RI)* RI x RI
+ // RI ÷ RI
+ if (Character::IsRegionalIndicator(prev_code_point) &&
+ Character::IsRegionalIndicator(next_code_point))
+ NOTREACHED() << "Do not use this function for regional indicators.";
+
+ // Rule GB9, x (Extend | ZWJ)
+ // Rule GB9a, x SpacingMark
+ if (next_prop == U_GCB_EXTEND ||
+ next_code_point == kZeroWidthJoinerCharacter ||
+ next_prop == U_GCB_SPACING_MARK)
+ return false;
+
+ // Rule GB9b, Prepend x
+ if (prev_prop == U_GCB_PREPEND)
+ return false;
+
+ // Cluster Indic syllables together.
+ if (IsIndicSyllabicCategoryVirama(prev_code_point) &&
+ u_getIntPropertyValue(next_code_point, UCHAR_GENERAL_CATEGORY) ==
+ U_OTHER_LETTER)
+ return false;
+
+ // Proposed Rule GB10, (E_Base | EBG) x E_Modifier
+ if ((Character::IsEmojiModifierBase(prev_code_point) ||
+ IsEBaseGAZ(prev_code_point)) &&
+ Character::IsModifier(next_code_point))
+ return false;
+
+ // Proposed Rule GB11, ZWJ x Emoji
+ if (prev_code_point == kZeroWidthJoinerCharacter &&
+ (Character::IsEmoji(next_code_point)))
+ return false;
+
+ // Rule GB999 any ÷ any
+ return true;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_util.h b/chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_util.h
new file mode 100644
index 00000000000..816ae163ca5
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_util.h
@@ -0,0 +1,23 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_STATE_MACHINE_UTIL_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_STATE_MACHINE_UTIL_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+
+namespace blink {
+
+// Returns true if there is a grapheme boundary between prevCodePoint and
+// nextCodePoint.
+// DO NOT USE this function directly since this doesn't care about preceding
+// regional indicator symbols. Use ForwardGraphemeBoundaryStateMachine or
+// BackwardGraphemeBoundaryStateMachine instead.
+CORE_EXPORT bool IsGraphemeBreak(UChar32 prev_code_point,
+ UChar32 next_code_point);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_STATE_MACHINE_UTIL_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_util_test.cc b/chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_util_test.cc
new file mode 100644
index 00000000000..8d71c543fa6
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/state_machine_util_test.cc
@@ -0,0 +1,169 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/state_machines/state_machine_util.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/platform/wtf/text/character_names.h"
+#include "third_party/blink/renderer/platform/wtf/text/unicode.h"
+
+namespace blink {
+
+TEST(StateMachineUtilTest, IsGraphmeBreak_LineBreak) {
+ // U+000AD (SOFT HYPHEN) has Control grapheme property.
+ const UChar32 kControl = WTF::Unicode::kSoftHyphenCharacter;
+
+ // Grapheme Cluster Boundary Rule GB3: CR x LF
+ EXPECT_FALSE(IsGraphemeBreak('\r', '\n'));
+ EXPECT_TRUE(IsGraphemeBreak('\n', '\r'));
+
+ // Grapheme Cluster Boundary Rule GB4: (Control | CR | LF) ÷
+ EXPECT_TRUE(IsGraphemeBreak('\r', 'a'));
+ EXPECT_TRUE(IsGraphemeBreak('\n', 'a'));
+ EXPECT_TRUE(IsGraphemeBreak(kControl, 'a'));
+
+ // Grapheme Cluster Boundary Rule GB5: ÷ (Control | CR | LF)
+ EXPECT_TRUE(IsGraphemeBreak('a', '\r'));
+ EXPECT_TRUE(IsGraphemeBreak('a', '\n'));
+ EXPECT_TRUE(IsGraphemeBreak('a', kControl));
+}
+
+TEST(StateMachineUtilTest, IsGraphmeBreak_Hangul) {
+ // U+1100 (HANGUL CHOSEONG KIYEOK) has L grapheme property.
+ const UChar32 kL = 0x1100;
+ // U+1160 (HANGUL JUNGSEONG FILLER) has V grapheme property.
+ const UChar32 kV = 0x1160;
+ // U+AC00 (HANGUL SYLLABLE GA) has LV grapheme property.
+ const UChar32 kLV = 0xAC00;
+ // U+AC01 (HANGUL SYLLABLE GAG) has LVT grapheme property.
+ const UChar32 kLVT = 0xAC01;
+ // U+11A8 (HANGUL JONGSEONG KIYEOK) has T grapheme property.
+ const UChar32 kT = 0x11A8;
+
+ // Grapheme Cluster Boundary Rule GB6: L x (L | V | LV | LVT)
+ EXPECT_FALSE(IsGraphemeBreak(kL, kL));
+ EXPECT_FALSE(IsGraphemeBreak(kL, kV));
+ EXPECT_FALSE(IsGraphemeBreak(kL, kLV));
+ EXPECT_FALSE(IsGraphemeBreak(kL, kLVT));
+ EXPECT_TRUE(IsGraphemeBreak(kL, kT));
+
+ // Grapheme Cluster Boundary Rule GB7: (LV | V) x (V | T)
+ EXPECT_TRUE(IsGraphemeBreak(kV, kL));
+ EXPECT_FALSE(IsGraphemeBreak(kV, kV));
+ EXPECT_TRUE(IsGraphemeBreak(kV, kLV));
+ EXPECT_TRUE(IsGraphemeBreak(kV, kLVT));
+ EXPECT_FALSE(IsGraphemeBreak(kV, kT));
+
+ // Grapheme Cluster Boundary Rule GB7: (LV | V) x (V | T)
+ EXPECT_TRUE(IsGraphemeBreak(kLV, kL));
+ EXPECT_FALSE(IsGraphemeBreak(kLV, kV));
+ EXPECT_TRUE(IsGraphemeBreak(kLV, kLV));
+ EXPECT_TRUE(IsGraphemeBreak(kLV, kLVT));
+ EXPECT_FALSE(IsGraphemeBreak(kLV, kT));
+
+ // Grapheme Cluster Boundary Rule GB8: (LVT | T) x T
+ EXPECT_TRUE(IsGraphemeBreak(kLVT, kL));
+ EXPECT_TRUE(IsGraphemeBreak(kLVT, kV));
+ EXPECT_TRUE(IsGraphemeBreak(kLVT, kLV));
+ EXPECT_TRUE(IsGraphemeBreak(kLVT, kLVT));
+ EXPECT_FALSE(IsGraphemeBreak(kLVT, kT));
+
+ // Grapheme Cluster Boundary Rule GB8: (LVT | T) x T
+ EXPECT_TRUE(IsGraphemeBreak(kT, kL));
+ EXPECT_TRUE(IsGraphemeBreak(kT, kV));
+ EXPECT_TRUE(IsGraphemeBreak(kT, kLV));
+ EXPECT_TRUE(IsGraphemeBreak(kT, kLVT));
+ EXPECT_FALSE(IsGraphemeBreak(kT, kT));
+}
+
+TEST(StateMachineUtilTest, IsGraphmeBreak_Extend_or_ZWJ) {
+ // U+0300 (COMBINING GRAVE ACCENT) has Extend grapheme property.
+ const UChar32 kExtend = 0x0300;
+ // Grapheme Cluster Boundary Rule GB9: x (Extend | ZWJ)
+ EXPECT_FALSE(IsGraphemeBreak('a', kExtend));
+ EXPECT_FALSE(IsGraphemeBreak('a', WTF::Unicode::kZeroWidthJoinerCharacter));
+ EXPECT_FALSE(IsGraphemeBreak(kExtend, kExtend));
+ EXPECT_FALSE(IsGraphemeBreak(WTF::Unicode::kZeroWidthJoinerCharacter,
+ WTF::Unicode::kZeroWidthJoinerCharacter));
+ EXPECT_FALSE(
+ IsGraphemeBreak(kExtend, WTF::Unicode::kZeroWidthJoinerCharacter));
+ EXPECT_FALSE(
+ IsGraphemeBreak(WTF::Unicode::kZeroWidthJoinerCharacter, kExtend));
+}
+
+TEST(StateMachineUtilTest, IsGraphmeBreak_SpacingMark) {
+ // U+0903 (DEVANAGARI SIGN VISARGA) has SpacingMark grapheme property.
+ const UChar32 kSpacingMark = 0x0903;
+
+ // Grapheme Cluster Boundary Rule GB9a: x SpacingMark.
+ EXPECT_FALSE(IsGraphemeBreak('a', kSpacingMark));
+}
+
+// TODO(nona): Introduce tests for GB9b rule once ICU grabs Unicod 9.0.
+// There is no character having Prepend grapheme property in Unicode 8.0.
+
+TEST(StateMachineUtilTest, IsGraphmeBreak_EmojiModifier) {
+ // U+261D (WHITE UP POINTING INDEX) has E_Base grapheme property.
+ const UChar32 kEBase = 0x261D;
+ // U+1F466 (BOY) has E_Base_GAZ grapheme property.
+ const UChar32 kEBaseGAZ = 0x1F466;
+ // U+1F3FB (EMOJI MODIFIER FITZPATRICK TYPE-1-2) has E_Modifier grapheme
+ // property.
+ const UChar32 kEModifier = 0x1F3FB;
+
+ // Grapheme Cluster Boundary Rule GB10: (E_Base, E_Base_GAZ) x E_Modifier
+ EXPECT_FALSE(IsGraphemeBreak(kEBase, kEModifier));
+ EXPECT_FALSE(IsGraphemeBreak(kEBaseGAZ, kEModifier));
+ EXPECT_FALSE(IsGraphemeBreak(kEBase, kEModifier));
+
+ EXPECT_TRUE(IsGraphemeBreak(kEBase, kEBase));
+ EXPECT_TRUE(IsGraphemeBreak(kEBaseGAZ, kEBase));
+ EXPECT_TRUE(IsGraphemeBreak(kEBase, kEBaseGAZ));
+ EXPECT_TRUE(IsGraphemeBreak(kEBaseGAZ, kEBaseGAZ));
+ EXPECT_TRUE(IsGraphemeBreak(kEModifier, kEModifier));
+}
+
+TEST(StateMachineUtilTest, IsGraphmeBreak_ZWJSequecne) {
+ // U+2764 (HEAVY BLACK HEART) has Glue_After_Zwj grapheme property.
+ const UChar32 kGlueAfterZwj = 0x2764;
+ // U+1F466 (BOY) has E_Base_GAZ grapheme property.
+ const UChar32 kEBaseGAZ = 0x1F466;
+ // U+1F5FA (WORLD MAP) doesn'T have Glue_After_Zwj or E_Base_GAZ property
+ // but has Emoji property.
+ const UChar32 kEmoji = 0x1F5FA;
+
+ // Grapheme Cluster Boundary Rule GB11: ZWJ x (Glue_After_Zwj | EBG)
+ EXPECT_FALSE(
+ IsGraphemeBreak(WTF::Unicode::kZeroWidthJoinerCharacter, kGlueAfterZwj));
+ EXPECT_FALSE(
+ IsGraphemeBreak(WTF::Unicode::kZeroWidthJoinerCharacter, kEBaseGAZ));
+ EXPECT_FALSE(
+ IsGraphemeBreak(WTF::Unicode::kZeroWidthJoinerCharacter, kEmoji));
+
+ EXPECT_TRUE(IsGraphemeBreak(kGlueAfterZwj, kEBaseGAZ));
+ EXPECT_TRUE(IsGraphemeBreak(kGlueAfterZwj, kGlueAfterZwj));
+ EXPECT_TRUE(IsGraphemeBreak(kEBaseGAZ, kGlueAfterZwj));
+
+ EXPECT_TRUE(IsGraphemeBreak(WTF::Unicode::kZeroWidthJoinerCharacter, 'a'));
+}
+
+TEST(StateMachineUtilTest, IsGraphmeBreak_IndicSyllabicCategoryVirama) {
+ // U+094D (DEVANAGARI SIGN VIRAMA) has Indic_Syllabic_Category=Virama
+ // property.
+ const UChar32 kVirama = 0x094D;
+
+ // U+0915 (DEVANAGARI LETTER KA). Should not break after kVirama and before
+ // this character.
+ const UChar32 kDevangariKa = 0x0915;
+
+ // Do not break after character having Indic_Syllabic_Category=Virama
+ // property if following character has General_Category=C(Other) property.
+ EXPECT_FALSE(IsGraphemeBreak(kVirama, kDevangariKa));
+
+ // Tamil virama is an exception (crbug.com/693697).
+ const UChar32 kTamilVirama = 0x0BCD;
+ EXPECT_TRUE(IsGraphemeBreak(kTamilVirama, kDevangariKa));
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.cc b/chromium/third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.cc
new file mode 100644
index 00000000000..ab3d0bcfa50
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.cc
@@ -0,0 +1,23 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.h"
+
+#include <ostream> // NOLINT
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+
+namespace blink {
+
+std::ostream& operator<<(std::ostream& os, TextSegmentationMachineState state) {
+ static const char* const kTexts[] = {
+ "Invalid", "NeedMoreCodeUnit", "NeedFollowingCodeUnit", "Finished",
+ };
+
+ const auto& it = std::begin(kTexts) + static_cast<size_t>(state);
+ DCHECK_GE(it, std::begin(kTexts)) << "Unknown state value";
+ DCHECK_LT(it, std::end(kTexts)) << "Unknown state value";
+ return os << *it;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.h b/chromium/third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.h
new file mode 100644
index 00000000000..03552f8409c
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/state_machines/text_segmentation_machine_state.h
@@ -0,0 +1,30 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_TEXT_SEGMENTATION_MACHINE_STATE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_TEXT_SEGMENTATION_MACHINE_STATE_H_
+
+#include <ostream> // NOLINT
+#include "third_party/blink/renderer/core/core_export.h"
+
+namespace blink {
+
+enum class TextSegmentationMachineState {
+ // Indicates the state machine is in invalid state.
+ kInvalid,
+ // Indicates the state machine needs more code units to transit the state.
+ kNeedMoreCodeUnit,
+ // Indicates the state machine needs following code units to transit the
+ // state.
+ kNeedFollowingCodeUnit,
+ // Indicates the state machine found a boundary.
+ kFinished,
+};
+
+CORE_EXPORT std::ostream& operator<<(std::ostream&,
+ TextSegmentationMachineState);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_STATE_MACHINES_TEXT_SEGMENTATION_MACHINE_STATE_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_backend_impl.cc b/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_backend_impl.cc
new file mode 100644
index 00000000000..530134128b8
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_backend_impl.cc
@@ -0,0 +1,63 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/suggestion/text_suggestion_backend_impl.h"
+
+#include "mojo/public/cpp/bindings/strong_binding.h"
+#include "third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+
+namespace blink {
+
+TextSuggestionBackendImpl::TextSuggestionBackendImpl(LocalFrame& frame)
+ : frame_(frame) {}
+
+// static
+void TextSuggestionBackendImpl::Create(
+ LocalFrame* frame,
+ mojom::blink::TextSuggestionBackendRequest request) {
+ mojo::MakeStrongBinding(std::unique_ptr<TextSuggestionBackendImpl>(
+ new TextSuggestionBackendImpl(*frame)),
+ std::move(request));
+}
+
+void TextSuggestionBackendImpl::ApplySpellCheckSuggestion(
+ const WTF::String& suggestion) {
+ if (frame_)
+ frame_->GetTextSuggestionController().ApplySpellCheckSuggestion(suggestion);
+}
+
+void TextSuggestionBackendImpl::ApplyTextSuggestion(int32_t marker_tag,
+ int32_t suggestion_index) {
+ if (frame_) {
+ frame_->GetTextSuggestionController().ApplyTextSuggestion(marker_tag,
+ suggestion_index);
+ }
+}
+
+void TextSuggestionBackendImpl::DeleteActiveSuggestionRange() {
+ if (frame_)
+ frame_->GetTextSuggestionController().DeleteActiveSuggestionRange();
+}
+
+void TextSuggestionBackendImpl::OnNewWordAddedToDictionary(
+ const WTF::String& word) {
+ if (frame_)
+ frame_->GetTextSuggestionController().OnNewWordAddedToDictionary(word);
+}
+
+void TextSuggestionBackendImpl::OnSuggestionMenuClosed() {
+ if (frame_)
+ frame_->GetTextSuggestionController().OnSuggestionMenuClosed();
+}
+
+void TextSuggestionBackendImpl::SuggestionMenuTimeoutCallback(
+ int32_t max_number_of_suggestions) {
+ if (frame_) {
+ frame_->GetTextSuggestionController().SuggestionMenuTimeoutCallback(
+ max_number_of_suggestions);
+ }
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_backend_impl.h b/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_backend_impl.h
new file mode 100644
index 00000000000..b611cda1043
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_backend_impl.h
@@ -0,0 +1,39 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SUGGESTION_TEXT_SUGGESTION_BACKEND_IMPL_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SUGGESTION_TEXT_SUGGESTION_BACKEND_IMPL_H_
+
+#include "third_party/blink/public/platform/input_messages.mojom-blink.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/platform/heap/persistent.h"
+
+namespace blink {
+
+class LocalFrame;
+
+// Implementation of mojom::blink::TextSuggestionBackend
+class CORE_EXPORT TextSuggestionBackendImpl final
+ : public mojom::blink::TextSuggestionBackend {
+ public:
+ static void Create(LocalFrame*, mojom::blink::TextSuggestionBackendRequest);
+
+ void ApplySpellCheckSuggestion(const String& suggestion) final;
+ void ApplyTextSuggestion(int32_t marker_tag, int32_t suggestion_index) final;
+ void DeleteActiveSuggestionRange() final;
+ void OnNewWordAddedToDictionary(const String& word) final;
+ void OnSuggestionMenuClosed() final;
+ void SuggestionMenuTimeoutCallback(int32_t max_number_of_suggestions) final;
+
+ private:
+ explicit TextSuggestionBackendImpl(LocalFrame&);
+
+ WeakPersistent<LocalFrame> frame_;
+
+ DISALLOW_COPY_AND_ASSIGN(TextSuggestionBackendImpl);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SUGGESTION_TEXT_SUGGESTION_BACKEND_IMPL_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller.cc b/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller.cc
new file mode 100644
index 00000000000..6b0d30d38b4
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller.cc
@@ -0,0 +1,638 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller.h"
+
+#include "services/service_manager/public/cpp/interface_provider.h"
+#include "third_party/blink/renderer/core/clipboard/data_transfer_access_policy.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+#include "third_party/blink/renderer/core/editing/markers/spell_check_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker_replacement_scope.h"
+#include "third_party/blink/renderer/core/editing/plain_text_range.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_checker.h"
+#include "third_party/blink/renderer/core/editing/suggestion/text_suggestion_info.h"
+#include "third_party/blink/renderer/core/frame/frame_view.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/layout/layout_theme.h"
+
+namespace blink {
+
+namespace {
+
+bool ShouldDeleteNextCharacter(const Node& marker_text_node,
+ const DocumentMarker& marker) {
+ // If the character immediately following the range to be deleted is a space,
+ // delete it if either of these conditions holds:
+ // - We're deleting at the beginning of the editable text (to avoid ending up
+ // with a space at the beginning)
+ // - The character immediately before the range being deleted is also a space
+ // (to avoid ending up with two adjacent spaces)
+ const EphemeralRange next_character_range =
+ PlainTextRange(marker.EndOffset(), marker.EndOffset() + 1)
+ .CreateRange(*marker_text_node.parentNode());
+ // No character immediately following the range (so it can't be a space)
+ if (next_character_range.IsNull())
+ return false;
+
+ const String next_character_str =
+ PlainText(next_character_range, TextIteratorBehavior::Builder().Build());
+ const UChar next_character = next_character_str[0];
+ // Character immediately following the range is not a space
+ if (next_character != kSpaceCharacter &&
+ next_character != kNoBreakSpaceCharacter)
+ return false;
+
+ // First case: we're deleting at the beginning of the editable text
+ if (marker.StartOffset() == 0)
+ return true;
+
+ const EphemeralRange prev_character_range =
+ PlainTextRange(marker.StartOffset() - 1, marker.StartOffset())
+ .CreateRange(*marker_text_node.parentNode());
+ // Not at beginning, but there's no character immediately before the range
+ // being deleted (so it can't be a space)
+ if (prev_character_range.IsNull())
+ return false;
+
+ const String prev_character_str =
+ PlainText(prev_character_range, TextIteratorBehavior::Builder().Build());
+ // Return true if the character immediately before the range is a space, false
+ // otherwise
+ const UChar prev_character = prev_character_str[0];
+ return prev_character == kSpaceCharacter ||
+ prev_character == kNoBreakSpaceCharacter;
+}
+
+EphemeralRangeInFlatTree ComputeRangeSurroundingCaret(
+ const PositionInFlatTree& caret_position) {
+ const Node* const position_node = caret_position.ComputeContainerNode();
+ const bool is_text_node = position_node->IsTextNode();
+ const unsigned position_offset_in_node =
+ caret_position.ComputeOffsetInContainerNode();
+
+ // If we're in the interior of a text node, we can avoid calling
+ // PreviousPositionOf/NextPositionOf for better efficiency.
+ if (is_text_node && position_offset_in_node != 0 &&
+ position_offset_in_node != ToText(position_node)->length()) {
+ return EphemeralRangeInFlatTree(
+ PositionInFlatTree(position_node, position_offset_in_node - 1),
+ PositionInFlatTree(position_node, position_offset_in_node + 1));
+ }
+
+ const PositionInFlatTree& previous_position =
+ PreviousPositionOf(caret_position, PositionMoveType::kGraphemeCluster);
+
+ const PositionInFlatTree& next_position =
+ NextPositionOf(caret_position, PositionMoveType::kGraphemeCluster);
+
+ return EphemeralRangeInFlatTree(
+ previous_position.IsNull() ? caret_position : previous_position,
+ next_position.IsNull() ? caret_position : next_position);
+}
+
+struct SuggestionInfosWithNodeAndHighlightColor {
+ STACK_ALLOCATED();
+
+ Persistent<Node> text_node;
+ Color highlight_color;
+ Vector<TextSuggestionInfo> suggestion_infos;
+};
+
+SuggestionInfosWithNodeAndHighlightColor ComputeSuggestionInfos(
+ const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>&
+ node_suggestion_marker_pairs,
+ size_t max_number_of_suggestions) {
+ // We look at all suggestion markers touching or overlapping the touched
+ // location to pull suggestions from. We preferentially draw suggestions from
+ // shorter markers first (since we assume they're more specific to the tapped
+ // location) until we hit our limit.
+ HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>
+ node_suggestion_marker_pairs_sorted_by_length =
+ node_suggestion_marker_pairs;
+ std::sort(node_suggestion_marker_pairs_sorted_by_length.begin(),
+ node_suggestion_marker_pairs_sorted_by_length.end(),
+ [](const std::pair<Node*, DocumentMarker*>& pair1,
+ const std::pair<Node*, DocumentMarker*>& pair2) {
+ const int length1 =
+ pair1.second->EndOffset() - pair1.second->StartOffset();
+ const int length2 =
+ pair2.second->EndOffset() - pair2.second->StartOffset();
+ return length1 < length2;
+ });
+
+ SuggestionInfosWithNodeAndHighlightColor
+ suggestion_infos_with_node_and_highlight_color;
+ // In theory, a user could tap right before/after the start of a node and we'd
+ // want to pull in suggestions from either side of the tap. However, this is
+ // an edge case that's unlikely to matter in practice (the user will most
+ // likely just tap in the node where they want to apply the suggestions) and
+ // it complicates implementation, so we require that all suggestions come
+ // from the same text node.
+ suggestion_infos_with_node_and_highlight_color.text_node =
+ node_suggestion_marker_pairs_sorted_by_length.front().first;
+
+ // The highlight color comes from the shortest suggestion marker touching or
+ // intersecting the tapped location. If there's no color set, we use the
+ // default text selection color.
+ const SuggestionMarker& first_suggestion_marker = *ToSuggestionMarker(
+ node_suggestion_marker_pairs_sorted_by_length.front().second);
+
+ suggestion_infos_with_node_and_highlight_color.highlight_color =
+ (first_suggestion_marker.SuggestionHighlightColor() == 0)
+ ? LayoutTheme::TapHighlightColor()
+ : first_suggestion_marker.SuggestionHighlightColor();
+
+ Vector<TextSuggestionInfo>& suggestion_infos =
+ suggestion_infos_with_node_and_highlight_color.suggestion_infos;
+ for (const std::pair<Node*, DocumentMarker*>& node_marker_pair :
+ node_suggestion_marker_pairs_sorted_by_length) {
+ if (node_marker_pair.first !=
+ suggestion_infos_with_node_and_highlight_color.text_node)
+ continue;
+
+ if (suggestion_infos.size() == max_number_of_suggestions)
+ break;
+
+ const SuggestionMarker* marker =
+ ToSuggestionMarker(node_marker_pair.second);
+ const Vector<String>& marker_suggestions = marker->Suggestions();
+ for (size_t suggestion_index = 0;
+ suggestion_index < marker_suggestions.size(); ++suggestion_index) {
+ const String& suggestion = marker_suggestions[suggestion_index];
+ if (suggestion_infos.size() == max_number_of_suggestions)
+ break;
+ if (std::find_if(suggestion_infos.begin(), suggestion_infos.end(),
+ [marker, &suggestion](const TextSuggestionInfo& info) {
+ return info.span_start ==
+ (int32_t)marker->StartOffset() &&
+ info.span_end == (int32_t)marker->EndOffset() &&
+ info.suggestion == suggestion;
+ }) != suggestion_infos.end())
+ continue;
+
+ TextSuggestionInfo suggestion_info;
+ suggestion_info.marker_tag = marker->Tag();
+ suggestion_info.suggestion_index = suggestion_index;
+ suggestion_info.span_start = marker->StartOffset();
+ suggestion_info.span_end = marker->EndOffset();
+ suggestion_info.suggestion = suggestion;
+ suggestion_infos.push_back(suggestion_info);
+ }
+ }
+
+ return suggestion_infos_with_node_and_highlight_color;
+}
+
+} // namespace
+
+TextSuggestionController::TextSuggestionController(LocalFrame& frame)
+ : is_suggestion_menu_open_(false), frame_(&frame) {}
+
+void TextSuggestionController::DocumentAttached(Document* document) {
+ DCHECK(document);
+ SetContext(document);
+}
+
+bool TextSuggestionController::IsMenuOpen() const {
+ return is_suggestion_menu_open_;
+}
+
+void TextSuggestionController::HandlePotentialSuggestionTap(
+ const PositionInFlatTree& caret_position) {
+ // TODO(crbug.com/779126): add support for suggestions in immersive mode.
+ if (GetDocument().GetSettings()->GetImmersiveModeEnabled())
+ return;
+
+ // It's theoretically possible, but extremely unlikely, that the user has
+ // managed to tap on some text after TextSuggestionController has told the
+ // browser to open the text suggestions menu, but before the browser has
+ // actually done so. In this case, we should just ignore the tap.
+ if (is_suggestion_menu_open_)
+ return;
+
+ const EphemeralRangeInFlatTree& range_to_check =
+ ComputeRangeSurroundingCaret(caret_position);
+
+ const std::pair<const Node*, const DocumentMarker*>& node_and_marker =
+ FirstMarkerIntersectingRange(
+ range_to_check, DocumentMarker::kSpelling | DocumentMarker::kGrammar |
+ DocumentMarker::kSuggestion);
+ if (!node_and_marker.first)
+ return;
+
+ if (!text_suggestion_host_) {
+ GetFrame().GetInterfaceProvider().GetInterface(
+ mojo::MakeRequest(&text_suggestion_host_));
+ }
+
+ text_suggestion_host_->StartSuggestionMenuTimer();
+}
+
+void TextSuggestionController::Trace(blink::Visitor* visitor) {
+ visitor->Trace(frame_);
+ DocumentShutdownObserver::Trace(visitor);
+}
+
+void TextSuggestionController::ReplaceActiveSuggestionRange(
+ const String& suggestion) {
+ const VisibleSelectionInFlatTree& selection =
+ GetFrame().Selection().ComputeVisibleSelectionInFlatTree();
+ if (selection.IsNone())
+ return;
+
+ const EphemeralRangeInFlatTree& range_to_check =
+ selection.IsRange() ? selection.ToNormalizedEphemeralRange()
+ : ComputeRangeSurroundingCaret(selection.Start());
+ const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>&
+ node_marker_pairs =
+ GetFrame().GetDocument()->Markers().MarkersIntersectingRange(
+ range_to_check, DocumentMarker::kActiveSuggestion);
+
+ if (node_marker_pairs.IsEmpty())
+ return;
+
+ Node* const marker_text_node = node_marker_pairs.front().first;
+ const DocumentMarker* const marker = node_marker_pairs.front().second;
+
+ const EphemeralRange& range_to_replace =
+ EphemeralRange(Position(marker_text_node, marker->StartOffset()),
+ Position(marker_text_node, marker->EndOffset()));
+ ReplaceRangeWithText(range_to_replace, suggestion);
+}
+
+void TextSuggestionController::ApplySpellCheckSuggestion(
+ const String& suggestion) {
+ ReplaceActiveSuggestionRange(suggestion);
+ OnSuggestionMenuClosed();
+}
+
+void TextSuggestionController::ApplyTextSuggestion(int32_t marker_tag,
+ uint32_t suggestion_index) {
+ const VisibleSelectionInFlatTree& selection =
+ GetFrame().Selection().ComputeVisibleSelectionInFlatTree();
+ if (selection.IsNone()) {
+ OnSuggestionMenuClosed();
+ return;
+ }
+
+ const EphemeralRangeInFlatTree& range_to_check =
+ selection.IsRange() ? selection.ToNormalizedEphemeralRange()
+ : ComputeRangeSurroundingCaret(selection.Start());
+
+ const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>&
+ node_marker_pairs =
+ GetFrame().GetDocument()->Markers().MarkersIntersectingRange(
+ range_to_check, DocumentMarker::kSuggestion);
+
+ const Node* marker_text_node = nullptr;
+ SuggestionMarker* marker = nullptr;
+ for (const std::pair<Member<Node>, Member<DocumentMarker>>& node_marker_pair :
+ node_marker_pairs) {
+ SuggestionMarker* suggestion_marker =
+ ToSuggestionMarker(node_marker_pair.second);
+ if (suggestion_marker->Tag() == marker_tag) {
+ marker_text_node = node_marker_pair.first;
+ marker = suggestion_marker;
+ break;
+ }
+ }
+
+ if (!marker) {
+ OnSuggestionMenuClosed();
+ return;
+ }
+
+ const EphemeralRange& range_to_replace =
+ EphemeralRange(Position(marker_text_node, marker->StartOffset()),
+ Position(marker_text_node, marker->EndOffset()));
+
+ const String& replacement = marker->Suggestions()[suggestion_index];
+ const String& new_suggestion = PlainText(range_to_replace);
+
+ {
+ SuggestionMarkerReplacementScope scope;
+ ReplaceRangeWithText(range_to_replace, replacement);
+ }
+
+ if (marker->IsMisspelling()) {
+ GetFrame().GetDocument()->Markers().RemoveSuggestionMarkerByTag(
+ marker_text_node, marker->Tag());
+ } else {
+ marker->SetSuggestion(suggestion_index, new_suggestion);
+ }
+
+ OnSuggestionMenuClosed();
+}
+
+void TextSuggestionController::DeleteActiveSuggestionRange() {
+ AttemptToDeleteActiveSuggestionRange();
+ OnSuggestionMenuClosed();
+}
+
+void TextSuggestionController::OnNewWordAddedToDictionary(const String& word) {
+ // Android pops up a dialog to let the user confirm they actually want to add
+ // the word to the dictionary; this method gets called as soon as the dialog
+ // is shown. So the word isn't actually in the dictionary here, even if the
+ // user will end up confirming the dialog, and we shouldn't try to re-run
+ // spellcheck here.
+
+ // Note: this actually matches the behavior in native Android text boxes
+ GetDocument().Markers().RemoveSpellingMarkersUnderWords(
+ Vector<String>({word}));
+ OnSuggestionMenuClosed();
+}
+
+void TextSuggestionController::OnSuggestionMenuClosed() {
+ if (!IsAvailable())
+ return;
+
+ GetDocument().Markers().RemoveMarkersOfTypes(
+ DocumentMarker::kActiveSuggestion);
+ GetFrame().Selection().SetCaretVisible(true);
+ is_suggestion_menu_open_ = false;
+}
+
+void TextSuggestionController::SuggestionMenuTimeoutCallback(
+ size_t max_number_of_suggestions) {
+ if (!IsAvailable())
+ return;
+
+ const VisibleSelectionInFlatTree& selection =
+ GetFrame().Selection().ComputeVisibleSelectionInFlatTree();
+ if (selection.IsNone())
+ return;
+
+ const EphemeralRangeInFlatTree& range_to_check =
+ selection.IsRange() ? selection.ToNormalizedEphemeralRange()
+ : ComputeRangeSurroundingCaret(selection.Start());
+
+ // We can show a menu if the user tapped on either a spellcheck marker or a
+ // suggestion marker. Suggestion markers take precedence (we don't even try
+ // to draw both underlines, suggestion wins).
+ const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>&
+ node_suggestion_marker_pairs =
+ GetFrame().GetDocument()->Markers().MarkersIntersectingRange(
+ range_to_check, DocumentMarker::kSuggestion);
+ if (!node_suggestion_marker_pairs.IsEmpty()) {
+ ShowSuggestionMenu(node_suggestion_marker_pairs, max_number_of_suggestions);
+ return;
+ }
+
+ // If we didn't find any suggestion markers, look for spell check markers.
+ const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>
+ node_spelling_marker_pairs =
+ GetFrame().GetDocument()->Markers().MarkersIntersectingRange(
+ range_to_check, DocumentMarker::MisspellingMarkers());
+ if (!node_spelling_marker_pairs.IsEmpty())
+ ShowSpellCheckMenu(node_spelling_marker_pairs.front());
+
+ // If we get here, that means the user tapped on a spellcheck or suggestion
+ // marker a few hundred milliseconds ago (to start the double-click timer)
+ // but it's gone now. Oh well...
+}
+
+void TextSuggestionController::ShowSpellCheckMenu(
+ const std::pair<Node*, DocumentMarker*>& node_spelling_marker_pair) {
+ Node* const marker_text_node = node_spelling_marker_pair.first;
+ SpellCheckMarker* const marker =
+ ToSpellCheckMarker(node_spelling_marker_pair.second);
+
+ const EphemeralRange active_suggestion_range =
+ EphemeralRange(Position(marker_text_node, marker->StartOffset()),
+ Position(marker_text_node, marker->EndOffset()));
+ const String& misspelled_word = PlainText(active_suggestion_range);
+ const String& description = marker->Description();
+
+ is_suggestion_menu_open_ = true;
+ GetFrame().Selection().SetCaretVisible(false);
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ active_suggestion_range, SK_ColorTRANSPARENT,
+ ui::mojom::ImeTextSpanThickness::kNone,
+ LayoutTheme::GetTheme().PlatformActiveSpellingMarkerHighlightColor());
+
+ Vector<String> suggestions;
+ description.Split('\n', suggestions);
+
+ Vector<mojom::blink::SpellCheckSuggestionPtr> suggestion_ptrs;
+ for (const String& suggestion : suggestions) {
+ mojom::blink::SpellCheckSuggestionPtr info_ptr(
+ mojom::blink::SpellCheckSuggestion::New());
+ info_ptr->suggestion = suggestion;
+ suggestion_ptrs.push_back(std::move(info_ptr));
+ }
+
+ const IntRect& absolute_bounds = GetFrame().Selection().AbsoluteCaretBounds();
+ const IntRect& viewport_bounds =
+ GetFrame().View()->ContentsToViewport(absolute_bounds);
+
+ text_suggestion_host_->ShowSpellCheckSuggestionMenu(
+ viewport_bounds.X(), viewport_bounds.MaxY(), std::move(misspelled_word),
+ std::move(suggestion_ptrs));
+}
+
+void TextSuggestionController::ShowSuggestionMenu(
+ const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>&
+ node_suggestion_marker_pairs,
+ size_t max_number_of_suggestions) {
+ DCHECK(!node_suggestion_marker_pairs.IsEmpty());
+
+ SuggestionInfosWithNodeAndHighlightColor
+ suggestion_infos_with_node_and_highlight_color = ComputeSuggestionInfos(
+ node_suggestion_marker_pairs, max_number_of_suggestions);
+
+ Vector<TextSuggestionInfo>& suggestion_infos =
+ suggestion_infos_with_node_and_highlight_color.suggestion_infos;
+ int span_union_start = suggestion_infos[0].span_start;
+ int span_union_end = suggestion_infos[0].span_end;
+ for (size_t i = 1; i < suggestion_infos.size(); ++i) {
+ span_union_start =
+ std::min(span_union_start, suggestion_infos[i].span_start);
+ span_union_end = std::max(span_union_end, suggestion_infos[i].span_end);
+ }
+
+ const Node* text_node =
+ suggestion_infos_with_node_and_highlight_color.text_node;
+ for (TextSuggestionInfo& info : suggestion_infos) {
+ const EphemeralRange prefix_range(Position(text_node, span_union_start),
+ Position(text_node, info.span_start));
+ const String& prefix = PlainText(prefix_range);
+
+ const EphemeralRange suffix_range(Position(text_node, info.span_end),
+ Position(text_node, span_union_end));
+ const String& suffix = PlainText(suffix_range);
+
+ info.prefix = prefix;
+ info.suffix = suffix;
+ }
+
+ const EphemeralRange marker_range(Position(text_node, span_union_start),
+ Position(text_node, span_union_end));
+
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ marker_range, SK_ColorTRANSPARENT, ui::mojom::ImeTextSpanThickness::kThin,
+ suggestion_infos_with_node_and_highlight_color.highlight_color);
+
+ is_suggestion_menu_open_ = true;
+ GetFrame().Selection().SetCaretVisible(false);
+
+ const String& misspelled_word = PlainText(marker_range);
+ CallMojoShowTextSuggestionMenu(
+ suggestion_infos_with_node_and_highlight_color.suggestion_infos,
+ misspelled_word);
+}
+
+void TextSuggestionController::CallMojoShowTextSuggestionMenu(
+ const Vector<TextSuggestionInfo>& text_suggestion_infos,
+ const String& misspelled_word) {
+ Vector<mojom::blink::TextSuggestionPtr> suggestion_info_ptrs;
+ for (const blink::TextSuggestionInfo& info : text_suggestion_infos) {
+ mojom::blink::TextSuggestionPtr info_ptr(
+ mojom::blink::TextSuggestion::New());
+ info_ptr->marker_tag = info.marker_tag;
+ info_ptr->suggestion_index = info.suggestion_index;
+ info_ptr->prefix = info.prefix;
+ info_ptr->suggestion = info.suggestion;
+ info_ptr->suffix = info.suffix;
+
+ suggestion_info_ptrs.push_back(std::move(info_ptr));
+ }
+
+ const IntRect& absolute_bounds = GetFrame().Selection().AbsoluteCaretBounds();
+ const IntRect& viewport_bounds =
+ GetFrame().View()->ContentsToViewport(absolute_bounds);
+
+ text_suggestion_host_->ShowTextSuggestionMenu(
+ viewport_bounds.X(), viewport_bounds.MaxY(), misspelled_word,
+ std::move(suggestion_info_ptrs));
+}
+
+Document& TextSuggestionController::GetDocument() const {
+ DCHECK(IsAvailable());
+ return *LifecycleContext();
+}
+
+bool TextSuggestionController::IsAvailable() const {
+ return LifecycleContext();
+}
+
+LocalFrame& TextSuggestionController::GetFrame() const {
+ DCHECK(frame_);
+ return *frame_;
+}
+
+std::pair<const Node*, const DocumentMarker*>
+TextSuggestionController::FirstMarkerIntersectingRange(
+ const EphemeralRangeInFlatTree& range,
+ DocumentMarker::MarkerTypes types) const {
+ const Node* const range_start_container =
+ range.StartPosition().ComputeContainerNode();
+ const unsigned range_start_offset =
+ range.StartPosition().ComputeOffsetInContainerNode();
+ const Node* const range_end_container =
+ range.EndPosition().ComputeContainerNode();
+ const unsigned range_end_offset =
+ range.EndPosition().ComputeOffsetInContainerNode();
+
+ for (const Node& node : range.Nodes()) {
+ if (!node.IsTextNode())
+ continue;
+
+ const unsigned start_offset =
+ node == range_start_container ? range_start_offset : 0;
+ const unsigned end_offset = node == range_end_container
+ ? range_end_offset
+ : ToText(node).length();
+
+ const DocumentMarker* const found_marker =
+ GetFrame().GetDocument()->Markers().FirstMarkerIntersectingOffsetRange(
+ ToText(node), start_offset, end_offset, types);
+ if (found_marker)
+ return std::make_pair(&node, found_marker);
+ }
+
+ return {};
+}
+
+std::pair<const Node*, const DocumentMarker*>
+TextSuggestionController::FirstMarkerTouchingSelection(
+ DocumentMarker::MarkerTypes types) const {
+ const VisibleSelectionInFlatTree& selection =
+ GetFrame().Selection().ComputeVisibleSelectionInFlatTree();
+ if (selection.IsNone())
+ return {};
+
+ const EphemeralRangeInFlatTree& range_to_check =
+ selection.IsRange()
+ ? EphemeralRangeInFlatTree(selection.Start(), selection.End())
+ : ComputeRangeSurroundingCaret(selection.Start());
+
+ return FirstMarkerIntersectingRange(range_to_check, types);
+}
+
+void TextSuggestionController::AttemptToDeleteActiveSuggestionRange() {
+ const std::pair<const Node*, const DocumentMarker*>& node_and_marker =
+ FirstMarkerTouchingSelection(DocumentMarker::kActiveSuggestion);
+ if (!node_and_marker.first)
+ return;
+
+ const Node* const marker_text_node = node_and_marker.first;
+ const DocumentMarker* const marker = node_and_marker.second;
+
+ const bool delete_next_char =
+ ShouldDeleteNextCharacter(*marker_text_node, *marker);
+
+ const EphemeralRange range_to_delete = EphemeralRange(
+ Position(marker_text_node, marker->StartOffset()),
+ Position(marker_text_node, marker->EndOffset() + delete_next_char));
+ ReplaceRangeWithText(range_to_delete, "");
+}
+
+void TextSuggestionController::ReplaceRangeWithText(const EphemeralRange& range,
+ const String& replacement) {
+ GetFrame().Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder().SetBaseAndExtent(range).Build());
+
+ // TODO(editing-dev): We should check whether |TextSuggestionController| is
+ // available or not.
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ // Dispatch 'beforeinput'.
+ Element* const target = FindEventTargetFrom(
+ GetFrame(), GetFrame().Selection().ComputeVisibleSelectionInDOMTree());
+
+ DataTransfer* const data_transfer = DataTransfer::Create(
+ DataTransfer::DataTransferType::kInsertReplacementText,
+ DataTransferAccessPolicy::kReadable,
+ DataObject::CreateFromString(replacement));
+
+ const bool is_canceled =
+ DispatchBeforeInputDataTransfer(
+ target, InputEvent::InputType::kInsertReplacementText,
+ data_transfer) != DispatchEventResult::kNotCanceled;
+
+ // 'beforeinput' event handler may destroy target frame.
+ if (!IsAvailable())
+ return;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. See http://crbug.com/590369 for more details.
+ GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ if (is_canceled)
+ return;
+ GetFrame().GetEditor().ReplaceSelectionWithText(
+ replacement, false, false, InputEvent::InputType::kInsertReplacementText);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller.h b/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller.h
new file mode 100644
index 00000000000..bfc7d5fe84b
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller.h
@@ -0,0 +1,81 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SUGGESTION_TEXT_SUGGESTION_CONTROLLER_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SUGGESTION_TEXT_SUGGESTION_CONTROLLER_H_
+
+#include "third_party/blink/public/platform/input_host.mojom-blink.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/dom/document_shutdown_observer.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+class Document;
+class DocumentMarker;
+class LocalFrame;
+struct TextSuggestionInfo;
+
+// This class handles functionality related to displaying a menu of text
+// suggestions (e.g. from spellcheck), and performing actions relating to those
+// suggestions. Android is currently the only platform that has such a menu.
+class CORE_EXPORT TextSuggestionController final
+ : public GarbageCollectedFinalized<TextSuggestionController>,
+ public DocumentShutdownObserver {
+ USING_GARBAGE_COLLECTED_MIXIN(TextSuggestionController);
+
+ public:
+ explicit TextSuggestionController(LocalFrame&);
+
+ void DocumentAttached(Document*);
+
+ bool IsMenuOpen() const;
+
+ void HandlePotentialSuggestionTap(const PositionInFlatTree& caret_position);
+
+ void ApplySpellCheckSuggestion(const String& suggestion);
+ void ApplyTextSuggestion(int32_t marker_tag, uint32_t suggestion_index);
+ void DeleteActiveSuggestionRange();
+ void OnNewWordAddedToDictionary(const String& word);
+ void OnSuggestionMenuClosed();
+ void SuggestionMenuTimeoutCallback(size_t max_number_of_suggestions);
+
+ void Trace(blink::Visitor*);
+
+ private:
+ Document& GetDocument() const;
+ bool IsAvailable() const;
+ LocalFrame& GetFrame() const;
+
+ std::pair<const Node*, const DocumentMarker*> FirstMarkerIntersectingRange(
+ const EphemeralRangeInFlatTree&,
+ DocumentMarker::MarkerTypes) const;
+ std::pair<const Node*, const DocumentMarker*> FirstMarkerTouchingSelection(
+ DocumentMarker::MarkerTypes) const;
+
+ void AttemptToDeleteActiveSuggestionRange();
+ void CallMojoShowTextSuggestionMenu(
+ const Vector<TextSuggestionInfo>& text_suggestion_infos,
+ const String& misspelled_word);
+ void ShowSpellCheckMenu(
+ const std::pair<Node*, DocumentMarker*>& node_spelling_marker_pair);
+ void ShowSuggestionMenu(
+ const HeapVector<std::pair<Member<Node>, Member<DocumentMarker>>>&
+ node_suggestion_marker_pairs,
+ size_t max_number_of_suggestions);
+ void ReplaceActiveSuggestionRange(const String&);
+ void ReplaceRangeWithText(const EphemeralRange&, const String& replacement);
+
+ bool is_suggestion_menu_open_;
+ const Member<LocalFrame> frame_;
+ mojom::blink::TextSuggestionHostPtr text_suggestion_host_;
+
+ DISALLOW_COPY_AND_ASSIGN(TextSuggestionController);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SUGGESTION_TEXT_SUGGESTION_CONTROLLER_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller_test.cc b/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller_test.cc
new file mode 100644
index 00000000000..f7906a27bbe
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller_test.cc
@@ -0,0 +1,442 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/suggestion/text_suggestion_controller.h"
+
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
+#include "third_party/blink/renderer/core/editing/markers/suggestion_marker_properties.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/spellcheck/spell_checker.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+
+using ui::mojom::ImeTextSpanThickness;
+
+namespace blink {
+
+class TextSuggestionControllerTest : public EditingTestBase {};
+
+TEST_F(TextSuggestionControllerTest, ApplySpellCheckSuggestion) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "spllchck"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ EphemeralRange(Position(text, 0), Position(text, 8)), Color::kBlack,
+ ImeTextSpanThickness::kThin, Color::kBlack);
+ // Select immediately before misspelling
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 0), Position(text, 0))
+ .Build());
+ GetDocument()
+ .GetFrame()
+ ->GetTextSuggestionController()
+ .ApplySpellCheckSuggestion("spellcheck");
+
+ EXPECT_EQ("spellcheck", text->textContent());
+
+ // Cursor should be at end of replaced text
+ const VisibleSelectionInFlatTree& selection =
+ GetFrame().Selection().ComputeVisibleSelectionInFlatTree();
+ EXPECT_EQ(text, selection.Start().ComputeContainerNode());
+ EXPECT_EQ(10, selection.Start().ComputeOffsetInContainerNode());
+ EXPECT_EQ(text, selection.End().ComputeContainerNode());
+ EXPECT_EQ(10, selection.End().ComputeOffsetInContainerNode());
+}
+
+TEST_F(TextSuggestionControllerTest, ApplyTextSuggestion) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "word1 word2 word3 word4"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ // Add marker on "word1". This marker should *not* be cleared by the
+ // replace operation.
+ GetDocument().Markers().AddSuggestionMarker(
+ EphemeralRange(Position(text, 0), Position(text, 5)),
+ SuggestionMarkerProperties::Builder()
+ .SetSuggestions(Vector<String>({"marker1"}))
+ .Build());
+
+ // Add marker on "word1 word2 word3 word4". This marker should *not* be
+ // cleared by the replace operation.
+ GetDocument().Markers().AddSuggestionMarker(
+ EphemeralRange(Position(text, 0), Position(text, 23)),
+ SuggestionMarkerProperties::Builder()
+ .SetSuggestions(Vector<String>({"marker2"}))
+ .Build());
+
+ // Add marker on "word2 word3". This marker should *not* be cleared by the
+ // replace operation.
+ GetDocument().Markers().AddSuggestionMarker(
+ EphemeralRange(Position(text, 6), Position(text, 17)),
+ SuggestionMarkerProperties::Builder()
+ .SetSuggestions(Vector<String>({"marker3"}))
+ .Build());
+
+ // Add marker on "word4". This marker should *not* be cleared by the
+ // replace operation.
+ GetDocument().Markers().AddSuggestionMarker(
+ EphemeralRange(Position(text, 18), Position(text, 23)),
+ SuggestionMarkerProperties::Builder()
+ .SetSuggestions(Vector<String>({"marker4"}))
+ .Build());
+
+ // Add marker on "word1 word2". This marker should be cleared by the
+ // replace operation.
+ GetDocument().Markers().AddSuggestionMarker(
+ EphemeralRange(Position(text, 0), Position(text, 11)),
+ SuggestionMarkerProperties::Builder()
+ .SetSuggestions(Vector<String>({"marker5"}))
+ .Build());
+
+ // Add marker on "word3 word4". This marker should be cleared by the
+ // replace operation.
+ GetDocument().Markers().AddSuggestionMarker(
+ EphemeralRange(Position(text, 12), Position(text, 23)),
+ SuggestionMarkerProperties::Builder()
+ .SetSuggestions(Vector<String>({"marker6"}))
+ .Build());
+
+ // Select immediately before word2.
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 6), Position(text, 6))
+ .Build());
+
+ // Replace "word2 word3" with "marker3" (marker should have tag 3; tags start
+ // from 1, not 0).
+ GetDocument().GetFrame()->GetTextSuggestionController().ApplyTextSuggestion(
+ 3, 0);
+
+ // This returns the markers sorted by start offset; we need them sorted by
+ // start *and* end offset, since we have multiple markers starting at 0.
+ DocumentMarkerVector markers = GetDocument().Markers().MarkersFor(text);
+ std::sort(markers.begin(), markers.end(),
+ [](const DocumentMarker* marker1, const DocumentMarker* marker2) {
+ if (marker1->StartOffset() != marker2->StartOffset())
+ return marker1->StartOffset() < marker2->StartOffset();
+ return marker1->EndOffset() < marker2->EndOffset();
+ });
+
+ EXPECT_EQ(4u, markers.size());
+
+ // marker1
+ EXPECT_EQ(0u, markers[0]->StartOffset());
+ EXPECT_EQ(5u, markers[0]->EndOffset());
+
+ // marker2
+ EXPECT_EQ(0u, markers[1]->StartOffset());
+ EXPECT_EQ(19u, markers[1]->EndOffset());
+
+ // marker3
+ EXPECT_EQ(6u, markers[2]->StartOffset());
+ EXPECT_EQ(13u, markers[2]->EndOffset());
+
+ const SuggestionMarker* const suggestion_marker =
+ ToSuggestionMarker(markers[2]);
+ EXPECT_EQ(1u, suggestion_marker->Suggestions().size());
+ EXPECT_EQ(String("word2 word3"), suggestion_marker->Suggestions()[0]);
+
+ // marker4
+ EXPECT_EQ(14u, markers[3]->StartOffset());
+ EXPECT_EQ(19u, markers[3]->EndOffset());
+
+ // marker5 and marker6 should've been cleared
+
+ // Cursor should be at end of replaced text
+ const VisibleSelectionInFlatTree& selection =
+ GetFrame().Selection().ComputeVisibleSelectionInFlatTree();
+ EXPECT_EQ(text, selection.Start().ComputeContainerNode());
+ EXPECT_EQ(13, selection.Start().ComputeOffsetInContainerNode());
+ EXPECT_EQ(text, selection.End().ComputeContainerNode());
+ EXPECT_EQ(13, selection.End().ComputeOffsetInContainerNode());
+}
+
+TEST_F(TextSuggestionControllerTest,
+ ApplyingMisspellingTextSuggestionClearsMarker) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "mispelled"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ // Add marker on "mispelled". This marker should be cleared by the replace
+ // operation.
+ GetDocument().Markers().AddSuggestionMarker(
+ EphemeralRange(Position(text, 0), Position(text, 9)),
+ SuggestionMarkerProperties::Builder()
+ .SetType(SuggestionMarker::SuggestionType::kMisspelling)
+ .SetSuggestions(Vector<String>({"misspelled"}))
+ .Build());
+
+ // Check the tag for the marker that was just added (the current tag value is
+ // not reset between test cases).
+ int32_t marker_tag =
+ ToSuggestionMarker(GetDocument().Markers().MarkersFor(text)[0])->Tag();
+
+ // Select immediately before "mispelled".
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 0), Position(text, 0))
+ .Build());
+
+ // Replace "mispelled" with "misspelled".
+ GetDocument().GetFrame()->GetTextSuggestionController().ApplyTextSuggestion(
+ marker_tag, 0);
+
+ EXPECT_EQ(0u, GetDocument().Markers().MarkersFor(text).size());
+ EXPECT_EQ("misspelled", text->textContent());
+}
+
+TEST_F(TextSuggestionControllerTest, DeleteActiveSuggestionRange_DeleteAtEnd) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "word1 word2"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ // Mark "word2" as the active suggestion range
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ EphemeralRange(Position(text, 6), Position(text, 11)),
+ Color::kTransparent, ImeTextSpanThickness::kThin, Color::kBlack);
+ // Select immediately before word2
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 6), Position(text, 6))
+ .Build());
+ GetDocument()
+ .GetFrame()
+ ->GetTextSuggestionController()
+ .DeleteActiveSuggestionRange();
+
+ EXPECT_EQ("word1\xA0", text->textContent());
+}
+
+TEST_F(TextSuggestionControllerTest,
+ DeleteActiveSuggestionRange_DeleteInMiddle) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "word1 word2 word3"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ // Mark "word2" as the active suggestion range
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ EphemeralRange(Position(text, 6), Position(text, 11)),
+ Color::kTransparent, ImeTextSpanThickness::kThin, Color::kBlack);
+ // Select immediately before word2
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 6), Position(text, 6))
+ .Build());
+ GetDocument()
+ .GetFrame()
+ ->GetTextSuggestionController()
+ .DeleteActiveSuggestionRange();
+
+ // One of the extra spaces around "word2" should have been removed
+ EXPECT_EQ("word1\xA0word3", text->textContent());
+}
+
+TEST_F(TextSuggestionControllerTest,
+ DeleteActiveSuggestionRange_DeleteAtBeginningWithSpaceAfter) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "word1 word2"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ // Mark "word1" as the active suggestion range
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ EphemeralRange(Position(text, 0), Position(text, 5)), Color::kTransparent,
+ ImeTextSpanThickness::kThin, Color::kBlack);
+ // Select immediately before word1
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 0), Position(text, 0))
+ .Build());
+ GetDocument()
+ .GetFrame()
+ ->GetTextSuggestionController()
+ .DeleteActiveSuggestionRange();
+
+ // The space after "word1" should have been removed (to avoid leaving an
+ // empty space at the beginning of the composition)
+ EXPECT_EQ("word2", text->textContent());
+}
+
+TEST_F(TextSuggestionControllerTest,
+ DeleteActiveSuggestionRange_DeleteEntireRange) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "word1"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ // Mark "word1" as the active suggestion range
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ EphemeralRange(Position(text, 0), Position(text, 5)), Color::kTransparent,
+ ImeTextSpanThickness::kThin, Color::kBlack);
+ // Select immediately before word1
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 0), Position(text, 0))
+ .Build());
+ GetDocument()
+ .GetFrame()
+ ->GetTextSuggestionController()
+ .DeleteActiveSuggestionRange();
+
+ EXPECT_EQ("", text->textContent());
+}
+
+// The following two cases test situations that probably shouldn't occur in
+// normal use (spell check/suggestoin markers not spanning a whole word), but
+// are included anyway to verify that DeleteActiveSuggestionRange() is
+// well-behaved in these cases
+
+TEST_F(TextSuggestionControllerTest,
+ DeleteActiveSuggestionRange_DeleteRangeWithTextBeforeAndSpaceAfter) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "word1word2 word3"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ // Mark "word2" as the active suggestion range
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ EphemeralRange(Position(text, 5), Position(text, 10)),
+ Color::kTransparent, ImeTextSpanThickness::kThin, Color::kBlack);
+ // Select immediately before word2
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 5), Position(text, 5))
+ .Build());
+ GetDocument()
+ .GetFrame()
+ ->GetTextSuggestionController()
+ .DeleteActiveSuggestionRange();
+
+ EXPECT_EQ("word1\xA0word3", text->textContent());
+}
+
+TEST_F(TextSuggestionControllerTest,
+ DeleteActiveSuggestionRange_DeleteRangeWithSpaceBeforeAndTextAfter) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "word1 word2word3"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ // Mark "word2" as the active suggestion range
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ EphemeralRange(Position(text, 6), Position(text, 11)),
+ Color::kTransparent, ImeTextSpanThickness::kThin, Color::kBlack);
+ // Select immediately before word2
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 6), Position(text, 6))
+ .Build());
+ GetDocument()
+ .GetFrame()
+ ->GetTextSuggestionController()
+ .DeleteActiveSuggestionRange();
+
+ EXPECT_EQ("word1\xA0word3", text->textContent());
+}
+
+TEST_F(TextSuggestionControllerTest,
+ DeleteActiveSuggestionRange_DeleteAtBeginningWithTextAfter) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "word1word2"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ // Mark "word1" as the active suggestion range
+ GetDocument().Markers().AddActiveSuggestionMarker(
+ EphemeralRange(Position(text, 0), Position(text, 5)), Color::kTransparent,
+ ImeTextSpanThickness::kThin, Color::kBlack);
+ // Select immediately before word1
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 0), Position(text, 0))
+ .Build());
+ GetDocument()
+ .GetFrame()
+ ->GetTextSuggestionController()
+ .DeleteActiveSuggestionRange();
+
+ EXPECT_EQ("word2", text->textContent());
+}
+
+TEST_F(TextSuggestionControllerTest,
+ DeleteActiveSuggestionRange_OnNewWordAddedToDictionary) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "embiggen"
+ "</div>");
+ Element* div = GetDocument().QuerySelector("div");
+ Node* text = div->firstChild();
+
+ // Mark "embiggen" as misspelled
+ GetDocument().Markers().AddSpellingMarker(
+ EphemeralRange(Position(text, 0), Position(text, 8)));
+ // Select inside before "embiggen"
+ GetDocument().GetFrame()->Selection().SetSelectionAndEndTyping(
+ SelectionInDOMTree::Builder()
+ .SetBaseAndExtent(Position(text, 1), Position(text, 1))
+ .Build());
+
+ // Add some other word to the dictionary
+ GetDocument()
+ .GetFrame()
+ ->GetTextSuggestionController()
+ .OnNewWordAddedToDictionary("cromulent");
+ // Verify the spelling marker is still present
+ EXPECT_NE(nullptr, GetDocument()
+ .GetFrame()
+ ->GetSpellChecker()
+ .GetSpellCheckMarkerUnderSelection()
+ .first);
+
+ // Add "embiggen" to the dictionary
+ GetDocument()
+ .GetFrame()
+ ->GetTextSuggestionController()
+ .OnNewWordAddedToDictionary("embiggen");
+ // Verify the spelling marker is gone
+ EXPECT_EQ(nullptr, GetDocument()
+ .GetFrame()
+ ->GetSpellChecker()
+ .GetSpellCheckMarkerUnderSelection()
+ .first);
+}
+
+TEST_F(TextSuggestionControllerTest, CallbackHappensAfterDocumentDestroyed) {
+ LocalFrame& frame = *GetDocument().GetFrame();
+ GetDocument().Shutdown();
+
+ // Shouldn't crash
+ frame.GetTextSuggestionController().SuggestionMenuTimeoutCallback(0);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_info.h b/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_info.h
new file mode 100644
index 00000000000..d43da7ce477
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/suggestion/text_suggestion_info.h
@@ -0,0 +1,26 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be found
+// in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SUGGESTION_TEXT_SUGGESTION_INFO_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_SUGGESTION_TEXT_SUGGESTION_INFO_H_
+
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+
+namespace blink {
+
+struct TextSuggestionInfo {
+ int32_t marker_tag;
+ uint32_t suggestion_index;
+
+ int32_t span_start;
+ int32_t span_end;
+
+ String prefix;
+ String suggestion;
+ String suffix;
+};
+
+} // namespace blink
+
+#endif // TextSuggestionList_h
diff --git a/chromium/third_party/blink/renderer/core/editing/testing/editing_test_base.cc b/chromium/third_party/blink/renderer/core/editing/testing/editing_test_base.cc
new file mode 100644
index 00000000000..5a7dff1e67c
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/testing/editing_test_base.cc
@@ -0,0 +1,113 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/selection_sample.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/html/html_collection.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
+
+namespace blink {
+
+namespace {
+
+Element* GetOrCreateElement(ContainerNode* parent,
+ const HTMLQualifiedName& tag_name) {
+ HTMLCollection* elements = parent->getElementsByTagNameNS(
+ tag_name.NamespaceURI(), tag_name.LocalName());
+ if (!elements->IsEmpty())
+ return elements->item(0);
+ return parent->ownerDocument()->CreateRawElement(
+ tag_name, CreateElementFlags::ByCreateElement());
+}
+
+} // namespace
+
+EditingTestBase::EditingTestBase() = default;
+
+EditingTestBase::~EditingTestBase() = default;
+
+void EditingTestBase::InsertStyleElement(const std::string& style_rules) {
+ Element* const head = GetOrCreateElement(&GetDocument(), HTMLNames::headTag);
+ DCHECK_EQ(head, GetOrCreateElement(&GetDocument(), HTMLNames::headTag));
+ Element* const style = GetDocument().CreateRawElement(
+ HTMLNames::styleTag, CreateElementFlags::ByCreateElement());
+ style->setTextContent(String(style_rules.data(), style_rules.size()));
+ head->appendChild(style);
+}
+
+Position EditingTestBase::SetCaretTextToBody(
+ const std::string& selection_text) {
+ const SelectionInDOMTree selection = SetSelectionTextToBody(selection_text);
+ DCHECK(selection.IsCaret())
+ << "|selection_text| should contain a caret marker '|'";
+ return selection.Base();
+}
+
+SelectionInDOMTree EditingTestBase::SetSelectionTextToBody(
+ const std::string& selection_text) {
+ return SetSelectionText(GetDocument().body(), selection_text);
+}
+
+SelectionInDOMTree EditingTestBase::SetSelectionText(
+ HTMLElement* element,
+ const std::string& selection_text) {
+ const SelectionInDOMTree selection =
+ SelectionSample::SetSelectionText(element, selection_text);
+ UpdateAllLifecyclePhases();
+ return selection;
+}
+
+std::string EditingTestBase::GetSelectionTextFromBody(
+ const SelectionInDOMTree& selection) const {
+ return SelectionSample::GetSelectionText(*GetDocument().body(), selection);
+}
+
+std::string EditingTestBase::GetSelectionTextFromBody() const {
+ return GetSelectionTextFromBody(Selection().GetSelectionInDOMTree());
+}
+
+std::string EditingTestBase::GetSelectionTextInFlatTreeFromBody(
+ const SelectionInFlatTree& selection) const {
+ return SelectionSample::GetSelectionTextInFlatTree(*GetDocument().body(),
+ selection);
+}
+
+std::string EditingTestBase::GetCaretTextFromBody(
+ const Position& position) const {
+ DCHECK(position.IsValidFor(GetDocument()))
+ << "A valid position must be provided " << position;
+ return GetSelectionTextFromBody(
+ SelectionInDOMTree::Builder().Collapse(position).Build());
+}
+
+ShadowRoot* EditingTestBase::CreateShadowRootForElementWithIDAndSetInnerHTML(
+ TreeScope& scope,
+ const char* host_element_id,
+ const char* shadow_root_content) {
+ ShadowRoot& shadow_root =
+ scope.getElementById(AtomicString::FromUTF8(host_element_id))
+ ->CreateShadowRootInternal();
+ shadow_root.SetInnerHTMLFromString(String::FromUTF8(shadow_root_content),
+ ASSERT_NO_EXCEPTION);
+ scope.GetDocument().View()->UpdateAllLifecyclePhases();
+ return &shadow_root;
+}
+
+ShadowRoot* EditingTestBase::SetShadowContent(const char* shadow_content,
+ const char* host) {
+ ShadowRoot* shadow_root = CreateShadowRootForElementWithIDAndSetInnerHTML(
+ GetDocument(), host, shadow_content);
+ return shadow_root;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/testing/editing_test_base.h b/chromium/third_party/blink/renderer/core/editing/testing/editing_test_base.h
new file mode 100644
index 00000000000..bd810b72533
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/testing/editing_test_base.h
@@ -0,0 +1,75 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_TESTING_EDITING_TEST_BASE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_TESTING_EDITING_TEST_BASE_H_
+
+#include <gtest/gtest.h>
+#include <memory>
+#include <string>
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/testing/page_test_base.h"
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+
+namespace blink {
+
+class FrameSelection;
+
+class EditingTestBase : public PageTestBase {
+ USING_FAST_MALLOC(EditingTestBase);
+
+ public:
+ static ShadowRoot* CreateShadowRootForElementWithIDAndSetInnerHTML(
+ TreeScope&,
+ const char* host_element_id,
+ const char* shadow_root_content);
+
+ protected:
+ EditingTestBase();
+ ~EditingTestBase() override;
+
+ // Insert STYLE element with |style_rules|, no need to have "<style>", into
+ // HEAD.
+ void InsertStyleElement(const std::string& style_rules);
+
+ // Returns |Position| for specified |caret_text|, which is HTML markup with
+ // caret marker "|".
+ Position SetCaretTextToBody(const std::string& caret_text);
+
+ // Returns |SelectionInDOMTree| for specified |selection_text| by using
+ // |SetSelectionText()| on BODY.
+ SelectionInDOMTree SetSelectionTextToBody(const std::string& selection_text);
+
+ // Sets |HTMLElement#innerHTML| with |selection_text|, which is HTML markup
+ // with selection markers "^" and "|" and returns |SelectionInDOMTree| of
+ // specified selection markers.
+ // See also |GetSelectionText()| which returns selection text from specified
+ // |ContainerNode| and |SelectionInDOMTree|.
+ // Note: Unlike |assert_selection()|, this function doesn't change
+ // |FrameSelection|.
+ SelectionInDOMTree SetSelectionText(HTMLElement*,
+ const std::string& selection_text);
+
+ // Returns selection text for child nodes of BODY with specific |Position|.
+ std::string GetCaretTextFromBody(const Position&) const;
+
+ // Returns selection text for child nodes of BODY with specified
+ // |SelectionInDOMTree|.
+ std::string GetSelectionTextFromBody(const SelectionInDOMTree&) const;
+
+ std::string GetSelectionTextFromBody() const;
+
+ // Returns selection text for child nodes of BODY with specified
+ // |SelectionInFlatTree|.
+ std::string GetSelectionTextInFlatTreeFromBody(
+ const SelectionInFlatTree&) const;
+
+ ShadowRoot* SetShadowContent(const char* shadow_content,
+ const char* shadow_host_id);
+
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_TESTING_EDITING_TEST_BASE_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/testing/editing_test_base_test.cc b/chromium/third_party/blink/renderer/core/editing/testing/editing_test_base_test.cc
new file mode 100644
index 00000000000..9bfff1d4666
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/testing/editing_test_base_test.cc
@@ -0,0 +1,37 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+#include "third_party/blink/renderer/core/editing/position.h"
+
+namespace blink {
+
+class EditingTestBaseTest : public EditingTestBase {};
+
+TEST_F(EditingTestBaseTest, GetCaretTextFromBody) {
+ SetBodyContent("<div>foo</div>");
+ Element* const div = GetDocument().QuerySelector("div");
+ Node* const foo = div->firstChild();
+ EXPECT_EQ("|<div>foo</div>",
+ GetCaretTextFromBody(Position::BeforeNode(*div)));
+
+ // TODO(editing-dev): Consider different serialization for the following two
+ // positions.
+ EXPECT_EQ("<div>|foo</div>",
+ GetCaretTextFromBody(Position::FirstPositionInNode(*div)));
+ EXPECT_EQ("<div>|foo</div>", GetCaretTextFromBody(Position(foo, 0)));
+
+ // TODO(editing-dev): Consider different serialization for the following two
+ // positions.
+ EXPECT_EQ("<div>foo|</div>", GetCaretTextFromBody(Position(foo, 3)));
+ EXPECT_EQ("<div>foo|</div>",
+ GetCaretTextFromBody(Position::LastPositionInNode(*div)));
+
+ EXPECT_EQ("<div>foo</div>|", GetCaretTextFromBody(Position::AfterNode(*div)));
+}
+
+// TODO(editing-dev): Add demos of other functions of EditingTestBase.
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/testing/selection_sample.cc b/chromium/third_party/blink/renderer/core/editing/testing/selection_sample.cc
new file mode 100644
index 00000000000..053f24776e4
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/testing/selection_sample.cc
@@ -0,0 +1,373 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/testing/selection_sample.h"
+
+#include <algorithm>
+
+#include "third_party/blink/renderer/core/dom/attribute.h"
+#include "third_party/blink/renderer/core/dom/character_data.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/processing_instruction.h"
+#include "third_party/blink/renderer/core/dom/shadow_root_init.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/html/html_collection.h"
+#include "third_party/blink/renderer/core/html/html_template_element.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
+
+namespace blink {
+
+namespace {
+
+void ConvertTemplatesToShadowRoots(HTMLElement& element) {
+ // |element| and descendant elements can have TEMPLATE element with
+ // |data-mode="open"|, which is required. Each elemnt can have only one
+ // TEMPLATE element.
+ HTMLCollection* const templates = element.getElementsByTagName("template");
+ HeapVector<Member<Element>> template_vector;
+ for (Element* template_element : *templates)
+ template_vector.push_back(template_element);
+ for (Element* template_element : template_vector) {
+ const AtomicString& data_mode = template_element->getAttribute("data-mode");
+ DCHECK_EQ(data_mode, "open");
+
+ Element* const parent = template_element->parentElement();
+ parent->removeChild(template_element);
+
+ Document* const document = element.ownerDocument();
+ ShadowRoot& shadow_root =
+ parent->AttachShadowRootInternal(ShadowRootType::kOpen);
+ Node* const fragment =
+ document->importNode(ToHTMLTemplateElement(template_element)->content(),
+ true, ASSERT_NO_EXCEPTION);
+ shadow_root.AppendChild(fragment);
+ }
+}
+
+// Parse selection text notation into Selection object.
+class Parser final {
+ STACK_ALLOCATED();
+
+ public:
+ Parser() = default;
+ ~Parser() = default;
+
+ // Set |selection_text| as inner HTML of |element| and returns
+ // |SelectionInDOMTree| marked up within |selection_text|.
+ SelectionInDOMTree SetSelectionText(HTMLElement* element,
+ const std::string& selection_text) {
+ element->SetInnerHTMLFromString(String::FromUTF8(selection_text.c_str()));
+ ConvertTemplatesToShadowRoots(*element);
+ Traverse(element);
+ if (anchor_node_ && focus_node_) {
+ return typename SelectionInDOMTree::Builder()
+ .Collapse(Position(anchor_node_, anchor_offset_))
+ .Extend(Position(focus_node_, focus_offset_))
+ .Build();
+ }
+ DCHECK(focus_node_) << "Need just '|', or '^' and '|'";
+ return typename SelectionInDOMTree::Builder()
+ .Collapse(Position(focus_node_, focus_offset_))
+ .Build();
+ }
+
+ private:
+ // Removes selection markers from |node| and records selection markers as
+ // |Node| and |offset|. The |node| is removed from container when |node|
+ // contains only selection markers.
+ void HandleCharacterData(CharacterData* node) {
+ int anchor_offset = -1;
+ int focus_offset = -1;
+ StringBuilder builder;
+ for (unsigned i = 0; i < node->length(); ++i) {
+ const UChar char_code = node->data()[i];
+ if (char_code == '^') {
+ DCHECK_EQ(anchor_offset, -1) << node->data();
+ anchor_offset = static_cast<int>(builder.length());
+ continue;
+ }
+ if (char_code == '|') {
+ DCHECK_EQ(focus_offset, -1) << node->data();
+ focus_offset = static_cast<int>(builder.length());
+ continue;
+ }
+ builder.Append(char_code);
+ }
+ if (anchor_offset == -1 && focus_offset == -1)
+ return;
+ node->setData(builder.ToString());
+ if (node->length() == 0) {
+ // Remove |node| if it contains only selection markers.
+ ContainerNode* const parent_node = node->parentNode();
+ DCHECK(parent_node) << node;
+ const int offset_in_parent = node->NodeIndex();
+ if (anchor_offset >= 0)
+ RecordSelectionAnchor(parent_node, offset_in_parent);
+ if (focus_offset >= 0)
+ RecordSelectionFocus(parent_node, offset_in_parent);
+ parent_node->removeChild(node);
+ return;
+ }
+ if (anchor_offset >= 0)
+ RecordSelectionAnchor(node, anchor_offset);
+ if (focus_offset >= 0)
+ RecordSelectionFocus(node, focus_offset);
+ }
+
+ void HandleElementNode(Element* element) {
+ if (ShadowRoot* shadow_root = element->ShadowRootIfV1())
+ HandleChildren(shadow_root);
+ HandleChildren(element);
+ }
+
+ void HandleChildren(ContainerNode* node) {
+ Node* runner = node->firstChild();
+ while (runner) {
+ Node* const next_sibling = runner->nextSibling();
+ // |Traverse()| may remove |runner|.
+ Traverse(runner);
+ runner = next_sibling;
+ }
+ }
+
+ void RecordSelectionAnchor(Node* node, int offset) {
+ DCHECK(!anchor_node_) << "Found more than one '^' in " << *anchor_node_
+ << " and " << *node;
+ anchor_node_ = node;
+ anchor_offset_ = offset;
+ }
+
+ void RecordSelectionFocus(Node* node, int offset) {
+ DCHECK(!focus_node_) << "Found more than one '|' in " << *focus_node_
+ << " and " << *node;
+ focus_node_ = node;
+ focus_offset_ = offset;
+ }
+
+ // Traverses descendants of |node|. The |node| may be removed when it is
+ // |CharacterData| node contains only selection markers.
+ void Traverse(Node* node) {
+ if (node->IsElementNode()) {
+ HandleElementNode(ToElement(node));
+ return;
+ }
+ if (node->IsCharacterDataNode()) {
+ HandleCharacterData(ToCharacterData(node));
+ return;
+ }
+ NOTREACHED() << node;
+ }
+
+ Member<Node> anchor_node_;
+ Member<Node> focus_node_;
+ int anchor_offset_ = 0;
+ int focus_offset_ = 0;
+};
+
+// Serialize DOM/Flat tree to selection text.
+template <typename Strategy>
+class Serializer final {
+ STACK_ALLOCATED();
+
+ public:
+ explicit Serializer(const SelectionTemplate<Strategy>& selection)
+ : selection_(selection) {}
+
+ std::string Serialize(const ContainerNode& root) {
+ SerializeChildren(root);
+ return builder_.ToString().Utf8().data();
+ }
+
+ private:
+ void HandleCharacterData(const CharacterData& node) {
+ const String text = node.data();
+ if (selection_.IsNone()) {
+ builder_.Append(text);
+ return;
+ }
+ const Node& base_node = *selection_.Base().ComputeContainerNode();
+ const Node& extent_node = *selection_.Extent().ComputeContainerNode();
+ const int base_offset = selection_.Base().ComputeOffsetInContainerNode();
+ const int extent_offset =
+ selection_.Extent().ComputeOffsetInContainerNode();
+ if (base_node == node && extent_node == node) {
+ if (base_offset == extent_offset) {
+ builder_.Append(text.Left(base_offset));
+ builder_.Append('|');
+ builder_.Append(text.Substring(base_offset));
+ return;
+ }
+ if (base_offset < extent_offset) {
+ builder_.Append(text.Left(base_offset));
+ builder_.Append('^');
+ builder_.Append(
+ text.Substring(base_offset, extent_offset - base_offset));
+ builder_.Append('|');
+ builder_.Append(text.Substring(extent_offset));
+ return;
+ }
+ builder_.Append(text.Left(extent_offset));
+ builder_.Append('|');
+ builder_.Append(
+ text.Substring(extent_offset, base_offset - extent_offset));
+ builder_.Append('^');
+ builder_.Append(text.Substring(base_offset));
+ return;
+ }
+ if (base_node == node) {
+ builder_.Append(text.Left(base_offset));
+ builder_.Append('^');
+ builder_.Append(text.Substring(base_offset));
+ return;
+ }
+ if (extent_node == node) {
+ builder_.Append(text.Left(extent_offset));
+ builder_.Append('|');
+ builder_.Append(text.Substring(extent_offset));
+ return;
+ }
+ builder_.Append(text);
+ }
+
+ void HandleAttribute(const Attribute& attribute) {
+ builder_.Append(attribute.GetName().ToString());
+ if (attribute.Value().IsEmpty())
+ return;
+ builder_.Append("=\"");
+ for (size_t i = 0; i < attribute.Value().length(); ++i) {
+ const UChar char_code = attribute.Value()[i];
+ if (char_code == '"') {
+ builder_.Append("&quot;");
+ continue;
+ }
+ if (char_code == '&') {
+ builder_.Append("&amp;");
+ continue;
+ }
+ builder_.Append(char_code);
+ }
+ builder_.Append('"');
+ }
+
+ void HandleAttributes(const Element& element) {
+ Vector<const Attribute*> attributes;
+ for (const Attribute& attribute : element.Attributes())
+ attributes.push_back(&attribute);
+ std::sort(attributes.begin(), attributes.end(),
+ [](const Attribute* attribute1, const Attribute* attribute2) {
+ return CodePointCompareLessThan(
+ attribute1->GetName().ToString(),
+ attribute2->GetName().ToString());
+ });
+ for (const Attribute* attribute : attributes) {
+ builder_.Append(' ');
+ HandleAttribute(*attribute);
+ }
+ }
+
+ void HandleElementNode(const Element& element) {
+ builder_.Append('<');
+ builder_.Append(element.TagQName().ToString());
+ HandleAttributes(element);
+ builder_.Append('>');
+ if (IsVoidElement(element))
+ return;
+ SerializeChildren(element);
+ builder_.Append("</");
+ builder_.Append(element.TagQName().ToString());
+ builder_.Append('>');
+ }
+
+ void HandleNode(const Node& node) {
+ if (node.IsElementNode()) {
+ HandleElementNode(ToElement(node));
+ return;
+ }
+ if (node.IsTextNode()) {
+ HandleCharacterData(ToCharacterData(node));
+ return;
+ }
+ if (node.getNodeType() == Node::kCommentNode) {
+ builder_.Append("<!--");
+ HandleCharacterData(ToCharacterData(node));
+ builder_.Append("-->");
+ return;
+ }
+ if (node.getNodeType() == Node::kProcessingInstructionNode) {
+ builder_.Append("<?");
+ builder_.Append(ToProcessingInstruction(node).target());
+ builder_.Append(' ');
+ HandleCharacterData(ToCharacterData(node));
+ builder_.Append("?>");
+ return;
+ }
+ NOTREACHED() << node;
+ }
+
+ void HandleSelection(const ContainerNode& node, int offset) {
+ if (selection_.IsNone())
+ return;
+ const PositionTemplate<Strategy> position(node, offset);
+ if (selection_.Extent().ToOffsetInAnchor() == position) {
+ builder_.Append('|');
+ return;
+ }
+ if (selection_.Base().ToOffsetInAnchor() != position)
+ return;
+ builder_.Append('^');
+ }
+
+ static bool IsVoidElement(const Element& element) {
+ if (Strategy::HasChildren(element))
+ return false;
+ return ElementCannotHaveEndTag(element);
+ }
+
+ void SerializeChildren(const ContainerNode& container) {
+ int offset_in_container = 0;
+ for (const Node& child : Strategy::ChildrenOf(container)) {
+ HandleSelection(container, offset_in_container);
+ HandleNode(child);
+ ++offset_in_container;
+ }
+ HandleSelection(container, offset_in_container);
+ }
+
+ StringBuilder builder_;
+ SelectionTemplate<Strategy> selection_;
+};
+
+} // namespace
+
+void SelectionSample::ConvertTemplatesToShadowRootsForTesring(
+ HTMLElement& element) {
+ ConvertTemplatesToShadowRoots(element);
+}
+
+SelectionInDOMTree SelectionSample::SetSelectionText(
+ HTMLElement* element,
+ const std::string& selection_text) {
+ SelectionInDOMTree selection =
+ Parser().SetSelectionText(element, selection_text);
+ DCHECK(!selection.IsNone()) << "|selection_text| should container caret "
+ "marker '|' or selection marker '^' and "
+ "'|'.";
+ return selection;
+}
+
+std::string SelectionSample::GetSelectionText(
+ const ContainerNode& root,
+ const SelectionInDOMTree& selection) {
+ return Serializer<EditingStrategy>(selection).Serialize(root);
+}
+
+std::string SelectionSample::GetSelectionTextInFlatTree(
+ const ContainerNode& root,
+ const SelectionInFlatTree& selection) {
+ return Serializer<EditingInFlatTreeStrategy>(selection).Serialize(root);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/testing/selection_sample.h b/chromium/third_party/blink/renderer/core/editing/testing/selection_sample.h
new file mode 100644
index 00000000000..c569e181737
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/testing/selection_sample.h
@@ -0,0 +1,51 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_TESTING_SELECTION_SAMPLE_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_TESTING_SELECTION_SAMPLE_H_
+
+#include <string>
+
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+
+namespace blink {
+
+class ContainerNode;
+class HTMLElement;
+
+// |SelectionSample| provides parsing HTML text with selection markers and
+// serializes DOM tree with selection markers.
+// Selection markers are represents by "^" for selection base and "|" for
+// selection extent like "assert_selection.js" in layout test.
+//
+// To set selection at before children or after children instead of start or
+// end of |Text| node, we should use selection marker only |Comment| node like:
+// <span><!--^-->foo<!--|--></span>
+// This notation yields selection of base=SPAN@0 and extent=SPAN@1.
+class SelectionSample final {
+ STATIC_ONLY(SelectionSample);
+
+ public:
+ // TDOO(editng-dev): We will have flat tree version of |SetSelectionText()|
+ // and |GetSelectionText()| when we need.
+ // Set |selection_text|, which is HTML markup with selection markers as inner
+ // HTML to |HTMLElement| and returns |SelectionInDOMTree|.
+ static SelectionInDOMTree SetSelectionText(HTMLElement*,
+ const std::string& selection_text);
+
+ // Note: We don't add namespace declaration if |ContainerNode| doesn't
+ // have it.
+ // Note: We don't escape "--" in comment.
+ static std::string GetSelectionText(const ContainerNode&,
+ const SelectionInDOMTree&);
+ static std::string GetSelectionTextInFlatTree(const ContainerNode&,
+ const SelectionInFlatTree&);
+ static void ConvertTemplatesToShadowRootsForTesring(HTMLElement&);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_TESTING_SELECTION_SAMPLE_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/testing/selection_sample_test.cc b/chromium/third_party/blink/renderer/core/editing/testing/selection_sample_test.cc
new file mode 100644
index 00000000000..92cbaa09dbf
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/testing/selection_sample_test.cc
@@ -0,0 +1,414 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/testing/selection_sample.h"
+
+#include "third_party/blink/renderer/core/dom/processing_instruction.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+namespace blink {
+
+class SelectionSampleTest : public EditingTestBase {
+ protected:
+ std::string SetAndGetSelectionText(const std::string& sample_text) {
+ return SelectionSample::GetSelectionText(
+ *GetDocument().body(),
+ SelectionSample::SetSelectionText(GetDocument().body(), sample_text));
+ }
+};
+
+TEST_F(SelectionSampleTest, GetSelectionTextFlatTree) {
+ const SelectionInDOMTree selection = SelectionSample::SetSelectionText(
+ GetDocument().body(),
+ "<p>"
+ " <template data-mode=open>"
+ " ze^ro <slot name=one></slot> <slot name=two></slot> three"
+ " </template>"
+ " <b slot=two>tw|o</b><b slot=one>one</b>"
+ "</p>");
+ GetDocument().body()->UpdateDistribution();
+ EXPECT_EQ(
+ "<p>"
+ " ze^ro <slot name=\"one\"><b slot=\"one\">one</b></slot> <slot "
+ "name=\"two\"><b slot=\"two\">tw|o</b></slot> three "
+ "</p>",
+ SelectionSample::GetSelectionTextInFlatTree(
+ *GetDocument().body(), ConvertToSelectionInFlatTree(selection)));
+}
+
+TEST_F(SelectionSampleTest, SetCommentInBody) {
+ const SelectionInDOMTree& selection = SelectionSample::SetSelectionText(
+ GetDocument().body(), "<!--^-->foo<!--|-->");
+ EXPECT_EQ("foo", GetDocument().body()->InnerHTMLAsString());
+ EXPECT_EQ(SelectionInDOMTree::Builder()
+ .Collapse(Position(GetDocument().body(), 0))
+ .Extend(Position(GetDocument().body(), 1))
+ .Build(),
+ selection);
+}
+
+TEST_F(SelectionSampleTest, SetCommentInElement) {
+ const SelectionInDOMTree& selection = SelectionSample::SetSelectionText(
+ GetDocument().body(), "<span id=sample><!--^-->foo<!--|--></span>");
+ const Element* const sample = GetDocument().body()->getElementById("sample");
+ EXPECT_EQ("<span id=\"sample\">foo</span>",
+ GetDocument().body()->InnerHTMLAsString());
+ EXPECT_EQ(SelectionInDOMTree::Builder()
+ .Collapse(Position(sample, 0))
+ .Extend(Position(sample, 1))
+ .Build(),
+ selection);
+}
+
+TEST_F(SelectionSampleTest, SetEmpty1) {
+ const SelectionInDOMTree& selection =
+ SelectionSample::SetSelectionText(GetDocument().body(), "|");
+ EXPECT_EQ("", GetDocument().body()->InnerHTMLAsString());
+ EXPECT_EQ(0u, GetDocument().body()->CountChildren());
+ EXPECT_EQ(SelectionInDOMTree::Builder()
+ .Collapse(Position(GetDocument().body(), 0))
+ .Build(),
+ selection);
+}
+
+TEST_F(SelectionSampleTest, SetEmpty2) {
+ const SelectionInDOMTree& selection =
+ SelectionSample::SetSelectionText(GetDocument().body(), "^|");
+ EXPECT_EQ("", GetDocument().body()->InnerHTMLAsString());
+ EXPECT_EQ(0u, GetDocument().body()->CountChildren());
+ EXPECT_EQ(SelectionInDOMTree::Builder()
+ .Collapse(Position(GetDocument().body(), 0))
+ .Build(),
+ selection);
+}
+
+TEST_F(SelectionSampleTest, SetElement) {
+ const SelectionInDOMTree& selection = SelectionSample::SetSelectionText(
+ GetDocument().body(), "<p>^<a>0</a>|<b>1</b></p>");
+ const Element* const sample = GetDocument().QuerySelector("p");
+ EXPECT_EQ(2u, sample->CountChildren())
+ << "We should remove Text node for '^' and '|'.";
+ EXPECT_EQ(SelectionInDOMTree::Builder()
+ .Collapse(Position(sample, 0))
+ .Extend(Position(sample, 1))
+ .Build(),
+ selection);
+}
+
+TEST_F(SelectionSampleTest, SetText) {
+ {
+ const auto& selection =
+ SelectionSample::SetSelectionText(GetDocument().body(), "^ab|c");
+ EXPECT_EQ("abc", GetDocument().body()->InnerHTMLAsString());
+ EXPECT_EQ(SelectionInDOMTree::Builder()
+ .Collapse(Position(GetDocument().body()->firstChild(), 0))
+ .Extend(Position(GetDocument().body()->firstChild(), 2))
+ .Build(),
+ selection);
+ }
+ {
+ const auto& selection =
+ SelectionSample::SetSelectionText(GetDocument().body(), "a^b|c");
+ EXPECT_EQ("abc", GetDocument().body()->InnerHTMLAsString());
+ EXPECT_EQ(SelectionInDOMTree::Builder()
+ .Collapse(Position(GetDocument().body()->firstChild(), 1))
+ .Extend(Position(GetDocument().body()->firstChild(), 2))
+ .Build(),
+ selection);
+ }
+ {
+ const auto& selection =
+ SelectionSample::SetSelectionText(GetDocument().body(), "ab^|c");
+ EXPECT_EQ("abc", GetDocument().body()->InnerHTMLAsString());
+ EXPECT_EQ(SelectionInDOMTree::Builder()
+ .Collapse(Position(GetDocument().body()->firstChild(), 2))
+ .Build(),
+ selection);
+ }
+ {
+ const auto& selection =
+ SelectionSample::SetSelectionText(GetDocument().body(), "ab|c^");
+ EXPECT_EQ("abc", GetDocument().body()->InnerHTMLAsString());
+ EXPECT_EQ(SelectionInDOMTree::Builder()
+ .Collapse(Position(GetDocument().body()->firstChild(), 3))
+ .Extend(Position(GetDocument().body()->firstChild(), 2))
+ .Build(),
+ selection);
+ }
+}
+
+// Demonstrates attribute handling in HTML parser and serializer.
+TEST_F(SelectionSampleTest, SerializeAttribute) {
+ EXPECT_EQ("<a x=\"1\" y=\"2\" z=\"3\">b|ar</a>",
+ SetAndGetSelectionText("<a z='3' x='1' y='2'>b|ar</a>"))
+ << "Attributes are alphabetically ordered.";
+ EXPECT_EQ("<a x=\"'\" y=\"&quot;\" z=\"&amp;\">f|o^o</a>",
+ SetAndGetSelectionText("<a x=\"'\" y='\"' z=&>f|o^o</a>"))
+ << "Attributes with character entity.";
+ EXPECT_EQ(
+ "<foo:a foo:x=\"1\" xmlns:foo=\"http://foo\">x|y</foo:a>",
+ SetAndGetSelectionText("<foo:a foo:x=1 xmlns:foo=http://foo>x|y</foo:a>"))
+ << "namespace prefix should be supported";
+ EXPECT_EQ(
+ "<foo:a foo:x=\"1\" xmlns:foo=\"http://foo\">x|y</foo:a>",
+ SetAndGetSelectionText("<foo:a foo:x=1 xmlns:Foo=http://foo>x|y</foo:a>"))
+ << "namespace prefix is converted to lowercase by HTML parrser";
+ EXPECT_EQ("<foo:a foo:x=\"1\" x=\"2\" xmlns:foo=\"http://foo\">xy|z</foo:a>",
+ SetAndGetSelectionText(
+ "<Foo:a x=2 Foo:x=1 xmlns:foo='http://foo'>xy|z</a>"))
+ << "namespace prefix affects attribute ordering";
+}
+
+TEST_F(SelectionSampleTest, SerializeComment) {
+ EXPECT_EQ("<!-- f|oo -->", SetAndGetSelectionText("<!-- f|oo -->"));
+}
+
+TEST_F(SelectionSampleTest, SerializeElement) {
+ EXPECT_EQ("<a>|</a>", SetAndGetSelectionText("<a>|</a>"));
+ EXPECT_EQ("<a>^</a>|", SetAndGetSelectionText("<a>^</a>|"));
+ EXPECT_EQ("<a>^foo</a><b>bar</b>|",
+ SetAndGetSelectionText("<a>^foo</a><b>bar</b>|"));
+}
+
+TEST_F(SelectionSampleTest, SerializeEmpty) {
+ EXPECT_EQ("|", SetAndGetSelectionText("|"));
+ EXPECT_EQ("|", SetAndGetSelectionText("^|"));
+ EXPECT_EQ("|", SetAndGetSelectionText("|^"));
+}
+
+TEST_F(SelectionSampleTest, SerializeNamespace) {
+ SetBodyContent("<div xmlns:foo='http://xyz'><foo:bar></foo:bar>");
+ ContainerNode& sample = *ToContainerNode(GetDocument().body()->firstChild());
+ EXPECT_EQ("<foo:bar></foo:bar>",
+ SelectionSample::GetSelectionText(sample, SelectionInDOMTree()))
+ << "GetSelectionText() does not insert namespace declaration.";
+}
+
+TEST_F(SelectionSampleTest, SerializeProcessingInstruction) {
+ EXPECT_EQ("<!--?foo ba|r ?-->", SetAndGetSelectionText("<?foo ba|r ?>"))
+ << "HTML parser turns PI into comment";
+}
+
+TEST_F(SelectionSampleTest, SerializeProcessingInstruction2) {
+ GetDocument().body()->appendChild(GetDocument().createProcessingInstruction(
+ "foo", "bar", ASSERT_NO_EXCEPTION));
+
+ // Note: PI ::= '<?' PITarget (S (Char* - (Char* '?>' Char*)))? '?>'
+ EXPECT_EQ("<?foo bar?>", SelectionSample::GetSelectionText(
+ *GetDocument().body(), SelectionInDOMTree()))
+ << "No space after 'bar'";
+}
+
+// Demonstrate magic TABLE element parsing.
+TEST_F(SelectionSampleTest, SerializeTable) {
+ EXPECT_EQ("|<table></table>", SetAndGetSelectionText("<table>|</table>"))
+ << "Parser moves Text before TABLE.";
+ EXPECT_EQ("<table>|</table>",
+ SetAndGetSelectionText("<table><!--|--!></table>"))
+ << "Parser does not inserts TBODY and comment is removed.";
+ EXPECT_EQ(
+ "|start^end<table><tbody><tr><td>a</td></tr></tbody></table>",
+ SetAndGetSelectionText("<table>|start<tr><td>a</td></tr>^end</table>"))
+ << "Parser moves |Text| nodes inside TABLE to before TABLE.";
+ EXPECT_EQ(
+ "<table>|<tbody><tr><td>a</td></tr></tbody>^</table>",
+ SetAndGetSelectionText(
+ "<table><!--|--><tbody><tr><td>a</td></tr></tbody><!--^--></table>"))
+ << "We can use |Comment| node to put selection marker inside TABLE.";
+ EXPECT_EQ("<table>|<tbody><tr><td>a</td></tr>^</tbody></table>",
+ SetAndGetSelectionText(
+ "<table><!--|--><tr><td>a</td></tr><!--^--></table>"))
+ << "Parser inserts TBODY auto magically.";
+}
+
+TEST_F(SelectionSampleTest, SerializeText) {
+ EXPECT_EQ("012^3456|789", SetAndGetSelectionText("012^3456|789"));
+ EXPECT_EQ("012|3456^789", SetAndGetSelectionText("012|3456^789"));
+}
+
+TEST_F(SelectionSampleTest, SerializeVoidElement) {
+ EXPECT_EQ("|<div></div>", SetAndGetSelectionText("|<div></div>"))
+ << "DIV requires end tag.";
+ EXPECT_EQ("|<br>", SetAndGetSelectionText("|<br>"))
+ << "BR doesn't need to have end tag.";
+ EXPECT_EQ("|<br>1<br>", SetAndGetSelectionText("|<br>1</br>"))
+ << "Parser converts </br> to <br>.";
+ EXPECT_EQ("|<img>", SetAndGetSelectionText("|<img>"))
+ << "IMG doesn't need to have end tag.";
+}
+
+TEST_F(SelectionSampleTest, SerializeVoidElementBR) {
+ Element* const br = GetDocument().CreateRawElement(HTMLNames::brTag);
+ br->appendChild(GetDocument().createTextNode("abc"));
+ GetDocument().body()->appendChild(br);
+ EXPECT_EQ(
+ "<br>abc|</br>",
+ SelectionSample::GetSelectionText(
+ *GetDocument().body(),
+ SelectionInDOMTree::Builder().Collapse(Position(br, 1)).Build()))
+ << "When BR has child nodes, it is not void element.";
+}
+
+TEST_F(SelectionSampleTest, ConvertTemplatesToShadowRoots) {
+ SetBodyContent(
+ "<div id=host>"
+ "<template data-mode='open'>"
+ "<div>shadow_first</div>"
+ "<div>shadow_second</div>"
+ "</template>"
+ "</div>");
+ Element* body = GetDocument().body();
+ Element* host = body->getElementById("host");
+ SelectionSample::ConvertTemplatesToShadowRootsForTesring(
+ *(ToHTMLElement(host)));
+ ShadowRoot* shadow_root = host->ShadowRootIfV1();
+ ASSERT_TRUE(shadow_root->IsShadowRoot());
+ EXPECT_EQ("<div>shadow_first</div><div>shadow_second</div>",
+ shadow_root->InnerHTMLAsString());
+}
+
+TEST_F(SelectionSampleTest, ConvertTemplatesToShadowRootsNoTemplates) {
+ SetBodyContent(
+ "<div id=host>"
+ "<div>first</div>"
+ "<div>second</div>"
+ "</div>");
+ Element* body = GetDocument().body();
+ Element* host = body->getElementById("host");
+ SelectionSample::ConvertTemplatesToShadowRootsForTesring(
+ *(ToHTMLElement(host)));
+ EXPECT_FALSE(host->ShadowRootIfV1());
+ EXPECT_EQ("<div>first</div><div>second</div>", host->InnerHTMLAsString());
+}
+
+TEST_F(SelectionSampleTest, ConvertTemplatesToShadowRootsMultipleTemplates) {
+ SetBodyContent(
+ "<div id=host1>"
+ "<template data-mode='open'>"
+ "<div>shadow_first</div>"
+ "<div>shadow_second</div>"
+ "</template>"
+ "</div>"
+ "<div id=host2>"
+ "<template data-mode='open'>"
+ "<div>shadow_third</div>"
+ "<div>shadow_forth</div>"
+ "</template>"
+ "</div>");
+ Element* body = GetDocument().body();
+ Element* host1 = body->getElementById("host1");
+ Element* host2 = body->getElementById("host2");
+ SelectionSample::ConvertTemplatesToShadowRootsForTesring(
+ *(ToHTMLElement(body)));
+ ShadowRoot* shadow_root_1 = host1->ShadowRootIfV1();
+ ShadowRoot* shadow_root_2 = host2->ShadowRootIfV1();
+
+ EXPECT_TRUE(shadow_root_1->IsShadowRoot());
+ EXPECT_EQ("<div>shadow_first</div><div>shadow_second</div>",
+ shadow_root_1->InnerHTMLAsString());
+ EXPECT_TRUE(shadow_root_2->IsShadowRoot());
+ EXPECT_EQ("<div>shadow_third</div><div>shadow_forth</div>",
+ shadow_root_2->InnerHTMLAsString());
+}
+
+TEST_F(SelectionSampleTest, TraverseShadowContent) {
+ HTMLElement* body = GetDocument().body();
+ const std::string content = "<div id=host>"
+ "<template data-mode='open'>"
+ "<div id=shadow1>^shadow_first</div>"
+ "<div id=shadow2>shadow_second|</div>"
+ "</template>"
+ "</div>";
+ const SelectionInDOMTree& selection =
+ SelectionSample::SetSelectionText(body, content);
+ EXPECT_EQ("<div id=\"host\"></div>", body->InnerHTMLAsString());
+
+ Element* host = body->getElementById("host");
+ ShadowRoot* shadow_root = host->ShadowRootIfV1();
+ EXPECT_TRUE(shadow_root->IsShadowRoot());
+ EXPECT_EQ(
+ "<div id=\"shadow1\">shadow_first</div>"
+ "<div id=\"shadow2\">shadow_second</div>",
+ shadow_root->InnerHTMLAsString());
+
+ EXPECT_EQ(Position(shadow_root->getElementById("shadow1")->firstChild(), 0),
+ selection.Base());
+ EXPECT_EQ(Position(shadow_root->getElementById("shadow2")->firstChild(), 13),
+ selection.Extent());
+}
+
+TEST_F(SelectionSampleTest, TraverseShadowContentWithSlot) {
+ HTMLElement* body = GetDocument().body();
+ const std::string content = "<div id=host>^foo"
+ "<template data-mode='open'>"
+ "<div id=shadow1>shadow_first</div>"
+ "<slot name=slot1>slot|</slot>"
+ "<div id=shadow2>shadow_second</div>"
+ "</template>"
+ "<span slot=slot1>bar</slot>"
+ "</div>";
+ const SelectionInDOMTree& selection =
+ SelectionSample::SetSelectionText(body, content);
+ EXPECT_EQ("<div id=\"host\">foo<span slot=\"slot1\">bar</span></div>",
+ body->InnerHTMLAsString());
+
+ Element* host = body->getElementById("host");
+ ShadowRoot* shadow_root = host->ShadowRootIfV1();
+ EXPECT_TRUE(shadow_root->IsShadowRoot());
+ EXPECT_EQ(
+ "<div id=\"shadow1\">shadow_first</div>"
+ "<slot name=\"slot1\">slot</slot>"
+ "<div id=\"shadow2\">shadow_second</div>",
+ shadow_root->InnerHTMLAsString());
+
+ EXPECT_EQ(Position(GetDocument().getElementById("host")->firstChild(), 0),
+ selection.Base());
+ EXPECT_EQ(
+ Position(shadow_root->QuerySelector("[name=slot1]")->firstChild(), 4),
+ selection.Extent());
+}
+
+TEST_F(SelectionSampleTest, TraverseMultipleShadowContents) {
+ HTMLElement* body = GetDocument().body();
+ const std::string content = "<div id=host1>"
+ "<template data-mode='open'>"
+ "<div id=shadow1>^shadow_first</div>"
+ "<div id=shadow2>shadow_second</div>"
+ "</template>"
+ "</div>"
+ "<div id=host2>"
+ "<template data-mode='open'>"
+ "<div id=shadow3>shadow_third</div>"
+ "<div id=shadow4>shadow_forth|</div>"
+ "</template>"
+ "</div>";
+ const SelectionInDOMTree& selection =
+ SelectionSample::SetSelectionText(body, content);
+ EXPECT_EQ("<div id=\"host1\"></div><div id=\"host2\"></div>",
+ body->InnerHTMLAsString());
+
+ Element* host1 = body->getElementById("host1");
+ ShadowRoot* shadow_root1 = host1->ShadowRootIfV1();
+ Element* host2 = body->getElementById("host2");
+ ShadowRoot* shadow_root2 = host2->ShadowRootIfV1();
+ EXPECT_TRUE(shadow_root1->IsShadowRoot());
+ EXPECT_TRUE(shadow_root2->IsShadowRoot());
+ EXPECT_EQ(
+ "<div id=\"shadow1\">shadow_first</div>"
+ "<div id=\"shadow2\">shadow_second</div>",
+ shadow_root1->InnerHTMLAsString());
+ EXPECT_EQ(
+ "<div id=\"shadow3\">shadow_third</div>"
+ "<div id=\"shadow4\">shadow_forth</div>",
+ shadow_root2->InnerHTMLAsString());
+
+ EXPECT_EQ(Position(shadow_root1->getElementById("shadow1")->firstChild(), 0),
+ selection.Base());
+ EXPECT_EQ(
+ Position(shadow_root2->getElementById("shadow4")->firstChild(), 12),
+ selection.Extent());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/text_affinity.cc b/chromium/third_party/blink/renderer/core/editing/text_affinity.cc
new file mode 100644
index 00000000000..7db9f3eb92b
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/text_affinity.cc
@@ -0,0 +1,27 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/text_affinity.h"
+
+#include <ostream> // NOLINT
+#include "third_party/blink/public/web/web_ax_enums.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+
+namespace blink {
+
+std::ostream& operator<<(std::ostream& ostream, TextAffinity affinity) {
+ switch (affinity) {
+ case TextAffinity::kDownstream:
+ return ostream << "TextAffinity::Downstream";
+ case TextAffinity::kUpstream:
+ return ostream << "TextAffinity::Upstream";
+ }
+ return ostream << "TextAffinity(" << static_cast<int>(affinity) << ')';
+}
+
+
+STATIC_ASSERT_ENUM(kWebAXTextAffinityUpstream, TextAffinity::kUpstream);
+STATIC_ASSERT_ENUM(kWebAXTextAffinityDownstream, TextAffinity::kDownstream);
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/text_affinity.h b/chromium/third_party/blink/renderer/core/editing/text_affinity.h
new file mode 100644
index 00000000000..b35115d0957
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/text_affinity.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2004 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_TEXT_AFFINITY_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_TEXT_AFFINITY_H_
+
+#include <iosfwd>
+#include "third_party/blink/renderer/core/core_export.h"
+
+namespace blink {
+
+enum class TextAffinity {
+ kUpstream,
+ kDownstream,
+
+ // PositionWithAffiity default affinity is downstream because the callers do
+ // not really care (they just want the deep position without regard to line
+ // position), and this is cheaper than kUpstream.
+ kDefault = kDownstream,
+
+ // Callers who do not know where on the line the position is, but would like
+ // kUpstream if at a line break or kDownstream otherwise, need a clear way to
+ // specify that. The constructors auto-correct kUpstream to kDownstream if the
+ // position is not at a line break.
+ kUpstreamIfPossible = kUpstream,
+};
+
+CORE_EXPORT std::ostream& operator<<(std::ostream&, TextAffinity);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_TEXT_AFFINITY_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/text_granularity.h b/chromium/third_party/blink/renderer/core/editing/text_granularity.h
new file mode 100644
index 00000000000..309d7cef664
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/text_granularity.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2004 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_TEXT_GRANULARITY_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_TEXT_GRANULARITY_H_
+
+namespace blink {
+
+// FIXME: This really should be broken up into more than one concept.
+// LocalFrame doesn't need the 3 boundaries in this enum.
+enum class TextGranularity {
+ kCharacter,
+ kWord,
+ kSentence,
+ kLine,
+ kParagraph,
+ kSentenceBoundary,
+ kLineBoundary,
+ kParagraphBoundary,
+ kDocumentBoundary
+};
+
+} // namespace blink
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/text_offset_mapping.cc b/chromium/third_party/blink/renderer/core/editing/text_offset_mapping.cc
new file mode 100644
index 00000000000..6d10168691f
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/text_offset_mapping.cc
@@ -0,0 +1,179 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/text_offset_mapping.h"
+
+#include "third_party/blink/renderer/core/dom/node.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/iterators/character_iterator.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/layout/layout_block.h"
+
+namespace blink {
+
+namespace {
+
+// Note: Since "inline-block" and "float" are not considered as text segment
+// boundary, we should not consider them as block for scanning.
+// Example in selection text:
+// <div>|ab<b style="display:inline-box">CD</b>ef</div>
+// selection.modify('extent', 'forward', 'word')
+// <div>^ab<b style="display:inline-box">CD</b>ef|</div>
+// See also test cases for "inline-block" and "float" in "TextIterator.cpp".
+bool IsBlockForTextOffsetMapping(const LayoutObject& object) {
+ return object.IsLayoutBlock() && !object.IsAtomicInlineLevel() &&
+ !object.IsFloatingOrOutOfFlowPositioned();
+}
+
+PositionInFlatTree ComputeEndPosition(const LayoutBlock& block) {
+ for (LayoutObject* runner = block.LastLeafChild(); runner;
+ runner = runner->PreviousInPreOrder(&block)) {
+ Node* node = runner->NonPseudoNode();
+ if (!node)
+ continue;
+ if (node->IsTextNode())
+ return PositionInFlatTree(node, ToText(node)->length());
+ return PositionInFlatTree::AfterNode(*node);
+ }
+ if (Node* block_node = block.NonPseudoNode()) {
+ // Empty DIV reaches here.
+ return PositionInFlatTree::LastPositionInNode(*block_node);
+ }
+ // TODO(editing-dev): Once we have the test case reaches here, we should
+ // change caller to handle this.
+ NOTREACHED() << block.DebugName();
+ return PositionInFlatTree();
+}
+
+PositionInFlatTree ComputeStartPosition(const LayoutBlock& block) {
+ for (LayoutObject* runner = block.FirstChild(); runner;
+ runner = runner->NextInPreOrder(&block)) {
+ Node* node = runner->NonPseudoNode();
+ if (!node)
+ continue;
+ if (node->IsTextNode())
+ return PositionInFlatTree(node, 0);
+ return PositionInFlatTree::BeforeNode(*node);
+ }
+ if (Node* block_node = block.NonPseudoNode()) {
+ // Empty DIV reaches here.
+ return PositionInFlatTree::FirstPositionInNode(*block_node);
+ }
+ // TODO(editing-dev): Once we have the test case reaches here, we should
+ // change caller to handle this.
+ NOTREACHED() << block.DebugName();
+ return PositionInFlatTree();
+}
+
+// Returns range of block containing |position|.
+// Note: Container node of |position| should be associated to |LayoutObject|.
+EphemeralRangeInFlatTree ComputeBlockRange(const LayoutBlock& block) {
+ DCHECK(!block.IsAtomicInlineLevel());
+ const PositionInFlatTree& start = ComputeStartPosition(block);
+ DCHECK(start.IsNotNull()) << block.DebugName();
+ const PositionInFlatTree& end = ComputeEndPosition(block);
+ DCHECK(end.IsNotNull()) << block.DebugName();
+ return EphemeralRangeInFlatTree(start, end);
+}
+
+String Ensure16Bit(const String& text) {
+ String text16(text);
+ text16.Ensure16Bit();
+ return text16;
+}
+
+} // namespace
+
+TextOffsetMapping::TextOffsetMapping(const LayoutBlock& block,
+ const TextIteratorBehavior behavior)
+ : behavior_(behavior),
+ range_(ComputeBlockRange(block)),
+ text16_(Ensure16Bit(PlainText(range_, behavior_))) {}
+
+TextOffsetMapping::TextOffsetMapping(const LayoutBlock& block)
+ : TextOffsetMapping(block,
+ TextIteratorBehavior::Builder()
+ .SetEmitsCharactersBetweenAllVisiblePositions(true)
+ .SetEmitsSmallXForTextSecurity(true)
+ .Build()) {}
+
+int TextOffsetMapping::ComputeTextOffset(
+ const PositionInFlatTree& position) const {
+ return TextIteratorInFlatTree::RangeLength(range_.StartPosition(), position,
+ behavior_);
+}
+
+PositionInFlatTree TextOffsetMapping::GetPositionBefore(unsigned offset) const {
+ DCHECK_LE(offset, text16_.length());
+ CharacterIteratorInFlatTree iterator(range_, behavior_);
+ if (offset >= 1 && offset == text16_.length()) {
+ iterator.Advance(offset - 1);
+ return iterator.GetPositionAfter();
+ }
+ iterator.Advance(offset);
+ return iterator.GetPositionBefore();
+}
+
+PositionInFlatTree TextOffsetMapping::GetPositionAfter(unsigned offset) const {
+ DCHECK_LE(offset, text16_.length());
+ CharacterIteratorInFlatTree iterator(range_, behavior_);
+ if (offset > 0)
+ iterator.Advance(offset - 1);
+ return iterator.GetPositionAfter();
+}
+
+EphemeralRangeInFlatTree TextOffsetMapping::ComputeRange(unsigned start,
+ unsigned end) const {
+ DCHECK_LE(end, text16_.length());
+ DCHECK_LE(start, end);
+ if (start == end)
+ return EphemeralRangeInFlatTree();
+ return EphemeralRangeInFlatTree(GetPositionBefore(start),
+ GetPositionAfter(end));
+}
+
+unsigned TextOffsetMapping::FindNonWhitespaceCharacterFrom(
+ unsigned offset) const {
+ for (unsigned runner = offset; runner < text16_.length(); ++runner) {
+ if (!IsWhitespace(text16_[runner]))
+ return runner;
+ }
+ return text16_.length();
+}
+
+// static
+const LayoutBlock& TextOffsetMapping::ComputeContainigBlock(
+ const PositionInFlatTree& position) {
+ const Node& container = *position.ComputeContainerNode();
+ for (LayoutObject* runner = container.GetLayoutObject(); runner;
+ runner = runner->ContainingBlock()) {
+ if (IsBlockForTextOffsetMapping(*runner))
+ return ToLayoutBlock(*runner);
+ }
+ NOTREACHED() << position;
+ return *ToLayoutBlock(container.GetLayoutObject());
+}
+
+// static
+LayoutBlock* TextOffsetMapping::NextBlockFor(const LayoutBlock& block) {
+ for (LayoutObject* runner = block.NextInPreOrderAfterChildren(); runner;
+ runner = runner->NextInPreOrder()) {
+ if (IsBlockForTextOffsetMapping(*runner))
+ return ToLayoutBlock(runner);
+ }
+ return nullptr;
+}
+
+// static
+LayoutBlock* TextOffsetMapping::PreviousBlockFor(const LayoutBlock& block) {
+ for (LayoutObject* runner = block.PreviousInPreOrder(); runner;
+ runner = runner->PreviousInPreOrder()) {
+ if (IsBlockForTextOffsetMapping(*runner) && !block.IsDescendantOf(runner))
+ return ToLayoutBlock(runner);
+ }
+ return nullptr;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/text_offset_mapping.h b/chromium/third_party/blink/renderer/core/editing/text_offset_mapping.h
new file mode 100644
index 00000000000..1863595f711
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/text_offset_mapping.h
@@ -0,0 +1,81 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_TEXT_OFFSET_MAPPING_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_TEXT_OFFSET_MAPPING_H_
+
+#include "base/macros.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator_behavior.h"
+#include "third_party/blink/renderer/platform/heap/member.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+
+namespace blink {
+
+class LayoutBlock;
+
+// Mapping between position and text offset in |LayoutBlock| == CSS Block
+// with using characters from |TextIterator|.
+//
+// This class is similar to |NGOffsetMapping| which uses |text_offset_| in
+// |NGInlineNodeData| except for
+// - Treats characters with CSS property "-webkit-text-security" as "x"
+// instead of a bullet (U+2022), which breaks words.
+// - Contains characters in float and inline-blocks. |NGOffsetMapping| treats
+// them as one object replacement character (U+FFFC). See |CollectInlines|
+// in |NGInlineNode|.
+class CORE_EXPORT TextOffsetMapping final {
+ STACK_ALLOCATED();
+
+ public:
+ // Constructor |TextOffsetMapping| for specified |LayoutBlock|.
+ explicit TextOffsetMapping(const LayoutBlock&);
+
+ ~TextOffsetMapping() = default;
+
+ // Returns range of |LayoutBlock|.
+ const EphemeralRangeInFlatTree GetRange() const { return range_; }
+
+ // Returns characters in subtree of |LayoutBlock|, collapsed whitespaces
+ // are not included.
+ const String& GetText() const { return text16_; }
+
+ // Returns offset in |text16_| of specified position.
+ int ComputeTextOffset(const PositionInFlatTree&) const;
+
+ // Returns position before |offset| in |text16_|
+ PositionInFlatTree GetPositionBefore(unsigned offset) const;
+
+ // Returns position after |offset| in |text16_|
+ PositionInFlatTree GetPositionAfter(unsigned offset) const;
+
+ // Returns a range specified by |start| and |end| offset in |text16_|.
+ EphemeralRangeInFlatTree ComputeRange(unsigned start, unsigned end) const;
+
+ // Returns an offset in |text16_| before non-whitespace character from
+ // |offset|, inclusive, otherwise returns |text16_.length()|.
+ // This function is used for computing trailing whitespace after word.
+ unsigned FindNonWhitespaceCharacterFrom(unsigned offset) const;
+
+ // Helper functions to constructor |TextOffsetMapping|.
+ static const LayoutBlock& ComputeContainigBlock(const PositionInFlatTree&);
+ static LayoutBlock* NextBlockFor(const LayoutBlock&);
+ static LayoutBlock* PreviousBlockFor(const LayoutBlock&);
+
+ private:
+ TextOffsetMapping(const LayoutBlock&, const TextIteratorBehavior);
+
+ const TextIteratorBehavior behavior_;
+ const EphemeralRangeInFlatTree range_;
+ const String text16_;
+
+ DISALLOW_COPY_AND_ASSIGN(TextOffsetMapping);
+};
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_TEXT_OFFSET_MAPPING_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/text_offset_mapping_test.cc b/chromium/third_party/blink/renderer/core/editing/text_offset_mapping_test.cc
new file mode 100644
index 00000000000..20826c82550
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/text_offset_mapping_test.cc
@@ -0,0 +1,239 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <string>
+
+#include "third_party/blink/renderer/core/editing/text_offset_mapping.h"
+
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
+#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+
+namespace blink {
+
+class ParameterizedTextOffsetMappingTest
+ : public ::testing::WithParamInterface<bool>,
+ private ScopedLayoutNGForTest,
+ public EditingTestBase {
+ protected:
+ ParameterizedTextOffsetMappingTest() : ScopedLayoutNGForTest(GetParam()) {}
+
+ bool LayoutNGEnabled() const { return GetParam(); }
+
+ std::string ComputeTextOffset(const std::string& selection_text) {
+ const PositionInFlatTree position =
+ ToPositionInFlatTree(SetSelectionTextToBody(selection_text).Base());
+ TextOffsetMapping mapping(
+ TextOffsetMapping::ComputeContainigBlock(position));
+ const String text = mapping.GetText();
+ const int offset = mapping.ComputeTextOffset(position);
+ StringBuilder builder;
+ builder.Append(text.Left(offset));
+ builder.Append('|');
+ builder.Append(text.Substring(offset));
+ const CString result8 = builder.ToString().Utf8();
+ return std::string(result8.data(), result8.length());
+ }
+
+ std::string GetRange(const std::string& selection_text) {
+ const PositionInFlatTree position =
+ ToPositionInFlatTree(SetSelectionTextToBody(selection_text).Base());
+ TextOffsetMapping mapping(
+ TextOffsetMapping::ComputeContainigBlock(position));
+ return GetSelectionTextInFlatTreeFromBody(
+ SelectionInFlatTree::Builder()
+ .SetBaseAndExtent(mapping.GetRange())
+ .Build());
+ }
+
+ std::string GetPositionBefore(const std::string& html_text, int offset) {
+ SetBodyContent(html_text);
+ TextOffsetMapping mapping(TextOffsetMapping::ComputeContainigBlock(
+ PositionInFlatTree(*GetDocument().body()->firstChild(), 0)));
+ return GetSelectionTextInFlatTreeFromBody(
+ SelectionInFlatTree::Builder()
+ .Collapse(mapping.GetPositionBefore(offset))
+ .Build());
+ }
+
+ std::string GetPositionAfter(const std::string& html_text, int offset) {
+ SetBodyContent(html_text);
+ TextOffsetMapping mapping(TextOffsetMapping::ComputeContainigBlock(
+ PositionInFlatTree(*GetDocument().body()->firstChild(), 0)));
+ return GetSelectionTextInFlatTreeFromBody(
+ SelectionInFlatTree::Builder()
+ .Collapse(mapping.GetPositionAfter(offset))
+ .Build());
+ }
+};
+
+INSTANTIATE_TEST_CASE_P(All,
+ ParameterizedTextOffsetMappingTest,
+ ::testing::Bool());
+
+TEST_P(ParameterizedTextOffsetMappingTest, ComputeTextOffsetBasic) {
+ EXPECT_EQ("|(1) abc def", ComputeTextOffset("<p>| (1) abc def</p>"));
+ EXPECT_EQ("|(1) abc def", ComputeTextOffset("<p> |(1) abc def</p>"));
+ EXPECT_EQ("(|1) abc def", ComputeTextOffset("<p> (|1) abc def</p>"));
+ EXPECT_EQ("(1|) abc def", ComputeTextOffset("<p> (1|) abc def</p>"));
+ EXPECT_EQ("(1)| abc def", ComputeTextOffset("<p> (1)| abc def</p>"));
+ EXPECT_EQ("(1) |abc def", ComputeTextOffset("<p> (1) |abc def</p>"));
+ EXPECT_EQ("(1) a|bc def", ComputeTextOffset("<p> (1) a|bc def</p>"));
+ EXPECT_EQ("(1) ab|c def", ComputeTextOffset("<p> (1) ab|c def</p>"));
+ EXPECT_EQ("(1) abc| def", ComputeTextOffset("<p> (1) abc| def</p>"));
+ EXPECT_EQ("(1) abc |def", ComputeTextOffset("<p> (1) abc |def</p>"));
+ EXPECT_EQ("(1) abc d|ef", ComputeTextOffset("<p> (1) abc d|ef</p>"));
+ EXPECT_EQ("(1) abc de|f", ComputeTextOffset("<p> (1) abc de|f</p>"));
+ EXPECT_EQ("(1) abc def|", ComputeTextOffset("<p> (1) abc def|</p>"));
+}
+
+TEST_P(ParameterizedTextOffsetMappingTest, ComputeTextOffsetWithFirstLetter) {
+ InsertStyleElement("p::first-letter {font-size:200%;}");
+ // Expectation should be as same as |ComputeTextOffsetBasic|
+ EXPECT_EQ("|(1) abc def", ComputeTextOffset("<p>| (1) abc def</p>"));
+ EXPECT_EQ("|(1) abc def", ComputeTextOffset("<p> |(1) abc def</p>"));
+ EXPECT_EQ("(|1) abc def", ComputeTextOffset("<p> (|1) abc def</p>"));
+ EXPECT_EQ("(1|) abc def", ComputeTextOffset("<p> (1|) abc def</p>"));
+ EXPECT_EQ("(1)| abc def", ComputeTextOffset("<p> (1)| abc def</p>"));
+ EXPECT_EQ("(1) |abc def", ComputeTextOffset("<p> (1) |abc def</p>"));
+ EXPECT_EQ("(1) a|bc def", ComputeTextOffset("<p> (1) a|bc def</p>"));
+ EXPECT_EQ("(1) ab|c def", ComputeTextOffset("<p> (1) ab|c def</p>"));
+ EXPECT_EQ("(1) abc| def", ComputeTextOffset("<p> (1) abc| def</p>"));
+ EXPECT_EQ("(1) abc |def", ComputeTextOffset("<p> (1) abc |def</p>"));
+ EXPECT_EQ("(1) abc d|ef", ComputeTextOffset("<p> (1) abc d|ef</p>"));
+ EXPECT_EQ("(1) abc de|f", ComputeTextOffset("<p> (1) abc de|f</p>"));
+ EXPECT_EQ("(1) abc def|", ComputeTextOffset("<p> (1) abc def|</p>"));
+}
+
+TEST_P(ParameterizedTextOffsetMappingTest, ComputeTextOffsetWithFloat) {
+ InsertStyleElement("b { float:right; }");
+ EXPECT_EQ("|aBCDe", ComputeTextOffset("<p>|a<b>BCD</b>e</p>"));
+ EXPECT_EQ("a|BCDe", ComputeTextOffset("<p>a|<b>BCD</b>e</p>"));
+ EXPECT_EQ("a|BCDe", ComputeTextOffset("<p>a<b>|BCD</b>e</p>"));
+ EXPECT_EQ("aB|CDe", ComputeTextOffset("<p>a<b>B|CD</b>e</p>"));
+ EXPECT_EQ("aBC|De", ComputeTextOffset("<p>a<b>BC|D</b>e</p>"));
+ EXPECT_EQ("aBCD|e", ComputeTextOffset("<p>a<b>BCD|</b>e</p>"));
+ EXPECT_EQ("aBCD|e", ComputeTextOffset("<p>a<b>BCD</b>|e</p>"));
+ EXPECT_EQ("aBCDe|", ComputeTextOffset("<p>a<b>BCD</b>e|</p>"));
+}
+
+TEST_P(ParameterizedTextOffsetMappingTest, ComputeTextOffsetWithInlineBlock) {
+ InsertStyleElement("b { display:inline-block; }");
+ EXPECT_EQ("|aBCDe", ComputeTextOffset("<p>|a<b>BCD</b>e</p>"));
+ EXPECT_EQ("a|BCDe", ComputeTextOffset("<p>a|<b>BCD</b>e</p>"));
+ EXPECT_EQ("a|BCDe", ComputeTextOffset("<p>a<b>|BCD</b>e</p>"));
+ EXPECT_EQ("aB|CDe", ComputeTextOffset("<p>a<b>B|CD</b>e</p>"));
+ EXPECT_EQ("aBC|De", ComputeTextOffset("<p>a<b>BC|D</b>e</p>"));
+ EXPECT_EQ("aBCD|e", ComputeTextOffset("<p>a<b>BCD|</b>e</p>"));
+ EXPECT_EQ("aBCD|e", ComputeTextOffset("<p>a<b>BCD</b>|e</p>"));
+ EXPECT_EQ("aBCDe|", ComputeTextOffset("<p>a<b>BCD</b>e|</p>"));
+}
+
+TEST_P(ParameterizedTextOffsetMappingTest, RangeOfAnonymousBlock) {
+ EXPECT_EQ("<div><p>abc</p>^def|<p>ghi</p></div>",
+ GetRange("<div><p>abc</p>d|ef<p>ghi</p></div>"));
+}
+
+TEST_P(ParameterizedTextOffsetMappingTest, RangeOfBlockOnInlineBlock) {
+ // display:inline-block doesn't introduce block.
+ EXPECT_EQ("^abc<p style=\"display:inline\">def<br>ghi</p>xyz|",
+ GetRange("|abc<p style=display:inline>def<br>ghi</p>xyz"));
+ EXPECT_EQ("^abc<p style=\"display:inline\">def<br>ghi</p>xyz|",
+ GetRange("abc<p style=display:inline>|def<br>ghi</p>xyz"));
+}
+
+TEST_P(ParameterizedTextOffsetMappingTest, RangeOfBlockWithAnonymousBlock) {
+ // "abc" and "xyz" are in anonymous block.
+
+ // Range is "abc"
+ EXPECT_EQ("^abc|<p>def</p>xyz", GetRange("|abc<p>def</p>xyz"));
+ EXPECT_EQ("^abc|<p>def</p>xyz", GetRange("a|bc<p>def</p>xyz"));
+
+ // Range is "def"
+ EXPECT_EQ("abc<p>^def|</p>xyz", GetRange("abc<p>|def</p>xyz"));
+ EXPECT_EQ("abc<p>^def|</p>xyz", GetRange("abc<p>d|ef</p>xyz"));
+
+ // Range is "xyz"
+ EXPECT_EQ("abc<p>def</p>^xyz|", GetRange("abc<p>def</p>|xyz"));
+ EXPECT_EQ("abc<p>def</p>^xyz|", GetRange("abc<p>def</p>xyz|"));
+}
+
+TEST_P(ParameterizedTextOffsetMappingTest, RangeOfBlockWithBR) {
+ EXPECT_EQ("^abc<br>xyz|", GetRange("abc|<br>xyz"))
+ << "BR doesn't affect block";
+}
+
+TEST_P(ParameterizedTextOffsetMappingTest, RangeOfBlockWithPRE) {
+ // "\n" doesn't affect block.
+ EXPECT_EQ("<pre>^abc\ndef\nghi\n|</pre>",
+ GetRange("<pre>|abc\ndef\nghi\n</pre>"));
+ EXPECT_EQ("<pre>^abc\ndef\nghi\n|</pre>",
+ GetRange("<pre>abc\n|def\nghi\n</pre>"));
+ EXPECT_EQ("<pre>^abc\ndef\nghi\n|</pre>",
+ GetRange("<pre>abc\ndef\n|ghi\n</pre>"));
+ EXPECT_EQ("<pre>^abc\ndef\nghi\n|</pre>",
+ GetRange("<pre>abc\ndef\nghi\n|</pre>"));
+}
+
+TEST_P(ParameterizedTextOffsetMappingTest, RangeOfBlockWithRUBY) {
+ EXPECT_EQ("<ruby>^abc|<rt>123</rt></ruby>",
+ GetRange("<ruby>|abc<rt>123</rt></ruby>"));
+ EXPECT_EQ("<ruby>abc<rt>^123|</rt></ruby>",
+ GetRange("<ruby>abc<rt>1|23</rt></ruby>"));
+}
+
+TEST_P(ParameterizedTextOffsetMappingTest, RangeOfBlockWithRUBYandBR) {
+ EXPECT_EQ("<ruby>^abc<br>def|<rt>123<br>456</rt></ruby>",
+ GetRange("<ruby>|abc<br>def<rt>123<br>456</rt></ruby>"))
+ << "RT(LayoutRubyRun) is a block";
+ EXPECT_EQ("<ruby>abc<br>def<rt>^123<br>456|</rt></ruby>",
+ GetRange("<ruby>abc<br>def<rt>123|<br>456</rt></ruby>"))
+ << "RUBY introduce LayoutRuleBase for 'abc'";
+}
+
+TEST_P(ParameterizedTextOffsetMappingTest, RangeOfBlockWithTABLE) {
+ EXPECT_EQ("^abc|<table><tbody><tr><td>one</td></tr></tbody></table>xyz",
+ GetRange("|abc<table><tr><td>one</td></tr></table>xyz"))
+ << "Before TABLE";
+ EXPECT_EQ("abc<table><tbody><tr><td>^one|</td></tr></tbody></table>xyz",
+ GetRange("abc<table><tr><td>o|ne</td></tr></table>xyz"))
+ << "In TD";
+ EXPECT_EQ("abc<table><tbody><tr><td>one</td></tr></tbody></table>^xyz|",
+ GetRange("abc<table><tr><td>one</td></tr></table>x|yz"))
+ << "After TABLE";
+}
+
+TEST_P(ParameterizedTextOffsetMappingTest, RangeOfEmptyBlock) {
+ EXPECT_EQ("<div><p>abc</p><p>|</p><p>ghi</p></div>",
+ GetRange("<div><p>abc</p><p>|</p><p>ghi</p></div>"));
+}
+
+TEST_P(ParameterizedTextOffsetMappingTest, GetPositionBefore) {
+ EXPECT_EQ(" |012 456 ", GetPositionBefore(" 012 456 ", 0));
+ EXPECT_EQ(" 0|12 456 ", GetPositionBefore(" 012 456 ", 1));
+ EXPECT_EQ(" 01|2 456 ", GetPositionBefore(" 012 456 ", 2));
+ EXPECT_EQ(" 012| 456 ", GetPositionBefore(" 012 456 ", 3));
+ EXPECT_EQ(" 012 |456 ", GetPositionBefore(" 012 456 ", 4));
+ EXPECT_EQ(" 012 4|56 ", GetPositionBefore(" 012 456 ", 5));
+ EXPECT_EQ(" 012 45|6 ", GetPositionBefore(" 012 456 ", 6));
+ EXPECT_EQ(" 012 456| ", GetPositionBefore(" 012 456 ", 7));
+ // We hit DCHECK for offset 8, because we walk on "012 456".
+}
+
+TEST_P(ParameterizedTextOffsetMappingTest, GetPositionAfter) {
+ EXPECT_EQ(" 0|12 456 ", GetPositionAfter(" 012 456 ", 0));
+ EXPECT_EQ(" 0|12 456 ", GetPositionAfter(" 012 456 ", 1));
+ EXPECT_EQ(" 01|2 456 ", GetPositionAfter(" 012 456 ", 2));
+ EXPECT_EQ(" 012| 456 ", GetPositionAfter(" 012 456 ", 3));
+ EXPECT_EQ(" 012 | 456 ", GetPositionAfter(" 012 456 ", 4));
+ EXPECT_EQ(" 012 4|56 ", GetPositionAfter(" 012 456 ", 5));
+ EXPECT_EQ(" 012 45|6 ", GetPositionAfter(" 012 456 ", 6));
+ EXPECT_EQ(" 012 456| ", GetPositionAfter(" 012 456 ", 7));
+ // We hit DCHECK for offset 8, because we walk on "012 456".
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_position.cc b/chromium/third_party/blink/renderer/core/editing/visible_position.cc
new file mode 100644
index 00000000000..f1e2380360a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_position.cc
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ * Portions Copyright (c) 2011 Motorola Mobility, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+
+#include <ostream> // NOLINT
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/text_affinity.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/platform/geometry/float_quad.h"
+#include "third_party/blink/renderer/platform/wtf/text/cstring.h"
+
+namespace blink {
+
+using namespace HTMLNames;
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy>::VisiblePositionTemplate()
+#if DCHECK_IS_ON()
+ : dom_tree_version_(0),
+ style_version_(0)
+#endif
+{
+}
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy>::VisiblePositionTemplate(
+ const PositionWithAffinityTemplate<Strategy>& position_with_affinity)
+ : position_with_affinity_(position_with_affinity)
+#if DCHECK_IS_ON()
+ ,
+ dom_tree_version_(position_with_affinity.GetDocument()->DomTreeVersion()),
+ style_version_(position_with_affinity.GetDocument()->StyleVersion())
+#endif
+{
+}
+
+template <typename Strategy>
+void VisiblePositionTemplate<Strategy>::Trace(blink::Visitor* visitor) {
+ visitor->Trace(position_with_affinity_);
+}
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy> VisiblePositionTemplate<Strategy>::Create(
+ const PositionWithAffinityTemplate<Strategy>& position_with_affinity) {
+ if (position_with_affinity.IsNull())
+ return VisiblePositionTemplate<Strategy>();
+ DCHECK(position_with_affinity.IsConnected()) << position_with_affinity;
+
+ Document& document = *position_with_affinity.GetDocument();
+ DCHECK(!document.NeedsLayoutTreeUpdate());
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ document.Lifecycle());
+
+ const PositionTemplate<Strategy> deep_position =
+ CanonicalPositionOf(position_with_affinity.GetPosition());
+ if (deep_position.IsNull())
+ return VisiblePositionTemplate<Strategy>();
+ const PositionWithAffinityTemplate<Strategy> downstream_position(
+ deep_position);
+ if (position_with_affinity.Affinity() == TextAffinity::kDownstream)
+ return VisiblePositionTemplate<Strategy>(downstream_position);
+
+ // When not at a line wrap, make sure to end up with
+ // |TextAffinity::Downstream| affinity.
+ const PositionWithAffinityTemplate<Strategy> upstream_position(
+ deep_position, TextAffinity::kUpstream);
+ if (InSameLine(downstream_position, upstream_position))
+ return VisiblePositionTemplate<Strategy>(downstream_position);
+ return VisiblePositionTemplate<Strategy>(upstream_position);
+}
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy> VisiblePositionTemplate<Strategy>::AfterNode(
+ const Node& node) {
+ return Create(PositionWithAffinityTemplate<Strategy>(
+ PositionTemplate<Strategy>::AfterNode(node)));
+}
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy> VisiblePositionTemplate<Strategy>::BeforeNode(
+ const Node& node) {
+ return Create(PositionWithAffinityTemplate<Strategy>(
+ PositionTemplate<Strategy>::BeforeNode(node)));
+}
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy>
+VisiblePositionTemplate<Strategy>::FirstPositionInNode(const Node& node) {
+ return Create(PositionWithAffinityTemplate<Strategy>(
+ PositionTemplate<Strategy>::FirstPositionInNode(node)));
+}
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy>
+VisiblePositionTemplate<Strategy>::InParentAfterNode(const Node& node) {
+ return Create(PositionWithAffinityTemplate<Strategy>(
+ PositionTemplate<Strategy>::InParentAfterNode(node)));
+}
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy>
+VisiblePositionTemplate<Strategy>::InParentBeforeNode(const Node& node) {
+ return Create(PositionWithAffinityTemplate<Strategy>(
+ PositionTemplate<Strategy>::InParentBeforeNode(node)));
+}
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy>
+VisiblePositionTemplate<Strategy>::LastPositionInNode(const Node& node) {
+ return Create(PositionWithAffinityTemplate<Strategy>(
+ PositionTemplate<Strategy>::LastPositionInNode(node)));
+}
+
+VisiblePosition CreateVisiblePosition(const Position& position,
+ TextAffinity affinity) {
+ return VisiblePosition::Create(PositionWithAffinity(position, affinity));
+}
+
+VisiblePosition CreateVisiblePosition(
+ const PositionWithAffinity& position_with_affinity) {
+ return VisiblePosition::Create(position_with_affinity);
+}
+
+VisiblePositionInFlatTree CreateVisiblePosition(
+ const PositionInFlatTree& position,
+ TextAffinity affinity) {
+ return VisiblePositionInFlatTree::Create(
+ PositionInFlatTreeWithAffinity(position, affinity));
+}
+
+VisiblePositionInFlatTree CreateVisiblePosition(
+ const PositionInFlatTreeWithAffinity& position_with_affinity) {
+ return VisiblePositionInFlatTree::Create(position_with_affinity);
+}
+
+#ifndef NDEBUG
+
+template <typename Strategy>
+void VisiblePositionTemplate<Strategy>::ShowTreeForThis() const {
+ DeepEquivalent().ShowTreeForThis();
+}
+
+#endif
+
+template <typename Strategy>
+bool VisiblePositionTemplate<Strategy>::IsValid() const {
+#if DCHECK_IS_ON()
+ if (IsNull())
+ return true;
+ Document& document = *position_with_affinity_.GetDocument();
+ return dom_tree_version_ == document.DomTreeVersion() &&
+ style_version_ == document.StyleVersion() &&
+ !document.NeedsLayoutTreeUpdate();
+#else
+ return true;
+#endif
+}
+
+template <typename Strategy>
+bool VisiblePositionTemplate<Strategy>::IsValidFor(
+ const Document& document) const {
+ return position_with_affinity_.IsValidFor(document);
+}
+
+template class CORE_TEMPLATE_EXPORT VisiblePositionTemplate<EditingStrategy>;
+template class CORE_TEMPLATE_EXPORT
+ VisiblePositionTemplate<EditingInFlatTreeStrategy>;
+
+std::ostream& operator<<(std::ostream& ostream,
+ const VisiblePosition& position) {
+ return ostream << position.DeepEquivalent() << '/' << position.Affinity();
+}
+
+std::ostream& operator<<(std::ostream& ostream,
+ const VisiblePositionInFlatTree& position) {
+ return ostream << position.DeepEquivalent() << '/' << position.Affinity();
+}
+
+} // namespace blink
+
+#ifndef NDEBUG
+
+void showTree(const blink::VisiblePosition* vpos) {
+ if (vpos) {
+ vpos->ShowTreeForThis();
+ return;
+ }
+ DVLOG(0) << "Cannot showTree for (nil) VisiblePosition.";
+}
+
+void showTree(const blink::VisiblePosition& vpos) {
+ vpos.ShowTreeForThis();
+}
+
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_position.h b/chromium/third_party/blink/renderer/core/editing/visible_position.h
new file mode 100644
index 00000000000..3cfa12a9a87
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_position.h
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2004, 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_VISIBLE_POSITION_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_VISIBLE_POSITION_H_
+
+#include <iosfwd>
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+#include "third_party/blink/renderer/core/editing/text_affinity.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+
+namespace blink {
+
+// |VisiblePosition| is an immutable object representing "canonical position"
+// with affinity.
+//
+// "canonical position" is roughly equivalent to a position where we can place
+// caret, see |canonicalPosition()| for actual definition.
+//
+// "affinity" represents a place of caret at wrapped line. UPSTREAM affinity
+// means caret is placed at end of line. DOWNSTREAM affinity means caret is
+// placed at start of line.
+//
+// Example of affinity:
+// abc^def where "^" represent |Position|
+// When above text line wrapped after "abc"
+// abc| UPSTREAM |VisiblePosition|
+// |def DOWNSTREAM |VisiblePosition|
+//
+// NOTE: UPSTREAM affinity will be used only if pos is at end of a wrapped line,
+// otherwise it will be converted to DOWNSTREAM.
+template <typename Strategy>
+class CORE_TEMPLATE_CLASS_EXPORT VisiblePositionTemplate final {
+ DISALLOW_NEW();
+
+ public:
+ VisiblePositionTemplate();
+
+ // Node: Other than |createVisiblePosition()| and
+ // |createVisiblePositionDeprecated()|, we should not use |create()|.
+ static VisiblePositionTemplate Create(
+ const PositionWithAffinityTemplate<Strategy>&);
+
+ // Intentionally delete |operator==()| and |operator!=()| for reducing
+ // compilation error message.
+ // TODO(yosin) We'll have |equals()| when we have use cases of checking
+ // equality of both position and affinity.
+ bool operator==(const VisiblePositionTemplate&) const = delete;
+ bool operator!=(const VisiblePositionTemplate&) const = delete;
+
+ bool IsValid() const;
+ bool IsValidFor(const Document&) const;
+
+ // TODO(editing-dev): We should have |DCHECK(isValid())| in the following
+ // functions. However, there are some clients storing a VisiblePosition and
+ // inspecting its properties after mutation. This should be fixed.
+ // See crbug.com/648949 for details.
+ bool IsNull() const { return position_with_affinity_.IsNull(); }
+ bool IsNotNull() const { return position_with_affinity_.IsNotNull(); }
+ bool IsOrphan() const { return DeepEquivalent().IsOrphan(); }
+
+ PositionTemplate<Strategy> DeepEquivalent() const {
+ return position_with_affinity_.GetPosition();
+ }
+ PositionTemplate<Strategy> ToParentAnchoredPosition() const {
+ return DeepEquivalent().ParentAnchoredEquivalent();
+ }
+ PositionWithAffinityTemplate<Strategy> ToPositionWithAffinity() const {
+ return position_with_affinity_;
+ }
+ TextAffinity Affinity() const { return position_with_affinity_.Affinity(); }
+
+ static VisiblePositionTemplate<Strategy> AfterNode(const Node&);
+ static VisiblePositionTemplate<Strategy> BeforeNode(const Node&);
+ static VisiblePositionTemplate<Strategy> FirstPositionInNode(const Node&);
+ static VisiblePositionTemplate<Strategy> InParentAfterNode(const Node&);
+ static VisiblePositionTemplate<Strategy> InParentBeforeNode(const Node&);
+ static VisiblePositionTemplate<Strategy> LastPositionInNode(const Node&);
+
+ void Trace(blink::Visitor*);
+
+#ifndef NDEBUG
+ void ShowTreeForThis() const;
+#endif
+
+ private:
+ explicit VisiblePositionTemplate(
+ const PositionWithAffinityTemplate<Strategy>&);
+
+ PositionWithAffinityTemplate<Strategy> position_with_affinity_;
+
+#if DCHECK_IS_ON()
+ uint64_t dom_tree_version_;
+ uint64_t style_version_;
+#endif
+};
+
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ VisiblePositionTemplate<EditingStrategy>;
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ VisiblePositionTemplate<EditingInFlatTreeStrategy>;
+
+using VisiblePosition = VisiblePositionTemplate<EditingStrategy>;
+using VisiblePositionInFlatTree =
+ VisiblePositionTemplate<EditingInFlatTreeStrategy>;
+
+CORE_EXPORT VisiblePosition
+CreateVisiblePosition(const Position&, TextAffinity = TextAffinity::kDefault);
+CORE_EXPORT VisiblePosition CreateVisiblePosition(const PositionWithAffinity&);
+CORE_EXPORT VisiblePositionInFlatTree
+CreateVisiblePosition(const PositionInFlatTree&,
+ TextAffinity = TextAffinity::kDefault);
+CORE_EXPORT VisiblePositionInFlatTree
+CreateVisiblePosition(const PositionInFlatTreeWithAffinity&);
+
+CORE_EXPORT std::ostream& operator<<(std::ostream&, const VisiblePosition&);
+CORE_EXPORT std::ostream& operator<<(std::ostream&,
+ const VisiblePositionInFlatTree&);
+
+} // namespace blink
+
+#ifndef NDEBUG
+// Outside the WebCore namespace for ease of invocation from gdb.
+void showTree(const blink::VisiblePosition*);
+void showTree(const blink::VisiblePosition&);
+#endif
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_VISIBLE_POSITION_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_position_test.cc b/chromium/third_party/blink/renderer/core/editing/visible_position_test.cc
new file mode 100644
index 00000000000..cb28a352879
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_position_test.cc
@@ -0,0 +1,112 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+
+#include "third_party/blink/renderer/core/css/css_style_declaration.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+
+namespace blink {
+
+class VisiblePositionTest : public EditingTestBase {};
+
+TEST_F(VisiblePositionTest, ShadowV0DistributedNodes) {
+ const char* body_content =
+ "<p id='host'>00<b id='one'>11</b><b id='two'>22</b>33</p>";
+ const char* shadow_content =
+ "<a><span id='s4'>44</span><content select=#two></content><span "
+ "id='s5'>55</span><content select=#one></content><span "
+ "id='s6'>66</span></a>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Element* body = GetDocument().body();
+ Element* one = body->QuerySelector("#one");
+ Element* two = body->QuerySelector("#two");
+ Element* four = shadow_root->QuerySelector("#s4");
+ Element* five = shadow_root->QuerySelector("#s5");
+
+ EXPECT_EQ(Position(one->firstChild(), 0),
+ CanonicalPositionOf(Position(one, 0)));
+ EXPECT_EQ(Position(one->firstChild(), 0),
+ CreateVisiblePosition(Position(one, 0)).DeepEquivalent());
+ EXPECT_EQ(Position(one->firstChild(), 2),
+ CanonicalPositionOf(Position(two, 0)));
+ EXPECT_EQ(Position(one->firstChild(), 2),
+ CreateVisiblePosition(Position(two, 0)).DeepEquivalent());
+
+ EXPECT_EQ(PositionInFlatTree(five->firstChild(), 2),
+ CanonicalPositionOf(PositionInFlatTree(one, 0)));
+ EXPECT_EQ(PositionInFlatTree(five->firstChild(), 2),
+ CreateVisiblePosition(PositionInFlatTree(one, 0)).DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(four->firstChild(), 2),
+ CanonicalPositionOf(PositionInFlatTree(two, 0)));
+ EXPECT_EQ(PositionInFlatTree(four->firstChild(), 2),
+ CreateVisiblePosition(PositionInFlatTree(two, 0)).DeepEquivalent());
+}
+
+#if DCHECK_IS_ON()
+
+TEST_F(VisiblePositionTest, NullIsValid) {
+ EXPECT_TRUE(VisiblePosition().IsValid());
+}
+
+TEST_F(VisiblePositionTest, NonNullIsValidBeforeMutation) {
+ SetBodyContent("<p>one</p>");
+
+ Element* paragraph = GetDocument().QuerySelector("p");
+ Position position(paragraph->firstChild(), 1);
+ EXPECT_TRUE(CreateVisiblePosition(position).IsValid());
+}
+
+TEST_F(VisiblePositionTest, NonNullInvalidatedAfterDOMChange) {
+ SetBodyContent("<p>one</p>");
+
+ Element* paragraph = GetDocument().QuerySelector("p");
+ Position position(paragraph->firstChild(), 1);
+ VisiblePosition null_visible_position;
+ VisiblePosition non_null_visible_position = CreateVisiblePosition(position);
+
+ Element* div = GetDocument().CreateRawElement(HTMLNames::divTag);
+ GetDocument().body()->AppendChild(div);
+
+ EXPECT_TRUE(null_visible_position.IsValid());
+ EXPECT_FALSE(non_null_visible_position.IsValid());
+
+ UpdateAllLifecyclePhases();
+
+ // Invalid VisiblePosition can never become valid again.
+ EXPECT_FALSE(non_null_visible_position.IsValid());
+}
+
+TEST_F(VisiblePositionTest, NonNullInvalidatedAfterStyleChange) {
+ SetBodyContent("<div>one</div><p>two</p>");
+
+ Element* paragraph = GetDocument().QuerySelector("p");
+ Element* div = GetDocument().QuerySelector("div");
+ Position position(paragraph->firstChild(), 1);
+
+ VisiblePosition visible_position1 = CreateVisiblePosition(position);
+ div->style()->setProperty(&GetDocument(), "color", "red", "important",
+ ASSERT_NO_EXCEPTION);
+ EXPECT_FALSE(visible_position1.IsValid());
+
+ UpdateAllLifecyclePhases();
+
+ VisiblePosition visible_position2 = CreateVisiblePosition(position);
+ div->style()->setProperty(&GetDocument(), "display", "none", "important",
+ ASSERT_NO_EXCEPTION);
+ EXPECT_FALSE(visible_position2.IsValid());
+
+ UpdateAllLifecyclePhases();
+
+ // Invalid VisiblePosition can never become valid again.
+ EXPECT_FALSE(visible_position1.IsValid());
+ EXPECT_FALSE(visible_position2.IsValid());
+}
+
+#endif
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_selection.cc b/chromium/third_party/blink/renderer/core/editing/visible_selection.cc
new file mode 100644
index 00000000000..d9d9f606214
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_selection.cc
@@ -0,0 +1,493 @@
+/*
+ * Copyright (C) 2004, 2005, 2006 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/iterators/character_iterator.h"
+#include "third_party/blink/renderer/core/editing/selection_adjuster.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/platform/geometry/layout_point.h"
+#include "third_party/blink/renderer/platform/wtf/assertions.h"
+#include "third_party/blink/renderer/platform/wtf/text/character_names.h"
+#include "third_party/blink/renderer/platform/wtf/text/cstring.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
+
+namespace blink {
+
+template <typename Strategy>
+VisibleSelectionTemplate<Strategy>::VisibleSelectionTemplate()
+ : affinity_(TextAffinity::kDownstream), base_is_first_(true) {}
+
+template <typename Strategy>
+VisibleSelectionTemplate<Strategy>::VisibleSelectionTemplate(
+ const SelectionTemplate<Strategy>& selection)
+ : base_(selection.Base()),
+ extent_(selection.Extent()),
+ affinity_(selection.Affinity()),
+ base_is_first_(selection.IsBaseFirst()) {}
+
+template <typename Strategy>
+VisibleSelectionTemplate<Strategy> VisibleSelectionTemplate<Strategy>::Create(
+ const SelectionTemplate<Strategy>& selection) {
+ return CreateWithGranularity(selection, TextGranularity::kCharacter);
+}
+
+VisibleSelection CreateVisibleSelection(const SelectionInDOMTree& selection) {
+ return VisibleSelection::Create(selection);
+}
+
+VisibleSelectionInFlatTree CreateVisibleSelection(
+ const SelectionInFlatTree& selection) {
+ return VisibleSelectionInFlatTree::Create(selection);
+}
+
+// TODO(editing-dev): We should move |ComputeVisibleSelection()| to here to
+// avoid forward declaration.
+template <typename Strategy>
+static SelectionTemplate<Strategy> ComputeVisibleSelection(
+ const SelectionTemplate<Strategy>&,
+ TextGranularity);
+
+template <typename Strategy>
+VisibleSelectionTemplate<Strategy>
+VisibleSelectionTemplate<Strategy>::CreateWithGranularity(
+ const SelectionTemplate<Strategy>& selection,
+ TextGranularity granularity) {
+ return VisibleSelectionTemplate(
+ ComputeVisibleSelection(selection, granularity));
+}
+
+VisibleSelection CreateVisibleSelectionWithGranularity(
+ const SelectionInDOMTree& selection,
+ TextGranularity granularity) {
+ return VisibleSelection::CreateWithGranularity(selection, granularity);
+}
+
+VisibleSelectionInFlatTree CreateVisibleSelectionWithGranularity(
+ const SelectionInFlatTree& selection,
+ TextGranularity granularity) {
+ return VisibleSelectionInFlatTree::CreateWithGranularity(selection,
+ granularity);
+}
+
+template <typename Strategy>
+static SelectionType ComputeSelectionType(
+ const EphemeralRangeTemplate<Strategy>& range) {
+ if (range.IsNull())
+ return kNoSelection;
+ DCHECK(!NeedsLayoutTreeUpdate(range.StartPosition())) << range;
+ if (range.IsCollapsed())
+ return kCaretSelection;
+ if (MostBackwardCaretPosition(range.StartPosition()) ==
+ MostBackwardCaretPosition(range.EndPosition()))
+ return kCaretSelection;
+ return kRangeSelection;
+}
+
+template <typename Strategy>
+VisibleSelectionTemplate<Strategy>::VisibleSelectionTemplate(
+ const VisibleSelectionTemplate<Strategy>& other)
+ : base_(other.base_),
+ extent_(other.extent_),
+ affinity_(other.affinity_),
+ base_is_first_(other.base_is_first_) {}
+
+template <typename Strategy>
+VisibleSelectionTemplate<Strategy>& VisibleSelectionTemplate<Strategy>::
+operator=(const VisibleSelectionTemplate<Strategy>& other) {
+ base_ = other.base_;
+ extent_ = other.extent_;
+ affinity_ = other.affinity_;
+ base_is_first_ = other.base_is_first_;
+ return *this;
+}
+
+template <typename Strategy>
+SelectionTemplate<Strategy> VisibleSelectionTemplate<Strategy>::AsSelection()
+ const {
+ if (base_.IsNull()) {
+ return typename SelectionTemplate<Strategy>::Builder()
+ .Build();
+ }
+ return typename SelectionTemplate<Strategy>::Builder()
+ .SetBaseAndExtent(base_, extent_)
+ .SetAffinity(affinity_)
+ .Build();
+}
+
+template <typename Strategy>
+bool VisibleSelectionTemplate<Strategy>::IsCaret() const {
+ return base_.IsNotNull() && base_ == extent_;
+}
+
+template <typename Strategy>
+bool VisibleSelectionTemplate<Strategy>::IsNone() const {
+ return base_.IsNull();
+}
+
+template <typename Strategy>
+bool VisibleSelectionTemplate<Strategy>::IsRange() const {
+ return base_ != extent_;
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> VisibleSelectionTemplate<Strategy>::Start() const {
+ return base_is_first_ ? base_ : extent_;
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> VisibleSelectionTemplate<Strategy>::End() const {
+ return base_is_first_ ? extent_ : base_;
+}
+
+EphemeralRange FirstEphemeralRangeOf(const VisibleSelection& selection) {
+ if (selection.IsNone())
+ return EphemeralRange();
+ Position start = selection.Start().ParentAnchoredEquivalent();
+ Position end = selection.End().ParentAnchoredEquivalent();
+ return EphemeralRange(start, end);
+}
+
+template <typename Strategy>
+EphemeralRangeTemplate<Strategy>
+VisibleSelectionTemplate<Strategy>::ToNormalizedEphemeralRange() const {
+ if (IsNone())
+ return EphemeralRangeTemplate<Strategy>();
+
+ // Make sure we have an updated layout since this function is called
+ // in the course of running edit commands which modify the DOM.
+ // Failing to ensure this can result in equivalentXXXPosition calls returning
+ // incorrect results.
+ DCHECK(!NeedsLayoutTreeUpdate(Start())) << *this;
+
+ if (IsCaret()) {
+ // If the selection is a caret, move the range start upstream. This
+ // helps us match the conventions of text editors tested, which make
+ // style determinations based on the character before the caret, if any.
+ const PositionTemplate<Strategy> start =
+ MostBackwardCaretPosition(Start()).ParentAnchoredEquivalent();
+ return EphemeralRangeTemplate<Strategy>(start, start);
+ }
+ // If the selection is a range, select the minimum range that encompasses
+ // the selection. Again, this is to match the conventions of text editors
+ // tested, which make style determinations based on the first character of
+ // the selection. For instance, this operation helps to make sure that the
+ // "X" selected below is the only thing selected. The range should not be
+ // allowed to "leak" out to the end of the previous text node, or to the
+ // beginning of the next text node, each of which has a different style.
+ //
+ // On a treasure map, <b>X</b> marks the spot.
+ // ^ selected
+ //
+ DCHECK(IsRange());
+ return NormalizeRange(EphemeralRangeTemplate<Strategy>(Start(), End()));
+}
+
+template <typename Strategy>
+static SelectionTemplate<Strategy> CanonicalizeSelection(
+ const SelectionTemplate<Strategy>& selection) {
+ if (selection.IsNone())
+ return SelectionTemplate<Strategy>();
+ const PositionTemplate<Strategy>& base =
+ CreateVisiblePosition(selection.Base(), selection.Affinity())
+ .DeepEquivalent();
+ if (selection.IsCaret()) {
+ if (base.IsNull())
+ return SelectionTemplate<Strategy>();
+ return
+ typename SelectionTemplate<Strategy>::Builder().Collapse(base).Build();
+ }
+ const PositionTemplate<Strategy>& extent =
+ CreateVisiblePosition(selection.Extent(), selection.Affinity())
+ .DeepEquivalent();
+ if (base.IsNotNull() && extent.IsNotNull()) {
+ return typename SelectionTemplate<Strategy>::Builder()
+ .SetBaseAndExtent(base, extent)
+ .Build();
+ }
+ if (base.IsNotNull()) {
+ return
+ typename SelectionTemplate<Strategy>::Builder().Collapse(base).Build();
+ }
+ if (extent.IsNotNull()) {
+ return typename SelectionTemplate<Strategy>::Builder()
+ .Collapse(extent)
+ .Build();
+ }
+ return SelectionTemplate<Strategy>();
+}
+
+template <typename Strategy>
+static SelectionTemplate<Strategy> ComputeVisibleSelection(
+ const SelectionTemplate<Strategy>& passed_selection,
+ TextGranularity granularity) {
+ DCHECK(!NeedsLayoutTreeUpdate(passed_selection.Base()));
+ DCHECK(!NeedsLayoutTreeUpdate(passed_selection.Extent()));
+
+ const SelectionTemplate<Strategy>& canonicalized_selection =
+ CanonicalizeSelection(passed_selection);
+
+ if (canonicalized_selection.IsNone())
+ return SelectionTemplate<Strategy>();
+
+ const SelectionTemplate<Strategy>& granularity_adjusted_selection =
+ SelectionAdjuster::AdjustSelectionRespectingGranularity(
+ canonicalized_selection, granularity);
+ const SelectionTemplate<Strategy>& shadow_adjusted_selection =
+ SelectionAdjuster::AdjustSelectionToAvoidCrossingShadowBoundaries(
+ granularity_adjusted_selection);
+ const SelectionTemplate<Strategy>& editing_adjusted_selection =
+ SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries(
+ shadow_adjusted_selection);
+ const EphemeralRangeTemplate<Strategy> editing_adjusted_range =
+ editing_adjusted_selection.ComputeRange();
+ // TODO(editing-dev): Implement
+ // const SelectionTemplate<Strategy>& adjusted_selection =
+ // AdjustSelectionType(editing_adjusted_range);
+ const SelectionType selection_type =
+ ComputeSelectionType(editing_adjusted_range);
+ if (selection_type == kCaretSelection) {
+ return typename SelectionTemplate<Strategy>::Builder()
+ .Collapse(PositionWithAffinityTemplate<Strategy>(
+ editing_adjusted_range.StartPosition(),
+ passed_selection.Affinity()))
+ .Build();
+ }
+
+ DCHECK_EQ(selection_type, kRangeSelection);
+ // "Constrain" the selection to be the smallest equivalent range of
+ // nodes. This is a somewhat arbitrary choice, but experience shows that
+ // it is useful to make to make the selection "canonical" (if only for
+ // purposes of comparing selections). This is an ideal point of the code
+ // to do this operation, since all selection changes that result in a
+ // RANGE come through here before anyone uses it.
+ // TODO(yosin) Canonicalizing is good, but haven't we already done it
+ // (when we set these two positions to |VisiblePosition|
+ // |DeepEquivalent()|s above)?
+ const EphemeralRangeTemplate<Strategy> range(
+ MostForwardCaretPosition(editing_adjusted_range.StartPosition()),
+ MostBackwardCaretPosition(editing_adjusted_range.EndPosition()));
+ if (canonicalized_selection.IsBaseFirst()) {
+ return typename SelectionTemplate<Strategy>::Builder()
+ .SetAsForwardSelection(range)
+ .Build();
+ }
+ return typename SelectionTemplate<Strategy>::Builder()
+ .SetAsBackwardSelection(range)
+ .Build();
+}
+
+template <typename Strategy>
+bool VisibleSelectionTemplate<Strategy>::IsValidFor(
+ const Document& document) const {
+ if (IsNone())
+ return true;
+ return base_.IsValidFor(document) && extent_.IsValidFor(document);
+}
+
+// TODO(yosin) This function breaks the invariant of this class.
+// But because we use VisibleSelection to store values in editing commands for
+// use when undoing the command, we need to be able to create a selection that
+// while currently invalid, will be valid once the changes are undone. This is a
+// design problem. To fix it we either need to change the invariants of
+// |VisibleSelection| or create a new class for editing to use that can
+// manipulate selections that are not currently valid.
+template <typename Strategy>
+VisibleSelectionTemplate<Strategy>
+VisibleSelectionTemplate<Strategy>::CreateWithoutValidationDeprecated(
+ const PositionTemplate<Strategy>& base,
+ const PositionTemplate<Strategy>& extent,
+ TextAffinity affinity) {
+ DCHECK(base.IsNotNull());
+ DCHECK(extent.IsNotNull());
+
+ VisibleSelectionTemplate<Strategy> visible_selection;
+ visible_selection.base_ = base;
+ visible_selection.extent_ = extent;
+ visible_selection.base_is_first_ = base.CompareTo(extent) <= 0;
+ if (base == extent) {
+ visible_selection.affinity_ = affinity;
+ return visible_selection;
+ }
+ // Since |affinity_| for non-|CaretSelection| is always |kDownstream|,
+ // we should keep this invariant. Note: This function can be called with
+ // |affinity_| is |kUpstream|.
+ visible_selection.affinity_ = TextAffinity::kDownstream;
+ return visible_selection;
+}
+
+template <typename Strategy>
+bool VisibleSelectionTemplate<Strategy>::IsContentEditable() const {
+ return IsEditablePosition(Start());
+}
+
+template <typename Strategy>
+Element* VisibleSelectionTemplate<Strategy>::RootEditableElement() const {
+ return RootEditableElementOf(Start());
+}
+
+template <typename Strategy>
+static bool EqualSelectionsAlgorithm(
+ const VisibleSelectionTemplate<Strategy>& selection1,
+ const VisibleSelectionTemplate<Strategy>& selection2) {
+ if (selection1.Affinity() != selection2.Affinity())
+ return false;
+
+ if (selection1.IsNone())
+ return selection2.IsNone();
+
+ const VisibleSelectionTemplate<Strategy> selection_wrapper1(selection1);
+ const VisibleSelectionTemplate<Strategy> selection_wrapper2(selection2);
+
+ return selection_wrapper1.Base() == selection_wrapper2.Base() &&
+ selection_wrapper1.Extent() == selection_wrapper2.Extent();
+}
+
+template <typename Strategy>
+bool VisibleSelectionTemplate<Strategy>::operator==(
+ const VisibleSelectionTemplate<Strategy>& other) const {
+ return EqualSelectionsAlgorithm<Strategy>(*this, other);
+}
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy>
+VisibleSelectionTemplate<Strategy>::VisibleStart() const {
+ return CreateVisiblePosition(
+ Start(), IsRange() ? TextAffinity::kDownstream : Affinity());
+}
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy>
+VisibleSelectionTemplate<Strategy>::VisibleEnd() const {
+ return CreateVisiblePosition(
+ End(), IsRange() ? TextAffinity::kUpstream : Affinity());
+}
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy>
+VisibleSelectionTemplate<Strategy>::VisibleBase() const {
+ return CreateVisiblePosition(
+ base_, IsRange() ? (IsBaseFirst() ? TextAffinity::kUpstream
+ : TextAffinity::kDownstream)
+ : Affinity());
+}
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy>
+VisibleSelectionTemplate<Strategy>::VisibleExtent() const {
+ return CreateVisiblePosition(
+ extent_, IsRange() ? (IsBaseFirst() ? TextAffinity::kDownstream
+ : TextAffinity::kUpstream)
+ : Affinity());
+}
+
+template <typename Strategy>
+void VisibleSelectionTemplate<Strategy>::Trace(blink::Visitor* visitor) {
+ visitor->Trace(base_);
+ visitor->Trace(extent_);
+}
+
+#ifndef NDEBUG
+
+template <typename Strategy>
+void VisibleSelectionTemplate<Strategy>::ShowTreeForThis() const {
+ if (!Start().AnchorNode()) {
+ LOG(INFO) << "\nselection is null";
+ return;
+ }
+ LOG(INFO) << "\n"
+ << Start()
+ .AnchorNode()
+ ->ToMarkedTreeString(Start().AnchorNode(), "S",
+ End().AnchorNode(), "E")
+ .Utf8()
+ .data()
+ << "start: " << Start().ToAnchorTypeAndOffsetString().Utf8().data()
+ << "\n"
+ << "end: " << End().ToAnchorTypeAndOffsetString().Utf8().data();
+}
+
+#endif
+
+template <typename Strategy>
+void VisibleSelectionTemplate<Strategy>::PrintTo(
+ const VisibleSelectionTemplate<Strategy>& selection,
+ std::ostream* ostream) {
+ if (selection.IsNone()) {
+ *ostream << "VisibleSelection()";
+ return;
+ }
+ *ostream << "VisibleSelection(base: " << selection.Base()
+ << " extent:" << selection.Extent()
+ << " start: " << selection.Start() << " end: " << selection.End()
+ << ' ' << selection.Affinity() << ' '
+ << ')';
+}
+
+template class CORE_TEMPLATE_EXPORT VisibleSelectionTemplate<EditingStrategy>;
+template class CORE_TEMPLATE_EXPORT
+ VisibleSelectionTemplate<EditingInFlatTreeStrategy>;
+
+std::ostream& operator<<(std::ostream& ostream,
+ const VisibleSelection& selection) {
+ VisibleSelection::PrintTo(selection, &ostream);
+ return ostream;
+}
+
+std::ostream& operator<<(std::ostream& ostream,
+ const VisibleSelectionInFlatTree& selection) {
+ VisibleSelectionInFlatTree::PrintTo(selection, &ostream);
+ return ostream;
+}
+
+} // namespace blink
+
+#ifndef NDEBUG
+
+void showTree(const blink::VisibleSelection& sel) {
+ sel.ShowTreeForThis();
+}
+
+void showTree(const blink::VisibleSelection* sel) {
+ if (sel)
+ sel->ShowTreeForThis();
+}
+
+void showTree(const blink::VisibleSelectionInFlatTree& sel) {
+ sel.ShowTreeForThis();
+}
+
+void showTree(const blink::VisibleSelectionInFlatTree* sel) {
+ if (sel)
+ sel->ShowTreeForThis();
+}
+#endif
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_selection.h b/chromium/third_party/blink/renderer/core/editing/visible_selection.h
new file mode 100644
index 00000000000..3bab691384a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_selection.h
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2004 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_VISIBLE_SELECTION_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_VISIBLE_SELECTION_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/editing_strategy.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/selection_type.h"
+#include "third_party/blink/renderer/core/editing/text_affinity.h"
+#include "third_party/blink/renderer/core/editing/text_granularity.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/platform/wtf/allocator.h"
+
+namespace blink {
+
+class SelectionAdjuster;
+
+const TextAffinity kSelDefaultAffinity = TextAffinity::kDownstream;
+
+template <typename Strategy>
+class CORE_TEMPLATE_CLASS_EXPORT VisibleSelectionTemplate {
+ DISALLOW_NEW();
+
+ public:
+ VisibleSelectionTemplate();
+ VisibleSelectionTemplate(const VisibleSelectionTemplate&);
+ VisibleSelectionTemplate& operator=(const VisibleSelectionTemplate&);
+
+ // Note: |create()| should be used only by |createVisibleSelection|.
+ static VisibleSelectionTemplate Create(const SelectionTemplate<Strategy>&);
+
+ // Note: |CreateWithGranularity()| should be used only by
+ // |CreateVisibleSelectionWithGranularity()|.
+ static VisibleSelectionTemplate CreateWithGranularity(
+ const SelectionTemplate<Strategy>&,
+ TextGranularity);
+
+ TextAffinity Affinity() const { return affinity_; }
+
+ SelectionTemplate<Strategy> AsSelection() const;
+ PositionTemplate<Strategy> Base() const { return base_; }
+ PositionTemplate<Strategy> Extent() const { return extent_; }
+ PositionTemplate<Strategy> Start() const;
+ PositionTemplate<Strategy> End() const;
+
+ VisiblePositionTemplate<Strategy> VisibleStart() const;
+ VisiblePositionTemplate<Strategy> VisibleEnd() const;
+ VisiblePositionTemplate<Strategy> VisibleBase() const;
+ VisiblePositionTemplate<Strategy> VisibleExtent() const;
+
+ bool operator==(const VisibleSelectionTemplate&) const;
+ bool operator!=(const VisibleSelectionTemplate& other) const {
+ return !operator==(other);
+ }
+
+ bool IsNone() const;
+ bool IsCaret() const;
+ bool IsRange() const;
+
+ // True if base() <= extent().
+ bool IsBaseFirst() const { return base_is_first_; }
+
+ // TODO(yosin) Most callers probably don't want these functions, but
+ // are using them for historical reasons. |toNormalizedEphemeralRange()|
+ // contracts the range around text, and moves the caret most backward
+ // visually equivalent position before returning the range/positions.
+ EphemeralRangeTemplate<Strategy> ToNormalizedEphemeralRange() const;
+
+ Element* RootEditableElement() const;
+ bool IsContentEditable() const;
+
+ bool IsValidFor(const Document&) const;
+
+ // TODO(editing-dev): |CreateWithoutValidationDeprecated()| is allowed
+ // only to use in |TypingCommand| to remove part of grapheme cluster.
+ // Note: |base| and |extent| can be disconnect position.
+ static VisibleSelectionTemplate<Strategy> CreateWithoutValidationDeprecated(
+ const PositionTemplate<Strategy>& base,
+ const PositionTemplate<Strategy>& extent,
+ TextAffinity);
+
+ void Trace(blink::Visitor*);
+
+#ifndef NDEBUG
+ void ShowTreeForThis() const;
+#endif
+ static void PrintTo(const VisibleSelectionTemplate&, std::ostream*);
+
+ private:
+ friend class SelectionAdjuster;
+
+ explicit VisibleSelectionTemplate(const SelectionTemplate<Strategy>&);
+
+ // We need to store these as Positions because VisibleSelection is
+ // used to store values in editing commands for use when
+ // undoing the command. We need to be able to create a selection that, while
+ // currently invalid, will be valid once the changes are undone.
+
+ // Where the first click happened
+ PositionTemplate<Strategy> base_;
+ // Where the end click happened
+ PositionTemplate<Strategy> extent_;
+
+ TextAffinity affinity_; // the upstream/downstream affinity of the caret
+
+ // these are cached, can be recalculated by validate()
+ bool base_is_first_ : 1; // True if base is before the extent
+};
+
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ VisibleSelectionTemplate<EditingStrategy>;
+extern template class CORE_EXTERN_TEMPLATE_EXPORT
+ VisibleSelectionTemplate<EditingInFlatTreeStrategy>;
+
+using VisibleSelection = VisibleSelectionTemplate<EditingStrategy>;
+using VisibleSelectionInFlatTree =
+ VisibleSelectionTemplate<EditingInFlatTreeStrategy>;
+
+CORE_EXPORT VisibleSelection CreateVisibleSelection(const SelectionInDOMTree&);
+CORE_EXPORT VisibleSelectionInFlatTree
+CreateVisibleSelection(const SelectionInFlatTree&);
+
+CORE_EXPORT VisibleSelection
+CreateVisibleSelectionWithGranularity(const SelectionInDOMTree&,
+ TextGranularity);
+
+CORE_EXPORT VisibleSelectionInFlatTree
+CreateVisibleSelectionWithGranularity(const SelectionInFlatTree&,
+ TextGranularity);
+
+// We don't yet support multi-range selections, so we only ever have one range
+// to return.
+CORE_EXPORT EphemeralRange FirstEphemeralRangeOf(const VisibleSelection&);
+
+CORE_EXPORT std::ostream& operator<<(std::ostream&, const VisibleSelection&);
+CORE_EXPORT std::ostream& operator<<(std::ostream&,
+ const VisibleSelectionInFlatTree&);
+
+PositionInFlatTree ComputeStartRespectingGranularity(
+ const PositionInFlatTreeWithAffinity&,
+ TextGranularity);
+
+PositionInFlatTree ComputeEndRespectingGranularity(
+ const PositionInFlatTree&,
+ const PositionInFlatTreeWithAffinity&,
+ TextGranularity);
+
+} // namespace blink
+
+#ifndef NDEBUG
+// Outside the WebCore namespace for ease of invocation from gdb.
+void showTree(const blink::VisibleSelection&);
+void showTree(const blink::VisibleSelection*);
+void showTree(const blink::VisibleSelectionInFlatTree&);
+void showTree(const blink::VisibleSelectionInFlatTree*);
+#endif
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_VISIBLE_SELECTION_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_selection_test.cc b/chromium/third_party/blink/renderer/core/editing/visible_selection_test.cc
new file mode 100644
index 00000000000..9709dac42bc
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_selection_test.cc
@@ -0,0 +1,694 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/selection_adjuster.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+
+#define LOREM_IPSUM \
+ "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod " \
+ "tempor " \
+ "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " \
+ "quis nostrud " \
+ "exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " \
+ "Duis aute irure " \
+ "dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat " \
+ "nulla pariatur." \
+ "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia " \
+ "deserunt " \
+ "mollit anim id est laborum."
+
+namespace blink {
+
+class VisibleSelectionTest : public EditingTestBase {
+ protected:
+ // Helper function to set the VisibleSelection base/extent.
+ template <typename Strategy>
+ void SetSelection(VisibleSelectionTemplate<Strategy>& selection, int base) {
+ SetSelection(selection, base, base);
+ }
+
+ // Helper function to set the VisibleSelection base/extent.
+ template <typename Strategy>
+ void SetSelection(VisibleSelectionTemplate<Strategy>& selection,
+ int base,
+ int extend) {
+ Node* node = GetDocument().body()->firstChild();
+ selection = CreateVisibleSelection(
+ typename SelectionTemplate<Strategy>::Builder(selection.AsSelection())
+ .Collapse(PositionTemplate<Strategy>(node, base))
+ .Extend(PositionTemplate<Strategy>(node, extend))
+ .Build());
+ }
+
+ std::string GetWordSelectionText(const std::string&);
+};
+
+std::string VisibleSelectionTest::GetWordSelectionText(
+ const std::string& selection_text) {
+ const PositionInFlatTree position =
+ ToPositionInFlatTree(SetSelectionTextToBody(selection_text).Base());
+ return GetSelectionTextInFlatTreeFromBody(
+ CreateVisibleSelectionWithGranularity(
+ SelectionInFlatTree::Builder().Collapse(position).Build(),
+ TextGranularity::kWord)
+ .AsSelection());
+}
+
+static void TestFlatTreePositionsToEqualToDOMTreePositions(
+ const VisibleSelection& selection,
+ const VisibleSelectionInFlatTree& selection_in_flat_tree) {
+ // Since DOM tree positions can't be map to flat tree version, e.g.
+ // shadow root, not distributed node, we map a position in flat tree
+ // to DOM tree position.
+ EXPECT_EQ(selection.Start(),
+ ToPositionInDOMTree(selection_in_flat_tree.Start()));
+ EXPECT_EQ(selection.End(), ToPositionInDOMTree(selection_in_flat_tree.End()));
+ EXPECT_EQ(selection.Base(),
+ ToPositionInDOMTree(selection_in_flat_tree.Base()));
+ EXPECT_EQ(selection.Extent(),
+ ToPositionInDOMTree(selection_in_flat_tree.Extent()));
+}
+
+template <typename Strategy>
+VisibleSelectionTemplate<Strategy> ExpandUsingGranularity(
+ const VisibleSelectionTemplate<Strategy>& selection,
+ TextGranularity granularity) {
+ return CreateVisibleSelectionWithGranularity(
+ typename SelectionTemplate<Strategy>::Builder()
+ .SetBaseAndExtent(selection.Base(), selection.Extent())
+ .Build(),
+ granularity);
+}
+
+TEST_F(VisibleSelectionTest, expandUsingGranularity) {
+ const char* body_content =
+ "<span id=host><a id=one>1</a><a id=two>22</a></span>";
+ const char* shadow_content =
+ "<p><b id=three>333</b><content select=#two></content><b "
+ "id=four>4444</b><span id=space> </span><content "
+ "select=#one></content><b id=five>55555</b></p>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* three = shadow_root->getElementById("three")->firstChild();
+ Node* four = shadow_root->getElementById("four")->firstChild();
+ Node* five = shadow_root->getElementById("five")->firstChild();
+
+ VisibleSelection selection;
+ VisibleSelectionInFlatTree selection_in_flat_tree;
+
+ // From a position at distributed node
+ selection = CreateVisibleSelection(
+ SelectionInDOMTree::Builder().Collapse(Position(one, 1)).Build());
+ selection = ExpandUsingGranularity(selection, TextGranularity::kWord);
+ selection_in_flat_tree =
+ CreateVisibleSelection(SelectionInFlatTree::Builder()
+ .Collapse(PositionInFlatTree(one, 1))
+ .Build());
+ selection_in_flat_tree =
+ ExpandUsingGranularity(selection_in_flat_tree, TextGranularity::kWord);
+
+ EXPECT_EQ(selection.Start(), selection.Base());
+ EXPECT_EQ(selection.End(), selection.Extent());
+ EXPECT_EQ(Position(one, 0), selection.Start());
+ EXPECT_EQ(Position(two, 2), selection.End());
+
+ EXPECT_EQ(selection_in_flat_tree.Start(), selection_in_flat_tree.Base());
+ EXPECT_EQ(selection_in_flat_tree.End(), selection_in_flat_tree.Extent());
+ EXPECT_EQ(PositionInFlatTree(one, 0), selection_in_flat_tree.Start());
+ EXPECT_EQ(PositionInFlatTree(five, 5), selection_in_flat_tree.End());
+
+ // From a position at distributed node
+ selection = CreateVisibleSelection(
+ SelectionInDOMTree::Builder().Collapse(Position(two, 1)).Build());
+ selection = ExpandUsingGranularity(selection, TextGranularity::kWord);
+ selection_in_flat_tree =
+ CreateVisibleSelection(SelectionInFlatTree::Builder()
+ .Collapse(PositionInFlatTree(two, 1))
+ .Build());
+ selection_in_flat_tree =
+ ExpandUsingGranularity(selection_in_flat_tree, TextGranularity::kWord);
+
+ EXPECT_EQ(selection.Start(), selection.Base());
+ EXPECT_EQ(selection.End(), selection.Extent());
+ EXPECT_EQ(Position(one, 0), selection.Start());
+ EXPECT_EQ(Position(two, 2), selection.End());
+
+ EXPECT_EQ(selection_in_flat_tree.Start(), selection_in_flat_tree.Base());
+ EXPECT_EQ(selection_in_flat_tree.End(), selection_in_flat_tree.Extent());
+ EXPECT_EQ(PositionInFlatTree(three, 0), selection_in_flat_tree.Start());
+ EXPECT_EQ(PositionInFlatTree(four, 4), selection_in_flat_tree.End());
+
+ // From a position at node in shadow tree
+ selection = CreateVisibleSelection(
+ SelectionInDOMTree::Builder().Collapse(Position(three, 1)).Build());
+ selection = ExpandUsingGranularity(selection, TextGranularity::kWord);
+ selection_in_flat_tree =
+ CreateVisibleSelection(SelectionInFlatTree::Builder()
+ .Collapse(PositionInFlatTree(three, 1))
+ .Build());
+ selection_in_flat_tree =
+ ExpandUsingGranularity(selection_in_flat_tree, TextGranularity::kWord);
+
+ EXPECT_EQ(selection.Start(), selection.Base());
+ EXPECT_EQ(selection.End(), selection.Extent());
+ EXPECT_EQ(Position(three, 0), selection.Start());
+ EXPECT_EQ(Position(four, 4), selection.End());
+
+ EXPECT_EQ(selection_in_flat_tree.Start(), selection_in_flat_tree.Base());
+ EXPECT_EQ(selection_in_flat_tree.End(), selection_in_flat_tree.Extent());
+ EXPECT_EQ(PositionInFlatTree(three, 0), selection_in_flat_tree.Start());
+ EXPECT_EQ(PositionInFlatTree(four, 4), selection_in_flat_tree.End());
+
+ // From a position at node in shadow tree
+ selection = CreateVisibleSelection(
+ SelectionInDOMTree::Builder().Collapse(Position(four, 1)).Build());
+ selection = ExpandUsingGranularity(selection, TextGranularity::kWord);
+ selection_in_flat_tree =
+ CreateVisibleSelection(SelectionInFlatTree::Builder()
+ .Collapse(PositionInFlatTree(four, 1))
+ .Build());
+ selection_in_flat_tree =
+ ExpandUsingGranularity(selection_in_flat_tree, TextGranularity::kWord);
+
+ EXPECT_EQ(selection.Start(), selection.Base());
+ EXPECT_EQ(selection.End(), selection.Extent());
+ EXPECT_EQ(Position(three, 0), selection.Start());
+ EXPECT_EQ(Position(four, 4), selection.End());
+
+ EXPECT_EQ(selection_in_flat_tree.Start(), selection_in_flat_tree.Base());
+ EXPECT_EQ(selection_in_flat_tree.End(), selection_in_flat_tree.Extent());
+ EXPECT_EQ(PositionInFlatTree(three, 0), selection_in_flat_tree.Start());
+ EXPECT_EQ(PositionInFlatTree(four, 4), selection_in_flat_tree.End());
+
+ // From a position at node in shadow tree
+ selection = CreateVisibleSelection(
+ SelectionInDOMTree::Builder().Collapse(Position(five, 1)).Build());
+ selection = ExpandUsingGranularity(selection, TextGranularity::kWord);
+ selection_in_flat_tree =
+ CreateVisibleSelection(SelectionInFlatTree::Builder()
+ .Collapse(PositionInFlatTree(five, 1))
+ .Build());
+ selection_in_flat_tree =
+ ExpandUsingGranularity(selection_in_flat_tree, TextGranularity::kWord);
+
+ EXPECT_EQ(selection.Start(), selection.Base());
+ EXPECT_EQ(selection.End(), selection.Extent());
+ EXPECT_EQ(Position(five, 0), selection.Start());
+ EXPECT_EQ(Position(five, 5), selection.End());
+
+ EXPECT_EQ(selection_in_flat_tree.Start(), selection_in_flat_tree.Base());
+ EXPECT_EQ(selection_in_flat_tree.End(), selection_in_flat_tree.Extent());
+ EXPECT_EQ(PositionInFlatTree(one, 0), selection_in_flat_tree.Start());
+ EXPECT_EQ(PositionInFlatTree(five, 5), selection_in_flat_tree.End());
+}
+
+// For http://wkb.ug/32622
+TEST_F(VisibleSelectionTest, ExpandUsingGranularityWithEmptyCell) {
+ const SelectionInDOMTree& selection_in_dom_tree = SetSelectionTextToBody(
+ "<div contentEditable><table cellspacing=0><tr>"
+ "<td id='first' width='50' height='25pt'>|</td>"
+ "<td id='second' width='50' height='25pt'></td>"
+ "</tr></table></div>");
+ const VisibleSelectionInFlatTree& selection =
+ CreateVisibleSelectionWithGranularity(
+ ConvertToSelectionInFlatTree(selection_in_dom_tree),
+ TextGranularity::kWord);
+ EXPECT_EQ(
+ "<div contenteditable><table cellspacing=\"0\"><tbody><tr>"
+ "<td height=\"25pt\" id=\"first\" width=\"50\">|</td>"
+ "<td height=\"25pt\" id=\"second\" width=\"50\"></td>"
+ "</tr></tbody></table></div>",
+ GetSelectionTextInFlatTreeFromBody(selection.AsSelection()));
+}
+
+TEST_F(VisibleSelectionTest, Initialisation) {
+ SetBodyContent(LOREM_IPSUM);
+
+ VisibleSelection selection;
+ VisibleSelectionInFlatTree selection_in_flat_tree;
+ SetSelection(selection, 0);
+ SetSelection(selection_in_flat_tree, 0);
+
+ EXPECT_FALSE(selection.IsNone());
+ EXPECT_FALSE(selection_in_flat_tree.IsNone());
+ EXPECT_TRUE(selection.IsCaret());
+ EXPECT_TRUE(selection_in_flat_tree.IsCaret());
+
+ Range* range = CreateRange(FirstEphemeralRangeOf(selection));
+ EXPECT_EQ(0u, range->startOffset());
+ EXPECT_EQ(0u, range->endOffset());
+ EXPECT_EQ("", range->GetText());
+ TestFlatTreePositionsToEqualToDOMTreePositions(selection,
+ selection_in_flat_tree);
+
+ const VisibleSelection no_selection =
+ CreateVisibleSelection(SelectionInDOMTree::Builder().Build());
+ EXPECT_TRUE(no_selection.IsNone());
+}
+
+TEST_F(VisibleSelectionTest, FirstLetter) {
+ SetBodyContent(
+ "<style>p::first-letter { font-color: red; }</style>"
+ "<p>abc def</p>");
+ const Element* sample = GetDocument().QuerySelector("p");
+ const SelectionInDOMTree selection =
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(sample->firstChild(), 0))
+ .Extend(Position(sample->firstChild(), 3))
+ .Build();
+ const VisibleSelection visible_selection = CreateVisibleSelection(selection);
+
+ EXPECT_EQ(selection, visible_selection.AsSelection());
+}
+
+TEST_F(VisibleSelectionTest, FirstLetterCollapsedWhitespace) {
+ SetBodyContent(
+ "<style>p::first-letter { font-color: red; }</style>"
+ "<p> abc def</p>");
+ const Element* sample = GetDocument().QuerySelector("p");
+ const SelectionInDOMTree selection =
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(sample->firstChild(), 0))
+ .Extend(Position(sample->firstChild(), 5))
+ .Build();
+ const VisibleSelection visible_selection = CreateVisibleSelection(selection);
+
+ EXPECT_EQ(SelectionInDOMTree::Builder()
+ .Collapse(Position(sample->firstChild(), 2))
+ .Extend(Position(sample->firstChild(), 5))
+ .Build(),
+ visible_selection.AsSelection())
+ << "VisibleSelection doesn't contains collapsed whitespaces";
+}
+
+TEST_F(VisibleSelectionTest, FirstLetterPartial) {
+ SetBodyContent(
+ "<style>p::first-letter { font-color: red; }</style>"
+ "<p>((a))bc def</p>");
+ const Element* sample = GetDocument().QuerySelector("p");
+ const SelectionInDOMTree selection =
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(sample->firstChild(), 1))
+ .Extend(Position(sample->firstChild(), 4))
+ .Build();
+ const VisibleSelection visible_selection = CreateVisibleSelection(selection);
+
+ EXPECT_EQ(selection, visible_selection.AsSelection())
+ << "Select '(a)' of '((a))";
+}
+
+TEST_F(VisibleSelectionTest, FirstLetterTextTransform) {
+ SetBodyContent(
+ "<style>p::first-letter { text-transform: uppercase; }</style>"
+ "<p>\u00DFbc def</p>"); // uppercase(U+00DF) = "SS"
+ const Element* sample = GetDocument().QuerySelector("p");
+ const SelectionInDOMTree selection =
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(sample->firstChild(), 0))
+ .Extend(Position(sample->firstChild(), 3))
+ .Build();
+ const VisibleSelection visible_selection = CreateVisibleSelection(selection);
+
+ EXPECT_EQ(selection, visible_selection.AsSelection());
+}
+
+TEST_F(VisibleSelectionTest, FirstLetterVisibilityHidden) {
+ SetBodyContent(
+ "<style>p::first-letter { visibility: hidden; }</style>"
+ "<p>abc def</p>");
+ const Element* sample = GetDocument().QuerySelector("p");
+ const SelectionInDOMTree selection =
+ SelectionInDOMTree::Builder()
+ .Collapse(Position(sample->firstChild(), 0))
+ .Extend(Position(sample->firstChild(), 3))
+ .Build();
+ const VisibleSelection visible_selection = CreateVisibleSelection(selection);
+
+ EXPECT_EQ(SelectionInDOMTree::Builder()
+ .Collapse(Position(sample->firstChild(), 1))
+ .Extend(Position(sample->firstChild(), 3))
+ .Build(),
+ visible_selection.AsSelection())
+ << "Exclude first-letter part since it is visibility::hidden";
+}
+
+// For http://crbug.com/695317
+TEST_F(VisibleSelectionTest, SelectAllWithInputElement) {
+ SetBodyContent("<input>123");
+ Element* const html_element = GetDocument().documentElement();
+ Element* const input = GetDocument().QuerySelector("input");
+ Node* const last_child = GetDocument().body()->lastChild();
+
+ const VisibleSelection& visible_selection_in_dom_tree =
+ CreateVisibleSelection(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::FirstPositionInNode(*html_element))
+ .Extend(Position::LastPositionInNode(*html_element))
+ .Build());
+ EXPECT_EQ(SelectionInDOMTree::Builder()
+ .Collapse(Position::BeforeNode(*input))
+ .Extend(Position(last_child, 3))
+ .Build(),
+ visible_selection_in_dom_tree.AsSelection());
+
+ const VisibleSelectionInFlatTree& visible_selection_in_flat_tree =
+ CreateVisibleSelection(
+ SelectionInFlatTree::Builder()
+ .Collapse(PositionInFlatTree::FirstPositionInNode(*html_element))
+ .Extend(PositionInFlatTree::LastPositionInNode(*html_element))
+ .Build());
+ EXPECT_EQ(SelectionInFlatTree::Builder()
+ .Collapse(PositionInFlatTree::BeforeNode(*input))
+ .Extend(PositionInFlatTree(last_child, 3))
+ .Build(),
+ visible_selection_in_flat_tree.AsSelection());
+}
+
+TEST_F(VisibleSelectionTest, GetWordSelectionTextWithTextSecurity) {
+ InsertStyleElement("s {-webkit-text-security:disc;}");
+ // Note: |CreateVisibleSelectionWithGranularity()| considers security
+ // characters as a sequence "x".
+ EXPECT_EQ("^abc<s>foo bar</s>baz|",
+ GetWordSelectionText("|abc<s>foo bar</s>baz"));
+ EXPECT_EQ("^abc<s>foo bar</s>baz|",
+ GetWordSelectionText("a|bc<s>foo bar</s>baz"));
+ EXPECT_EQ("^abc<s>foo bar</s>baz|",
+ GetWordSelectionText("abc|<s>foo bar</s>baz"));
+ EXPECT_EQ("^abc<s>foo bar</s>baz|",
+ GetWordSelectionText("abc<s>|foo bar</s>baz"));
+ EXPECT_EQ("^abc<s>foo bar</s>baz|",
+ GetWordSelectionText("abc<s>f|oo bar</s>baz"));
+ EXPECT_EQ("^abc<s>foo bar</s>baz|",
+ GetWordSelectionText("abc<s>fo|o bar</s>baz"));
+ EXPECT_EQ("^abc<s>foo bar</s>baz|",
+ GetWordSelectionText("abc<s>foo| bar</s>baz"));
+ EXPECT_EQ("^abc<s>foo bar</s>baz|",
+ GetWordSelectionText("abc<s>foo |bar</s>baz"));
+ EXPECT_EQ("^abc<s>foo bar</s>baz|",
+ GetWordSelectionText("abc<s>foo b|ar</s>baz"));
+ EXPECT_EQ("^abc<s>foo bar</s>baz|",
+ GetWordSelectionText("abc<s>foo ba|r</s>baz"));
+ EXPECT_EQ("^abc<s>foo bar</s>baz|",
+ GetWordSelectionText("abc<s>foo bar|</s>baz"));
+ EXPECT_EQ("^abc<s>foo bar</s>baz|",
+ GetWordSelectionText("abc<s>foo bar</s>|baz"));
+ EXPECT_EQ("^abc<s>foo bar</s>baz|",
+ GetWordSelectionText("abc<s>foo bar</s>b|az"));
+ EXPECT_EQ("^abc<s>foo bar</s>baz|",
+ GetWordSelectionText("abc<s>foo bar</s>ba|z"));
+ EXPECT_EQ("^abc<s>foo bar</s>baz|",
+ GetWordSelectionText("abc<s>foo bar</s>baz|"));
+}
+
+TEST_F(VisibleSelectionTest, ShadowCrossing) {
+ const char* body_content =
+ "<p id='host'>00<b id='one'>11</b><b id='two'>22</b>33</p>";
+ const char* shadow_content =
+ "<a><span id='s4'>44</span><content select=#two></content><span "
+ "id='s5'>55</span><content select=#one></content><span "
+ "id='s6'>66</span></a>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Element* body = GetDocument().body();
+ Element* host = body->QuerySelector("#host");
+ Element* one = body->QuerySelector("#one");
+ Element* six = shadow_root->QuerySelector("#s6");
+
+ VisibleSelection selection = CreateVisibleSelection(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::FirstPositionInNode(*one))
+ .Extend(Position::LastPositionInNode(*shadow_root))
+ .Build());
+ VisibleSelectionInFlatTree selection_in_flat_tree = CreateVisibleSelection(
+ SelectionInFlatTree::Builder()
+ .Collapse(PositionInFlatTree::FirstPositionInNode(*one))
+ .Extend(PositionInFlatTree::LastPositionInNode(*host))
+ .Build());
+
+ EXPECT_EQ(Position(host, PositionAnchorType::kBeforeAnchor),
+ selection.Start());
+ EXPECT_EQ(Position(host, PositionAnchorType::kBeforeAnchor), selection.End());
+ EXPECT_EQ(PositionInFlatTree(one->firstChild(), 0),
+ selection_in_flat_tree.Start());
+ EXPECT_EQ(PositionInFlatTree(six->firstChild(), 2),
+ selection_in_flat_tree.End());
+}
+
+TEST_F(VisibleSelectionTest, ShadowV0DistributedNodes) {
+ const char* body_content =
+ "<p id='host'>00<b id='one'>11</b><b id='two'>22</b>33</p>";
+ const char* shadow_content =
+ "<a><span id='s4'>44</span><content select=#two></content><span "
+ "id='s5'>55</span><content select=#one></content><span "
+ "id='s6'>66</span></a>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Element* body = GetDocument().body();
+ Element* one = body->QuerySelector("#one");
+ Element* two = body->QuerySelector("#two");
+ Element* five = shadow_root->QuerySelector("#s5");
+
+ VisibleSelection selection =
+ CreateVisibleSelection(SelectionInDOMTree::Builder()
+ .Collapse(Position::FirstPositionInNode(*one))
+ .Extend(Position::LastPositionInNode(*two))
+ .Build());
+ VisibleSelectionInFlatTree selection_in_flat_tree = CreateVisibleSelection(
+ SelectionInFlatTree::Builder()
+ .Collapse(PositionInFlatTree::FirstPositionInNode(*one))
+ .Extend(PositionInFlatTree::LastPositionInNode(*two))
+ .Build());
+
+ EXPECT_EQ(Position(one->firstChild(), 0), selection.Start());
+ EXPECT_EQ(Position(two->firstChild(), 2), selection.End());
+ EXPECT_EQ(PositionInFlatTree(five->firstChild(), 0),
+ selection_in_flat_tree.Start());
+ EXPECT_EQ(PositionInFlatTree(five->firstChild(), 2),
+ selection_in_flat_tree.End());
+}
+
+TEST_F(VisibleSelectionTest, ShadowNested) {
+ const char* body_content =
+ "<p id='host'>00<b id='one'>11</b><b id='two'>22</b>33</p>";
+ const char* shadow_content =
+ "<a><span id='s4'>44</span><content select=#two></content><span "
+ "id='s5'>55</span><content select=#one></content><span "
+ "id='s6'>66</span></a>";
+ const char* shadow_content2 =
+ "<span id='s7'>77</span><content></content><span id='s8'>88</span>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+ ShadowRoot* shadow_root2 = CreateShadowRootForElementWithIDAndSetInnerHTML(
+ *shadow_root, "s5", shadow_content2);
+
+ // Flat tree is something like below:
+ // <p id="host">
+ // <span id="s4">44</span>
+ // <b id="two">22</b>
+ // <span id="s5"><span id="s7">77>55</span id="s8">88</span>
+ // <b id="one">11</b>
+ // <span id="s6">66</span>
+ // </p>
+ Element* body = GetDocument().body();
+ Element* host = body->QuerySelector("#host");
+ Element* one = body->QuerySelector("#one");
+ Element* eight = shadow_root2->QuerySelector("#s8");
+
+ VisibleSelection selection = CreateVisibleSelection(
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::FirstPositionInNode(*one))
+ .Extend(Position::LastPositionInNode(*shadow_root2))
+ .Build());
+ VisibleSelectionInFlatTree selection_in_flat_tree = CreateVisibleSelection(
+ SelectionInFlatTree::Builder()
+ .Collapse(PositionInFlatTree::FirstPositionInNode(*one))
+ .Extend(PositionInFlatTree::AfterNode(*eight))
+ .Build());
+
+ EXPECT_EQ(Position(host, PositionAnchorType::kBeforeAnchor),
+ selection.Start());
+ EXPECT_EQ(Position(host, PositionAnchorType::kBeforeAnchor), selection.End());
+ EXPECT_EQ(PositionInFlatTree(eight->firstChild(), 2),
+ selection_in_flat_tree.Start());
+ EXPECT_EQ(PositionInFlatTree(eight->firstChild(), 2),
+ selection_in_flat_tree.End());
+}
+
+TEST_F(VisibleSelectionTest, WordGranularity) {
+ SetBodyContent(LOREM_IPSUM);
+
+ VisibleSelection selection;
+ VisibleSelectionInFlatTree selection_in_flat_tree;
+
+ // Beginning of a word.
+ {
+ SetSelection(selection, 0);
+ SetSelection(selection_in_flat_tree, 0);
+ selection = ExpandUsingGranularity(selection, TextGranularity::kWord);
+ selection_in_flat_tree =
+ ExpandUsingGranularity(selection_in_flat_tree, TextGranularity::kWord);
+
+ Range* range = CreateRange(FirstEphemeralRangeOf(selection));
+ EXPECT_EQ(0u, range->startOffset());
+ EXPECT_EQ(5u, range->endOffset());
+ EXPECT_EQ("Lorem", range->GetText());
+ TestFlatTreePositionsToEqualToDOMTreePositions(selection,
+ selection_in_flat_tree);
+ }
+
+ // Middle of a word.
+ {
+ SetSelection(selection, 8);
+ SetSelection(selection_in_flat_tree, 8);
+ selection = ExpandUsingGranularity(selection, TextGranularity::kWord);
+ selection_in_flat_tree =
+ ExpandUsingGranularity(selection_in_flat_tree, TextGranularity::kWord);
+
+ Range* range = CreateRange(FirstEphemeralRangeOf(selection));
+ EXPECT_EQ(6u, range->startOffset());
+ EXPECT_EQ(11u, range->endOffset());
+ EXPECT_EQ("ipsum", range->GetText());
+ TestFlatTreePositionsToEqualToDOMTreePositions(selection,
+ selection_in_flat_tree);
+ }
+
+ // End of a word.
+ // FIXME: that sounds buggy, we might want to select the word _before_ instead
+ // of the space...
+ {
+ SetSelection(selection, 5);
+ SetSelection(selection_in_flat_tree, 5);
+ selection = ExpandUsingGranularity(selection, TextGranularity::kWord);
+ selection_in_flat_tree =
+ ExpandUsingGranularity(selection_in_flat_tree, TextGranularity::kWord);
+
+ Range* range = CreateRange(FirstEphemeralRangeOf(selection));
+ EXPECT_EQ(5u, range->startOffset());
+ EXPECT_EQ(6u, range->endOffset());
+ EXPECT_EQ(" ", range->GetText());
+ TestFlatTreePositionsToEqualToDOMTreePositions(selection,
+ selection_in_flat_tree);
+ }
+
+ // Before comma.
+ // FIXME: that sounds buggy, we might want to select the word _before_ instead
+ // of the comma.
+ {
+ SetSelection(selection, 26);
+ SetSelection(selection_in_flat_tree, 26);
+ selection = ExpandUsingGranularity(selection, TextGranularity::kWord);
+ selection_in_flat_tree =
+ ExpandUsingGranularity(selection_in_flat_tree, TextGranularity::kWord);
+
+ Range* range = CreateRange(FirstEphemeralRangeOf(selection));
+ EXPECT_EQ(26u, range->startOffset());
+ EXPECT_EQ(27u, range->endOffset());
+ EXPECT_EQ(",", range->GetText());
+ TestFlatTreePositionsToEqualToDOMTreePositions(selection,
+ selection_in_flat_tree);
+ }
+
+ // After comma.
+ {
+ SetSelection(selection, 27);
+ SetSelection(selection_in_flat_tree, 27);
+ selection = ExpandUsingGranularity(selection, TextGranularity::kWord);
+ selection_in_flat_tree =
+ ExpandUsingGranularity(selection_in_flat_tree, TextGranularity::kWord);
+
+ Range* range = CreateRange(FirstEphemeralRangeOf(selection));
+ EXPECT_EQ(27u, range->startOffset());
+ EXPECT_EQ(28u, range->endOffset());
+ EXPECT_EQ(" ", range->GetText());
+ TestFlatTreePositionsToEqualToDOMTreePositions(selection,
+ selection_in_flat_tree);
+ }
+
+ // When selecting part of a word.
+ {
+ SetSelection(selection, 0, 1);
+ SetSelection(selection_in_flat_tree, 0, 1);
+ selection = ExpandUsingGranularity(selection, TextGranularity::kWord);
+ selection_in_flat_tree =
+ ExpandUsingGranularity(selection_in_flat_tree, TextGranularity::kWord);
+
+ Range* range = CreateRange(FirstEphemeralRangeOf(selection));
+ EXPECT_EQ(0u, range->startOffset());
+ EXPECT_EQ(5u, range->endOffset());
+ EXPECT_EQ("Lorem", range->GetText());
+ TestFlatTreePositionsToEqualToDOMTreePositions(selection,
+ selection_in_flat_tree);
+ }
+
+ // When selecting part of two words.
+ {
+ SetSelection(selection, 2, 8);
+ SetSelection(selection_in_flat_tree, 2, 8);
+ selection = ExpandUsingGranularity(selection, TextGranularity::kWord);
+ selection_in_flat_tree =
+ ExpandUsingGranularity(selection_in_flat_tree, TextGranularity::kWord);
+
+ Range* range = CreateRange(FirstEphemeralRangeOf(selection));
+ EXPECT_EQ(0u, range->startOffset());
+ EXPECT_EQ(11u, range->endOffset());
+ EXPECT_EQ("Lorem ipsum", range->GetText());
+ TestFlatTreePositionsToEqualToDOMTreePositions(selection,
+ selection_in_flat_tree);
+ }
+}
+
+// This is for crbug.com/627783, simulating restoring selection
+// in undo stack.
+TEST_F(VisibleSelectionTest, updateIfNeededWithShadowHost) {
+ SetBodyContent("<div id=host></div><div id=sample>foo</div>");
+ SetShadowContent("<content>", "host");
+ Element* sample = GetDocument().getElementById("sample");
+
+ // Simulates saving selection in undo stack.
+ VisibleSelection selection =
+ CreateVisibleSelection(SelectionInDOMTree::Builder()
+ .Collapse(Position(sample->firstChild(), 0))
+ .Build());
+ EXPECT_EQ(Position(sample->firstChild(), 0), selection.Start());
+
+ // Simulates modifying DOM tree to invalidate distribution.
+ Element* host = GetDocument().getElementById("host");
+ host->AppendChild(sample);
+ GetDocument().UpdateStyleAndLayout();
+
+ // Simulates to restore selection from undo stack.
+ selection = CreateVisibleSelection(selection.AsSelection());
+ EXPECT_EQ(Position(sample->firstChild(), 0), selection.Start());
+}
+
+// This is a regression test for https://crbug.com/825120
+TEST_F(VisibleSelectionTest, BackwardSelectionWithMultipleEmptyBodies) {
+ Element* body = GetDocument().body();
+ Element* new_body = GetDocument().CreateRawElement(HTMLNames::bodyTag);
+ body->appendChild(new_body);
+ GetDocument().UpdateStyleAndLayout();
+
+ const SelectionInDOMTree selection =
+ SelectionInDOMTree::Builder()
+ .Collapse(Position::BeforeNode(*new_body))
+ .Extend(Position::BeforeNode(*body))
+ .Build();
+ const VisibleSelection visible_selection = CreateVisibleSelection(selection);
+
+ EXPECT_EQ("^<body></body>", GetSelectionTextFromBody(selection));
+ EXPECT_EQ("|<body></body>",
+ GetSelectionTextFromBody(visible_selection.AsSelection()));
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_units.cc b/chromium/third_party/blink/renderer/core/editing/visible_units.cc
new file mode 100644
index 00000000000..9d2d5ff2af2
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_units.cc
@@ -0,0 +1,1490 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/first_letter_pseudo_element.h"
+#include "third_party/blink/renderer/core/dom/node_traversal.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/iterators/backwards_character_iterator.h"
+#include "third_party/blink/renderer/core/editing/iterators/backwards_text_buffer.h"
+#include "third_party/blink/renderer/core/editing/iterators/character_iterator.h"
+#include "third_party/blink/renderer/core/editing/iterators/forwards_text_buffer.h"
+#include "third_party/blink/renderer/core/editing/iterators/simplified_backwards_text_iterator.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/local_caret_rect.h"
+#include "third_party/blink/renderer/core/editing/position.h"
+#include "third_party/blink/renderer/core/editing/position_iterator.h"
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+#include "third_party/blink/renderer/core/editing/text_affinity.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+#include "third_party/blink/renderer/core/html/html_br_element.h"
+#include "third_party/blink/renderer/core/html_names.h"
+#include "third_party/blink/renderer/core/layout/api/line_layout_item.h"
+#include "third_party/blink/renderer/core/layout/hit_test_request.h"
+#include "third_party/blink/renderer/core/layout/hit_test_result.h"
+#include "third_party/blink/renderer/core/layout/layout_inline.h"
+#include "third_party/blink/renderer/core/layout/layout_text_fragment.h"
+#include "third_party/blink/renderer/core/layout/layout_view.h"
+#include "third_party/blink/renderer/core/layout/line/inline_iterator.h"
+#include "third_party/blink/renderer/core/layout/line/inline_text_box.h"
+#include "third_party/blink/renderer/core/svg_element_type_helpers.h"
+#include "third_party/blink/renderer/platform/heap/handle.h"
+#include "third_party/blink/renderer/platform/text/text_boundaries.h"
+
+namespace blink {
+
+template <typename PositionType>
+static PositionType CanonicalizeCandidate(const PositionType& candidate) {
+ if (candidate.IsNull())
+ return PositionType();
+ DCHECK(IsVisuallyEquivalentCandidate(candidate));
+ PositionType upstream = MostBackwardCaretPosition(candidate);
+ if (IsVisuallyEquivalentCandidate(upstream))
+ return upstream;
+ return candidate;
+}
+
+template <typename PositionType>
+static PositionType CanonicalPosition(const PositionType& position) {
+ // Sometimes updating selection positions can be extremely expensive and
+ // occur frequently. Often calling preventDefault on mousedown events can
+ // avoid doing unnecessary text selection work. http://crbug.com/472258.
+ TRACE_EVENT0("input", "VisibleUnits::canonicalPosition");
+
+ // FIXME (9535): Canonicalizing to the leftmost candidate means that if
+ // we're at a line wrap, we will ask layoutObjects to paint downstream
+ // carets for other layoutObjects. To fix this, we need to either a) add
+ // code to all paintCarets to pass the responsibility off to the appropriate
+ // layoutObject for VisiblePosition's like these, or b) canonicalize to the
+ // rightmost candidate unless the affinity is upstream.
+ if (position.IsNull())
+ return PositionType();
+
+ DCHECK(position.GetDocument());
+ DCHECK(!position.GetDocument()->NeedsLayoutTreeUpdate());
+
+ const PositionType& backward_candidate = MostBackwardCaretPosition(position);
+ if (IsVisuallyEquivalentCandidate(backward_candidate))
+ return backward_candidate;
+
+ const PositionType& forward_candidate = MostForwardCaretPosition(position);
+ if (IsVisuallyEquivalentCandidate(forward_candidate))
+ return forward_candidate;
+
+ // When neither upstream or downstream gets us to a candidate
+ // (upstream/downstream won't leave blocks or enter new ones), we search
+ // forward and backward until we find one.
+ const PositionType& next = CanonicalizeCandidate(NextCandidate(position));
+ const PositionType& prev = CanonicalizeCandidate(PreviousCandidate(position));
+
+ // The new position must be in the same editable element. Enforce that
+ // first. Unless the descent is from a non-editable html element to an
+ // editable body.
+ Node* const node = position.ComputeContainerNode();
+ if (node && node->GetDocument().documentElement() == node &&
+ !HasEditableStyle(*node) && node->GetDocument().body() &&
+ HasEditableStyle(*node->GetDocument().body()))
+ return next.IsNotNull() ? next : prev;
+
+ Element* const editing_root = RootEditableElementOf(position);
+ // If the html element is editable, descending into its body will look like
+ // a descent from non-editable to editable content since
+ // |rootEditableElementOf()| always stops at the body.
+ if ((editing_root &&
+ editing_root->GetDocument().documentElement() == editing_root) ||
+ position.AnchorNode()->IsDocumentNode())
+ return next.IsNotNull() ? next : prev;
+
+ Node* const next_node = next.AnchorNode();
+ Node* const prev_node = prev.AnchorNode();
+ const bool prev_is_in_same_editable_element =
+ prev_node && RootEditableElementOf(prev) == editing_root;
+ const bool next_is_in_same_editable_element =
+ next_node && RootEditableElementOf(next) == editing_root;
+ if (prev_is_in_same_editable_element && !next_is_in_same_editable_element)
+ return prev;
+
+ if (next_is_in_same_editable_element && !prev_is_in_same_editable_element)
+ return next;
+
+ if (!next_is_in_same_editable_element && !prev_is_in_same_editable_element)
+ return PositionType();
+
+ // The new position should be in the same block flow element. Favor that.
+ Element* const original_block =
+ node ? EnclosingBlockFlowElement(*node) : nullptr;
+ const bool next_is_outside_original_block =
+ !next_node->IsDescendantOf(original_block) && next_node != original_block;
+ const bool prev_is_outside_original_block =
+ !prev_node->IsDescendantOf(original_block) && prev_node != original_block;
+ if (next_is_outside_original_block && !prev_is_outside_original_block)
+ return prev;
+
+ return next;
+}
+
+Position CanonicalPositionOf(const Position& position) {
+ return CanonicalPosition(position);
+}
+
+PositionInFlatTree CanonicalPositionOf(const PositionInFlatTree& position) {
+ return CanonicalPosition(position);
+}
+
+template <typename Strategy>
+static PositionWithAffinityTemplate<Strategy>
+AdjustBackwardPositionToAvoidCrossingEditingBoundariesTemplate(
+ const PositionWithAffinityTemplate<Strategy>& pos,
+ const PositionTemplate<Strategy>& anchor) {
+ if (pos.IsNull())
+ return pos;
+
+ ContainerNode* highest_root = HighestEditableRoot(anchor);
+
+ // Return empty position if |pos| is not somewhere inside the editable
+ // region containing this position
+ if (highest_root && !pos.AnchorNode()->IsDescendantOf(highest_root))
+ return PositionWithAffinityTemplate<Strategy>();
+
+ // Return |pos| itself if the two are from the very same editable region, or
+ // both are non-editable
+ // TODO(yosin) In the non-editable case, just because the new position is
+ // non-editable doesn't mean movement to it is allowed.
+ // |VisibleSelection::adjustForEditableContent()| has this problem too.
+ if (HighestEditableRoot(pos.GetPosition()) == highest_root)
+ return pos;
+
+ // Return empty position if this position is non-editable, but |pos| is
+ // editable.
+ // TODO(yosin) Move to the previous non-editable region.
+ if (!highest_root)
+ return PositionWithAffinityTemplate<Strategy>();
+
+ // Return the last position before |pos| that is in the same editable region
+ // as this position
+ return PositionWithAffinityTemplate<Strategy>(
+ LastEditablePositionBeforePositionInRoot(pos.GetPosition(),
+ *highest_root));
+}
+
+PositionWithAffinity AdjustBackwardPositionToAvoidCrossingEditingBoundaries(
+ const PositionWithAffinity& pos,
+ const Position& anchor) {
+ return AdjustBackwardPositionToAvoidCrossingEditingBoundariesTemplate(pos,
+ anchor);
+}
+
+PositionInFlatTreeWithAffinity
+AdjustBackwardPositionToAvoidCrossingEditingBoundaries(
+ const PositionInFlatTreeWithAffinity& pos,
+ const PositionInFlatTree& anchor) {
+ return AdjustBackwardPositionToAvoidCrossingEditingBoundariesTemplate(pos,
+ anchor);
+}
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy>
+AdjustBackwardPositionToAvoidCrossingEditingBoundariesAlgorithm(
+ const VisiblePositionTemplate<Strategy>& pos,
+ const PositionTemplate<Strategy>& anchor) {
+ DCHECK(pos.IsValid()) << pos;
+ return CreateVisiblePosition(
+ AdjustBackwardPositionToAvoidCrossingEditingBoundaries(
+ pos.ToPositionWithAffinity(), anchor));
+}
+
+VisiblePosition AdjustBackwardPositionToAvoidCrossingEditingBoundaries(
+ const VisiblePosition& visiblePosition,
+ const Position& anchor) {
+ return AdjustBackwardPositionToAvoidCrossingEditingBoundariesAlgorithm(
+ visiblePosition, anchor);
+}
+
+VisiblePositionInFlatTree
+AdjustBackwardPositionToAvoidCrossingEditingBoundaries(
+ const VisiblePositionInFlatTree& visiblePosition,
+ const PositionInFlatTree& anchor) {
+ return AdjustBackwardPositionToAvoidCrossingEditingBoundariesAlgorithm(
+ visiblePosition, anchor);
+}
+
+template <typename Strategy>
+static PositionWithAffinityTemplate<Strategy>
+AdjustForwardPositionToAvoidCrossingEditingBoundariesTemplate(
+ const PositionWithAffinityTemplate<Strategy>& pos,
+ const PositionTemplate<Strategy>& anchor) {
+ if (pos.IsNull())
+ return pos;
+
+ ContainerNode* highest_root = HighestEditableRoot(anchor);
+
+ // Return empty position if |pos| is not somewhere inside the editable
+ // region containing this position
+ if (highest_root && !pos.AnchorNode()->IsDescendantOf(highest_root))
+ return PositionWithAffinityTemplate<Strategy>();
+
+ // Return |pos| itself if the two are from the very same editable region, or
+ // both are non-editable
+ // TODO(yosin) In the non-editable case, just because the new position is
+ // non-editable doesn't mean movement to it is allowed.
+ // |VisibleSelection::adjustForEditableContent()| has this problem too.
+ if (HighestEditableRoot(pos.GetPosition()) == highest_root)
+ return pos;
+
+ // Returns the last position in the highest non-editable ancestor of |anchor|.
+ if (!highest_root) {
+ const Node* last_non_editable = anchor.ComputeContainerNode();
+ for (const Node& ancestor : Strategy::AncestorsOf(*last_non_editable)) {
+ if (HasEditableStyle(ancestor)) {
+ return PositionWithAffinityTemplate<Strategy>(
+ PositionTemplate<Strategy>::LastPositionInNode(*last_non_editable));
+ }
+ last_non_editable = &ancestor;
+ }
+ return PositionWithAffinityTemplate<Strategy>();
+ }
+
+ // Return the next position after |pos| that is in the same editable region
+ // as this position
+ return PositionWithAffinityTemplate<Strategy>(
+ FirstEditablePositionAfterPositionInRoot(pos.GetPosition(),
+ *highest_root));
+}
+
+PositionWithAffinity AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ const PositionWithAffinity& pos,
+ const Position& anchor) {
+ return AdjustForwardPositionToAvoidCrossingEditingBoundariesTemplate(pos,
+ anchor);
+}
+
+PositionInFlatTreeWithAffinity
+AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ const PositionInFlatTreeWithAffinity& pos,
+ const PositionInFlatTree& anchor) {
+ return AdjustForwardPositionToAvoidCrossingEditingBoundariesTemplate(
+ PositionInFlatTreeWithAffinity(pos), anchor);
+}
+
+VisiblePosition AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ const VisiblePosition& pos,
+ const Position& anchor) {
+ DCHECK(pos.IsValid()) << pos;
+ return CreateVisiblePosition(
+ AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ pos.ToPositionWithAffinity(), anchor));
+}
+
+VisiblePositionInFlatTree AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ const VisiblePositionInFlatTree& pos,
+ const PositionInFlatTree& anchor) {
+ DCHECK(pos.IsValid()) << pos;
+ return CreateVisiblePosition(
+ AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ pos.ToPositionWithAffinity(), anchor));
+}
+
+template <typename Strategy>
+static ContainerNode* NonShadowBoundaryParentNode(Node* node) {
+ ContainerNode* parent = Strategy::Parent(*node);
+ return parent && !parent->IsShadowRoot() ? parent : nullptr;
+}
+
+template <typename Strategy>
+static Node* ParentEditingBoundary(const PositionTemplate<Strategy>& position) {
+ Node* const anchor_node = position.AnchorNode();
+ if (!anchor_node)
+ return nullptr;
+
+ Node* document_element = anchor_node->GetDocument().documentElement();
+ if (!document_element)
+ return nullptr;
+
+ Node* boundary = position.ComputeContainerNode();
+ while (boundary != document_element &&
+ NonShadowBoundaryParentNode<Strategy>(boundary) &&
+ HasEditableStyle(*anchor_node) ==
+ HasEditableStyle(*Strategy::Parent(*boundary)))
+ boundary = NonShadowBoundaryParentNode<Strategy>(boundary);
+
+ return boundary;
+}
+
+template <typename Strategy>
+static PositionTemplate<Strategy> PreviousBoundaryAlgorithm(
+ const VisiblePositionTemplate<Strategy>& c,
+ BoundarySearchFunction search_function) {
+ DCHECK(c.IsValid()) << c;
+ const PositionTemplate<Strategy> pos = c.DeepEquivalent();
+ Node* boundary = ParentEditingBoundary(pos);
+ if (!boundary)
+ return PositionTemplate<Strategy>();
+
+ const PositionTemplate<Strategy> start =
+ PositionTemplate<Strategy>::EditingPositionOf(boundary, 0)
+ .ParentAnchoredEquivalent();
+ const PositionTemplate<Strategy> end = pos.ParentAnchoredEquivalent();
+
+ ForwardsTextBuffer suffix_string;
+ if (RequiresContextForWordBoundary(CharacterBefore(c))) {
+ TextIteratorAlgorithm<Strategy> forwards_iterator(
+ end, PositionTemplate<Strategy>::AfterNode(*boundary));
+ while (!forwards_iterator.AtEnd()) {
+ forwards_iterator.CopyTextTo(&suffix_string);
+ int context_end_index = EndOfFirstWordBoundaryContext(
+ suffix_string.Data() + suffix_string.Size() -
+ forwards_iterator.length(),
+ forwards_iterator.length());
+ if (context_end_index < forwards_iterator.length()) {
+ suffix_string.Shrink(forwards_iterator.length() - context_end_index);
+ break;
+ }
+ forwards_iterator.Advance();
+ }
+ }
+
+ unsigned suffix_length = suffix_string.Size();
+ BackwardsTextBuffer string;
+ string.PushRange(suffix_string.Data(), suffix_string.Size());
+
+ // Treat bullets used in the text security mode as regular characters when
+ // looking for boundaries.
+ SimplifiedBackwardsTextIteratorAlgorithm<Strategy> it(
+ EphemeralRangeTemplate<Strategy>(start, end),
+ TextIteratorBehavior::Builder()
+ .SetEmitsSmallXForTextSecurity(true)
+ .Build());
+ unsigned next = 0;
+ bool need_more_context = false;
+ while (!it.AtEnd()) {
+ // iterate to get chunks until the searchFunction returns a non-zero
+ // value.
+ int run_offset = 0;
+ do {
+ run_offset += it.CopyTextTo(&string, run_offset, string.Capacity());
+ next = search_function(string.Data(), string.Size(),
+ string.Size() - suffix_length, kMayHaveMoreContext,
+ need_more_context);
+ } while (!next && run_offset < it.length());
+ if (next)
+ break;
+ it.Advance();
+ }
+ if (need_more_context) {
+ // The last search returned the beginning of the buffer and asked for
+ // more context, but there is no earlier text. Force a search with
+ // what's available.
+ next = search_function(string.Data(), string.Size(),
+ string.Size() - suffix_length, kDontHaveMoreContext,
+ need_more_context);
+ DCHECK(!need_more_context);
+ }
+
+ if (!next)
+ return it.AtEnd() ? it.StartPosition() : pos;
+
+ // Use the character iterator to translate the next value into a DOM
+ // position.
+ BackwardsCharacterIteratorAlgorithm<Strategy> char_it(
+ EphemeralRangeTemplate<Strategy>(start, end));
+ char_it.Advance(string.Size() - suffix_length - next);
+ // TODO(yosin) charIt can get out of shadow host.
+ return char_it.EndPosition();
+}
+
+template <typename Strategy>
+static PositionTemplate<Strategy> NextBoundaryAlgorithm(
+ const VisiblePositionTemplate<Strategy>& c,
+ BoundarySearchFunction search_function) {
+ DCHECK(c.IsValid()) << c;
+ PositionTemplate<Strategy> pos = c.DeepEquivalent();
+ Node* boundary = ParentEditingBoundary(pos);
+ if (!boundary)
+ return PositionTemplate<Strategy>();
+
+ Document& d = boundary->GetDocument();
+ const PositionTemplate<Strategy> start(pos.ParentAnchoredEquivalent());
+
+ BackwardsTextBuffer prefix_string;
+ if (RequiresContextForWordBoundary(CharacterAfter(c))) {
+ SimplifiedBackwardsTextIteratorAlgorithm<Strategy> backwards_iterator(
+ EphemeralRangeTemplate<Strategy>(
+ PositionTemplate<Strategy>::FirstPositionInNode(d), start));
+ while (!backwards_iterator.AtEnd()) {
+ backwards_iterator.CopyTextTo(&prefix_string);
+ int context_start_index = StartOfLastWordBoundaryContext(
+ prefix_string.Data(), backwards_iterator.length());
+ if (context_start_index > 0) {
+ prefix_string.Shrink(context_start_index);
+ break;
+ }
+ backwards_iterator.Advance();
+ }
+ }
+
+ unsigned prefix_length = prefix_string.Size();
+ ForwardsTextBuffer string;
+ string.PushRange(prefix_string.Data(), prefix_string.Size());
+
+ const PositionTemplate<Strategy> search_start =
+ PositionTemplate<Strategy>::EditingPositionOf(
+ start.AnchorNode(), start.OffsetInContainerNode());
+ const PositionTemplate<Strategy> search_end =
+ PositionTemplate<Strategy>::LastPositionInNode(*boundary);
+ // Treat bullets used in the text security mode as regular characters when
+ // looking for boundaries
+ TextIteratorAlgorithm<Strategy> it(
+ search_start, search_end,
+ TextIteratorBehavior::Builder()
+ .SetEmitsCharactersBetweenAllVisiblePositions(true)
+ .SetEmitsSmallXForTextSecurity(true)
+ .Build());
+ const unsigned kInvalidOffset = static_cast<unsigned>(-1);
+ unsigned next = kInvalidOffset;
+ unsigned offset = prefix_length;
+ bool need_more_context = false;
+ while (!it.AtEnd()) {
+ // Keep asking the iterator for chunks until the search function
+ // returns an end value not equal to the length of the string passed to
+ // it.
+ int run_offset = 0;
+ do {
+ run_offset += it.CopyTextTo(&string, run_offset, string.Capacity());
+ next = search_function(string.Data(), string.Size(), offset,
+ kMayHaveMoreContext, need_more_context);
+ if (!need_more_context) {
+ // When the search does not need more context, skip all examined
+ // characters except the last one, in case it is a boundary.
+ offset = string.Size();
+ U16_BACK_1(string.Data(), 0, offset);
+ }
+ } while (next == string.Size() && run_offset < it.length());
+ if (next != string.Size())
+ break;
+ it.Advance();
+ }
+ if (need_more_context) {
+ // The last search returned the end of the buffer and asked for more
+ // context, but there is no further text. Force a search with what's
+ // available.
+ next = search_function(string.Data(), string.Size(), prefix_length,
+ kDontHaveMoreContext, need_more_context);
+ DCHECK(!need_more_context);
+ }
+
+ if (it.AtEnd() && next == string.Size()) {
+ pos = it.StartPositionInCurrentContainer();
+ } else if (next != kInvalidOffset && next != prefix_length) {
+ // Use the character iterator to translate the next value into a DOM
+ // position.
+ CharacterIteratorAlgorithm<Strategy> char_it(
+ search_start, search_end,
+ TextIteratorBehavior::Builder()
+ .SetEmitsCharactersBetweenAllVisiblePositions(true)
+ .Build());
+ char_it.Advance(next - prefix_length - 1);
+ pos = char_it.EndPosition();
+
+ if (char_it.CharacterAt(0) == '\n') {
+ // TODO(yosin) workaround for collapsed range (where only start
+ // position is correct) emitted for some emitted newlines
+ // (see rdar://5192593)
+ const VisiblePositionTemplate<Strategy> vis_pos =
+ CreateVisiblePosition(pos);
+ if (vis_pos.DeepEquivalent() ==
+ CreateVisiblePosition(char_it.StartPosition()).DeepEquivalent()) {
+ char_it.Advance(1);
+ pos = char_it.StartPosition();
+ }
+ }
+ }
+
+ return pos;
+}
+
+Position NextBoundary(const VisiblePosition& visible_position,
+ BoundarySearchFunction search_function) {
+ return NextBoundaryAlgorithm(visible_position, search_function);
+}
+
+PositionInFlatTree NextBoundary(
+ const VisiblePositionInFlatTree& visible_position,
+ BoundarySearchFunction search_function) {
+ return NextBoundaryAlgorithm(visible_position, search_function);
+}
+
+Position PreviousBoundary(const VisiblePosition& visible_position,
+ BoundarySearchFunction search_function) {
+ return PreviousBoundaryAlgorithm(visible_position, search_function);
+}
+
+PositionInFlatTree PreviousBoundary(
+ const VisiblePositionInFlatTree& visible_position,
+ BoundarySearchFunction search_function) {
+ return PreviousBoundaryAlgorithm(visible_position, search_function);
+}
+
+// ---------
+
+template <typename Strategy>
+static VisiblePositionTemplate<Strategy> StartOfDocumentAlgorithm(
+ const VisiblePositionTemplate<Strategy>& visible_position) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ Node* node = visible_position.DeepEquivalent().AnchorNode();
+ if (!node || !node->GetDocument().documentElement())
+ return VisiblePositionTemplate<Strategy>();
+
+ return CreateVisiblePosition(PositionTemplate<Strategy>::FirstPositionInNode(
+ *node->GetDocument().documentElement()));
+}
+
+VisiblePosition StartOfDocument(const VisiblePosition& c) {
+ return StartOfDocumentAlgorithm<EditingStrategy>(c);
+}
+
+VisiblePositionInFlatTree StartOfDocument(const VisiblePositionInFlatTree& c) {
+ return StartOfDocumentAlgorithm<EditingInFlatTreeStrategy>(c);
+}
+
+template <typename Strategy>
+static VisiblePositionTemplate<Strategy> EndOfDocumentAlgorithm(
+ const VisiblePositionTemplate<Strategy>& visible_position) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ Node* node = visible_position.DeepEquivalent().AnchorNode();
+ if (!node || !node->GetDocument().documentElement())
+ return VisiblePositionTemplate<Strategy>();
+
+ Element* doc = node->GetDocument().documentElement();
+ return CreateVisiblePosition(
+ PositionTemplate<Strategy>::LastPositionInNode(*doc));
+}
+
+VisiblePosition EndOfDocument(const VisiblePosition& c) {
+ return EndOfDocumentAlgorithm<EditingStrategy>(c);
+}
+
+VisiblePositionInFlatTree EndOfDocument(const VisiblePositionInFlatTree& c) {
+ return EndOfDocumentAlgorithm<EditingInFlatTreeStrategy>(c);
+}
+
+bool IsStartOfDocument(const VisiblePosition& p) {
+ DCHECK(p.IsValid()) << p;
+ return p.IsNotNull() &&
+ PreviousPositionOf(p, kCanCrossEditingBoundary).IsNull();
+}
+
+bool IsEndOfDocument(const VisiblePosition& p) {
+ DCHECK(p.IsValid()) << p;
+ return p.IsNotNull() && NextPositionOf(p, kCanCrossEditingBoundary).IsNull();
+}
+
+// ---------
+
+VisiblePosition StartOfEditableContent(
+ const VisiblePosition& visible_position) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ ContainerNode* highest_root =
+ HighestEditableRoot(visible_position.DeepEquivalent());
+ if (!highest_root)
+ return VisiblePosition();
+
+ return VisiblePosition::FirstPositionInNode(*highest_root);
+}
+
+VisiblePosition EndOfEditableContent(const VisiblePosition& visible_position) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ ContainerNode* highest_root =
+ HighestEditableRoot(visible_position.DeepEquivalent());
+ if (!highest_root)
+ return VisiblePosition();
+
+ return VisiblePosition::LastPositionInNode(*highest_root);
+}
+
+bool IsEndOfEditableOrNonEditableContent(const VisiblePosition& position) {
+ DCHECK(position.IsValid()) << position;
+ return position.IsNotNull() && NextPositionOf(position).IsNull();
+}
+
+// TODO(yosin) We should rename |isEndOfEditableOrNonEditableContent()| what
+// this function does, e.g. |isLastVisiblePositionOrEndOfInnerEditor()|.
+bool IsEndOfEditableOrNonEditableContent(
+ const VisiblePositionInFlatTree& position) {
+ DCHECK(position.IsValid()) << position;
+ if (position.IsNull())
+ return false;
+ const VisiblePositionInFlatTree next_position = NextPositionOf(position);
+ if (next_position.IsNull())
+ return true;
+ // In DOM version, following condition, the last position of inner editor
+ // of INPUT/TEXTAREA element, by |nextPosition().isNull()|, because of
+ // an inner editor is an only leaf node.
+ if (!next_position.DeepEquivalent().IsAfterAnchor())
+ return false;
+ return IsTextControl(next_position.DeepEquivalent().AnchorNode());
+}
+
+static LayoutUnit BoundingBoxLogicalHeight(LayoutObject* o,
+ const LayoutRect& rect) {
+ return o->Style()->IsHorizontalWritingMode() ? rect.Height() : rect.Width();
+}
+
+// TODO(editing-dev): The semantics seems wrong when we're in a one-letter block
+// with first-letter style, e.g., <div>F</div>, where the letter is laid-out in
+// an anonymous first-letter LayoutTextFragment instead of the LayoutObject of
+// the text node. It seems weird to return false in this case.
+bool HasRenderedNonAnonymousDescendantsWithHeight(
+ const LayoutObject* layout_object) {
+ const LayoutObject* stop = layout_object->NextInPreOrderAfterChildren();
+ // TODO(editing-dev): Avoid single-character parameter names.
+ for (LayoutObject* o = layout_object->SlowFirstChild(); o && o != stop;
+ o = o->NextInPreOrder()) {
+ if (o->NonPseudoNode()) {
+ if ((o->IsText() && ToLayoutText(o)->HasNonCollapsedText()) ||
+ (o->IsBox() && ToLayoutBox(o)->PixelSnappedLogicalHeight()) ||
+ (o->IsLayoutInline() && IsEmptyInline(LineLayoutItem(o)) &&
+ // TODO(crbug.com/771398): Find alternative ways to check whether an
+ // empty LayoutInline is rendered, without checking InlineBox.
+ BoundingBoxLogicalHeight(o, ToLayoutInline(o)->LinesBoundingBox())))
+ return true;
+ }
+ }
+ return false;
+}
+
+VisiblePosition VisiblePositionForContentsPoint(const IntPoint& contents_point,
+ LocalFrame* frame) {
+ HitTestRequest request = HitTestRequest::kMove | HitTestRequest::kReadOnly |
+ HitTestRequest::kActive |
+ HitTestRequest::kIgnoreClipping;
+ HitTestResult result(request, contents_point);
+ frame->GetDocument()->GetLayoutView()->HitTest(result);
+
+ if (Node* node = result.InnerNode()) {
+ return CreateVisiblePosition(PositionRespectingEditingBoundary(
+ frame->Selection().ComputeVisibleSelectionInDOMTreeDeprecated().Start(),
+ result.LocalPoint(), node));
+ }
+ return VisiblePosition();
+}
+
+// TODO(yosin): We should use |associatedLayoutObjectOf()| in "VisibleUnits.cpp"
+// where it takes |LayoutObject| from |Position|.
+
+int CaretMinOffset(const Node* node) {
+ const LayoutObject* layout_object = AssociatedLayoutObjectOf(*node, 0);
+ return layout_object ? layout_object->CaretMinOffset() : 0;
+}
+
+int CaretMaxOffset(const Node* n) {
+ return EditingStrategy::CaretMaxOffset(*n);
+}
+
+template <typename Strategy>
+static bool InRenderedText(const PositionTemplate<Strategy>& position) {
+ Node* const anchor_node = position.AnchorNode();
+ if (!anchor_node || !anchor_node->IsTextNode())
+ return false;
+
+ const int offset_in_node = position.ComputeEditingOffset();
+ const LayoutObject* layout_object =
+ AssociatedLayoutObjectOf(*anchor_node, offset_in_node);
+ if (!layout_object)
+ return false;
+
+ const LayoutText* text_layout_object = ToLayoutText(layout_object);
+ const int text_offset =
+ offset_in_node - text_layout_object->TextStartOffset();
+ if (!text_layout_object->ContainsCaretOffset(text_offset))
+ return false;
+ // Return false for offsets inside composed characters.
+ // TODO(editing-dev): Previous/NextGraphemeBoundaryOf() work on DOM offsets,
+ // So they should use |offset_in_node| instead of |text_offset|.
+ return text_offset == text_layout_object->CaretMinOffset() ||
+ text_offset == NextGraphemeBoundaryOf(*anchor_node,
+ PreviousGraphemeBoundaryOf(
+ *anchor_node, text_offset));
+}
+
+bool RendersInDifferentPosition(const Position& position1,
+ const Position& position2) {
+ if (position1.IsNull() || position2.IsNull())
+ return false;
+ const LocalCaretRect& caret_rect1 =
+ LocalCaretRectOfPosition(PositionWithAffinity(position1));
+ const LocalCaretRect& caret_rect2 =
+ LocalCaretRectOfPosition(PositionWithAffinity(position2));
+ if (!caret_rect1.layout_object || !caret_rect2.layout_object)
+ return caret_rect1.layout_object != caret_rect2.layout_object;
+ return LocalToAbsoluteQuadOf(caret_rect1) !=
+ LocalToAbsoluteQuadOf(caret_rect2);
+}
+
+// TODO(editing-dev): Share code with IsVisuallyEquivalentCandidate if possible.
+bool EndsOfNodeAreVisuallyDistinctPositions(const Node* node) {
+ if (!node)
+ return false;
+
+ LayoutObject* layout_object = node->GetLayoutObject();
+ if (!layout_object)
+ return false;
+
+ if (!layout_object->IsInline())
+ return true;
+
+ // Don't include inline tables.
+ if (IsHTMLTableElement(*node))
+ return false;
+
+ // A Marquee elements are moving so we should assume their ends are always
+ // visibily distinct.
+ if (IsHTMLMarqueeElement(*node))
+ return true;
+
+ // There is a VisiblePosition inside an empty inline-block container.
+ return layout_object->IsAtomicInlineLevel() &&
+ CanHaveChildrenForEditing(node) &&
+ !ToLayoutBox(layout_object)->Size().IsEmpty() &&
+ !HasRenderedNonAnonymousDescendantsWithHeight(layout_object);
+}
+
+template <typename Strategy>
+static Node* EnclosingVisualBoundary(Node* node) {
+ while (node && !EndsOfNodeAreVisuallyDistinctPositions(node))
+ node = Strategy::Parent(*node);
+
+ return node;
+}
+
+// upstream() and downstream() want to return positions that are either in a
+// text node or at just before a non-text node. This method checks for that.
+template <typename Strategy>
+static bool IsStreamer(const PositionIteratorAlgorithm<Strategy>& pos) {
+ if (!pos.GetNode())
+ return true;
+
+ if (IsAtomicNode(pos.GetNode()))
+ return true;
+
+ return pos.AtStartOfNode();
+}
+
+template <typename Strategy>
+static PositionTemplate<Strategy> AdjustPositionForBackwardIteration(
+ const PositionTemplate<Strategy>& position) {
+ DCHECK(!position.IsNull());
+ if (!position.IsAfterAnchor())
+ return position;
+ if (IsUserSelectContain(*position.AnchorNode()))
+ return position.ToOffsetInAnchor();
+ return PositionTemplate<Strategy>::EditingPositionOf(
+ position.AnchorNode(), Strategy::CaretMaxOffset(*position.AnchorNode()));
+}
+
+template <typename Strategy>
+static PositionTemplate<Strategy> MostBackwardCaretPosition(
+ const PositionTemplate<Strategy>& position,
+ EditingBoundaryCrossingRule rule) {
+ DCHECK(!NeedsLayoutTreeUpdate(position)) << position;
+ TRACE_EVENT0("input", "VisibleUnits::mostBackwardCaretPosition");
+
+ Node* const start_node = position.AnchorNode();
+ if (!start_node)
+ return PositionTemplate<Strategy>();
+
+ // iterate backward from there, looking for a qualified position
+ Node* const boundary = EnclosingVisualBoundary<Strategy>(start_node);
+ // FIXME: PositionIterator should respect Before and After positions.
+ PositionIteratorAlgorithm<Strategy> last_visible(
+ AdjustPositionForBackwardIteration<Strategy>(position));
+ const bool start_editable = HasEditableStyle(*start_node);
+ Node* last_node = start_node;
+ bool boundary_crossed = false;
+ for (PositionIteratorAlgorithm<Strategy> current_pos = last_visible;
+ !current_pos.AtStart(); current_pos.Decrement()) {
+ Node* current_node = current_pos.GetNode();
+ // Don't check for an editability change if we haven't moved to a different
+ // node, to avoid the expense of computing hasEditableStyle().
+ if (current_node != last_node) {
+ // Don't change editability.
+ const bool current_editable = HasEditableStyle(*current_node);
+ if (start_editable != current_editable) {
+ if (rule == kCannotCrossEditingBoundary)
+ break;
+ boundary_crossed = true;
+ }
+ last_node = current_node;
+ }
+
+ // There is no caret position in non-text svg elements.
+ if (current_node->IsSVGElement() && !IsSVGTextElement(current_node))
+ continue;
+
+ // If we've moved to a position that is visually distinct, return the last
+ // saved position. There is code below that terminates early if we're
+ // *about* to move to a visually distinct position.
+ if (EndsOfNodeAreVisuallyDistinctPositions(current_node) &&
+ current_node != boundary)
+ return last_visible.DeprecatedComputePosition();
+
+ // skip position in non-laid out or invisible node
+ const LayoutObject* const layout_object =
+ AssociatedLayoutObjectOf(*current_node, current_pos.OffsetInLeafNode(),
+ LayoutObjectSide::kFirstLetterIfOnBoundary);
+ if (!layout_object ||
+ layout_object->Style()->Visibility() != EVisibility::kVisible)
+ continue;
+
+ if (rule == kCanCrossEditingBoundary && boundary_crossed) {
+ last_visible = current_pos;
+ break;
+ }
+
+ // track last visible streamer position
+ if (IsStreamer<Strategy>(current_pos))
+ last_visible = current_pos;
+
+ // Don't move past a position that is visually distinct. We could rely on
+ // code above to terminate and return lastVisible on the next iteration, but
+ // we terminate early to avoid doing a nodeIndex() call.
+ if (EndsOfNodeAreVisuallyDistinctPositions(current_node) &&
+ current_pos.AtStartOfNode())
+ return last_visible.DeprecatedComputePosition();
+
+ // Return position after tables and nodes which have content that can be
+ // ignored.
+ if (EditingIgnoresContent(*current_node) ||
+ IsDisplayInsideTable(current_node)) {
+ if (current_pos.AtEndOfNode())
+ return PositionTemplate<Strategy>::AfterNode(*current_node);
+ continue;
+ }
+
+ // return current position if it is in laid out text
+ if (!layout_object->IsText())
+ continue;
+ const LayoutText* const text_layout_object = ToLayoutText(layout_object);
+ if (!text_layout_object->HasNonCollapsedText())
+ continue;
+ const unsigned text_start_offset = text_layout_object->TextStartOffset();
+ if (current_node != start_node) {
+ // This assertion fires in layout tests in the case-transform.html test
+ // because of a mix-up between offsets in the text in the DOM tree with
+ // text in the layout tree which can have a different length due to case
+ // transformation.
+ // Until we resolve that, disable this so we can run the layout tests!
+ // DCHECK_GE(currentOffset, layoutObject->caretMaxOffset());
+ return PositionTemplate<Strategy>(
+ current_node, layout_object->CaretMaxOffset() + text_start_offset);
+ }
+
+ DCHECK_GE(current_pos.OffsetInLeafNode(),
+ static_cast<int>(text_layout_object->TextStartOffset()));
+ if (text_layout_object->IsAfterNonCollapsedCharacter(
+ current_pos.OffsetInLeafNode() -
+ text_layout_object->TextStartOffset()))
+ return current_pos.ComputePosition();
+ }
+ return last_visible.DeprecatedComputePosition();
+}
+
+Position MostBackwardCaretPosition(const Position& position,
+ EditingBoundaryCrossingRule rule) {
+ return MostBackwardCaretPosition<EditingStrategy>(position, rule);
+}
+
+PositionInFlatTree MostBackwardCaretPosition(const PositionInFlatTree& position,
+ EditingBoundaryCrossingRule rule) {
+ return MostBackwardCaretPosition<EditingInFlatTreeStrategy>(position, rule);
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> MostForwardCaretPosition(
+ const PositionTemplate<Strategy>& position,
+ EditingBoundaryCrossingRule rule) {
+ DCHECK(!NeedsLayoutTreeUpdate(position)) << position;
+ TRACE_EVENT0("input", "VisibleUnits::mostForwardCaretPosition");
+
+ Node* const start_node = position.AnchorNode();
+ if (!start_node)
+ return PositionTemplate<Strategy>();
+
+ // iterate forward from there, looking for a qualified position
+ Node* const boundary = EnclosingVisualBoundary<Strategy>(start_node);
+ // FIXME: PositionIterator should respect Before and After positions.
+ PositionIteratorAlgorithm<Strategy> last_visible(
+ position.IsAfterAnchor()
+ ? PositionTemplate<Strategy>::EditingPositionOf(
+ position.AnchorNode(),
+ Strategy::CaretMaxOffset(*position.AnchorNode()))
+ : position);
+ const bool start_editable = HasEditableStyle(*start_node);
+ Node* last_node = start_node;
+ bool boundary_crossed = false;
+ for (PositionIteratorAlgorithm<Strategy> current_pos = last_visible;
+ !current_pos.AtEnd(); current_pos.Increment()) {
+ Node* current_node = current_pos.GetNode();
+ // Don't check for an editability change if we haven't moved to a different
+ // node, to avoid the expense of computing hasEditableStyle().
+ if (current_node != last_node) {
+ // Don't change editability.
+ const bool current_editable = HasEditableStyle(*current_node);
+ if (start_editable != current_editable) {
+ if (rule == kCannotCrossEditingBoundary)
+ break;
+ boundary_crossed = true;
+ }
+
+ last_node = current_node;
+ }
+
+ // stop before going above the body, up into the head
+ // return the last visible streamer position
+ if (IsHTMLBodyElement(*current_node) && current_pos.AtEndOfNode())
+ break;
+
+ // There is no caret position in non-text svg elements.
+ if (current_node->IsSVGElement() && !IsSVGTextElement(current_node))
+ continue;
+
+ // Do not move to a visually distinct position.
+ if (EndsOfNodeAreVisuallyDistinctPositions(current_node) &&
+ current_node != boundary)
+ return last_visible.DeprecatedComputePosition();
+ // Do not move past a visually disinct position.
+ // Note: The first position after the last in a node whose ends are visually
+ // distinct positions will be [boundary->parentNode(),
+ // originalBlock->nodeIndex() + 1].
+ if (boundary && Strategy::Parent(*boundary) == current_node)
+ return last_visible.DeprecatedComputePosition();
+
+ // skip position in non-laid out or invisible node
+ const LayoutObject* const layout_object =
+ AssociatedLayoutObjectOf(*current_node, current_pos.OffsetInLeafNode());
+ if (!layout_object ||
+ layout_object->Style()->Visibility() != EVisibility::kVisible)
+ continue;
+
+ if (rule == kCanCrossEditingBoundary && boundary_crossed)
+ return current_pos.DeprecatedComputePosition();
+
+ // track last visible streamer position
+ if (IsStreamer<Strategy>(current_pos))
+ last_visible = current_pos;
+
+ // Return position before tables and nodes which have content that can be
+ // ignored.
+ if (EditingIgnoresContent(*current_node) ||
+ IsDisplayInsideTable(current_node)) {
+ if (current_pos.OffsetInLeafNode() <= layout_object->CaretMinOffset())
+ return PositionTemplate<Strategy>::EditingPositionOf(
+ current_node, layout_object->CaretMinOffset());
+ continue;
+ }
+
+ // return current position if it is in laid out text
+ if (!layout_object->IsText())
+ continue;
+ const LayoutText* const text_layout_object = ToLayoutText(layout_object);
+ if (!text_layout_object->HasNonCollapsedText())
+ continue;
+ const unsigned text_start_offset = text_layout_object->TextStartOffset();
+ if (current_node != start_node) {
+ DCHECK(current_pos.AtStartOfNode());
+ return PositionTemplate<Strategy>(
+ current_node, layout_object->CaretMinOffset() + text_start_offset);
+ }
+
+ DCHECK_GE(current_pos.OffsetInLeafNode(),
+ static_cast<int>(text_layout_object->TextStartOffset()));
+ if (text_layout_object->IsBeforeNonCollapsedCharacter(
+ current_pos.OffsetInLeafNode() -
+ text_layout_object->TextStartOffset()))
+ return current_pos.ComputePosition();
+ }
+ return last_visible.DeprecatedComputePosition();
+}
+
+Position MostForwardCaretPosition(const Position& position,
+ EditingBoundaryCrossingRule rule) {
+ return MostForwardCaretPosition<EditingStrategy>(position, rule);
+}
+
+PositionInFlatTree MostForwardCaretPosition(const PositionInFlatTree& position,
+ EditingBoundaryCrossingRule rule) {
+ return MostForwardCaretPosition<EditingInFlatTreeStrategy>(position, rule);
+}
+
+// Returns true if the visually equivalent positions around have different
+// editability. A position is considered at editing boundary if one of the
+// following is true:
+// 1. It is the first position in the node and the next visually equivalent
+// position is non editable.
+// 2. It is the last position in the node and the previous visually equivalent
+// position is non editable.
+// 3. It is an editable position and both the next and previous visually
+// equivalent positions are both non editable.
+template <typename Strategy>
+static bool AtEditingBoundary(const PositionTemplate<Strategy> positions) {
+ PositionTemplate<Strategy> next_position =
+ MostForwardCaretPosition(positions, kCanCrossEditingBoundary);
+ if (positions.AtFirstEditingPositionForNode() && next_position.IsNotNull() &&
+ !HasEditableStyle(*next_position.AnchorNode()))
+ return true;
+
+ PositionTemplate<Strategy> prev_position =
+ MostBackwardCaretPosition(positions, kCanCrossEditingBoundary);
+ if (positions.AtLastEditingPositionForNode() && prev_position.IsNotNull() &&
+ !HasEditableStyle(*prev_position.AnchorNode()))
+ return true;
+
+ return next_position.IsNotNull() &&
+ !HasEditableStyle(*next_position.AnchorNode()) &&
+ prev_position.IsNotNull() &&
+ !HasEditableStyle(*prev_position.AnchorNode());
+}
+
+template <typename Strategy>
+static bool IsVisuallyEquivalentCandidateAlgorithm(
+ const PositionTemplate<Strategy>& position) {
+ Node* const anchor_node = position.AnchorNode();
+ if (!anchor_node)
+ return false;
+
+ LayoutObject* layout_object = anchor_node->GetLayoutObject();
+ if (!layout_object)
+ return false;
+
+ if (layout_object->Style()->Visibility() != EVisibility::kVisible)
+ return false;
+
+ if (layout_object->IsBR()) {
+ // TODO(leviw) The condition should be
+ // m_anchorType == PositionAnchorType::BeforeAnchor, but for now we
+ // still need to support legacy positions.
+ if (position.IsAfterAnchor())
+ return false;
+ if (position.ComputeEditingOffset())
+ return false;
+ const Node* parent = Strategy::Parent(*anchor_node);
+ return parent->GetLayoutObject() &&
+ parent->GetLayoutObject()->IsSelectable();
+ }
+
+ if (layout_object->IsText())
+ return layout_object->IsSelectable() && InRenderedText(position);
+
+ if (layout_object->IsSVG()) {
+ // We don't consider SVG elements are contenteditable except for
+ // associated |layoutObject| returns |isText()| true,
+ // e.g. |LayoutSVGInlineText|.
+ return false;
+ }
+
+ if (IsDisplayInsideTable(anchor_node) ||
+ EditingIgnoresContent(*anchor_node)) {
+ if (!position.AtFirstEditingPositionForNode() &&
+ !position.AtLastEditingPositionForNode())
+ return false;
+ const Node* parent = Strategy::Parent(*anchor_node);
+ return parent->GetLayoutObject() &&
+ parent->GetLayoutObject()->IsSelectable();
+ }
+
+ if (anchor_node->GetDocument().documentElement() == anchor_node ||
+ anchor_node->IsDocumentNode())
+ return false;
+
+ if (!layout_object->IsSelectable())
+ return false;
+
+ if (layout_object->IsLayoutBlockFlow() || layout_object->IsFlexibleBox() ||
+ layout_object->IsLayoutGrid()) {
+ if (ToLayoutBlock(layout_object)->LogicalHeight() ||
+ anchor_node->GetDocument().body() == anchor_node) {
+ if (!HasRenderedNonAnonymousDescendantsWithHeight(layout_object))
+ return position.AtFirstEditingPositionForNode();
+ return HasEditableStyle(*anchor_node) && AtEditingBoundary(position);
+ }
+ } else {
+ return HasEditableStyle(*anchor_node) && AtEditingBoundary(position);
+ }
+
+ return false;
+}
+
+bool IsVisuallyEquivalentCandidate(const Position& position) {
+ return IsVisuallyEquivalentCandidateAlgorithm<EditingStrategy>(position);
+}
+
+bool IsVisuallyEquivalentCandidate(const PositionInFlatTree& position) {
+ return IsVisuallyEquivalentCandidateAlgorithm<EditingInFlatTreeStrategy>(
+ position);
+}
+
+template <typename Strategy>
+static VisiblePositionTemplate<Strategy> SkipToEndOfEditingBoundary(
+ const VisiblePositionTemplate<Strategy>& pos,
+ const PositionTemplate<Strategy>& anchor) {
+ DCHECK(pos.IsValid()) << pos;
+ if (pos.IsNull())
+ return pos;
+
+ ContainerNode* highest_root = HighestEditableRoot(anchor);
+ ContainerNode* highest_root_of_pos =
+ HighestEditableRoot(pos.DeepEquivalent());
+
+ // Return |pos| itself if the two are from the very same editable region,
+ // or both are non-editable.
+ if (highest_root_of_pos == highest_root)
+ return pos;
+
+ // If this is not editable but |pos| has an editable root, skip to the end
+ if (!highest_root && highest_root_of_pos)
+ return CreateVisiblePosition(
+ PositionTemplate<Strategy>(highest_root_of_pos,
+ PositionAnchorType::kAfterAnchor)
+ .ParentAnchoredEquivalent());
+
+ // That must mean that |pos| is not editable. Return the next position after
+ // |pos| that is in the same editable region as this position
+ DCHECK(highest_root);
+ return FirstEditableVisiblePositionAfterPositionInRoot(pos.DeepEquivalent(),
+ *highest_root);
+}
+
+template <typename Strategy>
+static UChar32 CharacterAfterAlgorithm(
+ const VisiblePositionTemplate<Strategy>& visible_position) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ // We canonicalize to the first of two equivalent candidates, but the second
+ // of the two candidates is the one that will be inside the text node
+ // containing the character after this visible position.
+ const PositionTemplate<Strategy> pos =
+ MostForwardCaretPosition(visible_position.DeepEquivalent());
+ if (!pos.IsOffsetInAnchor())
+ return 0;
+ Node* container_node = pos.ComputeContainerNode();
+ if (!container_node || !container_node->IsTextNode())
+ return 0;
+ unsigned offset = static_cast<unsigned>(pos.OffsetInContainerNode());
+ Text* text_node = ToText(container_node);
+ unsigned length = text_node->length();
+ if (offset >= length)
+ return 0;
+
+ return text_node->data().CharacterStartingAt(offset);
+}
+
+UChar32 CharacterAfter(const VisiblePosition& visible_position) {
+ return CharacterAfterAlgorithm<EditingStrategy>(visible_position);
+}
+
+UChar32 CharacterAfter(const VisiblePositionInFlatTree& visible_position) {
+ return CharacterAfterAlgorithm<EditingInFlatTreeStrategy>(visible_position);
+}
+
+template <typename Strategy>
+static UChar32 CharacterBeforeAlgorithm(
+ const VisiblePositionTemplate<Strategy>& visible_position) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ return CharacterAfter(PreviousPositionOf(visible_position));
+}
+
+UChar32 CharacterBefore(const VisiblePosition& visible_position) {
+ return CharacterBeforeAlgorithm<EditingStrategy>(visible_position);
+}
+
+UChar32 CharacterBefore(const VisiblePositionInFlatTree& visible_position) {
+ return CharacterBeforeAlgorithm<EditingInFlatTreeStrategy>(visible_position);
+}
+
+template <typename Strategy>
+static VisiblePositionTemplate<Strategy> NextPositionOfAlgorithm(
+ const PositionWithAffinityTemplate<Strategy>& position,
+ EditingBoundaryCrossingRule rule) {
+ const VisiblePositionTemplate<Strategy> next = CreateVisiblePosition(
+ NextVisuallyDistinctCandidate(position.GetPosition()),
+ position.Affinity());
+
+ switch (rule) {
+ case kCanCrossEditingBoundary:
+ return next;
+ case kCannotCrossEditingBoundary:
+ return AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ next, position.GetPosition());
+ case kCanSkipOverEditingBoundary:
+ return SkipToEndOfEditingBoundary(next, position.GetPosition());
+ }
+ NOTREACHED();
+ return AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ next, position.GetPosition());
+}
+
+VisiblePosition NextPositionOf(const VisiblePosition& visible_position,
+ EditingBoundaryCrossingRule rule) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ return NextPositionOfAlgorithm<EditingStrategy>(
+ visible_position.ToPositionWithAffinity(), rule);
+}
+
+VisiblePositionInFlatTree NextPositionOf(
+ const VisiblePositionInFlatTree& visible_position,
+ EditingBoundaryCrossingRule rule) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ return NextPositionOfAlgorithm<EditingInFlatTreeStrategy>(
+ visible_position.ToPositionWithAffinity(), rule);
+}
+
+template <typename Strategy>
+static VisiblePositionTemplate<Strategy> SkipToStartOfEditingBoundary(
+ const VisiblePositionTemplate<Strategy>& pos,
+ const PositionTemplate<Strategy>& anchor) {
+ DCHECK(pos.IsValid()) << pos;
+ if (pos.IsNull())
+ return pos;
+
+ ContainerNode* highest_root = HighestEditableRoot(anchor);
+ ContainerNode* highest_root_of_pos =
+ HighestEditableRoot(pos.DeepEquivalent());
+
+ // Return |pos| itself if the two are from the very same editable region, or
+ // both are non-editable.
+ if (highest_root_of_pos == highest_root)
+ return pos;
+
+ // If this is not editable but |pos| has an editable root, skip to the start
+ if (!highest_root && highest_root_of_pos)
+ return CreateVisiblePosition(PreviousVisuallyDistinctCandidate(
+ PositionTemplate<Strategy>(highest_root_of_pos,
+ PositionAnchorType::kBeforeAnchor)
+ .ParentAnchoredEquivalent()));
+
+ // That must mean that |pos| is not editable. Return the last position
+ // before |pos| that is in the same editable region as this position
+ DCHECK(highest_root);
+ return LastEditableVisiblePositionBeforePositionInRoot(pos.DeepEquivalent(),
+ *highest_root);
+}
+
+template <typename Strategy>
+static VisiblePositionTemplate<Strategy> PreviousPositionOfAlgorithm(
+ const PositionTemplate<Strategy>& position,
+ EditingBoundaryCrossingRule rule) {
+ const PositionTemplate<Strategy> prev_position =
+ PreviousVisuallyDistinctCandidate(position);
+
+ // return null visible position if there is no previous visible position
+ if (prev_position.AtStartOfTree())
+ return VisiblePositionTemplate<Strategy>();
+
+ // we should always be able to make the affinity |TextAffinity::Downstream|,
+ // because going previous from an |TextAffinity::Upstream| position can
+ // never yield another |TextAffinity::Upstream position| (unless line wrap
+ // length is 0!).
+ const VisiblePositionTemplate<Strategy> prev =
+ CreateVisiblePosition(prev_position);
+ if (prev.DeepEquivalent() == position)
+ return VisiblePositionTemplate<Strategy>();
+
+ switch (rule) {
+ case kCanCrossEditingBoundary:
+ return prev;
+ case kCannotCrossEditingBoundary:
+ return AdjustBackwardPositionToAvoidCrossingEditingBoundaries(prev,
+ position);
+ case kCanSkipOverEditingBoundary:
+ return SkipToStartOfEditingBoundary(prev, position);
+ }
+
+ NOTREACHED();
+ return AdjustBackwardPositionToAvoidCrossingEditingBoundaries(prev, position);
+}
+
+VisiblePosition PreviousPositionOf(const VisiblePosition& visible_position,
+ EditingBoundaryCrossingRule rule) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ return PreviousPositionOfAlgorithm<EditingStrategy>(
+ visible_position.DeepEquivalent(), rule);
+}
+
+VisiblePositionInFlatTree PreviousPositionOf(
+ const VisiblePositionInFlatTree& visible_position,
+ EditingBoundaryCrossingRule rule) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ return PreviousPositionOfAlgorithm<EditingInFlatTreeStrategy>(
+ visible_position.DeepEquivalent(), rule);
+}
+
+template <typename Strategy>
+static EphemeralRangeTemplate<Strategy> MakeSearchRange(
+ const PositionTemplate<Strategy>& pos) {
+ Node* node = pos.ComputeContainerNode();
+ if (!node)
+ return EphemeralRangeTemplate<Strategy>();
+ Document& document = node->GetDocument();
+ if (!document.documentElement())
+ return EphemeralRangeTemplate<Strategy>();
+ Element* boundary = EnclosingBlockFlowElement(*node);
+ if (!boundary)
+ return EphemeralRangeTemplate<Strategy>();
+
+ return EphemeralRangeTemplate<Strategy>(
+ pos, PositionTemplate<Strategy>::LastPositionInNode(*boundary));
+}
+
+template <typename Strategy>
+static PositionTemplate<Strategy> SkipWhitespaceAlgorithm(
+ const PositionTemplate<Strategy>& position) {
+ const EphemeralRangeTemplate<Strategy>& search_range =
+ MakeSearchRange(position);
+ if (search_range.IsNull())
+ return position;
+
+ CharacterIteratorAlgorithm<Strategy> char_it(
+ search_range.StartPosition(), search_range.EndPosition(),
+ TextIteratorBehavior::Builder()
+ .SetEmitsCharactersBetweenAllVisiblePositions(true)
+ .Build());
+ PositionTemplate<Strategy> runner = position;
+ // TODO(editing-dev): We should consider U+20E3, COMBINING ENCLOSING KEYCAP.
+ // When whitespace character followed by U+20E3, we should not consider
+ // it as trailing white space.
+ for (; char_it.length(); char_it.Advance(1)) {
+ UChar c = char_it.CharacterAt(0);
+ if ((!IsSpaceOrNewline(c) && c != kNoBreakSpaceCharacter) || c == '\n')
+ return runner;
+ runner = char_it.EndPosition();
+ }
+ return runner;
+}
+
+Position SkipWhitespace(const Position& position) {
+ return SkipWhitespaceAlgorithm(position);
+}
+
+PositionInFlatTree SkipWhitespace(const PositionInFlatTree& position) {
+ return SkipWhitespaceAlgorithm(position);
+}
+
+template <typename Strategy>
+static Vector<FloatQuad> ComputeTextBounds(
+ const EphemeralRangeTemplate<Strategy>& range) {
+ const PositionTemplate<Strategy>& start_position = range.StartPosition();
+ const PositionTemplate<Strategy>& end_position = range.EndPosition();
+ Node* const start_container = start_position.ComputeContainerNode();
+ DCHECK(start_container);
+ Node* const end_container = end_position.ComputeContainerNode();
+ DCHECK(end_container);
+ DCHECK(!start_container->GetDocument().NeedsLayoutTreeUpdate());
+
+ Vector<FloatQuad> result;
+ for (const Node& node : range.Nodes()) {
+ LayoutObject* const layout_object = node.GetLayoutObject();
+ if (!layout_object || !layout_object->IsText())
+ continue;
+ const LayoutText* layout_text = ToLayoutText(layout_object);
+ unsigned start_offset =
+ node == start_container ? start_position.OffsetInContainerNode() : 0;
+ unsigned end_offset = node == end_container
+ ? end_position.OffsetInContainerNode()
+ : std::numeric_limits<unsigned>::max();
+ layout_text->AbsoluteQuadsForRange(result, start_offset, end_offset);
+ }
+ return result;
+}
+
+template <typename Strategy>
+static FloatRect ComputeTextRectTemplate(
+ const EphemeralRangeTemplate<Strategy>& range) {
+ FloatRect result;
+ for (auto rect : ComputeTextBounds<Strategy>(range))
+ result.Unite(rect.BoundingBox());
+ return result;
+}
+
+IntRect ComputeTextRect(const EphemeralRange& range) {
+ return EnclosingIntRect(ComputeTextRectTemplate(range));
+}
+
+IntRect ComputeTextRect(const EphemeralRangeInFlatTree& range) {
+ return EnclosingIntRect(ComputeTextRectTemplate(range));
+}
+
+FloatRect ComputeTextFloatRect(const EphemeralRange& range) {
+ return ComputeTextRectTemplate(range);
+}
+
+IntRect FirstRectForRange(const EphemeralRange& range) {
+ DCHECK(!range.GetDocument().NeedsLayoutTreeUpdate());
+ DocumentLifecycle::DisallowTransitionScope disallow_transition(
+ range.GetDocument().Lifecycle());
+
+ LayoutUnit extra_width_to_end_of_line;
+ DCHECK(range.IsNotNull());
+
+ const PositionWithAffinity start_position(
+ CreateVisiblePosition(range.StartPosition()).DeepEquivalent(),
+ TextAffinity::kDownstream);
+ const IntRect start_caret_rect =
+ AbsoluteCaretRectOfPosition(start_position, &extra_width_to_end_of_line);
+ if (start_caret_rect.IsEmpty())
+ return IntRect();
+
+ const PositionWithAffinity end_position(
+ CreateVisiblePosition(range.EndPosition()).DeepEquivalent(),
+ TextAffinity::kUpstream);
+ const IntRect end_caret_rect = AbsoluteCaretRectOfPosition(end_position);
+ if (end_caret_rect.IsEmpty())
+ return IntRect();
+
+ if (start_caret_rect.Y() == end_caret_rect.Y()) {
+ // start and end are on the same line
+ return IntRect(
+ std::min(start_caret_rect.X(), end_caret_rect.X()),
+ start_caret_rect.Y(), abs(end_caret_rect.X() - start_caret_rect.X()),
+ std::max(start_caret_rect.Height(), end_caret_rect.Height()));
+ }
+
+ // start and end aren't on the same line, so go from start to the end of its
+ // line
+ return IntRect(
+ start_caret_rect.X(), start_caret_rect.Y(),
+ (start_caret_rect.Width() + extra_width_to_end_of_line).ToInt(),
+ start_caret_rect.Height());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_units.h b/chromium/third_party/blink/renderer/core/editing/visible_units.h
new file mode 100644
index 00000000000..80e867db067
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_units.h
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2004 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_VISIBLE_UNITS_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_VISIBLE_UNITS_H_
+
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/editing/editing_boundary.h"
+#include "third_party/blink/renderer/core/editing/forward.h"
+#include "third_party/blink/renderer/platform/geometry/layout_rect.h"
+#include "third_party/blink/renderer/platform/text/text_direction.h"
+#include "third_party/blink/renderer/platform/wtf/text/icu/unicode_icu.h"
+
+namespace blink {
+
+class LayoutUnit;
+class LayoutObject;
+class Node;
+class IntPoint;
+class IntRect;
+class LocalFrame;
+
+// |EWordSiste| is used as a parameter of |StartOfWord()| and |EndOfWord()|
+// to control a returning position when they are called for a position before
+// word boundary.
+enum EWordSide {
+ kNextWordIfOnBoundary = false,
+ kPreviousWordIfOnBoundary = true
+};
+
+// offset functions on Node
+CORE_EXPORT int CaretMinOffset(const Node*);
+CORE_EXPORT int CaretMaxOffset(const Node*);
+
+// Position
+// mostForward/BackwardCaretPosition are used for moving back and forth between
+// visually equivalent candidates.
+// For example, for the text node "foo bar" where whitespace is
+// collapsible, there are two candidates that map to the VisiblePosition
+// between 'b' and the space, after first space and before last space.
+//
+// mostBackwardCaretPosition returns the left candidate and also returs
+// [boundary, 0] for any of the positions from [boundary, 0] to the first
+// candidate in boundary, where
+// endsOfNodeAreVisuallyDistinctPositions(boundary) is true.
+//
+// mostForwardCaretPosition() returns the right one and also returns the
+// last position in the last atomic node in boundary for all of the positions
+// in boundary after the last candidate, where
+// endsOfNodeAreVisuallyDistinctPositions(boundary).
+CORE_EXPORT Position MostBackwardCaretPosition(
+ const Position&,
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary);
+CORE_EXPORT PositionInFlatTree MostBackwardCaretPosition(
+ const PositionInFlatTree&,
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary);
+CORE_EXPORT Position MostForwardCaretPosition(
+ const Position&,
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary);
+CORE_EXPORT PositionInFlatTree MostForwardCaretPosition(
+ const PositionInFlatTree&,
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary);
+
+CORE_EXPORT bool IsVisuallyEquivalentCandidate(const Position&);
+CORE_EXPORT bool IsVisuallyEquivalentCandidate(const PositionInFlatTree&);
+
+// Whether or not [node, 0] and [node, lastOffsetForEditing(node)] are their own
+// VisiblePositions.
+// If true, adjacent candidates are visually distinct.
+CORE_EXPORT bool EndsOfNodeAreVisuallyDistinctPositions(const Node*);
+
+CORE_EXPORT Position CanonicalPositionOf(const Position&);
+CORE_EXPORT PositionInFlatTree CanonicalPositionOf(const PositionInFlatTree&);
+
+CORE_EXPORT UChar32 CharacterAfter(const VisiblePosition&);
+CORE_EXPORT UChar32 CharacterAfter(const VisiblePositionInFlatTree&);
+CORE_EXPORT UChar32 CharacterBefore(const VisiblePosition&);
+CORE_EXPORT UChar32 CharacterBefore(const VisiblePositionInFlatTree&);
+
+CORE_EXPORT VisiblePosition
+NextPositionOf(const VisiblePosition&,
+ EditingBoundaryCrossingRule = kCanCrossEditingBoundary);
+CORE_EXPORT VisiblePositionInFlatTree
+NextPositionOf(const VisiblePositionInFlatTree&,
+ EditingBoundaryCrossingRule = kCanCrossEditingBoundary);
+CORE_EXPORT VisiblePosition
+PreviousPositionOf(const VisiblePosition&,
+ EditingBoundaryCrossingRule = kCanCrossEditingBoundary);
+CORE_EXPORT VisiblePositionInFlatTree
+PreviousPositionOf(const VisiblePositionInFlatTree&,
+ EditingBoundaryCrossingRule = kCanCrossEditingBoundary);
+
+// words
+// TODO(yoichio): Replace |startOfWord| to |startOfWordPosition| because
+// returned Position should be canonicalized with |previousBoundary()| by
+// TextItetator.
+CORE_EXPORT Position StartOfWordPosition(const VisiblePosition&,
+ EWordSide = kNextWordIfOnBoundary);
+CORE_EXPORT VisiblePosition StartOfWord(const VisiblePosition&,
+ EWordSide = kNextWordIfOnBoundary);
+CORE_EXPORT PositionInFlatTree
+StartOfWordPosition(const VisiblePositionInFlatTree&,
+ EWordSide = kNextWordIfOnBoundary);
+CORE_EXPORT VisiblePositionInFlatTree
+StartOfWord(const VisiblePositionInFlatTree&,
+ EWordSide = kNextWordIfOnBoundary);
+// TODO(yoichio): Replace |endOfWord| to |endOfWordPosition| because returned
+// Position should be canonicalized with |nextBoundary()| by TextItetator.
+CORE_EXPORT Position EndOfWordPosition(const VisiblePosition&,
+ EWordSide = kNextWordIfOnBoundary);
+CORE_EXPORT VisiblePosition EndOfWord(const VisiblePosition&,
+ EWordSide = kNextWordIfOnBoundary);
+CORE_EXPORT PositionInFlatTree
+EndOfWordPosition(const VisiblePositionInFlatTree&,
+ EWordSide = kNextWordIfOnBoundary);
+CORE_EXPORT VisiblePositionInFlatTree
+EndOfWord(const VisiblePositionInFlatTree&, EWordSide = kNextWordIfOnBoundary);
+CORE_EXPORT VisiblePosition PreviousWordPosition(const VisiblePosition&);
+CORE_EXPORT VisiblePosition NextWordPosition(const VisiblePosition&);
+
+// sentences
+CORE_EXPORT VisiblePosition StartOfSentence(const VisiblePosition&);
+CORE_EXPORT VisiblePositionInFlatTree
+StartOfSentence(const VisiblePositionInFlatTree&);
+CORE_EXPORT VisiblePosition EndOfSentence(const VisiblePosition&);
+CORE_EXPORT VisiblePositionInFlatTree
+EndOfSentence(const VisiblePositionInFlatTree&);
+VisiblePosition PreviousSentencePosition(const VisiblePosition&);
+VisiblePosition NextSentencePosition(const VisiblePosition&);
+EphemeralRange ExpandEndToSentenceBoundary(const EphemeralRange&);
+EphemeralRange ExpandRangeToSentenceBoundary(const EphemeralRange&);
+
+// lines
+// TODO(yosin) Return values of |VisiblePosition| version of |startOfLine()|
+// with shadow tree isn't defined well. We should not use it for shadow tree.
+CORE_EXPORT VisiblePosition StartOfLine(const VisiblePosition&);
+CORE_EXPORT VisiblePositionInFlatTree
+StartOfLine(const VisiblePositionInFlatTree&);
+// TODO(yosin) Return values of |VisiblePosition| version of |endOfLine()| with
+// shadow tree isn't defined well. We should not use it for shadow tree.
+CORE_EXPORT VisiblePosition EndOfLine(const VisiblePosition&);
+CORE_EXPORT VisiblePositionInFlatTree
+EndOfLine(const VisiblePositionInFlatTree&);
+enum EditableType { kContentIsEditable, kHasEditableAXRole };
+CORE_EXPORT VisiblePosition
+PreviousLinePosition(const VisiblePosition&,
+ LayoutUnit line_direction_point,
+ EditableType = kContentIsEditable);
+CORE_EXPORT VisiblePosition NextLinePosition(const VisiblePosition&,
+ LayoutUnit line_direction_point,
+ EditableType = kContentIsEditable);
+CORE_EXPORT bool InSameLine(const VisiblePosition&, const VisiblePosition&);
+CORE_EXPORT bool InSameLine(const VisiblePositionInFlatTree&,
+ const VisiblePositionInFlatTree&);
+CORE_EXPORT bool InSameLine(const PositionWithAffinity&,
+ const PositionWithAffinity&);
+CORE_EXPORT bool InSameLine(const PositionInFlatTreeWithAffinity&,
+ const PositionInFlatTreeWithAffinity&);
+CORE_EXPORT bool IsStartOfLine(const VisiblePosition&);
+CORE_EXPORT bool IsStartOfLine(const VisiblePositionInFlatTree&);
+CORE_EXPORT bool IsEndOfLine(const VisiblePosition&);
+CORE_EXPORT bool IsEndOfLine(const VisiblePositionInFlatTree&);
+// TODO(yosin) Return values of |VisiblePosition| version of
+// |logicalStartOfLine()| with shadow tree isn't defined well. We should not use
+// it for shadow tree.
+CORE_EXPORT VisiblePosition LogicalStartOfLine(const VisiblePosition&);
+CORE_EXPORT VisiblePositionInFlatTree
+LogicalStartOfLine(const VisiblePositionInFlatTree&);
+// TODO(yosin) Return values of |VisiblePosition| version of
+// |logicalEndOfLine()| with shadow tree isn't defined well. We should not use
+// it for shadow tree.
+CORE_EXPORT VisiblePosition LogicalEndOfLine(const VisiblePosition&);
+CORE_EXPORT VisiblePositionInFlatTree
+LogicalEndOfLine(const VisiblePositionInFlatTree&);
+CORE_EXPORT bool IsLogicalEndOfLine(const VisiblePosition&);
+CORE_EXPORT bool IsLogicalEndOfLine(const VisiblePositionInFlatTree&);
+
+// paragraphs (perhaps a misnomer, can be divided by line break elements)
+// TODO(yosin) Since return value of |startOfParagraph()| with |VisiblePosition|
+// isn't defined well on flat tree, we should not use it for a position in
+// flat tree.
+CORE_EXPORT VisiblePosition
+StartOfParagraph(const VisiblePosition&,
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary);
+CORE_EXPORT VisiblePositionInFlatTree
+StartOfParagraph(const VisiblePositionInFlatTree&,
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary);
+CORE_EXPORT VisiblePosition
+EndOfParagraph(const VisiblePosition&,
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary);
+CORE_EXPORT VisiblePositionInFlatTree
+EndOfParagraph(const VisiblePositionInFlatTree&,
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary);
+VisiblePosition StartOfNextParagraph(const VisiblePosition&);
+CORE_EXPORT bool IsStartOfParagraph(
+ const VisiblePosition&,
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary);
+CORE_EXPORT bool IsStartOfParagraph(const VisiblePositionInFlatTree&);
+CORE_EXPORT bool IsEndOfParagraph(
+ const VisiblePosition&,
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary);
+CORE_EXPORT bool IsEndOfParagraph(const VisiblePositionInFlatTree&);
+bool InSameParagraph(const VisiblePosition&,
+ const VisiblePosition&,
+ EditingBoundaryCrossingRule = kCannotCrossEditingBoundary);
+EphemeralRange ExpandToParagraphBoundary(const EphemeralRange&);
+
+// document
+CORE_EXPORT VisiblePosition StartOfDocument(const VisiblePosition&);
+CORE_EXPORT VisiblePositionInFlatTree
+StartOfDocument(const VisiblePositionInFlatTree&);
+CORE_EXPORT VisiblePosition EndOfDocument(const VisiblePosition&);
+CORE_EXPORT VisiblePositionInFlatTree
+EndOfDocument(const VisiblePositionInFlatTree&);
+bool IsStartOfDocument(const VisiblePosition&);
+bool IsEndOfDocument(const VisiblePosition&);
+
+// editable content
+VisiblePosition StartOfEditableContent(const VisiblePosition&);
+VisiblePosition EndOfEditableContent(const VisiblePosition&);
+CORE_EXPORT bool IsEndOfEditableOrNonEditableContent(const VisiblePosition&);
+CORE_EXPORT bool IsEndOfEditableOrNonEditableContent(
+ const VisiblePositionInFlatTree&);
+
+bool HasRenderedNonAnonymousDescendantsWithHeight(const LayoutObject*);
+
+// Returns a hit-tested VisiblePosition for the given point in contents-space
+// coordinates.
+CORE_EXPORT VisiblePosition VisiblePositionForContentsPoint(const IntPoint&,
+ LocalFrame*);
+
+CORE_EXPORT bool RendersInDifferentPosition(const Position&, const Position&);
+
+CORE_EXPORT Position SkipWhitespace(const Position&);
+CORE_EXPORT PositionInFlatTree SkipWhitespace(const PositionInFlatTree&);
+
+CORE_EXPORT IntRect ComputeTextRect(const EphemeralRange&);
+IntRect ComputeTextRect(const EphemeralRangeInFlatTree&);
+FloatRect ComputeTextFloatRect(const EphemeralRange&);
+
+// |FirstRectForRange| requires up-to-date layout.
+IntRect FirstRectForRange(const EphemeralRange&);
+
+// Export below functions only for |VisibleUnit| family.
+enum BoundarySearchContextAvailability {
+ kDontHaveMoreContext,
+ kMayHaveMoreContext
+};
+
+typedef unsigned (*BoundarySearchFunction)(const UChar*,
+ unsigned length,
+ unsigned offset,
+ BoundarySearchContextAvailability,
+ bool& need_more_context);
+
+CORE_EXPORT Position NextBoundary(const VisiblePosition&,
+ BoundarySearchFunction);
+PositionInFlatTree NextBoundary(const VisiblePositionInFlatTree&,
+ BoundarySearchFunction);
+Position PreviousBoundary(const VisiblePosition&, BoundarySearchFunction);
+PositionInFlatTree PreviousBoundary(const VisiblePositionInFlatTree&,
+ BoundarySearchFunction);
+
+CORE_EXPORT PositionWithAffinity
+AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ const PositionWithAffinity&,
+ const Position&);
+
+PositionInFlatTreeWithAffinity
+AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ const PositionInFlatTreeWithAffinity&,
+ const PositionInFlatTree&);
+
+PositionWithAffinity AdjustBackwardPositionToAvoidCrossingEditingBoundaries(
+ const PositionWithAffinity&,
+ const Position&);
+
+PositionInFlatTreeWithAffinity
+AdjustBackwardPositionToAvoidCrossingEditingBoundaries(
+ const PositionInFlatTreeWithAffinity&,
+ const PositionInFlatTree&);
+
+VisiblePosition AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ const VisiblePosition&,
+ const Position&);
+
+VisiblePositionInFlatTree AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ const VisiblePositionInFlatTree&,
+ const PositionInFlatTree&);
+
+// Export below functions only for |SelectionModifier|.
+VisiblePosition AdjustBackwardPositionToAvoidCrossingEditingBoundaries(
+ const VisiblePosition&,
+ const Position&);
+
+VisiblePositionInFlatTree
+AdjustBackwardPositionToAvoidCrossingEditingBoundaries(
+ const VisiblePositionInFlatTree&,
+ const PositionInFlatTree&);
+
+Position NextRootInlineBoxCandidatePosition(Node*,
+ const VisiblePosition&,
+ EditableType);
+
+CORE_EXPORT Position
+PreviousRootInlineBoxCandidatePosition(Node*,
+ const VisiblePosition&,
+ EditableType);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_VISIBLE_UNITS_H_
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_units_line.cc b/chromium/third_party/blink/renderer/core/editing/visible_units_line.cc
new file mode 100644
index 00000000000..5518aad6df6
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_units_line.cc
@@ -0,0 +1,804 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+
+#include "third_party/blink/renderer/core/dom/ax_object_cache.h"
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/inline_box_position.h"
+#include "third_party/blink/renderer/core/editing/rendered_position.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/layout/api/line_layout_block_flow.h"
+#include "third_party/blink/renderer/core/layout/line/inline_text_box.h"
+#include "third_party/blink/renderer/core/layout/line/root_inline_box.h"
+
+namespace blink {
+
+namespace {
+
+bool HasEditableStyle(const Node& node, EditableType editable_type) {
+ if (editable_type == kHasEditableAXRole) {
+ if (AXObjectCache* cache = node.GetDocument().ExistingAXObjectCache()) {
+ if (cache->RootAXEditableElement(&node))
+ return true;
+ }
+ }
+
+ return HasEditableStyle(node);
+}
+
+Element* RootEditableElement(const Node& node, EditableType editable_type) {
+ if (editable_type == kHasEditableAXRole) {
+ if (AXObjectCache* cache = node.GetDocument().ExistingAXObjectCache())
+ return const_cast<Element*>(cache->RootAXEditableElement(&node));
+ }
+
+ return RootEditableElement(node);
+}
+
+Element* RootAXEditableElementOf(const Position& position) {
+ Node* node = position.ComputeContainerNode();
+ if (!node)
+ return nullptr;
+
+ if (IsDisplayInsideTable(node))
+ node = node->parentNode();
+
+ return RootEditableElement(*node, kHasEditableAXRole);
+}
+
+bool HasAXEditableStyle(const Node& node) {
+ return HasEditableStyle(node, kHasEditableAXRole);
+}
+
+ContainerNode* HighestEditableRoot(const Position& position,
+ EditableType editable_type) {
+ if (editable_type == kHasEditableAXRole) {
+ return HighestEditableRoot(position, RootAXEditableElementOf,
+ HasAXEditableStyle);
+ }
+
+ return HighestEditableRoot(position);
+}
+
+ContainerNode* HighestEditableRootOfNode(const Node& node,
+ EditableType editable_type) {
+ return HighestEditableRoot(FirstPositionInOrBeforeNode(node), editable_type);
+}
+
+Node* PreviousNodeConsideringAtomicNodes(const Node& start) {
+ if (start.previousSibling()) {
+ Node* node = start.previousSibling();
+ while (!IsAtomicNode(node) && node->lastChild())
+ node = node->lastChild();
+ return node;
+ }
+ return start.parentNode();
+}
+
+Node* NextNodeConsideringAtomicNodes(const Node& start) {
+ if (!IsAtomicNode(&start) && start.hasChildren())
+ return start.firstChild();
+ if (start.nextSibling())
+ return start.nextSibling();
+ const Node* node = &start;
+ while (node && !node->nextSibling())
+ node = node->parentNode();
+ if (node)
+ return node->nextSibling();
+ return nullptr;
+}
+
+// Returns the previous leaf node or nullptr if there are no more. Delivers leaf
+// nodes as if the whole DOM tree were a linear chain of its leaf nodes.
+Node* PreviousAtomicLeafNode(const Node& start) {
+ Node* node = PreviousNodeConsideringAtomicNodes(start);
+ while (node) {
+ if (IsAtomicNode(node))
+ return node;
+ node = PreviousNodeConsideringAtomicNodes(*node);
+ }
+ return nullptr;
+}
+
+// Returns the next leaf node or nullptr if there are no more. Delivers leaf
+// nodes as if the whole DOM tree were a linear chain of its leaf nodes.
+Node* NextAtomicLeafNode(const Node& start) {
+ Node* node = NextNodeConsideringAtomicNodes(start);
+ while (node) {
+ if (IsAtomicNode(node))
+ return node;
+ node = NextNodeConsideringAtomicNodes(*node);
+ }
+ return nullptr;
+}
+
+Node* PreviousLeafWithSameEditability(const Node& node,
+ EditableType editable_type) {
+ const bool editable = HasEditableStyle(node, editable_type);
+ for (Node* runner = PreviousAtomicLeafNode(node); runner;
+ runner = PreviousAtomicLeafNode(*runner)) {
+ if (editable == HasEditableStyle(*runner, editable_type))
+ return runner;
+ }
+ return nullptr;
+}
+
+Node* NextLeafWithSameEditability(Node* node, EditableType editable_type) {
+ if (!node)
+ return nullptr;
+
+ const bool editable = HasEditableStyle(*node, editable_type);
+ for (Node* runner = NextAtomicLeafNode(*node); runner;
+ runner = NextAtomicLeafNode(*runner)) {
+ if (editable == HasEditableStyle(*runner, editable_type))
+ return runner;
+ }
+ return nullptr;
+}
+
+template <typename Strategy, typename Ordering>
+PositionWithAffinityTemplate<Strategy> StartPositionForLine(
+ const PositionWithAffinityTemplate<Strategy>& c) {
+ if (c.IsNull())
+ return PositionWithAffinityTemplate<Strategy>();
+
+ const RootInlineBox* root_box =
+ RenderedPosition(c.GetPosition(), c.Affinity()).RootBox();
+ if (!root_box) {
+ // There are VisiblePositions at offset 0 in blocks without
+ // RootInlineBoxes, like empty editable blocks and bordered blocks.
+ PositionTemplate<Strategy> p = c.GetPosition();
+ if (p.AnchorNode()->GetLayoutObject() &&
+ p.AnchorNode()->GetLayoutObject()->IsLayoutBlock() &&
+ !p.ComputeEditingOffset())
+ return c;
+
+ return PositionWithAffinityTemplate<Strategy>();
+ }
+
+ const auto& node_and_box = Ordering::StartNodeAndBoxOf(*root_box);
+ Node* const start_node = std::get<Node*>(node_and_box);
+ InlineBox* const start_box = std::get<InlineBox*>(node_and_box);
+ if (!start_node)
+ return PositionWithAffinityTemplate<Strategy>();
+ return PositionWithAffinityTemplate<Strategy>(
+ start_node->IsTextNode()
+ ? PositionTemplate<Strategy>(ToText(start_node),
+ ToInlineTextBox(start_box)->Start())
+ : PositionTemplate<Strategy>::BeforeNode(*start_node));
+}
+
+// Provides start and end of line in logical order for implementing Home and End
+// keys.
+struct LogicalOrdering {
+ static std::pair<Node*, InlineBox*> StartNodeAndBoxOf(
+ const RootInlineBox& root_box) {
+ InlineBox* start_box;
+ Node* const start_node = root_box.GetLogicalStartBoxWithNode(start_box);
+ if (!start_node)
+ return {nullptr, nullptr};
+ return {start_node, start_box};
+ }
+
+ static std::pair<Node*, InlineBox*> EndNodeAndBoxOf(
+ const RootInlineBox& root_box) {
+ InlineBox* end_box;
+ Node* const end_node = root_box.GetLogicalEndBoxWithNode(end_box);
+ if (!end_node)
+ return {nullptr, nullptr};
+ return {end_node, end_box};
+ }
+};
+
+// Provides start end end of line in visual order for implementing expanding
+// selection in line granularity.
+struct VisualOrdering {
+ static std::pair<Node*, InlineBox*> StartNodeAndBoxOf(
+ const RootInlineBox& root_box) {
+ // Generated content (e.g. list markers and CSS :before and :after
+ // pseudoelements) have no corresponding DOM element, and so cannot be
+ // represented by a VisiblePosition. Use whatever follows instead.
+ // TODO(editing-dev): We should consider text-direction of line to
+ // find non-pseudo node.
+ for (InlineBox* inline_box = root_box.FirstLeafChild(); inline_box;
+ inline_box = inline_box->NextLeafChild()) {
+ if (inline_box->GetLineLayoutItem().NonPseudoNode())
+ return {inline_box->GetLineLayoutItem().NonPseudoNode(), inline_box};
+ }
+ return {nullptr, nullptr};
+ }
+
+ static std::pair<Node*, InlineBox*> EndNodeAndBoxOf(
+ const RootInlineBox& root_box) {
+ // Generated content (e.g. list markers and CSS :before and :after
+ // pseudo elements) have no corresponding DOM element, and so cannot be
+ // represented by a VisiblePosition. Use whatever precedes instead.
+ // TODO(editing-dev): We should consider text-direction of line to
+ // find non-pseudo node.
+ for (InlineBox* inline_box = root_box.LastLeafChild(); inline_box;
+ inline_box = inline_box->PrevLeafChild()) {
+ if (inline_box->GetLineLayoutItem().NonPseudoNode())
+ return {inline_box->GetLineLayoutItem().NonPseudoNode(), inline_box};
+ }
+ return {nullptr, nullptr};
+ }
+};
+
+template <typename Strategy>
+PositionWithAffinityTemplate<Strategy> StartOfLineAlgorithm(
+ const PositionWithAffinityTemplate<Strategy>& c) {
+ // TODO: this is the current behavior that might need to be fixed.
+ // Please refer to https://bugs.webkit.org/show_bug.cgi?id=49107 for detail.
+ PositionWithAffinityTemplate<Strategy> vis_pos =
+ StartPositionForLine<Strategy, VisualOrdering>(c);
+ return AdjustBackwardPositionToAvoidCrossingEditingBoundaries(
+ vis_pos, c.GetPosition());
+}
+
+PositionWithAffinity StartOfLine(const PositionWithAffinity& current_position) {
+ return StartOfLineAlgorithm<EditingStrategy>(current_position);
+}
+
+PositionInFlatTreeWithAffinity StartOfLine(
+ const PositionInFlatTreeWithAffinity& current_position) {
+ return StartOfLineAlgorithm<EditingInFlatTreeStrategy>(current_position);
+}
+
+LayoutPoint AbsoluteLineDirectionPointToLocalPointInBlock(
+ const RootInlineBox* root,
+ LayoutUnit line_direction_point) {
+ DCHECK(root);
+ LineLayoutBlockFlow containing_block = root->Block();
+ FloatPoint absolute_block_point =
+ containing_block.LocalToAbsolute(FloatPoint());
+ if (containing_block.HasOverflowClip())
+ absolute_block_point -= FloatSize(containing_block.ScrolledContentOffset());
+
+ if (root->Block().IsHorizontalWritingMode()) {
+ return LayoutPoint(
+ LayoutUnit(line_direction_point - absolute_block_point.X()),
+ root->BlockDirectionPointInLine());
+ }
+
+ return LayoutPoint(
+ root->BlockDirectionPointInLine(),
+ LayoutUnit(line_direction_point - absolute_block_point.Y()));
+}
+
+bool InSameLine(const Node& node, const VisiblePosition& visible_position) {
+ if (!node.GetLayoutObject())
+ return true;
+ return InSameLine(CreateVisiblePosition(FirstPositionInOrBeforeNode(node)),
+ visible_position);
+}
+
+Node* FindNodeInPreviousLine(const Node& start_node,
+ const VisiblePosition& visible_position,
+ EditableType editable_type) {
+ for (Node* runner =
+ PreviousLeafWithSameEditability(start_node, editable_type);
+ runner;
+ runner = PreviousLeafWithSameEditability(*runner, editable_type)) {
+ if (!InSameLine(*runner, visible_position))
+ return runner;
+ }
+ return nullptr;
+}
+
+} // namespace
+
+// FIXME: consolidate with code in previousLinePosition.
+Position PreviousRootInlineBoxCandidatePosition(
+ Node* node,
+ const VisiblePosition& visible_position,
+ EditableType editable_type) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ ContainerNode* highest_root =
+ HighestEditableRoot(visible_position.DeepEquivalent(), editable_type);
+ Node* const previous_node =
+ FindNodeInPreviousLine(*node, visible_position, editable_type);
+ for (Node* runner = previous_node; runner && !runner->IsShadowRoot();
+ runner = PreviousLeafWithSameEditability(*runner, editable_type)) {
+ if (HighestEditableRootOfNode(*runner, editable_type) != highest_root)
+ break;
+
+ const Position& candidate =
+ IsHTMLBRElement(*runner)
+ ? Position::BeforeNode(*runner)
+ : Position::EditingPositionOf(runner, CaretMaxOffset(runner));
+ if (IsVisuallyEquivalentCandidate(candidate))
+ return candidate;
+ }
+ return Position();
+}
+
+Position NextRootInlineBoxCandidatePosition(
+ Node* node,
+ const VisiblePosition& visible_position,
+ EditableType editable_type) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ ContainerNode* highest_root =
+ HighestEditableRoot(visible_position.DeepEquivalent(), editable_type);
+ Node* next_node = NextLeafWithSameEditability(node, editable_type);
+ while (next_node && InSameLine(*next_node, visible_position))
+ next_node = NextLeafWithSameEditability(next_node, kContentIsEditable);
+
+ for (Node* runner = next_node; runner && !runner->IsShadowRoot();
+ runner = NextLeafWithSameEditability(runner, editable_type)) {
+ if (HighestEditableRootOfNode(*runner, editable_type) != highest_root)
+ break;
+
+ const Position& candidate =
+ Position::EditingPositionOf(runner, CaretMinOffset(runner));
+ if (IsVisuallyEquivalentCandidate(candidate))
+ return candidate;
+ }
+ return Position();
+}
+
+// FIXME: Rename this function to reflect the fact it ignores bidi levels.
+VisiblePosition StartOfLine(const VisiblePosition& current_position) {
+ DCHECK(current_position.IsValid()) << current_position;
+ return CreateVisiblePosition(
+ StartOfLine(current_position.ToPositionWithAffinity()));
+}
+
+VisiblePositionInFlatTree StartOfLine(
+ const VisiblePositionInFlatTree& current_position) {
+ DCHECK(current_position.IsValid()) << current_position;
+ return CreateVisiblePosition(
+ StartOfLine(current_position.ToPositionWithAffinity()));
+}
+
+template <typename Strategy>
+static PositionWithAffinityTemplate<Strategy> LogicalStartOfLineAlgorithm(
+ const PositionWithAffinityTemplate<Strategy>& c) {
+ // TODO: this is the current behavior that might need to be fixed.
+ // Please refer to https://bugs.webkit.org/show_bug.cgi?id=49107 for detail.
+ PositionWithAffinityTemplate<Strategy> vis_pos =
+ StartPositionForLine<Strategy, LogicalOrdering>(c);
+
+ if (ContainerNode* editable_root = HighestEditableRoot(c.GetPosition())) {
+ if (!editable_root->contains(
+ vis_pos.GetPosition().ComputeContainerNode())) {
+ return PositionWithAffinityTemplate<Strategy>(
+ PositionTemplate<Strategy>::FirstPositionInNode(*editable_root));
+ }
+ }
+
+ return AdjustBackwardPositionToAvoidCrossingEditingBoundaries(
+ vis_pos, c.GetPosition());
+}
+
+static PositionWithAffinity LogicalStartOfLine(
+ const PositionWithAffinity& position) {
+ return LogicalStartOfLineAlgorithm<EditingStrategy>(position);
+}
+
+static PositionInFlatTreeWithAffinity LogicalStartOfLine(
+ const PositionInFlatTreeWithAffinity& position) {
+ return LogicalStartOfLineAlgorithm<EditingInFlatTreeStrategy>(position);
+}
+
+VisiblePosition LogicalStartOfLine(const VisiblePosition& current_position) {
+ DCHECK(current_position.IsValid()) << current_position;
+ return CreateVisiblePosition(
+ LogicalStartOfLine(current_position.ToPositionWithAffinity()));
+}
+
+VisiblePositionInFlatTree LogicalStartOfLine(
+ const VisiblePositionInFlatTree& current_position) {
+ DCHECK(current_position.IsValid()) << current_position;
+ return CreateVisiblePosition(
+ LogicalStartOfLine(current_position.ToPositionWithAffinity()));
+}
+
+template <typename Strategy, typename Ordering>
+static PositionWithAffinityTemplate<Strategy> EndPositionForLine(
+ const PositionWithAffinityTemplate<Strategy>& c) {
+ if (c.IsNull())
+ return PositionWithAffinityTemplate<Strategy>();
+
+ const RootInlineBox* root_box =
+ RenderedPosition(c.GetPosition(), c.Affinity()).RootBox();
+ if (!root_box) {
+ // There are VisiblePositions at offset 0 in blocks without
+ // RootInlineBoxes, like empty editable blocks and bordered blocks.
+ const PositionTemplate<Strategy> p = c.GetPosition();
+ if (p.AnchorNode()->GetLayoutObject() &&
+ p.AnchorNode()->GetLayoutObject()->IsLayoutBlock() &&
+ !p.ComputeEditingOffset())
+ return c;
+ return PositionWithAffinityTemplate<Strategy>();
+ }
+
+ const auto& node_and_box = Ordering::EndNodeAndBoxOf(*root_box);
+ Node* const end_node = std::get<Node*>(node_and_box);
+ InlineBox* const end_box = std::get<InlineBox*>(node_and_box);
+ if (!end_node)
+ return PositionWithAffinityTemplate<Strategy>();
+
+ if (IsHTMLBRElement(*end_node)) {
+ return PositionWithAffinityTemplate<Strategy>(
+ PositionTemplate<Strategy>::BeforeNode(*end_node),
+ TextAffinity::kUpstreamIfPossible);
+ }
+ if (end_box->IsInlineTextBox() && end_node->IsTextNode()) {
+ InlineTextBox* end_text_box = ToInlineTextBox(end_box);
+ int end_offset = end_text_box->Start();
+ if (!end_text_box->IsLineBreak())
+ end_offset += end_text_box->Len();
+ return PositionWithAffinityTemplate<Strategy>(
+ PositionTemplate<Strategy>(ToText(end_node), end_offset),
+ TextAffinity::kUpstreamIfPossible);
+ }
+ return PositionWithAffinityTemplate<Strategy>(
+ PositionTemplate<Strategy>::AfterNode(*end_node),
+ TextAffinity::kUpstreamIfPossible);
+}
+
+// TODO(yosin) Rename this function to reflect the fact it ignores bidi levels.
+template <typename Strategy>
+static PositionWithAffinityTemplate<Strategy> EndOfLineAlgorithm(
+ const PositionWithAffinityTemplate<Strategy>& current_position) {
+ // TODO(yosin) this is the current behavior that might need to be fixed.
+ // Please refer to https://bugs.webkit.org/show_bug.cgi?id=49107 for detail.
+ const PositionWithAffinityTemplate<Strategy>& candidate_position =
+ EndPositionForLine<Strategy, VisualOrdering>(current_position);
+
+ // Make sure the end of line is at the same line as the given input
+ // position. Else use the previous position to obtain end of line. This
+ // condition happens when the input position is before the space character
+ // at the end of a soft-wrapped non-editable line. In this scenario,
+ // |endPositionForLine()| would incorrectly hand back a position in the next
+ // line instead. This fix is to account for the discrepancy between lines
+ // with "webkit-line-break:after-white-space" style versus lines without
+ // that style, which would break before a space by default.
+ if (InSameLine(current_position, candidate_position)) {
+ return AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ candidate_position, current_position.GetPosition());
+ }
+ const PositionWithAffinityTemplate<Strategy>& adjusted_position =
+ PreviousPositionOf(CreateVisiblePosition(current_position))
+ .ToPositionWithAffinity();
+ if (adjusted_position.IsNull())
+ return PositionWithAffinityTemplate<Strategy>();
+ return AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ EndPositionForLine<Strategy, VisualOrdering>(adjusted_position),
+ current_position.GetPosition());
+}
+
+static PositionWithAffinity EndOfLine(const PositionWithAffinity& position) {
+ return EndOfLineAlgorithm<EditingStrategy>(position);
+}
+
+static PositionInFlatTreeWithAffinity EndOfLine(
+ const PositionInFlatTreeWithAffinity& position) {
+ return EndOfLineAlgorithm<EditingInFlatTreeStrategy>(position);
+}
+
+// TODO(yosin) Rename this function to reflect the fact it ignores bidi levels.
+VisiblePosition EndOfLine(const VisiblePosition& current_position) {
+ DCHECK(current_position.IsValid()) << current_position;
+ return CreateVisiblePosition(
+ EndOfLine(current_position.ToPositionWithAffinity()));
+}
+
+VisiblePositionInFlatTree EndOfLine(
+ const VisiblePositionInFlatTree& current_position) {
+ DCHECK(current_position.IsValid()) << current_position;
+ return CreateVisiblePosition(
+ EndOfLine(current_position.ToPositionWithAffinity()));
+}
+
+template <typename Strategy>
+static bool InSameLogicalLine(
+ const PositionWithAffinityTemplate<Strategy>& position1,
+ const PositionWithAffinityTemplate<Strategy>& position2) {
+ return position1.IsNotNull() &&
+ LogicalStartOfLine(position1).GetPosition() ==
+ LogicalStartOfLine(position2).GetPosition();
+}
+
+template <typename Strategy>
+static PositionWithAffinityTemplate<Strategy> LogicalEndOfLineAlgorithm(
+ const PositionWithAffinityTemplate<Strategy>& current_position) {
+ // TODO(yosin) this is the current behavior that might need to be fixed.
+ // Please refer to https://bugs.webkit.org/show_bug.cgi?id=49107 for detail.
+ PositionWithAffinityTemplate<Strategy> vis_pos =
+ EndPositionForLine<Strategy, LogicalOrdering>(current_position);
+
+ // Make sure the end of line is at the same line as the given input
+ // position. For a wrapping line, the logical end position for the
+ // not-last-2-lines might incorrectly hand back the logical beginning of the
+ // next line. For example,
+ // <div contenteditable dir="rtl" style="line-break:before-white-space">xyz
+ // a xyz xyz xyz xyz xyz xyz xyz xyz xyz xyz </div>
+ // In this case, use the previous position of the computed logical end
+ // position.
+ if (!InSameLogicalLine(current_position, vis_pos)) {
+ vis_pos = PreviousPositionOf(CreateVisiblePosition(vis_pos))
+ .ToPositionWithAffinity();
+ }
+
+ if (ContainerNode* editable_root =
+ HighestEditableRoot(current_position.GetPosition())) {
+ if (!editable_root->contains(
+ vis_pos.GetPosition().ComputeContainerNode())) {
+ return PositionWithAffinityTemplate<Strategy>(
+ PositionTemplate<Strategy>::LastPositionInNode(*editable_root));
+ }
+ }
+
+ return AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ vis_pos, current_position.GetPosition());
+}
+
+static PositionWithAffinity LogicalEndOfLine(
+ const PositionWithAffinity& position) {
+ return LogicalEndOfLineAlgorithm<EditingStrategy>(position);
+}
+
+static PositionInFlatTreeWithAffinity LogicalEndOfLine(
+ const PositionInFlatTreeWithAffinity& position) {
+ return LogicalEndOfLineAlgorithm<EditingInFlatTreeStrategy>(position);
+}
+
+VisiblePosition LogicalEndOfLine(const VisiblePosition& current_position) {
+ DCHECK(current_position.IsValid()) << current_position;
+ return CreateVisiblePosition(
+ LogicalEndOfLine(current_position.ToPositionWithAffinity()));
+}
+
+VisiblePositionInFlatTree LogicalEndOfLine(
+ const VisiblePositionInFlatTree& current_position) {
+ DCHECK(current_position.IsValid()) << current_position;
+ return CreateVisiblePosition(
+ LogicalEndOfLine(current_position.ToPositionWithAffinity()));
+}
+
+template <typename Strategy>
+static bool InSameLineAlgorithm(
+ const PositionWithAffinityTemplate<Strategy>& position1,
+ const PositionWithAffinityTemplate<Strategy>& position2) {
+ if (position1.IsNull() || position2.IsNull())
+ return false;
+ DCHECK_EQ(position1.GetDocument(), position2.GetDocument());
+ DCHECK(!position1.GetDocument()->NeedsLayoutTreeUpdate());
+
+ PositionWithAffinityTemplate<Strategy> start_of_line1 =
+ StartOfLine(position1);
+ PositionWithAffinityTemplate<Strategy> start_of_line2 =
+ StartOfLine(position2);
+ if (start_of_line1 == start_of_line2)
+ return true;
+ PositionTemplate<Strategy> canonicalized1 =
+ CanonicalPositionOf(start_of_line1.GetPosition());
+ if (canonicalized1 == start_of_line2.GetPosition())
+ return true;
+ return canonicalized1 == CanonicalPositionOf(start_of_line2.GetPosition());
+}
+
+bool InSameLine(const PositionWithAffinity& a, const PositionWithAffinity& b) {
+ return InSameLineAlgorithm<EditingStrategy>(a, b);
+}
+
+bool InSameLine(const PositionInFlatTreeWithAffinity& position1,
+ const PositionInFlatTreeWithAffinity& position2) {
+ return InSameLineAlgorithm<EditingInFlatTreeStrategy>(position1, position2);
+}
+
+bool InSameLine(const VisiblePosition& position1,
+ const VisiblePosition& position2) {
+ DCHECK(position1.IsValid()) << position1;
+ DCHECK(position2.IsValid()) << position2;
+ return InSameLine(position1.ToPositionWithAffinity(),
+ position2.ToPositionWithAffinity());
+}
+
+bool InSameLine(const VisiblePositionInFlatTree& position1,
+ const VisiblePositionInFlatTree& position2) {
+ DCHECK(position1.IsValid()) << position1;
+ DCHECK(position2.IsValid()) << position2;
+ return InSameLine(position1.ToPositionWithAffinity(),
+ position2.ToPositionWithAffinity());
+}
+
+template <typename Strategy>
+static bool IsStartOfLineAlgorithm(const VisiblePositionTemplate<Strategy>& p) {
+ DCHECK(p.IsValid()) << p;
+ return p.IsNotNull() && p.DeepEquivalent() == StartOfLine(p).DeepEquivalent();
+}
+
+bool IsStartOfLine(const VisiblePosition& p) {
+ return IsStartOfLineAlgorithm<EditingStrategy>(p);
+}
+
+bool IsStartOfLine(const VisiblePositionInFlatTree& p) {
+ return IsStartOfLineAlgorithm<EditingInFlatTreeStrategy>(p);
+}
+
+template <typename Strategy>
+static bool IsEndOfLineAlgorithm(const VisiblePositionTemplate<Strategy>& p) {
+ DCHECK(p.IsValid()) << p;
+ return p.IsNotNull() && p.DeepEquivalent() == EndOfLine(p).DeepEquivalent();
+}
+
+bool IsEndOfLine(const VisiblePosition& p) {
+ return IsEndOfLineAlgorithm<EditingStrategy>(p);
+}
+
+bool IsEndOfLine(const VisiblePositionInFlatTree& p) {
+ return IsEndOfLineAlgorithm<EditingInFlatTreeStrategy>(p);
+}
+
+template <typename Strategy>
+static bool IsLogicalEndOfLineAlgorithm(
+ const VisiblePositionTemplate<Strategy>& p) {
+ DCHECK(p.IsValid()) << p;
+ return p.IsNotNull() &&
+ p.DeepEquivalent() == LogicalEndOfLine(p).DeepEquivalent();
+}
+
+bool IsLogicalEndOfLine(const VisiblePosition& p) {
+ return IsLogicalEndOfLineAlgorithm<EditingStrategy>(p);
+}
+
+bool IsLogicalEndOfLine(const VisiblePositionInFlatTree& p) {
+ return IsLogicalEndOfLineAlgorithm<EditingInFlatTreeStrategy>(p);
+}
+
+VisiblePosition PreviousLinePosition(const VisiblePosition& visible_position,
+ LayoutUnit line_direction_point,
+ EditableType editable_type) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+
+ Position p = visible_position.DeepEquivalent();
+ Node* node = p.AnchorNode();
+
+ if (!node)
+ return VisiblePosition();
+
+ LayoutObject* layout_object = node->GetLayoutObject();
+ if (!layout_object)
+ return VisiblePosition();
+
+ const RootInlineBox* root = nullptr;
+ const InlineBox* box = ComputeInlineBoxPosition(visible_position).inline_box;
+ if (box) {
+ root = box->Root().PrevRootBox();
+ // We want to skip zero height boxes.
+ // This could happen in case it is a TrailingFloatsRootInlineBox.
+ if (!root || !root->LogicalHeight() || !root->FirstLeafChild())
+ root = nullptr;
+ }
+
+ if (!root) {
+ Position position = PreviousRootInlineBoxCandidatePosition(
+ node, visible_position, editable_type);
+ if (position.IsNotNull()) {
+ RenderedPosition rendered_position((CreateVisiblePosition(position)));
+ root = rendered_position.RootBox();
+ if (!root)
+ return CreateVisiblePosition(position);
+ }
+ }
+
+ if (root) {
+ // FIXME: Can be wrong for multi-column layout and with transforms.
+ LayoutPoint point_in_line = AbsoluteLineDirectionPointToLocalPointInBlock(
+ root, line_direction_point);
+ LineLayoutItem line_layout_item =
+ root->ClosestLeafChildForPoint(point_in_line, IsEditablePosition(p))
+ ->GetLineLayoutItem();
+ Node* node = line_layout_item.GetNode();
+ if (node && EditingIgnoresContent(*node))
+ return VisiblePosition::InParentBeforeNode(*node);
+ return CreateVisiblePosition(
+ line_layout_item.PositionForPoint(point_in_line));
+ }
+
+ // Could not find a previous line. This means we must already be on the first
+ // line. Move to the start of the content in this block, which effectively
+ // moves us to the start of the line we're on.
+ Element* root_element = HasEditableStyle(*node, editable_type)
+ ? RootEditableElement(*node, editable_type)
+ : node->GetDocument().documentElement();
+ if (!root_element)
+ return VisiblePosition();
+ return VisiblePosition::FirstPositionInNode(*root_element);
+}
+
+VisiblePosition NextLinePosition(const VisiblePosition& visible_position,
+ LayoutUnit line_direction_point,
+ EditableType editable_type) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+
+ Position p = visible_position.DeepEquivalent();
+ Node* node = p.AnchorNode();
+
+ if (!node)
+ return VisiblePosition();
+
+ LayoutObject* layout_object = node->GetLayoutObject();
+ if (!layout_object)
+ return VisiblePosition();
+
+ const RootInlineBox* root = nullptr;
+ const InlineBox* box = ComputeInlineBoxPosition(visible_position).inline_box;
+ if (box) {
+ root = box->Root().NextRootBox();
+ // We want to skip zero height boxes.
+ // This could happen in case it is a TrailingFloatsRootInlineBox.
+ if (!root || !root->LogicalHeight() || !root->FirstLeafChild())
+ root = nullptr;
+ }
+
+ if (!root) {
+ // FIXME: We need do the same in previousLinePosition.
+ Node* child = NodeTraversal::ChildAt(*node, p.ComputeEditingOffset());
+ node = child ? child : &NodeTraversal::LastWithinOrSelf(*node);
+ Position position = NextRootInlineBoxCandidatePosition(
+ node, visible_position, editable_type);
+ if (position.IsNotNull()) {
+ RenderedPosition rendered_position((CreateVisiblePosition(position)));
+ root = rendered_position.RootBox();
+ if (!root)
+ return CreateVisiblePosition(position);
+ }
+ }
+
+ if (root) {
+ // FIXME: Can be wrong for multi-column layout and with transforms.
+ LayoutPoint point_in_line = AbsoluteLineDirectionPointToLocalPointInBlock(
+ root, line_direction_point);
+ LineLayoutItem line_layout_item =
+ root->ClosestLeafChildForPoint(point_in_line, IsEditablePosition(p))
+ ->GetLineLayoutItem();
+ Node* node = line_layout_item.GetNode();
+ if (node && EditingIgnoresContent(*node))
+ return VisiblePosition::InParentBeforeNode(*node);
+ return CreateVisiblePosition(
+ line_layout_item.PositionForPoint(point_in_line));
+ }
+
+ // Could not find a next line. This means we must already be on the last line.
+ // Move to the end of the content in this block, which effectively moves us
+ // to the end of the line we're on.
+ Element* root_element = HasEditableStyle(*node, editable_type)
+ ? RootEditableElement(*node, editable_type)
+ : node->GetDocument().documentElement();
+ if (!root_element)
+ return VisiblePosition();
+ return VisiblePosition::LastPositionInNode(*root_element);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_units_line_test.cc b/chromium/third_party/blink/renderer/core/editing/visible_units_line_test.cc
new file mode 100644
index 00000000000..9f004c6454d
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_units_line_test.cc
@@ -0,0 +1,626 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+#include "third_party/blink/renderer/core/layout/layout_text_fragment.h"
+
+namespace blink {
+
+class VisibleUnitsLineTest : public EditingTestBase {
+ protected:
+ static PositionWithAffinity PositionWithAffinityInDOMTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return PositionWithAffinity(CanonicalPositionOf(Position(&anchor, offset)),
+ affinity);
+ }
+
+ static VisiblePosition CreateVisiblePositionInDOMTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return CreateVisiblePosition(Position(&anchor, offset), affinity);
+ }
+
+ static PositionInFlatTreeWithAffinity PositionWithAffinityInFlatTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return PositionInFlatTreeWithAffinity(
+ CanonicalPositionOf(PositionInFlatTree(&anchor, offset)), affinity);
+ }
+
+ static VisiblePositionInFlatTree CreateVisiblePositionInFlatTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return CreateVisiblePosition(PositionInFlatTree(&anchor, offset), affinity);
+ }
+};
+
+TEST_F(VisibleUnitsLineTest, endOfLine) {
+ const char* body_content =
+ "<a id=host><b id=one>11</b><b id=two>22</b></a><i id=three>333</i><i "
+ "id=four>4444</i><br>";
+ const char* shadow_content =
+ "<div><u id=five>55555</u><content select=#two></content><br><u "
+ "id=six>666666</u><br><content select=#one></content><u "
+ "id=seven>7777777</u></div>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* three = GetDocument().getElementById("three")->firstChild();
+ Node* four = GetDocument().getElementById("four")->firstChild();
+ Node* five = shadow_root->getElementById("five")->firstChild();
+ Node* six = shadow_root->getElementById("six")->firstChild();
+ Node* seven = shadow_root->getElementById("seven")->firstChild();
+
+ EXPECT_EQ(
+ Position(seven, 7),
+ EndOfLine(CreateVisiblePositionInDOMTree(*one, 0)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(seven, 7),
+ EndOfLine(CreateVisiblePositionInFlatTree(*one, 0)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(seven, 7),
+ EndOfLine(CreateVisiblePositionInDOMTree(*one, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(seven, 7),
+ EndOfLine(CreateVisiblePositionInFlatTree(*one, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(seven, 7),
+ EndOfLine(CreateVisiblePositionInDOMTree(*two, 0)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(two, 2),
+ EndOfLine(CreateVisiblePositionInFlatTree(*two, 0)).DeepEquivalent());
+
+ // TODO(yosin) endOfLine(two, 1) -> (five, 5) is a broken result. We keep
+ // it as a marker for future change.
+ EXPECT_EQ(
+ Position(five, 5),
+ EndOfLine(CreateVisiblePositionInDOMTree(*two, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(two, 2),
+ EndOfLine(CreateVisiblePositionInFlatTree(*two, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(five, 5),
+ EndOfLine(CreateVisiblePositionInDOMTree(*three, 0)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(four, 4),
+ EndOfLine(CreateVisiblePositionInFlatTree(*three, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(four, 4),
+ EndOfLine(CreateVisiblePositionInDOMTree(*four, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(four, 4),
+ EndOfLine(CreateVisiblePositionInFlatTree(*four, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(five, 5),
+ EndOfLine(CreateVisiblePositionInDOMTree(*five, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(two, 2),
+ EndOfLine(CreateVisiblePositionInFlatTree(*five, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(six, 6),
+ EndOfLine(CreateVisiblePositionInDOMTree(*six, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(six, 6),
+ EndOfLine(CreateVisiblePositionInFlatTree(*six, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(seven, 7),
+ EndOfLine(CreateVisiblePositionInDOMTree(*seven, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(seven, 7),
+ EndOfLine(CreateVisiblePositionInFlatTree(*seven, 1)).DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsLineTest, isEndOfLine) {
+ const char* body_content =
+ "<a id=host><b id=one>11</b><b id=two>22</b></a><i id=three>333</i><i "
+ "id=four>4444</i><br>";
+ const char* shadow_content =
+ "<div><u id=five>55555</u><content select=#two></content><br><u "
+ "id=six>666666</u><br><content select=#one></content><u "
+ "id=seven>7777777</u></div>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* three = GetDocument().getElementById("three")->firstChild();
+ Node* four = GetDocument().getElementById("four")->firstChild();
+ Node* five = shadow_root->getElementById("five")->firstChild();
+ Node* six = shadow_root->getElementById("six")->firstChild();
+ Node* seven = shadow_root->getElementById("seven")->firstChild();
+
+ EXPECT_FALSE(IsEndOfLine(CreateVisiblePositionInDOMTree(*one, 0)));
+ EXPECT_FALSE(IsEndOfLine(CreateVisiblePositionInFlatTree(*one, 0)));
+
+ EXPECT_FALSE(IsEndOfLine(CreateVisiblePositionInDOMTree(*one, 1)));
+ EXPECT_FALSE(IsEndOfLine(CreateVisiblePositionInFlatTree(*one, 1)));
+
+ EXPECT_FALSE(IsEndOfLine(CreateVisiblePositionInDOMTree(*two, 2)));
+ EXPECT_TRUE(IsEndOfLine(CreateVisiblePositionInFlatTree(*two, 2)));
+
+ EXPECT_FALSE(IsEndOfLine(CreateVisiblePositionInDOMTree(*three, 3)));
+ EXPECT_FALSE(IsEndOfLine(CreateVisiblePositionInFlatTree(*three, 3)));
+
+ EXPECT_TRUE(IsEndOfLine(CreateVisiblePositionInDOMTree(*four, 4)));
+ EXPECT_TRUE(IsEndOfLine(CreateVisiblePositionInFlatTree(*four, 4)));
+
+ EXPECT_TRUE(IsEndOfLine(CreateVisiblePositionInDOMTree(*five, 5)));
+ EXPECT_FALSE(IsEndOfLine(CreateVisiblePositionInFlatTree(*five, 5)));
+
+ EXPECT_TRUE(IsEndOfLine(CreateVisiblePositionInDOMTree(*six, 6)));
+ EXPECT_TRUE(IsEndOfLine(CreateVisiblePositionInFlatTree(*six, 6)));
+
+ EXPECT_TRUE(IsEndOfLine(CreateVisiblePositionInDOMTree(*seven, 7)));
+ EXPECT_TRUE(IsEndOfLine(CreateVisiblePositionInFlatTree(*seven, 7)));
+}
+
+TEST_F(VisibleUnitsLineTest, isLogicalEndOfLine) {
+ const char* body_content =
+ "<a id=host><b id=one>11</b><b id=two>22</b></a><i id=three>333</i><i "
+ "id=four>4444</i><br>";
+ const char* shadow_content =
+ "<div><u id=five>55555</u><content select=#two></content><br><u "
+ "id=six>666666</u><br><content select=#one></content><u "
+ "id=seven>7777777</u></div>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* three = GetDocument().getElementById("three")->firstChild();
+ Node* four = GetDocument().getElementById("four")->firstChild();
+ Node* five = shadow_root->getElementById("five")->firstChild();
+ Node* six = shadow_root->getElementById("six")->firstChild();
+ Node* seven = shadow_root->getElementById("seven")->firstChild();
+
+ EXPECT_FALSE(IsLogicalEndOfLine(CreateVisiblePositionInDOMTree(*one, 0)));
+ EXPECT_FALSE(IsLogicalEndOfLine(CreateVisiblePositionInFlatTree(*one, 0)));
+
+ EXPECT_FALSE(IsLogicalEndOfLine(CreateVisiblePositionInDOMTree(*one, 1)));
+ EXPECT_FALSE(IsLogicalEndOfLine(CreateVisiblePositionInFlatTree(*one, 1)));
+
+ EXPECT_FALSE(IsLogicalEndOfLine(CreateVisiblePositionInDOMTree(*two, 2)));
+ EXPECT_TRUE(IsLogicalEndOfLine(CreateVisiblePositionInFlatTree(*two, 2)));
+
+ EXPECT_FALSE(IsLogicalEndOfLine(CreateVisiblePositionInDOMTree(*three, 3)));
+ EXPECT_FALSE(IsLogicalEndOfLine(CreateVisiblePositionInFlatTree(*three, 3)));
+
+ EXPECT_TRUE(IsLogicalEndOfLine(CreateVisiblePositionInDOMTree(*four, 4)));
+ EXPECT_TRUE(IsLogicalEndOfLine(CreateVisiblePositionInFlatTree(*four, 4)));
+
+ EXPECT_TRUE(IsLogicalEndOfLine(CreateVisiblePositionInDOMTree(*five, 5)));
+ EXPECT_FALSE(IsLogicalEndOfLine(CreateVisiblePositionInFlatTree(*five, 5)));
+
+ EXPECT_TRUE(IsLogicalEndOfLine(CreateVisiblePositionInDOMTree(*six, 6)));
+ EXPECT_TRUE(IsLogicalEndOfLine(CreateVisiblePositionInFlatTree(*six, 6)));
+
+ EXPECT_TRUE(IsLogicalEndOfLine(CreateVisiblePositionInDOMTree(*seven, 7)));
+ EXPECT_TRUE(IsLogicalEndOfLine(CreateVisiblePositionInFlatTree(*seven, 7)));
+}
+
+TEST_F(VisibleUnitsLineTest, inSameLine) {
+ const char* body_content =
+ "<p id='host'>00<b id='one'>11</b><b id='two'>22</b>33</p>";
+ const char* shadow_content =
+ "<div><span id='s4'>44</span><content select=#two></content><br><span "
+ "id='s5'>55</span><br><content select=#one></content><span "
+ "id='s6'>66</span></div>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Element* body = GetDocument().body();
+ Element* one = body->QuerySelector("#one");
+ Element* two = body->QuerySelector("#two");
+ Element* four = shadow_root->QuerySelector("#s4");
+ Element* five = shadow_root->QuerySelector("#s5");
+
+ EXPECT_TRUE(InSameLine(PositionWithAffinityInDOMTree(*one, 0),
+ PositionWithAffinityInDOMTree(*two, 0)));
+ EXPECT_TRUE(InSameLine(PositionWithAffinityInDOMTree(*one->firstChild(), 0),
+ PositionWithAffinityInDOMTree(*two->firstChild(), 0)));
+ EXPECT_FALSE(
+ InSameLine(PositionWithAffinityInDOMTree(*one->firstChild(), 0),
+ PositionWithAffinityInDOMTree(*five->firstChild(), 0)));
+ EXPECT_FALSE(
+ InSameLine(PositionWithAffinityInDOMTree(*two->firstChild(), 0),
+ PositionWithAffinityInDOMTree(*four->firstChild(), 0)));
+
+ EXPECT_TRUE(InSameLine(CreateVisiblePositionInDOMTree(*one, 0),
+ CreateVisiblePositionInDOMTree(*two, 0)));
+ EXPECT_TRUE(
+ InSameLine(CreateVisiblePositionInDOMTree(*one->firstChild(), 0),
+ CreateVisiblePositionInDOMTree(*two->firstChild(), 0)));
+ EXPECT_FALSE(
+ InSameLine(CreateVisiblePositionInDOMTree(*one->firstChild(), 0),
+ CreateVisiblePositionInDOMTree(*five->firstChild(), 0)));
+ EXPECT_FALSE(
+ InSameLine(CreateVisiblePositionInDOMTree(*two->firstChild(), 0),
+ CreateVisiblePositionInDOMTree(*four->firstChild(), 0)));
+
+ EXPECT_FALSE(InSameLine(PositionWithAffinityInFlatTree(*one, 0),
+ PositionWithAffinityInFlatTree(*two, 0)));
+ EXPECT_FALSE(
+ InSameLine(PositionWithAffinityInFlatTree(*one->firstChild(), 0),
+ PositionWithAffinityInFlatTree(*two->firstChild(), 0)));
+ EXPECT_FALSE(
+ InSameLine(PositionWithAffinityInFlatTree(*one->firstChild(), 0),
+ PositionWithAffinityInFlatTree(*five->firstChild(), 0)));
+ EXPECT_TRUE(
+ InSameLine(PositionWithAffinityInFlatTree(*two->firstChild(), 0),
+ PositionWithAffinityInFlatTree(*four->firstChild(), 0)));
+
+ EXPECT_FALSE(InSameLine(CreateVisiblePositionInFlatTree(*one, 0),
+ CreateVisiblePositionInFlatTree(*two, 0)));
+ EXPECT_FALSE(
+ InSameLine(CreateVisiblePositionInFlatTree(*one->firstChild(), 0),
+ CreateVisiblePositionInFlatTree(*two->firstChild(), 0)));
+ EXPECT_FALSE(
+ InSameLine(CreateVisiblePositionInFlatTree(*one->firstChild(), 0),
+ CreateVisiblePositionInFlatTree(*five->firstChild(), 0)));
+ EXPECT_TRUE(
+ InSameLine(CreateVisiblePositionInFlatTree(*two->firstChild(), 0),
+ CreateVisiblePositionInFlatTree(*four->firstChild(), 0)));
+}
+
+TEST_F(VisibleUnitsLineTest, isStartOfLine) {
+ const char* body_content =
+ "<a id=host><b id=one>11</b><b id=two>22</b></a><i id=three>333</i><i "
+ "id=four>4444</i><br>";
+ const char* shadow_content =
+ "<div><u id=five>55555</u><content select=#two></content><br><u "
+ "id=six>666666</u><br><content select=#one></content><u "
+ "id=seven>7777777</u></div>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* three = GetDocument().getElementById("three")->firstChild();
+ Node* four = GetDocument().getElementById("four")->firstChild();
+ Node* five = shadow_root->getElementById("five")->firstChild();
+ Node* six = shadow_root->getElementById("six")->firstChild();
+ Node* seven = shadow_root->getElementById("seven")->firstChild();
+
+ EXPECT_TRUE(IsStartOfLine(CreateVisiblePositionInDOMTree(*one, 0)));
+ EXPECT_TRUE(IsStartOfLine(CreateVisiblePositionInFlatTree(*one, 0)));
+
+ EXPECT_FALSE(IsStartOfLine(CreateVisiblePositionInDOMTree(*one, 1)));
+ EXPECT_FALSE(IsStartOfLine(CreateVisiblePositionInFlatTree(*one, 1)));
+
+ EXPECT_FALSE(IsStartOfLine(CreateVisiblePositionInDOMTree(*two, 0)));
+ EXPECT_FALSE(IsStartOfLine(CreateVisiblePositionInFlatTree(*two, 0)));
+
+ EXPECT_FALSE(IsStartOfLine(CreateVisiblePositionInDOMTree(*three, 0)));
+ EXPECT_TRUE(IsStartOfLine(CreateVisiblePositionInFlatTree(*three, 0)));
+
+ EXPECT_FALSE(IsStartOfLine(CreateVisiblePositionInDOMTree(*four, 0)));
+ EXPECT_FALSE(IsStartOfLine(CreateVisiblePositionInFlatTree(*four, 0)));
+
+ EXPECT_TRUE(IsStartOfLine(CreateVisiblePositionInDOMTree(*five, 0)));
+ EXPECT_TRUE(IsStartOfLine(CreateVisiblePositionInFlatTree(*five, 0)));
+
+ EXPECT_TRUE(IsStartOfLine(CreateVisiblePositionInDOMTree(*six, 0)));
+ EXPECT_TRUE(IsStartOfLine(CreateVisiblePositionInFlatTree(*six, 0)));
+
+ EXPECT_FALSE(IsStartOfLine(CreateVisiblePositionInDOMTree(*seven, 0)));
+ EXPECT_FALSE(IsStartOfLine(CreateVisiblePositionInFlatTree(*seven, 0)));
+}
+
+TEST_F(VisibleUnitsLineTest, logicalEndOfLine) {
+ const char* body_content =
+ "<a id=host><b id=one>11</b><b id=two>22</b></a><i id=three>333</i><i "
+ "id=four>4444</i><br>";
+ const char* shadow_content =
+ "<div><u id=five>55555</u><content select=#two></content><br><u "
+ "id=six>666666</u><br><content select=#one></content><u "
+ "id=seven>7777777</u></div>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* three = GetDocument().getElementById("three")->firstChild();
+ Node* four = GetDocument().getElementById("four")->firstChild();
+ Node* five = shadow_root->getElementById("five")->firstChild();
+ Node* six = shadow_root->getElementById("six")->firstChild();
+ Node* seven = shadow_root->getElementById("seven")->firstChild();
+
+ EXPECT_EQ(Position(seven, 7),
+ LogicalEndOfLine(CreateVisiblePositionInDOMTree(*one, 0))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(seven, 7),
+ LogicalEndOfLine(CreateVisiblePositionInFlatTree(*one, 0))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(seven, 7),
+ LogicalEndOfLine(CreateVisiblePositionInDOMTree(*one, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(seven, 7),
+ LogicalEndOfLine(CreateVisiblePositionInFlatTree(*one, 1))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(seven, 7),
+ LogicalEndOfLine(CreateVisiblePositionInDOMTree(*two, 0))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(two, 2),
+ LogicalEndOfLine(CreateVisiblePositionInFlatTree(*two, 0))
+ .DeepEquivalent());
+
+ // TODO(yosin) logicalEndOfLine(two, 1) -> (five, 5) is a broken result. We
+ // keep it as a marker for future change.
+ EXPECT_EQ(Position(five, 5),
+ LogicalEndOfLine(CreateVisiblePositionInDOMTree(*two, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(two, 2),
+ LogicalEndOfLine(CreateVisiblePositionInFlatTree(*two, 1))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(five, 5),
+ LogicalEndOfLine(CreateVisiblePositionInDOMTree(*three, 0))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(four, 4),
+ LogicalEndOfLine(CreateVisiblePositionInFlatTree(*three, 1))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(four, 4),
+ LogicalEndOfLine(CreateVisiblePositionInDOMTree(*four, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(four, 4),
+ LogicalEndOfLine(CreateVisiblePositionInFlatTree(*four, 1))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(five, 5),
+ LogicalEndOfLine(CreateVisiblePositionInDOMTree(*five, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(two, 2),
+ LogicalEndOfLine(CreateVisiblePositionInFlatTree(*five, 1))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(six, 6),
+ LogicalEndOfLine(CreateVisiblePositionInDOMTree(*six, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(six, 6),
+ LogicalEndOfLine(CreateVisiblePositionInFlatTree(*six, 1))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(seven, 7),
+ LogicalEndOfLine(CreateVisiblePositionInDOMTree(*seven, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(seven, 7),
+ LogicalEndOfLine(CreateVisiblePositionInFlatTree(*seven, 1))
+ .DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsLineTest, logicalStartOfLine) {
+ const char* body_content =
+ "<a id=host><b id=one>11</b><b id=two>22</b></a><i id=three>333</i><i "
+ "id=four>4444</i><br>";
+ const char* shadow_content =
+ "<div><u id=five>55555</u><content select=#two></content><br><u "
+ "id=six>666666</u><br><content select=#one></content><u "
+ "id=seven>7777777</u></div>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* three = GetDocument().getElementById("three")->firstChild();
+ Node* four = GetDocument().getElementById("four")->firstChild();
+ Node* five = shadow_root->getElementById("five")->firstChild();
+ Node* six = shadow_root->getElementById("six")->firstChild();
+ Node* seven = shadow_root->getElementById("seven")->firstChild();
+
+ EXPECT_EQ(Position(one, 0),
+ LogicalStartOfLine(CreateVisiblePositionInDOMTree(*one, 0))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(one, 0),
+ LogicalStartOfLine(CreateVisiblePositionInFlatTree(*one, 0))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(one, 0),
+ LogicalStartOfLine(CreateVisiblePositionInDOMTree(*one, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(one, 0),
+ LogicalStartOfLine(CreateVisiblePositionInFlatTree(*one, 1))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(one, 0),
+ LogicalStartOfLine(CreateVisiblePositionInDOMTree(*two, 0))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(five, 0),
+ LogicalStartOfLine(CreateVisiblePositionInFlatTree(*two, 0))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(five, 0),
+ LogicalStartOfLine(CreateVisiblePositionInDOMTree(*two, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(five, 0),
+ LogicalStartOfLine(CreateVisiblePositionInFlatTree(*two, 1))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(five, 0),
+ LogicalStartOfLine(CreateVisiblePositionInDOMTree(*three, 0))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(three, 0),
+ LogicalStartOfLine(CreateVisiblePositionInFlatTree(*three, 1))
+ .DeepEquivalent());
+
+ // TODO(yosin) logicalStartOfLine(four, 1) -> (two, 2) is a broken result.
+ // We keep it as a marker for future change.
+ EXPECT_EQ(Position(two, 2),
+ LogicalStartOfLine(CreateVisiblePositionInDOMTree(*four, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(three, 0),
+ LogicalStartOfLine(CreateVisiblePositionInFlatTree(*four, 1))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(five, 0),
+ LogicalStartOfLine(CreateVisiblePositionInDOMTree(*five, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(five, 0),
+ LogicalStartOfLine(CreateVisiblePositionInFlatTree(*five, 1))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(six, 0),
+ LogicalStartOfLine(CreateVisiblePositionInDOMTree(*six, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(six, 0),
+ LogicalStartOfLine(CreateVisiblePositionInFlatTree(*six, 1))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(one, 0),
+ LogicalStartOfLine(CreateVisiblePositionInDOMTree(*seven, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(one, 0),
+ LogicalStartOfLine(CreateVisiblePositionInFlatTree(*seven, 1))
+ .DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsLineTest, startOfLine) {
+ const char* body_content =
+ "<a id=host><b id=one>11</b><b id=two>22</b></a><i id=three>333</i><i "
+ "id=four>4444</i><br>";
+ const char* shadow_content =
+ "<div><u id=five>55555</u><content select=#two></content><br><u "
+ "id=six>666666</u><br><content select=#one></content><u "
+ "id=seven>7777777</u></div>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* three = GetDocument().getElementById("three")->firstChild();
+ Node* four = GetDocument().getElementById("four")->firstChild();
+ Node* five = shadow_root->getElementById("five")->firstChild();
+ Node* six = shadow_root->getElementById("six")->firstChild();
+ Node* seven = shadow_root->getElementById("seven")->firstChild();
+
+ EXPECT_EQ(
+ Position(one, 0),
+ StartOfLine(CreateVisiblePositionInDOMTree(*one, 0)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(one, 0),
+ StartOfLine(CreateVisiblePositionInFlatTree(*one, 0)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(one, 0),
+ StartOfLine(CreateVisiblePositionInDOMTree(*one, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(one, 0),
+ StartOfLine(CreateVisiblePositionInFlatTree(*one, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(one, 0),
+ StartOfLine(CreateVisiblePositionInDOMTree(*two, 0)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(five, 0),
+ StartOfLine(CreateVisiblePositionInFlatTree(*two, 0)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(five, 0),
+ StartOfLine(CreateVisiblePositionInDOMTree(*two, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(five, 0),
+ StartOfLine(CreateVisiblePositionInFlatTree(*two, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(five, 0),
+ StartOfLine(CreateVisiblePositionInDOMTree(*three, 0)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(three, 0),
+ StartOfLine(CreateVisiblePositionInFlatTree(*three, 1)).DeepEquivalent());
+
+ // TODO(yosin) startOfLine(four, 1) -> (two, 2) is a broken result. We keep
+ // it as a marker for future change.
+ EXPECT_EQ(
+ Position(two, 2),
+ StartOfLine(CreateVisiblePositionInDOMTree(*four, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(three, 0),
+ StartOfLine(CreateVisiblePositionInFlatTree(*four, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(five, 0),
+ StartOfLine(CreateVisiblePositionInDOMTree(*five, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(five, 0),
+ StartOfLine(CreateVisiblePositionInFlatTree(*five, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(six, 0),
+ StartOfLine(CreateVisiblePositionInDOMTree(*six, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(six, 0),
+ StartOfLine(CreateVisiblePositionInFlatTree(*six, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(one, 0),
+ StartOfLine(CreateVisiblePositionInDOMTree(*seven, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(one, 0),
+ StartOfLine(CreateVisiblePositionInFlatTree(*seven, 1)).DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsLineTest,
+ PreviousRootInlineBoxCandidatePositionWithDisplayNone) {
+ SetBodyContent(
+ "<div contenteditable>"
+ "<div id=one>one abc</div>"
+ "<div id=two>two <b id=none style=display:none>def</b> ghi</div>"
+ "</div>");
+ Element* const one = GetDocument().getElementById("one");
+ Element* const two = GetDocument().getElementById("two");
+ const VisiblePosition& visible_position =
+ CreateVisiblePosition(Position::LastPositionInNode(*two));
+ EXPECT_EQ(Position(one->firstChild(), 7),
+ PreviousRootInlineBoxCandidatePosition(
+ two->lastChild(), visible_position, kContentIsEditable));
+}
+
+TEST_F(VisibleUnitsLineTest, InSameLineSkippingEmptyEditableDiv) {
+ // This test records the InSameLine() results in
+ // editing/selection/skip-over-contenteditable.html
+ SetBodyContent(
+ "<p id=foo>foo</p>"
+ "<div contenteditable></div>"
+ "<p id=bar>bar</p>");
+ const Node* const foo = GetElementById("foo")->firstChild();
+ const Node* const bar = GetElementById("bar")->firstChild();
+
+ EXPECT_TRUE(InSameLine(
+ PositionWithAffinity(Position(foo, 3), TextAffinity::kDownstream),
+ PositionWithAffinity(Position(foo, 3), TextAffinity::kUpstream)));
+ EXPECT_FALSE(InSameLine(
+ PositionWithAffinity(Position(bar, 0), TextAffinity::kDownstream),
+ PositionWithAffinity(Position(foo, 3), TextAffinity::kDownstream)));
+ EXPECT_TRUE(InSameLine(
+ PositionWithAffinity(Position(bar, 3), TextAffinity::kDownstream),
+ PositionWithAffinity(Position(bar, 3), TextAffinity::kUpstream)));
+ EXPECT_FALSE(InSameLine(
+ PositionWithAffinity(Position(foo, 0), TextAffinity::kDownstream),
+ PositionWithAffinity(Position(bar, 0), TextAffinity::kDownstream)));
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_units_paragraph.cc b/chromium/third_party/blink/renderer/core/editing/visible_units_paragraph.cc
new file mode 100644
index 00000000000..50a31761453
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_units_paragraph.cc
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/layout/layout_text.h"
+
+namespace blink {
+
+namespace {
+
+bool NodeIsUserSelectAll(const Node* node) {
+ return node && node->GetLayoutObject() &&
+ node->GetLayoutObject()->Style()->UserSelect() == EUserSelect::kAll;
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> StartOfParagraphAlgorithm(
+ const PositionTemplate<Strategy>& position,
+ EditingBoundaryCrossingRule boundary_crossing_rule) {
+ Node* const start_node = position.AnchorNode();
+
+ if (!start_node)
+ return PositionTemplate<Strategy>();
+
+ if (IsRenderedAsNonInlineTableImageOrHR(start_node))
+ return PositionTemplate<Strategy>::BeforeNode(*start_node);
+
+ Element* const start_block = EnclosingBlock(
+ PositionTemplate<Strategy>::FirstPositionInOrBeforeNode(*start_node),
+ kCannotCrossEditingBoundary);
+ ContainerNode* const highest_root = HighestEditableRoot(position);
+ const bool start_node_is_editable = HasEditableStyle(*start_node);
+
+ Node* candidate_node = start_node;
+ PositionAnchorType candidate_type = position.AnchorType();
+ int candidate_offset = position.ComputeEditingOffset();
+
+ Node* previous_node_iterator = start_node;
+ while (previous_node_iterator) {
+ if (boundary_crossing_rule == kCannotCrossEditingBoundary &&
+ !NodeIsUserSelectAll(previous_node_iterator) &&
+ HasEditableStyle(*previous_node_iterator) != start_node_is_editable)
+ break;
+ if (boundary_crossing_rule == kCanSkipOverEditingBoundary) {
+ while (previous_node_iterator &&
+ HasEditableStyle(*previous_node_iterator) !=
+ start_node_is_editable) {
+ previous_node_iterator =
+ Strategy::PreviousPostOrder(*previous_node_iterator, start_block);
+ }
+ if (!previous_node_iterator ||
+ !previous_node_iterator->IsDescendantOf(highest_root))
+ break;
+ }
+
+ const LayoutObject* layout_object =
+ previous_node_iterator->GetLayoutObject();
+ if (!layout_object) {
+ previous_node_iterator =
+ Strategy::PreviousPostOrder(*previous_node_iterator, start_block);
+ continue;
+ }
+ const ComputedStyle& style = layout_object->StyleRef();
+ if (style.Visibility() != EVisibility::kVisible) {
+ previous_node_iterator =
+ Strategy::PreviousPostOrder(*previous_node_iterator, start_block);
+ continue;
+ }
+
+ if (layout_object->IsBR() || IsEnclosingBlock(previous_node_iterator))
+ break;
+
+ if (layout_object->IsText() &&
+ ToLayoutText(previous_node_iterator->GetLayoutObject())
+ ->ResolvedTextLength()) {
+ SECURITY_DCHECK(previous_node_iterator->IsTextNode());
+ if (style.PreserveNewline()) {
+ LayoutText* text =
+ ToLayoutText(previous_node_iterator->GetLayoutObject());
+ int index = text->TextLength();
+ if (previous_node_iterator == start_node && candidate_offset < index)
+ index = max(0, candidate_offset);
+ while (--index >= 0) {
+ if ((*text)[index] == '\n') {
+ return PositionTemplate<Strategy>(ToText(previous_node_iterator),
+ index + 1);
+ }
+ }
+ }
+ candidate_node = previous_node_iterator;
+ candidate_type = PositionAnchorType::kOffsetInAnchor;
+ candidate_offset = 0;
+ previous_node_iterator =
+ Strategy::PreviousPostOrder(*previous_node_iterator, start_block);
+ } else if (EditingIgnoresContent(*previous_node_iterator) ||
+ IsDisplayInsideTable(previous_node_iterator)) {
+ candidate_node = previous_node_iterator;
+ candidate_type = PositionAnchorType::kBeforeAnchor;
+ previous_node_iterator = previous_node_iterator->previousSibling()
+ ? previous_node_iterator->previousSibling()
+ : Strategy::PreviousPostOrder(
+ *previous_node_iterator, start_block);
+ } else {
+ previous_node_iterator =
+ Strategy::PreviousPostOrder(*previous_node_iterator, start_block);
+ }
+ }
+
+ if (candidate_type == PositionAnchorType::kOffsetInAnchor)
+ return PositionTemplate<Strategy>(candidate_node, candidate_offset);
+
+ return PositionTemplate<Strategy>(candidate_node, candidate_type);
+}
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy> StartOfParagraphAlgorithm(
+ const VisiblePositionTemplate<Strategy>& visible_position,
+ EditingBoundaryCrossingRule boundary_crossing_rule) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ return CreateVisiblePosition(StartOfParagraphAlgorithm(
+ visible_position.DeepEquivalent(), boundary_crossing_rule));
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> EndOfParagraphAlgorithm(
+ const PositionTemplate<Strategy>& position,
+ EditingBoundaryCrossingRule boundary_crossing_rule) {
+ Node* const start_node = position.AnchorNode();
+
+ if (!start_node)
+ return PositionTemplate<Strategy>();
+
+ if (IsRenderedAsNonInlineTableImageOrHR(start_node))
+ return PositionTemplate<Strategy>::AfterNode(*start_node);
+
+ Element* const start_block = EnclosingBlock(
+ PositionTemplate<Strategy>::FirstPositionInOrBeforeNode(*start_node),
+ kCannotCrossEditingBoundary);
+ ContainerNode* const highest_root = HighestEditableRoot(position);
+ const bool start_node_is_editable = HasEditableStyle(*start_node);
+
+ Node* candidate_node = start_node;
+ PositionAnchorType candidate_type = position.AnchorType();
+ int candidate_offset = position.ComputeEditingOffset();
+
+ Node* next_node_iterator = start_node;
+ while (next_node_iterator) {
+ if (boundary_crossing_rule == kCannotCrossEditingBoundary &&
+ !NodeIsUserSelectAll(next_node_iterator) &&
+ HasEditableStyle(*next_node_iterator) != start_node_is_editable)
+ break;
+ if (boundary_crossing_rule == kCanSkipOverEditingBoundary) {
+ while (next_node_iterator &&
+ HasEditableStyle(*next_node_iterator) != start_node_is_editable)
+ next_node_iterator = Strategy::Next(*next_node_iterator, start_block);
+ if (!next_node_iterator ||
+ !next_node_iterator->IsDescendantOf(highest_root))
+ break;
+ }
+
+ LayoutObject* const layout_object = next_node_iterator->GetLayoutObject();
+ if (!layout_object) {
+ next_node_iterator = Strategy::Next(*next_node_iterator, start_block);
+ continue;
+ }
+ const ComputedStyle& style = layout_object->StyleRef();
+ if (style.Visibility() != EVisibility::kVisible) {
+ next_node_iterator = Strategy::Next(*next_node_iterator, start_block);
+ continue;
+ }
+
+ if (layout_object->IsBR() || IsEnclosingBlock(next_node_iterator))
+ break;
+
+ // TODO(editing-dev): We avoid returning a position where the layoutObject
+ // can't accept the caret.
+ if (layout_object->IsText() &&
+ ToLayoutText(layout_object)->ResolvedTextLength()) {
+ SECURITY_DCHECK(next_node_iterator->IsTextNode());
+ LayoutText* const text = ToLayoutText(layout_object);
+ if (style.PreserveNewline()) {
+ const int length = ToLayoutText(layout_object)->TextLength();
+ for (int i = (next_node_iterator == start_node ? candidate_offset : 0);
+ i < length; ++i) {
+ if ((*text)[i] == '\n') {
+ return PositionTemplate<Strategy>(ToText(next_node_iterator),
+ i + text->TextStartOffset());
+ }
+ }
+ }
+
+ candidate_node = next_node_iterator;
+ candidate_type = PositionAnchorType::kOffsetInAnchor;
+ candidate_offset =
+ layout_object->CaretMaxOffset() + text->TextStartOffset();
+ next_node_iterator = Strategy::Next(*next_node_iterator, start_block);
+ } else if (EditingIgnoresContent(*next_node_iterator) ||
+ IsDisplayInsideTable(next_node_iterator)) {
+ candidate_node = next_node_iterator;
+ candidate_type = PositionAnchorType::kAfterAnchor;
+ next_node_iterator =
+ Strategy::NextSkippingChildren(*next_node_iterator, start_block);
+ } else {
+ next_node_iterator = Strategy::Next(*next_node_iterator, start_block);
+ }
+ }
+
+ if (candidate_type == PositionAnchorType::kOffsetInAnchor)
+ return PositionTemplate<Strategy>(candidate_node, candidate_offset);
+
+ return PositionTemplate<Strategy>(candidate_node, candidate_type);
+}
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy> EndOfParagraphAlgorithm(
+ const VisiblePositionTemplate<Strategy>& visible_position,
+ EditingBoundaryCrossingRule boundary_crossing_rule) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ return CreateVisiblePosition(EndOfParagraphAlgorithm(
+ visible_position.DeepEquivalent(), boundary_crossing_rule));
+}
+
+template <typename Strategy>
+bool IsStartOfParagraphAlgorithm(
+ const VisiblePositionTemplate<Strategy>& pos,
+ EditingBoundaryCrossingRule boundary_crossing_rule) {
+ DCHECK(pos.IsValid()) << pos;
+ return pos.IsNotNull() &&
+ pos.DeepEquivalent() ==
+ StartOfParagraph(pos, boundary_crossing_rule).DeepEquivalent();
+}
+
+template <typename Strategy>
+bool IsEndOfParagraphAlgorithm(
+ const VisiblePositionTemplate<Strategy>& pos,
+ EditingBoundaryCrossingRule boundary_crossing_rule) {
+ DCHECK(pos.IsValid()) << pos;
+ return pos.IsNotNull() &&
+ pos.DeepEquivalent() ==
+ EndOfParagraph(pos, boundary_crossing_rule).DeepEquivalent();
+}
+
+} // namespace
+
+VisiblePosition StartOfParagraph(
+ const VisiblePosition& c,
+ EditingBoundaryCrossingRule boundary_crossing_rule) {
+ return StartOfParagraphAlgorithm<EditingStrategy>(c, boundary_crossing_rule);
+}
+
+VisiblePositionInFlatTree StartOfParagraph(
+ const VisiblePositionInFlatTree& c,
+ EditingBoundaryCrossingRule boundary_crossing_rule) {
+ return StartOfParagraphAlgorithm<EditingInFlatTreeStrategy>(
+ c, boundary_crossing_rule);
+}
+
+VisiblePosition EndOfParagraph(
+ const VisiblePosition& c,
+ EditingBoundaryCrossingRule boundary_crossing_rule) {
+ return EndOfParagraphAlgorithm<EditingStrategy>(c, boundary_crossing_rule);
+}
+
+VisiblePositionInFlatTree EndOfParagraph(
+ const VisiblePositionInFlatTree& c,
+ EditingBoundaryCrossingRule boundary_crossing_rule) {
+ return EndOfParagraphAlgorithm<EditingInFlatTreeStrategy>(
+ c, boundary_crossing_rule);
+}
+
+// TODO(editing-dev): isStartOfParagraph(startOfNextParagraph(pos)) is not
+// always true
+VisiblePosition StartOfNextParagraph(const VisiblePosition& visible_position) {
+ DCHECK(visible_position.IsValid()) << visible_position;
+ VisiblePosition paragraph_end(
+ EndOfParagraph(visible_position, kCanSkipOverEditingBoundary));
+ VisiblePosition after_paragraph_end(
+ NextPositionOf(paragraph_end, kCannotCrossEditingBoundary));
+ // The position after the last position in the last cell of a table
+ // is not the start of the next paragraph.
+ if (TableElementJustBefore(after_paragraph_end))
+ return NextPositionOf(after_paragraph_end, kCannotCrossEditingBoundary);
+ return after_paragraph_end;
+}
+
+// TODO(editing-dev): isStartOfParagraph(startOfNextParagraph(pos)) is not
+// always true
+bool InSameParagraph(const VisiblePosition& a,
+ const VisiblePosition& b,
+ EditingBoundaryCrossingRule boundary_crossing_rule) {
+ DCHECK(a.IsValid()) << a;
+ DCHECK(b.IsValid()) << b;
+ return a.IsNotNull() &&
+ StartOfParagraph(a, boundary_crossing_rule).DeepEquivalent() ==
+ StartOfParagraph(b, boundary_crossing_rule).DeepEquivalent();
+}
+
+bool IsStartOfParagraph(const VisiblePosition& pos,
+ EditingBoundaryCrossingRule boundary_crossing_rule) {
+ return IsStartOfParagraphAlgorithm<EditingStrategy>(pos,
+ boundary_crossing_rule);
+}
+
+bool IsStartOfParagraph(const VisiblePositionInFlatTree& pos) {
+ return IsStartOfParagraphAlgorithm<EditingInFlatTreeStrategy>(
+ pos, kCannotCrossEditingBoundary);
+}
+
+bool IsEndOfParagraph(const VisiblePosition& pos,
+ EditingBoundaryCrossingRule boundary_crossing_rule) {
+ return IsEndOfParagraphAlgorithm<EditingStrategy>(pos,
+ boundary_crossing_rule);
+}
+
+bool IsEndOfParagraph(const VisiblePositionInFlatTree& pos) {
+ return IsEndOfParagraphAlgorithm<EditingInFlatTreeStrategy>(
+ pos, kCannotCrossEditingBoundary);
+}
+
+EphemeralRange ExpandToParagraphBoundary(const EphemeralRange& range) {
+ const VisiblePosition& start = CreateVisiblePosition(range.StartPosition());
+ DCHECK(start.IsNotNull()) << range.StartPosition();
+ const Position& paragraph_start = StartOfParagraph(start).DeepEquivalent();
+ DCHECK(paragraph_start.IsNotNull()) << range.StartPosition();
+
+ const VisiblePosition& end = CreateVisiblePosition(range.EndPosition());
+ DCHECK(end.IsNotNull()) << range.EndPosition();
+ const Position& paragraph_end = EndOfParagraph(end).DeepEquivalent();
+ DCHECK(paragraph_end.IsNotNull()) << range.EndPosition();
+
+ // TODO(editing-dev): There are some cases (crbug.com/640112) where we get
+ // |paragraphStart > paragraphEnd|, which is the reason we cannot directly
+ // return |EphemeralRange(paragraphStart, paragraphEnd)|. This is not
+ // desired, though. We should do more investigation to ensure that why
+ // |paragraphStart <= paragraphEnd| is violated.
+ const Position& result_start =
+ paragraph_start.IsNotNull() && paragraph_start <= range.StartPosition()
+ ? paragraph_start
+ : range.StartPosition();
+ const Position& result_end =
+ paragraph_end.IsNotNull() && paragraph_end >= range.EndPosition()
+ ? paragraph_end
+ : range.EndPosition();
+ return EphemeralRange(result_start, result_end);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_units_paragraph_test.cc b/chromium/third_party/blink/renderer/core/editing/visible_units_paragraph_test.cc
new file mode 100644
index 00000000000..fa676994aa4
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_units_paragraph_test.cc
@@ -0,0 +1,262 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+#include "third_party/blink/renderer/core/layout/layout_text_fragment.h"
+
+namespace blink {
+
+class VisibleUnitsParagraphTest : public EditingTestBase {
+ protected:
+ static PositionWithAffinity PositionWithAffinityInDOMTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return PositionWithAffinity(CanonicalPositionOf(Position(&anchor, offset)),
+ affinity);
+ }
+
+ static VisiblePosition CreateVisiblePositionInDOMTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return CreateVisiblePosition(Position(&anchor, offset), affinity);
+ }
+
+ static PositionInFlatTreeWithAffinity PositionWithAffinityInFlatTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return PositionInFlatTreeWithAffinity(
+ CanonicalPositionOf(PositionInFlatTree(&anchor, offset)), affinity);
+ }
+
+ static VisiblePositionInFlatTree CreateVisiblePositionInFlatTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return CreateVisiblePosition(PositionInFlatTree(&anchor, offset), affinity);
+ }
+};
+
+TEST_F(VisibleUnitsParagraphTest, endOfParagraphFirstLetter) {
+ SetBodyContent(
+ "<style>div::first-letter { color: red }</style><div "
+ "id=sample>1ab\nde</div>");
+
+ Node* sample = GetDocument().getElementById("sample");
+ Node* text = sample->firstChild();
+
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 0))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 2))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 3))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 4))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 5))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 6))
+ .DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsParagraphTest, endOfParagraphFirstLetterPre) {
+ SetBodyContent(
+ "<style>pre::first-letter { color: red }</style><pre "
+ "id=sample>1ab\nde</pre>");
+
+ Node* sample = GetDocument().getElementById("sample");
+ Node* text = sample->firstChild();
+
+ EXPECT_EQ(Position(text, 3),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 0))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 3),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 3),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 2))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 3))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 4))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 5))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 6))
+ .DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsParagraphTest, endOfParagraphShadow) {
+ const char* body_content =
+ "<a id=host><b id=one>1</b><b id=two>22</b></a><b id=three>333</b>";
+ const char* shadow_content =
+ "<p><content select=#two></content></p><p><content "
+ "select=#one></content></p>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ Element* one = GetDocument().getElementById("one");
+ Element* two = GetDocument().getElementById("two");
+ Element* three = GetDocument().getElementById("three");
+
+ EXPECT_EQ(
+ Position(three->firstChild(), 3),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*one->firstChild(), 1))
+ .DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(one->firstChild(), 1),
+ EndOfParagraph(CreateVisiblePositionInFlatTree(*one->firstChild(), 1))
+ .DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(three->firstChild(), 3),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*two->firstChild(), 2))
+ .DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(two->firstChild(), 2),
+ EndOfParagraph(CreateVisiblePositionInFlatTree(*two->firstChild(), 2))
+ .DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsParagraphTest, endOfParagraphSimple) {
+ SetBodyContent("<div id=sample>1ab\nde</div>");
+
+ Node* sample = GetDocument().getElementById("sample");
+ Node* text = sample->firstChild();
+
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 0))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 2))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 3))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 4))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 5))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 6))
+ .DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsParagraphTest, endOfParagraphSimplePre) {
+ SetBodyContent("<pre id=sample>1ab\nde</pre>");
+
+ Node* sample = GetDocument().getElementById("sample");
+ Node* text = sample->firstChild();
+
+ EXPECT_EQ(Position(text, 3),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 0))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 3),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 3),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 2))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 3),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 3))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 4))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 5))
+ .DeepEquivalent());
+ EXPECT_EQ(Position(text, 6),
+ EndOfParagraph(CreateVisiblePositionInDOMTree(*text, 6))
+ .DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsParagraphTest, isEndOfParagraph) {
+ const char* body_content =
+ "<a id=host><b id=one>1</b><b id=two>22</b></a><b id=three>333</b>";
+ const char* shadow_content =
+ "<p><content select=#two></content></p><p><content "
+ "select=#one></content></p>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* three = GetDocument().getElementById("three")->firstChild();
+
+ EXPECT_FALSE(IsEndOfParagraph(CreateVisiblePositionInDOMTree(*one, 0)));
+ EXPECT_FALSE(IsEndOfParagraph(CreateVisiblePositionInFlatTree(*one, 0)));
+
+ EXPECT_FALSE(IsEndOfParagraph(CreateVisiblePositionInDOMTree(*one, 1)));
+ EXPECT_TRUE(IsEndOfParagraph(CreateVisiblePositionInFlatTree(*one, 1)));
+
+ EXPECT_FALSE(IsEndOfParagraph(CreateVisiblePositionInDOMTree(*two, 2)));
+ EXPECT_TRUE(IsEndOfParagraph(CreateVisiblePositionInFlatTree(*two, 2)));
+
+ EXPECT_FALSE(IsEndOfParagraph(CreateVisiblePositionInDOMTree(*three, 0)));
+ EXPECT_FALSE(IsEndOfParagraph(CreateVisiblePositionInFlatTree(*three, 0)));
+
+ EXPECT_TRUE(IsEndOfParagraph(CreateVisiblePositionInDOMTree(*three, 3)));
+ EXPECT_TRUE(IsEndOfParagraph(CreateVisiblePositionInFlatTree(*three, 3)));
+}
+
+TEST_F(VisibleUnitsParagraphTest, isStartOfParagraph) {
+ const char* body_content =
+ "<b id=zero>0</b><a id=host><b id=one>1</b><b id=two>22</b></a><b "
+ "id=three>333</b>";
+ const char* shadow_content =
+ "<p><content select=#two></content></p><p><content "
+ "select=#one></content></p>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ Node* zero = GetDocument().getElementById("zero")->firstChild();
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* three = GetDocument().getElementById("three")->firstChild();
+
+ EXPECT_TRUE(IsStartOfParagraph(CreateVisiblePositionInDOMTree(*zero, 0)));
+ EXPECT_TRUE(IsStartOfParagraph(CreateVisiblePositionInFlatTree(*zero, 0)));
+
+ EXPECT_FALSE(IsStartOfParagraph(CreateVisiblePositionInDOMTree(*one, 0)));
+ EXPECT_TRUE(IsStartOfParagraph(CreateVisiblePositionInFlatTree(*one, 0)));
+
+ EXPECT_FALSE(IsStartOfParagraph(CreateVisiblePositionInDOMTree(*one, 1)));
+ EXPECT_FALSE(IsStartOfParagraph(CreateVisiblePositionInFlatTree(*one, 1)));
+
+ EXPECT_FALSE(IsStartOfParagraph(CreateVisiblePositionInDOMTree(*two, 0)));
+ EXPECT_TRUE(IsStartOfParagraph(CreateVisiblePositionInFlatTree(*two, 0)));
+
+ EXPECT_FALSE(IsStartOfParagraph(CreateVisiblePositionInDOMTree(*three, 0)));
+ EXPECT_TRUE(IsStartOfParagraph(CreateVisiblePositionInFlatTree(*three, 0)));
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_units_sentence.cc b/chromium/third_party/blink/renderer/core/editing/visible_units_sentence.cc
new file mode 100644
index 00000000000..424949e0741
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_units_sentence.cc
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/platform/text/text_break_iterator.h"
+
+namespace blink {
+
+namespace {
+
+unsigned EndSentenceBoundary(const UChar* characters,
+ unsigned length,
+ unsigned,
+ BoundarySearchContextAvailability,
+ bool&) {
+ TextBreakIterator* iterator = SentenceBreakIterator(characters, length);
+ return iterator->next();
+}
+
+unsigned NextSentencePositionBoundary(const UChar* characters,
+ unsigned length,
+ unsigned,
+ BoundarySearchContextAvailability,
+ bool&) {
+ // FIXME: This is identical to endSentenceBoundary. This isn't right, it needs
+ // to move to the equivlant position in the following sentence.
+ TextBreakIterator* iterator = SentenceBreakIterator(characters, length);
+ return iterator->following(0);
+}
+
+unsigned PreviousSentencePositionBoundary(const UChar* characters,
+ unsigned length,
+ unsigned,
+ BoundarySearchContextAvailability,
+ bool&) {
+ // FIXME: This is identical to startSentenceBoundary. I'm pretty sure that's
+ // not right.
+ TextBreakIterator* iterator = SentenceBreakIterator(characters, length);
+ // FIXME: The following function can return -1; we don't handle that.
+ return iterator->preceding(length);
+}
+
+unsigned StartSentenceBoundary(const UChar* characters,
+ unsigned length,
+ unsigned,
+ BoundarySearchContextAvailability,
+ bool&) {
+ TextBreakIterator* iterator = SentenceBreakIterator(characters, length);
+ // FIXME: The following function can return -1; we don't handle that.
+ return iterator->preceding(length);
+}
+
+// TODO(yosin) This includes the space after the punctuation that marks the end
+// of the sentence.
+template <typename Strategy>
+static VisiblePositionTemplate<Strategy> EndOfSentenceAlgorithm(
+ const VisiblePositionTemplate<Strategy>& c) {
+ DCHECK(c.IsValid()) << c;
+ return CreateVisiblePosition(NextBoundary(c, EndSentenceBoundary),
+ TextAffinity::kUpstreamIfPossible);
+}
+
+template <typename Strategy>
+VisiblePositionTemplate<Strategy> StartOfSentenceAlgorithm(
+ const VisiblePositionTemplate<Strategy>& c) {
+ DCHECK(c.IsValid()) << c;
+ return CreateVisiblePosition(PreviousBoundary(c, StartSentenceBoundary));
+}
+
+} // namespace
+
+VisiblePosition EndOfSentence(const VisiblePosition& c) {
+ return EndOfSentenceAlgorithm<EditingStrategy>(c);
+}
+
+VisiblePositionInFlatTree EndOfSentence(const VisiblePositionInFlatTree& c) {
+ return EndOfSentenceAlgorithm<EditingInFlatTreeStrategy>(c);
+}
+
+EphemeralRange ExpandEndToSentenceBoundary(const EphemeralRange& range) {
+ DCHECK(range.IsNotNull());
+ const VisiblePosition& visible_end =
+ CreateVisiblePosition(range.EndPosition());
+ DCHECK(visible_end.IsNotNull());
+ const Position& sentence_end = EndOfSentence(visible_end).DeepEquivalent();
+ // TODO(editing-dev): |sentenceEnd < range.endPosition()| is possible,
+ // which would trigger a DCHECK in EphemeralRange's constructor if we return
+ // it directly. However, this shouldn't happen and needs to be fixed.
+ return EphemeralRange(
+ range.StartPosition(),
+ sentence_end.IsNotNull() && sentence_end > range.EndPosition()
+ ? sentence_end
+ : range.EndPosition());
+}
+
+EphemeralRange ExpandRangeToSentenceBoundary(const EphemeralRange& range) {
+ DCHECK(range.IsNotNull());
+ const VisiblePosition& visible_start =
+ CreateVisiblePosition(range.StartPosition());
+ DCHECK(visible_start.IsNotNull());
+ const Position& sentence_start =
+ StartOfSentence(visible_start).DeepEquivalent();
+ // TODO(editing-dev): |sentenceStart > range.startPosition()| is possible,
+ // which would trigger a DCHECK in EphemeralRange's constructor if we return
+ // it directly. However, this shouldn't happen and needs to be fixed.
+ return ExpandEndToSentenceBoundary(EphemeralRange(
+ sentence_start.IsNotNull() && sentence_start < range.StartPosition()
+ ? sentence_start
+ : range.StartPosition(),
+ range.EndPosition()));
+}
+
+VisiblePosition NextSentencePosition(const VisiblePosition& c) {
+ DCHECK(c.IsValid()) << c;
+ VisiblePosition next =
+ CreateVisiblePosition(NextBoundary(c, NextSentencePositionBoundary),
+ TextAffinity::kUpstreamIfPossible);
+ return AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ next, c.DeepEquivalent());
+}
+
+VisiblePosition PreviousSentencePosition(const VisiblePosition& c) {
+ DCHECK(c.IsValid()) << c;
+ VisiblePosition prev = CreateVisiblePosition(
+ PreviousBoundary(c, PreviousSentencePositionBoundary));
+ return AdjustBackwardPositionToAvoidCrossingEditingBoundaries(
+ prev, c.DeepEquivalent());
+}
+
+VisiblePosition StartOfSentence(const VisiblePosition& c) {
+ return StartOfSentenceAlgorithm<EditingStrategy>(c);
+}
+
+VisiblePositionInFlatTree StartOfSentence(const VisiblePositionInFlatTree& c) {
+ return StartOfSentenceAlgorithm<EditingInFlatTreeStrategy>(c);
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_units_sentence_test.cc b/chromium/third_party/blink/renderer/core/editing/visible_units_sentence_test.cc
new file mode 100644
index 00000000000..f2ed9b0f81b
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_units_sentence_test.cc
@@ -0,0 +1,162 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+#include "third_party/blink/renderer/core/layout/layout_text_fragment.h"
+
+namespace blink {
+
+class VisibleUnitsSentenceTest : public EditingTestBase {
+ protected:
+ static PositionWithAffinity PositionWithAffinityInDOMTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return PositionWithAffinity(CanonicalPositionOf(Position(&anchor, offset)),
+ affinity);
+ }
+
+ static VisiblePosition CreateVisiblePositionInDOMTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return CreateVisiblePosition(Position(&anchor, offset), affinity);
+ }
+
+ static PositionInFlatTreeWithAffinity PositionWithAffinityInFlatTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return PositionInFlatTreeWithAffinity(
+ CanonicalPositionOf(PositionInFlatTree(&anchor, offset)), affinity);
+ }
+
+ static VisiblePositionInFlatTree CreateVisiblePositionInFlatTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return CreateVisiblePosition(PositionInFlatTree(&anchor, offset), affinity);
+ }
+};
+
+TEST_F(VisibleUnitsSentenceTest, endOfSentence) {
+ const char* body_content = "<a id=host><b id=one>1</b><b id=two>22</b></a>";
+ const char* shadow_content =
+ "<p><i id=three>333</i> <content select=#two></content> <content "
+ "select=#one></content> <i id=four>4444</i></p>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* three = shadow_root->getElementById("three")->firstChild();
+ Node* four = shadow_root->getElementById("four")->firstChild();
+
+ EXPECT_EQ(
+ Position(two, 2),
+ EndOfSentence(CreateVisiblePositionInDOMTree(*one, 0)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(four, 4),
+ EndOfSentence(CreateVisiblePositionInFlatTree(*one, 0)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(two, 2),
+ EndOfSentence(CreateVisiblePositionInDOMTree(*one, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(four, 4),
+ EndOfSentence(CreateVisiblePositionInFlatTree(*one, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(two, 2),
+ EndOfSentence(CreateVisiblePositionInDOMTree(*two, 0)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(four, 4),
+ EndOfSentence(CreateVisiblePositionInFlatTree(*two, 0)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(two, 2),
+ EndOfSentence(CreateVisiblePositionInDOMTree(*two, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(four, 4),
+ EndOfSentence(CreateVisiblePositionInFlatTree(*two, 1)).DeepEquivalent());
+
+ EXPECT_EQ(Position(four, 4),
+ EndOfSentence(CreateVisiblePositionInDOMTree(*three, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(four, 4),
+ EndOfSentence(CreateVisiblePositionInFlatTree(*three, 1))
+ .DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(four, 4),
+ EndOfSentence(CreateVisiblePositionInDOMTree(*four, 1)).DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(four, 4),
+ EndOfSentence(CreateVisiblePositionInFlatTree(*four, 1))
+ .DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsSentenceTest, startOfSentence) {
+ const char* body_content = "<a id=host><b id=one>1</b><b id=two>22</b></a>";
+ const char* shadow_content =
+ "<p><i id=three>333</i> <content select=#two></content> <content "
+ "select=#one></content> <i id=four>4444</i></p>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* three = shadow_root->getElementById("three")->firstChild();
+ Node* four = shadow_root->getElementById("four")->firstChild();
+
+ EXPECT_EQ(Position(one, 0),
+ StartOfSentence(CreateVisiblePositionInDOMTree(*one, 0))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(three, 0),
+ StartOfSentence(CreateVisiblePositionInFlatTree(*one, 0))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(one, 0),
+ StartOfSentence(CreateVisiblePositionInDOMTree(*one, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(three, 0),
+ StartOfSentence(CreateVisiblePositionInFlatTree(*one, 1))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(one, 0),
+ StartOfSentence(CreateVisiblePositionInDOMTree(*two, 0))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(three, 0),
+ StartOfSentence(CreateVisiblePositionInFlatTree(*two, 0))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(one, 0),
+ StartOfSentence(CreateVisiblePositionInDOMTree(*two, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(three, 0),
+ StartOfSentence(CreateVisiblePositionInFlatTree(*two, 1))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(three, 0),
+ StartOfSentence(CreateVisiblePositionInDOMTree(*three, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(three, 0),
+ StartOfSentence(CreateVisiblePositionInFlatTree(*three, 1))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(three, 0),
+ StartOfSentence(CreateVisiblePositionInDOMTree(*four, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(three, 0),
+ StartOfSentence(CreateVisiblePositionInFlatTree(*four, 1))
+ .DeepEquivalent());
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_units_test.cc b/chromium/third_party/blink/renderer/core/editing/visible_units_test.cc
new file mode 100644
index 00000000000..719e0b0978d
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_units_test.cc
@@ -0,0 +1,783 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
+#include "third_party/blink/renderer/core/dom/text.h"
+#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
+#include "third_party/blink/renderer/core/layout/layout_text_fragment.h"
+
+namespace blink {
+namespace visible_units_test {
+
+PositionWithAffinity PositionWithAffinityInDOMTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return PositionWithAffinity(CanonicalPositionOf(Position(&anchor, offset)),
+ affinity);
+}
+
+VisiblePosition CreateVisiblePositionInDOMTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return CreateVisiblePosition(Position(&anchor, offset), affinity);
+}
+
+PositionInFlatTreeWithAffinity PositionWithAffinityInFlatTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return PositionInFlatTreeWithAffinity(
+ CanonicalPositionOf(PositionInFlatTree(&anchor, offset)), affinity);
+}
+
+VisiblePositionInFlatTree CreateVisiblePositionInFlatTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return CreateVisiblePosition(PositionInFlatTree(&anchor, offset), affinity);
+}
+
+class VisibleUnitsTest : public EditingTestBase {};
+
+TEST_F(VisibleUnitsTest, caretMinOffset) {
+ const char* body_content = "<p id=one>one</p>";
+ SetBodyContent(body_content);
+
+ Element* one = GetDocument().getElementById("one");
+
+ EXPECT_EQ(0, CaretMinOffset(one->firstChild()));
+}
+
+TEST_F(VisibleUnitsTest, caretMinOffsetWithFirstLetter) {
+ const char* body_content =
+ "<style>#one:first-letter { font-size: 200%; }</style><p id=one>one</p>";
+ SetBodyContent(body_content);
+
+ Element* one = GetDocument().getElementById("one");
+
+ EXPECT_EQ(0, CaretMinOffset(one->firstChild()));
+}
+
+TEST_F(VisibleUnitsTest, characterAfter) {
+ const char* body_content =
+ "<p id='host'><b id='one'>1</b><b id='two'>22</b></p><b "
+ "id='three'>333</b>";
+ const char* shadow_content =
+ "<b id='four'>4444</b><content select=#two></content><content "
+ "select=#one></content><b id='five'>5555</b>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ Element* one = GetDocument().getElementById("one");
+ Element* two = GetDocument().getElementById("two");
+
+ EXPECT_EQ('2', CharacterAfter(
+ CreateVisiblePositionInDOMTree(*one->firstChild(), 1)));
+ EXPECT_EQ('5', CharacterAfter(
+ CreateVisiblePositionInFlatTree(*one->firstChild(), 1)));
+
+ EXPECT_EQ(
+ 0, CharacterAfter(CreateVisiblePositionInDOMTree(*two->firstChild(), 2)));
+ EXPECT_EQ('1', CharacterAfter(
+ CreateVisiblePositionInFlatTree(*two->firstChild(), 2)));
+}
+
+TEST_F(VisibleUnitsTest, canonicalPositionOfWithHTMLHtmlElement) {
+ const char* body_content =
+ "<html><div id=one contenteditable>1</div><span id=two "
+ "contenteditable=false>22</span><span id=three "
+ "contenteditable=false>333</span><span id=four "
+ "contenteditable=false>333</span></html>";
+ SetBodyContent(body_content);
+
+ Node* one = GetDocument().QuerySelector("#one");
+ Node* two = GetDocument().QuerySelector("#two");
+ Node* three = GetDocument().QuerySelector("#three");
+ Node* four = GetDocument().QuerySelector("#four");
+ Element* html = GetDocument().CreateRawElement(HTMLNames::htmlTag);
+ // Move two, three and four into second html element.
+ html->AppendChild(two);
+ html->AppendChild(three);
+ html->AppendChild(four);
+ one->appendChild(html);
+ UpdateAllLifecyclePhases();
+
+ EXPECT_EQ(Position(),
+ CanonicalPositionOf(Position(GetDocument().documentElement(), 0)));
+
+ EXPECT_EQ(Position(one->firstChild(), 0),
+ CanonicalPositionOf(Position(one, 0)));
+ EXPECT_EQ(Position(one->firstChild(), 1),
+ CanonicalPositionOf(Position(one, 1)));
+
+ EXPECT_EQ(Position(one->firstChild(), 0),
+ CanonicalPositionOf(Position(one->firstChild(), 0)));
+ EXPECT_EQ(Position(one->firstChild(), 1),
+ CanonicalPositionOf(Position(one->firstChild(), 1)));
+
+ EXPECT_EQ(Position(html, 0), CanonicalPositionOf(Position(html, 0)));
+ EXPECT_EQ(Position(html, 1), CanonicalPositionOf(Position(html, 1)));
+ EXPECT_EQ(Position(html, 2), CanonicalPositionOf(Position(html, 2)));
+
+ EXPECT_EQ(Position(two->firstChild(), 0),
+ CanonicalPositionOf(Position(two, 0)));
+ EXPECT_EQ(Position(two->firstChild(), 2),
+ CanonicalPositionOf(Position(two, 1)));
+}
+
+// For http://crbug.com/695317
+TEST_F(VisibleUnitsTest, canonicalPositionOfWithInputElement) {
+ SetBodyContent("<input>123");
+ Element* const input = GetDocument().QuerySelector("input");
+
+ EXPECT_EQ(Position::BeforeNode(*input),
+ CanonicalPositionOf(Position::FirstPositionInNode(
+ *GetDocument().documentElement())));
+
+ EXPECT_EQ(PositionInFlatTree::BeforeNode(*input),
+ CanonicalPositionOf(PositionInFlatTree::FirstPositionInNode(
+ *GetDocument().documentElement())));
+}
+
+TEST_F(VisibleUnitsTest, characterBefore) {
+ const char* body_content =
+ "<p id=host><b id=one>1</b><b id=two>22</b></p><b id=three>333</b>";
+ const char* shadow_content =
+ "<b id=four>4444</b><content select=#two></content><content "
+ "select=#one></content><b id=five>5555</b>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* five = shadow_root->getElementById("five")->firstChild();
+
+ EXPECT_EQ(0, CharacterBefore(CreateVisiblePositionInDOMTree(*one, 0)));
+ EXPECT_EQ('2', CharacterBefore(CreateVisiblePositionInFlatTree(*one, 0)));
+
+ EXPECT_EQ('1', CharacterBefore(CreateVisiblePositionInDOMTree(*one, 1)));
+ EXPECT_EQ('1', CharacterBefore(CreateVisiblePositionInFlatTree(*one, 1)));
+
+ EXPECT_EQ('1', CharacterBefore(CreateVisiblePositionInDOMTree(*two, 0)));
+ EXPECT_EQ('4', CharacterBefore(CreateVisiblePositionInFlatTree(*two, 0)));
+
+ EXPECT_EQ('4', CharacterBefore(CreateVisiblePositionInDOMTree(*five, 0)));
+ EXPECT_EQ('1', CharacterBefore(CreateVisiblePositionInFlatTree(*five, 0)));
+}
+
+TEST_F(VisibleUnitsTest, endOfDocument) {
+ const char* body_content = "<a id=host><b id=one>1</b><b id=two>22</b></a>";
+ const char* shadow_content =
+ "<p><content select=#two></content></p><p><content "
+ "select=#one></content></p>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ Element* one = GetDocument().getElementById("one");
+ Element* two = GetDocument().getElementById("two");
+
+ EXPECT_EQ(Position(two->firstChild(), 2),
+ EndOfDocument(CreateVisiblePositionInDOMTree(*one->firstChild(), 0))
+ .DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(one->firstChild(), 1),
+ EndOfDocument(CreateVisiblePositionInFlatTree(*one->firstChild(), 0))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(two->firstChild(), 2),
+ EndOfDocument(CreateVisiblePositionInDOMTree(*two->firstChild(), 1))
+ .DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(one->firstChild(), 1),
+ EndOfDocument(CreateVisiblePositionInFlatTree(*two->firstChild(), 1))
+ .DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsTest,
+ AdjustForwardPositionToAvoidCrossingEditingBoundariesNestedEditable) {
+ const SelectionInDOMTree& selection = SetSelectionTextToBody(
+ "<div contenteditable>"
+ "abc"
+ "<span contenteditable=\"false\">A^BC</span>"
+ "d|ef"
+ "</div>");
+ const PositionWithAffinity& result =
+ AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ PositionWithAffinity(selection.Extent()), selection.Base());
+ ASSERT_TRUE(result.IsNotNull());
+ EXPECT_EQ(
+ "<div contenteditable>"
+ "abc"
+ "<span contenteditable=\"false\">ABC|</span>"
+ "def"
+ "</div>",
+ GetCaretTextFromBody(result.GetPosition()));
+ EXPECT_EQ(TextAffinity::kDownstream, result.Affinity());
+}
+
+TEST_F(VisibleUnitsTest, isEndOfEditableOrNonEditableContent) {
+ const char* body_content =
+ "<a id=host><b id=one contenteditable>1</b><b id=two>22</b></a>";
+ const char* shadow_content =
+ "<content select=#two></content></p><p><content select=#one></content>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ Element* one = GetDocument().getElementById("one");
+ Element* two = GetDocument().getElementById("two");
+
+ EXPECT_FALSE(IsEndOfEditableOrNonEditableContent(
+ CreateVisiblePositionInDOMTree(*one->firstChild(), 1)));
+ EXPECT_TRUE(IsEndOfEditableOrNonEditableContent(
+ CreateVisiblePositionInFlatTree(*one->firstChild(), 1)));
+
+ EXPECT_TRUE(IsEndOfEditableOrNonEditableContent(
+ CreateVisiblePositionInDOMTree(*two->firstChild(), 2)));
+ EXPECT_FALSE(IsEndOfEditableOrNonEditableContent(
+ CreateVisiblePositionInFlatTree(*two->firstChild(), 2)));
+}
+
+TEST_F(VisibleUnitsTest, isEndOfEditableOrNonEditableContentWithInput) {
+ const char* body_content = "<input id=sample value=ab>cde";
+ SetBodyContent(body_content);
+
+ Node* text = ToTextControl(GetDocument().getElementById("sample"))
+ ->InnerEditorElement()
+ ->firstChild();
+
+ EXPECT_FALSE(IsEndOfEditableOrNonEditableContent(
+ CreateVisiblePositionInDOMTree(*text, 0)));
+ EXPECT_FALSE(IsEndOfEditableOrNonEditableContent(
+ CreateVisiblePositionInFlatTree(*text, 0)));
+
+ EXPECT_FALSE(IsEndOfEditableOrNonEditableContent(
+ CreateVisiblePositionInDOMTree(*text, 1)));
+ EXPECT_FALSE(IsEndOfEditableOrNonEditableContent(
+ CreateVisiblePositionInFlatTree(*text, 1)));
+
+ EXPECT_TRUE(IsEndOfEditableOrNonEditableContent(
+ CreateVisiblePositionInDOMTree(*text, 2)));
+ EXPECT_TRUE(IsEndOfEditableOrNonEditableContent(
+ CreateVisiblePositionInFlatTree(*text, 2)));
+}
+
+TEST_F(VisibleUnitsTest, IsVisuallyEquivalentCandidateWithHTMLHtmlElement) {
+ const char* body_content =
+ "<html><div id=one contenteditable>1</div><span id=two "
+ "contenteditable=false>22</span><span id=three "
+ "contenteditable=false>333</span><span id=four "
+ "contenteditable=false>333</span></html>";
+ SetBodyContent(body_content);
+
+ Node* one = GetDocument().QuerySelector("#one");
+ Node* two = GetDocument().QuerySelector("#two");
+ Node* three = GetDocument().QuerySelector("#three");
+ Node* four = GetDocument().QuerySelector("#four");
+ Element* html = GetDocument().CreateRawElement(HTMLNames::htmlTag);
+ // Move two, three and four into second html element.
+ html->AppendChild(two);
+ html->AppendChild(three);
+ html->AppendChild(four);
+ one->appendChild(html);
+ UpdateAllLifecyclePhases();
+
+ EXPECT_FALSE(IsVisuallyEquivalentCandidate(
+ Position(GetDocument().documentElement(), 0)));
+
+ EXPECT_FALSE(IsVisuallyEquivalentCandidate(Position(one, 0)));
+ EXPECT_FALSE(IsVisuallyEquivalentCandidate(Position(one, 1)));
+
+ EXPECT_TRUE(IsVisuallyEquivalentCandidate(Position(one->firstChild(), 0)));
+ EXPECT_TRUE(IsVisuallyEquivalentCandidate(Position(one->firstChild(), 1)));
+
+ EXPECT_TRUE(IsVisuallyEquivalentCandidate(Position(html, 0)));
+ EXPECT_TRUE(IsVisuallyEquivalentCandidate(Position(html, 1)));
+ EXPECT_TRUE(IsVisuallyEquivalentCandidate(Position(html, 2)));
+
+ EXPECT_FALSE(IsVisuallyEquivalentCandidate(Position(two, 0)));
+ EXPECT_FALSE(IsVisuallyEquivalentCandidate(Position(two, 1)));
+}
+
+TEST_F(VisibleUnitsTest, isVisuallyEquivalentCandidateWithHTMLBodyElement) {
+ const char* body_content =
+ "<div id=one contenteditable>1</div><span id=two "
+ "contenteditable=false>22</span><span id=three "
+ "contenteditable=false>333</span><span id=four "
+ "contenteditable=false>333</span>";
+ SetBodyContent(body_content);
+
+ Node* one = GetDocument().QuerySelector("#one");
+ Node* two = GetDocument().QuerySelector("#two");
+ Node* three = GetDocument().QuerySelector("#three");
+ Node* four = GetDocument().QuerySelector("#four");
+ Element* body = GetDocument().CreateRawElement(HTMLNames::bodyTag);
+ Element* empty_body = GetDocument().CreateRawElement(HTMLNames::bodyTag);
+ Element* div = GetDocument().CreateRawElement(HTMLNames::divTag);
+ Element* br = GetDocument().CreateRawElement(HTMLNames::brTag);
+ empty_body->appendChild(div);
+ empty_body->appendChild(br);
+ one->appendChild(empty_body);
+ // Move two, three and four into second body element.
+ body->appendChild(two);
+ body->AppendChild(three);
+ body->AppendChild(four);
+ one->appendChild(body);
+ GetDocument().UpdateStyleAndLayout();
+
+ EXPECT_FALSE(IsVisuallyEquivalentCandidate(
+ Position(GetDocument().documentElement(), 0)));
+
+ EXPECT_FALSE(IsVisuallyEquivalentCandidate(Position(one, 0)));
+ EXPECT_FALSE(IsVisuallyEquivalentCandidate(Position(one, 1)));
+
+ EXPECT_TRUE(IsVisuallyEquivalentCandidate(Position(one->firstChild(), 0)));
+ EXPECT_TRUE(IsVisuallyEquivalentCandidate(Position(one->firstChild(), 1)));
+
+ EXPECT_TRUE(IsVisuallyEquivalentCandidate(Position(body, 0)));
+ EXPECT_TRUE(IsVisuallyEquivalentCandidate(Position(body, 1)));
+ EXPECT_TRUE(IsVisuallyEquivalentCandidate(Position(body, 2)));
+
+ EXPECT_FALSE(IsVisuallyEquivalentCandidate(Position(two, 0)));
+ EXPECT_FALSE(IsVisuallyEquivalentCandidate(Position(two, 1)));
+
+ EXPECT_FALSE(IsVisuallyEquivalentCandidate(Position(empty_body, 0)));
+ EXPECT_FALSE(IsVisuallyEquivalentCandidate(Position(empty_body, 1)));
+}
+
+TEST_F(VisibleUnitsTest, isVisuallyEquivalentCandidateWithDocument) {
+ UpdateAllLifecyclePhases();
+
+ EXPECT_FALSE(IsVisuallyEquivalentCandidate(Position(&GetDocument(), 0)));
+}
+
+TEST_F(VisibleUnitsTest, mostBackwardCaretPositionAfterAnchor) {
+ const char* body_content =
+ "<p id='host'><b id='one'>1</b></p><b id='two'>22</b>";
+ const char* shadow_content =
+ "<b id='two'>22</b><content select=#one></content><b id='three'>333</b>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ Element* host = GetDocument().getElementById("host");
+
+ EXPECT_EQ(Position::LastPositionInNode(*host),
+ MostForwardCaretPosition(Position::AfterNode(*host)));
+ EXPECT_EQ(PositionInFlatTree::LastPositionInNode(*host),
+ MostForwardCaretPosition(PositionInFlatTree::AfterNode(*host)));
+}
+
+TEST_F(VisibleUnitsTest, mostBackwardCaretPositionFirstLetter) {
+ // Note: first-letter pseudo element contains letter and punctuations.
+ const char* body_content =
+ "<style>p:first-letter {color:red;}</style><p id=sample> (2)45 </p>";
+ SetBodyContent(body_content);
+
+ Node* sample = GetDocument().getElementById("sample")->firstChild();
+
+ EXPECT_EQ(Position(sample->parentNode(), 0),
+ MostBackwardCaretPosition(Position(sample, 0)));
+ EXPECT_EQ(Position(sample->parentNode(), 0),
+ MostBackwardCaretPosition(Position(sample, 1)));
+ EXPECT_EQ(Position(sample, 2),
+ MostBackwardCaretPosition(Position(sample, 2)));
+ EXPECT_EQ(Position(sample, 3),
+ MostBackwardCaretPosition(Position(sample, 3)));
+ EXPECT_EQ(Position(sample, 4),
+ MostBackwardCaretPosition(Position(sample, 4)));
+ EXPECT_EQ(Position(sample, 5),
+ MostBackwardCaretPosition(Position(sample, 5)));
+ EXPECT_EQ(Position(sample, 6),
+ MostBackwardCaretPosition(Position(sample, 6)));
+ EXPECT_EQ(Position(sample, 6),
+ MostBackwardCaretPosition(Position(sample, 7)));
+ EXPECT_EQ(Position(sample, 6),
+ MostBackwardCaretPosition(
+ Position::LastPositionInNode(*sample->parentNode())));
+ EXPECT_EQ(
+ Position(sample, 6),
+ MostBackwardCaretPosition(Position::AfterNode(*sample->parentNode())));
+ EXPECT_EQ(Position::LastPositionInNode(*GetDocument().body()),
+ MostBackwardCaretPosition(
+ Position::LastPositionInNode(*GetDocument().body())));
+}
+
+TEST_F(VisibleUnitsTest, mostBackwardCaretPositionFirstLetterSplit) {
+ V8TestingScope scope;
+
+ const char* body_content =
+ "<style>p:first-letter {color:red;}</style><p id=sample>abc</p>";
+ SetBodyContent(body_content);
+
+ Node* sample = GetDocument().getElementById("sample");
+ Node* first_letter = sample->firstChild();
+ // Split "abc" into "a" "bc"
+ Text* remaining = ToText(first_letter)->splitText(1, ASSERT_NO_EXCEPTION);
+ UpdateAllLifecyclePhases();
+
+ EXPECT_EQ(Position(sample, 0),
+ MostBackwardCaretPosition(Position(first_letter, 0)));
+ EXPECT_EQ(Position(first_letter, 1),
+ MostBackwardCaretPosition(Position(first_letter, 1)));
+ EXPECT_EQ(Position(first_letter, 1),
+ MostBackwardCaretPosition(Position(remaining, 0)));
+ EXPECT_EQ(Position(remaining, 1),
+ MostBackwardCaretPosition(Position(remaining, 1)));
+ EXPECT_EQ(Position(remaining, 2),
+ MostBackwardCaretPosition(Position(remaining, 2)));
+ EXPECT_EQ(Position(remaining, 2),
+ MostBackwardCaretPosition(Position::LastPositionInNode(*sample)));
+ EXPECT_EQ(Position(remaining, 2),
+ MostBackwardCaretPosition(Position::AfterNode(*sample)));
+}
+
+TEST_F(VisibleUnitsTest, mostForwardCaretPositionAfterAnchor) {
+ const char* body_content = "<p id='host'><b id='one'>1</b></p>";
+ const char* shadow_content =
+ "<b id='two'>22</b><content select=#one></content><b id='three'>333</b>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+ UpdateAllLifecyclePhases();
+
+ Element* host = GetDocument().getElementById("host");
+ Element* one = GetDocument().getElementById("one");
+ Element* three = shadow_root->getElementById("three");
+
+ EXPECT_EQ(Position(one->firstChild(), 1),
+ MostBackwardCaretPosition(Position::AfterNode(*host)));
+ EXPECT_EQ(PositionInFlatTree(three->firstChild(), 3),
+ MostBackwardCaretPosition(PositionInFlatTree::AfterNode(*host)));
+}
+
+TEST_F(VisibleUnitsTest, mostForwardCaretPositionFirstLetter) {
+ // Note: first-letter pseudo element contains letter and punctuations.
+ const char* body_content =
+ "<style>p:first-letter {color:red;}</style><p id=sample> (2)45 </p>";
+ SetBodyContent(body_content);
+
+ Node* sample = GetDocument().getElementById("sample")->firstChild();
+
+ EXPECT_EQ(Position(GetDocument().body(), 0),
+ MostForwardCaretPosition(
+ Position::FirstPositionInNode(*GetDocument().body())));
+ EXPECT_EQ(
+ Position(sample, 1),
+ MostForwardCaretPosition(Position::BeforeNode(*sample->parentNode())));
+ EXPECT_EQ(Position(sample, 1),
+ MostForwardCaretPosition(
+ Position::FirstPositionInNode(*sample->parentNode())));
+ EXPECT_EQ(Position(sample, 1), MostForwardCaretPosition(Position(sample, 0)));
+ EXPECT_EQ(Position(sample, 1), MostForwardCaretPosition(Position(sample, 1)));
+ EXPECT_EQ(Position(sample, 2), MostForwardCaretPosition(Position(sample, 2)));
+ EXPECT_EQ(Position(sample, 3), MostForwardCaretPosition(Position(sample, 3)));
+ EXPECT_EQ(Position(sample, 4), MostForwardCaretPosition(Position(sample, 4)));
+ EXPECT_EQ(Position(sample, 5), MostForwardCaretPosition(Position(sample, 5)));
+ EXPECT_EQ(Position(sample, 7), MostForwardCaretPosition(Position(sample, 6)));
+ EXPECT_EQ(Position(sample, 7), MostForwardCaretPosition(Position(sample, 7)));
+}
+
+TEST_F(VisibleUnitsTest, nextPositionOf) {
+ const char* body_content =
+ "<b id=zero>0</b><p id=host><b id=one>1</b><b id=two>22</b></p><b "
+ "id=three>333</b>";
+ const char* shadow_content =
+ "<b id=four>4444</b><content select=#two></content><content "
+ "select=#one></content><b id=five>55555</b>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Element* zero = GetDocument().getElementById("zero");
+ Element* one = GetDocument().getElementById("one");
+ Element* two = GetDocument().getElementById("two");
+ Element* three = GetDocument().getElementById("three");
+ Element* four = shadow_root->getElementById("four");
+ Element* five = shadow_root->getElementById("five");
+
+ EXPECT_EQ(Position(one->firstChild(), 0),
+ NextPositionOf(CreateVisiblePosition(Position(zero, 1)))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(four->firstChild(), 0),
+ NextPositionOf(CreateVisiblePosition(PositionInFlatTree(zero, 1)))
+ .DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(one->firstChild(), 1),
+ NextPositionOf(CreateVisiblePosition(Position(one, 0))).DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(one->firstChild(), 1),
+ NextPositionOf(CreateVisiblePosition(PositionInFlatTree(one, 0)))
+ .DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(two->firstChild(), 1),
+ NextPositionOf(CreateVisiblePosition(Position(one, 1))).DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(five->firstChild(), 1),
+ NextPositionOf(CreateVisiblePosition(PositionInFlatTree(one, 1)))
+ .DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(three->firstChild(), 0),
+ NextPositionOf(CreateVisiblePosition(Position(two, 1))).DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(one->firstChild(), 1),
+ NextPositionOf(CreateVisiblePosition(PositionInFlatTree(two, 1)))
+ .DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsTest, previousPositionOf) {
+ const char* body_content =
+ "<b id=zero>0</b><p id=host><b id=one>1</b><b id=two>22</b></p><b "
+ "id=three>333</b>";
+ const char* shadow_content =
+ "<b id=four>4444</b><content select=#two></content><content "
+ "select=#one></content><b id=five>55555</b>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* zero = GetDocument().getElementById("zero")->firstChild();
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* three = GetDocument().getElementById("three")->firstChild();
+ Node* four = shadow_root->getElementById("four")->firstChild();
+ Node* five = shadow_root->getElementById("five")->firstChild();
+
+ EXPECT_EQ(Position(zero, 0),
+ PreviousPositionOf(CreateVisiblePosition(Position(zero, 1)))
+ .DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(zero, 0),
+ PreviousPositionOf(CreateVisiblePosition(PositionInFlatTree(zero, 1)))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(zero, 1),
+ PreviousPositionOf(CreateVisiblePosition(Position(one, 0)))
+ .DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(two, 1),
+ PreviousPositionOf(CreateVisiblePosition(PositionInFlatTree(one, 0)))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(one, 0),
+ PreviousPositionOf(CreateVisiblePosition(Position(one, 1)))
+ .DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(two, 2),
+ PreviousPositionOf(CreateVisiblePosition(PositionInFlatTree(one, 1)))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(one, 0),
+ PreviousPositionOf(CreateVisiblePosition(Position(two, 0)))
+ .DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(four, 3),
+ PreviousPositionOf(CreateVisiblePosition(PositionInFlatTree(two, 0)))
+ .DeepEquivalent());
+
+ // DOM tree to shadow tree
+ EXPECT_EQ(Position(two, 2),
+ PreviousPositionOf(CreateVisiblePosition(Position(three, 0)))
+ .DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(five, 5),
+ PreviousPositionOf(CreateVisiblePosition(PositionInFlatTree(three, 0)))
+ .DeepEquivalent());
+
+ // Shadow tree to DOM tree
+ EXPECT_EQ(Position(),
+ PreviousPositionOf(CreateVisiblePosition(Position(four, 0)))
+ .DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(zero, 1),
+ PreviousPositionOf(CreateVisiblePosition(PositionInFlatTree(four, 0)))
+ .DeepEquivalent());
+
+ // Note: Canonicalization maps (five, 0) to (four, 4) in DOM tree and
+ // (one, 1) in flat tree.
+ EXPECT_EQ(Position(four, 4),
+ PreviousPositionOf(CreateVisiblePosition(Position(five, 1)))
+ .DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(one, 1),
+ PreviousPositionOf(CreateVisiblePosition(PositionInFlatTree(five, 1)))
+ .DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsTest, previousPositionOfOneCharPerLine) {
+ const char* body_content =
+ "<div id=sample style='font-size: 500px'>A&#x714a;&#xfa67;</div>";
+ SetBodyContent(body_content);
+
+ Node* sample = GetDocument().getElementById("sample")->firstChild();
+
+ // In case of each line has one character, VisiblePosition are:
+ // [C,Dn] [C,Up] [B, Dn] [B, Up]
+ // A A A A|
+ // B B| |B B
+ // |C C C C
+ EXPECT_EQ(PositionWithAffinity(Position(sample, 1)),
+ PreviousPositionOf(CreateVisiblePosition(Position(sample, 2)))
+ .ToPositionWithAffinity());
+ EXPECT_EQ(PositionWithAffinity(Position(sample, 1)),
+ PreviousPositionOf(CreateVisiblePosition(Position(sample, 2),
+ TextAffinity::kUpstream))
+ .ToPositionWithAffinity());
+}
+
+TEST_F(VisibleUnitsTest, previousPositionOfNoPreviousPosition) {
+ SetBodyContent(
+ "<span contenteditable='true'>"
+ "<span> </span>"
+ " " // This whitespace causes no previous position.
+ "<div id='anchor'> bar</div>"
+ "</span>");
+ const Position position(GetDocument().getElementById("anchor")->firstChild(),
+ 1);
+ EXPECT_EQ(
+ Position(),
+ PreviousPositionOf(CreateVisiblePosition(position)).DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsTest, rendersInDifferentPositionAfterAnchor) {
+ const char* body_content = "<p id='sample'>00</p>";
+ SetBodyContent(body_content);
+ Element* sample = GetDocument().getElementById("sample");
+
+ EXPECT_FALSE(RendersInDifferentPosition(Position(), Position()));
+ EXPECT_FALSE(
+ RendersInDifferentPosition(Position(), Position::AfterNode(*sample)))
+ << "if one of position is null, the reuslt is false.";
+ EXPECT_FALSE(RendersInDifferentPosition(Position::AfterNode(*sample),
+ Position(sample, 1)));
+ EXPECT_FALSE(RendersInDifferentPosition(Position::LastPositionInNode(*sample),
+ Position(sample, 1)));
+}
+
+TEST_F(VisibleUnitsTest, rendersInDifferentPositionAfterAnchorWithHidden) {
+ const char* body_content =
+ "<p><span id=one>11</span><span id=two style='display:none'> "
+ "</span></p>";
+ SetBodyContent(body_content);
+ Element* one = GetDocument().getElementById("one");
+ Element* two = GetDocument().getElementById("two");
+
+ EXPECT_TRUE(RendersInDifferentPosition(Position::LastPositionInNode(*one),
+ Position(two, 0)))
+ << "two doesn't have layout object";
+}
+
+TEST_F(VisibleUnitsTest,
+ rendersInDifferentPositionAfterAnchorWithDifferentLayoutObjects) {
+ const char* body_content =
+ "<p><span id=one>11</span><span id=two> </span></p>";
+ SetBodyContent(body_content);
+ Element* one = GetDocument().getElementById("one");
+ Element* two = GetDocument().getElementById("two");
+
+ EXPECT_FALSE(RendersInDifferentPosition(Position::LastPositionInNode(*one),
+ Position(two, 0)));
+ EXPECT_FALSE(RendersInDifferentPosition(Position::LastPositionInNode(*one),
+ Position(two, 1)))
+ << "width of two is zero since contents is collapsed whitespaces";
+}
+
+TEST_F(VisibleUnitsTest, renderedOffset) {
+ const char* body_content =
+ "<div contenteditable><span id='sample1'>1</span><span "
+ "id='sample2'>22</span></div>";
+ SetBodyContent(body_content);
+ Element* sample1 = GetDocument().getElementById("sample1");
+ Element* sample2 = GetDocument().getElementById("sample2");
+
+ EXPECT_FALSE(
+ RendersInDifferentPosition(Position::AfterNode(*sample1->firstChild()),
+ Position(sample2->firstChild(), 0)));
+ EXPECT_FALSE(RendersInDifferentPosition(
+ Position::LastPositionInNode(*sample1->firstChild()),
+ Position(sample2->firstChild(), 0)));
+}
+
+TEST_F(VisibleUnitsTest, startOfDocument) {
+ const char* body_content = "<a id=host><b id=one>1</b><b id=two>22</b></a>";
+ const char* shadow_content =
+ "<p><content select=#two></content></p><p><content "
+ "select=#one></content></p>";
+ SetBodyContent(body_content);
+ SetShadowContent(shadow_content, "host");
+
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+
+ EXPECT_EQ(Position(one, 0),
+ StartOfDocument(CreateVisiblePositionInDOMTree(*one, 0))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(two, 0),
+ StartOfDocument(CreateVisiblePositionInFlatTree(*one, 0))
+ .DeepEquivalent());
+
+ EXPECT_EQ(Position(one, 0),
+ StartOfDocument(CreateVisiblePositionInDOMTree(*two, 1))
+ .DeepEquivalent());
+ EXPECT_EQ(PositionInFlatTree(two, 0),
+ StartOfDocument(CreateVisiblePositionInFlatTree(*two, 1))
+ .DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsTest,
+ endsOfNodeAreVisuallyDistinctPositionsWithInvisibleChild) {
+ // Repro case of crbug.com/582247
+ const char* body_content =
+ "<button> </button><script>document.designMode = 'on'</script>";
+ SetBodyContent(body_content);
+
+ Node* button = GetDocument().QuerySelector("button");
+ EXPECT_TRUE(EndsOfNodeAreVisuallyDistinctPositions(button));
+}
+
+TEST_F(VisibleUnitsTest,
+ endsOfNodeAreVisuallyDistinctPositionsWithEmptyLayoutChild) {
+ // Repro case of crbug.com/584030
+ const char* body_content =
+ "<button><rt><script>document.designMode = 'on'</script></rt></button>";
+ SetBodyContent(body_content);
+
+ Node* button = GetDocument().QuerySelector("button");
+ EXPECT_TRUE(EndsOfNodeAreVisuallyDistinctPositions(button));
+}
+
+// Regression test for crbug.com/675429
+TEST_F(VisibleUnitsTest,
+ canonicalizationWithCollapsedSpaceAndIsolatedCombiningCharacter) {
+ SetBodyContent("<p> &#x20E3;</p>"); // Leading space is necessary
+
+ Node* paragraph = GetDocument().QuerySelector("p");
+ Node* text = paragraph->firstChild();
+ Position start = CanonicalPositionOf(Position::BeforeNode(*paragraph));
+ EXPECT_EQ(Position(text, 2), start);
+}
+
+static unsigned MockBoundarySearch(const UChar*,
+ unsigned,
+ unsigned,
+ BoundarySearchContextAvailability,
+ bool&) {
+ return true;
+}
+
+// Regression test for crbug.com/788661
+TEST_F(VisibleUnitsTest, NextBoundaryOfEditableTableWithLeadingSpaceInOutput) {
+ VisiblePosition pos = CreateVisiblePosition(SetCaretTextToBody(
+ // The leading whitespace is necessary for bug repro
+ "<output> <table contenteditable><!--|--></table></output>"));
+ Position result = NextBoundary(pos, MockBoundarySearch);
+ EXPECT_EQ("<output> <table contenteditable>|</table></output>",
+ GetCaretTextFromBody(result));
+}
+
+} // namespace visible_units_test
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_units_word.cc b/chromium/third_party/blink/renderer/core/editing/visible_units_word.cc
new file mode 100644
index 00000000000..327143ebc63
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_units_word.cc
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+
+#include "third_party/blink/renderer/core/editing/editing_utilities.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
+#include "third_party/blink/renderer/platform/text/text_boundaries.h"
+
+namespace blink {
+
+namespace {
+
+unsigned EndWordBoundary(
+ const UChar* characters,
+ unsigned length,
+ unsigned offset,
+ BoundarySearchContextAvailability may_have_more_context,
+ bool& need_more_context) {
+ DCHECK_LE(offset, length);
+ if (may_have_more_context &&
+ EndOfFirstWordBoundaryContext(characters + offset, length - offset) ==
+ static_cast<int>(length - offset)) {
+ need_more_context = true;
+ return length;
+ }
+ need_more_context = false;
+ return FindWordEndBoundary(characters, length, offset);
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> EndOfWordAlgorithm(
+ const VisiblePositionTemplate<Strategy>& c,
+ EWordSide side) {
+ DCHECK(c.IsValid()) << c;
+ VisiblePositionTemplate<Strategy> p = c;
+ if (side == kPreviousWordIfOnBoundary) {
+ if (IsStartOfParagraph(c))
+ return c.DeepEquivalent();
+
+ p = PreviousPositionOf(c);
+ if (p.IsNull())
+ return c.DeepEquivalent();
+ } else if (IsEndOfParagraph(c)) {
+ return c.DeepEquivalent();
+ }
+
+ return NextBoundary(p, EndWordBoundary);
+}
+
+unsigned NextWordPositionBoundary(
+ const UChar* characters,
+ unsigned length,
+ unsigned offset,
+ BoundarySearchContextAvailability may_have_more_context,
+ bool& need_more_context) {
+ if (may_have_more_context &&
+ EndOfFirstWordBoundaryContext(characters + offset, length - offset) ==
+ static_cast<int>(length - offset)) {
+ need_more_context = true;
+ return length;
+ }
+ need_more_context = false;
+ return FindNextWordForward(characters, length, offset);
+}
+
+unsigned PreviousWordPositionBoundary(
+ const UChar* characters,
+ unsigned length,
+ unsigned offset,
+ BoundarySearchContextAvailability may_have_more_context,
+ bool& need_more_context) {
+ if (may_have_more_context &&
+ !StartOfLastWordBoundaryContext(characters, offset)) {
+ need_more_context = true;
+ return 0;
+ }
+ need_more_context = false;
+ return FindNextWordBackward(characters, length, offset);
+}
+
+unsigned StartWordBoundary(
+ const UChar* characters,
+ unsigned length,
+ unsigned offset,
+ BoundarySearchContextAvailability may_have_more_context,
+ bool& need_more_context) {
+ TRACE_EVENT0("blink", "startWordBoundary");
+ DCHECK(offset);
+ if (may_have_more_context &&
+ !StartOfLastWordBoundaryContext(characters, offset)) {
+ need_more_context = true;
+ return 0;
+ }
+ need_more_context = false;
+ U16_BACK_1(characters, 0, offset);
+ return FindWordStartBoundary(characters, length, offset);
+}
+
+template <typename Strategy>
+PositionTemplate<Strategy> StartOfWordAlgorithm(
+ const VisiblePositionTemplate<Strategy>& c,
+ EWordSide side) {
+ DCHECK(c.IsValid()) << c;
+ // TODO(yosin) This returns a null VP for c at the start of the document
+ // and |side| == |kPreviousWordIfOnBoundary|
+ VisiblePositionTemplate<Strategy> p = c;
+ if (side == kNextWordIfOnBoundary) {
+ // at paragraph end, the startofWord is the current position
+ if (IsEndOfParagraph(c))
+ return c.DeepEquivalent();
+
+ p = NextPositionOf(c);
+ if (p.IsNull())
+ return c.DeepEquivalent();
+ }
+ return PreviousBoundary(p, StartWordBoundary);
+}
+
+} // namespace
+
+Position EndOfWordPosition(const VisiblePosition& position, EWordSide side) {
+ return EndOfWordAlgorithm<EditingStrategy>(position, side);
+}
+
+VisiblePosition EndOfWord(const VisiblePosition& position, EWordSide side) {
+ return CreateVisiblePosition(EndOfWordPosition(position, side),
+ TextAffinity::kUpstreamIfPossible);
+}
+
+PositionInFlatTree EndOfWordPosition(const VisiblePositionInFlatTree& position,
+ EWordSide side) {
+ return EndOfWordAlgorithm<EditingInFlatTreeStrategy>(position, side);
+}
+
+VisiblePositionInFlatTree EndOfWord(const VisiblePositionInFlatTree& position,
+ EWordSide side) {
+ return CreateVisiblePosition(EndOfWordPosition(position, side),
+ TextAffinity::kUpstreamIfPossible);
+}
+
+VisiblePosition NextWordPosition(const VisiblePosition& c) {
+ DCHECK(c.IsValid()) << c;
+ VisiblePosition next =
+ CreateVisiblePosition(NextBoundary(c, NextWordPositionBoundary),
+ TextAffinity::kUpstreamIfPossible);
+ return AdjustForwardPositionToAvoidCrossingEditingBoundaries(
+ next, c.DeepEquivalent());
+}
+
+VisiblePosition PreviousWordPosition(const VisiblePosition& c) {
+ DCHECK(c.IsValid()) << c;
+ VisiblePosition prev =
+ CreateVisiblePosition(PreviousBoundary(c, PreviousWordPositionBoundary));
+ return AdjustBackwardPositionToAvoidCrossingEditingBoundaries(
+ prev, c.DeepEquivalent());
+}
+
+Position StartOfWordPosition(const VisiblePosition& position, EWordSide side) {
+ return StartOfWordAlgorithm<EditingStrategy>(position, side);
+}
+
+VisiblePosition StartOfWord(const VisiblePosition& position, EWordSide side) {
+ return CreateVisiblePosition(StartOfWordPosition(position, side));
+}
+
+PositionInFlatTree StartOfWordPosition(
+ const VisiblePositionInFlatTree& position,
+ EWordSide side) {
+ return StartOfWordAlgorithm<EditingInFlatTreeStrategy>(position, side);
+}
+
+VisiblePositionInFlatTree StartOfWord(const VisiblePositionInFlatTree& position,
+ EWordSide side) {
+ return CreateVisiblePosition(StartOfWordPosition(position, side));
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/visible_units_word_test.cc b/chromium/third_party/blink/renderer/core/editing/visible_units_word_test.cc
new file mode 100644
index 00000000000..7ad7fa0cb6c
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/visible_units_word_test.cc
@@ -0,0 +1,455 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
+#include "third_party/blink/renderer/core/editing/visible_position.h"
+
+namespace blink {
+
+class VisibleUnitsWordTest : public EditingTestBase {
+ protected:
+ std::string DoStartOfWord(
+ const std::string& selection_text,
+ EWordSide word_side = EWordSide::kNextWordIfOnBoundary) {
+ const Position position = SetSelectionTextToBody(selection_text).Base();
+ return GetCaretTextFromBody(
+ StartOfWord(CreateVisiblePosition(position), word_side)
+ .DeepEquivalent());
+ }
+
+ std::string DoEndOfWord(
+ const std::string& selection_text,
+ EWordSide word_side = EWordSide::kNextWordIfOnBoundary) {
+ const Position position = SetSelectionTextToBody(selection_text).Base();
+ return GetCaretTextFromBody(
+ EndOfWord(CreateVisiblePosition(position), word_side).DeepEquivalent());
+ }
+
+ std::string DoNextWord(const std::string& selection_text) {
+ const Position position = SetSelectionTextToBody(selection_text).Base();
+ return GetCaretTextFromBody(
+ NextWordPosition(CreateVisiblePosition(position)).DeepEquivalent());
+ }
+
+ std::string DoPreviousWord(const std::string& selection_text) {
+ const Position position = SetSelectionTextToBody(selection_text).Base();
+ const Position result =
+ PreviousWordPosition(CreateVisiblePosition(position)).DeepEquivalent();
+ if (result.IsNull())
+ return GetSelectionTextFromBody(SelectionInDOMTree());
+ return GetCaretTextFromBody(result);
+ }
+
+ // To avoid name conflict in jumbo build, following functions should be here.
+ static VisiblePosition CreateVisiblePositionInDOMTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return CreateVisiblePosition(Position(&anchor, offset), affinity);
+ }
+
+ static VisiblePositionInFlatTree CreateVisiblePositionInFlatTree(
+ Node& anchor,
+ int offset,
+ TextAffinity affinity = TextAffinity::kDownstream) {
+ return CreateVisiblePosition(PositionInFlatTree(&anchor, offset), affinity);
+ }
+};
+
+TEST_F(VisibleUnitsWordTest, StartOfWordBasic) {
+ EXPECT_EQ("<p> |(1) abc def</p>", DoStartOfWord("<p>| (1) abc def</p>"));
+ EXPECT_EQ("<p> |(1) abc def</p>", DoStartOfWord("<p> |(1) abc def</p>"));
+ EXPECT_EQ("<p> (|1) abc def</p>", DoStartOfWord("<p> (|1) abc def</p>"));
+ EXPECT_EQ("<p> (1|) abc def</p>", DoStartOfWord("<p> (1|) abc def</p>"));
+ EXPECT_EQ("<p> (1)| abc def</p>", DoStartOfWord("<p> (1)| abc def</p>"));
+ EXPECT_EQ("<p> (1) |abc def</p>", DoStartOfWord("<p> (1) |abc def</p>"));
+ EXPECT_EQ("<p> (1) |abc def</p>", DoStartOfWord("<p> (1) a|bc def</p>"));
+ EXPECT_EQ("<p> (1) |abc def</p>", DoStartOfWord("<p> (1) ab|c def</p>"));
+ EXPECT_EQ("<p> (1) abc| def</p>", DoStartOfWord("<p> (1) abc| def</p>"));
+ EXPECT_EQ("<p> (1) abc |def</p>", DoStartOfWord("<p> (1) abc |def</p>"));
+ EXPECT_EQ("<p> (1) abc |def</p>", DoStartOfWord("<p> (1) abc d|ef</p>"));
+ EXPECT_EQ("<p> (1) abc |def</p>", DoStartOfWord("<p> (1) abc de|f</p>"));
+ EXPECT_EQ("<p> (1) abc def|</p>", DoStartOfWord("<p> (1) abc def|</p>"));
+ EXPECT_EQ("<p> (1) abc def|</p>", DoStartOfWord("<p> (1) abc def</p>|"));
+}
+
+TEST_F(VisibleUnitsWordTest, StartOfWordPreviousWordIfOnBoundaryBasic) {
+ EXPECT_EQ("<p> |(1) abc def</p>",
+ DoStartOfWord("<p>| (1) abc def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> |(1) abc def</p>",
+ DoStartOfWord("<p> |(1) abc def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> |(1) abc def</p>",
+ DoStartOfWord("<p> (|1) abc def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (|1) abc def</p>",
+ DoStartOfWord("<p> (1|) abc def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1|) abc def</p>",
+ DoStartOfWord("<p> (1)| abc def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1)| abc def</p>",
+ DoStartOfWord("<p> (1) |abc def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) |abc def</p>",
+ DoStartOfWord("<p> (1) a|bc def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) |abc def</p>",
+ DoStartOfWord("<p> (1) ab|c def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) |abc def</p>",
+ DoStartOfWord("<p> (1) abc| def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) abc| def</p>",
+ DoStartOfWord("<p> (1) abc |def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) abc |def</p>",
+ DoStartOfWord("<p> (1) abc d|ef</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) abc |def</p>",
+ DoStartOfWord("<p> (1) abc de|f</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) abc |def</p>",
+ DoStartOfWord("<p> (1) abc def|</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) abc |def</p>",
+ DoStartOfWord("<p> (1) abc def</p>|",
+ EWordSide::kPreviousWordIfOnBoundary));
+}
+
+TEST_F(VisibleUnitsWordTest, StartOfWordCrossing) {
+ EXPECT_EQ("<b>|abc</b><i>def</i>", DoStartOfWord("<b>abc</b><i>|def</i>"));
+ EXPECT_EQ("<b>abc</b><i>def|</i>", DoStartOfWord("<b>abc</b><i>def</i>|"));
+}
+
+TEST_F(VisibleUnitsWordTest, StartOfWordFirstLetter) {
+ InsertStyleElement("p::first-letter {font-size:200%;}");
+ // Note: Expectations should match with |StartOfWordBasic|.
+ EXPECT_EQ("<p> |(1) abc def</p>", DoStartOfWord("<p>| (1) abc def</p>"));
+ EXPECT_EQ("<p> |(1) abc def</p>", DoStartOfWord("<p> |(1) abc def</p>"));
+ EXPECT_EQ("<p> (|1) abc def</p>", DoStartOfWord("<p> (|1) abc def</p>"));
+ EXPECT_EQ("<p> (1|) abc def</p>", DoStartOfWord("<p> (1|) abc def</p>"));
+ EXPECT_EQ("<p> (1)| abc def</p>", DoStartOfWord("<p> (1)| abc def</p>"));
+ EXPECT_EQ("<p> (1) |abc def</p>", DoStartOfWord("<p> (1) |abc def</p>"));
+ EXPECT_EQ("<p> (1) |abc def</p>", DoStartOfWord("<p> (1) a|bc def</p>"));
+ EXPECT_EQ("<p> (1) |abc def</p>", DoStartOfWord("<p> (1) ab|c def</p>"));
+ EXPECT_EQ("<p> (1) abc| def</p>", DoStartOfWord("<p> (1) abc| def</p>"));
+ EXPECT_EQ("<p> (1) abc |def</p>", DoStartOfWord("<p> (1) abc |def</p>"));
+ EXPECT_EQ("<p> (1) abc |def</p>", DoStartOfWord("<p> (1) abc d|ef</p>"));
+ EXPECT_EQ("<p> (1) abc |def</p>", DoStartOfWord("<p> (1) abc de|f</p>"));
+ EXPECT_EQ("<p> (1) abc def|</p>", DoStartOfWord("<p> (1) abc def|</p>"));
+ EXPECT_EQ("<p> (1) abc def|</p>", DoStartOfWord("<p> (1) abc def</p>|"));
+}
+
+TEST_F(VisibleUnitsWordTest, StartOfWordShadowDOM) {
+ const char* body_content =
+ "<a id=host><b id=one>1</b> <b id=two>22</b></a><i id=three>333</i>";
+ const char* shadow_content =
+ "<p><u id=four>44444</u><content select=#two></content><span id=space> "
+ "</span><content select=#one></content><u id=five>55555</u></p>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* three = GetDocument().getElementById("three")->firstChild();
+ Node* four = shadow_root->getElementById("four")->firstChild();
+ Node* five = shadow_root->getElementById("five")->firstChild();
+ Node* space = shadow_root->getElementById("space")->firstChild();
+
+ EXPECT_EQ(
+ Position(one, 0),
+ StartOfWord(CreateVisiblePositionInDOMTree(*one, 0)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(space, 1),
+ StartOfWord(CreateVisiblePositionInFlatTree(*one, 0)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(one, 0),
+ StartOfWord(CreateVisiblePositionInDOMTree(*one, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(space, 1),
+ StartOfWord(CreateVisiblePositionInFlatTree(*one, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(one, 0),
+ StartOfWord(CreateVisiblePositionInDOMTree(*two, 0)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(four, 0),
+ StartOfWord(CreateVisiblePositionInFlatTree(*two, 0)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(one, 0),
+ StartOfWord(CreateVisiblePositionInDOMTree(*two, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(four, 0),
+ StartOfWord(CreateVisiblePositionInFlatTree(*two, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(one, 0),
+ StartOfWord(CreateVisiblePositionInDOMTree(*three, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(space, 1),
+ StartOfWord(CreateVisiblePositionInFlatTree(*three, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(four, 0),
+ StartOfWord(CreateVisiblePositionInDOMTree(*four, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(four, 0),
+ StartOfWord(CreateVisiblePositionInFlatTree(*four, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(space, 1),
+ StartOfWord(CreateVisiblePositionInDOMTree(*five, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(space, 1),
+ StartOfWord(CreateVisiblePositionInFlatTree(*five, 1)).DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsWordTest, StartOfWordTextSecurity) {
+ // Note: |StartOfWord()| considers security characters as a sequence "x".
+ InsertStyleElement("s {-webkit-text-security:disc;}");
+ EXPECT_EQ("|abc<s>foo bar</s>baz", DoStartOfWord("|abc<s>foo bar</s>baz"));
+ EXPECT_EQ("|abc<s>foo bar</s>baz", DoStartOfWord("abc|<s>foo bar</s>baz"));
+ EXPECT_EQ("|abc<s>foo bar</s>baz", DoStartOfWord("abc<s>|foo bar</s>baz"));
+ EXPECT_EQ("|abc<s>foo bar</s>baz", DoStartOfWord("abc<s>f|oo bar</s>baz"));
+ EXPECT_EQ("|abc<s>foo bar</s>baz", DoStartOfWord("abc<s>foo| bar</s>baz"));
+ EXPECT_EQ("|abc<s>foo bar</s>baz", DoStartOfWord("abc<s>foo |bar</s>baz"));
+ EXPECT_EQ("|abc<s>foo bar</s>baz", DoStartOfWord("abc<s>foo bar|</s>baz"));
+ EXPECT_EQ("|abc<s>foo bar</s>baz", DoStartOfWord("abc<s>foo bar</s>|baz"));
+ EXPECT_EQ("|abc<s>foo bar</s>baz", DoStartOfWord("abc<s>foo bar</s>b|az"));
+}
+
+TEST_F(VisibleUnitsWordTest, EndOfWordBasic) {
+ EXPECT_EQ("<p> (|1) abc def</p>", DoEndOfWord("<p>| (1) abc def</p>"));
+ EXPECT_EQ("<p> (|1) abc def</p>", DoEndOfWord("<p> |(1) abc def</p>"));
+ EXPECT_EQ("<p> (1|) abc def</p>", DoEndOfWord("<p> (|1) abc def</p>"));
+ EXPECT_EQ("<p> (1)| abc def</p>", DoEndOfWord("<p> (1|) abc def</p>"));
+ EXPECT_EQ("<p> (1) |abc def</p>", DoEndOfWord("<p> (1)| abc def</p>"));
+ EXPECT_EQ("<p> (1) abc| def</p>", DoEndOfWord("<p> (1) |abc def</p>"));
+ EXPECT_EQ("<p> (1) abc| def</p>", DoEndOfWord("<p> (1) a|bc def</p>"));
+ EXPECT_EQ("<p> (1) abc| def</p>", DoEndOfWord("<p> (1) ab|c def</p>"));
+ EXPECT_EQ("<p> (1) abc |def</p>", DoEndOfWord("<p> (1) abc| def</p>"));
+ EXPECT_EQ("<p> (1) abc def|</p>", DoEndOfWord("<p> (1) abc |def</p>"));
+ EXPECT_EQ("<p> (1) abc def|</p>", DoEndOfWord("<p> (1) abc d|ef</p>"));
+ EXPECT_EQ("<p> (1) abc def|</p>", DoEndOfWord("<p> (1) abc de|f</p>"));
+ EXPECT_EQ("<p> (1) abc def|</p>", DoEndOfWord("<p> (1) abc def|</p>"));
+ EXPECT_EQ("<p> (1) abc def|</p>", DoEndOfWord("<p> (1) abc def</p>|"));
+}
+
+TEST_F(VisibleUnitsWordTest, EndOfWordPreviousWordIfOnBoundaryBasic) {
+ EXPECT_EQ("<p> |(1) abc def</p>",
+ DoEndOfWord("<p>| (1) abc def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> |(1) abc def</p>",
+ DoEndOfWord("<p> |(1) abc def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (|1) abc def</p>",
+ DoEndOfWord("<p> (|1) abc def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1|) abc def</p>",
+ DoEndOfWord("<p> (1|) abc def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1)| abc def</p>",
+ DoEndOfWord("<p> (1)| abc def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) |abc def</p>",
+ DoEndOfWord("<p> (1) |abc def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) abc| def</p>",
+ DoEndOfWord("<p> (1) a|bc def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) abc| def</p>",
+ DoEndOfWord("<p> (1) ab|c def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) abc| def</p>",
+ DoEndOfWord("<p> (1) abc| def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) abc |def</p>",
+ DoEndOfWord("<p> (1) abc |def</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) abc def|</p>",
+ DoEndOfWord("<p> (1) abc d|ef</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) abc def|</p>",
+ DoEndOfWord("<p> (1) abc de|f</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) abc def|</p>",
+ DoEndOfWord("<p> (1) abc def|</p>",
+ EWordSide::kPreviousWordIfOnBoundary));
+ EXPECT_EQ("<p> (1) abc def|</p>",
+ DoEndOfWord("<p> (1) abc def</p>|",
+ EWordSide::kPreviousWordIfOnBoundary));
+}
+
+TEST_F(VisibleUnitsWordTest, EndOfWordShadowDOM) {
+ const char* body_content =
+ "<a id=host><b id=one>1</b> <b id=two>22</b></a><i id=three>333</i>";
+ const char* shadow_content =
+ "<p><u id=four>44444</u><content select=#two></content><span id=space> "
+ "</span><content select=#one></content><u id=five>55555</u></p>";
+ SetBodyContent(body_content);
+ ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host");
+
+ Node* one = GetDocument().getElementById("one")->firstChild();
+ Node* two = GetDocument().getElementById("two")->firstChild();
+ Node* three = GetDocument().getElementById("three")->firstChild();
+ Node* four = shadow_root->getElementById("four")->firstChild();
+ Node* five = shadow_root->getElementById("five")->firstChild();
+
+ EXPECT_EQ(
+ Position(three, 3),
+ EndOfWord(CreateVisiblePositionInDOMTree(*one, 0)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(five, 5),
+ EndOfWord(CreateVisiblePositionInFlatTree(*one, 0)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(three, 3),
+ EndOfWord(CreateVisiblePositionInDOMTree(*one, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(five, 5),
+ EndOfWord(CreateVisiblePositionInFlatTree(*one, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(three, 3),
+ EndOfWord(CreateVisiblePositionInDOMTree(*two, 0)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(two, 2),
+ EndOfWord(CreateVisiblePositionInFlatTree(*two, 0)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(three, 3),
+ EndOfWord(CreateVisiblePositionInDOMTree(*two, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(two, 2),
+ EndOfWord(CreateVisiblePositionInFlatTree(*two, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(three, 3),
+ EndOfWord(CreateVisiblePositionInDOMTree(*three, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(three, 3),
+ EndOfWord(CreateVisiblePositionInFlatTree(*three, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(four, 5),
+ EndOfWord(CreateVisiblePositionInDOMTree(*four, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(two, 2),
+ EndOfWord(CreateVisiblePositionInFlatTree(*four, 1)).DeepEquivalent());
+
+ EXPECT_EQ(
+ Position(five, 5),
+ EndOfWord(CreateVisiblePositionInDOMTree(*five, 1)).DeepEquivalent());
+ EXPECT_EQ(
+ PositionInFlatTree(five, 5),
+ EndOfWord(CreateVisiblePositionInFlatTree(*five, 1)).DeepEquivalent());
+}
+
+TEST_F(VisibleUnitsWordTest, EndOfWordTextSecurity) {
+ // Note: |EndOfWord()| considers security characters as a sequence "x".
+ InsertStyleElement("s {-webkit-text-security:disc;}");
+ EXPECT_EQ("abc<s>foo bar</s>baz|", DoEndOfWord("|abc<s>foo bar</s>baz"));
+ EXPECT_EQ("abc<s>foo bar</s>baz|", DoEndOfWord("abc|<s>foo bar</s>baz"));
+ EXPECT_EQ("abc<s>foo bar</s>baz|", DoEndOfWord("abc<s>|foo bar</s>baz"));
+ EXPECT_EQ("abc<s>foo bar</s>baz|", DoEndOfWord("abc<s>f|oo bar</s>baz"));
+ EXPECT_EQ("abc<s>foo bar</s>baz|", DoEndOfWord("abc<s>foo| bar</s>baz"));
+ EXPECT_EQ("abc<s>foo bar</s>baz|", DoEndOfWord("abc<s>foo |bar</s>baz"));
+ EXPECT_EQ("abc<s>foo bar</s>baz|", DoEndOfWord("abc<s>foo bar|</s>baz"));
+ EXPECT_EQ("abc<s>foo bar</s>baz|", DoEndOfWord("abc<s>foo bar</s>|baz"));
+ EXPECT_EQ("abc<s>foo bar</s>baz|", DoEndOfWord("abc<s>foo bar</s>b|az"));
+}
+
+TEST_F(VisibleUnitsWordTest, NextWordBasic) {
+ EXPECT_EQ("<p> (1|) abc def</p>", DoNextWord("<p>| (1) abc def</p>"));
+ EXPECT_EQ("<p> (1|) abc def</p>", DoNextWord("<p> |(1) abc def</p>"));
+ EXPECT_EQ("<p> (1|) abc def</p>", DoNextWord("<p> (|1) abc def</p>"));
+ EXPECT_EQ("<p> (1) abc| def</p>", DoNextWord("<p> (1|) abc def</p>"));
+ EXPECT_EQ("<p> (1) abc| def</p>", DoNextWord("<p> (1)| abc def</p>"));
+ EXPECT_EQ("<p> (1) abc| def</p>", DoNextWord("<p> (1) |abc def</p>"));
+ EXPECT_EQ("<p> (1) abc| def</p>", DoNextWord("<p> (1) a|bc def</p>"));
+ EXPECT_EQ("<p> (1) abc| def</p>", DoNextWord("<p> (1) ab|c def</p>"));
+ EXPECT_EQ("<p> (1) abc def|</p>", DoNextWord("<p> (1) abc| def</p>"));
+ EXPECT_EQ("<p> (1) abc def|</p>", DoNextWord("<p> (1) abc |def</p>"));
+ EXPECT_EQ("<p> (1) abc def|</p>", DoNextWord("<p> (1) abc d|ef</p>"));
+ EXPECT_EQ("<p> (1) abc def|</p>", DoNextWord("<p> (1) abc de|f</p>"));
+ EXPECT_EQ("<p> (1) abc def|</p>", DoNextWord("<p> (1) abc def|</p>"));
+ EXPECT_EQ("<p> (1) abc def|</p>", DoNextWord("<p> (1) abc def</p>|"));
+}
+
+TEST_F(VisibleUnitsWordTest, NextWordCrossingBlock) {
+ EXPECT_EQ("<p>abc|</p><p>def</p>", DoNextWord("<p>|abc</p><p>def</p>"));
+ EXPECT_EQ("<p>abc</p><p>def|</p>", DoNextWord("<p>abc|</p><p>def</p>"));
+}
+
+TEST_F(VisibleUnitsWordTest, NextWordMixedEditability) {
+ EXPECT_EQ(
+ "<p contenteditable>"
+ "abc<b contenteditable=\"false\">def ghi</b>|jkl mno</p>",
+ DoNextWord("<p contenteditable>"
+ "|abc<b contenteditable=false>def ghi</b>jkl mno</p>"));
+ EXPECT_EQ(
+ "<p contenteditable>"
+ "abc<b contenteditable=\"false\">def| ghi</b>jkl mno</p>",
+ DoNextWord("<p contenteditable>"
+ "abc<b contenteditable=false>|def ghi</b>jkl mno</p>"));
+ EXPECT_EQ(
+ "<p contenteditable>"
+ "abc<b contenteditable=\"false\">def ghi|</b>jkl mno</p>",
+ DoNextWord("<p contenteditable>"
+ "abc<b contenteditable=false>def |ghi</b>jkl mno</p>"));
+ EXPECT_EQ(
+ "<p contenteditable>"
+ "abc<b contenteditable=\"false\">def ghi|</b>jkl mno</p>",
+ DoNextWord("<p contenteditable>"
+ "abc<b contenteditable=false>def ghi|</b>jkl mno</p>"));
+}
+
+TEST_F(VisibleUnitsWordTest, NextWordPunctuation) {
+ EXPECT_EQ("abc|.def", DoNextWord("|abc.def"));
+ EXPECT_EQ("abc|.def", DoNextWord("a|bc.def"));
+ EXPECT_EQ("abc|.def", DoNextWord("ab|c.def"));
+ EXPECT_EQ("abc.def|", DoNextWord("abc|.def"));
+ EXPECT_EQ("abc.def|", DoNextWord("abc.|def"));
+
+ EXPECT_EQ("abc|...def", DoNextWord("|abc...def"));
+ EXPECT_EQ("abc|...def", DoNextWord("a|bc...def"));
+ EXPECT_EQ("abc|...def", DoNextWord("ab|c...def"));
+ EXPECT_EQ("abc...def|", DoNextWord("abc|...def"));
+ EXPECT_EQ("abc...def|", DoNextWord("abc.|..def"));
+ EXPECT_EQ("abc...def|", DoNextWord("abc..|.def"));
+ EXPECT_EQ("abc...def|", DoNextWord("abc...|def"));
+}
+
+TEST_F(VisibleUnitsWordTest, NextWordSkipTab) {
+ InsertStyleElement("s { white-space: pre }");
+ EXPECT_EQ("<p><s>\t</s>foo|</p>", DoNextWord("<p><s>\t|</s>foo</p>"));
+}
+
+//----
+
+TEST_F(VisibleUnitsWordTest, PreviousWordBasic) {
+ EXPECT_EQ("<p> |(1) abc def</p>", DoPreviousWord("<p>| (1) abc def</p>"));
+ EXPECT_EQ("<p> |(1) abc def</p>", DoPreviousWord("<p> |(1) abc def</p>"));
+ EXPECT_EQ("<p> |(1) abc def</p>", DoPreviousWord("<p> (|1) abc def</p>"));
+ EXPECT_EQ("<p> (|1) abc def</p>", DoPreviousWord("<p> (1|) abc def</p>"));
+ EXPECT_EQ("<p> (|1) abc def</p>", DoPreviousWord("<p> (1)| abc def</p>"));
+ EXPECT_EQ("<p> (|1) abc def</p>", DoPreviousWord("<p> (1) |abc def</p>"));
+ EXPECT_EQ("<p> (1) |abc def</p>", DoPreviousWord("<p> (1) a|bc def</p>"));
+ EXPECT_EQ("<p> (1) |abc def</p>", DoPreviousWord("<p> (1) ab|c def</p>"));
+ EXPECT_EQ("<p> (1) |abc def</p>", DoPreviousWord("<p> (1) abc| def</p>"));
+ EXPECT_EQ("<p> (1) |abc def</p>", DoPreviousWord("<p> (1) abc |def</p>"));
+ EXPECT_EQ("<p> (1) abc |def</p>", DoPreviousWord("<p> (1) abc d|ef</p>"));
+ EXPECT_EQ("<p> (1) abc |def</p>", DoPreviousWord("<p> (1) abc de|f</p>"));
+ EXPECT_EQ("<p> (1) abc |def</p>", DoPreviousWord("<p> (1) abc def|</p>"));
+ EXPECT_EQ("<p> (1) abc |def</p>", DoPreviousWord("<p> (1) abc def</p>|"));
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/web_substring_util.mm b/chromium/third_party/blink/renderer/core/editing/web_substring_util.mm
new file mode 100644
index 00000000000..b6c0e511d0a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/web_substring_util.mm
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2005, 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "third_party/blink/public/web/mac/web_substring_util.h"
+
+#import <Cocoa/Cocoa.h>
+
+#include "third_party/blink/public/platform/web_rect.h"
+#include "third_party/blink/public/web/web_frame_widget.h"
+#include "third_party/blink/public/web/web_hit_test_result.h"
+#include "third_party/blink/public/web/web_local_frame.h"
+#include "third_party/blink/renderer/bindings/core/v8/exception_state.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/element.h"
+#include "third_party/blink/renderer/core/dom/node.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
+#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
+#include "third_party/blink/renderer/core/editing/frame_selection.h"
+#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
+#include "third_party/blink/renderer/core/editing/plain_text_range.h"
+#include "third_party/blink/renderer/core/editing/selection_template.h"
+#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/editing/visible_units.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/frame/local_frame_view.h"
+#include "third_party/blink/renderer/core/frame/visual_viewport.h"
+#include "third_party/blink/renderer/core/frame/web_frame_widget_base.h"
+#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
+#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/layout/hit_test_result.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/core/page/page.h"
+#include "third_party/blink/renderer/core/style/computed_style.h"
+#include "third_party/blink/renderer/platform/fonts/font.h"
+#include "third_party/blink/renderer/platform/mac/color_mac.h"
+
+namespace blink {
+
+namespace {
+
+NSAttributedString* AttributedSubstringFromRange(const EphemeralRange& range,
+ float font_scale) {
+ NSMutableAttributedString* string = [[NSMutableAttributedString alloc] init];
+ NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
+ size_t length = range.EndPosition().ComputeOffsetInContainerNode() -
+ range.StartPosition().ComputeOffsetInContainerNode();
+
+ unsigned position = 0;
+
+ // TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
+ // needs to be audited. see http://crbug.com/590369 for more details.
+ range.StartPosition()
+ .GetDocument()
+ ->UpdateStyleAndLayoutIgnorePendingStylesheets();
+
+ for (TextIterator it(range.StartPosition(), range.EndPosition());
+ !it.AtEnd() && [string length] < length; it.Advance()) {
+ unsigned num_characters = it.length();
+ if (!num_characters)
+ continue;
+
+ const Node* container = it.CurrentContainer();
+ LayoutObject* layout_object = container->GetLayoutObject();
+ DCHECK(layout_object);
+ if (!layout_object)
+ continue;
+
+ const ComputedStyle* style = layout_object->Style();
+ FontPlatformData font_platform_data =
+ style->GetFont().PrimaryFont()->PlatformData();
+ font_platform_data.text_size_ *= font_scale;
+ NSFont* font = toNSFont(font_platform_data.CtFont());
+ // If the platform font can't be loaded, or the size is incorrect comparing
+ // to the computed style, it's likely that the site is using a web font.
+ // For now, just use the default font instead.
+ // TODO(rsesek): Change the font activation flags to allow other processes
+ // to use the font.
+ // TODO(shuchen): Support scaling the font as necessary according to CSS
+ // transforms, not just pinch-zoom.
+ if (!font || floor(font_platform_data.size()) !=
+ floor([[font fontDescriptor] pointSize])) {
+ font = [NSFont systemFontOfSize:style->GetFont()
+ .GetFontDescription()
+ .ComputedSize() *
+ font_scale];
+ }
+ [attrs setObject:font forKey:NSFontAttributeName];
+
+ if (style->VisitedDependentColor(GetCSSPropertyColor()).Alpha())
+ [attrs
+ setObject:NsColor(style->VisitedDependentColor(GetCSSPropertyColor()))
+ forKey:NSForegroundColorAttributeName];
+ else
+ [attrs removeObjectForKey:NSForegroundColorAttributeName];
+ if (style->VisitedDependentColor(GetCSSPropertyBackgroundColor()).Alpha())
+ [attrs setObject:NsColor(style->VisitedDependentColor(
+ GetCSSPropertyBackgroundColor()))
+ forKey:NSBackgroundColorAttributeName];
+ else
+ [attrs removeObjectForKey:NSBackgroundColorAttributeName];
+
+ ForwardsTextBuffer characters;
+ it.CopyTextTo(&characters);
+ NSString* substring =
+ [[[NSString alloc] initWithCharacters:characters.Data()
+ length:characters.Size()] autorelease];
+ [string replaceCharactersInRange:NSMakeRange(position, 0)
+ withString:substring];
+ [string setAttributes:attrs range:NSMakeRange(position, num_characters)];
+ position += num_characters;
+ }
+ return [string autorelease];
+}
+
+WebPoint GetBaselinePoint(LocalFrameView* frame_view,
+ const EphemeralRange& range,
+ NSAttributedString* string) {
+ IntRect string_rect = frame_view->ContentsToViewport(ComputeTextRect(range));
+ IntPoint string_point = string_rect.MinXMaxYCorner();
+
+ // Adjust for the font's descender. AppKit wants the baseline point.
+ if ([string length]) {
+ NSDictionary* attributes = [string attributesAtIndex:0 effectiveRange:NULL];
+ if (NSFont* font = [attributes objectForKey:NSFontAttributeName])
+ string_point.Move(0, ceil([font descender]));
+ }
+ return string_point;
+}
+
+} // namespace
+
+NSAttributedString* WebSubstringUtil::AttributedWordAtPoint(
+ WebFrameWidget* frame_widget,
+ WebPoint point,
+ WebPoint& baseline_point) {
+ HitTestResult result = static_cast<WebFrameWidgetBase*>(frame_widget)
+ ->CoreHitTestResultAt(point);
+
+ if (!result.InnerNode())
+ return nil;
+ LocalFrame* frame = result.InnerNode()->GetDocument().GetFrame();
+ EphemeralRange range =
+ frame->GetEditor().RangeForPoint(result.RoundedPointInInnerNodeFrame());
+ if (range.IsNull())
+ return nil;
+
+ // Expand to word under point.
+ const VisibleSelection& selection = CreateVisibleSelectionWithGranularity(
+ SelectionInDOMTree::Builder().SetBaseAndExtent(range).Build(),
+ TextGranularity::kWord);
+ const EphemeralRange word_range = selection.ToNormalizedEphemeralRange();
+
+ // Convert to NSAttributedString.
+ NSAttributedString* string = AttributedSubstringFromRange(
+ word_range, frame->GetPage()->GetVisualViewport().Scale());
+ baseline_point = GetBaselinePoint(frame->View(), word_range, string);
+ return string;
+}
+
+NSAttributedString* WebSubstringUtil::AttributedSubstringInRange(
+ WebLocalFrame* web_frame,
+ size_t location,
+ size_t length) {
+ return WebSubstringUtil::AttributedSubstringInRange(web_frame, location,
+ length, nil);
+}
+
+NSAttributedString* WebSubstringUtil::AttributedSubstringInRange(
+ WebLocalFrame* web_frame,
+ size_t location,
+ size_t length,
+ WebPoint* baseline_point) {
+ LocalFrame* frame = ToWebLocalFrameImpl(web_frame)->GetFrame();
+ if (frame->View()->NeedsLayout())
+ frame->View()->UpdateLayout();
+
+ Element* editable = frame->Selection().RootEditableElementOrDocumentElement();
+ if (!editable)
+ return nil;
+ const EphemeralRange ephemeral_range(
+ PlainTextRange(location, location + length).CreateRange(*editable));
+ if (ephemeral_range.IsNull())
+ return nil;
+
+ NSAttributedString* result = AttributedSubstringFromRange(
+ ephemeral_range, frame->GetPage()->GetVisualViewport().Scale());
+ if (baseline_point)
+ *baseline_point = GetBaselinePoint(frame->View(), ephemeral_range, result);
+ return result;
+}
+
+} // namespace blink
diff --git a/chromium/third_party/blink/renderer/core/editing/writing_direction.h b/chromium/third_party/blink/renderer/core/editing/writing_direction.h
new file mode 100644
index 00000000000..af5e583cb94
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/editing/writing_direction.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2008 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_WRITING_DIRECTION_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_WRITING_DIRECTION_H_
+
+namespace blink {
+
+enum class WritingDirection { kNatural, kLeftToRight, kRightToLeft };
+
+} // namespace blink
+
+#endif